@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.
- package/CHANGELOG.md +36 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +13 -0
- package/dist/ai/spawnDirector.js +41 -17
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +214 -129
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +52 -0
- package/dist/combat/shotOrigin.js +86 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +44 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +25 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +14 -1
- package/dist/game/lootTable.js +28 -7
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +116 -4
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +27 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +33 -3
- package/dist/input/axisInput.js +60 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +44 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +72 -8
- package/dist/runtime/gameContext.js +374 -61
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +18 -0
- package/dist/runtime/inputSnapshot.js +7 -0
- package/dist/runtime/motionIntents.d.ts +16 -6
- package/dist/runtime/motionIntents.js +31 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +50 -0
- package/dist/scene/colliders.js +101 -0
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +51 -5
- package/dist/scene/entityStore.js +119 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +17 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +212 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +21 -4
- package/dist/world/environmentSummary.js +22 -3
- package/dist/world/features.d.ts +185 -8
- package/dist/world/features.js +64 -6
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +69 -3
- package/dist/world/terrain.js +309 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +3013 -1726
- 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
|
|
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
|
-
|
|
65
|
+
sceneRaycast?: SceneRaycastApi;
|
|
51
66
|
objects?: ProjectileObjectsDeps;
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
50
|
-
return
|
|
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
|
|
71
|
-
if (
|
|
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
|
|
78
|
-
for (const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
142
|
-
const
|
|
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
|
|
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 {
|
|
229
|
+
return {
|
|
230
|
+
rawHits,
|
|
231
|
+
visible,
|
|
232
|
+
origin: resolved?.origin,
|
|
233
|
+
direction: resolved?.direction,
|
|
234
|
+
};
|
|
181
235
|
}
|
|
182
|
-
function
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
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 [
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
227
|
-
const
|
|
228
|
-
const
|
|
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 (
|
|
232
|
-
for (let pellet = 0; pellet < pellets; pellet
|
|
233
|
-
const target =
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
}
|