incanto 0.14.0 → 0.16.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 (29) hide show
  1. package/dist/3d.d.ts +34 -0
  2. package/dist/3d.js +3 -3
  3. package/dist/{create-game-OSntcIiM.js → create-game-B5GWzy1E.js} +2 -109
  4. package/dist/index.js +1 -1
  5. package/dist/{physics-3d-D_kUdOjr.js → physics-3d-CDlvC9Z_.js} +1 -1
  6. package/dist/react.js +1 -1
  7. package/dist/{register-C9-16x_t.js → register-Lp2qn1Wq.js} +435 -37
  8. package/dist/test.js +3 -3
  9. package/editor/assets/{agent8-BDC4LhjC.js → agent8-Cw7igxDG.js} +1 -1
  10. package/editor/assets/{index-7qOYg7t8.js → index-N3Hnrv0w.js} +217 -118
  11. package/editor/index.html +1 -1
  12. package/package.json +1 -1
  13. package/schemas/scene.schema.json +25 -0
  14. package/skills/incanto-environment.md +14 -2
  15. package/skills/incanto-node-reference.md +6 -0
  16. package/templates-app/beacon-isle-3d/PROJECT/Status.md +1 -1
  17. package/templates-app/beacon-isle-3d/generate-world.ts +23 -18
  18. package/templates-app/beacon-isle-3d/package.json +1 -1
  19. package/templates-app/beacon-isle-3d/src/behaviors.ts +70 -4
  20. package/templates-app/beacon-isle-3d/src/game.scene.json +20 -21
  21. package/templates-app/tps-3d/package.json +1 -1
  22. package/templates-app/village-quest-3d/PROJECT/Context.md +1 -1
  23. package/templates-app/village-quest-3d/PROJECT/Requirements.md +1 -1
  24. package/templates-app/village-quest-3d/PROJECT/Structure.md +6 -1
  25. package/templates-app/village-quest-3d/generate-dressing.ts +769 -0
  26. package/templates-app/village-quest-3d/package.json +1 -1
  27. package/templates-app/village-quest-3d/src/behaviors.ts +74 -8
  28. package/templates-app/village-quest-3d/src/grove.scene.json +1 -13
  29. package/templates-app/village-quest-3d/src/village.scene.json +2989 -162
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-7qOYg7t8.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-N3Hnrv0w.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-D8QvwvOm.css">
11
11
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -6449,6 +6449,31 @@
6449
6449
  "underwater": {
6450
6450
  "type": "boolean",
6451
6451
  "default": true
6452
+ },
6453
+ "swell": {
6454
+ "type": "number",
6455
+ "default": 0
6456
+ },
6457
+ "swellDirectionDeg": {
6458
+ "type": "number",
6459
+ "default": 0
6460
+ },
6461
+ "swellWavelength": {
6462
+ "type": "number",
6463
+ "default": 30
6464
+ },
6465
+ "whitecaps": {
6466
+ "type": "number",
6467
+ "default": 0
6468
+ },
6469
+ "preset": {
6470
+ "type": "string",
6471
+ "enum": ["custom", "ocean", "pool", "lake", "pond"],
6472
+ "default": "custom"
6473
+ },
6474
+ "causticsAbove": {
6475
+ "type": "number",
6476
+ "default": 0
6452
6477
  }
6453
6478
  },
6454
6479
  "additionalProperties": false
@@ -608,8 +608,14 @@ non-default composition.
608
608
  ## Water3D
609
609
 
610
610
  A commercial-quality water surface (in `incanto/3d`, registered by
611
- `registerNodes3D`). The default `quality: 'fancy'` is a real water shader:
612
- 9-iteration simplex-FBM wave displacement with analytic normals, THREE
611
+ `registerNodes3D`). **Four curated water types are one prop away:**
612
+ `preset: 'ocean' | 'pool' | 'lake' | 'pond'` — see the prop table; the four
613
+ `examples/water-*-3d` templates stage one each (cove pier, resort pool,
614
+ mirror-lake dock, walled garden pond). The default `quality: 'fancy'` is a
615
+ real water shader:
616
+ 9-iteration simplex-FBM wave displacement with analytic normals, optional
617
+ DIRECTIONAL TRAVELING SWELL trains + open-water whitecaps (`swell` /
618
+ `whitecaps` — the open-ocean look; see the prop table), THREE
613
619
  animated ANISOTROPIC detail-normal layers — wind-stretched (~2.8× along the
614
620
  wind), ridge-sharpened fine ripple in crossing directions, each layer
