scense_runtime/wwwroot/runtime.js
2026-08-29 21:10:24 +08:00

509 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*!
* runtime.js — 元景 W-10 运行时执行引擎前端核心v1
*
* 能力覆盖W-10a ~ W-10i
* W-10a 世界运行时初始化:构建运行态实体树(父子树 + 扁平索引)
* W-10b 实体运行时状态管理state 机idle/active/done/failed…+ 位置/速度
* W-10c 脚本运行时执行调度:与 script_engine 编译产物actions 数组)对接
* W-10d 事件分发:事件队列 -> 路由到对应实体绑定的脚本
* W-10e 定时器管理addTimer / 按间隔触发(不依赖 setIntervaltick 驱动)
* W-10f 碰撞检测AABB 两两检测 -> 触发 onCollide 脚本/事件
* W-10g 运行时暂停/恢复pause() / resume(),事件循环挂起
* W-10h 错误处理与降级try/catch 捕获,单脚本失败不整页崩溃
* W-10i 独立世界本地引擎:后端不可用时本地构建世界继续运行
*
* 对接script_engine 编译产物格式
* { id, name, actions: [ { op:'move'|'set_state'|'emit'|'delay'|'log'|'fail', ... } ] }
*
* 用法:
* var rt = new RuntimeEngine({ worldId: 'w1', canvas: canvasEl });
* rt.initWorld(serverData); // serverData 可来自 /runtime/api/load_world.dspy
* rt.start();
* rt.on('log', function(m){ ... }); // UI 订阅日志
*/
(function (global) {
'use strict';
var ENGINE_VERSION = '1.0.0';
// ================= 工具 =================
function now() { return Date.now(); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
// ================= 实体W-10b =================
function makeEntity(node) {
var props = node.props || {};
return {
id: String(node.id),
name: String(node.name || node.id),
type: String(node.type || 'object'),
state: String(node.state || 'idle'),
x: Number(node.x) || 0,
y: Number(node.y) || 0,
vx: 0,
vy: 0,
width: Number(props.width) || 40,
height: Number(props.height) || 40,
props: Object.assign({}, props),
children: [],
scripts: Array.isArray(props.scripts) ? props.scripts.slice() : [],
onCollide: props.onCollide || null,
timers: []
};
}
function flattenTree(roots, out) {
out = out || [];
for (var i = 0; i < roots.length; i++) {
out.push(roots[i]);
if (roots[i].children && roots[i].children.length) {
flattenTree(roots[i].children, out);
}
}
return out;
}
// ================= 脚本执行W-10c与 script_engine 编译产物对接) =================
function executeScript(script, ctx, rt) {
if (!script || typeof script !== 'object') {
throw new Error('runtime: invalid script object (compile artifact missing)');
}
if (!Array.isArray(script.actions) || script.actions.length === 0) {
throw new Error('runtime: script has no actions: ' + (script.id || '?'));
}
var results = [];
for (var i = 0; i < script.actions.length; i++) {
var act = script.actions[i];
if (!act || typeof act.op !== 'string') {
throw new Error('runtime: action missing op at index ' + i);
}
results.push(executeAction(act, ctx, rt, script));
}
return { ok: true, script: script.id || '?', results: results };
}
function executeAction(act, ctx, rt, script) {
var e = ctx.entity;
switch (act.op) {
case 'move': {
var dx = Number(act.dx) || 0;
var dy = Number(act.dy) || 0;
e.vx = dx;
e.vy = dy;
e.x += dx;
e.y += dy;
return { op: 'move', dx: dx, dy: dy, x: e.x, y: e.y };
}
case 'set_state': {
e.state = String(act.state || 'idle');
return { op: 'set_state', state: e.state };
}
case 'emit': {
rt.dispatchEvent(String(act.event || 'custom'), act.entity_id || e.id, act.payload || {});
return { op: 'emit', event: act.event || 'custom' };
}
case 'delay': {
var ms = Number(act.ms) || 0;
rt.addTimer(ms, function () {
rt.dispatchEvent('timer', e.id, { script: script ? script.id : '' });
});
return { op: 'delay', ms: ms };
}
case 'log': {
rt.log('[' + e.id + '] ' + (act.message || ''));
return { op: 'log', message: act.message || '' };
}
case 'fail': {
// 用于验证 W-10h 错误降级:主动抛错,由 handleError 捕获,不整页崩溃
throw new Error('runtime: script failure: ' + (act.message || 'boom'));
}
default:
throw new Error('runtime: unknown action op: ' + act.op);
}
}
// ================= 运行时引擎 =================
function RuntimeEngine(opts) {
opts = opts || {};
this.worldId = opts.worldId || 'local';
this.canvas = opts.canvas || null;
this.tickMs = Number(opts.tickMs) || 16;
this.entities = {}; // id -> entity扁平索引
this.roots = []; // 实体树根
this.entityList = []; // 扁平列表(碰撞/渲染遍历)
this.eventQueue = []; // 事件队列W-10d
this.timers = []; // 定时器列表W-10e
this.scriptStore = {}; // scriptId -> script脚本注册表
this.running = false;
this.paused = false;
this.mode = 'independent'; // 'server' | 'independent'W-10i
this.degraded = false;
this.errorCount = 0;
this.lastErrors = [];
this.frame = 0;
this._loopId = 0;
this._last = 0;
this._listeners = {};
this.world = null;
}
// ---------- W-10a 世界运行时初始化 ----------
RuntimeEngine.prototype.initWorld = function (serverData) {
serverData = serverData || {};
this.reset();
if (serverData.ok && serverData.degraded !== true && Array.isArray(serverData.entities)) {
// 服务端辅助可用:用服务端世界 + 实体
this.mode = 'server';
this.world = serverData.world || { id: this.worldId, name: this.worldId };
this.buildTree(serverData.entities);
this.log('世界初始化完成server 模式):' + (this.world.name || this.worldId)
+ ',实体 ' + this.entityList.length + ' 个');
} else {
// W-10i 独立模式:后端不可用/查无数据 -> 本地构建
this.mode = 'independent';
this.degraded = true;
this.world = { id: this.worldId, name: '独立世界', mode: 'independent' };
this.buildTree(this._localWorldEntities());
this.log('后端不可用已降级独立模式W-10i本地构建 '
+ this.entityList.length + ' 个实体');
}
this._loadSampleScripts();
this.emit('world', { mode: this.mode, world: this.world, entities: this.entityList });
return this.getStatus();
};
RuntimeEngine.prototype._localWorldEntities = function () {
// 独立模式内置演示世界W-10i
return [
{ id: 'hero', name: '主角', type: 'character', x: 60, y: 120,
props: { width: 44, height: 44, scripts: ['script_click', 'script_move'] } },
{ id: 'box1', name: '箱子A', type: 'prop', x: 260, y: 120,
props: { width: 40, height: 40, scripts: ['script_timer'] } },
{ id: 'box2', name: '箱子B', type: 'prop', x: 460, y: 120,
props: { width: 40, height: 40, scripts: ['script_fail'] } },
{ id: 'npc1', name: 'NPC-1', type: 'npc', x: 160, y: 300,
props: { width: 40, height: 40, scripts: ['script_click'] } },
{ id: 'npc2', name: 'NPC-2', type: 'npc', x: 360, y: 300,
props: { width: 40, height: 40, scripts: ['script_move'] } }
];
};
RuntimeEngine.prototype.buildTree = function (entities) {
// 扁平实体列表 -> 父子树 + 扁平索引(服务端 build_entity_tree 的 JS 镜像)
var byId = {};
var parentMap = {};
var i, eid;
for (i = 0; i < entities.length; i++) {
eid = String(entities[i].id || entities[i].entity_id || '');
if (!eid) { continue; }
var ent = makeEntity(entities[i]);
byId[eid] = ent;
parentMap[eid] = entities[i].parent_id || entities[i].parent || null;
}
var roots = [];
for (eid in byId) {
if (!Object.prototype.hasOwnProperty.call(byId, eid)) { continue; }
var pid = parentMap[eid];
if (pid && byId[pid]) {
byId[pid].children.push(byId[eid]);
} else {
roots.push(byId[eid]);
}
}
this.roots = roots;
this.entityList = flattenTree(roots);
var self = this;
this.entityList.forEach(function (e) { self.entities[e.id] = e; });
};
RuntimeEngine.prototype._loadSampleScripts = function () {
// 示例脚本(与 script_engine 编译产物 actions 格式一致)
this.scriptStore['script_move'] = {
id: 'script_move', name: '移动',
actions: [{ op: 'log', message: '执行 move 脚本' }, { op: 'move', dx: 12, dy: 0 }]
};
this.scriptStore['script_click'] = {
id: 'script_click', name: '点击响应',
actions: [
{ op: 'log', message: '收到 click 事件,执行点击脚本' },
{ op: 'set_state', state: 'active' },
{ op: 'emit', event: 'active_changed', payload: { source: 'click' } }
]
};
this.scriptStore['script_timer'] = {
id: 'script_timer', name: '定时器',
actions: [{ op: 'log', message: '定时器触发(每 3s' }, { op: 'move', dx: 0, dy: 10 }]
};
this.scriptStore['script_fail'] = {
id: 'script_fail', name: '错误演示',
actions: [{ op: 'log', message: '即将故意抛错以验证降级' }, { op: 'fail', message: '演示脚本异常' }]
};
};
RuntimeEngine.prototype.bindScript = function (entityId, script) {
if (script && script.id) { this.scriptStore[script.id] = script; }
var e = this.entities[entityId];
if (!e) { throw new Error('runtime: entity not found: ' + entityId); }
e.scripts.push(script && script.id ? script.id : '');
return true;
};
// ---------- W-10g 启动 / 暂停 / 恢复 ----------
RuntimeEngine.prototype.start = function () {
if (this.running && !this.paused) { return this.getStatus(); }
this.running = true;
this.paused = false;
var self = this;
this._last = now();
this._loopId = global.setTimeout(function loop() {
if (!self.running) { return; }
var ts = now();
self.tick(ts);
self._loopId = global.setTimeout(loop, self.tickMs);
}, this.tickMs);
this.log('运行时已启动tick=' + this.tickMs + 'ms');
this.emit('status', this.getStatus());
return this.getStatus();
};
RuntimeEngine.prototype.pause = function () {
this.paused = true;
this.log('运行时已暂停W-10g');
this.emit('status', this.getStatus());
return this.getStatus();
};
RuntimeEngine.prototype.resume = function () {
if (!this.running) { return this.start(); }
this.paused = false;
this._last = now();
this.log('运行时已恢复W-10g');
this.emit('status', this.getStatus());
return this.getStatus();
};
RuntimeEngine.prototype.stop = function () {
this.running = false;
if (this._loopId) { global.clearTimeout(this._loopId); this._loopId = 0; }
this.log('运行时已停止');
};
RuntimeEngine.prototype.reset = function () {
this.stop();
this.entities = {};
this.roots = [];
this.entityList = [];
this.eventQueue = [];
this.timers = [];
this.errorCount = 0;
this.lastErrors = [];
this.frame = 0;
};
// ---------- 事件循环主 tick ----------
RuntimeEngine.prototype.tick = function (ts) {
if (this.paused || !this.running) { return; }
var dt = Math.min(ts - this._last, 100) / 1000;
this._last = ts;
this.frame++;
try {
this.update(dt); // 实体状态更新W-10b
this.processEvents(); // 事件分发W-10d
this.processTimers(); // 定时器W-10e
this.checkCollisions(); // 碰撞检测W-10f
} catch (err) {
this.handleError(err); // W-10h 捕获,不整页崩溃
}
this.emit('frame', { frame: this.frame, dt: dt });
};
RuntimeEngine.prototype.update = function (dt) {
var i;
for (i = 0; i < this.entityList.length; i++) {
var e = this.entityList[i];
e.x += e.vx * dt * 60;
e.y += e.vy * dt * 60;
// 边界约束
e.x = clamp(e.x, 10, 720);
e.y = clamp(e.y, 10, 460);
// vx/vy 衰减(移动脚本一次性位移后归零)
if (Math.abs(e.vx) < 0.1) { e.vx = 0; }
if (Math.abs(e.vy) < 0.1) { e.vy = 0; }
}
};
// ---------- W-10d 事件分发 ----------
RuntimeEngine.prototype.dispatchEvent = function (type, entityId, payload) {
this.eventQueue.push({
type: String(type || 'custom'),
entityId: entityId ? String(entityId) : '',
payload: payload || {},
ts: now()
});
if (this.eventQueue.length > 500) { this.eventQueue.shift(); }
return this.eventQueue.length;
};
RuntimeEngine.prototype.processEvents = function () {
while (this.eventQueue.length) {
var ev = this.eventQueue.shift();
this._routeEvent(ev);
}
};
RuntimeEngine.prototype._routeEvent = function (ev) {
// 路由到对应实体绑定的脚本ev.entityId -> entity.scripts -> scriptStore
var e = ev.entityId ? this.entities[ev.entityId] : null;
if (!e) {
this.log('事件 ' + ev.type + ' 无目标实体,忽略');
return;
}
if (!e.scripts || e.scripts.length === 0) {
this.log('实体 ' + e.id + ' 未绑定脚本,事件 ' + ev.type + ' 忽略');
return;
}
var self = this;
var matched = false;
e.scripts.forEach(function (sid) {
var script = self.scriptStore[sid];
if (!script) { return; }
matched = true;
self.log('路由事件 ' + ev.type + ' -> 实体 ' + e.id + ' 脚本 ' + sid);
try {
executeScript(script, { entity: e, event: ev }, self);
if (e.state !== 'failed') { e.state = 'done'; }
} catch (err) {
e.state = 'failed';
self.handleError(err);
}
});
if (!matched) {
this.log('实体 ' + e.id + ' 的脚本未注册,事件 ' + ev.type + ' 忽略');
}
};
// ---------- W-10e 定时器管理 ----------
RuntimeEngine.prototype.addTimer = function (ms, fn) {
if (typeof fn !== 'function') { return; }
this.timers.push({ due: now() + Math.max(ms, 0), fn: fn, fired: false });
return this.timers.length;
};
RuntimeEngine.prototype.processTimers = function () {
var t = now();
var pending = [];
var i;
for (i = 0; i < this.timers.length; i++) {
var tm = this.timers[i];
if (tm.due <= t) {
try {
tm.fn();
} catch (err) {
this.handleError(err);
}
} else {
pending.push(tm);
}
}
this.timers = pending;
};
// ---------- W-10f 碰撞检测AABB ----------
RuntimeEngine.prototype.checkCollisions = function () {
var list = this.entityList;
var i, j;
for (i = 0; i < list.length; i++) {
for (j = i + 1; j < list.length; j++) {
var a = list[i], b = list[j];
if (aabbHit(a, b)) {
this.log('碰撞:' + a.id + ' <-> ' + b.id);
this._fireCollide(a, b);
this._fireCollide(b, a);
}
}
}
};
function aabbHit(a, b) {
return a.x < b.x + b.width && a.x + a.width > b.x &&
a.y < b.y + b.height && a.y + a.height > b.y;
}
RuntimeEngine.prototype._fireCollide = function (self, other) {
if (typeof self.onCollide === 'string' && this.scriptStore[self.onCollide]) {
try {
executeScript(this.scriptStore[self.onCollide],
{ entity: self, event: { type: 'collide', other: other.id } }, this);
} catch (err) {
this.handleError(err);
}
}
};
// ---------- W-10h 错误处理与降级 ----------
RuntimeEngine.prototype.handleError = function (err) {
this.errorCount++;
var msg = (err && err.message) ? err.message : String(err);
this.lastErrors.push(msg);
if (this.lastErrors.length > 50) { this.lastErrors.shift(); }
this.log('[错误] ' + msg + '(已降级,不影响引擎运行,累计 ' + this.errorCount + ' 次)');
this.emit('error', { message: msg, count: this.errorCount });
};
RuntimeEngine.prototype.log = function (msg) {
this.emit('log', { ts: new Date().toISOString(), message: msg });
};
// ---------- UI 事件订阅 ----------
RuntimeEngine.prototype.on = function (name, fn) {
if (!this._listeners[name]) { this._listeners[name] = []; }
this._listeners[name].push(fn);
return this;
};
RuntimeEngine.prototype.emit = function (name, data) {
var fns = this._listeners[name];
if (!fns) { return; }
for (var i = 0; i < fns.length; i++) {
try { fns[i](data); } catch (err) { /* 订阅者异常不影响引擎 */ }
}
};
// ---------- 状态查询 ----------
RuntimeEngine.prototype.getStatus = function () {
return {
engine: 'runtime-js-v' + ENGINE_VERSION,
worldId: this.worldId,
mode: this.mode,
degraded: this.degraded,
running: this.running,
paused: this.paused,
frame: this.frame,
entities: this.entityList.length,
eventQueue: this.eventQueue.length,
timers: this.timers.length,
errorCount: this.errorCount
};
};
RuntimeEngine.prototype.getEntity = function (id) {
return this.entities[id] || null;
};
RuntimeEngine.prototype.getEntities = function () {
return this.entityList;
};
// 点击实体 -> 触发事件W-10d 入口UI 点击 -> dispatchEvent('click')
RuntimeEngine.prototype.trigger = function (entityId, eventType) {
return this.dispatchEvent(eventType || 'click', entityId, { source: 'ui' });
};
// ================= 导出 =================
global.RuntimeEngine = RuntimeEngine;
global.RUNTIME_ENGINE_VERSION = ENGINE_VERSION;
})(window);