@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,149 @@
1
+ /** How a transient effect's intensity curves from peak to zero over its life. */
2
+ export type ScreenEffectEasing = "linear" | "easeIn" | "easeOut" | "easeInOut";
3
+ /**
4
+ * The screen region an effect tints. `"full"` grades the whole frame (a flash);
5
+ * `"vignette"` grades only the edges, leaving the center clear (a directional or
6
+ * ambient border tint). Purely a shape hint for the renderer — no genre meaning.
7
+ */
8
+ export type ScreenEffectShape = "full" | "vignette";
9
+ /**
10
+ * One screen-feedback effect, fully data-first so a whole controller serializes.
11
+ * `kind` is a free label the game assigns and styles ("damage", "heal", "poison",
12
+ * "boost", …); the model never interprets it. Everything visible is a parameter:
13
+ * color, peak intensity, region shape, easing, and either a transient duration or
14
+ * a sustained (optionally oscillating) hold.
15
+ */
16
+ export interface ScreenEffectSpec {
17
+ /** Free-string label the game owns; the model treats it as opaque. */
18
+ kind: string;
19
+ /** CSS color the region is tinted with (e.g. `"#ff2b2b"`, `"rgb(80,200,120)"`). */
20
+ color: string;
21
+ /** Peak opacity `0..1` at full strength. Default `1`. */
22
+ intensity?: number;
23
+ /** Which region is tinted. Default `"full"`. */
24
+ shape?: ScreenEffectShape;
25
+ /** Intensity curve for a transient effect. Default `"easeOut"`. */
26
+ easing?: ScreenEffectEasing;
27
+ /**
28
+ * Lifetime in ms for a transient effect (ignored when `sustained`). The effect
29
+ * peaks immediately and eases to zero over this window. Default `400`.
30
+ */
31
+ durationMs?: number;
32
+ /**
33
+ * When true the effect holds until {@link ScreenEffectsController.clear} instead
34
+ * of expiring — a low-health tint, a status aura. Default `false`.
35
+ */
36
+ sustained?: boolean;
37
+ /**
38
+ * Oscillation frequency in Hz for a sustained effect (a pulse). `0` holds steady
39
+ * at `intensity`; `>0` breathes between `minIntensity` and `intensity`. Ignored
40
+ * for transient effects. Default `0`.
41
+ */
42
+ pulseHz?: number;
43
+ /** Trough opacity `0..1` a pulsing sustained effect dips to. Default `0`. */
44
+ minIntensity?: number;
45
+ }
46
+ /**
47
+ * A live effect in the current composite: its label, color, region, and the
48
+ * eased opacity `0..1` to draw *right now*. These objects are pooled and reused
49
+ * across {@link ScreenEffectsController.composite} calls — read them, don't retain.
50
+ */
51
+ export interface ScreenEffect {
52
+ /** Stable id assigned at trigger time (use it to {@link ScreenEffectsController.clear}). */
53
+ id: number;
54
+ /** The game-owned label from the spec. */
55
+ kind: string;
56
+ /** CSS color to tint with. */
57
+ color: string;
58
+ /** Region to tint. */
59
+ shape: ScreenEffectShape;
60
+ /** Current eased opacity `0..1`. */
61
+ intensity: number;
62
+ }
63
+ /** A persisted effect record (all spec fields resolved plus its start time and id). */
64
+ export interface StoredScreenEffect {
65
+ id: number;
66
+ kind: string;
67
+ color: string;
68
+ shape: ScreenEffectShape;
69
+ easing: ScreenEffectEasing;
70
+ intensity: number;
71
+ durationMs: number;
72
+ sustained: boolean;
73
+ pulseHz: number;
74
+ minIntensity: number;
75
+ /** Clock time (ms) the effect started, for elapsed-time evaluation. */
76
+ startAt: number;
77
+ }
78
+ /** Serializable state of every active effect, for save/restore and replay. */
79
+ export interface ScreenEffectsSnapshot {
80
+ /** Wall-clock (ms) the snapshot was taken, so restore re-anchors elapsed time. */
81
+ now: number;
82
+ /** Next id to hand out. */
83
+ nextId: number;
84
+ /** Every active effect, transient and sustained. */
85
+ effects: readonly StoredScreenEffect[];
86
+ }
87
+ /** A live, clock-driven screen-feedback controller. */
88
+ export interface ScreenEffectsController {
89
+ /**
90
+ * Trigger a transient full-screen flash. Convenience for {@link trigger} with
91
+ * `shape: "full"`. Returns the new effect id.
92
+ */
93
+ flash(kind: string, spec: Omit<ScreenEffectSpec, "kind" | "shape" | "sustained">): number;
94
+ /**
95
+ * Trigger a transient edge vignette. Convenience for {@link trigger} with
96
+ * `shape: "vignette"`. Returns the new effect id.
97
+ */
98
+ vignette(kind: string, spec: Omit<ScreenEffectSpec, "kind" | "shape" | "sustained">): number;
99
+ /**
100
+ * Start a sustained pulse that holds until {@link clear}. Convenience for a
101
+ * sustained effect; pass `pulseHz > 0` to breathe. Returns the new effect id.
102
+ */
103
+ pulse(kind: string, spec: Omit<ScreenEffectSpec, "kind" | "sustained">): number;
104
+ /** Trigger any effect from a full spec. Returns the new effect id. */
105
+ trigger(spec: ScreenEffectSpec): number;
106
+ /** Remove one effect by id (typically a sustained one). No-op if unknown. */
107
+ clear(id: number): void;
108
+ /** Remove every active effect. */
109
+ clearAll(): void;
110
+ /**
111
+ * Re-evaluate every effect against the clock: recompute eased intensities, reap
112
+ * expired transients, and notify subscribers. Allocation-free on the hot path.
113
+ * Pass an explicit `nowMs` to drive deterministically, else the injected clock.
114
+ */
115
+ advance(nowMs?: number): void;
116
+ /**
117
+ * The current composite: active effects with their eased `intensity`. The array
118
+ * and its entries are pooled and overwritten on the next call — do not retain.
119
+ */
120
+ composite(): readonly ScreenEffect[];
121
+ /** Number of active effects. */
122
+ active(): number;
123
+ /** Observe changes (trigger, clear, advance). Returns an unsubscribe fn. */
124
+ subscribe(listener: () => void): () => void;
125
+ /** Serializable state for a save. */
126
+ snapshot(): ScreenEffectsSnapshot;
127
+ /** Restore from a {@link ScreenEffectsSnapshot}. */
128
+ restore(snapshot: ScreenEffectsSnapshot): void;
129
+ }
130
+ /** Options for {@link createScreenEffects}. */
131
+ export interface ScreenEffectsOptions {
132
+ /** Injected clock (ms). Default `Date.now`. */
133
+ now?: () => number;
134
+ }
135
+ /**
136
+ * A serializable screen-feedback controller: a game triggers transient flashes and
137
+ * edge vignettes (a red damage hit, a green heal flash) or sustained, optionally
138
+ * oscillating tints (a low-health pulse), and reads back a composite of the effects
139
+ * to draw right now with their eased opacities. It is clock-driven — call
140
+ * `advance()` each frame against an injected `now` — and allocation-aware: the
141
+ * composite array and its entries are pooled and reused, so steady-state ticking
142
+ * never allocates. Nothing here is genre-specific: `kind` is a free label the game
143
+ * owns and styles, and vignette / flash / pulse are just parameterizations of the
144
+ * same data (region shape, decay easing, sustained oscillation). A shell overlay
145
+ * subscribes and renders; `snapshot`/`restore` round-trips through a save.
146
+ *
147
+ * @capability screen-effects serializable, pooled screen-feedback controller — transient flash/vignette and sustained low-health-style pulses with eased opacities, free-string kinds the game styles, and snapshot/restore
148
+ */
149
+ export declare function createScreenEffects(options?: ScreenEffectsOptions): ScreenEffectsController;
@@ -0,0 +1,191 @@
1
+ function clamp01(v) {
2
+ return v < 0 ? 0 : v > 1 ? 1 : v;
3
+ }
4
+ /** Eased progress `0..1` for a transient effect's decay curve. */
5
+ function easeProgress(t, easing) {
6
+ const p = clamp01(t);
7
+ switch (easing) {
8
+ case "linear":
9
+ return p;
10
+ case "easeIn":
11
+ return p * p;
12
+ case "easeInOut":
13
+ return p < 0.5 ? 2 * p * p : 1 - (-2 * p + 2) ** 2 / 2;
14
+ case "easeOut":
15
+ default:
16
+ return 1 - (1 - p) ** 2;
17
+ }
18
+ }
19
+ const TWO_PI = Math.PI * 2;
20
+ /**
21
+ * A serializable screen-feedback controller: a game triggers transient flashes and
22
+ * edge vignettes (a red damage hit, a green heal flash) or sustained, optionally
23
+ * oscillating tints (a low-health pulse), and reads back a composite of the effects
24
+ * to draw right now with their eased opacities. It is clock-driven — call
25
+ * `advance()` each frame against an injected `now` — and allocation-aware: the
26
+ * composite array and its entries are pooled and reused, so steady-state ticking
27
+ * never allocates. Nothing here is genre-specific: `kind` is a free label the game
28
+ * owns and styles, and vignette / flash / pulse are just parameterizations of the
29
+ * same data (region shape, decay easing, sustained oscillation). A shell overlay
30
+ * subscribes and renders; `snapshot`/`restore` round-trips through a save.
31
+ *
32
+ * @capability screen-effects serializable, pooled screen-feedback controller — transient flash/vignette and sustained low-health-style pulses with eased opacities, free-string kinds the game styles, and snapshot/restore
33
+ */
34
+ export function createScreenEffects(options = {}) {
35
+ const now = options.now ?? Date.now;
36
+ const effects = [];
37
+ const listeners = new Set();
38
+ // Pooled composite output — reused across composite() calls, never per-tick allocated.
39
+ const viewPool = [];
40
+ const viewOut = [];
41
+ let nextId = 1;
42
+ function notify() {
43
+ for (const listener of listeners)
44
+ listener();
45
+ }
46
+ /** Eased current opacity for one effect at `t` ms clock time. */
47
+ function evaluate(effect, t) {
48
+ const elapsed = t - effect.startAt;
49
+ if (effect.sustained) {
50
+ if (effect.pulseHz <= 0)
51
+ return effect.intensity;
52
+ const phase = (elapsed / 1000) * effect.pulseHz * TWO_PI;
53
+ const osc = (Math.sin(phase) + 1) / 2; // 0..1
54
+ return effect.minIntensity + (effect.intensity - effect.minIntensity) * osc;
55
+ }
56
+ if (effect.durationMs <= 0)
57
+ return 0;
58
+ const progress = elapsed / effect.durationMs;
59
+ if (progress >= 1)
60
+ return 0;
61
+ return effect.intensity * (1 - easeProgress(progress, effect.easing));
62
+ }
63
+ function refresh(t) {
64
+ // Iterate backwards: reap expired transients with swap-remove, no allocation.
65
+ for (let i = effects.length - 1; i >= 0; i--) {
66
+ const effect = effects[i];
67
+ if (!effect.sustained && t - effect.startAt >= effect.durationMs) {
68
+ const last = effects.length - 1;
69
+ if (i !== last)
70
+ effects[i] = effects[last];
71
+ effects.pop();
72
+ continue;
73
+ }
74
+ effect.current = evaluate(effect, t);
75
+ }
76
+ }
77
+ function add(spec) {
78
+ const id = nextId++;
79
+ const t = now();
80
+ const effect = {
81
+ id,
82
+ kind: spec.kind,
83
+ color: spec.color,
84
+ shape: spec.shape ?? "full",
85
+ easing: spec.easing ?? "easeOut",
86
+ intensity: clamp01(spec.intensity ?? 1),
87
+ durationMs: spec.durationMs ?? 400,
88
+ sustained: spec.sustained ?? false,
89
+ pulseHz: Math.max(0, spec.pulseHz ?? 0),
90
+ minIntensity: clamp01(spec.minIntensity ?? 0),
91
+ startAt: t,
92
+ current: 0,
93
+ };
94
+ effect.current = evaluate(effect, t);
95
+ effects.push(effect);
96
+ notify();
97
+ return id;
98
+ }
99
+ return {
100
+ flash(kind, spec) {
101
+ return add({ ...spec, kind, shape: "full", sustained: false });
102
+ },
103
+ vignette(kind, spec) {
104
+ return add({ ...spec, kind, shape: "vignette", sustained: false });
105
+ },
106
+ pulse(kind, spec) {
107
+ return add({ shape: "vignette", ...spec, kind, sustained: true });
108
+ },
109
+ trigger(spec) {
110
+ return add(spec);
111
+ },
112
+ clear(id) {
113
+ const index = effects.findIndex((effect) => effect.id === id);
114
+ if (index === -1)
115
+ return;
116
+ const last = effects.length - 1;
117
+ if (index !== last)
118
+ effects[index] = effects[last];
119
+ effects.pop();
120
+ notify();
121
+ },
122
+ clearAll() {
123
+ if (effects.length === 0)
124
+ return;
125
+ effects.length = 0;
126
+ notify();
127
+ },
128
+ advance(nowMs) {
129
+ refresh(nowMs ?? now());
130
+ notify();
131
+ },
132
+ composite() {
133
+ viewOut.length = 0;
134
+ for (let i = 0; i < effects.length; i++) {
135
+ const effect = effects[i];
136
+ if (effect.current <= 0)
137
+ continue;
138
+ let view = viewPool[viewOut.length];
139
+ if (view === undefined) {
140
+ view = { id: 0, kind: "", color: "", shape: "full", intensity: 0 };
141
+ viewPool[viewOut.length] = view;
142
+ }
143
+ view.id = effect.id;
144
+ view.kind = effect.kind;
145
+ view.color = effect.color;
146
+ view.shape = effect.shape;
147
+ view.intensity = effect.current;
148
+ viewOut.push(view);
149
+ }
150
+ return viewOut;
151
+ },
152
+ active() {
153
+ return effects.length;
154
+ },
155
+ subscribe(listener) {
156
+ listeners.add(listener);
157
+ return () => listeners.delete(listener);
158
+ },
159
+ snapshot() {
160
+ return {
161
+ now: now(),
162
+ nextId,
163
+ effects: effects.map((effect) => ({
164
+ id: effect.id,
165
+ kind: effect.kind,
166
+ color: effect.color,
167
+ shape: effect.shape,
168
+ easing: effect.easing,
169
+ intensity: effect.intensity,
170
+ durationMs: effect.durationMs,
171
+ sustained: effect.sustained,
172
+ pulseHz: effect.pulseHz,
173
+ minIntensity: effect.minIntensity,
174
+ startAt: effect.startAt,
175
+ })),
176
+ };
177
+ },
178
+ restore(snapshot) {
179
+ effects.length = 0;
180
+ nextId = snapshot.nextId;
181
+ const t = now();
182
+ const drift = t - snapshot.now;
183
+ for (const stored of snapshot.effects) {
184
+ const effect = { ...stored, startAt: stored.startAt + drift, current: 0 };
185
+ effect.current = evaluate(effect, t);
186
+ effects.push(effect);
187
+ }
188
+ notify();
189
+ },
190
+ };
191
+ }
@@ -1,4 +1,7 @@
1
+ import type { ModelAnimationConfig } from "../game/playableGame.js";
1
2
  import type { SceneMarkerLike } from "./sceneShapes.js";
