logo

Babylon.js Market

Claude Code Plugins

The Two Skills

Two Claude Code plugins shipped inside @babylonjsmarket/ecs and @babylonjsmarket/arcade — install once, stop fighting hallucinated APIs

The Two Skills

Both ship under the arcade-ecs: namespace so they don't collide with skills from other plugins. Claude auto-routes by description match — you describe what you want, the right skill fires.

arcade-ecs:ecs-api — API reference

Triggers when: Claude is writing or reviewing code that uses @babylonjsmarket/ecs or @babylonjsmarket/arcade. Also when you ask "how does X work" about a Component/System/World/EventBus method.

What it carries:

  • The canonical public surface of every class — Component, Entity, System, World, EventBus, SceneLoader — derived from the source files at packages/ecs/src/ECS/. Not training data.
  • An anti-pattern table of plausible-looking method names that don't exist but show up in generated code:
  • entity.getOrThrow(C) → use entity.get(C)!
  • entity.getComponent(C) / entity.hasComponent(C) → use entity.get(C) / entity.has(C)
  • world.eventBus → use world.getEventBus() (the field is protected)
  • onAttachOverride / onDetachOverride on a System → those are Component hooks; Systems use onInitialize / onShutdown
  • this.entities.find(...)this.entities is a Set, not an Array; use world.getEntity(id) for O(1) lookup
  • A pointer to the source files of record so any specific claim can be re-verified.

Effect on generated code: the anti-patterns above are exactly what Claude tends to invent without the skill. With the skill loaded, they get caught before the first save.

arcade-ecs:scaffold-ecs — Component scaffolder

Triggers when: you describe a component concept — e.g. "add a Cooldown component", "create a Score system", "scaffold a Pickup mechanic."

What it produces: one TypeScript file at src/components/{Name}/{Name}.ts containing:

import { Component, System, EventBus, type ISystemQuery } from '@babylonjsmarket/ecs';

export const NameEvents = { /* output events */ } as const;
export const NameInputEvents = { /* listened events */ } as const;

export class NameComponent extends Component {
  // pure data fields go here
}

export class NameSystem extends System {
  query: ISystemQuery = { required: [NameComponent] };
  private unsubscribes: Array<() => void> = [];

  constructor(eventBus: EventBus) { super(eventBus); }

  onInitialize() {
    // subscribe; collect every returned unsubscribe
  }

  onShutdown() {
    this.unsubscribes.forEach((u) => u());
    this.unsubscribes = [];
  }

  onUpdate(_dt: number) {
    for (const entity of this.entities) {
      const c = entity.get(NameComponent)!;
      // ...
    }
  }
}

A sibling {Name}.test.ts (Vitest skeleton against World + EventBus) is written too, if your project already has test files nearby. Projects without tests stay without tests.

Conventions baked in:

  • Component is pure data — no @babylonjs/* imports, no DOM, no side effects in the constructor.
  • System subscriptions go in onInitialize and unsubscribes go in onShutdown. The unsubscribe-tracking pattern is non-negotiable; listeners leak otherwise.
  • Events are PascalCase consts pointing to lowercase dotted strings (NameEvents.CHANGED = 'name.changed'). Never hand-write string literals at call sites.

When neither fires

The skills are scoped narrowly. If you're working on the website, on a CLI tool unrelated to the ECS, or on scene loading mechanics that don't fit the "add a Component" pattern, neither skill triggers. That's intentional — broad auto-invocation makes skills noisy.

To force-invoke explicitly, prefix your message with the namespaced name:

/arcade-ecs:scaffold-ecs add a Cooldown component

But in practice, describing what you want plainly is enough.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search