348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
/* scense 全屏游戏页交互逻辑(独立 HTML 页内脚本,允许 fetch/setInterval)
|
||
*
|
||
* 依赖:
|
||
* - runtime.js(W-10 运行时引擎 RuntimeEngine)—— 世界初始化/实体状态/脚本调度/暂停恢复
|
||
* - /scense/game/api/*.dspy(W-08 会话与结果)
|
||
*
|
||
* 功能点:W-08b 开始玩(进入运行态) / W-08c 暂停 / W-08d 继续 /
|
||
* W-08e 结束结算 / W-08f 重新开始 / W-08g 分数结果展示 /
|
||
* W-08h 分享链接 / W-08i 玩中交互(点选实体触发脚本) / W-08j 退出保存进度
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
var CFG = {gameId: '', sessionId: '', shareToken: '', worldId: ''};
|
||
var session = null; // 当前会话
|
||
var engine = null; // W-10 RuntimeEngine 实例
|
||
var score = 0;
|
||
var level = 1;
|
||
var startTs = 0;
|
||
var clockTimer = null;
|
||
var statusBar = null;
|
||
|
||
function $id(id) { return document.getElementById(id); }
|
||
|
||
/* API 基础路径:由页面 URL 推导(/scense/game/game_play.html -> /scense) */
|
||
function apiBase() {
|
||
return window.location.pathname.replace(/\/game\/.*$/, '');
|
||
}
|
||
|
||
function setStatus(t) {
|
||
if (statusBar) { statusBar.textContent = t; statusBar.className = 'hud-status ' + (t === '运行中' ? 'ok' : ''); }
|
||
}
|
||
|
||
function api(name, params) {
|
||
return fetch(apiBase() + '/game/api/' + name + '.dspy', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(params || {})
|
||
}).then(function (r) { return r.json(); });
|
||
}
|
||
|
||
function elapsedMs() { return startTs ? (Date.now() - startTs) : 0; }
|
||
|
||
function refreshHud() {
|
||
$id('hud_score').textContent = score;
|
||
$id('hud_level').textContent = level;
|
||
}
|
||
|
||
function startClock() {
|
||
if (clockTimer) clearInterval(clockTimer);
|
||
clockTimer = setInterval(function () {
|
||
var s = Math.floor(elapsedMs() / 1000);
|
||
var m = Math.floor(s / 60); s = s % 60;
|
||
$id('hud_time').textContent = (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
|
||
}, 500);
|
||
}
|
||
|
||
/* ---------- W-08b 开始玩:初始化会话 + 进入运行态 ---------- */
|
||
function startGame() {
|
||
setStatus('启动中…');
|
||
api('game_start', {game_id: CFG.gameId, session_id: CFG.sessionId || '', world_id: CFG.worldId || ''})
|
||
.then(function (res) {
|
||
if (!res || !res.success) { setStatus('启动失败: ' + (res && res.error || '未知错误')); return; }
|
||
session = res.data.session;
|
||
CFG.sessionId = session.session_id;
|
||
CFG.shareToken = session.share_token;
|
||
CFG.worldId = session.world_id;
|
||
score = session.score || 0;
|
||
level = session.level || 1;
|
||
startTs = Date.now() - (session.elapsed_ms || 0);
|
||
refreshHud();
|
||
startClock();
|
||
initEngine(); // 进入 W-10 运行态
|
||
showPanel('hud');
|
||
setStatus('运行中');
|
||
enableControls(true);
|
||
});
|
||
}
|
||
|
||
/* ---------- W-10 运行时引擎初始化(runtime.js) ---------- */
|
||
function initEngine() {
|
||
if (typeof RuntimeEngine === 'undefined') {
|
||
setStatus('运行时引擎未加载(runtime.js),降级独立模式');
|
||
initDegradedEngine();
|
||
return;
|
||
}
|
||
engine = new RuntimeEngine({
|
||
worldId: CFG.worldId,
|
||
canvas: $id('game_canvas'),
|
||
onEntityClick: onEntityClick, // W-08i 玩中交互:点选实体
|
||
onEntityState: onEntityState,
|
||
onScriptLog: onScriptLog,
|
||
onError: onEngineError
|
||
});
|
||
engine.initWorld(CFG.worldId).then(function (info) {
|
||
setStatus('运行中' + (info && info.degraded ? '(独立模式)' : ''));
|
||
engine.start();
|
||
});
|
||
}
|
||
|
||
/* 独立模式本地引擎(后端不可用时降级,W-10i) */
|
||
function initDegradedEngine() {
|
||
var canvas = $id('game_canvas');
|
||
var ctx = canvas && canvas.getContext('2d');
|
||
var entities = [
|
||
{id: 'hero', x: 60, y: 240, w: 40, h: 40, color: '#2563EB', state: 'idle', label: '英雄'},
|
||
{id: 'chest', x: 400, y: 200, w: 36, h: 36, color: '#D97706', state: 'idle', label: '宝箱'},
|
||
{id: 'slime', x: 260, y: 320, w: 34, h: 34, color: '#16A34A', state: 'idle', label: '史莱姆'}
|
||
];
|
||
engine = {
|
||
paused: false,
|
||
entities: entities,
|
||
start: function () { this.draw(); },
|
||
stop: function () {},
|
||
pause: function () { this.paused = true; },
|
||
resume: function () { this.paused = false; this.draw(); },
|
||
restart: function () { this.paused = false; this.draw(); },
|
||
draw: function () {
|
||
if (!ctx) return;
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
ctx.fillStyle = '#F9FAFB';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
entities.forEach(function (e) {
|
||
ctx.fillStyle = e.color;
|
||
ctx.fillRect(e.x, e.y, e.w, e.h);
|
||
ctx.fillStyle = '#111827';
|
||
ctx.font = '12px sans-serif';
|
||
ctx.fillText(e.label + (e.state === 'active' ? ' ★' : ''), e.x - 4, e.y - 6);
|
||
});
|
||
},
|
||
findEntity: function (x, y) {
|
||
for (var i = entities.length - 1; i >= 0; i--) {
|
||
var e = entities[i];
|
||
if (x >= e.x && x <= e.x + e.w && y >= e.y && y <= e.y + e.h) return e;
|
||
}
|
||
return null;
|
||
}
|
||
};
|
||
if (canvas) canvas.onclick = function (ev) {
|
||
if (engine.paused) return;
|
||
var r = canvas.getBoundingClientRect();
|
||
var ex = ev.clientX - r.left, ey = ev.clientY - r.top;
|
||
var ent = engine.findEntity(ex, ey);
|
||
if (ent) onEntityClick(ent);
|
||
};
|
||
engine.draw();
|
||
}
|
||
|
||
/* ---------- W-08i 玩中交互:点选实体触发脚本逻辑 ---------- */
|
||
function onEntityClick(entity) {
|
||
if (!entity) return;
|
||
if (engine && engine.paused) { setStatus('已暂停,请先继续'); return; }
|
||
// 得分规则:点击宝箱/史莱姆得 10 分并触发实体脚本(state 变化)
|
||
score += 10;
|
||
refreshHud();
|
||
logLine('点击实体 ' + entity.label + '(' + entity.id + ')→ 触发脚本 action,得分 +10');
|
||
if (engine && engine.dispatchEvent) {
|
||
engine.dispatchEvent({type: 'click', entity_id: entity.id, payload: {score: 10}});
|
||
} else if (engine && engine.entities) {
|
||
var hit = null;
|
||
for (var i = 0; i < engine.entities.length; i++) {
|
||
if (engine.entities[i].id === entity.id) { hit = engine.entities[i]; break; }
|
||
}
|
||
if (hit) { hit.state = 'active'; engine.draw && engine.draw(); }
|
||
setTimeout(function () { if (hit) { hit.state = 'idle'; engine.draw && engine.draw(); } }, 800);
|
||
}
|
||
}
|
||
|
||
function onEntityState(e) {
|
||
logLine('实体状态: ' + (e && (e.entity_id || e.id)) + ' -> ' + (e && e.state));
|
||
}
|
||
|
||
function onScriptLog(msg) {
|
||
logLine('脚本: ' + (msg && msg.message || msg || ''));
|
||
}
|
||
|
||
function onEngineError(err) {
|
||
logLine('运行时错误: ' + (err && err.message || err || '') + '(已降级继续)');
|
||
}
|
||
|
||
function logLine(text) {
|
||
var box = $id('game_log');
|
||
if (!box) return;
|
||
var line = document.createElement('div');
|
||
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + text;
|
||
box.appendChild(line);
|
||
box.scrollTop = box.scrollHeight;
|
||
while (box.children.length > 60) box.removeChild(box.firstChild);
|
||
}
|
||
|
||
/* ---------- W-08c 暂停 ---------- */
|
||
function pauseGame() {
|
||
if (!session) return;
|
||
api('game_pause', {session_id: CFG.sessionId}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('暂停失败: ' + (res && res.error)); return; }
|
||
if (engine && engine.pause) engine.pause();
|
||
if (clockTimer) clearInterval(clockTimer);
|
||
setStatus('已暂停');
|
||
$id('btn_pause').style.display = 'none';
|
||
$id('btn_resume').style.display = 'inline-block';
|
||
});
|
||
}
|
||
|
||
/* ---------- W-08d 继续 ---------- */
|
||
function resumeGame() {
|
||
if (!session) return;
|
||
api('game_resume', {session_id: CFG.sessionId}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('继续失败: ' + (res && res.error)); return; }
|
||
if (engine && engine.resume) engine.resume();
|
||
startClock();
|
||
setStatus('运行中');
|
||
$id('btn_pause').style.display = 'inline-block';
|
||
$id('btn_resume').style.display = 'none';
|
||
});
|
||
}
|
||
|
||
/* ---------- W-08e/g 结束结算 ---------- */
|
||
function endGame() {
|
||
if (!session) return;
|
||
var dur = elapsedMs();
|
||
api('game_end', {
|
||
session_id: CFG.sessionId, score: score, duration_ms: dur, level: level,
|
||
result_json: JSON.stringify({actions: 'clicks', entities: engine ? (engine.entities || []).length : 0})
|
||
}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('结算失败: ' + (res && res.error)); return; }
|
||
if (engine && engine.stop) engine.stop();
|
||
if (clockTimer) clearInterval(clockTimer);
|
||
var r = res.data.result;
|
||
$id('result_score').textContent = r.score;
|
||
$id('result_level').textContent = r.level;
|
||
$id('result_duration').textContent = Math.round(r.duration_ms / 1000) + ' 秒';
|
||
$id('result_time').textContent = r.created_at;
|
||
showPanel('result');
|
||
setStatus('已结束');
|
||
enableControls(false);
|
||
});
|
||
}
|
||
|
||
/* ---------- W-08f 重新开始 ---------- */
|
||
function restartGame() {
|
||
if (!session) return;
|
||
api('game_restart', {session_id: CFG.sessionId}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('重开失败: ' + (res && res.error)); return; }
|
||
score = 0; level = 1; startTs = Date.now();
|
||
refreshHud();
|
||
if (engine && engine.restart) engine.restart(); else if (engine && engine.start) engine.start();
|
||
startClock();
|
||
showPanel('hud');
|
||
enableControls(true);
|
||
$id('btn_pause').style.display = 'inline-block';
|
||
$id('btn_resume').style.display = 'none';
|
||
setStatus('运行中');
|
||
});
|
||
}
|
||
|
||
/* ---------- W-08h 分享链接 ---------- */
|
||
function shareGame() {
|
||
if (!session) return;
|
||
api('game_share', {action: 'generate', session_id: CFG.sessionId}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('分享失败: ' + (res && res.error)); return; }
|
||
var url = location.origin + apiBase() + '/game/game_play.html?share_token=' + res.data.share_token;
|
||
$id('share_url').value = url;
|
||
$id('share_box').style.display = 'block';
|
||
logLine('已生成分享链接');
|
||
});
|
||
}
|
||
|
||
/* ---------- W-08j 退出保存进度 ---------- */
|
||
function saveProgress() {
|
||
if (!session) return;
|
||
api('game_save', {
|
||
session_id: CFG.sessionId, progress_json: JSON.stringify({entities: (engine && engine.entities || []).length}),
|
||
score: score, level: level, elapsed_ms: elapsedMs()
|
||
}).then(function (res) {
|
||
if (res && res.success) { logLine('进度已保存'); }
|
||
});
|
||
}
|
||
|
||
function exitGame() {
|
||
saveProgress();
|
||
setTimeout(function () { window.close(); }, 400);
|
||
}
|
||
|
||
/* ---------- 分享链接进入(W-08h 解析 token) ---------- */
|
||
function enterByShareToken(token) {
|
||
api('game_share', {action: 'parse', token: token}).then(function (res) {
|
||
if (!res || !res.success) { setStatus('分享链接无效: ' + (res && res.error)); return; }
|
||
CFG.gameId = res.data.game_id;
|
||
CFG.sessionId = res.data.session_id;
|
||
CFG.shareToken = token;
|
||
CFG.worldId = res.data.world_id || '';
|
||
$id('page_title').textContent = '分享进入 · ' + res.data.game_name;
|
||
setStatus('已通过分享链接进入');
|
||
startGame(); // 复用会话进入运行态
|
||
});
|
||
}
|
||
|
||
/* ---------- UI 辅助 ---------- */
|
||
function showPanel(name) {
|
||
['hud', 'result'].forEach(function (p) {
|
||
$id('panel_' + p).style.display = (p === name) ? 'block' : 'none';
|
||
});
|
||
}
|
||
|
||
function enableControls(on) {
|
||
['btn_pause', 'btn_resume', 'btn_end', 'btn_restart', 'btn_share', 'btn_save', 'btn_exit']
|
||
.forEach(function (id) { var b = $id(id); if (b) b.disabled = !on; });
|
||
}
|
||
|
||
function bindEvents() {
|
||
$id('btn_pause').onclick = pauseGame;
|
||
$id('btn_resume').onclick = resumeGame;
|
||
$id('btn_end').onclick = endGame;
|
||
$id('btn_restart').onclick = restartGame;
|
||
$id('btn_share').onclick = shareGame;
|
||
$id('btn_save').onclick = saveProgress;
|
||
$id('btn_exit').onclick = exitGame;
|
||
$id('btn_copy').onclick = function () {
|
||
var u = $id('share_url');
|
||
u.select();
|
||
try { document.execCommand('copy'); setStatus('已复制分享链接'); } catch (e) { setStatus('复制失败,请手动复制'); }
|
||
};
|
||
$id('btn_result_again').onclick = restartGame;
|
||
$id('btn_result_back').onclick = exitGame;
|
||
window.addEventListener('beforeunload', function () { saveProgress(); });
|
||
}
|
||
|
||
/* ---------- 启动 ---------- */
|
||
function boot() {
|
||
var qs = new URLSearchParams(location.search);
|
||
CFG.gameId = qs.get('game_id') || '';
|
||
CFG.sessionId = qs.get('session_id') || '';
|
||
statusBar = $id('hud_status');
|
||
bindEvents();
|
||
showPanel('hud');
|
||
enableControls(false);
|
||
var token = qs.get('share_token');
|
||
if (token) { enterByShareToken(token); return; }
|
||
if (CFG.gameId) { startGame(); return; }
|
||
setStatus('缺少 game_id / share_token');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', boot);
|
||
} else {
|
||
boot();
|
||
}
|
||
})();
|