incanto 0.13.0 → 0.15.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/dist/3d.d.ts +40 -11
- package/dist/3d.js +3 -3
- package/dist/{create-game-J6g4QInv.js → create-game-BS-K-GY1.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{physics-3d-DhaAaKJo.js → physics-3d-TmOuCy7J.js} +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-BFw4c83z.js → register-Bh_3Z39N.js} +164 -39
- package/dist/test.js +3 -3
- package/editor/assets/{agent8-BF8Jfbkl.js → agent8-DqQ2CQ5A.js} +1 -1
- package/editor/assets/{index-DSq0gdhE.js → index-CofH0yGY.js} +79 -7
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +19 -6
- package/skills/incanto-environment.md +22 -17
- package/skills/incanto-node-reference.md +7 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +1 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +23 -20
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/behaviors.ts +70 -4
- package/templates-app/beacon-isle-3d/src/game.scene.json +43 -67
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/PROJECT/Context.md +1 -1
- package/templates-app/village-quest-3d/PROJECT/Requirements.md +1 -1
- package/templates-app/village-quest-3d/PROJECT/Structure.md +6 -1
- package/templates-app/village-quest-3d/generate-dressing.ts +769 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +74 -8
- package/templates-app/village-quest-3d/src/grove.scene.json +1 -13
- 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-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-CofH0yGY.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
|
@@ -2371,8 +2371,7 @@
|
|
|
2371
2371
|
"default": 0.5
|
|
2372
2372
|
},
|
|
2373
2373
|
"drape": {
|
|
2374
|
-
"
|
|
2375
|
-
"default": false
|
|
2374
|
+
"default": null
|
|
2376
2375
|
},
|
|
2377
2376
|
"terrain": {
|
|
2378
2377
|
"type": "string",
|
|
@@ -2559,8 +2558,7 @@
|
|
|
2559
2558
|
"default": 1
|
|
2560
2559
|
},
|
|
2561
2560
|
"drape": {
|
|
2562
|
-
"
|
|
2563
|
-
"default": false
|
|
2561
|
+
"default": null
|
|
2564
2562
|
},
|
|
2565
2563
|
"terrain": {
|
|
2566
2564
|
"type": "string",
|
|
@@ -5656,8 +5654,7 @@
|
|
|
5656
5654
|
"default": true
|
|
5657
5655
|
},
|
|
5658
5656
|
"drape": {
|
|
5659
|
-
"
|
|
5660
|
-
"default": false
|
|
5657
|
+
"default": null
|
|
5661
5658
|
},
|
|
5662
5659
|
"terrain": {
|
|
5663
5660
|
"type": "string",
|
|
@@ -6452,6 +6449,22 @@
|
|
|
6452
6449
|
"underwater": {
|
|
6453
6450
|
"type": "boolean",
|
|
6454
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
|
|
6455
6468
|
}
|
|
6456
6469
|
},
|
|
6457
6470
|
"additionalProperties": false
|
|
@@ -358,14 +358,13 @@ tops radially away with a smooth falloff as a character wades through.
|
|
|
358
358
|
| `coverage` | `0.85` | mesh: carpet fill in (0, 1] — below 1 noise carves dirt patches (load-time check) |
|
|
359
359
|
| `flowers` | `0` | DEPRECATED — confetti heads; use a `Flowers3D` node (load-time check 0–0.2) |
|
|
360
360
|
| `seed` | `1` | placement seed — identical field across runs/machines |
|
|
361
|
-
| `drape` | `
|
|
362
|
-
| `terrain` | `""` | drape target node path; empty = **auto-find the first Terrain3D** in the tree
|
|
361
|
+
| `drape` | `null` | **AUTO by default**: whenever the scene has a Terrain3D, every blade roots on the ground under it (samples `heightAt`) — omit this prop and rolling terrain just works. `true` forces it (keeps retrying until a terrain appears — streamed worlds), `false` opts out (flat carpet at the node's Y) |
|
|
362
|
+
| `terrain` | `""` | drape target node path; empty = **auto-find the first Terrain3D** in the tree. A non-empty path that is NOT a Terrain3D fails at load — no silent floating |
|
|
363
363
|
|
|
364
|
-
> **Rolling terrain
|
|
365
|
-
>
|
|
366
|
-
>
|
|
367
|
-
>
|
|
368
|
-
> generators still do the latter for backward-compatible output).
|
|
364
|
+
> **Rolling terrain just works.** Since 0.14 draping is on by default: with a
|
|
365
|
+
> Terrain3D in the scene the blades root on the ground per-instance, no wiring.
|
|
366
|
+
> Only set `drape: false` if you WANT a flat carpet at the node's Y (e.g. grass
|
|
367
|
+
> on a flat MeshInstance3D platform above the terrain).
|
|
369
368
|
|
|
370
369
|
## Flowers3D
|
|
371
370
|
|
|
@@ -415,8 +414,8 @@ billboard confetti. Deterministic from the `seed` PROP.
|
|
|
415
414
|
| `height` | `0.45` | plant height, meters (±25% per-plant jitter) |
|
|
416
415
|
| `clustering` | `0.6` | 0 uniform … 1 tight Voronoi patches (load-time check) |
|
|
417
416
|
| `sway` | `0.5` | head-bob wind strength; 0 disables |
|
|
418
|
-
| `drape` | `
|
|
419
|
-
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D |
|
|
417
|
+
| `drape` | `null` | AUTO by default — plants root on the scene's Terrain3D when one exists; `true` forces, `false` opts out — see the Foliage3D drape note above |
|
|
418
|
+
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
|
|
420
419
|
|
|
421
420
|
## Tree3D
|
|
422
421
|
Procedural trees grown by the ported ez-tree generator (MIT, Dan Greenheck;
|
|
@@ -484,8 +483,8 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
|
|
|
484
483
|
| `leafFadeStart` | `0` | **leaf LOD** — leaf cards begin collapsing toward their branch at this camera distance (m). `0`/`0` disables (full leaves at any range) |
|
|
485
484
|
| `leafFadeEnd` | `0` | leaf LOD — cards fully collapsed (zero overdraw) by here; tune ≈ the scene `fog.far` so the thinning hides in fog (load-time check: `> leafFadeStart`) |
|
|
486
485
|
| `leafShadows` | `true` | `false` keeps trunk/branch shadows but drops the expensive alpha-cutout LEAF shadow pass — big FPS in dense forests |
|
|
487
|
-
| `drape` | `
|
|
488
|
-
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (
|
|
486
|
+
| `drape` | `null` | AUTO by default: each scattered instance roots at the ground height under it (instead of the node's single Y) whenever the scene has a Terrain3D — a `count > 1` grove on rolling/carved terrain never floats or buries. `true` forces, `false` opts out |
|
|
487
|
+
| `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
|
|
489
488
|
|
|
490
489
|
> **Tree LOD / forest FPS.** Leaf overdraw + the leaf shadow pass dominate a
|
|
491
490
|
> dense-forest frame (~90% in profiling). For big groves set `leafFadeStart`/
|
|
@@ -493,11 +492,11 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
|
|
|
493
492
|
> leaf cards (branch silhouette stays), and `leafShadows: false` to drop the leaf
|
|
494
493
|
> shadow pass. Both default off → existing scenes are unchanged.
|
|
495
494
|
|
|
496
|
-
> **Floating trees?**
|
|
497
|
-
>
|
|
498
|
-
>
|
|
499
|
-
>
|
|
500
|
-
>
|
|
495
|
+
> **Floating trees?** Since 0.14 groves drape automatically whenever the scene
|
|
496
|
+
> has a Terrain3D — each instance roots on the ground under it. The drape is a
|
|
497
|
+
> pure heightAt lookup — no rng draw, so the seed/grove layout is
|
|
498
|
+
> byte-identical, only the Y shifts. `drape: false` restores the flat
|
|
499
|
+
> single-Y placement.
|
|
501
500
|
|
|
502
501
|
## Terrain3D
|
|
503
502
|
Procedural heightfield terrain with biome texture splatting (in `incanto/3d`,
|
|
@@ -610,7 +609,9 @@ non-default composition.
|
|
|
610
609
|
|
|
611
610
|
A commercial-quality water surface (in `incanto/3d`, registered by
|
|
612
611
|
`registerNodes3D`). The default `quality: 'fancy'` is a real water shader:
|
|
613
|
-
9-iteration simplex-FBM wave displacement with analytic normals,
|
|
612
|
+
9-iteration simplex-FBM wave displacement with analytic normals, optional
|
|
613
|
+
DIRECTIONAL TRAVELING SWELL trains + open-water whitecaps (`swell` /
|
|
614
|
+
`whitecaps` — the open-ocean look; see the prop table), THREE
|
|
614
615
|
animated ANISOTROPIC detail-normal layers — wind-stretched (~2.8× along the
|
|
615
616
|
wind), ridge-sharpened fine ripple in crossing directions, each layer
|
|
616
617
|
distance-faded at its own range so the far field goes glassy instead of
|
|
@@ -669,6 +670,10 @@ hue reads grey.
|
|
|
669
670
|
| `detailStrength` | `0.3` | animated detail-normal strength (≥ 0; 0 = vertex normals only) |
|
|
670
671
|
| `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) |
|
|
671
672
|
| `refraction` | `true` | screen-space refraction of the submerged scene |
|
|
673
|
+
| `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 |
|
|
674
|
+
| `swellDirectionDeg` | `0` | compass direction the swell TRAVELS toward (0 = +z, 90 = +x) |
|
|
675
|
+
| `swellWavelength` | `30` | primary swell wavelength, meters (≥ 2, load-time check); the crossing trains derive from it |
|
|
676
|
+
| `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 |
|
|
672
677
|
| `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) |
|
|
673
678
|
|
|
674
679
|
Performance notes (fancy): beyond drawing the surface, each rendered frame
|
|
@@ -330,7 +330,7 @@ Signals: `movementStateChanged(state)`
|
|
|
330
330
|
| `height` | `0.45` | number |
|
|
331
331
|
| `clustering` | `0.6` | number |
|
|
332
332
|
| `sway` | `0.5` | number |
|
|
333
|
-
| `drape` | `
|
|
333
|
+
| `drape` | `null` | null |
|
|
334
334
|
| `terrain` | `""` | string |
|
|
335
335
|
|
|
336
336
|
## `Foliage3D` — `incanto/3d`
|
|
@@ -362,7 +362,7 @@ Signals: `movementStateChanged(state)`
|
|
|
362
362
|
| `coverage` | `0.85` | number |
|
|
363
363
|
| `flowers` | `0` | number |
|
|
364
364
|
| `seed` | `1` | number |
|
|
365
|
-
| `drape` | `
|
|
365
|
+
| `drape` | `null` | null |
|
|
366
366
|
| `terrain` | `""` | string |
|
|
367
367
|
|
|
368
368
|
## `HudLayer` — `incanto`
|
|
@@ -836,7 +836,7 @@ Signals: `timeout`
|
|
|
836
836
|
| `leafFadeStart` | `0` | number |
|
|
837
837
|
| `leafFadeEnd` | `0` | number |
|
|
838
838
|
| `leafShadows` | `true` | boolean |
|
|
839
|
-
| `drape` | `
|
|
839
|
+
| `drape` | `null` | null |
|
|
840
840
|
| `terrain` | `""` | string |
|
|
841
841
|
|
|
842
842
|
## `UILayer` — `incanto/2d`
|
|
@@ -953,6 +953,10 @@ 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 |
|
|
956
960
|
|
|
957
961
|
Signals: `entered(body)` · `exited(body)`
|
|
958
962
|
|
|
@@ -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
|
|
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
|
|
|
@@ -101,7 +101,6 @@ for (let i = 0; i < 400 && grassPlaced < 14; i++) {
|
|
|
101
101
|
position: [round2(x), round2(y), round2(z)],
|
|
102
102
|
area: [34, 34],
|
|
103
103
|
density: 10,
|
|
104
|
-
terrain: '../../Ground/Surface',
|
|
105
104
|
},
|
|
106
105
|
});
|
|
107
106
|
}
|
|
@@ -119,7 +118,6 @@ for (let i = 0; i < 300 && flowersPlaced < 9; i++) {
|
|
|
119
118
|
position: [round2(x), round2(y), round2(z)],
|
|
120
119
|
area: [12, 12],
|
|
121
120
|
seed: Math.floor(rand() * 1e9),
|
|
122
|
-
terrain: '../../Ground/Surface',
|
|
123
121
|
},
|
|
124
122
|
});
|
|
125
123
|
}
|
|
@@ -180,7 +178,7 @@ const ward = (i: number, [x, y, z]: [number, number, number]): unknown => ({
|
|
|
180
178
|
type: 'MeshInstance3D',
|
|
181
179
|
props: {
|
|
182
180
|
mesh: 'cylinder',
|
|
183
|
-
size: [
|
|
181
|
+
size: [0.8, 1.2, 0.8], // RADIUS (cylinder size[0]) = collider box half-extent
|
|
184
182
|
material: { color: '#7d8494', roughness: 0.9 },
|
|
185
183
|
castShadow: true,
|
|
186
184
|
receiveShadow: true,
|
|
@@ -319,7 +317,7 @@ const scene = {
|
|
|
319
317
|
fog: { near: 110, far: 560 },
|
|
320
318
|
shadows: { mapSize: 2048, radius: 1.2 },
|
|
321
319
|
exposure: 1.12,
|
|
322
|
-
ambient: { color: '#ffe8d4', intensity: 0.
|
|
320
|
+
ambient: { color: '#ffe8d4', intensity: 0.85 },
|
|
323
321
|
iblIntensity: 0.9,
|
|
324
322
|
bloom: { threshold: 1.2, strength: 0.55 },
|
|
325
323
|
post: { vignette: 0.28, saturation: 1.12, contrast: 1.02 },
|
|
@@ -382,7 +380,24 @@ const scene = {
|
|
|
382
380
|
// must keep animating — auditScene flags exactly this mistake.
|
|
383
381
|
children: (terrain.children ?? []).map((c) =>
|
|
384
382
|
c.type === 'Water3D' || c.name === 'Clouds'
|
|
385
|
-
? 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
|
|
386
401
|
: { ...c, props: { ...(c.props ?? {}), static: true } },
|
|
387
402
|
),
|
|
388
403
|
},
|
|
@@ -409,7 +424,7 @@ const scene = {
|
|
|
409
424
|
type: 'MeshInstance3D',
|
|
410
425
|
props: {
|
|
411
426
|
mesh: 'cylinder',
|
|
412
|
-
size: [
|
|
427
|
+
size: [1.5, 11, 1.5], // RADIUS = collider half-extent (was 3 → camera clipped inside the 1.5 m shell)
|
|
413
428
|
material: { color: '#e8e2d4', roughness: 0.8 },
|
|
414
429
|
castShadow: true,
|
|
415
430
|
receiveShadow: true,
|
|
@@ -420,7 +435,7 @@ const scene = {
|
|
|
420
435
|
type: 'MeshInstance3D',
|
|
421
436
|
props: {
|
|
422
437
|
mesh: 'cylinder',
|
|
423
|
-
size: [
|
|
438
|
+
size: [1.56, 1.6, 1.56],
|
|
424
439
|
position: [0, 1.2, 0],
|
|
425
440
|
material: { color: '#b8452e', roughness: 0.8 },
|
|
426
441
|
},
|
|
@@ -584,18 +599,6 @@ const scene = {
|
|
|
584
599
|
},
|
|
585
600
|
],
|
|
586
601
|
},
|
|
587
|
-
{
|
|
588
|
-
name: 'DashTrail',
|
|
589
|
-
type: 'Trail3D',
|
|
590
|
-
props: {
|
|
591
|
-
width: 0.5,
|
|
592
|
-
color: '#ffffff',
|
|
593
|
-
opacity: 0.28,
|
|
594
|
-
seconds: 0.45,
|
|
595
|
-
emitting: false,
|
|
596
|
-
position: [0, -0.4, 0],
|
|
597
|
-
},
|
|
598
|
-
},
|
|
599
602
|
{
|
|
600
603
|
name: 'SwingSfx',
|
|
601
604
|
type: 'AudioPlayer',
|
|
@@ -609,7 +612,7 @@ const scene = {
|
|
|
609
612
|
type: 'Node',
|
|
610
613
|
script: {
|
|
611
614
|
name: 'DayNight',
|
|
612
|
-
props: { paused: true, startHour: 17.
|
|
615
|
+
props: { paused: true, startHour: 17.25, nightDarkness: 0.32, maxSunElevationDeg: 48 },
|
|
613
616
|
},
|
|
614
617
|
},
|
|
615
618
|
{
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|