@jgengine/core 0.8.0 → 0.9.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 (228) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.js +1 -1
  11. package/dist/ai/threat.d.ts +2 -0
  12. package/dist/ai/threat.js +34 -0
  13. package/dist/cartridge/runtime.d.ts +15 -0
  14. package/dist/cartridge/runtime.js +556 -0
  15. package/dist/cartridge/spec.d.ts +188 -0
  16. package/dist/cartridge/spec.js +19 -0
  17. package/dist/cartridge/validate.d.ts +2 -0
  18. package/dist/cartridge/validate.js +78 -0
  19. package/dist/combat/abilityKit.d.ts +17 -2
  20. package/dist/combat/abilityKit.js +62 -13
  21. package/dist/combat/castRunner.d.ts +40 -0
  22. package/dist/combat/castRunner.js +56 -0
  23. package/dist/combat/comboPoints.d.ts +14 -0
  24. package/dist/combat/comboPoints.js +52 -0
  25. package/dist/combat/defensiveWindow.d.ts +1 -1
  26. package/dist/combat/defensiveWindow.js +5 -3
  27. package/dist/combat/projectiles.d.ts +33 -12
  28. package/dist/combat/projectiles.js +201 -129
  29. package/dist/combat/resistance.d.ts +16 -4
  30. package/dist/combat/resistance.js +43 -7
  31. package/dist/combat/resourcePool.d.ts +20 -0
  32. package/dist/combat/resourcePool.js +55 -0
  33. package/dist/combat/shotOrigin.d.ts +40 -0
  34. package/dist/combat/shotOrigin.js +57 -0
  35. package/dist/combat/telegraph.d.ts +25 -0
  36. package/dist/combat/telegraph.js +47 -0
  37. package/dist/crafting/recipe.js +4 -1
  38. package/dist/devtools/devtools.d.ts +72 -24
  39. package/dist/devtools/devtools.js +449 -101
  40. package/dist/devtools/transformTunables.js +3 -3
  41. package/dist/devtools/tunableSchema.d.ts +140 -0
  42. package/dist/devtools/tunableSchema.js +476 -0
  43. package/dist/faction/factions.d.ts +33 -0
  44. package/dist/faction/factions.js +76 -0
  45. package/dist/faction/reputation.d.ts +32 -0
  46. package/dist/faction/reputation.js +82 -0
  47. package/dist/game/chat.d.ts +2 -0
  48. package/dist/game/chat.js +9 -1
  49. package/dist/game/chatFilter.d.ts +15 -0
  50. package/dist/game/chatFilter.js +55 -0
  51. package/dist/game/controlGate.d.ts +4 -0
  52. package/dist/game/controlGate.js +7 -0
  53. package/dist/game/defineGame.d.ts +2 -0
  54. package/dist/game/events.d.ts +10 -0
  55. package/dist/game/gamePhase.d.ts +20 -0
  56. package/dist/game/gamePhase.js +21 -0
  57. package/dist/game/lootTable.d.ts +10 -1
  58. package/dist/game/lootTable.js +27 -7
  59. package/dist/game/playableGame.d.ts +50 -1
  60. package/dist/game/playableGame.js +1 -1
  61. package/dist/game/quest.d.ts +7 -3
  62. package/dist/game/quest.js +17 -8
  63. package/dist/game/race.d.ts +39 -1
  64. package/dist/game/race.js +96 -11
  65. package/dist/game/recordBook.d.ts +29 -0
  66. package/dist/game/recordBook.js +86 -0
  67. package/dist/game/talents.d.ts +47 -0
  68. package/dist/game/talents.js +140 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/input/axisInput.d.ts +26 -3
  72. package/dist/input/axisInput.js +44 -4
  73. package/dist/input/bindingOverrides.d.ts +25 -0
  74. package/dist/input/bindingOverrides.js +95 -0
  75. package/dist/input/pointerAxis.d.ts +30 -0
  76. package/dist/input/pointerAxis.js +31 -0
  77. package/dist/input/touchScheme.d.ts +6 -0
  78. package/dist/input/touchScheme.js +45 -7
  79. package/dist/meta/changelog.d.ts +1 -1
  80. package/dist/meta/changelog.js +23 -1
  81. package/dist/movement/dash.d.ts +3 -0
  82. package/dist/movement/dash.js +19 -5
  83. package/dist/movement/glideModel.d.ts +44 -0
  84. package/dist/movement/glideModel.js +54 -0
  85. package/dist/movement/grappleSwing.d.ts +34 -0
  86. package/dist/movement/grappleSwing.js +56 -0
  87. package/dist/movement/leaderTrail.d.ts +29 -0
  88. package/dist/movement/leaderTrail.js +67 -0
  89. package/dist/movement/steering.d.ts +27 -0
  90. package/dist/movement/steering.js +40 -0
  91. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  92. package/dist/multiplayer/feedWriteGate.js +12 -0
  93. package/dist/multiplayer/presenceModel.d.ts +14 -2
  94. package/dist/multiplayer/presenceModel.js +15 -3
  95. package/dist/nav/corridors.d.ts +30 -0
  96. package/dist/nav/corridors.js +69 -0
  97. package/dist/nav/navGrid.js +30 -4
  98. package/dist/nav/railGraph.d.ts +66 -0
  99. package/dist/nav/railGraph.js +129 -0
  100. package/dist/nav/timetable.d.ts +41 -0
  101. package/dist/nav/timetable.js +94 -0
  102. package/dist/physics/buoyancy.d.ts +6 -0
  103. package/dist/physics/buoyancy.js +14 -0
  104. package/dist/physics/flowTube.d.ts +32 -0
  105. package/dist/physics/flowTube.js +64 -0
  106. package/dist/physics/forceVolume.d.ts +27 -0
  107. package/dist/physics/forceVolume.js +38 -0
  108. package/dist/physics/kinematicVehicle.d.ts +52 -0
  109. package/dist/physics/kinematicVehicle.js +81 -0
  110. package/dist/physics/physicsWorld.d.ts +13 -6
  111. package/dist/physics/physicsWorld.js +169 -40
  112. package/dist/physics/ragdoll.js +1 -1
  113. package/dist/physics/vehicleBody.js +2 -1
  114. package/dist/random/seedLink.d.ts +5 -0
  115. package/dist/random/seedLink.js +35 -0
  116. package/dist/runtime/cameraDirector.d.ts +6 -1
  117. package/dist/runtime/cameraDirector.js +6 -0
  118. package/dist/runtime/commandRunner.d.ts +2 -2
  119. package/dist/runtime/commandRunner.js +2 -2
  120. package/dist/runtime/gameContext.d.ts +21 -0
  121. package/dist/runtime/gameContext.js +184 -9
  122. package/dist/runtime/gameRuntime.d.ts +5 -1
  123. package/dist/runtime/gameRuntime.js +13 -12
  124. package/dist/runtime/hostPersistence.d.ts +9 -1
  125. package/dist/runtime/hostPersistence.js +23 -8
  126. package/dist/runtime/inputSnapshot.d.ts +5 -0
  127. package/dist/runtime/inputSnapshot.js +5 -0
  128. package/dist/runtime/motionIntents.d.ts +14 -6
  129. package/dist/runtime/motionIntents.js +20 -2
  130. package/dist/runtime/snapshot.d.ts +1 -0
  131. package/dist/runtime/snapshot.js +1 -1
  132. package/dist/runtime/visibility.d.ts +74 -0
  133. package/dist/runtime/visibility.js +127 -0
  134. package/dist/scene/colliders.d.ts +44 -0
  135. package/dist/scene/colliders.js +77 -0
  136. package/dist/scene/entityStore.d.ts +22 -5
  137. package/dist/scene/entityStore.js +59 -12
  138. package/dist/scene/objectQuery.d.ts +13 -2
  139. package/dist/scene/objectQuery.js +34 -2
  140. package/dist/scene/objectStore.d.ts +2 -0
  141. package/dist/scene/objectStore.js +31 -12
  142. package/dist/scene/sceneRaycast.d.ts +80 -0
  143. package/dist/scene/sceneRaycast.js +284 -0
  144. package/dist/scene/spatial.d.ts +11 -16
  145. package/dist/scene/spatial.js +99 -38
  146. package/dist/sensor/recordingBuffer.d.ts +6 -0
  147. package/dist/sensor/recordingBuffer.js +7 -0
  148. package/dist/sensor/replayLoop.d.ts +39 -0
  149. package/dist/sensor/replayLoop.js +64 -0
  150. package/dist/sensor/visionCone.d.ts +31 -0
  151. package/dist/sensor/visionCone.js +72 -0
  152. package/dist/session/extraction.d.ts +2 -2
  153. package/dist/session/extraction.js +13 -2
  154. package/dist/settings/settingsModel.d.ts +97 -0
  155. package/dist/settings/settingsModel.js +100 -0
  156. package/dist/stats/statModifiers.d.ts +5 -2
  157. package/dist/stats/statModifiers.js +32 -14
  158. package/dist/store/observableKeyedStore.js +8 -2
  159. package/dist/survival/decayMeter.d.ts +1 -1
  160. package/dist/survival/decayMeter.js +0 -1
  161. package/dist/time/beatClock.d.ts +18 -0
  162. package/dist/time/beatClock.js +24 -0
  163. package/dist/time/idleProgress.d.ts +29 -0
  164. package/dist/time/idleProgress.js +31 -0
  165. package/dist/time/serverTick.d.ts +7 -1
  166. package/dist/time/serverTick.js +27 -4
  167. package/dist/time/stateSchedule.d.ts +57 -0
  168. package/dist/time/stateSchedule.js +121 -0
  169. package/dist/turn/commit.d.ts +2 -2
  170. package/dist/turn/commit.js +2 -2
  171. package/dist/ui/hudLayout.d.ts +58 -0
  172. package/dist/ui/hudLayout.js +207 -0
  173. package/dist/ui/hudScale.d.ts +44 -0
  174. package/dist/ui/hudScale.js +55 -0
  175. package/dist/visibility/assetStreaming.d.ts +66 -0
  176. package/dist/visibility/assetStreaming.js +201 -0
  177. package/dist/visibility/bounds.d.ts +66 -0
  178. package/dist/visibility/bounds.js +113 -0
  179. package/dist/visibility/camera.d.ts +21 -0
  180. package/dist/visibility/camera.js +3 -0
  181. package/dist/visibility/config.d.ts +21 -0
  182. package/dist/visibility/config.js +1 -0
  183. package/dist/visibility/diagnostics.d.ts +26 -0
  184. package/dist/visibility/diagnostics.js +36 -0
  185. package/dist/visibility/distance.d.ts +11 -0
  186. package/dist/visibility/distance.js +30 -0
  187. package/dist/visibility/frustum.d.ts +47 -0
  188. package/dist/visibility/frustum.js +186 -0
  189. package/dist/visibility/occlusion.d.ts +36 -0
  190. package/dist/visibility/occlusion.js +45 -0
  191. package/dist/visibility/settings.d.ts +75 -0
  192. package/dist/visibility/settings.js +49 -0
  193. package/dist/visibility/simulationCulling.d.ts +34 -0
  194. package/dist/visibility/simulationCulling.js +36 -0
  195. package/dist/visibility/spatialIndex.d.ts +45 -0
  196. package/dist/visibility/spatialIndex.js +198 -0
  197. package/dist/visibility/visibilitySystem.d.ts +90 -0
  198. package/dist/visibility/visibilitySystem.js +257 -0
  199. package/dist/world/buildings.d.ts +6 -0
  200. package/dist/world/buildings.js +152 -0
  201. package/dist/world/carve.d.ts +7 -2
  202. package/dist/world/carve.js +17 -7
  203. package/dist/world/cellStates.d.ts +45 -0
  204. package/dist/world/cellStates.js +103 -0
  205. package/dist/world/environmentSummary.d.ts +10 -2
  206. package/dist/world/environmentSummary.js +11 -2
  207. package/dist/world/features.d.ts +56 -8
  208. package/dist/world/features.js +24 -3
  209. package/dist/world/gridInstances.d.ts +7 -8
  210. package/dist/world/gridInstances.js +145 -7
  211. package/dist/world/lod.d.ts +17 -0
  212. package/dist/world/lod.js +66 -0
  213. package/dist/world/mapLayers.d.ts +60 -0
  214. package/dist/world/mapLayers.js +44 -0
  215. package/dist/world/massing.d.ts +45 -0
  216. package/dist/world/massing.js +581 -0
  217. package/dist/world/minimap.d.ts +13 -1
  218. package/dist/world/minimap.js +21 -2
  219. package/dist/world/terrain.d.ts +42 -3
  220. package/dist/world/terrain.js +197 -16
  221. package/dist/world/water.d.ts +7 -0
  222. package/dist/world/water.js +19 -4
  223. package/dist/world/weather.d.ts +3 -18
  224. package/dist/world/weather.js +4 -17
  225. package/dist/world/windZones.d.ts +47 -0
  226. package/dist/world/windZones.js +59 -0
  227. package/llms.txt +2585 -1704
  228. package/package.json +12 -2
