incanto 0.74.0 → 0.76.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.
Files changed (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -17,7 +17,9 @@
17
17
  },
18
18
  "touch": "joystick"
19
19
  },
20
- "jump": { "type": "button", "keys": ["Space", "KeyW", "ArrowUp"], "touch": "button" }
20
+ "jump": { "type": "button", "keys": ["Space", "KeyW", "ArrowUp"], "touch": "button" },
21
+ "pause": { "type": "button", "keys": ["Escape"] },
22
+ "restart": { "type": "button", "keys": ["KeyR"] }
21
23
  },
22
24
  "assets": {
23
25
  "knight": {
@@ -1235,6 +1237,23 @@
1235
1237
  },
1236
1238
  "uid": "n_odjl6nsgedmaiijc"
1237
1239
  },
1240
+ {
1241
+ "name": "Vitals",
1242
+ "type": "Node",
1243
+ "script": {
1244
+ "name": "Health",
1245
+ "props": {
1246
+ "max": 3,
1247
+ "invulnerableFor": 1.1,
1248
+ "knockback": 420,
1249
+ "knockbackSeconds": 0.22,
1250
+ "knockUp": 360,
1251
+ "staggerSeconds": 0.22,
1252
+ "applyKnockback": false
1253
+ }
1254
+ },
1255
+ "uid": "n_636k806hf3tc9wxe"
1256
+ },
1238
1257
  {
1239
1258
  "name": "Feet",
1240
1259
  "type": "Area2D",
@@ -1334,6 +1353,98 @@
1334
1353
  "color": "#f4f4f4"
1335
1354
  },
1336
1355
  "uid": "n_xzbq736pxmlog2ze"
1356
+ },
1357
+ {
1358
+ "name": "PauseMenu",
1359
+ "type": "UiPanel",
1360
+ "uid": "n_18nwr09ab7ql2z4r",
1361
+ "props": {
1362
+ "anchor": "center",
1363
+ "layout": "column",
1364
+ "gap": 12,
1365
+ "padding": 22,
1366
+ "visible": false,
1367
+ "background": "rgba(10,14,22,0.92)",
1368
+ "radius": 12
1369
+ },
1370
+ "children": [
1371
+ {
1372
+ "name": "PausedTitle",
1373
+ "type": "UiText",
1374
+ "uid": "n_zbj0pg98lnk8rv82",
1375
+ "props": { "text": "PAUSED", "size": 26 }
1376
+ },
1377
+ {
1378
+ "name": "Resume",
1379
+ "type": "UiButton",
1380
+ "uid": "n_pn2pcwtmdaxii0ir",
1381
+ "props": { "text": "RESUME", "size": 18 }
1382
+ },
1383
+ {
1384
+ "name": "PauseOptions",
1385
+ "type": "UiButton",
1386
+ "uid": "n_rlol3m8e7ev1akf3",
1387
+ "props": { "text": "OPTIONS", "size": 16 }
1388
+ },
1389
+ {
1390
+ "name": "Restart",
1391
+ "type": "UiButton",
1392
+ "uid": "n_08baib5wy10vg0ar",
1393
+ "props": { "text": "START OVER", "size": 16 }
1394
+ }
1395
+ ]
1396
+ },
1397
+ {
1398
+ "name": "Options",
1399
+ "type": "UiPanel",
1400
+ "uid": "n_rnlo15z2gqd6hma0",
1401
+ "props": {
1402
+ "anchor": "center",
1403
+ "layout": "column",
1404
+ "gap": 10,
1405
+ "padding": 22,
1406
+ "visible": false,
1407
+ "background": "rgba(10,14,22,0.92)",
1408
+ "radius": 12
1409
+ },
1410
+ "children": [
1411
+ {
1412
+ "name": "OptionsTitle",
1413
+ "type": "UiText",
1414
+ "uid": "n_dybfe2jy973oxrw8",
1415
+ "props": { "text": "OPTIONS", "size": 24 }
1416
+ },
1417
+ {
1418
+ "name": "Master",
1419
+ "type": "UiVolumeSlider",
1420
+ "uid": "n_5rd1metkz071rwyl",
1421
+ "props": { "label": "master", "bus": "master", "width": 260 }
1422
+ },
1423
+ {
1424
+ "name": "Sfx",
1425
+ "type": "UiVolumeSlider",
1426
+ "uid": "n_3jz8z3sx850i1vlh",
1427
+ "props": { "label": "effects", "bus": "sfx", "width": 260 }
1428
+ },
1429
+ {
1430
+ "name": "Mute",
1431
+ "type": "UiMuteToggle",
1432
+ "uid": "n_9jn0ncey479106t8",
1433
+ "props": { "label": "mute everything" }
1434
+ },
1435
+ {
1436
+ "name": "Quality",
1437
+ "type": "UiQualitySelect",
1438
+ "uid": "n_w0dd38u2gqui7oli",
1439
+ "props": { "label": "quality" }
1440
+ },
1441
+ {
1442
+ "name": "Back",
1443
+ "type": "UiButton",
1444
+ "uid": "n_huasc2fyi2sc1ejj",
1445
+ "props": { "text": "BACK", "size": 16 }
1446
+ }
1447
+ ]
1337
1448
  }
