@jgengine/core 0.7.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.
- package/CHANGELOG.md +63 -0
- package/README.md +2 -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 +5 -0
- package/dist/ai/spawnDirector.js +14 -2
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- 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 +22 -2
- package/dist/combat/abilityKit.js +72 -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/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +64 -6
- package/dist/combat/projectiles.js +232 -74
- 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 +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +30 -1
- package/dist/combat/telegraph.js +61 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/recipe.js +4 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +205 -0
- package/dist/devtools/devtools.js +832 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -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 +68 -0
- package/dist/game/chat.js +173 -0
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +35 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +286 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +44 -1
- package/dist/game/race.js +116 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +89 -0
- package/dist/input/touchScheme.js +153 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +59 -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 +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +17 -2
- package/dist/multiplayer/presenceModel.js +30 -4
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +48 -5
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- 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/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -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 +43 -5
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +54 -8
- package/dist/physics/physicsWorld.js +400 -79
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +18 -18
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +15 -0
- package/dist/runtime/cameraDirector.js +31 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +65 -2
- package/dist/runtime/gameContext.js +280 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +30 -1
- package/dist/runtime/hostPersistence.js +70 -5
- package/dist/runtime/inputSnapshot.d.ts +12 -0
- package/dist/runtime/inputSnapshot.js +17 -0
- package/dist/runtime/motionIntents.d.ts +33 -0
- package/dist/runtime/motionIntents.js +43 -0
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +30 -5
- package/dist/scene/entityStore.js +83 -13
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +29 -0
- package/dist/scene/objectQuery.js +140 -0
- package/dist/scene/objectStore.d.ts +13 -0
- package/dist/scene/objectStore.js +130 -6
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +17 -1
- package/dist/scene/spatial.js +140 -17
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- 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/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- 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.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- 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/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- 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/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- 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/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 +198 -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 +61 -0
- package/dist/world/environmentSummary.js +138 -0
- package/dist/world/features.d.ts +138 -9
- package/dist/world/features.js +73 -2
- package/dist/world/gridInstances.d.ts +12 -0
- package/dist/world/gridInstances.js +160 -0
- 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/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +62 -1
- package/dist/world/terrain.js +253 -9
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- 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 +4983 -0
- package/package.json +14 -3
|
@@ -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);
|
package/dist/combat/effects.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface EffectSystemDeps {
|
|
|
65
65
|
onLethal?(instanceId: string, ctx: LethalContext): void;
|
|
66
66
|
}
|
|
67
67
|
export interface EffectSystem {
|
|
68
|
-
canReceive(instanceId: string, effect: string): string | null;
|
|
68
|
+
canReceive(instanceId: string, effect: string, magnitude?: number): string | null;
|
|
69
69
|
preview(input: SingleTargetEffectInput): number;
|
|
70
70
|
applyEffect(input: EffectInput): EffectResult[];
|
|
71
71
|
}
|
package/dist/combat/effects.js
CHANGED
|
@@ -40,18 +40,21 @@ export function createEffectSystem(deps) {
|
|
|
40
40
|
}
|
|
41
41
|
return result;
|
|
42
42
|
}
|
|
43
|
-
function canReceive(instanceId, effect) {
|
|
43
|
+
function canReceive(instanceId, effect, magnitude) {
|
|
44
44
|
const rule = resolveRule(instanceId, effect);
|
|
45
45
|
if (rule === null)
|
|
46
46
|
return "not-receivable";
|
|
47
47
|
const stats = deps.resolveStats(instanceId);
|
|
48
48
|
if (stats === undefined)
|
|
49
49
|
return "unknown-instance";
|
|
50
|
-
const
|
|
50
|
+
const restorative = magnitude !== undefined && magnitude < 0;
|
|
51
|
+
const anyPoolHasHeadroom = rule.order.some((statId) => {
|
|
51
52
|
const stat = stats[statId];
|
|
52
|
-
|
|
53
|
+
if (stat === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return restorative ? stat.current < stat.max : stat.current > stat.min;
|
|
53
56
|
});
|
|
54
|
-
if (!
|
|
57
|
+
if (!anyPoolHasHeadroom)
|
|
55
58
|
return "pools-depleted";
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
@@ -80,13 +83,13 @@ export function createEffectSystem(deps) {
|
|
|
80
83
|
return { instanceId, effect, applied, lethal };
|
|
81
84
|
}
|
|
82
85
|
function applyTo(instanceId, effect, via, from, scale) {
|
|
83
|
-
if (canReceive(instanceId, effect) !== null)
|
|
84
|
-
return null;
|
|
85
86
|
const rule = resolveRule(instanceId, effect);
|
|
86
87
|
const stats = deps.resolveStats(instanceId);
|
|
87
88
|
if (rule === null || stats === undefined)
|
|
88
89
|
return null;
|
|
89
90
|
const drainMagnitude = modifiedDrainMagnitude(baseDrainMagnitude(effect, via) * scale, rule);
|
|
91
|
+
if (canReceive(instanceId, effect, drainMagnitude) !== null)
|
|
92
|
+
return null;
|
|
90
93
|
const result = drainPools(instanceId, effect, rule, stats, drainMagnitude);
|
|
91
94
|
if (result.lethal)
|
|
92
95
|
deps.onLethal?.(instanceId, { from, via, effect });
|
|
@@ -1,18 +1,39 @@
|
|
|
1
|
+
import type { BallisticSweep } from "../physics/ballisticSweep.js";
|
|
1
2
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
3
|
+
import type { EntityColliderSet } from "../scene/colliders.js";
|
|
4
|
+
import { type SceneRaycastApi } from "../scene/sceneRaycast.js";
|
|
2
5
|
import type { Aim } from "../scene/spatial.js";
|
|
3
6
|
import type { CombatSpatialDeps, EffectResult, EffectSystem, EffectVia } from "./effects.js";
|
|
7
|
+
import { type ShotOriginPolicy } from "./shotOrigin.js";
|
|
4
8
|
export interface ProjectileShotInput {
|
|
5
9
|
from: string;
|
|
6
10
|
via: EffectVia;
|
|
7
11
|
aim: Aim;
|
|
8
12
|
effect: string;
|
|
13
|
+
/** Defaults to `{ kind: "legacy" }` — aim.origin or shooter position. */
|
|
14
|
+
originPolicy?: ShotOriginPolicy;
|
|
9
15
|
}
|
|
10
|
-
export interface
|
|
16
|
+
export interface EntityRaycastHit {
|
|
17
|
+
kind: "entity";
|
|
11
18
|
instanceId: string;
|
|
12
19
|
distance: number;
|
|
13
20
|
at: EntityPosition;
|
|
21
|
+
colliderName?: string;
|
|
22
|
+
damageEligible?: boolean;
|
|
23
|
+
blocks?: boolean;
|
|
14
24
|
}
|
|
15
|
-
export
|
|
25
|
+
export interface ObjectRaycastHit {
|
|
26
|
+
kind: "object";
|
|
27
|
+
instanceId: string;
|
|
28
|
+
catalogId: string;
|
|
29
|
+
distance: number;
|
|
30
|
+
at: EntityPosition;
|
|
31
|
+
colliderName?: string;
|
|
32
|
+
damageEligible?: boolean;
|
|
33
|
+
blocks?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type RaycastHit = EntityRaycastHit | ObjectRaycastHit;
|
|
36
|
+
export type Raycast = (from: string, aim: Aim, range: number, originPolicy?: ShotOriginPolicy) => RaycastHit[];
|
|
16
37
|
export interface ProjectileSettleReport {
|
|
17
38
|
from: string;
|
|
18
39
|
origin: EntityPosition;
|
|
@@ -20,26 +41,63 @@ export interface ProjectileSettleReport {
|
|
|
20
41
|
effect: string;
|
|
21
42
|
hit: boolean;
|
|
22
43
|
}
|
|
44
|
+
export interface ProjectileObjectsDeps {
|
|
45
|
+
list(): readonly {
|
|
46
|
+
instanceId: string;
|
|
47
|
+
catalogId: string;
|
|
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;
|
|
56
|
+
}[];
|
|
57
|
+
halfExtents?(catalogId: string): [number, number, number] | null;
|
|
58
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
59
|
+
}
|
|
23
60
|
export interface ProjectileSystemDeps {
|
|
24
61
|
effects: EffectSystem;
|
|
25
62
|
spatial: CombatSpatialDeps;
|
|
26
63
|
getStat(itemId: string, stat: string): number | null;
|
|
27
64
|
raycast?: Raycast;
|
|
65
|
+
sceneRaycast?: SceneRaycastApi;
|
|
66
|
+
objects?: ProjectileObjectsDeps;
|
|
67
|
+
entityCollidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
68
|
+
rotationYOf?(instanceId: string): number | undefined;
|
|
69
|
+
sweepBallistic?: BallisticSweep;
|
|
70
|
+
defaultOriginPolicy?: ShotOriginPolicy;
|
|
28
71
|
now?: () => number;
|
|
29
72
|
onSettle?(report: ProjectileSettleReport): void;
|
|
30
73
|
}
|
|
74
|
+
export type ProjectileHit = {
|
|
75
|
+
kind: "entity";
|
|
76
|
+
instanceId: string;
|
|
77
|
+
distance: number;
|
|
78
|
+
colliderName?: string;
|
|
79
|
+
damageEligible?: boolean;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "object";
|
|
82
|
+
instanceId: string;
|
|
83
|
+
catalogId: string;
|
|
84
|
+
distance: number;
|
|
85
|
+
colliderName?: string;
|
|
86
|
+
damageEligible?: boolean;
|
|
87
|
+
};
|
|
31
88
|
export interface ProjectilePrediction {
|
|
32
|
-
hits:
|
|
33
|
-
instanceId: string;
|
|
34
|
-
distance: number;
|
|
35
|
-
}[];
|
|
89
|
+
hits: ProjectileHit[];
|
|
36
90
|
blocked?: boolean;
|
|
91
|
+
origin?: EntityPosition;
|
|
92
|
+
direction?: EntityPosition;
|
|
93
|
+
firstImpact?: ProjectileHit | null;
|
|
37
94
|
}
|
|
38
95
|
export type SettleResult = {
|
|
39
96
|
status: "settled";
|
|
40
97
|
shotId: string;
|
|
41
98
|
at: [number, number, number];
|
|
42
99
|
hits: EffectResult[];
|
|
100
|
+
origin?: [number, number, number];
|
|
43
101
|
} | {
|
|
44
102
|
status: "rejected";
|
|
45
103
|
shotId: string;
|
|
@@ -1,69 +1,169 @@
|
|
|
1
|
+
import { createSceneRaycast, firstImpact, hitsUntilBlocked, } from "../scene/sceneRaycast.js";
|
|
2
|
+
import { aimDirection, aimSpreadDeg, resolveShot, } from "./shotOrigin.js";
|
|
1
3
|
const DEFAULT_RANGE = 100;
|
|
2
4
|
const DEFAULT_PROJECTILE_SPEED = 15;
|
|
3
5
|
const GRAVITY = 9.8;
|
|
4
6
|
const BASE_HIT_RADIUS = 0.5;
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
if (length === 0)
|
|
8
|
-
return null;
|
|
9
|
-
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
7
|
+
function isEntityHit(hit) {
|
|
8
|
+
return hit.kind === "entity";
|
|
10
9
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
return normalize(aim.direction);
|
|
14
|
-
const cosPitch = Math.cos(aim.pitch);
|
|
15
|
-
return [Math.sin(aim.yaw) * cosPitch, Math.sin(aim.pitch), Math.cos(aim.yaw) * cosPitch];
|
|
10
|
+
function isObjectHit(hit) {
|
|
11
|
+
return hit.kind === "object";
|
|
16
12
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
13
|
+
function sceneHitToRaycast(hit) {
|
|
14
|
+
if (hit.targetKind === "entity") {
|
|
15
|
+
return {
|
|
16
|
+
kind: "entity",
|
|
17
|
+
instanceId: hit.instanceId,
|
|
18
|
+
distance: hit.distance,
|
|
19
|
+
at: hit.point,
|
|
20
|
+
colliderName: hit.colliderName,
|
|
21
|
+
damageEligible: hit.damageEligible,
|
|
22
|
+
blocks: hit.blocks,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
kind: "object",
|
|
27
|
+
instanceId: hit.instanceId,
|
|
28
|
+
catalogId: hit.catalogId ?? hit.targetKind,
|
|
29
|
+
distance: hit.distance,
|
|
30
|
+
at: hit.point,
|
|
31
|
+
colliderName: hit.colliderName,
|
|
32
|
+
damageEligible: hit.damageEligible,
|
|
33
|
+
blocks: hit.blocks,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function toProjectileHit(hit) {
|
|
37
|
+
if (isObjectHit(hit)) {
|
|
38
|
+
return {
|
|
39
|
+
kind: "object",
|
|
40
|
+
instanceId: hit.instanceId,
|
|
41
|
+
catalogId: hit.catalogId,
|
|
42
|
+
distance: hit.distance,
|
|
43
|
+
...(hit.colliderName !== undefined ? { colliderName: hit.colliderName } : {}),
|
|
44
|
+
...(hit.damageEligible !== undefined ? { damageEligible: hit.damageEligible } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
kind: "entity",
|
|
49
|
+
instanceId: hit.instanceId,
|
|
50
|
+
distance: hit.distance,
|
|
51
|
+
...(hit.colliderName !== undefined ? { colliderName: hit.colliderName } : {}),
|
|
52
|
+
...(hit.damageEligible !== undefined ? { damageEligible: hit.damageEligible } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function asSceneHits(hits) {
|
|
56
|
+
return hits.map((hit) => ({
|
|
57
|
+
targetKind: hit.kind === "entity" ? "entity" : "object",
|
|
58
|
+
instanceId: hit.instanceId,
|
|
59
|
+
catalogId: hit.kind === "object" ? hit.catalogId : undefined,
|
|
60
|
+
colliderName: hit.colliderName ?? "body",
|
|
61
|
+
purpose: hit.damageEligible === false ? "physical" : "damage",
|
|
62
|
+
damageEligible: hit.kind === "entity" ? hit.damageEligible !== false : hit.damageEligible === true,
|
|
63
|
+
blocks: hit.blocks !== false,
|
|
64
|
+
distance: hit.distance,
|
|
65
|
+
point: hit.at,
|
|
66
|
+
normal: [0, 1, 0],
|
|
67
|
+
}));
|
|
19
68
|
}
|
|
20
69
|
export function createProjectileSystem(deps) {
|
|
21
70
|
const shots = new Map();
|
|
22
71
|
let shotCounter = 0;
|
|
23
72
|
const now = deps.now ?? (() => Date.now());
|
|
73
|
+
const defaultPolicy = deps.defaultOriginPolicy ?? { kind: "legacy" };
|
|
24
74
|
function itemStat(via, stat) {
|
|
25
75
|
return via.item === undefined ? null : deps.getStat(via.item, stat);
|
|
26
76
|
}
|
|
27
|
-
function shotOrigin(from, aim) {
|
|
28
|
-
return "origin" in aim ? aim.origin : deps.spatial.positionOf(from);
|
|
29
|
-
}
|
|
30
77
|
function withWeaponSpread(via, aim) {
|
|
31
78
|
if ("origin" in aim || aim.spread !== undefined)
|
|
32
79
|
return aim;
|
|
33
80
|
const spread = itemStat(via, "spread");
|
|
34
81
|
return spread === null ? aim : { ...aim, spread };
|
|
35
82
|
}
|
|
83
|
+
function resolveShotFor(from, aim, policy) {
|
|
84
|
+
return resolveShot({
|
|
85
|
+
positionOf: deps.spatial.positionOf,
|
|
86
|
+
rotationYOf: deps.rotationYOf,
|
|
87
|
+
}, from, aim, policy ?? defaultPolicy);
|
|
88
|
+
}
|
|
89
|
+
const internalSceneRaycast = deps.sceneRaycast ??
|
|
90
|
+
createSceneRaycast({
|
|
91
|
+
entities: {
|
|
92
|
+
list: () => {
|
|
93
|
+
const ids = deps.spatial.inRadius([0, 0, 0], 1e9);
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
const position = deps.spatial.positionOf(id);
|
|
97
|
+
if (position === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
out.push({ id, position, rotationY: deps.rotationYOf?.(id) ?? 0 });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
},
|
|
103
|
+
collidersOf: deps.entityCollidersOf,
|
|
104
|
+
inRadius: (center, radius) => deps.spatial.inRadius(center, radius),
|
|
105
|
+
get: (id) => {
|
|
106
|
+
const position = deps.spatial.positionOf(id);
|
|
107
|
+
if (position === undefined)
|
|
108
|
+
return null;
|
|
109
|
+
return { id, position, rotationY: deps.rotationYOf?.(id) ?? 0 };
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
objects: deps.objects === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: {
|
|
115
|
+
list: () => deps.objects.list().map((object) => ({
|
|
116
|
+
instanceId: object.instanceId,
|
|
117
|
+
catalogId: object.catalogId,
|
|
118
|
+
position: [object.position[0], object.position[1], object.position[2]],
|
|
119
|
+
rotationY: object.rotationY ?? 0,
|
|
120
|
+
})),
|
|
121
|
+
inBox: deps.objects.inBox
|
|
122
|
+
? (min, max) => deps.objects.inBox(min, max).map((object) => ({
|
|
123
|
+
instanceId: object.instanceId,
|
|
124
|
+
catalogId: object.catalogId,
|
|
125
|
+
position: [object.position[0], object.position[1], object.position[2]],
|
|
126
|
+
rotationY: object.rotationY ?? 0,
|
|
127
|
+
}))
|
|
128
|
+
: undefined,
|
|
129
|
+
halfExtentsOf: deps.objects.halfExtents,
|
|
130
|
+
collidersOf: deps.objects.collidersOf,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
36
133
|
const raycast = deps.raycast ??
|
|
37
|
-
((from, aim, range) => {
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
return [];
|
|
41
|
-
const direction = aimDirection(aim);
|
|
42
|
-
if (direction === null)
|
|
134
|
+
((from, aim, range, originPolicy) => {
|
|
135
|
+
const resolved = resolveShotFor(from, aim, originPolicy);
|
|
136
|
+
if (resolved === null)
|
|
43
137
|
return [];
|
|
138
|
+
const { origin, direction } = resolved;
|
|
139
|
+
const all = internalSceneRaycast.raycastAll({
|
|
140
|
+
origin,
|
|
141
|
+
direction,
|
|
142
|
+
maxDistance: range,
|
|
143
|
+
excludeInstanceIds: [from],
|
|
144
|
+
});
|
|
145
|
+
const until = hitsUntilBlocked(all);
|
|
44
146
|
const spreadRad = (aimSpreadDeg(aim) * Math.PI) / 180;
|
|
45
|
-
const
|
|
46
|
-
for (const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
continue;
|
|
64
|
-
hits.push({ instanceId, distance: along, at: position });
|
|
147
|
+
const mapped = [];
|
|
148
|
+
for (const hit of until) {
|
|
149
|
+
if (hit.targetKind === "entity" && spreadRad > 0) {
|
|
150
|
+
const position = deps.spatial.positionOf(hit.instanceId);
|
|
151
|
+
if (position !== undefined) {
|
|
152
|
+
const dx = position[0] - origin[0];
|
|
153
|
+
const dy = position[1] - origin[1];
|
|
154
|
+
const dz = position[2] - origin[2];
|
|
155
|
+
const along = dx * direction[0] + dy * direction[1] + dz * direction[2];
|
|
156
|
+
const px = dx - direction[0] * along;
|
|
157
|
+
const py = dy - direction[1] * along;
|
|
158
|
+
const pz = dz - direction[2] * along;
|
|
159
|
+
const perpendicular = Math.sqrt(px * px + py * py + pz * pz);
|
|
160
|
+
if (perpendicular > 0.5 + Math.tan(spreadRad) * along)
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
mapped.push(sceneHitToRaycast(hit));
|
|
65
165
|
}
|
|
66
|
-
return
|
|
166
|
+
return mapped;
|
|
67
167
|
});
|
|
68
168
|
function resolveRange(via) {
|
|
69
169
|
return itemStat(via, "range") ?? DEFAULT_RANGE;
|
|
@@ -71,9 +171,10 @@ export function createProjectileSystem(deps) {
|
|
|
71
171
|
function isBallistic(via) {
|
|
72
172
|
return itemStat(via, "projectile.fuseTime") !== null || itemStat(via, "explosion.radius") !== null;
|
|
73
173
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
const
|
|
174
|
+
function ballisticArc(input) {
|
|
175
|
+
const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
|
|
176
|
+
const origin = resolved?.origin ?? [0, 0, 0];
|
|
177
|
+
const direction = resolved?.direction ?? aimDirection(input.aim) ?? [0, 0, 1];
|
|
77
178
|
const speed = itemStat(input.via, "projectile.speed") ?? DEFAULT_PROJECTILE_SPEED;
|
|
78
179
|
const gravityScale = itemStat(input.via, "projectile.gravity") ?? 1;
|
|
79
180
|
const fuseTime = itemStat(input.via, "projectile.fuseTime");
|
|
@@ -85,37 +186,66 @@ export function createProjectileSystem(deps) {
|
|
|
85
186
|
: flightCap;
|
|
86
187
|
const flightTime = Math.min(impactTime, flightCap);
|
|
87
188
|
const settledY = Math.max(0, origin[1] + verticalSpeed * flightTime - 0.5 * gravity * flightTime * flightTime);
|
|
88
|
-
return
|
|
89
|
-
origin
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
189
|
+
return {
|
|
190
|
+
origin,
|
|
191
|
+
velocity: [direction[0] * speed, verticalSpeed, direction[2] * speed],
|
|
192
|
+
gravity,
|
|
193
|
+
flightTime,
|
|
194
|
+
landing: [
|
|
195
|
+
origin[0] + direction[0] * speed * flightTime,
|
|
196
|
+
settledY,
|
|
197
|
+
origin[2] + direction[2] * speed * flightTime,
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function ballisticSettlePoint(input) {
|
|
202
|
+
const arc = ballisticArc(input);
|
|
203
|
+
const sweep = deps.sweepBallistic;
|
|
204
|
+
if (sweep !== undefined) {
|
|
205
|
+
const impact = sweep(arc.origin, arc.velocity, arc.gravity, arc.flightTime);
|
|
206
|
+
if (impact !== null)
|
|
207
|
+
return impact.point;
|
|
208
|
+
}
|
|
209
|
+
return arc.landing;
|
|
93
210
|
}
|
|
94
211
|
function predictHits(input) {
|
|
95
212
|
const aim = withWeaponSpread(input.via, input.aim);
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
213
|
+
const resolved = resolveShotFor(input.from, aim, input.originPolicy);
|
|
214
|
+
const rawHits = raycast(input.from, aim, resolveRange(input.via), input.originPolicy);
|
|
215
|
+
const visible = rawHits.filter((hit) => isObjectHit(hit) || deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
216
|
+
return {
|
|
217
|
+
rawHits,
|
|
218
|
+
visible,
|
|
219
|
+
origin: resolved?.origin,
|
|
220
|
+
direction: resolved?.direction,
|
|
221
|
+
};
|
|
99
222
|
}
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
return [first.at[0], first.at[1], first.at[2]];
|
|
104
|
-
const origin = shotOrigin(input.from, input.aim);
|
|
105
|
-
const direction = origin === undefined ? null : aimDirection(input.aim);
|
|
106
|
-
if (origin === undefined || direction === null)
|
|
223
|
+
function missPoint(input) {
|
|
224
|
+
const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
|
|
225
|
+
if (resolved === null)
|
|
107
226
|
return [0, 0, 0];
|
|
108
227
|
const range = resolveRange(input.via);
|
|
109
|
-
return [
|
|
228
|
+
return [
|
|
229
|
+
resolved.origin[0] + resolved.direction[0] * range,
|
|
230
|
+
resolved.origin[1] + resolved.direction[1] * range,
|
|
231
|
+
resolved.origin[2] + resolved.direction[2] * range,
|
|
232
|
+
];
|
|
110
233
|
}
|
|
111
234
|
return {
|
|
112
235
|
willHitProjectile(input) {
|
|
113
|
-
const { rawHits, visible } = predictHits(input);
|
|
236
|
+
const { rawHits, visible, origin, direction } = predictHits(input);
|
|
237
|
+
const impact = firstImpact(asSceneHits(rawHits));
|
|
238
|
+
const solidBlock = impact !== null && impact.blocks && !impact.damageEligible;
|
|
114
239
|
const prediction = {
|
|
115
|
-
hits: visible.map(
|
|
240
|
+
hits: visible.map(toProjectileHit),
|
|
241
|
+
...(origin !== undefined ? { origin } : {}),
|
|
242
|
+
...(direction !== undefined ? { direction } : {}),
|
|
243
|
+
firstImpact: impact === null ? null : toProjectileHit(sceneHitToRaycast(impact)),
|
|
116
244
|
};
|
|
117
245
|
if (rawHits.length > 0 && visible.length === 0)
|
|
118
246
|
prediction.blocked = true;
|
|
247
|
+
else if (solidBlock)
|
|
248
|
+
prediction.blocked = true;
|
|
119
249
|
return prediction;
|
|
120
250
|
},
|
|
121
251
|
fireProjectile(input) {
|
|
@@ -132,19 +262,38 @@ export function createProjectileSystem(deps) {
|
|
|
132
262
|
return { status: "rejected", shotId, reason: "already-settled" };
|
|
133
263
|
shot.settled = true;
|
|
134
264
|
const { input } = shot;
|
|
135
|
-
const
|
|
265
|
+
const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
|
|
266
|
+
const origin = resolved?.origin ?? [0, 0, 0];
|
|
267
|
+
const originTuple = [origin[0], origin[1], origin[2]];
|
|
136
268
|
if (isBallistic(input.via)) {
|
|
137
269
|
const at = ballisticSettlePoint(input);
|
|
138
|
-
|
|
139
|
-
|
|
270
|
+
const splashRadius = itemStat(input.via, "explosion.radius");
|
|
271
|
+
const radius = splashRadius ?? BASE_HIT_RADIUS;
|
|
272
|
+
const hits = deps.effects.applyEffect({
|
|
273
|
+
from: input.from,
|
|
274
|
+
effect: input.effect,
|
|
275
|
+
via: input.via,
|
|
276
|
+
at,
|
|
277
|
+
radius,
|
|
278
|
+
falloff: splashRadius === null ? "none" : "linear",
|
|
279
|
+
});
|
|
280
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
|
|
281
|
+
return { status: "settled", shotId, at, hits, origin: originTuple };
|
|
140
282
|
}
|
|
141
|
-
const { visible } = predictHits(input);
|
|
142
|
-
const
|
|
283
|
+
const { visible, rawHits } = predictHits(input);
|
|
284
|
+
const ordered = asSceneHits(rawHits);
|
|
285
|
+
const untilBlock = hitsUntilBlocked(ordered);
|
|
286
|
+
const impact = firstImpact(untilBlock);
|
|
287
|
+
const solidBlock = impact !== null && impact.blocks && !impact.damageEligible;
|
|
288
|
+
const damageEntityHits = visible.filter((hit) => isEntityHit(hit) &&
|
|
289
|
+
hit.damageEligible !== false &&
|
|
290
|
+
(!solidBlock || hit.distance <= (impact?.distance ?? Infinity) + 1e-9) &&
|
|
291
|
+
deps.effects.canReceive(hit.instanceId, input.effect) === null);
|
|
143
292
|
const pellets = Math.max(1, Math.round(itemStat(input.via, "pellets") ?? 1));
|
|
144
293
|
const hits = [];
|
|
145
|
-
if (
|
|
146
|
-
for (let pellet = 0; pellet < pellets; pellet
|
|
147
|
-
const target =
|
|
294
|
+
if (!solidBlock && damageEntityHits.length > 0) {
|
|
295
|
+
for (let pellet = 0; pellet < pellets; pellet += 1) {
|
|
296
|
+
const target = damageEntityHits[pellet % damageEntityHits.length];
|
|
148
297
|
hits.push(...deps.effects.applyEffect({
|
|
149
298
|
from: input.from,
|
|
150
299
|
to: target.instanceId,
|
|
@@ -153,9 +302,18 @@ export function createProjectileSystem(deps) {
|
|
|
153
302
|
}));
|
|
154
303
|
}
|
|
155
304
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
305
|
+
let at;
|
|
306
|
+
if (impact !== null) {
|
|
307
|
+
at = [impact.point[0], impact.point[1], impact.point[2]];
|
|
308
|
+
}
|
|
309
|
+
else if (damageEntityHits[0] !== undefined) {
|
|
310
|
+
at = [damageEntityHits[0].at[0], damageEntityHits[0].at[1], damageEntityHits[0].at[2]];
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
at = missPoint(input);
|
|
314
|
+
}
|
|
315
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: hits.length > 0 });
|
|
316
|
+
return { status: "settled", shotId, at, hits, origin: originTuple };
|
|
159
317
|
},
|
|
160
318
|
};
|
|
161
319
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
export type ResistVerdict = "immune" | "resist" | "normal" | "vulnerable";
|
|
2
|
-
export
|
|
3
|
-
|
|
2
|
+
export declare class UnknownResistanceCategoryError extends Error {
|
|
3
|
+
readonly category: string;
|
|
4
|
+
constructor(category: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class UnknownResistancePropertyError extends Error {
|
|
7
|
+
readonly category: string;
|
|
8
|
+
readonly property: string;
|
|
9
|
+
constructor(category: string, property: string);
|
|
10
|
+
}
|
|
11
|
+
export interface ResistanceMatrix<TCategory extends string = string, TProperty extends string = string> {
|
|
12
|
+
categories: Partial<Record<TCategory, Partial<Record<TProperty, ResistVerdict>>>>;
|
|
13
|
+
categoryIds?: readonly TCategory[];
|
|
14
|
+
propertyIds?: readonly TProperty[];
|
|
4
15
|
multipliers?: Partial<Record<ResistVerdict, number>>;
|
|
5
16
|
default?: ResistVerdict;
|
|
17
|
+
unknownCategory?: ResistVerdict;
|
|
6
18
|
}
|
|
7
19
|
export interface ResistanceResult {
|
|
8
20
|
verdict: ResistVerdict;
|
|
9
21
|
multiplier: number;
|
|
10
22
|
immune: boolean;
|
|
11
23
|
}
|
|
12
|
-
export declare function resolveResistance(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): ResistanceResult;
|
|
13
|
-
export declare function resistanceScale(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): number;
|
|
24
|
+
export declare function resolveResistance<TCategory extends string = string, TProperty extends string = string>(matrix: ResistanceMatrix<TCategory, TProperty>, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): ResistanceResult;
|
|
25
|
+
export declare function resistanceScale<TCategory extends string = string, TProperty extends string = string>(matrix: ResistanceMatrix<TCategory, TProperty>, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): number;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
export class UnknownResistanceCategoryError extends Error {
|
|
2
|
+
category;
|
|
3
|
+
constructor(category) {
|
|
4
|
+
super(`Unknown resistance category: ${category}`);
|
|
5
|
+
this.name = "UnknownResistanceCategoryError";
|
|
6
|
+
this.category = category;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class UnknownResistancePropertyError extends Error {
|
|
10
|
+
category;
|
|
11
|
+
property;
|
|
12
|
+
constructor(category, property) {
|
|
13
|
+
super(`Unknown resistance property "${property}" for category "${category}"`);
|
|
14
|
+
this.name = "UnknownResistancePropertyError";
|
|
15
|
+
this.category = category;
|
|
16
|
+
this.property = property;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
1
19
|
const DEFAULT_MULTIPLIERS = {
|
|
2
20
|
immune: 0,
|
|
3
21
|
resist: 0.5,
|
|
@@ -17,13 +35,35 @@ function summaryVerdict(matrix, immune, net) {
|
|
|
17
35
|
return "resist";
|
|
18
36
|
return "normal";
|
|
19
37
|
}
|
|
38
|
+
function fromVerdict(matrix, verdict) {
|
|
39
|
+
if (verdict === "immune")
|
|
40
|
+
return { verdict: "immune", multiplier: 0, immune: true };
|
|
41
|
+
const multiplier = multiplierFor(matrix, verdict);
|
|
42
|
+
return { verdict, multiplier, immune: false };
|
|
43
|
+
}
|
|
44
|
+
function categoryTable(matrix, category) {
|
|
45
|
+
return matrix.categories[category];
|
|
46
|
+
}
|
|
20
47
|
export function resolveResistance(matrix, category, targetProperties) {
|
|
21
|
-
const byProperty = matrix
|
|
48
|
+
const byProperty = categoryTable(matrix, category);
|
|
49
|
+
if (byProperty === undefined) {
|
|
50
|
+
if (matrix.unknownCategory !== undefined)
|
|
51
|
+
return fromVerdict(matrix, matrix.unknownCategory);
|
|
52
|
+
const catalog = matrix.categoryIds ?? Object.keys(matrix.categories);
|
|
53
|
+
if (catalog.length === 0 && matrix.default !== undefined) {
|
|
54
|
+
return fromVerdict(matrix, matrix.default);
|
|
55
|
+
}
|
|
56
|
+
throw new UnknownResistanceCategoryError(category);
|
|
57
|
+
}
|
|
58
|
+
const propertyCatalog = matrix.propertyIds !== undefined ? new Set(matrix.propertyIds) : null;
|
|
22
59
|
const fallback = matrix.default ?? "normal";
|
|
23
60
|
let net = 1;
|
|
24
61
|
let matched = false;
|
|
25
62
|
let immune = false;
|
|
26
63
|
for (const property of targetProperties) {
|
|
64
|
+
if (propertyCatalog !== null && !propertyCatalog.has(property)) {
|
|
65
|
+
throw new UnknownResistancePropertyError(category, property);
|
|
66
|
+
}
|
|
27
67
|
const verdict = byProperty[property];
|
|
28
68
|
if (verdict === undefined)
|
|
29
69
|
continue;
|
|
@@ -32,12 +72,8 @@ export function resolveResistance(matrix, category, targetProperties) {
|
|
|
32
72
|
immune = true;
|
|
33
73
|
net *= multiplierFor(matrix, verdict);
|
|
34
74
|
}
|
|
35
|
-
if (!matched)
|
|
36
|
-
|
|
37
|
-
return { verdict: "immune", multiplier: 0, immune: true };
|
|
38
|
-
const multiplier = multiplierFor(matrix, fallback);
|
|
39
|
-
return { verdict: fallback, multiplier, immune: false };
|
|
40
|
-
}
|
|
75
|
+
if (!matched)
|
|
76
|
+
return fromVerdict(matrix, fallback);
|
|
41
77
|
const multiplier = immune ? 0 : net;
|
|
42
78
|
return { verdict: summaryVerdict(matrix, immune, multiplier), multiplier, immune };
|
|
43
79
|
}
|