@jgengine/core 0.6.0 → 0.7.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 +167 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +281 -4
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +35 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +83 -0
- package/dist/physics/physicsWorld.js +261 -1
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +64 -2
- package/dist/runtime/gameContext.js +154 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +1 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export interface FrustumCamera {
|
|
3
|
+
position: EntityPosition;
|
|
4
|
+
lookAt: EntityPosition;
|
|
5
|
+
/** Vertical field of view in degrees. Default 55. */
|
|
6
|
+
fovDeg?: number;
|
|
7
|
+
/** Width / height. Default 16 / 9. */
|
|
8
|
+
aspect?: number;
|
|
9
|
+
near?: number;
|
|
10
|
+
far?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FrustumTarget {
|
|
13
|
+
id: string;
|
|
14
|
+
position: EntityPosition;
|
|
15
|
+
/** Subject radius in world units, used by the framing-size score. Default 0.5. */
|
|
16
|
+
radius?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface FrustumProjection {
|
|
19
|
+
inView: boolean;
|
|
20
|
+
distance: number;
|
|
21
|
+
/** -1..1, 0 = screen center, negative = left/down. */
|
|
22
|
+
screenX: number;
|
|
23
|
+
screenY: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Project a world point into the camera's view. Pure geometry — no three.js —
|
|
27
|
+
* so a game can run "is this framed?" checks headlessly (photo-mode scoring,
|
|
28
|
+
* server-side kill-cam validation) as well as from the live render camera.
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectToView(camera: FrustumCamera, point: EntityPosition): FrustumProjection;
|
|
31
|
+
export interface FramingConfig {
|
|
32
|
+
/** World-unit distance from camera that reads as "ideally framed" size. Default 6. */
|
|
33
|
+
idealDistance?: number;
|
|
34
|
+
/** Weight of screen-center placement in the framing score. Default 0.6. */
|
|
35
|
+
centerWeight?: number;
|
|
36
|
+
/** Weight of subject size (distance-to-ideal) in the framing score. Default 0.4. */
|
|
37
|
+
sizeWeight?: number;
|
|
38
|
+
}
|
|
39
|
+
/** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
|
|
40
|
+
export declare function framingScore(projection: FrustumProjection, config?: FramingConfig): number;
|
|
41
|
+
export interface FrustumSample {
|
|
42
|
+
id: string;
|
|
43
|
+
inView: boolean;
|
|
44
|
+
distance: number;
|
|
45
|
+
screenX: number;
|
|
46
|
+
screenY: number;
|
|
47
|
+
framing: number;
|
|
48
|
+
/** Seconds continuously in view, up to and including this tick; resets to 0 the instant the subject leaves view. */
|
|
49
|
+
dwellSeconds: number;
|
|
50
|
+
}
|
|
51
|
+
export interface FrustumSensor {
|
|
52
|
+
tick(camera: FrustumCamera, targets: readonly FrustumTarget[], dt: number): FrustumSample[];
|
|
53
|
+
reset(id?: string): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A view-frustum sensor on a held camera object: which entities are in frame,
|
|
57
|
+
* how well framed they are, and how long they've stayed on-screen (photo-mode
|
|
58
|
+
* "is this subject framed", Content Warning-style monster-filming scoring).
|
|
59
|
+
*/
|
|
60
|
+
export declare function createFrustumSensor(config?: FramingConfig): FrustumSensor;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function subtract(a, b) {
|
|
2
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
3
|
+
}
|
|
4
|
+
function length(v) {
|
|
5
|
+
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
6
|
+
}
|
|
7
|
+
function normalize(v) {
|
|
8
|
+
const len = length(v);
|
|
9
|
+
return len === 0 ? [0, 0, 0] : [v[0] / len, v[1] / len, v[2] / len];
|
|
10
|
+
}
|
|
11
|
+
function cross(a, b) {
|
|
12
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
13
|
+
}
|
|
14
|
+
function dot(a, b) {
|
|
15
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
16
|
+
}
|
|
17
|
+
const WORLD_UP = [0, 1, 0];
|
|
18
|
+
/**
|
|
19
|
+
* Project a world point into the camera's view. Pure geometry — no three.js —
|
|
20
|
+
* so a game can run "is this framed?" checks headlessly (photo-mode scoring,
|
|
21
|
+
* server-side kill-cam validation) as well as from the live render camera.
|
|
22
|
+
*/
|
|
23
|
+
export function projectToView(camera, point) {
|
|
24
|
+
const forward = normalize(subtract(camera.lookAt, camera.position));
|
|
25
|
+
const relative = subtract(point, camera.position);
|
|
26
|
+
const distance = length(relative);
|
|
27
|
+
const depth = dot(relative, forward);
|
|
28
|
+
const near = camera.near ?? 0.1;
|
|
29
|
+
const far = camera.far ?? Number.POSITIVE_INFINITY;
|
|
30
|
+
if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0) {
|
|
31
|
+
return { inView: false, distance, screenX: 0, screenY: 0 };
|
|
32
|
+
}
|
|
33
|
+
if (depth <= near || depth >= far) {
|
|
34
|
+
return { inView: false, distance, screenX: 0, screenY: 0 };
|
|
35
|
+
}
|
|
36
|
+
let right = normalize(cross(forward, WORLD_UP));
|
|
37
|
+
if (right[0] === 0 && right[1] === 0 && right[2] === 0) {
|
|
38
|
+
right = normalize(cross(forward, [1, 0, 0]));
|
|
39
|
+
}
|
|
40
|
+
const up = cross(right, forward);
|
|
41
|
+
const fovDeg = camera.fovDeg ?? 55;
|
|
42
|
+
const aspect = camera.aspect ?? 16 / 9;
|
|
43
|
+
const halfHeight = Math.tan((fovDeg * Math.PI) / 360);
|
|
44
|
+
const halfWidth = halfHeight * aspect;
|
|
45
|
+
const screenX = dot(relative, right) / (depth * halfWidth);
|
|
46
|
+
const screenY = dot(relative, up) / (depth * halfHeight);
|
|
47
|
+
const inView = Math.abs(screenX) <= 1 && Math.abs(screenY) <= 1;
|
|
48
|
+
return { inView, distance, screenX, screenY };
|
|
49
|
+
}
|
|
50
|
+
/** 0 (not framed) .. 1 (dead-center, ideal distance) framing quality for an in-view projection. */
|
|
51
|
+
export function framingScore(projection, config) {
|
|
52
|
+
if (!projection.inView)
|
|
53
|
+
return 0;
|
|
54
|
+
const idealDistance = config?.idealDistance ?? 6;
|
|
55
|
+
const centerWeight = config?.centerWeight ?? 0.6;
|
|
56
|
+
const sizeWeight = config?.sizeWeight ?? 0.4;
|
|
57
|
+
const radial = Math.min(1, Math.sqrt(projection.screenX * projection.screenX + projection.screenY * projection.screenY));
|
|
58
|
+
const centerScore = 1 - radial;
|
|
59
|
+
const sizeScore = idealDistance <= 0
|
|
60
|
+
? 0
|
|
61
|
+
: Math.max(0, 1 - Math.abs(projection.distance - idealDistance) / idealDistance);
|
|
62
|
+
return Math.max(0, Math.min(1, centerWeight * centerScore + sizeWeight * sizeScore));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A view-frustum sensor on a held camera object: which entities are in frame,
|
|
66
|
+
* how well framed they are, and how long they've stayed on-screen (photo-mode
|
|
67
|
+
* "is this subject framed", Content Warning-style monster-filming scoring).
|
|
68
|
+
*/
|
|
69
|
+
export function createFrustumSensor(config) {
|
|
70
|
+
const dwell = new Map();
|
|
71
|
+
return {
|
|
72
|
+
tick(camera, targets, dt) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const samples = [];
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
seen.add(target.id);
|
|
77
|
+
const projection = projectToView(camera, target.position);
|
|
78
|
+
const framing = framingScore(projection, config);
|
|
79
|
+
const nextDwell = projection.inView ? (dwell.get(target.id) ?? 0) + dt : 0;
|
|
80
|
+
dwell.set(target.id, nextDwell);
|
|
81
|
+
samples.push({
|
|
82
|
+
id: target.id,
|
|
83
|
+
inView: projection.inView,
|
|
84
|
+
distance: projection.distance,
|
|
85
|
+
screenX: projection.screenX,
|
|
86
|
+
screenY: projection.screenY,
|
|
87
|
+
framing,
|
|
88
|
+
dwellSeconds: nextDwell,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const id of dwell.keys()) {
|
|
92
|
+
if (!seen.has(id))
|
|
93
|
+
dwell.delete(id);
|
|
94
|
+
}
|
|
95
|
+
return samples;
|
|
96
|
+
},
|
|
97
|
+
reset(id) {
|
|
98
|
+
if (id === undefined)
|
|
99
|
+
dwell.clear();
|
|
100
|
+
else
|
|
101
|
+
dwell.delete(id);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export type HiddenStateValue = number | string | boolean;
|
|
3
|
+
export interface HiddenStateSource {
|
|
4
|
+
id: string;
|
|
5
|
+
position: EntityPosition;
|
|
6
|
+
variables: Readonly<Record<string, HiddenStateValue>>;
|
|
7
|
+
}
|
|
8
|
+
export interface SensorProbeOptions {
|
|
9
|
+
range: number;
|
|
10
|
+
variableId: string;
|
|
11
|
+
/** How reading `strength` falls off with distance. Default "linear". */
|
|
12
|
+
falloff?: "linear" | "none";
|
|
13
|
+
}
|
|
14
|
+
export interface SensorReading {
|
|
15
|
+
sourceId: string;
|
|
16
|
+
variableId: string;
|
|
17
|
+
value: HiddenStateValue;
|
|
18
|
+
/** 1 at the source, 0 at the edge of `range` ("none" falloff always reads 1 in range). */
|
|
19
|
+
strength: number;
|
|
20
|
+
distance: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A sensor verb: probe a hidden zone/entity state variable in range and surface
|
|
24
|
+
* a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
|
|
25
|
+
* `proximityPrompt` (a UI affordance), this reads world state the player has no
|
|
26
|
+
* other way to see — the strongest in-range reading wins, mirroring how a real
|
|
27
|
+
* handheld sensor needle settles on the loudest nearby source.
|
|
28
|
+
*/
|
|
29
|
+
export declare function probeHiddenState(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading | null;
|
|
30
|
+
/** All in-range readings for `variableId`, strongest first. */
|
|
31
|
+
export declare function probeHiddenStateAll(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): SensorReading[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { distanceBetween } from "../scene/spatial.js";
|
|
2
|
+
function strengthAt(distance, range, falloff) {
|
|
3
|
+
if (falloff === "none")
|
|
4
|
+
return 1;
|
|
5
|
+
if (range <= 0)
|
|
6
|
+
return 0;
|
|
7
|
+
return Math.max(0, 1 - distance / range);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A sensor verb: probe a hidden zone/entity state variable in range and surface
|
|
11
|
+
* a reading (EMF reader, spirit box, thermometer, geiger counter). Unlike
|
|
12
|
+
* `proximityPrompt` (a UI affordance), this reads world state the player has no
|
|
13
|
+
* other way to see — the strongest in-range reading wins, mirroring how a real
|
|
14
|
+
* handheld sensor needle settles on the loudest nearby source.
|
|
15
|
+
*/
|
|
16
|
+
export function probeHiddenState(origin, sources, options) {
|
|
17
|
+
const readings = probeHiddenStateAll(origin, sources, options);
|
|
18
|
+
return readings.length === 0 ? null : readings[0];
|
|
19
|
+
}
|
|
20
|
+
/** All in-range readings for `variableId`, strongest first. */
|
|
21
|
+
export function probeHiddenStateAll(origin, sources, options) {
|
|
22
|
+
const { range, variableId, falloff = "linear" } = options;
|
|
23
|
+
const readings = [];
|
|
24
|
+
for (const source of sources) {
|
|
25
|
+
if (!(variableId in source.variables))
|
|
26
|
+
continue;
|
|
27
|
+
const distance = distanceBetween(origin, source.position);
|
|
28
|
+
if (distance > range)
|
|
29
|
+
continue;
|
|
30
|
+
readings.push({
|
|
31
|
+
sourceId: source.id,
|
|
32
|
+
variableId,
|
|
33
|
+
value: source.variables[variableId],
|
|
34
|
+
strength: strengthAt(distance, range, falloff),
|
|
35
|
+
distance,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
readings.sort((a, b) => b.strength - a.strength);
|
|
39
|
+
return readings;
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RecordingFrame<T> {
|
|
2
|
+
/** Game-time seconds (from `ctx.time`), so pause/fast-forward scrub the recording consistently with the sim. */
|
|
3
|
+
t: number;
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
export interface RecordingBufferOptions {
|
|
7
|
+
/** Evict frames older than `latestT - maxDurationSeconds`. Default unlimited. */
|
|
8
|
+
maxDurationSeconds?: number;
|
|
9
|
+
/** Hard cap on frame count, oldest evicted first. Default unlimited. */
|
|
10
|
+
maxFrames?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RecordingBuffer<T> {
|
|
13
|
+
append(t: number, data: T): void;
|
|
14
|
+
/** Nearest frame at or before `t`, or null if the buffer is empty or `t` precedes every frame. */
|
|
15
|
+
seek(t: number): RecordingFrame<T> | null;
|
|
16
|
+
/** Frames with `fromT <= t <= toT`, in recorded order. */
|
|
17
|
+
range(fromT: number, toT: number): RecordingFrame<T>[];
|
|
18
|
+
clear(): void;
|
|
19
|
+
duration(): number;
|
|
20
|
+
frames(): readonly RecordingFrame<T>[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A session-recording buffer for replay / photo mode / kill-cam: append
|
|
24
|
+
* timestamped snapshots (poses, camera state, whatever `T` is) on game-time,
|
|
25
|
+
* then seek/scrub the recording independent of the live sim. Frames are
|
|
26
|
+
* expected in non-decreasing `t` order (one push per tick); `seek`/`range`
|
|
27
|
+
* binary-search on that assumption.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createRecordingBuffer<T>(options?: RecordingBufferOptions): RecordingBuffer<T>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function evict(frames, options) {
|
|
2
|
+
if (options.maxDurationSeconds !== undefined && frames.length > 0) {
|
|
3
|
+
const cutoff = frames[frames.length - 1].t - options.maxDurationSeconds;
|
|
4
|
+
let dropTo = 0;
|
|
5
|
+
while (dropTo < frames.length && frames[dropTo].t < cutoff)
|
|
6
|
+
dropTo += 1;
|
|
7
|
+
if (dropTo > 0)
|
|
8
|
+
frames.splice(0, dropTo);
|
|
9
|
+
}
|
|
10
|
+
if (options.maxFrames !== undefined && frames.length > options.maxFrames) {
|
|
11
|
+
frames.splice(0, frames.length - options.maxFrames);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A session-recording buffer for replay / photo mode / kill-cam: append
|
|
16
|
+
* timestamped snapshots (poses, camera state, whatever `T` is) on game-time,
|
|
17
|
+
* then seek/scrub the recording independent of the live sim. Frames are
|
|
18
|
+
* expected in non-decreasing `t` order (one push per tick); `seek`/`range`
|
|
19
|
+
* binary-search on that assumption.
|
|
20
|
+
*/
|
|
21
|
+
export function createRecordingBuffer(options = {}) {
|
|
22
|
+
const frames = [];
|
|
23
|
+
function floorIndex(t) {
|
|
24
|
+
let lo = 0;
|
|
25
|
+
let hi = frames.length - 1;
|
|
26
|
+
let result = -1;
|
|
27
|
+
while (lo <= hi) {
|
|
28
|
+
const mid = (lo + hi) >> 1;
|
|
29
|
+
if (frames[mid].t <= t) {
|
|
30
|
+
result = mid;
|
|
31
|
+
lo = mid + 1;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
hi = mid - 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
append(t, data) {
|
|
41
|
+
frames.push({ t, data });
|
|
42
|
+
evict(frames, options);
|
|
43
|
+
},
|
|
44
|
+
seek(t) {
|
|
45
|
+
const index = floorIndex(t);
|
|
46
|
+
return index === -1 ? null : frames[index];
|
|
47
|
+
},
|
|
48
|
+
range(fromT, toT) {
|
|
49
|
+
return frames.filter((frame) => frame.t >= fromT && frame.t <= toT);
|
|
50
|
+
},
|
|
51
|
+
clear() {
|
|
52
|
+
frames.length = 0;
|
|
53
|
+
},
|
|
54
|
+
duration() {
|
|
55
|
+
return frames.length === 0 ? 0 : frames[frames.length - 1].t - frames[0].t;
|
|
56
|
+
},
|
|
57
|
+
frames() {
|
|
58
|
+
return frames;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export interface RevealHit {
|
|
3
|
+
instanceId: string;
|
|
4
|
+
distance: number;
|
|
5
|
+
tags: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface RevealQueryOptions {
|
|
8
|
+
resolvePosition: (instanceId: string) => EntityPosition | undefined;
|
|
9
|
+
resolveTags: (instanceId: string) => readonly string[];
|
|
10
|
+
candidates: () => string[];
|
|
11
|
+
}
|
|
12
|
+
export interface RevealQuery {
|
|
13
|
+
inRadius(center: EntityPosition | string, radius: number, tags: readonly string[]): RevealHit[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `inRadius` already ignores occlusion; combat's AoE resolution is what layers a
|
|
17
|
+
* line-of-sight filter on top of it (see `effect({ at, radius, los })`). This is
|
|
18
|
+
* the same unfiltered radius query, scoped to catalog-declared tags and shaped
|
|
19
|
+
* for a vision readout (Dark Sight / detective-vision reveal) instead of damage
|
|
20
|
+
* resolution — it never consults an occluder, by design.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createRevealQuery(options: RevealQueryOptions): RevealQuery;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { distanceBetween } from "../scene/spatial.js";
|
|
2
|
+
/**
|
|
3
|
+
* `inRadius` already ignores occlusion; combat's AoE resolution is what layers a
|
|
4
|
+
* line-of-sight filter on top of it (see `effect({ at, radius, los })`). This is
|
|
5
|
+
* the same unfiltered radius query, scoped to catalog-declared tags and shaped
|
|
6
|
+
* for a vision readout (Dark Sight / detective-vision reveal) instead of damage
|
|
7
|
+
* resolution — it never consults an occluder, by design.
|
|
8
|
+
*/
|
|
9
|
+
export function createRevealQuery(options) {
|
|
10
|
+
const { resolvePosition, resolveTags, candidates } = options;
|
|
11
|
+
return {
|
|
12
|
+
inRadius(center, radius, tags) {
|
|
13
|
+
const centerId = typeof center === "string" ? center : null;
|
|
14
|
+
const centerPosition = typeof center === "string" ? resolvePosition(center) : center;
|
|
15
|
+
if (centerPosition === undefined)
|
|
16
|
+
return [];
|
|
17
|
+
const hits = [];
|
|
18
|
+
for (const instanceId of candidates()) {
|
|
19
|
+
if (instanceId === centerId)
|
|
20
|
+
continue;
|
|
21
|
+
const position = resolvePosition(instanceId);
|
|
22
|
+
if (position === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
const distance = distanceBetween(centerPosition, position);
|
|
25
|
+
if (distance > radius)
|
|
26
|
+
continue;
|
|
27
|
+
const entityTags = resolveTags(instanceId);
|
|
28
|
+
if (tags.length > 0 && !tags.some((tag) => entityTags.includes(tag)))
|
|
29
|
+
continue;
|
|
30
|
+
hits.push({ instanceId, distance, tags: entityTags });
|
|
31
|
+
}
|
|
32
|
+
hits.sort((a, b) => a.distance - b.distance);
|
|
33
|
+
return hits;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ContestedPhase = "idle" | "active" | "paused" | "contested" | "complete" | "interrupted";
|
|
2
|
+
export type ContestReaction = "pause" | "decay";
|
|
3
|
+
export interface ContestedChannelConfig {
|
|
4
|
+
duration: number;
|
|
5
|
+
interruptOnDamage?: boolean;
|
|
6
|
+
resetOnInterrupt?: boolean;
|
|
7
|
+
favorability?: Record<string, number>;
|
|
8
|
+
ratePerOccupant?: boolean;
|
|
9
|
+
contested?: ContestReaction;
|
|
10
|
+
decayRate?: number;
|
|
11
|
+
}
|
|
12
|
+
export type ContestedEventKind = "start" | "tick" | "contested" | "paused" | "complete" | "interrupted";
|
|
13
|
+
export interface ContestedEvent {
|
|
14
|
+
kind: ContestedEventKind;
|
|
15
|
+
owner: string | null;
|
|
16
|
+
progress: number;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ContestedSnapshot {
|
|
20
|
+
phase: ContestedPhase;
|
|
21
|
+
progress: number;
|
|
22
|
+
owner: string | null;
|
|
23
|
+
remaining: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ContestedChannel {
|
|
26
|
+
start(team: string): ContestedEvent | null;
|
|
27
|
+
tick(dt: number, occupants?: Record<string, number>): ContestedEvent[];
|
|
28
|
+
damage(reason?: string): ContestedEvent | null;
|
|
29
|
+
reset(): void;
|
|
30
|
+
progress(): number;
|
|
31
|
+
phase(): ContestedPhase;
|
|
32
|
+
owner(): string | null;
|
|
33
|
+
remaining(): number;
|
|
34
|
+
snapshot(): ContestedSnapshot;
|
|
35
|
+
}
|
|
36
|
+
export declare function createContestedChannel(config: ContestedChannelConfig): ContestedChannel;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
function occupantsOf(occupants, team) {
|
|
2
|
+
return occupants?.[team] ?? 0;
|
|
3
|
+
}
|
|
4
|
+
function anyContester(occupants, owner) {
|
|
5
|
+
if (occupants === undefined)
|
|
6
|
+
return false;
|
|
7
|
+
for (const [team, count] of Object.entries(occupants)) {
|
|
8
|
+
if (team !== owner && count > 0)
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
export function createContestedChannel(config) {
|
|
14
|
+
const duration = config.duration > 0 ? config.duration : Number.EPSILON;
|
|
15
|
+
const interruptOnDamage = config.interruptOnDamage ?? true;
|
|
16
|
+
const resetOnInterrupt = config.resetOnInterrupt ?? false;
|
|
17
|
+
const favorability = config.favorability ?? {};
|
|
18
|
+
const contested = config.contested ?? "pause";
|
|
19
|
+
const decayRate = config.decayRate !== undefined ? Math.abs(config.decayRate) : 1 / duration;
|
|
20
|
+
let phase = "idle";
|
|
21
|
+
let progress = 0;
|
|
22
|
+
let owner = null;
|
|
23
|
+
function fillRate(occupantCount) {
|
|
24
|
+
const multiplier = favorability[owner ?? ""] ?? 1;
|
|
25
|
+
const scale = config.ratePerOccupant ? Math.max(1, occupantCount) : 1;
|
|
26
|
+
return (multiplier * scale) / duration;
|
|
27
|
+
}
|
|
28
|
+
function snapshot() {
|
|
29
|
+
return { phase, progress, owner, remaining: Math.max(0, (1 - progress) * duration) };
|
|
30
|
+
}
|
|
31
|
+
function emit(kind, reason) {
|
|
32
|
+
const event = { kind, owner, progress };
|
|
33
|
+
if (reason !== undefined)
|
|
34
|
+
event.reason = reason;
|
|
35
|
+
return event;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
start(team) {
|
|
39
|
+
if (phase === "active" || phase === "contested" || phase === "paused")
|
|
40
|
+
return null;
|
|
41
|
+
owner = team;
|
|
42
|
+
progress = phase === "interrupted" && !resetOnInterrupt ? progress : 0;
|
|
43
|
+
phase = "active";
|
|
44
|
+
return emit("start");
|
|
45
|
+
},
|
|
46
|
+
tick(dt, occupants) {
|
|
47
|
+
if (dt <= 0)
|
|
48
|
+
return [];
|
|
49
|
+
if (phase !== "active" && phase !== "paused" && phase !== "contested")
|
|
50
|
+
return [];
|
|
51
|
+
if (owner === null)
|
|
52
|
+
return [];
|
|
53
|
+
const ownerCount = occupantsOf(occupants, owner);
|
|
54
|
+
if (occupants !== undefined && ownerCount <= 0) {
|
|
55
|
+
if (phase !== "paused") {
|
|
56
|
+
phase = "paused";
|
|
57
|
+
return [emit("paused", "owner-left")];
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (anyContester(occupants, owner)) {
|
|
62
|
+
if (contested === "pause") {
|
|
63
|
+
const changed = phase !== "contested";
|
|
64
|
+
phase = "contested";
|
|
65
|
+
return changed ? [emit("contested")] : [];
|
|
66
|
+
}
|
|
67
|
+
progress = Math.max(0, progress - decayRate * dt);
|
|
68
|
+
phase = "contested";
|
|
69
|
+
return [emit("contested")];
|
|
70
|
+
}
|
|
71
|
+
phase = "active";
|
|
72
|
+
progress = Math.min(1, progress + fillRate(ownerCount) * dt);
|
|
73
|
+
if (progress >= 1) {
|
|
74
|
+
phase = "complete";
|
|
75
|
+
return [emit("tick"), emit("complete")];
|
|
76
|
+
}
|
|
77
|
+
return [emit("tick")];
|
|
78
|
+
},
|
|
79
|
+
damage(reason) {
|
|
80
|
+
if (phase !== "active" && phase !== "paused" && phase !== "contested")
|
|
81
|
+
return null;
|
|
82
|
+
if (!interruptOnDamage)
|
|
83
|
+
return null;
|
|
84
|
+
phase = "interrupted";
|
|
85
|
+
if (resetOnInterrupt)
|
|
86
|
+
progress = 0;
|
|
87
|
+
return emit("interrupted", reason ?? "damage");
|
|
88
|
+
},
|
|
89
|
+
reset() {
|
|
90
|
+
phase = "idle";
|
|
91
|
+
progress = 0;
|
|
92
|
+
owner = null;
|
|
93
|
+
},
|
|
94
|
+
progress: () => progress,
|
|
95
|
+
phase: () => phase,
|
|
96
|
+
owner: () => owner,
|
|
97
|
+
remaining: () => Math.max(0, (1 - progress) * duration),
|
|
98
|
+
snapshot,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ConsolationPolicy, type ContainerSnapshot, type DeliveryEntry, type DeliveryQueue, type InsurancePolicy, type ItemStack } from "../inventory/storageTier.js";
|
|
2
|
+
import { type ContestedChannel, type ContestedEvent } from "./contestedChannel.js";
|
|
3
|
+
import type { RingPoint } from "./ring.js";
|
|
4
|
+
export interface ExtractPoint {
|
|
5
|
+
id: string;
|
|
6
|
+
center: RingPoint;
|
|
7
|
+
radius: number;
|
|
8
|
+
holdSeconds: number;
|
|
9
|
+
favorability?: Record<string, number>;
|
|
10
|
+
interruptOnDamage?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface RaidSessionConfig {
|
|
13
|
+
extracts: readonly ExtractPoint[];
|
|
14
|
+
insurance?: InsurancePolicy;
|
|
15
|
+
consolation?: ConsolationPolicy;
|
|
16
|
+
}
|
|
17
|
+
export type RaidStatus = "in-raid" | "extracting" | "extracted" | "dead";
|
|
18
|
+
export interface ExtractionAttempt {
|
|
19
|
+
extractId: string;
|
|
20
|
+
channel: ContestedChannel;
|
|
21
|
+
}
|
|
22
|
+
export interface ExtractionResult {
|
|
23
|
+
userId: string;
|
|
24
|
+
extractId: string;
|
|
25
|
+
banked: readonly ItemStack[];
|
|
26
|
+
}
|
|
27
|
+
export interface DeathResult {
|
|
28
|
+
userId: string;
|
|
29
|
+
kept: readonly ItemStack[];
|
|
30
|
+
lost: readonly ItemStack[];
|
|
31
|
+
scheduled: DeliveryEntry | null;
|
|
32
|
+
consolationLoadoutId: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface RaidPlayerSnapshot {
|
|
35
|
+
status: RaidStatus;
|
|
36
|
+
extractId: string | null;
|
|
37
|
+
progress: number;
|
|
38
|
+
remaining: number;
|
|
39
|
+
}
|
|
40
|
+
export interface RaidSession {
|
|
41
|
+
beginExtract(userId: string, extractId: string, team?: string): ContestedEvent | null;
|
|
42
|
+
tickExtract(userId: string, dt: number, occupants?: Record<string, number>): ContestedEvent[];
|
|
43
|
+
damage(userId: string, reason?: string): ContestedEvent | null;
|
|
44
|
+
cancelExtract(userId: string): void;
|
|
45
|
+
resolveExtraction(userId: string, containers: readonly ContainerSnapshot[]): ExtractionResult;
|
|
46
|
+
resolveDeath(userId: string, containers: readonly ContainerSnapshot[], now: number, rng?: () => number): DeathResult;
|
|
47
|
+
claimDeliveries(now: number): readonly DeliveryEntry[];
|
|
48
|
+
pendingDeliveries(userId?: string): readonly DeliveryEntry[];
|
|
49
|
+
deliveries(): DeliveryQueue;
|
|
50
|
+
extractPoint(extractId: string): ExtractPoint | undefined;
|
|
51
|
+
status(userId: string): RaidStatus;
|
|
52
|
+
playerSnapshot(userId: string): RaidPlayerSnapshot;
|
|
53
|
+
}
|
|
54
|
+
export declare function createRaidSession(config: RaidSessionConfig): RaidSession;
|