@jgengine/core 0.7.0 → 0.8.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 (159) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/ai/spawnDirector.d.ts +5 -0
  4. package/dist/ai/spawnDirector.js +13 -1
  5. package/dist/anim/easing.d.ts +14 -0
  6. package/dist/anim/easing.js +55 -0
  7. package/dist/behaviour/behaviour.d.ts +136 -0
  8. package/dist/behaviour/behaviour.js +388 -0
  9. package/dist/cards/cardPile.d.ts +1 -0
  10. package/dist/cards/cardPile.js +12 -3
  11. package/dist/combat/abilityKit.d.ts +5 -0
  12. package/dist/combat/abilityKit.js +10 -0
  13. package/dist/combat/effects.d.ts +1 -1
  14. package/dist/combat/effects.js +9 -6
  15. package/dist/combat/projectiles.d.ts +42 -5
  16. package/dist/combat/projectiles.js +98 -12
  17. package/dist/combat/telegraph.d.ts +5 -1
  18. package/dist/combat/telegraph.js +14 -0
  19. package/dist/commands/commandRegistry.d.ts +1 -1
  20. package/dist/commands/commandRegistry.js +2 -1
  21. package/dist/data/dataSource.d.ts +26 -0
  22. package/dist/data/dataSource.js +93 -0
  23. package/dist/data/devProxy.d.ts +11 -0
  24. package/dist/data/devProxy.js +38 -0
  25. package/dist/data/fetchJson.d.ts +20 -0
  26. package/dist/data/fetchJson.js +43 -0
  27. package/dist/data/jsonDataSource.d.ts +4 -0
  28. package/dist/data/jsonDataSource.js +6 -0
  29. package/dist/devtools/devtools.d.ts +157 -0
  30. package/dist/devtools/devtools.js +484 -0
  31. package/dist/devtools/transformTunables.d.ts +14 -0
  32. package/dist/devtools/transformTunables.js +38 -0
  33. package/dist/game/chat.d.ts +66 -0
  34. package/dist/game/chat.js +165 -0
  35. package/dist/game/defineGame.d.ts +8 -2
  36. package/dist/game/defineGame.js +2 -1
  37. package/dist/game/events.d.ts +25 -0
  38. package/dist/game/levelSequence.d.ts +44 -0
  39. package/dist/game/levelSequence.js +78 -0
  40. package/dist/game/playableGame.d.ts +237 -6
  41. package/dist/game/playableGame.js +6 -1
  42. package/dist/game/progression.d.ts +1 -0
  43. package/dist/game/progression.js +23 -2
  44. package/dist/game/race.d.ts +5 -0
  45. package/dist/game/race.js +20 -0
  46. package/dist/game/social.d.ts +42 -0
  47. package/dist/game/social.js +122 -0
  48. package/dist/game/spawnPoints.d.ts +19 -0
  49. package/dist/game/spawnPoints.js +23 -0
  50. package/dist/game/worldItem.d.ts +3 -0
  51. package/dist/game/worldItem.js +12 -6
  52. package/dist/input/actionBindings.d.ts +10 -0
  53. package/dist/input/actionBindings.js +14 -0
  54. package/dist/input/gestureSurface.d.ts +31 -0
  55. package/dist/input/gestureSurface.js +120 -0
  56. package/dist/input/pointer.d.ts +45 -0
  57. package/dist/input/pointer.js +48 -0
  58. package/dist/input/touchScheme.d.ts +83 -0
  59. package/dist/input/touchScheme.js +115 -0
  60. package/dist/item/affix.d.ts +1 -1
  61. package/dist/item/affix.js +1 -19
  62. package/dist/meta/changelog.d.ts +1 -1
  63. package/dist/meta/changelog.js +37 -1
  64. package/dist/movement/movementModel.d.ts +36 -3
  65. package/dist/movement/movementModel.js +81 -5
  66. package/dist/movement/voxelController.d.ts +61 -0
  67. package/dist/movement/voxelController.js +229 -0
  68. package/dist/multiplayer/chatContract.d.ts +42 -0
  69. package/dist/multiplayer/chatContract.js +57 -0
  70. package/dist/multiplayer/identity.d.ts +13 -0
  71. package/dist/multiplayer/identity.js +20 -0
  72. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  73. package/dist/multiplayer/poseSyncGate.js +13 -1
  74. package/dist/multiplayer/presenceModel.d.ts +3 -0
  75. package/dist/multiplayer/presenceModel.js +15 -1
  76. package/dist/multiplayer/voiceContract.d.ts +44 -0
  77. package/dist/multiplayer/voiceContract.js +108 -0
  78. package/dist/nav/navConstrain.d.ts +14 -0
  79. package/dist/nav/navConstrain.js +26 -0
  80. package/dist/nav/navFromEnvironment.d.ts +11 -0
  81. package/dist/nav/navFromEnvironment.js +16 -0
  82. package/dist/nav/navGrid.d.ts +10 -0
  83. package/dist/nav/navGrid.js +18 -1
  84. package/dist/nav/pathFollow.d.ts +4 -2
  85. package/dist/nav/pathFollow.js +5 -3
  86. package/dist/physics/ballisticSweep.d.ts +18 -0
  87. package/dist/physics/ballisticSweep.js +49 -0
  88. package/dist/physics/forceVolume.js +5 -5
  89. package/dist/physics/physicsWorld.d.ts +41 -2
  90. package/dist/physics/physicsWorld.js +232 -40
  91. package/dist/physics/spatialGrid.d.ts +1 -1
  92. package/dist/physics/spatialGrid.js +5 -0
  93. package/dist/physics/vehicleBody.d.ts +6 -0
  94. package/dist/physics/vehicleBody.js +16 -17
  95. package/dist/puzzle/cellGrid.d.ts +26 -0
  96. package/dist/puzzle/cellGrid.js +110 -0
  97. package/dist/puzzle/fallingPiece.d.ts +24 -0
  98. package/dist/puzzle/fallingPiece.js +51 -0
  99. package/dist/random/nameGen.d.ts +18 -0
  100. package/dist/random/nameGen.js +35 -0
  101. package/dist/random/rng.d.ts +2 -0
  102. package/dist/random/rng.js +23 -0
  103. package/dist/runtime/adapter.d.ts +35 -0
  104. package/dist/runtime/adapter.js +37 -1
  105. package/dist/runtime/cameraDirector.d.ts +10 -0
  106. package/dist/runtime/cameraDirector.js +25 -0
  107. package/dist/runtime/gameContext.d.ts +44 -2
  108. package/dist/runtime/gameContext.js +100 -4
  109. package/dist/runtime/hostPersistence.d.ts +21 -0
  110. package/dist/runtime/hostPersistence.js +50 -0
  111. package/dist/runtime/inputSnapshot.d.ts +7 -0
  112. package/dist/runtime/inputSnapshot.js +12 -0
  113. package/dist/runtime/motionIntents.d.ts +25 -0
  114. package/dist/runtime/motionIntents.js +25 -0
  115. package/dist/runtime/transport.d.ts +32 -0
  116. package/dist/scene/entityStore.d.ts +8 -0
  117. package/dist/scene/entityStore.js +25 -2
  118. package/dist/scene/movementSpeed.d.ts +35 -0
  119. package/dist/scene/movementSpeed.js +20 -0
  120. package/dist/scene/objectQuery.d.ts +18 -0
  121. package/dist/scene/objectQuery.js +108 -0
  122. package/dist/scene/objectStore.d.ts +11 -0
  123. package/dist/scene/objectStore.js +109 -4
  124. package/dist/scene/paintLayer.d.ts +15 -0
  125. package/dist/scene/paintLayer.js +53 -0
  126. package/dist/scene/spatial.d.ts +21 -0
  127. package/dist/scene/spatial.js +63 -1
  128. package/dist/sensor/concealment.d.ts +21 -0
  129. package/dist/sensor/concealment.js +63 -0
  130. package/dist/sensor/freezeMonitor.d.ts +17 -0
  131. package/dist/sensor/freezeMonitor.js +35 -0
  132. package/dist/session/roles.d.ts +6 -0
  133. package/dist/session/roles.js +32 -0
  134. package/dist/session/roundState.d.ts +41 -17
  135. package/dist/session/roundState.js +67 -38
  136. package/dist/tactics/fallingGrid.d.ts +62 -0
  137. package/dist/tactics/fallingGrid.js +158 -0
  138. package/dist/tactics/tacticalGrid.d.ts +11 -0
  139. package/dist/tactics/tacticalGrid.js +17 -0
  140. package/dist/time/simClock.d.ts +25 -1
  141. package/dist/time/simClock.js +28 -3
  142. package/dist/turn/intent.d.ts +14 -0
  143. package/dist/turn/intent.js +29 -0
  144. package/dist/turn/turnLoop.d.ts +2 -0
  145. package/dist/turn/turnLoop.js +8 -1
  146. package/dist/world/environmentSummary.d.ts +53 -0
  147. package/dist/world/environmentSummary.js +129 -0
  148. package/dist/world/features.d.ts +87 -6
  149. package/dist/world/features.js +52 -2
  150. package/dist/world/gridInstances.d.ts +13 -0
  151. package/dist/world/gridInstances.js +22 -0
  152. package/dist/world/placementController.d.ts +22 -1
  153. package/dist/world/placementController.js +56 -0
  154. package/dist/world/terrain.d.ts +23 -1
  155. package/dist/world/terrain.js +64 -1
  156. package/dist/world/voxelField.d.ts +54 -0
  157. package/dist/world/voxelField.js +241 -0
  158. package/llms.txt +4102 -0
  159. package/package.json +3 -2
