let entities = [], scripts = [], instances = [], input = {}, physics = {}, commands = [], frame = 0; const merge = (a, b) => { for (const [k, v] of Object.entries(b)) { if (["__proto__", "constructor", "prototype"].includes(k)) continue; a[k] = v && typeof v === "object" && !Array.isArray(v) ? merge(a[k] || {}, v) : v; } return a; }; function api(i) { return { state: i.state, params: i.params, get input() { return input; }, physics: (id = i.id) => structuredClone( physics[id] || { grounded: false, velocity: { x: 0, y: 0, z: 0 }, contacts: [], }, ), velocity(value) { commands.push({ type: "velocity", id: i.id, value }); }, teleport(position, yaw) { commands.push({ type: "teleport", id: i.id, position, yaw }); }, emit(name, data = {}) { commands.push({ type: "event", id: i.id, name, data }); }, get: (id = i.id) => structuredClone(entities.find((n) => n.id === id) || null), entities: () => structuredClone(entities), position: (id = i.id) => [ ...(entities.find((n) => n.id === id)?.transform.position || [0, 0, 0]), ], patch(id, patch) { const n = entities.find((n) => n.id === id); if (n) { merge(n, structuredClone(patch)); commands.push({ type: "patch", id, patch }); } }, move(delta) { commands.push({ type: "move", id: i.id, delta }); }, impulse2D(value) { commands.push({ type: "impulse2d", id: i.id, value }); }, rotate2D(z) { this.patch(i.id, { transform: { rotation: [0, 0, z] } }); }, rotate(y) { this.patch(i.id, { transform: { rotation: [0, y, 0] } }); }, animate(name, loop = true) { commands.push({ type: "animate", id: i.id, name, loop }); }, effect(name, id = i.id) { commands.push({ type: "effect", id, name }); }, log(message) { commands.push({ type: "log", id: i.id, message: String(message) }); }, destroy(id = i.id) { this.patch(id, { enabled: false }); }, spawn(template, position) { commands.push({ type: "spawn", template, position }); }, scene(sceneId) { commands.push({ type: "scene", sceneId }); }, }; } function fail(i, e) { i.failed = true; commands.push({ type: "error", id: i.id, scriptId: i.scriptId, message: String(e?.stack || e), }); } function reconcile() { instances = instances.filter((i) => entities.some( (n) => n.id === i.id && n.components.script?.scriptId === i.scriptId, ), ); for (const n of entities) { if (!n.enabled || instances.some((i) => i.id === n.id)) continue; const binding = n.components.script, def = scripts.find((s) => s.id === binding?.scriptId); if (!def) continue; const i = { id: n.id, scriptId: def.id, state: {}, params: { ...Object.fromEntries( Object.entries(def.fields).map(([k, v]) => [k, v.default]), ), ...binding.params, }, behavior: null, failed: false, }; instances.push(i); try { i.behavior = new Function("return (" + def.source + ");")(); if (!i.behavior || typeof i.behavior !== "object") throw Error("Скрипт должен вернуть {start, update}"); i.behavior.start?.(api(i)); } catch (e) { fail(i, e); } } } self.onmessage = (e) => { const m = e.data; commands = []; entities = m.entities; physics = m.physics || {}; if (m.type === "init") { scripts = m.scripts; instances = []; reconcile(); postMessage({ type: "ready", commands }); } else { input = m.input; reconcile(); for (const i of instances) { if (i.failed || !entities.some((n) => n.id === i.id && n.enabled)) continue; try { for (const event of m.events2d || []) if (event.entityId === i.id) i.behavior.collision2d?.(api(i), event); i.behavior.update?.(api(i), m.dt); } catch (e) { fail(i, e); } } postMessage({ type: "frame", frame: ++frame, commands: commands.length < 5000 ? commands : [{ type: "error", message: "Лимит 5000 команд за кадр" }], }); } };