Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .claude/skills/new-ecs-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
name: new-ecs-package
description: Scaffold a new ECS component or system in this repo (components-systems), following exactly the pattern of existing packages. Use when the user asks to add a new NanoForge component or system in this repo.
---

# New ECS component / system

Each package under `packages/` is a plain folder (no `package.json` of its own) containing `index.ts` + `nanoforge.manifest.json`. No centralized registration step is needed: `tsconfig.json`/`vitest.config.ts` already map `@components/*` and `@systems/*` to `./packages/*`, and `tsconfig.json` includes `packages/**/*.ts` — a new folder is automatically picked up.

Before creating a package, check that one doesn't already exist under a similar name (`ls packages/ | grep -i <keyword>`).

## Component (`*.component`)

Folder `packages/nf-<kebab-name>.component/`, `index.ts` file:

```ts
import { type EditorComponentManifest } from "@nanoforge-dev/ecs-lib";

export class ClassName {
name = this.constructor.name;

constructor(
public fieldA: number,
public fieldB: number,
) {}
}

// * Required to generate code
export default ClassName.name;

// * Required for the editor to display the component and generate code
export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
name: "ClassName",
description: "<short description, third person, in English like the other manifests>",
params: [
{
type: "number",
name: "fieldA",
description: "<field description>",
example: 0,
default: 0,
},
// one object per constructor field
],
};
```

And `nanoforge.manifest.json` alongside it:

```json
{
"name": "nf/<kebab-name>",
"type": "component",
"description": "<same description>",
"tags": ["<tag1>", "<tag2>"]
}
```

Real reference: `packages/nf-position-2d.component/`.

## System (`*.system`)

Folder `packages/nf-<kebab-name>.system/`, `index.ts` file:

```ts
import { type Context } from "@nanoforge-dev/common";
import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-lib";

import { ComponentA } from "../components/nf-component-a.component";
import { ComponentB } from "../components/nf-component-b.component";

export function functionName(registry: Registry, ctx: Context) {
const entities = registry.getZipper([ComponentA, ComponentB]);

entities.forEach(({ ComponentA, ComponentB }) => {
// system logic
});
}

// * Required to generate code
export default functionName.name;

// * Required for the editor to display the system and generate code
export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
name: "functionName",
description: "<short description>",
dependencies: ["ComponentA", "ComponentB"],
};
```

Note: the components used are imported via a relative path (`../components/nf-xxx.component`), not via the `@components/*` alias — this is the convention observed in all existing systems (e.g. `nf-move-2d.system`).

And `nanoforge.manifest.json`:

```json
{
"name": "nf/<kebab-name>",
"type": "system",
"description": "<same description>",
"tags": ["<tag1>", "<tag2>"],
"dependencies": ["nf/<component-a>", "nf/<component-b>"]
}
```

Real reference: `packages/nf-move-2d.system/`.

## After creation

1. Suggest adding a test in `test/systems.spec.ts` (imported via the `@systems/nf-<name>.system` alias), without forcing it — it's a file shared across all systems, to be edited carefully so as not to break existing tests.
2. Run `pnpm run lint` and `pnpm run build` to verify that the new package compiles and complies with the repo's ESLint/Prettier rules before proposing a commit.
3. Never publish or modify `pnpm-workspace.yaml` as part of this skill — adding a component/system requires no additional dependency beyond those already present in the `core` catalog.
91 changes: 91 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# components-systems

## Role

`@nanoforge-dev/components-systems` is a monorepo that gathers a collection of generic, reusable ECS (Entity-Component-System) components and systems for building games with the NanoForge engine. Each component/system is published as a module that can be installed individually via the NanoForge CLI (`nf install nf/position-2d`, `nf install nf/move-2d`, ...).

## Tech Stack

- **Language**: TypeScript (strict, decorators enabled, ESNext, "Bundler" module resolution)
- **Monorepo management**: pnpm workspaces + pnpm catalogs (`core`, `ci`, `lint`, `test` categories in `pnpm-workspace.yaml`)
- **Build**: `tsc -b` (project references)
- **Tests**: Vitest (with integrated typechecking via `tsconfig.spec.json` and v8 coverage)
- **Lint/format**: ESLint (shared config `@nanoforge-dev/utils-eslint-config`) + Prettier (import-sorting plugin `@trivago/prettier-plugin-sort-imports`)
- **Git hooks**: Husky + lint-staged
- **Commits**: Conventional Commits convention (commitlint)
- **Changelog/release**: git-cliff
- **Node**: v25 required (`.nvmrc`, `engines.node` in `package.json`), pnpm 11.9.0 (`packageManager`)