@@ -0,0 +1,53 @@
1
+ import { type GeneratedBuilding } from "./buildings.js";
2
+ import type { BuildingEnvironmentDescriptor, EnvironmentWorldFeature, WorldBounds } from "./features.js";
3
+ import type { Aabb } from "./geometry.js";
4
+ export declare function resolveStructureBuildings(descriptor: BuildingEnvironmentDescriptor): GeneratedBuilding[];
5
+ export interface TerrainHeightStats {
6
+ min: number;
7
+ max: number;
8
+ mean: number;
9
+ finite: boolean;
10
+ }
11
+ export interface TerrainSummary {
12
+ bounds: WorldBounds;
13
+ height: TerrainHeightStats;
14
+ waterLevel?: number;
15
+ }
16
+ export interface StructureSummary {
17
+ style: string;
18
+ requested: number;
19
+ buildings: number;
20
+ parts: number;
21
+ bounds: Aabb;
22
+ }
23
+ export interface VegetationSummary {
24
+ area: WorldBounds;
25
+ density: number;
26
+ }
27
+ export interface WaterSummary {
28
+ level: number;
29
+ bounds: WorldBounds;
30
+ }
31
+ export interface WeatherSummary {
32
+ kind: "rain" | "snow";
33
+ density: number;
34
+ }
35
+ export interface EnvironmentCounts {
36
+ terrain: number;
37
+ structureGroups: number;
38
+ buildings: number;
39
+ buildingParts: number;
40
+ vegetationFields: number;
41
+ waterBodies: number;
42
+ weatherSystems: number;
43
+ }
44
+ export interface EnvironmentSummary {
45
+ terrain?: TerrainSummary;
46
+ structures: readonly StructureSummary[];
47
+ vegetation: readonly VegetationSummary[];
48
+ water: readonly WaterSummary[];
49
+ weather: readonly WeatherSummary[];
50
+ counts: EnvironmentCounts;
51
+ isEmpty: boolean;
52
+ }
53
+ export declare function summarizeEnvironment(feature: EnvironmentWorldFeature): EnvironmentSummary;
@@ -0,0 +1,129 @@
1
+ import { generateBuildingDistrict } from "./buildings.js";
2
+ import { resolveTerrainField } from "./terrain.js";
3
+ export function resolveStructureBuildings(descriptor) {
4
+ const columns = Math.max(1, Math.ceil(Math.sqrt(descriptor.count)));
5
+ const rows = Math.max(1, Math.ceil(descriptor.count / columns));
6
+ const spacing = descriptor.spacing;
7
+ const [centerX, centerZ] = descriptor.position ?? [0, 0];
8
+ const origin = [
9
+ centerX - ((columns - 1) * (descriptor.footprint.w + spacing)) / 2,
10
+ centerZ - ((rows - 1) * (descriptor.footprint.d + spacing)) / 2,
11
+ ];
12
+ return generateBuildingDistrict({
13
+ rows,
14
+ columns,
15
+ origin,
16
+ lotSize: descriptor.footprint,
17
+ streetWidth: spacing,
18
+ floorRange: descriptor.stories,
19
+ base: { floorHeight: descriptor.storyHeight },
20
+ ...(descriptor.seed === undefined ? {} : { seed: descriptor.seed }),
21
+ }).slice(0, descriptor.count);
22
+ }
23
+ const TERRAIN_SAMPLE_RESOLUTION = 16;
24
+ function sampleTerrainHeights(descriptor) {
25
+ const field = resolveTerrainField(descriptor);
26
+ const halfW = descriptor.bounds.w / 2;
27
+ const halfD = descriptor.bounds.d / 2;
28
+ const steps = TERRAIN_SAMPLE_RESOLUTION;
29
+ let min = Infinity;
30
+ let max = -Infinity;
31
+ let sum = 0;
32
+ let finite = true;
33
+ for (let ix = 0; ix <= steps; ix += 1) {
34
+ for (let iz = 0; iz <= steps; iz += 1) {
35
+ const x = -halfW + (descriptor.bounds.w * ix) / steps;
36
+ const z = -halfD + (descriptor.bounds.d * iz) / steps;
37
+ const height = field.sampleHeight(x, z);
38
+ if (!Number.isFinite(height)) {
39
+ finite = false;
40
+ continue;
41
+ }
42
+ if (height < min)
43
+ min = height;
44
+ if (height > max)
45
+ max = height;
46
+ sum += height;
47
+ }
48
+ }
49
+ const count = (steps + 1) * (steps + 1);
50
+ if (!finite)
51
+ return { min: NaN, max: NaN, mean: NaN, finite: false };
52
+ return { min, max, mean: sum / count, finite: true };
53
+ }
54
+ function unionAabb(buildings) {
55
+ if (buildings.length === 0)
56
+ return { minX: 0, minZ: 0, maxX: 0, maxZ: 0 };
57
+ let minX = Infinity;
58
+ let minZ = Infinity;
59
+ let maxX = -Infinity;
60
+ let maxZ = -Infinity;
61
+ for (const building of buildings) {
62
+ if (building.bounds.minX < minX)
63
+ minX = building.bounds.minX;
64
+ if (building.bounds.minZ < minZ)
65
+ minZ = building.bounds.minZ;
66
+ if (building.bounds.maxX > maxX)
67
+ maxX = building.bounds.maxX;
68
+ if (building.bounds.maxZ > maxZ)
69
+ maxZ = building.bounds.maxZ;
70
+ }
71
+ return { minX, minZ, maxX, maxZ };
72
+ }
73
+ function summarizeStructures(descriptor) {
74
+ const buildings = resolveStructureBuildings(descriptor);
75
+ return {
76
+ style: descriptor.style,
77
+ requested: descriptor.count,
78
+ buildings: buildings.length,
79
+ parts: buildings.reduce((sum, building) => sum + building.parts.length, 0),
80
+ bounds: unionAabb(buildings),
81
+ };
82
+ }
83
+ function summarizeTerrain(descriptor) {
84
+ return {
85
+ bounds: descriptor.bounds,
86
+ height: sampleTerrainHeights(descriptor),
87
+ ...(descriptor.waterLevel === undefined ? {} : { waterLevel: descriptor.waterLevel }),
88
+ };
89
+ }
90
+ export function summarizeEnvironment(feature) {
91
+ const terrain = feature.terrain === undefined ? undefined : summarizeTerrain(feature.terrain);
92
+ const structures = (feature.structures ?? []).map(summarizeStructures);
93
+ const vegetation = (feature.vegetation ?? []).map((entry) => ({
94
+ area: entry.area,
95
+ density: entry.density,
96
+ }));
97
+ const water = (feature.water ?? []).map((entry) => ({
98
+ level: entry.level,
99
+ bounds: entry.bounds,
100
+ }));
101
+ const weather = (feature.weather ?? []).map((entry) => ({
102
+ kind: entry.kind,
103
+ density: entry.density,
104
+ }));
105
+ const buildings = structures.reduce((sum, entry) => sum + entry.buildings, 0);
106
+ const counts = {
107
+ terrain: terrain === undefined ? 0 : 1,
108
+ structureGroups: structures.length,
109
+ buildings,
110
+ buildingParts: structures.reduce((sum, entry) => sum + entry.parts, 0),
111
+ vegetationFields: vegetation.length,
112
+ waterBodies: water.length,
113
+ weatherSystems: weather.length,
114
+ };
115
+ const isEmpty = counts.terrain === 0 &&
116
+ buildings === 0 &&
117
+ counts.vegetationFields === 0 &&
118
+ counts.waterBodies === 0 &&
119
+ counts.weatherSystems === 0;
120
+ return {
121
+ ...(terrain === undefined ? {} : { terrain }),
122
+ structures,
123
+ vegetation,
124
+ water,
125
+ weather,
126
+ counts,
127
+ isEmpty,
128
+ };
129
+ }
@@ -6,17 +6,36 @@ export type EnvironmentVec2 = readonly [number, number];
6
6
  export interface EnvironmentArea extends WorldBounds {
7
7
  h?: number;
8
8
  }
