@jgengine/core 0.13.0 → 0.15.0-next.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 (223) hide show
  1. package/CHANGELOG.md +529 -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/authoring.d.ts +6 -4
  12. package/dist/authoring.js +6 -4
  13. package/dist/combat/castRunner.d.ts +13 -1
  14. package/dist/combat/castRunner.js +5 -1
  15. package/dist/combat/comboMeter.d.ts +140 -0
  16. package/dist/combat/comboMeter.js +215 -0
  17. package/dist/combat/comboPoints.d.ts +11 -0
  18. package/dist/combat/comboPoints.js +11 -2
  19. package/dist/combat/damageResolution.d.ts +1 -1
  20. package/dist/combat/defensiveWindow.d.ts +5 -0
  21. package/dist/combat/effects.d.ts +24 -0
  22. package/dist/combat/effects.js +6 -1
  23. package/dist/combat/projectiles.d.ts +6 -0
  24. package/dist/combat/projectiles.js +2 -2
  25. package/dist/combat/regenShield.d.ts +40 -2
  26. package/dist/combat/regenShield.js +66 -18
  27. package/dist/combat/resourcePool.d.ts +19 -0
  28. package/dist/combat/resourcePool.js +20 -2
  29. package/dist/combat/shotOrigin.d.ts +5 -0
  30. package/dist/combat/statusApplication.d.ts +1 -1
  31. package/dist/combat/statusApplication.js +2 -1
  32. package/dist/combat/statusEffectView.d.ts +56 -0
  33. package/dist/combat/statusEffectView.js +52 -0
  34. package/dist/combat.d.ts +4 -2
  35. package/dist/combat.js +2 -0
  36. package/dist/devtools/fallbackSeams.d.ts +3 -2
  37. package/dist/devtools/fallbackSeams.js +1 -1
  38. package/dist/devtools/textureErrors.d.ts +40 -0
  39. package/dist/devtools/textureErrors.js +68 -0
  40. package/dist/economy/shopStock.d.ts +110 -0
  41. package/dist/economy/shopStock.js +157 -0
  42. package/dist/economy/wallet.d.ts +6 -0
  43. package/dist/economy/wallet.js +1 -0
  44. package/dist/game/achievements.d.ts +80 -0
  45. package/dist/game/achievements.js +124 -0
  46. package/dist/game/cameraConfig.d.ts +7 -0
  47. package/dist/game/codex.d.ts +60 -0
  48. package/dist/game/codex.js +89 -0
  49. package/dist/game/credits.d.ts +36 -0
  50. package/dist/game/credits.js +0 -0
  51. package/dist/game/defineGame.d.ts +28 -3
  52. package/dist/game/defineGame.js +5 -0
  53. package/dist/game/dialogueGraph.d.ts +116 -0
  54. package/dist/game/dialogueGraph.js +103 -0
  55. package/dist/game/eventTicker.d.ts +95 -0
  56. package/dist/game/eventTicker.js +91 -0
  57. package/dist/game/events.d.ts +24 -5
  58. package/dist/game/events.js +18 -0
  59. package/dist/game/gamePhase.d.ts +1 -1
  60. package/dist/game/gamePhase.js +1 -1
  61. package/dist/game/leaderboardRank.d.ts +71 -0
  62. package/dist/game/leaderboardRank.js +76 -0
  63. package/dist/game/lootTable.d.ts +10 -1
  64. package/dist/game/lootTable.js +4 -2
  65. package/dist/game/notifications.d.ts +57 -0
  66. package/dist/game/notifications.js +107 -0
  67. package/dist/game/playableGame.d.ts +39 -2
  68. package/dist/game/quest.d.ts +26 -0
  69. package/dist/game/quest.js +31 -0
  70. package/dist/game/runDraft.js +2 -1
  71. package/dist/game/saveSlots.d.ts +102 -0
  72. package/dist/game/saveSlots.js +136 -0
  73. package/dist/game/talentTreeView.d.ts +106 -0
  74. package/dist/game/talentTreeView.js +114 -0
  75. package/dist/game/worldItem.js +2 -1
  76. package/dist/gameplay.d.ts +15 -4
  77. package/dist/gameplay.js +13 -2
  78. package/dist/i18n/i18n.d.ts +51 -0
  79. package/dist/i18n/i18n.js +82 -0
  80. package/dist/input/axisInput.d.ts +1 -1
  81. package/dist/input/axisInput.js +18 -2
  82. package/dist/input/rebindSession.d.ts +141 -0
  83. package/dist/input/rebindSession.js +235 -0
  84. package/dist/input/touchControlsMode.d.ts +16 -0
  85. package/dist/input/touchControlsMode.js +23 -0
  86. package/dist/input/touchScheme.d.ts +30 -1
  87. package/dist/input/touchScheme.js +31 -1
  88. package/dist/interaction/promptRegistry.d.ts +78 -0
  89. package/dist/interaction/promptRegistry.js +130 -0
  90. package/dist/inventory/storageTier.js +3 -2
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +21 -1
  93. package/dist/movement/movementModel.d.ts +26 -3
  94. package/dist/movement/movementModel.js +121 -15
  95. package/dist/movement/playerMovement.js +161 -25
  96. package/dist/physics/drivableVehicle.js +3 -1
  97. package/dist/physics/kinematicVehicle.d.ts +56 -1
  98. package/dist/physics/kinematicVehicle.js +99 -10
  99. package/dist/physics/vehicleObstacles.js +19 -0
  100. package/dist/random/resolveRng.d.ts +14 -0
  101. package/dist/random/resolveRng.js +31 -0
  102. package/dist/render/postProcessing.d.ts +12 -1
  103. package/dist/runtime/cameraDirector.d.ts +2 -2
  104. package/dist/runtime/context/combat.js +8 -0
  105. package/dist/runtime/context/scene.d.ts +2 -0
  106. package/dist/runtime/context/scene.js +48 -1
  107. package/dist/runtime/gameContext.js +8 -2
  108. package/dist/runtime/gameContextTypes.d.ts +39 -0
  109. package/dist/runtime/inputSnapshot.d.ts +10 -1
  110. package/dist/runtime/inputSnapshot.js +13 -1
  111. package/dist/runtime/runtimeSave.d.ts +3 -3
  112. package/dist/runtime/runtimeSave.js +11 -2
  113. package/dist/scene/assetDiagnostics.js +2 -2
  114. package/dist/scene/autoTarget.js +2 -1
  115. package/dist/scene/builtinSceneKinds.d.ts +6 -1
  116. package/dist/scene/builtinSceneKinds.js +10 -3
  117. package/dist/scene/captureCheck.js +3 -2
  118. package/dist/scene/colliders.d.ts +20 -0
  119. package/dist/scene/colliders.js +78 -0
  120. package/dist/scene/collisionMesh.d.ts +8 -0
  121. package/dist/scene/collisionMesh.js +48 -0
  122. package/dist/scene/entityStore.d.ts +4 -1
  123. package/dist/scene/entityStore.js +3 -0
  124. package/dist/scene/objectStore.d.ts +10 -0
  125. package/dist/scene/objectStore.js +29 -6
  126. package/dist/scene/sceneKinds.d.ts +37 -0
  127. package/dist/scene/sceneKinds.js +19 -0
  128. package/dist/scene/sequenceDirector.d.ts +110 -0
  129. package/dist/scene/sequenceDirector.js +146 -0
  130. package/dist/session/roles.js +3 -2
  131. package/dist/settings/settingsModel.d.ts +1 -0
  132. package/dist/settings/settingsModel.js +1 -0
  133. package/dist/stats/rollCheck.js +4 -2
  134. package/dist/time/dayNightCycle.d.ts +114 -0
  135. package/dist/time/dayNightCycle.js +208 -0
  136. package/dist/time/timerSet.d.ts +132 -0
  137. package/dist/time/timerSet.js +243 -0
  138. package/dist/ui/accessibility.d.ts +51 -0
  139. package/dist/ui/accessibility.js +83 -0
  140. package/dist/ui/coachMarks.d.ts +99 -0
  141. package/dist/ui/coachMarks.js +126 -0
  142. package/dist/ui/floatingText.d.ts +114 -0
  143. package/dist/ui/floatingText.js +203 -0
  144. package/dist/ui/hudScale.d.ts +16 -1
  145. package/dist/ui/hudScale.js +22 -1
  146. package/dist/ui/modalStack.d.ts +143 -0
  147. package/dist/ui/modalStack.js +160 -0
  148. package/dist/ui/objectiveBanner.d.ts +123 -0
  149. package/dist/ui/objectiveBanner.js +162 -0
  150. package/dist/ui/photoMode.d.ts +32 -0
  151. package/dist/ui/photoMode.js +51 -0
  152. package/dist/ui/radialMenu.d.ts +47 -0
  153. package/dist/ui/radialMenu.js +74 -0
  154. package/dist/ui/screenMarkers.d.ts +128 -0
  155. package/dist/ui/screenMarkers.js +118 -0
  156. package/dist/ui.d.ts +8 -0
  157. package/dist/ui.js +8 -0
  158. package/dist/vfx/cameraShake.d.ts +97 -0
  159. package/dist/vfx/cameraShake.js +136 -0
  160. package/dist/vfx/damageDirection.d.ts +102 -0
  161. package/dist/vfx/damageDirection.js +188 -0
  162. package/dist/vfx/particles.d.ts +103 -0
  163. package/dist/vfx/particles.js +256 -0
  164. package/dist/vfx/screenEffects.d.ts +149 -0
  165. package/dist/vfx/screenEffects.js +191 -0
  166. package/dist/world/authoredObjects.d.ts +18 -0
  167. package/dist/world/authoredObjects.js +21 -0
  168. package/dist/world/authoredSpawn.js +19 -0
  169. package/dist/world/buildingLots.d.ts +54 -11
  170. package/dist/world/buildingLots.js +116 -30
  171. package/dist/world/cityBlocks.d.ts +45 -0
  172. package/dist/world/cityBlocks.js +159 -0
  173. package/dist/world/cityContent.d.ts +46 -2
  174. package/dist/world/cityContent.js +160 -2
  175. package/dist/world/cityGenerator.d.ts +261 -23
  176. package/dist/world/cityGenerator.js +643 -33
  177. package/dist/world/cityGeometry.d.ts +20 -0
  178. package/dist/world/cityGeometry.js +91 -0
  179. package/dist/world/cityKind.js +9 -46
  180. package/dist/world/fastTravel.d.ts +64 -0
  181. package/dist/world/fastTravel.js +108 -0
  182. package/dist/world/features.d.ts +16 -0
  183. package/dist/world/features.js +2 -0
  184. package/dist/world/grassKind.js +4 -1
  185. package/dist/world/grounded.d.ts +11 -0
  186. package/dist/world/grounded.js +12 -0
  187. package/dist/world/mapAnnotations.d.ts +75 -0
  188. package/dist/world/mapAnnotations.js +139 -0
  189. package/dist/world/markers.js +1 -0
  190. package/dist/world/resourceNode.d.ts +1 -1
  191. package/dist/world/resourceNode.js +2 -1
  192. package/dist/world/roads.d.ts +294 -5
  193. package/dist/world/roads.js +1262 -21
  194. package/dist/world/scatterCoverage.d.ts +98 -0
  195. package/dist/world/scatterCoverage.js +101 -0
  196. package/dist/world/scatterRegion.js +6 -0
  197. package/dist/world/sharedAnnotations.d.ts +84 -0
  198. package/dist/world/sharedAnnotations.js +76 -0
  199. package/dist/world/spawnOverride.d.ts +37 -0
  200. package/dist/world/spawnOverride.js +50 -0
  201. package/dist/world/streetGenerator.d.ts +60 -11
  202. package/dist/world/streetGenerator.js +1786 -129
  203. package/dist/world/terrain.d.ts +1 -0
  204. package/dist/world/terrain.js +2 -0
  205. package/dist/world/waypoints.d.ts +63 -0
  206. package/dist/world/waypoints.js +137 -0
  207. package/dist/world.d.ts +18 -2
  208. package/dist/world.js +18 -2
  209. package/package.json +2 -2
  210. package/skills/ce-handoff/SKILL.md +149 -0
  211. package/skills/game-design/SKILL.md +2 -0
  212. package/skills/jgengine/SKILL.md +7 -3
  213. package/skills/jgengine/api.md +17 -7
  214. package/skills/jgengine/capabilities.md +12 -0
  215. package/skills/jgengine/recipes/minimal-game.md +7 -4
  216. package/skills/jgengine-combat/api.md +40 -4
  217. package/skills/jgengine-combat/capabilities.md +12 -0
  218. package/skills/jgengine-combat/reference.md +2 -0
  219. package/skills/jgengine-gameplay/api.md +212 -11
  220. package/skills/jgengine-gameplay/capabilities.md +66 -1
  221. package/skills/jgengine-world/api.md +408 -12
  222. package/skills/jgengine-world/capabilities.md +127 -2
  223. package/skills/jgengine-world/reference.md +15 -1
