@pix3/cli 1.6.3 → 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.
- package/LICENSE +201 -201
- package/README.md +722 -788
- package/dist/index.js +557 -421
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
- package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
- package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
- package/dist/validate/prebuilt/bundle-main.js +223 -18
- package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
- package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
- package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
- package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
- package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
- package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
- package/kit/files/AGENTS.md +76 -84
- package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
- package/kit/kit.json +8 -5
- package/package.json +62 -54
- package/templates/2d/files/scenes/main.pix3scene +31 -0
- package/templates/2d/template.yaml +11 -0
- package/templates/3d/files/scenes/main.pix3scene +50 -0
- package/templates/3d/template.yaml +11 -0
- package/templates/base/files/README.md +39 -0
- package/templates/base/files/gitignore +11 -0
- package/templates/base/files/index.html +23 -0
- package/templates/base/files/package.json +28 -0
- package/templates/base/files/src/main.ts +3 -0
- package/templates/base/files/tsconfig.json +17 -0
- package/templates/base/files/vite.config.ts +10 -0
- package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
- package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
- package/templates/empty-2d/files/README.md +0 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
- package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-2d/template.yaml +0 -7
- package/templates/empty-3d/files/README.md +0 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
- package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-3d/template.yaml +0 -7
- package/templates/idea-blank/files/README.md +0 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
- package/templates/idea-blank/template.yaml +0 -10
- package/templates/minigame-2d/files/README.md +0 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
- package/templates/minigame-2d/template.yaml +0 -10
- package/templates/playable-2d/files/README.md +0 -30
- package/templates/playable-2d/files/design/recipe.md +0 -83
- package/templates/playable-2d/files/design/tests/reachability.json +0 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
- package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/playable-2d/template.yaml +0 -9
- package/templates/playable-3d/files/README.md +0 -32
- package/templates/playable-3d/files/design/recipe.md +0 -127
- package/templates/playable-3d/files/design/tests/reachability.json +0 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
- package/templates/playable-3d/template.yaml +0 -9
- package/templates/recipe-arena-2d/files/README.md +0 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
- package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
- package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
- package/templates/recipe-arena-2d/template.yaml +0 -10
- package/templates/recipe-blank-2d/files/README.md +0 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
- package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
- package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
- package/templates/recipe-blank-2d/template.yaml +0 -10
- package/templates/recipe-bouncer-2d/files/README.md +0 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
- package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
- package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
- package/templates/recipe-bouncer-2d/template.yaml +0 -10
- package/templates/recipe-grid-3d/files/README.md +0 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-grid-3d/template.yaml +0 -10
- package/templates/recipe-tapper-2d/files/README.md +0 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
- package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
- package/templates/recipe-tapper-2d/template.yaml +0 -10
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# Pix3 Scene File (YAML)
|
|
2
|
-
# Bouncer 2D — the MENU scene. This is the project's ENTRY / export scene
|
|
3
|
-
# (Project Settings → Default Export Scene Path): a full build and "Play Game (Entry Scene)"
|
|
4
|
-
# boot here. PLAY transitions to the game scene (main.pix3scene) via a fade.
|
|
5
|
-
# Wired by scripts/MenuFlow.ts. Iterate on the GAME in main.pix3scene.
|
|
6
|
-
#
|
|
7
|
-
# Same neon recipe as the game scene: darkest ColorRect + a tinted radial
|
|
8
|
-
# gradient placeholder, a `PostProcess` node so bright accents bloom, and
|
|
9
|
-
# Label2D's own glow on the text. Nothing here is a CanvasLayer2D, so the title
|
|
10
|
-
# gets BOTH the canvas glow and the bloom — keep `menuBloomIntensity` modest or
|
|
11
|
-
# large white type turns to mush.
|
|
12
|
-
version: 1.0.0
|
|
13
|
-
metadata:
|
|
14
|
-
author: Pix3 Recipe
|
|
15
|
-
description: Menu scene — entry point; PLAY transitions to the game
|
|
16
|
-
root:
|
|
17
|
-
- id: menu-post-fx
|
|
18
|
-
type: PostProcess
|
|
19
|
-
name: Menu Post FX
|
|
20
|
-
properties:
|
|
21
|
-
affect2D: true
|
|
22
|
-
bloomEnabled: true
|
|
23
|
-
bloomIntensity: 0.7
|
|
24
|
-
bloomThreshold: 0.6
|
|
25
|
-
bloomSmoothing: 0.2
|
|
26
|
-
bloomRadius: 0.8
|
|
27
|
-
vignetteEnabled: true
|
|
28
|
-
vignetteOffset: 0.28
|
|
29
|
-
vignetteDarkness: 0.55
|
|
30
|
-
chromaticAberrationEnabled: false
|
|
31
|
-
children: []
|
|
32
|
-
- id: menu-root
|
|
33
|
-
type: Group2D
|
|
34
|
-
name: Menu Root
|
|
35
|
-
properties:
|
|
36
|
-
width: 1080
|
|
37
|
-
height: 1920
|
|
38
|
-
layout:
|
|
39
|
-
enabled: true
|
|
40
|
-
horizontalAlign: stretch
|
|
41
|
-
verticalAlign: stretch
|
|
42
|
-
components:
|
|
43
|
-
- id: menu-flow
|
|
44
|
-
type: user:MenuFlow
|
|
45
|
-
enabled: true
|
|
46
|
-
config:
|
|
47
|
-
gameScene: res://scenes/main.pix3scene
|
|
48
|
-
playButton: play-button
|
|
49
|
-
children:
|
|
50
|
-
- id: menu-background
|
|
51
|
-
type: ColorRect2D
|
|
52
|
-
name: Menu Background
|
|
53
|
-
properties:
|
|
54
|
-
width: 1080
|
|
55
|
-
height: 1920
|
|
56
|
-
color: "#05030f"
|
|
57
|
-
transform:
|
|
58
|
-
position: [0, 0]
|
|
59
|
-
scale: [1, 1]
|
|
60
|
-
rotation: 0
|
|
61
|
-
layout:
|
|
62
|
-
enabled: true
|
|
63
|
-
horizontalAlign: stretch
|
|
64
|
-
verticalAlign: stretch
|
|
65
|
-
children: []
|
|
66
|
-
- id: menu-bg-glow
|
|
67
|
-
type: Sprite2D
|
|
68
|
-
name: Menu Background Glow
|
|
69
|
-
properties:
|
|
70
|
-
texture: { type: 'texture', url: 'res://sprites/ph-bg.png' }
|
|
71
|
-
width: 1080
|
|
72
|
-
height: 1920
|
|
73
|
-
effects:
|
|
74
|
-
- type: core:tint
|
|
75
|
-
params: { color: "#5a2df0", amount: 1 }
|
|
76
|
-
transform:
|
|
77
|
-
position: [0, 0]
|
|
78
|
-
scale: [1, 1]
|
|
79
|
-
rotation: 0
|
|
80
|
-
layout:
|
|
81
|
-
enabled: true
|
|
82
|
-
horizontalAlign: stretch
|
|
83
|
-
verticalAlign: stretch
|
|
84
|
-
children: []
|
|
85
|
-
# The project title is patched into this node on project creation — keep
|
|
86
|
-
# the id. `width` is set so a long title WRAPS instead of running off
|
|
87
|
-
# screen (Label2D word-wraps to its box; 0 would auto-size and overflow).
|
|
88
|
-
- id: title-label
|
|
89
|
-
type: Label2D
|
|
90
|
-
name: Title Label
|
|
91
|
-
properties:
|
|
92
|
-
label: "{{PROJECT_NAME}}"
|
|
93
|
-
labelFontSize: 112
|
|
94
|
-
labelColor: "#ffffff"
|
|
95
|
-
labelAlign: center
|
|
96
|
-
width: 960
|
|
97
|
-
glowColor: "#2ef2ff"
|
|
98
|
-
glowStrength: 2.5
|
|
99
|
-
transform:
|
|
100
|
-
position: [0, 420]
|
|
101
|
-
scale: [1, 1]
|
|
102
|
-
rotation: 0
|
|
103
|
-
layout:
|
|
104
|
-
enabled: true
|
|
105
|
-
horizontalAlign: center
|
|
106
|
-
verticalAlign: center
|
|
107
|
-
components:
|
|
108
|
-
- id: title-pop
|
|
109
|
-
type: core:PopIn
|
|
110
|
-
enabled: true
|
|
111
|
-
config:
|
|
112
|
-
from: 0.55
|
|
113
|
-
duration: 0.5
|
|
114
|
-
easing: backOut
|
|
115
|
-
playOnStart: true
|
|
116
|
-
triggerEvent: ""
|
|
117
|
-
- id: title-breathe
|
|
118
|
-
type: core:Sine
|
|
119
|
-
enabled: true
|
|
120
|
-
config:
|
|
121
|
-
amplitude: 12
|
|
122
|
-
frequency: 0.28
|
|
123
|
-
axis: y
|
|
124
|
-
children: []
|
|
125
|
-
- id: title-rule
|
|
126
|
-
type: ColorRect2D
|
|
127
|
-
name: Title Rule
|
|
128
|
-
properties:
|
|
129
|
-
width: 520
|
|
130
|
-
height: 6
|
|
131
|
-
color: "#2ef2ff"
|
|
132
|
-
transform:
|
|
133
|
-
position: [0, 352]
|
|
134
|
-
scale: [1, 1]
|
|
135
|
-
rotation: 0
|
|
136
|
-
layout:
|
|
137
|
-
enabled: true
|
|
138
|
-
horizontalAlign: center
|
|
139
|
-
verticalAlign: center
|
|
140
|
-
children: []
|
|
141
|
-
- id: subtitle-label
|
|
142
|
-
type: Label2D
|
|
143
|
-
name: Subtitle Label
|
|
144
|
-
properties:
|
|
145
|
-
label: "KEEP THE BALL ALIVE."
|
|
146
|
-
labelFontSize: 40
|
|
147
|
-
labelColor: "#8ffcff"
|
|
148
|
-
labelAlign: center
|
|
149
|
-
glowColor: "#2ef2ff"
|
|
150
|
-
glowStrength: 1
|
|
151
|
-
transform:
|
|
152
|
-
position: [0, 280]
|
|
153
|
-
scale: [1, 1]
|
|
154
|
-
rotation: 0
|
|
155
|
-
layout:
|
|
156
|
-
enabled: true
|
|
157
|
-
horizontalAlign: center
|
|
158
|
-
verticalAlign: center
|
|
159
|
-
children: []
|
|
160
|
-
- id: play-button
|
|
161
|
-
type: Button2D
|
|
162
|
-
name: Play Button
|
|
163
|
-
properties:
|
|
164
|
-
width: 480
|
|
165
|
-
height: 140
|
|
166
|
-
label: "PLAY"
|
|
167
|
-
labelFontSize: 56
|
|
168
|
-
labelColor: "#0d0724"
|
|
169
|
-
backgroundColor: "#ffd23f"
|
|
170
|
-
hoverColor: "#ffe98a"
|
|
171
|
-
pressedColor: "#d49a1f"
|
|
172
|
-
buttonAction: play
|
|
173
|
-
transform:
|
|
174
|
-
position: [0, -60]
|
|
175
|
-
scale: [1, 1]
|
|
176
|
-
rotation: 0
|
|
177
|
-
layout:
|
|
178
|
-
enabled: true
|
|
179
|
-
horizontalAlign: center
|
|
180
|
-
verticalAlign: center
|
|
181
|
-
components:
|
|
182
|
-
- id: play-pop
|
|
183
|
-
type: core:PopIn
|
|
184
|
-
enabled: true
|
|
185
|
-
config:
|
|
186
|
-
from: 0.7
|
|
187
|
-
duration: 0.45
|
|
188
|
-
easing: backOut
|
|
189
|
-
playOnStart: true
|
|
190
|
-
triggerEvent: ""
|
|
191
|
-
children: []
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Pix3 Scene File (YAML)
|
|
2
|
-
# Bouncer 2D — the RESULT overlay (win/lose), in its own file so the gameplay
|
|
3
|
-
# scene opens clean. `main.pix3scene` carries it as a one-line
|
|
4
|
-
# `instance:` reference marked `visible: false`, which hides it in the EDITOR
|
|
5
|
-
# only; `user:GameRules` reveals it at run time by setting `visible`.
|
|
6
|
-
#
|
|
7
|
-
# Edit it here, full-screen, in its own tab. To preview it composited over the
|
|
8
|
-
# game, tick the eye on `result-overlay` in main.pix3scene — just do not commit
|
|
9
|
-
# that tick, or every iteration starts behind a GAME OVER screen again.
|
|
10
|
-
#
|
|
11
|
-
# Stable node ids (design/recipe.md is the contract — do not rename these):
|
|
12
|
-
# result-overlay, result-label, retry-button
|
|
13
|
-
version: 1.0.0
|
|
14
|
-
metadata:
|
|
15
|
-
author: Pix3 Recipe
|
|
16
|
-
description: Result overlay — dim, glowing win/lose headline and a RETRY button
|
|
17
|
-
root:
|
|
18
|
-
- id: result-overlay
|
|
19
|
-
type: Group2D
|
|
20
|
-
name: Result Overlay
|
|
21
|
-
properties:
|
|
22
|
-
width: 1080
|
|
23
|
-
height: 1920
|
|
24
|
-
initiallyVisible: false
|
|
25
|
-
layout:
|
|
26
|
-
enabled: true
|
|
27
|
-
horizontalAlign: stretch
|
|
28
|
-
verticalAlign: stretch
|
|
29
|
-
children:
|
|
30
|
-
- id: result-dim
|
|
31
|
-
type: ColorRect2D
|
|
32
|
-
name: Result Dim
|
|
33
|
-
properties:
|
|
34
|
-
width: 1080
|
|
35
|
-
height: 1920
|
|
36
|
-
color: "#05030f"
|
|
37
|
-
opacity: 0.78
|
|
38
|
-
transform:
|
|
39
|
-
position: [0, 0]
|
|
40
|
-
scale: [1, 1]
|
|
41
|
-
rotation: 0
|
|
42
|
-
layout:
|
|
43
|
-
enabled: true
|
|
44
|
-
horizontalAlign: stretch
|
|
45
|
-
verticalAlign: stretch
|
|
46
|
-
children: []
|
|
47
|
-
- id: result-label
|
|
48
|
-
type: Label2D
|
|
49
|
-
name: Result Label
|
|
50
|
-
properties:
|
|
51
|
-
label: "GAME OVER"
|
|
52
|
-
labelFontSize: 96
|
|
53
|
-
labelColor: "#ffffff"
|
|
54
|
-
labelAlign: center
|
|
55
|
-
glowColor: "#2ef2ff"
|
|
56
|
-
glowStrength: 2.5
|
|
57
|
-
transform:
|
|
58
|
-
position: [0, 160]
|
|
59
|
-
scale: [1, 1]
|
|
60
|
-
rotation: 0
|
|
61
|
-
layout:
|
|
62
|
-
enabled: true
|
|
63
|
-
horizontalAlign: center
|
|
64
|
-
verticalAlign: center
|
|
65
|
-
children: []
|
|
66
|
-
- id: retry-button
|
|
67
|
-
type: Button2D
|
|
68
|
-
name: Retry Button
|
|
69
|
-
properties:
|
|
70
|
-
width: 420
|
|
71
|
-
height: 130
|
|
72
|
-
label: "RETRY"
|
|
73
|
-
labelFontSize: 50
|
|
74
|
-
labelColor: "#0d0724"
|
|
75
|
-
backgroundColor: "#ffd23f"
|
|
76
|
-
hoverColor: "#ffe98a"
|
|
77
|
-
pressedColor: "#d49a1f"
|
|
78
|
-
buttonAction: retry
|
|
79
|
-
transform:
|
|
80
|
-
position: [0, -60]
|
|
81
|
-
scale: [1, 1]
|
|
82
|
-
rotation: 0
|
|
83
|
-
layout:
|
|
84
|
-
enabled: true
|
|
85
|
-
horizontalAlign: center
|
|
86
|
-
verticalAlign: center
|
|
87
|
-
children: []
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BallBody — the ball's motion and collisions.
|
|
3
|
-
*
|
|
4
|
-
* The engine ships no rigidbody solver, and `core:Hitbox2D` is an axis-aligned
|
|
5
|
-
* overlap test that ignores rotation — useless for a fast ball and impossible
|
|
6
|
-
* for an angled paddle. So this component runs the swept solver in
|
|
7
|
-
* `ball-collision.ts` and builds its colliders every frame from the LIVE WORLD
|
|
8
|
-
* TRANSFORMS of ordinary marker nodes:
|
|
9
|
-
*
|
|
10
|
-
* `wallsNode` / `paddlesNode` children → oriented boxes (4 segments each),
|
|
11
|
-
* so a rotated paddle or flipper works
|
|
12
|
-
* `bumpersNode` children → circles
|
|
13
|
-
* `drainNode` → trigger box: entering it loses a ball
|
|
14
|
-
*
|
|
15
|
-
* Because the geometry is re-read each frame, rotating or moving a marker node
|
|
16
|
-
* (from a script, an AnimationPlayer clip, or the inspector) just works.
|
|
17
|
-
*
|
|
18
|
-
* On contact it emits `ball-hit` (kind, nodeId, speed, x, y) on its own node,
|
|
19
|
-
* and `ball-drained` when it is lost. TouchRules turns those into score/damage.
|
|
20
|
-
* Never import rapier: it is a ~2 MB lazy wasm payload this does not need.
|
|
21
|
-
*/
|
|
22
|
-
import { NodeBase, Script, type PropertySchema } from '@pix3/runtime';
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
stepBall,
|
|
26
|
-
type BallOptions,
|
|
27
|
-
type BallState,
|
|
28
|
-
type CircleCollider,
|
|
29
|
-
type SegmentCollider,
|
|
30
|
-
} from './ball-collision';
|
|
31
|
-
|
|
32
|
-
/** Oriented box in world space: centre, unit axes, half extents. */
|
|
33
|
-
interface Obb {
|
|
34
|
-
cx: number;
|
|
35
|
-
cy: number;
|
|
36
|
-
ux: number;
|
|
37
|
-
uy: number;
|
|
38
|
-
hw: number;
|
|
39
|
-
hh: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Any 2D node that carries a size (ColorRect2D, Sprite2D, Group2D, …). */
|
|
43
|
-
interface Sized {
|
|
44
|
-
width?: number;
|
|
45
|
-
height?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function readObb(node: NodeBase): Obb {
|
|
49
|
-
node.updateWorldMatrix(true, false);
|
|
50
|
-
const e = node.matrixWorld.elements;
|
|
51
|
-
const scaleX = Math.hypot(e[0], e[1]) || 1;
|
|
52
|
-
const scaleY = Math.hypot(e[4], e[5]) || 1;
|
|
53
|
-
const sized = node as unknown as Sized;
|
|
54
|
-
return {
|
|
55
|
-
cx: e[12],
|
|
56
|
-
cy: e[13],
|
|
57
|
-
ux: e[0] / scaleX,
|
|
58
|
-
uy: e[1] / scaleX,
|
|
59
|
-
hw: ((Number(sized.width) || 0) / 2) * scaleX,
|
|
60
|
-
hh: ((Number(sized.height) || 0) / 2) * scaleY,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function obbSegments(obb: Obb, restitution: number, id: string, out: SegmentCollider[]): void {
|
|
65
|
-
// Perpendicular of (ux, uy) is (-uy, ux) — the box's local Y axis in world space.
|
|
66
|
-
const ax = obb.ux * obb.hw;
|
|
67
|
-
const ay = obb.uy * obb.hw;
|
|
68
|
-
const bx = -obb.uy * obb.hh;
|
|
69
|
-
const by = obb.ux * obb.hh;
|
|
70
|
-
const corners = [
|
|
71
|
-
[obb.cx - ax - bx, obb.cy - ay - by],
|
|
72
|
-
[obb.cx + ax - bx, obb.cy + ay - by],
|
|
73
|
-
[obb.cx + ax + bx, obb.cy + ay + by],
|
|
74
|
-
[obb.cx - ax + bx, obb.cy - ay + by],
|
|
75
|
-
];
|
|
76
|
-
for (let i = 0; i < 4; i++) {
|
|
77
|
-
const from = corners[i];
|
|
78
|
-
const to = corners[(i + 1) % 4];
|
|
79
|
-
out.push({ ax: from[0], ay: from[1], bx: to[0], by: to[1], restitution, id });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function pointInObb(obb: Obb, x: number, y: number): boolean {
|
|
84
|
-
const dx = x - obb.cx;
|
|
85
|
-
const dy = y - obb.cy;
|
|
86
|
-
const along = dx * obb.ux + dy * obb.uy;
|
|
87
|
-
const across = dx * -obb.uy + dy * obb.ux;
|
|
88
|
-
return Math.abs(along) <= obb.hw && Math.abs(across) <= obb.hh;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function childNodes(parent: NodeBase | null): NodeBase[] {
|
|
92
|
-
if (!parent) {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
return parent.children.filter((child): child is NodeBase => child instanceof NodeBase);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export class BallBody extends Script {
|
|
99
|
-
private readonly state: BallState = { x: 0, y: 0, vx: 0, vy: 0 };
|
|
100
|
-
private readonly segments: SegmentCollider[] = [];
|
|
101
|
-
private readonly circles: CircleCollider[] = [];
|
|
102
|
-
private readonly kinds = new Map<string, string>();
|
|
103
|
-
private homeX = 0;
|
|
104
|
-
private homeY = 0;
|
|
105
|
-
private respawnIn = 0;
|
|
106
|
-
|
|
107
|
-
constructor(id: string, type: string) {
|
|
108
|
-
super(id, type);
|
|
109
|
-
this.config = {
|
|
110
|
-
radius: 30,
|
|
111
|
-
gravity: -2400,
|
|
112
|
-
maxSpeed: 2600,
|
|
113
|
-
minSpeed: 420,
|
|
114
|
-
damping: 0.999,
|
|
115
|
-
substeps: 4,
|
|
116
|
-
launchSpeed: 1150,
|
|
117
|
-
launchAngleDeg: 72,
|
|
118
|
-
wallsNode: 'walls',
|
|
119
|
-
paddlesNode: 'paddles',
|
|
120
|
-
bumpersNode: 'bumpers',
|
|
121
|
-
drainNode: 'drain',
|
|
122
|
-
wallRestitution: 1,
|
|
123
|
-
paddleRestitution: 1.05,
|
|
124
|
-
bumperRestitution: 1.3,
|
|
125
|
-
resetDelaySec: 0.7,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
static getPropertySchema(): PropertySchema {
|
|
130
|
-
const num = (name: string, label: string, min: number, max: number, step: number, group: string) => ({
|
|
131
|
-
name,
|
|
132
|
-
type: 'number' as const,
|
|
133
|
-
ui: { label, group, min, max, step, slider: true },
|
|
134
|
-
getValue: (s: unknown) => (s as BallBody).config[name],
|
|
135
|
-
setValue: (s: unknown, v: unknown) => {
|
|
136
|
-
const n = Number(v);
|
|
137
|
-
(s as BallBody).config[name] = Math.min(max, Math.max(min, Number.isFinite(n) ? n : min));
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
const str = (name: string, label: string) => ({
|
|
141
|
-
name,
|
|
142
|
-
type: 'string' as const,
|
|
143
|
-
ui: { label, group: 'Colliders' },
|
|
144
|
-
getValue: (s: unknown) => (s as BallBody).config[name],
|
|
145
|
-
setValue: (s: unknown, v: unknown) => {
|
|
146
|
-
(s as BallBody).config[name] = typeof v === 'string' ? v : '';
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
nodeType: 'BallBody',
|
|
152
|
-
properties: [
|
|
153
|
-
num('radius', 'Radius', 2, 400, 1, 'Body'),
|
|
154
|
-
num('gravity', 'Gravity (px/s²)', -12000, 12000, 50, 'Body'),
|
|
155
|
-
num('maxSpeed', 'Max Speed', 100, 12000, 50, 'Body'),
|
|
156
|
-
num('minSpeed', 'Min Speed', 0, 4000, 10, 'Body'),
|
|
157
|
-
num('damping', 'Damping', 0.9, 1, 0.001, 'Body'),
|
|
158
|
-
num('substeps', 'Substeps', 1, 16, 1, 'Body'),
|
|
159
|
-
num('launchSpeed', 'Launch Speed', 0, 6000, 25, 'Launch'),
|
|
160
|
-
num('launchAngleDeg', 'Launch Angle (°)', 5, 175, 1, 'Launch'),
|
|
161
|
-
num('resetDelaySec', 'Reset Delay (s)', 0, 5, 0.1, 'Launch'),
|
|
162
|
-
num('wallRestitution', 'Wall Bounce', 0, 2, 0.01, 'Bounce'),
|
|
163
|
-
num('paddleRestitution', 'Paddle Bounce', 0, 2, 0.01, 'Bounce'),
|
|
164
|
-
num('bumperRestitution', 'Bumper Bounce', 0, 3, 0.01, 'Bounce'),
|
|
165
|
-
str('wallsNode', 'Walls Node'),
|
|
166
|
-
str('paddlesNode', 'Paddles Node'),
|
|
167
|
-
str('bumpersNode', 'Bumpers Node'),
|
|
168
|
-
str('drainNode', 'Drain Node'),
|
|
169
|
-
],
|
|
170
|
-
groups: {
|
|
171
|
-
Body: { label: 'Body', expanded: true },
|
|
172
|
-
Launch: { label: 'Launch', expanded: true },
|
|
173
|
-
Bounce: { label: 'Bounce', expanded: true },
|
|
174
|
-
Colliders: { label: 'Collider Sources', expanded: false },
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
onStart(): void {
|
|
180
|
-
const node = this.node;
|
|
181
|
-
if (!node) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
node.updateWorldMatrix(true, false);
|
|
185
|
-
this.homeX = node.matrixWorld.elements[12];
|
|
186
|
-
this.homeY = node.matrixWorld.elements[13];
|
|
187
|
-
this.launch();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
onUpdate(dt: number): void {
|
|
191
|
-
const node = this.node;
|
|
192
|
-
if (!node || dt <= 0) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (this.respawnIn > 0) {
|
|
196
|
-
this.respawnIn -= dt;
|
|
197
|
-
if (this.respawnIn <= 0) {
|
|
198
|
-
this.launch();
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
this.rebuildColliders();
|
|
204
|
-
const options: BallOptions = {
|
|
205
|
-
radius: Math.max(1, Number(this.config.radius) || 1),
|
|
206
|
-
gravity: Number(this.config.gravity) || 0,
|
|
207
|
-
maxSpeed: Number(this.config.maxSpeed) || 0,
|
|
208
|
-
minSpeed: Number(this.config.minSpeed) || 0,
|
|
209
|
-
damping: Number(this.config.damping) || 1,
|
|
210
|
-
substeps: Number(this.config.substeps) || 1,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
for (const hit of stepBall(this.state, options, this.segments, this.circles, dt)) {
|
|
214
|
-
node.emit('ball-hit', this.kinds.get(hit.id) ?? 'wall', hit.id, hit.speed, hit.x, hit.y);
|
|
215
|
-
}
|
|
216
|
-
this.writeBackPosition();
|
|
217
|
-
this.checkDrain();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Send the ball on its way (also used after a drain). Override the angle for a plunger. */
|
|
221
|
-
launch(): void {
|
|
222
|
-
const speed = Math.max(0, Number(this.config.launchSpeed) || 0);
|
|
223
|
-
const angle = ((Number(this.config.launchAngleDeg) || 90) * Math.PI) / 180;
|
|
224
|
-
const dir = Math.random() < 0.5 ? -1 : 1;
|
|
225
|
-
this.state.x = this.homeX;
|
|
226
|
-
this.state.y = this.homeY;
|
|
227
|
-
this.state.vx = Math.cos(angle) * speed * dir;
|
|
228
|
-
this.state.vy = Math.sin(angle) * speed;
|
|
229
|
-
this.writeBackPosition();
|
|
230
|
-
this.node?.emit('ball-launched');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private rebuildColliders(): void {
|
|
234
|
-
this.segments.length = 0;
|
|
235
|
-
this.circles.length = 0;
|
|
236
|
-
this.kinds.clear();
|
|
237
|
-
|
|
238
|
-
const wallBounce = Number(this.config.wallRestitution) || 1;
|
|
239
|
-
for (const child of childNodes(this.findNode(String(this.config.wallsNode ?? '')))) {
|
|
240
|
-
obbSegments(readObb(child), wallBounce, child.nodeId, this.segments);
|
|
241
|
-
this.kinds.set(child.nodeId, 'wall');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const paddleBounce = Number(this.config.paddleRestitution) || 1;
|
|
245
|
-
for (const child of childNodes(this.findNode(String(this.config.paddlesNode ?? '')))) {
|
|
246
|
-
obbSegments(readObb(child), paddleBounce, child.nodeId, this.segments);
|
|
247
|
-
this.kinds.set(child.nodeId, 'paddle');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const bumperBounce = Number(this.config.bumperRestitution) || 1;
|
|
251
|
-
for (const child of childNodes(this.findNode(String(this.config.bumpersNode ?? '')))) {
|
|
252
|
-
const obb = readObb(child);
|
|
253
|
-
this.circles.push({ cx: obb.cx, cy: obb.cy, r: Math.max(1, obb.hw), restitution: bumperBounce, id: child.nodeId });
|
|
254
|
-
this.kinds.set(child.nodeId, 'bumper');
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private checkDrain(): void {
|
|
259
|
-
const drain = this.findNode(String(this.config.drainNode ?? ''));
|
|
260
|
-
if (!drain) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const obb = readObb(drain);
|
|
264
|
-
const below = this.state.y < obb.cy - obb.hh;
|
|
265
|
-
if (!below && !pointInObb(obb, this.state.x, this.state.y)) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const node = this.node;
|
|
269
|
-
node?.emit('ball-hit', 'drain', drain.nodeId, Math.hypot(this.state.vx, this.state.vy), this.state.x, this.state.y);
|
|
270
|
-
node?.emit('ball-drained');
|
|
271
|
-
this.state.vx = 0;
|
|
272
|
-
this.state.vy = 0;
|
|
273
|
-
this.respawnIn = Math.max(0.05, Number(this.config.resetDelaySec) || 0.05);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** World simulation → the ball node's parent-local position. */
|
|
277
|
-
private writeBackPosition(): void {
|
|
278
|
-
const node = this.node;
|
|
279
|
-
const parent = node?.parentNode ?? null;
|
|
280
|
-
if (!node) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (!parent) {
|
|
284
|
-
node.position.set(this.state.x, this.state.y, node.position.z);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
parent.updateWorldMatrix(true, false);
|
|
288
|
-
const e = parent.matrixWorld.elements;
|
|
289
|
-
const dx = this.state.x - e[12];
|
|
290
|
-
const dy = this.state.y - e[13];
|
|
291
|
-
const det = e[0] * e[5] - e[1] * e[4];
|
|
292
|
-
if (Math.abs(det) < 1e-8) {
|
|
293
|
-
node.position.set(dx, dy, node.position.z);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
node.position.set((e[5] * dx - e[4] * dy) / det, (e[0] * dy - e[1] * dx) / det, node.position.z);
|
|
297
|
-
}
|
|
298
|
-
}
|