@downdraft/engine 0.3.0 → 0.3.1

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.
Files changed (82) hide show
  1. package/app/src/renderer/bootstrap.ts +0 -6
  2. package/app/src/renderer/feature-log.ts +1 -9
  3. package/app/src/renderer/mcp-harness.ts +8 -11
  4. package/app/src/renderer/standard-automation-tools.ts +8 -145
  5. package/core/src/module/host.ts +3 -6
  6. package/core/src/module/index.ts +1 -1
  7. package/core/src/module/module.ts +4 -47
  8. package/core/src/physics/interface.ts +5 -0
  9. package/core/src/platform/render-surface.ts +13 -0
  10. package/core/src/render/canvas-resize-watcher.ts +1 -10
  11. package/core/src/render/game-renderer.ts +40 -18
  12. package/core/src/render/index.ts +0 -2
  13. package/core/src/render/input-manager.ts +7 -7
  14. package/core/src/render/renderer-module-host.ts +2 -4
  15. package/core/src/ui-font-scale.ts +5 -38
  16. package/core/src/worker/rpc.ts +65 -0
  17. package/dist/.dd-manifest.json +859 -884
  18. package/dist/app/src/renderer/bootstrap.js +0 -4
  19. package/dist/app/src/renderer/feature-log.js +1 -10
  20. package/dist/app/src/renderer/mcp-harness.js +8 -11
  21. package/dist/app/src/renderer/standard-automation-tools.js +6 -138
  22. package/dist/core/src/module/host.js +3 -6
  23. package/dist/core/src/render/canvas-resize-watcher.js +1 -10
  24. package/dist/core/src/render/game-renderer.js +44 -18
  25. package/dist/core/src/render/index.js +0 -1
  26. package/dist/core/src/render/input-manager.js +7 -7
  27. package/dist/core/src/render/renderer-module-host.js +2 -4
  28. package/dist/core/src/ui-font-scale.js +5 -38
  29. package/dist/core/src/worker/rpc.js +59 -0
  30. package/dist/libraries/blitz-ui/src/native-module.js +8 -1
  31. package/dist/libraries/devtools/src/native-providers.js +0 -2
  32. package/dist/libraries/entities/src/model-renderer.js +87 -27
  33. package/dist/libraries/entities/src/shaders/model.wgsl +13 -4
  34. package/dist/libraries/networking/src/index.js +1 -0
  35. package/dist/libraries/networking/src/ws-server.js +387 -0
  36. package/dist/libraries/physics-rapier/src/api.js +15 -0
  37. package/dist/libraries/physics-rapier/src/backend.js +15 -0
  38. package/dist/libraries/physics-rapier/src/rapier-backend.js +5 -0
  39. package/dist/libraries/postfx/src/post-process-stack.js +1 -3
  40. package/dist/libraries/sand/src/materials.js +2 -32
  41. package/dist/libraries/sand/src/sand-world.js +0 -3
  42. package/dist/modules/devtools/src/api.js +8 -23
  43. package/dist/modules/devtools/src/data-bridge.js +0 -11
  44. package/dist/modules/devtools/src/index.js +0 -6
  45. package/dist/modules/devtools/src/init.js +8 -42
  46. package/dist/modules/devtools/src/scene-inspector.js +1 -4
  47. package/dist/modules/devtools/src/worker-sync.js +3 -11
  48. package/dist/test/src/vision-test.js +1 -3
  49. package/libraries/blitz-ui/src/native-module.ts +8 -1
  50. package/libraries/devtools/src/native-providers.ts +0 -2
  51. package/libraries/entities/src/model-renderer.ts +88 -27
  52. package/libraries/entities/src/shaders/model.wgsl +13 -4
  53. package/libraries/networking/src/index.ts +4 -0
  54. package/libraries/networking/src/ws-server.ts +397 -0
  55. package/libraries/physics-rapier/src/api.ts +15 -0
  56. package/libraries/physics-rapier/src/backend.ts +15 -0
  57. package/libraries/physics-rapier/src/rapier-backend.ts +6 -0
  58. package/libraries/postfx/src/post-process-stack.ts +1 -2
  59. package/libraries/sand/src/materials.ts +2 -32
  60. package/libraries/sand/src/sand-world.ts +0 -4
  61. package/modules/devtools/README.md +1 -3
  62. package/modules/devtools/src/api.ts +10 -34
  63. package/modules/devtools/src/data-bridge.ts +0 -16
  64. package/modules/devtools/src/index.ts +1 -11
  65. package/modules/devtools/src/init.ts +8 -51
  66. package/modules/devtools/src/scene-inspector.ts +2 -6
  67. package/modules/devtools/src/types.ts +5 -90
  68. package/modules/devtools/src/worker-sync.ts +3 -13
  69. package/package.json +4 -5
  70. package/test/src/vision-test.ts +2 -4
  71. package/core/src/render/sab-canvas-overlay.ts +0 -122
  72. package/dist/core/src/render/sab-canvas-overlay.js +0 -83
  73. package/dist/modules/devtools/src/debug-overlay.js +0 -181
  74. package/dist/modules/devtools/src/doctor-panel.js +0 -163
  75. package/dist/modules/devtools/src/label-overlay.js +0 -129
  76. package/dist/modules/devtools/src/material-stats-panel.js +0 -171
  77. package/dist/modules/devtools/src/sim-stats-panel.js +0 -250
  78. package/modules/devtools/src/debug-overlay.ts +0 -238
  79. package/modules/devtools/src/doctor-panel.ts +0 -182
  80. package/modules/devtools/src/label-overlay.ts +0 -154
  81. package/modules/devtools/src/material-stats-panel.ts +0 -188
  82. package/modules/devtools/src/sim-stats-panel.ts +0 -272