@@ -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;
@@ -0,0 +1,83 @@
1
+ /** The out-of-the-box accessibility state (all assistance off, `textScale` 1). */
2
+ export const DEFAULT_ACCESSIBILITY = {
3
+ reducedMotion: false,
4
+ highContrast: false,
5
+ textScale: 1,
6
+ colorblind: "none",
7
+ captions: false,
8
+ };
9
+ /** Minimum UI text scale. */
10
+ export const TEXT_SCALE_MIN = 0.75;
11
+ /** Maximum UI text scale. */
12
+ export const TEXT_SCALE_MAX = 2;
13
+ /** Clamp a text-scale multiplier into the supported `[TEXT_SCALE_MIN, TEXT_SCALE_MAX]` range. */
14
+ export function clampTextScale(scale) {
15
+ if (Number.isNaN(scale))
16
+ return 1;
17
+ return Math.max(TEXT_SCALE_MIN, Math.min(TEXT_SCALE_MAX, scale));
18
+ }
19
+ /** Return `ms` when motion is allowed, or `reduced` (default 0) when `reducedMotion` is set — the animation-duration gate. */
20
+ export function reducedMotionDuration(state, ms, reduced = 0) {
21
+ return state.reducedMotion ? reduced : ms;
22
+ }
23
+ /**
24
+ * `feColorMatrix` `values` strings for each colorblind mode — simulate how a
25
+ * viewer with that condition perceives color (and a grayscale mode). `"none"`
26
+ * maps to `null` (no filter). Consumed by the React `ColorblindFilters` defs.
27
+ */
28
+ export const COLORBLIND_MATRICES = {
29
+ none: null,
30
+ protanopia: "0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0",
31
+ deuteranopia: "0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0",
32
+ tritanopia: "0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0",
33
+ grayscale: "0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0",
34
+ };
35
+ /**
36
+ * Serializable, observable store of accessibility preferences (reduced motion,
37
+ * high contrast, text scale, colorblind mode, captions). Genre-agnostic plumbing
38
+ * like input rebinding — a game binds these to its settings UI and adapts its
39
+ * presentation; `snapshot`/`restore` round-trip through a save or settings blob.
40
+ *
41
+ * @capability accessibility-store serializable, observable accessibility preferences (reduced motion, high contrast, text scale, colorblind mode, captions) a game binds to its settings and presentation
42
+ */
43
+ export function createAccessibilityStore(initial = {}) {
44
+ let state = normalize({ ...DEFAULT_ACCESSIBILITY, ...initial });
45
+ const listeners = new Set();
46
+ function normalize(next) {
47
+ return { ...next, textScale: clampTextScale(next.textScale) };
48
+ }
49
+ function commit(next) {
50
+ const normalized = normalize(next);
51
+ if (normalized.reducedMotion === state.reducedMotion &&
52
+ normalized.highContrast === state.highContrast &&
53
+ normalized.textScale === state.textScale &&
54
+ normalized.colorblind === state.colorblind &&
55
+ normalized.captions === state.captions) {
56
+ return;
57
+ }
58
+ state = normalized;
59
+ for (const listener of listeners)
60
+ listener();
61
+ }
62
+ return {
63
+ get() {
64
+ return state;
65
+ },
66
+ set(patch) {
67
+ commit({ ...state, ...patch });
68
+ },
69
+ reset() {
70
+ commit({ ...DEFAULT_ACCESSIBILITY });
71
+ },
72
+ subscribe(listener) {
73
+ listeners.add(listener);
74
+ return () => listeners.delete(listener);
75
+ },
76
+ snapshot() {
77
+ return state;
78
+ },
79
+ restore(snapshot) {
80
+ commit({ ...DEFAULT_ACCESSIBILITY, ...snapshot });
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,99 @@
1
+ /** Where a coach-mark callout sits relative to its anchor (or `"center"` for an unanchored callout). */
2
+ export type CoachMarkPlacement = "top" | "bottom" | "left" | "right" | "center";
3
+ /** One onboarding hint: an ordered, optionally-anchored, optionally-gated callout. */
4
+ export interface CoachMarkStep {
5
+ /** Stable id — also the key tracked in the persisted "seen" set. */
6
+ id: string;
7
+ title: string;
8
+ body?: string;
9
+ /**
10
+ * CSS selector (or a logical key the presenter resolves) for the element the
11
+ * callout points at. Omitted → the step renders as a centered callout.
12
+ */
13
+ anchor?: string;
14
+ /** Preferred side of the anchor. Default `"bottom"` when anchored, `"center"` otherwise. */
15
+ placement?: CoachMarkPlacement;
16
+ /**
17
+ * Serializable trigger id. While set, the step stays hidden until that id is
18
+ * in the sequence's satisfied set (see {@link CoachMarkSequence.satisfy}).
19
+ * Kept data-first — a string, not a function — so a whole sequence serializes.
20
+ */
21
+ condition?: string;
22
+ }
23
+ /** The active step plus its position, returned by {@link CoachMarkSequence.current}. */
24
+ export interface CoachMarkView {
25
+ /** The step to render. */
26
+ step: CoachMarkStep;
27
+ /** 0-based position of this step within the full ordered list. */
28
+ index: number;
29
+ /** Total number of steps in the sequence. */
30
+ total: number;
31
+ /** Un-seen steps left, including this one (for an "N of M" style counter). */
32
+ remaining: number;
33
+ }
34
+ /** Serializable persisted state — the seen set (with timestamps) and satisfied triggers. */
35
+ export interface CoachMarkSnapshot {
36
+ /** Ids of steps that have been advanced past, dismissed, or skipped. */
37
+ seen: readonly string[];
38
+ /** When each seen id was first marked, from the injected clock. */
39
+ seenAt: Readonly<Record<string, number>>;
40
+ /** Currently-satisfied trigger ids (gate state). */
41
+ satisfied: readonly string[];
42
+ }
43
+ /** Options for {@link createCoachMarkSequence}. */
44
+ export interface CoachMarkSequenceOptions {
45
+ /** Ordered hint steps. */
46
+ steps: readonly CoachMarkStep[];
47
+ /** Injected clock (ms) used to timestamp when a step is marked seen. Default `Date.now`. */
48
+ now?: () => number;
49
+ /** Ids of steps already seen (e.g. restored from a save) so they never re-show. */
50
+ seen?: Iterable<string>;
51
+ /** Trigger ids already satisfied when the sequence starts. */
52
+ satisfied?: Iterable<string>;
53
+ }
54
+ /** An ordered, observable, serializable onboarding coach-mark run. */
55
+ export interface CoachMarkSequence {
56
+ /** The configured steps, in order. */
57
+ readonly steps: readonly CoachMarkStep[];
58
+ /** First un-seen step whose condition (if any) is satisfied, or `null` when none is eligible. */
59
+ current(): CoachMarkView | null;
60
+ /** Mark the current step seen and return the next eligible step (or `null`). */
61
+ advance(): CoachMarkView | null;
62
+ /** Alias of {@link CoachMarkSequence.advance}. */
63
+ next(): CoachMarkView | null;
64
+ /** Mark a specific step seen so it never re-shows. */
65
+ dismiss(id: string): void;
66
+ /** Mark every step seen — "skip tour". */
67
+ skipAll(): void;
68
+ /** Add trigger ids to the satisfied set, un-gating any steps waiting on them. */
69
+ satisfy(...triggerIds: string[]): void;
70
+ /** Replace the satisfied trigger set wholesale (e.g. re-derived from game state). */
71
+ setSatisfied(triggerIds: Iterable<string>): void;
72
+ /** Whether the given trigger id is currently satisfied. */
73
+ isTriggerSatisfied(triggerId: string): boolean;
74
+ /** Whether a step id has been seen. */
75
+ isSeen(id: string): boolean;
76
+ /** Clock time a step was first seen, or `undefined` if not yet seen. */
77
+ seenAt(id: string): number | undefined;
78
+ /** True once every step has been seen. */
79
+ isComplete(): boolean;
80
+ /** How many steps remain un-seen (whether or not currently gated). */
81
+ remaining(): number;
82
+ /** Observe changes (advance, dismiss, satisfy, restore). Returns an unsubscribe fn. */
83
+ subscribe(listener: () => void): () => void;
84
+ /** Serializable state for a save. */
85
+ snapshot(): CoachMarkSnapshot;
86
+ /** Restore from a {@link CoachMarkSnapshot}. */
87
+ restore(snapshot: CoachMarkSnapshot): void;
88
+ }
89
+ /**
90
+ * An onboarding coach-mark run: an ordered sequence of hint steps, each shown as
91
+ * a centered callout or anchored to a UI element, with a persisted "seen" set so
92
+ * completed onboarding stays done across sessions, and data-first condition
93
+ * gating so a step waits until its trigger id is satisfied. Advance/skip/dismiss
94
+ * drive it; `snapshot`/`restore` round-trip the seen set through a save. Purely a
95
+ * model — a React host renders `current()`.
96
+ *
97
+ * @capability coach-marks ordered, gated, persist-once onboarding coach-marks/tutorial callouts — anchored or centered steps with a seen set and Next/Skip
98
+ */
99
+ export declare function createCoachMarkSequence(options: CoachMarkSequenceOptions): CoachMarkSequence;