@pix3/cli 1.6.4 → 2.0.0-alpha.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 (168) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +722 -788
  3. package/dist/index.js +557 -421
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
  6. package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
  7. package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
  8. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  9. package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
  10. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  11. package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
  12. package/dist/validate/prebuilt/bundle-main.js +223 -18
  13. package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
  14. package/dist/validate/prebuilt/runtime.js +1 -1
  15. package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
  16. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
  17. package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
  18. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
  19. package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
  20. package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
  21. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
  22. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
  23. package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
  24. package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
  25. package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
  26. package/kit/files/AGENTS.md +76 -84
  27. package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
  28. package/kit/kit.json +8 -5
  29. package/package.json +62 -54
  30. package/templates/2d/files/scenes/main.pix3scene +31 -0
  31. package/templates/2d/template.yaml +11 -0
  32. package/templates/3d/files/scenes/main.pix3scene +50 -0
  33. package/templates/3d/template.yaml +11 -0
  34. package/templates/base/files/README.md +39 -0
  35. package/templates/base/files/gitignore +11 -0
  36. package/templates/base/files/index.html +23 -0
  37. package/templates/base/files/package.json +28 -0
  38. package/templates/base/files/src/main.ts +3 -0
  39. package/templates/base/files/tsconfig.json +17 -0
  40. package/templates/base/files/vite.config.ts +10 -0
  41. package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
  42. package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
  43. package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
  44. package/templates/empty-2d/files/README.md +0 -20
  45. package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
  46. package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
  47. package/templates/empty-2d/template.yaml +0 -7
  48. package/templates/empty-3d/files/README.md +0 -20
  49. package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
  50. package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
  51. package/templates/empty-3d/template.yaml +0 -7
  52. package/templates/idea-blank/files/README.md +0 -22
  53. package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
  54. package/templates/idea-blank/template.yaml +0 -10
  55. package/templates/minigame-2d/files/README.md +0 -44
  56. package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
  57. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
  58. package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
  59. package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
  60. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
  61. package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
  62. package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
  63. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
  64. package/templates/minigame-2d/template.yaml +0 -10
  65. package/templates/playable-2d/files/README.md +0 -30
  66. package/templates/playable-2d/files/design/recipe.md +0 -83
  67. package/templates/playable-2d/files/design/tests/reachability.json +0 -4
  68. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
  69. package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
  70. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
  71. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
  72. package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
  73. package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
  74. package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
  75. package/templates/playable-2d/template.yaml +0 -9
  76. package/templates/playable-3d/files/README.md +0 -32
  77. package/templates/playable-3d/files/design/recipe.md +0 -127
  78. package/templates/playable-3d/files/design/tests/reachability.json +0 -4
  79. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
  80. package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
  81. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
  82. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
  83. package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
  84. package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
  85. package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
  86. package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
  87. package/templates/playable-3d/template.yaml +0 -9
  88. package/templates/recipe-arena-2d/files/README.md +0 -48
  89. package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
  90. package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
  91. package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
  92. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
  93. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
  94. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  95. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
  96. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
  97. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
  98. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
  99. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
  100. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
  101. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
  102. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
  103. package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
  104. package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
  105. package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
  106. package/templates/recipe-arena-2d/template.yaml +0 -10
  107. package/templates/recipe-blank-2d/files/README.md +0 -61
  108. package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
  109. package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
  110. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
  111. package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
  112. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
  113. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
  114. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
  115. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
  116. package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
  117. package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
  118. package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
  119. package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
  120. package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
  121. package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
  122. package/templates/recipe-blank-2d/template.yaml +0 -10
  123. package/templates/recipe-bouncer-2d/files/README.md +0 -81
  124. package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
  125. package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
  126. package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
  127. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
  128. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
  129. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
  130. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
  131. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
  132. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
  133. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
  134. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
  135. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
  136. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
  137. package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
  138. package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
  139. package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
  140. package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
  141. package/templates/recipe-bouncer-2d/template.yaml +0 -10
  142. package/templates/recipe-grid-3d/files/README.md +0 -22
  143. package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
  144. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
  145. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
  146. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
  147. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
  148. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
  149. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
  150. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
  151. package/templates/recipe-grid-3d/template.yaml +0 -10
  152. package/templates/recipe-tapper-2d/files/README.md +0 -39
  153. package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
  154. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
  155. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
  156. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
  157. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
  158. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
  159. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
  160. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
  161. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
  162. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
  163. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
  164. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
  165. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
  166. package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
  167. package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
  168. package/templates/recipe-tapper-2d/template.yaml +0 -10