3
+ /** A placement's authored rig animation override — the stored `marker.meta.animation` contract (#1274). */
4
+ export type AuthoredAnimation = ModelAnimationConfig | "auto" | "none";
2
5
  /**
3
6
  * One authored catalog prop resolved from an editor marker — grounded at `x`/`z` with yaw, ready
4
7
  * for `ctx.scene.object.place` or {@link placeAuthoredObjects}.
@@ -11,6 +14,12 @@ export interface AuthoredObject {
11
14
  instanceId: string;
12
15
  /** Extra lift in meters from the marker (`catalogId` field / meta), applied on top of terrain height. */
13
16
  verticalOffset: number;
17
+ /**
18
+ * Per-placement rig animation override authored at `marker.meta.animation` (#1274), threaded onto the
19
+ * placed object's `ModelConfig.animation` so the override applies in game. `undefined` when the marker
20
+ * carries no override — behavior is exactly the catalog-resolved default.
21
+ */
22
+ animation?: AuthoredAnimation;
14
23
  }
15
24
  /** Minimal marker shape {@link resolveAuthoredObjects} reads; any `EditorMarker` satisfies it. */
16
25
  export interface AuthoredObjectMarkerLike extends SceneMarkerLike {
@@ -26,6 +35,7 @@ export interface AuthoredObjectPlaceTarget {
26
35
  place(catalogId: string, x: number, y: number, z: number, options?: {
27
36
  instanceId?: string;
28
37
  rotation?: number;
38
+ animation?: AuthoredAnimation;
29
39
  onExisting?: "throw" | "replace" | "keep";
30
40
  }): string;
31
41
  }
@@ -42,6 +52,14 @@ export interface PlaceAuthoredObjectsOptions {
42
52
  * @capability authored-objects place catalog mesh props from an editor document
43
53
  */
44
54
  export declare function markerCatalogId(marker: AuthoredObjectMarkerLike): string | null;
55
+ /**
56
+ * The authored per-placement rig animation override stored at `marker.meta.animation` (#1274), or
57
+ * `undefined` when absent/malformed. `"auto"` / `"none"` pass through as the string modes; any other
58
+ * object is a `ModelAnimationConfig` the shell applies verbatim (validated at author time in the
59
+ * editor). Runtime trusts the persisted document here exactly like it trusts position and rotation.
60
+ * @capability authored-objects per-placement animation override from marker.meta.animation
61
+ */
62
+ export declare function markerAnimation(marker: AuthoredObjectMarkerLike): AuthoredAnimation | undefined;
45
63
  /**
46
64
  * Marker kinds that are spawnable entities (materialized by `authoredEntitySpawns`), not static
47
65
  * catalog props. {@link resolveAuthoredObjects} skips them by default so a placed `mob`/`boss` is
@@ -16,6 +16,24 @@ export function markerCatalogId(marker) {
16
16
  return marker.catalogId;
17
17
  return metaString(marker.meta, "catalogId");
18
18
  }
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ /**
23
+ * The authored per-placement rig animation override stored at `marker.meta.animation` (#1274), or
24
+ * `undefined` when absent/malformed. `"auto"` / `"none"` pass through as the string modes; any other
25
+ * object is a `ModelAnimationConfig` the shell applies verbatim (validated at author time in the
26
+ * editor). Runtime trusts the persisted document here exactly like it trusts position and rotation.
27
+ * @capability authored-objects per-placement animation override from marker.meta.animation
28
+ */
29
+ export function markerAnimation(marker) {
30
+ const value = marker.meta?.["animation"];
31
+ if (value === "auto" || value === "none")
32
+ return value;
33
+ if (isRecord(value))
34
+ return value;
35
+ return undefined;
36
+ }
19
37
  function markerVerticalOffset(marker) {
20
38
  const fromMeta = metaNumber(marker.meta, "verticalOffset") ??
21
39
  metaNumber(marker.meta, "offsetY") ??
@@ -45,6 +63,7 @@ export function resolveAuthoredObjects(document, options = {}) {
45
63
  const catalogId = markerCatalogId(marker);
46
64
  if (catalogId === null)
47
65
  continue;
66
+ const animation = markerAnimation(marker);
48
67
  out.push({
49
68
  catalogId,
50
69
  x: marker.position.x,
@@ -52,6 +71,7 @@ export function resolveAuthoredObjects(document, options = {}) {
52
71
  rotationY: marker.rotationY ?? 0,
53
72
  instanceId: marker.id,
54
73
  verticalOffset: markerVerticalOffset(marker),
74
+ ...(animation === undefined ? {} : { animation }),
55
75
  });
56
76
  }
57
77
  return out;
@@ -70,6 +90,7 @@ export function placeAuthoredObjects(store, objects, sampleHeight, options = {})
70
90
  ids.push(store.place(object.catalogId, object.x, y, object.z, {
71
91
  instanceId: object.instanceId,
72
92
  rotation: object.rotationY,
93
+ ...(object.animation === undefined ? {} : { animation: object.animation }),
73
94
  ...(onExisting === undefined ? {} : { onExisting }),
74
95
  }));
75
96
  }
@@ -1,3 +1,4 @@
1
+ import { readSpawnOverride } from "./spawnOverride.js";
1
2
  /** Marker kind {@link authoredSpawnPosition} resolves by default. */
2
3
  export const PLAYER_SPAWN_KIND = "player_spawn";
3
4
  /**
@@ -8,6 +9,14 @@ export const PLAYER_SPAWN_KIND = "player_spawn";
8
9
  export function markersOfKind(document, kind) {
9
10
  return document.markers.filter((marker) => marker.kind === kind);
10
11
  }
12
+ /**
13
+ * True when a query targets the *default player spawn* (default kind, no explicit id) — the only
14
+ * resolution an installed {@link readSpawnOverride} may replace. Marker queries for other kinds or
15
+ * a named id always read the authored document, so a capture-time override cannot perturb them.
16
+ */
17
+ function targetsDefaultPlayerSpawn(options) {
18
+ return options.id === undefined && (options.kind === undefined || options.kind === PLAYER_SPAWN_KIND);
19
+ }
11
20
  /**
12
21
  * Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the
13
22
  * document has none. Reads the first `player_spawn` marker by default, so dragging the marker in
@@ -15,6 +24,11 @@ export function markersOfKind(document, kind) {
15
24
  * @capability authored-spawn read spawn points and markers from the editor document
16
25
  */
17
26
  export function authoredSpawnPosition(document, options = {}) {
27
+ if (targetsDefaultPlayerSpawn(options)) {
28
+ const override = readSpawnOverride();
29
+ if (override !== null)
30
+ return [override.x, override.y, override.z];
31
+ }
18
32
  const kind = options.kind ?? PLAYER_SPAWN_KIND;
19
33
  const marker = options.id === undefined
20
34
  ? document.markers.find((entry) => entry.kind === kind)
@@ -29,6 +43,11 @@ export function authoredSpawnPosition(document, options = {}) {
29
43
  * @capability authored-spawn read spawn points and markers from the editor document
30
44
  */
31
45
  export function authoredSpawnRotation(document, options = {}) {
46
+ if (targetsDefaultPlayerSpawn(options)) {
47
+ const override = readSpawnOverride();
48
+ if (override?.rotationY !== undefined)
49
+ return override.rotationY;
50
+ }
32
51
  const kind = options.kind ?? PLAYER_SPAWN_KIND;
33
52
  const marker = options.id === undefined
34
53
  ? document.markers.find((entry) => entry.kind === kind)
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @capability world-environment lots aligned to and facing road frontage, with a setback sidewalk strip
13
13
  */
14
- import type { Vec2 } from "./cityGeometry.js";
14
+ import { type Vec2 } from "./cityGeometry.js";
15
15
  import type { WorldBounds } from "./features.js";
16
16
  /** One road the frontage placer lines with buildings: a centerline polyline and its full width. */
17
17
  export interface RoadFrontage {
@@ -27,24 +27,50 @@ export interface LotArea {
27
27
  /** Half-extents along world x and z. */
28
28
  halfExtents: Vec2;
29
29
  }
30
+ /** One weighted plot size a frontage can mix in: `w` along the road, `d` into the block. */
31
+ export interface PlotVariant extends WorldBounds {
32
+ /** Relative pick weight among the variants. Default 1. */
33
+ weight?: number;
34
+ }
30
35
  /** Options for {@link deriveBuildingLots}. */
31
36
  export interface BuildingLotOptions {
32
37
  /** Roads whose frontage is lined with lots. */
33
38
  roads: readonly RoadFrontage[];
34
- /** Lot footprint: `w` is frontage width (along the road), `d` is depth (into the block). Default `{ w: 12, d: 10 }`. */
35
- footprint?: WorldBounds;
39
+ /**
40
+ * Plot size(s): `w` is frontage width (along the road), `d` is depth (into the block). Pass one
41
+ * {@link WorldBounds} for a uniform district, or a weighted {@link PlotVariant} list to mix plot
42
+ * sizes along the same frontage — narrow-deep apartment slices beside wide detached-house
43
+ * parcels beside tight terraces. Variant rolls are seeded per road AND side, so the two sides of
44
+ * a street can read differently. Default `{ w: 12, d: 10 }`.
45
+ */
46
+ footprint?: WorldBounds | readonly PlotVariant[];
36
47
  /** Gap between neighbouring lots along a frontage, world units. Default 2. */
37
48
  spacing?: number;
38
49
  /** Sidewalk strip between the curb (`width / 2`) and the building front, world units. Default 3. */
39
50
  setback?: number;
51
+ /**
52
+ * Manhattan streetwall dial (0..1). Omit (default) keeps the legacy look byte-for-byte. When set,
53
+ * frontage is compacted as it rises: the along-road gap between neighbours scales from `spacing` at
54
+ * `~0.45` down to 0 at 1 (buildings shoulder-to-shoulder), and each lot's frontage width widens by
55
+ * a modest factor above 0.45 so consecutive lots touch instead of leaving gaps. Below ~0.45 the gap
56
+ * grows past `spacing`, so a low dial reads sparser than the default. Depth is unchanged. This is
57
+ * pure frontage compaction; block-interior fill lives in the content pass (`resolveCityLotContent`).
58
+ */
59
+ blockFill?: number;
40
60
  /** Place lots on both sides of each road. Default true. */
41
61
  bothSides?: boolean;
42
62
  /** Clip region; lots whose center falls outside are dropped. Omit to keep every lot. */
43
63
  area?: LotArea;
44
64
  /**
45
- * Seed forwarded by callers for parity with other feature configs. Lot GEOMETRY is fully
46
- * determined by the roads and dials (so a layout is identical without it); per-building variation
47
- * (floors, massing) is seeded downstream where the lots become buildings.
65
+ * Extra road corridors lots must keep clear of WITHOUT lining them with frontage — service
66
+ * alleys, lanes, trails. Every lot's full footprint is kept off these (and off `roads`).
67
+ */
68
+ avoid?: readonly RoadFrontage[];
69
+ /**
70
+ * Seed for the plot-variant rolls when `footprint` is a variant list (identical inputs always
71
+ * yield the identical layout, with or without it). With a single footprint, lot geometry is
72
+ * fully determined by the roads and dials; per-building variation (floors, massing) is seeded
73
+ * downstream where the lots become buildings.
48
74
  */
49
75
  seed?: string;
50
76
  /** Hard cap on emitted lots so a long network stays bounded. Default 400. */
@@ -65,6 +91,20 @@ export interface PlacedBuildingLot {
65
91
  /** Distance from the lot's front face to the road centerline (= `width / 2 + setback`). */
66
92
  frontDistance: number;
67
93
  }
94
+ /**
95
+ * Frontage-fill reference point: at this dial value the compaction is a no-op (legacy spacing/width),
96
+ * so callers can leave `blockFill` unset for the classic look and this value reproduces it exactly.
97
+ */
98
+ export declare const FRONTAGE_FILL_REFERENCE = 0.45;
99
+ /**
100
+ * Map a `blockFill` dial to frontage compaction factors. `undefined` ⇒ identity (legacy path stays
101
+ * byte-identical). At {@link FRONTAGE_FILL_REFERENCE} the factors are exactly `{1, 1}`; toward 1 the
102
+ * gap collapses and lots widen, toward 0 the gap grows (sparser than default). @internal
103
+ */
104
+ export declare function frontageCompaction(blockFill: number | undefined): {
105
+ spacingScale: number;
106
+ widthScale: number;
107
+ };
68
108
  /**
69
109
  * Yaw that turns the building FRONT (local +z) to face the road. The lot center sits at
70
110
  * `roadPoint + outwardNormal * d`, so the road lies along `-outwardNormal` from the center. In the
@@ -74,11 +114,14 @@ export interface PlacedBuildingLot {
74
114
  */
75
115
  export declare function facingRotation(outwardNormal: Vec2): number;
76
116
  /**
77
- * Derive street-aligned building lots from road frontage. Lots are stepped along each road at
78
- * `footprint.w + spacing`, offset to each side by `width / 2 + setback + footprint.d / 2`, and
79
- * turned to face the road. Cross-road overlaps are rejected by a bounded nearest-neighbour test, so
80
- * two roads meeting at a corner don't stack buildings. Deterministic: identical inputs (including
81
- * seed) always yield the identical list.
117
+ * Derive street-aligned building lots (PLOTS) from road frontage. Each side of each road walks the
118
+ * frontage placing plots at `plot.w + spacing` pitch — rolling the next plot size from the weighted
119
+ * `footprint` variants when several are given — offset outward by `width / 2 + setback + plot.d / 2`
120
+ * and turned to face the road. The PLOT CONTRACT is enforced here, exactly: no two plots ever overlap
121
+ * (true oriented-rect separation with the spacing gap, so corners and curved frontage can't stack
122
+ * buildings), and no plot footprint ever touches any road corridor in `roads` or `avoid` — not
123
+ * just its own frontage road. Deterministic: identical inputs (including seed) always yield the
124
+ * identical list.
82
125
  * @capability building-lots derive street-facing building lots from road frontage
83
126
  */
84
127
  export declare function deriveBuildingLots(options: BuildingLotOptions): PlacedBuildingLot[];