@jgengine/core 0.11.0 → 0.12.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 (219) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -1,6 +1,7 @@
1
1
  import type { BuildingPaletteOverrides, BuildingStyle } from "./buildings.js";
2
2
  import type { AvoidZone } from "./geometry.js";
3
3
  import type { TerrainPathProfile } from "./pathTerrain.js";
4
+ import type { PlaceWorldFeature } from "./place.js";
4
5
  import type { TerraformSnapshot } from "./terraform.js";
5
6
  import type { VolumetricCloudsConfig } from "./volumetricClouds.js";
6
7
  export interface WorldBounds {
@@ -464,8 +465,13 @@ export interface PlotsWorldConfig extends WorldGridConfig {
464
465
  export interface TilemapWorldConfig extends WorldGridConfig {
465
466
  map: string;
466
467
  }
467
- /** A declared world shape — biomes, voxel grid, plots, tilemap, environment, or flat — passed to `defineGame`. */
468
- export type WorldFeature = ({
468
+ /**
469
+ * A declared world shape passed to `defineGame`. The preferred model is the place feature from
470
+ * `world()` (`@jgengine/core/world/place`): substrate + laws, with all dressing authored in the
471
+ * editor. The remaining members — biomes, voxel grid, plots, tilemap, environment, flat — are the
472
+ * legacy code-declared shapes kept for existing games.
473
+ */
474
+ export type WorldFeature = PlaceWorldFeature | ({
469
475
  kind: "biomes";
470
476
  } & BiomesWorldConfig) | ({
471
477
  kind: "voxel";
@@ -478,7 +484,16 @@ export type WorldFeature = ({
478
484
  };
479
485
  /** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. Elevated pads (absolute `elevation`) float free and carve nothing. */
480
486
  export declare function padFlattenMasks(pads: readonly PadEnvironmentDescriptor[]): readonly TerrainFlattenMask[];
481
- /** Composes an `environment()` world feature from terrain, sky, weather, vegetation, water, structures, roads, and pads. */
487
+ /** Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
488
+ *
489
+ * Legacy dressing-in-code path — not how a world is defined. A world is the place you play in
490
+ * (`world()` from `world/place`: ground + laws); sky look, foliage scatter, props, and sculpt are
491
+ * scene content authored in the editor. Reach for `environment()` only when consuming
492
+ * editor/preset-written scene data (`environmentContentFromDocument`) or maintaining an existing
493
+ * game; never scaffold new worlds from it.
494
+ *
495
+ * @capability world-environment compose editor/preset-derived outdoor dressing — terrain, weather, water, structures — into a legacy environment feature
496
+ */
482
497
  export declare function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature;
483
498
  /** Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks. */
484
499
  export declare function terrain(config?: TerrainEnvironmentConfig): TerrainEnvironmentDescriptor;
@@ -43,7 +43,16 @@ function bandWeatherDescriptors(terrainDescriptor) {
43
43
  }
44
44
  return out;
45
45
  }
46
- /** Composes an `environment()` world feature from terrain, sky, weather, vegetation, water, structures, roads, and pads. */
46
+ /** Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
47
+ *
48
+ * Legacy dressing-in-code path — not how a world is defined. A world is the place you play in
49
+ * (`world()` from `world/place`: ground + laws); sky look, foliage scatter, props, and sculpt are
50
+ * scene content authored in the editor. Reach for `environment()` only when consuming
51
+ * editor/preset-written scene data (`environmentContentFromDocument`) or maintaining an existing
52
+ * game; never scaffold new worlds from it.
53
+ *
54
+ * @capability world-environment compose editor/preset-derived outdoor dressing — terrain, weather, water, structures — into a legacy environment feature
55
+ */
47
56
  export function environment(config = {}) {
48
57
  const explicitWeather = list(config.weather);
49
58
  const bandWeather = bandWeatherDescriptors(config.terrain);
@@ -12,7 +12,7 @@ import { registerSceneKind } from "../scene/sceneKinds.js";
12
12
  export const GRASS_FIELD_KIND = "grass_field";
13
13
  /** Grass defaults: a lush, lightly-swaying meadow. */
14
14
  export const GRASS_DEFAULTS = {
15
- density: 4,
15
+ density: 12,
16
16
  bladeHeight: 0.6,
17
17
  colorBase: "#3f6f37",
18
18
  colorTip: "#9ac25a",
@@ -26,7 +26,7 @@ export const GRASS_DEFAULTS = {
26
26
  /** The grass parameter schema — drives the inspector and `meta` parse via the studio seam. */
27
27
  export const GRASS_SCHEMA = {
28
28
  fields: [
29
- { type: "range", key: "density", label: "density", min: 0.5, max: 16, step: 0.5, default: GRASS_DEFAULTS.density, unit: "/m²" },
29
+ { type: "range", key: "density", label: "density", min: 0.5, max: 160, step: 0.5, default: GRASS_DEFAULTS.density, unit: "/m²" },
30
30
  { type: "range", key: "bladeHeight", label: "blade height", min: 0.1, max: 2, step: 0.05, default: GRASS_DEFAULTS.bladeHeight, unit: "m" },
31
31
  { type: "color", key: "colorBase", label: "base color", default: GRASS_DEFAULTS.colorBase },
32
32
  { type: "color", key: "colorTip", label: "tip color", default: GRASS_DEFAULTS.colorTip },
@@ -1,19 +1,32 @@
1
1
  export type MarkerPosition = readonly [number, number, number];
2
- export interface MapMarker<TMeta = unknown> {
2
+ /**
3
+ * Small, display-only marker shape consumed by map renderers. Existing games
4
+ * can project their own entities to this view without adopting marker
5
+ * lifecycle storage or duplicating them into a {@link MarkerSet}.
6
+ */
7
+ export interface MarkerView<TMeta = unknown> {
3
8
  id: string;
4
- kind: string;
5
9
  position: MarkerPosition;
10
+ kind?: string;
6
11
  label?: string;
12
+ /** Optional world heading in radians for presentations that draw directional markers. */
13
+ heading?: number;
14
+ meta?: TMeta;
15
+ }
16
+ /** A marker owned by {@link MarkerSet}, including its lifecycle and query fields. */
17
+ export interface MapMarker<TMeta = unknown> extends MarkerView<TMeta> {
18
+ kind: string;
7
19
  owner?: string;
8
20
  createdAt: number;
9
21
  expiresAt?: number;
10
- meta?: TMeta;
11
22
  }
12
23
  export interface MarkerInput<TMeta = unknown> {
13
24
  id?: string;
14
25
  kind: string;
15
26
  position: MarkerPosition;
16
27
  label?: string;
28
+ /** Optional world heading in radians for directional map presentations. */
29
+ heading?: number;
17
30
  owner?: string;
18
31
  createdAt?: number;
19
32
  expiresAt?: number;
@@ -37,7 +50,7 @@ export interface MarkerKindStyle {
37
50
  priority: number;
38
51
  }
39
52
  export declare const DEFAULT_MARKER_KINDS: Record<string, MarkerKindStyle>;
40
- export declare function markerKindStyle(kind: string, styles?: Record<string, MarkerKindStyle>): MarkerKindStyle;
53
+ export declare function markerKindStyle(kind: string | undefined, styles?: Record<string, MarkerKindStyle>): MarkerKindStyle;
41
54
  export interface MarkerSet<TMeta = unknown> {
42
55
  add(marker: MarkerInput<TMeta>): string;
43
56
  remove(id: string): boolean;
@@ -49,4 +62,37 @@ export interface MarkerSet<TMeta = unknown> {
49
62
  subscribe(listener: () => void): () => void;
50
63
  snapshot(): readonly MapMarker<TMeta>[];
51
64
  }
65
+ /**
66
+ * Observable marker snapshots owned by an external project. `getSnapshot`
67
+ * must return the same array identity until the source changes and calls its
68
+ * subscribers, matching React's external-store contract.
69
+ */
70
+ export interface MarkerSource<TMarker extends MarkerView = MarkerView> {
71
+ subscribe(listener: () => void): () => void;
72
+ getSnapshot(): readonly TMarker[];
73
+ /** Optional hydration snapshot; omit when the client snapshot is also valid during SSR. */
74
+ getServerSnapshot?: () => readonly TMarker[];
75
+ }
76
+ /** Marker data accepted by portable consumers: static views, an external source, or a native set. */
77
+ export type MarkerCollection<TMarker extends MarkerView = MarkerView> = readonly TMarker[] | MarkerSource<TMarker> | MarkerSet;
78
+ /** Configuration for projecting a caller-owned collection into display-only markers. */
79
+ export interface MarkerSourceOptions<TEntity, TMarker extends MarkerView = MarkerView> {
80
+ /** Subscribe to changes in the caller-owned collection. Static collections may omit this. */
81
+ subscribe?: (listener: () => void) => () => void;
82
+ /** Read the current caller-owned collection. Keep its identity stable between changes. */
83
+ getSnapshot: () => readonly TEntity[];
84
+ /** Optional server collection used to keep hydration output deterministic. */
85
+ getServerSnapshot?: () => readonly TEntity[];
86
+ /** Project one caller-owned record into the minimal marker view. */
87
+ project: (value: TEntity, index: number) => TMarker;
88
+ }
89
+ /**
90
+ * Adapt a caller-owned array/store to an observable marker source. Projection
91
+ * is cached between source changes, so React reads do not copy the collection
92
+ * per frame. The caller retains ownership of entities, updates, persistence,
93
+ * and subscription scheduling.
94
+ *
95
+ * @capability portable-marker-source project caller-owned arrays or subscribable stores into cached minimap marker snapshots without a MarkerSet
96
+ */
97
+ export declare function createMarkerSource<TEntity, TMarker extends MarkerView = MarkerView>(options: MarkerSourceOptions<TEntity, TMarker>): MarkerSource<TMarker>;
52
98
  export declare function createMarkerSet<TMeta = unknown>(now?: () => number): MarkerSet<TMeta>;
@@ -10,8 +10,55 @@ export const DEFAULT_MARKER_KINDS = {
10
10
  ping: { id: "ping", color: "#22d3ee", glyph: "◎", priority: 65 },
11
11
  };
12
12
  export function markerKindStyle(kind, styles = DEFAULT_MARKER_KINDS) {
13
+ if (kind === undefined)
14
+ return { id: "marker", color: "#e2e8f0", glyph: "\u2022", priority: 30 };
13
15
  return styles[kind] ?? { id: kind, color: "#e2e8f0", glyph: "•", priority: 30 };
14
16
  }
17
+ function projectedSnapshot(readSource, project) {
18
+ let dirty = true;
19
+ let source;
20
+ let snapshot = [];
21
+ return {
22
+ invalidate() {
23
+ dirty = true;
24
+ },
25
+ read() {
26
+ const next = readSource();
27
+ if (dirty || next !== source) {
28
+ source = next;
29
+ snapshot = next.map(project);
30
+ dirty = false;
31
+ }
32
+ return snapshot;
33
+ },
34
+ };
35
+ }
36
+ /**
37
+ * Adapt a caller-owned array/store to an observable marker source. Projection
38
+ * is cached between source changes, so React reads do not copy the collection
39
+ * per frame. The caller retains ownership of entities, updates, persistence,
40
+ * and subscription scheduling.
41
+ *
42
+ * @capability portable-marker-source project caller-owned arrays or subscribable stores into cached minimap marker snapshots without a MarkerSet
43
+ */
44
+ export function createMarkerSource(options) {
45
+ const client = projectedSnapshot(options.getSnapshot, options.project);
46
+ const server = options.getServerSnapshot === undefined
47
+ ? client
48
+ : projectedSnapshot(options.getServerSnapshot, options.project);
49
+ return {
50
+ subscribe(listener) {
51
+ if (options.subscribe === undefined)
52
+ return () => undefined;
53
+ return options.subscribe(() => {
54
+ client.invalidate();
55
+ listener();
56
+ });
57
+ },
58
+ getSnapshot: client.read,
59
+ getServerSnapshot: server.read,
60
+ };
61
+ }
15
62
  function distanceXZ(a, b) {
16
63
  return Math.hypot(a[0] - b[0], a[2] - b[2]);
17
64
  }
@@ -38,6 +85,8 @@ export function createMarkerSet(now = Date.now) {
38
85
  };
39
86
  if (input.label !== undefined)
40
87
  marker.label = input.label;
88
+ if (input.heading !== undefined)
89
+ marker.heading = input.heading;
41
90
  if (input.owner !== undefined)
42
91
  marker.owner = input.owner;
43
92
  if (input.expiresAt !== undefined)
@@ -1,3 +1,4 @@
1
+ import { clamp } from "../math/scalar.js";
1
2
  export const MASSING_COMPOSITIONS = [
2
3
  "bar",
3
4
  "split",
@@ -17,7 +18,6 @@ export const MASSING_PROFILES = [
17
18
  "offset",
18
19
  "twisted",
19
20
  ];
20
- const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
21
21
  export const hashMassingSeed = (text) => [...text].reduce((sum, ch) => sum + ch.charCodeAt(0), 0);
22
22
  export const massingBase = (spec, cap = 0.28) => Math.min(Math.max(0, spec.pilotis), Math.max(0, spec.height) * cap);
23
23
  export const isVillageMassing = (spec) => spec.composition === "cluster" && spec.height < 36 && spec.articulation > 56 && spec.moduleDensity > 2;
@@ -0,0 +1,78 @@
1
+ import type { Vec2 } from "./geometry.js";
2
+ /**
3
+ * Deterministic minimap terrain bake (#1036): rasterize an authored world's terrain (plus optional
4
+ * biome zones and water) into a top-down RGBA image and a matching world-bounds rectangle, then
5
+ * encode it as a PNG data URI. The editor runs this as a bake action and stores the result on the
6
+ * scene document; runtime feeds it straight into the existing `Minimap` / `WorldMap` `background` +
7
+ * `mapBounds` props — no new runtime render path, and the same authored world always bakes the same
8
+ * image (safe for verify/CI). Pure: no canvas, no DOM, `core` still imports nothing external.
9
+ */
10
+ /** World-space rectangle the bake spans (matches the react `MapBounds` shape structurally). */
11
+ export interface MinimapBakeBounds {
12
+ minX: number;
13
+ minZ: number;
14
+ maxX: number;
15
+ maxZ: number;
16
+ }
17
+ /** One biome/region tint painted over the terrain shade where the ground falls inside its polygon. */
18
+ export interface MinimapBakeZone {
19
+ /** Closed XZ polygon in world space. */
20
+ polygon: readonly Vec2[];
21
+ /** `#rgb` / `#rrggbb` tint. */
22
+ color: string;
23
+ /** Blend strength 0..1. Default 0.5. */
24
+ alpha?: number;
25
+ }
26
+ /** The authored world a bake reads — a height sampler, its bounds, and optional zones/water. */
27
+ export interface MinimapBakeSource {
28
+ bounds: MinimapBakeBounds;
29
+ sampleHeight(x: number, z: number): number;
30
+ /** Optional surface normal for slope shading; flat shading when omitted. */
31
+ sampleNormal?(x: number, z: number): readonly [number, number, number];
32
+ zones?: readonly MinimapBakeZone[];
33
+ /** Ground at or below this height renders as water. */
34
+ waterLevel?: number;
35
+ }
36
+ /** The height→color ramp and water color used by the bake. */
37
+ export interface MinimapBakePalette {
38
+ low: string;
39
+ high: string;
40
+ water: string;
41
+ }
42
+ /** The built-in height→color ramp and water color a bake uses when no palette override is given. */
43
+ export declare const DEFAULT_MINIMAP_PALETTE: MinimapBakePalette;
44
+ /** Options for {@link bakeMinimapImage}. */
45
+ export interface MinimapBakeOptions {
46
+ /** Pixel resolution of the longer axis; the shorter axis keeps world aspect. Default 128. */
47
+ resolution?: number;
48
+ palette?: Partial<MinimapBakePalette>;
49
+ }
50
+ /** A baked minimap image: RGBA pixels plus the world bounds they span. */
51
+ export interface MinimapBake {
52
+ width: number;
53
+ height: number;
54
+ /** Row-major RGBA, length `width * height * 4`. */
55
+ pixels: Uint8ClampedArray;
56
+ bounds: MinimapBakeBounds;
57
+ }
58
+ /**
59
+ * Rasterizes a top-down terrain image: height maps onto a low→high color ramp (normalized over the
60
+ * sampled height range), slope darkens the shade, water fills below `waterLevel`, and each zone
61
+ * polygon tints the ground it covers. Deterministic for a fixed source. The bounds are preserved so
62
+ * a player-centered minimap pans the image correctly.
63
+ * @capability minimap-bake rasterize authored terrain + zones into a top-down minimap image
64
+ */
65
+ export declare function bakeMinimapImage(source: MinimapBakeSource, options?: MinimapBakeOptions): MinimapBake;
66
+ /** Encodes an RGBA bake to raw PNG bytes (pure; stored DEFLATE, valid for any decoder). */
67
+ export declare function encodeBakePng(bake: MinimapBake): Uint8Array;
68
+ /**
69
+ * Encodes a bake as a `data:image/png;base64,...` URI — the exact string the `Minimap` /
70
+ * `WorldMap` `background` prop takes. Deterministic for a fixed bake.
71
+ * @capability minimap-bake encode a baked minimap as a PNG data URI for the Minimap background
72
+ */
73
+ export declare function minimapBakeToPngDataUri(bake: MinimapBake): string;
74
+ /** One-call convenience: rasterize a source and return `{ background, mapBounds }` for a minimap. */
75
+ export declare function bakeMinimapBackground(source: MinimapBakeSource, options?: MinimapBakeOptions): {
76
+ background: string;
77
+ mapBounds: MinimapBakeBounds;
78
+ };
@@ -0,0 +1,208 @@
1
+ import { pointInPolygon } from "./scatterRegion.js";
2
+ /** The built-in height→color ramp and water color a bake uses when no palette override is given. */
3
+ export const DEFAULT_MINIMAP_PALETTE = {
4
+ low: "#3f5d34",
5
+ high: "#cdbb93",
6
+ water: "#2f5d78",
7
+ };
8
+ function parseHex(hex) {
9
+ let value = hex.trim().replace(/^#/, "");
10
+ if (value.length === 3)
11
+ value = value.split("").map((c) => c + c).join("");
12
+ const int = Number.parseInt(value, 16);
13
+ if (!Number.isFinite(int) || value.length !== 6)
14
+ return { r: 0, g: 0, b: 0 };
15
+ return { r: (int >> 16) & 0xff, g: (int >> 8) & 0xff, b: int & 0xff };
16
+ }
17
+ function mix(a, b, t) {
18
+ const k = Math.max(0, Math.min(1, t));
19
+ return {
20
+ r: Math.round(a.r + (b.r - a.r) * k),
21
+ g: Math.round(a.g + (b.g - a.g) * k),
22
+ b: Math.round(a.b + (b.b - a.b) * k),
23
+ };
24
+ }
25
+ /**
26
+ * Rasterizes a top-down terrain image: height maps onto a low→high color ramp (normalized over the
27
+ * sampled height range), slope darkens the shade, water fills below `waterLevel`, and each zone
28
+ * polygon tints the ground it covers. Deterministic for a fixed source. The bounds are preserved so
29
+ * a player-centered minimap pans the image correctly.
30
+ * @capability minimap-bake rasterize authored terrain + zones into a top-down minimap image
31
+ */
32
+ export function bakeMinimapImage(source, options = {}) {
33
+ const resolution = Math.max(8, Math.floor(options.resolution ?? 128));
34
+ const palette = { ...DEFAULT_MINIMAP_PALETTE, ...options.palette };
35
+ const low = parseHex(palette.low);
36
+ const high = parseHex(palette.high);
37
+ const water = parseHex(palette.water);
38
+ const { minX, minZ, maxX, maxZ } = source.bounds;
39
+ const spanX = Math.max(1e-6, maxX - minX);
40
+ const spanZ = Math.max(1e-6, maxZ - minZ);
41
+ // Longer world axis gets `resolution` pixels; the other keeps aspect.
42
+ const width = spanX >= spanZ ? resolution : Math.max(8, Math.round((spanX / spanZ) * resolution));
43
+ const height = spanZ >= spanX ? resolution : Math.max(8, Math.round((spanZ / spanX) * resolution));
44
+ const worldX = (px) => minX + ((px + 0.5) / width) * spanX;
45
+ const worldZ = (py) => minZ + ((py + 0.5) / height) * spanZ;
46
+ // First pass: height range for a stable ramp.
47
+ let hMin = Infinity;
48
+ let hMax = -Infinity;
49
+ const heights = new Float64Array(width * height);
50
+ for (let py = 0; py < height; py += 1) {
51
+ for (let px = 0; px < width; px += 1) {
52
+ const h = source.sampleHeight(worldX(px), worldZ(py));
53
+ heights[py * width + px] = h;
54
+ if (h < hMin)
55
+ hMin = h;
56
+ if (h > hMax)
57
+ hMax = h;
58
+ }
59
+ }
60
+ const hSpan = Math.max(1e-6, hMax - hMin);
61
+ const pixels = new Uint8ClampedArray(width * height * 4);
62
+ const zones = source.zones ?? [];
63
+ for (let py = 0; py < height; py += 1) {
64
+ for (let px = 0; px < width; px += 1) {
65
+ const wx = worldX(px);
66
+ const wz = worldZ(py);
67
+ const h = heights[py * width + px];
68
+ let color;
69
+ if (source.waterLevel !== undefined && h <= source.waterLevel) {
70
+ color = water;
71
+ }
72
+ else {
73
+ color = mix(low, high, (h - hMin) / hSpan);
74
+ if (source.sampleNormal !== undefined) {
75
+ const n = source.sampleNormal(wx, wz);
76
+ const shade = 0.62 + 0.38 * Math.max(0, Math.min(1, n[1]));
77
+ color = { r: Math.round(color.r * shade), g: Math.round(color.g * shade), b: Math.round(color.b * shade) };
78
+ }
79
+ }
80
+ for (const zone of zones) {
81
+ if (pointInPolygon([wx, wz], zone.polygon)) {
82
+ color = mix(color, parseHex(zone.color), zone.alpha ?? 0.5);
83
+ }
84
+ }
85
+ const index = (py * width + px) * 4;
86
+ pixels[index] = color.r;
87
+ pixels[index + 1] = color.g;
88
+ pixels[index + 2] = color.b;
89
+ pixels[index + 3] = 255;
90
+ }
91
+ }
92
+ return { width, height, pixels, bounds: { minX, minZ, maxX, maxZ } };
93
+ }
94
+ // --- Minimal pure PNG encoder (RGBA, stored/uncompressed DEFLATE) ------------------------------
95
+ const CRC_TABLE = (() => {
96
+ const table = new Uint32Array(256);
97
+ for (let n = 0; n < 256; n += 1) {
98
+ let c = n;
99
+ for (let k = 0; k < 8; k += 1)
100
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
101
+ table[n] = c >>> 0;
102
+ }
103
+ return table;
104
+ })();
105
+ function crc32(bytes) {
106
+ let c = 0xffffffff;
107
+ for (let i = 0; i < bytes.length; i += 1)
108
+ c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
109
+ return (c ^ 0xffffffff) >>> 0;
110
+ }
111
+ function adler32(bytes) {
112
+ let a = 1;
113
+ let b = 0;
114
+ for (let i = 0; i < bytes.length; i += 1) {
115
+ a = (a + bytes[i]) % 65521;
116
+ b = (b + a) % 65521;
117
+ }
118
+ return ((b << 16) | a) >>> 0;
119
+ }
120
+ /** Wraps raw bytes in a zlib stream using only stored (uncompressed) DEFLATE blocks. */
121
+ function zlibStore(data) {
122
+ const blocks = [0x78, 0x01]; // zlib header (no compression)
123
+ let offset = 0;
124
+ while (offset < data.length) {
125
+ const chunk = Math.min(0xffff, data.length - offset);
126
+ const final = offset + chunk >= data.length ? 1 : 0;
127
+ blocks.push(final, chunk & 0xff, (chunk >> 8) & 0xff, ~chunk & 0xff, (~chunk >> 8) & 0xff);
128
+ for (let i = 0; i < chunk; i += 1)
129
+ blocks.push(data[offset + i]);
130
+ offset += chunk;
131
+ }
132
+ const checksum = adler32(data);
133
+ blocks.push((checksum >>> 24) & 0xff, (checksum >>> 16) & 0xff, (checksum >>> 8) & 0xff, checksum & 0xff);
134
+ return Uint8Array.from(blocks);
135
+ }
136
+ function chunk(type, data) {
137
+ const typeBytes = Uint8Array.from([...type].map((c) => c.charCodeAt(0)));
138
+ const body = new Uint8Array(typeBytes.length + data.length);
139
+ body.set(typeBytes, 0);
140
+ body.set(data, typeBytes.length);
141
+ const crc = crc32(body);
142
+ const out = new Uint8Array(4 + body.length + 4);
143
+ const view = new DataView(out.buffer);
144
+ view.setUint32(0, data.length);
145
+ out.set(body, 4);
146
+ view.setUint32(4 + body.length, crc);
147
+ return out;
148
+ }
149
+ function base64(bytes) {
150
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
151
+ let out = "";
152
+ for (let i = 0; i < bytes.length; i += 3) {
153
+ const b0 = bytes[i];
154
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
155
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
156
+ out += chars[b0 >> 2];
157
+ out += chars[((b0 & 3) << 4) | (b1 >> 4)];
158
+ out += i + 1 < bytes.length ? chars[((b1 & 15) << 2) | (b2 >> 6)] : "=";
159
+ out += i + 2 < bytes.length ? chars[b2 & 63] : "=";
160
+ }
161
+ return out;
162
+ }
163
+ /** Encodes an RGBA bake to raw PNG bytes (pure; stored DEFLATE, valid for any decoder). */
164
+ export function encodeBakePng(bake) {
165
+ const { width, height, pixels } = bake;
166
+ const ihdr = new Uint8Array(13);
167
+ const ihdrView = new DataView(ihdr.buffer);
168
+ ihdrView.setUint32(0, width);
169
+ ihdrView.setUint32(4, height);
170
+ ihdr[8] = 8; // bit depth
171
+ ihdr[9] = 6; // color type RGBA
172
+ // 10,11,12 = compression/filter/interlace = 0
173
+ // Filtered scanlines: filter byte 0 (none) per row.
174
+ const raw = new Uint8Array(height * (1 + width * 4));
175
+ for (let y = 0; y < height; y += 1) {
176
+ const rowStart = y * (1 + width * 4);
177
+ raw[rowStart] = 0;
178
+ raw.set(pixels.subarray(y * width * 4, (y + 1) * width * 4), rowStart + 1);
179
+ }
180
+ const signature = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
181
+ const parts = [
182
+ signature,
183
+ chunk("IHDR", ihdr),
184
+ chunk("IDAT", zlibStore(raw)),
185
+ chunk("IEND", new Uint8Array(0)),
186
+ ];
187
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
188
+ const out = new Uint8Array(total);
189
+ let offset = 0;
190
+ for (const part of parts) {
191
+ out.set(part, offset);
192
+ offset += part.length;
193
+ }
194
+ return out;
195
+ }
196
+ /**
197
+ * Encodes a bake as a `data:image/png;base64,...` URI — the exact string the `Minimap` /
198
+ * `WorldMap` `background` prop takes. Deterministic for a fixed bake.
199
+ * @capability minimap-bake encode a baked minimap as a PNG data URI for the Minimap background
200
+ */
201
+ export function minimapBakeToPngDataUri(bake) {
202
+ return `data:image/png;base64,${base64(encodeBakePng(bake))}`;
203
+ }
204
+ /** One-call convenience: rasterize a source and return `{ background, mapBounds }` for a minimap. */
205
+ export function bakeMinimapBackground(source, options = {}) {
206
+ const bake = bakeMinimapImage(source, options);
207
+ return { background: minimapBakeToPngDataUri(bake), mapBounds: bake.bounds };
208
+ }
@@ -0,0 +1,17 @@
1
+ import type { WorldXZ } from "./minimap.js";
2
+ /** A world-space segment defining a linear "track" minimap — points are projected onto the `from`→`to` axis. */
3
+ export interface TrackAxis {
4
+ from: WorldXZ;
5
+ to: WorldXZ;
6
+ }
7
+ /**
8
+ * Project a world point onto the `from`→`to` segment of `axis` and return the
9
+ * clamped 0..1 fraction of how far along the track it lies (0 at `from`, 1 at
10
+ * `to`). The linear analogue of `projectToMinimap` — feeds a horizontal
11
+ * corridor/route progress rail. Accepts XZ (`[x, z]`) or XYZ (`[x, y, z]`)
12
+ * points (index 0 = x, index 2 when length 3 else index 1 = z). A zero-length
13
+ * axis returns 0.
14
+ *
15
+ * @capability minimap-track project a world point to a 0..1 fraction along a linear track axis
16
+ */
17
+ export declare function trackFraction(point: WorldXZ | readonly [number, number, number], axis: TrackAxis): number;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Project a world point onto the `from`→`to` segment of `axis` and return the
3
+ * clamped 0..1 fraction of how far along the track it lies (0 at `from`, 1 at
4
+ * `to`). The linear analogue of `projectToMinimap` — feeds a horizontal
5
+ * corridor/route progress rail. Accepts XZ (`[x, z]`) or XYZ (`[x, y, z]`)
6
+ * points (index 0 = x, index 2 when length 3 else index 1 = z). A zero-length
7
+ * axis returns 0.
8
+ *
9
+ * @capability minimap-track project a world point to a 0..1 fraction along a linear track axis
10
+ */
11
+ export function trackFraction(point, axis) {
12
+ const px = point[0];
13
+ const pz = point.length === 3 ? point[2] : point[1];
14
+ const ax = axis.to[0] - axis.from[0];
15
+ const az = axis.to[1] - axis.from[1];
16
+ const lengthSq = ax * ax + az * az;
17
+ if (lengthSq === 0)
18
+ return 0;
19
+ const dot = (px - axis.from[0]) * ax + (pz - axis.from[1]) * az;
20
+ const fraction = dot / lengthSq;
21
+ return fraction < 0 ? 0 : fraction > 1 ? 1 : fraction;
22
+ }