@jgengine/core 0.7.0 → 0.8.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 +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +5 -0
- package/dist/game/race.js +20 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +16 -17
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -2,6 +2,38 @@ const DEFAULT_RANGE = 100;
|
|
|
2
2
|
const DEFAULT_PROJECTILE_SPEED = 15;
|
|
3
3
|
const GRAVITY = 9.8;
|
|
4
4
|
const BASE_HIT_RADIUS = 0.5;
|
|
5
|
+
const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
6
|
+
function isEntityHit(hit) {
|
|
7
|
+
return hit.kind === "entity";
|
|
8
|
+
}
|
|
9
|
+
function isObjectHit(hit) {
|
|
10
|
+
return hit.kind === "object";
|
|
11
|
+
}
|
|
12
|
+
function raySegmentAabbDistance(origin, direction, range, min, max) {
|
|
13
|
+
let tMin = 0;
|
|
14
|
+
let tMax = range;
|
|
15
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
16
|
+
const o = origin[axis];
|
|
17
|
+
const d = direction[axis];
|
|
18
|
+
const lo = min[axis];
|
|
19
|
+
const hi = max[axis];
|
|
20
|
+
if (Math.abs(d) < 1e-9) {
|
|
21
|
+
if (o < lo || o > hi)
|
|
22
|
+
return null;
|
|
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;
|
|
34
|
+
}
|
|
35
|
+
return tMin;
|
|
36
|
+
}
|
|
5
37
|
function normalize(vector) {
|
|
6
38
|
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
|
7
39
|
if (length === 0)
|
|
@@ -42,7 +74,7 @@ export function createProjectileSystem(deps) {
|
|
|
42
74
|
if (direction === null)
|
|
43
75
|
return [];
|
|
44
76
|
const spreadRad = (aimSpreadDeg(aim) * Math.PI) / 180;
|
|
45
|
-
const
|
|
77
|
+
const entityHits = [];
|
|
46
78
|
for (const instanceId of deps.spatial.inRadius(origin, range)) {
|
|
47
79
|
if (instanceId === from)
|
|
48
80
|
continue;
|
|
@@ -61,8 +93,42 @@ export function createProjectileSystem(deps) {
|
|
|
61
93
|
const perpendicular = Math.sqrt(px * px + py * py + pz * pz);
|
|
62
94
|
if (perpendicular > BASE_HIT_RADIUS + Math.tan(spreadRad) * along)
|
|
63
95
|
continue;
|
|
64
|
-
|
|
96
|
+
entityHits.push({ kind: "entity", instanceId, distance: along, at: position });
|
|
65
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
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const blockDistance = nearestObject?.distance ?? Infinity;
|
|
129
|
+
const hits = entityHits.filter((hit) => hit.distance <= blockDistance);
|
|
130
|
+
if (nearestObject !== null)
|
|
131
|
+
hits.push(nearestObject);
|
|
66
132
|
return hits.sort((a, b) => a.distance - b.distance);
|
|
67
133
|
});
|
|
68
134
|
function resolveRange(via) {
|
|
@@ -71,7 +137,7 @@ export function createProjectileSystem(deps) {
|
|
|
71
137
|
function isBallistic(via) {
|
|
72
138
|
return itemStat(via, "projectile.fuseTime") !== null || itemStat(via, "explosion.radius") !== null;
|
|
73
139
|
}
|
|
74
|
-
function
|
|
140
|
+
function ballisticArc(input) {
|
|
75
141
|
const origin = shotOrigin(input.from, input.aim) ?? [0, 0, 0];
|
|
76
142
|
const direction = aimDirection(input.aim) ?? [0, 0, 1];
|
|
77
143
|
const speed = itemStat(input.via, "projectile.speed") ?? DEFAULT_PROJECTILE_SPEED;
|
|
@@ -85,16 +151,32 @@ export function createProjectileSystem(deps) {
|
|
|
85
151
|
: flightCap;
|
|
86
152
|
const flightTime = Math.min(impactTime, flightCap);
|
|
87
153
|
const settledY = Math.max(0, origin[1] + verticalSpeed * flightTime - 0.5 * gravity * flightTime * flightTime);
|
|
88
|
-
return
|
|
89
|
-
origin
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
154
|
+
return {
|
|
155
|
+
origin,
|
|
156
|
+
velocity: [direction[0] * speed, verticalSpeed, direction[2] * speed],
|
|
157
|
+
gravity,
|
|
158
|
+
flightTime,
|
|
159
|
+
landing: [
|
|
160
|
+
origin[0] + direction[0] * speed * flightTime,
|
|
161
|
+
settledY,
|
|
162
|
+
origin[2] + direction[2] * speed * flightTime,
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function ballisticSettlePoint(input) {
|
|
167
|
+
const arc = ballisticArc(input);
|
|
168
|
+
const sweep = deps.sweepBallistic;
|
|
169
|
+
if (sweep !== undefined) {
|
|
170
|
+
const impact = sweep(arc.origin, arc.velocity, arc.gravity, arc.flightTime);
|
|
171
|
+
if (impact !== null)
|
|
172
|
+
return impact.point;
|
|
173
|
+
}
|
|
174
|
+
return arc.landing;
|
|
93
175
|
}
|
|
94
176
|
function predictHits(input) {
|
|
95
177
|
const aim = withWeaponSpread(input.via, input.aim);
|
|
96
178
|
const rawHits = raycast(input.from, aim, resolveRange(input.via));
|
|
97
|
-
const visible = rawHits.filter((hit) => deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
179
|
+
const visible = rawHits.filter((hit) => isObjectHit(hit) || deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
98
180
|
return { rawHits, visible };
|
|
99
181
|
}
|
|
100
182
|
function settledAt(input, hits) {
|
|
@@ -112,7 +194,9 @@ export function createProjectileSystem(deps) {
|
|
|
112
194
|
willHitProjectile(input) {
|
|
113
195
|
const { rawHits, visible } = predictHits(input);
|
|
114
196
|
const prediction = {
|
|
115
|
-
hits: visible.map((
|
|
197
|
+
hits: visible.map((hit) => isObjectHit(hit)
|
|
198
|
+
? { kind: "object", instanceId: hit.instanceId, catalogId: hit.catalogId, distance: hit.distance }
|
|
199
|
+
: { kind: "entity", instanceId: hit.instanceId, distance: hit.distance }),
|
|
116
200
|
};
|
|
117
201
|
if (rawHits.length > 0 && visible.length === 0)
|
|
118
202
|
prediction.blocked = true;
|
|
@@ -139,7 +223,9 @@ export function createProjectileSystem(deps) {
|
|
|
139
223
|
return { status: "settled", shotId, at, hits: [] };
|
|
140
224
|
}
|
|
141
225
|
const { visible } = predictHits(input);
|
|
142
|
-
const
|
|
226
|
+
const entityHits = visible.filter(isEntityHit);
|
|
227
|
+
const objectHits = visible.filter(isObjectHit);
|
|
228
|
+
const receivable = entityHits.filter((hit) => deps.effects.canReceive(hit.instanceId, input.effect) === null);
|
|
143
229
|
const pellets = Math.max(1, Math.round(itemStat(input.via, "pellets") ?? 1));
|
|
144
230
|
const hits = [];
|
|
145
231
|
if (receivable.length > 0) {
|
|
@@ -153,7 +239,7 @@ export function createProjectileSystem(deps) {
|
|
|
153
239
|
}));
|
|
154
240
|
}
|
|
155
241
|
}
|
|
156
|
-
const at = settledAt(input, receivable);
|
|
242
|
+
const at = settledAt(input, receivable.length > 0 ? receivable : objectHits);
|
|
157
243
|
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: receivable.length > 0 });
|
|
158
244
|
return { status: "settled", shotId, at, hits };
|
|
159
245
|
},
|
|
@@ -19,9 +19,13 @@ export interface TelegraphConfig {
|
|
|
19
19
|
shape: TelegraphShape;
|
|
20
20
|
at: EntityPosition;
|
|
21
21
|
dir?: number;
|
|
22
|
-
windupMs
|
|
22
|
+
windupMs?: number;
|
|
23
|
+
turns?: number;
|
|
23
24
|
kind?: string;
|
|
24
25
|
}
|
|
25
26
|
export declare function telegraphProgress(windupMs: number, startedAtMs: number, nowMs: number): number;
|
|
26
27
|
export declare function telegraphFired(windupMs: number, startedAtMs: number, nowMs: number): boolean;
|
|
28
|
+
export declare function telegraphTurnProgress(config: TelegraphConfig, startedTurn: number, currentTurn: number): number;
|
|
29
|
+
export declare function telegraphFiredAtTurn(config: TelegraphConfig, startedTurn: number, currentTurn: number): boolean;
|
|
30
|
+
export declare function telegraphTurnsRemaining(config: TelegraphConfig, startedTurn: number, currentTurn: number): number;
|
|
27
31
|
export declare function pointInTelegraph(config: TelegraphConfig, point: EntityPosition): boolean;
|
package/dist/combat/telegraph.js
CHANGED
|
@@ -6,6 +6,20 @@ export function telegraphProgress(windupMs, startedAtMs, nowMs) {
|
|
|
6
6
|
export function telegraphFired(windupMs, startedAtMs, nowMs) {
|
|
7
7
|
return nowMs - startedAtMs >= windupMs;
|
|
8
8
|
}
|
|
9
|
+
export function telegraphTurnProgress(config, startedTurn, currentTurn) {
|
|
10
|
+
const turns = config.turns ?? 0;
|
|
11
|
+
if (turns <= 0)
|
|
12
|
+
return 1;
|
|
13
|
+
return Math.max(0, Math.min(1, (currentTurn - startedTurn) / turns));
|
|
14
|
+
}
|
|
15
|
+
export function telegraphFiredAtTurn(config, startedTurn, currentTurn) {
|
|
16
|
+
const turns = config.turns ?? 0;
|
|
17
|
+
return currentTurn - startedTurn >= turns;
|
|
18
|
+
}
|
|
19
|
+
export function telegraphTurnsRemaining(config, startedTurn, currentTurn) {
|
|
20
|
+
const turns = config.turns ?? 0;
|
|
21
|
+
return Math.max(0, turns - (currentTurn - startedTurn));
|
|
22
|
+
}
|
|
9
23
|
function planarDelta(config, point) {
|
|
10
24
|
return { dx: point[0] - config.at[0], dz: point[2] - config.at[2] };
|
|
11
25
|
}
|
|
@@ -3,7 +3,7 @@ export interface CommandRejection {
|
|
|
3
3
|
}
|
|
4
4
|
export interface CommandDefinition<TState, TInput> {
|
|
5
5
|
validate?(state: TState, input: TInput): CommandRejection | null;
|
|
6
|
-
apply(state: TState, input: TInput): TState;
|
|
6
|
+
apply(state: TState, input: TInput): TState | void;
|
|
7
7
|
}
|
|
8
8
|
export type CommandResult<TState> = {
|
|
9
9
|
status: "applied";
|
|
@@ -20,7 +20,8 @@ export function createCommandRegistry() {
|
|
|
20
20
|
const rejection = definition.validate?.(state, input) ?? null;
|
|
21
21
|
if (rejection)
|
|
22
22
|
return { status: "rejected", reason: rejection.reason };
|
|
23
|
-
|
|
23
|
+
const next = definition.apply(state, input);
|
|
24
|
+
return { status: "applied", state: next === undefined ? state : next };
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type DataSourceStatus = "idle" | "loading" | "ready" | "error";
|
|
2
|
+
export interface DataSourceState<T> {
|
|
3
|
+
readonly status: DataSourceStatus;
|
|
4
|
+
readonly data: T | undefined;
|
|
5
|
+
readonly error: Error | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface DataSourceClock {
|
|
8
|
+
setInterval(handler: () => void, intervalMs: number): unknown;
|
|
9
|
+
clearInterval(handle: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
export interface DataSourceOptions {
|
|
12
|
+
intervalMs?: number;
|
|
13
|
+
clock?: DataSourceClock;
|
|
14
|
+
}
|
|
15
|
+
export interface RefreshOptions {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DataSource<T> {
|
|
19
|
+
getState(): DataSourceState<T>;
|
|
20
|
+
subscribe(listener: (state: DataSourceState<T>) => void): () => void;
|
|
21
|
+
refresh(options?: RefreshOptions): Promise<void>;
|
|
22
|
+
startPolling(intervalMs?: number): void;
|
|
23
|
+
stopPolling(): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createDataSource<T>(load: (signal: AbortSignal) => Promise<T>, options?: DataSourceOptions): DataSource<T>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const defaultClock = {
|
|
2
|
+
setInterval: (handler, intervalMs) => globalThis.setInterval(handler, intervalMs),
|
|
3
|
+
clearInterval: (handle) => globalThis.clearInterval(handle),
|
|
4
|
+
};
|
|
5
|
+
export function createDataSource(load, options = {}) {
|
|
6
|
+
const clock = options.clock ?? defaultClock;
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
let state = { status: "idle", data: undefined, error: undefined };
|
|
9
|
+
let controller;
|
|
10
|
+
let inFlight;
|
|
11
|
+
let pollHandle;
|
|
12
|
+
let disposed = false;
|
|
13
|
+
function setState(next) {
|
|
14
|
+
state = next;
|
|
15
|
+
for (const listener of listeners)
|
|
16
|
+
listener(state);
|
|
17
|
+
}
|
|
18
|
+
async function run() {
|
|
19
|
+
const ownController = new AbortController();
|
|
20
|
+
controller = ownController;
|
|
21
|
+
setState({ status: "loading", data: state.data, error: undefined });
|
|
22
|
+
try {
|
|
23
|
+
const data = await load(ownController.signal);
|
|
24
|
+
if (ownController.signal.aborted)
|
|
25
|
+
return;
|
|
26
|
+
setState({ status: "ready", data, error: undefined });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (ownController.signal.aborted)
|
|
30
|
+
return;
|
|
31
|
+
setState({
|
|
32
|
+
status: "error",
|
|
33
|
+
data: state.data,
|
|
34
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (controller === ownController)
|
|
39
|
+
controller = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function refresh(refreshOptions = {}) {
|
|
43
|
+
if (disposed)
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
if (inFlight !== undefined) {
|
|
46
|
+
if (!refreshOptions.force)
|
|
47
|
+
return inFlight;
|
|
48
|
+
controller?.abort();
|
|
49
|
+
}
|
|
50
|
+
const promise = run().finally(() => {
|
|
51
|
+
if (inFlight === promise)
|
|
52
|
+
inFlight = undefined;
|
|
53
|
+
});
|
|
54
|
+
inFlight = promise;
|
|
55
|
+
return promise;
|
|
56
|
+
}
|
|
57
|
+
function stopPolling() {
|
|
58
|
+
if (pollHandle !== undefined) {
|
|
59
|
+
clock.clearInterval(pollHandle);
|
|
60
|
+
pollHandle = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function startPolling(intervalMs) {
|
|
64
|
+
stopPolling();
|
|
65
|
+
const resolvedInterval = intervalMs ?? options.intervalMs;
|
|
66
|
+
if (resolvedInterval === undefined) {
|
|
67
|
+
throw new Error("startPolling requires an intervalMs (pass one, or set options.intervalMs at creation)");
|
|
68
|
+
}
|
|
69
|
+
void refresh();
|
|
70
|
+
pollHandle = clock.setInterval(() => {
|
|
71
|
+
void refresh();
|
|
72
|
+
}, resolvedInterval);
|
|
73
|
+
}
|
|
74
|
+
function dispose() {
|
|
75
|
+
disposed = true;
|
|
76
|
+
stopPolling();
|
|
77
|
+
controller?.abort();
|
|
78
|
+
listeners.clear();
|
|
79
|
+
}
|
|
80
|
+
if (options.intervalMs !== undefined)
|
|
81
|
+
startPolling(options.intervalMs);
|
|
82
|
+
return {
|
|
83
|
+
getState: () => state,
|
|
84
|
+
subscribe(listener) {
|
|
85
|
+
listeners.add(listener);
|
|
86
|
+
return () => listeners.delete(listener);
|
|
87
|
+
},
|
|
88
|
+
refresh,
|
|
89
|
+
startPolling,
|
|
90
|
+
stopPolling,
|
|
91
|
+
dispose,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DevProxyTable {
|
|
2
|
+
readonly [routeName: string]: string;
|
|
3
|
+
}
|
|
4
|
+
export interface ProxiedUrlOptions {
|
|
5
|
+
dev?: boolean;
|
|
6
|
+
table?: DevProxyTable;
|
|
7
|
+
prefix?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseDevProxyTable(raw: string | undefined): DevProxyTable;
|
|
10
|
+
export declare const DEFAULT_DEV_PROXY_PREFIX = "/proxy";
|
|
11
|
+
export declare function proxiedUrl(target: string, options?: ProxiedUrlOptions): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function readImportMetaEnv() {
|
|
2
|
+
try {
|
|
3
|
+
const meta = import.meta;
|
|
4
|
+
return meta.env ?? {};
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function parseDevProxyTable(raw) {
|
|
11
|
+
if (raw === undefined || raw.length === 0)
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
16
|
+
return {};
|
|
17
|
+
const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string");
|
|
18
|
+
return Object.fromEntries(entries);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const DEFAULT_DEV_PROXY_PREFIX = "/proxy";
|
|
25
|
+
export function proxiedUrl(target, options = {}) {
|
|
26
|
+
const env = readImportMetaEnv();
|
|
27
|
+
const dev = options.dev ?? env.DEV === true;
|
|
28
|
+
if (!dev)
|
|
29
|
+
return target;
|
|
30
|
+
const table = options.table ?? parseDevProxyTable(env.VITE_JGENGINE_DEV_PROXY);
|
|
31
|
+
const prefix = options.prefix ?? DEFAULT_DEV_PROXY_PREFIX;
|
|
32
|
+
for (const [routeName, base] of Object.entries(table)) {
|
|
33
|
+
if (target.startsWith(base)) {
|
|
34
|
+
return `${prefix}/${routeName}${target.slice(base.length)}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FetchImpl = typeof fetch;
|
|
2
|
+
export interface FetchJsonOptions {
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
fetchImpl?: FetchImpl;
|
|
8
|
+
}
|
|
9
|
+
export declare class HttpStatusError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly statusText: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
constructor(status: number, statusText: string, url: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class JsonParseError extends Error {
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly cause: unknown;
|
|
18
|
+
constructor(url: string, cause: unknown);
|
|
19
|
+
}
|
|
20
|
+
export declare function fetchJson<T>(url: string, options?: FetchJsonOptions): Promise<T>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class HttpStatusError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
statusText;
|
|
4
|
+
url;
|
|
5
|
+
constructor(status, statusText, url) {
|
|
6
|
+
super(`HTTP ${status} ${statusText} for ${url}`);
|
|
7
|
+
this.name = "HttpStatusError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.statusText = statusText;
|
|
10
|
+
this.url = url;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class JsonParseError extends Error {
|
|
14
|
+
url;
|
|
15
|
+
cause;
|
|
16
|
+
constructor(url, cause) {
|
|
17
|
+
super(`failed to parse JSON response from ${url}`);
|
|
18
|
+
this.name = "JsonParseError";
|
|
19
|
+
this.url = url;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function fetchJson(url, options = {}) {
|
|
24
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
25
|
+
const response = await fetchImpl(url, {
|
|
26
|
+
method: options.method,
|
|
27
|
+
headers: options.headers,
|
|
28
|
+
body: options.body,
|
|
29
|
+
signal: options.signal,
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new HttpStatusError(response.status, response.statusText, url);
|
|
33
|
+
}
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
if (text.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch (cause) {
|
|
41
|
+
throw new JsonParseError(url, cause);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type DataSource, type DataSourceOptions } from "./dataSource.js";
|
|
2
|
+
import { type FetchJsonOptions } from "./fetchJson.js";
|
|
3
|
+
export type JsonDataSourceOptions = DataSourceOptions & Omit<FetchJsonOptions, "signal">;
|
|
4
|
+
export declare function createJsonDataSource<T>(url: string, options?: JsonDataSourceOptions): DataSource<T>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createDataSource } from "./dataSource.js";
|
|
2
|
+
import { fetchJson } from "./fetchJson.js";
|
|
3
|
+
export function createJsonDataSource(url, options = {}) {
|
|
4
|
+
const { intervalMs, clock, ...fetchOptions } = options;
|
|
5
|
+
return createDataSource((signal) => fetchJson(url, { ...fetchOptions, signal }), { intervalMs, clock });
|
|
6
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ChangeSignal } from "../store/changeSignal.js";
|
|
2
|
+
export type DevtoolsControlKind = "slider" | "toggle" | "color" | "select" | "text";
|
|
3
|
+
export interface TunableOptions<T> {
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
options?: readonly T[];
|
|
8
|
+
label?: string;
|
|
9
|
+
group?: string;
|
|
10
|
+
onChange?: (value: T) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface DevtoolsControl {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly kind: DevtoolsControlKind;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly group: string;
|
|
17
|
+
readonly initial: unknown;
|
|
18
|
+
readonly min?: number;
|
|
19
|
+
readonly max?: number;
|
|
20
|
+
readonly step?: number;
|
|
21
|
+
readonly options?: readonly unknown[];
|
|
22
|
+
read(): unknown;
|
|
23
|
+
write(value: unknown): void;
|
|
24
|
+
reset(): void;
|
|
25
|
+
}
|
|
26
|
+
export interface Tunable<T> {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly kind: DevtoolsControlKind;
|
|
29
|
+
readonly initial: T;
|
|
30
|
+
readonly value: T;
|
|
31
|
+
set(value: T): void;
|
|
32
|
+
reset(): void;
|
|
33
|
+
subscribe(listener: (value: T) => void): () => void;
|
|
34
|
+
}
|
|
35
|
+
export interface FrameStats {
|
|
36
|
+
fps: number;
|
|
37
|
+
avgFrameMs: number;
|
|
38
|
+
p95FrameMs: number;
|
|
39
|
+
maxFrameMs: number;
|
|
40
|
+
avgSimMs: number;
|
|
41
|
+
maxSimMs: number;
|
|
42
|
+
longFrames: number;
|
|
43
|
+
samples: number;
|
|
44
|
+
recentFrameMs: readonly number[];
|
|
45
|
+
}
|
|
46
|
+
export interface RenderSample {
|
|
47
|
+
drawCalls: number;
|
|
48
|
+
triangles: number;
|
|
49
|
+
geometries: number;
|
|
50
|
+
textures: number;
|
|
51
|
+
}
|
|
52
|
+
export interface LatencyStats {
|
|
53
|
+
lastMs: number;
|
|
54
|
+
avgMs: number;
|
|
55
|
+
minMs: number;
|
|
56
|
+
maxMs: number;
|
|
57
|
+
samples: number;
|
|
58
|
+
}
|
|
59
|
+
export type DevtoolsLogLevel = "log" | "info" | "warn" | "error";
|
|
60
|
+
export interface DevtoolsLogEntry {
|
|
61
|
+
at: number;
|
|
62
|
+
level: DevtoolsLogLevel;
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
export interface DiscoveredEntry {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly table: string;
|
|
68
|
+
readonly key: string;
|
|
69
|
+
readonly kind: DevtoolsControlKind;
|
|
70
|
+
readonly initial: unknown;
|
|
71
|
+
readonly enabled: boolean;
|
|
72
|
+
read(): unknown;
|
|
73
|
+
}
|
|
74
|
+
export interface TunableAccessor {
|
|
75
|
+
initial: unknown;
|
|
76
|
+
get(): unknown;
|
|
77
|
+
set(value: unknown): void;
|
|
78
|
+
}
|
|
79
|
+
export interface DevtoolsOverrides {
|
|
80
|
+
enabled: string[];
|
|
81
|
+
values: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
export interface DevtoolsSnapshot {
|
|
84
|
+
at: number;
|
|
85
|
+
frame: FrameStats | null;
|
|
86
|
+
render: RenderSample | null;
|
|
87
|
+
latency: LatencyStats | null;
|
|
88
|
+
logs: readonly DevtoolsLogEntry[];
|
|
89
|
+
probes: Record<string, unknown>;
|
|
90
|
+
controls: readonly {
|
|
91
|
+
name: string;
|
|
92
|
+
kind: DevtoolsControlKind;
|
|
93
|
+
value: unknown;
|
|
94
|
+
initial: unknown;
|
|
95
|
+
}[];
|
|
96
|
+
discovered: readonly {
|
|
97
|
+
id: string;
|
|
98
|
+
kind: DevtoolsControlKind;
|
|
99
|
+
value: unknown;
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
export interface Devtools {
|
|
104
|
+
frame: {
|
|
105
|
+
record(sample: {
|
|
106
|
+
frameMs: number;
|
|
107
|
+
simMs: number;
|
|
108
|
+
}): void;
|
|
109
|
+
stats(): FrameStats | null;
|
|
110
|
+
};
|
|
111
|
+
render: {
|
|
112
|
+
record(sample: RenderSample): void;
|
|
113
|
+
latest(): RenderSample | null;
|
|
114
|
+
};
|
|
115
|
+
logs: {
|
|
116
|
+
push(level: DevtoolsLogLevel, message: string): void;
|
|
117
|
+
captureConsole(): void;
|
|
118
|
+
list(): readonly DevtoolsLogEntry[];
|
|
119
|
+
clear(): void;
|
|
120
|
+
};
|
|
121
|
+
latency: {
|
|
122
|
+
record(ms: number): void;
|
|
123
|
+
stats(): LatencyStats | null;
|
|
124
|
+
};
|
|
125
|
+
controls: {
|
|
126
|
+
register<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T>;
|
|
127
|
+
list(): readonly DevtoolsControl[];
|
|
128
|
+
get(name: string): DevtoolsControl | null;
|
|
129
|
+
remove(name: string): void;
|
|
130
|
+
resetAll(): void;
|
|
131
|
+
};
|
|
132
|
+
discover: {
|
|
133
|
+
bind(table: string, key: string, accessor: TunableAccessor): void;
|
|
134
|
+
scanTable(table: string, candidate: unknown): number;
|
|
135
|
+
scanModule(exports: Record<string, unknown>): number;
|
|
136
|
+
list(): readonly DiscoveredEntry[];
|
|
137
|
+
enable(id: string): void;
|
|
138
|
+
disable(id: string): void;
|
|
139
|
+
clear(): void;
|
|
140
|
+
};
|
|
141
|
+
overrides: {
|
|
142
|
+
export(): DevtoolsOverrides;
|
|
143
|
+
apply(overrides: DevtoolsOverrides): void;
|
|
144
|
+
};
|
|
145
|
+
probes: {
|
|
146
|
+
register(name: string, read: () => unknown): () => void;
|
|
147
|
+
read(): Record<string, unknown>;
|
|
148
|
+
};
|
|
149
|
+
signal: ChangeSignal;
|
|
150
|
+
snapshot(): DevtoolsSnapshot;
|
|
151
|
+
}
|
|
152
|
+
export declare function formatLogMessage(args: readonly unknown[]): string;
|
|
153
|
+
export declare function createDevtools(): Devtools;
|
|
154
|
+
export declare const devtools: Devtools;
|
|
155
|
+
export declare function tunable<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T>;
|
|
156
|
+
export declare function snapshotDevtools(): DevtoolsSnapshot;
|
|
157
|
+
export declare function instrumentLatency<T extends object>(target: T, methods: readonly (keyof T)[], record?: (ms: number) => void): T;
|