@codyswann/lisa 2.178.5 → 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.
Files changed (142) hide show
  1. package/dist/configs/eslint/phaser.d.ts +2 -8
  2. package/dist/configs/eslint/phaser.d.ts.map +1 -1
  3. package/dist/configs/eslint/phaser.js +143 -17
  4. package/dist/configs/eslint/phaser.js.map +1 -1
  5. package/eslint-plugin-phaser/README.md +19 -0
  6. package/eslint-plugin-phaser/index.js +35 -0
  7. package/eslint-plugin-phaser/package.json +10 -0
  8. package/eslint-plugin-phaser/rules/no-allocation-in-update.js +189 -0
  9. package/eslint-plugin-phaser/rules/no-create-in-update.js +200 -0
  10. package/eslint-plugin-phaser/rules/require-shutdown-cleanup.js +191 -0
  11. package/package.json +5 -3
  12. package/phaser/package-lisa/package.lisa.json +11 -3
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa/skills/analyze-claude-remote/SKILL.md +72 -2
  16. package/plugins/lisa/skills/generate-claude-remote-build-script/SKILL.md +30 -1
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-agy/skills/analyze-claude-remote/SKILL.md +72 -2
  19. package/plugins/lisa-agy/skills/generate-claude-remote-build-script/SKILL.md +30 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/skills/analyze-claude-remote/SKILL.md +72 -2
  27. package/plugins/lisa-copilot/skills/generate-claude-remote-build-script/SKILL.md +30 -1
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/skills/analyze-claude-remote/SKILL.md +72 -2
  30. package/plugins/lisa-cursor/skills/generate-claude-remote-build-script/SKILL.md +30 -1
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/agents/ops-specialist.md +2 -2
  34. package/plugins/lisa-expo/skills/ops-check-logs/SKILL.md +8 -2
  35. package/plugins/lisa-expo/skills/ops-db-ops/SKILL.md +7 -3
  36. package/plugins/lisa-expo/skills/ops-deploy/SKILL.md +6 -2
  37. package/plugins/lisa-expo/skills/ops-run-local/SKILL.md +5 -2
  38. package/plugins/lisa-expo-agy/agents/ops-specialist.md +2 -2
  39. package/plugins/lisa-expo-agy/plugin.json +1 -1
  40. package/plugins/lisa-expo-agy/skills/ops-check-logs/SKILL.md +8 -2
  41. package/plugins/lisa-expo-agy/skills/ops-db-ops/SKILL.md +7 -3
  42. package/plugins/lisa-expo-agy/skills/ops-deploy/SKILL.md +6 -2
  43. package/plugins/lisa-expo-agy/skills/ops-run-local/SKILL.md +5 -2
  44. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-copilot/agents/ops-specialist.agent.md +2 -2
  46. package/plugins/lisa-expo-copilot/skills/ops-check-logs/SKILL.md +8 -2
  47. package/plugins/lisa-expo-copilot/skills/ops-db-ops/SKILL.md +7 -3
  48. package/plugins/lisa-expo-copilot/skills/ops-deploy/SKILL.md +6 -2
  49. package/plugins/lisa-expo-copilot/skills/ops-run-local/SKILL.md +5 -2
  50. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo-cursor/agents/ops-specialist.md +2 -2
  52. package/plugins/lisa-expo-cursor/skills/ops-check-logs/SKILL.md +8 -2
  53. package/plugins/lisa-expo-cursor/skills/ops-db-ops/SKILL.md +7 -3
  54. package/plugins/lisa-expo-cursor/skills/ops-deploy/SKILL.md +6 -2
  55. package/plugins/lisa-expo-cursor/skills/ops-run-local/SKILL.md +5 -2
  56. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/rules/phaser.md +111 -44
  74. package/plugins/lisa-phaser/skills/phaser-accessibility/SKILL.md +135 -0
  75. package/plugins/lisa-phaser/skills/phaser-accessibility/agents/openai.yaml +4 -0
  76. package/plugins/lisa-phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
  77. package/plugins/lisa-phaser/skills/phaser-asset-pipeline/agents/openai.yaml +4 -0
  78. package/plugins/lisa-phaser/skills/phaser-build-deploy/SKILL.md +148 -0
  79. package/plugins/lisa-phaser/skills/phaser-build-deploy/agents/openai.yaml +4 -0
  80. package/plugins/lisa-phaser/skills/phaser-i18n/SKILL.md +122 -0
  81. package/plugins/lisa-phaser/skills/phaser-i18n/agents/openai.yaml +4 -0
  82. package/plugins/lisa-phaser/skills/phaser-services/SKILL.md +208 -0
  83. package/plugins/lisa-phaser/skills/phaser-services/agents/openai.yaml +4 -0
  84. package/plugins/lisa-phaser/skills/phaser-testing/SKILL.md +103 -10
  85. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  86. package/plugins/lisa-phaser-agy/skills/phaser-accessibility/SKILL.md +135 -0
  87. package/plugins/lisa-phaser-agy/skills/phaser-asset-pipeline/SKILL.md +148 -0
  88. package/plugins/lisa-phaser-agy/skills/phaser-build-deploy/SKILL.md +148 -0
  89. package/plugins/lisa-phaser-agy/skills/phaser-i18n/SKILL.md +122 -0
  90. package/plugins/lisa-phaser-agy/skills/phaser-services/SKILL.md +208 -0
  91. package/plugins/lisa-phaser-agy/skills/phaser-testing/SKILL.md +103 -10
  92. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-phaser-copilot/rules/phaser.md +111 -44
  94. package/plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md +135 -0
  95. package/plugins/lisa-phaser-copilot/skills/phaser-asset-pipeline/SKILL.md +148 -0
  96. package/plugins/lisa-phaser-copilot/skills/phaser-build-deploy/SKILL.md +148 -0
  97. package/plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md +122 -0
  98. package/plugins/lisa-phaser-copilot/skills/phaser-services/SKILL.md +208 -0
  99. package/plugins/lisa-phaser-copilot/skills/phaser-testing/SKILL.md +103 -10
  100. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-phaser-cursor/rules/phaser.mdc +111 -44
  102. package/plugins/lisa-phaser-cursor/skills/phaser-accessibility/SKILL.md +135 -0
  103. package/plugins/lisa-phaser-cursor/skills/phaser-asset-pipeline/SKILL.md +148 -0
  104. package/plugins/lisa-phaser-cursor/skills/phaser-build-deploy/SKILL.md +148 -0
  105. package/plugins/lisa-phaser-cursor/skills/phaser-i18n/SKILL.md +122 -0
  106. package/plugins/lisa-phaser-cursor/skills/phaser-services/SKILL.md +208 -0
  107. package/plugins/lisa-phaser-cursor/skills/phaser-testing/SKILL.md +103 -10
  108. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-rails/agents/ops-specialist.md +1 -1
  111. package/plugins/lisa-rails-agy/agents/ops-specialist.md +1 -1
  112. package/plugins/lisa-rails-agy/plugin.json +1 -1
  113. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-rails-copilot/agents/ops-specialist.agent.md +1 -1
  115. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  116. package/plugins/lisa-rails-cursor/agents/ops-specialist.md +1 -1
  117. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  119. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  120. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  124. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  125. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  127. package/plugins/src/base/skills/analyze-claude-remote/SKILL.md +72 -2
  128. package/plugins/src/base/skills/generate-claude-remote-build-script/SKILL.md +30 -1
  129. package/plugins/src/expo/agents/ops-specialist.md +2 -2
  130. package/plugins/src/expo/skills/ops-check-logs/SKILL.md +8 -2
  131. package/plugins/src/expo/skills/ops-db-ops/SKILL.md +7 -3
  132. package/plugins/src/expo/skills/ops-deploy/SKILL.md +6 -2
  133. package/plugins/src/expo/skills/ops-run-local/SKILL.md +5 -2
  134. package/plugins/src/phaser/rules/phaser.md +111 -44
  135. package/plugins/src/phaser/skills/phaser-accessibility/SKILL.md +135 -0
  136. package/plugins/src/phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
  137. package/plugins/src/phaser/skills/phaser-build-deploy/SKILL.md +148 -0
  138. package/plugins/src/phaser/skills/phaser-i18n/SKILL.md +122 -0
  139. package/plugins/src/phaser/skills/phaser-services/SKILL.md +208 -0
  140. package/plugins/src/phaser/skills/phaser-testing/SKILL.md +103 -10
  141. package/plugins/src/rails/agents/ops-specialist.md +1 -1
  142. package/tsconfig/phaser.json +8 -1
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: phaser-services
3
+ description: This skill should be used when wiring cross-cutting services and shared state in a Phaser 4 game — the typed registry wrapper for global state, a single dedicated EventsCenter bus (never game.events), the .on()/.off() listener discipline enforced in scene shutdown, SoundService (mobile audio unlock on first gesture), InputService (semantic actions instead of raw keys), and SaveService (versioned localStorage with a migration chain). Use it when adding global state, an event bus, audio/input/save plumbing, or fixing listener leaks and lost saves. Pairs with phaser-scenes, phaser-testing, and phaser-i18n.
4
+ ---
5
+
6
+ # Phaser 4 Services and Shared State
7
+
8
+ ## Overview
9
+
10
+ Cross-cutting concerns — global state, app events, sound, input, save — live in
11
+ `src/services/**` as thin, typed singletons, **not** scattered through scenes.
12
+ Two rules anchor everything: state flows through a typed registry wrapper, and
13
+ app messaging flows through **one dedicated EventsCenter** that is never
14
+ `game.events`. Direct `localStorage` access outside `src/services/**` is
15
+ lint-banned — persistence goes through SaveService.
16
+
17
+ ## Global state: a typed wrapper over the registry
18
+
19
+ `this.registry` is a game-wide key/value `DataManager` shared across scenes. Raw
20
+ access is stringly-typed and untestable, so wrap it once:
21
+
22
+ ```ts
23
+ // src/services/state.ts
24
+ interface GameState { coins: number; level: number; muted: boolean; seed: number; }
25
+ const DEFAULTS: GameState = { coins: 0, level: 1, muted: false, seed: 0 };
26
+
27
+ export class GameStore {
28
+ constructor(private reg: Phaser.Data.DataManager) {
29
+ for (const k in DEFAULTS) if (!reg.has(k)) reg.set(k, (DEFAULTS as any)[k]);
30
+ }
31
+ get<K extends keyof GameState>(k: K): GameState[K] { return this.reg.get(k); }
32
+ set<K extends keyof GameState>(k: K, v: GameState[K]) { this.reg.set(k, v); }
33
+ onChange<K extends keyof GameState>(k: K, fn: (v: GameState[K]) => void) {
34
+ const h = (_p: unknown, key: string, v: GameState[K]) => { if (key === k) fn(v); };
35
+ this.reg.events.on(Phaser.Data.Events.CHANGE_DATA, h);
36
+ return () => this.reg.events.off(Phaser.Data.Events.CHANGE_DATA, h); // caller .off()s in shutdown
37
+ }
38
+ }
39
+ ```
40
+
41
+ Construct it once (in Boot) and read it via the registry. Keys are typed by
42
+ `GameState`, so a typo is a compile error. The registry holds *small* shared
43
+ state (settings, run seed, currency) — not GameObjects and not per-frame data.
44
+
45
+ ## The EventsCenter bus (never `game.events`)
46
+
47
+ App-level messaging ("enemy-died", "score-changed", "level-cleared") uses a
48
+ single dedicated emitter. Reusing `game.events` (the engine's own bus) is
49
+ lint-banned — it mixes your events with engine lifecycle events and is
50
+ impossible to fully tear down.
51
+
52
+ ```ts
53
+ // src/services/event-center.ts
54
+ export const EventCenter = new Phaser.Events.EventEmitter();
55
+
56
+ // typed key constants come from src/assets.ts (generated) — no raw strings
57
+ import { GameEvent } from "../assets";
58
+ EventCenter.emit(GameEvent.ScoreChanged, score);
59
+ ```
60
+
61
+ ## Listener discipline: every `.on()` has a matching `.off()`
62
+
63
+ This is the leak rule the `require-shutdown-cleanup` lint rule and the testing
64
+ leak gate ([[phaser-testing]]) enforce. **Any** external listener — `EventCenter`,
65
+ `this.input`, `this.scale`, `this.time`, `window`, the registry — must be removed
66
+ in the scene's `shutdown`. Listeners on the scene's own display objects die with
67
+ the scene; external ones do not, and they fire again (doubled) after a restart.
68
+
69
+ ```ts
70
+ export class Game extends Phaser.Scene {
71
+ private cleanups: Array<() => void> = [];
72
+
73
+ create() {
74
+ const onResize = (s: Phaser.Structs.Size) => this.layout(s.width, s.height);
75
+ this.scale.on(Phaser.Scale.Events.RESIZE, onResize);
76
+ this.cleanups.push(() => this.scale.off(Phaser.Scale.Events.RESIZE, onResize));
77
+
78
+ const onScore = (n: number) => this.hud.setScore(n);
79
+ EventCenter.on(GameEvent.ScoreChanged, onScore);
80
+ this.cleanups.push(() => EventCenter.off(GameEvent.ScoreChanged, onScore));
81
+
82
+ this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.shutdown, this);
83
+ }
84
+
85
+ shutdown() { this.cleanups.forEach(fn => fn()); this.cleanups.length = 0; }
86
+ }
87
+ ```
88
+
89
+ Keeping the matching `.off()` next to each `.on()` (a `cleanups` array, or named
90
+ handler methods you remove in `shutdown`) is the pattern that survives the leak
91
+ gate. Prefer `.once()` where a listener should fire only once.
92
+
93
+ ## SoundService — audio unlock on first gesture
94
+
95
+ Web Audio starts **suspended** until a user gesture; sound played before then is
96
+ silently dropped ("works on my machine, silent on the phone"). Centralize the
97
+ unlock and all playback:
98
+
99
+ ```ts
100
+ // src/services/sound-service.ts
101
+ export class SoundService {
102
+ private unlocked = false;
103
+ constructor(private sound: Phaser.Sound.BaseSoundManager, private store: GameStore) {}
104
+
105
+ attachUnlock(scene: Phaser.Scene) {
106
+ if (this.unlocked) return;
107
+ scene.input.once(Phaser.Input.Events.POINTER_DOWN, () => this.resume());
108
+ // Phaser also fires its own unlock; resume() is idempotent
109
+ this.sound.once(Phaser.Sound.Events.UNLOCKED, () => this.resume());
110
+ }
111
+ private resume() { if ((this.sound as any).context?.state === "suspended") (this.sound as any).context.resume(); this.unlocked = true; }
112
+ play(key: string, cfg?: Phaser.Types.Sound.SoundConfig) { if (!this.store.get("muted")) this.sound.play(key, cfg); }
113
+ }
114
+ ```
115
+
116
+ Scenes call `soundService.play(Sfx.Jump)` — never `this.sound.play("jump")`.
117
+ Sound keys are generated constants ([[phaser-asset-pipeline]]).
118
+
119
+ ## InputService — semantic actions, not raw keys
120
+
121
+ Scenes should ask "is *jump* down?", not "is the spacebar down?". A semantic
122
+ layer makes rebinding, gamepads, and touch swap in without touching game code,
123
+ and keeps replays deterministic (record actions, not hardware).
124
+
125
+ ```ts
126
+ // src/services/input-service.ts
127
+ export type Action = "left" | "right" | "jump" | "fire" | "pause";
128
+
129
+ export class InputService {
130
+ private down = new Set<Action>();
131
+ private bindings: Record<number, Action> = {};
132
+ constructor(kb: Phaser.Input.Keyboard.KeyboardPlugin) {
133
+ this.bindings[Phaser.Input.Keyboard.KeyCodes.LEFT] = "left";
134
+ this.bindings[Phaser.Input.Keyboard.KeyCodes.SPACE] = "jump";
135
+ kb.on("keydown", (e: KeyboardEvent) => { const a = this.bindings[e.keyCode]; if (a) this.down.add(a); });
136
+ kb.on("keyup", (e: KeyboardEvent) => { const a = this.bindings[e.keyCode]; if (a) this.down.delete(a); });
137
+ }
138
+ isDown(a: Action) { return this.down.has(a); }
139
+ snapshot(): readonly Action[] { return [...this.down]; } // for replay capture
140
+ }
141
+ ```
142
+
143
+ `update()` reads `input.isDown("jump")` and forwards the action set to pure logic
144
+ in `src/logic/**`. (Register/remove the keyboard listeners with the same on/off
145
+ discipline above.)
146
+
147
+ ## SaveService — versioned localStorage with a migration chain
148
+
149
+ Raw `localStorage` outside `src/services/**` is lint-banned. Saves carry a schema
150
+ `VERSION`; on load, unknown-but-older saves run forward through a migration chain,
151
+ so an old player's data is never silently lost or crash-loaded.
152
+
153
+ ```ts
154
+ // src/services/save-service.ts
155
+ const KEY = "save:v"; const VERSION = 3;
156
+ interface SaveV3 { version: 3; coins: number; unlocked: string[]; settings: { muted: boolean } }
157
+
158
+ const migrations: Record<number, (s: any) => any> = {
159
+ 1: s => ({ ...s, unlocked: [], version: 2 }), // v1 -> v2
160
+ 2: s => ({ ...s, settings: { muted: false }, version: 3 }), // v2 -> v3
161
+ };
162
+
163
+ export class SaveService {
164
+ load(): SaveV3 {
165
+ const raw = localStorage.getItem(KEY); // only legal localStorage access lives here
166
+ if (!raw) return this.fresh();
167
+ try {
168
+ let s = JSON.parse(raw);
169
+ while (s.version < VERSION) s = migrations[s.version](s);
170
+ return s as SaveV3;
171
+ } catch { return this.fresh(); } // corrupt save -> fresh, never crash
172
+ }
173
+ save(s: SaveV3) { localStorage.setItem(KEY, JSON.stringify({ ...s, version: VERSION })); }
174
+ private fresh(): SaveV3 { return { version: VERSION, coins: 0, unlocked: [], settings: { muted: false } }; }
175
+ }
176
+ ```
177
+
178
+ Bump `VERSION` and add the `n -> n+1` migration whenever the shape changes; never
179
+ mutate the read path to "just handle" an old shape inline.
180
+
181
+ ## Errors and telemetry
182
+
183
+ A vendor-neutral telemetry/analytics abstraction (`src/services/telemetry.ts`)
184
+ exposes `track(event, props)` and `captureError(err, ctx)` behind a stable
185
+ interface so the concrete sink (PostHog, Sentry, none) swaps without touching
186
+ game code. Wire Phaser's error surfaces to it — `window.onerror`, the loader's
187
+ `FILE_LOAD_ERROR`, and `try/catch` around scene transitions all route to
188
+ `captureError`. Strings shown to players come from the i18n catalog
189
+ ([[phaser-i18n]]), never hardcoded.
190
+
191
+ ## Project conventions
192
+
193
+ - Services live in `src/services/**`; that directory is the **only** place raw
194
+ `localStorage` is permitted.
195
+ - One EventsCenter instance, exported once; never `game.events` for app events.
196
+ - Every external `.on()` is paired with an `.off()` removed in `shutdown` (lint:
197
+ `require-shutdown-cleanup`; verified by the leak gate in [[phaser-testing]]).
198
+ - Service logic that is pure (migrations, action mapping, mute gating) belongs in
199
+ or beside `src/logic/**` so Vitest covers it.
200
+
201
+ ## Verification
202
+
203
+ Services are verified by the runtime gates: the leak gate ([[phaser-testing]])
204
+ proves listeners are removed (start/stop a scene N times, counts return to
205
+ baseline); a SaveService unit test feeds each old version through the migration
206
+ chain and asserts the current shape; and on a real phone, the first tap unlocks
207
+ audio (sound plays) — confirm before committing.
208
+ </content>
@@ -0,0 +1,4 @@
1
+ display_name: "Phaser Services"
2
+ short_description: "wiring cross-cutting services and shared state in a Phaser 4 game — the typed registry wrapper for global state, a single dedicated…"
3
+ default_prompt:
4
+ - "Use $phaser-services: wiring cross-cutting services and shared state in a Phaser 4 game — the typed registry wrapper for global state, a single dedicated…."
@@ -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 Playwright smoke tests that prove the game actually boots and renders. 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-physics.
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. **Playwright smoke test** — the game boots in a real browser, renders past
17
- the Preloader, no console errors.
18
-
19
- There is no official Phaser testing harness — this layering IS the strategy.
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). Playwright smoke runs as
91
- its own script/CI job against a built preview.
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) or observable behavior (layer 4).
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/**`, and the Playwright smoke fails when you deliberately break
99
- boot (rename a pack file) a smoke test that can't fail is decoration.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-phaser",
3
- "version": "2.178.5",
3
+ "version": "2.179.0",
4
4
  "description": "Phaser 4 game-development rules for TypeScript projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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>