144 lines
3.9 KiB
JavaScript
144 lines
3.9 KiB
JavaScript
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 });
|
|
},
|
|
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 {
|
|
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 команд за кадр" }],
|
|
});
|
|
}
|
|
};
|