9
+ export interface TerrainColors {
10
+ low?: string;
11
+ high?: string;
12
+ waterline?: string;
13
+ }
14
+ export interface TerrainFlattenMask {
15
+ center: EnvironmentVec2;
16
+ radius: number;
17
+ /** Target flat height; defaults to the noise field's height at `center`. */
18
+ height?: number;
19
+ /** Extra blend-ring width outside `radius` where the flat height smoothsteps back to the noise value; default `radius * 0.5`. */
20
+ falloff?: number;
21
+ }
9
22
  export interface TerrainEnvironmentConfig {
10
23
  bounds?: WorldBounds;
11
24
  height?: number;
12
25
  heightMap?: string;
26
+ /** Named palette preset (see `TERRAIN_MATERIAL_PALETTES` in `world/terrain`); default "grass". Overridden field-by-field by `colors`. */
13
27
  material?: string;
28
+ /** Explicit low/high/waterline hex colors; any field left unset falls back to the resolved `material` preset. */
29
+ colors?: TerrainColors;
30
+ segments?: number;
14
31
  seed?: string;
15
32
  frequency?: number;
16
33
  octaves?: number;
17
34
  ridged?: boolean;
18
35
  baseHeight?: number;
19
36
  waterLevel?: number;
37
+ /** Flat pads carved into the noise field, e.g. for building pads or spawn circles. */
38
+ flatten?: readonly TerrainFlattenMask[];
20
39
  }
