@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
@@ -63,6 +63,26 @@ export declare function insetRingUniform(ring: readonly Vec2[], distance: number
63
63
  export declare function polygonMeanWidth(ring: readonly Vec2[]): number;
64
64
  /** All four corners of a rotated rect (center, half-extents, yaw in engine rotationY convention). */
65
65
  export declare function rectCorners(cx: number, cz: number, hw: number, hd: number, rotationY: number): Vec2[];
66
+ /** A rotated rectangle: center, half-extents, and yaw in the engine rotationY convention. */
67
+ export interface OrientedRect {
68
+ x: number;
69
+ z: number;
70
+ hw: number;
71
+ hd: number;
72
+ angle: number;
73
+ }
74
+ /**
75
+ * Separating-axis test for two rotated rectangles: true when a gap exists (no overlap).
76
+ * @capability city-district exact overlap test between two rotated rectangles (plots, lots, footprints)
77
+ */
78
+ export declare function rectsSeparated(a: OrientedRect, b: OrientedRect): boolean;
79
+ /**
80
+ * True when a rotated rect stays at least `clearance` away from every segment of a polyline —
81
+ * the plot-contract test that keeps building plots off road corridors (pass the road half-width
82
+ * as the clearance). Exact segment-vs-expanded-rect intersection, not corner sampling.
83
+ * @capability city-district keep a rotated rect (plot/footprint) clear of a road corridor polyline
84
+ */
85
+ export declare function rectClearsPolyline(rect: OrientedRect, path: readonly Vec2[], clearance: number): boolean;
66
86
  /** True when the whole rotated rect (corners + edge midpoints) sits inside the ring. */
67
87
  export declare function rectInsidePolygon(ring: readonly Vec2[], cx: number, cz: number, hw: number, hd: number, rotationY: number): boolean;
68
88
  /**
@@ -328,6 +328,97 @@ export function rectCorners(cx, cz, hw, hd, rotationY) {
328
328
  }
329
329
  return out;
330
330
  }
331
+ /**
332
+ * Separating-axis test for two rotated rectangles: true when a gap exists (no overlap).
333
+ * @capability city-district exact overlap test between two rotated rectangles (plots, lots, footprints)
334
+ */
335
+ export function rectsSeparated(a, b) {
336
+ const axes = [];
337
+ for (const angle of [a.angle, b.angle]) {
338
+ axes.push([Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]);
339
+ }
340
+ const ca = rectCorners(a.x, a.z, a.hw, a.hd, a.angle);
341
+ const cb = rectCorners(b.x, b.z, b.hw, b.hd, b.angle);
342
+ for (const [ax, az] of axes) {
343
+ let minA = Infinity;
344
+ let maxA = -Infinity;
345
+ let minB = Infinity;
346
+ let maxB = -Infinity;
347
+ for (const [x, z] of ca) {
348
+ const p = x * ax + z * az;
349
+ if (p < minA)
350
+ minA = p;
351
+ if (p > maxA)
352
+ maxA = p;
353
+ }
354
+ for (const [x, z] of cb) {
355
+ const p = x * ax + z * az;
356
+ if (p < minB)
357
+ minB = p;
358
+ if (p > maxB)
359
+ maxB = p;
360
+ }
361
+ if (maxA < minB || maxB < minA)
362
+ return true; // gap on this axis → no overlap
363
+ }
364
+ return false;
365
+ }
366
+ /** True when a segment touches an axis-aligned box (separating-axis: x, z, and the segment normal). */
367
+ function segmentTouchesAabb(ax, az, bx, bz, minX, minZ, maxX, maxZ) {
368
+ if (Math.max(ax, bx) < minX || Math.min(ax, bx) > maxX)
369
+ return false;
370
+ if (Math.max(az, bz) < minZ || Math.min(az, bz) > maxZ)
371
+ return false;
372
+ const nx = -(bz - az);
373
+ const nz = bx - ax;
374
+ const d0 = nx * (minX - ax) + nz * (minZ - az);
375
+ const d1 = nx * (maxX - ax) + nz * (minZ - az);
376
+ const d2 = nx * (maxX - ax) + nz * (maxZ - az);
377
+ const d3 = nx * (minX - ax) + nz * (maxZ - az);
378
+ if (d0 > 0 && d1 > 0 && d2 > 0 && d3 > 0)
379
+ return false;
380
+ if (d0 < 0 && d1 < 0 && d2 < 0 && d3 < 0)
381
+ return false;
382
+ return true;
383
+ }
384
+ /**
385
+ * True when a rotated rect stays at least `clearance` away from every segment of a polyline —
386
+ * the plot-contract test that keeps building plots off road corridors (pass the road half-width
387
+ * as the clearance). Exact segment-vs-expanded-rect intersection, not corner sampling.
388
+ * @capability city-district keep a rotated rect (plot/footprint) clear of a road corridor polyline
389
+ */
390
+ export function rectClearsPolyline(rect, path, clearance) {
391
+ if (path.length < 2)
392
+ return true;
393
+ const c = Math.cos(rect.angle);
394
+ const s = Math.sin(rect.angle);
395
+ const ex = rect.hw + clearance;
396
+ const ez = rect.hd + clearance;
397
+ // Cheap world-space bbox reject before transforming segments into the rect frame.
398
+ const reach = Math.hypot(ex, ez);
399
+ const minX = rect.x - reach;
400
+ const maxX = rect.x + reach;
401
+ const minZ = rect.z - reach;
402
+ const maxZ = rect.z + reach;
403
+ let px = path[0][0];
404
+ let pz = path[0][1];
405
+ let plx = (px - rect.x) * c - (pz - rect.z) * s;
406
+ let plz = (px - rect.x) * s + (pz - rect.z) * c;
407
+ for (let i = 1; i < path.length; i += 1) {
408
+ const qx = path[i][0];
409
+ const qz = path[i][1];
410
+ const inside = !(Math.max(px, qx) < minX || Math.min(px, qx) > maxX || Math.max(pz, qz) < minZ || Math.min(pz, qz) > maxZ);
411
+ const qlx = (qx - rect.x) * c - (qz - rect.z) * s;
412
+ const qlz = (qx - rect.x) * s + (qz - rect.z) * c;
413
+ if (inside && segmentTouchesAabb(plx, plz, qlx, qlz, -ex, -ez, ex, ez))
414
+ return false;
415
+ px = qx;
416
+ pz = qz;
417
+ plx = qlx;
418
+ plz = qlz;
419
+ }
420
+ return true;
421
+ }
331
422
  /** True when the whole rotated rect (corners + edge midpoints) sits inside the ring. */
332
423
  export function rectInsidePolygon(ring, cx, cz, hw, hd, rotationY) {
333
424
  const corners = rectCorners(cx, cz, hw, hd, rotationY);
@@ -35,7 +35,7 @@ import { furnitureSpots } from "./streets.js";
35
35
  import { CITY_BUILDING_BUDGET, budgetWarning, placedCoverage } from "./scatterCoverage.js";
36
36
  import { generateStreets, streetNetworkMode, } from "./streetGenerator.js";
37
37
  import { buildablePolygon, carveCorridors, conformPolygonToRing, cutParcel, extractBlocks, isSliverBlock, pointsInPolygon, RingWalker, } from "./cityBlocks.js";
38
- import { fitRectInPolygon, rayDistanceToRing, pointInPolygon, polygonArea, polygonCentroid, polygonMeanWidth, polygonsOverlap, insetRingUniform, ringBounds, } from "./cityGeometry.js";
38
+ import { fitRectInPolygon, rayDistanceToRing, pointInPolygon, polygonArea, polygonCentroid, polygonMeanWidth, polygonsOverlap, insetRingUniform, rectsSeparated, ringBounds, } from "./cityGeometry.js";
39
39
  /** The editor volume kind marking a box as a procedural city district. */
40
40
  export const CITY_KIND = "city";
41
41
  /** The editor volume kind that locally overrides a city's zone band/mix — a district in a district. */
@@ -666,49 +666,6 @@ class StreetIndex {
666
666
  return null;
667
667
  }
668
668
  }
669
- /** Separating-axis test for two rotated rectangles (center, half-extents, yaw): true when a gap exists. */
670
- function rectsSeparated(a, b) {
671
- const axes = [];
672
- for (const angle of [a.angle, b.angle]) {
673
- axes.push([Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]);
674
- }
675
- const corners = (r) => {
676
- const c = Math.cos(r.angle);
677
- const s = Math.sin(r.angle);
678
- const out = [];
679
- for (const [dx, dz] of [
680
- [r.hw, r.hd],
681
- [r.hw, -r.hd],
682
- [-r.hw, r.hd],
683
- [-r.hw, -r.hd],
684
- ]) {
685
- out.push([r.x + dx * c + dz * s, r.z - dx * s + dz * c]);
686
- }
687
- return out;
688
- };
689
- const ca = corners(a);
690
- const cb = corners(b);
691
- for (const [ax, az] of axes) {
692
- let minA = Infinity, maxA = -Infinity, minB = Infinity, maxB = -Infinity;
693
- for (const [x, z] of ca) {
694
- const p = x * ax + z * az;
695
- if (p < minA)
696
- minA = p;
697
- if (p > maxA)
698
- maxA = p;
699
- }
700
- for (const [x, z] of cb) {
701
- const p = x * ax + z * az;
702
- if (p < minB)
703
- minB = p;
704
- if (p > maxB)
705
- maxB = p;
706
- }
707
- if (maxA < minB || maxB < minA)
708
- return true; // gap on this axis → no overlap
709
- }
710
- return false;
711
- }
712
669
  /** Broadphase grid over placed lots so overlap tests only touch neighbors. */
713
670
  class LotIndex {
714
671
  cells = new Map();
@@ -0,0 +1,64 @@
1
+ import type { WorldXZ } from "./minimap.js";
2
+ /** A fast-travel destination the game defines. Discovery is tracked separately. */
3
+ export interface TravelPointDef<TMeta = unknown> {
4
+ id: string;
5
+ name: string;
6
+ /** World-XZ location — feeds distance sorting and map markers. */
7
+ position: WorldXZ;
8
+ region?: string;
9
+ icon?: string;
10
+ /** Discovered from the start (a home base / starting waypoint). */
11
+ initial?: boolean;
12
+ meta?: TMeta;
13
+ }
14
+ /** A destination plus its discovery state (and distance from a query origin, when given). */
15
+ export interface TravelPointView<TMeta = unknown> extends TravelPointDef<TMeta> {
16
+ discovered: boolean;
17
+ discoveredAt: number | null;
18
+ /** XZ distance from the query origin passed to `list`/`destinations`/`nearest`, else undefined. */
19
+ distance?: number;
20
+ }
21
+ /** Serializable discovery state. */
22
+ export interface FastTravelSnapshot {
23
+ discovered: Record<string, number>;
24
+ }
25
+ /** A network of fast-travel points with per-player discovery + distance queries. */
26
+ export interface FastTravelNetwork<TMeta = unknown> {
27
+ /** Unlock a point; returns the newly-discovered view, or null if unknown or already discovered. */
28
+ discover(id: string): TravelPointView<TMeta> | null;
29
+ isDiscovered(id: string): boolean;
30
+ get(id: string): TravelPointView<TMeta> | null;
31
+ /** All points (discovery state + optional distance), in definition order — or by distance when `from` is given. */
32
+ list(from?: WorldXZ): readonly TravelPointView<TMeta>[];
33
+ /** Discovered points only (distance-sorted when `from` is given). */
34
+ destinations(from?: WorldXZ): readonly TravelPointView<TMeta>[];
35
+ /** Nearest discovered point to `from` (optionally excluding an id — e.g. the one you're standing on). */
36
+ nearest(from: WorldXZ, options?: {
37
+ exclude?: string;
38
+ }): TravelPointView<TMeta> | null;
39
+ /** Whether a destination can be traveled to (discovered). Combat/cost gates are the game's to add. */
40
+ canTravel(id: string): boolean;
41
+ discoveredCount(): number;
42
+ total(): number;
43
+ subscribe(listener: () => void): () => void;
44
+ snapshot(): FastTravelSnapshot;
45
+ restore(snapshot: FastTravelSnapshot): void;
46
+ }
47
+ /** Options for {@link createFastTravelNetwork}. */
48
+ export interface FastTravelOptions<TMeta = unknown> {
49
+ points: readonly TravelPointDef<TMeta>[];
50
+ /** Injected clock (ms). Default `Date.now`. */
51
+ now?: () => number;
52
+ /** Fires when a point is discovered for the first time — wire to a toast/notification. */
53
+ onDiscover?: (point: TravelPointView<TMeta>) => void;
54
+ }
55
+ /**
56
+ * A fast-travel network: defined destinations plus per-player discovery, with
57
+ * distance-sorted queries, a `nearest` lookup, a `canTravel` gate, an
58
+ * `onDiscover` seam, and serializable `snapshot`/`restore`. Travel itself is a
59
+ * teleport the game applies to `TravelPointView.position`; this owns the
60
+ * unlock/where-can-I-go state. Points marked `initial` start discovered.
61
+ *
62
+ * @capability fast-travel network of fast-travel destinations with discovery, distance-sorted queries, nearest lookup, a travel gate, and serializable state
63
+ */
64
+ export declare function createFastTravelNetwork<TMeta = unknown>(options: FastTravelOptions<TMeta>): FastTravelNetwork<TMeta>;
@@ -0,0 +1,108 @@
1
+ function distanceXZ(a, b) {
2
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
3
+ }
4
+ /**
5
+ * A fast-travel network: defined destinations plus per-player discovery, with
6
+ * distance-sorted queries, a `nearest` lookup, a `canTravel` gate, an
7
+ * `onDiscover` seam, and serializable `snapshot`/`restore`. Travel itself is a
8
+ * teleport the game applies to `TravelPointView.position`; this owns the
9
+ * unlock/where-can-I-go state. Points marked `initial` start discovered.
10
+ *
11
+ * @capability fast-travel network of fast-travel destinations with discovery, distance-sorted queries, nearest lookup, a travel gate, and serializable state
12
+ */
13
+ export function createFastTravelNetwork(options) {
14
+ const now = options.now ?? Date.now;
15
+ const order = [];
16
+ const defs = new Map();
17
+ const discoveredAt = new Map();
18
+ for (const def of options.points) {
19
+ if (defs.has(def.id))
20
+ continue;
21
+ defs.set(def.id, def);
22
+ order.push(def.id);
23
+ if (def.initial === true)
24
+ discoveredAt.set(def.id, 0);
25
+ }
26
+ const listeners = new Set();
27
+ function notify() {
28
+ for (const listener of listeners)
29
+ listener();
30
+ }
31
+ function viewOf(id, from) {
32
+ const def = defs.get(id);
33
+ const at = discoveredAt.get(id) ?? null;
34
+ return {
35
+ ...def,
36
+ discovered: at !== null,
37
+ discoveredAt: at,
38
+ ...(from !== undefined ? { distance: distanceXZ(def.position, from) } : {}),
39
+ };
40
+ }
41
+ function views(from) {
42
+ const all = order.map((id) => viewOf(id, from));
43
+ if (from !== undefined)
44
+ all.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0));
45
+ return all;
46
+ }
47
+ const network = {
48
+ discover(id) {
49
+ if (!defs.has(id) || discoveredAt.has(id))
50
+ return null;
51
+ discoveredAt.set(id, now());
52
+ const view = viewOf(id);
53
+ options.onDiscover?.(view);
54
+ notify();
55
+ return view;
56
+ },
57
+ isDiscovered(id) {
58
+ return discoveredAt.has(id);
59
+ },
60
+ get(id) {
61
+ return defs.has(id) ? viewOf(id) : null;
62
+ },
63
+ list(from) {
64
+ return views(from);
65
+ },
66
+ destinations(from) {
67
+ return views(from).filter((view) => view.discovered);
68
+ },
69
+ nearest(from, opts = {}) {
70
+ let best = null;
71
+ for (const view of views(from)) {
72
+ if (!view.discovered || view.id === opts.exclude)
73
+ continue;
74
+ if (best === null || (view.distance ?? 0) < (best.distance ?? 0))
75
+ best = view;
76
+ }
77
+ return best;
78
+ },
79
+ canTravel(id) {
80
+ return discoveredAt.has(id);
81
+ },
82
+ discoveredCount() {
83
+ return discoveredAt.size;
84
+ },
85
+ total() {
86
+ return order.length;
87
+ },
88
+ subscribe(listener) {
89
+ listeners.add(listener);
90
+ return () => listeners.delete(listener);
91
+ },
92
+ snapshot() {
93
+ return { discovered: Object.fromEntries(discoveredAt) };
94
+ },
95
+ restore(snapshot) {
96
+ discoveredAt.clear();
97
+ for (const id of order)
98
+ if (defs.get(id).initial === true)
99
+ discoveredAt.set(id, 0);
100
+ for (const [id, at] of Object.entries(snapshot.discovered)) {
101
+ if (defs.has(id))
102
+ discoveredAt.set(id, at);
103
+ }
104
+ notify();
105
+ },
106
+ };
107
+ return network;
108
+ }
@@ -76,6 +76,14 @@ export interface TerrainDetailMaterialConfig {
76
76
  repeat?: number;
77
77
  /** Blend strength of the texture over the procedural/vertex-colour base, 0..1. Default 1. */
78
78
  strength?: number;
79
+ /**
80
+ * How much of the map's OWN colour replaces the terrain's authored colour, 0..1. Default 1 —
81
+ * at which the map's albedo wins and the biome tint, macro sweeps, and slope-rock blend under it
82
+ * are painted over, so a whole region flattens to the texture's single hue. Drop toward 0 to keep
83
+ * the authored colour and take only the map's grain (its light and shade), which is what a
84
+ * hand-tinted world wants from a stock PBR set.
85
+ */
86
+ tint?: number;
79
87
  }
