incanto 0.69.0 → 0.71.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/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- 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 +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- 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 +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.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-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.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-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- 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 +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- 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 +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- 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 +1205 -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/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- 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 +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- 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 +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- 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 +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.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-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.js +0 -10958
|
@@ -34,7 +34,7 @@ $ bunx incanto verify # finds your scene AND your behaviou
|
|
|
34
34
|
✓ loads — the scene is legal and its assets resolve
|
|
35
35
|
! /Game/Chest: script Chest — target '%Ke' matches nothing
|
|
36
36
|
? plays — 8 runs played without reaching a win (4 lost, 4 ran out the clock)
|
|
37
|
-
|
|
37
|
+
? feels — 5 of 7 fired — silent: /Game/Boss/Roar, /Game/Boss/Boom
|
|
38
38
|
· agrees — not run — this scene has no `multiplayer` header
|
|
39
39
|
? draws — the dev server is running on :5173, but no page answered
|
|
40
40
|
|
|
@@ -106,8 +106,17 @@ and a run without `--behaviors` reports that instead: neither is a broken game.
|
|
|
106
106
|
`feels` is the sound-and-effects rung: it lists what the scene DECLARES against
|
|
107
107
|
what actually fired during the playtest. A game whose feedback is wired but
|
|
108
108
|
never triggered plays perfectly and feels dead, and every other rung calls it
|
|
109
|
-
healthy. Nothing firing at all is a
|
|
110
|
-
|
|
109
|
+
healthy. Nothing firing at all is a FAILURE. Some nodes staying silent is
|
|
110
|
+
`unmeasured` (`?`) with the names listed — a silent emitter is either a broken
|
|
111
|
+
wire or a moment random play never reached, and the two look identical from
|
|
112
|
+
here. Reach it in a scripted run and this rung can tell you which. It ticks
|
|
113
|
+
(`✓`) only when every declared emitter fired.
|
|
114
|
+
|
|
115
|
+
`says` reads the running page's own log. Errors and failed assets fail it;
|
|
116
|
+
WARNINGS are carried under it with a `!` and it still passes — a camera whose
|
|
117
|
+
`follow` points at a node with no position, a `DayNight` with no sky to move, a
|
|
118
|
+
body with no collider. Those leave a game running and wrong, and they are the
|
|
119
|
+
lines worth reading before anything else on this list.
|
|
111
120
|
|
|
112
121
|
Exit 1 only when a rung actually failed. `--behaviors src/behaviors.ts` matters:
|
|
113
122
|
without it your game logic is stubbed and only the structure is played.
|
|
@@ -221,6 +230,22 @@ console.log(result.describe()); // run report + final scene, one line per
|
|
|
221
230
|
if (!result.ok) process.exit(1); // failures carry atMs + label + message
|
|
222
231
|
```
|
|
223
232
|
|
|
233
|
+
**An assert fails on `false`, on a non-empty STRING, or on a throw.** The
|
|
234
|
+
string is used as the message, which is how a harness explains itself:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
assert: (ctx) => {
|
|
238
|
+
const lives = (ctx.getNode('Score').behavior as never as { lives: number }).lives;
|
|
239
|
+
if (lives <= 0) return `the keep fell — ${lives} lives`; // the failure, in words
|
|
240
|
+
return null; // nothing to say
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`true`, `null`, `undefined` and `''` are passes. Anything else — a number, an
|
|
245
|
+
object — is recorded as a failure naming the contract, because a check that
|
|
246
|
+
cannot fail is not a check. (It used to read only `false` and a throw, so every
|
|
247
|
+
assert in the shape above passed by not being `false`.)
|
|
248
|
+
|
|
224
249
|
Run it with `bunx tsx verify.ts` (or inside a vitest test). Physics enables
|
|
225
250
|
automatically from the scene's `dimension`. `result.snapshots` holds periodic
|
|
226
251
|
captures; `result.logs` holds everything behaviors logged via `this.log`.
|
|
@@ -232,6 +257,84 @@ into the process-global registries (hot-replacing same names). In a test
|
|
|
232
257
|
suite, isolate with `clearRegistry()`/`clearBehaviors()` between files if
|
|
233
258
|
you register conflicting classes.
|
|
234
259
|
|
|
260
|
+
### Clicking, for a game the mouse plays
|
|
261
|
+
|
|
262
|
+
`press`/`release`/`vector`/`key` are all ACTIONS, and `Clickable` reads the mouse
|
|
263
|
+
BUTTON — so a match-3, a tower defense, a card game, a point-and-click, an RTS or
|
|
264
|
+
a flip puzzle needs one more verb:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
steps: [
|
|
268
|
+
{ atMs: 100, click: 'Board/Tile3' }, // by node path — the usual way
|
|
269
|
+
{ atMs: 400, click: [300, 100] }, // or a raw point, in the scene's units
|
|
270
|
+
]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A click is a press and a release on two DIFFERENT frames — `clicked` fires on
|
|
274
|
+
release over the node the press started on — and that is handled for you. So is
|
|
275
|
+
the picker: headless there is no renderer and therefore no raycast, so a
|
|
276
|
+
**geometric** one is installed for the run, answering from the same world boxes
|
|
277
|
+
`framing()` measures. A game that installs a picker of its own keeps it — its
|
|
278
|
+
answer always wins.
|
|
279
|
+
|
|
280
|
+
2D coordinates are world pixels (the space every 2D `position` in the file is
|
|
281
|
+
already written in); 3D casts a ray from the scene's current camera. A `click`
|
|
282
|
+
naming a path that resolves to nothing is a FAILURE, not a click into space.
|
|
283
|
+
|
|
284
|
+
`geometricPicker(scene)` from `incanto/test` is the same thing on its own, for a
|
|
285
|
+
harness that wants to ask "what is under this point" directly.
|
|
286
|
+
|
|
287
|
+
**A `click` naming a HUD widget PRESSES it** — `click: 'Hud/Shop/Buy'` is the
|
|
288
|
+
whole verb for a menu, a shop, a settings screen, an idle game or a card game.
|
|
289
|
+
There is no cursor involved: a widget lives in the DOM overlay, not the world,
|
|
290
|
+
so the click goes to what a MOUSE does to that widget — a button presses, a
|
|
291
|
+
toggle flips, a select advances, and a `UiDialogue` ADVANCES THE LINE without
|
|
292
|
+
answering a waiting choice, exactly as clicking the box does in a browser
|
|
293
|
+
(`advance()` refuses to skip choices). To answer one, do what a player does and
|
|
294
|
+
press the choice, or call `talk.choose(i)` from a `do:` step. Enter and the
|
|
295
|
+
pad's A are a different verb and DO answer.
|
|
296
|
+
A widget that is hidden — or that sits under a hidden `UiPanel` or a hidden
|
|
297
|
+
`HudLayer` — is a FAILURE naming it, because a player could not have pressed it
|
|
298
|
+
either.
|
|
299
|
+
|
|
300
|
+
**A drag needs the two edges apart**, and `click` puts them at one point:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
steps: [
|
|
304
|
+
{ atMs: 100, at: [400, 300], mouse: 'down' }, // press, here
|
|
305
|
+
{ atMs: 200, at: [300, 380] }, // and move while held
|
|
306
|
+
{ atMs: 300, mouse: 'up' }, // let go, there
|
|
307
|
+
]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`at` on its own is a HOVER — the state a game shows for a cursor that has not
|
|
311
|
+
clicked yet. `button` picks a different one (0 left · 1 middle · 2 right) for
|
|
312
|
+
`mouse` and for `click` alike. Written with `click`, a drag-and-launch game
|
|
313
|
+
launches with a vector of `from - to` where both are the same point: **it fires
|
|
314
|
+
every shot and every shot goes nowhere**, and the run reports fine.
|
|
315
|
+
|
|
316
|
+
The aiming side is on the engine, not the renderer: a behaviour reads
|
|
317
|
+
`this.engine.pointerWorld()` (and `toWorld`/`toScreen`), and a `click` or `at`
|
|
318
|
+
step installs a geometric one for the run alongside the picker.
|
|
319
|
+
|
|
320
|
+
### Asking the game a question
|
|
321
|
+
|
|
322
|
+
An assert needs to READ something, and the readable thing is often neither a
|
|
323
|
+
prop nor a method. Every node type's **`Reads:`** line in
|
|
324
|
+
`incanto-node-reference.md` lists its read-only accessors — the ones that exist
|
|
325
|
+
for exactly this:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
(ctx.getNode('Hud/Banner') as never as { showing: string }).showing // '' when idle
|
|
329
|
+
(ctx.getNode('Talk') as never as { active: boolean }).active // mid-dialogue?
|
|
330
|
+
(ctx.getNode('Gun').behavior as never as { liveCount: number }).liveCount
|
|
331
|
+
(ctx.getNode('Player').behavior as never as { isDead: boolean }).isDead
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`banner.text` does not exist and reads as `undefined`, which an assert will
|
|
335
|
+
happily compare against and pass. Look the accessor up rather than guessing a
|
|
336
|
+
prop name.
|
|
337
|
+
|
|
235
338
|
## 4. Read the scene like a screenshot
|
|
236
339
|
|
|
237
340
|
```ts
|
|
@@ -300,19 +403,27 @@ from geometry alone — no renderer, no canvas, no browser:
|
|
|
300
403
|
|
|
301
404
|
```
|
|
302
405
|
> framing
|
|
303
|
-
camera /World/Player/Cam at [0, 2, 8] looking [0, -0.2, -0.97] fov 60
|
|
406
|
+
camera /World/Player/Cam at [0, 2, 8] looking [0, -0.2, -0.97] up [0, 1, 0] fov 60
|
|
304
407
|
lit by: /World/Sun (DirectionalLight3D), environment.sky
|
|
305
|
-
3 on screen, 1 off screen, 2 behind the camera
|
|
408
|
+
3 on screen, 1 off screen, 2 behind the camera, 1 hidden (visible: false)
|
|
306
409
|
onScreen /World/Island (MeshInstance3D) [0, 0, 0] screen [0.02, -0.11] 8.2m
|
|
307
410
|
behind /World/Chest (ModelInstance3D) [0, 0, 40] 32m
|
|
308
411
|
offscreen /World/Gem (MeshInstance3D) [80, 0, 0] screen [4.4, 0] 80.1m
|
|
412
|
+
hidden /World/Boss (ModelInstance3D) [0, 0, -12] 20.1m
|
|
309
413
|
overlap /World/Platform ∩ /World/Gem
|
|
310
414
|
```
|
|
311
415
|
|
|
312
|
-
Read it for the
|
|
416
|
+
Read it for the five visual failures that look identical from a node list:
|
|
313
417
|
|
|
314
418
|
- **`behind`** — the thing is fine, the camera has its back to it. Indistinguishable
|
|
315
419
|
from "my model did not load" until you can see this line.
|
|
420
|
+
- **`hidden`** — `visible: false` on the node or on any ancestor. The renderer skips
|
|
421
|
+
the whole subtree, so it is NOT in view; a screen drawing nothing used to report
|
|
422
|
+
every one of them as in view. `NOTHING IS DRAWN` when that is all of them.
|
|
423
|
+
- **`up`** — which way is up on screen. Roll changes NOTHING else, so a camera rolled
|
|
424
|
+
180° prints the same `at` and the same `looking` as an upright one; the report
|
|
425
|
+
says `is UPSIDE DOWN` when `up.y` goes negative (a top-down camera measures
|
|
426
|
+
`up.y = 0` and is left alone).
|
|
316
427
|
- **`lit by: NOTHING`** — the scene renders black. No error, no log, nothing wrong
|
|
317
428
|
with any node.
|
|
318
429
|
- **`overlap`** — two declared colliders intersect: the gem is inside the platform.
|
|
@@ -322,6 +433,13 @@ Also `incanto-play`'s `framing` command, and `describeFraming(scene)` /
|
|
|
322
433
|
as on screen when its `size` box (or collider) is, not only its origin — so a
|
|
323
434
|
200 m island whose origin sits off to the left is correctly `onScreen`.
|
|
324
435
|
|
|
436
|
+
**HUD widgets are counted separately** (`report.widgets`), because asking
|
|
437
|
+
whether the camera can see one is a category error — and because leaving them
|
|
438
|
+
out meant a shop screen made entirely of widgets and an empty scene file both
|
|
439
|
+
read `0/0 on screen`. A scene with no drawable node AND no widget now says
|
|
440
|
+
`nothing on screen`, and the gate treats it as broken: a scene that draws
|
|
441
|
+
nothing looks exactly like one that failed to load.
|
|
442
|
+
|
|
325
443
|
**A 2D scene gets a 2D report**, in pixels: the `viewport.design` window centred
|
|
326
444
|
on the current `Camera2D` and CLAMPED by its `limits`, which is the view the
|
|
327
445
|
renderer actually draws. Nothing is ever `behind` a 2D camera and nothing is
|
|
@@ -334,7 +452,7 @@ the instant a frame composites, so `canvas.toDataURL()` and every screenshot
|
|
|
334
452
|
built on it come back BLANK without it:
|
|
335
453
|
|
|
336
454
|
```ts
|
|
337
|
-
createGame3D({ ..., preserveDrawingBuffer: true });
|
|
455
|
+
createGame3D({ ..., preserveDrawingBuffer: true }); // createGame2D too
|
|
338
456
|
// or in the scene: "environment": { "rendering": { "preserveDrawingBuffer": true } }
|
|
339
457
|
```
|
|
340
458
|
|
|
@@ -520,6 +638,44 @@ The same words `incanto-play` takes — `press`/`release ACTION`, `vector ACTION
|
|
|
520
638
|
newlines. `--do-file` reads a longer plan from a file. One vocabulary for the
|
|
521
639
|
headless gamepad and the live one.
|
|
522
640
|
|
|
641
|
+
**Pointing at things**, for the games that are played that way:
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
$ bunx incanto-frame --do "at %Mole; click; step 300" --out whacked.png
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`at X Y` puts the cursor on a canvas pixel and `at <node path>` puts it wherever
|
|
648
|
+
that node is ON SCREEN — the page knows, and you cannot compute it from out
|
|
649
|
+
here. **A `click` before anything has aimed is refused**, because it would land
|
|
650
|
+
wherever the cursor was left, and a plan whose `at` failed would otherwise
|
|
651
|
+
report the aim false and the click true. Headless, `click` also answers with
|
|
652
|
+
what it HIT (`"hit": "/Game/Field/Hole11/Mole"`, or `null`) — the reason to aim
|
|
653
|
+
is to hit something, and from outside the process there is no way to ask. **`incanto-play` takes the same two words**, headless, installing a
|
|
654
|
+
geometric picker for them (there is no renderer to raycast with):
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
$ printf 'step 700\nat Field/Hole12/Mole\nclick\ndescribe\nquit\n' \
|
|
658
|
+
| bunx incanto-play src/game.scene.json --behaviors src/behaviors.ts
|
|
659
|
+
{"ok":true,"cmd":"at","at":[560,291]}
|
|
660
|
+
{"ok":true,"cmd":"click","t":733}
|
|
661
|
+
… /Game/Hud/Score UiText text="Moles 1 / 12"
|
|
662
|
+
``` `click` (optionally `left`/`middle`/`right`) presses and releases where the
|
|
663
|
+
cursor is, with a frame in between so the game gets to show that it is being
|
|
664
|
+
pressed.
|
|
665
|
+
|
|
666
|
+
**A drag is `mouse` — press, move, let go**, because `click` is both edges at
|
|
667
|
+
one point and a slingshot, a selection box, a swipe and an item carried between
|
|
668
|
+
inventory slots are all "press here, move there, let go":
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
$ bunx incanto-frame --do "at 400 300; mouse left down; at 300 380; mouse left up; step 400"
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`mouse` takes the same three words `click` does, and the same `0|1|2` the
|
|
675
|
+
`incanto-play` REPL takes. The language used to have only `pointer DX DY`, which is a delta —
|
|
676
|
+
the mouse-LOOK question — so an agent driving its own match-3, tower defense,
|
|
677
|
+
card game or point-and-click could reach no state at all.
|
|
678
|
+
|
|
523
679
|
`step` waits on the REAL clock: this is the running game, with its own loop, its
|
|
524
680
|
own physics and its own animation, and stepping it by hand would fight all three.
|
|
525
681
|
|
|
@@ -529,6 +685,17 @@ then land on a game that never advances: the capture comes back UNCHANGED and
|
|
|
529
685
|
looks perfectly healthy. `drove 0 frames` is refused with an error rather than
|
|
530
686
|
handed over as a picture.
|
|
531
687
|
|
|
688
|
+
**A PLAIN capture works on a hidden page.** Only a DRIVE needs the window in
|
|
689
|
+
front. `captureFrame` waits ~120 ms for the game loop and then draws a frame
|
|
690
|
+
itself, exactly because a hidden tab gets none — so the pixels are current, the
|
|
691
|
+
world is simply paused. Measured, hiding every node from the console and
|
|
692
|
+
capturing again: 2D `luminance 0.09 → 0.00 BLACK SCREEN`, 3D `0.64 → 0.96,
|
|
693
|
+
subject 44.3% → 22.0%`.
|
|
694
|
+
|
|
695
|
+
That is the whole point for an agent: **your terminal is in front and the game
|
|
696
|
+
is behind it, and `draws` still answers.** The report says the world is paused,
|
|
697
|
+
and the `draws` rung carries that as a note under a pass.
|
|
698
|
+
|
|
532
699
|
#### Look at it
|
|
533
700
|
|
|
534
701
|
```
|
|
@@ -779,6 +946,32 @@ rows until the orange box lands on the thing you're hunting.
|
|
|
779
946
|
inside a `static` subtree, HUD widgets outside a HudLayer. Programmatic:
|
|
780
947
|
`auditScene(json)` from `incanto` or `incanto/test` returns the warnings.
|
|
781
948
|
|
|
949
|
+
**Including a HUD line that names the wrong key.** `tps-3d`'s pause menu read
|
|
950
|
+
`Esc to resume · R to restart` for exactly three hours after `restart` was moved
|
|
951
|
+
off `KeyR` — the binding moved and the sentence about it did not, which is the
|
|
952
|
+
whole failure mode of any fact written down twice. The check is narrow on
|
|
953
|
+
purpose: it fires only when a line names both a key it recognises AND a word
|
|
954
|
+
that is a DECLARED ACTION in the same scene, so "Esc to resume" says nothing
|
|
955
|
+
(there is no `resume` action) and prose is left alone.
|
|
956
|
+
|
|
957
|
+
**Including one key bound to two actions.** `tps-3d` shipped `reload` and
|
|
958
|
+
`restart` both on `KeyR`: pressing R reloaded, and `restart` — which nothing
|
|
959
|
+
read — looked broken. Two actions on one key are legal, sometimes deliberate,
|
|
960
|
+
so this warns rather than refuses; the sibling question ("does anything read
|
|
961
|
+
this action at all?") is `input.unreadActions()`, in `incanto-playtesting.md`.
|
|
962
|
+
|
|
963
|
+
**Including a `Clickable` nobody listens to.** It produces `clicked`, `hovered`
|
|
964
|
+
and `unhovered` and nothing else, so attaching it and wiring none of them costs
|
|
965
|
+
a pick every frame and can never do anything — the likeliest mistake a mouse
|
|
966
|
+
game makes, and one with no error anywhere. A board of sixteen unwired tiles
|
|
967
|
+
used to print `1/1 scene(s) valid`.
|
|
968
|
+
|
|
969
|
+
```
|
|
970
|
+
warn: Game/A: Clickable produces nothing but 'clicked'/'hovered'/'unhovered',
|
|
971
|
+
and no connection listens to any of them — so it can never do anything.
|
|
972
|
+
Wire one, or listen in code with `node.on('clicked', …)`.
|
|
973
|
+
```
|
|
974
|
+
|
|
782
975
|
**Including a node path that points at nothing.** `Chase.target`,
|
|
783
976
|
`Camera2D.follow`, `Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain` —
|
|
784
977
|
these hold a path to another node, and a wrong one is SILENT: a connection that
|
|
@@ -793,8 +986,12 @@ warn: World/Cam: follow — '/Level/Player' starts at 'Level', but the scene
|
|
|
793
986
|
```
|
|
794
987
|
|
|
795
988
|
An empty value is never reported — `""` is the default of most of these and
|
|
796
|
-
means "not set"
|
|
797
|
-
|
|
989
|
+
means "not set" (and `getNodeOrNull("")` returns null, so the guard you would
|
|
990
|
+
write for it is unnecessary). A behavior of YOUR OWN is reported too, once you
|
|
991
|
+
hand the checker the file that registers it: `incanto-check src --behaviors
|
|
992
|
+
src/behaviors.ts`. Without the flag it has no schema for your behaviours and
|
|
993
|
+
cannot know which of their props are paths — so pass it, the way every
|
|
994
|
+
scaffolded project's `check` script does.
|
|
798
995
|
|
|
799
996
|
## Deterministic replay (record once, regression-test forever)
|
|
800
997
|
|
|
@@ -811,7 +1008,7 @@ save.set('replays/level1', rec.stop());
|
|
|
811
1008
|
|
|
812
1009
|
// in a headless test:
|
|
813
1010
|
const engine = new Engine({ seed: SAME_SEED });
|
|
814
|
-
engine.setScene(loadScene(sceneJson));
|
|
1011
|
+
engine.setScene(loadScene(sceneJson, { engine }));
|
|
815
1012
|
replay(engine, recording, { onTick(i) { /* mid-run asserts */ } });
|
|
816
1013
|
expect(player.position).toEqual(expected); // exact, not approximate
|
|
817
1014
|
```
|
|
@@ -83,6 +83,34 @@ null when the ray misses (looking at the sky). `pick(sx, sy)` gives you the NODE
|
|
|
83
83
|
under a pixel — a different question, and the only one 3D could answer before
|
|
84
84
|
0.66.
|
|
85
85
|
|
|
86
|
+
**Inside a Behavior there is no renderer**, so the same four questions are on
|
|
87
|
+
the engine, installed by whichever renderer is running and cleared when it is
|
|
88
|
+
disposed:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
this.engine.pointerWorld() // where the cursor is → number[] | null
|
|
92
|
+
this.engine.toWorld(sx, sy) // any screen point → [x, y] | [x, y, z]
|
|
93
|
+
this.engine.toScreen(node.position) // → { x, y, behind } | null
|
|
94
|
+
this.engine.screenRay(sx, sy) // 3D only → { origin, dir } for castRay
|
|
95
|
+
this.engine.pickAt(sx, sy) // which node (cached per frame)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The answer is the scene's own shape — `[x, y]` in 2D, `[x, y, z]` on the ground
|
|
99
|
+
in 3D — so it goes straight back into a `position`. All are null with no
|
|
100
|
+
renderer; a `runScript` `click` step (and `incanto-play`'s `at`) installs
|
|
101
|
+
geometric `toWorld`/`toScreen`/`screenRay` for the run, so a game that AIMS is
|
|
102
|
+
testable headless and not only one that clicks.
|
|
103
|
+
|
|
104
|
+
**On uneven ground, cast the ray** — `toWorld` lands on a plane, which is the
|
|
105
|
+
wrong answer under a hill or a crate:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const at = this.engine.input.pointerPosition();
|
|
109
|
+
const ray = at && this.engine.screenRay?.(at.x, at.y);
|
|
110
|
+
const hit = ray && this.engine.physics?.castRay(ray.origin, ray.dir, 100);
|
|
111
|
+
if (hit) place(hit.point); // WHERE it landed, not how far
|
|
112
|
+
```
|
|
113
|
+
|
|
86
114
|
Perf note: `useNodeProp` deep-compares per frame — subscribe to LEAF values
|
|
87
115
|
(`'UI/Score', 'text'`), not big objects like a whole `animations` map.
|
|
88
116
|
|
|
@@ -40,7 +40,9 @@ the vite config and the verify harness.
|
|
|
40
40
|
|
|
41
41
|
Pick by shape, not by subject: `tps-3d` (third-person combat), `platformer-2d`,
|
|
42
42
|
`star-survivor` (top-down survivor), `village-quest-3d` (quest/NPC),
|
|
43
|
-
`beacon-isle-3d` (open world)
|
|
43
|
+
`beacon-isle-3d` (open world), `molehill-2d` (**played with the MOUSE** — no
|
|
44
|
+
character, no keyboard: the shape a match-3, a tower defense, a card game, a
|
|
45
|
+
point-and-click or an RTS starts from).
|
|
44
46
|
|
|
45
47
|
---
|
|
46
48
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* (quest, enemies, HUD) — regenerate the world without touching the game.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { newUid } from 'incanto';
|
|
9
10
|
import { buildTerrainNav, Terrain3D } from 'incanto/3d';
|
|
10
11
|
import { generateTerrain } from 'incanto/env';
|
|
11
12
|
|
|
@@ -145,6 +146,8 @@ dressing.push({
|
|
|
145
146
|
name: 'ShoreRocks',
|
|
146
147
|
type: 'InstancedMesh3D',
|
|
147
148
|
props: {
|
|
149
|
+
// Sunk into the slope on purpose — see the note by the player's Catch.
|
|
150
|
+
snapToGround: false,
|
|
148
151
|
mesh: 'gem',
|
|
149
152
|
size: [0.6, 0.45, 0.6],
|
|
150
153
|
material: { color: '#7e8590', roughness: 0.95, flatShading: true },
|
|
@@ -166,7 +169,7 @@ const ward = (i: number, [x, y, z]: [number, number, number]): unknown => ({
|
|
|
166
169
|
name: `Ward${i}`,
|
|
167
170
|
type: 'Node3D',
|
|
168
171
|
groups: ['ward'],
|
|
169
|
-
props: { position: [round2(x), round2(y), round2(z)] },
|
|
172
|
+
props: { position: [round2(x), round2(y), round2(z)], snapToGround: false },
|
|
170
173
|
children: [
|
|
171
174
|
{
|
|
172
175
|
name: 'Plinth',
|
|
@@ -228,7 +231,7 @@ const shade = (wardIdx: number, n: number, [x, y, z]: [number, number, number]):
|
|
|
228
231
|
name: `Shade${wardIdx}_${n}`,
|
|
229
232
|
type: 'Node3D',
|
|
230
233
|
groups: ['shade', `shade-ward${wardIdx}`],
|
|
231
|
-
props: { position: [round2(x), round2(y + 0.9), round2(z)] },
|
|
234
|
+
props: { position: [round2(x), round2(y + 0.9), round2(z)], snapToGround: false },
|
|
232
235
|
script: { name: 'PathFollow', props: { speed: 2.6, loop: true } },
|
|
233
236
|
children: [
|
|
234
237
|
{
|
|
@@ -411,6 +414,9 @@ const scene = {
|
|
|
411
414
|
round2(heightAt(keeperSite[0] - 5, keeperSite[2] - 4)),
|
|
412
415
|
round2(keeperSite[2] - 4),
|
|
413
416
|
],
|
|
417
|
+
// The lamp sits at the top of the tower: the ASSEMBLY owns its
|
|
418
|
+
// children's height, so `findFloatingProps` is not asked about them.
|
|
419
|
+
snapToGround: false,
|
|
414
420
|
static: true,
|
|
415
421
|
},
|
|
416
422
|
children: [
|
|
@@ -462,7 +468,7 @@ const scene = {
|
|
|
462
468
|
{
|
|
463
469
|
name: 'Keeper',
|
|
464
470
|
type: 'Node3D',
|
|
465
|
-
props: { position: keeperSite.map(round2) },
|
|
471
|
+
props: { position: keeperSite.map(round2), snapToGround: false },
|
|
466
472
|
script: { name: 'Interactable', props: { action: 'interact', range: 3 } },
|
|
467
473
|
children: [
|
|
468
474
|
{
|
|
@@ -511,6 +517,9 @@ const scene = {
|
|
|
511
517
|
name: 'Player',
|
|
512
518
|
type: 'RigidBody3D',
|
|
513
519
|
groups: ['player'],
|
|
520
|
+
// Its Health is saveable, and a save is keyed by uid — minted, pasted,
|
|
521
|
+
// and stable across regenerations for the same reason the HUD's are.
|
|
522
|
+
uid: 'n_3cqocoi9v0cvoxdv',
|
|
514
523
|
script: { name: 'Health', props: { max: 100, invulnerableFor: 0.8 } },
|
|
515
524
|
props: {
|
|
516
525
|
position: [round2(playerSpawn[0]), round2(playerSpawn[1] + 1.2), round2(playerSpawn[2])],
|
|
@@ -519,11 +528,24 @@ const scene = {
|
|
|
519
528
|
collider: { shape: 'capsule', radius: 0.34, height: 1 },
|
|
520
529
|
},
|
|
521
530
|
children: [
|
|
531
|
+
{
|
|
532
|
+
/*
|
|
533
|
+
* An open terrain has an EDGE. With no catch, walking off it falls
|
|
534
|
+
* forever — `incanto-playtest` calls that `fell` and treats it as a
|
|
535
|
+
* failure whatever the game set out to be.
|
|
536
|
+
*/
|
|
537
|
+
name: 'Catch',
|
|
538
|
+
type: 'Respawn',
|
|
539
|
+
},
|
|
522
540
|
{
|
|
523
541
|
name: 'Controller',
|
|
524
542
|
type: 'CharacterController3D',
|
|
525
543
|
props: {
|
|
526
544
|
view: 'free',
|
|
545
|
+
// The two `incanto-feel` prints a note about on every run, and the
|
|
546
|
+
// 3D character skill calls coyote time the single biggest one.
|
|
547
|
+
coyoteSeconds: 0.12,
|
|
548
|
+
jumpBufferSeconds: 0.15,
|
|
527
549
|
camDistance: 5.2,
|
|
528
550
|
eyeHeight: 1.1,
|
|
529
551
|
maxSpeed: 4.4,
|
|
@@ -623,6 +645,7 @@ const scene = {
|
|
|
623
645
|
// ROOT child (a positioned parent would double the offset on screen)
|
|
624
646
|
props: {
|
|
625
647
|
visible: false,
|
|
648
|
+
snapToGround: false,
|
|
626
649
|
position: [
|
|
627
650
|
round2(keeperSite[0] + 16),
|
|
628
651
|
round2(heightAt(keeperSite[0] + 16, keeperSite[2] - 14)),
|
|
@@ -768,7 +791,10 @@ const scene = {
|
|
|
768
791
|
{
|
|
769
792
|
name: 'Quest',
|
|
770
793
|
type: 'UiText',
|
|
771
|
-
|
|
794
|
+
// MINTED by newUid() and pasted, not invented: a save is keyed by
|
|
795
|
+
// uid and this file is regenerated, so they have to be both
|
|
796
|
+
// crypto-shaped and stable across `bun run world`.
|
|
797
|
+
uid: 'n_3txwnq8m201xbwoz',
|
|
772
798
|
props: {
|
|
773
799
|
text: 'Find the lighthouse keeper [E]',
|
|
774
800
|
size: 16,
|
|
@@ -779,20 +805,25 @@ const scene = {
|
|
|
779
805
|
{
|
|
780
806
|
name: 'HP',
|
|
781
807
|
type: 'UiBar',
|
|
782
|
-
uid: '
|
|
808
|
+
uid: 'n_vp09cmk3syxo2lcz',
|
|
783
809
|
props: { anchor: 'topRight', value: 100, max: 100, width: 170 },
|
|
784
810
|
},
|
|
785
|
-
{
|
|
811
|
+
{
|
|
812
|
+
name: 'Banner',
|
|
813
|
+
type: 'UiBanner',
|
|
814
|
+
uid: 'n_xwkphpoa7ac9w451',
|
|
815
|
+
props: { anchor: 'center' },
|
|
816
|
+
},
|
|
786
817
|
{
|
|
787
818
|
name: 'Dialogue',
|
|
788
819
|
type: 'UiDialogue',
|
|
789
|
-
uid: '
|
|
820
|
+
uid: 'n_7wosfkulqmue3jpv',
|
|
790
821
|
props: { anchor: 'bottom', charsPerSecond: 45 },
|
|
791
822
|
},
|
|
792
823
|
{
|
|
793
824
|
name: 'Hint',
|
|
794
825
|
type: 'UiText',
|
|
795
|
-
uid: '
|
|
826
|
+
uid: 'n_x3funbse7a8nkfnu',
|
|
796
827
|
props: {
|
|
797
828
|
text: 'WASD move · Shift sprint · E interact · click/F strike',
|
|
798
829
|
size: 12,
|
|
@@ -811,7 +842,44 @@ const scene = {
|
|
|
811
842
|
],
|
|
812
843
|
};
|
|
813
844
|
|
|
814
|
-
|
|
845
|
+
/*
|
|
846
|
+
* A REGENERATED file keeps the identities the old one had.
|
|
847
|
+
*
|
|
848
|
+
* A uid is the key a save is written against, and this script overwrites the
|
|
849
|
+
* whole scene — so minting fresh ones every run would break every save on the
|
|
850
|
+
* next `bun run world`, and emitting none at all (which is what it used to do)
|
|
851
|
+
* left eighteen scripted nodes unkeyed, so a save could not record what the run
|
|
852
|
+
* consumed. `verify:uids` said so; nothing said it to whoever ran the command.
|
|
853
|
+
*
|
|
854
|
+
* Reused by PATH: the same seed builds the same island, so the same node is at
|
|
855
|
+
* the same place. Anything new gets a real `newUid()`.
|
|
856
|
+
*/
|
|
857
|
+
const scenePath = `${import.meta.dir}/src/game.scene.json`;
|
|
858
|
+
const previous = new Map<string, string>();
|
|
859
|
+
try {
|
|
860
|
+
const old = JSON.parse(await Bun.file(scenePath).text()) as { root?: unknown };
|
|
861
|
+
const remember = (node: Record<string, unknown>, path: string): void => {
|
|
862
|
+
const here = `${path}/${String(node.name ?? '?')}`;
|
|
863
|
+
if (typeof node.uid === 'string') previous.set(here, node.uid);
|
|
864
|
+
for (const child of (node.children ?? []) as Record<string, unknown>[]) remember(child, here);
|
|
865
|
+
};
|
|
866
|
+
if (old.root) remember(old.root as Record<string, unknown>, '');
|
|
867
|
+
} catch {
|
|
868
|
+
// No previous file: every uid below is minted fresh, which is the first run.
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const keyIdentities = (node: Record<string, unknown>, path: string): void => {
|
|
872
|
+
const here = `${path}/${String(node.name ?? '?')}`;
|
|
873
|
+
// Only what a save would write down: a node carrying a behavior.
|
|
874
|
+
if (node.script && typeof node.uid !== 'string') node.uid = previous.get(here) ?? newUid();
|
|
875
|
+
else if (typeof node.uid === 'string' && previous.has(here))
|
|
876
|
+
node.uid = previous.get(here) as string;
|
|
877
|
+
for (const child of (node.children ?? []) as Record<string, unknown>[])
|
|
878
|
+
keyIdentities(child, here);
|
|
879
|
+
};
|
|
880
|
+
keyIdentities(scene.root as unknown as Record<string, unknown>, '');
|
|
881
|
+
|
|
882
|
+
await Bun.write(scenePath, `${JSON.stringify(scene, null, 2)}\n`);
|
|
815
883
|
console.log(
|
|
816
884
|
`beacon isle generated: keeper ${keeperSite.map(round2)}, wards ${wardSites.map((w) => w.map(round2)).join(' | ')}, ${placed} groves, ${grassPlaced} grass, ${flowersPlaced} flowers`,
|
|
817
885
|
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"dev": "vite",
|
|
7
7
|
"build": "vite build",
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"check": "incanto-check src",
|
|
9
|
+
"check": "incanto-check src --behaviors src/behaviors.ts",
|
|
10
10
|
"verify": "bun verify.ts",
|
|
11
11
|
"world": "bun generate-world.ts"
|
|
12
12
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@dimforge/rapier2d-compat": "0.19.3",
|
|
15
15
|
"@dimforge/rapier3d-compat": "0.19.3",
|
|
16
16
|
"@pixiv/three-vrm": "^3.5.3",
|
|
17
|
-
"incanto": "^0.
|
|
17
|
+
"incanto": "^0.71.0",
|
|
18
18
|
"three": "^0.184.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -88,6 +88,27 @@ function spawnFootDust(root: Node, at: [number, number, number], scale = 1): voi
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export class IsleDirector extends Behavior {
|
|
91
|
+
/**
|
|
92
|
+
* The isle HAS an ending, and never said so.
|
|
93
|
+
*
|
|
94
|
+
* Three wards lit, the Shadeheart down, fireworks over the lighthouse and
|
|
95
|
+
* `BEACON ISLE SHINES` on the banner — and no `won` anywhere, so every
|
|
96
|
+
* instrument that asks the scene whether it is finishable answered no. On
|
|
97
|
+
* the template `bunx incanto-new my-game` scaffolds by DEFAULT:
|
|
98
|
+
*
|
|
99
|
+
* ```
|
|
100
|
+
* ? plays — nothing declares a win — 8 runs played without a defect,
|
|
101
|
+
* and there was no end to reach
|
|
102
|
+
* next: if it is meant to be finishable, emit `won`
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Both sentences were true about the WIRING and wrong about the game. A
|
|
106
|
+
* signal is the cheapest of the three routes the skill offers (`GameFlow`,
|
|
107
|
+
* `ScoreKeeper`, your own behaviour) and the only one that does not also
|
|
108
|
+
* freeze the world — the isle keeps playing after you win it.
|
|
109
|
+
*/
|
|
110
|
+
static readonly signals: readonly string[] = ['won'];
|
|
111
|
+
|
|
91
112
|
private wind: Voice | null = null;
|
|
92
113
|
private nav: TerrainNav | null = null;
|
|
93
114
|
private repathClock = 0;
|
|
@@ -445,6 +466,7 @@ export class IsleDirector extends Behavior {
|
|
|
445
466
|
(this.node.getNode('WinSfx') as AudioPlayer).play();
|
|
446
467
|
this.fireworksIn = 0.4;
|
|
447
468
|
this.syncQuestLine();
|
|
469
|
+
this.node.emit('won');
|
|
448
470
|
});
|
|
449
471
|
return;
|
|
450
472
|
}
|