21
40
  export interface RainEnvironmentConfig {
22
41
  area?: EnvironmentArea;
@@ -46,14 +65,31 @@ export interface GrassEnvironmentConfig {
46
65
  }
47
66
  export interface OceanEnvironmentConfig {
48
67
  bounds?: WorldBounds;
68
+ position?: EnvironmentVec2;
49
69
  level?: number;
50
70
  waveHeight?: number;
51
71
  waveScale?: number;
52
72
  waveSpeed?: number;
53
73
  color?: string;
54
74
  }
75
+ export interface SkyEnvironmentConfig {
76
+ /** Fixed look used when `timeOfDay` is off (or no clock is available); default "day". */
77
+ preset?: "day" | "dusk" | "night";
78
+ /** Drive sun/sky from the world clock's `calendar().dayFraction` instead of the fixed `preset`. */
79
+ timeOfDay?: boolean;
80
+ horizonColor?: string;
81
+ zenithColor?: string;
82
+ sunIntensity?: number;
83
+ ambientIntensity?: number;
84
+ fog?: {
85
+ color?: string;
86
+ near?: number;
87
+ far?: number;
88
+ };
89
+ }
55
90
  export interface BuildingEnvironmentConfig {
56
91
  count?: number;
92
+ position?: EnvironmentVec2;
57
93
  footprint?: WorldBounds;
58
94
  stories?: readonly [number, number];
59
95
  storyHeight?: number;
@@ -61,6 +97,18 @@ export interface BuildingEnvironmentConfig {
61
97
  style?: string;
62
98
  seed?: string;
63
99
  }
100
+ export type PadSize = readonly [number, number] | {
101
+ radius: number;
102
+ };
103
+ export interface PadEnvironmentConfig {
104
+ center: EnvironmentVec2;
105
+ size: PadSize;
106
+ /** Surface offset above the terrain height at `center`; default `0.05` — just proud of the ground. */
107
+ height?: number;
108
+ color?: string;
109
+ /** Rotation applied to rectangular pads; ignored for circular pads. */
110
+ rotationY?: number;
111
+ }
64
112
  export type TerrainEnvironmentDescriptor = {
65
113
  kind: "terrain";
66
114
  } & Required<Pick<TerrainEnvironmentConfig, "bounds" | "height">> & Omit<TerrainEnvironmentConfig, "bounds" | "height">;
@@ -75,10 +123,16 @@ export type GrassEnvironmentDescriptor = {
75
123
  } & Required<Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors">> & Pick<GrassEnvironmentConfig, "seed">;
76
124
  export type OceanEnvironmentDescriptor = {
77
125
  kind: "ocean";
78
- } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">>;
126
+ } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position">;
79
127
  export type BuildingEnvironmentDescriptor = {
80
128
  kind: "building";
81
- } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed">;
129
+ } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position">;
130
+ export type PadEnvironmentDescriptor = {
131
+ kind: "pad";
132
+ } & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY">;
133
+ export type SkyEnvironmentDescriptor = {
134
+ kind: "sky";
135
+ } & Required<Pick<SkyEnvironmentConfig, "preset" | "timeOfDay">> & Omit<SkyEnvironmentConfig, "preset" | "timeOfDay">;
82
136
  export type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor;