80
88
  export interface TerrainFlattenMask {
81
89
  center: EnvironmentVec2;
@@ -283,6 +291,14 @@ export interface SkyEnvironmentConfig {
283
291
  hazeStrength?: number;
284
292
  /** Sun-glow brightness multiplier around the sun disc. Default 1. */
285
293
  sunGlowStrength?: number;
294
+ /**
295
+ * How fast the gradient reaches `zenithColor` off the horizon. Below 1 pulls the zenith hue down
296
+ * into the frame — the difference between a sky an eye-level camera actually sees and a horizon
297
+ * wash; above 1 keeps colour banked overhead. Default 0.65.
298
+ */
299
+ gradientExponent?: number;
300
+ /** Procedural cloud-band opacity over the gradient: 0 is a clear sky, 1 is heavy overcast. Default 0.35. */
301
+ cloudiness?: number;
286
302
  fog?: {
287
303
  color?: string;
288
304
  near?: number;
@@ -122,6 +122,8 @@ export function sky(config = {}) {
122
122
  ...(config.radius === undefined ? {} : { radius: config.radius }),
123
123
  ...(config.hazeStrength === undefined ? {} : { hazeStrength: config.hazeStrength }),
124
124
  ...(config.sunGlowStrength === undefined ? {} : { sunGlowStrength: config.sunGlowStrength }),
125
+ ...(config.gradientExponent === undefined ? {} : { gradientExponent: config.gradientExponent }),
126
+ ...(config.cloudiness === undefined ? {} : { cloudiness: config.cloudiness }),
125
127
  ...(config.fog === undefined ? {} : { fog: config.fog }),
126
128
  ...(config.volumetricClouds === undefined ? {} : { volumetricClouds: config.volumetricClouds }),
127
129
  });
@@ -0,0 +1,11 @@
1
+ import type { GameContext } from "../runtime/gameContext.js";
2
+ /**
3
+ * Ground an XZ point onto the live world surface as `[x, groundY, z]`.
4
+ *
5
+ * The one-line pose pattern every game re-derives as
6
+ * `[x, ctx.world.groundHeightAt(x, z), z]` — use this instead so spawn,
7
+ * placement, and AI pose sites stay consistent.
8
+ *
9
+ * @capability grounded ground an XZ point onto the world surface as a pose triple
10
+ */
11
+ export declare function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Ground an XZ point onto the live world surface as `[x, groundY, z]`.
3
+ *
4
+ * The one-line pose pattern every game re-derives as
5
+ * `[x, ctx.world.groundHeightAt(x, z), z]` — use this instead so spawn,
6
+ * placement, and AI pose sites stay consistent.
7
+ *
8
+ * @capability grounded ground an XZ point onto the world surface as a pose triple
9
+ */
10
+ export function grounded(ctx, x, z) {
11
+ return [x, ctx.world.groundHeightAt(x, z), z];
12
+ }
@@ -0,0 +1,75 @@
1
+ import type { WorldXZ } from "./minimap.js";
2
+ import type { MapLayerTone, MapRoute, MapZone, MapZoneShape } from "./mapLayers.js";
3
+ /** A freehand drawn line on the map — a world-XZ polyline. */
4
+ export interface MapStroke {
5
+ id: string;
6
+ points: readonly WorldXZ[];
7
+ tone?: MapLayerTone;
8
+ /** Line width in map pixels. */
9
+ width?: number;
10
+ }
11
+ /** A drawn area annotation (circle/rect/polygon), reusing the map-zone shape vocabulary. */
12
+ export interface MapShapeAnnotation {
13
+ id: string;
14
+ shape: MapZoneShape;
15
+ tone?: MapLayerTone;
16
+ label?: string;
17
+ }
18
+ /** A text note pinned at a world point. */
19
+ export interface MapNote {
20
+ id: string;
21
+ position: WorldXZ;
22
+ text: string;
23
+ tone?: MapLayerTone;
24
+ }
25
+ /** Whole serializable state of an annotation layer — drop into a save blob. */
26
+ export interface AnnotationSnapshot {
27
+ strokes: readonly MapStroke[];
28
+ shapes: readonly MapShapeAnnotation[];
29
+ notes: readonly MapNote[];
30
+ }
31
+ /** Player-drawn map annotation layer — strokes, shapes, and notes over caller-owned map data. */
32
+ export interface AnnotationLayer {
33
+ /** Add a freehand stroke; returns its id. Ignored (returns "") when fewer than 2 points. */
34
+ addStroke(points: readonly WorldXZ[], options?: {
35
+ id?: string;
36
+ tone?: MapLayerTone;
37
+ width?: number;
38
+ }): string;
39
+ addShape(shape: MapZoneShape, options?: {
40
+ id?: string;
41
+ tone?: MapLayerTone;
42
+ label?: string;
43
+ }): string;
44
+ addNote(position: WorldXZ, text: string, options?: {
45
+ id?: string;
46
+ tone?: MapLayerTone;
47
+ }): string;
48
+ remove(id: string): boolean;
49
+ clear(): void;
50
+ strokes(): readonly MapStroke[];
51
+ shapes(): readonly MapShapeAnnotation[];
52
+ notes(): readonly MapNote[];
53
+ /** Project strokes into `MapRoute`s for a map's `routes` prop. Stable identity between changes. */
54
+ routes(): readonly MapRoute[];
55
+ /** Project shapes into `MapZone`s for a map's `zones` prop. Stable identity between changes. */
56
+ zones(): readonly MapZone[];
57
+ subscribe(listener: () => void): () => void;
58
+ snapshot(): AnnotationSnapshot;
59
+ restore(snapshot: AnnotationSnapshot): void;
60
+ }
61
+ /** Options for {@link createAnnotationLayer}. */
62
+ export interface AnnotationLayerOptions {
63
+ /** Default tone applied to strokes/shapes/notes added without an explicit one. Default `"info"`. */
64
+ defaultTone?: MapLayerTone;
65
+ }
66
+ /**
67
+ * Player-drawn map annotation layer: freehand `strokes`, area `shapes`, and
68
+ * pinned `notes`, all world-XZ and serializable. `routes()`/`zones()` project
69
+ * strokes/shapes into the exact shapes `Minimap`/`WorldMap`/`FullscreenMap`
70
+ * already render via their `routes`/`zones` props, so drawing needs no new
71
+ * renderer. State is plain data; `snapshot`/`restore` round-trip through a save.
72
+ *
73
+ * @capability map-annotations player-drawn map annotation layer — freehand strokes, area shapes, and pinned notes, projected into the map's routes/zones props and serializable
74
+ */
75
+ export declare function createAnnotationLayer(options?: AnnotationLayerOptions): AnnotationLayer;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Player-drawn map annotation layer: freehand `strokes`, area `shapes`, and
3
+ * pinned `notes`, all world-XZ and serializable. `routes()`/`zones()` project
4
+ * strokes/shapes into the exact shapes `Minimap`/`WorldMap`/`FullscreenMap`
5
+ * already render via their `routes`/`zones` props, so drawing needs no new
6
+ * renderer. State is plain data; `snapshot`/`restore` round-trip through a save.
7
+ *
8
+ * @capability map-annotations player-drawn map annotation layer — freehand strokes, area shapes, and pinned notes, projected into the map's routes/zones props and serializable
9
+ */
10
+ export function createAnnotationLayer(options = {}) {
11
+ const defaultTone = options.defaultTone ?? "info";
12
+ const strokeStore = new Map();
13
+ const shapeStore = new Map();
14
+ const noteStore = new Map();
15
+ const listeners = new Set();
16
+ let counter = 0;
17
+ let strokeCache = null;
18
+ let shapeCache = null;
19
+ let noteCache = null;
20
+ let routeCache = null;
21
+ let zoneCache = null;
22
+ function invalidate() {
23
+ strokeCache = null;
24
+ shapeCache = null;
25
+ noteCache = null;
26
+ routeCache = null;
27
+ zoneCache = null;
28
+ for (const listener of listeners)
29
+ listener();
30
+ }
31
+ function generateId() {
32
+ let id = `annotation-${counter}`;
33
+ while (strokeStore.has(id) || shapeStore.has(id) || noteStore.has(id)) {
34
+ counter += 1;
35
+ id = `annotation-${counter}`;
36
+ }
37
+ counter += 1;
38
+ return id;
39
+ }
40
+ const layer = {
41
+ addStroke(points, opts = {}) {
42
+ if (points.length < 2)
43
+ return "";
44
+ const id = opts.id ?? generateId();
45
+ const stroke = {
46
+ id,
47
+ points: points.map((p) => [p[0], p[1]]),
48
+ tone: opts.tone ?? defaultTone,
49
+ ...(opts.width !== undefined ? { width: opts.width } : {}),
50
+ };
51
+ strokeStore.set(id, stroke);
52
+ invalidate();
53
+ return id;
54
+ },
55
+ addShape(shape, opts = {}) {
56
+ const id = opts.id ?? generateId();
57
+ shapeStore.set(id, { id, shape, tone: opts.tone ?? defaultTone, ...(opts.label !== undefined ? { label: opts.label } : {}) });
58
+ invalidate();
59
+ return id;
60
+ },
61
+ addNote(position, text, opts = {}) {
62
+ const id = opts.id ?? generateId();
63
+ noteStore.set(id, { id, position: [position[0], position[1]], text, tone: opts.tone ?? defaultTone });
64
+ invalidate();
65
+ return id;
66
+ },
67
+ remove(id) {
68
+ const existed = strokeStore.delete(id) || shapeStore.delete(id) || noteStore.delete(id);
69
+ if (existed)
70
+ invalidate();
71
+ return existed;
72
+ },
73
+ clear() {
74
+ if (strokeStore.size === 0 && shapeStore.size === 0 && noteStore.size === 0)
75
+ return;
76
+ strokeStore.clear();
77
+ shapeStore.clear();
78
+ noteStore.clear();
79
+ invalidate();
80
+ },
81
+ strokes() {
82
+ if (strokeCache === null)
83
+ strokeCache = [...strokeStore.values()];
84
+ return strokeCache;
85
+ },
86
+ shapes() {
87
+ if (shapeCache === null)
88
+ shapeCache = [...shapeStore.values()];
89
+ return shapeCache;
90
+ },
91
+ notes() {
92
+ if (noteCache === null)
93
+ noteCache = [...noteStore.values()];
94
+ return noteCache;
95
+ },
96
+ routes() {
97
+ if (routeCache === null) {
98
+ routeCache = layer.strokes().map((stroke) => ({
99
+ id: stroke.id,
100
+ points: stroke.points,
101
+ ...(stroke.tone !== undefined ? { tone: stroke.tone } : {}),
102
+ ...(stroke.width !== undefined ? { width: stroke.width } : {}),
103
+ }));
104
+ }
105
+ return routeCache;
106
+ },
107
+ zones() {
108
+ if (zoneCache === null) {
109
+ zoneCache = layer.shapes().map((annotation) => ({
110
+ id: annotation.id,
111
+ shape: annotation.shape,
112
+ ...(annotation.tone !== undefined ? { tone: annotation.tone } : {}),
113
+ ...(annotation.label !== undefined ? { label: annotation.label } : {}),
114
+ }));
115
+ }
116
+ return zoneCache;
117
+ },
118
+ subscribe(listener) {
119
+ listeners.add(listener);
120
+ return () => listeners.delete(listener);
121
+ },
122
+ snapshot() {
123
+ return { strokes: layer.strokes(), shapes: layer.shapes(), notes: layer.notes() };
124
+ },
125
+ restore(snapshot) {
126
+ strokeStore.clear();
127
+ shapeStore.clear();
128
+ noteStore.clear();
129
+ for (const stroke of snapshot.strokes)
130
+ strokeStore.set(stroke.id, stroke);
131
+ for (const shape of snapshot.shapes)
132
+ shapeStore.set(shape.id, shape);
133
+ for (const note of snapshot.notes)
134
+ noteStore.set(note.id, note);
135
+ invalidate();
136
+ },
137
+ };
138
+ return layer;
139
+ }
@@ -8,6 +8,7 @@ export const DEFAULT_MARKER_KINDS = {
8
8
  enemy: { id: "enemy", color: "#f87171", glyph: "✖", priority: 80 },
9
9
  danger: { id: "danger", color: "#fb923c", glyph: "⚠", priority: 85 },
10
10
  ping: { id: "ping", color: "#22d3ee", glyph: "◎", priority: 65 },
11
+ waypoint: { id: "waypoint", color: "#f59e0b", glyph: "⚑", priority: 95 },
11
12
  };
12
13
  export function markerKindStyle(kind, styles = DEFAULT_MARKER_KINDS) {
13
14
  if (kind === undefined)
@@ -71,7 +71,7 @@ export interface ResourceNodeState {
71
71
  /** Construction options for {@link createResourceNodeField}: node definitions and an optional RNG/suppression hook. */
72
72
  export interface ResourceNodeFieldConfig {
73
73
  nodes: readonly ResourceNodeDef[];
74
- /** Injected [0,1) generator for deterministic yield variance; defaults to `Math.random`. */
74
+ /** Injected [0,1) generator for deterministic yield variance. Omitting it falls back to `Math.random` and warns once. */
75
75
  rng?: () => number;
76
76
  /**
77
77
  * Freeze respawn for a node while this returns true — a structure within radius, a nearby occupant.
@@ -1,4 +1,5 @@
1
1
  import { clamp } from "../math/scalar.js";
2
+ import { resolveRng } from "../random/resolveRng.js";
2
3
  function resolveQuantity(quantity, rng) {
3
4
  if (typeof quantity === "number")
4
5
  return quantity;
@@ -29,7 +30,7 @@ function toState(node) {
29
30
  * @capability resource-node depletable, respawning harvest nodes with tool-dependent yields
30
31
  */
31
32
  export function createResourceNodeField(config) {
32
- const rng = config.rng ?? Math.random;
33
+ const rng = resolveRng(config.rng, "createResourceNode");
33
34
  const nodes = new Map();
34
35
  const order = [];
35
36
  for (const def of config.nodes) {