incanto 0.68.0 → 0.70.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/assets/catalog.json +9 -5
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +19 -5
- package/bin/incanto-check.mjs +92 -15
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +128 -5
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +165 -44
- package/bin/incanto.mjs +4 -2
- package/dist/2d.d.ts +285 -36
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +158 -10
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-DaMxqfNE.d.ts} +33 -14
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-DZExDn9o.d.ts} +809 -44
- package/dist/{create-game-IZIydDwI.js → create-game-Bwvh6q8A.js} +148 -61
- package/dist/{create-game-DbWtVTxD.js → create-game-C7ffQWW7.js} +103 -49
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1066 -864
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-D6Q5BxeE.js} +299 -46
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-DM1eu_cV.js → gameplay-BfHkuzVb.js} +825 -221
- package/dist/gameplay.d.ts +187 -7
- package/dist/gameplay.js +1 -1
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +156 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-CcB533FR.d.ts → loader-Cff09LMm.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CE0Qvy3V.js} +136 -11
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-CpH-2gn5.js} +104 -23
- package/dist/{teardown-D2NEmxPB.js → picking-CQJ_PJKh.js} +106 -14
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-3ta-2Xig.js → register-6DYnKZcy.js} +652 -831
- package/dist/{register-ibCjm-wH.js → register-Bkk0wSDB.js} +348 -30
- package/dist/{replay-CYvhVHHN.js → replay-DjAkAzMq.js} +224 -14
- package/dist/{replay-Dvn8aeBd.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-BC4PNtJX.js → save-slots-BXVg148r.js} +4558 -2294
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
- package/dist/{split-screen-CYwDkbLF.js → split-screen-PL78oVXP.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-D7RIqXYF.js → src-Cxfiv1Hg.js} +2 -17
- package/dist/test-iHYVUcDK.js +4036 -0
- package/dist/test.d.ts +542 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +54 -3
- package/dist/vite.js +349 -17
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-C8aDbGmR.js} +56 -1
- package/editor/assets/agent8-D0MS174y.js +1 -0
- package/editor/assets/{debug-BBhKuBNV.js → debug-BnXkKuYu.js} +2 -2
- package/editor/assets/index-CIu3uc3l.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +7 -16
- package/schemas/scene.schema.json +30 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +25 -3
- package/skills/incanto-audio.md +27 -5
- package/skills/incanto-behaviors-and-scripts.md +83 -6
- package/skills/incanto-building-2d-games.md +106 -8
- package/skills/incanto-building-3d-games.md +118 -6
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +121 -14
- package/skills/incanto-hud.md +128 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +222 -58
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +78 -2
- package/skills/incanto-save-slots.md +188 -6
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +292 -12
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +5 -2
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
- package/templates-app/beacon-isle-3d/src/main.ts +24 -4
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1255 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +143 -625
- package/templates-app/platformer-2d/src/main.ts +35 -13
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +41 -195
- package/templates-app/star-survivor/src/main.ts +28 -7
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +78 -217
- package/templates-app/tps-3d/src/main.ts +39 -17
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
- package/templates-app/village-quest-3d/src/main.ts +24 -4
- package/templates-app/village-quest-3d/src/village.scene.json +199 -838
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-IWIqk0HJ.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-8hoHeRmo.js +0 -2340
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BrrHOjMJ.js +0 -1
- package/editor/assets/index-eVd0BToA.js +0 -10958
|
@@ -81,7 +81,7 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
|
|
|
81
81
|
### `MeshInstance3D`
|
|
82
82
|
| Prop | Default | Notes |
|
|
83
83
|
|---|---|---|
|
|
84
|
-
| `mesh` | `"box"` | `box \| sphere \| capsule \| plane \| cylinder \| gem` — `gem` is a faceted crystal (icosahedron); with `material.flatShading: true` + low `roughness` it reads as a sparkling jewel (spin/tumble it for the sparkle) |
|
|
84
|
+
| `mesh` | `"box"` | `box \| sphere \| capsule \| plane \| cylinder \| gem` — `gem` is a faceted crystal (icosahedron); with `material.flatShading: true` + low `roughness` it reads as a sparkling jewel (spin/tumble it for the sparkle). **This list is the whole list**: for a CURVED hull — a car body, a boat, a canopy, a fuselage — there is no primitive, and stacking boxes never gives you one. Use `LoftMesh3D` (below) |
|
|
85
85
|
| `size` | `[1,1,1]` | box: extents · sphere/gem: radius = x · capsule: radius = x, height = y · plane: x·z ground (laid FLAT on XZ) · cylinder: radius = x, height = y |
|
|
86
86
|
| `material` | `{}` | `{color: '#hex', metalness: 0..1, roughness: 0..1, opacity: 0..1, clearcoat: 0..1, clearcoatRoughness: 0..1, envMapIntensity: >=0, wireframe, flatShading, depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false, depthTest/Write true). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `depthTest: false` (+ a high `renderOrder`) makes a flat `plane`/`cylinder` an ALWAYS-ON-TOP ground decal — AoE telegraphs, selection/spawn rings — that never z-fights with or is hidden by bumpy terrain (`depthWrite: false` keeps it from occluding later effects). `map`/`normalMap` are texture URLs (lazy-loaded, headless-safe; map samples sRGB, normalMap linear), tiled `repeat` times across each face — box/plane UVs span 0..1 per face, so for worldspace density use `repeat: [length/tile, height/tile]` (e.g. a 6×2.5 m brick wall at one tile per 2 m → `[3, 1.25]`). `color` tints the map (near-white keeps the texture's own color). CAR PAINT: `clearcoat: 1` adds a glossy lacquer layer with its own reflection over a metallic base (`clearcoatRoughness` ~0.05-0.1 = mirror lacquer) — THE premium vehicle-paint look; `envMapIntensity` scales how strongly the surface mirrors the scene's sky/HDRI (glass windows want ~2, matte surfaces <1; needs a `sky`/`hdri` environment to reflect — at night with no sky there is nothing to mirror). NB: under ACES a very bright `emissiveIntensity` blows out to white — keep it modest (~1–2) + a saturated color for a vivid GLOWING look. Unknown keys, malformed `repeat`, or `repeat` without a map hard-fail at load |
|
|
87
87
|
| `castShadow` / `receiveShadow` | `false` | |
|
|
@@ -270,10 +270,43 @@ and `Fill.position = [-(1 - frac) / 2, 0, 0.01]`. The offsets stay local to the
|
|
|
270
270
|
(rotated) bar, so the drain always hugs the bar's left edge.
|
|
271
271
|
|
|
272
272
|
### `Camera3D`
|
|
273
|
-
`fov: 60`, `near: 0.1`, `far: 1000`, `current: false`.
|
|
273
|
+
`fov: 60`, `near: 0.1`, `far: 1000`, `current: false`, `lookAt: ""`.
|
|
274
274
|
Mark exactly ONE camera `current: true` (otherwise the first camera in tree order is used).
|
|
275
|
-
Aspect ratio is automatic.
|
|
276
|
-
|
|
275
|
+
Aspect ratio is automatic.
|
|
276
|
+
|
|
277
|
+
**Point it with `lookAt`, not with a `rotation` you worked out.**
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{ "name": "Cam", "type": "Camera3D",
|
|
281
|
+
"props": { "current": true, "position": [7, 6, 9], "lookAt": "%Player" } }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`lookAt` is a node path, re-aimed every frame, so it also tracks a target that
|
|
285
|
+
moves. It orients with WORLD UP, which is the part you cannot write yourself in
|
|
286
|
+
scene JSON — and cannot write in game code either without importing three, which
|
|
287
|
+
this engine's own guardrail forbids ("mutate node props — never touch three.js
|
|
288
|
+
objects").
|
|
289
|
+
|
|
290
|
+
This is not a convenience. A hand-written `rotation` for a camera is
|
|
291
|
+
**wrong at every heading but one**, and the way it is wrong is invisible from
|
|
292
|
+
inside the frame:
|
|
293
|
+
|
|
294
|
+
- The one published direction→rotation formula (`incanto-3d-character.md`, "the
|
|
295
|
+
+Z-FORWARD rule", *"There is ONE facing formula"*) is derived for a SKIN — a rod,
|
|
296
|
+
which has no meaningful up. Measured over 72 look headings, applying it to a
|
|
297
|
+
camera aims perfectly (dot 1.000 in all 72) and puts the world **upside down in
|
|
298
|
+
36 of them**. Its own published check, `rodAxis · (target − eye) ≈ 1`, scores
|
|
299
|
+
1.000 on every upside-down one.
|
|
300
|
+
- **No two-angle recipe can be right.** `rotation` is Euler XYZ, so
|
|
301
|
+
`[pitch, yaw, 0]` is Rx·Ry while a level camera is Ry·Rx: an upright camera's
|
|
302
|
+
third angle is NON-ZERO. The `[-26, 36, 0]` this document recommended for years
|
|
303
|
+
aims at 0.9924 and tilts the horizon **16°**; the level answer is
|
|
304
|
+
`[-33.69, 32.91, 19.91]`, which nobody would write by hand.
|
|
305
|
+
- A camera looks down its own local **−Z**, unlike every skin in the engine.
|
|
306
|
+
|
|
307
|
+
`framing()` now prints `up [x, y, z]` and the roll in degrees, so a tilted or
|
|
308
|
+
inverted camera is something you can SEE in a report rather than something you
|
|
309
|
+
notice in a screenshot.
|
|
277
310
|
|
|
278
311
|
### Lights
|
|
279
312
|
- `DirectionalLight3D` — `color: '#ffffff'`, `intensity: 1`, `castShadow: false`.
|
|
@@ -320,8 +353,66 @@ atmosphere recipe below).
|
|
|
320
353
|
|
|
321
354
|
## Reference example
|
|
322
355
|
|
|
323
|
-
|
|
324
|
-
|
|
356
|
+
The smallest 3D scene that renders something you can see. Every line matters:
|
|
357
|
+
without the light it is black, without `current: true` the camera is a guess,
|
|
358
|
+
and without `ambient` the unlit faces are pitch dark.
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"format": 1,
|
|
363
|
+
"type": "scene",
|
|
364
|
+
"dimension": "3d",
|
|
365
|
+
"name": "Hello",
|
|
366
|
+
"environment": {
|
|
367
|
+
"sky": { "type": "atmosphere", "elevationDeg": 35, "azimuthDeg": 140 },
|
|
368
|
+
"ambient": { "color": "#ffffff", "intensity": 0.2 },
|
|
369
|
+
"shadows": true
|
|
370
|
+
},
|
|
371
|
+
"root": {
|
|
372
|
+
"name": "World",
|
|
373
|
+
"type": "Node3D",
|
|
374
|
+
"children": [
|
|
375
|
+
{ "name": "Sun", "type": "DirectionalLight3D",
|
|
376
|
+
"props": { "position": [6, 10, 4], "intensity": 2, "castShadow": true } },
|
|
377
|
+
{ "name": "Camera", "type": "Camera3D",
|
|
378
|
+
"props": { "position": [7, 6, 9], "rotation": [-26, 36, 0], "current": true } },
|
|
379
|
+
{ "name": "Ground", "type": "MeshInstance3D",
|
|
380
|
+
"props": { "mesh": "plane", "size": [40, 1, 40], "receiveShadow": true,
|
|
381
|
+
"material": { "color": "#6b8f3a", "roughness": 1 } } },
|
|
382
|
+
{ "name": "Pivot", "type": "Node3D", "script": { "name": "Spin" },
|
|
383
|
+
"children": [
|
|
384
|
+
{ "name": "Cube", "type": "MeshInstance3D",
|
|
385
|
+
"props": { "mesh": "box", "position": [2, 0.5, 0], "castShadow": true,
|
|
386
|
+
"material": { "color": "#e0563f", "roughness": 0.4 } } },
|
|
387
|
+
{ "name": "Ball", "type": "MeshInstance3D",
|
|
388
|
+
"props": { "mesh": "sphere", "size": [0.6, 0.6, 0.6], "position": [-2, 0.6, 0],
|
|
389
|
+
"castShadow": true,
|
|
390
|
+
"material": { "color": "#4f8fe0", "metalness": 0.6, "roughness": 0.2 } } },
|
|
391
|
+
{ "name": "Gem", "type": "MeshInstance3D",
|
|
392
|
+
"props": { "mesh": "gem", "size": [0.5, 0.5, 0.5], "position": [0, 0.8, 2],
|
|
393
|
+
"castShadow": true,
|
|
394
|
+
"material": { "color": "#ffd166", "flatShading": true, "roughness": 0.1 } } }
|
|
395
|
+
] }
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Rotating the PIVOT orbits all three children — no maths in the behaviour:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { Behavior } from 'incanto';
|
|
405
|
+
|
|
406
|
+
export class Spin extends Behavior {
|
|
407
|
+
override update(dt: number): void {
|
|
408
|
+
const node = this.node as unknown as { rotation: number[] };
|
|
409
|
+
node.rotation = [0, (node.rotation[1] ?? 0) + 40 * dt, 0];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Check it before you open a browser: `bunx incanto-check` says the scene is legal
|
|
415
|
+
and lit, and `bunx incanto-verify` walks the whole ladder.
|
|
325
416
|
|
|
326
417
|
## Atmosphere (sky, fog, shadows, exposure)
|
|
327
418
|
|
|
@@ -466,6 +557,13 @@ for (const o of off) console.error(` ${o.path}: ${o.gap > 0 ? 'floats' : 'burie
|
|
|
466
557
|
Put it in your `verify.ts` next to `auditScene`. It costs nothing and it is the
|
|
467
558
|
difference between shipping a floating bridge and not.
|
|
468
559
|
|
|
560
|
+
**Mark what is not meant to touch the ground.** `snapToGround: false` is the
|
|
561
|
+
escape and it INHERITS, so one mark exempts a whole assembly: a cloud made of
|
|
562
|
+
puffs, a palm made of trunk segments, a lamp at the top of a lighthouse, a rock
|
|
563
|
+
sunk into the slope on purpose. Put it on the group, not on its parts — before
|
|
564
|
+
0.73 it read the node's own prop only, and this repo's own flagship template
|
|
565
|
+
reported 64 floating cloud puffs.
|
|
566
|
+
|
|
469
567
|
## The loading screen has to cover the WHOLE boot
|
|
470
568
|
|
|
471
569
|
Downloading the assets is the first half. Building a dressed scene and
|
|
@@ -511,6 +609,20 @@ previous explosion across the level) as
|
|
|
511
609
|
point sprites in meters: preset distances auto-scale ÷100 (presets are authored
|
|
512
610
|
in 2D px). See incanto-building-2d-games for the full prop list.
|
|
513
611
|
|
|
612
|
+
**Firing one from a behaviour is `replay()`** — move the emitter, then replay:
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
const dust = this.node.getNode('Dust') as Particles3D;
|
|
616
|
+
dust.position = [x, 0.2, z];
|
|
617
|
+
dust.replay(); // `emitting: false` stays false; this is the game asking
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Not `play`, not `burst`, not `emit` (that one belongs to signals). The 2D skill
|
|
621
|
+
has always named it and this one did not, so a 3D game built from this page
|
|
622
|
+
alone had a one-shot it could not fire. `particles()` is the same node's test
|
|
623
|
+
hook — the live particle list, for a harness asking whether anything came out.
|
|
624
|
+
`engine.effects.countOf('explosion')` is the cheaper question.
|
|
625
|
+
|
|
514
626
|
Points render through a built-in soft sprite with a SOLID opaque core (not hard
|
|
515
627
|
squares), so colours read even at small sizes, and `'normal'` blend fades cleanly
|
|
516
628
|
to transparent (per-particle RGBA alpha). `paletteColors` (per-particle
|
package/skills/incanto-editor.md
CHANGED
|
@@ -18,8 +18,20 @@ bunx incanto-editor level.scene.json --output build/level.scene.json --port 5179
|
|
|
18
18
|
|
|
19
19
|
**Project mode** (no file argument — the usual way) scans the current directory for
|
|
20
20
|
`*.scene.json` (skipping node_modules/dist/hidden dirs): one scene auto-opens; several
|
|
21
|
-
open the scene browser.
|
|
22
|
-
|
|
21
|
+
open the scene browser.
|
|
22
|
+
|
|
23
|
+
**What the server will and will not touch**, since the browser lists every
|
|
24
|
+
`.json` and the answer comes from the server:
|
|
25
|
+
|
|
26
|
+
| | |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| reads | any `.json` **under the launch directory** — but only sends it if it IS a scene; anything else comes back with the reason and its contents never leave |
|
|
29
|
+
| writes | a scene, over a file that is already a scene, or to a file that does not exist yet |
|
|
30
|
+
| refuses | a path outside the project, a non-scene body, and **overwriting a file that is not a scene** — `?file=package.json` gets a 409 naming what it found |
|
|
31
|
+
|
|
32
|
+
That last row used to be missing: the write path checked the body and never what
|
|
33
|
+
it was about to land on, so one query parameter could replace a manifest with a
|
|
34
|
+
scene. The GET on the same endpoint already refused it.
|
|
23
35
|
|
|
24
36
|
### The scene browser (`scenes`)
|
|
25
37
|
|
|
@@ -108,7 +120,13 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
108
120
|
changed. A prop is rewritten only if its type declares `nodePath: true`, so a
|
|
109
121
|
text prop that merely reads "Player" is never touched. `+` adds a child of any registered
|
|
110
122
|
type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
|
|
111
|
-
(dimension, gravity with real inputs, environment/input/assets/multiplayer
|
|
123
|
+
(dimension, gravity with real inputs, environment/input/assets/multiplayer),
|
|
124
|
+
including a **STRINGS table** — one row per `@t:` key, one column per
|
|
125
|
+
language. It shows the two things raw JSON cannot: an untranslated cell shows
|
|
126
|
+
the English it will silently fall back to (greyed, because that fallback is
|
|
127
|
+
the design), and a key the BASE locale is missing is marked in red, because
|
|
128
|
+
that one has nothing to fall back to and paints the raw key on screen.
|
|
129
|
+
`+ key`, `+ language`, and deleting a key that a prop still says asks first.
|
|
112
130
|
- **CONNECTIONS on the selected node** — the inspector's last section shows what
|
|
113
131
|
the node **emits** and what it **receives**, each row being signal → target →
|
|
114
132
|
handler with a ✕ to remove and `+ connect` to add. Signals come from the node
|
|
@@ -116,8 +134,12 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
116
134
|
node in the scene; the handler is a dropdown of real methods when the target
|
|
117
135
|
has no script, and free text when it does (those handlers live in your
|
|
118
136
|
TypeScript, which the editor never loads). An end that no longer resolves is
|
|
119
|
-
marked red with the reason. For what a row cannot express — `filter
|
|
120
|
-
|
|
137
|
+
marked red with the reason. For what a row cannot express — `filter` and `once`
|
|
138
|
+
— the scene header still has `connections` as raw JSON. (It used to say "bound
|
|
139
|
+
args" as well: a connection has never had them. Its keys are exactly
|
|
140
|
+
`[signal, from, to, handler, once, filter]`, and since they became a closed
|
|
141
|
+
set an invented one is a load error rather than a key that quietly does
|
|
142
|
+
nothing.)
|
|
121
143
|
**Validation now runs WITH connections**: the editor used to strip them before
|
|
122
144
|
loading, so it reported green for a scene the game hard-fails to open.
|
|
123
145
|
- **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the
|
|
@@ -167,8 +189,11 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
|
|
|
167
189
|
`invulnerableFor`/`freeOnDeath` with their defaults; a `Patrol`'s `mode` is a
|
|
168
190
|
`loop`/`pingpong` menu), delta-only like every other field. A name that is NOT
|
|
169
191
|
built in is your game's TypeScript: it keeps the raw-JSON props box and says
|
|
170
|
-
so, with copy-paste Behavior boilerplate in its help —
|
|
171
|
-
|
|
192
|
+
so, with copy-paste Behavior boilerplate in its help — `groups` (name chips)
|
|
193
|
+
and `tags` (the node's free-form identity JSON: what
|
|
194
|
+
`connections[].filter.tag` matches on, and where a chart, a card or a loot
|
|
195
|
+
entry lives; a value the loader would reject is marked invalid rather than
|
|
196
|
+
written). Every one has a `?` help popover with examples. Values equal to the
|
|
172
197
|
default are removed (delta-only, like the serializer).
|
|
173
198
|
**A prop that holds a NODE PATH** (`Chase.target`, `Camera2D.follow`,
|
|
174
199
|
`Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain`…) offers every node
|
|
@@ -358,6 +383,20 @@ editing wherever the simulation happened to be. Games hosting the editor
|
|
|
358
383
|
themselves (`createGame3D({ editor: … })`) get a **save** button only when they
|
|
359
384
|
pass `editor.save` — otherwise edits live as long as the session does.
|
|
360
385
|
|
|
386
|
+
**The handle you booted with does not survive the round trip.** ▶ builds a NEW
|
|
387
|
+
game, so after one ✎→▶ the `game` your app is holding refers to something that
|
|
388
|
+
no longer exists. It is inert rather than destructive — a stale `dispose()` and
|
|
389
|
+
a stale `openEditor()` both do nothing — but if your app keeps using the handle,
|
|
390
|
+
take the live one:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
let game = await createGame3D({
|
|
394
|
+
scene, canvas,
|
|
395
|
+
debug: DEBUG,
|
|
396
|
+
editor: DEBUG && { library: true, onReboot: (next) => { game = next; } },
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
361
400
|
## Animating a model from the inspector
|
|
362
401
|
|
|
363
402
|
Select a `ModelInstance3D` and everything about what it is DOING sits in one
|
|
@@ -201,6 +201,24 @@ template). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
|
|
|
201
201
|
voxel `generateIsland` (radial dome falloff, sandy shore) also remains for
|
|
202
202
|
the same use case.
|
|
203
203
|
|
|
204
|
+
**Digging and building** is the node's own API, and for a long time none of it
|
|
205
|
+
was written down here — the whole point of a voxel world is that it changes:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const world = this.node.getNode('World') as VoxelGrid3D;
|
|
209
|
+
world.tileAt(x, y, z); // the tile there, or undefined
|
|
210
|
+
world.removeBlockAt(x, y, z); // dig — true if there was one
|
|
211
|
+
world.addBlock({ x, y, z, tile: 3 });
|
|
212
|
+
world.setBlocks(blocks); // replace the lot (emits `blocksChanged`)
|
|
213
|
+
world.blockCount(); // how many are standing
|
|
214
|
+
world.blocks(); // every one of them, to save or inspect
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Grid coordinates, not world ones — multiply by the node's `blockSize` to place
|
|
218
|
+
something on a block. `TileMap2D` answers the same questions in two dimensions
|
|
219
|
+
(`cellAt`/`tileAt`/`worldAt`/`setTile`), and that one takes WORLD coordinates
|
|
220
|
+
because a flat world has a cursor in it.
|
|
221
|
+
|
|
204
222
|
## Library
|
|
205
223
|
|
|
206
224
|
```ts
|
|
@@ -609,7 +627,7 @@ is radians of `atan(|∇h|)`. Default textures stream from the live agent8 CDN
|
|
|
609
627
|
| `theme` | `"island"` | see table — `custom` requires `layers` |
|
|
610
628
|
| `roughness` | `0.5` | detail-octave persistence (higher = more rugged) |
|
|
611
629
|
| `detail` | `4` | detail octave count |
|
|
612
|
-
| `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` |
|
|
630
|
+
| `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` — a fraction of the SCALE, not of the peak, so the default plateau lands around 18% of the realized height (measured: maxHeight 28 → band at 26.6, peak 150). Read `heightAt` before choosing one |
|
|
613
631
|
| `islandEdge` | `null` | `null` = theme decides; force the rim wrap on/off with a boolean |
|
|
614
632
|
| `layers` | `[]` | custom splat layers (theme `custom` only) |
|
|
615
633
|
| `textureBase` | agent8 CDN | base URL: `<base>/<name>.png` + `<base>/<name>_normal.png` |
|
|
@@ -14,6 +14,7 @@ bunx incanto-feel src/game.scene.json
|
|
|
14
14
|
```
|
|
15
15
|
feel of /Game/Player
|
|
16
16
|
|
|
17
|
+
pushed 'move' [1, 0]
|
|
17
18
|
top speed 5.62 u/s
|
|
18
19
|
to full speed 383 ms
|
|
19
20
|
to a stop 133 ms
|
|
@@ -28,12 +29,58 @@ feel of /Game/Player
|
|
|
28
29
|
jump buffer 150 ms
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
A `✗` or `!` line above the numbers is what the ENGINE said while they were
|
|
33
|
+
being taken — a script that threw and was quarantined, a camera following a node
|
|
34
|
+
with no position. The measurements below it are still real (a behaviour is
|
|
35
|
+
quarantined, the node is not), but a page of confident numbers taken while a
|
|
36
|
+
script was dying reads as a healthy character, so read those lines first.
|
|
37
|
+
|
|
31
38
|
## Why this exists
|
|
32
39
|
|
|
40
|
+
**`top speed` is what the character SUSTAINS, not its fastest frame.** The probe
|
|
41
|
+
holds `move` for five seconds and takes the 90th percentile of the run, because
|
|
42
|
+
the maximum belongs to whatever hit the character last. Measured on the shipped
|
|
43
|
+
`platformer-2d`, whose `RUN_SPEED` is 250: it reported `top speed 300 u/s` —
|
|
44
|
+
that is `KNOCK_X`, from an enemy the probe walked into — and `to full speed
|
|
45
|
+
2367 ms`, which was the walk to the enemy, on a character that reaches its speed
|
|
46
|
+
in ONE frame. Four numbers, all about a character being attacked. When the peak
|
|
47
|
+
and the sustained speed disagree the report says so:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
top speed 250 u/s
|
|
51
|
+
to full speed 17 ms
|
|
52
|
+
peak 300 u/s ← SOMETHING ELSE moved it
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Measure feel somewhere nothing can hit you.
|
|
56
|
+
|
|
57
|
+
**A character that jumps in MID-AIR has no coyote window to measure.** The probe
|
|
58
|
+
lifts the body, waits, presses, and reads "vertical velocity went up" as a jump —
|
|
59
|
+
so a double jump answers yes at any delay, and so does any behaviour that pushes
|
|
60
|
+
the body up while `jump` is held (a glide, a jetpack, an updraft). It used to
|
|
61
|
+
print `coyote window always (no window)`, which reads as the opposite of the
|
|
62
|
+
truth, on two of the six shipped starters — one of which declares
|
|
63
|
+
`coyoteSeconds: 0.12` five lines from its glide. It says
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
coyote window not measurable — this character jumps in MID-AIR
|
|
67
|
+
jump buffer 33 ms (suspect — see above)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
and names the declared value it cannot confirm. To measure the window, probe a
|
|
71
|
+
build with the air jump off.
|
|
72
|
+
|
|
33
73
|
**`jumpVelocity: 4` is not a height.** It is an impulse, and what it produces
|
|
34
74
|
depends on gravity, mass, drag and `fallGravity`. The apex in world units and the
|
|
35
75
|
hang time in milliseconds are what you are actually choosing.
|
|
36
76
|
|
|
77
|
+
**It says what it pushed.** The `pushed` line names the action AND the direction
|
|
78
|
+
that produced every number under it. The probe used to hold `(1, 0)` and only
|
|
79
|
+
`(1, 0)`, so a game whose movement reads the OTHER axis of the same action —
|
|
80
|
+
`-v.y`, which is this engine's own documented forward, `getVector` being y-down —
|
|
81
|
+
measured nothing and printed the nothing as `top speed 0 u/s` with four blank
|
|
82
|
+
rows and exit 0. A racer that its own harness clocked at 41 m/s reported 0.
|
|
83
|
+
|
|
37
84
|
**The windows are MEASURED, not read.** `coyoteMs` comes from probing — lift the
|
|
38
85
|
character clear of its support, wait, press, see whether it launched, binary
|
|
39
86
|
search the boundary. A `coyoteSeconds: 0.12` that the controller never consults
|
|
@@ -52,6 +99,29 @@ reports **0 ms**. That was a real bug in this engine's own 3D controller until
|
|
|
52
99
|
| `coyote window` | **0 reads to players as "the controls are broken"**, not as a missing feature. |
|
|
53
100
|
| `jump buffer` | 0 throws away a press made a few frames before landing. |
|
|
54
101
|
|
|
102
|
+
## When it cannot move your player
|
|
103
|
+
|
|
104
|
+
If the default push finds no motion, the tool SEARCHES: every declared `vector2`
|
|
105
|
+
action, each pushed ±x and ±y, and it keeps whichever moved the body furthest.
|
|
106
|
+
That is a fallback, never a replacement — a scene that already answered cannot
|
|
107
|
+
have its numbers changed by it. Two real games it rescues: a throttle on `-v.y`,
|
|
108
|
+
and a game where an unrelated `aim` happened to be declared FIRST in the input
|
|
109
|
+
map (the default move action is "the first `vector2`").
|
|
110
|
+
|
|
111
|
+
When nothing moves it says so instead of printing a zero:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
pushed 'move', 'aim' every way (±x, ±y) — the player did not move
|
|
115
|
+
|
|
116
|
+
could not move /Game/Turret: no motion in 300 frames with 'move', 'aim' pushed
|
|
117
|
+
±x and ±y. Either the movement is not wired to a vector action, or it is
|
|
118
|
+
driven by buttons. Nothing below was measured.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--move <action>` pins the action explicitly, and pinning it turns the search
|
|
122
|
+
OFF — you asked about that action, so a silent answer about a different one
|
|
123
|
+
would be worse than no answer.
|
|
124
|
+
|
|
55
125
|
## It reports, it does not grade
|
|
56
126
|
|
|
57
127
|
There are no PASS/FAIL bands here on purpose. A twitch platformer and a survival
|
|
@@ -71,7 +71,15 @@ warn: World/Enemy: Chase.target — '%Playerr' matches no node in this scene.
|
|
|
71
71
|
|
|
72
72
|
— which is the one failure that used to be completely silent (a prop path
|
|
73
73
|
resolves with `getNodeOrNull`, so the scene opens and the enemy just never
|
|
74
|
-
chases).
|
|
74
|
+
chases). It works for a behaviour of YOUR OWN too, given the file that registers it:
|
|
75
|
+
`incanto-check src --behaviors src/behaviors.ts` prints the identical sentence
|
|
76
|
+
for `Archer.arrowTemplate`. Without the flag the checker has no schema for your
|
|
77
|
+
behaviours and their node-path props are invisible to it — which is why every
|
|
78
|
+
scaffolded project's `check` script passes it, and why a hand-written one that
|
|
79
|
+
does not loses the check silently. `auditScene` from `incanto/test` is the same
|
|
80
|
+
audit from inside your harness, once your behaviours are registered.
|
|
81
|
+
|
|
82
|
+
`Chase.target` and `FollowCamera.target` are `required`: an EMPTY one
|
|
75
83
|
is a load error naming the node, not a surprise in the browser.
|
|
76
84
|
|
|
77
85
|
**Never write `/root/<RootName>/...`.** If your root node is named `Game`, the
|
|
@@ -99,6 +107,7 @@ Hit points with regen and post-hit invulnerability (i-frames). The universal
|
|
|
99
107
|
| `regenPerSec` | `0` | HP regained per second (0 = off) |
|
|
100
108
|
| `invulnerableFor` | `0` | seconds of immunity after a hit (i-frames; 0 = off) |
|
|
101
109
|
| `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
|
|
110
|
+
| `freeParent` | `false` | with `freeOnDeath`, free the PARENT — the enemy, when `Health` rides a child node |
|
|
102
111
|
|
|
103
112
|
Signals: `damaged(amount, current)` · `healed(amount, current)` ·
|
|
104
113
|
`healthChanged(current, max)` · `died` · `revived(current)`
|
|
@@ -143,6 +152,13 @@ still while the character heals. Use `damaged`/`healed` for reacting to the HIT
|
|
|
143
152
|
> node-local, so it clones perfectly and frees the whole enemy with no wiring.
|
|
144
153
|
> (Connections are still the right tool for NON-cloned nodes, e.g. the player's
|
|
145
154
|
> `died → ScoreKeeper.loseLife`.)
|
|
155
|
+
>
|
|
156
|
+
> A node carries ONE script, so if the root is already taken — a `Patrol` you
|
|
157
|
+
> did not give `moveParent`, an `Area2D` with a `Clickable` — put `Health` on a
|
|
158
|
+
> child and add **`freeParent: true`**. Without it the child frees ITSELF and
|
|
159
|
+
> the body walks on with no health at all: an immortal enemy that takes damage
|
|
160
|
+
> from nothing and reports nothing. Same prop, same reason, as `Lifetime` and
|
|
161
|
+
> `DamageOnContact`.
|
|
146
162
|
|
|
147
163
|
## Lifetime
|
|
148
164
|
|
|
@@ -175,7 +191,10 @@ once; reaching 0 lives emits `lost` once.
|
|
|
175
191
|
| `lives` | `0` | starting lives (0 = lives disabled) |
|
|
176
192
|
| `scoreToWin` | `0` | score that triggers `won` (0 = disabled) |
|
|
177
193
|
|
|
178
|
-
Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)`
|
|
194
|
+
Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)` ·
|
|
195
|
+
`livesChanged(lives)` — the EVENT and the COUNT: `lifeLost` fires when a life is
|
|
196
|
+
spent (flash, respawn), `livesChanged` also fires when a save is restored, so a
|
|
197
|
+
lives counter belongs on that one.
|
|
179
198
|
Methods: `addScore(n)` · `setScore(n)` · `loseLife()`
|
|
180
199
|
|
|
181
200
|
```json
|
|
@@ -377,7 +396,7 @@ author markers). Emits `reachedPoint(index)` on arrival.
|
|
|
377
396
|
|
|
378
397
|
| Prop | Default | Meaning |
|
|
379
398
|
|---|---|---|
|
|
380
|
-
| `points` | `[]` | waypoints: position arrays or node-path strings (
|
|
399
|
+
| `points` | `[]` | waypoints: position arrays (LOCAL to the moved node) or node-path strings (that marker's WORLD position) — required |
|
|
381
400
|
| `speed` | `60` | units per second along the path |
|
|
382
401
|
| `loop` | `true` | wrap to the first point after the last (loop mode) |
|
|
383
402
|
| `mode` | `"loop"` | `'loop'` wraps · `'pingpong'` reverses at the ends |
|
|
@@ -388,6 +407,18 @@ author markers). Emits `reachedPoint(index)` on arrival.
|
|
|
388
407
|
|
|
389
408
|
Signals: `reachedPoint(index)`
|
|
390
409
|
|
|
410
|
+
**The two waypoint forms are in different frames, on purpose.** An array is a
|
|
411
|
+
LOCAL offset — the only thing numbers typed into a prop can mean. A node path
|
|
412
|
+
means *where that marker actually is*, so markers under `/root/Path` and
|
|
413
|
+
walkers under a spawn container work, which is how a lane is organised. Before
|
|
414
|
+
0.71 a node path was read as a local offset too, so a walker whose parent was
|
|
415
|
+
not at the markers' origin walked a shifted copy of the path in silence.
|
|
416
|
+
|
|
417
|
+
Need to CHANGE the route at runtime rather than author it? That is
|
|
418
|
+
`PathFollow` and its `setPath()` (see *Pathfinding*, below) — `Patrol.points` is read live
|
|
419
|
+
but the index and direction are not re-derived, so writing points mid-route
|
|
420
|
+
moves the corners under a walker that is still heading for the old one.
|
|
421
|
+
|
|
391
422
|
```json
|
|
392
423
|
{ "name": "Guard", "type": "Node2D",
|
|
393
424
|
"script": { "name": "Patrol", "props": { "points": [[100, 0], [300, 0]], "speed": 80, "mode": "pingpong" } } }
|
|
@@ -569,6 +600,13 @@ placeTower(pad);
|
|
|
569
600
|
"you cannot afford that" is one wire to a banner. It saves through `serialize()`
|
|
570
601
|
like `Health` and `ScoreKeeper`.
|
|
571
602
|
|
|
603
|
+
A NON-POSITIVE amount is ignored, the way `Health.damage`/`heal` have always
|
|
604
|
+
ignored one — and a negative is reported, because it is a sign flip in the
|
|
605
|
+
caller rather than an offer. `spend(-5)` used to PAY you five (any negative
|
|
606
|
+
passes the affordability test, and the subtraction then added) and `earn(-5)`
|
|
607
|
+
to take five, going round `canAfford` and `refused` entirely. `spend(0)` returns
|
|
608
|
+
`true`: you can always afford nothing.
|
|
609
|
+
|
|
572
610
|
## Wander
|
|
573
611
|
|
|
574
612
|
Seeded random roaming inside a circle around the spawn point — idle critters,
|
|
@@ -658,6 +696,15 @@ are NOT cloned):
|
|
|
658
696
|
- **(B) scene `connection`** (fine for a single authored entity, NOT for clones):
|
|
659
697
|
`{ "signal": "movementStateChanged", "from": "Enemy/AI", "to": "Enemy/Skin", "handler": "setClip" }`.
|
|
660
698
|
|
|
699
|
+
**Subscribing in `onReady` is safe to do to anything, including a node that
|
|
700
|
+
outlives you.** A connection made there is owned by the node making it, so
|
|
701
|
+
`free()` takes it with the node — you do not have to keep the returned
|
|
702
|
+
disconnect and call it yourself. That matters most for the shape this pattern
|
|
703
|
+
invites: every enemy subscribing to the score keeper or the player, where the
|
|
704
|
+
emitter outlives thousands of listeners. (Wiring a signal from a `update()` or
|
|
705
|
+
from outside a lifecycle method still hands ownership to you — keep the
|
|
706
|
+
disconnect, or pass `{ owner: someNode }`.)
|
|
707
|
+
|
|
661
708
|
Same `ZombieSkin` is the place to also FACE the heading and GROUND the body each frame
|
|
662
709
|
(see incanto-3d-character.md "+Z-FORWARD rule" and incanto-3d-models.md "Grounding").
|
|
663
710
|
|
|
@@ -715,7 +762,7 @@ all a raft, a buoy, a crate or a body needs to bob, settle and ride the swell.
|
|
|
715
762
|
|
|
716
763
|
| Prop | Default | Meaning |
|
|
717
764
|
|---|---|---|
|
|
718
|
-
| `water` | `""` | Water3D node path; empty = the first Water3D in the tree (a wrong path fails hard) |
|
|
765
|
+
| `water` | `""` | Water3D node path; empty = the first Water3D in the tree (a wrong path fails hard; NO water in the scene is an `incanto-check` warning) |
|
|
719
766
|
| `draft` | `0.35` | where the resting WATERLINE sits below the body origin, in meters — the hull floats with its origin that far above the water |
|
|
720
767
|
| `stiffness` | `3` | how stiffly it returns to that waterline (extra g of lift per draft of submersion): 1 wallows, 3 is a firm dinghy, 8 is a cork |
|
|
721
768
|
| `damping` | `3.5` | vertical damping (1/s) — how fast the bobbing settles |
|
|
@@ -775,7 +822,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
|
|
|
775
822
|
| Prop | Default | Meaning |
|
|
776
823
|
|---|---|---|
|
|
777
824
|
| `prefab` | `""` | node path of the template to clone (required) |
|
|
778
|
-
| `interval` | `1` | seconds between spawns |
|
|
825
|
+
| `interval` | `1` | seconds between spawns — `0` is one clone per FRAME and is refused unless `max` or `total` caps it |
|
|
779
826
|
| `max` | `0` | max LIVE instances (0 = unlimited) |
|
|
780
827
|
| `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
|
|
781
828
|
| `autoStart` | `true` | begin ticking at ready |
|
|
@@ -855,19 +902,55 @@ union-typed prop so its default is `null` (= `'forward'`).
|
|
|
855
902
|
|
|
856
903
|
In 3D, `'forward'` is the engine's ONE facing convention — **+Z**, the same
|
|
857
904
|
`atan2(dx, dz)` basis `CharacterController3D` uses and `incanto-3d-character.md`
|
|
858
|
-
spells out under "the +Z-FORWARD rule". So aim the
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
905
|
+
spells out under "the +Z-FORWARD rule". So aim the BULLET with that formula and
|
|
906
|
+
it flies at what you aimed at; do not add 180°. (Before 0.62 this returned
|
|
907
|
+
three.js's −Z instead, so every `'forward'` shot left the muzzle pointing at the
|
|
908
|
+
shooter's back — at the right speed, with no error, which reads as a gun that
|
|
909
|
+
simply never hits anything.) In 2D `rotation` is a scalar and `'forward'` is +x
|
|
910
|
+
turned clockwise by it, unchanged.
|
|
911
|
+
|
|
912
|
+
**Aim the bullet, not the shooter.** Turning a turret does not turn a bullet
|
|
913
|
+
parented under it: world position is composed TRANSLATION-ONLY, so a bolt under
|
|
914
|
+
a turret at `rotation: 90` still lands at world `[100, 0]`, not `[0, 100]`. Set
|
|
915
|
+
the heading on the projectile itself.
|
|
864
916
|
|
|
865
917
|
| Prop | Default | Meaning |
|
|
866
918
|
|---|---|---|
|
|
867
|
-
| `speed` | `300` | units per second along `direction` |
|
|
868
|
-
| `direction` | `null` | a vector `[x,y(,z)]` OR `'forward'` (`null` = forward) |
|
|
919
|
+
| `speed` | `300` | units per second along `direction` — **live**, rescales in flight |
|
|
920
|
+
| `direction` | `null` | a vector `[x,y(,z)]` OR `'forward'` (`null` = forward) — **live** |
|
|
869
921
|
| `gravity` | `0` | constant +y (downward, 2D y-down) pull for arcing shots |
|
|
870
922
|
|
|
923
|
+
### Aiming a bullet you just spawned
|
|
924
|
+
|
|
925
|
+
Every prop above is LIVE, and the heading is resolved on the projectile's FIRST
|
|
926
|
+
STEP — which is after `spawned(node)` fires. That window is the whole point:
|
|
927
|
+
`SpawnSource.spawn` ends with `addChild(clone)` and `Spawner` emits `spawned`
|
|
928
|
+
afterwards, so a handler is by definition "too late" for anything baked at
|
|
929
|
+
`onReady`.
|
|
930
|
+
|
|
931
|
+
```ts
|
|
932
|
+
spawner.on('spawned', (bolt) => {
|
|
933
|
+
// either spelling works — set the node's rotation…
|
|
934
|
+
(bolt as { rotation: number }).rotation = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|
935
|
+
// …or hand the behavior a vector
|
|
936
|
+
(bolt.behavior as Projectile).direction = [dx / len, dy / len];
|
|
937
|
+
});
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
Mid-flight, writing `direction` or `speed` re-bakes immediately (the fall
|
|
941
|
+
`gravity` has accumulated is kept — it is a fact about the world, not about the
|
|
942
|
+
shot). What is deliberately NOT live is `rotation` after that first step: a
|
|
943
|
+
projectile that spins for looks must not have its spin become its trajectory.
|
|
944
|
+
Call **`aim()`** to re-derive from the current rotation on purpose:
|
|
945
|
+
|
|
946
|
+
```ts
|
|
947
|
+
bolt.rotation = newHeadingDeg;
|
|
948
|
+
(bolt.behavior as Projectile).aim(); // or aim([dx, dy]) to pass a vector
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
Before 0.70 all of this was dead: `direction` accepted the write, READ BACK the
|
|
952
|
+
new value, and the bolt kept flying the old way.
|
|
953
|
+
|
|
871
954
|
```json
|
|
872
955
|
{ "name": "Bullet", "type": "Area2D", "props": { "rotation": 0 },
|
|
873
956
|
"script": { "name": "Projectile", "props": { "speed": 600, "direction": "forward" } } }
|
|
@@ -1168,6 +1251,14 @@ The panel is hidden while playing and shown while paused — `GameFlow` owns its
|
|
|
1168
1251
|
`visible`, so do not set it yourself. `pauseAction` and `pausePanelPath` rename
|
|
1169
1252
|
either half; `pausePanelPath: ""` turns it off.
|
|
1170
1253
|
|
|
1254
|
+
While the panel is up its `HudLayer` gets `focusNavigation` — arrow keys, d-pad
|
|
1255
|
+
and A — and the ring starts on the first widget. The layer's own value is put
|
|
1256
|
+
back on resume, so a game that navigates by pad all the time keeps doing so —
|
|
1257
|
+
unless the game CHANGED it during the pause, in which case the game had the last
|
|
1258
|
+
word and keeps it.
|
|
1259
|
+
That is the one thing every pause menu needs and nobody remembers: a menu you
|
|
1260
|
+
can only click is not playable on a controller.
|
|
1261
|
+
|
|
1171
1262
|
The pause key is polled in `update()`, which still runs at `timeScale 0` — a
|
|
1172
1263
|
menu that cannot be closed by the key that opened it is worse than no menu.
|
|
1173
1264
|
|
|
@@ -1181,7 +1272,7 @@ physics, input. Listen to the `flowChanged(state)` signal for custom UI.
|
|
|
1181
1272
|
`restartScene(engine)` is exported standalone.
|
|
1182
1273
|
|
|
1183
1274
|
**Any scene swap thaws.** The freeze lives on the engine, not on the flow, so
|
|
1184
|
-
leaving a game-over screen by your own route — `engine.setScene(loadScene(next))`
|
|
1275
|
+
leaving a game-over screen by your own route — `engine.setScene(loadScene(next, { engine }))`
|
|
1185
1276
|
from a menu button — puts `timeScale` back to 1 as surely as `flow.restart()`
|
|
1186
1277
|
does. You cannot restart into a frozen level.
|
|
1187
1278
|
|
|
@@ -1189,11 +1280,27 @@ Multi-scene games: `flow.goToScene(nextSceneJson, { fadeSeconds: 0.4 })`
|
|
|
1189
1280
|
fades to black, swaps, fades back (headless = instant). Title → level →
|
|
1190
1281
|
next level is three JSON files and this one call.
|
|
1191
1282
|
|
|
1283
|
+
**A swap builds the physics world the incoming scene needs.** `physics: 'auto'`
|
|
1284
|
+
can only look at the scene the game BOOTS with, and a title screen has no
|
|
1285
|
+
bodies — so this used to leave the level with no physics at all: it rendered
|
|
1286
|
+
perfectly, nothing fell, nothing collided, nothing could be collected or hurt.
|
|
1287
|
+
`createGame2D`/`createGame3D` now hand the engine a way to build one, and the
|
|
1288
|
+
swap asks. It is asynchronous (Rapier is WASM), so the first frames after a
|
|
1289
|
+
swap into a physics scene run unsimulated — imperceptible, and the alternative
|
|
1290
|
+
was a game that never simulated. `physics: false` stays off across swaps, and a
|
|
1291
|
+
scene with no bodies still pays for nothing.
|
|
1292
|
+
|
|
1192
1293
|
## DayNight (live 24-hour cycle)
|
|
1193
1294
|
|
|
1194
1295
|
Drives the scene `environment` (needs an atmosphere sky): sun elevation/
|
|
1195
1296
|
azimuth arc, ambient + exposure ramps with a moonlight floor.
|
|
1196
1297
|
|
|
1298
|
+
**No `sky` in the scene environment = nothing to drive.** The clock still runs
|
|
1299
|
+
and `dayPhaseChanged` still fires, so the signals look right and the screen
|
|
1300
|
+
never changes; the behaviour says so once, in the engine log. Add
|
|
1301
|
+
`"sky": { "type": "atmosphere" }` to the scene's `environment` — or set it at
|
|
1302
|
+
runtime, which it keeps looking for.
|
|
1303
|
+
|
|
1197
1304
|
```jsonc
|
|
1198
1305
|
{ "script": { "name": "DayNight", "props": { "daySeconds": 240, "startHour": 9 } } }
|
|
1199
1306
|
```
|