@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,74 @@
1
+ /** Stable identifier for a visibility-tracked object or camera. */
2
+ export type VisibilityId = string | number;
3
+ /** World-space point; `z` is optional for 2D adapters. */
4
+ export interface VisibilityPoint {
5
+ x: number;
6
+ y: number;
7
+ z?: number;
8
+ }
9
+ /** Sphere bounds used for visibility/streaming distance checks. */
10
+ export interface VisibilityBounds {
11
+ center: VisibilityPoint;
12
+ radius: number;
13
+ }
14
+ /** A camera's position and reach, as seen by the visibility system. */
15
+ export interface CameraVisibilityContext {
16
+ id: VisibilityId;
17
+ position: VisibilityPoint;
18
+ /** Approximate visible radius in world units. Adapters may derive this from a frustum. */
19
+ visibleRadius: number;
20
+ /** Extra distance used to request assets before an object becomes visible. */
21
+ preloadMargin?: number;
22
+ /** Cameras such as editor thumbnails can opt out of influencing streaming. */
23
+ influencesStreaming?: boolean;
24
+ }
25
+ /** Per-object escape hatches that bypass or tune the default visibility policy. */
26
+ export interface VisibilityOverrides {
27
+ alwaysVisible?: boolean;
28
+ neverUnload?: boolean;
29
+ cullingDisabled?: boolean;
30
+ streamingDisabled?: boolean;
31
+ minRenderDistance?: number;
32
+ maxRenderDistance?: number;
33
+ preloadMargin?: number;
34
+ }
35
+ /** An entity/asset tracked by `VisibilitySystem.evaluate`. */
36
+ export interface VisibilityObject {
37
+ id: VisibilityId;
38
+ bounds: VisibilityBounds;
39
+ overrides?: VisibilityOverrides;
40
+ }
41
+ /** Baseline policy settings a `VisibilitySystem` is constructed with. */
42
+ export interface VisibilityDefaults {
43
+ enabled: boolean;
44
+ streamingEnabled: boolean;
45
+ minRenderDistance: number;
46
+ maxRenderDistance: number;
47
+ preloadMargin: number;
48
+ unloadGraceMs: number;
49
+ hysteresis: number;
50
+ }
51
+ /** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
52
+ export declare const DEFAULT_VISIBILITY_SETTINGS: Readonly<VisibilityDefaults>;
53
+ /** Result of evaluating one object against the active cameras this tick. */
54
+ export interface VisibilityDecision {
55
+ render: boolean;
56
+ load: boolean;
57
+ unload: boolean;
58
+ reason: "always-visible" | "culling-disabled" | "visible" | "preload" | "distance-culled" | "outside-view" | "grace-period" | "streaming-disabled";
59
+ }
60
+ /**
61
+ * Engine-level visibility and asset-residency policy.
62
+ *
63
+ * Render culling and asset streaming are intentionally separate. This class never
64
+ * destroys entities or throttles simulation; adapters decide how to apply results.
65
+ */
66
+ export declare class VisibilitySystem {
67
+ #private;
68
+ readonly settings: VisibilityDefaults;
69
+ constructor(settings?: Partial<VisibilityDefaults>);
70
+ evaluate(object: VisibilityObject, cameras: readonly CameraVisibilityContext[], now?: number): VisibilityDecision;
71
+ setLoaded(id: VisibilityId, loaded: boolean, now?: number): void;
72
+ forget(id: VisibilityId): void;
73
+ clear(): void;
74
+ }
@@ -0,0 +1,127 @@
1
+ /** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
2
+ export const DEFAULT_VISIBILITY_SETTINGS = Object.freeze({
3
+ enabled: true,
4
+ streamingEnabled: true,
5
+ minRenderDistance: 0,
6
+ maxRenderDistance: Number.POSITIVE_INFINITY,
7
+ preloadMargin: 24,
8
+ unloadGraceMs: 5_000,
9
+ hysteresis: 8,
10
+ });
11
+ function squaredDistance(a, b) {
12
+ const dx = a.x - b.x;
13
+ const dy = a.y - b.y;
14
+ const dz = (a.z ?? 0) - (b.z ?? 0);
15
+ return dx * dx + dy * dy + dz * dz;
16
+ }
17
+ function intersectsRadius(camera, bounds, extraMargin) {
18
+ const radius = Math.max(0, camera.visibleRadius + extraMargin + bounds.radius);
19
+ return squaredDistance(camera.position, bounds.center) <= radius * radius;
20
+ }
21
+ /**
22
+ * Engine-level visibility and asset-residency policy.
23
+ *
24
+ * Render culling and asset streaming are intentionally separate. This class never
25
+ * destroys entities or throttles simulation; adapters decide how to apply results.
26
+ */
27
+ export class VisibilitySystem {
28
+ settings;
29
+ #states = new Map();
30
+ constructor(settings = {}) {
31
+ this.settings = { ...DEFAULT_VISIBILITY_SETTINGS, ...settings };
32
+ }
33
+ evaluate(object, cameras, now = Date.now()) {
34
+ const overrides = object.overrides ?? {};
35
+ const state = this.#states.get(object.id) ?? { loaded: false, lastNeededAt: now };
36
+ if (overrides.alwaysVisible) {
37
+ this.#markNeeded(object.id, state, now);
38
+ return { render: true, load: true, unload: false, reason: "always-visible" };
39
+ }
40
+ if (!this.settings.enabled || overrides.cullingDisabled) {
41
+ this.#markNeeded(object.id, state, now);
42
+ return { render: true, load: true, unload: false, reason: "culling-disabled" };
43
+ }
44
+ const minDistance = Math.max(0, overrides.minRenderDistance ?? this.settings.minRenderDistance);
45
+ const maxDistance = Math.max(minDistance, overrides.maxRenderDistance ?? this.settings.maxRenderDistance);
46
+ const minDistanceSq = minDistance * minDistance;
47
+ const maxDistanceWithRadius = maxDistance + object.bounds.radius;
48
+ const maxDistanceSq = maxDistanceWithRadius * maxDistanceWithRadius;
49
+ let insideDistance = false;
50
+ let visible = false;
51
+ let preload = false;
52
+ for (const camera of cameras) {
53
+ const distanceSq = squaredDistance(camera.position, object.bounds.center);
54
+ if (distanceSq < minDistanceSq || distanceSq > maxDistanceSq)
55
+ continue;
56
+ insideDistance = true;
57
+ if (intersectsRadius(camera, object.bounds, this.settings.hysteresis)) {
58
+ visible = true;
59
+ }
60
+ if (camera.influencesStreaming !== false) {
61
+ const margin = overrides.preloadMargin ?? camera.preloadMargin ?? this.settings.preloadMargin;
62
+ if (intersectsRadius(camera, object.bounds, margin + this.settings.hysteresis)) {
63
+ preload = true;
64
+ }
65
+ }
66
+ }
67
+ if (visible) {
68
+ this.#markNeeded(object.id, state, now);
69
+ return { render: true, load: true, unload: false, reason: "visible" };
70
+ }
71
+ if (overrides.streamingDisabled || !this.settings.streamingEnabled) {
72
+ state.loaded = true;
73
+ this.#states.set(object.id, state);
74
+ return {
75
+ render: false,
76
+ load: true,
77
+ unload: false,
78
+ reason: "streaming-disabled",
79
+ };
80
+ }
81
+ if (preload) {
82
+ this.#markNeeded(object.id, state, now);
83
+ return { render: false, load: true, unload: false, reason: "preload" };
84
+ }
85
+ if (overrides.neverUnload) {
86
+ state.loaded = true;
87
+ this.#states.set(object.id, state);
88
+ return {
89
+ render: false,
90
+ load: true,
91
+ unload: false,
92
+ reason: insideDistance ? "outside-view" : "distance-culled",
93
+ };
94
+ }
95
+ const graceActive = state.loaded && now - state.lastNeededAt < this.settings.unloadGraceMs;
96
+ if (graceActive) {
97
+ this.#states.set(object.id, state);
98
+ return { render: false, load: true, unload: false, reason: "grace-period" };
99
+ }
100
+ state.loaded = false;
101
+ this.#states.set(object.id, state);
102
+ return {
103
+ render: false,
104
+ load: false,
105
+ unload: true,
106
+ reason: insideDistance ? "outside-view" : "distance-culled",
107
+ };
108
+ }
109
+ setLoaded(id, loaded, now = Date.now()) {
110
+ const previous = this.#states.get(id);
111
+ this.#states.set(id, {
112
+ loaded,
113
+ lastNeededAt: loaded ? now : previous?.lastNeededAt ?? now,
114
+ });
115
+ }
116
+ forget(id) {
117
+ this.#states.delete(id);
118
+ }
119
+ clear() {
120
+ this.#states.clear();
121
+ }
122
+ #markNeeded(id, state, now) {
123
+ state.loaded = true;
124
+ state.lastNeededAt = now;
125
+ this.#states.set(id, state);
126
+ }
127
+ }
@@ -0,0 +1,44 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ export type ColliderPurpose = "physical" | "damage";
3
+ export type ColliderShape = {
4
+ kind: "sphere";
5
+ radius: number;
6
+ offset?: EntityPosition;
7
+ } | {
8
+ kind: "aabb";
9
+ halfExtents: EntityPosition;
10
+ offset?: EntityPosition;
11
+ };
12
+ export interface ColliderDef {
13
+ name: string;
14
+ purpose: ColliderPurpose;
15
+ shape: ColliderShape;
16
+ /** Defaults true for `purpose: "damage"`, false for `purpose: "physical"`. */
17
+ damageEligible?: boolean;
18
+ /** Defaults true — a blocking collider stops a ray at first impact. */
19
+ blocks?: boolean;
20
+ }
21
+ export interface EntityColliderSet {
22
+ /** Single physical collision body (push / obstruction). */
23
+ body?: ColliderDef;
24
+ /** Named damage hitboxes (zero or more). */
25
+ hitboxes?: readonly ColliderDef[];
26
+ }
27
+ export interface ResolvedCollider {
28
+ name: string;
29
+ purpose: ColliderPurpose;
30
+ shape: ColliderShape;
31
+ damageEligible: boolean;
32
+ blocks: boolean;
33
+ }
34
+ export declare const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
35
+ export declare const DEFAULT_OBJECT_HALF_EXTENTS: EntityPosition;
36
+ export declare function defaultEntityColliders(): EntityColliderSet;
37
+ export declare function defaultObjectColliders(halfExtents?: EntityPosition): EntityColliderSet;
38
+ export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
39
+ export declare function worldOffset(local: EntityPosition | undefined, position: EntityPosition, rotationY: number): EntityPosition;
40
+ export declare function colliderWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition;
41
+ export declare function colliderBounds(collider: ResolvedCollider, position: EntityPosition, rotationY: number): {
42
+ min: EntityPosition;
43
+ max: EntityPosition;
44
+ };
@@ -0,0 +1,77 @@
1
+ export const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
2
+ export const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
3
+ export function defaultEntityColliders() {
4
+ return {
5
+ hitboxes: [
6
+ {
7
+ name: "body",
8
+ purpose: "damage",
9
+ shape: { kind: "sphere", radius: DEFAULT_ENTITY_HIT_RADIUS },
10
+ damageEligible: true,
11
+ blocks: false,
12
+ },
13
+ ],
14
+ };
15
+ }
16
+ export function defaultObjectColliders(halfExtents = DEFAULT_OBJECT_HALF_EXTENTS) {
17
+ return {
18
+ body: {
19
+ name: "body",
20
+ purpose: "physical",
21
+ shape: { kind: "aabb", halfExtents },
22
+ damageEligible: false,
23
+ blocks: true,
24
+ },
25
+ };
26
+ }
27
+ export function resolveColliders(set) {
28
+ if (set === null || set === undefined)
29
+ return [];
30
+ const out = [];
31
+ if (set.body !== undefined)
32
+ out.push(resolveOne(set.body));
33
+ if (set.hitboxes !== undefined) {
34
+ for (const hitbox of set.hitboxes)
35
+ out.push(resolveOne(hitbox));
36
+ }
37
+ return out;
38
+ }
39
+ function resolveOne(def) {
40
+ const damageEligible = def.damageEligible ?? def.purpose === "damage";
41
+ const blocks = def.blocks ?? def.purpose === "physical";
42
+ return {
43
+ name: def.name,
44
+ purpose: def.purpose,
45
+ shape: def.shape,
46
+ damageEligible,
47
+ blocks,
48
+ };
49
+ }
50
+ export function worldOffset(local, position, rotationY) {
51
+ if (local === undefined)
52
+ return position;
53
+ const cos = Math.cos(rotationY);
54
+ const sin = Math.sin(rotationY);
55
+ const lx = local[0];
56
+ const ly = local[1];
57
+ const lz = local[2];
58
+ return [position[0] + lx * cos + lz * sin, position[1] + ly, position[2] - lx * sin + lz * cos];
59
+ }
60
+ export function colliderWorldCenter(collider, position, rotationY) {
61
+ return worldOffset(collider.shape.offset, position, rotationY);
62
+ }
63
+ export function colliderBounds(collider, position, rotationY) {
64
+ const center = colliderWorldCenter(collider, position, rotationY);
65
+ if (collider.shape.kind === "sphere") {
66
+ const r = collider.shape.radius;
67
+ return {
68
+ min: [center[0] - r, center[1] - r, center[2] - r],
69
+ max: [center[0] + r, center[1] + r, center[2] + r],
70
+ };
71
+ }
72
+ const h = collider.shape.halfExtents;
73
+ return {
74
+ min: [center[0] - h[0], center[1] - h[1], center[2] - h[2]],
75
+ max: [center[0] + h[0], center[1] + h[1], center[2] + h[2]],
76
+ };
77
+ }
@@ -10,7 +10,7 @@ export type SpawnPositionInput = EntityPosition | {
10
10
  y: number;
11
11
  z: number;
12
12
  };
13
- export interface SceneEntity<TMeta = undefined> {
13
+ export interface SceneEntity<TMeta = unknown> {
14
14
  id: string;
15
15
  name: string;
16
16
  position: EntityPosition;
@@ -22,12 +22,13 @@ export interface SceneEntity<TMeta = undefined> {
22
22
  role: EntityRole;
23
23
  movement: EntityMovement;
24
24
  behaviors: readonly BehaviorDescriptor[];
25
+ /** Game-defined per-instance data set at `spawn`/`update` and carried to `renderEntity`/queries — narrow with a cast or type guard on read (#286.1); never encode state in the id/name string. */
25
26
  meta: TMeta;
26
27
  }
27
28
  /** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
28
29
  export declare function groundSpeed(entity: SceneEntity<unknown>): number;
29
30
  export declare function movedWhileFrozen(entity: SceneEntity<unknown>, threshold?: number): boolean;
30
- export interface SpawnOptions<TMeta = undefined> {
31
+ export interface SpawnOptions<TMeta = unknown> {
31
32
  id?: string;
32
33
  position?: SpawnPositionInput;
33
34
  rotationY?: number;
@@ -37,6 +38,8 @@ export interface SpawnOptions<TMeta = undefined> {
37
38
  movement?: EntityMovement;
38
39
  behaviors?: readonly BehaviorDescriptor[];
39
40
  meta?: TMeta;
41
+ /** When `id` is already spawned: `"throw"` (default), `"replace"` (fresh spawn over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
42
+ onExisting?: "throw" | "replace" | "keep";
40
43
  }
41
44
  export interface EntityPose {
42
45
  position: SpawnPositionInput;
@@ -50,11 +53,25 @@ export interface SpawnPose {
50
53
  position: EntityPosition;
51
54
  rotationY: number;
52
55
  }
53
- export interface EntityStore<TMeta = undefined> {
56
+ export interface PoseConstraintFrame {
57
+ entityId: string;
58
+ current: EntityPosition;
59
+ next: EntityPosition;
60
+ dt?: number;
61
+ }
62
+ /** Return a replacement position to constrain the step, or nothing to accept it. */
63
+ export type PoseConstraint = (frame: PoseConstraintFrame) => readonly [number, number, number] | undefined | void;
64
+ export type EntityUpdatePatch<TMeta = unknown> = Partial<Pick<SceneEntity<TMeta>, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">> & {
65
+ /** Accepts the same friendly shapes as `spawn`/`setPose` (#286.13). Raw patch semantics — velocity is not derived; use `setPose` with `dt` for that. */
66
+ position?: SpawnPositionInput;
67
+ };
68
+ export interface EntityStore<TMeta = unknown> {
54
69
  spawn(name: string, options?: SpawnOptions<TMeta>): string;
55
70
  despawn(id: string): boolean;
56
- update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "name" | "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
71
+ update(id: string, patch: EntityUpdatePatch<TMeta>): boolean;
57
72
  setPose(id: string, pose: EntityPose): boolean;
73
+ /** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */
74
+ setPoseConstraint(id: string, constraint: PoseConstraint | null): void;
58
75
  get(id: string): SceneEntity<TMeta> | null;
59
76
  list(): readonly SceneEntity<TMeta>[];
60
77
  clear(): void;
@@ -63,4 +80,4 @@ export interface EntityStore<TMeta = undefined> {
63
80
  spawnPoseOf(id: string): SpawnPose | null;
64
81
  resetToSpawn(id: string): boolean;
65
82
  }
66
- export declare function createEntityStore<TMeta = undefined>(): EntityStore<TMeta>;
83
+ export declare function createEntityStore<TMeta = unknown>(): EntityStore<TMeta>;
@@ -20,6 +20,7 @@ function toEntityPosition(position) {
20
20
  export function createEntityStore() {
21
21
  const store = createObservableKeyedStore();
22
22
  const spawnPoses = new Map();
23
+ const constraints = new Map();
23
24
  let nextCounter = 1;
24
25
  function generateId() {
25
26
  let id = `entity-${nextCounter}`;
@@ -33,7 +34,11 @@ export function createEntityStore() {
33
34
  return {
34
35
  spawn(name, options = {}) {
35
36
  if (options.id !== undefined && store.has(options.id)) {
36
- throw new Error(`Scene entity id "${options.id}" is already spawned.`);
37
+ const onExisting = options.onExisting ?? "throw";
38
+ if (onExisting === "keep")
39
+ return options.id;
40
+ if (onExisting === "throw")
41
+ throw new Error(`Scene entity id "${options.id}" is already spawned.`);
37
42
  }
38
43
  const id = options.id ?? generateId();
39
44
  const position = toEntityPosition(options.position);
@@ -58,20 +63,57 @@ export function createEntityStore() {
58
63
  const existed = store.has(id);
59
64
  store.delete(id);
60
65
  spawnPoses.delete(id);
66
+ constraints.delete(id);
61
67
  return existed;
62
68
  },
63
69
  update(id, patch) {
64
70
  const current = store.get(id);
65
71
  if (!current)
66
72
  return false;
67
- store.set(id, { ...current, ...patch });
73
+ if (patch.position !== undefined)
74
+ current.position = toEntityPosition(patch.position);
75
+ if (patch.name !== undefined)
76
+ current.name = patch.name;
77
+ if (patch.rotationY !== undefined)
78
+ current.rotationY = patch.rotationY;
79
+ if (patch.rotationX !== undefined)
80
+ current.rotationX = patch.rotationX;
81
+ if (patch.rotationZ !== undefined)
82
+ current.rotationZ = patch.rotationZ;
83
+ if (patch.role !== undefined)
84
+ current.role = patch.role;
85
+ if (patch.movement !== undefined)
86
+ current.movement = patch.movement;
87
+ if (patch.behaviors !== undefined)
88
+ current.behaviors = patch.behaviors;
89
+ if (patch.meta !== undefined)
90
+ current.meta = patch.meta;
91
+ store.set(id, current);
68
92
  return true;
69
93
  },
94
+ setPoseConstraint(id, constraint) {
95
+ if (constraint === null)
96
+ constraints.delete(id);
97
+ else
98
+ constraints.set(id, constraint);
99
+ },
70
100
  setPose(id, pose) {
71
101
  const current = store.get(id);
72
102
  if (!current)
73
103
  return false;
74
- const position = toEntityPosition(pose.position);
104
+ let position = toEntityPosition(pose.position);
105
+ const constraint = constraints.get(id);
106
+ if (constraint !== undefined) {
107
+ const frame = {
108
+ entityId: id,
109
+ current: current.position,
110
+ next: position,
111
+ ...(pose.dt === undefined ? {} : { dt: pose.dt }),
112
+ };
113
+ const replacement = constraint(frame);
114
+ if (replacement !== undefined)
115
+ position = toEntityPosition(replacement);
116
+ }
75
117
  const velocity = pose.dt !== undefined && pose.dt > 0
76
118
  ? [
77
119
  (position[0] - current.position[0]) / pose.dt,
@@ -79,14 +121,15 @@ export function createEntityStore() {
79
121
  (position[2] - current.position[2]) / pose.dt,
80
122
  ]
81
123
  : current.velocity;
82
- store.set(id, {
83
- ...current,
84
- position,
85
- velocity,
86
- rotationY: pose.rotationY ?? current.rotationY,
87
- rotationX: pose.rotationX ?? current.rotationX,
88
- rotationZ: pose.rotationZ ?? current.rotationZ,
89
- });
124
+ current.position = position;
125
+ current.velocity = velocity;
126
+ if (pose.rotationY !== undefined)
127
+ current.rotationY = pose.rotationY;
128
+ if (pose.rotationX !== undefined)
129
+ current.rotationX = pose.rotationX;
130
+ if (pose.rotationZ !== undefined)
131
+ current.rotationZ = pose.rotationZ;
132
+ store.set(id, current);
90
133
  return true;
91
134
  },
92
135
  get(id) {
@@ -99,6 +142,7 @@ export function createEntityStore() {
99
142
  for (const entity of store.arraySnapshot()) {
100
143
  store.delete(entity.id);
101
144
  }
145
+ constraints.clear();
102
146
  },
103
147
  subscribe(listener) {
104
148
  return store.subscribe(listener);
@@ -114,7 +158,10 @@ export function createEntityStore() {
114
158
  const current = store.get(id);
115
159
  if (pose === undefined || current === undefined)
116
160
  return false;
117
- store.set(id, { ...current, position: pose.position, rotationY: pose.rotationY, velocity: [0, 0, 0] });
161
+ current.position = pose.position;
162
+ current.rotationY = pose.rotationY;
163
+ current.velocity = [0, 0, 0];
164
+ store.set(id, current);
118
165
  return true;
119
166
  },
120
167
  };
@@ -14,5 +14,16 @@ export interface ObjectRaycastHit {
14
14
  point: EntityPosition;
15
15
  normal: EntityPosition;
16
16
  }
17
- export declare function raycastObjects(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit | null;
18
- export declare function raycastObjectsAll(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit[];
17
+ /** Optional broadphase for large object sets — typically `ObjectStore.inBox`. */
18
+ export interface ObjectQueryBroadphase {
19
+ inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
20
+ }
21
+ export type ObjectRaycastSource = readonly SceneObject[] | ObjectQueryBroadphase;
22
+ declare function normalize(direction: EntityPosition): EntityPosition;
23
+ declare function intersectAabb(origin: EntityPosition, direction: EntityPosition, min: EntityPosition, max: EntityPosition, maxDistance: number): {
24
+ distance: number;
25
+ normal: EntityPosition;
26
+ } | null;
27
+ export declare function raycastObjects(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit | null;
28
+ export declare function raycastObjectsAll(source: ObjectRaycastSource, input: ObjectRaycastInput): ObjectRaycastHit[];
29
+ export { intersectAabb, normalize as normalizeDirection };
@@ -5,6 +5,9 @@ function normalize(direction) {
5
5
  return [0, 0, 0];
6
6
  return [direction[0] / length, direction[1] / length, direction[2] / length];
7
7
  }
8
+ function isBroadphase(source) {
9
+ return !Array.isArray(source) && typeof source.inBox === "function";
10
+ }
8
11
  function intersectAabb(origin, direction, min, max, maxDistance) {
9
12
  let tMin = 0;
10
13
  let tMax = maxDistance;
@@ -73,11 +76,38 @@ function hitFor(object, origin, direction, halfExtents, maxDistance) {
73
76
  normal: hit.normal,
74
77
  };
75
78
  }
76
- export function raycastObjects(objects, input) {
79
+ function raySegmentBounds(origin, direction, maxDistance, halfExtents) {
80
+ const endX = origin[0] + direction[0] * maxDistance;
81
+ const endY = origin[1] + direction[1] * maxDistance;
82
+ const endZ = origin[2] + direction[2] * maxDistance;
83
+ const padX = halfExtents[0];
84
+ const padY = halfExtents[1];
85
+ const padZ = halfExtents[2];
86
+ return {
87
+ min: [
88
+ Math.min(origin[0], endX) - padX,
89
+ Math.min(origin[1], endY) - padY,
90
+ Math.min(origin[2], endZ) - padZ,
91
+ ],
92
+ max: [
93
+ Math.max(origin[0], endX) + padX,
94
+ Math.max(origin[1], endY) + padY,
95
+ Math.max(origin[2], endZ) + padZ,
96
+ ],
97
+ };
98
+ }
99
+ function resolveCandidates(source, origin, direction, maxDistance, halfExtents) {
100
+ if (!isBroadphase(source))
101
+ return source;
102
+ const bounds = raySegmentBounds(origin, direction, maxDistance, halfExtents);
103
+ return source.inBox(bounds.min, bounds.max);
104
+ }
105
+ export function raycastObjects(source, input) {
77
106
  const direction = normalize(input.direction);
78
107
  if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
79
108
  return null;
80
109
  const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
110
+ const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
81
111
  let nearest = null;
82
112
  for (const object of objects) {
83
113
  if (input.filter !== undefined && !input.filter(object))
@@ -90,11 +120,12 @@ export function raycastObjects(objects, input) {
90
120
  }
91
121
  return nearest;
92
122
  }
93
- export function raycastObjectsAll(objects, input) {
123
+ export function raycastObjectsAll(source, input) {
94
124
  const direction = normalize(input.direction);
95
125
  if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
96
126
  return [];
97
127
  const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
128
+ const objects = resolveCandidates(source, input.origin, direction, input.maxDistance, halfExtents);
98
129
  const hits = [];
99
130
  for (const object of objects) {
100
131
  if (input.filter !== undefined && !input.filter(object))
@@ -106,3 +137,4 @@ export function raycastObjectsAll(objects, input) {
106
137
  hits.sort((a, b) => a.distance - b.distance);
107
138
  return hits;
108
139
  }
140
+ export { intersectAabb, normalize as normalizeDirection };
@@ -17,6 +17,8 @@ export interface PlaceOptions {
17
17
  parentSpace?: string;
18
18
  rotation?: number;
19
19
  visual?: ObjectVisual;
20
+ /** When `instanceId` is already placed: `"throw"` (default), `"replace"` (fresh placement over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
21
+ onExisting?: "throw" | "replace" | "keep";
20
22
  }
21
23
  export interface ObjectListFilter {
22
24
  parentSpace?: string;
@@ -42,8 +42,14 @@ export function createObjectStore() {
42
42
  }
43
43
  return {
44
44
  place(catalogId, x, y, z, options = {}) {
45
- if (options.instanceId !== undefined && store.has(options.instanceId)) {
46
- throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
45
+ const existing = options.instanceId === undefined ? null : store.get(options.instanceId);
46
+ if (existing !== null && existing !== undefined) {
47
+ const onExisting = options.onExisting ?? "throw";
48
+ if (onExisting === "keep")
49
+ return options.instanceId;
50
+ if (onExisting === "throw")
51
+ throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
52
+ indexRemove(options.instanceId, existing.position);
47
53
  }
48
54
  const instanceId = options.instanceId ?? generateId();
49
55
  const position = [x, y, z];
@@ -137,16 +143,29 @@ export function createObjectStore() {
137
143
  },
138
144
  inBox(min, max) {
139
145
  const hits = [];
140
- for (const bucket of cellIndex.values()) {
141
- for (const instanceId of bucket) {
142
- const object = store.get(instanceId);
143
- if (object === undefined)
144
- continue;
145
- const [x, y, z] = object.position;
146
- if (x >= min[0] && x <= max[0] &&
147
- y >= min[1] && y <= max[1] &&
148
- z >= min[2] && z <= max[2]) {
149
- hits.push(object);
146
+ const minCx = cellIndexOf(min[0]);
147
+ const maxCx = cellIndexOf(max[0]);
148
+ const minCy = cellIndexOf(min[1]);
149
+ const maxCy = cellIndexOf(max[1]);
150
+ const minCz = cellIndexOf(min[2]);
151
+ const maxCz = cellIndexOf(max[2]);
152
+ for (let cx = minCx; cx <= maxCx; cx += 1) {
153
+ for (let cy = minCy; cy <= maxCy; cy += 1) {
154
+ for (let cz = minCz; cz <= maxCz; cz += 1) {
155
+ const bucket = cellIndex.get(cellKey(cx, cy, cz));
156
+ if (bucket === undefined)
157
+ continue;
158
+ for (const instanceId of bucket) {
159
+ const object = store.get(instanceId);
160
+ if (object === undefined)
161
+ continue;
162
+ const [x, y, z] = object.position;
163
+ if (x >= min[0] && x <= max[0] &&
164
+ y >= min[1] && y <= max[1] &&
165
+ z >= min[2] && z <= max[2]) {
166
+ hits.push(object);
167
+ }
168
+ }
150
169
  }
151
170
  }
152
171
  }