@codyswann/lisa 2.178.6 → 2.180.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 (145) 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/merge/.claude/settings.json +1 -0
  13. package/phaser/merge/.mcp.json +8 -0
  14. package/phaser/package-lisa/package.lisa.json +11 -3
  15. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-agy/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/rules/phaser.md +134 -44
  48. package/plugins/lisa-phaser/skills/phaser-accessibility/SKILL.md +135 -0
  49. package/plugins/lisa-phaser/skills/phaser-accessibility/agents/openai.yaml +4 -0
  50. package/plugins/lisa-phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
  51. package/plugins/lisa-phaser/skills/phaser-asset-pipeline/agents/openai.yaml +4 -0
  52. package/plugins/lisa-phaser/skills/phaser-build-deploy/SKILL.md +148 -0
  53. package/plugins/lisa-phaser/skills/phaser-build-deploy/agents/openai.yaml +4 -0
  54. package/plugins/lisa-phaser/skills/phaser-i18n/SKILL.md +122 -0
  55. package/plugins/lisa-phaser/skills/phaser-i18n/agents/openai.yaml +4 -0
  56. package/plugins/lisa-phaser/skills/phaser-project-structure/SKILL.md +5 -5
  57. package/plugins/lisa-phaser/skills/phaser-services/SKILL.md +208 -0
  58. package/plugins/lisa-phaser/skills/phaser-services/agents/openai.yaml +4 -0
  59. package/plugins/lisa-phaser/skills/phaser-testing/SKILL.md +104 -11
  60. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  61. package/plugins/lisa-phaser-agy/skills/phaser-accessibility/SKILL.md +135 -0
  62. package/plugins/lisa-phaser-agy/skills/phaser-asset-pipeline/SKILL.md +148 -0
  63. package/plugins/lisa-phaser-agy/skills/phaser-build-deploy/SKILL.md +148 -0
  64. package/plugins/lisa-phaser-agy/skills/phaser-i18n/SKILL.md +122 -0
  65. package/plugins/lisa-phaser-agy/skills/phaser-project-structure/SKILL.md +5 -5
  66. package/plugins/lisa-phaser-agy/skills/phaser-services/SKILL.md +208 -0
  67. package/plugins/lisa-phaser-agy/skills/phaser-testing/SKILL.md +104 -11
  68. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-copilot/rules/phaser.md +134 -44
  70. package/plugins/lisa-phaser-copilot/skills/phaser-accessibility/SKILL.md +135 -0
  71. package/plugins/lisa-phaser-copilot/skills/phaser-asset-pipeline/SKILL.md +148 -0
  72. package/plugins/lisa-phaser-copilot/skills/phaser-build-deploy/SKILL.md +148 -0
  73. package/plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md +122 -0
  74. package/plugins/lisa-phaser-copilot/skills/phaser-project-structure/SKILL.md +5 -5
  75. package/plugins/lisa-phaser-copilot/skills/phaser-services/SKILL.md +208 -0
  76. package/plugins/lisa-phaser-copilot/skills/phaser-testing/SKILL.md +104 -11
  77. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser-cursor/rules/phaser.mdc +134 -44
  79. package/plugins/lisa-phaser-cursor/skills/phaser-accessibility/SKILL.md +135 -0
  80. package/plugins/lisa-phaser-cursor/skills/phaser-asset-pipeline/SKILL.md +148 -0
  81. package/plugins/lisa-phaser-cursor/skills/phaser-build-deploy/SKILL.md +148 -0
  82. package/plugins/lisa-phaser-cursor/skills/phaser-i18n/SKILL.md +122 -0
  83. package/plugins/lisa-phaser-cursor/skills/phaser-project-structure/SKILL.md +5 -5
  84. package/plugins/lisa-phaser-cursor/skills/phaser-services/SKILL.md +208 -0
  85. package/plugins/lisa-phaser-cursor/skills/phaser-testing/SKILL.md +104 -11
  86. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-rails-agy/plugin.json +1 -1
  89. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  94. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  99. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/src/phaser/rules/phaser.md +134 -44
  102. package/plugins/src/phaser/skills/phaser-accessibility/SKILL.md +135 -0
  103. package/plugins/src/phaser/skills/phaser-asset-pipeline/SKILL.md +148 -0
  104. package/plugins/src/phaser/skills/phaser-build-deploy/SKILL.md +148 -0
  105. package/plugins/src/phaser/skills/phaser-i18n/SKILL.md +122 -0
  106. package/plugins/src/phaser/skills/phaser-project-structure/SKILL.md +5 -5
  107. package/plugins/src/phaser/skills/phaser-services/SKILL.md +208 -0
  108. package/plugins/src/phaser/skills/phaser-testing/SKILL.md +104 -11
  109. package/tsconfig/phaser.json +8 -1
  110. package/plugins/lisa-phaser/skills/phaser-assets/SKILL.md +0 -96
  111. package/plugins/lisa-phaser/skills/phaser-assets/agents/openai.yaml +0 -4
  112. package/plugins/lisa-phaser/skills/phaser-gameobjects/SKILL.md +0 -94
  113. package/plugins/lisa-phaser/skills/phaser-gameobjects/agents/openai.yaml +0 -4
  114. package/plugins/lisa-phaser/skills/phaser-physics/SKILL.md +0 -86
  115. package/plugins/lisa-phaser/skills/phaser-physics/agents/openai.yaml +0 -4
  116. package/plugins/lisa-phaser/skills/phaser-rendering/SKILL.md +0 -89
  117. package/plugins/lisa-phaser/skills/phaser-rendering/agents/openai.yaml +0 -4
  118. package/plugins/lisa-phaser/skills/phaser-scenes/SKILL.md +0 -86
  119. package/plugins/lisa-phaser/skills/phaser-scenes/agents/openai.yaml +0 -4
  120. package/plugins/lisa-phaser/skills/phaser-v3-migration/SKILL.md +0 -81
  121. package/plugins/lisa-phaser/skills/phaser-v3-migration/agents/openai.yaml +0 -4
  122. package/plugins/lisa-phaser-agy/skills/phaser-assets/SKILL.md +0 -96
  123. package/plugins/lisa-phaser-agy/skills/phaser-gameobjects/SKILL.md +0 -94
  124. package/plugins/lisa-phaser-agy/skills/phaser-physics/SKILL.md +0 -86
  125. package/plugins/lisa-phaser-agy/skills/phaser-rendering/SKILL.md +0 -89
  126. package/plugins/lisa-phaser-agy/skills/phaser-scenes/SKILL.md +0 -86
  127. package/plugins/lisa-phaser-agy/skills/phaser-v3-migration/SKILL.md +0 -81
  128. package/plugins/lisa-phaser-copilot/skills/phaser-assets/SKILL.md +0 -96
  129. package/plugins/lisa-phaser-copilot/skills/phaser-gameobjects/SKILL.md +0 -94
  130. package/plugins/lisa-phaser-copilot/skills/phaser-physics/SKILL.md +0 -86
  131. package/plugins/lisa-phaser-copilot/skills/phaser-rendering/SKILL.md +0 -89
  132. package/plugins/lisa-phaser-copilot/skills/phaser-scenes/SKILL.md +0 -86
  133. package/plugins/lisa-phaser-copilot/skills/phaser-v3-migration/SKILL.md +0 -81
  134. package/plugins/lisa-phaser-cursor/skills/phaser-assets/SKILL.md +0 -96
  135. package/plugins/lisa-phaser-cursor/skills/phaser-gameobjects/SKILL.md +0 -94
  136. package/plugins/lisa-phaser-cursor/skills/phaser-physics/SKILL.md +0 -86
  137. package/plugins/lisa-phaser-cursor/skills/phaser-rendering/SKILL.md +0 -89
  138. package/plugins/lisa-phaser-cursor/skills/phaser-scenes/SKILL.md +0 -86
  139. package/plugins/lisa-phaser-cursor/skills/phaser-v3-migration/SKILL.md +0 -81
  140. package/plugins/src/phaser/skills/phaser-assets/SKILL.md +0 -96
  141. package/plugins/src/phaser/skills/phaser-gameobjects/SKILL.md +0 -94
  142. package/plugins/src/phaser/skills/phaser-physics/SKILL.md +0 -86
  143. package/plugins/src/phaser/skills/phaser-rendering/SKILL.md +0 -89
  144. package/plugins/src/phaser/skills/phaser-scenes/SKILL.md +0 -86
  145. package/plugins/src/phaser/skills/phaser-v3-migration/SKILL.md +0 -81