83
137
  export type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor;
84
138
  export type WaterEnvironmentDescriptor = OceanEnvironmentDescriptor;
@@ -86,36 +140,59 @@ export type StructureEnvironmentDescriptor = BuildingEnvironmentDescriptor;
86
140
  export type EnvironmentDescriptorList<T> = T | readonly T[];
87
141
  export interface EnvironmentWorldConfig {
88
142
  terrain?: TerrainEnvironmentDescriptor;
143
+ sky?: SkyEnvironmentDescriptor;
89
144
  weather?: EnvironmentDescriptorList<WeatherEnvironmentDescriptor>;
90
145
  vegetation?: EnvironmentDescriptorList<VegetationEnvironmentDescriptor>;
91
146
  water?: EnvironmentDescriptorList<WaterEnvironmentDescriptor>;
92
147
  structures?: EnvironmentDescriptorList<StructureEnvironmentDescriptor>;
148
+ /** Ground pads, e.g. platforms or paved patches; each implicitly flattens the terrain beneath it. */
149
+ pads?: readonly PadEnvironmentDescriptor[];
93
150
  }
94
151
  export interface EnvironmentWorldFeature {
95
152
  kind: "environment";
96
153
  terrain?: TerrainEnvironmentDescriptor;
154
+ sky?: SkyEnvironmentDescriptor;
97
155
  weather?: readonly WeatherEnvironmentDescriptor[];
98
156
  vegetation?: readonly VegetationEnvironmentDescriptor[];
99
157
  water?: readonly WaterEnvironmentDescriptor[];
100
158
  structures?: readonly StructureEnvironmentDescriptor[];
159
+ pads?: readonly PadEnvironmentDescriptor[];
160
+ }
161
+ export interface WorldGridCell {
162
+ x: number;
163
+ z: number;
164
+ /** Extruded box height for this cell; falls back to the config's `baseHeight`, then `1`. */
165
+ height?: number;
166
+ /** Per-cell tint; falls back to the config's `defaultColor`. */
167
+ color?: string;
168
+ }
169
+ /** Shared by `biomes()`/`voxel()`/`plots()`/`tilemap()` so the shell can render their declared content as instanced boxes without a hand-written renderer. */
170
+ export interface WorldGridConfig {
171
+ cells?: readonly WorldGridCell[];
172
+ /** World-unit size of one grid cell. Default 1. */
173
+ cellSize?: number;
174
+ /** Height used for cells that omit their own `height`. Default 1. */
175
+ baseHeight?: number;
176
+ /** Color used for cells that omit their own `color`. */
177
+ defaultColor?: string;
101
178
  }