@@ -0,0 +1,387 @@
1
+ // ============================================================================
2
+ // ws-server.ts — minimal tri-runtime WebSocket server for host-authoritative
3
+ // LAN/WAN sessions. The networking library ships client transports
4
+ // (WebSocketTransport, WebRTCTransport) but had no listener — this fills the
5
+ // host side so a game instance can accept direct connections without any
6
+ // external service.
7
+ //
8
+ // Runtime selection is capability-based (see platform/runtime.ts):
9
+ // Bun → Bun.serve({ fetch, websocket })
10
+ // Deno → Deno.serve + Deno.upgradeWebSocket
11
+ // Node → node:http + a hand-rolled RFC6455 handshake/framer (pure JS,
12
+ // no native deps — also the fallback for any runtime with node:http)
13
+ //
14
+ // Semantics intentionally mirror the browser WebSocket surface: text frames
15
+ // arrive as `string`, binary frames as `Uint8Array`. Ping/pong/close and
16
+ // fragmented-frame reassembly are handled internally. Message cap matches
17
+ // WebSocketTransport's 16 MiB.
18
+ // ============================================================================
19
+ import { createLogger } from "@downdraft/engine/util/logger";
20
+ const log = createLogger();
21
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
22
+ const MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
23
+ function makeServerBase(handlers) {
24
+ const conns = new Set();
25
+ let nextId = 1;
26
+ const deliver = (conn, data) => {
27
+ const size = typeof data === "string" ? data.length : data.byteLength;
28
+ if (size > MAX_MESSAGE_SIZE) {
29
+ conn.close(1009, "message too large");
30
+ return;
31
+ }
32
+ try {
33
+ handlers.onMessage?.(conn, data);
34
+ }
35
+ catch (e) {
36
+ log.error("WsServer", `onMessage threw: ${e}`);
37
+ }
38
+ };
39
+ const drop = (conn, code, reason) => {
40
+ if (!conns.delete(conn))
41
+ return;
42
+ try {
43
+ handlers.onClose?.(conn, code, reason);
44
+ }
45
+ catch (e) {
46
+ log.error("WsServer", `onClose threw: ${e}`);
47
+ }
48
+ };
49
+ return { conns, nextId: () => nextId++, deliver, drop };
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Bun — Bun.serve with the `websocket` handler map
53
+ // ---------------------------------------------------------------------------
54
+ function startBunServer(opts) {
55
+ const Bun = globalThis.Bun;
56
+ const base = makeServerBase(opts);
57
+ const server = Bun.serve({
58
+ port: opts.port,
59
+ hostname: opts.host ?? "0.0.0.0",
60
+ fetch(req, srv) {
61
+ const ip = srv.requestIP(req);
62
+ const conn = {
63
+ id: base.nextId(),
64
+ remoteAddress: `${ip?.address ?? "?"}:${ip?.port ?? "?"}`,
65
+ raw: null,
66
+ send(data) { if (this.raw)
67
+ this.raw.send(data instanceof ArrayBuffer ? new Uint8Array(data) : data); },
68
+ close(code = 1000, reason = "") { if (this.raw)
69
+ this.raw.close(code, reason); },
70
+ };
71
+ return srv.upgrade(req, { data: { conn } }) ? undefined : new Response("WebSocket upgrade required", { status: 426 });
72
+ },
73
+ websocket: {
74
+ open(ws) {
75
+ const conn = ws.data.conn;
76
+ conn.raw = ws;
77
+ base.conns.add(conn);
78
+ try {
79
+ opts.onConnection?.(conn);
80
+ }
81
+ catch (e) {
82
+ log.error("WsServer", `onConnection threw: ${e}`);
83
+ }
84
+ },
85
+ message(ws, msg) {
86
+ const conn = ws.data.conn;
87
+ base.deliver(conn, typeof msg === "string" ? msg : new Uint8Array(msg));
88
+ },
89
+ close(ws, code, reason) {
90
+ base.drop(ws.data.conn, code, reason);
91
+ },
92
+ },
93
+ });
94
+ return Promise.resolve({
95
+ port: server.port,
96
+ host: opts.host ?? "0.0.0.0",
97
+ connections: () => [...base.conns],
98
+ async close() {
99
+ [...base.conns].forEach((c) => c.close(1001, "server shutdown"));
100
+ await server.stop(true);
101
+ },
102
+ });
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // Deno — Deno.serve + Deno.upgradeWebSocket
106
+ // ---------------------------------------------------------------------------
107
+ function startDenoServer(opts) {
108
+ const Deno = globalThis.Deno;
109
+ const base = makeServerBase(opts);
110
+ const server = Deno.serve({ port: opts.port, hostname: opts.host ?? "0.0.0.0" }, (req, info) => {
111
+ if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
112
+ return new Response("WebSocket upgrade required", { status: 426 });
113
+ }
114
+ const { socket, response } = Deno.upgradeWebSocket(req);
115
+ const conn = {
116
+ id: base.nextId(),
117
+ remoteAddress: info?.remoteAddr ? `${info.remoteAddr.hostname}:${info.remoteAddr.port}` : "?",
118
+ raw: socket,
119
+ send(data) { try {
120
+ socket.send(data);
121
+ }
122
+ catch { /* closing */ } },
123
+ close(code = 1000, reason = "") { try {
124
+ socket.close(code, reason);
125
+ }
126
+ catch { /* already closed */ } },
127
+ };
128
+ socket.binaryType = "arraybuffer";
129
+ socket.onopen = () => {
130
+ base.conns.add(conn);
131
+ try {
132
+ opts.onConnection?.(conn);
133
+ }
134
+ catch (e) {
135
+ log.error("WsServer", `onConnection threw: ${e}`);
136
+ }
137
+ };
138
+ socket.onmessage = (e) => {
139
+ base.deliver(conn, typeof e.data === "string" ? e.data : new Uint8Array(e.data));
140
+ };
141
+ socket.onclose = (e) => base.drop(conn, e.code, e.reason);
142
+ socket.onerror = () => base.drop(conn, 1006, "socket error");
143
+ return response;
144
+ });
145
+ const bound = server.addr;
146
+ return Promise.resolve({
147
+ port: bound.port,
148
+ host: bound.hostname,
149
+ connections: () => [...base.conns],
150
+ async close() {
151
+ [...base.conns].forEach((c) => c.close(1001, "server shutdown"));
152
+ await server.shutdown();
153
+ },
154
+ });
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Node (and generic fallback) — node:http + hand-rolled RFC6455
158
+ // ---------------------------------------------------------------------------
159
+ /** Encode one server→client frame. Server frames are never masked. */
160
+ function encodeWsFrame(opcode, payload) {
161
+ const len = payload.byteLength;
162
+ let header;
163
+ if (len < 126) {
164
+ header = new Uint8Array(2);
165
+ header[1] = len;
166
+ }
167
+ else if (len <= 0xffff) {
168
+ header = new Uint8Array(4);
169
+ header[1] = 126;
170
+ header[2] = len >>> 8;
171
+ header[3] = len & 0xff;
172
+ }
173
+ else {
174
+ header = new Uint8Array(10);
175
+ header[1] = 127;
176
+ const dv = new DataView(header.buffer);
177
+ dv.setUint32(2, Math.floor(len / 0x100000000));
178
+ dv.setUint32(6, len >>> 0);
179
+ }
180
+ header[0] = 0x80 | (opcode & 0x0f);
181
+ const out = new Uint8Array(header.length + len);
182
+ out.set(header, 0);
183
+ out.set(payload, header.length);
184
+ return out;
185
+ }
186
+ const OP_CONT = 0x0, OP_TEXT = 0x1, OP_BIN = 0x2, OP_CLOSE = 0x8, OP_PING = 0x9, OP_PONG = 0xa;
187
+ async function startNodeServer(opts) {
188
+ const http = await import("node:http");
189
+ const crypto = await import("node:crypto");
190
+ const base = makeServerBase(opts);
191
+ const httpServer = http.createServer((_req, res) => {
192
+ res.writeHead(426, { "Content-Type": "text/plain" });
193
+ res.end("WebSocket upgrade required");
194
+ });
195
+ httpServer.on("upgrade", (req, socket) => {
196
+ socket.setNoDelay(true);
197
+ const key = req.headers["sec-websocket-key"];
198
+ const upgrade = req.headers.upgrade?.toLowerCase();
199
+ if (upgrade !== "websocket" || typeof key !== "string") {
200
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
201
+ socket.destroy();
202
+ return;
203
+ }
204
+ const accept = crypto.createHash("sha1").update(key + WS_GUID).digest("base64");
205
+ socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
206
+ "Upgrade: websocket\r\n" +
207
+ "Connection: Upgrade\r\n" +
208
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`);
209
+ let closed = false;
210
+ const conn = {
211
+ id: base.nextId(),
212
+ remoteAddress: `${socket.remoteAddress ?? "?"}:${socket.remotePort ?? "?"}`,
213
+ raw: socket,
214
+ send(data) {
215
+ if (closed || socket.destroyed)
216
+ return;
217
+ if (typeof data === "string") {
218
+ socket.write(Buffer.from(encodeWsFrame(OP_TEXT, Buffer.from(data, "utf8"))));
219
+ }
220
+ else {
221
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
222
+ socket.write(Buffer.from(encodeWsFrame(OP_BIN, bytes)));
223
+ }
224
+ },
225
+ close(code = 1000, reason = "") {
226
+ if (closed)
227
+ return;
228
+ closed = true;
229
+ const payload = new Uint8Array(2 + reason.length);
230
+ payload[0] = code >>> 8;
231
+ payload[1] = code & 0xff;
232
+ payload.set(new TextEncoder().encode(reason), 2);
233
+ try {
234
+ socket.write(Buffer.from(encodeWsFrame(OP_CLOSE, payload)));
235
+ socket.end();
236
+ }
237
+ catch { /* socket already gone */ }
238
+ },
239
+ };
240
+ // ── Incremental frame parser ──
241
+ let buf = Buffer.alloc(0);
242
+ let fragOp = -1;
243
+ const frags = [];
244
+ const finishMessage = (op, payload) => {
245
+ if (payload.byteLength > MAX_MESSAGE_SIZE) {
246
+ conn.close(1009, "message too large");
247
+ return;
248
+ }
249
+ if (op === OP_TEXT)
250
+ base.deliver(conn, payload.toString("utf8"));
251
+ else
252
+ base.deliver(conn, new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength));
253
+ };
254
+ const handleFrame = (fin, op, payload) => {
255
+ if (op === OP_CLOSE) {
256
+ const code = payload.byteLength >= 2 ? payload.readUInt16BE(0) : 1000;
257
+ const reason = payload.byteLength > 2 ? payload.subarray(2).toString("utf8") : "";
258
+ conn.close(code, reason);
259
+ base.drop(conn, code, reason);
260
+ return;
261
+ }
262
+ if (op === OP_PING) {
263
+ if (!closed && !socket.destroyed)
264
+ socket.write(Buffer.from(encodeWsFrame(OP_PONG, payload)));
265
+ return;
266
+ }
267
+ if (op === OP_PONG)
268
+ return;
269
+ if (op === OP_CONT && fragOp < 0) {
270
+ conn.close(1002, "unexpected continuation");
271
+ return;
272
+ }
273
+ if ((op === OP_TEXT || op === OP_BIN) && fragOp >= 0) {
274
+ conn.close(1002, "interleaved frame");
275
+ return;
276
+ }
277
+ if (op !== OP_CONT)
278
+ fragOp = op;
279
+ frags.push(payload);
280
+ if (!fin)
281
+ return;
282
+ const full = Buffer.concat(frags);
283
+ frags.length = 0;
284
+ const finalOp = fragOp;
285
+ fragOp = -1;
286
+ finishMessage(finalOp, full);
287
+ };
288
+ socket.on("data", (chunk) => {
289
+ buf = buf.byteLength ? Buffer.concat([buf, chunk]) : chunk;
290
+ // Parse as many complete frames as the buffer holds.
291
+ for (;;) {
292
+ if (buf.byteLength < 2)
293
+ return;
294
+ const b0 = buf[0], b1 = buf[1];
295
+ const fin = (b0 & 0x80) !== 0;
296
+ const op = b0 & 0x0f;
297
+ const masked = (b1 & 0x80) !== 0;
298
+ let len = b1 & 0x7f;
299
+ let off = 2;
300
+ if (len === 126) {
301
+ if (buf.byteLength < off + 2)
302
+ return;
303
+ len = buf.readUInt16BE(off);
304
+ off += 2;
305
+ }
306
+ else if (len === 127) {
307
+ if (buf.byteLength < off + 8)
308
+ return;
309
+ const hi = buf.readUInt32BE(off), lo = buf.readUInt32BE(off + 4);
310
+ off += 8;
311
+ if (hi > 0 || lo > MAX_MESSAGE_SIZE) {
312
+ conn.close(1009, "message too large");
313
+ return;
314
+ }
315
+ len = lo;
316
+ }
317
+ const maskLen = masked ? 4 : 0;
318
+ if (buf.byteLength < off + maskLen + len)
319
+ return;
320
+ let payload = buf.subarray(off + maskLen, off + maskLen + len);
321
+ if (masked) {
322
+ const mask = buf.subarray(off, off + 4);
323
+ const unmasked = Buffer.allocUnsafe(payload.byteLength);
324
+ for (let i = 0; i < payload.byteLength; i++)
325
+ unmasked[i] = payload[i] ^ mask[i & 3];
326
+ payload = unmasked;
327
+ }
328
+ else {
329
+ payload = Buffer.from(payload); // detach — subarray aliases buf
330
+ }
331
+ buf = buf.subarray(off + maskLen + len);
332
+ handleFrame(fin, op, payload);
333
+ if (closed)
334
+ return;
335
+ }
336
+ });
337
+ const onSocketEnd = () => base.drop(conn, 1006, "socket closed");
338
+ socket.on("close", onSocketEnd);
339
+ socket.on("error", onSocketEnd);
340
+ base.conns.add(conn);
341
+ try {
342
+ opts.onConnection?.(conn);
343
+ }
344
+ catch (e) {
345
+ log.error("WsServer", `onConnection threw: ${e}`);
346
+ }
347
+ });
348
+ await new Promise((resolve, reject) => {
349
+ httpServer.once("error", reject);
350
+ httpServer.listen(opts.port, opts.host ?? "0.0.0.0", () => {
351
+ httpServer.removeListener("error", reject);
352
+ resolve();
353
+ });
354
+ });
355
+ const addr = httpServer.address();
356
+ const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
357
+ return {
358
+ port: boundPort,
359
+ host: opts.host ?? "0.0.0.0",
360
+ connections: () => [...base.conns],
361
+ async close() {
362
+ [...base.conns].forEach((c) => c.close(1001, "server shutdown"));
363
+ await new Promise((resolve) => httpServer.close(() => resolve()));
364
+ // httpServer.close waits for open sockets; force-destroy stragglers.
365
+ [...base.conns].forEach((c) => c.raw.destroy());
366
+ base.conns.clear();
367
+ },
368
+ };
369
+ }
370
+ // ---------------------------------------------------------------------------
371
+ // Entry point
372
+ // ---------------------------------------------------------------------------
373
+ /**
374
+ * Start a WebSocket server. Resolves once the socket is bound and listening.
375
+ * Runtime is picked by capability: Bun.serve → Deno.serve → node:http.
376
+ */
377
+ export function createWebSocketServer(opts) {
378
+ const g = globalThis;
379
+ const bun = g.Bun;
380
+ if (typeof bun?.serve === "function")
381
+ return startBunServer(opts);
382
+ const deno = g.Deno;
383
+ if (typeof deno?.serve === "function" && typeof deno.upgradeWebSocket === "function") {
384
+ return startDenoServer(opts);
385
+ }
386
+ return startNodeServer(opts);
387
+ }
@@ -235,6 +235,21 @@ export class UniversalPhysicsAPI {
235
235
  setIntegrationDt(realmId, dt) {
236
236
  this.backend.setIntegrationDt(realmId, dt);
237
237
  }
238
+ /**
239
+ * Change gravity across all realms (applies to near/mid/far). Best-effort:
240
+ * backends that can't mutate realm gravity omit `setRealmGravity`, in which
241
+ * case callers should fall back to per-body `gravityScale` in BodyDesc.
242
+ */
243
+ setGravity(gravity) {
244
+ [RealmTier.Near, RealmTier.Mid, RealmTier.Far].forEach((tier) => {
245
+ try {
246
+ const realm = this.realmManager.getRealm(tier);
247
+ this.backend.setRealmGravity?.(realm.id, gravity);
248
+ }
249
+ catch { /* realm tier not initialized */ }
250
+ });
251
+ this.config.gravity = gravity;
252
+ }
238
253
  // --- Realm ---
239
254
  getRealmTier(body) {
240
255
  return this.realmManager.getRealmForBody(body);
@@ -442,6 +442,21 @@ export class RapierPhysicsBackend {
442
442
  this.lib.setIntegrationDt(realmId, dt);
443
443
  }
444
444
  }
445
+ /**
446
+ * Change a live realm's gravity. Updates the stored realm config (the
447
+ * no-lib JS fallback stepper reads it) and forwards to the lib when it
448
+ * supports runtime gravity (the WASM Rapier world.gravity is mutable; the
449
+ * native FFI lib may not implement it — callers should treat this as
450
+ * best-effort).
451
+ */
452
+ setRealmGravity(realmId, gravity) {
453
+ const realm = this.realms.get(realmId);
454
+ if (realm)
455
+ realm.config.gravity = gravity;
456
+ if (this.lib && this.lib.setRealmGravity) {
457
+ this.lib.setRealmGravity(realmId, gravity);
458
+ }
459
+ }
445
460
  setSleepThresholds(realmId, linearThreshold, angularThreshold) {
446
461
  if (this.lib) {
447
462
  this.lib.setSleepThresholds(realmId, linearThreshold, angularThreshold);
@@ -154,6 +154,11 @@ async function doLoadPhysicsLib() {
154
154
  entityByBodyHandle.delete(id);
155
155
  bodyIdByHandle.delete(id);
156
156
  },
157
+ setRealmGravity(id, gravity) {
158
+ const world = realms.get(id);
159
+ if (world)
160
+ world.gravity = { x: gravity[0], y: gravity[1], z: gravity[2] };
161
+ },
157
162
  createBody(realmId, bodyId, desc, entity) {
158
163
  const world = realms.get(realmId);
159
164
  if (!world)
@@ -443,9 +443,7 @@ export class PostProcessStack {
443
443
  createGlyphAtlas() {
444
444
  const chars = " .:-=+*#%@";
445
445
  const gw = 16, gh = 16, cols = chars.length;
446
- const canvas = document.createElement("canvas");
447
- canvas.width = cols * gw;
448
- canvas.height = gh;
446
+ const canvas = new OffscreenCanvas(cols * gw, gh);
449
447
  const ctx = canvas.getContext("2d");
450
448
  ctx.fillStyle = "black";
451
449
  ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -72,32 +72,18 @@ export var Material;
72
72
  // --- Ingredients ---
73
73
  // Ice (frozen water — produced by cooling)
74
74
  Material[Material["Ice"] = 64] = "Ice";
75
+ // (IDs 67–68, 71–73, 76–77, 79–81, 83–88 were inert alchemy materials —
76
+ // retired; keep the gaps so old saves don't remap onto new materials.)
75
77
  // Liquids
76
78
  Material[Material["Ether"] = 65] = "Ether";
77
79
  Material[Material["Blood"] = 66] = "Blood";
78
- Material[Material["Syrup"] = 67] = "Syrup";
79
- Material[Material["NightshadeExtract"] = 68] = "NightshadeExtract";
80
80
  Material[Material["TrollBlood"] = 69] = "TrollBlood";
81
81
  Material[Material["LiquidShadow"] = 70] = "LiquidShadow";
82
- Material[Material["LoveEssence"] = 71] = "LoveEssence";
83
- Material[Material["HateEssence"] = 72] = "HateEssence";
84
- Material[Material["DreamMist"] = 73] = "DreamMist";
85
82
  Material[Material["VoidEssence"] = 74] = "VoidEssence";
86
83
  // Powders (solid, fall)
87
84
  Material[Material["Sulfur"] = 75] = "Sulfur";
88
- Material[Material["GroundEyeOfNewt"] = 76] = "GroundEyeOfNewt";
89
- Material[Material["GroundBatWing"] = 77] = "GroundBatWing";
90
85
  Material[Material["BoneDust"] = 78] = "BoneDust";
91
- Material[Material["IronFilings"] = 79] = "IronFilings";
92
- Material[Material["MoonstoneDust"] = 80] = "MoonstoneDust";
93
- Material[Material["CrystalDust"] = 81] = "CrystalDust";
94
86
  Material[Material["MushroomSpores"] = 82] = "MushroomSpores";
95
- Material[Material["DragonScale"] = 83] = "DragonScale";
96
- Material[Material["PhoenixFeather"] = 84] = "PhoenixFeather";
97
- Material[Material["UnicornHorn"] = 85] = "UnicornHorn";
98
- Material[Material["MandrakeRoot"] = 86] = "MandrakeRoot";
99
- Material[Material["SpiderSilk"] = 87] = "SpiderSilk";
100
- Material[Material["GraveDust"] = 88] = "GraveDust";
101
87
  Material[Material["StarShard"] = 89] = "StarShard";
102
88
  Material[Material["TimeSand"] = 90] = "TimeSand";
103
89
  // Gases
@@ -232,29 +218,13 @@ export const MATERIALS = {
232
218
  // --- Liquids ---
233
219
  [Material.Ether]: def(65, "Ether", [0.55, 0.45, 0.85, 0.8], { gravity: 1.5, gravityDir: 1, density: 0.7, liquid: true, flammable: true, burnTime: 40, albedo: 0.2, reflectivity: 0.4, brightness: 1.1 }),
234
220
  [Material.Blood]: def(66, "Blood", [0.7, 0.05, 0.05, 0.95], { gravity: 2, gravityDir: 1, density: 1.06, liquid: true, albedo: 0.3, reflectivity: 0.2, brightness: 0.8 }),
235
- [Material.Syrup]: def(67, "Syrup", [0.6, 0.4, 0.15, 0.95], { gravity: 1, gravityDir: 1, density: 1.4, liquid: true, albedo: 0.3, reflectivity: 0.3, brightness: 0.9 }),
236
- [Material.NightshadeExtract]: def(68, "Nightshade Extract", [0.3, 0.05, 0.35, 0.95], { gravity: 2, gravityDir: 1, density: 1.1, liquid: true, albedo: 0.2, reflectivity: 0.1, brightness: 0.7 }),
237
221
  [Material.TrollBlood]: def(69, "Troll Blood", [0.4, 0.2, 0.15, 0.95], { gravity: 2, gravityDir: 1, density: 1.2, liquid: true, albedo: 0.25, reflectivity: 0.15, brightness: 0.8 }),
238
222
  [Material.LiquidShadow]: def(70, "Liquid Shadow", [0.05, 0.05, 0.12, 0.98], { gravity: 3, gravityDir: 1, density: 2.0, liquid: true, albedo: 0.0, reflectivity: 0.0, brightness: 0.3 }),
239
- [Material.LoveEssence]: def(71, "Love Essence", [0.95, 0.4, 0.6, 0.9], { gravity: 1.5, gravityDir: 1, density: 0.9, liquid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.0 }),
240
- [Material.HateEssence]: def(72, "Hate Essence", [0.2, 0.1, 0.15, 0.9], { gravity: 2, gravityDir: 1, density: 1.3, liquid: true, albedo: 0.2, reflectivity: 0.2, brightness: 0.7 }),
241
- [Material.DreamMist]: def(73, "Dream Mist", [0.6, 0.7, 0.95, 0.6], { gravity: 1, gravityDir: -1, density: 0.3, liquid: true, albedo: 0.2, reflectivity: 0.5, brightness: 1.2 }),
242
223
  [Material.VoidEssence]: def(74, "Void Essence", [0.1, 0.0, 0.2, 0.95], { gravity: 4, gravityDir: 1, density: 3.0, liquid: true, albedo: 0.0, reflectivity: 0.1, brightness: 0.5 }),
243
224
  // --- Alchemy game: powders (solid, fall) ---
244
225
  [Material.Sulfur]: def(75, "Sulfur", [0.9, 0.85, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, flammable: true, burnTime: 60, albedo: 0.5, reflectivity: 0.1, brightness: 0.9 }),
245
- [Material.GroundEyeOfNewt]: def(76, "Ground Eye of Newt", [0.5, 0.35, 0.2, 1.0], { gravity: 1, gravityDir: 1, density: 1.2, solid: true, albedo: 0.4, brightness: 0.8 }),
246
- [Material.GroundBatWing]: def(77, "Ground Bat Wing", [0.3, 0.25, 0.35, 1.0], { gravity: 0.8, gravityDir: 1, density: 0.6, solid: true, albedo: 0.3, brightness: 0.7 }),
247
226
  [Material.BoneDust]: def(78, "Bone Dust", [0.85, 0.82, 0.75, 1.0], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.5, brightness: 0.8 }),
248
- [Material.IronFilings]: def(79, "Iron Filings", [0.55, 0.55, 0.6, 1.0], { gravity: 1.5, gravityDir: 1, density: 7.8, solid: true, magnetic: true, albedo: 0.5, reflectivity: 0.3 }),
249
- [Material.MoonstoneDust]: def(80, "Moonstone Dust", [0.8, 0.85, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 1.0, solid: true, albedo: 0.4, reflectivity: 0.5, brightness: 1.3 }),
250
- [Material.CrystalDust]: def(81, "Crystal Dust", [0.6, 0.9, 0.95, 1.0], { gravity: 1, gravityDir: 1, density: 2.5, solid: true, albedo: 0.3, reflectivity: 0.6, brightness: 1.2 }),
251
227
  [Material.MushroomSpores]: def(82, "Mushroom Spores", [0.6, 0.5, 0.4, 0.9], { gravity: 0.5, gravityDir: 1, density: 0.4, solid: true, albedo: 0.3, brightness: 0.8 }),
252
- [Material.DragonScale]: def(83, "Dragon Scale", [0.5, 0.15, 0.1, 1.0], { gravity: 1.5, gravityDir: 1, density: 5.0, solid: true, albedo: 0.4, reflectivity: 0.3, brightness: 1.0 }),
253
- [Material.PhoenixFeather]: def(84, "Phoenix Feather", [0.95, 0.5, 0.15, 0.9], { gravity: 0.3, gravityDir: 1, density: 0.3, solid: true, flammable: true, burnTime: 200, albedo: 0.4, brightness: 1.4 }),
254
- [Material.UnicornHorn]: def(85, "Unicorn Horn", [0.95, 0.9, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.5, reflectivity: 0.5, brightness: 1.5 }),
255
- [Material.MandrakeRoot]: def(86, "Mandrake Root", [0.5, 0.4, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.3, solid: true, albedo: 0.4, brightness: 0.7 }),
256
- [Material.SpiderSilk]: def(87, "Spider Silk", [0.9, 0.9, 0.85, 0.8], { gravity: 0.2, gravityDir: 1, density: 0.2, solid: true, albedo: 0.4, reflectivity: 0.2, brightness: 0.9 }),
257
- [Material.GraveDust]: def(88, "Grave Dust", [0.3, 0.28, 0.25, 1.0], { gravity: 1, gravityDir: 1, density: 1.8, solid: true, albedo: 0.3, brightness: 0.6 }),
258
228
  [Material.StarShard]: def(89, "Star Shard", [0.7, 0.8, 1.0, 1.0], { gravity: 1, gravityDir: 1, density: 2.0, solid: true, albedo: 0.3, reflectivity: 0.7, brightness: 1.6 }),
259
229
  [Material.TimeSand]: def(90, "Time Sand", [0.4, 0.3, 0.6, 0.9], { gravity: 1, gravityDir: 1, density: 1.5, solid: true, albedo: 0.3, reflectivity: 0.4, brightness: 1.2 }),
260
230
  // --- Alchemy game: gases + byproducts ---
@@ -2207,9 +2207,6 @@ export class SandWorld {
2207
2207
  continue;
2208
2208
  }
2209
2209
  }
2210
- // --- PhoenixFeather near fire/lava: glows brighter, resists burning ---
2211
- // (flammable but long burnTime already set; here we just let it shimmer)
2212
- // --- DragonScale: very dense, sinks through liquids (handled by density) ---
2213
2210
  // --- LiquidShadow + light source (fire/plasma) → dissipates to smoke ---
2214
2211
  if (mat === Material.LiquidShadow) {
2215
2212
  let lightNi = -1;
@@ -3,16 +3,15 @@
3
3
  // engine systems, regardless of which thread they run on.
4
4
  //
5
5
  // Realm detection determines transport:
6
- // - Main realm: panels/feeds/commands registered directly on
6
+ // - Main realm: feeds/commands registered directly on
7
7
  // window.__sceneInspector via DevToolsDataBridge.
8
- // - Worker realm: panels/feeds/commands registered in a worker-side
9
- // registry. Data feeds are written to a devtools SAB region (zero-copy,
10
- // synchronous reads). Commands are forwarded via worker RPC. Panel
11
- // declarations are synced to the renderer via a one-time manifest RPC.
8
+ // - Worker realm: feeds/commands registered in a worker-side registry.
9
+ // Data feeds are written to a devtools SAB region (zero-copy,
10
+ // synchronous reads). Commands are forwarded via worker RPC.
12
11
  //
13
12
  // Plugins import `devtools` from @downdraft/engine/modules/devtools and call
14
- // registerPanel/registerDataFeed/registerCommand/registerSABStat — the same
15
- // API works in both realms.
13
+ // registerDataFeed/registerCommand/registerSABStat — the same API works in
14
+ // both realms.
16
15
  // ============================================================================
17
16
  import { resourceToken } from "@downdraft/engine/ecs/resource";
18
17
  import { createLogger } from "@downdraft/engine/util/logger";
@@ -50,7 +49,7 @@ export const DEVTOOLS_REALM = detectRealm();
50
49
  // synchronously. No RPC polling needed.
51
50
  //
52
51
  // Layout:
53
- // [0..3] manifestVersion: Uint32 (bumped when panels/feeds change)
52
+ // [0..3] manifestVersion: Uint32 (bumped when feeds/commands change)
54
53
  // [4..7] feedCount: Uint32
55
54
  // [8..] feed entries: each is [lengthBytes: Uint32][JSON bytes...]
56
55
  // (lengthBytes = 0 means "not yet written" / "unavailable")
@@ -81,8 +80,6 @@ export function allocateDevToolsSAB(maxFeedBlobBytes, statsBytes) {
81
80
  }
82
81
  class DevToolsAPIImpl {
83
82
  realm;
84
- panels = new Map();
85
- toggles = new Map();
86
83
  dataFeeds = new Map();
87
84
  dataFeedNames = []; // ordered list for feedIndex assignment
88
85
  commands = new Map();
@@ -99,14 +96,6 @@ class DevToolsAPIImpl {
99
96
  constructor(realm) {
100
97
  this.realm = realm;
101
98
  }
102
- registerPanel(ext) {
103
- this.panels.set(ext.id, ext);
104
- this.bumpManifest();
105
- }
106
- registerOverlayToggle(toggle) {
107
- this.toggles.set(toggle.id, toggle);
108
- this.bumpManifest();
109
- }
110
99
  registerDataFeed(name, fn, writeRateHz = 10) {
111
100
  if (this.dataFeeds.has(name)) {
112
101
  // Update fn/rate, keep feedIndex and hasWritten
@@ -183,8 +172,6 @@ class DevToolsAPIImpl {
183
172
  }
184
173
  getManifest() {
185
174
  return {
186
- panels: Array.from(this.panels.values()),
187
- toggles: Array.from(this.toggles.values()),
188
175
  dataFeeds: this.dataFeedNames.map((name, i) => ({
189
176
  name,
190
177
  feedIndex: i,
@@ -276,8 +263,6 @@ class DevToolsAPIImpl {
276
263
  return Array.from(this.sabStats.keys());
277
264
  }
278
265
  clear() {
279
- this.panels.clear();
280
- this.toggles.clear();
281
266
  this.dataFeeds.clear();
282
267
  this.dataFeedNames = [];
283
268
  this.commands.clear();
@@ -301,7 +286,7 @@ const _devtools = new DevToolsAPIImpl(DEVTOOLS_REALM);
301
286
  * the appropriate transport for data feeds (direct call vs SAB) and
302
287
  * commands (direct call vs RPC forwarding).
303
288
  *
304
- * Plugins import this and call registerPanel/registerDataFeed/registerCommand.
289
+ * Plugins import this and call registerDataFeed/registerCommand.
305
290
  * The same code works in both realms.
306
291
  */
307
292
  export const devtools = _devtools;
@@ -28,14 +28,6 @@ export class DevToolsDataBridge {
28
28
  getPerformanceMetricsProvider() {
29
29
  return null;
30
30
  }
31
- /** Games override this to declare custom DevTools panel tabs. */
32
- getPanelExtensions() {
33
- return [];
34
- }
35
- /** Games override this to declare custom overlay toggles. */
36
- getOverlayToggles() {
37
- return [];
38
- }
39
31
  /** Games override this to expose sim stats + controls (pause/step/speed/clear). */
40
32
  getSimStatsProvider() {
41
33
  return null;
@@ -208,9 +200,6 @@ export class DevToolsDataBridge {
208
200
  getGPUAdapterInfo: () => {
209
201
  return this.cachedGpuAdapterInfo;
210
202
  },
211
- // --- Panel extensions (game-specific tabs and overlay toggles) ---
212
- getPanelExtensions: () => this.getPanelExtensions(),
213
- getOverlayToggles: () => this.getOverlayToggles(),
214
203
  // --- Sim stats & controls (optional — games implement ISimStatsProvider) ---
215
204
  getSimStats: () => this.getSimStatsProvider()?.getSimStats() ?? null,
216
205
  pauseSim: () => { this.getSimStatsProvider()?.pauseSim(); },
@@ -8,17 +8,13 @@ export { SkeletonRenderer } from "./skeleton-renderer.js";
8
8
  export { useDebugStore } from "./debug-store.js";
9
9
  export { useSceneStore } from "./scene-store.js";
10
10
  // Debug overlays (generic rendering, game provides data via interfaces)
11
- export { DebugOverlay } from "./debug-overlay.js";
12
11
  export { DebugRaycast } from "./debug-raycast.js";
13
- export { LabelOverlay } from "./label-overlay.js";
14
12
  export { SceneSync } from "./scene-sync.js";
15
13
  // Asset utilities
16
14
  export { asyncFetchArrayBuffer, bufferCache, evictThumbnailCache, findBinForGLTF, findMTLForOBJ, syncFetchArrayBuffer, thumbnailCache } from "./asset-utils.js";
17
15
  // Data bridge (non-abstract, data-feeds only) & base inspector & interfaces
18
16
  export { DevToolsDataBridge } from "./data-bridge.js";
19
- export { createDoctorPanelExtension } from "./doctor-panel.js";
20
17
  export { BaseSceneInspector } from "./scene-inspector.js";
21
- export { createSimStatsPanelExtension } from "./sim-stats-panel.js";
22
18
  // Unified DevTools API — single registration surface for plugins, games, and
23
19
  // engine systems. Auto-detects realm (main vs worker) and chooses transport.
24
20
  export { allocateDevToolsSAB, computeDevToolsSABLayout, devtools, DEVTOOLS_REALM, DEVTOOLS_SAB_MAX_FEEDS } from "./api.js";
@@ -39,8 +35,6 @@ export { ProfilingBridge } from "./profiling-bridge.js";
39
35
  export { wireProfilingBridge } from "./profiling-hooks.js";
40
36
  // Debug view descriptors — declarative registration of profiler overlay views
41
37
  export { BUILTIN_VIEW_DESCRIPTORS } from "./debug-view-descriptors.js";
42
- // Material stats panel — reusable DevTools tab for the material system
43
- export { createMaterialStatsPanelExtension } from "./material-stats-panel.js";
44
38
  // Blitz devtools — docked in-window DevTools UI (F12)
45
39
  export { BlitzDevtoolsHost, createSelfHostedDevtoolsUi } from "./blitz/host.js";
46
40
  export { BlitzDevtoolsTok, createDevtoolsUiModule } from "./blitz/module.js";