/*! * 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 / 按间隔触发(不依赖 setInterval,tick 驱动) * 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 可来自 /scense_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);