Files
2026-09-12 04:58:59 +03:00

165 lines
4.3 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 });
},
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 команд за кадр" }],
});
}
};