1338
1449
  ],
1339
1450
  "uid": "n_ng718r3uiw1madux"
@@ -1476,6 +1587,12 @@
1476
1587
  "props": { "size": [160, 20], "color": "#9aa3bd", "anchor": [0.5, 0.5] }
1477
1588
  }
1478
1589
  ]
1590
+ },
1591
+ {
1592
+ "name": "Flow",
1593
+ "type": "Node",
1594
+ "uid": "n_i26ub26uzhmxyulv",
1595
+ "script": { "name": "GameFlow" }
1479
1596
  }
1480
1597
  ],
1481
1598
  "uid": "n_d9t51gs0vhpnp2jj"
@@ -1510,6 +1627,31 @@
1510
1627
  { "signal": "collected", "from": "Gem1", "to": "SfxCheckpoint", "handler": "play" },
1511
1628
  { "signal": "collected", "from": "Gem2", "to": "SfxCheckpoint", "handler": "play" },
1512
1629
  { "signal": "won", "from": "/root", "to": "SfxWin", "handler": "play" },
1513
- { "signal": "lost", "from": "/root", "to": "SfxLose", "handler": "play" }
1630
+ { "signal": "lost", "from": "/root", "to": "SfxLose", "handler": "play" },
1631
+ {
1632
+ "from": "/root/HUD/PauseMenu/Resume",
1633
+ "signal": "pressed",
1634
+ "to": "/root/Flow",
1635
+ "handler": "resume"
1636
+ },
1637
+ {
1638
+ "from": "/root/HUD/PauseMenu/PauseOptions",
1639
+ "signal": "pressed",
1640
+ "to": "/root/Flow",
1641
+ "handler": "screen",
1642
+ "args": ["/root/HUD/Options"]
1643
+ },
1644
+ {
1645
+ "from": "/root/HUD/PauseMenu/Restart",
1646
+ "signal": "pressed",
1647
+ "to": "/root/Flow",
1648
+ "handler": "restart"
1649
+ },
1650
+ {
1651
+ "from": "/root/HUD/Options/Back",
1652
+ "signal": "pressed",
1653
+ "to": "/root/Flow",
1654
+ "handler": "back"
1655
+ }
1514
1656
  ]
1515
1657
  }
