@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,159 @@
1
+ /**
2
+ * FeelKit: the engine's game-feel kit behind one-liners a
3
+ * template calls from game events:
4
+ *
5
+ * const feel = new FeelKit({ camera: rig, particles, project, preset: "default" });
6
+ * const { now, dt } = feel.tick(performance.now()); // hit-stop aware render clock
7
+ * feel.hit({ id, x, y, z, amount, dirX, dirZ }); // flash, knockback, number, sound, a touch of shake
8
+ * feel.kill({ x, y, z, big }); // burst, shake, hit-stop when big
9
+ * feel.hurt(12); // shake, stop, rumble, `hurtPulse` for a vignette
10
+ * feel.update(dt); // once per frame, after the events
11
+ *
12
+ * Nothing here needs three.js: the camera is any TraumaTarget (a CameraRig,
13
+ * or the kit's own Trauma), particles any ParticleSink, and world to screen
14
+ * an injected projector.
15
+ */
16
+ import { FeelAudio, Footsteps } from "./audio.js";
17
+ import type { Surface } from "./audio.js";
18
+ import { FeelClock } from "./clock.js";
19
+ import type { ClockTick } from "./clock.js";
20
+ import { HitFlash } from "./flash.js";
21
+ import { KnockbackOffsets } from "./knockback.js";
22
+ import { DamageNumbers } from "./numbers.js";
23
+ import { Rumble } from "./rumble.js";
24
+ import { Trauma } from "./trauma.js";
25
+ import { Tweens } from "./tween.js";
26
+ import type { ParticleSink, Projector, TraumaTarget } from "./types.js";
27
+ /** Tunables for every one-liner. Pick a named preset or override fields. */
28
+ export interface FeelPreset {
29
+ /** Trauma per hit, and the most hits may add in one frame (a nova through 50 enemies is one kick). */
30
+ readonly hitTrauma: number;
31
+ readonly hitTraumaPerFrame: number;
32
+ readonly killTrauma: number;
33
+ readonly bigKillTrauma: number;
34
+ /** Hurt trauma at a hit of 10% max health; scales with damage up to 2x. */
35
+ readonly hurtTrauma: number;
36
+ readonly bigKillStopMs: number;
37
+ readonly hurtStopMs: number;
38
+ readonly levelUpStopMs: number;
39
+ /** Visual knockback strength (m of offset) of a normal and a big hit. */
40
+ readonly knockback: number;
41
+ readonly bigKnockback: number;
42
+ readonly flashSeconds: number;
43
+ /** FOV punches, degrees (negative zooms in). */
44
+ readonly bigKillPunch: number;
45
+ readonly hurtPunch: number;
46
+ readonly levelUpPunch: number;
47
+ readonly dashPunch: number;
48
+ readonly landTrauma: number;
49
+ /** Rumble [strong, weak, ms]. */
50
+ readonly hurtRumble: readonly [number, number, number];
51
+ readonly bigKillRumble: readonly [number, number, number];
52
+ readonly landRumble: readonly [number, number, number];
53
+ /** Particle counts for kill bursts. */
54
+ readonly killParticles: number;
55
+ readonly bigKillParticles: number;
56
+ /** Most damage numbers started per frame. */
57
+ readonly numbersPerFrame: number;
58
+ }
59
+ export type FeelPresetName = "subtle" | "default" | "heavy";
60
+ export declare const FEEL_PRESETS: Readonly<Record<FeelPresetName, FeelPreset>>;
61
+ export interface HitEvent {
62
+ readonly x: number;
63
+ readonly y: number;
64
+ readonly z: number;
65
+ readonly amount: number;
66
+ /** Entity id (flash, knockback and number merging key on it). */
67
+ readonly id?: number;
68
+ readonly big?: boolean;
69
+ /** Direction the hit pushes, in the ground plane. */
70
+ readonly dirX?: number;
71
+ readonly dirZ?: number;
72
+ /** Damage-number colour key. */
73
+ readonly kind?: string;
74
+ }
75
+ export interface KillEvent {
76
+ readonly x: number;
77
+ readonly y: number;
78
+ readonly z: number;
79
+ readonly big?: boolean;
80
+ readonly id?: number;
81
+ /** Burst tint 0xRRGGBB. */
82
+ readonly color?: number;
83
+ /** Particle preset name. Default "kill" or "bigKill". */
84
+ readonly particles?: string;
85
+ readonly dirX?: number;
86
+ readonly dirZ?: number;
87
+ }
88
+ export interface FeelKitOptions {
89
+ readonly preset?: FeelPresetName | (Partial<FeelPreset> & {
90
+ readonly base?: FeelPresetName;
91
+ });
92
+ /** Where shake and punches go (a CameraRig). Default: the kit's own `trauma`, which a template applies itself. */
93
+ readonly camera?: TraumaTarget;
94
+ readonly particles?: ParticleSink | null;
95
+ /** World to screen, for damage numbers and sound panning. */
96
+ readonly project?: Projector;
97
+ /** Screen width in px for panning (read each call). */
98
+ readonly screenWidth?: () => number;
99
+ /** Damage numbers: an existing pool, a host element to build one in, or null for none. Default: document.body in a browser. */
100
+ readonly numbers?: DamageNumbers | HTMLElement | null;
101
+ readonly audio?: FeelAudio | null;
102
+ readonly rumble?: Rumble | null;
103
+ readonly clock?: FeelClock;
104
+ }
105
+ export declare class FeelKit {
106
+ readonly preset: FeelPreset;
107
+ readonly clock: FeelClock;
108
+ readonly flash: HitFlash;
109
+ readonly knock: KnockbackOffsets;
110
+ /** The kit's own shake when no camera is given (apply trauma.x/y/roll yourself). */
111
+ readonly trauma: Trauma;
112
+ readonly tweens: Tweens;
113
+ readonly numbers: DamageNumbers | null;
114
+ readonly audio: FeelAudio | null;
115
+ readonly rumble: Rumble | null;
116
+ readonly footsteps: Footsteps | null;
117
+ particles: ParticleSink | null;
118
+ /** 0..1, jumps to 1 on hurt and fades: drive a red vignette with it. */
119
+ hurtPulse: number;
120
+ /** Effects fired so far (playtest counters). */
121
+ readonly counts: {
122
+ hits: number;
123
+ kills: number;
124
+ bigKills: number;
125
+ hurts: number;
126
+ levelUps: number;
127
+ lands: number;
128
+ dashes: number;
129
+ };
130
+ /** Where shake and punches go. Assignable, so a template can build the kit before its camera. */
131
+ camera: TraumaTarget;
132
+ /** World to screen for numbers and panning. Assignable for the same reason. */
133
+ project: Projector | undefined;
134
+ private readonly width;
135
+ private frameTrauma;
136
+ private frameNumbers;
137
+ private stepSpeed;
138
+ private stepGrounded;
139
+ constructor(opts?: FeelKitOptions);
140
+ /** The render clock for this frame (hit-stop and slow motion aware). */
141
+ tick(realNow?: number): ClockTick;
142
+ private shake;
143
+ private punch;
144
+ private pan;
145
+ /** An enemy (or anything) took a hit. */
146
+ hit(e: HitEvent): void;
147
+ /** Something died. Big kills (bosses, elites) stop time, punch the camera and rumble. */
148
+ kill(e: KillEvent): void;
149
+ /** The local player took `amount` damage (pass maxHealth to scale by how much it hurt). */
150
+ hurt(amount: number, maxHealth?: number): void;
151
+ levelUp(): void;
152
+ /** Landed from a jump or fall; `intensity` 0..1 (fall speed over a hard landing's). */
153
+ land(intensity?: number): void;
154
+ dash(): void;
155
+ /** The local character's ground speed this frame (m/s); steps play at a cadence from it during `update`. */
156
+ footstep(speed: number, grounded?: boolean, surface?: Surface): void;
157
+ /** Advance every effect; call once per frame after the frame's events, with the clock's dt. */
158
+ update(dt: number, realDt?: number): void;
159
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * FeelKit: the engine's game-feel kit behind one-liners a
3
+ * template calls from game events:
4
+ *
5
+ * const feel = new FeelKit({ camera: rig, particles, project, preset: "default" });
6
+ * const { now, dt } = feel.tick(performance.now()); // hit-stop aware render clock
7
+ * feel.hit({ id, x, y, z, amount, dirX, dirZ }); // flash, knockback, number, sound, a touch of shake
8
+ * feel.kill({ x, y, z, big }); // burst, shake, hit-stop when big
9
+ * feel.hurt(12); // shake, stop, rumble, `hurtPulse` for a vignette
10
+ * feel.update(dt); // once per frame, after the events
11
+ *
12
+ * Nothing here needs three.js: the camera is any TraumaTarget (a CameraRig,
13
+ * or the kit's own Trauma), particles any ParticleSink, and world to screen
14
+ * an injected projector.
15
+ */
16
+ import { FeelAudio, Footsteps } from "./audio.js";
17
+ import { FeelClock } from "./clock.js";
18
+ import { HitFlash } from "./flash.js";
19
+ import { KnockbackOffsets } from "./knockback.js";
20
+ import { DamageNumbers } from "./numbers.js";
21
+ import { Rumble } from "./rumble.js";
22
+ import { Trauma } from "./trauma.js";
23
+ import { Tweens } from "./tween.js";
24
+ export const FEEL_PRESETS = {
25
+ subtle: {
26
+ hitTrauma: 0.01, hitTraumaPerFrame: 0.04, killTrauma: 0.015, bigKillTrauma: 0.3, hurtTrauma: 0.3,
27
+ bigKillStopMs: 35, hurtStopMs: 20, levelUpStopMs: 30, knockback: 0.12, bigKnockback: 0.25, flashSeconds: 0.07,
28
+ bigKillPunch: 1.5, hurtPunch: 1, levelUpPunch: -2, dashPunch: 1.5, landTrauma: 0.12,
29
+ hurtRumble: [0.35, 0.2, 90], bigKillRumble: [0.4, 0.25, 120], landRumble: [0.15, 0.1, 60],
30
+ killParticles: 5, bigKillParticles: 18, numbersPerFrame: 8,
31
+ },
32
+ default: {
33
+ hitTrauma: 0.02, hitTraumaPerFrame: 0.08, killTrauma: 0.03, bigKillTrauma: 0.5, hurtTrauma: 0.45,
34
+ bigKillStopMs: 55, hurtStopMs: 35, levelUpStopMs: 45, knockback: 0.2, bigKnockback: 0.4, flashSeconds: 0.09,
35
+ bigKillPunch: 3, hurtPunch: 2, levelUpPunch: -4, dashPunch: 3, landTrauma: 0.2,
36
+ hurtRumble: [0.6, 0.35, 120], bigKillRumble: [0.7, 0.4, 160], landRumble: [0.25, 0.15, 70],
37
+ killParticles: 8, bigKillParticles: 28, numbersPerFrame: 12,
38
+ },
39
+ heavy: {
40
+ hitTrauma: 0.035, hitTraumaPerFrame: 0.14, killTrauma: 0.05, bigKillTrauma: 0.75, hurtTrauma: 0.6,
41
+ bigKillStopMs: 85, hurtStopMs: 55, levelUpStopMs: 70, knockback: 0.32, bigKnockback: 0.6, flashSeconds: 0.12,
42
+ bigKillPunch: 5, hurtPunch: 3, levelUpPunch: -6, dashPunch: 4.5, landTrauma: 0.32,
43
+ hurtRumble: [0.85, 0.5, 160], bigKillRumble: [1, 0.6, 220], landRumble: [0.4, 0.25, 90],
44
+ killParticles: 12, bigKillParticles: 40, numbersPerFrame: 16,
45
+ },
46
+ };
47
+ const realNowMs = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
48
+ export class FeelKit {
49
+ preset;
50
+ clock;
51
+ flash;
52
+ knock;
53
+ /** The kit's own shake when no camera is given (apply trauma.x/y/roll yourself). */
54
+ trauma;
55
+ tweens = new Tweens();
56
+ numbers;
57
+ audio;
58
+ rumble;
59
+ footsteps;
60
+ particles;
61
+ /** 0..1, jumps to 1 on hurt and fades: drive a red vignette with it. */
62
+ hurtPulse = 0;
63
+ /** Effects fired so far (playtest counters). */
64
+ counts = { hits: 0, kills: 0, bigKills: 0, hurts: 0, levelUps: 0, lands: 0, dashes: 0 };
65
+ /** Where shake and punches go. Assignable, so a template can build the kit before its camera. */
66
+ camera;
67
+ /** World to screen for numbers and panning. Assignable for the same reason. */
68
+ project;
69
+ width;
70
+ frameTrauma = 0;
71
+ frameNumbers = 0;
72
+ stepSpeed = 0;
73
+ stepGrounded = true;
74
+ constructor(opts = {}) {
75
+ const p = opts.preset ?? "default";
76
+ this.preset = typeof p === "string" ? FEEL_PRESETS[p] : { ...FEEL_PRESETS[p.base ?? "default"], ...p };
77
+ this.clock = opts.clock ?? new FeelClock();
78
+ this.flash = new HitFlash(this.preset.flashSeconds);
79
+ this.knock = new KnockbackOffsets();
80
+ this.trauma = new Trauma();
81
+ this.camera = opts.camera ?? this.trauma;
82
+ this.particles = opts.particles ?? null;
83
+ this.project = opts.project;
84
+ this.width = opts.screenWidth ?? (() => (typeof innerWidth !== "undefined" ? innerWidth : 0));
85
+ const hasDom = typeof document !== "undefined";
86
+ if (opts.numbers === null)
87
+ this.numbers = null;
88
+ else if (opts.numbers instanceof DamageNumbers)
89
+ this.numbers = opts.numbers;
90
+ else if (opts.numbers !== undefined)
91
+ this.numbers = new DamageNumbers(opts.numbers);
92
+ else
93
+ this.numbers = hasDom && document.body ? new DamageNumbers(document.body) : null;
94
+ this.audio = opts.audio === undefined ? new FeelAudio() : opts.audio;
95
+ this.rumble = opts.rumble === undefined ? new Rumble() : opts.rumble;
96
+ const audio = this.audio;
97
+ this.footsteps = audio ? new Footsteps((pitch, gain) => void audio.play("step", { pitch, gain })) : null;
98
+ }
99
+ /** The render clock for this frame (hit-stop and slow motion aware). */
100
+ tick(realNow = realNowMs()) {
101
+ return this.clock.tick(realNow);
102
+ }
103
+ shake(amount) {
104
+ if (amount > 0)
105
+ this.camera.addTrauma(amount);
106
+ }
107
+ punch(deg) {
108
+ if (deg !== 0)
109
+ this.camera.punch?.(deg);
110
+ }
111
+ pan(x, y, z) {
112
+ if (!this.project)
113
+ return {};
114
+ const s = this.project(x, y, z);
115
+ return { screenX: s.x, width: this.width() };
116
+ }
117
+ /** An enemy (or anything) took a hit. */
118
+ hit(e) {
119
+ const P = this.preset;
120
+ this.counts.hits++;
121
+ if (e.id !== undefined) {
122
+ this.flash.trigger(e.id);
123
+ if (e.dirX !== undefined || e.dirZ !== undefined)
124
+ this.knock.kick(e.id, e.dirX ?? 0, e.dirZ ?? 0, e.big ? P.bigKnockback : P.knockback);
125
+ }
126
+ if (this.numbers && this.project && this.frameNumbers < P.numbersPerFrame) {
127
+ const s = this.project(e.x, e.y, e.z);
128
+ if (s.onScreen) {
129
+ this.frameNumbers++;
130
+ this.numbers.add({ x: s.x, y: s.y, amount: e.amount, big: e.big === true, ...(e.id !== undefined ? { id: e.id } : {}), ...(e.kind ? { kind: e.kind } : {}) });
131
+ }
132
+ }
133
+ this.audio?.impact(e.amount, this.pan(e.x, e.y, e.z));
134
+ const room = P.hitTraumaPerFrame - this.frameTrauma;
135
+ if (room > 0) {
136
+ const t = Math.min(room, e.big ? P.hitTrauma * 3 : P.hitTrauma);
137
+ this.frameTrauma += t;
138
+ this.shake(t);
139
+ }
140
+ }
141
+ /** Something died. Big kills (bosses, elites) stop time, punch the camera and rumble. */
142
+ kill(e) {
143
+ const P = this.preset;
144
+ this.counts.kills++;
145
+ const big = e.big === true;
146
+ this.particles?.burst(e.particles ?? (big ? "bigKill" : "kill"), e.x, e.y, e.z, {
147
+ count: big ? P.bigKillParticles : P.killParticles,
148
+ ...(e.color !== undefined ? { color: e.color } : {}),
149
+ ...(e.dirX !== undefined ? { dirX: e.dirX } : {}),
150
+ ...(e.dirZ !== undefined ? { dirZ: e.dirZ } : {}),
151
+ });
152
+ this.audio?.play(big ? "bigKill" : "kill", this.pan(e.x, e.y, e.z));
153
+ if (!big) {
154
+ const room = P.hitTraumaPerFrame - this.frameTrauma;
155
+ if (room > 0) {
156
+ const t = Math.min(room, P.killTrauma);
157
+ this.frameTrauma += t;
158
+ this.shake(t);
159
+ }
160
+ return;
161
+ }
162
+ this.counts.bigKills++;
163
+ this.shake(P.bigKillTrauma);
164
+ this.clock.hitStop(P.bigKillStopMs);
165
+ this.punch(P.bigKillPunch);
166
+ const [s, w, ms] = P.bigKillRumble;
167
+ this.rumble?.pulse(s, w, ms);
168
+ }
169
+ /** The local player took `amount` damage (pass maxHealth to scale by how much it hurt). */
170
+ hurt(amount, maxHealth = 100) {
171
+ if (!(amount > 0))
172
+ return;
173
+ const P = this.preset;
174
+ this.counts.hurts++;
175
+ const k = Math.min(2, Math.max(0.5, amount / Math.max(1, maxHealth * 0.1)));
176
+ this.shake(P.hurtTrauma * k);
177
+ this.clock.hitStop(P.hurtStopMs);
178
+ this.punch(P.hurtPunch);
179
+ this.hurtPulse = 1;
180
+ const [s, w, ms] = P.hurtRumble;
181
+ this.rumble?.pulse(Math.min(1, s * k), Math.min(1, w * k), ms);
182
+ this.audio?.play("hurt");
183
+ }
184
+ levelUp() {
185
+ this.counts.levelUps++;
186
+ this.clock.hitStop(this.preset.levelUpStopMs);
187
+ this.punch(this.preset.levelUpPunch);
188
+ this.audio?.play("levelUp");
189
+ }
190
+ /** Landed from a jump or fall; `intensity` 0..1 (fall speed over a hard landing's). */
191
+ land(intensity = 1) {
192
+ const k = Math.max(0, Math.min(1, intensity));
193
+ if (k <= 0.05)
194
+ return;
195
+ this.counts.lands++;
196
+ this.shake(this.preset.landTrauma * k);
197
+ const [s, w, ms] = this.preset.landRumble;
198
+ this.rumble?.pulse(s * k, w * k, ms);
199
+ this.audio?.play("land", { gain: 0.5 + 0.5 * k });
200
+ }
201
+ dash() {
202
+ this.counts.dashes++;
203
+ this.punch(this.preset.dashPunch);
204
+ this.audio?.play("dash");
205
+ }
206
+ /** The local character's ground speed this frame (m/s); steps play at a cadence from it during `update`. */
207
+ footstep(speed, grounded = true, surface) {
208
+ this.stepSpeed = speed;
209
+ this.stepGrounded = grounded;
210
+ if (surface && this.footsteps)
211
+ this.footsteps.surface = surface;
212
+ }
213
+ /** Advance every effect; call once per frame after the frame's events, with the clock's dt. */
214
+ update(dt, realDt = dt) {
215
+ this.flash.update(dt);
216
+ this.knock.update(dt);
217
+ this.numbers?.update(dt);
218
+ this.tweens.tick(realDt);
219
+ if (this.camera === this.trauma)
220
+ this.trauma.update(realDt);
221
+ this.footsteps?.update(dt, this.stepSpeed, this.stepGrounded);
222
+ this.hurtPulse = Math.max(0, this.hurtPulse - realDt * 2.5);
223
+ this.frameTrauma = 0;
224
+ this.frameNumbers = 0;
225
+ }
226
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Client-side visual knockback and squash per entity. The authoritative
3
+ * sim moves the entity; this adds a springy offset and a squash-stretch on
4
+ * top of the interpolated position, so a hit reads instantly even ~100 ms
5
+ * in the past. Read `get(id)` each frame and add it when drawing.
6
+ */
7
+ export interface KnockbackOptions {
8
+ /** Offset spring frequency, Hz. Default 7. */
9
+ readonly frequency?: number;
10
+ /** Offset damping ratio (below 1 wobbles back). Default 0.5. */
11
+ readonly damping?: number;
12
+ /** Largest offset, m. Default 0.6. */
13
+ readonly maxOffset?: number;
14
+ /** Squash per unit of strength (0 disables). Default 0.35. */
15
+ readonly squash?: number;
16
+ }
17
+ export interface KnockbackSample {
18
+ /** Offset to add to the drawn position, m. */
19
+ x: number;
20
+ z: number;
21
+ /** Scale to multiply the drawn scale by (squash on y, bulge on x and z). */
22
+ sx: number;
23
+ sy: number;
24
+ sz: number;
25
+ }
26
+ export declare class KnockbackOffsets {
27
+ private readonly m;
28
+ private readonly out;
29
+ private readonly o;
30
+ /** Kicks so far (playtest counters). */
31
+ kicks: number;
32
+ constructor(opts?: KnockbackOptions);
33
+ /** Shove `id` along (dirX, dirZ) with `strength` (about metres of peak offset). */
34
+ kick(id: number, dirX: number, dirZ: number, strength?: number): void;
35
+ /** This frame's offset and scale for `id` (a shared object: read it before the next call). */
36
+ get(id: number): Readonly<KnockbackSample>;
37
+ update(dt: number): void;
38
+ /** Entities still moving. */
39
+ get size(): number;
40
+ clear(): void;
41
+ }
@@ -0,0 +1,70 @@
1
+ import { stepSpring } from "./spring.js";
2
+ const REST = Object.freeze({ x: 0, z: 0, sx: 1, sy: 1, sz: 1 });
3
+ export class KnockbackOffsets {
4
+ m = new Map();
5
+ out = { x: 0, z: 0, sx: 1, sy: 1, sz: 1 };
6
+ o;
7
+ /** Kicks so far (playtest counters). */
8
+ kicks = 0;
9
+ constructor(opts = {}) {
10
+ this.o = { frequency: 7, damping: 0.5, maxOffset: 0.6, squash: 0.35, ...opts };
11
+ }
12
+ /** Shove `id` along (dirX, dirZ) with `strength` (about metres of peak offset). */
13
+ kick(id, dirX, dirZ, strength = 1) {
14
+ if (!(strength > 0))
15
+ return;
16
+ const len = Math.hypot(dirX, dirZ);
17
+ const nx = len > 1e-6 ? dirX / len : 0;
18
+ const nz = len > 1e-6 ? dirZ / len : 0;
19
+ let e = this.m.get(id);
20
+ if (!e) {
21
+ e = { x: 0, z: 0, vx: 0, vz: 0, s: 0, vs: 0 };
22
+ this.m.set(id, e);
23
+ }
24
+ this.kicks++;
25
+ // A peak offset of about `strength` for this spring: v0 = strength * omega (scaled for damping).
26
+ const v = strength * this.o.frequency * Math.PI * 2 * 0.6;
27
+ e.vx += nx * v;
28
+ e.vz += nz * v;
29
+ e.vs += this.o.squash * strength * 30;
30
+ }
31
+ /** This frame's offset and scale for `id` (a shared object: read it before the next call). */
32
+ get(id) {
33
+ const e = this.m.get(id);
34
+ if (!e)
35
+ return REST;
36
+ const o = this.out;
37
+ o.x = e.x;
38
+ o.z = e.z;
39
+ const s = Math.max(-0.5, Math.min(0.5, e.s));
40
+ o.sy = 1 - s;
41
+ o.sx = o.sz = 1 + s * 0.5;
42
+ return o;
43
+ }
44
+ update(dt) {
45
+ if (dt <= 0)
46
+ return;
47
+ const w = this.o.frequency * Math.PI * 2;
48
+ const z = this.o.damping;
49
+ const max = this.o.maxOffset;
50
+ for (const [id, e] of this.m) {
51
+ [e.x, e.vx] = stepSpring(e.x, e.vx, 0, w, z, dt);
52
+ [e.z, e.vz] = stepSpring(e.z, e.vz, 0, w, z, dt);
53
+ [e.s, e.vs] = stepSpring(e.s, e.vs, 0, w * 1.6, 0.35, dt);
54
+ const len = Math.hypot(e.x, e.z);
55
+ if (len > max) {
56
+ e.x *= max / len;
57
+ e.z *= max / len;
58
+ }
59
+ if (Math.abs(e.x) + Math.abs(e.z) + Math.abs(e.s) < 1e-3 && Math.abs(e.vx) + Math.abs(e.vz) + Math.abs(e.vs) < 1e-2)
60
+ this.m.delete(id);
61
+ }
62
+ }
63
+ /** Entities still moving. */
64
+ get size() {
65
+ return this.m.size;
66
+ }
67
+ clear() {
68
+ this.m.clear();
69
+ }
70
+ }
@@ -0,0 +1,6 @@
1
+ /** Seeded integer hash to -1..1 (deterministic, no Math.random). */
2
+ export declare function hashSigned(i: number, seed: number): number;
3
+ /** Smooth 1D value noise in -1..1, continuous in `t`, fixed for a seed. */
4
+ export declare function valueNoise1(t: number, seed: number): number;
5
+ /** A small seeded PRNG (mulberry32), 0..1, for cosmetic variation that should not use Math.random. */
6
+ export declare function seededRandom(seed: number): () => number;
@@ -0,0 +1,30 @@
1
+ /** Seeded integer hash to -1..1 (deterministic, no Math.random). */
2
+ export function hashSigned(i, seed) {
3
+ let h = Math.imul((i | 0) ^ Math.imul(seed | 0, 0x9e3779b1), 0x27d4eb2d);
4
+ h ^= h >>> 15;
5
+ h = Math.imul(h, 0x85ebca6b);
6
+ h ^= h >>> 13;
7
+ h = Math.imul(h, 0xc2b2ae35);
8
+ h ^= h >>> 16;
9
+ return ((h >>> 0) / 4294967295) * 2 - 1;
10
+ }
11
+ /** Smooth 1D value noise in -1..1, continuous in `t`, fixed for a seed. */
12
+ export function valueNoise1(t, seed) {
13
+ const i = Math.floor(t);
14
+ const f = t - i;
15
+ const u = f * f * (3 - 2 * f);
16
+ const a = hashSigned(i, seed);
17
+ const b = hashSigned(i + 1, seed);
18
+ return a + (b - a) * u;
19
+ }
20
+ /** A small seeded PRNG (mulberry32), 0..1, for cosmetic variation that should not use Math.random. */
21
+ export function seededRandom(seed) {
22
+ let s = seed >>> 0;
23
+ return () => {
24
+ s = (s + 0x6d2b79f5) >>> 0;
25
+ let t = s;
26
+ t = Math.imul(t ^ (t >>> 15), t | 1);
27
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
28
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
29
+ };
30
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Floating damage numbers. `NumberStack` is the DOM-free core (slots,
3
+ * merging, motion); `DamageNumbers` draws it with a fixed pool of DOM nodes
4
+ * moved by transforms only (no layout per frame).
5
+ */
6
+ export interface NumberInput {
7
+ /** Screen position in CSS pixels. */
8
+ readonly x: number;
9
+ readonly y: number;
10
+ readonly amount: number;
11
+ /** Entity id: rapid hits on one id merge into one growing number. */
12
+ readonly id?: number;
13
+ /** Big hits (bosses, crits) pop larger. */
14
+ readonly big?: boolean;
15
+ /** A colour key ("damage", "crit", "heal", "xp", ...) mapped by DamageNumbers. */
16
+ readonly kind?: string;
17
+ }
18
+ export interface NumberSlot {
19
+ active: boolean;
20
+ id: number;
21
+ amount: number;
22
+ /** 0..1 through its life. */
23
+ t: number;
24
+ x: number;
25
+ y: number;
26
+ /** Sideways drift, px over the whole life. */
27
+ drift: number;
28
+ big: boolean;
29
+ kind: string;
30
+ /** When it was last added to (ms, the caller's clock). */
31
+ at: number;
32
+ /** Generation, bumped on reuse (renderers compare it to restyle). */
33
+ gen: number;
34
+ }
35
+ export interface NumberStackOptions {
36
+ /** Hits on the same id within this many ms merge. Default 120. */
37
+ readonly mergeMs?: number;
38
+ /** Seconds a number lives. Default 0.75. */
39
+ readonly lifeSeconds?: number;
40
+ /** Rise over its life, px. Default 34. */
41
+ readonly rise?: number;
42
+ }
43
+ export interface NumberDraw {
44
+ readonly x: number;
45
+ readonly y: number;
46
+ readonly scale: number;
47
+ readonly opacity: number;
48
+ }
49
+ export declare class NumberStack {
50
+ readonly slots: NumberSlot[];
51
+ /** Numbers shown (merges do not count). */
52
+ shown: number;
53
+ merged: number;
54
+ private readonly o;
55
+ private seq;
56
+ constructor(capacity?: number, opts?: NumberStackOptions);
57
+ /** Add a number; returns its slot index and whether it merged into an existing one. */
58
+ add(nowMs: number, n: NumberInput): {
59
+ index: number;
60
+ merged: boolean;
61
+ };
62
+ update(dt: number): void;
63
+ /** Where and how to draw a slot: an arc (rise with ease-out, sideways drift), a pop, then a fade. */
64
+ draw(s: NumberSlot): NumberDraw;
65
+ get active(): number;
66
+ }
67
+ export interface DamageNumbersOptions extends NumberStackOptions {
68
+ /** Pool size (most numbers on screen). Default 48. */
69
+ readonly capacity?: number;
70
+ /** Class on every node (style it in the template). Default "dmg"; big ones also get "big". */
71
+ readonly className?: string;
72
+ /** Colour per kind (CSS colours). */
73
+ readonly colors?: Readonly<Record<string, string>>;
74
+ /** Number formatting. Default rounds. */
75
+ readonly format?: (amount: number) => string;
76
+ }
77
+ /** The DOM pool. Construct only in a browser (it creates nodes under `host`). */
78
+ export declare class DamageNumbers {
79
+ readonly stack: NumberStack;
80
+ private readonly els;
81
+ private readonly colors;
82
+ private readonly format;
83
+ private readonly cls;
84
+ constructor(host: HTMLElement, opts?: DamageNumbersOptions | number);
85
+ /** Numbers shown so far (playtest counters). */
86
+ get shown(): number;
87
+ /** The classic call: show(x, y, amount, big). */
88
+ show(x: number, y: number, amount: number, big?: boolean): void;
89
+ add(n: NumberInput, nowMs?: number): void;
90
+ update(dt: number): void;
91
+ }