@@ -0,0 +1,198 @@
1
+ import { aabbIntersects } from "./bounds.js";
2
+ import { aabbInFrustum } from "./frustum.js";
3
+ function cellOf(value, size) {
4
+ return Math.floor(value / size);
5
+ }
6
+ function packCell(x, y, z) {
7
+ return `${x}:${y}:${z}`;
8
+ }
9
+ export function createSpatialIndex(options = {}) {
10
+ const size = options.cellSize ?? 16;
11
+ const maxSpan = options.maxCellSpan ?? 6;
12
+ const cells = new Map();
13
+ const oversized = new Set();
14
+ const entries = new Map();
15
+ // Allocation-free dedup for queries: an id is "seen" for query N when stamps.get(id) === N.
16
+ const stamps = new Map();
17
+ let queryStamp = 0;
18
+ function rangeOf(bounds) {
19
+ return {
20
+ x0: cellOf(bounds.minX, size), y0: cellOf(bounds.minY, size), z0: cellOf(bounds.minZ, size),
21
+ x1: cellOf(bounds.maxX, size), y1: cellOf(bounds.maxY, size), z1: cellOf(bounds.maxZ, size),
22
+ };
23
+ }
24
+ function spanTooBig(range) {
25
+ return (range.x1 - range.x0 > maxSpan ||
26
+ range.y1 - range.y0 > maxSpan ||
27
+ range.z1 - range.z0 > maxSpan);
28
+ }
29
+ function addToCells(id, range) {
30
+ for (let x = range.x0; x <= range.x1; x += 1) {
31
+ for (let y = range.y0; y <= range.y1; y += 1) {
32
+ for (let z = range.z0; z <= range.z1; z += 1) {
33
+ const key = packCell(x, y, z);
34
+ let bucket = cells.get(key);
35
+ if (bucket === undefined) {
36
+ bucket = new Set();
37
+ cells.set(key, bucket);
38
+ }
39
+ bucket.add(id);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ function removeFromCells(id, range) {
45
+ for (let x = range.x0; x <= range.x1; x += 1) {
46
+ for (let y = range.y0; y <= range.y1; y += 1) {
47
+ for (let z = range.z0; z <= range.z1; z += 1) {
48
+ const key = packCell(x, y, z);
49
+ const bucket = cells.get(key);
50
+ if (bucket === undefined)
51
+ continue;
52
+ bucket.delete(id);
53
+ if (bucket.size === 0)
54
+ cells.delete(key);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ function rangesEqual(a, b) {
60
+ return a.x0 === b.x0 && a.y0 === b.y0 && a.z0 === b.z0 && a.x1 === b.x1 && a.y1 === b.y1 && a.z1 === b.z1;
61
+ }
62
+ function place(id, bounds, dynamic) {
63
+ const range = rangeOf(bounds);
64
+ if (spanTooBig(range)) {
65
+ oversized.add(id);
66
+ entries.set(id, { range: null, dynamic });
67
+ return;
68
+ }
69
+ addToCells(id, range);
70
+ entries.set(id, { range, dynamic });
71
+ }
72
+ function collectCell(key, out) {
73
+ const bucket = cells.get(key);
74
+ if (bucket === undefined)
75
+ return;
76
+ for (const id of bucket) {
77
+ if (stamps.get(id) === queryStamp)
78
+ continue;
79
+ stamps.set(id, queryStamp);
80
+ out.push(id);
81
+ }
82
+ }
83
+ function collectOversized(out) {
84
+ for (const id of oversized) {
85
+ if (stamps.get(id) === queryStamp)
86
+ continue;
87
+ stamps.set(id, queryStamp);
88
+ out.push(id);
89
+ }
90
+ }
91
+ function collectBoxCells(minX, minY, minZ, maxX, maxY, maxZ, out, cellFilter) {
92
+ queryStamp += 1;
93
+ out.length = 0;
94
+ const x0 = cellOf(minX, size), y0 = cellOf(minY, size), z0 = cellOf(minZ, size);
95
+ 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);
102
+ }
103
+ }
104
+ }
105
+ collectOversized(out);
106
+ }
107
+ return {
108
+ insert(id, bounds, dynamic = false) {
109
+ const existing = entries.get(id);
110
+ if (existing !== undefined) {
111
+ this.update(id, bounds);
112
+ return;
113
+ }
114
+ place(id, bounds, dynamic);
115
+ },
116
+ update(id, bounds) {
117
+ const existing = entries.get(id);
118
+ if (existing === undefined) {
119
+ place(id, bounds, true);
120
+ return;
121
+ }
122
+ const range = rangeOf(bounds);
123
+ const tooBig = spanTooBig(range);
124
+ if (tooBig) {
125
+ if (existing.range !== null)
126
+ removeFromCells(id, existing.range);
127
+ oversized.add(id);
128
+ existing.range = null;
129
+ return;
130
+ }
131
+ if (existing.range === null) {
132
+ oversized.delete(id);
133
+ addToCells(id, range);
134
+ existing.range = range;
135
+ return;
136
+ }
137
+ if (rangesEqual(existing.range, range))
138
+ return;
139
+ removeFromCells(id, existing.range);
140
+ addToCells(id, range);
141
+ existing.range = range;
142
+ },
143
+ remove(id) {
144
+ const existing = entries.get(id);
145
+ if (existing === undefined)
146
+ return;
147
+ if (existing.range === null)
148
+ oversized.delete(id);
149
+ else
150
+ removeFromCells(id, existing.range);
151
+ entries.delete(id);
152
+ stamps.delete(id);
153
+ },
154
+ has(id) {
155
+ return entries.has(id);
156
+ },
157
+ size() {
158
+ return entries.size;
159
+ },
160
+ queryFrustum(frustum, out) {
161
+ collectBoxCells(frustum.minX, frustum.minY, frustum.minZ, frustum.maxX, frustum.maxY, frustum.maxZ, out, (x, y, z) => aabbInFrustum(frustum, x * size, y * size, z * size, (x + 1) * size, (y + 1) * size, (z + 1) * size));
162
+ return out;
163
+ },
164
+ queryBox(minX, minY, minZ, maxX, maxY, maxZ, out) {
165
+ collectBoxCells(minX, minY, minZ, maxX, maxY, maxZ, out);
166
+ return out;
167
+ },
168
+ querySphere(cx, cy, cz, radius, out) {
169
+ collectBoxCells(cx - radius, cy - radius, cz - radius, cx + radius, cy + radius, cz + radius, out);
170
+ return out;
171
+ },
172
+ cells() {
173
+ const result = [];
174
+ 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
+ result.push({
178
+ 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,
182
+ });
183
+ }
184
+ return result;
185
+ },
186
+ cellSize() {
187
+ return size;
188
+ },
189
+ clear() {
190
+ cells.clear();
191
+ oversized.clear();
192
+ entries.clear();
193
+ stamps.clear();
194
+ },
195
+ };
196
+ }
197
+ /** Precise AABB-vs-AABB test re-exported for callers refining a broad-phase result. */
198
+ export { aabbIntersects };
@@ -0,0 +1,90 @@
1
+ import type { Vec3, RenderBounds, BoundsSpec } from "./bounds.js";
2
+ import type { BoundsCache } from "./bounds.js";
3
+ import type { CameraVisibilityContext, CameraKind } from "./camera.js";
4
+ import type { SpatialIndex } from "./spatialIndex.js";
5
+ import type { CullingSettings, ObjectVisibilityOverrides } from "./settings.js";
6
+ import type { OcclusionTester } from "./occlusion.js";
7
+ import type { VisibilityStats } from "./diagnostics.js";
8
+ /**
9
+ * A scene object the visibility system considers. A normal game object already carries a
10
+ * position and a version counter, so it becomes cullable automatically — no separate
11
+ * "cullable" component. Everything else is optional override.
12
+ */
13
+ export interface Renderable {
14
+ readonly id: string;
15
+ readonly position: Vec3;
16
+ /** Bumps when transform, geometry, bounds, or overrides change — gates bounds/override recompute. */
17
+ readonly version: number;
18
+ readonly bounds?: BoundsSpec;
19
+ readonly overrides?: ObjectVisibilityOverrides;
20
+ readonly layer?: string;
21
+ /** Fast-path exemption for UI, skyboxes, debug overlays, camera-attached, marked always-visible. */
22
+ readonly alwaysVisible?: boolean;
23
+ /** Asset ids this object needs loaded; feeds the streaming preload/retain sets. */
24
+ readonly assets?: readonly string[];
25
+ }
26
+ export interface VisibilitySystemOptions {
27
+ readonly renderables: () => Iterable<Renderable>;
28
+ readonly cameras: () => readonly CameraVisibilityContext[];
29
+ readonly settings?: Partial<CullingSettings>;
30
+ readonly sceneOverrides?: ObjectVisibilityOverrides;
31
+ readonly globalOverrides?: ObjectVisibilityOverrides;
32
+ readonly layerOverrides?: (layer: string) => ObjectVisibilityOverrides | undefined;
33
+ readonly occlusion?: OcclusionTester;
34
+ /** Injected shared index. Defaults to an internally-owned uniform 3D hash. */
35
+ readonly index?: SpatialIndex;
36
+ readonly boundsCache?: BoundsCache;
37
+ readonly now?: () => number;
38
+ }
39
+ export interface VisibilityResult {
40
+ /** Objects to submit to the renderer this frame. Valid until the next `update()` (reused set). */
41
+ readonly visible: ReadonlySet<string>;
42
+ /** Objects within the preload region — load their assets, whether or not they render. */
43
+ readonly preload: ReadonlySet<string>;
44
+ readonly stats: VisibilityStats;
45
+ }
46
+ export interface VisibilityDebugSnapshot {
47
+ cameras: {
48
+ id: string;
49
+ kind: CameraKind;
50
+ corners: number[];
51
+ preloadMargin: number;
52
+ }[];
53
+ bounds: {
54
+ id: string;
55
+ minX: number;
56
+ minY: number;
57
+ minZ: number;
58
+ maxX: number;
59
+ maxY: number;
60
+ maxZ: number;
61
+ visible: boolean;
62
+ }[];
63
+ partitions: {
64
+ key: string;
65
+ count: number;
66
+ minX: number;
67
+ minY: number;
68
+ minZ: number;
69
+ maxX: number;
70
+ maxY: number;
71
+ maxZ: number;
72
+ }[];
73
+ culled: string[];
74
+ stats: VisibilityStats;
75
+ }
76
+ export interface VisibilitySystem {
77
+ update(): VisibilityResult;
78
+ isVisible(id: string): boolean;
79
+ isPreloaded(id: string): boolean;
80
+ boundsOf(id: string): RenderBounds | undefined;
81
+ settings(): CullingSettings;
82
+ setSettings(patch: Partial<CullingSettings>): void;
83
+ stats(): VisibilityStats;
84
+ /** Assets required by the preload set — hand to an AssetStreamingSystem. */
85
+ requiredAssets(): ReadonlySet<string>;
86
+ index(): SpatialIndex;
87
+ debugSnapshot(): VisibilityDebugSnapshot;
88
+ clear(): void;
89
+ }
90
+ export declare function createVisibilitySystem(options: VisibilitySystemOptions): VisibilitySystem;
@@ -0,0 +1,257 @@
1
+ import { DEFAULT_BOUNDS, createBoundsCache } from "./bounds.js";
2
+ import { cameraInfluencesStreaming } from "./camera.js";
3
+ import { createFrustum, updateFrustum, dilateFrustum, boundsInFrustum } from "./frustum.js";
4
+ import { culledByDistance } from "./distance.js";
5
+ import { createSpatialIndex } from "./spatialIndex.js";
6
+ import { DEFAULT_CULLING_SETTINGS, mergeCullingSettings, resolveOverrides } from "./settings.js";
7
+ import { noOcclusion } from "./occlusion.js";
8
+ import { createVisibilityStats, resetCullingStats, nowMs } from "./diagnostics.js";
9
+ export function createVisibilitySystem(options) {
10
+ let settings = mergeCullingSettings(DEFAULT_CULLING_SETTINGS, options.settings ?? {});
11
+ const occlusion = options.occlusion ?? noOcclusion;
12
+ const index = options.index ?? createSpatialIndex();
13
+ const bounds = options.boundsCache ?? createBoundsCache();
14
+ const clock = options.now ?? nowMs;
15
+ const records = new Map();
16
+ const stats = createVisibilityStats();
17
+ let visible = new Set();
18
+ let prevVisible = new Set();
19
+ const preload = new Set();
20
+ const requiredAssets = new Set();
21
+ const candidateScratch = [];
22
+ const consideredStamp = new Map();
23
+ let consideredCounter = 0;
24
+ const baseFrustum = createFrustum();
25
+ const preloadFrustum = createFrustum();
26
+ const hysteresisFrustum = createFrustum();
27
+ let updateCounter = 0;
28
+ function specOf(r, resolved) {
29
+ return resolved.bounds ?? r.bounds ?? DEFAULT_BOUNDS;
30
+ }
31
+ function recordFor(r) {
32
+ let record = records.get(r.id);
33
+ if (record !== undefined && record.version === r.version) {
34
+ record.lastSeen = updateCounter;
35
+ return record;
36
+ }
37
+ const resolved = resolveOverrides(r.overrides, r.layer !== undefined ? options.layerOverrides?.(r.layer) : undefined, options.sceneOverrides, options.globalOverrides);
38
+ const alwaysVisible = resolved.alwaysVisible || r.alwaysVisible === true;
39
+ const dynamic = resolved.classification !== "static";
40
+ if (record === undefined) {
41
+ record = { version: r.version, resolved, alwaysVisible, dynamic, assets: r.assets, lastSeen: updateCounter };
42
+ records.set(r.id, record);
43
+ }
44
+ else {
45
+ record.version = r.version;
46
+ record.resolved = resolved;
47
+ record.alwaysVisible = alwaysVisible;
48
+ record.dynamic = dynamic;
49
+ record.assets = r.assets;
50
+ record.lastSeen = updateCounter;
51
+ }
52
+ return record;
53
+ }
54
+ function maintain() {
55
+ const alwaysVisibleList = [];
56
+ for (const r of options.renderables()) {
57
+ const record = recordFor(r);
58
+ const b = bounds.get(r.id, r.version, specOf(r, record.resolved), r.position);
59
+ index.insert(r.id, b, record.dynamic);
60
+ stats.totalObjects += 1;
61
+ if (record.alwaysVisible)
62
+ alwaysVisibleList.push(r);
63
+ }
64
+ // Prune objects that vanished this frame.
65
+ for (const [id, record] of records) {
66
+ if (record.lastSeen === updateCounter)
67
+ continue;
68
+ index.remove(id);
69
+ bounds.delete(id);
70
+ records.delete(id);
71
+ }
72
+ return alwaysVisibleList;
73
+ }
74
+ function markConsidered(id) {
75
+ if (consideredStamp.get(id) === consideredCounter)
76
+ return false;
77
+ consideredStamp.set(id, consideredCounter);
78
+ return true;
79
+ }
80
+ function distanceLimits(camera, resolved) {
81
+ const min = resolved.minRenderDistance ?? settings.defaultMinRenderDistance;
82
+ let max = resolved.maxRenderDistance ?? settings.defaultMaxRenderDistance;
83
+ if (camera.maxRenderDistance !== undefined)
84
+ max = Math.min(max, camera.maxRenderDistance);
85
+ return { min, max };
86
+ }
87
+ function evaluateCamera(camera) {
88
+ const streaming = cameraInfluencesStreaming(camera);
89
+ updateFrustum(baseFrustum, camera.view);
90
+ dilateFrustum(baseFrustum, settings.preloadMargin, preloadFrustum);
91
+ dilateFrustum(baseFrustum, settings.hysteresis, hysteresisFrustum);
92
+ const cullingOff = camera.cullingDisabled === true || !settings.enabled;
93
+ const t0 = clock();
94
+ index.queryFrustum(preloadFrustum, candidateScratch);
95
+ stats.spatialQueryMs += clock() - t0;
96
+ const t1 = clock();
97
+ for (const id of candidateScratch) {
98
+ const record = records.get(id);
99
+ if (record === undefined)
100
+ continue;
101
+ const b = bounds.peek(id);
102
+ if (b === undefined)
103
+ continue;
104
+ const fresh = markConsidered(id);
105
+ if (fresh)
106
+ stats.consideredForRender += 1;
107
+ const resolved = record.resolved;
108
+ const cx = camera.view.position[0], cy = camera.view.position[1], cz = camera.view.position[2];
109
+ let isVisible;
110
+ if (cullingOff || record.alwaysVisible || resolved.cullingDisabled) {
111
+ isVisible = true;
112
+ }
113
+ else {
114
+ const custom = resolved.customVisibility?.(camera);
115
+ if (custom !== undefined) {
116
+ isVisible = custom;
117
+ }
118
+ else {
119
+ const frustum = prevVisible.has(id) ? hysteresisFrustum : baseFrustum;
120
+ if (settings.frustumCulling && !boundsInFrustum(frustum, b)) {
121
+ if (fresh)
122
+ stats.rejectedByFrustum += 1;
123
+ isVisible = false;
124
+ }
125
+ else {
126
+ const limits = distanceLimits(camera, resolved);
127
+ if (settings.distanceCulling &&
128
+ culledByDistance(cx, cy, cz, b.centerX, b.centerY, b.centerZ, limits.min, limits.max, b.radius, settings.hysteresis)) {
129
+ if (fresh)
130
+ stats.rejectedByDistance += 1;
131
+ isVisible = false;
132
+ }
133
+ else if (settings.occlusionCulling &&
134
+ occlusion.enabled &&
135
+ occlusion.isOccluded({ camera: camera.view, bounds: b, occluders: [] })) {
136
+ if (fresh)
137
+ stats.rejectedByOcclusion += 1;
138
+ isVisible = false;
139
+ }
140
+ else {
141
+ isVisible = true;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ // Preload region: candidates already come from the preload-dilated frustum. Anything
147
+ // visible, or within the preload region and not distance-culled far past the margin, preloads.
148
+ const inPreload = isVisible || resolved.pinned || (!cullingOff && withinPreloadDistance(camera, resolved, b));
149
+ if (inPreload) {
150
+ preload.add(id);
151
+ if (streaming && record.assets !== undefined) {
152
+ for (const asset of record.assets)
153
+ requiredAssets.add(asset);
154
+ }
155
+ }
156
+ if (isVisible)
157
+ visible.add(id);
158
+ }
159
+ stats.cullingMs += clock() - t1;
160
+ }
161
+ function withinPreloadDistance(camera, resolved, b) {
162
+ const limits = distanceLimits(camera, resolved);
163
+ const max = limits.max === Infinity ? Infinity : limits.max + settings.preloadMargin;
164
+ return !culledByDistance(camera.view.position[0], camera.view.position[1], camera.view.position[2], b.centerX, b.centerY, b.centerZ, 0, max, b.radius, 0);
165
+ }
166
+ function addAlwaysVisible(list) {
167
+ for (const r of list) {
168
+ visible.add(r.id);
169
+ preload.add(r.id);
170
+ const record = records.get(r.id);
171
+ if (record?.assets !== undefined) {
172
+ for (const asset of record.assets)
173
+ requiredAssets.add(asset);
174
+ }
175
+ }
176
+ }
177
+ return {
178
+ update() {
179
+ updateCounter += 1;
180
+ consideredCounter += 1;
181
+ resetCullingStats(stats);
182
+ // Swap current → prev, reuse the old prev as the new current (cleared).
183
+ const swap = prevVisible;
184
+ prevVisible = visible;
185
+ visible = swap;
186
+ visible.clear();
187
+ preload.clear();
188
+ requiredAssets.clear();
189
+ const alwaysVisibleList = maintain();
190
+ const cameras = options.cameras();
191
+ for (const camera of cameras)
192
+ evaluateCamera(camera);
193
+ addAlwaysVisible(alwaysVisibleList);
194
+ stats.visible = visible.size;
195
+ stats.preloaded = preload.size;
196
+ stats.drawCallsAvoided = Math.max(0, stats.totalObjects - visible.size);
197
+ return { visible, preload, stats };
198
+ },
199
+ isVisible(id) {
200
+ return visible.has(id);
201
+ },
202
+ isPreloaded(id) {
203
+ return preload.has(id);
204
+ },
205
+ boundsOf(id) {
206
+ return bounds.peek(id);
207
+ },
208
+ settings() {
209
+ return settings;
210
+ },
211
+ setSettings(patch) {
212
+ settings = mergeCullingSettings(settings, patch);
213
+ },
214
+ stats() {
215
+ return stats;
216
+ },
217
+ requiredAssets() {
218
+ return requiredAssets;
219
+ },
220
+ index() {
221
+ return index;
222
+ },
223
+ debugSnapshot() {
224
+ const cameras = options.cameras().map((camera) => {
225
+ updateFrustum(baseFrustum, camera.view);
226
+ return {
227
+ id: camera.id,
228
+ kind: camera.kind ?? "main",
229
+ corners: Array.from(baseFrustum.corners),
230
+ preloadMargin: settings.preloadMargin,
231
+ };
232
+ });
233
+ const boundsOut = [];
234
+ const culled = [];
235
+ for (const id of records.keys()) {
236
+ const b = bounds.peek(id);
237
+ if (b === undefined)
238
+ continue;
239
+ const vis = visible.has(id);
240
+ boundsOut.push({ id, minX: b.minX, minY: b.minY, minZ: b.minZ, maxX: b.maxX, maxY: b.maxY, maxZ: b.maxZ, visible: vis });
241
+ if (!vis)
242
+ culled.push(id);
243
+ }
244
+ return { cameras, bounds: boundsOut, partitions: index.cells(), culled, stats };
245
+ },
246
+ clear() {
247
+ index.clear();
248
+ bounds.clear();
249
+ records.clear();
250
+ visible.clear();
251
+ prevVisible.clear();
252
+ preload.clear();
253
+ requiredAssets.clear();
254
+ consideredStamp.clear();
255
+ },
256
+ };
257
+ }
@@ -92,6 +92,12 @@ export interface BuildingGridConfig {
92
92
  floorRange?: readonly [number, number];
93
93
  }
94
94
  export declare const DEFAULT_BUILDING_CONFIG: BuildingConfig;
95
+ export type BuildingPalette = Record<BuildingPartKind, string>;
96
+ export type BuildingPaletteOverrides = Partial<BuildingPalette>;
97
+ export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
98
+ export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
99
+ export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, BuildingPalette>;
100
+ export declare function resolveBuildingPalette(style?: BuildingStyle, overrides?: BuildingPaletteOverrides): BuildingPalette;
95
101
  export declare function createBuildingConfig(input?: BuildingConfigInput): BuildingConfig;
96
102
  export declare function generateBuilding(input?: BuildingConfigInput): GeneratedBuilding;
97
103
  export declare function createBuildingGrid(config: BuildingGridConfig): BuildingLot[];