@@ -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 the official scenes skill.
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 the official loading-assets skill, 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 (the official `loading-assets` skill 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 (the official `loading-assets` skill).
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
+ the official `loading-assets` skill / [[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>
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: phaser-build-deploy
3
+ description: This skill should be used when configuring the production build or deploying a Phaser 4 game — the Vite prod config (manualChunks to split the phaser vendor chunk, terser two-pass minification), content-hashed assets with immutable caching, the base path per host, vite-plugin-pwa (autoUpdate + globPatterns covering game assets), a bundle-size budget, and static hosting. Use it when setting up the build, fixing caching/base-path issues, adding the PWA, or enforcing bundle size. Pairs with phaser-asset-pipeline, phaser-project-structure, and phaser-testing.
4
+ ---
5
+
6
+ # Phaser 4 Build and Deploy
7
+
8
+ ## Overview
9
+
10
+ The game ships as static files built by **Vite** (with TypeScript 6, `phaser`
11
+ pinned `^4.2.0`). A good prod build does four things: split the large `phaser`
12
+ vendor chunk so app code can change without re-downloading the engine, minify
13
+ hard, content-hash everything for immutable caching, and register a PWA service
14
+ worker that precaches game assets. A bundle-size budget keeps the result honest.
15
+ The asset pipeline ([[phaser-asset-pipeline]]) runs first; this skill packages
16
+ its output.
17
+
18
+ ## Vite production config
19
+
20
+ ```ts
21
+ // vite.config.ts
22
+ import { defineConfig } from "vite";
23
+ import { VitePWA } from "vite-plugin-pwa";
24
+
25
+ export default defineConfig({
26
+ base: process.env.BASE_PATH ?? "/", // "/" for root domains, "/repo/" for project pages
27
+ build: {
28
+ target: "es2022",
29
+ assetsInlineLimit: 0, // never inline game assets — keep them cacheable files
30
+ minify: "terser",
31
+ terserOptions: { compress: { passes: 2, drop_console: true }, format: { comments: false } },
32
+ rollupOptions: {
33
+ output: {
34
+ manualChunks: { phaser: ["phaser"] }, // engine in its own long-lived chunk
35
+ entryFileNames: "assets/[name]-[hash].js", // content hash on everything
36
+ chunkFileNames: "assets/[name]-[hash].js",
37
+ assetFileNames: "assets/[name]-[hash][extname]",
38
+ },
39
+ },
40
+ },
41
+ });
42
+ ```
43
+
44
+ `manualChunks: { phaser: ["phaser"] }` is the single highest-value line: Phaser is
45
+ hundreds of KB and rarely changes, so isolating it means a gameplay tweak only
46
+ busts the small app chunk. Two-pass terser squeezes meaningfully more than one.
47
+
48
+ ## Asset hashing and immutable caching
49
+
50
+ Every emitted file carries a content hash, so it can be cached forever and a
51
+ change produces a new filename (cache-busted automatically):
52
+
53
+ ```
54
+ /assets/index-3f2a.js Cache-Control: public, max-age=31536000, immutable
55
+ /assets/phaser-9b1c.js Cache-Control: public, max-age=31536000, immutable
56
+ /index.html Cache-Control: no-cache (must revalidate to pick up new hashes)
57
+ ```
58
+
59
+ `index.html` is the only file served `no-cache` — it points at the hashed assets,
60
+ so revalidating it is enough to roll the whole app forward. Game assets under
61
+ `public/assets/**` ([[phaser-asset-pipeline]]) are hashed by the PWA precache
62
+ manifest below even though Vite copies them verbatim.
63
+
64
+ ## Base path per host
65
+
66
+ `base` must match where the game is served:
67
+
68
+ - Root domain / custom domain → `base: "/"`.
69
+ - GitHub Pages project site → `base: "/<repo>/"`.
70
+ - Subpath behind a reverse proxy → that subpath.
71
+
72
+ A wrong `base` is the classic "works on `bun run dev`, blank canvas on deploy"
73
+ bug — assets 404 because the URLs are absolute to the wrong root. Drive it from an
74
+ env var so the same build config serves every host.
75
+
76
+ ## PWA: vite-plugin-pwa (on by default)
77
+
78
+ The PWA is on by default: install-to-home-screen, offline play, and auto-update.
79
+ `registerType: "autoUpdate"` ships new versions without a manual prompt; the
80
+ critical part is extending `globPatterns` to precache **game assets** (atlases,
81
+ audio sprites, fonts) — the Workbox default only catches JS/CSS/HTML and would
82
+ leave a "installed but can't load its art offline" game.
83
+
84
+ ```ts
85
+ VitePWA({
86
+ registerType: "autoUpdate",
87
+ includeAssets: ["favicon.svg"],
88
+ workbox: {
89
+ globPatterns: ["**/*.{js,css,html,png,jpg,svg,webp,woff2,json,fnt,mp3,m4a,ogg}"],
90
+ // critically include packed game assets:
91
+ globDirectory: "dist",
92
+ additionalManifestEntries: [], // or widen globPatterns to assets/**/*.{png,json,fnt,ogg,m4a}
93
+ maximumFileSizeToCacheInBytes: 8 * 1024 * 1024, // raise above default 2MB for atlas pages
94
+ },
95
+ manifest: { name: "Game", short_name: "Game", display: "fullscreen", orientation: "landscape",
96
+ background_color: "#000000", theme_color: "#000000", icons: [/* 192/512 */] },
97
+ })
98
+ ```
99
+
100
+ Set `maximumFileSizeToCacheInBytes` above the default 2 MB or large atlas pages
101
+ silently fall out of the precache.
102
+
103
+ ## Bundle-size budget
104
+
105
+ A byte budget, checked after `bun run build`, fails the PR when a stray dependency
106
+ or an un-split engine chunk bloats the download. This is the same budget the
107
+ bundle-size gate in [[phaser-testing]] enforces in CI.
108
+
109
+ ```jsonc
110
+ // size-limit config (or a custom post-build check)
111
+ [
112
+ { "name": "app", "path": "dist/assets/index-*.js", "limit": "60 kB" },
113
+ { "name": "phaser", "path": "dist/assets/phaser-*.js", "limit": "400 kB" }
114
+ ]
115
+ ```
116
+
117
+ Separate budgets per chunk catch the right regressions: app growth from your code
118
+ vs. an accidental second copy of a big dependency landing in the phaser chunk.
119
+
120
+ ## Static deploy
121
+
122
+ Output is plain static files in `dist/` — host on any static CDN (GitHub Pages,
123
+ Netlify, Cloudflare Pages, S3+CloudFront). Requirements: serve over HTTPS (PWA +
124
+ Web Audio expect a secure context), set the cache headers above (immutable for
125
+ hashed assets, no-cache for `index.html`), and for client-routed builds add an
126
+ SPA fallback to `index.html`. Build with the host's base path:
127
+
128
+ ```bash
129
+ BASE_PATH=/my-game/ bun run build # then publish dist/
130
+ ```
131
+
132
+ ## Project conventions
133
+
134
+ - Run the asset pipeline before the build (`prebuild`) so `public/assets/**` and
135
+ `src/assets.ts` are current ([[phaser-asset-pipeline]]).
136
+ - `phaser` is always its own manualChunk; never let it merge into the app chunk.
137
+ - `base` comes from an env var, never hardcoded to one host.
138
+ - The bundle-size budget is committed and CI-gated ([[phaser-testing]]).
139
+ - Build pins: `phaser ^4.2.0`, Vite, TypeScript 6.
140
+
141
+ ## Verification
142
+
143
+ Verified by `bun run build` succeeding under the size budget, then `bun run
144
+ preview` (served at the deploy `base`) booting the game with no 404s and no
145
+ console errors. Confirm the PWA registers (Application → Service Workers), load
146
+ once online then go offline and reload to confirm assets are precached, and
147
+ confirm a redeploy auto-updates clients without a manual cache clear.
148
+ </content>
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: phaser-i18n
3
+ description: This skill should be used when localizing a Phaser 4 game — a typed string catalog so no user-facing text is hardcoded, runtime locale switching that re-renders open text, interpolation/pluralization, and the BitmapText vs Text trade-offs (glyph coverage, RTL, CJK) localization forces. Use it when adding any player-facing string, building a language selector, or fixing missing-glyph/hardcoded-text issues. Pairs with phaser-services, phaser-accessibility, and phaser-asset-pipeline.
4
+ ---
5
+
6
+ # Phaser 4 Internationalization
7
+
8
+ ## Overview
9
+
10
+ No user-facing string is hardcoded in a scene. All player-visible text comes from
11
+ a **typed string catalog** keyed by typed constants, so a missing or misspelled
12
+ key is a compile error and every string has a home for translation. The catalog
13
+ is a small typed wrapper (no heavy dependency required); locale switching
14
+ re-renders any open text. Announced strings ([[phaser-accessibility]]) and
15
+ service messages ([[phaser-services]]) draw from the same catalog.
16
+
17
+ ## The typed catalog
18
+
19
+ One module owns the locales and the lookup. The key type is derived from the
20
+ default locale so every locale must cover the same keys:
21
+
22
+ ```ts
23
+ // src/i18n/catalog.ts
24
+ const en = {
25
+ "menu.play": "Play",
26
+ "menu.settings": "Settings",
27
+ "hud.score": "Score: {score}",
28
+ "result.cleared": "Level {level} cleared!",
29
+ "lives": "{n} life|{n} lives", // singular|plural
30
+ } as const;
31
+
32
+ const es: Record<keyof typeof en, string> = {
33
+ "menu.play": "Jugar", "menu.settings": "Ajustes",
34
+ "hud.score": "Puntos: {score}", "result.cleared": "¡Nivel {level} superado!",
35
+ "lives": "{n} vida|{n} vidas",
36
+ };
37
+
38
+ export type StringKey = keyof typeof en;
39
+ const locales = { en, es } as const;
40
+ export type Locale = keyof typeof locales;
41
+ ```
42
+
43
+ ## The `t()` function: interpolation + pluralization
44
+
45
+ ```ts
46
+ // src/i18n/i18n.ts
47
+ let current: Locale = "en";
48
+ export function setLocale(l: Locale) { current = l; EventCenter.emit(GameEvent.LocaleChanged); }
49
+ export function getLocale() { return current; }
50
+
51
+ export function t(key: StringKey, params?: Record<string, string | number>): string {
52
+ let s = (locales[current][key] ?? locales.en[key]) as string; // fall back to en, never crash
53
+ if (s.includes("|") && params && "n" in params) // pick plural form
54
+ s = (Number(params.n) === 1 ? s.split("|")[0] : s.split("|")[1]);
55
+ return s.replace(/\{(\w+)\}/g, (_, k) => String(params?.[k] ?? `{${k}}`));
56
+ }
57
+ ```
58
+
59
+ Usage is always `t(...)` with a typed key — never a raw string in a scene:
60
+
61
+ ```ts
62
+ this.add.bitmapText(x, y, Font.UI, t("hud.score", { score: 0 }));
63
+ this.announce(t("result.cleared", { level })); // [[phaser-accessibility]] live region
64
+ ```
65
+
66
+ The plural/interpolation rules are pure functions — put them in `src/logic/**`
67
+ so Vitest covers them ([[phaser-testing]]).
68
+
69
+ ## Locale switching at runtime
70
+
71
+ Changing language must update text that is already on screen. Emit a
72
+ `LocaleChanged` event on the EventsCenter ([[phaser-services]]); each scene with
73
+ visible text subscribes and re-applies `t()` to its labels, then removes the
74
+ listener in `shutdown` (the on/off discipline).
75
+
76
+ ```ts
77
+ create() {
78
+ const refresh = () => this.scoreText.setText(t("hud.score", { score: this.score }));
79
+ EventCenter.on(GameEvent.LocaleChanged, refresh);
80
+ this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => EventCenter.off(GameEvent.LocaleChanged, refresh));
81
+ }
82
+ ```
83
+
84
+ Persist the chosen locale via SaveService and apply it on boot; default from
85
+ `navigator.language` when there is no saved choice.
86
+
87
+ ## BitmapText vs Text — the localization trade-off
88
+
89
+ The performance advice "use BitmapText for hot text" (the official `game-object-components` skill)
90
+ collides with i18n: a bitmap font only contains the glyphs it was generated with.
91
+
92
+ - **BitmapText** — fastest, but the BMFont must include every glyph the locale
93
+ needs. Fine for digits/Latin HUD; generate per-script font pages
94
+ ([[phaser-asset-pipeline]]) if you ship CJK/Cyrillic/etc. via BitmapText.
95
+ - **Text** (canvas) — renders any glyph the loaded web font supports, handles
96
+ diacritics and combining marks, and is the safe choice for arbitrary
97
+ translated body text and user-generated content. Cost is rasterization per
98
+ change, so it is for static/low-churn strings.
99
+
100
+ Rule of thumb: BitmapText for high-churn numeric/short HUD with a covered glyph
101
+ set; Text for translated prose and any locale whose script the bitmap font does
102
+ not include. For RTL locales (Arabic/Hebrew), use canvas `Text` with
103
+ `rtl: true`/right alignment and lay out mirrored — BitmapText does not shape RTL.
104
+
105
+ ## Project conventions
106
+
107
+ - Every player-facing string is a `t(StringKey, …)` call — no inline literals in
108
+ scenes/entities (this mirrors the no-raw-string-keys discipline).
109
+ - The default locale defines the key type; other locales must satisfy it
110
+ (compile error on a missing key).
111
+ - Interpolation/plural logic lives in `src/logic/**`; the catalog and `t()` live
112
+ in `src/i18n/**`.
113
+ - Locale persists via SaveService and is announced through the live region for
114
+ screen-reader users ([[phaser-accessibility]]).
115
+
116
+ ## Verification
117
+
118
+ Verified by switching locale at runtime and confirming on-screen text updates
119
+ live (no reload), a missing key fails `bun run typecheck`, and pluralization unit
120
+ tests pass for n=0/1/many. For non-Latin locales, confirm glyphs render (no tofu
121
+ boxes) — that is the signal you need a per-script BMFont page or canvas Text.
122
+ </content>
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: phaser-project-structure
3
- description: This skill should be used when creating, restructuring, or reasoning about a Phaser 4 game project — the game config, the Vite + TypeScript layout, the Boot → Preloader → MainMenu → Game scene flow, scale/resolution setup for desktop and mobile, and where code and assets belong. Use it before scaffolding a project, adding a major subsystem, or deciding where a file should live. Pairs with phaser-scenes, phaser-assets, phaser-rendering, and phaser-testing.
3
+ description: This skill should be used when creating, restructuring, or reasoning about a Phaser 4 game project — the game config, the Vite + TypeScript layout, the Boot → Preloader → MainMenu → Game scene flow, scale/resolution setup for desktop and mobile, and where code and assets belong. Use it before scaffolding a project, adding a major subsystem, or deciding where a file should live. Pairs with the official scenes skill, the official loading-assets skill, the official filters-and-postfx skill, and phaser-testing.
4
4
  ---
5
5
 
6
6
  # Phaser 4 Project Structure
7
7
 
8
8
  ## Overview
9
9
 
10
- This stack targets **Phaser 4** (v4.1+ "Salusa", npm package `phaser`), built
10
+ This stack targets **Phaser 4** (v4.2+, npm package `phaser@^4.2.0`), built
11
11
  with **Vite + TypeScript** — the layout the official `phaserjs/template-vite-ts`
12
12
  template and `npm create @phaserjs/game@latest` scaffold. Phaser 4 ships its own
13
13
  type definitions (`types/phaser.d.ts`); do not add `@types/phaser`.
@@ -55,17 +55,17 @@ v4-specific config facts:
55
55
  - Pixel-art games: `pixelArt: true` (nearest-neighbor + roundPixels), or the new
56
56
  **`render.smoothPixelArt: true`** (WebGL-only) for pixel art that rotates or
57
57
  scales smoothly. Pick one per project and record the choice.
58
- - Custom render nodes register under `render.renderNodes` — see [[phaser-rendering]].
58
+ - Custom render nodes register under `render.renderNodes` — see the official `filters-and-postfx` skill.
59
59
  - `Phaser.HEADLESS` exists for logic-only boots (tests) — see [[phaser-testing]].
60
60
 
61
61
  ## Scene flow
62
62
 
63
- Four-stage boot, in order (see [[phaser-scenes]] for lifecycle detail):
63
+ Four-stage boot, in order (see the official `scenes` skill for lifecycle detail):
64
64
 
65
65
  1. **Boot** — loads only the handful of assets the Preloader's loading screen
66
66
  needs (logo, progress-bar art). No game assets here.
67
67
  2. **Preloader** — renders the loading UI and loads everything else, preferably
68
- via a single asset-pack manifest (see [[phaser-assets]]), then starts MainMenu.
68
+ via a single asset-pack manifest (see the official `loading-assets` skill), then starts MainMenu.
69
69
  3. **MainMenu** — entry UI; starts Game.
70
70
  4. **Game** (+ overlay scenes like `HUD`, `Pause` run in parallel) — gameplay.
71
71