@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
import { type ColliderPurpose, type EntityColliderSet } from "./colliders.js";
|
|
3
|
+
import type { SceneObject } from "./objectStore.js";
|
|
4
|
+
export type SceneRaycastTargetKind = "entity" | "object" | "terrain" | "wall";
|
|
5
|
+
export interface SceneRaycastHit {
|
|
6
|
+
targetKind: SceneRaycastTargetKind;
|
|
7
|
+
instanceId: string;
|
|
8
|
+
catalogId?: string;
|
|
9
|
+
colliderName: string;
|
|
10
|
+
purpose: ColliderPurpose;
|
|
11
|
+
damageEligible: boolean;
|
|
12
|
+
blocks: boolean;
|
|
13
|
+
distance: number;
|
|
14
|
+
point: EntityPosition;
|
|
15
|
+
normal: EntityPosition;
|
|
16
|
+
}
|
|
17
|
+
export interface SceneRaycastFilter {
|
|
18
|
+
entities?: boolean;
|
|
19
|
+
objects?: boolean;
|
|
20
|
+
terrain?: boolean;
|
|
21
|
+
walls?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SceneRaycastInput {
|
|
24
|
+
origin: EntityPosition;
|
|
25
|
+
direction: EntityPosition;
|
|
26
|
+
maxDistance: number;
|
|
27
|
+
excludeInstanceIds?: ReadonlySet<string> | readonly string[];
|
|
28
|
+
filter?: SceneRaycastFilter;
|
|
29
|
+
/** When set, only hits satisfying the predicate are kept (applied after geometry). */
|
|
30
|
+
accept?: (hit: SceneRaycastHit) => boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SceneEntityQuerySource {
|
|
33
|
+
list(): readonly {
|
|
34
|
+
id: string;
|
|
35
|
+
position: EntityPosition;
|
|
36
|
+
rotationY: number;
|
|
37
|
+
name?: string;
|
|
38
|
+
}[];
|
|
39
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
40
|
+
inRadius?(center: EntityPosition, radius: number): readonly string[];
|
|
41
|
+
get?(instanceId: string): {
|
|
42
|
+
id: string;
|
|
43
|
+
position: EntityPosition;
|
|
44
|
+
rotationY: number;
|
|
45
|
+
name?: string;
|
|
46
|
+
} | null;
|
|
47
|
+
}
|
|
48
|
+
export interface SceneObjectQuerySource {
|
|
49
|
+
list(): readonly SceneObject[];
|
|
50
|
+
inBox?(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
|
|
51
|
+
collidersOf?(instanceId: string): EntityColliderSet | null | undefined;
|
|
52
|
+
halfExtentsOf?(catalogId: string): EntityPosition | null | undefined;
|
|
53
|
+
}
|
|
54
|
+
export interface TerrainRaycastSource {
|
|
55
|
+
sampleHeight(x: number, z: number): number;
|
|
56
|
+
}
|
|
57
|
+
export interface WallSegment {
|
|
58
|
+
id: string;
|
|
59
|
+
/** Segment endpoints on the XZ plane; extruded by halfHeight along Y around yCenter. */
|
|
60
|
+
a: readonly [number, number];
|
|
61
|
+
b: readonly [number, number];
|
|
62
|
+
yCenter?: number;
|
|
63
|
+
halfHeight?: number;
|
|
64
|
+
thickness?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface SceneRaycastDeps {
|
|
67
|
+
entities?: SceneEntityQuerySource;
|
|
68
|
+
objects?: SceneObjectQuerySource;
|
|
69
|
+
terrain?: TerrainRaycastSource;
|
|
70
|
+
walls?: readonly WallSegment[];
|
|
71
|
+
}
|
|
72
|
+
export interface SceneRaycastApi {
|
|
73
|
+
raycast(input: SceneRaycastInput): SceneRaycastHit | null;
|
|
74
|
+
raycastAll(input: SceneRaycastInput): SceneRaycastHit[];
|
|
75
|
+
}
|
|
76
|
+
/** First impact: nearest hit that blocks, or nearest hit if none block. */
|
|
77
|
+
export declare function firstImpact(hits: readonly SceneRaycastHit[]): SceneRaycastHit | null;
|
|
78
|
+
/** Hits up to and including the first blocking collider (damage hitboxes before a wall stay). */
|
|
79
|
+
export declare function hitsUntilBlocked(hits: readonly SceneRaycastHit[]): SceneRaycastHit[];
|
|
80
|
+
export declare function createSceneRaycast(deps: SceneRaycastDeps): SceneRaycastApi;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { colliderBounds, colliderWorldCenter, defaultEntityColliders, defaultObjectColliders, resolveColliders, } from "./colliders.js";
|
|
2
|
+
import { intersectAabb, normalizeDirection } from "./objectQuery.js";
|
|
3
|
+
function excluded(exclude, id) {
|
|
4
|
+
if (exclude === undefined)
|
|
5
|
+
return false;
|
|
6
|
+
if (typeof exclude.has === "function") {
|
|
7
|
+
return exclude.has(id);
|
|
8
|
+
}
|
|
9
|
+
for (const value of exclude) {
|
|
10
|
+
if (value === id)
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function enabled(filter, key) {
|
|
16
|
+
if (filter === undefined)
|
|
17
|
+
return true;
|
|
18
|
+
return filter[key] !== false;
|
|
19
|
+
}
|
|
20
|
+
function rayHitsSphere(origin, direction, center, radius, maxDistance) {
|
|
21
|
+
const ox = origin[0] - center[0];
|
|
22
|
+
const oy = origin[1] - center[1];
|
|
23
|
+
const oz = origin[2] - center[2];
|
|
24
|
+
const b = ox * direction[0] + oy * direction[1] + oz * direction[2];
|
|
25
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
26
|
+
const disc = b * b - c;
|
|
27
|
+
if (disc < 0)
|
|
28
|
+
return null;
|
|
29
|
+
const sqrt = Math.sqrt(disc);
|
|
30
|
+
let t = -b - sqrt;
|
|
31
|
+
if (t < 0)
|
|
32
|
+
t = -b + sqrt;
|
|
33
|
+
if (t < 0 || t > maxDistance)
|
|
34
|
+
return null;
|
|
35
|
+
const px = origin[0] + direction[0] * t - center[0];
|
|
36
|
+
const py = origin[1] + direction[1] * t - center[1];
|
|
37
|
+
const pz = origin[2] + direction[2] * t - center[2];
|
|
38
|
+
const len = Math.hypot(px, py, pz) || 1;
|
|
39
|
+
return { distance: t, normal: [px / len, py / len, pz / len] };
|
|
40
|
+
}
|
|
41
|
+
function rayHitsCollider(origin, direction, maxDistance, collider, position, rotationY) {
|
|
42
|
+
if (collider.shape.kind === "sphere") {
|
|
43
|
+
const center = colliderWorldCenter(collider, position, rotationY);
|
|
44
|
+
const hit = rayHitsSphere(origin, direction, center, collider.shape.radius, maxDistance);
|
|
45
|
+
if (hit === null)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
distance: hit.distance,
|
|
49
|
+
normal: hit.normal,
|
|
50
|
+
point: [
|
|
51
|
+
origin[0] + direction[0] * hit.distance,
|
|
52
|
+
origin[1] + direction[1] * hit.distance,
|
|
53
|
+
origin[2] + direction[2] * hit.distance,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const bounds = colliderBounds(collider, position, rotationY);
|
|
58
|
+
const hit = intersectAabb(origin, direction, bounds.min, bounds.max, maxDistance);
|
|
59
|
+
if (hit === null)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
distance: hit.distance,
|
|
63
|
+
normal: hit.normal,
|
|
64
|
+
point: [
|
|
65
|
+
origin[0] + direction[0] * hit.distance,
|
|
66
|
+
origin[1] + direction[1] * hit.distance,
|
|
67
|
+
origin[2] + direction[2] * hit.distance,
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function pushColliderHits(hits, targetKind, instanceId, catalogId, colliders, position, rotationY, origin, direction, maxDistance) {
|
|
72
|
+
for (const collider of colliders) {
|
|
73
|
+
const hit = rayHitsCollider(origin, direction, maxDistance, collider, position, rotationY);
|
|
74
|
+
if (hit === null)
|
|
75
|
+
continue;
|
|
76
|
+
hits.push({
|
|
77
|
+
targetKind,
|
|
78
|
+
instanceId,
|
|
79
|
+
...(catalogId !== undefined ? { catalogId } : {}),
|
|
80
|
+
colliderName: collider.name,
|
|
81
|
+
purpose: collider.purpose,
|
|
82
|
+
damageEligible: collider.damageEligible,
|
|
83
|
+
blocks: collider.blocks,
|
|
84
|
+
distance: hit.distance,
|
|
85
|
+
point: hit.point,
|
|
86
|
+
normal: hit.normal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function wallAabb(wall) {
|
|
91
|
+
const thickness = wall.thickness ?? 0.25;
|
|
92
|
+
const halfHeight = wall.halfHeight ?? 1.5;
|
|
93
|
+
const yCenter = wall.yCenter ?? halfHeight;
|
|
94
|
+
const ax = wall.a[0];
|
|
95
|
+
const az = wall.a[1];
|
|
96
|
+
const bx = wall.b[0];
|
|
97
|
+
const bz = wall.b[1];
|
|
98
|
+
const dx = bx - ax;
|
|
99
|
+
const dz = bz - az;
|
|
100
|
+
const len = Math.hypot(dx, dz) || 1;
|
|
101
|
+
const nx = (-dz / len) * thickness;
|
|
102
|
+
const nz = (dx / len) * thickness;
|
|
103
|
+
const xs = [ax + nx, ax - nx, bx + nx, bx - nx];
|
|
104
|
+
const zs = [az + nz, az - nz, bz + nz, bz - nz];
|
|
105
|
+
const minX = Math.min(...xs);
|
|
106
|
+
const maxX = Math.max(...xs);
|
|
107
|
+
const minZ = Math.min(...zs);
|
|
108
|
+
const maxZ = Math.max(...zs);
|
|
109
|
+
return {
|
|
110
|
+
min: [minX, yCenter - halfHeight, minZ],
|
|
111
|
+
max: [maxX, yCenter + halfHeight, maxZ],
|
|
112
|
+
center: [(ax + bx) / 2, yCenter, (az + bz) / 2],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function rayHitsTerrain(origin, direction, maxDistance, terrain) {
|
|
116
|
+
const steps = Math.max(8, Math.ceil(maxDistance * 2));
|
|
117
|
+
const step = maxDistance / steps;
|
|
118
|
+
let prevAbove = null;
|
|
119
|
+
let prevT = 0;
|
|
120
|
+
for (let i = 0; i <= steps; i += 1) {
|
|
121
|
+
const t = Math.min(maxDistance, i * step);
|
|
122
|
+
const x = origin[0] + direction[0] * t;
|
|
123
|
+
const y = origin[1] + direction[1] * t;
|
|
124
|
+
const z = origin[2] + direction[2] * t;
|
|
125
|
+
const ground = terrain.sampleHeight(x, z);
|
|
126
|
+
const above = y >= ground;
|
|
127
|
+
if (prevAbove === true && !above) {
|
|
128
|
+
let lo = prevT;
|
|
129
|
+
let hi = t;
|
|
130
|
+
for (let refine = 0; refine < 8; refine += 1) {
|
|
131
|
+
const mid = (lo + hi) / 2;
|
|
132
|
+
const my = origin[1] + direction[1] * mid;
|
|
133
|
+
const mg = terrain.sampleHeight(origin[0] + direction[0] * mid, origin[2] + direction[2] * mid);
|
|
134
|
+
if (my >= mg)
|
|
135
|
+
lo = mid;
|
|
136
|
+
else
|
|
137
|
+
hi = mid;
|
|
138
|
+
}
|
|
139
|
+
const distance = hi;
|
|
140
|
+
const px = origin[0] + direction[0] * distance;
|
|
141
|
+
const py = origin[1] + direction[1] * distance;
|
|
142
|
+
const pz = origin[2] + direction[2] * distance;
|
|
143
|
+
return {
|
|
144
|
+
targetKind: "terrain",
|
|
145
|
+
instanceId: "terrain",
|
|
146
|
+
colliderName: "ground",
|
|
147
|
+
purpose: "physical",
|
|
148
|
+
damageEligible: false,
|
|
149
|
+
blocks: true,
|
|
150
|
+
distance,
|
|
151
|
+
point: [px, py, pz],
|
|
152
|
+
normal: [0, 1, 0],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
prevAbove = above;
|
|
156
|
+
prevT = t;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function candidateEntityIds(entities, origin, maxDistance) {
|
|
161
|
+
if (entities.inRadius !== undefined && entities.get !== undefined) {
|
|
162
|
+
const ids = entities.inRadius(origin, maxDistance + 2);
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const id of ids) {
|
|
165
|
+
const entity = entities.get(id);
|
|
166
|
+
if (entity !== null && entity !== undefined)
|
|
167
|
+
out.push(entity);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
return entities.list();
|
|
172
|
+
}
|
|
173
|
+
function candidateObjects(objects, origin, direction, maxDistance) {
|
|
174
|
+
if (objects.inBox !== undefined) {
|
|
175
|
+
const end = [
|
|
176
|
+
origin[0] + direction[0] * maxDistance,
|
|
177
|
+
origin[1] + direction[1] * maxDistance,
|
|
178
|
+
origin[2] + direction[2] * maxDistance,
|
|
179
|
+
];
|
|
180
|
+
const pad = 2;
|
|
181
|
+
const min = [
|
|
182
|
+
Math.min(origin[0], end[0]) - pad,
|
|
183
|
+
Math.min(origin[1], end[1]) - pad,
|
|
184
|
+
Math.min(origin[2], end[2]) - pad,
|
|
185
|
+
];
|
|
186
|
+
const max = [
|
|
187
|
+
Math.max(origin[0], end[0]) + pad,
|
|
188
|
+
Math.max(origin[1], end[1]) + pad,
|
|
189
|
+
Math.max(origin[2], end[2]) + pad,
|
|
190
|
+
];
|
|
191
|
+
return objects.inBox(min, max);
|
|
192
|
+
}
|
|
193
|
+
return objects.list();
|
|
194
|
+
}
|
|
195
|
+
function gatherHits(deps, input) {
|
|
196
|
+
const direction = normalizeDirection(input.direction);
|
|
197
|
+
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
198
|
+
return [];
|
|
199
|
+
const hits = [];
|
|
200
|
+
const { origin, maxDistance } = input;
|
|
201
|
+
if (enabled(input.filter, "entities") && deps.entities !== undefined) {
|
|
202
|
+
for (const entity of candidateEntityIds(deps.entities, origin, maxDistance)) {
|
|
203
|
+
if (excluded(input.excludeInstanceIds, entity.id))
|
|
204
|
+
continue;
|
|
205
|
+
const set = deps.entities.collidersOf?.(entity.id) ?? defaultEntityColliders();
|
|
206
|
+
const colliders = resolveColliders(set);
|
|
207
|
+
pushColliderHits(hits, "entity", entity.id, entity.name, colliders.length > 0 ? colliders : resolveColliders(defaultEntityColliders()), entity.position, entity.rotationY, origin, direction, maxDistance);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (enabled(input.filter, "objects") && deps.objects !== undefined) {
|
|
211
|
+
for (const object of candidateObjects(deps.objects, origin, direction, maxDistance)) {
|
|
212
|
+
if (excluded(input.excludeInstanceIds, object.instanceId))
|
|
213
|
+
continue;
|
|
214
|
+
const half = deps.objects.halfExtentsOf?.(object.catalogId) ?? undefined;
|
|
215
|
+
const set = deps.objects.collidersOf?.(object.instanceId) ??
|
|
216
|
+
defaultObjectColliders(half === null || half === undefined ? undefined : half);
|
|
217
|
+
const colliders = resolveColliders(set);
|
|
218
|
+
pushColliderHits(hits, "object", object.instanceId, object.catalogId, colliders, object.position, object.rotationY, origin, direction, maxDistance);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (enabled(input.filter, "walls") && deps.walls !== undefined) {
|
|
222
|
+
for (const wall of deps.walls) {
|
|
223
|
+
if (excluded(input.excludeInstanceIds, wall.id))
|
|
224
|
+
continue;
|
|
225
|
+
const box = wallAabb(wall);
|
|
226
|
+
const hit = intersectAabb(origin, direction, box.min, box.max, maxDistance);
|
|
227
|
+
if (hit === null)
|
|
228
|
+
continue;
|
|
229
|
+
hits.push({
|
|
230
|
+
targetKind: "wall",
|
|
231
|
+
instanceId: wall.id,
|
|
232
|
+
colliderName: "wall",
|
|
233
|
+
purpose: "physical",
|
|
234
|
+
damageEligible: false,
|
|
235
|
+
blocks: true,
|
|
236
|
+
distance: hit.distance,
|
|
237
|
+
point: [
|
|
238
|
+
origin[0] + direction[0] * hit.distance,
|
|
239
|
+
origin[1] + direction[1] * hit.distance,
|
|
240
|
+
origin[2] + direction[2] * hit.distance,
|
|
241
|
+
],
|
|
242
|
+
normal: hit.normal,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (enabled(input.filter, "terrain") && deps.terrain !== undefined) {
|
|
247
|
+
const terrainHit = rayHitsTerrain(origin, direction, maxDistance, deps.terrain);
|
|
248
|
+
if (terrainHit !== null)
|
|
249
|
+
hits.push(terrainHit);
|
|
250
|
+
}
|
|
251
|
+
hits.sort((a, b) => a.distance - b.distance || a.instanceId.localeCompare(b.instanceId) || a.colliderName.localeCompare(b.colliderName));
|
|
252
|
+
if (input.accept !== undefined)
|
|
253
|
+
return hits.filter(input.accept);
|
|
254
|
+
return hits;
|
|
255
|
+
}
|
|
256
|
+
/** First impact: nearest hit that blocks, or nearest hit if none block. */
|
|
257
|
+
export function firstImpact(hits) {
|
|
258
|
+
for (const hit of hits) {
|
|
259
|
+
if (hit.blocks)
|
|
260
|
+
return hit;
|
|
261
|
+
}
|
|
262
|
+
return hits[0] ?? null;
|
|
263
|
+
}
|
|
264
|
+
/** Hits up to and including the first blocking collider (damage hitboxes before a wall stay). */
|
|
265
|
+
export function hitsUntilBlocked(hits) {
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const hit of hits) {
|
|
268
|
+
out.push(hit);
|
|
269
|
+
if (hit.blocks)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
export function createSceneRaycast(deps) {
|
|
275
|
+
return {
|
|
276
|
+
raycast(input) {
|
|
277
|
+
const hits = gatherHits(deps, input);
|
|
278
|
+
return firstImpact(hits);
|
|
279
|
+
},
|
|
280
|
+
raycastAll(input) {
|
|
281
|
+
return gatherHits(deps, input);
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
package/dist/scene/spatial.d.ts
CHANGED
|
@@ -19,21 +19,24 @@ export interface MoveTowardOptions {
|
|
|
19
19
|
dt: number;
|
|
20
20
|
}
|
|
21
21
|
export interface SpatialGridOptions {
|
|
22
|
-
/** Cell edge over the x/z plane; tune to ≈ the typical query radius so a query touches a handful of cells. */
|
|
23
22
|
cellSize: number;
|
|
24
23
|
}
|
|
25
24
|
export interface SpatialApiOptions {
|
|
26
25
|
resolvePosition: (instanceId: string) => EntityPosition | undefined;
|
|
27
|
-
candidates: () => string[];
|
|
26
|
+
candidates: () => readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* When omitted, `hasLineOfSight` always returns true for known entities (open-field bypass).
|
|
29
|
+
* Wire a wall/collision test here for combat LoS; `true` means the segment is blocked.
|
|
30
|
+
*/
|
|
28
31
|
occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* frozen positions pay for one rebuild, not one linear scan each. Results are exactly the linear
|
|
34
|
-
* scan's results (same filtering; see `SpatialApi.invalidate` for the staleness contract).
|
|
33
|
+
* Broadphase for `inRadius`/`queryArc`. Defaults to `{ cellSize: 8 }`. Pass `false` to force a
|
|
34
|
+
* linear scan. When enabled, the index rebuilds lazily on first use after `invalidate()` or when
|
|
35
|
+
* `getVersion()` advances — call `invalidate()` after moves unless a version source is wired.
|
|
35
36
|
*/
|
|
36
|
-
grid?: SpatialGridOptions;
|
|
37
|
+
grid?: SpatialGridOptions | false;
|
|
38
|
+
/** When this number changes between queries, the grid rebuilds without an explicit `invalidate()`. */
|
|
39
|
+
getVersion?: () => number;
|
|
37
40
|
}
|
|
38
41
|
export interface SpatialApi {
|
|
39
42
|
distance(aInstanceId: string, bInstanceId: string): number | null;
|
|
@@ -41,14 +44,6 @@ export interface SpatialApi {
|
|
|
41
44
|
hasLineOfSight(fromInstanceId: string, toInstanceId: string): boolean;
|
|
42
45
|
queryArc(options: QueryArcOptions): string[];
|
|
43
46
|
moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null;
|
|
44
|
-
/**
|
|
45
|
-
* Marks the `grid` broadphase index stale so the next `inRadius`/`queryArc` call rebuilds it from
|
|
46
|
-
* `candidates()` and `resolvePosition()`. Call this after moving, spawning, or despawning a
|
|
47
|
-
* candidate; a candidate absent from the index at query time is never skipped (it falls back to
|
|
48
|
-
* an exact check), but a candidate that *moved* since the last rebuild can be missed until you
|
|
49
|
-
* invalidate — so treat "invalidate after any position change" as the contract, not an
|
|
50
|
-
* optimization. A no-op when no `grid` config was supplied. Safe to call every tick.
|
|
51
|
-
*/
|
|
52
47
|
invalidate(): void;
|
|
53
48
|
}
|
|
54
49
|
export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number;
|
package/dist/scene/spatial.js
CHANGED
|
@@ -17,58 +17,117 @@ function forwardXZ(aim) {
|
|
|
17
17
|
function cellCoord(value, cellSize) {
|
|
18
18
|
return Math.floor(value / cellSize);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const CELL_KEY_OFFSET = 0x8000;
|
|
21
|
+
function packCell(cx, cz) {
|
|
22
|
+
return ((cx + CELL_KEY_OFFSET) << 16) | ((cz + CELL_KEY_OFFSET) & 0xffff);
|
|
22
23
|
}
|
|
24
|
+
const DEFAULT_CELL_SIZE = 8;
|
|
23
25
|
export function createSpatialApi(options) {
|
|
24
|
-
const { resolvePosition, candidates, occluder,
|
|
25
|
-
const
|
|
26
|
+
const { resolvePosition, candidates, occluder, getVersion } = options;
|
|
27
|
+
const gridConfig = options.grid === false ? undefined : (options.grid ?? { cellSize: DEFAULT_CELL_SIZE });
|
|
28
|
+
const cellSize = gridConfig?.cellSize;
|
|
26
29
|
let gridIndex = null;
|
|
27
30
|
let gridDirty = true;
|
|
31
|
+
let lastVersion;
|
|
28
32
|
function resolveTarget(target) {
|
|
29
33
|
return typeof target === "string" ? resolvePosition(target) : target;
|
|
30
34
|
}
|
|
35
|
+
function takeBucket(index) {
|
|
36
|
+
if (index.poolCursor < index.bucketPool.length) {
|
|
37
|
+
const bucket = index.bucketPool[index.poolCursor];
|
|
38
|
+
index.poolCursor += 1;
|
|
39
|
+
bucket.length = 0;
|
|
40
|
+
return bucket;
|
|
41
|
+
}
|
|
42
|
+
const bucket = [];
|
|
43
|
+
index.bucketPool.push(bucket);
|
|
44
|
+
index.poolCursor += 1;
|
|
45
|
+
return bucket;
|
|
46
|
+
}
|
|
31
47
|
function ensureGrid() {
|
|
32
48
|
if (cellSize === undefined)
|
|
33
49
|
return null;
|
|
50
|
+
const version = getVersion?.();
|
|
51
|
+
if (version !== undefined && version !== lastVersion) {
|
|
52
|
+
gridDirty = true;
|
|
53
|
+
lastVersion = version;
|
|
54
|
+
}
|
|
34
55
|
if (!gridDirty && gridIndex !== null)
|
|
35
56
|
return gridIndex;
|
|
36
|
-
const
|
|
37
|
-
const
|
|
57
|
+
const previous = gridIndex;
|
|
58
|
+
const cells = previous?.cells ?? new Map();
|
|
59
|
+
cells.clear();
|
|
60
|
+
const indexed = previous?.indexed ?? new Set();
|
|
61
|
+
indexed.clear();
|
|
62
|
+
const index = {
|
|
63
|
+
cells,
|
|
64
|
+
indexed,
|
|
65
|
+
bucketPool: previous?.bucketPool ?? [],
|
|
66
|
+
poolCursor: 0,
|
|
67
|
+
};
|
|
38
68
|
for (const instanceId of candidates()) {
|
|
39
69
|
const position = resolvePosition(instanceId);
|
|
40
70
|
if (position === undefined)
|
|
41
71
|
continue;
|
|
42
72
|
indexed.add(instanceId);
|
|
43
|
-
const key =
|
|
73
|
+
const key = packCell(cellCoord(position[0], cellSize), cellCoord(position[2], cellSize));
|
|
44
74
|
let bucket = cells.get(key);
|
|
45
75
|
if (bucket === undefined) {
|
|
46
|
-
bucket =
|
|
76
|
+
bucket = takeBucket(index);
|
|
47
77
|
cells.set(key, bucket);
|
|
48
78
|
}
|
|
49
79
|
bucket.push(instanceId);
|
|
50
80
|
}
|
|
51
|
-
gridIndex =
|
|
81
|
+
gridIndex = index;
|
|
52
82
|
gridDirty = false;
|
|
53
|
-
return
|
|
83
|
+
return index;
|
|
54
84
|
}
|
|
55
|
-
function
|
|
85
|
+
function collectNear(index, centerX, centerZ, radius, out) {
|
|
56
86
|
const size = cellSize;
|
|
57
87
|
const cxMin = cellCoord(centerX - radius, size);
|
|
58
88
|
const cxMax = cellCoord(centerX + radius, size);
|
|
59
89
|
const czMin = cellCoord(centerZ - radius, size);
|
|
60
90
|
const czMax = cellCoord(centerZ + radius, size);
|
|
61
|
-
|
|
91
|
+
out.length = 0;
|
|
92
|
+
const seen = collectNearSeen;
|
|
93
|
+
seen.clear();
|
|
62
94
|
for (let cz = czMin; cz <= czMax; cz += 1) {
|
|
63
95
|
for (let cx = cxMin; cx <= cxMax; cx += 1) {
|
|
64
|
-
const bucket = index.cells.get(
|
|
96
|
+
const bucket = index.cells.get(packCell(cx, cz));
|
|
65
97
|
if (bucket === undefined)
|
|
66
98
|
continue;
|
|
67
|
-
for (const id of bucket)
|
|
68
|
-
|
|
99
|
+
for (const id of bucket) {
|
|
100
|
+
if (seen.has(id))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(id);
|
|
103
|
+
out.push(id);
|
|
104
|
+
}
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
|
-
|
|
107
|
+
}
|
|
108
|
+
const collectNearSeen = new Set();
|
|
109
|
+
const nearScratch = [];
|
|
110
|
+
const resultScratch = [];
|
|
111
|
+
function appendUnindexed(index, out) {
|
|
112
|
+
for (const instanceId of candidates()) {
|
|
113
|
+
if (index.indexed.has(instanceId))
|
|
114
|
+
continue;
|
|
115
|
+
out.push(instanceId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function filterInRadius(ids, centerId, centerPosition, radius, filter) {
|
|
119
|
+
resultScratch.length = 0;
|
|
120
|
+
for (const instanceId of ids) {
|
|
121
|
+
if (instanceId === centerId)
|
|
122
|
+
continue;
|
|
123
|
+
if (filter !== undefined && !filter(instanceId))
|
|
124
|
+
continue;
|
|
125
|
+
const position = resolvePosition(instanceId);
|
|
126
|
+
if (position !== undefined && distanceBetween(centerPosition, position) <= radius) {
|
|
127
|
+
resultScratch.push(instanceId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return resultScratch.slice();
|
|
72
131
|
}
|
|
73
132
|
return {
|
|
74
133
|
distance(aInstanceId, bInstanceId) {
|
|
@@ -84,17 +143,12 @@ export function createSpatialApi(options) {
|
|
|
84
143
|
if (centerPosition === undefined)
|
|
85
144
|
return [];
|
|
86
145
|
const index = ensureGrid();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
|
|
94
|
-
return false;
|
|
95
|
-
const position = resolvePosition(instanceId);
|
|
96
|
-
return position !== undefined && distanceBetween(centerPosition, position) <= radius;
|
|
97
|
-
});
|
|
146
|
+
if (index === null) {
|
|
147
|
+
return filterInRadius(candidates(), centerId, centerPosition, radius, filter);
|
|
148
|
+
}
|
|
149
|
+
collectNear(index, centerPosition[0], centerPosition[2], radius, nearScratch);
|
|
150
|
+
appendUnindexed(index, nearScratch);
|
|
151
|
+
return filterInRadius(nearScratch, centerId, centerPosition, radius, filter);
|
|
98
152
|
},
|
|
99
153
|
hasLineOfSight(fromInstanceId, toInstanceId) {
|
|
100
154
|
const from = resolvePosition(fromInstanceId);
|
|
@@ -114,25 +168,32 @@ export function createSpatialApi(options) {
|
|
|
114
168
|
return [];
|
|
115
169
|
const minDot = Math.cos((halfAngleDeg * Math.PI) / 180);
|
|
116
170
|
const index = ensureGrid();
|
|
117
|
-
const
|
|
118
|
-
|
|
171
|
+
const ids = index === null
|
|
172
|
+
? candidates()
|
|
173
|
+
: (collectNear(index, origin[0], origin[2], radius, nearScratch),
|
|
174
|
+
appendUnindexed(index, nearScratch),
|
|
175
|
+
nearScratch);
|
|
176
|
+
resultScratch.length = 0;
|
|
177
|
+
for (const instanceId of ids) {
|
|
119
178
|
if (instanceId === from)
|
|
120
|
-
|
|
121
|
-
if (near !== null && index.indexed.has(instanceId) && !near.has(instanceId))
|
|
122
|
-
return false;
|
|
179
|
+
continue;
|
|
123
180
|
const position = resolvePosition(instanceId);
|
|
124
181
|
if (position === undefined)
|
|
125
|
-
|
|
182
|
+
continue;
|
|
126
183
|
const dx = position[0] - origin[0];
|
|
127
184
|
const dz = position[2] - origin[2];
|
|
128
185
|
const planarDistance = Math.sqrt(dx * dx + dz * dz);
|
|
129
186
|
if (planarDistance > radius)
|
|
130
|
-
|
|
131
|
-
if (planarDistance === 0)
|
|
132
|
-
|
|
187
|
+
continue;
|
|
188
|
+
if (planarDistance === 0) {
|
|
189
|
+
resultScratch.push(instanceId);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
133
192
|
const dot = (forward[0] * dx + forward[1] * dz) / planarDistance;
|
|
134
|
-
|
|
135
|
-
|
|
193
|
+
if (dot >= minDot)
|
|
194
|
+
resultScratch.push(instanceId);
|
|
195
|
+
}
|
|
196
|
+
return resultScratch.slice();
|
|
136
197
|
},
|
|
137
198
|
invalidate() {
|
|
138
199
|
gridDirty = true;
|
|
@@ -9,10 +9,16 @@ export interface RecordingBufferOptions {
|
|
|
9
9
|
/** Hard cap on frame count, oldest evicted first. Default unlimited. */
|
|
10
10
|
maxFrames?: number;
|
|
11
11
|
}
|
|
12
|
+
export interface RecordingPair<T> {
|
|
13
|
+
before: RecordingFrame<T> | null;
|
|
14
|
+
after: RecordingFrame<T> | null;
|
|
15
|
+
}
|
|
12
16
|
export interface RecordingBuffer<T> {
|
|
13
17
|
append(t: number, data: T): void;
|
|
14
18
|
/** Nearest frame at or before `t`, or null if the buffer is empty or `t` precedes every frame. */
|
|
15
19
|
seek(t: number): RecordingFrame<T> | null;
|
|
20
|
+
/** The frames bracketing `t` for interpolated playback: `before` is `seek(t)`, `after` is the next recorded frame (null past the end). */
|
|
21
|
+
seekPair(t: number): RecordingPair<T>;
|
|
16
22
|
/** Frames with `fromT <= t <= toT`, in recorded order. */
|
|
17
23
|
range(fromT: number, toT: number): RecordingFrame<T>[];
|
|
18
24
|
clear(): void;
|
|
@@ -45,6 +45,13 @@ export function createRecordingBuffer(options = {}) {
|
|
|
45
45
|
const index = floorIndex(t);
|
|
46
46
|
return index === -1 ? null : frames[index];
|
|
47
47
|
},
|
|
48
|
+
seekPair(t) {
|
|
49
|
+
const index = floorIndex(t);
|
|
50
|
+
return {
|
|
51
|
+
before: index === -1 ? null : frames[index],
|
|
52
|
+
after: frames[index + 1] ?? null,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
48
55
|
range(fromT, toT) {
|
|
49
56
|
return frames.filter((frame) => frame.t >= fromT && frame.t <= toT);
|
|
50
57
|
},
|