@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
@@ -1,3 +1,4 @@
1
+ import type { ModelAnimationConfig } from "../game/playableGame.js";
1
2
  import type { EntityPosition } from "./entityStore.js";
2
3
  export interface ObjectVisual {
3
4
  scale?: number | readonly [number, number, number];
@@ -11,12 +12,21 @@ export interface SceneObject {
11
12
  rotationY: number;
12
13
  parentSpace?: string;
13
14
  visual?: ObjectVisual;
15
+ /**
16
+ * Per-placement rig animation override applied on top of catalog resolution — the same shape as
17
+ * `ModelConfig.animation` (role→clip states, `"auto"`/`"none"`, speeds, fade, one-shot bindings).
18
+ * Authored in the editor as `marker.meta.animation` (#1274) and threaded here by
19
+ * `resolveAuthoredObjects` → `placeAuthoredObjects`; `undefined` keeps the catalog-resolved default.
20
+ */
21
+ animation?: ModelAnimationConfig | "auto" | "none";
14
22
  }
15
23
  export interface PlaceOptions {
16
24
  instanceId?: string;
17
25
  parentSpace?: string;
18
26
  rotation?: number;
19
27
  visual?: ObjectVisual;
28
+ /** Per-placement rig animation override stored on the placed {@link SceneObject}; same shape as `ModelConfig.animation`. Omit to keep the catalog-resolved default. */
29
+ animation?: ModelAnimationConfig | "auto" | "none";
20
30
  /** When `instanceId` is already placed: `"throw"` (default), `"replace"` (fresh placement over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
21
31
  onExisting?: "throw" | "replace" | "keep";
22
32
  }
@@ -1,5 +1,12 @@
1
1
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
2
  const CELL_SIZE = 1;
3
+ /**
4
+ * When an `inBox` query spans more than this many 1 m cells, walk the object list instead of the
5
+ * cell grid. Tall/wide colliders (city buildings) can inflate movement broadphase to hundreds of
6
+ * metres on each axis — at 1 m cells that is O(10⁶) empty `Map.get`s per frame and freezes `pose`.
7
+ * Linear scan over a few hundred objects is cheaper the moment the cell volume exceeds the scene.
8
+ */
9
+ const IN_BOX_CELL_VOLUME_LIMIT = 4096;
3
10
  function cellIndexOf(value) {
4
11
  return Math.floor(value / CELL_SIZE);
5
12
  }
@@ -9,6 +16,10 @@ function cellKey(cx, cy, cz) {
9
16
  function cellKeyOf(position) {
10
17
  return cellKey(cellIndexOf(position[0]), cellIndexOf(position[1]), cellIndexOf(position[2]));
11
18
  }
19
+ function objectInAabb(object, min, max) {
20
+ const [x, y, z] = object.position;
21
+ return x >= min[0] && x <= max[0] && y >= min[1] && y <= max[1] && z >= min[2] && z <= max[2];
22
+ }
12
23
  export function createObjectStore() {
13
24
  const store = createObservableKeyedStore();
14
25
  const cellIndex = new Map();
@@ -60,6 +71,7 @@ export function createObjectStore() {
60
71
  rotationY: options.rotation ?? 0,
61
72
  ...(options.parentSpace !== undefined ? { parentSpace: options.parentSpace } : {}),
62
73
  ...(options.visual !== undefined ? { visual: options.visual } : {}),
74
+ ...(options.animation !== undefined ? { animation: options.animation } : {}),
63
75
  });
64
76
  indexAdd(instanceId, position);
65
77
  return instanceId;
@@ -99,6 +111,7 @@ export function createObjectStore() {
99
111
  position: current.position,
100
112
  rotationY: current.rotationY,
101
113
  ...(current.parentSpace !== undefined ? { parentSpace: current.parentSpace } : {}),
114
+ ...(current.animation !== undefined ? { animation: current.animation } : {}),
102
115
  ...(visual !== undefined ? { visual } : {}),
103
116
  });
104
117
  return true;
@@ -145,13 +158,27 @@ export function createObjectStore() {
145
158
  return results;
146
159
  },
147
160
  inBox(min, max) {
148
- const hits = [];
149
161
  const minCx = cellIndexOf(min[0]);
150
162
  const maxCx = cellIndexOf(max[0]);
151
163
  const minCy = cellIndexOf(min[1]);
152
164
  const maxCy = cellIndexOf(max[1]);
153
165
  const minCz = cellIndexOf(min[2]);
154
166
  const maxCz = cellIndexOf(max[2]);
167
+ const cellsX = Math.max(0, maxCx - minCx + 1);
168
+ const cellsY = Math.max(0, maxCy - minCy + 1);
169
+ const cellsZ = Math.max(0, maxCz - minCz + 1);
170
+ const cellVolume = cellsX * cellsY * cellsZ;
171
+ // Huge queries (city-scale movement broadphase inflated by tall building colliders): scanning
172
+ // every empty 1 m cell is far more expensive than testing the few hundred placed objects.
173
+ if (cellVolume > IN_BOX_CELL_VOLUME_LIMIT) {
174
+ const hits = [];
175
+ for (const object of store.arraySnapshot()) {
176
+ if (objectInAabb(object, min, max))
177
+ hits.push(object);
178
+ }
179
+ return hits;
180
+ }
181
+ const hits = [];
155
182
  for (let cx = minCx; cx <= maxCx; cx += 1) {
156
183
  for (let cy = minCy; cy <= maxCy; cy += 1) {
157
184
  for (let cz = minCz; cz <= maxCz; cz += 1) {
@@ -162,12 +189,8 @@ export function createObjectStore() {
162
189
  const object = store.get(instanceId);
163
190
  if (object === undefined)
164
191
  continue;
165
- const [x, y, z] = object.position;
166
- if (x >= min[0] && x <= max[0] &&
167
- y >= min[1] && y <= max[1] &&
168
- z >= min[2] && z <= max[2]) {
192
+ if (objectInAabb(object, min, max))
169
193
  hits.push(object);
170
- }
171
194
  }
172
195
  }
173
196
  }
@@ -265,6 +265,24 @@ export interface SceneKindResolveContext {
265
265
  * inspector, `+ Add` menu, and `AuthoredScene` renderer lookup all read this registry.
266
266
  */
267
267
  export declare function registerSceneKind<TResolved>(definition: SceneKindDefinition<TResolved>): void;
268
+ /**
269
+ * Declare a game's own **click-to-place** marker kind in one call — the light path for logical
270
+ * markers a game reads off the document by `kind` (a stash, checkpoint, spawn point, bounty…), with
271
+ * none of a full studio's schema/resolver boilerplate. Registering it makes the kind a click-to-place
272
+ * tool in the editor's `+ Add` menu (grouped under `category`) and gives its markers an inspector for
273
+ * any `fields` you pass. Call at module load from a module the editor loads (a game's `editorLayers`
274
+ * import graph). Rendering is unaffected — the game still reads `kind` off `editorLayers.markers`.
275
+ */
276
+ export declare function definePlaceableMarkerKind(options: {
277
+ kind: string;
278
+ label: string;
279
+ /** `+ Add` menu section header this kind groups under. Defaults to `"Custom"`. */
280
+ category?: string;
281
+ /** Accent hex for the menu entry + inspector header. */
282
+ accent?: string;
283
+ /** Optional inspector fields written into each marker's `meta` (e.g. a payout value). */
284
+ fields?: readonly ParamField[];
285
+ }): string;
268
286
  /** The registered definition for a kind, or undefined if the kind is a plain (non-studio) kind. @internal */
269
287
  export declare function getSceneKind(kind: string): SceneKindDefinition | undefined;
270
288
  /** True when a kind has a registered studio definition (schema/inspector/renderer). @internal */
@@ -198,6 +198,25 @@ const registry = new Map();
198
198
  export function registerSceneKind(definition) {
199
199
  registry.set(definition.kind, definition);
200
200
  }
201
+ /**
202
+ * Declare a game's own **click-to-place** marker kind in one call — the light path for logical
203
+ * markers a game reads off the document by `kind` (a stash, checkpoint, spawn point, bounty…), with
204
+ * none of a full studio's schema/resolver boilerplate. Registering it makes the kind a click-to-place
205
+ * tool in the editor's `+ Add` menu (grouped under `category`) and gives its markers an inspector for
206
+ * any `fields` you pass. Call at module load from a module the editor loads (a game's `editorLayers`
207
+ * import graph). Rendering is unaffected — the game still reads `kind` off `editorLayers.markers`.
208
+ */
209
+ export function definePlaceableMarkerKind(options) {
210
+ registerSceneKind({
211
+ kind: options.kind,
212
+ target: "marker",
213
+ label: options.label,
214
+ addCategory: options.category ?? "Custom",
215
+ ...(options.accent === undefined ? {} : { accent: options.accent }),
216
+ schema: { fields: options.fields ?? [] },
217
+ });
218
+ return options.kind;
219
+ }
201
220
  /** The registered definition for a kind, or undefined if the kind is a plain (non-studio) kind. @internal */
202
221
  export function getSceneKind(kind) {
203
222
  return registry.get(kind);
@@ -0,0 +1,110 @@
1
+ /** One scheduled beat of a cutscene: a typed cue that fires when the playhead reaches `atMs`. */
2
+ export interface SequenceCue<Payload = unknown> {
3
+ /** Playhead time (ms from the start of the timeline) at which this cue fires. Must be `>= 0`. */
4
+ atMs: number;
5
+ /**
6
+ * Free-string tag naming what this cue is — `"camera"`, `"dialogue"`, `"fade"`, anything.
7
+ * The director never interprets it; the game switches on `kind` to act on the {@link SequenceCue.payload}.
8
+ */
9
+ kind: string;
10
+ /** Arbitrary serializable data the game reads when the cue fires. Opaque to the director. */
11
+ payload?: Payload;
12
+ /** Optional stable id, surfaced on emission and useful for logging/debugging. */
13
+ id?: string;
14
+ }
15
+ /** A cue plus the resolved firing context passed to {@link SequenceDirector.onCue} listeners. */
16
+ export interface EmittedCue<Payload = unknown> {
17
+ /** The cue that fired. */
18
+ cue: SequenceCue<Payload>;
19
+ /** Its index in the ordered cue list. */
20
+ index: number;
21
+ /** Playhead (ms) at the moment it was emitted — `>= cue.atMs`, possibly well past on a large jump. */
22
+ atPlayheadMs: number;
23
+ }
24
+ /** A read-only view of the director's playback state, returned by {@link SequenceDirector.state}. */
25
+ export interface SequenceState {
26
+ /** Current playhead position in ms from the start of the timeline. */
27
+ playheadMs: number;
28
+ /** Total timeline length — the largest cue `atMs` (0 when empty). */
29
+ durationMs: number;
30
+ /** Whether the clock is currently advancing the playhead. */
31
+ playing: boolean;
32
+ /** True once the playhead has reached the end (every cue has fired). */
33
+ done: boolean;
34
+ /** Fraction of the timeline elapsed, in `[0, 1]`; `1` when `durationMs` is 0. */
35
+ progress: number;
36
+ /** How many cues have fired so far (a prefix of the ordered list). */
37
+ firedCount: number;
38
+ /** Total number of cues. */
39
+ total: number;
40
+ }
41
+ /** Serializable snapshot — enough to resume a cutscene exactly where a save left it. */
42
+ export interface SequenceSnapshot {
43
+ /** Playhead position in ms. */
44
+ playheadMs: number;
45
+ /** Index of the next cue to fire; cues before it are considered already fired. */
46
+ nextIndex: number;
47
+ /** Whether playback was running when captured. */
48
+ playing: boolean;
49
+ }
50
+ /** Options for {@link createSequenceDirector}. */
51
+ export interface SequenceDirectorOptions<Payload = unknown> {
52
+ /** The timeline: cues in any order (sorted by `atMs` internally, stably). */
53
+ cues: readonly SequenceCue<Payload>[];
54
+ /** Injected clock (ms) that drives playback. Default `Date.now`; inject a fake for deterministic tests. */
55
+ now?: () => number;
56
+ /** Start playing immediately (default `false` — call {@link SequenceDirector.play}). */
57
+ autoplay?: boolean;
58
+ }
59
+ /** A cue listener, called once per cue as it fires. Returns nothing. */
60
+ export type CueListener<Payload = unknown> = (emitted: EmittedCue<Payload>) => void;
61
+ /**
62
+ * A data-driven cutscene: an ordered timeline of typed cues that fire on a single
63
+ * injected clock. See {@link createSequenceDirector}.
64
+ */
65
+ export interface SequenceDirector<Payload = unknown> {
66
+ /** The cues, sorted by `atMs`. */
67
+ readonly cues: readonly SequenceCue<Payload>[];
68
+ /** Start (or resume) playback from the current playhead. No-op if already playing or done. */
69
+ play(): void;
70
+ /** Freeze the playhead, capturing elapsed clock time up to now. No-op if not playing. */
71
+ pause(): void;
72
+ /**
73
+ * Jump the playhead to `ms` (clamped to `[0, durationMs]`). Moving forward fires every
74
+ * un-fired cue up to `ms` in order (once each, even across a large jump); moving backward
75
+ * re-arms cues now in the future without emitting them.
76
+ */
77
+ seek(ms: number): void;
78
+ /** Fast-forward to the end: emit every remaining cue in order, then stop and mark done. */
79
+ skip(): void;
80
+ /** Halt and rewind: pause, reset the playhead to 0, and re-arm every cue. */
81
+ stop(): void;
82
+ /**
83
+ * Advance the playhead by the elapsed injected-clock time since the last tick/resume and
84
+ * emit any cues now due. A host calls this once per frame while playing; a no-op when paused.
85
+ */
86
+ tick(): void;
87
+ /** Current playback state (playhead, duration, playing/done, progress). */
88
+ state(): SequenceState;
89
+ /** Register a cue listener; returns an unsubscribe fn. */
90
+ onCue(listener: CueListener<Payload>): () => void;
91
+ /** Observe state changes (play/pause/seek/skip/stop/tick/restore); returns an unsubscribe fn. */
92
+ subscribe(listener: () => void): () => void;
93
+ /** Serializable state for a save. */
94
+ snapshot(): SequenceSnapshot;
95
+ /** Restore from a {@link SequenceSnapshot}; does not re-emit already-fired cues. */
96
+ restore(snapshot: SequenceSnapshot): void;
97
+ }
98
+ /**
99
+ * A serializable cutscene / sequence director: an ordered timeline of typed cues
100
+ * (`{ atMs, kind, payload }`) advanced by one injected clock, firing each cue once
101
+ * and in order as its time passes — even across a large seek — with play/pause/
102
+ * seek/skip/stop controls. The director only *schedules and emits* cues; it never
103
+ * interprets what a `kind` means, so the same primitive drives camera moves,
104
+ * dialogue lines, fades, or any game event. `snapshot`/`restore` round-trip the
105
+ * playhead and which cues have fired. Deterministic (no wall clock beyond the
106
+ * injected `now`) and allocation-free on the tick path.
107
+ *
108
+ * @capability sequence-director data-driven cutscene timeline — fire typed camera/dialogue/event cues on one injected clock with play/pause/seek/skip and snapshot/restore
109
+ */
110
+ export declare function createSequenceDirector<Payload = unknown>(options: SequenceDirectorOptions<Payload>): SequenceDirector<Payload>;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * A serializable cutscene / sequence director: an ordered timeline of typed cues
3
+ * (`{ atMs, kind, payload }`) advanced by one injected clock, firing each cue once
4
+ * and in order as its time passes — even across a large seek — with play/pause/
5
+ * seek/skip/stop controls. The director only *schedules and emits* cues; it never
6
+ * interprets what a `kind` means, so the same primitive drives camera moves,
7
+ * dialogue lines, fades, or any game event. `snapshot`/`restore` round-trip the
8
+ * playhead and which cues have fired. Deterministic (no wall clock beyond the
9
+ * injected `now`) and allocation-free on the tick path.
10
+ *
11
+ * @capability sequence-director data-driven cutscene timeline — fire typed camera/dialogue/event cues on one injected clock with play/pause/seek/skip and snapshot/restore
12
+ */
13
+ export function createSequenceDirector(options) {
14
+ const cues = [...options.cues].sort((a, b) => a.atMs - b.atMs);
15
+ for (const cue of cues) {
16
+ if (!(cue.atMs >= 0) || !Number.isFinite(cue.atMs)) {
17
+ throw new Error(`sequence cue "${cue.kind}" needs a finite atMs >= 0, got ${cue.atMs}`);
18
+ }
19
+ }
20
+ const durationMs = cues.length > 0 ? cues[cues.length - 1].atMs : 0;
21
+ const now = options.now ?? Date.now;
22
+ const cueListeners = new Set();
23
+ const stateListeners = new Set();
24
+ let playheadMs = 0;
25
+ let nextIndex = 0;
26
+ let playing = false;
27
+ let lastNow = now();
28
+ function notify() {
29
+ for (const listener of stateListeners)
30
+ listener();
31
+ }
32
+ /** Emit every un-fired cue with `atMs <= playheadMs`, in order. Allocation-free. */
33
+ function fireDue() {
34
+ let fired = false;
35
+ while (nextIndex < cues.length && cues[nextIndex].atMs <= playheadMs) {
36
+ const cue = cues[nextIndex];
37
+ const index = nextIndex;
38
+ nextIndex += 1;
39
+ fired = true;
40
+ for (const listener of cueListeners)
41
+ listener({ cue, index, atPlayheadMs: playheadMs });
42
+ }
43
+ return fired;
44
+ }
45
+ /** Fold elapsed injected-clock time into the playhead and fire due cues. */
46
+ function advanceToNow() {
47
+ const t = now();
48
+ const delta = t - lastNow;
49
+ lastNow = t;
50
+ if (delta <= 0)
51
+ return false;
52
+ playheadMs = Math.min(durationMs, playheadMs + delta);
53
+ const fired = fireDue();
54
+ const stopped = playheadMs >= durationMs && playing;
55
+ if (playheadMs >= durationMs)
56
+ playing = false;
57
+ return fired || stopped;
58
+ }
59
+ function isDone() {
60
+ return nextIndex >= cues.length && playheadMs >= durationMs;
61
+ }
62
+ return {
63
+ cues,
64
+ play() {
65
+ if (playing || isDone())
66
+ return;
67
+ playing = true;
68
+ lastNow = now();
69
+ notify();
70
+ },
71
+ pause() {
72
+ if (!playing)
73
+ return;
74
+ advanceToNow();
75
+ playing = false;
76
+ notify();
77
+ },
78
+ seek(ms) {
79
+ const target = Math.max(0, Math.min(durationMs, ms));
80
+ if (target < playheadMs) {
81
+ // Backward scrub: re-arm cues now in the future, emit nothing.
82
+ playheadMs = target;
83
+ let i = 0;
84
+ while (i < cues.length && cues[i].atMs <= target)
85
+ i += 1;
86
+ nextIndex = i;
87
+ }
88
+ else {
89
+ playheadMs = target;
90
+ fireDue();
91
+ }
92
+ lastNow = now();
93
+ if (playheadMs >= durationMs && nextIndex >= cues.length)
94
+ playing = false;
95
+ notify();
96
+ },
97
+ skip() {
98
+ playheadMs = durationMs;
99
+ fireDue();
100
+ playing = false;
101
+ notify();
102
+ },
103
+ stop() {
104
+ playheadMs = 0;
105
+ nextIndex = 0;
106
+ playing = false;
107
+ lastNow = now();
108
+ notify();
109
+ },
110
+ tick() {
111
+ if (!playing)
112
+ return;
113
+ if (advanceToNow())
114
+ notify();
115
+ },
116
+ state() {
117
+ return {
118
+ playheadMs,
119
+ durationMs,
120
+ playing,
121
+ done: isDone(),
122
+ progress: durationMs > 0 ? Math.max(0, Math.min(1, playheadMs / durationMs)) : 1,
123
+ firedCount: nextIndex,
124
+ total: cues.length,
125
+ };
126
+ },
127
+ onCue(listener) {
128
+ cueListeners.add(listener);
129
+ return () => cueListeners.delete(listener);
130
+ },
131
+ subscribe(listener) {
132
+ stateListeners.add(listener);
133
+ return () => stateListeners.delete(listener);
134
+ },
135
+ snapshot() {
136
+ return { playheadMs, nextIndex, playing };
137
+ },
138
+ restore(snapshot) {
139
+ playheadMs = Math.max(0, Math.min(durationMs, snapshot.playheadMs));
140
+ nextIndex = Math.max(0, Math.min(cues.length, snapshot.nextIndex));
141
+ playing = snapshot.playing;
142
+ lastNow = now();
143
+ notify();
144
+ },
145
+ };
146
+ }
@@ -1,14 +1,15 @@
1
1
  import { shuffleWithRng } from "../cards/cardPile.js";
2
+ import { resolveRng } from "../random/resolveRng.js";
2
3
  /**
3
4
  * Assign hidden or team roles across players by ratio — social-deduction and team-shuffle setups.
4
5
  *
5
6
  * @capability role-assign assign hidden or team roles to players by ratio
6
7
  * @internal
7
8
  */
8
- export function assignRoles(userIds, roles, rng = Math.random) {
9
+ export function assignRoles(userIds, roles, rng) {
9
10
  if (roles.length === 0)
10
11
  throw new Error("assignRoles needs at least one role");
11
- const shuffled = shuffleWithRng(userIds, rng);
12
+ const shuffled = shuffleWithRng(userIds, resolveRng(rng, "assignRoles"));
12
13
  const assignment = {};
13
14
  let index = 0;
14
15
  const countRoles = roles.filter((role) => role.count !== undefined);
@@ -68,6 +68,7 @@ export declare const SETTING_IDS: {
68
68
  readonly graphicsShadows: "graphics.shadows";
69
69
  readonly graphicsUiScale: "graphics.uiScale";
70
70
  readonly touchStyle: "controls.touchStyle";
71
+ readonly touchJoystick: "controls.touchJoystick";
71
72
  };
72
73
  export type GraphicsQuality = "low" | "medium" | "high";
73
74
  export declare const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[];
@@ -12,6 +12,7 @@ export const SETTING_IDS = {
12
12
  graphicsShadows: "graphics.shadows",
13
13
  graphicsUiScale: "graphics.uiScale",
14
14
  touchStyle: "controls.touchStyle",
15
+ touchJoystick: "controls.touchJoystick",
15
16
  };
16
17
  export const GRAPHICS_QUALITY_OPTIONS = [
17
18
  { value: "low", label: "Low" },
@@ -1,12 +1,14 @@
1
+ import { resolveRng } from "../random/resolveRng.js";
1
2
  /**
2
3
  * Resolve a tabletop-style pass/fail roll against a target number with modifiers and crit/fumble bands.
3
4
  *
4
5
  * @capability dice-check resolve a pass/fail roll against a target number with modifiers and crits
5
6
  */
6
- export function rollCheck(input, rng = Math.random) {
7
+ export function rollCheck(input, rng) {
8
+ const random = resolveRng(rng, "rollCheck");
7
9
  const sides = input.diceSides ?? 20;
8
10
  const advantage = input.advantage ?? "normal";
9
- const rollDie = () => Math.floor(rng() * sides) + 1;
11
+ const rollDie = () => Math.floor(random() * sides) + 1;
10
12
  const rolls = advantage === "normal" ? [rollDie()] : [rollDie(), rollDie()];
11
13
  const roll = advantage === "advantage"
12
14
  ? Math.max(...rolls)
@@ -0,0 +1,114 @@
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
+ /** One day-night keyframe: at a normalized day position, the phase label and target colors the game styles. */
12
+ export interface DayNightKeyframe {
13
+ /** Normalized day position in [0,1) where this keyframe sits; the cycle wraps past 1 back to 0. */
14
+ at: number;
15
+ /** Free-form phase label (e.g. `"dawn"`, `"day"`, `"dusk"`, `"night"`, or anything). The model never interprets it. */
16
+ phase: string;
17
+ /** Primary tint/grade color at this keyframe as `"#rrggbb"`. */
18
+ color: string;
19
+ /** Optional secondary color (e.g. sky zenith or key-light color). Falls back to `color` when sampled. */
20
+ lightColor?: string;
21
+ /** Optional light level in [0,1] a presenter maps to intensity. Falls back to `1` when sampled. */
22
+ intensity?: number;
23
+ }
24
+ /** The interpolated day-night look at a moment: day fraction, active phase, and blended colors. */
25
+ export interface DayNightSample {
26
+ /** Current normalized day fraction in [0,1). */
27
+ dayFraction: number;
28
+ /** Phase label of the keyframe segment currently active (the free string the game styles). */
29
+ phase: string;
30
+ /** Blended primary tint/grade color as `"#rrggbb"`. */
31
+ color: string;
32
+ /** Blended secondary color as `"#rrggbb"` (equals `color` when no keyframe set `lightColor`). */
33
+ lightColor: string;
34
+ /** Blended light level in [0,1]. */
35
+ intensity: number;
36
+ }
37
+ /** Serializable day-night position — accumulated clock offset, pause state, and speed — for save/load. */
38
+ export interface DayNightSnapshot {
39
+ /** Accumulated game-time in ms since the cycle's zero, before wrapping into a day. */
40
+ elapsedMs: number;
41
+ /** Whether the clock is paused. */
42
+ paused: boolean;
43
+ /** Clock rate multiplier in effect. */
44
+ speed: number;
45
+ }
46
+ /** Options for {@link createDayNightCycle}. */
47
+ export interface DayNightCycleOptions {
48
+ /** At least one keyframe; sorted and wrapped into [0,1) internally. Colors and phases are free-form. */
49
+ keyframes: readonly DayNightKeyframe[];
50
+ /** Real milliseconds for one full day at speed 1. Default `120000` (two minutes). */
51
+ dayLengthMs?: number;
52
+ /** Normalized day fraction to start at, in [0,1). Default `0`. */
53
+ start?: number;
54
+ /** Clock rate multiplier (`2` runs twice as fast). Default `1`. */
55
+ speed?: number;
56
+ /** Start with the clock paused. Default `false`. */
57
+ startPaused?: boolean;
58
+ /** Injected clock in ms so advancement stays deterministic under test. Default `Date.now`. */
59
+ now?: () => number;
60
+ }
61
+ /** A running, observable, serializable day-night cycle. A presenter renders `sample()`. */
62
+ export interface DayNightCycle {
63
+ /** The configured keyframes, sorted by `at` and wrapped into [0,1). */
64
+ readonly keyframes: readonly DayNightKeyframe[];
65
+ /** Real milliseconds for one full day at speed 1. */
66
+ readonly dayLengthMs: number;
67
+ /** Current normalized day fraction in [0,1), advanced from the injected clock. */
68
+ dayFraction(): number;
69
+ /** The free-form phase label active at the current day fraction. */
70
+ phase(): string;
71
+ /** The interpolated look at the current day fraction. */
72
+ sample(): DayNightSample;
73
+ /** The interpolated look at an explicit day fraction, without touching the clock. */
74
+ sampleAt(fraction: number): DayNightSample;
75
+ /**
76
+ * Calendar adapter (`{ dayFraction }`) so the cycle drops straight into any
77
+ * `{ calendar(): { dayFraction: number } }` sky/daylight seam that already
78
+ * consumes a day fraction — no glue in the game.
79
+ */
80
+ calendar(): {
81
+ dayFraction: number;
82
+ };
83
+ /** Jump the clock to a normalized day fraction (wrapped into [0,1)). */
84
+ setDayFraction(fraction: number): void;
85
+ /** Whether the clock is paused. */
86
+ isPaused(): boolean;
87
+ /** Pause advancement. */
88
+ pause(): void;
89
+ /** Resume advancement. */
90
+ play(): void;
91
+ /** Toggle pause/resume. */
92
+ toggle(): void;
93
+ /** Current clock rate multiplier. */
94
+ speed(): number;
95
+ /** Set the clock rate multiplier; `0` or negative pauses. */
96
+ setSpeed(multiplier: number): void;
97
+ /** Observe control changes (pause/play/speed/setDayFraction/restore). Returns an unsubscribe fn. Continuous advance is read via `sample()`, not events. */
98
+ subscribe(listener: () => void): () => void;
99
+ /** Serializable position for a save. */
100
+ snapshot(): DayNightSnapshot;
101
+ /** Restore position from a {@link DayNightSnapshot}. */
102
+ restore(snapshot: DayNightSnapshot): void;
103
+ }
104
+ /**
105
+ * Creates a turnkey day-night cycle: a serializable model that advances a normalized
106
+ * day fraction on an injected clock and blends per-keyframe phase labels and tint/light
107
+ * colors. Wire this one model, then drive an existing sky/daylight seam from `sample()`
108
+ * (or drop it straight into a `{ calendar(): { dayFraction } }` seam via `calendar()`)
109
+ * to get a moving day-night cycle with color grading — no hand-rolled clock or lerp.
110
+ * `phase` labels and colors are free-form; the model never interprets their meaning.
111
+ *
112
+ * @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
113
+ */
114
+ export declare function createDayNightCycle(options: DayNightCycleOptions): DayNightCycle;