@jgengine/core 0.9.0 → 0.10.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 (114) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/ai/spawnDirector.d.ts +13 -0
  3. package/dist/ai/spawnDirector.js +40 -16
  4. package/dist/ai/spawnPoint.d.ts +5 -0
  5. package/dist/ai/spawnPoint.js +5 -0
  6. package/dist/audio/audioFalloff.d.ts +5 -2
  7. package/dist/audio/music.d.ts +53 -0
  8. package/dist/audio/music.js +34 -0
  9. package/dist/audio/synth.d.ts +60 -0
  10. package/dist/audio/synth.js +8 -0
  11. package/dist/authoring.d.ts +14 -0
  12. package/dist/authoring.js +14 -0
  13. package/dist/cartridge/runtime.js +2 -2
  14. package/dist/combat/dotField.d.ts +43 -0
  15. package/dist/combat/dotField.js +48 -0
  16. package/dist/combat/projectiles.d.ts +2 -2
  17. package/dist/combat/projectiles.js +18 -5
  18. package/dist/combat/regenShield.d.ts +35 -0
  19. package/dist/combat/regenShield.js +45 -0
  20. package/dist/combat/shotOrigin.d.ts +13 -1
  21. package/dist/combat/shotOrigin.js +31 -2
  22. package/dist/editor/commands.d.ts +65 -0
  23. package/dist/editor/commands.js +193 -0
  24. package/dist/editor/document.d.ts +40 -0
  25. package/dist/editor/document.js +153 -0
  26. package/dist/editor/index.d.ts +30 -0
  27. package/dist/editor/index.js +6 -0
  28. package/dist/editor/types.d.ts +69 -0
  29. package/dist/editor/types.js +29 -0
  30. package/dist/game/connectedPlayers.d.ts +28 -0
  31. package/dist/game/connectedPlayers.js +21 -0
  32. package/dist/game/defineGame.d.ts +42 -1
  33. package/dist/game/defineGame.js +1 -0
  34. package/dist/game/events.d.ts +15 -0
  35. package/dist/game/keyValueStore.d.ts +25 -0
  36. package/dist/game/keyValueStore.js +61 -0
  37. package/dist/game/levelSequence.d.ts +24 -2
  38. package/dist/game/levelSequence.js +51 -2
  39. package/dist/game/lootTable.d.ts +4 -0
  40. package/dist/game/lootTable.js +1 -0
  41. package/dist/game/modelAnimation.d.ts +8 -0
  42. package/dist/game/modelAnimation.js +18 -0
  43. package/dist/game/playableGame.d.ts +68 -5
  44. package/dist/game/recordBook.d.ts +3 -5
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.js +7 -0
  47. package/dist/input/actionBindings.d.ts +2 -0
  48. package/dist/input/actionBindings.js +1 -0
  49. package/dist/input/axisInput.d.ts +7 -0
  50. package/dist/input/axisInput.js +16 -0
  51. package/dist/interaction/contextMenu.d.ts +1 -0
  52. package/dist/interaction/contextMenu.js +1 -0
  53. package/dist/interaction/proximityPrompt.d.ts +3 -1
  54. package/dist/interaction/proximityPrompt.js +3 -3
  55. package/dist/meta/changelog.d.ts +1 -1
  56. package/dist/meta/changelog.js +22 -1
  57. package/dist/movement/movementModel.d.ts +4 -0
  58. package/dist/movement/movementModel.js +13 -0
  59. package/dist/movement/playerMovement.d.ts +41 -0
  60. package/dist/movement/playerMovement.js +181 -0
  61. package/dist/random/rng.d.ts +2 -0
  62. package/dist/random/rng.js +2 -0
  63. package/dist/render/postProcessing.d.ts +56 -0
  64. package/dist/render/postProcessing.js +1 -0
  65. package/dist/runtime/adapter.d.ts +19 -0
  66. package/dist/runtime/adapter.js +11 -5
  67. package/dist/runtime/gameContext.d.ts +53 -10
  68. package/dist/runtime/gameContext.js +192 -54
  69. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  70. package/dist/runtime/hostedGameRunner.js +62 -0
  71. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  72. package/dist/runtime/hostedWorldSession.js +57 -0
  73. package/dist/runtime/inputSnapshot.d.ts +13 -0
  74. package/dist/runtime/inputSnapshot.js +2 -0
  75. package/dist/runtime/motionIntents.d.ts +2 -0
  76. package/dist/runtime/motionIntents.js +11 -0
  77. package/dist/runtime/worldChannel.d.ts +58 -0
  78. package/dist/runtime/worldChannel.js +69 -0
  79. package/dist/runtime/worldMirror.d.ts +24 -0
  80. package/dist/runtime/worldMirror.js +31 -0
  81. package/dist/runtime/worldReplication.d.ts +46 -0
  82. package/dist/runtime/worldReplication.js +185 -0
  83. package/dist/runtime/worldSnapshot.d.ts +17 -0
  84. package/dist/runtime/worldSnapshot.js +15 -0
  85. package/dist/scene/assetCatalog.d.ts +1 -0
  86. package/dist/scene/colliders.d.ts +6 -0
  87. package/dist/scene/colliders.js +25 -1
  88. package/dist/scene/entityStats.d.ts +4 -0
  89. package/dist/scene/entityStats.js +21 -0
  90. package/dist/scene/entityStore.d.ts +29 -0
  91. package/dist/scene/entityStore.js +60 -0
  92. package/dist/store/observableKeyedStore.d.ts +4 -0
  93. package/dist/store/observableKeyedStore.js +9 -0
  94. package/dist/tactics/fallingGrid.d.ts +2 -2
  95. package/dist/tactics/fallingGrid.js +2 -2
  96. package/dist/ui/gameLayout.d.ts +125 -0
  97. package/dist/ui/gameLayout.js +129 -0
  98. package/dist/ui/orientation.d.ts +39 -0
  99. package/dist/ui/orientation.js +49 -0
  100. package/dist/ui/swingTimer.d.ts +42 -0
  101. package/dist/ui/swingTimer.js +38 -0
  102. package/dist/visibility/spatialIndex.js +34 -20
  103. package/dist/world/environmentSummary.d.ts +11 -2
  104. package/dist/world/environmentSummary.js +11 -1
  105. package/dist/world/features.d.ts +134 -5
  106. package/dist/world/features.js +40 -3
  107. package/dist/world/roads.d.ts +47 -0
  108. package/dist/world/roads.js +146 -0
  109. package/dist/world/streets.d.ts +80 -0
  110. package/dist/world/streets.js +144 -0
  111. package/dist/world/terrain.d.ts +32 -5
  112. package/dist/world/terrain.js +124 -12
  113. package/llms.txt +485 -79
  114. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ /** The local player's auto-attack state for one frame. `swingTimer` counts DOWN in seconds to 0 (= a swing lands). */