102
- export interface BiomesWorldConfig {
179
+ export interface BiomesWorldConfig extends WorldGridConfig {
103
180
  map: string;
104
181
  zones: string;
105
182
  bounds?: WorldBounds;
106
183
  }
107
- export interface VoxelWorldConfig {
184
+ export interface VoxelWorldConfig extends WorldGridConfig {
108
185
  seed: string;
109
186
  generate?: string;
110
187
  streaming?: {
111
188
  radius: number;
112
189
  };
113
190
  }
114
- export interface PlotsWorldConfig {
191
+ export interface PlotsWorldConfig extends WorldGridConfig {
115
192
  city?: string;
116
193
  interiors?: string;
117
194
  }
118
- export interface TilemapWorldConfig {
195
+ export interface TilemapWorldConfig extends WorldGridConfig {
119
196
  map: string;
120
197
  }
121
198
  export type WorldFeature = ({
@@ -129,13 +206,17 @@ export type WorldFeature = ({
129
206
  } & TilemapWorldConfig) | EnvironmentWorldFeature | {
130
207
  kind: "flat";
131
208
  };
209
+ /** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. */
210
+ export declare function padFlattenMasks(pads: readonly PadEnvironmentDescriptor[]): readonly TerrainFlattenMask[];
132
211
  export declare function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature;
133
212
  export declare function terrain(config?: TerrainEnvironmentConfig): TerrainEnvironmentDescriptor;
213
+ export declare function sky(config?: SkyEnvironmentConfig): SkyEnvironmentDescriptor;
134
214
  export declare function rain(config?: RainEnvironmentConfig): RainEnvironmentDescriptor;
135
215
  export declare function snow(config?: SnowEnvironmentConfig): SnowEnvironmentDescriptor;
136
216
  export declare function grass(config?: GrassEnvironmentConfig): GrassEnvironmentDescriptor;
137
217
  export declare function ocean(config?: OceanEnvironmentConfig): OceanEnvironmentDescriptor;
138
218
  export declare function building(config?: BuildingEnvironmentConfig): BuildingEnvironmentDescriptor;
219
+ export declare function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor;
139
220
  export declare function biomes(config: BiomesWorldConfig): WorldFeature;
140
221
  export declare function voxel(config: VoxelWorldConfig): WorldFeature;
141
222
  export declare function plots(config?: PlotsWorldConfig): WorldFeature;
@@ -7,18 +7,39 @@ function list(value) {
7
7
  function withOptional(base, optional) {
8
8
  return optional === undefined ? base : { ...base, ...optional };
9
9
  }
10
+ /** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. */
11
+ export function padFlattenMasks(pads) {
12
+ return pads.map((padDescriptor) => ({
13
+ center: padDescriptor.center,
14
+ radius: padHalfExtent(padDescriptor.size) * 1.2,
15
+ }));
16
+ }
17
+ function padHalfExtent(size) {
18
+ return "radius" in size ? size.radius : Math.max(size[0], size[1]) / 2;
19
+ }
20
+ function withPadFlatten(terrainDescriptor, pads) {
21
+ if (terrainDescriptor === undefined || pads === undefined || pads.length === 0)
22
+ return terrainDescriptor;
23
+ return {
24
+ ...terrainDescriptor,
25
+ flatten: [...(terrainDescriptor.flatten ?? []), ...padFlattenMasks(pads)],
26
+ };
27
+ }
10
28
  export function environment(config = {}) {
11
29
  const weather = list(config.weather);
12
30
  const vegetation = list(config.vegetation);
13
31
  const water = list(config.water);
14
32
  const structures = list(config.structures);
33
+ const terrainDescriptor = withPadFlatten(config.terrain, config.pads);
15
34
  return {
16
35
  kind: "environment",
17
- ...(config.terrain === undefined ? {} : { terrain: config.terrain }),
36
+ ...(terrainDescriptor === undefined ? {} : { terrain: terrainDescriptor }),
37
+ ...(config.sky === undefined ? {} : { sky: config.sky }),
18
38
  ...(weather === undefined ? {} : { weather }),
19
39
  ...(vegetation === undefined ? {} : { vegetation }),
20
40
  ...(water === undefined ? {} : { water }),
21
41
  ...(structures === undefined ? {} : { structures }),
42
+ ...(config.pads === undefined ? {} : { pads: config.pads }),
22
43
  };
23
44
  }
24
45
  export function terrain(config = {}) {
@@ -29,12 +50,28 @@ export function terrain(config = {}) {
29
50
  }, {
30
51
  ...(config.heightMap === undefined ? {} : { heightMap: config.heightMap }),
31
52
  ...(config.material === undefined ? {} : { material: config.material }),
53
+ ...(config.colors === undefined ? {} : { colors: config.colors }),
54
+ ...(config.segments === undefined ? {} : { segments: config.segments }),
32
55
  ...(config.seed === undefined ? {} : { seed: config.seed }),
33
56
  ...(config.frequency === undefined ? {} : { frequency: config.frequency }),
34
57
  ...(config.octaves === undefined ? {} : { octaves: config.octaves }),
35
58
  ...(config.ridged === undefined ? {} : { ridged: config.ridged }),
36
59
  ...(config.baseHeight === undefined ? {} : { baseHeight: config.baseHeight }),
37
60
  ...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
61
+ ...(config.flatten === undefined ? {} : { flatten: config.flatten }),
62
+ });
63
+ }
64
+ export function sky(config = {}) {
65
+ return withOptional({
66
+ kind: "sky",
67
+ preset: config.preset ?? "day",
68
+ timeOfDay: config.timeOfDay ?? false,
69
+ }, {
70
+ ...(config.horizonColor === undefined ? {} : { horizonColor: config.horizonColor }),
71
+ ...(config.zenithColor === undefined ? {} : { zenithColor: config.zenithColor }),
72
+ ...(config.sunIntensity === undefined ? {} : { sunIntensity: config.sunIntensity }),
73
+ ...(config.ambientIntensity === undefined ? {} : { ambientIntensity: config.ambientIntensity }),
74
+ ...(config.fog === undefined ? {} : { fog: config.fog }),
38
75
  });
39
76
  }
40
77
  export function rain(config = {}) {
@@ -80,6 +117,7 @@ export function ocean(config = {}) {
80
117
  waveScale: config.waveScale ?? 18,
81
118
  waveSpeed: config.waveSpeed ?? 0.55,
82
119
  color: config.color ?? "#1d7fa3",
120
+ ...(config.position === undefined ? {} : { position: config.position }),
83
121
  };
84
122
  }
85
123
  export function building(config = {}) {
@@ -91,7 +129,19 @@ export function building(config = {}) {
91
129
  storyHeight: config.storyHeight ?? 3,
92
130
  spacing: config.spacing ?? 2,
93
131
  style: config.style ?? "generic",
94
- }, config.seed === undefined ? undefined : { seed: config.seed });
132
+ }, {
133
+ ...(config.seed === undefined ? {} : { seed: config.seed }),
134
+ ...(config.position === undefined ? {} : { position: config.position }),
135
+ });
136
+ }
137
+ export function pad(config) {
138
+ return withOptional({
139
+ kind: "pad",
140
+ center: config.center,
141
+ size: config.size,
142
+ height: config.height ?? 0.05,
143
+ color: config.color ?? "#8b8680",
144
+ }, config.rotationY === undefined ? undefined : { rotationY: config.rotationY });
95
145
  }
96
146
  export function biomes(config) {
97
147
  return { kind: "biomes", ...config };
@@ -0,0 +1,13 @@
1
+ import type { WorldGridConfig } from "./features.js";
2
+ export interface GridInstanceTransform {
3
+ position: readonly [number, number, number];
4
+ scale: readonly [number, number, number];
5
+ color: string;
6
+ }
7
+ /**
8
+ * Pure cell→instance math for `biomes()`/`voxel()`/`plots()`/`tilemap()` world features: one
9
+ * extruded box per declared cell, grounded at y=0 and centered on its cell in x/z. Consumed by
10
+ * the shell's grid-world renderer so it can drive a single `THREE.InstancedMesh` without
11
+ * duplicating this math per game.
12
+ */
13
+ export declare function resolveGridInstances(config: WorldGridConfig): readonly GridInstanceTransform[];
@@ -0,0 +1,22 @@
1
+ const DEFAULT_CELL_SIZE = 1;
2
+ const DEFAULT_BASE_HEIGHT = 1;
3
+ const DEFAULT_GRID_COLOR = "#8a8f98";
4
+ /**
5
+ * Pure cell→instance math for `biomes()`/`voxel()`/`plots()`/`tilemap()` world features: one
6
+ * extruded box per declared cell, grounded at y=0 and centered on its cell in x/z. Consumed by
7
+ * the shell's grid-world renderer so it can drive a single `THREE.InstancedMesh` without
8
+ * duplicating this math per game.
9
+ */
10
+ export function resolveGridInstances(config) {
11
+ const cells = config.cells ?? [];
12
+ const cellSize = config.cellSize ?? DEFAULT_CELL_SIZE;
13
+ const defaultColor = config.defaultColor ?? DEFAULT_GRID_COLOR;
14
+ return cells.map((cell) => {
15
+ const height = cell.height ?? config.baseHeight ?? DEFAULT_BASE_HEIGHT;
16
+ return {
17
+ position: [cell.x * cellSize, height / 2, cell.z * cellSize],
18
+ scale: [cellSize, height, cellSize],
19
+ color: cell.color ?? defaultColor,
20
+ };
21
+ });
22
+ }
@@ -6,12 +6,24 @@ export interface PlacementHit {
6
6
  point: PlacementVec3;
7
7
  normal: PlacementVec3;
8
8
  }
9
+ /** A fixed buildable slot the placement controller snaps to instead of grid/free positioning. */
10
+ export interface PlacementSlot {
11
+ id: string;
12
+ center: PlacementVec3;
13
+ /** Capture radius around `center`. Defaults to `config.slotRadius`, then `1.5`. */
14
+ radius?: number;
15
+ /** `false` hides the slot from hover resolution without releasing it. Default `true`. */
16
+ enabled?: boolean;
17
+ }
9
18
  export interface PlacementControllerConfig {
10
19
  footprint: Footprint;
11
20
  rules?: PlacementRules;
12
21
  snapMode?: SnapMode;
13
22
  grid?: number;
14
23
  quarterTurns?: number;
24
+ slots?: readonly PlacementSlot[];
25
+ /** Fallback capture radius for slots that don't set their own. Default `1.5`. */
26
+ slotRadius?: number;
15
27
  }
16
28
  export interface PlacementPreview {
17
29
  center: Vec2;
@@ -20,9 +32,11 @@ export interface PlacementPreview {
20
32
  footprint: Footprint;
21
33
  aabb: Aabb;
22
34
  valid: boolean;
23
- reason?: "out-of-bounds" | "overlap";
35
+ reason?: "out-of-bounds" | "overlap" | "no-slot";
24
36
  snapMode: SnapMode;
25
37
  normal: PlacementVec3;
38
+ /** Id of the resolved slot, when the controller is in slot mode and a slot matched. */
39
+ slotId?: string;
26
40
  }
27
41
  export interface PlacementCommit {
28
42
  center: Vec2;
@@ -31,6 +45,8 @@ export interface PlacementCommit {
31
45
  quarterTurns: number;
32
46
  footprint: Footprint;
33
47
  aabb: Aabb;
48
+ /** Id of the slot this commit occupied, when the controller is in slot mode. */
49
+ slotId?: string;
34
50
  }
35
51
  export interface PlacementController {
36
52
  hover(hit: PlacementHit): PlacementPreview;
@@ -41,6 +57,11 @@ export interface PlacementController {
41
57
  setFootprint(footprint: Footprint): void;
42
58
  setRules(rules: PlacementRules): void;
43
59
  setGrid(grid: number): void;
60
+ setSlots(slots: readonly PlacementSlot[]): void;
61
+ /** Id of the currently hovered slot, or `null` when idle, out of slot mode, or unmatched. */
62
+ hoveredSlotId(): string | null;
63
+ /** Frees a previously committed slot so it can match again. */
64
+ releaseSlot(id: string): void;
44
65
  commit(): PlacementCommit | null;
45
66
  reset(): void;
46
67
  }
@@ -1,6 +1,10 @@
1
1
  import { footprintAabb, snapToGrid } from "./geometry.js";
2
2
  import { validatePlacement } from "./placement.js";
3
3
  const SNAP_ORDER = ["grid", "free", "surface"];
4
+ const DEFAULT_SLOT_RADIUS = 1.5;
5
+ function distanceVec3(a, b) {
6
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
7
+ }
4
8
  export function quarterTurnsToRotationY(quarterTurns) {
5
9
  const turns = ((quarterTurns % 4) + 4) % 4;
6
10
  return turns === 0 ? 0 : -turns * (Math.PI / 2);
@@ -11,6 +15,9 @@ export function createPlacementController(config) {
11
15
  let snapMode = config.snapMode ?? "grid";
12
16
  let grid = config.grid ?? rules.snap ?? 1;
13
17
  let quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
18
+ let slots = config.slots;
19
+ const slotRadius = config.slotRadius ?? DEFAULT_SLOT_RADIUS;
20
+ const occupiedSlots = new Set();
14
21
  let lastHit = null;
15
22
  let preview = null;
16
23
  function snapCenter(raw) {
@@ -18,7 +25,40 @@ export function createPlacementController(config) {
18
25
  return snapToGrid(raw, grid);
19
26
  return raw;
20
27
  }
28
+ function nearestSlot(hit) {
29
+ let best = null;
30
+ let bestDistance = Number.POSITIVE_INFINITY;
31
+ for (const slot of slots ?? []) {
32
+ if (slot.enabled === false || occupiedSlots.has(slot.id))
33
+ continue;
34
+ const distance = distanceVec3(hit.point, slot.center);
35
+ if (distance <= (slot.radius ?? slotRadius) && distance < bestDistance) {
36
+ best = slot;
37
+ bestDistance = distance;
38
+ }
39
+ }
40
+ return best;
41
+ }
42
+ function computeSlotPreview(hit) {
43
+ const slot = nearestSlot(hit);
44
+ const center = slot === null ? [hit.point[0], hit.point[2]] : [slot.center[0], slot.center[2]];
45
+ const next = {
46
+ center,
47
+ y: slot === null ? hit.point[1] : slot.center[1],
48
+ quarterTurns,
49
+ footprint,
50
+ aabb: footprintAabb(center, footprint, quarterTurns),
51
+ valid: slot !== null,
52
+ snapMode,
53
+ normal: hit.normal,
54
+ ...(slot === null ? { reason: "no-slot" } : { slotId: slot.id }),
55
+ };
56
+ preview = next;
57
+ return next;
58
+ }
21
59
  function compute(hit) {
60
+ if (slots !== undefined)
61
+ return computeSlotPreview(hit);
22
62
  const raw = [hit.point[0], hit.point[2]];
23
63
  const center = snapCenter(raw);
24
64
  const rulesWithoutSnap = snapMode === "grid" ? { ...rules, snap: undefined } : rules;
@@ -76,9 +116,24 @@ export function createPlacementController(config) {
76
116
  if (lastHit !== null)
77
117
  compute(lastHit);
78
118
  },
119
+ setSlots(next) {
120
+ slots = next;
121
+ if (lastHit !== null)
122
+ compute(lastHit);
123
+ },
124
+ hoveredSlotId() {
125
+ return preview?.slotId ?? null;
126
+ },
127
+ releaseSlot(id) {
128
+ occupiedSlots.delete(id);
129
+ if (lastHit !== null)
130
+ compute(lastHit);
131
+ },
79
132
  commit() {
80
133
  if (preview === null || !preview.valid)
81
134
  return null;
135
+ if (preview.slotId !== undefined)
136
+ occupiedSlots.add(preview.slotId);
82
137
  return {
83
138
  center: preview.center,
84
139
  y: preview.y,
@@ -86,6 +141,7 @@ export function createPlacementController(config) {
86
141
  quarterTurns,
87
142
  footprint: preview.footprint,
88
143
  aabb: preview.aabb,
144
+ ...(preview.slotId !== undefined ? { slotId: preview.slotId } : {}),
89
145
  };
90
146
  },
91
147
  reset() {