@jgengine/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -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.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -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/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- 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 +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/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/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- 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 +29 -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 +1 -0
- package/dist/index.js +1 -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/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/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -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/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/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- 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 +21 -0
- package/dist/runtime/gameContext.js +184 -9
- 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/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -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/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -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.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- 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/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 +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- 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/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -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 +2585 -1704
- 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);
|
|
@@ -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: "legacy" }` — aim.origin or shooter position. */
|
|
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,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
|
-
const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
6
7
|
function isEntityHit(hit) {
|
|
7
8
|
return hit.kind === "entity";
|
|
8
9
|
}
|
|
9
10
|
function isObjectHit(hit) {
|
|
10
11
|
return hit.kind === "object";
|
|
11
12
|
}
|
|
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;
|
|
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
|
+
};
|
|
34
24
|
}
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
};
|
|
42
35
|
}
|
|
43
|
-
function
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
};
|
|
48
54
|
}
|
|
49
|
-
function
|
|
50
|
-
return
|
|
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
|
+
}));
|
|
51
68
|
}
|
|
52
69
|
export function createProjectileSystem(deps) {
|
|
53
70
|
const shots = new Map();
|
|
54
71
|
let shotCounter = 0;
|
|
55
72
|
const now = deps.now ?? (() => Date.now());
|
|
73
|
+
const defaultPolicy = deps.defaultOriginPolicy ?? { kind: "legacy" };
|
|
56
74
|
function itemStat(via, stat) {
|
|
57
75
|
return via.item === undefined ? null : deps.getStat(via.item, stat);
|
|
58
76
|
}
|
|
59
|
-
function shotOrigin(from, aim) {
|
|
60
|
-
return "origin" in aim ? aim.origin : deps.spatial.positionOf(from);
|
|
61
|
-
}
|
|
62
77
|
function withWeaponSpread(via, aim) {
|
|
63
78
|
if ("origin" in aim || aim.spread !== undefined)
|
|
64
79
|
return aim;
|
|
65
80
|
const spread = itemStat(via, "spread");
|
|
66
81
|
return spread === null ? aim : { ...aim, spread };
|
|
67
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
|
+
});
|
|
68
133
|
const raycast = deps.raycast ??
|
|
69
|
-
((from, aim, range) => {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
return [];
|
|
73
|
-
const direction = aimDirection(aim);
|
|
74
|
-
if (direction === null)
|
|
134
|
+
((from, aim, range, originPolicy) => {
|
|
135
|
+
const resolved = resolveShotFor(from, aim, originPolicy);
|
|
136
|
+
if (resolved === null)
|
|
75
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);
|
|
76
146
|
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
|
-
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
|
-
};
|
|
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
|
+
}
|
|
126
163
|
}
|
|
164
|
+
mapped.push(sceneHitToRaycast(hit));
|
|
127
165
|
}
|
|
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);
|
|
166
|
+
return mapped;
|
|
133
167
|
});
|
|
134
168
|
function resolveRange(via) {
|
|
135
169
|
return itemStat(via, "range") ?? DEFAULT_RANGE;
|
|
@@ -138,8 +172,9 @@ export function createProjectileSystem(deps) {
|
|
|
138
172
|
return itemStat(via, "projectile.fuseTime") !== null || itemStat(via, "explosion.radius") !== null;
|
|
139
173
|
}
|
|
140
174
|
function ballisticArc(input) {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
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];
|
|
143
178
|
const speed = itemStat(input.via, "projectile.speed") ?? DEFAULT_PROJECTILE_SPEED;
|
|
144
179
|
const gravityScale = itemStat(input.via, "projectile.gravity") ?? 1;
|
|
145
180
|
const fuseTime = itemStat(input.via, "projectile.fuseTime");
|
|
@@ -175,31 +210,42 @@ export function createProjectileSystem(deps) {
|
|
|
175
210
|
}
|
|
176
211
|
function predictHits(input) {
|
|
177
212
|
const aim = withWeaponSpread(input.via, input.aim);
|
|
178
|
-
const
|
|
213
|
+
const resolved = resolveShotFor(input.from, aim, input.originPolicy);
|
|
214
|
+
const rawHits = raycast(input.from, aim, resolveRange(input.via), input.originPolicy);
|
|
179
215
|
const visible = rawHits.filter((hit) => isObjectHit(hit) || deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
180
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
rawHits,
|
|
218
|
+
visible,
|
|
219
|
+
origin: resolved?.origin,
|
|
220
|
+
direction: resolved?.direction,
|
|
221
|
+
};
|
|
181
222
|
}
|
|
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)
|
|
223
|
+
function missPoint(input) {
|
|
224
|
+
const resolved = resolveShotFor(input.from, input.aim, input.originPolicy);
|
|
225
|
+
if (resolved === null)
|
|
189
226
|
return [0, 0, 0];
|
|
190
227
|
const range = resolveRange(input.via);
|
|
191
|
-
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
|
+
];
|
|
192
233
|
}
|
|
193
234
|
return {
|
|
194
235
|
willHitProjectile(input) {
|
|
195
|
-
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;
|
|
196
239
|
const prediction = {
|
|
197
|
-
hits: visible.map(
|
|
198
|
-
|
|
199
|
-
|
|
240
|
+
hits: visible.map(toProjectileHit),
|
|
241
|
+
...(origin !== undefined ? { origin } : {}),
|
|
242
|
+
...(direction !== undefined ? { direction } : {}),
|
|
243
|
+
firstImpact: impact === null ? null : toProjectileHit(sceneHitToRaycast(impact)),
|
|
200
244
|
};
|
|
201
245
|
if (rawHits.length > 0 && visible.length === 0)
|
|
202
246
|
prediction.blocked = true;
|
|
247
|
+
else if (solidBlock)
|
|
248
|
+
prediction.blocked = true;
|
|
203
249
|
return prediction;
|
|
204
250
|
},
|
|
205
251
|
fireProjectile(input) {
|
|
@@ -216,21 +262,38 @@ export function createProjectileSystem(deps) {
|
|
|
216
262
|
return { status: "rejected", shotId, reason: "already-settled" };
|
|
217
263
|
shot.settled = true;
|
|
218
264
|
const { input } = shot;
|
|
219
|
-
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]];
|
|
220
268
|
if (isBallistic(input.via)) {
|
|
221
269
|
const at = ballisticSettlePoint(input);
|
|
222
|
-
|
|
223
|
-
|
|
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 };
|
|
224
282
|
}
|
|
225
|
-
const { visible } = predictHits(input);
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
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);
|
|
229
292
|
const pellets = Math.max(1, Math.round(itemStat(input.via, "pellets") ?? 1));
|
|
230
293
|
const hits = [];
|
|
231
|
-
if (
|
|
232
|
-
for (let pellet = 0; pellet < pellets; pellet
|
|
233
|
-
const target =
|
|
294
|
+
if (!solidBlock && damageEntityHits.length > 0) {
|
|
295
|
+
for (let pellet = 0; pellet < pellets; pellet += 1) {
|
|
296
|
+
const target = damageEntityHits[pellet % damageEntityHits.length];
|
|
234
297
|
hits.push(...deps.effects.applyEffect({
|
|
235
298
|
from: input.from,
|
|
236
299
|
to: target.instanceId,
|
|
@@ -239,9 +302,18 @@ export function createProjectileSystem(deps) {
|
|
|
239
302
|
}));
|
|
240
303
|
}
|
|
241
304
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 };
|
|
245
317
|
},
|
|
246
318
|
};
|
|
247
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
|
}
|