2
+ export interface SwingPlayerInput {
3
+ autoAttack: boolean;
4
+ swingTimer: number;
5
+ weapon: {
6
+ speed: number;
7
+ };
8
+ }
9
+ /** The current target, or the fields the bar needs from it. */
10
+ export interface SwingTargetInput {
11
+ dead: boolean;
12
+ /** Entity kind; an `"object"` target (a node/chest) never shows a swing bar. */
13
+ kind: string;
14
+ }
15
+ /** Whether the bar shows a "ready" label or the seconds-remaining countdown. */
16
+ export type SwingLabelKind = "ready" | "seconds";
17
+ /** One frame of swing-bar state plus the cursor (`nextPeriod`/`nextTimer`) the caller carries back into the next call. */
18
+ export interface SwingTimerState {
19
+ /** Whether the bar should render at all. */
20
+ visible: boolean;
21
+ /** Fill 0..1, growing toward 1 as the next swing nears. */
22
+ frac: number;
23
+ /** True the moment the swing is ready to land. */
24
+ ready: boolean;
25
+ labelKind: SwingLabelKind;
26
+ /** Seconds remaining until the next swing (0 when ready). */
27
+ seconds: number;
28
+ /** Recovered swing period this frame — pass back as `prevPeriod` next call. */
29
+ nextPeriod: number;
30
+ /** This frame's timer — pass back as `prevTimer` next call to detect the reset edge. */
31
+ nextTimer: number;
32
+ }
33
+ /** Threshold above which a timer jump is treated as a fresh swing (period reset). */
34
+ export declare const SWING_EDGE_EPSILON = 0.0001;
35
+ /**
36
+ * Pure swing-timer bar state — no hidden state, no clock, no DOM. The caller
37
+ * threads `prevPeriod`/`prevTimer` back each frame. The period is recovered on
38
+ * the reset edge (when `swingTimer` jumps up = a new swing began) as
39
+ * `max(swingTimer, weapon.speed)`, so the fill is correct even without knowing
40
+ * the weapon's exact cadence. Hidden unless auto-attacking a live, non-object target.
41
+ */
42
+ export declare function swingTimerState(player: SwingPlayerInput, target: SwingTargetInput | null, prevPeriod: number, prevTimer: number): SwingTimerState;
@@ -0,0 +1,38 @@
1
+ /** Threshold above which a timer jump is treated as a fresh swing (period reset). */
2
+ export const SWING_EDGE_EPSILON = 1e-4;
3
+ function clamp01(v) {
4
+ return v < 0 ? 0 : v > 1 ? 1 : v;
5
+ }
6
+ const HIDDEN = {
7
+ visible: false,
8
+ frac: 0,
9
+ ready: false,
10
+ labelKind: "seconds",
11
+ seconds: 0,
12
+ };
13
+ /**
14
+ * Pure swing-timer bar state — no hidden state, no clock, no DOM. The caller
15
+ * threads `prevPeriod`/`prevTimer` back each frame. The period is recovered on
16
+ * the reset edge (when `swingTimer` jumps up = a new swing began) as
17
+ * `max(swingTimer, weapon.speed)`, so the fill is correct even without knowing
18
+ * the weapon's exact cadence. Hidden unless auto-attacking a live, non-object target.
19
+ */
20
+ export function swingTimerState(player, target, prevPeriod, prevTimer) {
21
+ const swingTimer = player.swingTimer;
22
+ if (!player.autoAttack || target === null || target.dead || target.kind === "object") {
23
+ return { ...HIDDEN, nextPeriod: prevPeriod, nextTimer: swingTimer };
24
+ }
25
+ const isResetEdge = swingTimer > prevTimer + SWING_EDGE_EPSILON || prevPeriod <= 0;
26
+ const period = isResetEdge ? Math.max(swingTimer, player.weapon.speed) : prevPeriod;
27
+ const frac = period <= 0 ? 1 : clamp01(1 - swingTimer / period);
28
+ const ready = swingTimer <= 0;
29
+ return {
30
+ visible: true,
31
+ frac,
32
+ ready,
33
+ labelKind: ready ? "ready" : "seconds",
34
+ seconds: Math.max(0, swingTimer),
35
+ nextPeriod: period,
36
+ nextTimer: swingTimer,
37
+ };
38
+ }
@@ -33,10 +33,10 @@ export function createSpatialIndex(options = {}) {
33
33
  const key = packCell(x, y, z);
34
34
  let bucket = cells.get(key);
35
35
  if (bucket === undefined) {
36
- bucket = new Set();
36
+ bucket = { x, y, z, ids: new Set() };
37
37
  cells.set(key, bucket);
38
38
  }
39
- bucket.add(id);
39
+ bucket.ids.add(id);
40
40
  }
