Identify the category — ask which of the four types the new action belongs to.
Add the variant — append a new branch to the correct discriminated union in packages/game/lib/core/actions.ts.
Handle it exhaustively — add a case to every switch that covers that union; the compiler will error on missing cases (never check at the end).
Write a unit test — pure function test in the co-located *.test.ts file that exercises the new branch.
Conventions
Tag field is always type (not kind, not action).
Payload fields are Readonly<{…}> inline — no separate payload type unless reused.
No classes. No this. No mutation.
Branded IDs for any entity reference: Brand<string, "EnemyId">.
Template
// packages/game/lib/core/actions.ts (add to the correct union)
| { readonlytype: "your-action"; readonlypayload: Readonly<{ /* … */ }> }
Exhaustiveness guard (add to every switch that covers this union):
default: {
const_exhaustive: never = action;
return _exhaustive;
}
For RenderCommand variants, also update packages/game/lib/render/project-render-commands.ts, packages/game/lib/effects/execute-render-command.ts, and their co-located tests.