scense_runtime/wwwroot/runtime.js

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 / 按间隔触发(不依赖 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);