@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,327 @@
1
+ /**
2
+ * Footstep and impact audio hooks: named cues with a
3
+ * throttle, a voice limit, pitch and gain variation, stereo pan from the
4
+ * screen and distance falloff. Every cue has a procedural synth recipe by
5
+ * default (no audio files); `addBuffer` swaps in a real CC0 sample later
6
+ * under the same name. WebAudio is created on the first user gesture.
7
+ */
8
+ import { seededRandom } from "./noise.js";
9
+ /** The built-in cues every template can use by name. */
10
+ export const DEFAULT_CUES = {
11
+ hit: { wave: "square", f0: 420, f1: 140, ms: 60, gain: 0.35, minGapMs: 45, voices: 3 },
12
+ impact: { wave: "noise", f0: 900, f1: 200, ms: 90, gain: 0.5, minGapMs: 50, voices: 3 },
13
+ kill: { wave: "triangle", f0: 900, f1: 300, ms: 70, gain: 0.4, minGapMs: 60, voices: 3 },
14
+ bigKill: { wave: "sawtooth", f0: 160, f1: 40, ms: 260, gain: 0.8, minGapMs: 120, voices: 2 },
15
+ hurt: { wave: "sawtooth", f0: 220, f1: 70, ms: 180, gain: 0.9, minGapMs: 120, voices: 1 },
16
+ levelUp: { wave: "triangle", f0: 520, f1: 1040, ms: 260, gain: 0.8, minGapMs: 200, voices: 1, pitchVar: 0 },
17
+ pick: { wave: "sine", f0: 660, f1: 990, ms: 90, gain: 0.6, minGapMs: 80, voices: 2 },
18
+ coin: { wave: "sine", f0: 1320, f1: 1980, ms: 70, gain: 0.35, minGapMs: 35, voices: 3 },
19
+ dash: { wave: "noise", f0: 2400, f1: 500, ms: 140, gain: 0.45, minGapMs: 100, voices: 1 },
20
+ land: { wave: "noise", f0: 500, f1: 120, ms: 110, gain: 0.55, minGapMs: 120, voices: 1 },
21
+ step: { wave: "noise", f0: 700, f1: 180, ms: 45, gain: 0.22, minGapMs: 90, voices: 2, pitchVar: 0.1 },
22
+ };
23
+ /**
24
+ * The DOM-free decision core: may a cue start now? Per-cue gap and voice
25
+ * cap, and a global voice cap.
26
+ */
27
+ export class CueGate {
28
+ maxVoices;
29
+ last = new Map();
30
+ playing = [];
31
+ constructor(maxVoices = 16) {
32
+ this.maxVoices = maxVoices;
33
+ }
34
+ allow(name, nowMs, durationMs, minGapMs, cueVoices) {
35
+ for (let i = this.playing.length - 1; i >= 0; i--)
36
+ if ((this.playing[i]?.until ?? 0) <= nowMs)
37
+ this.playing.splice(i, 1);
38
+ const last = this.last.get(name);
39
+ if (last !== undefined && nowMs - last < minGapMs)
40
+ return false;
41
+ if (this.playing.length >= this.maxVoices)
42
+ return false;
43
+ let mine = 0;
44
+ for (const p of this.playing)
45
+ if (p.name === name)
46
+ mine++;
47
+ if (mine >= cueVoices)
48
+ return false;
49
+ this.last.set(name, nowMs);
50
+ this.playing.push({ name, until: nowMs + durationMs });
51
+ return true;
52
+ }
53
+ get voices() {
54
+ return this.playing.length;
55
+ }
56
+ }
57
+ /** Gain for a source `distance` m away (inverse falloff, 0 past maxDistance). */
58
+ export function distanceGain(distance, refDistance = 6, maxDistance = 40, rolloff = 1) {
59
+ if (!(distance > refDistance))
60
+ return 1;
61
+ if (distance >= maxDistance)
62
+ return 0;
63
+ return refDistance / (refDistance + rolloff * (distance - refDistance));
64
+ }
65
+ export class FeelAudio {
66
+ muted = false;
67
+ /** Cues actually started (playtest counters). */
68
+ played = 0;
69
+ gate;
70
+ ctx = null;
71
+ masterNode = null;
72
+ noise = null;
73
+ masterGain;
74
+ cues = new Map();
75
+ unlocked = [];
76
+ rnd;
77
+ o;
78
+ constructor(opts = {}) {
79
+ this.masterGain = opts.master ?? 0.18;
80
+ this.gate = new CueGate(opts.voices ?? 16);
81
+ this.rnd = seededRandom(opts.seed ?? 7);
82
+ this.o = {
83
+ refDistance: opts.refDistance ?? 6,
84
+ maxDistance: opts.maxDistance ?? 40,
85
+ context: opts.context,
86
+ now: opts.now ?? (() => (typeof performance !== "undefined" ? performance.now() : Date.now())),
87
+ };
88
+ for (const [name, c] of Object.entries(DEFAULT_CUES))
89
+ this.define(name, c, c);
90
+ }
91
+ /** Start WebAudio (call from a user gesture; browsers block audio before one). Safe to call repeatedly. */
92
+ unlock() {
93
+ if (this.ctx) {
94
+ if (this.ctx.state === "suspended")
95
+ void this.ctx.resume().catch(() => undefined);
96
+ return;
97
+ }
98
+ try {
99
+ const make = this.o.context ?? (typeof AudioContext !== "undefined" ? () => new AudioContext() : undefined);
100
+ if (!make)
101
+ return;
102
+ this.ctx = make();
103
+ this.masterNode = this.ctx.createGain();
104
+ this.masterNode.gain.value = this.masterGain;
105
+ this.masterNode.connect(this.ctx.destination);
106
+ }
107
+ catch {
108
+ this.ctx = null;
109
+ this.masterNode = null;
110
+ return;
111
+ }
112
+ const ctx = this.ctx;
113
+ for (const fn of this.unlocked.splice(0))
114
+ fn(ctx);
115
+ }
116
+ /** Run `fn` with the AudioContext once audio is unlocked (now, if it already is). */
117
+ whenUnlocked(fn) {
118
+ if (this.ctx)
119
+ fn(this.ctx);
120
+ else
121
+ this.unlocked.push(fn);
122
+ }
123
+ /** The AudioContext, once unlocked (music and other buses hang off it). */
124
+ get context() {
125
+ return this.ctx;
126
+ }
127
+ /** Unlock on the first pointer or key press on `target` (default window). */
128
+ unlockOnGesture(target) {
129
+ const t = target ?? (typeof window !== "undefined" ? window : undefined);
130
+ if (!t)
131
+ return;
132
+ const go = () => this.unlock();
133
+ t.addEventListener("pointerdown", go, { once: true });
134
+ t.addEventListener("keydown", go, { once: true });
135
+ t.addEventListener("touchstart", go, { once: true });
136
+ }
137
+ get ready() {
138
+ return this.ctx !== null;
139
+ }
140
+ get master() {
141
+ return this.masterGain;
142
+ }
143
+ set master(v) {
144
+ this.masterGain = Math.max(0, Math.min(1, v));
145
+ if (this.masterNode)
146
+ this.masterNode.gain.value = this.masterGain;
147
+ }
148
+ /** Define or replace a cue's synth recipe and options. */
149
+ define(name, recipe, opts = {}) {
150
+ const prev = this.cues.get(name);
151
+ this.cues.set(name, {
152
+ recipe,
153
+ buffer: prev?.buffer ?? null,
154
+ minGapMs: opts.minGapMs ?? prev?.minGapMs ?? 40,
155
+ pitchVar: opts.pitchVar ?? prev?.pitchVar ?? 0.06,
156
+ gainVar: opts.gainVar ?? prev?.gainVar ?? 0.1,
157
+ voices: opts.voices ?? prev?.voices ?? 4,
158
+ });
159
+ }
160
+ /**
161
+ * Use a real sample for a cue (an AudioBuffer, or a URL fetched and decoded once audio is unlocked).
162
+ * `opts.gain` is the sample's level before the master gain (default: the cue's synth level).
163
+ */
164
+ async addBuffer(name, source, opts = {}) {
165
+ let buffer;
166
+ if (typeof source === "string") {
167
+ if (!this.ctx)
168
+ this.unlock();
169
+ const ctx = this.ctx;
170
+ if (!ctx || typeof fetch === "undefined")
171
+ return;
172
+ const data = await (await fetch(source)).arrayBuffer();
173
+ buffer = await ctx.decodeAudioData(data);
174
+ }
175
+ else
176
+ buffer = source;
177
+ const prev = this.cues.get(name);
178
+ const base = prev?.recipe ?? { wave: "sine", f0: 440, f1: 440, ms: buffer.duration * 1000, gain: 1 };
179
+ const recipe = opts.gain !== undefined ? { ...base, gain: opts.gain } : base;
180
+ this.define(name, recipe, opts);
181
+ const cue = this.cues.get(name);
182
+ if (cue)
183
+ cue.buffer = buffer;
184
+ }
185
+ has(name) {
186
+ return this.cues.has(name);
187
+ }
188
+ /** Play a cue. Returns false when muted, locked, unknown, throttled or out of voices. */
189
+ play(name, opts = {}) {
190
+ const cue = this.cues.get(name);
191
+ if (!cue || this.muted)
192
+ return false;
193
+ const ctx = this.ctx;
194
+ const master = this.masterNode;
195
+ if (!ctx || !master)
196
+ return false;
197
+ let gain = cue.recipe.gain * (opts.gain ?? 1) * (1 + (this.rnd() * 2 - 1) * cue.gainVar);
198
+ if (opts.distance !== undefined)
199
+ gain *= distanceGain(opts.distance, this.o.refDistance, this.o.maxDistance);
200
+ if (gain <= 0.001)
201
+ return false;
202
+ const ms = cue.buffer ? cue.buffer.duration * 1000 : cue.recipe.ms;
203
+ if (!this.gate.allow(name, this.o.now(), ms, cue.minGapMs, cue.voices))
204
+ return false;
205
+ const pitch = (opts.pitch ?? 1) * (1 + (this.rnd() * 2 - 1) * cue.pitchVar);
206
+ let pan = opts.pan;
207
+ if (pan === undefined && opts.screenX !== undefined && opts.width !== undefined && opts.width > 0)
208
+ pan = (opts.screenX / opts.width) * 2 - 1;
209
+ try {
210
+ this.voice(ctx, master, cue, gain, pitch, Math.max(-1, Math.min(1, pan ?? 0)));
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ this.played++;
216
+ return true;
217
+ }
218
+ /** An impact scaled by damage (bigger hits are louder and lower). */
219
+ impact(damage, opts = {}) {
220
+ const k = Math.min(1, Math.max(0, Math.log10(1 + Math.max(0, damage)) / 2));
221
+ return this.play("impact", { ...opts, gain: (opts.gain ?? 1) * (0.5 + k * 0.7), pitch: (opts.pitch ?? 1) * (1.2 - k * 0.5) });
222
+ }
223
+ voice(ctx, master, cue, gain, pitch, pan) {
224
+ const t = ctx.currentTime;
225
+ const g = ctx.createGain();
226
+ let out = g;
227
+ if (pan !== 0 && typeof ctx.createStereoPanner === "function") {
228
+ const p = ctx.createStereoPanner();
229
+ p.pan.value = pan;
230
+ g.connect(p);
231
+ out = p;
232
+ }
233
+ out.connect(master);
234
+ if (cue.buffer) {
235
+ const src = ctx.createBufferSource();
236
+ src.buffer = cue.buffer;
237
+ src.playbackRate.value = pitch;
238
+ g.gain.setValueAtTime(gain, t);
239
+ src.connect(g);
240
+ src.start(t);
241
+ return;
242
+ }
243
+ const r = cue.recipe;
244
+ const dur = r.ms / 1000;
245
+ g.gain.setValueAtTime(gain, t);
246
+ g.gain.exponentialRampToValueAtTime(0.001, t + dur);
247
+ if (r.wave === "noise") {
248
+ const src = ctx.createBufferSource();
249
+ src.buffer = this.noiseBuffer(ctx);
250
+ const f = ctx.createBiquadFilter();
251
+ f.type = "bandpass";
252
+ f.Q.value = 1.2;
253
+ f.frequency.setValueAtTime(r.f0 * pitch, t);
254
+ f.frequency.exponentialRampToValueAtTime(Math.max(20, r.f1 * pitch), t + dur);
255
+ src.connect(f).connect(g);
256
+ src.start(t);
257
+ src.stop(t + dur + 0.02);
258
+ return;
259
+ }
260
+ const o = ctx.createOscillator();
261
+ o.type = r.wave;
262
+ o.frequency.setValueAtTime(r.f0 * pitch, t);
263
+ o.frequency.exponentialRampToValueAtTime(Math.max(20, r.f1 * pitch), t + dur);
264
+ o.connect(g);
265
+ o.start(t);
266
+ o.stop(t + dur + 0.02);
267
+ }
268
+ noiseBuffer(ctx) {
269
+ if (this.noise)
270
+ return this.noise;
271
+ const len = Math.floor(ctx.sampleRate * 0.5);
272
+ const b = ctx.createBuffer(1, len, ctx.sampleRate);
273
+ const d = b.getChannelData(0);
274
+ const rnd = seededRandom(99);
275
+ for (let i = 0; i < len; i++)
276
+ d[i] = rnd() * 2 - 1;
277
+ this.noise = b;
278
+ return b;
279
+ }
280
+ }
281
+ /** Surfaces change a footstep's pitch and weight. */
282
+ export const SURFACES = {
283
+ grass: { pitch: 0.85, gain: 0.8 },
284
+ dirt: { pitch: 0.9, gain: 0.9 },
285
+ stone: { pitch: 1.25, gain: 1 },
286
+ wood: { pitch: 1.05, gain: 1 },
287
+ metal: { pitch: 1.6, gain: 1.1 },
288
+ };
289
+ /**
290
+ * Footstep cadence from speed: steps per second = speed / stride, with an
291
+ * alternating left/right pitch. Calls `play(pitch, gain)` on each step.
292
+ */
293
+ export class Footsteps {
294
+ play;
295
+ surface = "dirt";
296
+ steps = 0;
297
+ phase = 0;
298
+ left = false;
299
+ o;
300
+ constructor(play, opts = {}) {
301
+ this.play = play;
302
+ this.o = { stride: 1.4, maxRate: 6, minSpeed: 0.6, ...opts };
303
+ }
304
+ /** Advance by `dt` seconds at `speed` m/s. Returns the number of steps played. */
305
+ update(dt, speed, grounded = true) {
306
+ if (dt <= 0)
307
+ return 0;
308
+ if (!grounded || speed < this.o.minSpeed) {
309
+ // Next step lands soon after moving again, not a full stride later.
310
+ this.phase = Math.min(this.phase, 0.7);
311
+ return 0;
312
+ }
313
+ const rate = Math.min(this.o.maxRate, speed / this.o.stride);
314
+ this.phase += rate * dt;
315
+ let n = 0;
316
+ while (this.phase >= 1) {
317
+ this.phase -= 1;
318
+ this.left = !this.left;
319
+ const s = SURFACES[this.surface];
320
+ const weight = Math.min(1.2, 0.6 + speed / 12);
321
+ this.play(s.pitch * (this.left ? 1 : 0.92), s.gain * weight);
322
+ this.steps++;
323
+ n++;
324
+ }
325
+ return n;
326
+ }
327
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The render clock: hit-stop and slow motion for the DRAWN world only.
3
+ * The netcode never pauses; the clock returns a render time `now` that
4
+ * runs behind real time while frozen or slowed, then catches up smoothly so
5
+ * the world never jumps. `now` never runs ahead of real time.
6
+ */
7
+ export interface FeelClockOptions {
8
+ /** Milliseconds over which accumulated lag eases back to zero. Default 200. */
9
+ readonly catchUpMs?: number;
10
+ /** Most the drawn world may fall behind (keeps interpolation buffers valid). Default 400. */
11
+ readonly maxLagMs?: number;
12
+ /** Clamp on one frame's real delta (tab switches). Default 100. */
13
+ readonly maxFrameMs?: number;
14
+ }
15
+ export interface ClockTick {
16
+ /** Time to draw the world at (ms, same base as `realNow`). */
17
+ readonly now: number;
18
+ /** Scaled frame delta in seconds (0 while frozen). */
19
+ readonly dt: number;
20
+ /** Unscaled frame delta in seconds (UI, shake and audio run on this). */
21
+ readonly realDt: number;
22
+ /** The time scale this frame (0 frozen, below 1 slowed, above 1 catching up). */
23
+ readonly timeScale: number;
24
+ }
25
+ export declare class FeelClock {
26
+ /** Hit-stops requested so far (playtest counters). */
27
+ stops: number;
28
+ /** Slow-motion requests so far. */
29
+ slows: number;
30
+ private frozenUntil;
31
+ private lag;
32
+ private lastReal;
33
+ private slowScale;
34
+ private slowUntil;
35
+ private slowEaseMs;
36
+ private readonly o;
37
+ constructor(opts?: FeelClockOptions);
38
+ /** Freeze the drawn world for `ms`. Longer stops win; they do not stack. */
39
+ hitStop(ms: number, realNow?: number): void;
40
+ /** Slow the drawn world to `scale` (0..1) for `ms`, then ease back to full speed over `easeMs`. */
41
+ slowMo(scale: number, ms: number, easeMs?: number, realNow?: number): void;
42
+ /** Current slow-motion scale (1 when none). */
43
+ private slowAt;
44
+ /** Advance with the frame's real time. */
45
+ tick(realNow?: number): ClockTick;
46
+ /** True while a hit-stop holds the drawn world. */
47
+ get stopped(): boolean;
48
+ /** Milliseconds the drawn world is behind real time. */
49
+ get lagMs(): number;
50
+ }
51
+ /** The name templates already use (template-survivor's RenderClock): same class. */
52
+ export { FeelClock as RenderClock };
@@ -0,0 +1,80 @@
1
+ const nowMs = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
2
+ export class FeelClock {
3
+ /** Hit-stops requested so far (playtest counters). */
4
+ stops = 0;
5
+ /** Slow-motion requests so far. */
6
+ slows = 0;
7
+ frozenUntil = 0;
8
+ lag = 0;
9
+ lastReal = 0;
10
+ slowScale = 1;
11
+ slowUntil = 0;
12
+ slowEaseMs = 0;
13
+ o;
14
+ constructor(opts = {}) {
15
+ this.o = { catchUpMs: 200, maxLagMs: 400, maxFrameMs: 100, ...opts };
16
+ }
17
+ /** Freeze the drawn world for `ms`. Longer stops win; they do not stack. */
18
+ hitStop(ms, realNow = this.lastReal || nowMs()) {
19
+ if (!(ms > 0))
20
+ return;
21
+ this.stops++;
22
+ this.frozenUntil = Math.max(this.frozenUntil, realNow + ms);
23
+ }
24
+ /** Slow the drawn world to `scale` (0..1) for `ms`, then ease back to full speed over `easeMs`. */
25
+ slowMo(scale, ms, easeMs = 200, realNow = this.lastReal || nowMs()) {
26
+ if (!(ms > 0))
27
+ return;
28
+ this.slows++;
29
+ const s = Math.min(1, Math.max(0.01, scale));
30
+ // The deeper slow wins while both are active.
31
+ if (realNow < this.slowUntil + this.slowEaseMs)
32
+ this.slowScale = Math.min(this.slowScale, s);
33
+ else
34
+ this.slowScale = s;
35
+ this.slowUntil = Math.max(this.slowUntil, realNow + ms);
36
+ this.slowEaseMs = Math.max(0, easeMs);
37
+ }
38
+ /** Current slow-motion scale (1 when none). */
39
+ slowAt(realNow) {
40
+ if (realNow < this.slowUntil)
41
+ return this.slowScale;
42
+ if (this.slowEaseMs > 0 && realNow < this.slowUntil + this.slowEaseMs) {
43
+ const t = (realNow - this.slowUntil) / this.slowEaseMs;
44
+ const e = t * t * (3 - 2 * t);
45
+ return this.slowScale + (1 - this.slowScale) * e;
46
+ }
47
+ return 1;
48
+ }
49
+ /** Advance with the frame's real time. */
50
+ tick(realNow = nowMs()) {
51
+ const realDt = this.lastReal === 0 ? 1000 / 60 : Math.max(0, Math.min(this.o.maxFrameMs, realNow - this.lastReal));
52
+ this.lastReal = realNow;
53
+ if (realNow < this.frozenUntil) {
54
+ this.lag = Math.min(this.o.maxLagMs, this.lag + realDt);
55
+ return { now: realNow - this.lag, dt: 0, realDt: realDt / 1000, timeScale: 0 };
56
+ }
57
+ const scale = this.slowAt(realNow);
58
+ if (scale < 1) {
59
+ const lagged = Math.min(this.o.maxLagMs, this.lag + realDt * (1 - scale));
60
+ const drawn = realDt - (lagged - this.lag);
61
+ this.lag = lagged;
62
+ return { now: realNow - this.lag, dt: drawn / 1000, realDt: realDt / 1000, timeScale: realDt > 0 ? drawn / realDt : 1 };
63
+ }
64
+ // Catch up over ~catchUpMs so the world never jumps.
65
+ const catchUp = Math.min(this.lag, this.lag * Math.min(1, realDt / this.o.catchUpMs) + 0.5);
66
+ this.lag = Math.max(0, this.lag - catchUp);
67
+ const drawn = realDt + catchUp;
68
+ return { now: realNow - this.lag, dt: drawn / 1000, realDt: realDt / 1000, timeScale: realDt > 0 ? drawn / realDt : 1 };
69
+ }
70
+ /** True while a hit-stop holds the drawn world. */
71
+ get stopped() {
72
+ return this.lastReal < this.frozenUntil;
73
+ }
74
+ /** Milliseconds the drawn world is behind real time. */
75
+ get lagMs() {
76
+ return this.lag;
77
+ }
78
+ }
79
+ /** The name templates already use (template-survivor's RenderClock): same class. */
80
+ export { FeelClock as RenderClock };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Hit flash: per-entity timers that read as 0..1 intensity, for instanced
3
+ * crowds that tint an instance toward white (or any colour) on a hit.
4
+ */
5
+ export declare class HitFlash {
6
+ seconds: number;
7
+ private readonly t;
8
+ /** Flashes triggered so far (playtest counters). */
9
+ count: number;
10
+ /** `seconds`: how long a full flash lasts. */
11
+ constructor(seconds?: number);
12
+ /** Start (or refresh) a flash on `id`; `strength` 0..1 scales its length. */
13
+ trigger(id: number, strength?: number): void;
14
+ /** 0..1 flash intensity of `id` now. */
15
+ value(id: number): number;
16
+ /** Advance every flash; expired entries are pruned. */
17
+ update(dt: number): void;
18
+ /** Live flashes (for tests and budgets). */
19
+ get size(): number;
20
+ clear(): void;
21
+ }
22
+ /** Colour multiplier for a flash: 1 at rest, 1 + boost at full flash (instance colours above 1 read as glowing white). */
23
+ export declare const flashMultiplier: (flash: number, boost?: number) => number;
24
+ /**
25
+ * Write a flashed instance colour into `out` at `offset` (rgb floats):
26
+ * base colour mixed toward `tint` and boosted by the flash.
27
+ */
28
+ export declare function flashColor(out: Float32Array | number[], offset: number, flash: number, base?: readonly [number, number, number], tint?: readonly [number, number, number], boost?: number): void;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Hit flash: per-entity timers that read as 0..1 intensity, for instanced
3
+ * crowds that tint an instance toward white (or any colour) on a hit.
4
+ */
5
+ export class HitFlash {
6
+ seconds;
7
+ t = new Map();
8
+ /** Flashes triggered so far (playtest counters). */
9
+ count = 0;
10
+ /** `seconds`: how long a full flash lasts. */
11
+ constructor(seconds = 0.09) {
12
+ this.seconds = seconds;
13
+ }
14
+ /** Start (or refresh) a flash on `id`; `strength` 0..1 scales its length. */
15
+ trigger(id, strength = 1) {
16
+ const s = Math.max(0, Math.min(1, strength)) * this.seconds;
17
+ if (s <= 0)
18
+ return;
19
+ this.count++;
20
+ this.t.set(id, Math.max(this.t.get(id) ?? 0, s));
21
+ }
22
+ /** 0..1 flash intensity of `id` now. */
23
+ value(id) {
24
+ const v = this.t.get(id);
25
+ return v === undefined || this.seconds <= 0 ? 0 : Math.min(1, v / this.seconds);
26
+ }
27
+ /** Advance every flash; expired entries are pruned. */
28
+ update(dt) {
29
+ if (dt <= 0)
30
+ return;
31
+ for (const [id, v] of this.t) {
32
+ const n = v - dt;
33
+ if (n <= 0)
34
+ this.t.delete(id);
35
+ else
36
+ this.t.set(id, n);
37
+ }
38
+ }
39
+ /** Live flashes (for tests and budgets). */
40
+ get size() {
41
+ return this.t.size;
42
+ }
43
+ clear() {
44
+ this.t.clear();
45
+ }
46
+ }
47
+ /** Colour multiplier for a flash: 1 at rest, 1 + boost at full flash (instance colours above 1 read as glowing white). */
48
+ export const flashMultiplier = (flash, boost = 2.5) => 1 + Math.max(0, Math.min(1, flash)) * boost;
49
+ /**
50
+ * Write a flashed instance colour into `out` at `offset` (rgb floats):
51
+ * base colour mixed toward `tint` and boosted by the flash.
52
+ */
53
+ export function flashColor(out, offset, flash, base = [1, 1, 1], tint = [1, 1, 1], boost = 2.5) {
54
+ const f = Math.max(0, Math.min(1, flash));
55
+ const m = 1 + f * boost;
56
+ for (let i = 0; i < 3; i++) {
57
+ const b = base[i] ?? 1;
58
+ out[offset + i] = (b + ((tint[i] ?? 1) - b) * f) * m;
59
+ }
60
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The game-feel kit: one-line components every
3
+ * template uses. See kit.ts for the FeelKit facade.
4
+ */
5
+ export { FeelKit, FEEL_PRESETS } from "./kit.js";
6
+ export type { FeelKitOptions, FeelPreset, FeelPresetName, HitEvent, KillEvent } from "./kit.js";
7
+ export { Trauma } from "./trauma.js";
8
+ export type { TraumaOptions } from "./trauma.js";
9
+ export { FeelClock, RenderClock } from "./clock.js";
10
+ export type { ClockTick, FeelClockOptions } from "./clock.js";
11
+ export { HitFlash, flashColor, flashMultiplier } from "./flash.js";
12
+ export { KnockbackOffsets } from "./knockback.js";
13
+ export type { KnockbackOptions, KnockbackSample } from "./knockback.js";
14
+ export { Spring, Spring3, stepSpring } from "./spring.js";
15
+ export type { SpringOptions } from "./spring.js";
16
+ export { DamageNumbers, NumberStack } from "./numbers.js";
17
+ export type { DamageNumbersOptions, NumberDraw, NumberInput, NumberSlot, NumberStackOptions } from "./numbers.js";
18
+ export { Ease, Tweens, uiTweens, pop, shakeEl, countUp } from "./tween.js";
19
+ export type { EaseFn, EaseName, FeelElement, TweenOptions, TweenProps } from "./tween.js";
20
+ export { FeelAudio, Footsteps, CueGate, DEFAULT_CUES, SURFACES, distanceGain } from "./audio.js";
21
+ export type { CueOptions, FeelAudioOptions, FootstepOptions, PlayOptions, Surface, SynthRecipe } from "./audio.js";
22
+ export { SoundPlayer } from "./sound.js";
23
+ export type { MusicElement, SoundBank, SoundBankEntry, SoundPlayerOptions } from "./sound.js";
24
+ export { Rumble } from "./rumble.js";
25
+ export type { GamepadLike, NavigatorLike, RumbleActuator, RumbleOptions } from "./rumble.js";
26
+ export { hashSigned, valueNoise1, seededRandom } from "./noise.js";
27
+ export type { ParticleBurstOptions, ParticleSink, Projector, TraumaTarget } from "./types.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The game-feel kit: one-line components every
3
+ * template uses. See kit.ts for the FeelKit facade.
4
+ */
5
+ export { FeelKit, FEEL_PRESETS } from "./kit.js";
6
+ export { Trauma } from "./trauma.js";
7
+ export { FeelClock, RenderClock } from "./clock.js";
8
+ export { HitFlash, flashColor, flashMultiplier } from "./flash.js";
9
+ export { KnockbackOffsets } from "./knockback.js";
10
+ export { Spring, Spring3, stepSpring } from "./spring.js";
11
+ export { DamageNumbers, NumberStack } from "./numbers.js";
12
+ export { Ease, Tweens, uiTweens, pop, shakeEl, countUp } from "./tween.js";
13
+ export { FeelAudio, Footsteps, CueGate, DEFAULT_CUES, SURFACES, distanceGain } from "./audio.js";
14
+ export { SoundPlayer } from "./sound.js";
15
+ export { Rumble } from "./rumble.js";
16
+ export { hashSigned, valueNoise1, seededRandom } from "./noise.js";