### Declared NanoForge dependencies

External dependencies declared as devDependencies, resolved via the pnpm `core` catalog (versions centralized in `pnpm-workspace.yaml`):

- `@nanoforge-dev/common`
- `@nanoforge-dev/ecs-lib`
- `@nanoforge-dev/ecs-server`
- `@nanoforge-dev/graphics-2d`
- `@nanoforge-dev/input`
- `@nanoforge-dev/network-client`
- `@nanoforge-dev/network-server`

`@nanoforge-dev/actions` is declared via the `ci` catalog.

## Project Structure

```
components-systems/
├── packages/ # 31 packages, one per component or system
│ ├── nf-position-2d.component/
│ │ ├── index.ts # component class + default export + EDITOR_COMPONENT_MANIFEST
│ │ └── nanoforge.manifest.json # metadata (name "nf/...", type, description, tags)
│ ├── nf-velocity-2d.component/
│ ├── nf-move-2d.system/
│ │ ├── index.ts # system function + EDITOR_SYSTEM_MANIFEST
│ │ └── nanoforge.manifest.json # dependencies on other components/systems
│ ├── nf-collision-2d.system/
│ └── ... (other components/systems: health, hitbox, gravity, damage, drawable, etc.)
├── test/
│ └── systems.spec.ts # Vitest tests
├── .github/workflows/ # tests.yml, release.yml, pr-triage.yml, issue-triage.yml, label-sync.yml
├── tsconfig.json # @components/* and @systems/* paths -> ./packages/*
├── vitest.config.ts
├── pnpm-workspace.yaml # shared dependency catalogs
└── package.json
```

Each package follows an `nf-<name>.component` or `nf-<name>.system` naming scheme, with no `package.json` of its own: they are plain folders containing an `index.ts` and a `nanoforge.manifest.json`, consumed directly by the NanoForge tooling (CLI/editor) rather than published individually to npm.

### Component pattern (`*.component`)

A simple data-holding class, with:
- `export default ClassName.name;` (required for code generation)
- `export const EDITOR_COMPONENT_MANIFEST` (typed as `EditorComponentManifest` from `@nanoforge-dev/ecs-lib`) describing the parameters for the visual editor (type, name, description, example, default value)

Example (`nf-position-2d.component`): a `Position2D { x: number; y: number }` class.

### System pattern (`*.system`)

A pure function `(registry: Registry, ctx?: Context) => void` that retrieves entities via `registry.getZipper([...Components])` or `getIndexedZipper`, then mutates their components. Accompanied by:
- `export default functionName.name;`
- `export const EDITOR_SYSTEM_MANIFEST` (typed as `EditorSystemManifest` from `@nanoforge-dev/ecs-lib`) listing the dependencies (names of required components)

Example (`nf-move-2d.system`): updates `Position2D` from `Velocity2D` and the delta-time (`ctx.app.delta`).

Each package also has a `nanoforge.manifest.json` in the format `{ name: "nf/<slug>", type: "component"|"system", description, tags, dependencies? }`, used by the NanoForge CLI/registry for installation and dependency resolution.

## Main Commands

- `pnpm run build` → `tsc -b` (incremental build via project references)
- `pnpm run test` (= `test:unit`) → `vitest run --config vitest.config.ts`
- `pnpm run lint` → `prettier --check .` + `eslint --format=pretty packages`
- `pnpm run format` → `prettier --write .` + `eslint --fix --format=pretty packages`
- `pnpm run changelog` → `git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ./ --include-path './*'`

## Conventions

- Imports use the `@components/*` and `@systems/*` aliases, both mapped to `./packages/*` in `tsconfig.json` and `vitest.config.ts`.
- Components/systems used internally (by other systems in the same repo) are imported via relative paths such as `../components/nf-position-2d.component` (the literal path used in systems, e.g. `nf-move-2d.system`, `nf-gravity-2d.system`, `nf-collision-2d.system`).
- Tests (`test/systems.spec.ts`) import via the `@systems/...` aliases.
- Categorization tags in the manifests (e.g. `"base"`, `"mouvement"`) for classification in the CLI/registry/editor.
- Dependencies between systems and components are declared explicitly in `nanoforge.manifest.json` (the `dependencies` field), mirroring the actual TypeScript imports.
- Commit convention enforced by commitlint (Conventional Commits), checked by a Husky hook.
Loading