@jgengine/core 0.5.0 → 0.7.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/CHANGELOG.md +191 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +5 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +291 -5
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +53 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +66 -2
  128. package/dist/runtime/gameContext.js +157 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ const DEFAULT_ARRIVE_RADIUS = 0.75;
2
+ function idleWorker() {
3
+ return { jobId: null, phase: "idle", worked: 0, cycles: 0 };
4
+ }
5
+ export function createJobBoard() {
6
+ const jobs = new Map();
7
+ const workers = new Map();
8
+ let counter = 1;
9
+ function workerState(workerId) {
10
+ let state = workers.get(workerId);
11
+ if (state === undefined) {
12
+ state = idleWorker();
13
+ workers.set(workerId, state);
14
+ }
15
+ return state;
16
+ }
17
+ function begin(workerId, job) {
18
+ const state = workerState(workerId);
19
+ job.worker = workerId;
20
+ state.jobId = job.id;
21
+ state.phase = "travelling";
22
+ state.worked = 0;
23
+ state.cycles = 0;
24
+ }
25
+ function detach(workerId, requeue) {
26
+ const state = workers.get(workerId);
27
+ if (state === undefined || state.jobId === null)
28
+ return;
29
+ const job = jobs.get(state.jobId);
30
+ if (job !== undefined && job.worker === workerId)
31
+ job.worker = requeue ? null : job.worker;
32
+ state.jobId = null;
33
+ state.phase = "idle";
34
+ state.worked = 0;
35
+ }
36
+ return {
37
+ post(job) {
38
+ const id = job.id ?? `job-${counter++}`;
39
+ jobs.set(id, {
40
+ id,
41
+ station: job.station,
42
+ work: job.work,
43
+ priority: job.priority ?? 0,
44
+ arriveRadius: job.arriveRadius ?? DEFAULT_ARRIVE_RADIUS,
45
+ repeat: job.repeat ?? false,
46
+ worker: null,
47
+ });
48
+ return id;
49
+ },
50
+ cancel(jobId) {
51
+ const job = jobs.get(jobId);
52
+ if (job === undefined)
53
+ return false;
54
+ if (job.worker !== null)
55
+ detach(job.worker, false);
56
+ jobs.delete(jobId);
57
+ return true;
58
+ },
59
+ assign(workerId, jobId) {
60
+ const job = jobs.get(jobId);
61
+ if (job === undefined)
62
+ return false;
63
+ if (job.worker !== null && job.worker !== workerId)
64
+ detach(job.worker, true);
65
+ detach(workerId, true);
66
+ begin(workerId, job);
67
+ return true;
68
+ },
69
+ claim(workerId) {
70
+ const existing = workerState(workerId);
71
+ if (existing.jobId !== null && jobs.has(existing.jobId))
72
+ return existing.jobId;
73
+ let best = null;
74
+ for (const job of jobs.values()) {
75
+ if (job.worker !== null)
76
+ continue;
77
+ if (best === null || job.priority > best.priority)
78
+ best = job;
79
+ }
80
+ if (best === null) {
81
+ existing.phase = "idle";
82
+ return null;
83
+ }
84
+ begin(workerId, best);
85
+ return best.id;
86
+ },
87
+ release(workerId) {
88
+ detach(workerId, true);
89
+ },
90
+ advance(workerId, dt, ctx) {
91
+ const state = workerState(workerId);
92
+ if (state.jobId === null) {
93
+ state.phase = "idle";
94
+ return null;
95
+ }
96
+ const job = jobs.get(state.jobId);
97
+ if (job === undefined) {
98
+ state.jobId = null;
99
+ state.phase = "idle";
100
+ state.worked = 0;
101
+ return null;
102
+ }
103
+ if (state.phase === "travelling" || state.phase === "idle") {
104
+ state.phase = ctx.distanceToStation <= job.arriveRadius ? "working" : "travelling";
105
+ }
106
+ if (state.phase !== "working" || dt <= 0)
107
+ return null;
108
+ if (ctx.distanceToStation > job.arriveRadius) {
109
+ state.phase = "travelling";
110
+ return null;
111
+ }
112
+ state.worked += dt;
113
+ if (state.worked < job.work)
114
+ return null;
115
+ state.cycles += 1;
116
+ const report = { jobId: job.id, workerId, cycle: state.cycles };
117
+ if (job.repeat) {
118
+ state.worked -= job.work;
119
+ state.phase = "working";
120
+ }
121
+ else {
122
+ job.worker = null;
123
+ jobs.delete(job.id);
124
+ state.jobId = null;
125
+ state.phase = "done";
126
+ state.worked = 0;
127
+ }
128
+ return report;
129
+ },
130
+ worker(workerId) {
131
+ return { ...workerState(workerId) };
132
+ },
133
+ station(workerId) {
134
+ const state = workers.get(workerId);
135
+ if (state === undefined || state.jobId === null)
136
+ return null;
137
+ return jobs.get(state.jobId)?.station ?? null;
138
+ },
139
+ jobOf(workerId) {
140
+ return workers.get(workerId)?.jobId ?? null;
141
+ },
142
+ get(jobId) {
143
+ return jobs.get(jobId) ?? null;
144
+ },
145
+ queued() {
146
+ return [...jobs.values()].filter((job) => job.worker === null);
147
+ },
148
+ active() {
149
+ return [...jobs.values()].filter((job) => job.worker !== null);
150
+ },
151
+ list() {
152
+ return [...jobs.values()];
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,56 @@
1
+ import type { NavPoint } from "../nav/navGrid.js";
2
+ export interface SpawnEntry {
3
+ id: string;
4
+ cost: number;
5
+ weight?: number;
6
+ minWave?: number;
7
+ }
8
+ export interface WaveManifest {
9
+ budget: number;
10
+ entries: readonly SpawnEntry[];
11
+ duration?: number;
12
+ budgetPerSecond?: number;
13
+ }
14
+ export interface SpawnDirectorConfig {
15
+ waves: readonly WaveManifest[];
16
+ maxAlive?: number;
17
+ escalationPerSecond?: number;
18
+ alertBudgetPerSecond?: number;
19
+ alertDecayPerSecond?: number;
20
+ playerBudgetPerSecond?: number;
21
+ maxSpawnsPerTick?: number;
22
+ loop?: boolean;
23
+ seed?: number;
24
+ }
25
+ export interface SpawnDirectorState {
26
+ wave: number;
27
+ elapsed: number;
28
+ waveElapsed: number;
29
+ budget: number;
30
+ alert: number;
31
+ spawnedThisWave: number;
32
+ spawnedTotal: number;
33
+ rng: number;
34
+ done: boolean;
35
+ }
36
+ export interface DirectorContext {
37
+ alive: number;
38
+ players?: number;
39
+ }
40
+ export interface SpawnRequest {
41
+ entryId: string;
42
+ cost: number;
43
+ wave: number;
44
+ }
45
+ export interface DirectorStep {
46
+ state: SpawnDirectorState;
47
+ spawns: SpawnRequest[];
48
+ }
49
+ export declare function createSpawnDirectorState(config: SpawnDirectorConfig): SpawnDirectorState;
50
+ export declare function raiseAlert(state: SpawnDirectorState, amount: number): SpawnDirectorState;
51
+ export declare function advanceWave(config: SpawnDirectorConfig, state: SpawnDirectorState): SpawnDirectorState;
52
+ export declare function advanceSpawnDirector(config: SpawnDirectorConfig, state: SpawnDirectorState, dt: number, ctx: DirectorContext): DirectorStep;
53
+ export declare function pickSpawnPoint(points: readonly NavPoint[], players: readonly NavPoint[], options: {
54
+ roll: number;
55
+ bias?: number;
56
+ }): NavPoint | null;
@@ -0,0 +1,150 @@
1
+ const DEFAULT_MAX_SPAWNS_PER_TICK = 64;
2
+ const DEFAULT_ALERT_DECAY = 0.1;
3
+ function nextRandom(seed) {
4
+ const next = (seed + 0x6d2b79f5) | 0;
5
+ let t = Math.imul(next ^ (next >>> 15), 1 | next);
6
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
7
+ const value = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
8
+ return [value, next];
9
+ }
10
+ function clamp01(value) {
11
+ return value < 0 ? 0 : value > 1 ? 1 : value;
12
+ }
13
+ export function createSpawnDirectorState(config) {
14
+ const first = config.waves[0];
15
+ return {
16
+ wave: 0,
17
+ elapsed: 0,
18
+ waveElapsed: 0,
19
+ budget: first?.budget ?? 0,
20
+ alert: 0,
21
+ spawnedThisWave: 0,
22
+ spawnedTotal: 0,
23
+ rng: (config.seed ?? 1) | 0,
24
+ done: config.waves.length === 0,
25
+ };
26
+ }
27
+ export function raiseAlert(state, amount) {
28
+ return { ...state, alert: clamp01(state.alert + amount) };
29
+ }
30
+ export function advanceWave(config, state) {
31
+ const loop = config.loop ?? false;
32
+ const lastIndex = config.waves.length - 1;
33
+ if (state.wave >= lastIndex && !loop)
34
+ return { ...state, done: true };
35
+ const nextWave = state.wave >= lastIndex ? 0 : state.wave + 1;
36
+ const manifest = config.waves[nextWave];
37
+ return {
38
+ ...state,
39
+ wave: nextWave,
40
+ waveElapsed: 0,
41
+ spawnedThisWave: 0,
42
+ budget: state.budget + manifest.budget,
43
+ done: false,
44
+ };
45
+ }
46
+ function pickEntry(entries, wave, budget, rng) {
47
+ let total = 0;
48
+ const affordable = [];
49
+ for (const entry of entries) {
50
+ if (entry.cost > budget)
51
+ continue;
52
+ if ((entry.minWave ?? 0) > wave)
53
+ continue;
54
+ affordable.push(entry);
55
+ total += entry.weight ?? 1;
56
+ }
57
+ if (affordable.length === 0 || total <= 0)
58
+ return null;
59
+ const [roll, nextRng] = nextRandom(rng);
60
+ let cursor = roll * total;
61
+ for (const entry of affordable) {
62
+ cursor -= entry.weight ?? 1;
63
+ if (cursor <= 0)
64
+ return { entry, rng: nextRng };
65
+ }
66
+ return { entry: affordable[affordable.length - 1], rng: nextRng };
67
+ }
68
+ export function advanceSpawnDirector(config, state, dt, ctx) {
69
+ if (state.done || dt <= 0)
70
+ return { state, spawns: [] };
71
+ const maxAlive = config.maxAlive ?? Number.POSITIVE_INFINITY;
72
+ const maxSpawns = config.maxSpawnsPerTick ?? DEFAULT_MAX_SPAWNS_PER_TICK;
73
+ const alertDecay = config.alertDecayPerSecond ?? DEFAULT_ALERT_DECAY;
74
+ const players = ctx.players ?? 0;
75
+ let wave = state.wave;
76
+ let elapsed = state.elapsed + dt;
77
+ let waveElapsed = state.waveElapsed + dt;
78
+ let budget = state.budget;
79
+ let spawnedThisWave = state.spawnedThisWave;
80
+ let done = state.done;
81
+ const alert = Math.max(0, state.alert - alertDecay * dt);
82
+ const trickle = config.waves[wave]?.budgetPerSecond ?? 0;
83
+ const escalation = (config.escalationPerSecond ?? 0) * elapsed;
84
+ const alertBonus = (config.alertBudgetPerSecond ?? 0) * alert;
85
+ const playerBonus = (config.playerBudgetPerSecond ?? 0) * players;
86
+ budget += (trickle + escalation + alertBonus + playerBonus) * dt;
87
+ const lastIndex = config.waves.length - 1;
88
+ const loop = config.loop ?? false;
89
+ let guard = config.waves.length + 1;
90
+ while (guard > 0) {
91
+ guard -= 1;
92
+ const manifest = config.waves[wave];
93
+ if (manifest?.duration === undefined || waveElapsed < manifest.duration)
94
+ break;
95
+ if (wave >= lastIndex && !loop) {
96
+ done = true;
97
+ break;
98
+ }
99
+ waveElapsed -= manifest.duration;
100
+ wave = wave >= lastIndex ? 0 : wave + 1;
101
+ spawnedThisWave = 0;
102
+ budget += config.waves[wave].budget;
103
+ }
104
+ const spawns = [];
105
+ let rng = state.rng;
106
+ let spawnedTotal = state.spawnedTotal;
107
+ let alive = ctx.alive;
108
+ const entries = config.waves[wave]?.entries ?? [];
109
+ while (spawns.length < maxSpawns && alive < maxAlive) {
110
+ const picked = pickEntry(entries, wave, budget, rng);
111
+ if (picked === null)
112
+ break;
113
+ rng = picked.rng;
114
+ budget -= picked.entry.cost;
115
+ spawns.push({ entryId: picked.entry.id, cost: picked.entry.cost, wave });
116
+ spawnedThisWave += 1;
117
+ spawnedTotal += 1;
118
+ alive += 1;
119
+ }
120
+ return {
121
+ state: { wave, elapsed, waveElapsed, budget, alert, spawnedThisWave, spawnedTotal, rng, done },
122
+ spawns,
123
+ };
124
+ }
125
+ export function pickSpawnPoint(points, players, options) {
126
+ if (points.length === 0)
127
+ return null;
128
+ const bias = options.bias ?? 1;
129
+ const weights = [];
130
+ let total = 0;
131
+ for (const point of points) {
132
+ let nearest = Number.POSITIVE_INFINITY;
133
+ for (const player of players) {
134
+ const d = Math.hypot(point[0] - player[0], point[1] - player[1]);
135
+ if (d < nearest)
136
+ nearest = d;
137
+ }
138
+ const distance = nearest === Number.POSITIVE_INFINITY ? 0 : nearest;
139
+ const weight = Math.max(1e-6, (1 + distance) ** -bias);
140
+ weights.push(weight);
141
+ total += weight;
142
+ }
143
+ let cursor = clamp01(options.roll) * total;
144
+ for (let i = 0; i < points.length; i += 1) {
145
+ cursor -= weights[i];
146
+ if (cursor <= 0)
147
+ return points[i];
148
+ }
149
+ return points[points.length - 1];
150
+ }
@@ -0,0 +1,25 @@
1
+ export interface ThreatTableConfig {
2
+ decayPerSecond?: number;
3
+ max?: number;
4
+ forgetBelow?: number;
5
+ }
6
+ export interface ThreatEntry {
7
+ sourceId: string;
8
+ threat: number;
9
+ }
10
+ export interface HighestThreatOptions {
11
+ current?: string | null;
12
+ stickiness?: number;
13
+ }
14
+ export interface ThreatTable {
15
+ add(sourceId: string, amount: number): number;
16
+ set(sourceId: string, amount: number): void;
17
+ threatOf(sourceId: string): number;
18
+ decay(dt: number): void;
19
+ highest(options?: HighestThreatOptions): string | null;
20
+ ranked(): ThreatEntry[];
21
+ remove(sourceId: string): void;
22
+ clear(): void;
23
+ size(): number;
24
+ }
25
+ export declare function createThreatTable(config?: ThreatTableConfig): ThreatTable;
@@ -0,0 +1,67 @@
1
+ const DEFAULT_FORGET_BELOW = 0;
2
+ export function createThreatTable(config = {}) {
3
+ const decayPerSecond = Math.max(0, config.decayPerSecond ?? 0);
4
+ const max = config.max ?? Number.POSITIVE_INFINITY;
5
+ const forgetBelow = config.forgetBelow ?? DEFAULT_FORGET_BELOW;
6
+ const table = new Map();
7
+ function store(sourceId, value) {
8
+ const clamped = Math.min(max, value);
9
+ if (clamped <= forgetBelow) {
10
+ table.delete(sourceId);
11
+ return 0;
12
+ }
13
+ table.set(sourceId, clamped);
14
+ return clamped;
15
+ }
16
+ return {
17
+ add(sourceId, amount) {
18
+ return store(sourceId, (table.get(sourceId) ?? 0) + amount);
19
+ },
20
+ set(sourceId, amount) {
21
+ store(sourceId, amount);
22
+ },
23
+ threatOf(sourceId) {
24
+ return table.get(sourceId) ?? 0;
25
+ },
26
+ decay(dt) {
27
+ if (decayPerSecond <= 0 || dt <= 0)
28
+ return;
29
+ const loss = decayPerSecond * dt;
30
+ for (const [sourceId, value] of [...table])
31
+ store(sourceId, value - loss);
32
+ },
33
+ highest(options = {}) {
34
+ let bestId = null;
35
+ let bestThreat = Number.NEGATIVE_INFINITY;
36
+ for (const [sourceId, value] of table) {
37
+ if (value > bestThreat) {
38
+ bestThreat = value;
39
+ bestId = sourceId;
40
+ }
41
+ }
42
+ if (bestId === null)
43
+ return null;
44
+ const current = options.current ?? null;
45
+ if (current !== null && current !== bestId && table.has(current)) {
46
+ const stickiness = options.stickiness ?? 1;
47
+ if (bestThreat < table.get(current) * stickiness)
48
+ return current;
49
+ }
50
+ return bestId;
51
+ },
52
+ ranked() {
53
+ return [...table]
54
+ .map(([sourceId, threat]) => ({ sourceId, threat }))
55
+ .sort((a, b) => b.threat - a.threat || a.sourceId.localeCompare(b.sourceId));
56
+ },
57
+ remove(sourceId) {
58
+ table.delete(sourceId);
59
+ },
60
+ clear() {
61
+ table.clear();
62
+ },
63
+ size() {
64
+ return table.size;
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,38 @@
1
+ export type AudioBusId = string;
2
+ export type FalloffCurve = "linear" | "inverse" | "none";
3
+ export interface AudioFalloffConfig {
4
+ /** Distance (world units) at/under which gain is 1. Default 1. */
5
+ minDistance?: number;
6
+ /** Distance at/beyond which gain is 0. Default 24. */
7
+ maxDistance?: number;
8
+ /** "linear" ramps 1→0 between min/max; "inverse" is an inverse-square-style rolloff clamped to the same range; "none" is always 1 (no distance attenuation). Default "linear". */
9
+ curve?: FalloffCurve;
10
+ }
11
+ export interface AudioBusDef {
12
+ id: AudioBusId;
13
+ /** Bus volume multiplier, 0..1+. Default 1. */
14
+ gain?: number;
15
+ }
16
+ export interface SoundDef {
17
+ id: string;
18
+ /** Playable source — a URL/data URI the shell's Web Audio glue fetches and decodes. */
19
+ url: string;
20
+ bus: AudioBusId;
21
+ /** Base gain before bus/falloff multipliers. Default 1. */
22
+ gain?: number;
23
+ loop?: boolean;
24
+ /** Positional emitters attenuate by distance from the listener; non-positional sounds (UI, music) play at flat gain. Default true. */
25
+ positional?: boolean;
26
+ falloff?: AudioFalloffConfig;
27
+ }
28
+ export declare function computeFalloffGain(distance: number, config?: AudioFalloffConfig): number;
29
+ export declare function resolveEmitterGain(distance: number, sound: Pick<SoundDef, "gain" | "positional" | "falloff">, busGain: number): number;
30
+ export declare function distance3(a: {
31
+ x: number;
32
+ y: number;
33
+ z: number;
34
+ }, b: {
35
+ x: number;
36
+ y: number;
37
+ z: number;
38
+ }): number;
@@ -0,0 +1,32 @@
1
+ const DEFAULT_MIN_DISTANCE = 1;
2
+ const DEFAULT_MAX_DISTANCE = 24;
3
+ export function computeFalloffGain(distance, config = {}) {
4
+ const curve = config.curve ?? "linear";
5
+ if (curve === "none")
6
+ return 1;
7
+ const min = Math.max(0, config.minDistance ?? DEFAULT_MIN_DISTANCE);
8
+ const max = Math.max(min + Number.EPSILON, config.maxDistance ?? DEFAULT_MAX_DISTANCE);
9
+ const clamped = Math.max(0, distance);
10
+ if (clamped <= min)
11
+ return 1;
12
+ if (clamped >= max)
13
+ return 0;
14
+ const t = (clamped - min) / (max - min);
15
+ if (curve === "inverse") {
16
+ const inv = min / clamped;
17
+ const invAtMax = min / max;
18
+ return Math.min(1, Math.max(0, (inv - invAtMax) / (1 - invAtMax)));
19
+ }
20
+ return 1 - t;
21
+ }
22
+ export function resolveEmitterGain(distance, sound, busGain) {
23
+ const base = sound.gain ?? 1;
24
+ const spatial = sound.positional === false ? 1 : computeFalloffGain(distance, sound.falloff);
25
+ return base * spatial * busGain;
26
+ }
27
+ export function distance3(a, b) {
28
+ const dx = a.x - b.x;
29
+ const dy = a.y - b.y;
30
+ const dz = a.z - b.z;
31
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
32
+ }
@@ -0,0 +1,63 @@
1
+ export type Side = string;
2
+ export interface LaneRuleInput<C> {
3
+ lane: number;
4
+ side: Side;
5
+ cards: readonly C[];
6
+ subtotal: number;
7
+ }
8
+ export interface LaneRule<C> {
9
+ id: string;
10
+ apply: (input: LaneRuleInput<C>) => number;
11
+ }
12
+ export interface LaneBoardConfig<C> {
13
+ laneCount: number;
14
+ sides: readonly Side[];
15
+ power: (card: C) => number;
16
+ laneRules?: readonly (LaneRule<C> | null | undefined)[];
17
+ }
18
+ export interface LaneCells<C> {
19
+ readonly [side: Side]: readonly C[];
20
+ }
21
+ export interface LaneBoardState<C> {
22
+ readonly lanes: readonly LaneCells<C>[];
23
+ }
24
+ export interface LaneAggregate {
25
+ lane: number;
26
+ side: Side;
27
+ subtotal: number;
28
+ total: number;
29
+ count: number;
30
+ }
31
+ export interface LaneOutcome {
32
+ lane: number;
33
+ totals: Readonly<Record<Side, number>>;
34
+ winner: Side | null;
35
+ }
36
+ export type LaneRejection = "invalid-lane" | "invalid-side";
37
+ export interface LanePlaceResult<C> {
38
+ status: "ok";
39
+ state: LaneBoardState<C>;
40
+ }
41
+ export interface LanePlaceRejected {
42
+ status: "rejected";
43
+ reason: LaneRejection;
44
+ }
45
+ export type LaneResult<C> = LanePlaceResult<C> | LanePlaceRejected;
46
+ export declare function createLaneBoardState<C>(config: LaneBoardConfig<C>): LaneBoardState<C>;
47
+ export declare function placeCard<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number, side: Side, card: C): LaneResult<C>;
48
+ export declare function removeCard<C>(state: LaneBoardState<C>, lane: number, side: Side, predicate: (card: C, index: number) => boolean): LaneResult<C>;
49
+ export declare function laneAggregate<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number, side: Side): LaneAggregate;
50
+ export declare function laneOutcome<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>, lane: number): LaneOutcome;
51
+ export declare function boardTotals<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>): Readonly<Record<Side, number>>;
52
+ export declare function lanesWon<C>(state: LaneBoardState<C>, config: LaneBoardConfig<C>): Readonly<Record<Side, number>>;
53
+ export interface LaneBoard<C> {
54
+ state(): LaneBoardState<C>;
55
+ place(lane: number, side: Side, card: C): LaneResult<C>;
56
+ remove(lane: number, side: Side, predicate: (card: C, index: number) => boolean): LaneResult<C>;
57
+ aggregate(lane: number, side: Side): LaneAggregate;
58
+ outcome(lane: number): LaneOutcome;
59
+ totals(): Readonly<Record<Side, number>>;
60
+ lanesWon(): Readonly<Record<Side, number>>;
61
+ reset(next?: LaneBoardState<C>): void;
62
+ }
63
+ export declare function createLaneBoard<C>(config: LaneBoardConfig<C>): LaneBoard<C>;