@@ -124,6 +124,8 @@ interface Player {
124
124
  let enemiesBefore = 0;
125
125
  let enemiesAfterStomp = 0;
126
126
  let heartsAfterHit = 3;
127
+ let hitKnock = 0; // px the knight was kicked AWAY from Gob2 while holding TOWARDS it
128
+ let hitLift = 0; // px it rose (knockUp)
127
129
 
128
130
  const result = await runScript(sceneJson, {
129
131
  durationMs: 4000,
@@ -165,10 +167,21 @@ interface Player {
165
167
  do: (ctx) => {
166
168
  const player = ctx.getNode('Player') as unknown as Body;
167
169
  const gob = ctx.getNode('Gob2') as unknown as Body;
168
- player.position = [gob.position[0] ?? 0, gob.position[1] ?? 0];
170
+ // just LEFT of the goblin, overlapping it, and holding RIGHT into it:
171
+ // the hit's kick must win the stick (stagger) and carry the knight left and up
172
+ player.position = [(gob.position[0] ?? 0) - 12, gob.position[1] ?? 0];
169
173
  player.velocity = [0, 0];
174
+ ctx.engine.input.setActionVector('move', 1, 0);
170
175
  for (let i = 0; i < 3; i++) ctx.engine.step();
171
176
  heartsAfterHit = (ctx.getNode('Player').behavior as unknown as Player).hearts;
177
+ const x0 = player.position[0] ?? 0;
178
+ const y0 = player.position[1] ?? 0;
179
+ for (let i = 0; i < 12; i++) {
180
+ ctx.engine.step();
181
+ hitKnock = Math.max(hitKnock, x0 - (player.position[0] ?? 0));
182
+ hitLift = Math.max(hitLift, y0 - (player.position[1] ?? 0));
183
+ }
184
+ ctx.engine.input.setActionVector('move', 0, 0);
172
185
  },
173
186
  },
174
187
  ],
@@ -184,6 +197,10 @@ interface Player {
184
197
  scores.some((s) => s >= 50),
185
198
  );
186
199
  ok('a side hit cost a heart', heartsAfterHit < 3);
200
+ ok(
201
+ `…and kicked the knight ${hitKnock.toFixed(0)} px AWAY against a held stick, ${hitLift.toFixed(0)} px up (Health.knockback/knockUp/staggerSeconds)`,
202
+ hitKnock > 20 && hitLift > 10,
203
+ );
187
204
  ok('runScript reported no failures (coin/stomp/hit path)', result.ok);
188
205
  }
189
206
 
@@ -42,3 +42,10 @@
42
42
  - Spawning leans on the built-in `Spawner` + `Director` scatter (the engine's clone
43
43
  path is internal). Add enemy types by copying a template + spawner into the
44
44
  `Director.spawners` list.
45
+
46
+ ## Pausing
47
+
48
+ ESC pauses and shows the pause menu; OPTIONS opens the settings over it and
49
+ BACK returns to the menu that asked. All of it is `GameFlow` (`pausePanelPath`,
50
+ `screen(path)`, `back()`) wired with `connections` — there is no TypeScript for
51
+ any screen in this game.
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
- "incanto": "^0.74.0",
14
+ "incanto": "^0.76.0",
15
15
  "three": "^0.184.0"
16
16
  },
