@pix3/cli 1.6.0 → 1.6.2

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 (126) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +788 -754
  3. package/dist/index.js +208 -203
  4. package/dist/runtime-types.json +1 -1
  5. package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-7MZGPAUK.mjs} +1 -1
  6. package/dist/smoke/prebuilt/chunk-7WOFGLW3.mjs +239 -0
  7. package/dist/smoke/prebuilt/runtime.mjs +1 -1
  8. package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
  9. package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
  10. package/dist/validate/prebuilt/bundle-main.js +13 -13
  11. package/dist/validate/prebuilt/chunk-HKG6YI6Y.js +239 -0
  12. package/dist/validate/prebuilt/runtime.js +1 -1
  13. package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
  14. package/kit/files/.claude/skills/pix3-nodes/reference.md +32 -24
  15. package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
  16. package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -28
  17. package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
  18. package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
  19. package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
  20. package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
  21. package/kit/files/AGENTS.md +203 -203
  22. package/kit/files/CLAUDE.md +1 -1
  23. package/kit/kit.json +2 -2
  24. package/package.json +54 -54
  25. package/templates/empty-2d/files/README.md +20 -20
  26. package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
  27. package/templates/empty-2d/template.yaml +7 -7
  28. package/templates/empty-3d/files/README.md +20 -20
  29. package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
  30. package/templates/empty-3d/template.yaml +7 -7
  31. package/templates/idea-blank/files/README.md +22 -22
  32. package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
  33. package/templates/idea-blank/template.yaml +10 -10
  34. package/templates/minigame-2d/files/README.md +44 -44
  35. package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
  36. package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
  37. package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
  38. package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
  39. package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
  40. package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
  41. package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
  42. package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
  43. package/templates/minigame-2d/template.yaml +10 -10
  44. package/templates/playable-2d/files/README.md +30 -30
  45. package/templates/playable-2d/files/design/recipe.md +83 -83
  46. package/templates/playable-2d/files/design/tests/reachability.json +4 -4
  47. package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
  48. package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
  49. package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
  50. package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
  51. package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
  52. package/templates/playable-2d/template.yaml +9 -9
  53. package/templates/playable-3d/files/README.md +32 -32
  54. package/templates/playable-3d/files/design/recipe.md +127 -127
  55. package/templates/playable-3d/files/design/tests/reachability.json +4 -4
  56. package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
  57. package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
  58. package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
  59. package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
  60. package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
  61. package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
  62. package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
  63. package/templates/playable-3d/template.yaml +9 -9
  64. package/templates/recipe-arena-2d/files/README.md +48 -48
  65. package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
  66. package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
  67. package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
  68. package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  69. package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
  70. package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
  71. package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
  72. package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
  73. package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
  74. package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
  75. package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
  76. package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
  77. package/templates/recipe-arena-2d/template.yaml +10 -10
  78. package/templates/recipe-blank-2d/files/README.md +61 -61
  79. package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
  80. package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
  81. package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
  82. package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
  83. package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
  84. package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
  85. package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
  86. package/templates/recipe-blank-2d/template.yaml +10 -10
  87. package/templates/recipe-bouncer-2d/files/README.md +81 -81
  88. package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
  89. package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
  90. package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
  91. package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
  92. package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
  93. package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
  94. package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
  95. package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
  96. package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
  97. package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
  98. package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
  99. package/templates/recipe-bouncer-2d/template.yaml +10 -10
  100. package/templates/recipe-grid-3d/files/README.md +22 -22
  101. package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
  102. package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
  103. package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
  104. package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
  105. package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
  106. package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
  107. package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
  108. package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
  109. package/templates/recipe-grid-3d/template.yaml +10 -10
  110. package/templates/recipe-tapper-2d/files/README.md +39 -39
  111. package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
  112. package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
  113. package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
  114. package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
  115. package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
  116. package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
  117. package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
  118. package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
  119. package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
  120. package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
  121. package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
  122. package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
  123. package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
  124. package/templates/recipe-tapper-2d/template.yaml +10 -10
  125. package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
  126. package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
@@ -1,174 +1,174 @@
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
+ /**
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 +1,10 @@
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
+ 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 +1,61 @@
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
+ # 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.