incanto 0.72.0 → 0.73.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-CAu-ucHg.js} +7 -7
- package/dist/{create-game-C9rWqqAC.js → create-game-CbTLFjis.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-BsFJM3v0.js} +277 -60
- package/dist/{gameplay-BlQe-M07.js → gameplay-CRte0OVf.js} +589 -35
- package/dist/gameplay.d.ts +136 -4
- 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-CovokK7s.js} +94 -11
- package/dist/{physics-3d-BzOCw1VS.js → physics-3d-BiK6Emwn.js} +202 -15
- package/dist/{picking-C9McVJrO.js → picking-Dtd8JInh.js} +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-C8HuRkOf.js → register-CY9QCCZs.js} +45 -9
- package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
- 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-D-dnVrFY.js} +1 -1
- package/dist/{test-Djuq3Vhq.js → test-DZmCFz0Q.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-GuZ5u5bi.js} +1 -1
- package/editor/assets/{debug-DLrQm-az.js → debug-BbxionO2.js} +1 -1
- package/editor/assets/{index-BEKuxYcE.js → index-CNlTzQbm.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 +174 -15
- package/skills/incanto-node-reference.md +122 -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
|
@@ -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.
|
|
@@ -1350,6 +1350,132 @@
|
|
|
1350
1350
|
}
|
|
1351
1351
|
],
|
|
1352
1352
|
"uid": "n_i3xs3gnb3cyrar1u"
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
"name": "CourseFloor",
|
|
1356
|
+
"type": "StaticBody2D",
|
|
1357
|
+
"uid": "n_9p55c0wu33vbe194",
|
|
1358
|
+
"props": { "position": [3300, 520], "collider": { "shape": "rect", "size": [900, 120] } },
|
|
1359
|
+
"children": [
|
|
1360
|
+
{
|
|
1361
|
+
"name": "Skin",
|
|
1362
|
+
"type": "ColorRect2D",
|
|
1363
|
+
"uid": "n_56chxbokw5okl2ar",
|
|
1364
|
+
"props": { "size": [900, 120], "color": "#6b5638", "anchor": [0.5, 0.5] }
|
|
1365
|
+
}
|
|
1366
|
+
]
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
"name": "Trampoline",
|
|
1370
|
+
"type": "StaticBody2D",
|
|
1371
|
+
"uid": "n_vqjbknb54e0zq787",
|
|
1372
|
+
"props": {
|
|
1373
|
+
"position": [3000, 455],
|
|
1374
|
+
"collider": { "shape": "rect", "size": [120, 10] },
|
|
1375
|
+
"restitution": 1
|
|
1376
|
+
},
|
|
1377
|
+
"children": [
|
|
1378
|
+
{
|
|
1379
|
+
"name": "Skin",
|
|
1380
|
+
"type": "ColorRect2D",
|
|
1381
|
+
"uid": "n_bed7i5blyg8p63ij",
|
|
1382
|
+
"props": { "size": [120, 10], "color": "#3b82f6", "anchor": [0.5, 0.5] }
|
|
1383
|
+
}
|
|
1384
|
+
]
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
"name": "Bouncer",
|
|
1388
|
+
"type": "RigidBody2D",
|
|
1389
|
+
"uid": "n_8vd73zp82idtm5gx",
|
|
1390
|
+
"props": { "position": [3000, 150], "collider": { "shape": "circle", "radius": 14 } },
|
|
1391
|
+
"children": [
|
|
1392
|
+
{
|
|
1393
|
+
"name": "Skin",
|
|
1394
|
+
"type": "ColorRect2D",
|
|
1395
|
+
"uid": "n_n18pdy731ltnmb3l",
|
|
1396
|
+
"props": { "size": [28, 28], "color": "#f4f1de", "anchor": [0.5, 0.5] }
|
|
1397
|
+
}
|
|
1398
|
+
]
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
"name": "Ice",
|
|
1402
|
+
"type": "StaticBody2D",
|
|
1403
|
+
"uid": "n_zlp89ikkls0xkbwn",
|
|
1404
|
+
"props": {
|
|
1405
|
+
"position": [3400, 455],
|
|
1406
|
+
"collider": { "shape": "rect", "size": [400, 10] },
|
|
1407
|
+
"friction": 0
|
|
1408
|
+
},
|
|
1409
|
+
"children": [
|
|
1410
|
+
{
|
|
1411
|
+
"name": "Skin",
|
|
1412
|
+
"type": "ColorRect2D",
|
|
1413
|
+
"uid": "n_opic7894wramqynx",
|
|
1414
|
+
"props": { "size": [400, 10], "color": "#bfe9ff", "anchor": [0.5, 0.5] }
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
"name": "Sled",
|
|
1420
|
+
"type": "RigidBody2D",
|
|
1421
|
+
"uid": "n_nhcrprd8no2pzb9i",
|
|
1422
|
+
"props": {
|
|
1423
|
+
"position": [3215, 435],
|
|
1424
|
+
"collider": { "shape": "circle", "radius": 14 },
|
|
1425
|
+
"linearVelocity": [260, 0]
|
|
1426
|
+
},
|
|
1427
|
+
"children": [
|
|
1428
|
+
{
|
|
1429
|
+
"name": "Skin",
|
|
1430
|
+
"type": "ColorRect2D",
|
|
1431
|
+
"uid": "n_y2owjzo7khqchken",
|
|
1432
|
+
"props": { "size": [28, 28], "color": "#8c6a3f", "anchor": [0.5, 0.5] }
|
|
1433
|
+
}
|
|
1434
|
+
]
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"name": "Hound",
|
|
1438
|
+
"type": "CharacterBody2D",
|
|
1439
|
+
"uid": "n_kmnt12lx070t311c",
|
|
1440
|
+
"props": { "position": [3650, 440], "collider": { "shape": "rect", "size": [30, 40] } },
|
|
1441
|
+
"children": [
|
|
1442
|
+
{
|
|
1443
|
+
"name": "AI",
|
|
1444
|
+
"type": "Node2D",
|
|
1445
|
+
"uid": "n_juz8aw6j311cstbs",
|
|
1446
|
+
"script": {
|
|
1447
|
+
"name": "Chase",
|
|
1448
|
+
"props": {
|
|
1449
|
+
"target": "/root/Player",
|
|
1450
|
+
"speed": 160,
|
|
1451
|
+
"stopRange": 24,
|
|
1452
|
+
"loseRange": 500,
|
|
1453
|
+
"moveParent": true,
|
|
1454
|
+
"ground": true
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
"name": "Skin",
|
|
1460
|
+
"type": "ColorRect2D",
|
|
1461
|
+
"uid": "n_k5h72ttrivo8vy0m",
|
|
1462
|
+
"props": { "size": [30, 40], "color": "#6b4d8a", "anchor": [0.5, 0.5] }
|
|
1463
|
+
}
|
|
1464
|
+
]
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
"name": "Perch",
|
|
1468
|
+
"type": "StaticBody2D",
|
|
1469
|
+
"uid": "n_t6vi97e88isgglef",
|
|
1470
|
+
"props": { "position": [3650, 300], "collider": { "shape": "rect", "size": [160, 20] } },
|
|
1471
|
+
"children": [
|
|
1472
|
+
{
|
|
1473
|
+
"name": "Skin",
|
|
1474
|
+
"type": "ColorRect2D",
|
|
1475
|
+
"uid": "n_m7qcxz6z1sc6rfbv",
|
|
1476
|
+
"props": { "size": [160, 20], "color": "#9aa3bd", "anchor": [0.5, 0.5] }
|
|
1477
|
+
}
|
|
1478
|
+
]
|
|
1353
1479
|
}
|
|
1354
1480
|
],
|
|
1355
1481
|
"uid": "n_d9t51gs0vhpnp2jj"
|
|
@@ -273,3 +273,57 @@ interface Player {
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK — Castle Run plays end to end');
|
|
276
|
+
|
|
277
|
+
// ---- TWIN: this round's 3D surfaces, in two dimensions ----------------------------
|
|
278
|
+
// StaticBody2D.restitution/friction (#1295) and Chase.ground (#1297) were built
|
|
279
|
+
// with the 3D case in hand and 2D tests beside it; a course past the last
|
|
280
|
+
// ground slab composes them in the 2D flagship and measures each. y is DOWN.
|
|
281
|
+
{
|
|
282
|
+
type Body = { position: number[] };
|
|
283
|
+
const bouncerY: number[] = [];
|
|
284
|
+
let sledX = 0;
|
|
285
|
+
let houndY = 0;
|
|
286
|
+
let houndX = 0;
|
|
287
|
+
const result = await runScript(sceneJson, {
|
|
288
|
+
behaviors,
|
|
289
|
+
durationMs: 4200,
|
|
290
|
+
steps: [
|
|
291
|
+
...Array.from({ length: 30 }, (_, i) => ({
|
|
292
|
+
atMs: 100 + i * 100,
|
|
293
|
+
do: (ctx: { getNode(p: string): unknown }) => {
|
|
294
|
+
bouncerY.push((ctx.getNode('Bouncer') as Body).position[1] ?? 0);
|
|
295
|
+
},
|
|
296
|
+
})),
|
|
297
|
+
{
|
|
298
|
+
atMs: 3100,
|
|
299
|
+
do: (ctx) => {
|
|
300
|
+
sledX = (ctx.getNode('Sled') as unknown as Body).position[0] ?? 0;
|
|
301
|
+
// the player on the perch above the hound: a ground chaser closes in below, never up
|
|
302
|
+
const player = ctx.getNode('Player') as unknown as Body;
|
|
303
|
+
player.position = [3650, 260];
|
|
304
|
+
for (let i = 0; i < 120; i++) ctx.engine.step();
|
|
305
|
+
const hound = ctx.getNode('Hound') as unknown as Body;
|
|
306
|
+
houndX = hound.position[0] ?? 0;
|
|
307
|
+
houndY = hound.position[1] ?? 0;
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
ok('runScript reported no failures (TWIN path)', result.ok);
|
|
313
|
+
let touch = bouncerY.findIndex((y, i) => i > 0 && y < (bouncerY[i - 1] ?? 0));
|
|
314
|
+
if (touch < 1) touch = 1;
|
|
315
|
+
const low = bouncerY[touch - 1] ?? 0;
|
|
316
|
+
const rebound = low - Math.min(...bouncerY.slice(touch));
|
|
317
|
+
ok(
|
|
318
|
+
`StaticBody2D restitution 1: a dead ball climbs back ${rebound.toFixed(0)} px of its 300 px drop`,
|
|
319
|
+
rebound > 180,
|
|
320
|
+
);
|
|
321
|
+
ok(
|
|
322
|
+
`StaticBody2D friction 0: the sled slides to x ${sledX.toFixed(0)} across the ice (from 3215)`,
|
|
323
|
+
sledX > 3450,
|
|
324
|
+
);
|
|
325
|
+
ok(
|
|
326
|
+
`Chase.ground: the hound stays on the floor (y ${houndY.toFixed(0)}, floor 440) under a player on the perch, ${Math.abs(houndX - 3650).toFixed(0)} px off its x`,
|
|
327
|
+
houndY > 400 && Math.abs(houndX - 3650) < 60,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"Vanguard" — a 3D THIRD-PERSON shooter. The camera rides over your soldier's
|
|
6
6
|
shoulder (you SEE your character); the crosshair is where the hitscan rifle
|
|
7
7
|
fires. Move with WASD, look with the mouse (click locks the pointer), Shift to
|
|
8
|
-
sprint, Space to jump, click / F to fire, R to reload, Enter to restart. Survive and clear three
|
|
8
|
+
sprint, Space to jump (tap for a short hop, hold for the full jump, press again in the air for a second), click / F to fire, R to reload, Enter to restart. Survive and clear three
|
|
9
9
|
escalating waves of chasing enemies to **SECURE THE AREA**; let them drain your
|
|
10
10
|
HP to 0 and the **MISSION FAILS**.
|
|
11
11
|
|