incanto 0.72.0 → 0.74.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/bin/incanto-check.mjs +114 -42
- package/bin/incanto-verify.mjs +9 -2
- package/dist/2d.d.ts +29 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +137 -15
- package/dist/3d.js +6 -6
- package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
- package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
- package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
- package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
- package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/editor.js +965 -893
- package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
- package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
- package/dist/gameplay.d.ts +141 -7
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -6
- package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
- package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
- package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
- package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
- package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
- package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
- package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
- package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
- package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
- package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
- package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
- package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
- package/dist/test.d.ts +56 -5
- package/dist/test.js +2 -2
- package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
- package/dist/vite.js +3 -3
- package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
- package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
- package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +634 -10
- package/skills/incanto-3d-character.md +113 -5
- package/skills/incanto-assets.md +9 -3
- package/skills/incanto-behaviors-and-scripts.md +26 -2
- package/skills/incanto-building-2d-games.md +9 -0
- package/skills/incanto-building-3d-games.md +45 -0
- package/skills/incanto-editor.md +6 -1
- package/skills/incanto-environment.md +7 -2
- package/skills/incanto-game-feel.md +16 -11
- package/skills/incanto-gameplay-behaviors.md +179 -16
- package/skills/incanto-node-reference.md +124 -45
- package/skills/incanto-physics-and-input.md +67 -5
- package/skills/incanto-scene-json-authoring.md +9 -3
- package/skills/incanto-verifying-your-game.md +19 -2
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
- package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
- package/templates-app/beacon-isle-3d/verify.ts +144 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +126 -0
- package/templates-app/platformer-2d/verify.ts +54 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/PROJECT/Status.md +40 -1
- package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/behaviors.ts +9 -0
- package/templates-app/tps-3d/src/cover.scene.json +26 -0
- package/templates-app/tps-3d/src/game.scene.json +331 -75
- package/templates-app/tps-3d/src/main.ts +14 -0
- package/templates-app/tps-3d/src/pillar.scene.json +25 -0
- package/templates-app/tps-3d/verify.ts +582 -3
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -35,6 +35,10 @@ independent; enable the one matching your scene.
|
|
|
35
35
|
`{"shape":"heightfield"}` (STATIC-only; reads the grid from a `Terrain3D` child — see the
|
|
36
36
|
incanto-environment skill).
|
|
37
37
|
Malformed colliders are hard `BAD_FORMAT` errors at enable time.
|
|
38
|
+
The collider FOLLOWS the terrain: reshape the `Terrain3D` at runtime — `maxHeight`,
|
|
39
|
+
`seed`, `roughness`, a channel a `River3D` cuts — and the heightfield is rebuilt on the
|
|
40
|
+
next physics step, so the ground is walked on where it is drawn (the editor's collider
|
|
41
|
+
outline moves with it for the same reason).
|
|
38
42
|
|
|
39
43
|
**`{"shape":"auto"}` is the one to reach for on props.** It shapes the collider like the
|
|
40
44
|
`MeshInstance3D` the body CARRIES — box→cuboid, sphere→ball, cylinder/capsule→their own
|
|
@@ -49,6 +53,10 @@ the corners. Give the body its mesh as a CHILD and let the shape follow:
|
|
|
49
53
|
"children": [ { "name": "Deck", "type": "MeshInstance3D",
|
|
50
54
|
"props": { "mesh": "box", "size": [14, 0.22, 2.2] } } ] }
|
|
51
55
|
```
|
|
56
|
+
It FOLLOWS the mesh, too: resize it, turn the box into a sphere, or move it inside the
|
|
57
|
+
body at runtime and the collider is rebuilt on the next physics step — the same rule the
|
|
58
|
+
heightfield keeps with its terrain. A body that takes its shape from another node moves
|
|
59
|
+
with that node, not only with its own `collider` prop.
|
|
52
60
|
|
|
53
61
|
The child's OWN `position`, `rotation` and `scale` are part of the shape: raise the plank on
|
|
54
62
|
the child instead of the body and the collider goes up with it; scale the crate 4× and it is
|
|
@@ -77,19 +85,48 @@ The body simply does not exist until the model has downloaded — nothing is inv
|
|
|
77
85
|
meantime — and it appears the step the asset lands. A body is ONE shape, so `auto` fits one
|
|
78
86
|
mesh or one model and warns when the body carries more.
|
|
79
87
|
|
|
80
|
-
- **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider
|
|
88
|
+
- **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`, and the SURFACE it
|
|
89
|
+
is: `friction 0.5` (0 = ice, 1+ = glue) and `restitution 0` (1 = trampoline; above 1
|
|
90
|
+
is allowed and is a catapult). Two things a pad under the PLAYER taught: the controller's
|
|
91
|
+
falls run at `fallGravity` (2.5×) and its rises at 1×, so a `restitution 1` pad injects
|
|
92
|
+
energy on every bounce of a character that falls onto it (measured: 6.9 m, then 11.6 m) —
|
|
93
|
+
use ~0.8 for a pad players land on again and again; and `friction 0` is not ice for a
|
|
94
|
+
CharacterController3D, because the controller brakes itself (its own damping, not the
|
|
95
|
+
floor's friction, stops it) — an ice patch slows a crate, not the character. A static
|
|
96
|
+
body's authored value WINS the pair — restitution above 0 combines by Max, friction below
|
|
97
|
+
the default by Min and above it by Max — so a trampoline launches a dead ball and an ice
|
|
98
|
+
patch slides a grippy crate. The same on a `RigidBody`: its own `friction`/`restitution`
|
|
99
|
+
win the pair too; when both sides speak, Rapier's order (Max > Multiply > Min > Average)
|
|
100
|
+
decides. Left at the defaults it pairs by average, as it always did.
|
|
101
|
+
On a `Terrain3D`, `snapToGround: <lift>` on the body sits the pad on the island at load
|
|
102
|
+
(beacon-isle's trampoline: `snapToGround: 0.1` for a 0.2 m slab).
|
|
81
103
|
- **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
|
|
82
104
|
`fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
|
|
83
105
|
read back every step; `velocity` is an alias for it). 3D also has
|
|
84
|
-
`angularVelocity
|
|
85
|
-
|
|
106
|
+
`angularVelocity` (RADIANS per second about each world axis — the one place the
|
|
107
|
+
engine is not in degrees). Every one of them is LIVE — write it mid-run and the solver
|
|
108
|
+
picks it up on the next step. Three things a first composition of these measured:
|
|
109
|
+
an authored `restitution` or `friction` WINS the pair whichever side wrote it — a `0.75`
|
|
110
|
+
ball on a plain floor keeps 75% of its speed (it used to average with the floor's 0 and
|
|
111
|
+
keep 37%), a `friction: 0` body slides on a plain floor as if the floor were ice (the
|
|
112
|
+
character skill's recipe, honoured at last: walk 4.0, not 3.7);
|
|
113
|
+
`gravityScale: 0` with an `angularVelocity` and a `linearVelocity` is a floating,
|
|
114
|
+
spinning, drifting prop and a NEGATIVE `gravityScale` rises; and the body's
|
|
115
|
+
`rotation` is written back as Euler XYZ, which reports a yaw past 90° as
|
|
116
|
+
`[180, 180 − yaw, 180]` — recover a heading from all three, never from `rotation[1]`.
|
|
86
117
|
- **`Area2D/3D`** — sensor. Emits `triggerEnter(other)` / `triggerExit(other)`. Never blocks
|
|
87
118
|
movement. Solid bodies emit the same signals on real contact (one mental model).
|
|
88
119
|
Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
|
|
89
120
|
enemy-hitbox Area) — neither side needs to be a Body.
|
|
90
121
|
- **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
|
|
91
122
|
(capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`,
|
|
92
|
-
`stepHeight` (3D `0.35` m / 2D `35` px — see below)
|
|
123
|
+
`stepHeight` (3D `0.35` m / 2D `35` px — see below), and
|
|
124
|
+
**`collideWithCharacters true`** — whether OTHER characters are obstacles to
|
|
125
|
+
this one. Leave it on for a few enemies; turn it OFF for a horde: eighty
|
|
126
|
+
chasers around a player cost 64–286 ms a STEP with it on (each slide
|
|
127
|
+
shape-casts against every neighbour — O(n²) in the crowd, and 93% of the
|
|
128
|
+
profile was Rapier) and 1.6 ms off. The horde overlaps itself and nobody
|
|
129
|
+
minds; it still collides with the floor and the walls.
|
|
93
130
|
API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`,
|
|
94
131
|
**`isOnWall()`** and **`isOnCeiling()`**. Which wall is answered in the shape
|
|
95
132
|
each dimension has: 2D's `wallSide()` is `-1` left / `+1` right, 3D's
|
|
@@ -336,7 +373,8 @@ per frame from `update(dt)` scaled by `dt`. In 2D the impulse is in px·kg/s,
|
|
|
336
373
|
y-down, exactly like `linearVelocity`.
|
|
337
374
|
|
|
338
375
|
Readable/writable body state, in BOTH adapters: `velocity` (= `linearVelocity`),
|
|
339
|
-
`angularVelocity`, `mass`, `gravityScale`, `friction`, `restitution
|
|
376
|
+
`angularVelocity`, `mass`, `gravityScale`, `friction`, `restitution`,
|
|
377
|
+
`linearDamping`, `angularDamping`. Read the
|
|
340
378
|
spin to see how fast a lever is swinging; write it to launch a spinning body.
|
|
341
379
|
3D's is a 3-vector about the world axes, 2D's is the scalar rad/s about z with
|
|
342
380
|
the same sign as `rotation` (clockwise, because 2D is y-down). There is no
|
|
@@ -352,6 +390,30 @@ body is CREATED. Changing it mid-run does nothing, and a locked body's
|
|
|
352
390
|
> in both, `mass`/`friction`/`restitution` were read once at creation in both,
|
|
353
391
|
> and `gravityScale` was live in 3D and dead in 2D.
|
|
354
392
|
|
|
393
|
+
### A ball that never stops (`angularDamping`, `linearDamping`)
|
|
394
|
+
|
|
395
|
+
A sphere on a flat floor, one impulse: 4.27 m/s at one second and 4.27 at
|
|
396
|
+
eight. Rapier has no rolling resistance — friction only converts slip into
|
|
397
|
+
spin, and a rolling ball is a wheel — so a golf ball, a bowling ball, a marble
|
|
398
|
+
and a kicked can all rolled to the edge of the world. Two props, per second,
|
|
399
|
+
`0` by default (nothing you built changes):
|
|
400
|
+
|
|
401
|
+
| prop | what it damps | reach for it when |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| `angularDamping` | the SPIN, so a rolling body coasts to a stop | anything that rolls: `2` stops a golf ball from 4 m/s in about four seconds, `0.5` is a long coast |
|
|
404
|
+
| `linearDamping` | the VELOCITY itself, `e^(-k·t)` | drag through air or water: a parachute, a puck on felt, a body that must not fly forever |
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{ "name": "Ball", "type": "RigidBody3D",
|
|
408
|
+
"props": { "friction": 0.6, "restitution": 0.3, "angularDamping": 2,
|
|
409
|
+
"collider": { "shape": "sphere", "radius": 0.2 } } }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Both are live — a green that slows the ball, a pond that drags it — and both
|
|
413
|
+
exist on `RigidBody2D` in the same terms. "At rest" is yours to decide: read
|
|
414
|
+
`physics.velocityOf(ball)` and call it stopped under ~0.05 for a few frames.
|
|
415
|
+
`examples/minigolf-3d` is the composition.
|
|
416
|
+
|
|
355
417
|
## What is inside this Area right now?
|
|
356
418
|
|
|
357
419
|
`triggerEnter(other)` / `triggerExit(other)` tell you about CROSSINGS. A pressure
|
|
@@ -103,7 +103,11 @@ Rules the engine enforces with **hard errors** (it never warns silently):
|
|
|
103
103
|
On export, default-equal values are omitted automatically.
|
|
104
104
|
3. Prop keys and value kinds are validated against the type's schema:
|
|
105
105
|
unknown key → `UNKNOWN_PROP` (message lists valid keys); wrong JSON kind
|
|
106
|
-
(e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`.
|
|
106
|
+
(e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`. A prop
|
|
107
|
+
whose default is `null` means AUTO — the engine decides — and validates the
|
|
108
|
+
other kinds it declares (`drape`/`collide`/`islandEdge`/`Chase.ground` take
|
|
109
|
+
a boolean, `snapToGround` a boolean or a lift in metres, `reflectivity` a
|
|
110
|
+
number); the editor offers the boolean ones as auto / on / off.
|
|
107
111
|
A few props accept TWO kinds — `Flowers3D.density` takes a preset name or
|
|
108
112
|
plants per m², `Water3D.underwater` takes `true`/`false` or a settings
|
|
109
113
|
object — and `incanto-node-reference.md` names both in its Kind column
|
|
@@ -425,8 +429,10 @@ const resolveScene = (path: string) => (path === 'crate.scene.json' ? crateJson
|
|
|
425
429
|
await runScript(sceneJson, { durationMs: 4000, resolveScene, steps: [...] });
|
|
426
430
|
```
|
|
427
431
|
|
|
428
|
-
`examples/tiles-2d` is the worked example: two crates that ARE
|
|
429
|
-
`crate.scene.json`, one of them adding a rope child of its own.
|
|
432
|
+
`examples/tiles-2d` is the worked example in 2D: two crates that ARE
|
|
433
|
+
`crate.scene.json`, one of them adding a rope child of its own. `examples/tps-3d`
|
|
434
|
+
is the 3D one: the arena's cover blocks and pillars are `cover.scene.json` and
|
|
435
|
+
`pillar.scene.json`, placed with a `position` override each.
|
|
430
436
|
|
|
431
437
|
## Error codes you will see (all hard failures at load)
|
|
432
438
|
|
|
@@ -41,6 +41,10 @@ $ bunx incanto verify # finds your scene AND your behaviou
|
|
|
41
41
|
passes what was measured — plays, draws not measured.
|
|
42
42
|
next: nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`
|
|
43
43
|
```
|
|
44
|
+
`--runs N` gives the `plays` rung more seeds (default 8). Eight see a one-in-eight event
|
|
45
|
+
64% of the time; `bun run sweep` asks the six shipped starters for sixteen, and a starter that
|
|
46
|
+
falls out of the world once in twelve runs is how the arena in tps-3d got its `Respawn`.
|
|
47
|
+
|
|
44
48
|
|
|
45
49
|
### `agrees` — do two clients hold the same values?
|
|
46
50
|
|
|
@@ -72,7 +76,10 @@ encodes so you do not have to remember them:
|
|
|
72
76
|
warned about is printed under it with a `!` — `loads` runs `incanto-check`,
|
|
73
77
|
and the ladder used to read only whether it passed and drop the warnings, so a
|
|
74
78
|
scene the check describes as "It will render black" came back as
|
|
75
|
-
`✓ loads — the scene is legal and its assets resolve
|
|
79
|
+
`✓ loads — the scene is legal and its assets resolve` (LOCAL art is looked for on
|
|
80
|
+
disk; REMOTE urls are asked with a HEAD request — a 404 drops the claim to
|
|
81
|
+
`the scene is legal`, and a host that cannot be reached is a `note:`, never a
|
|
82
|
+
failure);
|
|
76
83
|
- an **unmeasured** rung (`?`) is not a failure. "No dev server" means the
|
|
77
84
|
question was never asked; treating that as a broken game sends you editing a
|
|
78
85
|
scene that is fine;
|
|
@@ -218,7 +225,7 @@ const result = await runScript(sceneJson, {
|
|
|
218
225
|
durationMs: 3000,
|
|
219
226
|
seed: 42, // same seed → identical run
|
|
220
227
|
steps: [
|
|
221
|
-
{ atMs: 0, vector: ['move', 1, 0] }, // hold right
|
|
228
|
+
{ atMs: 0, vector: ['move', 1, 0] }, // hold right (y is DOWN: forward is [0, -1])
|
|
222
229
|
{ atMs: 1000, press: 'jump' },
|
|
223
230
|
{ atMs: 1100, release: 'jump' },
|
|
224
231
|
{ atMs: 1500, label: 'jump apex reached',
|
|
@@ -252,6 +259,16 @@ captures; `result.logs` holds everything behaviors logged via `this.log`.
|
|
|
252
259
|
Steps that never execute (atMs beyond the run) are reported as failures —
|
|
253
260
|
an assert cannot "pass" by not running.
|
|
254
261
|
|
|
262
|
+
**A game that has ended does not advance.** `GameFlow` stops the clock
|
|
263
|
+
(`timeScale 0`) on a win or a loss, and a pause does the same; after that every
|
|
264
|
+
`ctx.engine.step()` is a no-op and every value you read is the last one before
|
|
265
|
+
it stopped. A harness that lost its player three times inside one `do:` then
|
|
266
|
+
measured `0.00` for everything after it. The report now names each `do:`/`assert:`
|
|
267
|
+
step that ran with the clock stopped (`! at 4000ms [measure apex]: the clock
|
|
268
|
+
was stopped …`, also `result.stalled`) — read that line before reading the
|
|
269
|
+
zeros. Measure before the game ends, or `restartScene` / read `Flow.state` in
|
|
270
|
+
the step.
|
|
271
|
+
|
|
255
272
|
Note: `validateScene`/`runScript` register node types and your `behaviors`
|
|
256
273
|
into the process-global registries (hot-replacing same names). In a test
|
|
257
274
|
suite, isolate with `clearRegistry()`/`clearBehaviors()` between files if
|
|
@@ -36,3 +36,11 @@
|
|
|
36
36
|
|
|
37
37
|
- Boss attack telegraphs (charge-up glow, ground slam particles).
|
|
38
38
|
- A hurt/attack clip for shades once one lands in the catalog.
|
|
39
|
+
|
|
40
|
+
## This round's surfaces, on the island (2026-09-04)
|
|
41
|
+
|
|
42
|
+
`Pad` (StaticBody3D, `restitution 1`, `snapToGround: 0.1`) with a dead `Ball` dropped on it,
|
|
43
|
+
and `Walker` (CharacterBody3D + `Chase` on a child, `slopeLimitDeg 50`, `stepHeight 0.5`) on
|
|
44
|
+
the west slope, `loseRange 25` so it never joins the quest. `verify.ts` measures the rebound
|
|
45
|
+
(2.4 m of 3), the chase up the slope (8.5 m of 10, +1.75 m) and that the walker stays on the
|
|
46
|
+
terrain (0.97 m over `heightAt`).
|
|
@@ -22,3 +22,6 @@ verify.ts audit + quest E2E + hunt + death/respawn + replay
|
|
|
22
22
|
keeps lit wards; `IsleDirector.onReady` re-applies their glow.
|
|
23
23
|
- The whole Terrain subtree is static EXCEPT Sea and Clouds (auditScene
|
|
24
24
|
catches the freeze-the-water mistake if you regress this).
|
|
25
|
+
|
|
26
|
+
- `Pad`, `Ball`, `Walker` — the harness's fixtures for a trampoline and a ground chaser on the
|
|
27
|
+
heightfield.
|
|
@@ -2759,6 +2759,142 @@
|
|
|
2759
2759
|
}
|
|
2760
2760
|
],
|
|
2761
2761
|
"uid": "n_0q1r3a6rijr3hc1w"
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
"name": "Pad",
|
|
2765
|
+
"type": "StaticBody3D",
|
|
2766
|
+
"uid": "n_uae445vafun14t1w",
|
|
2767
|
+
"props": {
|
|
2768
|
+
"position": [2, 0, 66],
|
|
2769
|
+
"snapToGround": 0.1,
|
|
2770
|
+
"collider": { "shape": "box", "size": [2, 0.2, 2] },
|
|
2771
|
+
"restitution": 1
|
|
2772
|
+
},
|
|
2773
|
+
"children": [
|
|
2774
|
+
{
|
|
2775
|
+
"name": "Skin",
|
|
2776
|
+
"type": "MeshInstance3D",
|
|
2777
|
+
"uid": "n_r5z35fqhlhntir5y",
|
|
2778
|
+
"props": {
|
|
2779
|
+
"mesh": "box",
|
|
2780
|
+
"size": [2, 0.2, 2],
|
|
2781
|
+
"castShadow": true,
|
|
2782
|
+
"material": { "color": "#3b82f6", "roughness": 0.7 }
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
]
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
"name": "Ball",
|
|
2789
|
+
"type": "RigidBody3D",
|
|
2790
|
+
"uid": "n_rbwioxy4v827q3a4",
|
|
2791
|
+
"props": { "position": [2, 60, 66], "collider": { "shape": "sphere", "radius": 0.3 } },
|
|
2792
|
+
"children": [
|
|
2793
|
+
{
|
|
2794
|
+
"name": "Skin",
|
|
2795
|
+
"type": "MeshInstance3D",
|
|
2796
|
+
"uid": "n_hy0cclrpbgge9wiy",
|
|
2797
|
+
"props": {
|
|
2798
|
+
"mesh": "sphere",
|
|
2799
|
+
"size": [0.3, 0.3, 0.3],
|
|
2800
|
+
"castShadow": true,
|
|
2801
|
+
"material": { "color": "#f4f1de" }
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
]
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
"name": "Walker",
|
|
2808
|
+
"type": "CharacterBody3D",
|
|
2809
|
+
"uid": "n_mjjx1odvbl668a8p",
|
|
2810
|
+
"props": {
|
|
2811
|
+
"position": [-92, 0, -64],
|
|
2812
|
+
"snapToGround": 0.95,
|
|
2813
|
+
"slopeLimitDeg": 50,
|
|
2814
|
+
"stepHeight": 0.5,
|
|
2815
|
+
"collider": { "shape": "capsule", "radius": 0.4, "height": 1.1 }
|
|
2816
|
+
},
|
|
2817
|
+
"children": [
|
|
2818
|
+
{
|
|
2819
|
+
"name": "AI",
|
|
2820
|
+
"type": "Node3D",
|
|
2821
|
+
"uid": "n_73h5b3fzsep3ksnf",
|
|
2822
|
+
"script": {
|
|
2823
|
+
"name": "Chase",
|
|
2824
|
+
"props": {
|
|
2825
|
+
"target": "/root/Player",
|
|
2826
|
+
"speed": 2.4,
|
|
2827
|
+
"stopRange": 1.5,
|
|
2828
|
+
"loseRange": 25,
|
|
2829
|
+
"moveParent": true
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
"name": "Skin",
|
|
2835
|
+
"type": "MeshInstance3D",
|
|
2836
|
+
"uid": "n_lt6ypdouri4cicwd",
|
|
2837
|
+
"props": {
|
|
2838
|
+
"mesh": "capsule",
|
|
2839
|
+
"size": [0.4, 1.1, 0.4],
|
|
2840
|
+
"castShadow": true,
|
|
2841
|
+
"material": { "color": "#6b4d8a", "roughness": 0.7 }
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
]
|
|
2845
|
+
},
|
|
2846
|
+
{
|
|
2847
|
+
"name": "Buoy",
|
|
2848
|
+
"type": "RigidBody3D",
|
|
2849
|
+
"uid": "n_y0kpfi62yas7ai09",
|
|
2850
|
+
"props": {
|
|
2851
|
+
"position": [-150, 9.6, 0],
|
|
2852
|
+
"snapToGround": false,
|
|
2853
|
+
"mass": 20,
|
|
2854
|
+
"collider": { "shape": "box", "size": [1, 1, 1] }
|
|
2855
|
+
},
|
|
2856
|
+
"script": {
|
|
2857
|
+
"name": "Buoyancy",
|
|
2858
|
+
"props": { "water": "/root/Terrain/Sea", "samples": 1, "drag": 0.1, "drift": 0 }
|
|
2859
|
+
},
|
|
2860
|
+
"children": [
|
|
2861
|
+
{
|
|
2862
|
+
"name": "Skin",
|
|
2863
|
+
"type": "MeshInstance3D",
|
|
2864
|
+
"uid": "n_tz2qooe24d5evtq2",
|
|
2865
|
+
"props": {
|
|
2866
|
+
"mesh": "box",
|
|
2867
|
+
"size": [1, 1, 1],
|
|
2868
|
+
"castShadow": true,
|
|
2869
|
+
"material": { "color": "#f59e0b", "roughness": 0.6 }
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
]
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
"name": "Crate",
|
|
2876
|
+
"type": "RigidBody3D",
|
|
2877
|
+
"uid": "n_x0on4nkgt4l83sic",
|
|
2878
|
+
"props": {
|
|
2879
|
+
"position": [-150, 9.6, 8],
|
|
2880
|
+
"snapToGround": false,
|
|
2881
|
+
"mass": 20,
|
|
2882
|
+
"collider": { "shape": "box", "size": [1, 1, 1] }
|
|
2883
|
+
},
|
|
2884
|
+
"script": { "name": "Buoyancy", "props": { "drift": 0 } },
|
|
2885
|
+
"children": [
|
|
2886
|
+
{
|
|
2887
|
+
"name": "Skin",
|
|
2888
|
+
"type": "MeshInstance3D",
|
|
2889
|
+
"uid": "n_u0ckada9zfkpv9zz",
|
|
2890
|
+
"props": {
|
|
2891
|
+
"mesh": "box",
|
|
2892
|
+
"size": [1, 1, 1],
|
|
2893
|
+
"castShadow": true,
|
|
2894
|
+
"material": { "color": "#8b5a2b", "roughness": 0.8 }
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
]
|
|
2762
2898
|
}
|
|
2763
2899
|
],
|
|
2764
2900
|
"uid": "n_nxftk1ep8wbi1bn9"
|
|
@@ -285,3 +285,147 @@ const ok = (label: string, cond: boolean): void => {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK');
|
|
288
|
+
|
|
289
|
+
// ---- HEIGHTFIELD: this round's surfaces, on terrain instead of a flat arena -----
|
|
290
|
+
// A trampoline pad snapped to the island (`snapToGround: 0.1` on a StaticBody3D
|
|
291
|
+
// with `restitution 1`), a dead ball dropped on it, and a `CharacterBody3D`
|
|
292
|
+
// walker chasing the player up the island's slope (`Chase.ground` auto: the
|
|
293
|
+
// KCC's slopeLimitDeg and stepHeight decide, gravity keeps it on the ground).
|
|
294
|
+
{
|
|
295
|
+
type Vec = { position: number[] };
|
|
296
|
+
type Terrain = { heightAt(x: number, z: number): number };
|
|
297
|
+
const ballY: number[] = [];
|
|
298
|
+
let walkerFrom: number[] = [];
|
|
299
|
+
let walkerTo: number[] = [];
|
|
300
|
+
let groundUnderWalker = 0;
|
|
301
|
+
let targetY = 0;
|
|
302
|
+
let closed = 0;
|
|
303
|
+
const result = await runScript(gameJson, {
|
|
304
|
+
behaviors: BEHAVIORS,
|
|
305
|
+
durationMs: 8000,
|
|
306
|
+
steps: [
|
|
307
|
+
{
|
|
308
|
+
atMs: 100,
|
|
309
|
+
do: (ctx) => {
|
|
310
|
+
const pad = ctx.getNode('/root/Pad') as unknown as Vec;
|
|
311
|
+
const ball = ctx.getNode('/root/Ball') as unknown as Vec;
|
|
312
|
+
ball.position = [pad.position[0] ?? 0, (pad.position[1] ?? 0) + 3, pad.position[2] ?? 0];
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
...Array.from({ length: 30 }, (_, i) => ({
|
|
316
|
+
atMs: 200 + i * 100,
|
|
317
|
+
do: (ctx: { getNode(p: string): unknown }) => {
|
|
318
|
+
ballY.push((ctx.getNode('/root/Ball') as Vec).position[1] ?? 0);
|
|
319
|
+
},
|
|
320
|
+
})),
|
|
321
|
+
{
|
|
322
|
+
atMs: 3300,
|
|
323
|
+
do: (ctx) => {
|
|
324
|
+
const terrain = ctx.getNode('/root/Terrain/Ground/Surface') as unknown as Terrain;
|
|
325
|
+
const walker = ctx.getNode('/root/Walker') as unknown as Vec;
|
|
326
|
+
const player = ctx.getNode('/root/Player') as unknown as Vec;
|
|
327
|
+
const [wx = 0, , wz = 0] = walker.position;
|
|
328
|
+
// the steepest way UP from the walker, 10 m out: that is where the player goes
|
|
329
|
+
let best = { x: wx + 10, z: wz, h: -Infinity };
|
|
330
|
+
for (let a = 0; a < 16; a++) {
|
|
331
|
+
const x = wx + 10 * Math.cos((a / 16) * Math.PI * 2);
|
|
332
|
+
const z = wz + 10 * Math.sin((a / 16) * Math.PI * 2);
|
|
333
|
+
const h = terrain.heightAt(x, z);
|
|
334
|
+
if (h > best.h) best = { x, z, h };
|
|
335
|
+
}
|
|
336
|
+
player.position = [best.x, best.h + 1.4, best.z];
|
|
337
|
+
targetY = best.h;
|
|
338
|
+
walkerFrom = [...walker.position];
|
|
339
|
+
const d0 = Math.hypot(best.x - wx, best.z - wz);
|
|
340
|
+
for (let i = 0; i < 240; i++) ctx.engine.step();
|
|
341
|
+
walkerTo = [...walker.position];
|
|
342
|
+
const [tx = 0, , tz = 0] = walkerTo;
|
|
343
|
+
groundUnderWalker = terrain.heightAt(tx, tz);
|
|
344
|
+
closed = d0 - Math.hypot(best.x - tx, best.z - tz);
|
|
345
|
+
player.position = [-4.5, 18.73, 70.5]; // and back to the spawn for the quest
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
ok('runScript reported no failures (HEIGHTFIELD path)', result.ok);
|
|
351
|
+
let touch = ballY.findIndex((y, i) => i > 0 && y > (ballY[i - 1] ?? 0));
|
|
352
|
+
if (touch < 1) touch = 1;
|
|
353
|
+
const low = ballY[touch - 1] ?? 0;
|
|
354
|
+
const rebound = Math.max(...ballY.slice(touch)) - low;
|
|
355
|
+
ok(
|
|
356
|
+
`a pad snapped to the island with restitution 1 launches a dead ball back up ${rebound.toFixed(2)} m of its 3 m drop`,
|
|
357
|
+
rebound > 1.8,
|
|
358
|
+
);
|
|
359
|
+
const rose = (walkerTo[1] ?? 0) - (walkerFrom[1] ?? 0);
|
|
360
|
+
const gap = targetY - (walkerFrom[1] ?? 0);
|
|
361
|
+
ok(
|
|
362
|
+
`the walker chased the player UP the slope: closed ${closed.toFixed(1)} m of 10, rose ${rose.toFixed(2)} m of a ${gap.toFixed(2)} m climb`,
|
|
363
|
+
closed > 5 && rose > gap * 0.5,
|
|
364
|
+
);
|
|
365
|
+
const hover = (walkerTo[1] ?? 0) - groundUnderWalker;
|
|
366
|
+
ok(
|
|
367
|
+
`…and stayed on the ground while doing it (capsule centre ${hover.toFixed(2)} m over the terrain, 0.95 expected)`,
|
|
368
|
+
hover > 0.5 && hover < 1.6,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- FLOATS: what `Buoyancy.water`, `samples` and `drag` do on the open sea --------
|
|
373
|
+
// Set by no example: every raft in the repo took the first Water3D, four samples
|
|
374
|
+
// and the default drag. Two crates float west of the island (the heightfield
|
|
375
|
+
// ends at x −120): the Buoy names its water, samples once and says `drag: 0.1`;
|
|
376
|
+
// the Crate keeps the defaults. Both `drift: 0`, so a 3 m/s shove is the only
|
|
377
|
+
// thing moving them and drag is the only thing that differs. Asserted by how
|
|
378
|
+
// far each travels in 2 s — e^{-drag·t} says 5.4 m against 2.8 m — and by both
|
|
379
|
+
// still sitting at the waterline afterwards.
|
|
380
|
+
{
|
|
381
|
+
type Vec = { position: number[] };
|
|
382
|
+
type Body = { mass: number; applyImpulse(i: [number, number, number]): void };
|
|
383
|
+
type Water = { heightAt(x: number, z: number): number };
|
|
384
|
+
let buoyRest = 0;
|
|
385
|
+
let crateRest = 0;
|
|
386
|
+
let buoyRun = 0;
|
|
387
|
+
let crateRun = 0;
|
|
388
|
+
let buoyOver = 99;
|
|
389
|
+
let crateOver = 99;
|
|
390
|
+
const result = await runScript(gameJson, {
|
|
391
|
+
behaviors: BEHAVIORS,
|
|
392
|
+
durationMs: 8000,
|
|
393
|
+
steps: [
|
|
394
|
+
{
|
|
395
|
+
atMs: 100,
|
|
396
|
+
do: (ctx) => {
|
|
397
|
+
const sea = ctx.getNode('/root/Terrain/Sea') as unknown as Water;
|
|
398
|
+
const buoy = ctx.getNode('/root/Buoy') as unknown as Vec & Body;
|
|
399
|
+
const crate = ctx.getNode('/root/Crate') as unknown as Vec & Body;
|
|
400
|
+
for (let i = 0; i < 240; i++) ctx.engine.step(); // 4 s to settle on the waterline
|
|
401
|
+
buoyRest = buoy.position[1] ?? 0;
|
|
402
|
+
crateRest = crate.position[1] ?? 0;
|
|
403
|
+
const bx = buoy.position[0] ?? 0;
|
|
404
|
+
const cx = crate.position[0] ?? 0;
|
|
405
|
+
buoy.applyImpulse([3 * buoy.mass, 0, 0]);
|
|
406
|
+
crate.applyImpulse([3 * crate.mass, 0, 0]);
|
|
407
|
+
for (let i = 0; i < 120; i++) ctx.engine.step();
|
|
408
|
+
buoyRun = (buoy.position[0] ?? 0) - bx;
|
|
409
|
+
crateRun = (crate.position[0] ?? 0) - cx;
|
|
410
|
+
buoyOver =
|
|
411
|
+
(buoy.position[1] ?? 0) - sea.heightAt(buoy.position[0] ?? 0, buoy.position[2] ?? 0);
|
|
412
|
+
crateOver =
|
|
413
|
+
(crate.position[1] ?? 0) - sea.heightAt(crate.position[0] ?? 0, crate.position[2] ?? 0);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
ok('runScript reported no failures (FLOATS path)', result.ok);
|
|
419
|
+
ok(
|
|
420
|
+
`water "/root/Terrain/Sea", samples 1: the Buoy settles at y ${buoyRest.toFixed(2)} (sea 9.19 + draft 0.35 ≈ 9.54), the Crate at ${crateRest.toFixed(2)}`,
|
|
421
|
+
Math.abs(buoyRest - 9.54) < 0.5 && Math.abs(crateRest - 9.54) < 0.5,
|
|
422
|
+
);
|
|
423
|
+
ok(
|
|
424
|
+
`drag 0.1 vs 0.9: the same 3 m/s shove carries the Buoy ${buoyRun.toFixed(2)} m and the Crate ${crateRun.toFixed(2)} m in 2 s (5.4 vs 2.8 expected)`,
|
|
425
|
+
buoyRun > crateRun * 1.5 && buoyRun > 4 && crateRun > 1.5 && crateRun < 3.5,
|
|
426
|
+
);
|
|
427
|
+
ok(
|
|
428
|
+
`…and both still ride the waterline (${buoyOver.toFixed(2)} m and ${crateOver.toFixed(2)} m over the sea)`,
|
|
429
|
+
Math.abs(buoyOver - 0.35) < 0.5 && Math.abs(crateOver - 0.35) < 0.5,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
@@ -58,3 +58,10 @@
|
|
|
58
58
|
- One hand-authored level — extend by copying nodes into the right GROUP
|
|
59
59
|
(`enemy`/`hazard`/`pit`/`checkpoint`/`platform`); `PlayerController` picks them
|
|
60
60
|
up with no extra wiring.
|
|
61
|
+
|
|
62
|
+
## The round's surfaces, in 2D (2026-09-04)
|
|
63
|
+
|
|
64
|
+
A course past GroundD (x 3000+): `Trampoline` (`restitution 1`) with a dead `Bouncer`, `Ice`
|
|
65
|
+
(`friction 0`) with a `Sled` at 260 px/s, and a `Hound` (`CharacterBody2D` + `Chase`
|
|
66
|
+
`ground: true`) under a `Perch`. `verify.ts` measures the rebound (287 of 300 px), the slide
|
|
67
|
+
(400 px of ice) and that the hound stays on the floor under a perched player.
|
|
@@ -75,3 +75,6 @@ The vibe-coding rules: read the skills, JSON structure, built-ins first, omit
|
|
|
75
75
|
uids, declared assets, y-down space, collider props, GROUP-DRIVEN centralised
|
|
76
76
|
damage (§8), deferred engine access (§11), debug draw, delta discipline, verify
|
|
77
77
|
like a user.
|
|
78
|
+
|
|
79
|
+
- `CourseFloor`, `Trampoline`, `Bouncer`, `Ice`, `Sled`, `Hound`, `Perch` — the harness's course for
|
|
80
|
+
the 2D surfaces and the ground chaser.
|