@codyswann/lisa 2.178.6 → 2.179.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/eslint/phaser.d.ts +2 -8
- package/dist/configs/eslint/phaser.d.ts.map +1 -1
- package/dist/configs/eslint/phaser.js +143 -17
- package/dist/configs/eslint/phaser.js.map +1 -1
- package/eslint-plugin-phaser/README.md +19 -0
- package/eslint-plugin-phaser/index.js +35 -0
- package/eslint-plugin-phaser/package.json +10 -0
- package/eslint-plugin-phaser/rules/no-allocation-in-update.js +189 -0
- package/eslint-plugin-phaser/rules/no-create-in-update.js +200 -0
- package/eslint-plugin-phaser/rules/require-shutdown-cleanup.js +191 -0
- package/package.json +5 -3
- package/phaser/package-lisa/package.lisa.json +11 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/rules/phaser.md +111 -44
- package/plugins/lisa-phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser/skills/phaser-accessibility/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-asset-pipeline/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser/skills/phaser-build-deploy/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser/skills/phaser-i18n/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser/skills/phaser-services/agents/openai.yaml +4 -0
- package/plugins/lisa-phaser/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-agy/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-agy/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-agy/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-agy/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/rules/phaser.md +111 -44
- package/plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-copilot/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/rules/phaser.mdc +111 -44
- package/plugins/lisa-phaser-cursor/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-services/SKILL.md +208 -0
- package/plugins/lisa-phaser-cursor/skills/phaser-testing/SKILL.md +103 -10
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/phaser/rules/phaser.md +111 -44
- package/plugins/src/phaser/skills/phaser-accessibility/SKILL.md +135 -0
- package/plugins/src/phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-build-deploy/SKILL.md +148 -0
- package/plugins/src/phaser/skills/phaser-i18n/SKILL.md +122 -0
- package/plugins/src/phaser/skills/phaser-services/SKILL.md +208 -0
- package/plugins/src/phaser/skills/phaser-testing/SKILL.md +103 -10
- package/tsconfig/phaser.json +8 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: phaser-testing
|
|
3
|
-
description: This skill should be used when writing or designing tests for a Phaser 4 game — unit-testing pure game logic with Vitest, keeping logic Phaser-free so it tests without a browser, the Phaser.HEADLESS renderer for logic-only boots, asset-manifest coverage tests, and
|
|
3
|
+
description: This skill should be used when writing or designing tests for a Phaser 4 game — unit-testing pure game logic with Vitest, keeping logic Phaser-free so it tests without a browser, the Phaser.HEADLESS renderer for logic-only boots, asset-manifest coverage tests, and the CI runtime gates (boot smoke, allocation/perf budget, leak gate, determinism gate, deterministic Playwright visual regression, bundle-size budget) that verify scenes and entities that are excluded from unit coverage. Use it when adding tests, setting up CI verification, or deciding what is testable at which level. Pairs with phaser-project-structure, phaser-assets, and phaser-services.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Phaser 4 Testing
|
|
@@ -13,10 +13,17 @@ pyramid, bottom-up:
|
|
|
13
13
|
1. **Vitest unit tests** over `src/logic/**` — the bulk of coverage.
|
|
14
14
|
2. **Manifest/contract tests** — cheap structural checks (asset packs, scene
|
|
15
15
|
keys, anim definitions).
|
|
16
|
-
3. **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
3. **Headless integration** (sparingly) — `Phaser.HEADLESS` for things that
|
|
17
|
+
genuinely need the engine loop.
|
|
18
|
+
4. **Playwright smoke** — the game boots in a real browser, renders past the
|
|
19
|
+
Preloader, no console errors.
|
|
20
|
+
5. **CI runtime gates** — scenes/entities are excluded from unit coverage and
|
|
21
|
+
instead verified by deterministic runtime gates: boot smoke, allocation/perf
|
|
22
|
+
budget, leak gate, determinism gate, visual regression, bundle-size budget.
|
|
23
|
+
|
|
24
|
+
There is no official Phaser testing harness — this layering IS the strategy. The
|
|
25
|
+
dividing line: pure logic is unit-tested; everything that needs the engine is a
|
|
26
|
+
runtime gate, never a brittle unit test that mocks the world.
|
|
20
27
|
|
|
21
28
|
## Layer 1: pure logic under Vitest (the rule that makes it possible)
|
|
22
29
|
|
|
@@ -85,15 +92,101 @@ Convention: scenes set `window.__sceneReady = key` in `create()` (dev/test
|
|
|
85
92
|
builds) so tests await real readiness instead of sleeping. Run against
|
|
86
93
|
`bun run dev` (or `vite preview` in CI).
|
|
87
94
|
|
|
95
|
+
## Layer 5: CI runtime gates (how scenes get verified)
|
|
96
|
+
|
|
97
|
+
Scenes and entities are deliberately **excluded from unit coverage** — mocking
|
|
98
|
+
the engine to "unit test" a scene tests the mock. Instead, CI runs a set of
|
|
99
|
+
deterministic gates against a real (or headless) game. Each gate fails the build
|
|
100
|
+
on regression; together they are the contract that the engine-coupled layer
|
|
101
|
+
keeps working.
|
|
102
|
+
|
|
103
|
+
**Boot smoke** — the Playwright test above, hardened: page error listener, wait
|
|
104
|
+
for `__sceneReady`, assert zero console errors. This is the floor.
|
|
105
|
+
|
|
106
|
+
**Allocation / perf budget** — run the game for N frames and assert the frame
|
|
107
|
+
budget and heap growth. Per-frame allocation is what the `no-allocation-in-update`
|
|
108
|
+
and `no-create-in-update` lint rules forbid statically; this gate catches what
|
|
109
|
+
slips through dynamically.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
test("steady-state frames stay within budget", async ({ page }) => {
|
|
113
|
+
await page.goto("/"); await bootTo(page, "Game");
|
|
114
|
+
const stats = await page.evaluate(async () => {
|
|
115
|
+
const g = (window as any).__game as Phaser.Game;
|
|
116
|
+
const frames: number[] = []; let last = performance.now();
|
|
117
|
+
for (let i = 0; i < 600; i++) { await new Promise(r => g.events.once("postrender", r)); const n = performance.now(); frames.push(n - last); last = n; }
|
|
118
|
+
return { p95: frames.sort((a,b)=>a-b)[Math.floor(frames.length*0.95)], heap: (performance as any).memory?.usedJSHeapSize };
|
|
119
|
+
});
|
|
120
|
+
expect(stats.p95).toBeLessThan(20); // ~50fps floor on CI hardware
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Leak gate** — start and stop a scene N times and assert that texture count,
|
|
125
|
+
event-listener count, active tweens, and timers all return to baseline. This is
|
|
126
|
+
the runtime enforcement of the `require-shutdown-cleanup` rule and the
|
|
127
|
+
on/off-discipline in [[phaser-services]].
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
test("scene start/stop leaves no leaks", async ({ page }) => {
|
|
131
|
+
await page.goto("/"); await bootTo(page, "MainMenu");
|
|
132
|
+
const before = await page.evaluate(() => (window as any).__game.textures.getTextureKeys().length);
|
|
133
|
+
for (let i = 0; i < 20; i++) await page.evaluate(async () => {
|
|
134
|
+
const g = (window as any).__game as Phaser.Game; g.scene.start("Game");
|
|
135
|
+
await new Promise(r => setTimeout(r, 50)); g.scene.stop("Game");
|
|
136
|
+
await new Promise(r => setTimeout(r, 50));
|
|
137
|
+
});
|
|
138
|
+
const after = await page.evaluate(() => (window as any).__game.textures.getTextureKeys().length);
|
|
139
|
+
expect(after).toBeLessThanOrEqual(before); // no per-cycle texture growth
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Determinism gate** — boot twice with the same seed, drive the same inputs, and
|
|
144
|
+
assert an identical state hash. Logic exposes a serializable snapshot; the gate
|
|
145
|
+
hashes it. Same seed → same hash, always. This is the runtime backstop for the
|
|
146
|
+
no-`Math.random()`/`Date.now()`/`performance.now()` rules ([[phaser-services]]).
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const run = (seed: number) => page.evaluate(s => (window as any).__sim(s, REPLAY), seed);
|
|
150
|
+
expect(hash(await run(1234))).toBe(hash(await run(1234)));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Deterministic visual regression** — `toHaveScreenshot` under **software GL**
|
|
154
|
+
(`--use-gl=swiftshader`), a **frozen frame** (pause the loop / step a fixed
|
|
155
|
+
number of ticks at a fixed delta), and **masked dynamic regions** (timers,
|
|
156
|
+
particles). Without all three, screenshots flake. Pin Playwright's browser and
|
|
157
|
+
OS in CI so the baseline is stable.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await page.evaluate(() => { const g=(window as any).__game; g.loop.sleep(); g.step(0,16.6); });
|
|
161
|
+
await expect(page).toHaveScreenshot("game.png", { mask: [page.locator("#timer")], maxDiffPixelRatio: 0.01 });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Bundle-size budget** — assert the built bundle stays under a byte budget so a
|
|
165
|
+
stray dependency or an un-split `phaser` chunk fails the PR. Wire it to the prod
|
|
166
|
+
build's `manualChunks` split ([[phaser-build-deploy]]).
|
|
167
|
+
|
|
168
|
+
```jsonc
|
|
169
|
+
// size-limit / custom check after `bun run build`
|
|
170
|
+
[{ "path": "dist/assets/index-*.js", "limit": "60 kB" },
|
|
171
|
+
{ "path": "dist/assets/phaser-*.js", "limit": "400 kB" }]
|
|
172
|
+
```
|
|
173
|
+
|
|
88
174
|
## Project conventions
|
|
89
175
|
|
|
90
|
-
- `bun run test` = Vitest (layers 1–2, coverage-gated
|
|
91
|
-
|
|
176
|
+
- `bun run test` = Vitest (layers 1–2, coverage-gated **over `src/logic/**`
|
|
177
|
+
only** — scenes/entities are out of scope here). The layer-5 gates run as their
|
|
178
|
+
own CI jobs against a built `vite preview` (and a headless boot for the
|
|
179
|
+
determinism gate).
|
|
180
|
+
- Build pins are part of the contract: `phaser ^4.2.0`, Vite, TypeScript 6.
|
|
92
181
|
- Tests never assert on private scene fields; they assert on logic outputs
|
|
93
|
-
(layer 1)
|
|
182
|
+
(layer 1), observable behavior (layer 4), or the runtime invariants the gates
|
|
183
|
+
measure (layer 5).
|
|
94
184
|
|
|
95
185
|
## Verification
|
|
96
186
|
|
|
97
187
|
The testing setup itself is verified when `bun run test` passes with coverage
|
|
98
|
-
over `src/logic/**`,
|
|
99
|
-
|
|
188
|
+
over `src/logic/**`, the Playwright smoke fails when you deliberately break boot
|
|
189
|
+
(rename a pack file), and each layer-5 gate fails on its targeted regression —
|
|
190
|
+
leak gate when a listener loses its `.off()`, determinism gate when a
|
|
191
|
+
`Math.random()` sneaks in, visual gate when the frame changes. A gate that
|
|
192
|
+
can't fail is decoration.
|
|
@@ -1,59 +1,126 @@
|
|
|
1
1
|
# Phaser 4 Project Rules
|
|
2
2
|
|
|
3
|
-
This is a **Phaser 4** (v4.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
This is a **Phaser 4** (v4.2+, npm `phaser@^4.2.0`) TypeScript game project built
|
|
4
|
+
on the official `template-vite-ts` layout (Vite + TypeScript 6). Phaser 4 is the
|
|
5
|
+
only supported target — never introduce Phaser 3 idioms. These decisions are
|
|
6
|
+
**locked** and most are **lint-enforced**; do not disable a rule, fix the code.
|
|
7
|
+
|
|
8
|
+
## Locked architecture decisions
|
|
9
|
+
|
|
10
|
+
1. **Thin scenes over a pure-logic core.** Scenes and GameObjects are thin
|
|
11
|
+
adapters that wire pure logic to the engine. All game rules live in
|
|
12
|
+
`src/logic/**` as plain TypeScript with **zero `phaser` imports** (data in,
|
|
13
|
+
data out). This is what makes the game unit-testable. ECS (bitECS / Miniplex)
|
|
14
|
+
is an opt-in option, not the default — reach for it only at high entity
|
|
15
|
+
counts. See `phaser-project-structure` and `phaser-testing`.
|
|
16
|
+
2. **Canvas-only, framework-agnostic UI.** UI is plain TS + DOM/canvas (the
|
|
17
|
+
`template-vite-ts` baseline). A React/Vue EventBus bridge is documented only
|
|
18
|
+
as an upgrade path — it is not the default and is not added unless required.
|
|
19
|
+
3. **Arcade physics by default; Matter is opt-in.** Matter is heavier and does
|
|
20
|
+
not interop with Arcade. Switching is a one-line config swap. See
|
|
21
|
+
`phaser-physics`.
|
|
22
|
+
4. **Determinism.** No `Math.random()`, `Date.now()`, or `performance.now()` in
|
|
23
|
+
game code. Use the seeded `Phaser.Math.RandomDataGenerator` (`Phaser.Math.RND`
|
|
24
|
+
or a local instance seeded from one place) so replays and tests reproduce.
|
|
25
|
+
5. **State and events.** Global state goes through a **typed wrapper over the
|
|
26
|
+
registry**; cross-cutting events go through a **single dedicated
|
|
27
|
+
EventsCenter** (`new Phaser.Events.EventEmitter()`) — **never reuse
|
|
28
|
+
`game.events`**. Every external `.on()` (`this.input` / `scale` / `time` /
|
|
29
|
+
`window` / the custom bus) must have a matching `.off()` in the scene's
|
|
30
|
+
`shutdown`. See `phaser-services`.
|
|
31
|
+
6. **Performance.** Pool transient objects via `Group`s; make **no allocations
|
|
32
|
+
and create no objects/tweens/timers inside `update()`**; atlas everything; use
|
|
33
|
+
`BitmapText` for high-churn text; use `SpriteGPULayer` / `TilemapGPULayer` for
|
|
34
|
+
mass rendering. See `phaser-gameobjects`.
|
|
35
|
+
7. **Baked-in asset pipeline.** Raw art in `assets/src` is packed at build time
|
|
36
|
+
(free-tex-packer-core atlases, audiosprite, BMFont) into `public/assets`, and
|
|
37
|
+
a codegen step emits typed keys to `src/assets.ts` so a missing or renamed key
|
|
38
|
+
is a **compile error**. No raw string asset / scene / event keys anywhere.
|
|
39
|
+
See `phaser-asset-pipeline`.
|
|
40
|
+
8. **PWA on by default** via `vite-plugin-pwa` (`registerType: "autoUpdate"`,
|
|
41
|
+
`globPatterns` extended to cover game assets). See `phaser-build-deploy`.
|
|
42
|
+
9. **Cross-cutting services are scaffolded** in `src/services/**`: SoundService
|
|
43
|
+
(mobile audio unlock on first gesture), InputService (semantic actions, not
|
|
44
|
+
raw keys), SaveService (typed `localStorage` with a schema `VERSION` +
|
|
45
|
+
migration chain — never raw `localStorage`), a typed i18n string catalog (no
|
|
46
|
+
hardcoded user-facing strings), accessibility (prefers-reduced-motion,
|
|
47
|
+
pause-on-blur, keyboard-navigable menus, screen-reader live region), and a
|
|
48
|
+
vendor-neutral telemetry/analytics abstraction. In-game errors are wired to
|
|
49
|
+
the observability sink. See `phaser-services`, `phaser-i18n`,
|
|
50
|
+
`phaser-accessibility`.
|
|
51
|
+
10. **Testing is a pyramid.** Pure logic is unit-tested with Vitest;
|
|
52
|
+
scenes/entities are excluded from unit coverage and verified by **runtime
|
|
53
|
+
gates** in CI (boot smoke, allocation/perf budget, leak gate, determinism
|
|
54
|
+
gate, deterministic visual regression, bundle-size budget). See
|
|
55
|
+
`phaser-testing`. Build pins: `phaser ^4.2.0`, Vite, TypeScript 6.
|
|
56
|
+
|
|
57
|
+
## Enforced lint rules (prose must match machine enforcement)
|
|
58
|
+
|
|
59
|
+
The lint config enforces the rules below. Each exists because the prose above
|
|
60
|
+
relies on it — keep them in sync.
|
|
61
|
+
|
|
62
|
+
**`no-restricted-imports`**
|
|
63
|
+
|
|
64
|
+
- Importing `phaser` anywhere under `src/logic/**` is banned. The logic core
|
|
65
|
+
stays engine-free so Vitest runs it without a browser.
|
|
66
|
+
|
|
67
|
+
**`no-restricted-syntax`** bans:
|
|
68
|
+
|
|
69
|
+
- Removed v3 idioms: `setPipeline` / `setPostPipeline` / `resetPipeline`,
|
|
70
|
+
`setTintFill`, `preFX` / `postFX`, `Phaser.Geom.Point`, `Phaser.Struct`,
|
|
71
|
+
`BitmapMask`, `Mesh`, `Plane`, `Camera3D`. Use the v4 replacements
|
|
72
|
+
(RenderNodes, Filters, `Phaser.Math.Vector2`, native `Set`/`Map`, GPU layers).
|
|
73
|
+
- `Math.random()` / `Date.now()` / `performance.now()` in `src` game code — use
|
|
74
|
+
the seeded RND and the scene clock (`this.time.now`).
|
|
75
|
+
- Reusing `game.events` for app messaging — use the dedicated EventsCenter.
|
|
76
|
+
- `debug: true` inside a physics/game config object literal — debug stays off in
|
|
77
|
+
committed code (toggle locally, never commit it on).
|
|
78
|
+
|
|
79
|
+
**Direct-`localStorage` ban**
|
|
80
|
+
|
|
81
|
+
- Direct `localStorage` access outside `src/services/**` is banned. Persist
|
|
82
|
+
through SaveService (versioned + migrated).
|
|
83
|
+
|
|
84
|
+
**Custom `eslint-plugin-phaser` rules**
|
|
85
|
+
|
|
86
|
+
- `no-create-in-update` — no `this.add.*`, `tweens.add`, `time.addEvent`,
|
|
87
|
+
`physics.add.*`, or `new Phaser.*` inside an `update()` method. Create in
|
|
88
|
+
`create()`; in the loop, reuse and pool.
|
|
89
|
+
- `no-allocation-in-update` — no object/array literals, closures, or
|
|
90
|
+
`.map`/`.filter`/`.reduce` chains inside `update()`. Hoist scratch objects.
|
|
91
|
+
- `require-shutdown-cleanup` — a `Scene` that registers persistent external
|
|
92
|
+
listeners must define a `shutdown` handler that removes them.
|
|
20
93
|
|
|
21
94
|
## Architecture
|
|
22
95
|
|
|
23
96
|
- **One scene class per file** under `src/scenes/`, named after the scene key.
|
|
24
97
|
The scene flow is `Boot → Preloader → MainMenu → Game` (plus overlays). Boot
|
|
25
98
|
loads only what the Preloader needs; the Preloader loads everything else.
|
|
26
|
-
- **Pure game logic lives
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
(`this.load.pack`), not ad-hoc `load.image` lists scattered across scenes.
|
|
33
|
-
|
|
34
|
-
## Determinism
|
|
35
|
-
|
|
36
|
-
- **No `Math.random()` in game code.** Use the scene-seeded
|
|
37
|
-
`Phaser.Math.RandomDataGenerator` (`Phaser.Math.RND` or a local instance with
|
|
38
|
-
an explicit seed) so replays and tests are reproducible.
|
|
39
|
-
- Arcade physics keeps its v4 default `fixedStep: true`. Do not switch physics to
|
|
40
|
-
variable step to "fix" a tunneling bug — fix the body/velocity configuration.
|
|
99
|
+
- **Pure game logic lives in `src/logic/**`** with no `phaser` imports — plain
|
|
100
|
+
TypeScript that takes and returns data. Scenes/entities are thin orchestrators
|
|
101
|
+
that wire that logic to GameObjects. See `phaser-testing`.
|
|
102
|
+
- **Keys are typed constants** generated into `src/assets.ts` — never inline
|
|
103
|
+
magic strings for texture/audio/animation/scene/event keys. Load via asset-pack
|
|
104
|
+
manifests (`this.load.pack`), not ad-hoc `load.image` lists.
|
|
41
105
|
|
|
42
106
|
## Performance
|
|
43
107
|
|
|
44
|
-
- **No allocations in `update()
|
|
45
|
-
|
|
108
|
+
- **No allocations in `update()`** and **no object/tween/timer creation in
|
|
109
|
+
`update()`** (both lint-enforced) — hoist scratch objects, reuse vectors.
|
|
46
110
|
- **Pool, don't churn.** Bullets, enemies, particles, and pickups come from
|
|
47
|
-
`Group` pools (`get
|
|
48
|
-
- **Mass rendering uses the GPU layers.** Large
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- Target WebGL; the Canvas renderer is deprecated in v4 and only
|
|
52
|
-
explicit, documented fallback
|
|
111
|
+
`Group` pools (`get` / `killAndHide`), never `new` / `destroy` per spawn.
|
|
112
|
+
- **Mass rendering uses the GPU layers.** Large sprite fields → `SpriteGPULayer`;
|
|
113
|
+
large tile maps → `TilemapGPULayer`. Atlas everything (see
|
|
114
|
+
`phaser-asset-pipeline`); `BitmapText` for high-churn text.
|
|
115
|
+
- Target WebGL; the Canvas renderer is deprecated in v4 and acceptable only as an
|
|
116
|
+
explicit, documented fallback.
|
|
53
117
|
|
|
54
118
|
## Verification
|
|
55
119
|
|
|
56
|
-
Before reporting any change complete: run `bun run typecheck`, `bun run
|
|
57
|
-
and `bun run build`. For changes that affect rendering or input,
|
|
58
|
-
real browser — `bun run dev` plus a Playwright check (the game
|
|
59
|
-
renders, no console errors).
|
|
120
|
+
Before reporting any change complete: run `bun run typecheck`, `bun run lint`,
|
|
121
|
+
`bun run test`, and `bun run build`. For changes that affect rendering or input,
|
|
122
|
+
verify in the real browser — `bun run dev` plus a Playwright check (the game
|
|
123
|
+
boots, the canvas renders, no console errors). CI additionally runs the runtime
|
|
124
|
+
gates (`phaser-testing`). A green typecheck alone is not proof a game works.
|
|
125
|
+
</content>
|
|
126
|
+
</invoke>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phaser-accessibility
|
|
3
|
+
description: This skill should be used when making a Phaser 4 game accessible — honoring prefers-reduced-motion, pausing on window/tab blur, keyboard navigation and visible focus for menus, colorblind-safe palettes, and a screen-reader live region that announces game state through the DOM. Use it when building menus, settings, HUD, or any player-facing UI, or when an accessibility requirement comes up. Pairs with phaser-services, phaser-i18n, and phaser-scenes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Phaser 4 Accessibility
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The canvas is opaque to assistive tech, so accessibility is something you build
|
|
11
|
+
in, not get for free. Five pillars carry most of the value: respect
|
|
12
|
+
`prefers-reduced-motion`, pause when the window loses focus, make menus fully
|
|
13
|
+
keyboard-navigable with visible focus, use colorblind-safe palettes, and mirror
|
|
14
|
+
game state into a DOM **live region** for screen readers. These wire through the
|
|
15
|
+
shared services ([[phaser-services]]) and use translated strings ([[phaser-i18n]]).
|
|
16
|
+
|
|
17
|
+
## prefers-reduced-motion
|
|
18
|
+
|
|
19
|
+
Read the media query once and gate non-essential motion (screen shake, parallax,
|
|
20
|
+
big tweens, particle bursts) behind it. Reduced motion means "convey the same
|
|
21
|
+
information with less movement", not "remove feedback".
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/services/a11y.ts
|
|
25
|
+
export const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
26
|
+
|
|
27
|
+
// in a scene
|
|
28
|
+
if (!reduceMotion) this.cameras.main.shake(120, 0.004);
|
|
29
|
+
this.tweens.add({ targets: panel, alpha: 1, duration: reduceMotion ? 0 : 250 });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Expose it as a togglable setting too (some players want it on regardless of OS),
|
|
33
|
+
stored via SaveService ([[phaser-services]]).
|
|
34
|
+
|
|
35
|
+
## Pause on blur
|
|
36
|
+
|
|
37
|
+
A game that keeps running while the tab is hidden drains battery, desyncs audio,
|
|
38
|
+
and is hostile to players who switch away. Phaser already exposes blur/focus
|
|
39
|
+
events — pause the loop and audio, resume on focus.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
this.game.events.on(Phaser.Core.Events.BLUR, () => { this.scene.pause(); this.sound.pauseAll(); });
|
|
43
|
+
this.game.events.on(Phaser.Core.Events.FOCUS, () => { this.scene.resume(); this.sound.resumeAll(); });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Set `Phaser.Types.Core.GameConfig.autoFocus` and `disableContextMenu` as needed.
|
|
47
|
+
(These are listeners — remove them in `shutdown` per the on/off discipline in
|
|
48
|
+
[[phaser-services]].) For competitive/timed play, pausing on blur is also the
|
|
49
|
+
fair behavior.
|
|
50
|
+
|
|
51
|
+
## Keyboard navigation and visible focus
|
|
52
|
+
|
|
53
|
+
Every menu must be operable without a pointer: arrow keys / Tab move focus, Enter
|
|
54
|
+
or Space activates, Escape backs out. Keep a focus index over a typed list of
|
|
55
|
+
items and render an unmistakable focus indicator (outline/scale, not color alone).
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
private items: MenuItem[] = [];
|
|
59
|
+
private focus = 0;
|
|
60
|
+
|
|
61
|
+
private move(delta: number) {
|
|
62
|
+
this.items[this.focus].setFocused(false);
|
|
63
|
+
this.focus = Phaser.Math.Wrap(this.focus + delta, 0, this.items.length);
|
|
64
|
+
this.items[this.focus].setFocused(true); // visible outline + announce
|
|
65
|
+
this.announce(this.items[this.focus].label);
|
|
66
|
+
}
|
|
67
|
+
create() {
|
|
68
|
+
this.input.keyboard!.on("keydown-DOWN", () => this.move(1));
|
|
69
|
+
this.input.keyboard!.on("keydown-UP", () => this.move(-1));
|
|
70
|
+
this.input.keyboard!.on("keydown-ENTER",() => this.items[this.focus].activate());
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use the semantic InputService actions ([[phaser-services]]) so the same bindings
|
|
75
|
+
serve gamepad and touch. Never rely on hover-only affordances.
|
|
76
|
+
|
|
77
|
+
## Colorblind-safe palettes
|
|
78
|
+
|
|
79
|
+
Never encode meaning in hue alone — pair every color cue with a shape, icon,
|
|
80
|
+
label, or pattern (red enemy *and* a spike icon; green/red status *and* a check/x).
|
|
81
|
+
Choose palettes that stay distinguishable across deuteranopia/protanopia
|
|
82
|
+
/tritanopia, and offer a colorblind palette option in settings.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// src/logic/palette.ts — pure, testable, no phaser import
|
|
86
|
+
export const palettes = {
|
|
87
|
+
default: { ally: 0x2e7d32, enemy: 0xc62828, neutral: 0xf9a825 },
|
|
88
|
+
deuter: { ally: 0x0072b2, enemy: 0xd55e00, neutral: 0xf0e442 }, // Okabe–Ito, CB-safe
|
|
89
|
+
} as const;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Keep palette selection in `src/logic/**` so contrast/choice logic is unit-tested,
|
|
93
|
+
and store the player's choice via SaveService.
|
|
94
|
+
|
|
95
|
+
## Screen-reader live region
|
|
96
|
+
|
|
97
|
+
Mirror important state changes into an ARIA live region in the DOM (outside the
|
|
98
|
+
canvas) so screen readers announce them. One polite region for status, one
|
|
99
|
+
assertive for urgent events.
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<!-- index.html, alongside the game container -->
|
|
103
|
+
<div id="sr-status" aria-live="polite" class="sr-only"></div>
|
|
104
|
+
<div id="sr-alert" aria-live="assertive" class="sr-only"></div>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// src/services/a11y.ts
|
|
109
|
+
const status = document.getElementById("sr-status")!;
|
|
110
|
+
export function announce(msg: string, urgent = false) {
|
|
111
|
+
const el = urgent ? document.getElementById("sr-alert")! : status;
|
|
112
|
+
el.textContent = ""; el.textContent = msg; // clear+set so identical repeats re-announce
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Announce menu focus, score milestones, level changes, and game-over. Messages
|
|
117
|
+
come from the i18n catalog ([[phaser-i18n]]) — never hardcoded — and `.sr-only`
|
|
118
|
+
keeps the region visually hidden but readable by AT.
|
|
119
|
+
|
|
120
|
+
## Project conventions
|
|
121
|
+
|
|
122
|
+
- A11y plumbing lives in `src/services/a11y.ts`; palette/choice logic lives in
|
|
123
|
+
`src/logic/**` (unit-tested).
|
|
124
|
+
- Accessibility settings (reduced motion, colorblind palette) persist via
|
|
125
|
+
SaveService and apply on boot ([[phaser-services]]).
|
|
126
|
+
- All announced/visible text comes from the i18n catalog ([[phaser-i18n]]).
|
|
127
|
+
|
|
128
|
+
## Verification
|
|
129
|
+
|
|
130
|
+
Verified by manual passes that double as runtime checks: tab through every menu
|
|
131
|
+
with the pointer unplugged and confirm visible focus + activation; set the OS to
|
|
132
|
+
reduce-motion and confirm shake/parallax stop while feedback remains; blur the
|
|
133
|
+
tab and confirm the game pauses and audio stops; and run a screen reader (VoiceOver
|
|
134
|
+
/NVDA) to confirm the live region announces menu focus and game state.
|
|
135
|
+
</content>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phaser-asset-pipeline
|
|
3
|
+
description: This skill should be used when setting up or changing the build-time asset pipeline of a Phaser 4 game — packing raw art in assets/src into public/assets (free-tex-packer-core texture atlases, audiosprite audio sprites, BMFont bitmap fonts) and the codegen step that emits typed keys to src/assets.ts so a missing or renamed key is a compile error. Use it when adding source art, wiring packing into the build, regenerating typed keys, or eliminating raw string asset keys. Pairs with phaser-assets, phaser-build-deploy, and phaser-services.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Phaser 4 Asset Pipeline
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Runtime assets are **generated, not hand-placed**. Raw source art lives in
|
|
11
|
+
`assets/src/**` (PNGs, audio, font sources); a build step packs it into
|
|
12
|
+
`public/assets/**` (atlases, audio sprites, bitmap fonts) and **codegens typed
|
|
13
|
+
key constants into `src/assets.ts`**. The payoff: a missing or renamed asset is a
|
|
14
|
+
**compile error**, not a green square or silent-missing audio at runtime. No raw
|
|
15
|
+
string asset / scene / event keys anywhere — they all come from the generated
|
|
16
|
+
module ([[phaser-assets]] covers loading those keys at runtime).
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
| Path | Role |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `assets/src/sprites/**` | Raw individual PNGs — the art source of truth |
|
|
23
|
+
| `assets/src/audio/**` | Raw audio clips (wav/ogg sources) |
|
|
24
|
+
| `assets/src/fonts/**` | Font sources for BMFont generation |
|
|
25
|
+
| `public/assets/**` | **Generated** packed output (atlases, audiosprites, fonts, `pack.json`) — git-ignored or committed as a build artifact |
|
|
26
|
+
| `src/assets.ts` | **Generated** typed key constants — never edited by hand |
|
|
27
|
+
| `scripts/pack-assets.mjs` | The pipeline driver (runs the three packers + codegen) |
|
|
28
|
+
|
|
29
|
+
## Step 1: texture atlases with free-tex-packer-core
|
|
30
|
+
|
|
31
|
+
`free-tex-packer-core` packs many PNGs into atlas pages plus a JSON manifest, in
|
|
32
|
+
a Node script (no GUI, runs in CI):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// scripts/pack-assets.mjs (excerpt)
|
|
36
|
+
import { packAsync } from "free-tex-packer-core";
|
|
37
|
+
import { glob } from "glob";
|
|
38
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
39
|
+
|
|
40
|
+
const images = await Promise.all(
|
|
41
|
+
(await glob("assets/src/sprites/**/*.png")).map(async p => ({
|
|
42
|
+
path: p.replace("assets/src/sprites/", ""), contents: await readFile(p),
|
|
43
|
+
})));
|
|
44
|
+
|
|
45
|
+
const files = await packAsync(images, {
|
|
46
|
+
textureName: "game", width: 2048, height: 2048,
|
|
47
|
+
padding: 2, allowRotation: false, allowTrim: true,
|
|
48
|
+
exporter: "JsonHash", removeFileExtension: true,
|
|
49
|
+
});
|
|
50
|
+
for (const f of files) await writeFile(`public/assets/atlases/${f.name}`, f.buffer ?? f.content);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The frame names in the manifest become the atlas frame keys you reference as
|
|
54
|
+
generated constants. Atlas everything that renders together — loose images break
|
|
55
|
+
sprite batching ([[phaser-assets]]).
|
|
56
|
+
|
|
57
|
+
## Step 2: audio sprites with audiosprite
|
|
58
|
+
|
|
59
|
+
`audiosprite` concatenates short SFX into one file (in multiple codecs) plus a
|
|
60
|
+
JSON map of `{ start, end }` per clip — one request, one decode, instead of dozens:
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
// package.json script
|
|
64
|
+
"pack:audio": "audiosprite --output public/assets/audio/sfx --export ogg,m4a --format howler2 assets/src/audio/*.wav"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Provide at least two codecs (`ogg` + `m4a`) so every browser can play one. Load
|
|
68
|
+
with `this.load.audioSprite(Audio.Sfx, "audio/sfx.json")` and play by sprite key.
|
|
69
|
+
|
|
70
|
+
## Step 3: bitmap fonts with BMFont
|
|
71
|
+
|
|
72
|
+
`BitmapText` is the performant choice for high-churn text (scores, timers) —
|
|
73
|
+
[[phaser-assets]] / [[phaser-i18n]]. Generate the `.fnt` (XML/JSON) + PNG page
|
|
74
|
+
from a font source with a BMFont tool (`msdf-bmfont-xml` or the `bmfont` CLI) in
|
|
75
|
+
the same script:
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
"pack:font": "msdf-bmfont -o public/assets/fonts/ui --font-size 42 --texture-size 1024 1024 assets/src/fonts/ui.ttf"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Load with `this.load.bitmapFont(Font.UI, "fonts/ui.png", "fonts/ui.fnt")`.
|
|
82
|
+
|
|
83
|
+
## Step 4: codegen typed keys into `src/assets.ts`
|
|
84
|
+
|
|
85
|
+
After packing, walk the generated manifests and emit `as const` key maps. This is
|
|
86
|
+
the step that turns runtime failures into compile failures:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// scripts/pack-assets.mjs (codegen excerpt)
|
|
90
|
+
const atlas = JSON.parse(await readFile("public/assets/atlases/game.json", "utf8"));
|
|
91
|
+
const frames = Object.keys(atlas.frames); // raw keys, e.g. "player/idle"
|
|
92
|
+
const sfx = Object.keys(JSON.parse(await readFile("public/assets/audio/sfx.json","utf8")).spritemap);
|
|
93
|
+
|
|
94
|
+
const out = `// AUTO-GENERATED by scripts/pack-assets.mjs — do not edit.
|
|
95
|
+
export const Tex = { GameAtlas: "game" } as const;
|
|
96
|
+
export const Frame = { ${frames.map(k => `${toIdent(k)}: "${k}"`).join(", ")} } as const;
|
|
97
|
+
export const Audio = { Sfx: "sfx" } as const;
|
|
98
|
+
export const Sfx = { ${sfx.map(s => `${toIdent(s)}: "${s}"`).join(", ")} } as const;
|
|
99
|
+
export const Font = { UI: "ui" } as const;
|
|
100
|
+
export const SceneKeys = { Boot:"Boot", Preloader:"Preloader", MainMenu:"MainMenu", Game:"Game" } as const;
|
|
101
|
+
export const GameEvent = { ScoreChanged:"score-changed", EnemyDied:"enemy-died" } as const;
|
|
102
|
+
`;
|
|
103
|
+
await writeFile("src/assets.ts", out);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Scenes import `Tex`, `Frame`, `Sfx`, `Font`, `SceneKeys`, `GameEvent` — never an
|
|
107
|
+
inline string. Rename art, re-run the pipeline, and every now-stale reference
|
|
108
|
+
fails `bun run typecheck` immediately. (Scene/event keys are kept in the same
|
|
109
|
+
generated module so they share the no-raw-string discipline; edit them via the
|
|
110
|
+
codegen template, not by hand.)
|
|
111
|
+
|
|
112
|
+
## Step 5: wire into the build
|
|
113
|
+
|
|
114
|
+
The pipeline runs before dev and before build so generated output is never stale:
|
|
115
|
+
|
|
116
|
+
```jsonc
|
|
117
|
+
// package.json
|
|
118
|
+
"scripts": {
|
|
119
|
+
"assets": "node scripts/pack-assets.mjs",
|
|
120
|
+
"predev": "bun run assets",
|
|
121
|
+
"prebuild": "bun run assets",
|
|
122
|
+
"dev": "vite",
|
|
123
|
+
"build": "vite build"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For fast iteration, a `--watch` mode on the pack script re-packs changed source
|
|
128
|
+
art. In CI the pack step runs once before `vite build`; the build then hashes the
|
|
129
|
+
generated files for immutable caching ([[phaser-build-deploy]]).
|
|
130
|
+
|
|
131
|
+
## Project conventions
|
|
132
|
+
|
|
133
|
+
- `assets/src/**` is the source of truth; `public/assets/**` and `src/assets.ts`
|
|
134
|
+
are generated — treat them as build output (re-runnable, not hand-edited).
|
|
135
|
+
- One atlas per render group, one audiosprite per SFX set, BMFont for hot text.
|
|
136
|
+
- A contract test asserts every constant in `src/assets.ts` resolves to an entry
|
|
137
|
+
in the generated `pack.json`/manifests ([[phaser-testing]]).
|
|
138
|
+
- The generated `src/assets.ts` is the single source of asset/scene/event keys —
|
|
139
|
+
the no-raw-string-keys rule depends on it existing.
|
|
140
|
+
|
|
141
|
+
## Verification
|
|
142
|
+
|
|
143
|
+
The pipeline is verified by deleting a source PNG and re-running `bun run assets`:
|
|
144
|
+
the corresponding generated constant disappears and `bun run typecheck` fails at
|
|
145
|
+
every use site (proving keys are compile-checked). Then boot the game and confirm
|
|
146
|
+
the atlas/audiosprite/font load with no `FILE_LOAD_ERROR` and render/play
|
|
147
|
+
correctly.
|
|
148
|
+
</content>
|