615
621
  distance-faded at its own range so the far field goes glassy instead of
@@ -668,6 +674,12 @@ hue reads grey.
668
674
  | `detailStrength` | `0.3` | animated detail-normal strength (≥ 0; 0 = vertex normals only) |
669
675
  | `absorption` | `0.15` | Beer's-law constant per meter of water depth (≥ 0; lower = clearer/wider turquoise band; red absorbs ~3× faster than blue under the hood) |
670
676
  | `refraction` | `true` | screen-space refraction of the submerged scene |
677
+ | `preset` | `"custom"` | **THE one-choice water type** (0.16.0): `ocean` \| `pool` \| `lake` \| `pond` bundles every wave/color/clarity/caustics knob into a hand-tuned sea state. A preset value applies ONLY where a prop still sits at its schema default — pick the type, then override any detail prop and your value wins. ocean = traveling swell + whitecaps + wide turquoise→deep absorption · pool = near-flat crystal water + strong above-water caustics · lake = calm high-reflectivity mirror, green-teal murk · pond = still, mossy, opaque quickly. `custom` (default) changes nothing |
678
+ | `causticsAbove` | `0` | above-water CAUSTICS intensity 0–3 (fancy + `refraction` only): the dancing light web on the submerged bottom seen THROUGH the surface — the pool / tropical-shallows look. Reconstructed on the real refracted bottom position, strongest through the first meters of water, distance-faded. Presets set it (pool 1.0); override freely |
679
+ | `swell` | `0` | **THE open-ocean dial** (fancy only, 0.15.0): directional traveling swell amplitude in meters. Three peaked wave trains (primary + two crossing sets) march across the surface at real deep-water speeds; crests light up toward the peak color and wave FACES get sun-lit/shadow shading, so the swell reads at any distance. 0.15 = lively lake chop, 0.4 = coastal sea, 0.7 = heavy weather. `0` (default) keeps the legacy calm look byte-identical |
680
+ | `swellDirectionDeg` | `0` | compass direction the swell TRAVELS toward (0 = +z, 90 = +x) |
681
+ | `swellWavelength` | `30` | primary swell wavelength, meters (≥ 2, load-time check); the crossing trains derive from it |
682
+ | `whitecaps` | `0` | open-water whitecap foam on the tallest crests, 0–1 (fancy only; load-time check). Pairs with `swell` — the caps ride its crest lines and fade with distance so the horizon stays clean |
671
683
  | `underwater` | `true` | submerged-camera look: ONLY when the camera EYE is below this surface (inside its XZ footprint) — wading with the camera above the water never tints the view — the scene switches to short-range underwater fog + tint, the sky is hidden, and animated **caustics** (depth-aware, projected onto the submerged floor/props) play. `false` disables it; an object `{ color?, visibility?, caustics? }` overrides the murk hue / view distance (m, default 22) / caustics. `caustics` is `true` (default) \| `false` \| `{ color?, intensity?, scale?, speed? }`. The murk color defaults to a darkened shade of `color`. Works for both qualities (the caustics composite runs only while the camera is genuinely underwater) |
672
684
 
673
685
  Performance notes (fancy): beyond drawing the surface, each rendered frame
@@ -953,6 +953,12 @@ Signals: `blocksChanged`
953
953
  | `absorption` | `0.15` | number |
954
954
  | `refraction` | `true` | boolean |
955
955
  | `underwater` | `true` | boolean |
956
+ | `swell` | `0` | number |
957
+ | `swellDirectionDeg` | `0` | number |
958
+ | `swellWavelength` | `30` | number |
959
+ | `whitecaps` | `0` | number |
960
+ | `preset` | `"custom"` | one of: `custom` `ocean` `pool` `lake` `pond` |
961
+ | `causticsAbove` | `0` | number |
956
962
 
957
963
  Signals: `entered(body)` · `exited(body)`
958
964
 
@@ -11,7 +11,7 @@
11
11
  dusk start → each lit ward buys back light → the boss drags the isle to a
12
12
  deep-red horizon (17.8 — sun kept ABOVE the horizon; below it the
13
13
  atmosphere sky and its IBL go black) → victory brings morning.
