# Scaffold Render Command

> Add a new RenderCommand variant to the retained scene description and wire its Canvas executor in the impure shell

- Skill: `dkolba/scaffold-render-command` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dkolba/scaffold-render-command`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dkolba/scaffold-render-command/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: dkolba (https://skillmd.com/u/dkolba)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dkolba/scaffold-render-command

---


# Scaffold Render Command

Use when adding a new visual element to the game (sprite, tile, text, shape, etc.).

## The Two-Part Rule

Every render concern is split across two functions — never combined:

| Part           | Layer      | Pure?     | What it does                                              |
| -------------- | ---------- | --------- | --------------------------------------------------------- |
| **Projection** | `render/`  | ✅ Pure   | `state → RenderCommand[]` — describes _what_ to draw      |
| **Executor**   | `effects/` | ❌ Impure | `RenderCommand → Canvas draw calls` — _actually_ draws it |

**Never write to `CanvasRenderingContext2D` inside the projection.** The projection must be fully testable without a Canvas.

---

## Steps

### 1. Add the variant to `RenderCommand`

In `packages/game/lib/core/actions.ts`:

```ts
export type RenderCommand =
  | {
      readonly type: "fill-rect";
      readonly xPos: number;
      readonly yPos: number;
      readonly width: number;
      readonly height: number;
      readonly color: string;
    }
  | { readonly type: "YOUR_COMMAND"; readonly /* payload fields */ };
```

### 2. Implement the projection (pure)

In `packages/game/lib/render/project-render-commands.ts`, compose the new command into the root foreground projection. Extract a helper only when it is reused or materially improves readability.

```ts
import type { RenderCommand } from "../core/actions.ts";
import type { GameState } from "../core/types.ts";

const projectYourThing = (state: GameState): ReadonlyArray<RenderCommand> =>
  state.entities.map((e) => ({
    type: "YOUR_COMMAND",
    /* derive fields from state only — no Canvas access */
  }));

export default projectYourThing;
```

The root projection returns the ordered `ReadonlyArray<RenderCommand>` for the current foreground frame. It does not emit the animated background; the background remains shell-rendered before foreground commands.

### 3. Implement the executor (impure)

In `packages/game/lib/effects/execute-render-command.ts`, add a `case` to the executor switch:

```ts
case "YOUR_COMMAND": {
  context./* Canvas draw calls here */;
  break;
}
```

End the switch with an exhaustiveness guard:

```ts
default: {
  const _exhaustive: never = command;
  return _exhaustive;
}
```

### 4. Write tests

```ts
import { describe, expect, it } from "vitest";
import projectYourThing from "./your-command.js";

describe("projectYourThing", () => {
  it("produces the expected render commands from state", () => {
    const state = /* minimal GameState */;
    expect(projectYourThing(state)).toStrictEqual([
      { type: "YOUR_COMMAND", /* expected fields */ },
    ]);
  });
});
```

No `CanvasRenderingContext2D` mock is needed for projection tests — the projection is pure. Add or update browser-provider tests in `packages/game/lib/effects/execute-render-command.test.ts` for the Canvas executor branch. If the command changes player/enemy observability, update `renderStatsForState` and its tests.

---

## Prohibited

- Writing to `ctx.fillStyle` / `ctx.drawImage` / etc. inside `render/`
- Reading from the DOM or Canvas inside the projection
- Returning `void` from the projection — it must return `ReadonlyArray<RenderCommand>`
- Forgetting `RenderStats` when the visible output changes. The effects renderer reports latest stats for `window.__bruffTestApi.getRenderStats()`.

