@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.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,13 @@
1
+ /** Linear interpolation. */
2
+ export const lerp = (a, b, t) => a + (b - a) * t;
3
+ /** Interpolate angles along the shortest arc (radians). */
4
+ export function lerpAngle(a, b, t) {
5
+ let d = (b - a) % (Math.PI * 2);
6
+ if (d > Math.PI)
7
+ d -= Math.PI * 2;
8
+ else if (d < -Math.PI)
9
+ d += Math.PI * 2;
10
+ return a + d * t;
11
+ }
12
+ /** Frame-rate independent exponential smoothing factor for a rate in 1/s. */
13
+ export const damp = (rate, dt) => 1 - Math.exp(-rate * dt);
@@ -0,0 +1,63 @@
1
+ import { AnimationMixer } from "three/webgpu";
2
+ import type { AnimationClip, Object3D } from "three/webgpu";
3
+ export type LocomotionState = "idle" | "walk" | "run";
4
+ export interface LocomotionThresholds {
5
+ /** Speed (m/s) at and above which the character walks. Default 0.4. */
6
+ readonly walk: number;
7
+ /** Speed at and above which it runs. Default 4. */
8
+ readonly run: number;
9
+ /** Hysteresis band, m/s, so speeds near a threshold do not flicker. Default 0.25. */
10
+ readonly band: number;
11
+ }
12
+ export declare const DEFAULT_LOCOMOTION: LocomotionThresholds;
13
+ /** The locomotion state for a speed, with hysteresis around the current state. Pure. */
14
+ export declare function locomotionState(speed: number, current: LocomotionState, t?: LocomotionThresholds): LocomotionState;
15
+ export interface AnimatedCharacterOptions {
16
+ /** Clip names for each locomotion state. Default "idle", "walk", "run". */
17
+ readonly clips?: Partial<Record<LocomotionState, string>>;
18
+ readonly thresholds?: Partial<LocomotionThresholds>;
19
+ /** Speed (m/s) at which the walk and run clips play at timeScale 1 (foot speed matching). */
20
+ readonly walkSpeed?: number;
21
+ readonly runSpeed?: number;
22
+ /** Default crossfade, s. Default 0.2. */
23
+ readonly fade?: number;
24
+ }
25
+ export interface PlayOptions {
26
+ readonly fade?: number;
27
+ readonly loop?: "repeat" | "once";
28
+ readonly timeScale?: number;
29
+ }
30
+ /**
31
+ * A skinned character (glTF or procedural) with an AnimationMixer:
32
+ * `setSpeed(mps)` drives idle/walk/run with crossfades and playback rate
33
+ * matched to ground speed; `oneShot("attack")` plays once and blends back
34
+ * to locomotion; `play(name)` for anything else. Call `update(dt)` per frame.
35
+ */
36
+ export declare class AnimatedCharacter {
37
+ readonly root: Object3D;
38
+ readonly mixer: AnimationMixer;
39
+ private readonly actions;
40
+ private readonly names;
41
+ private readonly th;
42
+ private readonly walkSpeed;
43
+ private readonly runSpeed;
44
+ private readonly fade;
45
+ private current;
46
+ private loco;
47
+ private oneShotAction;
48
+ private speed;
49
+ constructor(root: Object3D, clips: readonly AnimationClip[], opts?: AnimatedCharacterOptions);
50
+ has(name: string): boolean;
51
+ get state(): LocomotionState;
52
+ get playing(): string | null;
53
+ /** Crossfade to a clip. Returns false if the clip does not exist. */
54
+ play(name: string, opts?: PlayOptions): boolean;
55
+ /** Play a clip once (attack, hit), then return to locomotion. */
56
+ oneShot(name: string, opts?: Omit<PlayOptions, "loop">): boolean;
57
+ /** Ground speed in m/s; picks the locomotion clip and its playback rate. */
58
+ setSpeed(mps: number): void;
59
+ private rateFor;
60
+ private enterLocomotion;
61
+ update(dt: number): void;
62
+ dispose(): void;
63
+ }
@@ -0,0 +1,128 @@
1
+ import { AnimationMixer, LoopOnce, LoopRepeat } from "three/webgpu";
2
+ export const DEFAULT_LOCOMOTION = { walk: 0.4, run: 4, band: 0.25 };
3
+ /** The locomotion state for a speed, with hysteresis around the current state. Pure. */
4
+ export function locomotionState(speed, current, t = DEFAULT_LOCOMOTION) {
5
+ const up = (th) => speed >= th + t.band;
6
+ const down = (th) => speed < th - t.band;
7
+ switch (current) {
8
+ case "idle":
9
+ return up(t.run) ? "run" : up(t.walk) ? "walk" : "idle";
10
+ case "walk":
11
+ return up(t.run) ? "run" : down(t.walk) ? "idle" : "walk";
12
+ case "run":
13
+ return down(t.walk) ? "idle" : down(t.run) ? "walk" : "run";
14
+ }
15
+ }
16
+ /**
17
+ * A skinned character (glTF or procedural) with an AnimationMixer:
18
+ * `setSpeed(mps)` drives idle/walk/run with crossfades and playback rate
19
+ * matched to ground speed; `oneShot("attack")` plays once and blends back
20
+ * to locomotion; `play(name)` for anything else. Call `update(dt)` per frame.
21
+ */
22
+ export class AnimatedCharacter {
23
+ root;
24
+ mixer;
25
+ actions = new Map();
26
+ names;
27
+ th;
28
+ walkSpeed;
29
+ runSpeed;
30
+ fade;
31
+ current = null;
32
+ loco = "idle";
33
+ oneShotAction = null;
34
+ speed = 0;
35
+ constructor(root, clips, opts = {}) {
36
+ this.root = root;
37
+ this.mixer = new AnimationMixer(root);
38
+ for (const c of clips)
39
+ this.actions.set(c.name, this.mixer.clipAction(c));
40
+ this.names = { idle: "idle", walk: "walk", run: "run", ...opts.clips };
41
+ this.th = { ...DEFAULT_LOCOMOTION, ...opts.thresholds };
42
+ this.walkSpeed = opts.walkSpeed ?? 2;
43
+ this.runSpeed = opts.runSpeed ?? 6;
44
+ this.fade = opts.fade ?? 0.2;
45
+ this.mixer.addEventListener("finished", (e) => {
46
+ if (e.action === this.oneShotAction) {
47
+ this.oneShotAction = null;
48
+ this.enterLocomotion(this.fade, true);
49
+ }
50
+ });
51
+ this.enterLocomotion(0, true);
52
+ }
53
+ has(name) {
54
+ return this.actions.has(name);
55
+ }
56
+ get state() {
57
+ return this.loco;
58
+ }
59
+ get playing() {
60
+ return this.current?.getClip().name ?? null;
61
+ }
62
+ /** Crossfade to a clip. Returns false if the clip does not exist. */
63
+ play(name, opts = {}) {
64
+ const next = this.actions.get(name);
65
+ if (!next)
66
+ return false;
67
+ const fade = opts.fade ?? this.fade;
68
+ next.reset();
69
+ next.enabled = true;
70
+ next.setEffectiveWeight(1);
71
+ next.setEffectiveTimeScale(opts.timeScale ?? 1);
72
+ if (opts.loop === "once") {
73
+ next.setLoop(LoopOnce, 1);
74
+ next.clampWhenFinished = true;
75
+ }
76
+ else
77
+ next.setLoop(LoopRepeat, Infinity);
78
+ if (this.current && this.current !== next && fade > 0)
79
+ next.crossFadeFrom(this.current, fade, true);
80
+ else if (this.current && this.current !== next)
81
+ this.current.stop();
82
+ next.play();
83
+ this.current = next;
84
+ return true;
85
+ }
86
+ /** Play a clip once (attack, hit), then return to locomotion. */
87
+ oneShot(name, opts = {}) {
88
+ if (!this.play(name, { ...opts, loop: "once" }))
89
+ return false;
90
+ this.oneShotAction = this.current;
91
+ return true;
92
+ }
93
+ /** Ground speed in m/s; picks the locomotion clip and its playback rate. */
94
+ setSpeed(mps) {
95
+ this.speed = Math.max(0, mps);
96
+ if (this.oneShotAction)
97
+ return;
98
+ const next = locomotionState(this.speed, this.loco, this.th);
99
+ if (next !== this.loco) {
100
+ this.loco = next;
101
+ this.enterLocomotion(this.fade, false);
102
+ }
103
+ this.current?.setEffectiveTimeScale(this.rateFor(this.loco));
104
+ }
105
+ rateFor(s) {
106
+ if (s === "walk")
107
+ return Math.min(2, Math.max(0.5, this.speed / this.walkSpeed));
108
+ if (s === "run")
109
+ return Math.min(2, Math.max(0.6, this.speed / this.runSpeed));
110
+ return 1;
111
+ }
112
+ enterLocomotion(fade, recompute) {
113
+ if (recompute)
114
+ this.loco = locomotionState(this.speed, "idle", this.th);
115
+ const want = this.names[this.loco];
116
+ const name = this.actions.has(want) ? want : this.names.idle;
117
+ if (this.current?.getClip().name === name && this.current.isRunning())
118
+ return;
119
+ this.play(name, { fade, timeScale: this.rateFor(this.loco) });
120
+ }
121
+ update(dt) {
122
+ this.mixer.update(dt);
123
+ }
124
+ dispose() {
125
+ this.mixer.stopAllAction();
126
+ this.mixer.uncacheRoot(this.root);
127
+ }
128
+ }
@@ -0,0 +1,32 @@
1
+ import { Color } from "three/webgpu";
2
+ import type { Scene } from "three/webgpu";
3
+ import type { AtmospherePreset, LookPreset } from "./presets.js";
4
+ export interface AtmosphereOptions {
5
+ readonly preset?: LookPreset;
6
+ readonly overrides?: Partial<AtmospherePreset>;
7
+ /** Draw the sky gradient as the background (default true). */
8
+ readonly sky?: boolean;
9
+ }
10
+ /**
11
+ * Fog matched to a sky gradient, so the far ground melts into the
12
+ * background instead of meeting a hard color. Linear or exp2 distance fog,
13
+ * plus an optional low height fog (ground mist). Values live in uniforms:
14
+ * changing them never recompiles a shader.
15
+ */
16
+ export declare class Atmosphere {
17
+ private readonly scene;
18
+ private readonly opts;
19
+ readonly fogColor: import("three/webgpu").UniformNode<"color", Color>;
20
+ readonly skyTop: import("three/webgpu").UniformNode<"color", Color>;
21
+ readonly near: import("three/webgpu").UniformNode<"float", number>;
22
+ readonly far: import("three/webgpu").UniformNode<"float", number>;
23
+ readonly density: import("three/webgpu").UniformNode<"float", number>;
24
+ readonly heightDensity: import("three/webgpu").UniformNode<"float", number>;
25
+ readonly heightTop: import("three/webgpu").UniformNode<"float", number>;
26
+ private mode;
27
+ private height;
28
+ constructor(scene: Scene, opts?: AtmosphereOptions);
29
+ setPreset(preset: LookPreset, overrides?: Partial<AtmospherePreset>): void;
30
+ private build;
31
+ dispose(): void;
32
+ }
@@ -0,0 +1,59 @@
1
+ import { Color } from "three/webgpu";
2
+ import { densityFogFactor, exponentialHeightFogFactor, fog, max, mix, rangeFogFactor, screenUV, smoothstep, uniform, } from "three/tsl";
3
+ import { resolveLook } from "./presets.js";
4
+ /**
5
+ * Fog matched to a sky gradient, so the far ground melts into the
6
+ * background instead of meeting a hard color. Linear or exp2 distance fog,
7
+ * plus an optional low height fog (ground mist). Values live in uniforms:
8
+ * changing them never recompiles a shader.
9
+ */
10
+ export class Atmosphere {
11
+ scene;
12
+ opts;
13
+ fogColor = uniform(new Color());
14
+ skyTop = uniform(new Color());
15
+ near = uniform(30);
16
+ far = uniform(90);
17
+ density = uniform(0.015);
18
+ heightDensity = uniform(0);
19
+ heightTop = uniform(1);
20
+ mode = "linear";
21
+ height = false;
22
+ constructor(scene, opts = {}) {
23
+ this.scene = scene;
24
+ this.opts = opts;
25
+ this.setPreset(opts.preset ?? "studio", opts.overrides);
26
+ }
27
+ setPreset(preset, overrides = {}) {
28
+ const a = { ...resolveLook(preset).atmosphere, ...overrides };
29
+ this.fogColor.value.setHex(a.fog);
30
+ this.skyTop.value.setHex(a.skyTop);
31
+ this.near.value = a.near;
32
+ this.far.value = a.far;
33
+ this.density.value = a.density;
34
+ this.heightDensity.value = a.heightFog;
35
+ this.heightTop.value = a.heightFogTop;
36
+ const height = a.heightFog > 0;
37
+ const rebuild = a.mode !== this.mode || height !== this.height || this.scene.fogNode === undefined || this.scene.fogNode === null;
38
+ this.mode = a.mode;
39
+ this.height = height;
40
+ if (rebuild)
41
+ this.build(a.heightFog > 0);
42
+ }
43
+ build(height) {
44
+ const distance = this.mode === "exp2" ? densityFogFactor(this.density) : rangeFogFactor(this.near, this.far);
45
+ const factor = height ? max(distance, exponentialHeightFogFactor(this.heightDensity, this.heightTop)) : distance;
46
+ this.scene.fog = null;
47
+ this.scene.fogNode = fog(this.fogColor, factor);
48
+ if (this.opts.sky ?? true) {
49
+ // Screen-space gradient: horizon (fog) color low on screen, sky color at the top.
50
+ this.scene.backgroundNode = mix(this.skyTop, this.fogColor, smoothstep(0, 0.85, screenUV.y));
51
+ }
52
+ // A plain background color as well, for readers of scene.background.
53
+ this.scene.background = new Color(this.fogColor.value);
54
+ }
55
+ dispose() {
56
+ this.scene.fogNode = null;
57
+ this.scene.backgroundNode = null;
58
+ }
59
+ }
@@ -0,0 +1,68 @@
1
+ import { InstancedBufferAttribute, Mesh, MeshStandardNodeMaterial } from "three/webgpu";
2
+ import type { BufferGeometry, Material, MeshStandardMaterial, Node, Object3D } from "three/webgpu";
3
+ export interface CrowdMotionOptions {
4
+ /** Vertical hop per step at full speed, m. Default 0.12. */
5
+ readonly bob?: number;
6
+ /** Side-to-side roll per step at full speed, radians. Default 0.18. */
7
+ readonly waddle?: number;
8
+ /** Forward lean at full speed, radians. Default 0.16. */
9
+ readonly lean?: number;
10
+ /** Squash on a hit (0..1 of height). Default 0.32. */
11
+ readonly squash?: number;
12
+ /** Steps per second at a standstill and per m/s of speed. Default [1.2, 0.9]. */
13
+ readonly cadence?: readonly [number, number];
14
+ /** Speed (m/s) treated as "full speed". Default 4. */
15
+ readonly fullSpeed?: number;
16
+ /** Idle breathing scale amplitude. Default 0.03. */
17
+ readonly breathe?: number;
18
+ /** Emissive added at full hit flash. Default 2.5. */
19
+ readonly flash?: number;
20
+ /** Draw as camera-facing sprites (render/sprite.ts): no roll or lean, mirrored to face the way they walk. Default: from the geometry. */
21
+ readonly billboard?: boolean;
22
+ }
23
+ /** The per-instance attributes a crowd-motion material reads. */
24
+ export interface CrowdAttributes {
25
+ /** x, y, z, yaw. */
26
+ readonly xform: InstancedBufferAttribute;
27
+ /** phase (0..1), speed (m/s), hit (0..1 squash), flash (0..1 emissive). */
28
+ readonly anim: InstancedBufferAttribute;
29
+ /** uniform scale, unused x3. */
30
+ readonly extra: InstancedBufferAttribute;
31
+ }
32
+ type MotionMaterial = Material & {
33
+ positionNode: Node | null;
34
+ emissiveNode?: Node | null;
35
+ };
36
+ /**
37
+ * Vertex animation for crowds without skeletons: bob, waddle, forward
38
+ * lean and step cadence from each instance's speed, a squash-and-stretch on
39
+ * hit, a hit flash, idle breathing. Applied to an existing node material
40
+ * (positionNode, emissiveNode); geometry stands on y = 0 and faces -z.
41
+ */
42
+ export declare function applyCrowdMotion(material: MotionMaterial, attrs: CrowdAttributes, time: Node<"float">, opts?: CrowdMotionOptions): MotionMaterial;
43
+ /** Copy a classic MeshStandardMaterial into a node material (so positionNode can be set). */
44
+ export declare function toNodeMaterial(m: MeshStandardMaterial): MeshStandardNodeMaterial;
45
+ /**
46
+ * An instanced crowd with procedural animation: one draw, per-instance
47
+ * position, yaw, scale, phase, speed, hit squash and flash. Write instances
48
+ * between `begin()` and `end(time)` each frame; only the written range uploads.
49
+ */
50
+ export declare class AnimatedCrowd {
51
+ readonly capacity: number;
52
+ readonly mesh: Mesh;
53
+ readonly time: import("three/webgpu").UniformNode<"float", number>;
54
+ readonly attrs: CrowdAttributes;
55
+ count: number;
56
+ constructor(geometry: BufferGeometry, material: MeshStandardMaterial | MeshStandardNodeMaterial, capacity: number, opts?: CrowdMotionOptions);
57
+ begin(): void;
58
+ /**
59
+ * One instance. `phase` 0..1 desyncs steps (use a hash of the id),
60
+ * `speed` m/s drives cadence and amplitude, `hit` 0..1 squashes,
61
+ * `flash` 0..1 brightens.
62
+ */
63
+ push(x: number, y: number, z: number, yaw: number, phase: number, speed: number, hit?: number, flash?: number, scale?: number): void;
64
+ end(timeSeconds: number): void;
65
+ addTo(parent: Object3D): this;
66
+ dispose(): void;
67
+ }
68
+ export {};
@@ -0,0 +1,163 @@
1
+ import { DynamicDrawUsage, InstancedBufferAttribute, InstancedBufferGeometry, Mesh, MeshStandardNodeMaterial, } from "three/webgpu";
2
+ import { abs, clamp, cos, float, floor, max, instancedBufferAttribute, materialEmissive, positionGeometry, select, sin, uniform, varying, vec3, } from "three/tsl";
3
+ import { billboardWorld, isSprite } from "./sprite.js";
4
+ /**
5
+ * Vertex animation for crowds without skeletons: bob, waddle, forward
6
+ * lean and step cadence from each instance's speed, a squash-and-stretch on
7
+ * hit, a hit flash, idle breathing. Applied to an existing node material
8
+ * (positionNode, emissiveNode); geometry stands on y = 0 and faces -z.
9
+ */
10
+ export function applyCrowdMotion(material, attrs, time, opts = {}) {
11
+ const o = {
12
+ bob: 0.12,
13
+ waddle: 0.18,
14
+ lean: 0.16,
15
+ squash: 0.32,
16
+ cadence: [1.2, 0.9],
17
+ fullSpeed: 4,
18
+ breathe: 0.03,
19
+ flash: 2.5,
20
+ ...opts,
21
+ };
22
+ const X = instancedBufferAttribute(attrs.xform, "vec4");
23
+ const A = instancedBufferAttribute(attrs.anim, "vec4");
24
+ const E = instancedBufferAttribute(attrs.extra, "vec4");
25
+ const speed = A.y;
26
+ const amp = clamp(speed.div(o.fullSpeed), 0, 1);
27
+ const cadence = float(o.cadence[0]).add(speed.mul(o.cadence[1]));
28
+ const ph = A.x.mul(6.2832).add(float(time).mul(cadence).mul(6.2832));
29
+ const s = sin(ph);
30
+ const breath = sin(float(time).mul(2.1).add(A.x.mul(6.2832))).mul(o.breathe).mul(float(1).sub(amp));
31
+ const hit = A.z;
32
+ const sy = float(1).add(breath).add(s.mul(0.06).mul(amp)).sub(hit.mul(o.squash));
33
+ const sxz = float(1).add(hit.mul(o.squash * 0.5)).sub(breath.mul(0.5));
34
+ const g = positionGeometry;
35
+ const px = g.x.mul(sxz);
36
+ const py = g.y.mul(sy);
37
+ const pz = g.z.mul(sxz);
38
+ // Roll about the forward axis (waddle), then pitch forward (lean): the top moves toward -z.
39
+ const roll = s.mul(o.waddle).mul(amp);
40
+ const cr = cos(roll);
41
+ const sr = sin(roll);
42
+ const rx = px.mul(cr).sub(py.mul(sr));
43
+ const ry = px.mul(sr).add(py.mul(cr));
44
+ const lean = amp.mul(o.lean).add(hit.mul(-0.25));
45
+ const cl = cos(lean);
46
+ const sl = sin(lean);
47
+ const ly = ry.mul(cl).add(pz.mul(sl));
48
+ const lz = pz.mul(cl).sub(ry.mul(sl));
49
+ const scale = E.x;
50
+ const bob = abs(s).mul(o.bob).mul(amp);
51
+ if (opts.billboard) {
52
+ // A sprite keeps its bob and squash; it faces the camera and turns to the side it walks toward (yaw -pi/2 is +x).
53
+ const flip = select(sin(X.w).greaterThan(0.05), float(-1), float(1));
54
+ // Whole multiples only (an elite's 1.5 draws at 2): one sprite texel stays one picture pixel.
55
+ const whole = max(float(1), floor(scale.add(0.5)));
56
+ material.positionNode = billboardWorld(vec3(X.x, X.y.add(bob), X.z), px.mul(whole), py.mul(whole), flip);
57
+ if ("emissiveNode" in material)
58
+ material.emissiveNode = materialEmissive.add(vec3(varying(A.w)).mul(o.flash));
59
+ return material;
60
+ }
61
+ const cy = cos(X.w);
62
+ const syaw = sin(X.w);
63
+ const wx = rx.mul(cy).add(lz.mul(syaw)).mul(scale);
64
+ const wz = rx.mul(syaw).negate().add(lz.mul(cy)).mul(scale);
65
+ material.positionNode = vec3(wx.add(X.x), ly.mul(scale).add(X.y).add(bob), wz.add(X.z));
66
+ if ("emissiveNode" in material)
67
+ material.emissiveNode = materialEmissive.add(vec3(varying(A.w)).mul(o.flash));
68
+ return material;
69
+ }
70
+ /** Copy a classic MeshStandardMaterial into a node material (so positionNode can be set). */
71
+ export function toNodeMaterial(m) {
72
+ const n = new MeshStandardNodeMaterial();
73
+ n.color.copy(m.color);
74
+ n.emissive.copy(m.emissive);
75
+ n.emissiveIntensity = m.emissiveIntensity;
76
+ n.roughness = m.roughness;
77
+ n.metalness = m.metalness;
78
+ n.flatShading = m.flatShading;
79
+ n.map = m.map;
80
+ n.transparent = m.transparent;
81
+ n.opacity = m.opacity;
82
+ n.side = m.side;
83
+ n.alphaTest = m.alphaTest;
84
+ return n;
85
+ }
86
+ /**
87
+ * An instanced crowd with procedural animation: one draw, per-instance
88
+ * position, yaw, scale, phase, speed, hit squash and flash. Write instances
89
+ * between `begin()` and `end(time)` each frame; only the written range uploads.
90
+ */
91
+ export class AnimatedCrowd {
92
+ capacity;
93
+ mesh;
94
+ time = uniform(0);
95
+ attrs;
96
+ count = 0;
97
+ constructor(geometry, material, capacity, opts = {}) {
98
+ this.capacity = capacity;
99
+ const g = new InstancedBufferGeometry();
100
+ if (geometry.index)
101
+ g.setIndex(geometry.index);
102
+ for (const [name, a] of Object.entries(geometry.attributes))
103
+ g.setAttribute(name, a);
104
+ g.instanceCount = 0;
105
+ const mk = () => {
106
+ const a = new InstancedBufferAttribute(new Float32Array(capacity * 4), 4);
107
+ a.setUsage(DynamicDrawUsage);
108
+ return a;
109
+ };
110
+ this.attrs = { xform: mk(), anim: mk(), extra: mk() };
111
+ const mat = material.isNodeMaterial ? material : toNodeMaterial(material);
112
+ applyCrowdMotion(mat, this.attrs, this.time, { billboard: isSprite(geometry), ...opts });
113
+ this.mesh = new Mesh(g, mat);
114
+ this.mesh.frustumCulled = false;
115
+ this.mesh.castShadow = true;
116
+ this.mesh.receiveShadow = false;
117
+ this.mesh.name = "AnimatedCrowd";
118
+ }
119
+ begin() {
120
+ this.count = 0;
121
+ }
122
+ /**
123
+ * One instance. `phase` 0..1 desyncs steps (use a hash of the id),
124
+ * `speed` m/s drives cadence and amplitude, `hit` 0..1 squashes,
125
+ * `flash` 0..1 brightens.
126
+ */
127
+ push(x, y, z, yaw, phase, speed, hit = 0, flash = 0, scale = 1) {
128
+ if (this.count >= this.capacity)
129
+ return;
130
+ const o = this.count * 4;
131
+ const X = this.attrs.xform.array;
132
+ const A = this.attrs.anim.array;
133
+ const E = this.attrs.extra.array;
134
+ X[o] = x;
135
+ X[o + 1] = y;
136
+ X[o + 2] = z;
137
+ X[o + 3] = yaw;
138
+ A[o] = phase;
139
+ A[o + 1] = speed;
140
+ A[o + 2] = hit < 0 ? 0 : hit > 1 ? 1 : hit;
141
+ A[o + 3] = flash < 0 ? 0 : flash > 1 ? 1 : flash;
142
+ E[o] = scale;
143
+ this.count++;
144
+ }
145
+ end(timeSeconds) {
146
+ this.time.value = timeSeconds;
147
+ this.mesh.geometry.instanceCount = this.count;
148
+ for (const a of [this.attrs.xform, this.attrs.anim, this.attrs.extra]) {
149
+ a.clearUpdateRanges();
150
+ if (this.count > 0)
151
+ a.addUpdateRange(0, this.count * 4);
152
+ a.needsUpdate = true;
153
+ }
154
+ }
155
+ addTo(parent) {
156
+ parent.add(this.mesh);
157
+ return this;
158
+ }
159
+ dispose() {
160
+ this.mesh.removeFromParent();
161
+ this.mesh.geometry.dispose();
162
+ }
163
+ }
@@ -0,0 +1,64 @@
1
+ import { Mesh } from "three/webgpu";
2
+ import type { Object3D } from "three/webgpu";
3
+ import { ParticleRing } from "./particles.js";
4
+ import type { DecalLook, RingLook } from "../styles/kit.js";
5
+ export type DecalKind = "scorch" | "splat" | "ring";
6
+ export interface DecalOptions {
7
+ /** Diameter, m. Default 1.6. */
8
+ size?: number;
9
+ color?: number;
10
+ /** Seconds before it has faded out. Default 6. */
11
+ life?: number;
12
+ /** Peak opacity 0..1. Default 0.75. */
13
+ opacity?: number;
14
+ /** Rotation about y, radians (default random). */
15
+ rotation?: number;
16
+ /** Color multiplier above 1 makes it glow under bloom. Default 1. */
17
+ glow?: number;
18
+ }
19
+ /**
20
+ * Ground decals (scorch marks, splats, rings) in one draw: flat quads on
21
+ * the floor, shapes drawn procedurally in the fragment stage (no textures),
22
+ * faded over their life. A ring buffer: the oldest decal is reused.
23
+ */
24
+ export declare class GroundDecals {
25
+ readonly time: import("three/webgpu").UniformNode<"float", number>;
26
+ readonly ring: ParticleRing;
27
+ readonly mesh: Mesh;
28
+ private readonly a;
29
+ private readonly b;
30
+ private readonly c;
31
+ private readonly rand;
32
+ /** Height above the floor, m. */
33
+ floorY: number;
34
+ constructor(parent: Object3D, capacity?: number, random?: () => number, look?: DecalLook);
35
+ /** Stamp a decal on the floor at (x, z). */
36
+ add(kind: DecalKind, x: number, z: number, opts?: DecalOptions): void;
37
+ update(timeSeconds: number): void;
38
+ dispose(): void;
39
+ }
40
+ export interface RingOptions {
41
+ color?: number;
42
+ /** Seconds to expand and fade. Default 0.32. */
43
+ duration?: number;
44
+ /** Brightness multiplier (bloom). Default 1.6. */
45
+ glow?: number;
46
+ }
47
+ /**
48
+ * Expanding shockwave rings on the floor (nova blasts, landings, dash
49
+ * starts): one additive draw, pooled. Generalized from EMBERWATCH's rings.
50
+ */
51
+ export declare class GroundRings {
52
+ readonly time: import("three/webgpu").UniformNode<"float", number>;
53
+ readonly ring: ParticleRing;
54
+ readonly mesh: Mesh;
55
+ private readonly a;
56
+ private readonly b;
57
+ /** Rings fired so far (playtest checks). */
58
+ fired: number;
59
+ constructor(parent: Object3D, capacity?: number, look?: RingLook);
60
+ /** A ring at (x, z) reaching `radius` m. */
61
+ fire(x: number, z: number, radius: number, opts?: RingOptions): void;
62
+ update(timeSeconds: number): void;
63
+ dispose(): void;
64
+ }