17
17
  "devDependencies": {
@@ -36,7 +36,7 @@ interface HealthLike {
36
36
  current: number;
37
37
  max: number;
38
38
  isDead: boolean;
39
- damage(n: number): void;
39
+ damage(n: number, from?: readonly number[]): void;
40
40
  heal(n: number): void;
41
41
  }
42
42
  interface ScoreLike {
@@ -50,7 +50,6 @@ const BOLT_SPEED = 520;
50
50
  const BOLT_LIFE = 0.9;
51
51
  const PICKUP_RADIUS = 30;
52
52
  const SURVIVE_SECONDS = 90;
53
- const HIT_KNOCK = 18; // px an enemy is shoved back when a bolt connects (hit feedback)
54
53
 
55
54
  interface Bolt {
56
55
  node: Node2D;
@@ -238,13 +237,15 @@ export class Survivor extends Behavior {
238
237
  if (dist2(nx, ny, en.position[0] ?? 0, en.position[1] ?? 0) < 26 * 26) {
239
238
  b.hit.add(e);
240
239
  // HIT FEEDBACK: damage (→ the enemy's EnemyFace flashes on 'damaged'),
241
- // shove it back along the bolt, and pop an impact spark at the contact.
242
- (e as unknown as { behavior?: HealthLike }).behavior?.damage?.(this.damage);
240
+ // and pop an impact spark at the contact. The shove back along the
241
+ // bolt is the enemy's `Health.knockback` now — the hit's source is a
242
+ // point one px behind it, opposite the bolt's travel, so the kick
243
+ // lands along the bolt exactly as the hand-written one did.
243
244
  const il = Math.hypot(b.vx, b.vy) || 1;
244
- en.position = [
245
- (en.position[0] ?? 0) + (b.vx / il) * HIT_KNOCK,
246
- (en.position[1] ?? 0) + (b.vy / il) * HIT_KNOCK,
247
- ];
245
+ (e as unknown as { behavior?: HealthLike }).behavior?.damage?.(this.damage, [
246
+ (en.position[0] ?? 0) - b.vx / il,
247
+ (en.position[1] ?? 0) - b.vy / il,
248
+ ]);
248
249
  this.spark(nx, ny);
249
250
  if (b.pierce <= 0) {
250
251
  dead = true;
@@ -16,7 +16,9 @@
16
16
  "right": ["KeyD", "ArrowRight"]
17
17
  },
18
18
  "touch": "joystick"
19
- }
19
+ },
20
+ "pause": { "type": "button", "keys": ["Escape"] },
21
+ "restart": { "type": "button", "keys": ["KeyR"] }
20
22
  },
21
23
  "assets": {
22
24
  "knight": {
@@ -108,7 +110,10 @@
108
110
  "visible": false,
109
111
  "collider": { "shape": "circle", "radius": 15 }
110
112
  },
111
- "script": { "name": "Health", "props": { "max": 3, "freeOnDeath": true } },
113
+ "script": {
114
+ "name": "Health",
115
+ "props": { "max": 3, "freeOnDeath": true, "knockback": 240, "staggerSeconds": 0.1 }
116
+ },
112
117
  "children": [
113
118
  {
114
119
  "name": "Skin",
@@ -185,7 +190,10 @@
185
190
  "visible": false,
186
191
  "collider": { "shape": "circle", "radius": 14 }
187
192
  },
188
- "script": { "name": "Health", "props": { "max": 5, "freeOnDeath": true } },
193
+ "script": {
194
+ "name": "Health",
195
+ "props": { "max": 5, "freeOnDeath": true, "knockback": 240, "staggerSeconds": 0.1 }
196
+ },
189
197
  "children": [
190
198
  {
191
199
  "name": "Skin",
@@ -303,6 +311,111 @@
303
311
  "type": "AudioPlayer",
304
312
  "props": { "preset": "lose", "bus": "sfx", "volume": 0.8 },
305
313
  "uid": "n_9c48ixvudwh3egqc"
314
+ },
315
+ {
316
+ "name": "Flow",
317
+ "type": "Node",
318
+ "uid": "n_k1b5tdq7qfgute05",
319
+ "script": { "name": "GameFlow" }
320
+ },
321
+ {
322
+ "name": "Hud",
323
+ "type": "HudLayer",
324
+ "uid": "n_t21l3ly0b78tuyhf",
325
+ "children": [
326
+ {
327
+ "name": "PauseMenu",
328
+ "type": "UiPanel",
329
+ "uid": "n_4zudk60s8o480dzn",
330
+ "props": {
331
+ "anchor": "center",
332
+ "layout": "column",
333
+ "gap": 12,
334
+ "padding": 22,
335
+ "visible": false,
336
+ "background": "rgba(10,14,22,0.92)",
337
+ "radius": 12
338
+ },
339
+ "children": [
340
+ {
341
+ "name": "PausedTitle",
342
+ "type": "UiText",
343
+ "uid": "n_jkw1i6abam6b2ich",
344
+ "props": { "text": "PAUSED", "size": 26 }
345
+ },
346
+ {
347
+ "name": "Resume",
348
+ "type": "UiButton",
349
+ "uid": "n_4xe73y1h6dfx33xx",
350
+ "props": { "text": "RESUME", "size": 18 }
351
+ },
352
+ {
353
+ "name": "PauseOptions",
354
+ "type": "UiButton",
355
+ "uid": "n_vld87yos9y2n1ktz",
356
+ "props": { "text": "OPTIONS", "size": 16 }
357
+ },
358
+ {
359
+ "name": "Restart",
360
+ "type": "UiButton",
361
+ "uid": "n_vwt3fbpsozqfdm80",
362
+ "props": { "text": "START OVER", "size": 16 }
363
+ }
364
+ ]
365
+ },
366
+ {
367
+ "name": "Options",
368
+ "type": "UiPanel",
369
+ "uid": "n_u1q9tf8qi4kd95xc",
370
+ "props": {
371
+ "anchor": "center",
372
+ "layout": "column",
373
+ "gap": 10,
374
+ "padding": 22,
375
+ "visible": false,
376
+ "background": "rgba(10,14,22,0.92)",
377
+ "radius": 12
378
+ },
379
+ "children": [
380
+ {
381
+ "name": "OptionsTitle",
382
+ "type": "UiText",
383
+ "uid": "n_9m4wm8glz3ah8rr3",
384
+ "props": { "text": "OPTIONS", "size": 24 }
385
+ },
386
+ {
387
+ "name": "Master",
388
+ "type": "UiVolumeSlider",
389
+ "uid": "n_0xk7mn0j15d0kaq2",
390
+ "props": { "label": "master", "bus": "master", "width": 260 }
391
+ },
392
+ {
393
+ "name": "Sfx",
394
+ "type": "UiVolumeSlider",
395
+ "uid": "n_fdvxcfvz0c2axgm9",
396
+ "props": { "label": "effects", "bus": "sfx", "width": 260 }
397
+ },
398
+ {
399
+ "name": "Mute",
400
+ "type": "UiMuteToggle",
401
+ "uid": "n_f8cpyypniznhbja3",
402
+ "props": { "label": "mute everything" }
403
+ },
404
+ {
405
+ "name": "Quality",
406
+ "type": "UiQualitySelect",
407
+ "uid": "n_d97o1nvn1ujgjfwe",
408
+ "props": { "label": "quality" }
409
+ },
410
+ {
411
+ "name": "Back",
412
+ "type": "UiButton",
413
+ "uid": "n_0c24jyr9ddbkk5vb",
414
+ "props": { "text": "BACK", "size": 16 }
415
+ }
416
+ ]
417
+ }
418
+ ]
306
419
  }
307
420
  ],
308
421
  "uid": "n_tlqu945vs98khzn8"
@@ -311,6 +424,31 @@
311
424
  { "signal": "died", "from": "Player/Vitals", "to": "/root", "handler": "loseLife" },
312
425
  { "signal": "damaged", "from": "Player/Vitals", "to": "SfxHurt", "handler": "play" },
313
426
  { "signal": "won", "from": "/root", "to": "SfxWin", "handler": "play" },
314
- { "signal": "lost", "from": "/root", "to": "SfxLose", "handler": "play" }
427
+ { "signal": "lost", "from": "/root", "to": "SfxLose", "handler": "play" },
428
+ {
429
+ "from": "/root/Hud/PauseMenu/Resume",
430
+ "signal": "pressed",
431
+ "to": "/root/Flow",
432
+ "handler": "resume"
433
+ },
434
+ {
435
+ "from": "/root/Hud/PauseMenu/PauseOptions",
436
+ "signal": "pressed",
437
+ "to": "/root/Flow",
438
+ "handler": "screen",
439
+ "args": ["/root/Hud/Options"]
440
+ },
441
+ {
442
+ "from": "/root/Hud/PauseMenu/Restart",
443
+ "signal": "pressed",
444
+ "to": "/root/Flow",
445
+ "handler": "restart"
446
+ },
447
+ {
448
+ "from": "/root/Hud/Options/Back",
449
+ "signal": "pressed",
450
+ "to": "/root/Flow",
451
+ "handler": "back"
452
+ }
315
453
  ]
316
454
  }
@@ -90,6 +90,7 @@ interface HealthB {
90
90
  {
91
91
  let kills = 0;
92
92
  let xpShards = 0;
93
+ let retreat = 0; // px a bolt shoved the goblin back from its closest approach (Health.knockback)
93
94
  const result = await runScript(sceneJson, {
94
95
  durationMs: 5000,
95
96
  seed: 2,
@@ -107,9 +108,20 @@ interface HealthB {
107
108
  };
108
109
  const enemy = spawner.spawn() as unknown as Vec | null;
109
110
  if (enemy) enemy.position = [(player.position[0] ?? 0) + 200, player.position[1] ?? 0];
111
+ let nearest = Number.POSITIVE_INFINITY;
110
112
  for (let i = 0; i < 260; i++) {
111
113
  if ((player.behavior as unknown as SurvivorB).kills >= 1) break;
112
114
  ctx.engine.step();
115
+ // a chaser only ever closes in; every step it is FURTHER than its
116
+ // closest approach so far is a hit that moved it
117
+ if (enemy && (enemy as unknown as { tree: unknown }).tree) {
118
+ const d = Math.hypot(
119
+ (enemy.position[0] ?? 0) - (player.position[0] ?? 0),
120
+ (enemy.position[1] ?? 0) - (player.position[1] ?? 0),
121
+ );
122
+ nearest = Math.min(nearest, d);
123
+ retreat = Math.max(retreat, d - nearest);
124
+ }
113
125
  }
114
126
  kills = (player.behavior as unknown as SurvivorB).kills;
115
127
  xpShards = ctx.scene.tree.getNodesInGroup('xp').length;
@@ -118,6 +130,7 @@ interface HealthB {
118
130
  ],
119
131
  });
120
132
  ok('the auto-attack killed an enemy', kills >= 1);
133
+ ok(`a bolt SHOVED the goblin back ${retreat.toFixed(1)} px (Health.knockback)`, retreat >= 10);
121
134
  ok('a dead enemy dropped an XP shard', xpShards >= 1);
122
135
  ok('runScript reported no failures (attack path)', result.ok);
123
136
  }
@@ -78,3 +78,9 @@ camera to 0.45 m (4.2 in the open); standing still on the lift for two seconds,
78
78
  and Cliff plateaus, the Lift and a double jump reach over the 3 m walls (24 playtests fell
79
79
  3 times before it, none after). `verify.ts` drops the player over Wall1 and asserts the catch.
80
80
 
81
+ ## Pausing
82
+
83
+ ESC pauses and shows the pause menu; OPTIONS opens the settings over it and
84
+ BACK returns to the menu that asked. All of it is `GameFlow` (`pausePanelPath`,
85
+ `screen(path)`, `back()`) wired with `connections` — there is no TypeScript for
86
+ any screen in this game.
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.74.0",
16
+ "incanto": "^0.76.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -809,22 +809,58 @@
809
809
  "uid": "n_ffjlx3r053emjjhc"
810
810
  },
811
811
  {
812
- "name": "ResumeHint",
812
+ "name": "Resume",
813
+ "type": "UiButton",
814
+ "uid": "n_ibf6agaxzcxjhcmg",
815
+ "props": { "text": "RESUME", "size": 18 }
816
+ },
817
+ {
818
+ "name": "PauseOptions",
819
+ "type": "UiButton",
820
+ "uid": "n_kdecsevdetjk0q1i",
821
+ "props": { "text": "OPTIONS", "size": 16 }
822
+ },
823
+ {
824
+ "name": "Restart",
825
+ "type": "UiButton",
826
+ "uid": "n_vaxmkxrqot30vd7u",
827
+ "props": { "text": "START OVER", "size": 16 }
828
+ }
829
+ ],
830
+ "uid": "n_8bcregfz1ajwi03l"
831
+ },
832
+ {
833
+ "name": "Options",
834
+ "type": "UiPanel",
835
+ "uid": "n_36ox1wee9cdz2cgj",
836
+ "props": {
837
+ "anchor": "center",
838
+ "layout": "column",
839
+ "gap": 10,
840
+ "padding": 22,
841
+ "visible": false,
842
+ "background": "rgba(10,14,22,0.92)",
843
+ "radius": 12
844
+ },
845
+ "children": [
846
+ {
847
+ "name": "OptionsTitle",
813
848
  "type": "UiText",
814
- "props": {
815
- "text": "Esc to resume · Enter to restart",
816
- "size": 13,
817
- "color": "#c9d2dd"
818
- },
819
- "uid": "n_atxlw44pqdn146zj"
849
+ "uid": "n_3nt1ztixpsvjtb86",
850
+ "props": { "text": "OPTIONS", "size": 24 }
820
851
  },
821
852
  { "name": "Volume", "type": "UiVolumeSlider", "uid": "n_5p4124fbnjxx8s0c" },
822
853
  { "name": "Mute", "type": "UiMuteToggle", "uid": "n_e1lyn3mvsy3vj2qr" },
823
854
  { "name": "Quality", "type": "UiQualitySelect", "uid": "n_35r1h71i75yft3ex" },
824
855
  { "name": "RenderScale", "type": "UiRenderScaleSelect", "uid": "n_emg840df89fqsriw" },
825
- { "name": "FrameCap", "type": "UiFrameCapSelect", "uid": "n_mgqwgghn0bx900ip" }
826
- ],
827
- "uid": "n_8bcregfz1ajwi03l"
856
+ { "name": "FrameCap", "type": "UiFrameCapSelect", "uid": "n_mgqwgghn0bx900ip" },
857
+ {
858
+ "name": "Back",
859
+ "type": "UiButton",
860
+ "uid": "n_0ytaknljuqmthwpl",
861
+ "props": { "text": "BACK", "size": 16 }
862
+ }
863
+ ]
828
864
  }