41
41
  }
42
42
  }
@@ -49,8 +49,8 @@ export function createSpatialIndex(options = {}) {
49
49
  const bucket = cells.get(key);
50
50
  if (bucket === undefined)
51
51
  continue;
52
- bucket.delete(id);
53
- if (bucket.size === 0)
52
+ bucket.ids.delete(id);
53
+ if (bucket.ids.size === 0)
54
54
  cells.delete(key);
55
55
  }
56
56
  }
@@ -69,17 +69,20 @@ export function createSpatialIndex(options = {}) {
69
69
  addToCells(id, range);
70
70
  entries.set(id, { range, dynamic });
71
71
  }
72
- function collectCell(key, out) {
73
- const bucket = cells.get(key);
74
- if (bucket === undefined)
75
- return;
76
- for (const id of bucket) {
72
+ function collectBucket(bucket, out) {
73
+ for (const id of bucket.ids) {
77
74
  if (stamps.get(id) === queryStamp)
78
75
  continue;
79
76
  stamps.set(id, queryStamp);
80
77
  out.push(id);
81
78
  }
82
79
  }
80
+ function collectCell(key, out) {
81
+ const bucket = cells.get(key);
82
+ if (bucket === undefined)
83
+ return;
84
+ collectBucket(bucket, out);
85
+ }
83
86
  function collectOversized(out) {
84
87
  for (const id of oversized) {
85
88
  if (stamps.get(id) === queryStamp)
@@ -93,12 +96,25 @@ export function createSpatialIndex(options = {}) {
93
96
  out.length = 0;
94
97
  const x0 = cellOf(minX, size), y0 = cellOf(minY, size), z0 = cellOf(minZ, size);
95
98
  const x1 = cellOf(maxX, size), y1 = cellOf(maxY, size), z1 = cellOf(maxZ, size);
96
- for (let x = x0; x <= x1; x += 1) {
97
- for (let y = y0; y <= y1; y += 1) {
98
- for (let z = z0; z <= z1; z += 1) {
99
- if (cellFilter !== undefined && !cellFilter(x, y, z))
100
- continue;
101
- collectCell(packCell(x, y, z), out);
99
+ const rangeCellCount = (x1 - x0 + 1) * (y1 - y0 + 1) * (z1 - z0 + 1);
100
+ if (rangeCellCount > cells.size) {
101
+ for (const bucket of cells.values()) {
102
+ if (bucket.x < x0 || bucket.x > x1 || bucket.y < y0 || bucket.y > y1 || bucket.z < z0 || bucket.z > z1) {
103
+ continue;
104
+ }
105
+ if (cellFilter !== undefined && !cellFilter(bucket.x, bucket.y, bucket.z))
106
+ continue;
107
+ collectBucket(bucket, out);
108
+ }
109
+ }
110
+ else {
111
+ for (let x = x0; x <= x1; x += 1) {
112
+ for (let y = y0; y <= y1; y += 1) {
113
+ for (let z = z0; z <= z1; z += 1) {
114
+ if (cellFilter !== undefined && !cellFilter(x, y, z))
115
+ continue;
116
+ collectCell(packCell(x, y, z), out);
117
+ }
102
118
  }
103
119
  }
104
120
  }
@@ -172,13 +188,11 @@ export function createSpatialIndex(options = {}) {
172
188
  cells() {
173
189
  const result = [];
174
190
  for (const [key, bucket] of cells) {
175
- const parts = key.split(":");
176
- const x = Number(parts[0]), y = Number(parts[1]), z = Number(parts[2]);
177
191
  result.push({
178
192
  key,
179
- count: bucket.size,
180
- minX: x * size, minY: y * size, minZ: z * size,
181
- maxX: (x + 1) * size, maxY: (y + 1) * size, maxZ: (z + 1) * size,
193
+ count: bucket.ids.size,
194
+ minX: bucket.x * size, minY: bucket.y * size, minZ: bucket.z * size,
195
+ maxX: (bucket.x + 1) * size, maxY: (bucket.y + 1) * size, maxZ: (bucket.z + 1) * size,
182
196
  });
183
197
  }
184
198
  return result;
@@ -1,5 +1,5 @@
1
1
  import { type BuildingPalette, type BuildingStyle, type GeneratedBuilding } from "./buildings.js";
2
- import type { BuildingEnvironmentDescriptor, EnvironmentWorldFeature, WorldBounds } from "./features.js";
2
+ import type { BuildingEnvironmentDescriptor, EnvironmentArea, EnvironmentWorldFeature, WorldBounds } from "./features.js";
3
3
  import type { Aabb } from "./geometry.js";
4
4
  import { type TerrainPalette } from "./terrain.js";
5
5
  export declare function resolveStructureBuildings(descriptor: BuildingEnvironmentDescriptor): GeneratedBuilding[];
@@ -24,7 +24,7 @@ export interface StructureSummary {
24
24
  bounds: Aabb;
25
25
  }
26
26
  export interface VegetationSummary {
27
- area: WorldBounds;
27
+ area: EnvironmentArea;
28
28
  density: number;
29
29
  }
30
30
  export interface WaterSummary {
@@ -33,8 +33,15 @@ export interface WaterSummary {
33
33
  }
34
34
  export interface WeatherSummary {
35
35
  kind: "rain" | "snow";
36
+ area: EnvironmentArea;
36
37
  density: number;
37
38
  }
39
+ /** One road descriptor's resolved footprint: vertex count, width, and total centerline length. */
40
+ export interface RoadSummary {
41
+ points: number;
42
+ width: number;
43
+ length: number;
44
+ }
38
45
  export interface EnvironmentCounts {
39
46
  terrain: number;
40
47
  islands: number;
@@ -44,6 +51,7 @@ export interface EnvironmentCounts {
44
51
  vegetationFields: number;
45
52
  waterBodies: number;
46
53
  weatherSystems: number;
54
+ roads: number;
47
55
  }
48
56
  export interface IslandSummary extends TerrainSummary {
49
57
  origin: readonly [number, number];
@@ -55,6 +63,7 @@ export interface EnvironmentSummary {
55
63
  vegetation: readonly VegetationSummary[];
56
64
  water: readonly WaterSummary[];
57
65
  weather: readonly WeatherSummary[];
66
+ roads: readonly RoadSummary[];
58
67
  counts: EnvironmentCounts;
59
68
  isEmpty: boolean;
60
69
  }
@@ -1,4 +1,5 @@
1
1
  import { generateBuildingDistrict, resolveBuildingPalette, } from "./buildings.js";
2
+ import { pathLength } from "./roads.js";
2
3
  import { resolveTerrainField, resolveTerrainPalette } from "./terrain.js";
3
4
  export function resolveStructureBuildings(descriptor) {
4
5
  const columns = Math.max(1, Math.ceil(Math.sqrt(descriptor.count)));
@@ -106,8 +107,14 @@ export function summarizeEnvironment(feature) {
106
107
  }));
107
108
  const weather = (feature.weather ?? []).map((entry) => ({
108
109
  kind: entry.kind,
110
+ area: entry.area,
109
111
  density: entry.density,
110
112
  }));
113
+ const roads = (feature.roads ?? []).map((entry) => ({
114
+ points: entry.path.length,
115
+ width: entry.width,
116
+ length: pathLength(entry.path),
117
+ }));
111
118
  const buildings = structures.reduce((sum, entry) => sum + entry.buildings, 0);
112
119
  const counts = {
113
120
  terrain: terrain === undefined ? 0 : 1,
@@ -118,13 +125,15 @@ export function summarizeEnvironment(feature) {
118
125
  vegetationFields: vegetation.length,
119
126
  waterBodies: water.length,
120
127
  weatherSystems: weather.length,
128
+ roads: roads.length,
121
129
  };
122
130
  const isEmpty = counts.terrain === 0 &&
123
131
  counts.islands === 0 &&
124
132
  buildings === 0 &&
125
133
  counts.vegetationFields === 0 &&
126
134
  counts.waterBodies === 0 &&
127
- counts.weatherSystems === 0;
135
+ counts.weatherSystems === 0 &&
136
+ counts.roads === 0;
128
137
  return {
129
138
  ...(terrain === undefined ? {} : { terrain }),
130
139
  islands,
@@ -132,6 +141,7 @@ export function summarizeEnvironment(feature) {
132
141
  vegetation,
133
142
  water,
134
143
  weather,
144
+ roads,
135
145
  counts,
136
146
  isEmpty,
137
147
  };
@@ -15,6 +15,34 @@ export interface TerrainColors {
15
15
  high?: string;
16
16
  waterline?: string;
17
17
  }
18
+ /**
19
+ * Procedural detail-surface layer for terrain: a noise-driven shader that keeps the
20
+ * biome-tinted base ground (from `colors`/`biomeBands`) and blends distinct rock,
21
+ * sand, and snow over it by slope, height, and waterline — turning a flat
22
+ * vertex-colour surface into varied, textured-reading ground with no image assets.
23
+ */
24
+ export interface TerrainDetailConfig {
25
+ /** Steep-slope rock colour. Default "#6f7175". */
26
+ rockColor?: string;
27
+ /** Waterline/beach sand colour. Default "#c2b283". */
28
+ sandColor?: string;
29
+ /** High-altitude snow colour. Default "#eef3f7". */
30
+ snowColor?: string;
31
+ /** Slope (0 flat … 1 vertical) at which rock starts taking over. Default 0.42. */
32
+ rockSlopeStart?: number;
33
+ /** World height above which snow appears. Default 26. */
34
+ snowHeight?: number;
35
+ /** Water level for the sand band; defaults to the terrain's `waterLevel`. */
36
+ waterLevel?: number;
37
+ /** Fine detail noise wavelength in world units. Default 5. */
38
+ detailScale?: number;
39
+ /** Large-scale colour-variation wavelength in world units. Default 45. */
40
+ macroScale?: number;
41
+ /** Surface roughness of the detail material. Default 0.9. */
42
+ roughness?: number;
43
+ /** Overall detail intensity 0..1. Default 1. */
44
+ strength?: number;
45
+ }
18
46
  export interface TerrainFlattenMask {
19
47
  center: EnvironmentVec2;
20
48
  radius: number;
@@ -23,16 +51,59 @@ export interface TerrainFlattenMask {
23
51
  /** Extra blend-ring width outside `radius` where the flat height smoothsteps back to the noise value; default `radius * 0.5`. */
24
52
  falloff?: number;
25
53
  }
26
- /** A circular palette zone painted over the base terrain palette — snow caps, ash wastes, sand shores on one heightfield. */
27
- export interface TerrainMaterialRegion {
28
- center: EnvironmentVec2;
29
- radius: number;
54
+ /** Palette and blend fields shared by every `TerrainMaterialRegion` shape. */
55
+ export interface TerrainRegionStyle {
30
56
  /** Named palette preset for this region (see `TERRAIN_MATERIAL_PALETTES` in `world/terrain`); overridden field-by-field by `colors`. */
31
57
  material?: TerrainMaterial;
32
58
  colors?: TerrainColors;
33
- /** Blend-ring width outside `radius` back to the surrounding palette; default `radius * 0.5`. */
59
+ /** Blend-ring width outside the region core back to the surrounding palette; default: half the region's characteristic size. */
34
60
  falloff?: number;
35
61
  }
62
+ /** A circular palette zone painted over the base terrain palette — snow caps, ash wastes, spawn circles. */
63
+ export interface TerrainCircleRegion extends TerrainRegionStyle {
64
+ shape?: "circle";
65
+ center: EnvironmentVec2;
66
+ radius: number;
67
+ }
68
+ /** A ribbon palette zone following a centerline — roads and rivers, instead of chaining overlapping circles. */
69
+ export interface TerrainPolylineRegion extends TerrainRegionStyle {
70
+ shape: "polyline";
71
+ /** Centerline vertices in world XZ; needs at least two points. */
72
+ points: readonly EnvironmentVec2[];
73
+ /** Full ribbon width; the painted core extends `width / 2` either side of the centerline. `falloff` defaults to `width * 0.5`. */
74
+ width: number;
75
+ }
76
+ /** A rectangular palette zone, optionally rotated about the world y axis — plazas, fields, districts. */
77
+ export interface TerrainRectRegion extends TerrainRegionStyle {
78
+ shape: "rect";
79
+ center: EnvironmentVec2;
80
+ /** Half-extents along the region's local x and z before rotation. `falloff` defaults to half the smaller extent. */
81
+ halfExtents: EnvironmentVec2;
82
+ /** Rotation about the world y axis in radians; default 0. */
83
+ rotationY?: number;
84
+ }
85
+ /**
86
+ * A palette zone painted over the base terrain palette. Circle (the default when no `shape` is given),
87
+ * `polyline` ribbons for roads/rivers, and rotatable `rect` districts all paint fully inside their core
88
+ * and blend back across `falloff`; later regions in the list win overlaps.
89
+ */
90
+ export type TerrainMaterialRegion = TerrainCircleRegion | TerrainPolylineRegion | TerrainRectRegion;
91
+ /**
92
+ * A z-ordered ground palette zone — the linear-boundary counterpart to the radial `materialRegions`.
93
+ * Adjacent bands cross-fade into each other across a `fade`-wide window centered on the midpoint z
94
+ * between their centers, so a multi-biome world (vale → marsh → peaks along z) blends its ground
95
+ * color instead of hard-switching. Order the list by ascending `z`.
96
+ */
97
+ export interface BiomeBand {
98
+ /** World-space center z of the zone. */
99
+ z: number;
100
+ /** Cross-fade window width straddling the boundary to the next band. Default 64. */
101
+ fade?: number;
102
+ /** Named palette preset for the zone (see `TERRAIN_MATERIAL_PALETTES`); overridden field-by-field by `colors`. */
103
+ material?: TerrainMaterial;
104
+ /** Explicit low/high/waterline hex colors for the zone. */
105
+ colors?: TerrainColors;
106
+ }
36
107
  export interface TerrainEnvironmentConfig {
37
108
  bounds?: WorldBounds;
38
109
  height?: number;
@@ -49,6 +120,8 @@ export interface TerrainEnvironmentConfig {
49
120
  colors?: TerrainColors;
50
121
  /** Palette zones blended over the base `material`/`colors` for multi-biome readability. */
51
122
  materialRegions?: readonly TerrainMaterialRegion[];
123
+ /** Ordered z-banded ground palettes that cross-fade along the world's z axis (vale → marsh → peaks). Painted under `materialRegions`. */
124
+ biomeBands?: readonly BiomeBand[];
52
125
  segments?: number;
53
126
  seed?: string;
54
127
  frequency?: number;
@@ -58,9 +131,13 @@ export interface TerrainEnvironmentConfig {
58
131
  waterLevel?: number;
59
132
  /** Flat pads carved into the noise field, e.g. for building pads or spawn circles. */
60
133
  flatten?: readonly TerrainFlattenMask[];
134
+ /** Procedural detail surface (noise-driven rock/sand/snow over the biome base). Omit for the flat vertex-colour ground. */
135
+ detail?: TerrainDetailConfig;
61
136
  }
62
137
  export interface RainEnvironmentConfig {
63
138
  area?: EnvironmentArea;
139
+ /** World-space center `[x, z]` of the band, matching `building()`/`ocean()` — sugar for `area.position`, letting a biome zone site its own weather away from the origin. */
140
+ position?: EnvironmentVec2;
64
141
  density?: number;
65
142
  speed?: number;
66
143
  dropLength?: number;
@@ -69,6 +146,8 @@ export interface RainEnvironmentConfig {
69
146
  }
70
147
  export interface SnowEnvironmentConfig {
71
148
  area?: EnvironmentArea;
149
+ /** World-space center `[x, z]` of the band, matching `building()`/`ocean()` — sugar for `area.position`, letting a biome zone site its own weather away from the origin. */
150
+ position?: EnvironmentVec2;
72
151
  density?: number;
73
152
  speed?: number;
74
153
  flakeSize?: number;
@@ -78,6 +157,8 @@ export interface SnowEnvironmentConfig {
78
157
  }
79
158
  export interface GrassEnvironmentConfig {
80
159
  area?: EnvironmentArea;
160
+ /** World-space center `[x, z]` of the band, matching `building()`/`ocean()` — sugar for `area.position`, letting a biome zone site its own vegetation away from the origin. */
161
+ position?: EnvironmentVec2;
81
162
  density?: number;
82
163
  bladeHeight?: readonly [number, number];
83
164
  bladeWidth?: number;
@@ -177,6 +258,36 @@ export type PadEnvironmentDescriptor = {
177
258
  export type SkyEnvironmentDescriptor = {
178
259
  kind: "sky";
179
260
  } & Required<Pick<SkyEnvironmentConfig, "preset" | "timeOfDay">> & Omit<SkyEnvironmentConfig, "preset" | "timeOfDay">;
261
+ /** Config for {@link road}: a flat asphalt ribbon draped over the terrain along a centerline. */
262
+ export interface RoadEnvironmentConfig {
263
+ /** Centerline vertices in world XZ; at least two points. */
264
+ path: readonly (readonly [number, number])[];
265
+ /** Ribbon width in world units. Default 8. */
266
+ width?: number;
267
+ /** Asphalt color. Default "#3b3e47". */
268
+ color?: string;
269
+ /** Paint a dashed centerline. Default true. */
270
+ markings?: boolean;
271
+ /** Centerline dash color. Default "#e8c74a". */
272
+ markingColor?: string;
273
+ /** Lift above the terrain to avoid z-fighting; stagger overlapping roads. Default 0.08. */
274
+ elevation?: number;
275
+ /** Sidewalk bands on both edges; `false` for none. Default `{ width: 2.6, color: "#a7adb8" }`. */
276
+ sidewalk?: {
277
+ width?: number;
278
+ color?: string;
279
+ } | false;
280
+ }
281
+ /** Resolved road descriptor produced by {@link road} and rendered by the shell environment scene. */
282
+ export type RoadEnvironmentDescriptor = {
283
+ kind: "road";
284
+ } & Required<Pick<RoadEnvironmentConfig, "path" | "width" | "color" | "markings" | "markingColor" | "elevation">> & {
285
+ /** Resolved sidewalk band, or `false` when the road has none. */
286
+ sidewalk: {
287
+ width: number;
288
+ color: string;
289
+ } | false;
290
+ };
180
291
  export type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor;
181
292
  export type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor;
182
293
  export type WaterEnvironmentDescriptor = OceanEnvironmentDescriptor;
@@ -191,6 +302,8 @@ export interface EnvironmentWorldConfig {
191
302
  vegetation?: EnvironmentDescriptorList<VegetationEnvironmentDescriptor>;
192
303
  water?: EnvironmentDescriptorList<WaterEnvironmentDescriptor>;
193
304
  structures?: EnvironmentDescriptorList<StructureEnvironmentDescriptor>;
305
+ /** Road ribbons draped over the terrain — see `road()`. */
306
+ roads?: EnvironmentDescriptorList<RoadEnvironmentDescriptor>;
194
307
  /** Ground pads, e.g. platforms or paved patches; each implicitly flattens the terrain beneath it. */
195
308
  pads?: readonly PadEnvironmentDescriptor[];
196
309
  }
@@ -203,6 +316,7 @@ export interface EnvironmentWorldFeature {
203
316
  vegetation?: readonly VegetationEnvironmentDescriptor[];
204
317
  water?: readonly WaterEnvironmentDescriptor[];
205
318
  structures?: readonly StructureEnvironmentDescriptor[];
319
+ roads?: readonly RoadEnvironmentDescriptor[];
206
320
  pads?: readonly PadEnvironmentDescriptor[];
207
321
  }
208
322
  export interface WorldGridCell {
@@ -242,6 +356,7 @@ export interface PlotsWorldConfig extends WorldGridConfig {
242
356
  export interface TilemapWorldConfig extends WorldGridConfig {
243
357
  map: string;
244
358
  }
359
+ /** A declared world shape — biomes, voxel grid, plots, tilemap, environment, or flat — passed to `defineGame`. */
245
360
  export type WorldFeature = ({
246
361
  kind: "biomes";
247
362
  } & BiomesWorldConfig) | ({
@@ -255,18 +370,32 @@ export type WorldFeature = ({
255
370
  };
256
371
  /** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. Elevated pads (absolute `elevation`) float free and carve nothing. */
257
372
  export declare function padFlattenMasks(pads: readonly PadEnvironmentDescriptor[]): readonly TerrainFlattenMask[];
373
+ /** Composes an `environment()` world feature from terrain, sky, weather, vegetation, water, structures, roads, and pads. */
258
374
  export declare function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature;
375
+ /** Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks. */
259
376
  export declare function terrain(config?: TerrainEnvironmentConfig): TerrainEnvironmentDescriptor;
260
377
  export declare function sky(config?: SkyEnvironmentConfig): SkyEnvironmentDescriptor;
378
+ /** Declares a rainfall weather effect for `environment()` — area, density, speed, and wind. */
261
379
  export declare function rain(config?: RainEnvironmentConfig): RainEnvironmentDescriptor;
380
+ /** Declares a snowfall weather effect for `environment()` — area, density, drift, and wind. */
262
381
  export declare function snow(config?: SnowEnvironmentConfig): SnowEnvironmentDescriptor;
382
+ /** Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors. */
263
383
  export declare function grass(config?: GrassEnvironmentConfig): GrassEnvironmentDescriptor;
384
+ /** Declares an ocean water body for `environment()` — bounds, level, and wave tuning. */
264
385
  export declare function ocean(config?: OceanEnvironmentConfig): OceanEnvironmentDescriptor;
386
+ /** Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain. */
387
+ export declare function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor;
265
388
  export declare function island(config: TerrainIslandConfig): TerrainIslandDescriptor;
389
+ /** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. */
266
390
  export declare function building(config?: BuildingEnvironmentConfig): BuildingEnvironmentDescriptor;
267
391
  export declare function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor;
392
+ /** Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain. */
268
393
  export declare function biomes(config: BiomesWorldConfig): WorldFeature;
394
+ /** Declares a voxel-grid world for block-based games. */
269
395
  export declare function voxel(config: VoxelWorldConfig): WorldFeature;
396
+ /** Declares a subdivided-plots world — farming, base-building, and other parcel-based layouts. */
270
397
  export declare function plots(config?: PlotsWorldConfig): WorldFeature;
398
+ /** Declares a 2D tilemap world from a map string. */
271
399
  export declare function tilemap(config: TilemapWorldConfig): WorldFeature;
400
+ /** Declares an empty flat world — the minimal `WorldFeature` for games with no terrain of their own. */
272
401
  export declare function flat(): WorldFeature;
@@ -27,11 +27,13 @@ function withPadFlatten(terrainDescriptor, pads) {
27
27
  flatten: [...(terrainDescriptor.flatten ?? []), ...padFlattenMasks(pads)],
28
28
  };
29
29
  }
30
+ /** Composes an `environment()` world feature from terrain, sky, weather, vegetation, water, structures, roads, and pads. */
30
31
  export function environment(config = {}) {
31
32
  const weather = list(config.weather);
32
33
  const vegetation = list(config.vegetation);
33
34
  const water = list(config.water);
34
35
  const structures = list(config.structures);
36
+ const roads = list(config.roads);
35
37
  const terrainDescriptor = withPadFlatten(config.terrain, config.pads);
36
38
  return {
37
39
  kind: "environment",
@@ -42,9 +44,11 @@ export function environment(config = {}) {
42
44
  ...(vegetation === undefined ? {} : { vegetation }),
43
45
  ...(water === undefined ? {} : { water }),
44
46
  ...(structures === undefined ? {} : { structures }),
47
+ ...(roads === undefined ? {} : { roads }),
45
48
  ...(config.pads === undefined ? {} : { pads: config.pads }),
46
49
  };
47
50
  }
51
+ /** Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks. */
48
52
  export function terrain(config = {}) {
49
53
  if (config.baseHeight !== undefined &&
50
54
  config.height === undefined &&
@@ -62,6 +66,7 @@ export function terrain(config = {}) {
62
66
  ...(config.material === undefined ? {} : { material: config.material }),
63
67
  ...(config.colors === undefined ? {} : { colors: config.colors }),
64
68
  ...(config.materialRegions === undefined ? {} : { materialRegions: config.materialRegions }),
69
+ ...(config.biomeBands === undefined ? {} : { biomeBands: config.biomeBands }),
65
70
  ...(config.segments === undefined ? {} : { segments: config.segments }),
66
71
  ...(config.seed === undefined ? {} : { seed: config.seed }),
67
72
  ...(config.frequency === undefined ? {} : { frequency: config.frequency }),
@@ -70,6 +75,7 @@ export function terrain(config = {}) {
70
75
  ...(config.baseHeight === undefined ? {} : { baseHeight: config.baseHeight }),
71
76
  ...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
72
77
  ...(config.flatten === undefined ? {} : { flatten: config.flatten }),
78
+ ...(config.detail === undefined ? {} : { detail: config.detail }),
73
79
  });
74
80
  }
75
81
  export function sky(config = {}) {
@@ -85,10 +91,14 @@ export function sky(config = {}) {
85
91
  ...(config.fog === undefined ? {} : { fog: config.fog }),
86
92
  });
87
93
  }
94
+ function withAreaPosition(area, position) {
95
+ return position !== undefined && area.position === undefined ? { ...area, position } : area;
96
+ }
97
+ /** Declares a rainfall weather effect for `environment()` — area, density, speed, and wind. */
88
98
  export function rain(config = {}) {
89
99
  return {
90
100
  kind: "rain",
91
- area: config.area ?? { w: 256, d: 256, h: 80 },
101
+ area: withAreaPosition(config.area ?? { w: 256, d: 256, h: 80 }, config.position),
92
102
  density: config.density ?? 0.65,
93
103
  speed: config.speed ?? 18,
94
104
  dropLength: config.dropLength ?? 0.8,
@@ -96,10 +106,11 @@ export function rain(config = {}) {
96
106
  color: config.color ?? "#9ec8ff",
97
107
  };
98
108
  }
109
+ /** Declares a snowfall weather effect for `environment()` — area, density, drift, and wind. */
99
110
  export function snow(config = {}) {
100
111
  return {
101
112
  kind: "snow",
102
- area: config.area ?? { w: 256, d: 256, h: 80 },
113
+ area: withAreaPosition(config.area ?? { w: 256, d: 256, h: 80 }, config.position),
103
114
  density: config.density ?? 0.35,
104
115
  speed: config.speed ?? 2.4,
105
116
  flakeSize: config.flakeSize ?? 0.08,
@@ -108,10 +119,11 @@ export function snow(config = {}) {
108
119
  color: config.color ?? "#ffffff",
109
120
  };
110
121
  }
122
+ /** Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors. */
111
123
  export function grass(config = {}) {
112
124
  return withOptional({
113
125
  kind: "grass",
114
- area: config.area ?? { w: 128, d: 128 },
126
+ area: withAreaPosition(config.area ?? { w: 128, d: 128 }, config.position),
115
127
  density: config.density ?? 4,
116
128
  bladeHeight: config.bladeHeight ?? [0.25, 0.9],
117
129
  bladeWidth: config.bladeWidth ?? 0.035,
@@ -119,6 +131,7 @@ export function grass(config = {}) {
119
131
  colors: config.colors ?? ["#3f7d2d", "#66a83f"],
120
132
  }, config.seed === undefined ? undefined : { seed: config.seed });
121
133
  }
134
+ /** Declares an ocean water body for `environment()` — bounds, level, and wave tuning. */
122
135
  export function ocean(config = {}) {
123
136
  return {
124
137
  kind: "ocean",
@@ -132,11 +145,30 @@ export function ocean(config = {}) {
132
145
  ...(config.levelAt === undefined ? {} : { levelAt: config.levelAt }),
133
146
  };
134
147
  }
148
+ /** Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain. */
149
+ export function road(config) {
150
+ if (config.path.length < 2) {
151
+ throw new Error("road: path needs at least two points");
152
+ }
153
+ return {
154
+ kind: "road",
155
+ path: config.path,
156
+ width: config.width ?? 8,
157
+ color: config.color ?? "#3b3e47",
158
+ markings: config.markings ?? true,
159
+ markingColor: config.markingColor ?? "#e8c74a",
160
+ elevation: config.elevation ?? 0.08,
161
+ sidewalk: config.sidewalk === false
162
+ ? false
163
+ : { width: config.sidewalk?.width ?? 2.6, color: config.sidewalk?.color ?? "#a7adb8" },
164
+ };
165
+ }
135
166
  export function island(config) {
136
167
  const { origin, ...terrainConfig } = config;
137
168
  const { kind: _kind, ...descriptor } = terrain(terrainConfig);
138
169
  return { ...descriptor, kind: "island", origin };
139
170
  }
171
+ /** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. */
140
172
  export function building(config = {}) {
141
173
  return withOptional({
142
174
  kind: "building",
@@ -164,18 +196,23 @@ export function pad(config) {
164
196
  ...(config.elevation === undefined ? {} : { elevation: config.elevation }),
165
197
  });
166
198
  }
199
+ /** Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain. */
167
200
  export function biomes(config) {
168
201
  return { kind: "biomes", ...config };
169
202
  }
203
+ /** Declares a voxel-grid world for block-based games. */
170
204
  export function voxel(config) {
171
205
  return { kind: "voxel", ...config };
172
206
  }
207
+ /** Declares a subdivided-plots world — farming, base-building, and other parcel-based layouts. */
173
208
  export function plots(config = {}) {
174
209
  return { kind: "plots", ...config };
175
210
  }
211
+ /** Declares a 2D tilemap world from a map string. */
176
212
  export function tilemap(config) {
177
213
  return { kind: "tilemap", ...config };
178
214
  }
215
+ /** Declares an empty flat world — the minimal `WorldFeature` for games with no terrain of their own. */
179
216
  export function flat() {
180
217
  return { kind: "flat" };
181
218
  }