@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
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { createChangeSignal } from "../store/changeSignal.js";
|
|
2
|
+
const FRAME_CAPACITY = 240;
|
|
3
|
+
const RECENT_FRAMES = 60;
|
|
4
|
+
const LONG_FRAME_MS = 33.4;
|
|
5
|
+
const LATENCY_CAPACITY = 120;
|
|
6
|
+
const LOG_CAPACITY = 200;
|
|
7
|
+
const LOG_MESSAGE_MAX = 400;
|
|
8
|
+
const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
9
|
+
function nowMs() {
|
|
10
|
+
const perf = globalThis.performance;
|
|
11
|
+
return perf !== undefined ? perf.now() : Date.now();
|
|
12
|
+
}
|
|
13
|
+
function inferKind(initial, hasOptions) {
|
|
14
|
+
if (hasOptions)
|
|
15
|
+
return "select";
|
|
16
|
+
if (typeof initial === "number")
|
|
17
|
+
return "slider";
|
|
18
|
+
if (typeof initial === "boolean")
|
|
19
|
+
return "toggle";
|
|
20
|
+
if (typeof initial === "string" && COLOR_PATTERN.test(initial))
|
|
21
|
+
return "color";
|
|
22
|
+
return "text";
|
|
23
|
+
}
|
|
24
|
+
function sliderBounds(initial, options) {
|
|
25
|
+
const min = options?.min ?? (initial < 0 ? initial * 2 : 0);
|
|
26
|
+
const max = options?.max ?? (initial > 0 ? initial * 2 : initial < 0 ? 0 : 1);
|
|
27
|
+
const step = options?.step ?? (max - min) / 100;
|
|
28
|
+
return { min, max, step };
|
|
29
|
+
}
|
|
30
|
+
function formatLogArg(arg) {
|
|
31
|
+
if (typeof arg === "string")
|
|
32
|
+
return arg;
|
|
33
|
+
if (arg instanceof Error)
|
|
34
|
+
return `${arg.name}: ${arg.message}`;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(arg) ?? String(arg);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return String(arg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function formatLogMessage(args) {
|
|
43
|
+
const message = args.map(formatLogArg).join(" ");
|
|
44
|
+
return message.length > LOG_MESSAGE_MAX ? `${message.slice(0, LOG_MESSAGE_MAX)}…` : message;
|
|
45
|
+
}
|
|
46
|
+
function percentile(sorted, fraction) {
|
|
47
|
+
if (sorted.length === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction));
|
|
50
|
+
return sorted[index];
|
|
51
|
+
}
|
|
52
|
+
const MAX_TABLE_ENTRIES = 64;
|
|
53
|
+
function discoverableKind(value) {
|
|
54
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
55
|
+
return "slider";
|
|
56
|
+
if (typeof value === "boolean")
|
|
57
|
+
return "toggle";
|
|
58
|
+
if (typeof value === "string" && COLOR_PATTERN.test(value))
|
|
59
|
+
return "color";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function discoverableEntries(candidate) {
|
|
63
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
64
|
+
return [];
|
|
65
|
+
const proto = Object.getPrototypeOf(candidate);
|
|
66
|
+
if (proto !== Object.prototype && proto !== null)
|
|
67
|
+
return [];
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
70
|
+
const kind = discoverableKind(value);
|
|
71
|
+
if (kind !== null)
|
|
72
|
+
entries.push([key, kind]);
|
|
73
|
+
if (entries.length > MAX_TABLE_ENTRIES)
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
export function createDevtools() {
|
|
79
|
+
const signal = createChangeSignal();
|
|
80
|
+
const frameMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
81
|
+
const simMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
82
|
+
let frameIndex = 0;
|
|
83
|
+
let frameCount = 0;
|
|
84
|
+
let renderSample = null;
|
|
85
|
+
const logEntries = [];
|
|
86
|
+
let consoleCaptured = false;
|
|
87
|
+
const latencySamples = [];
|
|
88
|
+
const controlRecords = new Map();
|
|
89
|
+
const discoveredRecords = new Map();
|
|
90
|
+
const probeReaders = new Map();
|
|
91
|
+
const readProbes = () => {
|
|
92
|
+
const values = {};
|
|
93
|
+
for (const [name, read] of probeReaders) {
|
|
94
|
+
try {
|
|
95
|
+
values[name] = read();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
values[name] = `probe failed: ${formatLogArg(error)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
};
|
|
103
|
+
const pushLog = (level, message) => {
|
|
104
|
+
logEntries.push({ at: Date.now(), level, message });
|
|
105
|
+
if (logEntries.length > LOG_CAPACITY)
|
|
106
|
+
logEntries.splice(0, logEntries.length - LOG_CAPACITY);
|
|
107
|
+
};
|
|
108
|
+
const frameStats = () => {
|
|
109
|
+
if (frameCount === 0)
|
|
110
|
+
return null;
|
|
111
|
+
const size = Math.min(frameCount, FRAME_CAPACITY);
|
|
112
|
+
const frames = [];
|
|
113
|
+
const sims = [];
|
|
114
|
+
for (let offset = 0; offset < size; offset += 1) {
|
|
115
|
+
const at = (frameIndex - size + offset + FRAME_CAPACITY) % FRAME_CAPACITY;
|
|
116
|
+
frames.push(frameMsBuffer[at]);
|
|
117
|
+
sims.push(simMsBuffer[at]);
|
|
118
|
+
}
|
|
119
|
+
const recent = frames.slice(-RECENT_FRAMES);
|
|
120
|
+
const recentAvg = recent.reduce((sum, ms) => sum + ms, 0) / recent.length;
|
|
121
|
+
const sorted = [...frames].sort((a, b) => a - b);
|
|
122
|
+
return {
|
|
123
|
+
fps: recentAvg > 0 ? 1000 / recentAvg : 0,
|
|
124
|
+
avgFrameMs: frames.reduce((sum, ms) => sum + ms, 0) / frames.length,
|
|
125
|
+
p95FrameMs: percentile(sorted, 0.95),
|
|
126
|
+
maxFrameMs: sorted[sorted.length - 1],
|
|
127
|
+
avgSimMs: sims.reduce((sum, ms) => sum + ms, 0) / sims.length,
|
|
128
|
+
maxSimMs: sims.reduce((max, ms) => Math.max(max, ms), 0),
|
|
129
|
+
longFrames: frames.filter((ms) => ms > LONG_FRAME_MS).length,
|
|
130
|
+
samples: size,
|
|
131
|
+
recentFrameMs: recent,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const latencyStats = () => {
|
|
135
|
+
if (latencySamples.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
let min = Number.POSITIVE_INFINITY;
|
|
138
|
+
let max = 0;
|
|
139
|
+
let sum = 0;
|
|
140
|
+
for (const ms of latencySamples) {
|
|
141
|
+
min = Math.min(min, ms);
|
|
142
|
+
max = Math.max(max, ms);
|
|
143
|
+
sum += ms;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
lastMs: latencySamples[latencySamples.length - 1],
|
|
147
|
+
avgMs: sum / latencySamples.length,
|
|
148
|
+
minMs: min,
|
|
149
|
+
maxMs: max,
|
|
150
|
+
samples: latencySamples.length,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const toControl = (record) => ({
|
|
154
|
+
name: record.name,
|
|
155
|
+
kind: record.kind,
|
|
156
|
+
label: record.label,
|
|
157
|
+
group: record.group,
|
|
158
|
+
initial: record.initial,
|
|
159
|
+
min: record.min,
|
|
160
|
+
max: record.max,
|
|
161
|
+
step: record.step,
|
|
162
|
+
options: record.options,
|
|
163
|
+
read: () => record.value,
|
|
164
|
+
write: (value) => writeControl(record, value),
|
|
165
|
+
reset: () => writeControl(record, record.initial),
|
|
166
|
+
});
|
|
167
|
+
const writeControl = (record, raw) => {
|
|
168
|
+
let next = raw;
|
|
169
|
+
if (record.kind === "slider" && typeof next === "number") {
|
|
170
|
+
if (record.min !== undefined)
|
|
171
|
+
next = Math.max(record.min, next);
|
|
172
|
+
if (record.max !== undefined)
|
|
173
|
+
next = Math.min(record.max, next);
|
|
174
|
+
}
|
|
175
|
+
if (Object.is(record.value, next))
|
|
176
|
+
return;
|
|
177
|
+
record.value = next;
|
|
178
|
+
for (const listener of record.listeners)
|
|
179
|
+
listener(next);
|
|
180
|
+
signal.notify();
|
|
181
|
+
};
|
|
182
|
+
const register = (name, initial, options) => {
|
|
183
|
+
const kind = inferKind(initial, options?.options !== undefined);
|
|
184
|
+
const existing = controlRecords.get(name);
|
|
185
|
+
const slashIndex = name.indexOf("/");
|
|
186
|
+
const derivedGroup = slashIndex > 0 ? name.slice(0, slashIndex) : "general";
|
|
187
|
+
const derivedLabel = slashIndex > 0 ? name.slice(slashIndex + 1) : name;
|
|
188
|
+
const bounds = kind === "slider" ? sliderBounds(initial, options) : undefined;
|
|
189
|
+
const record = existing !== undefined && existing.kind === kind
|
|
190
|
+
? existing
|
|
191
|
+
: {
|
|
192
|
+
name,
|
|
193
|
+
kind,
|
|
194
|
+
label: "",
|
|
195
|
+
group: "",
|
|
196
|
+
initial,
|
|
197
|
+
value: initial,
|
|
198
|
+
listeners: new Set(),
|
|
199
|
+
};
|
|
200
|
+
record.label = options?.label ?? derivedLabel;
|
|
201
|
+
record.group = options?.group ?? derivedGroup;
|
|
202
|
+
record.initial = initial;
|
|
203
|
+
record.min = bounds?.min;
|
|
204
|
+
record.max = bounds?.max;
|
|
205
|
+
record.step = bounds?.step;
|
|
206
|
+
record.options = options?.options;
|
|
207
|
+
if (options?.onChange !== undefined)
|
|
208
|
+
record.listeners.add(options.onChange);
|
|
209
|
+
controlRecords.set(name, record);
|
|
210
|
+
signal.notify();
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
kind,
|
|
214
|
+
initial,
|
|
215
|
+
get value() {
|
|
216
|
+
return record.value;
|
|
217
|
+
},
|
|
218
|
+
set: (value) => writeControl(record, value),
|
|
219
|
+
reset: () => writeControl(record, record.initial),
|
|
220
|
+
subscribe: (listener) => {
|
|
221
|
+
record.listeners.add(listener);
|
|
222
|
+
return () => record.listeners.delete(listener);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const bindDiscovered = (table, key, kind, accessor, source) => {
|
|
227
|
+
const id = `${table}/${key}`;
|
|
228
|
+
const existing = discoveredRecords.get(id);
|
|
229
|
+
if (existing === undefined) {
|
|
230
|
+
discoveredRecords.set(id, {
|
|
231
|
+
id,
|
|
232
|
+
table,
|
|
233
|
+
key,
|
|
234
|
+
kind,
|
|
235
|
+
initial: accessor.initial,
|
|
236
|
+
enabled: false,
|
|
237
|
+
source,
|
|
238
|
+
get: accessor.get,
|
|
239
|
+
set: accessor.set,
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (source !== null && existing.source === source)
|
|
244
|
+
return false;
|
|
245
|
+
existing.source = source;
|
|
246
|
+
existing.get = accessor.get;
|
|
247
|
+
existing.set = accessor.set;
|
|
248
|
+
const control = controlRecords.get(id);
|
|
249
|
+
if (existing.enabled && control !== undefined)
|
|
250
|
+
accessor.set(control.value);
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
const scanTable = (table, candidate) => {
|
|
254
|
+
const entries = discoverableEntries(candidate);
|
|
255
|
+
if (entries.length === 0)
|
|
256
|
+
return 0;
|
|
257
|
+
const target = candidate;
|
|
258
|
+
let changed = false;
|
|
259
|
+
for (const [key, kind] of entries) {
|
|
260
|
+
const accessor = {
|
|
261
|
+
initial: target[key],
|
|
262
|
+
get: () => target[key],
|
|
263
|
+
set: (value) => {
|
|
264
|
+
target[key] = value;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
if (bindDiscovered(table, key, kind, accessor, target))
|
|
268
|
+
changed = true;
|
|
269
|
+
}
|
|
270
|
+
if (changed)
|
|
271
|
+
signal.notify();
|
|
272
|
+
return entries.length;
|
|
273
|
+
};
|
|
274
|
+
const enableDiscovered = (id) => {
|
|
275
|
+
const record = discoveredRecords.get(id);
|
|
276
|
+
if (record === undefined || record.enabled)
|
|
277
|
+
return;
|
|
278
|
+
record.enabled = true;
|
|
279
|
+
const current = record.get();
|
|
280
|
+
const handle = register(id, record.initial, { group: record.table, label: record.key });
|
|
281
|
+
handle.subscribe((value) => record.set(value));
|
|
282
|
+
if (!Object.is(current, record.initial))
|
|
283
|
+
handle.set(current);
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
frame: {
|
|
287
|
+
record({ frameMs, simMs }) {
|
|
288
|
+
frameMsBuffer[frameIndex] = frameMs;
|
|
289
|
+
simMsBuffer[frameIndex] = simMs;
|
|
290
|
+
frameIndex = (frameIndex + 1) % FRAME_CAPACITY;
|
|
291
|
+
frameCount += 1;
|
|
292
|
+
},
|
|
293
|
+
stats: frameStats,
|
|
294
|
+
},
|
|
295
|
+
render: {
|
|
296
|
+
record(sample) {
|
|
297
|
+
renderSample = sample;
|
|
298
|
+
},
|
|
299
|
+
latest: () => renderSample,
|
|
300
|
+
},
|
|
301
|
+
logs: {
|
|
302
|
+
push: pushLog,
|
|
303
|
+
captureConsole() {
|
|
304
|
+
if (consoleCaptured)
|
|
305
|
+
return;
|
|
306
|
+
const globalConsole = globalThis.console;
|
|
307
|
+
if (globalConsole === undefined)
|
|
308
|
+
return;
|
|
309
|
+
consoleCaptured = true;
|
|
310
|
+
const levels = ["log", "info", "warn", "error"];
|
|
311
|
+
for (const level of levels) {
|
|
312
|
+
const original = globalConsole[level].bind(globalConsole);
|
|
313
|
+
globalConsole[level] = (...args) => {
|
|
314
|
+
pushLog(level, formatLogMessage(args));
|
|
315
|
+
original(...args);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
list: () => logEntries,
|
|
320
|
+
clear() {
|
|
321
|
+
logEntries.length = 0;
|
|
322
|
+
signal.notify();
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
latency: {
|
|
326
|
+
record(ms) {
|
|
327
|
+
latencySamples.push(ms);
|
|
328
|
+
if (latencySamples.length > LATENCY_CAPACITY)
|
|
329
|
+
latencySamples.splice(0, latencySamples.length - LATENCY_CAPACITY);
|
|
330
|
+
},
|
|
331
|
+
stats: latencyStats,
|
|
332
|
+
},
|
|
333
|
+
controls: {
|
|
334
|
+
register,
|
|
335
|
+
list: () => [...controlRecords.values()].map(toControl),
|
|
336
|
+
get: (name) => {
|
|
337
|
+
const record = controlRecords.get(name);
|
|
338
|
+
return record === undefined ? null : toControl(record);
|
|
339
|
+
},
|
|
340
|
+
remove(name) {
|
|
341
|
+
if (controlRecords.delete(name))
|
|
342
|
+
signal.notify();
|
|
343
|
+
},
|
|
344
|
+
resetAll() {
|
|
345
|
+
for (const record of controlRecords.values())
|
|
346
|
+
writeControl(record, record.initial);
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
discover: {
|
|
350
|
+
bind(table, key, accessor) {
|
|
351
|
+
const kind = discoverableKind(accessor.initial);
|
|
352
|
+
if (kind === null)
|
|
353
|
+
return;
|
|
354
|
+
if (bindDiscovered(table, key, kind, accessor, null))
|
|
355
|
+
signal.notify();
|
|
356
|
+
},
|
|
357
|
+
scanTable,
|
|
358
|
+
scanModule(exports) {
|
|
359
|
+
let total = 0;
|
|
360
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
361
|
+
total += scanTable(name, value);
|
|
362
|
+
}
|
|
363
|
+
return total;
|
|
364
|
+
},
|
|
365
|
+
list: () => [...discoveredRecords.values()].map((record) => ({
|
|
366
|
+
id: record.id,
|
|
367
|
+
table: record.table,
|
|
368
|
+
key: record.key,
|
|
369
|
+
kind: record.kind,
|
|
370
|
+
initial: record.initial,
|
|
371
|
+
enabled: record.enabled,
|
|
372
|
+
read: () => record.get(),
|
|
373
|
+
})),
|
|
374
|
+
enable(id) {
|
|
375
|
+
enableDiscovered(id);
|
|
376
|
+
signal.notify();
|
|
377
|
+
},
|
|
378
|
+
disable(id) {
|
|
379
|
+
const record = discoveredRecords.get(id);
|
|
380
|
+
if (record === undefined || !record.enabled)
|
|
381
|
+
return;
|
|
382
|
+
record.enabled = false;
|
|
383
|
+
record.set(record.initial);
|
|
384
|
+
controlRecords.delete(id);
|
|
385
|
+
signal.notify();
|
|
386
|
+
},
|
|
387
|
+
clear() {
|
|
388
|
+
for (const record of discoveredRecords.values()) {
|
|
389
|
+
if (record.enabled) {
|
|
390
|
+
record.set(record.initial);
|
|
391
|
+
controlRecords.delete(record.id);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
discoveredRecords.clear();
|
|
395
|
+
signal.notify();
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
overrides: {
|
|
399
|
+
export() {
|
|
400
|
+
const enabled = [...discoveredRecords.values()].filter((r) => r.enabled).map((r) => r.id);
|
|
401
|
+
const values = {};
|
|
402
|
+
for (const record of controlRecords.values()) {
|
|
403
|
+
if (!Object.is(record.value, record.initial))
|
|
404
|
+
values[record.name] = record.value;
|
|
405
|
+
}
|
|
406
|
+
return { enabled, values };
|
|
407
|
+
},
|
|
408
|
+
apply(overrides) {
|
|
409
|
+
for (const id of overrides.enabled)
|
|
410
|
+
enableDiscovered(id);
|
|
411
|
+
for (const [name, value] of Object.entries(overrides.values)) {
|
|
412
|
+
const record = controlRecords.get(name);
|
|
413
|
+
if (record !== undefined)
|
|
414
|
+
writeControl(record, value);
|
|
415
|
+
}
|
|
416
|
+
signal.notify();
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
probes: {
|
|
420
|
+
register(name, read) {
|
|
421
|
+
probeReaders.set(name, read);
|
|
422
|
+
signal.notify();
|
|
423
|
+
return () => {
|
|
424
|
+
probeReaders.delete(name);
|
|
425
|
+
signal.notify();
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
read: readProbes,
|
|
429
|
+
},
|
|
430
|
+
signal,
|
|
431
|
+
snapshot() {
|
|
432
|
+
return {
|
|
433
|
+
at: Date.now(),
|
|
434
|
+
frame: frameStats(),
|
|
435
|
+
render: renderSample,
|
|
436
|
+
latency: latencyStats(),
|
|
437
|
+
logs: [...logEntries],
|
|
438
|
+
probes: readProbes(),
|
|
439
|
+
controls: [...controlRecords.values()].map((record) => ({
|
|
440
|
+
name: record.name,
|
|
441
|
+
kind: record.kind,
|
|
442
|
+
value: record.value,
|
|
443
|
+
initial: record.initial,
|
|
444
|
+
})),
|
|
445
|
+
discovered: [...discoveredRecords.values()].map((record) => ({
|
|
446
|
+
id: record.id,
|
|
447
|
+
kind: record.kind,
|
|
448
|
+
value: record.get(),
|
|
449
|
+
enabled: record.enabled,
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
export const devtools = createDevtools();
|
|
456
|
+
export function tunable(name, initial, options) {
|
|
457
|
+
return devtools.controls.register(name, initial, options);
|
|
458
|
+
}
|
|
459
|
+
export function snapshotDevtools() {
|
|
460
|
+
return devtools.snapshot();
|
|
461
|
+
}
|
|
462
|
+
export function instrumentLatency(target, methods, record = devtools.latency.record) {
|
|
463
|
+
const wrapped = { ...target };
|
|
464
|
+
for (const method of methods) {
|
|
465
|
+
const fn = target[method];
|
|
466
|
+
if (typeof fn !== "function")
|
|
467
|
+
continue;
|
|
468
|
+
wrapped[method] = ((...args) => {
|
|
469
|
+
const start = nowMs();
|
|
470
|
+
const result = fn.call(target, ...args);
|
|
471
|
+
if (result instanceof Promise) {
|
|
472
|
+
return result.then((value) => {
|
|
473
|
+
record(nowMs() - start);
|
|
474
|
+
return value;
|
|
475
|
+
}, (error) => {
|
|
476
|
+
record(nowMs() - start);
|
|
477
|
+
throw error;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return wrapped;
|
|
484
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TunableTransformResult {
|
|
2
|
+
code: string;
|
|
3
|
+
bound: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function transformTunableExports(code: string, table: string): TunableTransformResult;
|
|
6
|
+
export declare function tunableModuleTable(id: string): string | null;
|
|
7
|
+
export declare function tunableDiscoveryPlugin(): {
|
|
8
|
+
name: string;
|
|
9
|
+
enforce: "pre";
|
|
10
|
+
transform(code: string, id: string): {
|
|
11
|
+
code: string;
|
|
12
|
+
map: null;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const TUNABLE_EXPORT_PATTERN = /^export const ([A-Za-z_$][\w$]*)\s*=\s*(-?\d+(?:\.\d+)?|true|false|"#(?:[0-9a-fA-F]{3,8})"|'#(?:[0-9a-fA-F]{3,8})')\s*;[ \t]*$/gm;
|
|
2
|
+
export function transformTunableExports(code, table) {
|
|
3
|
+
const bound = [];
|
|
4
|
+
const rewritten = code.replace(TUNABLE_EXPORT_PATTERN, (_match, name, literal) => {
|
|
5
|
+
bound.push(name);
|
|
6
|
+
return `export let ${name} = ${literal};`;
|
|
7
|
+
});
|
|
8
|
+
if (bound.length === 0)
|
|
9
|
+
return { code, bound };
|
|
10
|
+
const bindings = bound
|
|
11
|
+
.map((name) => `__jg_devtools.discover.bind(${JSON.stringify(table)}, ${JSON.stringify(name)}, { initial: ${name}, get: () => ${name}, set: (__jg_value) => { ${name} = __jg_value as never; } });`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
return {
|
|
14
|
+
code: `${rewritten}\nimport { devtools as __jg_devtools } from "@jgengine/core/devtools/devtools";\n${bindings}\n`,
|
|
15
|
+
bound,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const GAME_MODULE_PATTERN = /[/\\]Games[/\\][^/\\]+[/\\]src[/\\](.+)\.(ts|tsx)$/;
|
|
19
|
+
const SKIP_MODULE_PATTERN = /(\.test\.|[/\\]main\.tsx$)/;
|
|
20
|
+
export function tunableModuleTable(id) {
|
|
21
|
+
if (SKIP_MODULE_PATTERN.test(id))
|
|
22
|
+
return null;
|
|
23
|
+
const match = GAME_MODULE_PATTERN.exec(id);
|
|
24
|
+
return match === null ? null : match[1].replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
export function tunableDiscoveryPlugin() {
|
|
27
|
+
return {
|
|
28
|
+
name: "jgengine-tunable-discovery",
|
|
29
|
+
enforce: "pre",
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const table = tunableModuleTable(id);
|
|
32
|
+
if (table === null)
|
|
33
|
+
return null;
|
|
34
|
+
const result = transformTunableExports(code, table);
|
|
35
|
+
return result.bound.length === 0 ? null : { code: result.code, map: null };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GameEvents } from "./events.js";
|
|
2
|
+
import type { EmotesDeps } from "./social.js";
|
|
3
|
+
export type ChatChannelKind = "global" | "party" | "proximity";
|
|
4
|
+
export interface ChatRateLimit {
|
|
5
|
+
count: number;
|
|
6
|
+
perMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ChatChannelDef {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: ChatChannelKind;
|
|
11
|
+
radius?: number;
|
|
12
|
+
historyLimit?: number;
|
|
13
|
+
rateLimit?: ChatRateLimit;
|
|
14
|
+
}
|
|
15
|
+
export interface ChatMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
channelId: string;
|
|
18
|
+
fromUserId: string;
|
|
19
|
+
body: string;
|
|
20
|
+
at: number;
|
|
21
|
+
}
|
|
22
|
+
export type ChatRecipients = readonly string[] | "all";
|
|
23
|
+
export type ChatSendResult = {
|
|
24
|
+
message: ChatMessage;
|
|
25
|
+
recipients: ChatRecipients;
|
|
26
|
+
} | {
|
|
27
|
+
reason: string;
|
|
28
|
+
};
|
|
29
|
+
export interface ChatSnapshot {
|
|
30
|
+
messages: Record<string, ChatMessage[]>;
|
|
31
|
+
counter: number;
|
|
32
|
+
}
|
|
33
|
+
export interface Chat {
|
|
34
|
+
register(def: ChatChannelDef): void;
|
|
35
|
+
channels(): ChatChannelDef[];
|
|
36
|
+
send(fromUserId: string, channelId: string, body: string): ChatSendResult;
|
|
37
|
+
whisper(fromUserId: string, toUserId: string, body: string): ChatSendResult;
|
|
38
|
+
history(channelId: string, options?: {
|
|
39
|
+
limit?: number;
|
|
40
|
+
viewerUserId?: string;
|
|
41
|
+
}): ChatMessage[];
|
|
42
|
+
snapshot(): ChatSnapshot;
|
|
43
|
+
hydrate(data: ChatSnapshot): void;
|
|
44
|
+
}
|
|
45
|
+
export interface ChatDeps {
|
|
46
|
+
events: GameEvents;
|
|
47
|
+
now?: () => number;
|
|
48
|
+
party?: {
|
|
49
|
+
membersOf(userId: string): string[];
|
|
50
|
+
};
|
|
51
|
+
proximity?: EmotesDeps;
|
|
52
|
+
blockedBy?: (userId: string) => readonly string[];
|
|
53
|
+
maxBodyLength?: number;
|
|
54
|
+
defaultRateLimit?: ChatRateLimit;
|
|
55
|
+
}
|
|
56
|
+
export declare const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit;
|
|
57
|
+
export declare const DEFAULT_CHAT_HISTORY_LIMIT = 100;
|
|
58
|
+
export declare const DEFAULT_CHAT_BODY_LENGTH = 500;
|
|
59
|
+
export declare const DEFAULT_PROXIMITY_CHAT_RADIUS = 20;
|
|
60
|
+
export declare const WHISPER_CHANNEL_PREFIX = "whisper:";
|
|
61
|
+
export declare function whisperChannelId(a: string, b: string): string;
|
|
62
|
+
export interface ChatRateLimiter {
|
|
63
|
+
allow(key: string, atMs: number): boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter;
|
|
66
|
+
export declare function createChat(deps: ChatDeps): Chat;
|