14
- - **Gliding**: hold JUMP while falling — capped descent + dash trail.
14
+ - **Gliding**: hold JUMP while falling — capped descent; landing thumps a dust puff.
15
15
  - Keeper NPC tracks the player with `BoneLookAt3D`; shoreline rocks are one
16
16
  `InstancedMesh3D` draw call.
17
17
 
@@ -178,7 +178,7 @@ const ward = (i: number, [x, y, z]: [number, number, number]): unknown => ({
178
178
  type: 'MeshInstance3D',
179
179
  props: {
180
180
  mesh: 'cylinder',
181
- size: [1.6, 1.2, 1.6],
181
+ size: [0.8, 1.2, 0.8], // RADIUS (cylinder size[0]) = collider box half-extent
182
182
  material: { color: '#7d8494', roughness: 0.9 },
183
183
  castShadow: true,
184
184
  receiveShadow: true,
@@ -317,7 +317,7 @@ const scene = {
317
317
  fog: { near: 110, far: 560 },
318
318
  shadows: { mapSize: 2048, radius: 1.2 },
319
319
  exposure: 1.12,
320
- ambient: { color: '#ffe8d4', intensity: 0.66 },
320
+ ambient: { color: '#ffe8d4', intensity: 0.85 },
321
321
  iblIntensity: 0.9,
322
322
  bloom: { threshold: 1.2, strength: 0.55 },
323
323
  post: { vignette: 0.28, saturation: 1.12, contrast: 1.02 },
@@ -380,7 +380,24 @@ const scene = {
380
380
  // must keep animating — auditScene flags exactly this mistake.
381
381
  children: (terrain.children ?? []).map((c) =>
382
382
  c.type === 'Water3D' || c.name === 'Clouds'
383
- ? c
383
+ ? c.name === 'Sea'
384
+ ? {
385
+ ...c,
386
+ // the OPEN-OCEAN dial (0.15.0): directional swell trains +
387
+ // whitecaps + deeper absorption — the sea moves, banded and
388
+ // alive, instead of a flat noise sheet
389
+ props: {
390
+ ...(c.props ?? {}),
391
+ swell: 0.55,
392
+ swellWavelength: 42,
393
+ swellDirectionDeg: 262,
394
+ whitecaps: 0.45,
395
+ absorption: 0.28,
396
+ reflectionInterval: 400,
397
+ colors: { trough: '#0c3d58', surface: '#1a7d9c', peak: '#9adfe0' },
398
+ },
399
+ }
400
+ : c
384
401
  : { ...c, props: { ...(c.props ?? {}), static: true } },
385
402
  ),
386
403
  },
@@ -407,7 +424,7 @@ const scene = {
407
424
  type: 'MeshInstance3D',
408
425
  props: {
409
426
  mesh: 'cylinder',
410
- size: [3, 11, 3],
427
+ size: [1.5, 11, 1.5], // RADIUS = collider half-extent (was 3 → camera clipped inside the 1.5 m shell)
411
428
  material: { color: '#e8e2d4', roughness: 0.8 },
412
429
  castShadow: true,
413
430
  receiveShadow: true,
@@ -418,7 +435,7 @@ const scene = {
418
435
  type: 'MeshInstance3D',
419
436
  props: {
420
437
  mesh: 'cylinder',
421
- size: [3.06, 1.6, 3.06],
438
+ size: [1.56, 1.6, 1.56],
422
439
  position: [0, 1.2, 0],
423
440
  material: { color: '#b8452e', roughness: 0.8 },
424
441
  },
@@ -582,18 +599,6 @@ const scene = {
582
599
  },
583
600
  ],
584
601
  },
585
- {
586
- name: 'DashTrail',
587
- type: 'Trail3D',
588
- props: {
589
- width: 0.5,
590
- color: '#ffffff',
591
- opacity: 0.28,
592
- seconds: 0.45,
593
- emitting: false,
594
- position: [0, -0.4, 0],
595
- },
596
- },
597
602
  {
598
603
  name: 'SwingSfx',
599
604
  type: 'AudioPlayer',
@@ -607,7 +612,7 @@ const scene = {
607
612
  type: 'Node',
608
613
  script: {
609
614
  name: 'DayNight',
610
- props: { paused: true, startHour: 17.5, nightDarkness: 0.42, maxSunElevationDeg: 48 },
615
+ props: { paused: true, startHour: 17.25, nightDarkness: 0.32, maxSunElevationDeg: 48 },
611
616
  },
612
617
  },
613
618
  {
@@ -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.14.0",
17
+ "incanto": "^0.16.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -21,7 +21,7 @@ import type {
21
21
  Voice,
22
22
  } from 'incanto';
23
23
  import { Behavior, findPath } from 'incanto';
24
- import type { MeshInstance3D, Node3D, Terrain3D, Trail3D } from 'incanto/3d';
24
+ import type { Camera3D, MeshInstance3D, Node3D, Terrain3D, Trail3D } from 'incanto/3d';
25
25
  import { buildTerrainNav, Particles3D, type TerrainNav } from 'incanto/3d';
26
26
  import { type DayNight, goToScene } from 'incanto/gameplay';
27
27
  import gameJsonRaw from './game.scene.json';
@@ -54,6 +54,39 @@ const PATROL_SPEED = 2.6;
54
54
  const HUNT_SPEED = 3.6;
55
55
  const WARD_LIGHT_RANGE = 4;
56
56
 
57
+ /** Sprint feel — see sprintFeel(): FOV kick + per-stride dust. */
58
+ const FOV_BASE = 60;
59
+ const FOV_SPRINT = 66.5;
60
+ const STRIDE_SECONDS = 0.22;
61
+
62
+ /**
63
+ * One kicked-up earth puff, world-anchored at the foot spot (one-shot,
64
+ * self-removing). Non-additive expanding dust — NOT a glow: it reads as
65
+ * dirt leaving the ground, then thins out.
66
+ */
67
+ function spawnFootDust(root: Node, at: [number, number, number], scale = 1): void {
68
+ const puff = new Particles3D('FootDust');
69
+ puff.position = [at[0], at[1], at[2]];
70
+ puff.rate = 0;
71
+ puff.burst = Math.round(4 * scale);
72
+ puff.blend = 'normal';
73
+ puff.lifetime = [0.3, 0.5];
74
+ puff.speed = [8, 34];
75
+ puff.directionDeg = -90; // up out of the ground…
76
+ puff.spreadDeg = 75;
77
+ puff.gravity = [0, 30]; // …then settle back down
78
+ puff.drag = 2.5;
79
+ puff.sizeStart = 11 * scale;
80
+ puff.sizeEnd = 30 * scale; // dust EXPANDS as it dissipates
81
+ puff.colorStart = '#c2b394'; // dry peat kicked into the dusk light
82
+ puff.colorEnd = '#8a7c66';
83
+ puff.alphaStart = 0.24;
84
+ puff.alphaEnd = 0;
85
+ puff.maxParticles = 24;
86
+ puff.on('finished', () => puff.free());
87
+ root.addChild(puff);
88
+ }
89
+
57
90
  export class IsleDirector extends Behavior {
58
91
  private wind: Voice | null = null;
59
92
  private nav: TerrainNav | null = null;
@@ -63,6 +96,9 @@ export class IsleDirector extends Behavior {
63
96
  private bobClock = 0;
64
97
  private fireworksIn = -1;
65
98
  private glideActive = false;
99
+ private strideClock = 0;
100
+ private wasAirborne = false;
101
+ private fov = FOV_BASE;
66
102
 
67
103
  private get dialogue(): UiDialogue {
68
104
  return this.node.getNode('HUD/Dialogue') as UiDialogue;
@@ -139,6 +175,37 @@ export class IsleDirector extends Behavior {
139
175
  }
140
176
  }
141
177
 
178
+ /**
179
+ * Sprint FEEL — speed you can see instead of a ribbon glued to the hips:
180
+ * - a soft camera FOV kick while at full sprint (eased both ways)
181
+ * - kicked-up earth at the feet, one puff per stride
182
+ * - a bigger thump-puff on landing after airtime (jump or glide)
183
+ */
184
+ private sprintFeel(dt: number, state: string): void {
185
+ const camera = this.node.getNodeOrNull('Camera') as Camera3D | null;
186
+ if (camera) {
187
+ const target = state === 'fastRun' ? FOV_SPRINT : FOV_BASE;
188
+ this.fov += (target - this.fov) * Math.min(1, dt * 6);
189
+ camera.fov = this.fov;
190
+ }
191
+ const player = this.node.getNodeOrNull('Player') as (Node3D & Positioned) | null;
192
+ if (!player) return;
193
+ const [px, py, pz] = player.position;
194
+ const feet: [number, number, number] = [px ?? 0, (py ?? 0) - 0.62, pz ?? 0];
195
+ const airborne = state === 'airborne';
196
+ if (this.wasAirborne && !airborne) spawnFootDust(this.node, feet, 1.7); // landing thump
197
+ this.wasAirborne = airborne;
198
+ if (state === 'fastRun') {
199
+ this.strideClock -= dt;
200
+ if (this.strideClock <= 0) {
201
+ this.strideClock = STRIDE_SECONDS;
202
+ spawnFootDust(this.node, feet);
203
+ }
204
+ } else {
205
+ this.strideClock = 0;
206
+ }
207
+ }
208
+
142
209
  private ambient(dt: number, engine: Engine | null | undefined): void {
143
210
  const controller = this.node.getNode('Player/Controller') as unknown as {
144
211
  state: string;
@@ -152,8 +219,7 @@ export class IsleDirector extends Behavior {
152
219
  : { pitch: 0.85, volume: 0.2 };
153
220
  this.wind.set(w);
154
221
  }
155
- const dash = this.node.getNodeOrNull('Player/DashTrail') as Trail3D | null;
156
- if (dash && controller) dash.emitting = controller.state === 'fastRun';
222
+ if (controller) this.sprintFeel(dt, controller.state);
157
223
  // shades hover-bob and face their travel
158
224
  this.bobClock += dt;
159
225
  for (const node of this.node.tree?.getNodesInGroup('shade') ?? []) {
@@ -241,7 +307,7 @@ export class IsleDirector extends Behavior {
241
307
  ? 8.7
242
308
  : quest.bossRisen
243
309
  ? 17.8 // sun ~+2 deg — deep red horizon, still lit (below 18 the sky dies)
244
- : 17.5 - quest.lit.size * 0.35; // each ward buys back the light
310
+ : 17.25 - quest.lit.size * 0.4; // each ward buys back the light
245
311
  sky.hour += (targetHour - sky.hour) * Math.min(1, dt * 0.4);
246
312
  }
247
313
 
@@ -22,7 +22,7 @@
22
22
  "exposure": 1.12,
23
23
  "ambient": {
24
24
  "color": "#ffe8d4",
25
- "intensity": 0.66
25
+ "intensity": 0.85
26
26
  },
27
27
  "iblIntensity": 0.9,
28
28
  "bloom": {
@@ -138,7 +138,18 @@
138
138
  "props": {
139
139
  "size": [1920, 1920],
140
140
  "position": [0, 9.19, 0],
141
- "opacity": 1
141
+ "opacity": 1,
142
+ "swell": 0.55,
143
+ "swellWavelength": 42,
144
+ "swellDirectionDeg": 262,
145
+ "whitecaps": 0.45,
146
+ "absorption": 0.28,
147
+ "reflectionInterval": 400,
148
+ "colors": {
149
+ "trough": "#0c3d58",
150
+ "surface": "#1a7d9c",
151
+ "peak": "#9adfe0"
152
+ }
142
153
  }
143
154
  },
144
155
  {
@@ -1451,7 +1462,7 @@
1451
1462
  "type": "MeshInstance3D",
1452
1463
  "props": {
1453
1464
  "mesh": "cylinder",
1454
- "size": [3, 11, 3],
1465
+ "size": [1.5, 11, 1.5],
1455
1466
  "material": {
1456
1467
  "color": "#e8e2d4",
1457
1468
  "roughness": 0.8
@@ -1465,7 +1476,7 @@
1465
1476
  "type": "MeshInstance3D",
1466
1477
  "props": {
1467
1478
  "mesh": "cylinder",
1468
- "size": [3.06, 1.6, 3.06],
1479
+ "size": [1.56, 1.6, 1.56],
1469
1480
  "position": [0, 1.2, 0],
1470
1481
  "material": {
1471
1482
  "color": "#b8452e",
@@ -1602,7 +1613,7 @@
1602
1613
  "type": "MeshInstance3D",
1603
1614
  "props": {
1604
1615
  "mesh": "cylinder",
1605
- "size": [1.6, 1.2, 1.6],
1616
+ "size": [0.8, 1.2, 0.8],
1606
1617
  "material": {
1607
1618
  "color": "#7d8494",
1608
1619
  "roughness": 0.9
@@ -1690,7 +1701,7 @@
1690
1701
  "type": "MeshInstance3D",
1691
1702
  "props": {
1692
1703
  "mesh": "cylinder",
1693
- "size": [1.6, 1.2, 1.6],
1704
+ "size": [0.8, 1.2, 0.8],
1694
1705
  "material": {
1695
1706
  "color": "#7d8494",
1696
1707
  "roughness": 0.9
@@ -1778,7 +1789,7 @@
1778
1789
  "type": "MeshInstance3D",
1779
1790
  "props": {
1780
1791
  "mesh": "cylinder",
1781
- "size": [1.6, 1.2, 1.6],
1792
+ "size": [0.8, 1.2, 0.8],
1782
1793
  "material": {
1783
1794
  "color": "#7d8494",
1784
1795
  "roughness": 0.9
@@ -2667,18 +2678,6 @@
2667
2678
  }
2668
2679
  ]
2669
2680
  },
2670
- {
2671
- "name": "DashTrail",
2672
- "type": "Trail3D",
2673
- "props": {
2674
- "width": 0.5,
2675
- "color": "#ffffff",
2676
- "opacity": 0.28,
2677
- "seconds": 0.45,
2678
- "emitting": false,
2679
- "position": [0, -0.4, 0]
2680
- }
2681
- },
2682
2681
  {
2683
2682
  "name": "SwingSfx",
2684
2683
  "type": "AudioPlayer",
@@ -2705,8 +2704,8 @@
2705
2704
  "name": "DayNight",
2706
2705
  "props": {
2707
2706
  "paused": true,
2708
- "startHour": 17.5,
2709
- "nightDarkness": 0.42,
2707
+ "startHour": 17.25,
2708
+ "nightDarkness": 0.32,
2710
2709
  "maxSunElevationDeg": 48
2711
2710
  }
2712
2711
  }
@@ -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.14.0",
16
+ "incanto": "^0.16.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -19,7 +19,7 @@ again button.
19
19
  | `UiDialogue` + choices | the Elder conversation IS the quest state machine |
20
20
  | `UiButton` | the play-again button |
21
21
  | `findPath` + `PathFollow` | wolf patrol rings through the hedge maze |
22
- | `Trail3D` | sword arc + sprint dash trail |
22
+ | `Trail3D` | sword arc |
23
23
  | `engine.sfx.startVoice('wind')` | live ambience that rises as you run |
24
24
  | `goToScene` | village ⇄ grove, with the black fade |
25
25
  | controller `animations` map | idle/walk/run/sprint/jump — zero animation code |
@@ -26,7 +26,7 @@
26
26
  ## Feel
27
27
 
28
28
  - Wind ambience always present, louder + higher while sprinting.
29
- - Dash trail only while sprinting.
29
+ - Sprint FEEL: per-stride foot-dust puffs + a soft camera FOV kick (no body ribbon).
30
30
  - Village = warm sunset; grove = cold dusk, denser fog, moonlight.
31
31
 
32
32
  ## Verification (must stay green)
@@ -4,11 +4,16 @@
4
4
  index.html canvas + loading bar
5
5
  src/main.ts preload GLB/anim assets → createGame3D (village)
6
6
  src/village.scene.json sunset village: Elder (Interactable), gate Area3D,
7
- player rig (controller `animations` map, Trail3D ×2),
7
+ player rig (controller `animations` map, sword Trail3D),
8
8
  HUD (UiText/UiBanner/UiDialogue/UiButton)
9
9
  src/grove.scene.json dusk grove: 3 wolves (PathFollow), return portal,
10
10
  empty `Maze` holder (hedges are built at runtime)
11
11
  src/behaviors.ts GROVE_GRID + VillageDirector / GroveDirector / SwordStrike
12
+ generate-dressing.ts VILLAGE dressing generator: rebuilds environment + the
13
+ visual `Village` subtree (timber cottages, fences,
14
+ lanterns, stall, grass/flowers, treelines) — edit
15
+ numbers, `bun generate-dressing.ts`, reload. Gameplay
16
+ nodes (Elder/Player/HUD/GroveGate) are preserved.
12
17
  src/quest.ts cross-scene quest flags (module singleton)
13
18
  verify.ts audit + headless quest E2E + replay determinism
14
19
  ```