@netforge/engine 0.0.0-stage → 0.1.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/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +147 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +208 -0
- package/dist/net/protocol.js +8 -0
- package/dist/net/room-client.d.ts +57 -0
- package/dist/net/room-client.js +144 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms.txt +183 -0
- package/package.json +82 -3
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { PerspectiveCamera, Vector3 } from "three/webgpu";
|
|
2
|
+
import { Spring, stepSpring } from "./feel/spring.js";
|
|
3
|
+
import { Trauma } from "./feel/trauma.js";
|
|
4
|
+
import { damp } from "./interpolate.js";
|
|
5
|
+
/** Square bounds around the origin: a field of half size `half` plus `margin` of outside ground. */
|
|
6
|
+
export function fieldBounds(half, margin = 0) {
|
|
7
|
+
const h = half + margin;
|
|
8
|
+
return { minX: -h, maxX: h, minZ: -h, maxZ: h };
|
|
9
|
+
}
|
|
10
|
+
const CORNERS = [
|
|
11
|
+
[-1, -1],
|
|
12
|
+
[1, -1],
|
|
13
|
+
[1, 1],
|
|
14
|
+
[-1, 1],
|
|
15
|
+
];
|
|
16
|
+
const _a = new Vector3();
|
|
17
|
+
const _b = new Vector3();
|
|
18
|
+
/**
|
|
19
|
+
* The ground a camera sees: the box around where its four corner rays meet
|
|
20
|
+
* the plane y = `groundY`. A ray at or above the horizon counts as reaching
|
|
21
|
+
* `reach` m out along the ground. The camera's matrices must be current.
|
|
22
|
+
*/
|
|
23
|
+
export function groundFootprint(camera, groundY = 0, reach = 400) {
|
|
24
|
+
let minX = Infinity;
|
|
25
|
+
let maxX = -Infinity;
|
|
26
|
+
let minZ = Infinity;
|
|
27
|
+
let maxZ = -Infinity;
|
|
28
|
+
const origin = _a.setFromMatrixPosition(camera.matrixWorld);
|
|
29
|
+
const ox = origin.x;
|
|
30
|
+
const oy = origin.y;
|
|
31
|
+
const oz = origin.z;
|
|
32
|
+
for (const [x, y] of CORNERS) {
|
|
33
|
+
const p = _b.set(x, y, 0.5).unproject(camera);
|
|
34
|
+
let dx = p.x - ox;
|
|
35
|
+
const dy = p.y - oy;
|
|
36
|
+
let dz = p.z - oz;
|
|
37
|
+
let t;
|
|
38
|
+
if (dy < -1e-6 && oy > groundY)
|
|
39
|
+
t = (groundY - oy) / dy;
|
|
40
|
+
else {
|
|
41
|
+
// Above the horizon: as far as `reach` along the ground.
|
|
42
|
+
const flat = Math.hypot(dx, dz) || 1;
|
|
43
|
+
dx /= flat;
|
|
44
|
+
dz /= flat;
|
|
45
|
+
t = reach;
|
|
46
|
+
}
|
|
47
|
+
const gx = ox + dx * t;
|
|
48
|
+
const gz = oz + dz * t;
|
|
49
|
+
if (gx < minX)
|
|
50
|
+
minX = gx;
|
|
51
|
+
if (gx > maxX)
|
|
52
|
+
maxX = gx;
|
|
53
|
+
if (gz < minZ)
|
|
54
|
+
minZ = gz;
|
|
55
|
+
if (gz > maxZ)
|
|
56
|
+
maxZ = gz;
|
|
57
|
+
}
|
|
58
|
+
return { minX, maxX, minZ, maxZ };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* How far to move a view so `seen` stays inside `bounds`, per axis: zero
|
|
62
|
+
* when it already fits, the overshoot when it pokes out of one side, and
|
|
63
|
+
* centring when it is wider than the bounds.
|
|
64
|
+
*/
|
|
65
|
+
export function boundsShift(seen, bounds) {
|
|
66
|
+
const axis = (lo, hi, min, max) => {
|
|
67
|
+
if (hi - lo >= max - min)
|
|
68
|
+
return (min + max) / 2 - (lo + hi) / 2;
|
|
69
|
+
if (lo < min)
|
|
70
|
+
return min - lo;
|
|
71
|
+
if (hi > max)
|
|
72
|
+
return max - hi;
|
|
73
|
+
return 0;
|
|
74
|
+
};
|
|
75
|
+
return { x: axis(seen.minX, seen.maxX, bounds.minX, bounds.maxX), z: axis(seen.minZ, seen.maxZ, bounds.minZ, bounds.maxZ) };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Third-person orbit-follow camera with the camera parts of the feel kit:
|
|
79
|
+
* critically damped follow, smoothed and clamped lead, a dead zone, trauma
|
|
80
|
+
* shake and FOV punches. `yaw` matches the sim's convention (0 looks down
|
|
81
|
+
* -z), so the input layer can send the camera's yaw as the character's
|
|
82
|
+
* facing. Shake is applied as a per-frame `offset` and `roll` on top of the
|
|
83
|
+
* computed pose, so it never accumulates drift.
|
|
84
|
+
*/
|
|
85
|
+
export class CameraRig {
|
|
86
|
+
camera;
|
|
87
|
+
yaw = 0;
|
|
88
|
+
pitch = 0.45;
|
|
89
|
+
/** Trauma shake (add with `addTrauma`). */
|
|
90
|
+
trauma;
|
|
91
|
+
/** This frame's shake offset (world, m) and roll (rad). */
|
|
92
|
+
offset = new Vector3();
|
|
93
|
+
roll = 0;
|
|
94
|
+
focus = new Vector3();
|
|
95
|
+
focusVel = new Vector3();
|
|
96
|
+
lastTarget = new Vector3();
|
|
97
|
+
vel = new Vector3();
|
|
98
|
+
leadVec = new Vector3();
|
|
99
|
+
fovSpring;
|
|
100
|
+
initialised = false;
|
|
101
|
+
o;
|
|
102
|
+
bounds;
|
|
103
|
+
/** This frame's clamp shift (m, ground plane): non-zero while the bounds hold the camera back. */
|
|
104
|
+
clampShift = { x: 0, z: 0 };
|
|
105
|
+
constructor(aspect, opts = {}) {
|
|
106
|
+
this.o = {
|
|
107
|
+
distance: 9,
|
|
108
|
+
height: 1.2,
|
|
109
|
+
minPitch: -0.2,
|
|
110
|
+
maxPitch: 1.35,
|
|
111
|
+
follow: 12,
|
|
112
|
+
lead: 0.15,
|
|
113
|
+
leadMax: 6,
|
|
114
|
+
leadSmoothing: 10,
|
|
115
|
+
deadZone: 0,
|
|
116
|
+
fov: 60,
|
|
117
|
+
punchFrequency: 4,
|
|
118
|
+
punchDamping: 0.55,
|
|
119
|
+
groundY: 0,
|
|
120
|
+
...opts,
|
|
121
|
+
shake: opts.shake ?? {},
|
|
122
|
+
};
|
|
123
|
+
this.bounds = opts.bounds ?? null;
|
|
124
|
+
this.camera = new PerspectiveCamera(this.o.fov, aspect, 0.1, 500);
|
|
125
|
+
this.trauma = new Trauma({ maxOffset: 0.6, maxRoll: 0.035, ...this.o.shake });
|
|
126
|
+
this.fovSpring = new Spring(0, { frequency: this.o.punchFrequency, damping: this.o.punchDamping });
|
|
127
|
+
}
|
|
128
|
+
/** Camera distance from the focus, m (e.g. pull back on a portrait phone). */
|
|
129
|
+
setDistance(metres) {
|
|
130
|
+
this.o.distance = metres;
|
|
131
|
+
}
|
|
132
|
+
/** Keep the visible ground inside `bounds` (null lets the camera follow anywhere). */
|
|
133
|
+
setBounds(bounds) {
|
|
134
|
+
this.bounds = bounds;
|
|
135
|
+
}
|
|
136
|
+
/** The point the camera looks at (after follow and lead). */
|
|
137
|
+
get target() {
|
|
138
|
+
return this.focus;
|
|
139
|
+
}
|
|
140
|
+
/** Apply look input (radians). */
|
|
141
|
+
look(dYaw, dPitch) {
|
|
142
|
+
this.yaw += dYaw;
|
|
143
|
+
this.pitch = Math.min(this.o.maxPitch, Math.max(this.o.minPitch, this.pitch + dPitch));
|
|
144
|
+
}
|
|
145
|
+
/** Add trauma 0..1 (game feel, §13): the shake is trauma squared, so small hits barely move it. */
|
|
146
|
+
addTrauma(amount) {
|
|
147
|
+
this.trauma.add(amount);
|
|
148
|
+
}
|
|
149
|
+
/** The classic shake: about `amplitude` m of shake held for `seconds`, then fading. */
|
|
150
|
+
shake(amplitude, seconds) {
|
|
151
|
+
this.trauma.add(this.trauma.traumaFor(amplitude));
|
|
152
|
+
this.trauma.sustain(seconds * 0.5);
|
|
153
|
+
}
|
|
154
|
+
/** Kick the field of view by `degrees` (negative zooms in) and spring back. */
|
|
155
|
+
punch(degrees) {
|
|
156
|
+
this.fovSpring.value = Math.max(-20, Math.min(20, this.fovSpring.value + degrees));
|
|
157
|
+
}
|
|
158
|
+
/** Current FOV offset from the punch spring, degrees. */
|
|
159
|
+
get fovKick() {
|
|
160
|
+
return this.fovSpring.value;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Follow `target`. `dt` is the (hit-stop scaled) frame delta; pass
|
|
164
|
+
* `realDt` so shake and punches keep moving during a hit-stop.
|
|
165
|
+
*/
|
|
166
|
+
update(target, dt, realDt = dt) {
|
|
167
|
+
if (!this.initialised) {
|
|
168
|
+
this.focus.copy(target);
|
|
169
|
+
this.lastTarget.copy(target);
|
|
170
|
+
this.initialised = true;
|
|
171
|
+
}
|
|
172
|
+
if (dt > 0) {
|
|
173
|
+
this.vel.subVectors(target, this.lastTarget).divideScalar(dt);
|
|
174
|
+
this.lastTarget.copy(target);
|
|
175
|
+
// Lead: velocity times lead seconds, clamped, then smoothed.
|
|
176
|
+
let lx = this.vel.x * this.o.lead;
|
|
177
|
+
let lz = this.vel.z * this.o.lead;
|
|
178
|
+
const len = Math.hypot(lx, lz);
|
|
179
|
+
if (len > this.o.leadMax) {
|
|
180
|
+
lx *= this.o.leadMax / len;
|
|
181
|
+
lz *= this.o.leadMax / len;
|
|
182
|
+
}
|
|
183
|
+
const k = this.o.leadSmoothing > 0 ? damp(this.o.leadSmoothing, dt) : 1;
|
|
184
|
+
this.leadVec.x += (lx - this.leadVec.x) * k;
|
|
185
|
+
this.leadVec.z += (lz - this.leadVec.z) * k;
|
|
186
|
+
}
|
|
187
|
+
let goalX = target.x + this.leadVec.x;
|
|
188
|
+
let goalZ = target.z + this.leadVec.z;
|
|
189
|
+
const goalY = target.y + this.o.height;
|
|
190
|
+
if (this.o.deadZone > 0) {
|
|
191
|
+
const dx = goalX - this.focus.x;
|
|
192
|
+
const dz = goalZ - this.focus.z;
|
|
193
|
+
const d = Math.hypot(dx, dz);
|
|
194
|
+
if (d <= this.o.deadZone) {
|
|
195
|
+
goalX = this.focus.x;
|
|
196
|
+
goalZ = this.focus.z;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const s = (d - this.o.deadZone) / d;
|
|
200
|
+
goalX = this.focus.x + dx * s;
|
|
201
|
+
goalZ = this.focus.z + dz * s;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (dt > 0) {
|
|
205
|
+
const w = 2 * this.o.follow;
|
|
206
|
+
[this.focus.x, this.focusVel.x] = stepSpring(this.focus.x, this.focusVel.x, goalX, w, 1, dt);
|
|
207
|
+
[this.focus.y, this.focusVel.y] = stepSpring(this.focus.y, this.focusVel.y, goalY, w, 1, dt);
|
|
208
|
+
[this.focus.z, this.focusVel.z] = stepSpring(this.focus.z, this.focusVel.z, goalZ, w, 1, dt);
|
|
209
|
+
}
|
|
210
|
+
// Shake and punch run on real time.
|
|
211
|
+
this.trauma.update(realDt);
|
|
212
|
+
this.fovSpring.step(realDt);
|
|
213
|
+
const cy = Math.cos(this.yaw);
|
|
214
|
+
const sy = Math.sin(this.yaw);
|
|
215
|
+
// Camera right is (cos yaw, 0, -sin yaw); shake moves it sideways and up.
|
|
216
|
+
this.offset.set(cy * this.trauma.x, this.trauma.y, -sy * this.trauma.x);
|
|
217
|
+
this.roll = this.trauma.roll;
|
|
218
|
+
const cp = Math.cos(this.pitch);
|
|
219
|
+
const d = this.o.distance;
|
|
220
|
+
this.camera.position.set(this.focus.x + sy * cp * d + this.offset.x, this.focus.y + Math.sin(this.pitch) * d + this.offset.y, this.focus.z + cy * cp * d + this.offset.z);
|
|
221
|
+
this.camera.lookAt(this.focus.x + this.offset.x, this.focus.y + this.offset.y, this.focus.z + this.offset.z);
|
|
222
|
+
this.clampShift.x = 0;
|
|
223
|
+
this.clampShift.z = 0;
|
|
224
|
+
if (this.bounds) {
|
|
225
|
+
// Clamp on the un-shaken pose, so a shake near a wall still shakes.
|
|
226
|
+
this.camera.position.sub(this.offset);
|
|
227
|
+
this.camera.updateMatrixWorld();
|
|
228
|
+
const shift = boundsShift(groundFootprint(this.camera, this.o.groundY), this.bounds);
|
|
229
|
+
this.clampShift.x = shift.x;
|
|
230
|
+
this.clampShift.z = shift.z;
|
|
231
|
+
this.camera.position.add(this.offset);
|
|
232
|
+
this.camera.position.x += shift.x;
|
|
233
|
+
this.camera.position.z += shift.z;
|
|
234
|
+
}
|
|
235
|
+
if (this.roll !== 0)
|
|
236
|
+
this.camera.rotateZ(this.roll);
|
|
237
|
+
const fov = this.o.fov + this.fovSpring.value;
|
|
238
|
+
if (Math.abs(this.camera.fov - fov) > 1e-4) {
|
|
239
|
+
this.camera.fov = fov;
|
|
240
|
+
this.camera.updateProjectionMatrix();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
resize(aspect) {
|
|
244
|
+
this.camera.aspect = aspect;
|
|
245
|
+
this.camera.updateProjectionMatrix();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InstancedMesh, Material } from "three/webgpu";
|
|
2
|
+
import type { BufferGeometry } from "three/webgpu";
|
|
3
|
+
import type { RenderView } from "@netforge/engine-sim";
|
|
4
|
+
/**
|
|
5
|
+
* One InstancedMesh per entity kind: 2,000 enemies are one draw call
|
|
6
|
+
* (spec §5). Each frame writes interpolated matrices straight into the
|
|
7
|
+
* instance buffer (rotation about y only; no per-instance Object3D).
|
|
8
|
+
*/
|
|
9
|
+
export declare class InstancedCrowd {
|
|
10
|
+
private readonly kind;
|
|
11
|
+
private readonly scale;
|
|
12
|
+
readonly mesh: InstancedMesh;
|
|
13
|
+
private readonly capacity;
|
|
14
|
+
constructor(geometry: BufferGeometry, material: Material, capacity: number, kind: number, scale?: number);
|
|
15
|
+
/** Write this frame's instances from a sim render view at interpolation `alpha` (0..1). */
|
|
16
|
+
update(view: RenderView, alpha: number): number;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { InstancedMesh } from "three/webgpu";
|
|
2
|
+
import { lerp, lerpAngle } from "./interpolate.js";
|
|
3
|
+
/**
|
|
4
|
+
* One InstancedMesh per entity kind: 2,000 enemies are one draw call
|
|
5
|
+
* (spec §5). Each frame writes interpolated matrices straight into the
|
|
6
|
+
* instance buffer (rotation about y only; no per-instance Object3D).
|
|
7
|
+
*/
|
|
8
|
+
export class InstancedCrowd {
|
|
9
|
+
kind;
|
|
10
|
+
scale;
|
|
11
|
+
mesh;
|
|
12
|
+
capacity;
|
|
13
|
+
constructor(geometry, material, capacity, kind, scale = 1) {
|
|
14
|
+
this.kind = kind;
|
|
15
|
+
this.scale = scale;
|
|
16
|
+
this.capacity = capacity;
|
|
17
|
+
this.mesh = new InstancedMesh(geometry, material, capacity);
|
|
18
|
+
this.mesh.count = 0;
|
|
19
|
+
// Instances span the whole arena; culling one bounding sphere saves nothing.
|
|
20
|
+
this.mesh.frustumCulled = false;
|
|
21
|
+
}
|
|
22
|
+
/** Write this frame's instances from a sim render view at interpolation `alpha` (0..1). */
|
|
23
|
+
update(view, alpha) {
|
|
24
|
+
const m = this.mesh.instanceMatrix.array;
|
|
25
|
+
const s = this.scale;
|
|
26
|
+
let n = 0;
|
|
27
|
+
for (let i = 0; i < view.count && n < this.capacity; i++) {
|
|
28
|
+
if (view.kind[i] !== this.kind)
|
|
29
|
+
continue;
|
|
30
|
+
const x = lerp(view.prevX[i] ?? 0, view.x[i] ?? 0, alpha);
|
|
31
|
+
const y = lerp(view.prevY[i] ?? 0, view.y[i] ?? 0, alpha);
|
|
32
|
+
const z = lerp(view.prevZ[i] ?? 0, view.z[i] ?? 0, alpha);
|
|
33
|
+
const yaw = lerpAngle(view.prevYaw[i] ?? 0, view.yaw[i] ?? 0, alpha);
|
|
34
|
+
const c = Math.cos(yaw) * s;
|
|
35
|
+
const sn = Math.sin(yaw) * s;
|
|
36
|
+
const o = n * 16;
|
|
37
|
+
m[o] = c;
|
|
38
|
+
m[o + 1] = 0;
|
|
39
|
+
m[o + 2] = -sn;
|
|
40
|
+
m[o + 3] = 0;
|
|
41
|
+
m[o + 4] = 0;
|
|
42
|
+
m[o + 5] = s;
|
|
43
|
+
m[o + 6] = 0;
|
|
44
|
+
m[o + 7] = 0;
|
|
45
|
+
m[o + 8] = sn;
|
|
46
|
+
m[o + 9] = 0;
|
|
47
|
+
m[o + 10] = c;
|
|
48
|
+
m[o + 11] = 0;
|
|
49
|
+
m[o + 12] = x;
|
|
50
|
+
m[o + 13] = y;
|
|
51
|
+
m[o + 14] = z;
|
|
52
|
+
m[o + 15] = 1;
|
|
53
|
+
n++;
|
|
54
|
+
}
|
|
55
|
+
this.mesh.count = n;
|
|
56
|
+
this.mesh.instanceMatrix.needsUpdate = true;
|
|
57
|
+
return n;
|
|
58
|
+
}
|
|
59
|
+
dispose() {
|
|
60
|
+
this.mesh.dispose();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export interface SynthRecipe {
|
|
2
|
+
readonly wave: OscillatorType | "noise";
|
|
3
|
+
/** Start and end frequency, Hz. */
|
|
4
|
+
readonly f0: number;
|
|
5
|
+
readonly f1: number;
|
|
6
|
+
readonly ms: number;
|
|
7
|
+
readonly gain: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CueOptions {
|
|
10
|
+
/** Least time between two plays of this cue, ms. Default 40. */
|
|
11
|
+
readonly minGapMs?: number;
|
|
12
|
+
/** Random pitch spread, as a fraction (0.08 = plus or minus 8%). Default 0.06. */
|
|
13
|
+
readonly pitchVar?: number;
|
|
14
|
+
/** Random gain spread, fraction. Default 0.1. */
|
|
15
|
+
readonly gainVar?: number;
|
|
16
|
+
/** Most voices of this cue at once. Default 4. */
|
|
17
|
+
readonly voices?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PlayOptions {
|
|
20
|
+
/** Pitch multiplier (on top of the random variation). */
|
|
21
|
+
readonly pitch?: number;
|
|
22
|
+
/** Gain multiplier. */
|
|
23
|
+
readonly gain?: number;
|
|
24
|
+
/** Stereo pan -1..1 (wins over screenX). */
|
|
25
|
+
readonly pan?: number;
|
|
26
|
+
/** Pan from a screen x position in pixels and the screen width. */
|
|
27
|
+
readonly screenX?: number;
|
|
28
|
+
readonly width?: number;
|
|
29
|
+
/** Distance from the listener, m (inverse falloff past refDistance, silent past maxDistance). */
|
|
30
|
+
readonly distance?: number;
|
|
31
|
+
}
|
|
32
|
+
/** The built-in cues every template can use by name. */
|
|
33
|
+
export declare const DEFAULT_CUES: Readonly<Record<string, SynthRecipe & CueOptions>>;
|
|
34
|
+
/**
|
|
35
|
+
* The DOM-free decision core: may a cue start now? Per-cue gap and voice
|
|
36
|
+
* cap, and a global voice cap.
|
|
37
|
+
*/
|
|
38
|
+
export declare class CueGate {
|
|
39
|
+
maxVoices: number;
|
|
40
|
+
private readonly last;
|
|
41
|
+
private readonly playing;
|
|
42
|
+
constructor(maxVoices?: number);
|
|
43
|
+
allow(name: string, nowMs: number, durationMs: number, minGapMs: number, cueVoices: number): boolean;
|
|
44
|
+
get voices(): number;
|
|
45
|
+
}
|
|
46
|
+
/** Gain for a source `distance` m away (inverse falloff, 0 past maxDistance). */
|
|
47
|
+
export declare function distanceGain(distance: number, refDistance?: number, maxDistance?: number, rolloff?: number): number;
|
|
48
|
+
export interface FeelAudioOptions {
|
|
49
|
+
/** Master gain 0..1. Default 0.18. */
|
|
50
|
+
readonly master?: number;
|
|
51
|
+
/** Most voices at once across all cues. Default 16. */
|
|
52
|
+
readonly voices?: number;
|
|
53
|
+
readonly refDistance?: number;
|
|
54
|
+
readonly maxDistance?: number;
|
|
55
|
+
/** Build the context (tests pass a fake). Default `new AudioContext()`. */
|
|
56
|
+
readonly context?: () => AudioContext;
|
|
57
|
+
/** Clock in ms (tests). Default performance.now. */
|
|
58
|
+
readonly now?: () => number;
|
|
59
|
+
/** Seed for pitch and gain variation. */
|
|
60
|
+
readonly seed?: number;
|
|
61
|
+
}
|
|
62
|
+
export declare class FeelAudio {
|
|
63
|
+
muted: boolean;
|
|
64
|
+
/** Cues actually started (playtest counters). */
|
|
65
|
+
played: number;
|
|
66
|
+
readonly gate: CueGate;
|
|
67
|
+
private ctx;
|
|
68
|
+
private masterNode;
|
|
69
|
+
private noise;
|
|
70
|
+
private masterGain;
|
|
71
|
+
private readonly cues;
|
|
72
|
+
private readonly unlocked;
|
|
73
|
+
private readonly rnd;
|
|
74
|
+
private readonly o;
|
|
75
|
+
constructor(opts?: FeelAudioOptions);
|
|
76
|
+
/** Start WebAudio (call from a user gesture; browsers block audio before one). Safe to call repeatedly. */
|
|
77
|
+
unlock(): void;
|
|
78
|
+
/** Run `fn` with the AudioContext once audio is unlocked (now, if it already is). */
|
|
79
|
+
whenUnlocked(fn: (ctx: AudioContext) => void): void;
|
|
80
|
+
/** The AudioContext, once unlocked (music and other buses hang off it). */
|
|
81
|
+
get context(): AudioContext | null;
|
|
82
|
+
/** Unlock on the first pointer or key press on `target` (default window). */
|
|
83
|
+
unlockOnGesture(target?: EventTarget): void;
|
|
84
|
+
get ready(): boolean;
|
|
85
|
+
get master(): number;
|
|
86
|
+
set master(v: number);
|
|
87
|
+
/** Define or replace a cue's synth recipe and options. */
|
|
88
|
+
define(name: string, recipe: SynthRecipe, opts?: CueOptions): void;
|
|
89
|
+
/**
|
|
90
|
+
* Use a real sample for a cue (an AudioBuffer, or a URL fetched and decoded once audio is unlocked).
|
|
91
|
+
* `opts.gain` is the sample's level before the master gain (default: the cue's synth level).
|
|
92
|
+
*/
|
|
93
|
+
addBuffer(name: string, source: AudioBuffer | string, opts?: CueOptions & {
|
|
94
|
+
readonly gain?: number;
|
|
95
|
+
}): Promise<void>;
|
|
96
|
+
has(name: string): boolean;
|
|
97
|
+
/** Play a cue. Returns false when muted, locked, unknown, throttled or out of voices. */
|
|
98
|
+
play(name: string, opts?: PlayOptions): boolean;
|
|
99
|
+
/** An impact scaled by damage (bigger hits are louder and lower). */
|
|
100
|
+
impact(damage: number, opts?: PlayOptions): boolean;
|
|
101
|
+
private voice;
|
|
102
|
+
private noiseBuffer;
|
|
103
|
+
}
|
|
104
|
+
/** Surfaces change a footstep's pitch and weight. */
|
|
105
|
+
export declare const SURFACES: {
|
|
106
|
+
readonly grass: {
|
|
107
|
+
readonly pitch: 0.85;
|
|
108
|
+
readonly gain: 0.8;
|
|
109
|
+
};
|
|
110
|
+
readonly dirt: {
|
|
111
|
+
readonly pitch: 0.9;
|
|
112
|
+
readonly gain: 0.9;
|
|
113
|
+
};
|
|
114
|
+
readonly stone: {
|
|
115
|
+
readonly pitch: 1.25;
|
|
116
|
+
readonly gain: 1;
|
|
117
|
+
};
|
|
118
|
+
readonly wood: {
|
|
119
|
+
readonly pitch: 1.05;
|
|
120
|
+
readonly gain: 1;
|
|
121
|
+
};
|
|
122
|
+
readonly metal: {
|
|
123
|
+
readonly pitch: 1.6;
|
|
124
|
+
readonly gain: 1.1;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export type Surface = keyof typeof SURFACES;
|
|
128
|
+
export interface FootstepOptions {
|
|
129
|
+
/** Metres per step. Default 1.4. */
|
|
130
|
+
readonly stride?: number;
|
|
131
|
+
/** Most steps per second. Default 6. */
|
|
132
|
+
readonly maxRate?: number;
|
|
133
|
+
/** Below this speed (m/s) no steps play. Default 0.6. */
|
|
134
|
+
readonly minSpeed?: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Footstep cadence from speed: steps per second = speed / stride, with an
|
|
138
|
+
* alternating left/right pitch. Calls `play(pitch, gain)` on each step.
|
|
139
|
+
*/
|
|
140
|
+
export declare class Footsteps {
|
|
141
|
+
private readonly play;
|
|
142
|
+
surface: Surface;
|
|
143
|
+
steps: number;
|
|
144
|
+
private phase;
|
|
145
|
+
private left;
|
|
146
|
+
private readonly o;
|
|
147
|
+
constructor(play: (pitch: number, gain: number) => void, opts?: FootstepOptions);
|
|
148
|
+
/** Advance by `dt` seconds at `speed` m/s. Returns the number of steps played. */
|
|
149
|
+
update(dt: number, speed: number, grounded?: boolean): number;
|
|
150
|
+
}
|