@jgengine/core 0.14.0 → 0.15.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 (214) hide show
  1. package/CHANGELOG.md +500 -1
  2. package/dist/ai/difficulty.d.ts +154 -0
  3. package/dist/ai/difficulty.js +194 -0
  4. package/dist/ai/driver.d.ts +107 -0
  5. package/dist/ai/driver.js +175 -0
  6. package/dist/ai/flock.js +13 -23
  7. package/dist/ai/laneSelect.js +2 -1
  8. package/dist/ai/mobBrain.js +2 -1
  9. package/dist/ai/waveRunner.d.ts +81 -0
  10. package/dist/ai/waveRunner.js +104 -0
  11. package/dist/audio/musicState.d.ts +47 -0
  12. package/dist/audio/musicState.js +38 -0
  13. package/dist/audio/stepCadence.d.ts +47 -0
  14. package/dist/audio/stepCadence.js +29 -0
  15. package/dist/audio/synth.d.ts +12 -0
  16. package/dist/authoring.d.ts +6 -4
  17. package/dist/authoring.js +6 -4
  18. package/dist/combat/castRunner.d.ts +13 -1
  19. package/dist/combat/castRunner.js +5 -1
  20. package/dist/combat/comboMeter.d.ts +140 -0
  21. package/dist/combat/comboMeter.js +215 -0
  22. package/dist/combat/comboPoints.d.ts +11 -0
  23. package/dist/combat/comboPoints.js +11 -2
  24. package/dist/combat/damageResolution.d.ts +1 -1
  25. package/dist/combat/defensiveWindow.d.ts +5 -0
  26. package/dist/combat/effects.d.ts +24 -0
  27. package/dist/combat/effects.js +6 -1
  28. package/dist/combat/projectiles.d.ts +6 -0
  29. package/dist/combat/projectiles.js +2 -2
  30. package/dist/combat/regenShield.d.ts +40 -2
  31. package/dist/combat/regenShield.js +66 -18
  32. package/dist/combat/resourcePool.d.ts +19 -0
  33. package/dist/combat/resourcePool.js +20 -2
  34. package/dist/combat/shotOrigin.d.ts +5 -0
  35. package/dist/combat/statusApplication.d.ts +1 -1
  36. package/dist/combat/statusApplication.js +2 -1
  37. package/dist/combat/statusEffectView.d.ts +56 -0
  38. package/dist/combat/statusEffectView.js +52 -0
  39. package/dist/combat.d.ts +4 -2
  40. package/dist/combat.js +2 -0
  41. package/dist/devtools/fallbackSeams.d.ts +3 -2
  42. package/dist/devtools/fallbackSeams.js +1 -1
  43. package/dist/devtools/textureErrors.d.ts +40 -0
  44. package/dist/devtools/textureErrors.js +68 -0
  45. package/dist/economy/shopStock.d.ts +110 -0
  46. package/dist/economy/shopStock.js +157 -0
  47. package/dist/economy/wallet.d.ts +6 -0
  48. package/dist/economy/wallet.js +1 -0
  49. package/dist/game/achievements.d.ts +80 -0
  50. package/dist/game/achievements.js +124 -0
  51. package/dist/game/cameraConfig.d.ts +7 -0
  52. package/dist/game/codex.d.ts +60 -0
  53. package/dist/game/codex.js +89 -0
  54. package/dist/game/credits.d.ts +36 -0
  55. package/dist/game/credits.js +0 -0
  56. package/dist/game/defineGame.d.ts +28 -3
  57. package/dist/game/defineGame.js +5 -0
  58. package/dist/game/dialogueGraph.d.ts +116 -0
  59. package/dist/game/dialogueGraph.js +103 -0
  60. package/dist/game/eventTicker.d.ts +95 -0
  61. package/dist/game/eventTicker.js +91 -0
  62. package/dist/game/events.d.ts +24 -5
  63. package/dist/game/events.js +18 -0
  64. package/dist/game/gamePhase.d.ts +1 -1
  65. package/dist/game/gamePhase.js +1 -1
  66. package/dist/game/leaderboardRank.d.ts +71 -0
  67. package/dist/game/leaderboardRank.js +76 -0
  68. package/dist/game/lootTable.d.ts +10 -1
  69. package/dist/game/lootTable.js +4 -2
  70. package/dist/game/notifications.d.ts +57 -0
  71. package/dist/game/notifications.js +107 -0
  72. package/dist/game/playableGame.d.ts +39 -2
  73. package/dist/game/quest.d.ts +26 -0
  74. package/dist/game/quest.js +31 -0
  75. package/dist/game/runDraft.js +2 -1
  76. package/dist/game/saveSlots.d.ts +102 -0
  77. package/dist/game/saveSlots.js +136 -0
  78. package/dist/game/talentTreeView.d.ts +106 -0
  79. package/dist/game/talentTreeView.js +114 -0
  80. package/dist/game/worldItem.js +2 -1
  81. package/dist/gameplay.d.ts +15 -4
  82. package/dist/gameplay.js +13 -2
  83. package/dist/i18n/i18n.d.ts +51 -0
  84. package/dist/i18n/i18n.js +82 -0
  85. package/dist/input/axisInput.d.ts +1 -1
  86. package/dist/input/axisInput.js +18 -2
  87. package/dist/input/rebindSession.d.ts +141 -0
  88. package/dist/input/rebindSession.js +235 -0
  89. package/dist/input/touchControlsMode.d.ts +16 -0
  90. package/dist/input/touchControlsMode.js +23 -0
  91. package/dist/input/touchScheme.d.ts +30 -1
  92. package/dist/input/touchScheme.js +31 -1
  93. package/dist/interaction/promptRegistry.d.ts +78 -0
  94. package/dist/interaction/promptRegistry.js +130 -0
  95. package/dist/inventory/storageTier.js +3 -2
  96. package/dist/meta/changelog.d.ts +1 -1
  97. package/dist/meta/changelog.js +145 -1
  98. package/dist/movement/movementModel.d.ts +26 -3
  99. package/dist/movement/movementModel.js +121 -15
  100. package/dist/movement/playerMovement.js +161 -25
  101. package/dist/physics/drivableVehicle.js +3 -1
  102. package/dist/physics/kinematicVehicle.d.ts +56 -1
  103. package/dist/physics/kinematicVehicle.js +99 -10
  104. package/dist/physics/vehicleObstacles.js +19 -0
  105. package/dist/random/resolveRng.d.ts +14 -0
  106. package/dist/random/resolveRng.js +31 -0
  107. package/dist/render/postProcessing.d.ts +12 -1
  108. package/dist/runtime/cameraDirector.d.ts +2 -2
  109. package/dist/runtime/context/combat.js +8 -0
  110. package/dist/runtime/gameContext.js +6 -1
  111. package/dist/runtime/gameContextTypes.d.ts +18 -0
  112. package/dist/runtime/inputSnapshot.d.ts +10 -1
  113. package/dist/runtime/inputSnapshot.js +13 -1
  114. package/dist/runtime/runtimeSave.d.ts +3 -3
  115. package/dist/runtime/runtimeSave.js +11 -2
  116. package/dist/scene/assetDiagnostics.js +2 -2
  117. package/dist/scene/autoTarget.js +2 -1
  118. package/dist/scene/captureCheck.js +3 -2
  119. package/dist/scene/entityStore.d.ts +4 -1
  120. package/dist/scene/entityStore.js +3 -0
  121. package/dist/scene/objectStore.d.ts +10 -0
  122. package/dist/scene/objectStore.js +29 -6
  123. package/dist/scene/sceneKinds.d.ts +18 -0
  124. package/dist/scene/sceneKinds.js +19 -0
  125. package/dist/scene/sequenceDirector.d.ts +110 -0
  126. package/dist/scene/sequenceDirector.js +146 -0
  127. package/dist/session/roles.js +3 -2
  128. package/dist/settings/settingsModel.d.ts +1 -0
  129. package/dist/settings/settingsModel.js +1 -0
  130. package/dist/stats/rollCheck.js +4 -2
  131. package/dist/time/dayNightCycle.d.ts +114 -0
  132. package/dist/time/dayNightCycle.js +208 -0
  133. package/dist/time/timerSet.d.ts +132 -0
  134. package/dist/time/timerSet.js +243 -0
  135. package/dist/ui/accessibility.d.ts +51 -0
  136. package/dist/ui/accessibility.js +83 -0
  137. package/dist/ui/coachMarks.d.ts +99 -0
  138. package/dist/ui/coachMarks.js +126 -0
  139. package/dist/ui/floatingText.d.ts +114 -0
  140. package/dist/ui/floatingText.js +203 -0
  141. package/dist/ui/modalStack.d.ts +143 -0
  142. package/dist/ui/modalStack.js +160 -0
  143. package/dist/ui/objectiveBanner.d.ts +123 -0
  144. package/dist/ui/objectiveBanner.js +162 -0
  145. package/dist/ui/photoMode.d.ts +32 -0
  146. package/dist/ui/photoMode.js +51 -0
  147. package/dist/ui/radialMenu.d.ts +47 -0
  148. package/dist/ui/radialMenu.js +74 -0
  149. package/dist/ui/screenMarkers.d.ts +128 -0
  150. package/dist/ui/screenMarkers.js +118 -0
  151. package/dist/ui.d.ts +8 -0
  152. package/dist/ui.js +8 -0
  153. package/dist/vfx/cameraShake.d.ts +97 -0
  154. package/dist/vfx/cameraShake.js +136 -0
  155. package/dist/vfx/damageDirection.d.ts +102 -0
  156. package/dist/vfx/damageDirection.js +188 -0
  157. package/dist/vfx/particles.d.ts +103 -0
  158. package/dist/vfx/particles.js +256 -0
  159. package/dist/vfx/screenEffects.d.ts +149 -0
  160. package/dist/vfx/screenEffects.js +191 -0
  161. package/dist/world/authoredObjects.d.ts +18 -0
  162. package/dist/world/authoredObjects.js +21 -0
  163. package/dist/world/authoredSpawn.js +19 -0
  164. package/dist/world/buildingLots.d.ts +54 -11
  165. package/dist/world/buildingLots.js +116 -30
  166. package/dist/world/cityBlocks.d.ts +45 -0
  167. package/dist/world/cityBlocks.js +159 -0
  168. package/dist/world/cityContent.d.ts +46 -2
  169. package/dist/world/cityContent.js +160 -2
  170. package/dist/world/cityGenerator.d.ts +261 -23
  171. package/dist/world/cityGenerator.js +643 -33
  172. package/dist/world/cityGeometry.d.ts +20 -0
  173. package/dist/world/cityGeometry.js +91 -0
  174. package/dist/world/cityKind.js +1 -44
  175. package/dist/world/fastTravel.d.ts +64 -0
  176. package/dist/world/fastTravel.js +108 -0
  177. package/dist/world/features.d.ts +16 -0
  178. package/dist/world/features.js +2 -0
  179. package/dist/world/grounded.d.ts +11 -0
  180. package/dist/world/grounded.js +12 -0
  181. package/dist/world/mapAnnotations.d.ts +75 -0
  182. package/dist/world/mapAnnotations.js +139 -0
  183. package/dist/world/markers.js +1 -0
  184. package/dist/world/resourceNode.d.ts +1 -1
  185. package/dist/world/resourceNode.js +2 -1
  186. package/dist/world/roads.d.ts +294 -5
  187. package/dist/world/roads.js +1262 -21
  188. package/dist/world/sharedAnnotations.d.ts +84 -0
  189. package/dist/world/sharedAnnotations.js +76 -0
  190. package/dist/world/spawnOverride.d.ts +37 -0
  191. package/dist/world/spawnOverride.js +50 -0
  192. package/dist/world/streetGenerator.d.ts +60 -11
  193. package/dist/world/streetGenerator.js +1786 -129
  194. package/dist/world/terrain.d.ts +1 -0
  195. package/dist/world/terrain.js +2 -0
  196. package/dist/world/waypoints.d.ts +63 -0
  197. package/dist/world/waypoints.js +137 -0
  198. package/dist/world.d.ts +17 -2
  199. package/dist/world.js +17 -2
  200. package/package.json +1 -1
  201. package/skills/ce-handoff/SKILL.md +149 -0
  202. package/skills/game-design/SKILL.md +2 -0
  203. package/skills/jgengine/SKILL.md +3 -1
  204. package/skills/jgengine/api.md +17 -7
  205. package/skills/jgengine/capabilities.md +12 -0
  206. package/skills/jgengine/recipes/minimal-game.md +7 -4
  207. package/skills/jgengine-combat/api.md +40 -4
  208. package/skills/jgengine-combat/capabilities.md +12 -0
  209. package/skills/jgengine-combat/reference.md +2 -0
  210. package/skills/jgengine-gameplay/api.md +212 -11
  211. package/skills/jgengine-gameplay/capabilities.md +66 -1
  212. package/skills/jgengine-world/api.md +397 -11
  213. package/skills/jgengine-world/capabilities.md +123 -2
  214. package/skills/jgengine-world/reference.md +15 -1
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Turnkey day-night cycle: one serializable brain that advances a normalized day
3
+ * fraction on an injected clock and blends per-keyframe phase labels and tint/light
4
+ * colors. A game wires this one model and drives an existing sky/daylight seam from
5
+ * `sample()` instead of hand-rolling a clock plus a color lerp.
6
+ *
7
+ * The model is genre-agnostic: `phase` labels and colors are free-form values the
8
+ * game styles. The model never branches on what a phase string "means" — it only
9
+ * interpolates between the keyframes you give it.
10
+ */
11
+ /** Clamp a number into [0,1]. */
12
+ function clamp01(value) {
13
+ return value < 0 ? 0 : value > 1 ? 1 : value;
14
+ }
15
+ /** Wrap a number into [0,1), so `-0.1` → `0.9` and `1.2` → `0.2`. */
16
+ function wrap01(value) {
17
+ if (!Number.isFinite(value))
18
+ return 0;
19
+ return ((value % 1) + 1) % 1;
20
+ }
21
+ /** Linear interpolation between `a` and `b` by `t`. */
22
+ function lerp(a, b, t) {
23
+ return a + (b - a) * t;
24
+ }
25
+ /** Parse `"#rrggbb"` (or `"rrggbb"`) into an `[r,g,b]` byte triple. */
26
+ function hexToRgb(hex) {
27
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
28
+ const value = Number.parseInt(clean, 16);
29
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
30
+ }
31
+ /** Clamp and round a channel into a 0-255 byte. */
32
+ function clampByte(value) {
33
+ return Math.max(0, Math.min(255, Math.round(value)));
34
+ }
35
+ /** Encode an `[r,g,b]` triple back into `"#rrggbb"`. */
36
+ function rgbToHex(rgb) {
37
+ return `#${rgb.map((channel) => clampByte(channel).toString(16).padStart(2, "0")).join("")}`;
38
+ }
39
+ /** Interpolate two `"#rrggbb"` colors in RGB space by `t`. */
40
+ function lerpHex(a, b, t) {
41
+ const [ar, ag, ab] = hexToRgb(a);
42
+ const [br, bg, bb] = hexToRgb(b);
43
+ return rgbToHex([lerp(ar, br, t), lerp(ag, bg, t), lerp(ab, bb, t)]);
44
+ }
45
+ /** Sanitize a positive finite number, falling back to `fallback`. */
46
+ function positiveOr(value, fallback) {
47
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
48
+ }
49
+ /** Sort keyframes by wrapped `at` and fill in color/intensity fallbacks; never mutates the input. */
50
+ function normalizeKeyframes(keyframes) {
51
+ if (keyframes.length === 0) {
52
+ throw new Error("createDayNightCycle requires at least one keyframe");
53
+ }
54
+ return keyframes
55
+ .map((keyframe) => ({
56
+ at: wrap01(keyframe.at),
57
+ phase: keyframe.phase,
58
+ color: keyframe.color,
59
+ lightColor: keyframe.lightColor ?? keyframe.color,
60
+ intensity: keyframe.intensity ?? 1,
61
+ }))
62
+ .sort((a, b) => a.at - b.at);
63
+ }
64
+ /** Blend the normalized keyframe ring at a wrapped day fraction. */
65
+ function sampleKeyframes(sorted, fraction) {
66
+ const dayFraction = wrap01(fraction);
67
+ if (sorted.length === 1) {
68
+ const only = sorted[0];
69
+ return { dayFraction, phase: only.phase, color: only.color, lightColor: only.lightColor, intensity: only.intensity };
70
+ }
71
+ // Find the last keyframe at or before the fraction; the following one (wrapping past
72
+ // 1.0 back to the first) is the segment we interpolate across.
73
+ let fromIndex = sorted.length - 1;
74
+ for (let index = 0; index < sorted.length; index += 1) {
75
+ if (sorted[index].at <= dayFraction)
76
+ fromIndex = index;
77
+ else
78
+ break;
79
+ }
80
+ const from = sorted[fromIndex];
81
+ const to = sorted[(fromIndex + 1) % sorted.length];
82
+ const span = (to.at - from.at + 1) % 1;
83
+ const localRaw = (dayFraction - from.at + 1) % 1;
84
+ const localT = span <= 0 ? 0 : clamp01(localRaw / span);
85
+ return {
86
+ dayFraction,
87
+ phase: from.phase,
88
+ color: lerpHex(from.color, to.color, localT),
89
+ lightColor: lerpHex(from.lightColor, to.lightColor, localT),
90
+ intensity: lerp(from.intensity, to.intensity, localT),
91
+ };
92
+ }
93
+ /**
94
+ * Creates a turnkey day-night cycle: a serializable model that advances a normalized
95
+ * day fraction on an injected clock and blends per-keyframe phase labels and tint/light
96
+ * colors. Wire this one model, then drive an existing sky/daylight seam from `sample()`
97
+ * (or drop it straight into a `{ calendar(): { dayFraction } }` seam via `calendar()`)
98
+ * to get a moving day-night cycle with color grading — no hand-rolled clock or lerp.
99
+ * `phase` labels and colors are free-form; the model never interprets their meaning.
100
+ *
101
+ * @capability day-night-cycle one wired model gives a moving day-night cycle — running clock plus keyframed phase labels and tint/light colors a game drives an existing sky/daylight seam from
102
+ */
103
+ export function createDayNightCycle(options) {
104
+ const sorted = normalizeKeyframes(options.keyframes);
105
+ const keyframes = sorted.map((keyframe) => ({ ...keyframe }));
106
+ const dayLengthMs = positiveOr(options.dayLengthMs, 120_000);
107
+ const now = options.now ?? Date.now;
108
+ const listeners = new Set();
109
+ let elapsedMs = wrap01(options.start ?? 0) * dayLengthMs;
110
+ let paused = options.startPaused ?? false;
111
+ let speed = positiveOr(options.speed, 1);
112
+ let anchor = now();
113
+ function notify() {
114
+ for (const listener of listeners)
115
+ listener();
116
+ }
117
+ /** Accrue elapsed game-time from the injected clock since the last sync, then re-anchor. */
118
+ function sync() {
119
+ const t = now();
120
+ if (!paused)
121
+ elapsedMs += Math.max(0, t - anchor) * speed;
122
+ anchor = t;
123
+ }
124
+ function dayFraction() {
125
+ sync();
126
+ return wrap01(elapsedMs / dayLengthMs);
127
+ }
128
+ return {
129
+ keyframes,
130
+ dayLengthMs,
131
+ dayFraction,
132
+ phase() {
133
+ return sampleKeyframes(sorted, dayFraction()).phase;
134
+ },
135
+ sample() {
136
+ return sampleKeyframes(sorted, dayFraction());
137
+ },
138
+ sampleAt(fraction) {
139
+ return sampleKeyframes(sorted, fraction);
140
+ },
141
+ calendar() {
142
+ return { dayFraction: dayFraction() };
143
+ },
144
+ setDayFraction(fraction) {
145
+ sync();
146
+ elapsedMs = wrap01(fraction) * dayLengthMs;
147
+ notify();
148
+ },
149
+ isPaused() {
150
+ return paused;
151
+ },
152
+ pause() {
153
+ if (paused)
154
+ return;
155
+ sync();
156
+ paused = true;
157
+ notify();
158
+ },
159
+ play() {
160
+ if (!paused)
161
+ return;
162
+ sync();
163
+ paused = false;
164
+ notify();
165
+ },
166
+ toggle() {
167
+ if (paused)
168
+ this.play();
169
+ else
170
+ this.pause();
171
+ },
172
+ speed() {
173
+ return speed;
174
+ },
175
+ setSpeed(multiplier) {
176
+ sync();
177
+ if (!Number.isFinite(multiplier) || multiplier <= 0) {
178
+ if (!paused) {
179
+ paused = true;
180
+ notify();
181
+ }
182
+ return;
183
+ }
184
+ const changed = paused || multiplier !== speed;
185
+ speed = multiplier;
186
+ paused = false;
187
+ if (changed)
188
+ notify();
189
+ },
190
+ subscribe(listener) {
191
+ listeners.add(listener);
192
+ return () => {
193
+ listeners.delete(listener);
194
+ };
195
+ },
196
+ snapshot() {
197
+ sync();
198
+ return { elapsedMs, paused, speed };
199
+ },
200
+ restore(snapshot) {
201
+ elapsedMs = Number.isFinite(snapshot.elapsedMs) ? snapshot.elapsedMs : 0;
202
+ paused = snapshot.paused;
203
+ speed = positiveOr(snapshot.speed, 1);
204
+ anchor = now();
205
+ notify();
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A serializable set of named countdown / countup timers evaluated against an
3
+ * injected clock. One primitive covers round timers, respawn clocks, and ability
4
+ * cooldown/charge — they are the same mechanic (elapsed vs. duration on a clock),
5
+ * so `id` and any labels are free strings the engine never interprets.
6
+ *
7
+ * No hidden interval math: the set stores only accumulated elapsed time plus a
8
+ * clock anchor while running, and derives every read on demand. `subscribe`
9
+ * reports structural changes (start/pause/stop/reset); `poll` reports expiry
10
+ * edges so a game can react (respawn, round end, ability ready). `snapshot` /
11
+ * `restore` round-trip through a save by resolving elapsed at snapshot time and
12
+ * re-anchoring on restore, so pause/resume survives serialization.
13
+ */
14
+ /** Direction a timer counts. `"down"` leads with `remainingMs` (countdown); `"up"` leads with `elapsedMs` (charge). */
15
+ export type TimerDirection = "down" | "up";
16
+ /** Options for {@link TimerSet.start}. */
17
+ export interface TimerStartOptions {
18
+ /** Full span of the timer in milliseconds. Non-positive spans read as instantly expired. */
19
+ durationMs: number;
20
+ /** Count direction. Default `"down"`. Purely a read convenience — expiry is the same either way. */
21
+ direction?: TimerDirection;
22
+ /** When true the timer wraps at its duration and emits an expiry edge each cycle instead of stopping. Default `false`. */
23
+ loop?: boolean;
24
+ }
25
+ /**
26
+ * A single timer's resolved state for one read. All fields are plain numbers so
27
+ * a HUD can render without touching the model. For a looping timer the values
28
+ * describe the current cycle; `expired` is only ever `true` for a finished
29
+ * non-looping timer (loops signal completion through {@link TimerSet.poll}).
30
+ */
31
+ export interface TimerRead {
32
+ /** The timer's id (free string). */
33
+ id: string;
34
+ /** The configured count direction — lets a readout auto-pick remaining (`"down"`) vs elapsed (`"up"`). */
35
+ direction: TimerDirection;
36
+ /** Milliseconds left in this cycle, clamped to `0`. */
37
+ remainingMs: number;
38
+ /** Milliseconds elapsed in this cycle, clamped to `durationMs`. */
39
+ elapsedMs: number;
40
+ /** The configured span in milliseconds. */
41
+ durationMs: number;
42
+ /** Fill fraction `0..1` = `elapsedMs / durationMs` — a ring or bar fills as time passes. */
43
+ progress01: number;
44
+ /** Whether the timer is counting (not paused/stopped). */
45
+ running: boolean;
46
+ /** True once a non-looping timer has reached its duration. Always `false` for a looping timer. */
47
+ expired: boolean;
48
+ }
49
+ /** One timer's serializable state — elapsed resolved at snapshot time, ready to re-anchor on restore. */
50
+ export interface TimerSnapshot {
51
+ id: string;
52
+ durationMs: number;
53
+ direction: TimerDirection;
54
+ loop: boolean;
55
+ /** Elapsed milliseconds at the moment of the snapshot. */
56
+ elapsedMs: number;
57
+ running: boolean;
58
+ /** Whether this non-looping timer had already emitted its expiry edge. */
59
+ fired: boolean;
60
+ }
61
+ /** Serializable state of a whole {@link TimerSet}. */
62
+ export interface TimerSetSnapshot {
63
+ timers: TimerSnapshot[];
64
+ }
65
+ /** Options for {@link createTimerSet}. */
66
+ export interface TimerSetOptions {
67
+ /** Injected clock (ms). Default `Date.now`. Inject a sim clock for determinism. */
68
+ now?: () => number;
69
+ }
70
+ /** Listener notified when a timer newly expires. Receives the timer id. */
71
+ export type TimerExpiryListener = (id: string) => void;
72
+ /**
73
+ * A named set of countdown / countup timers on an injected clock. Start, pause,
74
+ * resume, stop, reset, and read timers by free-string id; observe structural
75
+ * changes with {@link TimerSet.subscribe} and expiry edges with
76
+ * {@link TimerSet.poll} / {@link TimerSet.onExpire}.
77
+ */
78
+ export interface TimerSet {
79
+ /** Start (or replace) a timer by id, running from zero. */
80
+ start(id: string, options: TimerStartOptions): void;
81
+ /** Pause a running timer, freezing its elapsed time. No-op if unknown or already paused. */
82
+ pause(id: string): void;
83
+ /** Resume a paused timer from where it stopped. No-op if unknown or already running. */
84
+ resume(id: string): void;
85
+ /** Halt a timer and reset its elapsed time to zero, keeping it in the set (not running). */
86
+ stop(id: string): void;
87
+ /** Restart a timer's elapsed time from zero, preserving its running state. */
88
+ reset(id: string): void;
89
+ /** Remove a timer from the set entirely. */
90
+ remove(id: string): void;
91
+ /** Pause every running timer (e.g. a global game pause). */
92
+ pauseAll(): void;
93
+ /** Resume every paused timer. */
94
+ resumeAll(): void;
95
+ /** Whether a timer with this id exists. */
96
+ has(id: string): boolean;
97
+ /** Every timer id currently in the set. */
98
+ ids(): readonly string[];
99
+ /**
100
+ * Resolve a timer's current state. Pass `out` to reuse an object and avoid
101
+ * per-read allocation in a hot HUD loop. Returns `null` for an unknown id.
102
+ */
103
+ read(id: string, out?: TimerRead): TimerRead | null;
104
+ /** Resolve every timer, newest reads. Allocates an array — for per-frame reads prefer {@link TimerSet.read} with `out`. */
105
+ list(): TimerRead[];
106
+ /**
107
+ * Advance expiry detection to the current clock time and return the ids that
108
+ * newly expired since the last poll (firing {@link TimerSet.onExpire}
109
+ * listeners for each). Looping timers report once per completed cycle. Call
110
+ * once per frame/tick to react to finished timers.
111
+ */
112
+ poll(): readonly string[];
113
+ /** Observe structural changes (start/pause/resume/stop/reset/remove/restore). Returns an unsubscribe fn. */
114
+ subscribe(listener: () => void): () => void;
115
+ /** Observe expiry edges surfaced by {@link TimerSet.poll}. Returns an unsubscribe fn. */
116
+ onExpire(listener: TimerExpiryListener): () => void;
117
+ /** Serializable state for a save. */
118
+ snapshot(): TimerSetSnapshot;
119
+ /** Restore from a {@link TimerSetSnapshot}, re-anchoring running timers to the current clock. */
120
+ restore(snapshot: TimerSetSnapshot): void;
121
+ }
122
+ /**
123
+ * Create a serializable set of named countdown / countup timers on an injected
124
+ * clock — one primitive for round timers, respawn clocks, and ability
125
+ * cooldown/charge. Start/pause/resume/stop/reset timers by free-string id, read
126
+ * `{ remainingMs, elapsedMs, durationMs, progress01, running, expired }` for a
127
+ * mm:ss readout or a radial/bar fill, and `poll` for expiry edges. Ids and
128
+ * labels carry no genre meaning; `snapshot`/`restore` round-trip through a save.
129
+ *
130
+ * @capability countdown-timers named countdown/countup timer set on an injected clock — round timers, respawn clocks, ability cooldown/charge; read remaining/elapsed/progress + expiry, snapshot/restore
131
+ */
132
+ export declare function createTimerSet(options?: TimerSetOptions): TimerSet;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * A serializable set of named countdown / countup timers evaluated against an
3
+ * injected clock. One primitive covers round timers, respawn clocks, and ability
4
+ * cooldown/charge — they are the same mechanic (elapsed vs. duration on a clock),
5
+ * so `id` and any labels are free strings the engine never interprets.
6
+ *
7
+ * No hidden interval math: the set stores only accumulated elapsed time plus a
8
+ * clock anchor while running, and derives every read on demand. `subscribe`
9
+ * reports structural changes (start/pause/stop/reset); `poll` reports expiry
10
+ * edges so a game can react (respawn, round end, ability ready). `snapshot` /
11
+ * `restore` round-trip through a save by resolving elapsed at snapshot time and
12
+ * re-anchoring on restore, so pause/resume survives serialization.
13
+ */
14
+ function clamp01(v) {
15
+ return v < 0 ? 0 : v > 1 ? 1 : v;
16
+ }
17
+ function rawElapsed(rec, t) {
18
+ const live = rec.running ? Math.max(0, t - rec.anchor) : 0;
19
+ return rec.base + live;
20
+ }
21
+ /**
22
+ * Create a serializable set of named countdown / countup timers on an injected
23
+ * clock — one primitive for round timers, respawn clocks, and ability
24
+ * cooldown/charge. Start/pause/resume/stop/reset timers by free-string id, read
25
+ * `{ remainingMs, elapsedMs, durationMs, progress01, running, expired }` for a
26
+ * mm:ss readout or a radial/bar fill, and `poll` for expiry edges. Ids and
27
+ * labels carry no genre meaning; `snapshot`/`restore` round-trip through a save.
28
+ *
29
+ * @capability countdown-timers named countdown/countup timer set on an injected clock — round timers, respawn clocks, ability cooldown/charge; read remaining/elapsed/progress + expiry, snapshot/restore
30
+ */
31
+ export function createTimerSet(options = {}) {
32
+ const now = options.now ?? Date.now;
33
+ const timers = new Map();
34
+ const listeners = new Set();
35
+ const expiryListeners = new Set();
36
+ function notify() {
37
+ for (const listener of listeners)
38
+ listener();
39
+ }
40
+ function completedCycles(rec, t) {
41
+ if (rec.durationMs <= 0)
42
+ return rec.loop ? 0 : 1;
43
+ const raw = rawElapsed(rec, t);
44
+ if (rec.loop)
45
+ return Math.floor(raw / rec.durationMs);
46
+ return raw >= rec.durationMs ? 1 : 0;
47
+ }
48
+ function fill(rec, t, out) {
49
+ const duration = rec.durationMs;
50
+ const raw = rawElapsed(rec, t);
51
+ let cycleElapsed;
52
+ let expired;
53
+ if (duration <= 0) {
54
+ cycleElapsed = 0;
55
+ expired = !rec.loop;
56
+ }
57
+ else if (rec.loop) {
58
+ cycleElapsed = raw % duration;
59
+ expired = false;
60
+ }
61
+ else {
62
+ cycleElapsed = raw < duration ? raw : duration;
63
+ expired = raw >= duration;
64
+ }
65
+ out.id = rec.id;
66
+ out.direction = rec.direction;
67
+ out.durationMs = duration;
68
+ out.elapsedMs = cycleElapsed;
69
+ out.remainingMs = duration > 0 ? duration - cycleElapsed : 0;
70
+ out.progress01 = duration > 0 ? clamp01(cycleElapsed / duration) : 1;
71
+ out.running = rec.running;
72
+ out.expired = expired;
73
+ return out;
74
+ }
75
+ function blankRead() {
76
+ return {
77
+ id: "",
78
+ direction: "down",
79
+ remainingMs: 0,
80
+ elapsedMs: 0,
81
+ durationMs: 0,
82
+ progress01: 0,
83
+ running: false,
84
+ expired: false,
85
+ };
86
+ }
87
+ return {
88
+ start(id, startOptions) {
89
+ timers.set(id, {
90
+ id,
91
+ durationMs: startOptions.durationMs,
92
+ direction: startOptions.direction ?? "down",
93
+ loop: startOptions.loop ?? false,
94
+ base: 0,
95
+ running: true,
96
+ anchor: now(),
97
+ reported: 0,
98
+ });
99
+ notify();
100
+ },
101
+ pause(id) {
102
+ const rec = timers.get(id);
103
+ if (rec === undefined || !rec.running)
104
+ return;
105
+ rec.base = rawElapsed(rec, now());
106
+ rec.running = false;
107
+ notify();
108
+ },
109
+ resume(id) {
110
+ const rec = timers.get(id);
111
+ if (rec === undefined || rec.running)
112
+ return;
113
+ rec.anchor = now();
114
+ rec.running = true;
115
+ notify();
116
+ },
117
+ stop(id) {
118
+ const rec = timers.get(id);
119
+ if (rec === undefined)
120
+ return;
121
+ rec.base = 0;
122
+ rec.running = false;
123
+ rec.reported = 0;
124
+ notify();
125
+ },
126
+ reset(id) {
127
+ const rec = timers.get(id);
128
+ if (rec === undefined)
129
+ return;
130
+ rec.base = 0;
131
+ rec.anchor = now();
132
+ rec.reported = 0;
133
+ notify();
134
+ },
135
+ remove(id) {
136
+ if (timers.delete(id))
137
+ notify();
138
+ },
139
+ pauseAll() {
140
+ const t = now();
141
+ let changed = false;
142
+ for (const rec of timers.values()) {
143
+ if (rec.running) {
144
+ rec.base = rawElapsed(rec, t);
145
+ rec.running = false;
146
+ changed = true;
147
+ }
148
+ }
149
+ if (changed)
150
+ notify();
151
+ },
152
+ resumeAll() {
153
+ const t = now();
154
+ let changed = false;
155
+ for (const rec of timers.values()) {
156
+ if (!rec.running) {
157
+ rec.anchor = t;
158
+ rec.running = true;
159
+ changed = true;
160
+ }
161
+ }
162
+ if (changed)
163
+ notify();
164
+ },
165
+ has(id) {
166
+ return timers.has(id);
167
+ },
168
+ ids() {
169
+ return [...timers.keys()];
170
+ },
171
+ read(id, out) {
172
+ const rec = timers.get(id);
173
+ if (rec === undefined)
174
+ return null;
175
+ return fill(rec, now(), out ?? blankRead());
176
+ },
177
+ list() {
178
+ const t = now();
179
+ const reads = [];
180
+ for (const rec of timers.values())
181
+ reads.push(fill(rec, t, blankRead()));
182
+ return reads;
183
+ },
184
+ poll() {
185
+ const t = now();
186
+ const fired = [];
187
+ for (const rec of timers.values()) {
188
+ const cycles = completedCycles(rec, t);
189
+ if (cycles > rec.reported) {
190
+ const times = cycles - rec.reported;
191
+ rec.reported = cycles;
192
+ for (let i = 0; i < times; i += 1) {
193
+ fired.push(rec.id);
194
+ for (const listener of expiryListeners)
195
+ listener(rec.id);
196
+ }
197
+ }
198
+ }
199
+ return fired;
200
+ },
201
+ subscribe(listener) {
202
+ listeners.add(listener);
203
+ return () => listeners.delete(listener);
204
+ },
205
+ onExpire(listener) {
206
+ expiryListeners.add(listener);
207
+ return () => expiryListeners.delete(listener);
208
+ },
209
+ snapshot() {
210
+ const t = now();
211
+ const out = [];
212
+ for (const rec of timers.values()) {
213
+ out.push({
214
+ id: rec.id,
215
+ durationMs: rec.durationMs,
216
+ direction: rec.direction,
217
+ loop: rec.loop,
218
+ elapsedMs: rawElapsed(rec, t),
219
+ running: rec.running,
220
+ fired: rec.reported > 0,
221
+ });
222
+ }
223
+ return { timers: out };
224
+ },
225
+ restore(snapshot) {
226
+ timers.clear();
227
+ const t = now();
228
+ for (const snap of snapshot.timers) {
229
+ timers.set(snap.id, {
230
+ id: snap.id,
231
+ durationMs: snap.durationMs,
232
+ direction: snap.direction,
233
+ loop: snap.loop,
234
+ base: snap.elapsedMs,
235
+ running: snap.running,
236
+ anchor: t,
237
+ reported: snap.fired ? 1 : 0,
238
+ });
239
+ }
240
+ notify();
241
+ },
242
+ };
243
+ }
@@ -0,0 +1,51 @@
1
+ /** Colorblind simulation/daltonization modes the engine ships matrices for. */
2
+ export type ColorblindMode = "none" | "protanopia" | "deuteranopia" | "tritanopia" | "grayscale";
3
+ /** Serializable accessibility preferences a game reads to adapt its presentation. */
4
+ export interface AccessibilityState {
5
+ /** Suppress non-essential motion/animation. */
6
+ reducedMotion: boolean;
7
+ /** Boost contrast (stronger borders/backgrounds, less transparency). */
8
+ highContrast: boolean;
9
+ /** UI text scale multiplier (`1` = default). See {@link clampTextScale} for bounds. */
10
+ textScale: number;
11
+ /** Colorblind filter applied to the presentation. */
12
+ colorblind: ColorblindMode;
13
+ /** Show subtitles/captions for dialogue and important audio. */
14
+ captions: boolean;
15
+ }
16
+ /** The out-of-the-box accessibility state (all assistance off, `textScale` 1). */
17
+ export declare const DEFAULT_ACCESSIBILITY: AccessibilityState;
18
+ /** Minimum UI text scale. */
19
+ export declare const TEXT_SCALE_MIN = 0.75;
20
+ /** Maximum UI text scale. */
21
+ export declare const TEXT_SCALE_MAX = 2;
22
+ /** Clamp a text-scale multiplier into the supported `[TEXT_SCALE_MIN, TEXT_SCALE_MAX]` range. */
23
+ export declare function clampTextScale(scale: number): number;
24
+ /** Return `ms` when motion is allowed, or `reduced` (default 0) when `reducedMotion` is set — the animation-duration gate. */
25
+ export declare function reducedMotionDuration(state: AccessibilityState, ms: number, reduced?: number): number;
26
+ /**
27
+ * `feColorMatrix` `values` strings for each colorblind mode — simulate how a
28
+ * viewer with that condition perceives color (and a grayscale mode). `"none"`
29
+ * maps to `null` (no filter). Consumed by the React `ColorblindFilters` defs.
30
+ */
31
+ export declare const COLORBLIND_MATRICES: Record<ColorblindMode, string | null>;
32
+ /** Observable, serializable accessibility-preferences store. */
33
+ export interface AccessibilityStore {
34
+ get(): AccessibilityState;
35
+ /** Merge a partial update (text scale is clamped); notifies on an actual change. */
36
+ set(patch: Partial<AccessibilityState>): void;
37
+ /** Restore defaults. */
38
+ reset(): void;
39
+ subscribe(listener: () => void): () => void;
40
+ snapshot(): AccessibilityState;
41
+ restore(snapshot: Partial<AccessibilityState>): void;
42
+ }
43
+ /**
44
+ * Serializable, observable store of accessibility preferences (reduced motion,
45
+ * high contrast, text scale, colorblind mode, captions). Genre-agnostic plumbing
46
+ * like input rebinding — a game binds these to its settings UI and adapts its
47
+ * presentation; `snapshot`/`restore` round-trip through a save or settings blob.
48
+ *
49
+ * @capability accessibility-store serializable, observable accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions) a game binds to its settings and presentation
50
+ */
51
+ export declare function createAccessibilityStore(initial?: Partial<AccessibilityState>): AccessibilityStore;