829
865
  ],
830
866
  "uid": "n_dimifr19c9et8cr4"
@@ -843,6 +879,31 @@
843
879
  "to": "Arena/Mine",
844
880
  "handler": "startTimer",
845
881
  "filter": { "group": "player" }
882
+ },
883
+ {
884
+ "from": "/root/HUD/PauseMenu/Resume",
885
+ "signal": "pressed",
886
+ "to": "/root/Flow",
887
+ "handler": "resume"
888
+ },
889
+ {
890
+ "from": "/root/HUD/PauseMenu/PauseOptions",
891
+ "signal": "pressed",
892
+ "to": "/root/Flow",
893
+ "handler": "screen",
894
+ "args": ["/root/HUD/Options"]
895
+ },
896
+ {
897
+ "from": "/root/HUD/PauseMenu/Restart",
898
+ "signal": "pressed",
899
+ "to": "/root/Flow",
900
+ "handler": "restart"
901
+ },
902
+ {
903
+ "from": "/root/HUD/Options/Back",
904
+ "signal": "pressed",
905
+ "to": "/root/Flow",
906
+ "handler": "back"
846
907
  }
847
908
  ]
848
909
  }
@@ -24,3 +24,10 @@
24
24
  ## Known limitations / ideas
25
25
 
26
26
  - A hurt/attack clip for the wolves once one lands in the animation catalog.
27
+
28
+ ## Pausing
29
+
30
+ ESC pauses and shows the pause menu; OPTIONS opens the settings over it and
31
+ BACK returns to the menu that asked. All of it is `GameFlow` (`pausePanelPath`,
32
+ `screen(path)`, `back()`) wired with `connections` — there is no TypeScript for
33
+ any screen in this game.
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.74.0",
16
+ "incanto": "^0.76.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {