@jgengine/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/CHANGELOG.md +36 -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.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -38,8 +38,10 @@ export function createDefensiveWindow(config) {
38
38
  if (openedAt === null)
39
39
  return null;
40
40
  const dt = nowMs - openedAt;
41
- if (dt < 0 || dt >= totalWindowMs(config))
41
+ if (dt < 0 || dt >= totalWindowMs(config)) {
42
+ openedAt = null;
42
43
  return null;
44
+ }
43
45
  return dt;
44
46
  }
45
47
  return {
@@ -49,8 +51,8 @@ export function createDefensiveWindow(config) {
49
51
  close() {
50
52
  openedAt = null;
51
53
  },
52
- isOpen() {
53
- return openedAt !== null;
54
+ isOpen(nowMs) {
55
+ return elapsed(nowMs) !== null;
54
56
  },
55
57
  evaluate(nowMs, attack) {
56
58
  const dt = elapsed(nowMs);
@@ -0,0 +1,43 @@
1
+ import type { BuildupProc } from "./breakMeters.js";
2
+ /** A single damage-over-time instance: how hard, how often, and for how long. */
3
+ export interface DotSpec {
4
+ /** Damage dealt at each interval. */
5
+ damagePerTick: number;
6
+ /** Milliseconds between damage ticks; clamped to a positive minimum. */
7
+ intervalMs: number;
8
+ /** Total lifetime in milliseconds; the DoT expires once its age reaches this. */
9
+ durationMs: number;
10
+ /** Label echoed on each emitted tick (bleed, poison, burn); defaults to the apply key. */
11
+ status?: string;
12
+ }
13
+ /** One damage event (or expiry marker) emitted by `DotField.tick` for the game to route through its effects. */
14
+ export interface DotTick {
15
+ /** The key the DoT was applied under. */
16
+ id: string;
17
+ /** The DoT's `status` label, or its key when none was given. */
18
+ status: string;
19
+ /** Damage to route through the game's effect pipeline this frame; 0 on a bare expiry marker. */
20
+ damage: number;
21
+ /** True on the entry that ends the DoT — remove any HUD icon on it. */
22
+ expired: boolean;
23
+ }
24
+ /**
25
+ * A pool of timed damage-over-time instances keyed by string (`bleed`, `poison`, `burn`) — the
26
+ * recurring-damage counterpart to `breakMeters`' one-shot buildup procs (#536.4). `tick` advances
27
+ * every active DoT and returns the damage due this frame, one entry per interval crossed, so the game
28
+ * routes each through its own effect pipeline; a pure scheduler that never applies damage itself.
29
+ */
30
+ export interface DotField {
31
+ /** Start or refresh a DoT under `id`; reapplying the same key restarts its age and lifetime. */
32
+ apply(id: string, spec: DotSpec): void;
33
+ /** Bridge a `breakMeters` buildup proc into recurring damage under `id`. */
34
+ applyProc(id: string, proc: BuildupProc, damagePerTick: number, intervalMs: number): void;
35
+ active(id: string): boolean;
36
+ cancel(id: string): void;
37
+ clear(): void;
38
+ count(): number;
39
+ /** Advance every active DoT; returns the damage due this frame plus an `expired` marker per finished DoT. */
40
+ tick(dtSeconds: number): DotTick[];
41
+ }
42
+ /** Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`. */
43
+ export declare function createDotField(): DotField;
@@ -0,0 +1,48 @@
1
+ /** Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`. */
2
+ export function createDotField() {
3
+ const dots = new Map();
4
+ return {
5
+ apply(id, spec) {
6
+ dots.set(id, {
7
+ status: spec.status ?? id,
8
+ damagePerTick: spec.damagePerTick,
9
+ intervalMs: Math.max(1, spec.intervalMs),
10
+ durationMs: Math.max(0, spec.durationMs),
11
+ ageMs: 0,
12
+ ticksEmitted: 0,
13
+ });
14
+ },
15
+ applyProc(id, proc, damagePerTick, intervalMs) {
16
+ this.apply(id, { damagePerTick, intervalMs, durationMs: proc.durationMs, status: proc.status });
17
+ },
18
+ active: (id) => dots.has(id),
19
+ cancel: (id) => void dots.delete(id),
20
+ clear: () => dots.clear(),
21
+ count: () => dots.size,
22
+ tick(dtSeconds) {
23
+ if (dtSeconds <= 0)
24
+ return [];
25
+ const dtMs = dtSeconds * 1000;
26
+ const emitted = [];
27
+ for (const [id, dot] of dots) {
28
+ dot.ageMs += dtMs;
29
+ const activeMs = Math.min(dot.ageMs, dot.durationMs);
30
+ const dueTicks = Math.floor(activeMs / dot.intervalMs);
31
+ let last = null;
32
+ while (dot.ticksEmitted < dueTicks) {
33
+ dot.ticksEmitted += 1;
34
+ last = { id, status: dot.status, damage: dot.damagePerTick, expired: false };
35
+ emitted.push(last);
36
+ }
37
+ if (dot.ageMs >= dot.durationMs) {
38
+ dots.delete(id);
39
+ if (last !== null)
40
+ last.expired = true;
41
+ else
42
+ emitted.push({ id, status: dot.status, damage: 0, expired: true });
43
+ }
44
+ }
45
+ return emitted;
46
+ },
47
+ };
48
+ }
@@ -1,30 +1,39 @@
1
1
  import type { BallisticSweep } from "../physics/ballisticSweep.js";
2
2
  import type { EntityPosition } from "../scene/entityStore.js";
3
+ import { type EntityColliderSet } from "../scene/colliders.js";
4
+ import { type SceneRaycastApi } from "../scene/sceneRaycast.js";
3
5
  import type { Aim } from "../scene/spatial.js";
4
6
  import type { CombatSpatialDeps, EffectResult, EffectSystem, EffectVia } from "./effects.js";
7
+ import { type ShotOriginPolicy } from "./shotOrigin.js";
5
8
  export interface ProjectileShotInput {
6
9
  from: string;
7
10
  via: EffectVia;
8
11
  aim: Aim;
9
12
  effect: string;
13
+ /** Defaults to `{ kind: "eye" }` — aim.origin when given, else shooter position at eye height. */
14
+ originPolicy?: ShotOriginPolicy;
10
15
  }
11
- /** A raycast hit against a scene entity — the ray reached `instanceId` at `distance` along its length. */
12
16
  export interface EntityRaycastHit {
13
17
  kind: "entity";
14
18
  instanceId: string;
15
19
  distance: number;
16
20
  at: EntityPosition;
21
+ colliderName?: string;
22
+ damageEligible?: boolean;
23
+ blocks?: boolean;
17
24
  }
18
- /** A raycast hit against a placed scene object's bounding box — see `ProjectileObjectsDeps.halfExtents`. */
19
25
  export interface ObjectRaycastHit {
20
26
  kind: "object";
21
27
  instanceId: string;
22
28
  catalogId: string;
23
29
  distance: number;
24
30
  at: EntityPosition;
31
+ colliderName?: string;
32
+ damageEligible?: boolean;
33
+ blocks?: boolean;
25
34
  }
26
35
  export type RaycastHit = EntityRaycastHit | ObjectRaycastHit;
27
- export type Raycast = (from: string, aim: Aim, range: number) => RaycastHit[];
36
+ export type Raycast = (from: string, aim: Aim, range: number, originPolicy?: ShotOriginPolicy) => RaycastHit[];
28
37
  export interface ProjectileSettleReport {
29
38
  from: string;
30
39
  origin: EntityPosition;
@@ -32,29 +41,33 @@ export interface ProjectileSettleReport {
32
41
  effect: string;
33
42
  hit: boolean;
34
43
  }
35
- /** Read-only scene object access for the default `raycast`; matches `ObjectStore.list()`/a catalog lookup structurally. */
36
44
  export interface ProjectileObjectsDeps {
37
45
  list(): readonly {
38
46
  instanceId: string;
39
47
  catalogId: string;
40
- position: [number, number, number];
48
+ position: readonly [number, number, number];
49
+ rotationY?: number;
50
+ }[];
51
+ inBox?(min: EntityPosition, max: EntityPosition): readonly {
52
+ instanceId: string;
53
+ catalogId: string;
54
+ position: readonly [number, number, number];
55
+ rotationY?: number;
41
56
  }[];
42
- /** Half-extents of the object's axis-aligned bounding box; `null`/omitted falls back to `[0.5, 0.5, 0.5]`. */
43
57
  halfExtents?(catalogId: string): [number, number, number] | null;
58
+ collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
44
59
  }
45
60
  export interface ProjectileSystemDeps {
46
61
  effects: EffectSystem;
47
62
  spatial: CombatSpatialDeps;
48
63
  getStat(itemId: string, stat: string): number | null;
49
64
  raycast?: Raycast;
50
- /** Optional object awareness for the default `raycast`; when set, placed objects can block or absorb a shot. */
65
+ sceneRaycast?: SceneRaycastApi;
51
66
  objects?: ProjectileObjectsDeps;
52
- /**
53
- * Optional collision-aware arc test for ballistic shots (see `createBallisticSweep` in
54
- * `physics/ballisticSweep`). A hit settles the shot at the impact point; `null` or omission
55
- * falls back to the closed-form landing.
56
- */
67
+ entityCollidersOf?(instanceId: string): EntityColliderSet | null | undefined;
68
+ rotationYOf?(instanceId: string): number | undefined;
57
69
  sweepBallistic?: BallisticSweep;
70
+ defaultOriginPolicy?: ShotOriginPolicy;
58
71
  now?: () => number;
59
72
  onSettle?(report: ProjectileSettleReport): void;
60
73
  }
@@ -62,21 +75,29 @@ export type ProjectileHit = {
62
75
  kind: "entity";
63
76
  instanceId: string;
64
77
  distance: number;
78
+ colliderName?: string;
79
+ damageEligible?: boolean;
65
80
  } | {
66
81
  kind: "object";
67
82
  instanceId: string;
68
83
  catalogId: string;
69
84
  distance: number;
85
+ colliderName?: string;
86
+ damageEligible?: boolean;
70
87
  };
71
88
  export interface ProjectilePrediction {
72
89
  hits: ProjectileHit[];
73
90
  blocked?: boolean;
91
+ origin?: EntityPosition;
92
+ direction?: EntityPosition;
93
+ firstImpact?: ProjectileHit | null;
74
94
  }
75
95
  export type SettleResult = {
76
96
  status: "settled";
77
97
  shotId: string;
78
98
  at: [number, number, number];
79
99
  hits: EffectResult[];
100
+ origin?: [number, number, number];
80
101
  } | {
81
102
  status: "rejected";
82
103
  shotId: string;
@@ -1,135 +1,182 @@
1
+ import { defaultEntityColliders, resolveColliders } from "../scene/colliders.js";
2
+ import { createSceneRaycast, firstImpact, hitsUntilBlocked, } from "../scene/sceneRaycast.js";
3
+ import { aimDirection, aimSpreadDeg, resolveShot, } from "./shotOrigin.js";
1
4
  const DEFAULT_RANGE = 100;
2
5
  const DEFAULT_PROJECTILE_SPEED = 15;
3
6
  const GRAVITY = 9.8;
4
7
  const BASE_HIT_RADIUS = 0.5;
5
- const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
6
8
  function isEntityHit(hit) {
7
9
  return hit.kind === "entity";
8
10
  }
9
11
  function isObjectHit(hit) {
10
12
  return hit.kind === "object";
11
13
  }
12
- function raySegmentAabbDistance(origin, direction, range, min, max) {
13
- let tMin = 0;
14
- let tMax = range;
15
- for (let axis = 0; axis < 3; axis += 1) {
16
- const o = origin[axis];
17
- const d = direction[axis];
18
- const lo = min[axis];
19
- const hi = max[axis];
20
- if (Math.abs(d) < 1e-9) {
21
- if (o < lo || o > hi)
22
- return null;
23
- continue;
24
- }
25
- const inv = 1 / d;
26
- let t1 = (lo - o) * inv;
27
- let t2 = (hi - o) * inv;
28
- if (t1 > t2)
29
- [t1, t2] = [t2, t1];
30
- tMin = Math.max(tMin, t1);
31
- tMax = Math.min(tMax, t2);
32
- if (tMin > tMax)
33
- return null;
14
+ function sceneHitToRaycast(hit) {
15
+ if (hit.targetKind === "entity") {
16
+ return {
17
+ kind: "entity",
18
+ instanceId: hit.instanceId,
19
+ distance: hit.distance,
20
+ at: hit.point,
21
+ colliderName: hit.colliderName,
22
+ damageEligible: hit.damageEligible,
23
+ blocks: hit.blocks,
24
+ };
34
25
  }
35
- return tMin;
36
- }
37
- function normalize(vector) {
38
- const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
39
- if (length === 0)
40
- return null;
41
- return [vector[0] / length, vector[1] / length, vector[2] / length];
26
+ return {
27
+ kind: "object",
28
+ instanceId: hit.instanceId,
29
+ catalogId: hit.catalogId ?? hit.targetKind,
30
+ distance: hit.distance,
31
+ at: hit.point,
32
+ colliderName: hit.colliderName,
33
+ damageEligible: hit.damageEligible,
34
+ blocks: hit.blocks,
35
+ };
42
36
  }
43
- function aimDirection(aim) {
44
- if ("origin" in aim)
45
- return normalize(aim.direction);
46
- const cosPitch = Math.cos(aim.pitch);
47
- return [Math.sin(aim.yaw) * cosPitch, Math.sin(aim.pitch), Math.cos(aim.yaw) * cosPitch];
37
+ function toProjectileHit(hit) {
38
+ if (isObjectHit(hit)) {
39
+ return {
40
+ kind: "object",
41
+ instanceId: hit.instanceId,
42
+ catalogId: hit.catalogId,
43
+ distance: hit.distance,
44
+ ...(hit.colliderName !== undefined ? { colliderName: hit.colliderName } : {}),
45
+ ...(hit.damageEligible !== undefined ? { damageEligible: hit.damageEligible } : {}),
46
+ };
47
+ }
48
+ return {
49
+ kind: "entity",
50
+ instanceId: hit.instanceId,
51
+ distance: hit.distance,
52
+ ...(hit.colliderName !== undefined ? { colliderName: hit.colliderName } : {}),
53
+ ...(hit.damageEligible !== undefined ? { damageEligible: hit.damageEligible } : {}),
54
+ };
48
55
  }
49
- function aimSpreadDeg(aim) {
50
- return "origin" in aim ? 0 : aim.spread ?? 0;
56
+ function asSceneHits(hits) {
57
+ return hits.map((hit) => ({
58
+ targetKind: hit.kind === "entity" ? "entity" : "object",
59
+ instanceId: hit.instanceId,
60
+ catalogId: hit.kind === "object" ? hit.catalogId : undefined,
61
+ colliderName: hit.colliderName ?? "body",
62
+ purpose: hit.damageEligible === false ? "physical" : "damage",
63
+ damageEligible: hit.kind === "entity" ? hit.damageEligible !== false : hit.damageEligible === true,
64
+ blocks: hit.blocks !== false,
65
+ distance: hit.distance,
66
+ point: hit.at,
67
+ normal: [0, 1, 0],
68
+ }));
51
69
  }
52
70
  export function createProjectileSystem(deps) {
53
71
  const shots = new Map();
54
72
  let shotCounter = 0;
55
73
  const now = deps.now ?? (() => Date.now());
74
+ const defaultPolicy = deps.defaultOriginPolicy ?? { kind: "eye" };
56
75
  function itemStat(via, stat) {
57
76
  return via.item === undefined ? null : deps.getStat(via.item, stat);
58
77
  }
59
- function shotOrigin(from, aim) {
60
- return "origin" in aim ? aim.origin : deps.spatial.positionOf(from);
61
- }
62
78
  function withWeaponSpread(via, aim) {
63
79
  if ("origin" in aim || aim.spread !== undefined)
64
80
  return aim;
65
81
  const spread = itemStat(via, "spread");
66
82
  return spread === null ? aim : { ...aim, spread };
67
83
  }
84
+ function resolveShotFor(from, aim, policy) {
85
+ return resolveShot({
86
+ positionOf: deps.spatial.positionOf,
87
+ rotationYOf: deps.rotationYOf,
88
+ collidersOf: deps.entityCollidersOf,
89
+ }, from, aim, policy ?? defaultPolicy);
90
+ }
91
+ const internalSceneRaycast = deps.sceneRaycast ??
92
+ createSceneRaycast({
93
+ entities: {
94
+ list: () => {
95
+ const ids = deps.spatial.inRadius([0, 0, 0], 1e9);
96
+ const out = [];
97
+ for (const id of ids) {
98
+ const position = deps.spatial.positionOf(id);
99
+ if (position === undefined)
100
+ continue;
101
+ out.push({ id, position, rotationY: deps.rotationYOf?.(id) ?? 0 });
102
+ }
103
+ return out;
104
+ },
105
+ collidersOf: deps.entityCollidersOf,
106
+ inRadius: (center, radius) => deps.spatial.inRadius(center, radius),
107
+ get: (id) => {
108
+ const position = deps.spatial.positionOf(id);
109
+ if (position === undefined)
110
+ return null;
111
+ return { id, position, rotationY: deps.rotationYOf?.(id) ?? 0 };
112
+ },
113
+ },
114
+ objects: deps.objects === undefined
115
+ ? undefined
116
+ : {
117
+ list: () => deps.objects.list().map((object) => ({
118
+ instanceId: object.instanceId,
119
+ catalogId: object.catalogId,
120
+ position: [object.position[0], object.position[1], object.position[2]],
121
+ rotationY: object.rotationY ?? 0,
122
+ })),
123
+ inBox: deps.objects.inBox
124
+ ? (min, max) => deps.objects.inBox(min, max).map((object) => ({
125
+ instanceId: object.instanceId,
126
+ catalogId: object.catalogId,
127
+ position: [object.position[0], object.position[1], object.position[2]],
128
+ rotationY: object.rotationY ?? 0,
129
+ }))
130
+ : undefined,
131
+ halfExtentsOf: deps.objects.halfExtents,
132
+ collidersOf: deps.objects.collidersOf,
133
+ },
134
+ });
68
135
  const raycast = deps.raycast ??
69
- ((from, aim, range) => {
70
- const origin = shotOrigin(from, aim);
71
- if (origin === undefined)
72
- return [];
73
- const direction = aimDirection(aim);
74
- if (direction === null)
136
+ ((from, aim, range, originPolicy) => {
137
+ const resolved = resolveShotFor(from, aim, originPolicy);
138
+ if (resolved === null)
75
139
  return [];
140
+ const { origin, direction } = resolved;
141
+ const all = internalSceneRaycast.raycastAll({
142
+ origin,
143
+ direction,
144
+ maxDistance: range,
145
+ excludeInstanceIds: [from],
146
+ });
147
+ const until = hitsUntilBlocked(all);
76
148
  const spreadRad = (aimSpreadDeg(aim) * Math.PI) / 180;
77
- const entityHits = [];
78
- for (const instanceId of deps.spatial.inRadius(origin, range)) {
79
- if (instanceId === from)
80
- continue;
81
- const position = deps.spatial.positionOf(instanceId);
82
- if (position === undefined)
83
- continue;
84
- const dx = position[0] - origin[0];
85
- const dy = position[1] - origin[1];
86
- const dz = position[2] - origin[2];
87
- const along = dx * direction[0] + dy * direction[1] + dz * direction[2];
88
- if (along <= 0 || along > range)
89
- continue;
90
- const px = dx - direction[0] * along;
91
- const py = dy - direction[1] * along;
92
- const pz = dz - direction[2] * along;
93
- const perpendicular = Math.sqrt(px * px + py * py + pz * pz);
94
- if (perpendicular > BASE_HIT_RADIUS + Math.tan(spreadRad) * along)
95
- continue;
96
- entityHits.push({ kind: "entity", instanceId, distance: along, at: position });
97
- }
98
- let nearestObject = null;
99
- for (const object of deps.objects?.list() ?? []) {
100
- const half = deps.objects?.halfExtents?.(object.catalogId) ?? DEFAULT_OBJECT_HALF_EXTENTS;
101
- const min = [
102
- object.position[0] - half[0],
103
- object.position[1] - half[1],
104
- object.position[2] - half[2],
105
- ];
106
- const max = [
107
- object.position[0] + half[0],
108
- object.position[1] + half[1],
109
- object.position[2] + half[2],
110
- ];
111
- const distance = raySegmentAabbDistance(origin, direction, range, min, max);
112
- if (distance === null)
113
- continue;
114
- if (nearestObject === null || distance < nearestObject.distance) {
115
- nearestObject = {
116
- kind: "object",
117
- instanceId: object.instanceId,
118
- catalogId: object.catalogId,
119
- distance,
120
- at: [
121
- origin[0] + direction[0] * distance,
122
- origin[1] + direction[1] * distance,
123
- origin[2] + direction[2] * distance,
124
- ],
125
- };
149
+ const mapped = [];
150
+ for (const hit of until) {
151
+ if (hit.targetKind === "entity" && spreadRad > 0) {
152
+ const position = deps.spatial.positionOf(hit.instanceId);
153
+ if (position !== undefined) {
154
+ const colliders = resolveColliders(deps.entityCollidersOf?.(hit.instanceId) ?? defaultEntityColliders());
155
+ const collider = colliders.find((candidate) => candidate.name === hit.colliderName) ?? colliders[0];
156
+ const localOffset = collider?.shape.offset ?? [0, 0, 0];
157
+ const slack = collider === undefined
158
+ ? BASE_HIT_RADIUS
159
+ : collider.shape.kind === "sphere"
160
+ ? collider.shape.radius
161
+ : Math.hypot(...collider.shape.halfExtents);
162
+ const rotationY = deps.rotationYOf?.(hit.instanceId) ?? 0;
163
+ const cos = Math.cos(rotationY);
164
+ const sin = Math.sin(rotationY);
165
+ const dx = position[0] + localOffset[0] * cos + localOffset[2] * sin - origin[0];
166
+ const dy = position[1] + localOffset[1] - origin[1];
167
+ const dz = position[2] - localOffset[0] * sin + localOffset[2] * cos - origin[2];
168
+ const along = dx * direction[0] + dy * direction[1] + dz * direction[2];
169
+ const px = dx - direction[0] * along;
170
+ const py = dy - direction[1] * along;
171
+ const pz = dz - direction[2] * along;
172
+ const perpendicular = Math.sqrt(px * px + py * py + pz * pz);
173
+ if (perpendicular > slack + Math.tan(spreadRad) * along)
174
+ continue;
175
+ }
126
176
  }
177
+ mapped.push(sceneHitToRaycast(hit));
127
178
  }
128
- const blockDistance = nearestObject?.distance ?? Infinity;
129
- const hits = entityHits.filter((hit) => hit.distance <= blockDistance);
130
- if (nearestObject !== null)
131
- hits.push(nearestObject);
132
- return hits.sort((a, b) => a.distance - b.distance);
179
+ return mapped;
133
180
  });
134
181
  function resolveRange(via) {
135
182
  return itemStat(via, "range") ?? DEFAULT_RANGE;
@@ -138,8 +185,9 @@ export function createProjectileSystem(deps) {
138
185
  return itemStat(via, "projectile.fuseTime") !== null || itemStat(via, "explosion.radius") !== null;
139
186
  }
140
187
  function ballisticArc(input) {
141
- const origin = shotOrigin(input.from, input.aim) ?? [0, 0, 0];
142
- const direction = aimDirection(input.aim) ?? [0, 0, 1];
188
+ const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
189
+ const origin = resolved?.origin ?? [0, 0, 0];
190
+ const direction = resolved?.direction ?? aimDirection(input.aim) ?? [0, 0, 1];
143
191
  const speed = itemStat(input.via, "projectile.speed") ?? DEFAULT_PROJECTILE_SPEED;
144
192
  const gravityScale = itemStat(input.via, "projectile.gravity") ?? 1;
145
193
  const fuseTime = itemStat(input.via, "projectile.fuseTime");
@@ -175,31 +223,42 @@ export function createProjectileSystem(deps) {
175
223
  }
176
224
  function predictHits(input) {
177
225
  const aim = withWeaponSpread(input.via, input.aim);
178
- const rawHits = raycast(input.from, aim, resolveRange(input.via));
226
+ const resolved = resolveShotFor(input.from, aim, input.originPolicy);
227
+ const rawHits = raycast(input.from, aim, resolveRange(input.via), input.originPolicy);
179
228
  const visible = rawHits.filter((hit) => isObjectHit(hit) || deps.spatial.hasLineOfSight(input.from, hit.instanceId));
180
- return { rawHits, visible };
229
+ return {
230
+ rawHits,
231
+ visible,
232
+ origin: resolved?.origin,
233
+ direction: resolved?.direction,
234
+ };
181
235
  }
182
- function settledAt(input, hits) {
183
- const first = hits[0];
184
- if (first !== undefined)
185
- return [first.at[0], first.at[1], first.at[2]];
186
- const origin = shotOrigin(input.from, input.aim);
187
- const direction = origin === undefined ? null : aimDirection(input.aim);
188
- if (origin === undefined || direction === null)
236
+ function missPoint(input) {
237
+ const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
238
+ if (resolved === null)
189
239
  return [0, 0, 0];
190
240
  const range = resolveRange(input.via);
191
- return [origin[0] + direction[0] * range, origin[1] + direction[1] * range, origin[2] + direction[2] * range];
241
+ return [
242
+ resolved.origin[0] + resolved.direction[0] * range,
243
+ resolved.origin[1] + resolved.direction[1] * range,
244
+ resolved.origin[2] + resolved.direction[2] * range,
245
+ ];
192
246
  }
193
247
  return {
194
248
  willHitProjectile(input) {
195
- const { rawHits, visible } = predictHits(input);
249
+ const { rawHits, visible, origin, direction } = predictHits(input);
250
+ const impact = firstImpact(asSceneHits(rawHits));
251
+ const solidBlock = impact !== null && impact.blocks && !impact.damageEligible;
196
252
  const prediction = {
197
- hits: visible.map((hit) => isObjectHit(hit)
198
- ? { kind: "object", instanceId: hit.instanceId, catalogId: hit.catalogId, distance: hit.distance }
199
- : { kind: "entity", instanceId: hit.instanceId, distance: hit.distance }),
253
+ hits: visible.map(toProjectileHit),
254
+ ...(origin !== undefined ? { origin } : {}),
255
+ ...(direction !== undefined ? { direction } : {}),
256
+ firstImpact: impact === null ? null : toProjectileHit(sceneHitToRaycast(impact)),
200
257
  };
201
258
  if (rawHits.length > 0 && visible.length === 0)
202
259
  prediction.blocked = true;
260
+ else if (solidBlock)
261
+ prediction.blocked = true;
203
262
  return prediction;
204
263
  },
205
264
  fireProjectile(input) {
@@ -216,21 +275,38 @@ export function createProjectileSystem(deps) {
216
275
  return { status: "rejected", shotId, reason: "already-settled" };
217
276
  shot.settled = true;
218
277
  const { input } = shot;
219
- const origin = shotOrigin(input.from, input.aim) ?? [0, 0, 0];
278
+ const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
279
+ const origin = resolved?.origin ?? [0, 0, 0];
280
+ const originTuple = [origin[0], origin[1], origin[2]];
220
281
  if (isBallistic(input.via)) {
221
282
  const at = ballisticSettlePoint(input);
222
- deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: false });
223
- return { status: "settled", shotId, at, hits: [] };
283
+ const splashRadius = itemStat(input.via, "explosion.radius");
284
+ const radius = splashRadius ?? BASE_HIT_RADIUS;
285
+ const hits = deps.effects.applyEffect({
286
+ from: input.from,
287
+ effect: input.effect,
288
+ via: input.via,
289
+ at,
290
+ radius,
291
+ falloff: splashRadius === null ? "none" : "linear",
292
+ });
293
+ deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
294
+ return { status: "settled", shotId, at, hits, origin: originTuple };
224
295
  }
225
- const { visible } = predictHits(input);
226
- const entityHits = visible.filter(isEntityHit);
227
- const objectHits = visible.filter(isObjectHit);
228
- const receivable = entityHits.filter((hit) => deps.effects.canReceive(hit.instanceId, input.effect) === null);
296
+ const { visible, rawHits } = predictHits(input);
297
+ const ordered = asSceneHits(rawHits);
298
+ const untilBlock = hitsUntilBlocked(ordered);
299
+ const impact = firstImpact(untilBlock);
300
+ const solidBlock = impact !== null && impact.blocks && !impact.damageEligible;
301
+ const damageEntityHits = visible.filter((hit) => isEntityHit(hit) &&
302
+ hit.damageEligible !== false &&
303
+ (!solidBlock || hit.distance <= (impact?.distance ?? Infinity) + 1e-9) &&
304
+ deps.effects.canReceive(hit.instanceId, input.effect) === null);
229
305
  const pellets = Math.max(1, Math.round(itemStat(input.via, "pellets") ?? 1));
230
306
  const hits = [];
231
- if (receivable.length > 0) {
232
- for (let pellet = 0; pellet < pellets; pellet++) {
233
- const target = receivable[pellet % receivable.length];
307
+ if (!solidBlock && damageEntityHits.length > 0) {
308
+ for (let pellet = 0; pellet < pellets; pellet += 1) {
309
+ const target = damageEntityHits[pellet % damageEntityHits.length];
234
310
  hits.push(...deps.effects.applyEffect({
235
311
  from: input.from,
236
312
  to: target.instanceId,
@@ -239,9 +315,18 @@ export function createProjectileSystem(deps) {
239
315
  }));
240
316
  }
241
317
  }
242
- const at = settledAt(input, receivable.length > 0 ? receivable : objectHits);
243
- deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: receivable.length > 0 });
244
- return { status: "settled", shotId, at, hits };
318
+ let at;
319
+ if (impact !== null) {
320
+ at = [impact.point[0], impact.point[1], impact.point[2]];
321
+ }
322
+ else if (damageEntityHits[0] !== undefined) {
323
+ at = [damageEntityHits[0].at[0], damageEntityHits[0].at[1], damageEntityHits[0].at[2]];
324
+ }
325
+ else {
326
+ at = missPoint(input);
327
+ }
328
+ deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
329
+ return { status: "settled", shotId, at, hits, origin: originTuple };
245
330
  },
246
331
  };
247
332
  }