@@ -1,174 +0,0 @@
1
- /**
2
- * TouchRules — turns contact into meaning.
3
- *
4
- * Every frame it overlap-tests a circle around the player against the
5
- * `core:Hitbox2D` groups listed in `rules`, and for each hit emits a SEMANTIC
6
- * signal on its own node (`game-root`):
7
- *
8
- * `touch-scored` (amount, x, y)
9
- * `touch-damaged` (amount, x, y)
10
- *
11
- * It never touches the score, the lives or the HUD — `GameRules` owns those and
12
- * listens for these two signals. Add an outcome by adding a rule here and a
13
- * listener there; nothing else has to change.
14
- *
15
- * `rules` format: `group:outcome:amount`, entries separated by `;`.
16
- * Outcomes: `score` | `damage`. Example: `pickup:score:1;hazard:damage:1`.
17
- */
18
- import { Script, type PropertySchema } from '@pix3/runtime';
19
-
20
- type TouchOutcome = 'score' | 'damage';
21
-
22
- interface TouchRule {
23
- group: string;
24
- outcome: TouchOutcome;
25
- amount: number;
26
- }
27
-
28
- function parseRules(text: string): TouchRule[] {
29
- const rules: TouchRule[] = [];
30
- for (const entry of text.split(';')) {
31
- const parts = entry.trim().split(':');
32
- if (parts.length < 2 || !parts[0].trim()) {
33
- continue;
34
- }
35
- const outcome: TouchOutcome = parts[1].trim() === 'damage' ? 'damage' : 'score';
36
- const amount = Number(parts[2] ?? 1);
37
- rules.push({
38
- group: parts[0].trim(),
39
- outcome,
40
- amount: Number.isFinite(amount) && amount > 0 ? amount : 1,
41
- });
42
- }
43
- return rules;
44
- }
45
-
46
- export class TouchRules extends Script {
47
- constructor(id: string, type: string) {
48
- super(id, type);
49
- this.config = {
50
- // Node id/name whose world position is the centre of the overlap test.
51
- playerNode: 'player',
52
- // Radius of the overlap test, in logical pixels.
53
- touchRadius: 62,
54
- // group:outcome:amount pairs, separated by ';'.
55
- rules: 'pickup:score:1;hazard:damage:1',
56
- // Free the touched node after resolving it.
57
- consume: true,
58
- // res:// audio paths; empty = silent.
59
- scoreSound: '',
60
- damageSound: '',
61
- };
62
- }
63
-
64
- static getPropertySchema(): PropertySchema {
65
- return {
66
- nodeType: 'TouchRules',
67
- properties: [
68
- {
69
- name: 'playerNode',
70
- type: 'string',
71
- ui: { label: 'Player Node', group: 'Contact' },
72
- getValue: s => (s as TouchRules).config.playerNode,
73
- setValue: (s, v) => {
74
- (s as TouchRules).config.playerNode = typeof v === 'string' ? v : '';
75
- },
76
- },
77
- {
78
- name: 'touchRadius',
79
- type: 'number',
80
- ui: { label: 'Touch Radius', group: 'Contact', min: 4, max: 600, step: 1, slider: true },
81
- getValue: s => (s as TouchRules).config.touchRadius,
82
- setValue: (s, v) => {
83
- const n = Number(v);
84
- (s as TouchRules).config.touchRadius = Math.min(600, Math.max(4, Number.isFinite(n) ? n : 4));
85
- },
86
- },
87
- {
88
- name: 'rules',
89
- type: 'string',
90
- ui: {
91
- label: 'Rules',
92
- description: "group:outcome:amount, ';'-separated. Outcome is score or damage.",
93
- group: 'Contact',
94
- },
95
- getValue: s => (s as TouchRules).config.rules,
96
- setValue: (s, v) => {
97
- (s as TouchRules).config.rules = typeof v === 'string' ? v : '';
98
- },
99
- },
100
- {
101
- name: 'consume',
102
- type: 'boolean',
103
- ui: { label: 'Consume On Touch', group: 'Contact' },
104
- getValue: s => (s as TouchRules).config.consume,
105
- setValue: (s, v) => {
106
- (s as TouchRules).config.consume = Boolean(v);
107
- },
108
- },
109
- {
110
- name: 'scoreSound',
111
- type: 'string',
112
- ui: { label: 'Score Sound', group: 'Feedback', editor: 'audio-resource' },
113
- getValue: s => (s as TouchRules).config.scoreSound,
114
- setValue: (s, v) => {
115
- (s as TouchRules).config.scoreSound = typeof v === 'string' ? v : '';
116
- },
117
- },
118
- {
119
- name: 'damageSound',
120
- type: 'string',
121
- ui: { label: 'Damage Sound', group: 'Feedback', editor: 'audio-resource' },
122
- getValue: s => (s as TouchRules).config.damageSound,
123
- setValue: (s, v) => {
124
- (s as TouchRules).config.damageSound = typeof v === 'string' ? v : '';
125
- },
126
- },
127
- ],
128
- groups: {
129
- Contact: { label: 'Contact', expanded: true },
130
- Feedback: { label: 'Feedback', expanded: false },
131
- },
132
- };
133
- }
134
-
135
- onUpdate(): void {
136
- const scene = this.scene;
137
- const owner = this.node;
138
- const player = this.findNode(String(this.config.playerNode ?? ''));
139
- if (!scene || !owner || !player) {
140
- return;
141
- }
142
-
143
- player.updateWorldMatrix(true, false);
144
- const px = player.matrixWorld.elements[12];
145
- const py = player.matrixWorld.elements[13];
146
- const radius = Math.max(1, Number(this.config.touchRadius) || 1);
147
- const consume = Boolean(this.config.consume);
148
-
149
- for (const rule of parseRules(String(this.config.rules ?? ''))) {
150
- for (const hit of scene.collision2d.overlapCircle(px, py, radius, rule.group)) {
151
- if (rule.outcome === 'damage') {
152
- owner.emit('touch-damaged', rule.amount, hit.x, hit.y);
153
- scene.juice.shake(player, { amplitude: 16, duration: 0.25 });
154
- scene.juice.flash({ color: '#ff4d6d', intensity: 0.4, durationSec: 0.22 });
155
- this.playSound(String(this.config.damageSound ?? ''));
156
- } else {
157
- owner.emit('touch-scored', rule.amount, hit.x, hit.y);
158
- scene.juice.punchScale(player, { amount: 0.22, duration: 0.22 });
159
- this.playSound(String(this.config.scoreSound ?? ''));
160
- }
161
- if (consume) {
162
- hit.node.queueFree();
163
- }
164
- }
165
- }
166
- }
167
-
168
- private playSound(path: string): void {
169
- if (!path) {
170
- return;
171
- }
172
- void this.scene?.audio.play(path, { bus: 'sfx', pitchVariation: 0.08 });
173
- }
174
- }
@@ -1,10 +0,0 @@
1
- id: recipe-arena-2d
2
- title: Arena 2D
3
- description: An avatar you steer inside a bounded field while spawners drip pickups and hazards; touch turns into score or damage, with lives, a timer and a win/lose flow. Dodgers, collectors, top-down survival, and (by swapping the movement script) runners. Its steering is continuous, so a grid or turn-based game (snake, sokoban) starts from Blank 2D instead.
4
- projectType: 2d
5
- targetPlatform: universal
6
- viewport: { width: 1080, height: 1920 }
7
- # The build/full-run entry scene (Project Settings → Default Export Scene Path).
8
- # The menu boots first; PLAY transitions to main.pix3scene (the game).
9
- entryScene: scenes/menu.pix3scene
10
- order: 60
@@ -1,61 +0,0 @@
1
- # Blank 2D
2
-
3
- Everything a run needs **except the mechanic**: a bounded field, a HUD, score /
4
- lives / timer bookkeeping, and a win/lose screen with RETRY. Press play in
5
- `scenes/main.pix3scene` and you get an empty field with `SCORE 0` — that is
6
- correct, and it is the point.
7
-
8
- Use this when the game's core loop is not what another recipe ships (grid or
9
- turn-based movement, word / card / board games, builders, idle). The first thing
10
- to build is the mechanic itself, controls included; building it here beats
11
- deleting somebody else's mechanic first. A falling, bouncing ball is the bouncer
12
- recipe's job — do not rebuild that here.
13
-
14
- The field is not grey: `post-fx` blooms anything bright and `bg-glow` lights the
15
- background, and `sprites/ph-*.png` is a shape library (disc, orb, ring, star,
16
- capsule) tinted with the palette at T0. Build the mechanic out of those shapes,
17
- not out of `ColorRect2D` squares — see `design/recipe.md` → Placeholders.
18
-
19
- **`design/recipe.md` is the contract.** It lists the stable node ids, the
20
- tunables (with their ranges), the extension points and what must not be renamed.
21
- Read it first; tools and agents grep it.
22
-
23
- ## Layout
24
-
25
- ```
26
- scenes/main.pix3scene the game — open and build here. There is no menu on purpose.
27
- scenes/ui/result.pix3scene win/lose overlay, instanced into main hidden (editor-only)
28
- scripts/GameRules.ts score / lives / timer / win / lose / end flow / restart
29
- scripts/ScoreHud.ts signals → HUD widgets (display only)
30
- sprites/ph-*.png shape library (near-white, tinted via core:tint)
31
- design/recipe.md the contract
32
- design/tests/ reachability ledger + one example routine (a format seed)
33
- ```
34
-
35
- ## How your mechanic plugs in
36
-
37
- Write a script, attach it to a node inside `board`, and score through
38
- `GameRules` — either by emitting on `game-root`:
39
-
40
- ```ts
41
- this.findNode('game-root')?.emit('score-added', 1);
42
- this.findNode('game-root')?.emit('life-lost', 1);
43
- ```
44
-
45
- …or by calling the component directly (`addScore`, `loseLife`, `finish`). The
46
- HUD, the end screen and RETRY then work with no further wiring.
47
-
48
- ## The rule that keeps it workable
49
-
50
- One file, one responsibility. Keep each script in the 70–140 line range: small
51
- files can be edited surgically; monoliths get rewritten wholesale and lose the
52
- comments that explained them. When a mechanic outgrows its file, split it rather
53
- than growing it.
54
-
55
- ## No menu yet
56
-
57
- A menu is a screen between you and the thing you are working on. Add one only
58
- once the game is fun: create `scenes/menu.pix3scene`, have its PLAY button
59
- `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`, then
60
- point Project Settings → Default Export Scene Path at it. `Play Scene` keeps
61
- running whatever scene is open, so the menu never gets in the way again.
@@ -1,148 +0,0 @@
1
- # Recipe: recipe-blank-2d
2
-
3
- ## What this is
4
-
5
- **Nothing plays yet, on purpose.** A bounded field, a HUD, and score / lives /
6
- timer / win-lose bookkeeping that already works — and no mechanic of any kind,
7
- including controls.
8
-
9
- Pick this recipe when the idea's core loop is not what another recipe ships:
10
- grid or turn-based movement (snake, sokoban, match-3), word / card / board games,
11
- builders, idle games without a ball. **Your first increment is the core
12
- mechanic itself, controls included** — that is the trade being made here, and it
13
- beats spending the first increment deleting somebody else's mechanic.
14
-
15
- Everything *around* the mechanic is done: you never hand-roll a score counter, a
16
- lives bar, a timer, an end screen or a retry. The look is done too: `post-fx`
17
- blooms anything bright and `bg-glow` lights the field, so a sprite tinted with a
18
- palette accent already glows on the first frame.
19
-
20
- ## Node map
21
-
22
- | id | what |
23
- | --- | --- |
24
- | `post-fx` | `PostProcess` (`affect2D`): bloom + vignette — tune, never delete |
25
- | `game-root` | root; hosts `GameRules`; every HUD signal is emitted here |
26
- | `game-background` | full-screen `ColorRect2D` (palette background) |
27
- | `bg-glow` | full-screen `Sprite2D`, the tinted radial gradient over it |
28
- | `board` | the play field (`Group2D`) — **build your mechanic inside this** |
29
- | `board-floor` | the field's visible plate; its rect is the field's extent |
30
- | `hud` | `CanvasLayer2D` overlay, hosts `ScoreHud` |
31
- | `score-label` / `time-label` / `lives-bar` | HUD widgets, signal-driven only |
32
- | `result-overlay` / `result-dim` / `result-label` / `retry-button` | end screen, own file `scenes/ui/result.pix3scene`; instanced into main with `visible: false` (editor-only hide — `GameRules` shows it) |
33
-
34
- Signals on `game-root`: your mechanic emits `score-added` (amount) and
35
- `life-lost` (amount); `GameRules` answers `score-changed`, `lives-changed`,
36
- `time-changed`, and `game-won` / `game-lost`.
37
-
38
- There is **one gameplay scene** (`scenes/main.pix3scene`) and no menu — see
39
- Extension points if the game eventually needs one.
40
-
41
- ## Placeholders
42
-
43
- A **shape library**, not entities: near-white PNGs already tinted with the
44
- palette at T0 (the role column decides the colour). Put them on a `Sprite2D`
45
- sized to the thing they stand for, and recolour with `effects: [{type: core:tint,
46
- params: {color}}]` when you need another accent. **A round thing is
47
- `ph-circle`/`ph-orb`, a coin or bumper is `ph-ring`, a bar or paddle is
48
- `ph-capsule`, a spark or star is `ph-star`.** A `ColorRect2D` is for panels,
49
- floors and bars — never for a ball, a coin, an enemy or anything the player
50
- looks at: a square ball on the first frame is what the user remembers. Real art
51
- replaces these through `generate_asset` in the art pass.
52
-
53
- | role | file | node/prefab |
54
- | --- | --- | --- |
55
- | background | `sprites/ph-bg.png` | `bg-glow` (radial gradient) |
56
- | player | `sprites/ph-circle.png` | (library — a solid disc with a soft edge) |
57
- | avatar | `sprites/ph-orb.png` | (library — a disc with a bright rim) |
58
- | collectible | `sprites/ph-ring.png` | (library — a ring; bumpers, coins, targets) |
59
- | hazard | `sprites/ph-star.png` | (library — a four-point spark) |
60
- | ui | `sprites/ph-capsule.png` | (library — a rounded bar; paddles, platforms, pills) |
61
-
62
- ## Tunables
63
-
64
- ```yaml
65
- tunables:
66
- winMode: { node: game-root, component: "user:GameRules", property: winMode, default: score }
67
- targetScore: { node: game-root, component: "user:GameRules", property: targetScore, min: 1, max: 99999, default: 10 }
68
- timeLimitSec: { node: game-root, component: "user:GameRules", property: timeLimitSec, min: 0, max: 600, default: 0 }
69
- startingLives: { node: game-root, component: "user:GameRules", property: startingLives, min: 1, max: 20, default: 3 }
70
- bgColor: { node: game-background, property: color, default: "#12141c" }
71
- boardColor: { node: board-floor, property: color, default: "#1d212e" }
72
- bloomIntensity: { node: post-fx, property: bloomIntensity, min: 0, max: 3, default: 0.8 }
73
- ```
74
-
75
- `component` present → `set_component_property`; absent → `set_property`. The
76
- schema clamps out-of-range values rather than rejecting them.
77
-
78
- `timeLimitSec: 0` means **no clock**: the run ends on the target score or on
79
- running out of lives, and `time-label` counts elapsed seconds up. Set a limit and
80
- it counts down and becomes a deadline (see `winMode`).
81
-
82
- ## Extension points
83
-
84
- - **The mechanic.** Write a script under `scripts/`, attach it to a node inside
85
- `board`, and let it own input and movement. It scores through `GameRules` —
86
- either `node.emit('score-added', 1)` on `game-root`, or a direct
87
- `addScore(1)` / `loseLife()` / `finish(true)` on the component. Nothing else in
88
- the project needs to change for the HUD and the end screen to work.
89
- - **Collision, when the mechanic needs it.** Three tiers, cheapest first. Overlap
90
- queries with no response: `scene.collision2d` + `core:Hitbox2D` (axis-aligned).
91
- Bouncing, pushing, gravity, sensors: the engine's own 2D rigid-body solver —
92
- `core:PhysicsBody2D` + `core:Collider2D` on the same node (a `core:Collider2D`
93
- alone is static world geometry), `scene.physics2d.getBody(node)` for
94
- `applyImpulse` / `setVelocity` / `teleport`, `setGravity(0, 0)` for top-down.
95
- **Never hand-write a 2D solver, and do not import rapier** (3D only, ~2 MB wasm).
96
- Grid games usually need no collision at all — compare cell coordinates.
97
- - **Spawning.** `scene.instantiate` a prefab into a container node, `queueFree`
98
- it when it leaves the field, and add that container to `GameRules.freezeNodes`
99
- so it — and every instance under it — stops on game over. Give your spawner a `clear()` method and `resetRun()`
100
- will empty the field for you.
101
- - **Juice is one-liners — add it WITH the mechanic, never as a later pass.**
102
- `scene.juice.burst({x, y})`, `floatText('+5', {at: node})`, `punchScale` /
103
- `shake` / `flash`, `scene.audio.sfx('score')`, `scene.time.hitstop(50)` on a
104
- contact START; `scene.juice.trail(ball)` for a light streak; `scene.tween.to(node,
105
- {scale: 1.2}, {durationSec: 0.15, yoyo: true, repeat: 1})` / `crossFade(a, b)` for any
106
- motion or panel switch instead of hand-lerping. Bright sprites bloom through `post-fx`
107
- by themselves.
108
- - **A menu, later.** Create `scenes/menu.pix3scene` with a root carrying a script
109
- that calls `scene.changeScene('res://scenes/main.pix3scene', {transition: 'fade'})`
110
- on PLAY, then set Project Settings → Default Export Scene Path to it. Do this
111
- **after** the game is fun, never before: while iterating, the menu is a screen
112
- between you and the thing you are working on.
113
-
114
- ## Ending a run belongs to GameRules
115
-
116
- The result overlay and RETRY are **owned by `GameRules`**: it hides the overlay on
117
- start and keeps `retry-button` **disabled**, then `finish(won)` shows the overlay,
118
- writes the result text, enables RETRY and freezes the `freezeNodes` nodes. For a
119
- custom win/lose condition call `finish(true|false)` — do NOT show the overlay
120
- yourself. A hand-rolled ending leaves RETRY on screen with its handler bound and
121
- `enabled: false`, i.e. a button that can never be pressed (`game_observe` reports
122
- `control: { enabled: false }`; that is the tell).
123
-
124
- ## Do not touch
125
-
126
- - The node ids above (rename `name`, never `id`), and the signal names.
127
- - `GameRules`' ownership of `result-overlay` and `retry-button`.
128
- - `post-fx`: keep the id and `affect2D: true` — without it a 2D scene gets no
129
- bloom. Tune the values, don't delete the node.
130
- - Do not import rapier, and do not add a menu scene early.
131
-
132
- ## Playtest contract
133
-
134
- terminalVisual: static
135
- terminalRestartRoutine: terminal-retry
136
-
137
- ## Verify
138
-
139
- 1. `play_start` on `scenes/main.pix3scene` — `game_observe` reports the snapshot
140
- with `phase: playing`, `score: 0`, `lives: 3`. That is the baseline, before any
141
- mechanic exists.
142
- 2. After the first mechanic increment, prove it by **state delta**, not by a
143
- screenshot: `game_input` the real input with `observe:` on the node that should
144
- react, and read `moved` / `activity` / the `game.changed` diff.
145
- 3. Scoring is proven the same way: one real input, then `score` up in the snapshot
146
- (and `score-label` reading `SCORE 1`).
147
- 4. `targetScore: 1`, replay, score once → `result-overlay` visible with `YOU WIN!`;
148
- `retry-button` (or the `restart` intent) starts a fresh run.
@@ -1,4 +0,0 @@
1
- {
2
- "version": 1,
3
- "proven": []
4
- }
@@ -1,27 +0,0 @@
1
- {
2
- "name": "restart",
3
- "description": "Dispatch the run's `restart` intent and confirm a fresh run came back up.",
4
- "note": "This is a FORMAT SEED, not a real test of your game — there is no mechanic here yet to test. Keep it (restart stays worth proving after a scene reload) and add a routine per mechanic next to it: run one with game_run {routine:'<name>'}. Steps are the game_input vocabulary plus {type:'command'} for a registered intent; `expect` holds the ordinary game_run predicates, ANDed and judged once after the last step against the frame pair around the run. Only a STEP takes a `why`. Prefer a `gameState` predicate over a node one wherever GameRules already publishes the value — it survives every relayout of the HUD.",
5
- "scope": "scenes/main.pix3scene",
6
- "params": {},
7
- "uses": ["game-root", "result-overlay", "retry-button"],
8
- "steps": [
9
- {
10
- "type": "command",
11
- "name": "restart",
12
- "why": "The intent RETRY dispatches, taken at the highest channel: it proves the wire without depending on where the button sits, and the dispatch is journalled with the frame it happened on."
13
- }
14
- ],
15
- "expect": [
16
- { "kind": "command", "name": "restart" },
17
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" },
18
- { "kind": "gameState", "path": "score", "op": "eq", "value": 0 },
19
- {
20
- "kind": "nodeProperty",
21
- "name": "result-overlay",
22
- "path": "visible",
23
- "op": "eq",
24
- "value": false
25
- }
26
- ]
27
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "terminal-retry",
3
- "description": "From the result screen, physically tap RETRY and check that play resumes.",
4
- "scope": "scenes/main.pix3scene",
5
- "params": {},
6
- "uses": ["result-overlay", "retry-button"],
7
- "steps": [
8
- {
9
- "type": "tap",
10
- "target": "retry-button",
11
- "holdMs": 200,
12
- "why": "A real pointer press proves the button is visible, enabled and reachable."
13
- },
14
- {
15
- "type": "wait",
16
- "frames": 30,
17
- "why": "Let the scene transition finish before reading the new game's phase."
18
- }
19
- ],
20
- "expect": [
21
- { "kind": "gameState", "path": "phase", "op": "eq", "value": "playing" }
22
- ]
23
- }
@@ -1,224 +0,0 @@
1
- # Pix3 Scene File (YAML)
2
- # Blank 2D — the GAME scene, and the one to build in. There is no menu scene on
3
- # purpose: nothing should stand between opening the project and the gameplay
4
- # while the mechanic is still being found.
5
- #
6
- # What is already wired: score / lives / timer bookkeeping, the HUD that shows
7
- # them, and a win/lose overlay with RETRY. What is NOT here: any mechanic at all,
8
- # including controls. That is your first increment — build it inside `board`.
9
- #
10
- # Stable node ids (design/recipe.md is the contract — do not rename these):
11
- # post-fx, game-root, game-background, bg-glow, board, board-floor,
12
- # hud, score-label, time-label, lives-bar,
13
- # result-overlay, result-dim, result-label, retry-button (scenes/ui/result.pix3scene)
14
- #
15
- # The look is two cheap ingredients, the same ones the bouncer recipe uses:
16
- # 1. `post-fx` (PostProcess, affect2D) — bloom picks up whatever is bright, so
17
- # a bright sprite glows for free. `bloomIntensity` is a recipe tunable the
18
- # Flow theme pack turns up for neon and down for pastel.
19
- # 2. `bg-glow` — a near-white radial-gradient placeholder (`sprites/ph-bg.png`)
20
- # tinted with the palette background, over the darkest ColorRect. It is what
21
- # keeps an empty field from reading as a grey slab.
22
- #
23
- # The win/lose overlay lives in its own file under scenes/ui/ and is instanced
24
- # here hidden (`visible: false` = editor only), so opening this scene shows the
25
- # field rather than a GAME OVER card.
26
- #
27
- # 2D coordinates: origin at the canvas centre, X right, Y UP. Node order in the
28
- # tree is paint order (later/deeper draws on top).
29
- version: 1.0.0
30
- metadata:
31
- author: Pix3 Recipe
32
- description: Blank gameplay field — bookkeeping, HUD and win/lose only; the mechanic is yours
33
- root:
34
- - id: post-fx
35
- type: PostProcess
36
- name: Post FX
37
- properties:
38
- affect2D: true
39
- bloomEnabled: true
40
- bloomIntensity: 0.8
41
- bloomThreshold: 0.6
42
- bloomSmoothing: 0.2
43
- bloomRadius: 0.75
44
- vignetteEnabled: true
45
- vignetteOffset: 0.3
46
- vignetteDarkness: 0.45
47
- chromaticAberrationEnabled: false
48
- children: []
49
- - id: game-root
50
- type: Group2D
51
- name: Game Root
52
- properties:
53
- width: 1080
54
- height: 1920
55
- layout:
56
- enabled: true
57
- horizontalAlign: stretch
58
- verticalAlign: stretch
59
- components:
60
- - id: game-rules
61
- type: user:GameRules
62
- enabled: true
63
- config:
64
- winMode: score
65
- targetScore: 10
66
- timeLimitSec: 0
67
- startingLives: 3
68
- resultNode: result-overlay
69
- resultLabel: result-label
70
- winText: "YOU WIN!"
71
- loseText: "GAME OVER"
72
- retryButton: retry-button
73
- gameScene: res://scenes/main.pix3scene
74
- # Add your mechanic's nodes here (comma-separated) so they stop on game over.
75
- freezeNodes: ""
76
- children:
77
- - id: game-background
78
- type: ColorRect2D
79
- name: Game Background
80
- properties:
81
- width: 1080
82
- height: 1920
83
- color: "#12141c"
84
- transform:
85
- position: [0, 0]
86
- scale: [1, 1]
87
- rotation: 0
88
- layout:
89
- enabled: true
90
- horizontalAlign: stretch
91
- verticalAlign: stretch
92
- children: []
93
- # Radial glow over the flat background. Near-white PNG + core:tint, so the
94
- # T0 palette recolours it (role `background` in design/recipe.md).
95
- - id: bg-glow
96
- type: Sprite2D
97
- name: Background Glow
98
- properties:
99
- texture: { type: 'texture', url: 'res://sprites/ph-bg.png' }
100
- width: 1080
101
- height: 1920
102
- opacity: 0.85
103
- effects:
104
- - type: core:tint
105
- params: { color: "#3a2f7a", amount: 1 }
106
- transform:
107
- position: [0, 0]
108
- scale: [1, 1]
109
- rotation: 0
110
- layout:
111
- enabled: true
112
- horizontalAlign: stretch
113
- verticalAlign: stretch
114
- children: []
115
- - id: board
116
- type: Group2D
117
- name: Board
118
- properties:
119
- width: 980
120
- height: 1480
121
- transform:
122
- position: [0, -80]
123
- scale: [1, 1]
124
- rotation: 0
125
- children:
126
- - id: board-floor
127
- type: ColorRect2D
128
- name: Board Floor
129
- properties:
130
- width: 980
131
- height: 1480
132
- color: "#1d212e"
133
- transform:
134
- position: [0, 0]
135
- scale: [1, 1]
136
- rotation: 0
137
- children: []
138
- - id: hud
139
- type: CanvasLayer2D
140
- name: HUD
141
- properties:
142
- width: 1080
143
- height: 1920
144
- layout:
145
- enabled: true
146
- horizontalAlign: stretch
147
- verticalAlign: stretch
148
- components:
149
- - id: score-hud
150
- type: user:ScoreHud
151
- enabled: true
152
- config:
153
- sourceNode: game-root
154
- scoreLabel: score-label
155
- scoreFormat: "SCORE {value}"
156
- timeLabel: time-label
157
- timeFormat: "{value}"
158
- livesBar: lives-bar
159
- children:
160
- - id: score-label
161
- type: Label2D
162
- name: Score Label
163
- properties:
164
- label: "SCORE 0"
165
- labelFontSize: 44
166
- labelColor: "#ffffff"
167
- labelAlign: left
168
- transform:
169
- position: [-340, 850]
170
- scale: [1, 1]
171
- rotation: 0
172
- layout:
173
- enabled: true
174
- horizontalAlign: left
175
- verticalAlign: top
176
- children: []
177
- - id: time-label
178
- type: Label2D
179
- name: Time Label
180
- properties:
181
- label: "0"
182
- labelFontSize: 44
183
- labelColor: "#f5ae39"
184
- labelAlign: center
185
- transform:
186
- position: [0, 850]
187
- scale: [1, 1]
188
- rotation: 0
189
- layout:
190
- enabled: true
191
- horizontalAlign: center
192
- verticalAlign: top
193
- children: []
194
- - id: lives-bar
195
- type: Bar2D
196
- name: Lives Bar
197
- properties:
198
- width: 240
199
- height: 28
200
- minValue: 0
201
- maxValue: 3
202
- value: 3
203
- barColor: "#ff4d6d"
204
- backBackgroundColor: "#2a3352"
205
- showBorder: false
206
- transform:
207
- position: [340, 855]
208
- scale: [1, 1]
209
- rotation: 0
210
- layout:
211
- enabled: true
212
- horizontalAlign: right
213
- verticalAlign: top
214
- children: []
215
- # Win/lose screen — authored in scenes/ui/result.pix3scene so this
216
- # scene opens on the GAME, not on a GAME OVER card. `visible: false`
217
- # hides it in the EDITOR only; `initiallyVisible` (in the overlay
218
- # file) is what play mode reads, and GameRules reveals it on the
219
- # win/lose transition. Tick the eye to preview it over the field.
220
- - id: result-overlay
221
- name: Result Overlay
222
- instance: res://scenes/ui/result.pix3scene
223
- properties:
224
- visible: false