feat(工作台): 单页游戏工作台——对象树+事件脚本编辑+测试新窗口
This commit is contained in:
parent
b9d4a650c5
commit
30d357c050
64
wwwroot/workbench/workbench.html
Normal file
64
wwwroot/workbench/workbench.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>元景 · 游戏工作台</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #F3F4F6; color: #1F2937; height: 100vh; display: flex; flex-direction: column; }
|
||||
header { background: #111827; color: #F9FAFB; padding: 10px 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
header h1 { font-size: 16px; margin-right: 8px; }
|
||||
header input[type=text] { padding: 6px 10px; border-radius: 4px; border: 1px solid #374151; background: #1F2937; color: #F9FAFB; width: 220px; }
|
||||
header label { font-size: 13px; display: flex; gap: 6px; align-items: center; }
|
||||
header button { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.btn-primary { background: #2563EB; color: #fff; }
|
||||
.btn-green { background: #16A34A; color: #fff; }
|
||||
.btn-gray { background: #4B5563; color: #fff; }
|
||||
main { flex: 1; display: flex; gap: 0; overflow: hidden; }
|
||||
#tree { width: 280px; background: #fff; border-right: 1px solid #E5E7EB; overflow-y: auto; padding: 10px; }
|
||||
#tree h3 { font-size: 13px; color: #6B7280; margin: 8px 0 4px; }
|
||||
.node { padding: 6px 8px; border-radius: 4px; cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.node:hover { background: #EFF6FF; }
|
||||
.node.sel { background: #DBEAFE; font-weight: 600; }
|
||||
.node .add { color: #2563EB; font-weight: 700; padding: 0 6px; }
|
||||
#editor { flex: 1; display: flex; flex-direction: column; padding: 12px; gap: 10px; overflow: hidden; }
|
||||
#editor .bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
#editor select, #editor button { padding: 6px 10px; border-radius: 4px; border: 1px solid #D1D5DB; background: #fff; font-size: 13px; cursor: pointer; }
|
||||
#editor textarea { flex: 1; border: 1px solid #D1D5DB; border-radius: 6px; padding: 12px; font-family: "JetBrains Mono", Consolas, monospace; font-size: 13px; resize: none; background: #1A1D26; color: #D1D5DB; }
|
||||
#msg { font-size: 13px; min-height: 20px; }
|
||||
.ok { color: #16A34A; } .err { color: #DC2626; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🎮 游戏工作台</h1>
|
||||
<input type="text" id="game_name" placeholder="游戏名称">
|
||||
<label><input type="checkbox" id="game_shared"> 世界共享</label>
|
||||
<button class="btn-primary" onclick="wb.saveGame()">保存游戏</button>
|
||||
<button class="btn-green" onclick="wb.test()">▶ 测试(新窗口运行 demo)</button>
|
||||
<button class="btn-gray" onclick="wb.reload()">刷新</button>
|
||||
</header>
|
||||
<main>
|
||||
<div id="tree">
|
||||
<h3>对象树(世界 1 · 场景 N · 实体 N)</h3>
|
||||
<div id="tree_body">加载中…</div>
|
||||
</div>
|
||||
<div id="editor">
|
||||
<div class="bar">
|
||||
<span id="cur_obj" style="font-weight:600;">未选择对象</span>
|
||||
<select id="event_sel">
|
||||
<option value="event_start">事件:开始</option>
|
||||
<option value="event_click">事件:点击</option>
|
||||
<option value="event_timer">事件:定时</option>
|
||||
<option value="event_collision">事件:碰撞</option>
|
||||
</select>
|
||||
<button class="btn-primary" onclick="wb.saveScript()">💾 保存脚本</button>
|
||||
<button onclick="wb.validate()">校验</button>
|
||||
</div>
|
||||
<textarea id="code" placeholder="为当前对象的所选事件编写逻辑脚本(Python)… 可用上下文:entity / scene / world / game / api(事件冒泡:实体→场景→世界→游戏)"></textarea>
|
||||
<div id="msg"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="workbench.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
143
wwwroot/workbench/workbench.js
Normal file
143
wwwroot/workbench/workbench.js
Normal file
@ -0,0 +1,143 @@
|
||||
/* 元景游戏工作台:game 聚合根单页编辑器 */
|
||||
(function () {
|
||||
function api(path, params) {
|
||||
return fetch(path, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(params || {})
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
function base() { return '/scense_game'; }
|
||||
|
||||
var state = { game: null, data: null, sel: null }; // sel: {type, id, name}
|
||||
|
||||
function msg(t, ok) {
|
||||
var el = document.getElementById('msg');
|
||||
el.textContent = t;
|
||||
el.className = ok ? 'ok' : 'err';
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
var name = document.getElementById('game_name').value.trim();
|
||||
if (state.game) { msg('游戏已存在: ' + state.game.id, true); return; }
|
||||
if (!name) { msg('先填游戏名称', false); return; }
|
||||
api(base() + '/game/api/game_create.dspy', {name: name,
|
||||
description: document.getElementById('game_shared').checked ? 'shared' : 'independent'})
|
||||
.then(function (r) {
|
||||
if (r.success) { msg('游戏创建成功,世界已自动建立', true); reload(); }
|
||||
else msg(r.message || '创建失败', false);
|
||||
});
|
||||
}
|
||||
|
||||
function reload() {
|
||||
// 列表取第一个 game 或新建后带入
|
||||
api(base() + '/game/api/game_list.dspy', {}).then(function (r) {
|
||||
var games = (r.data && r.data.games) || [];
|
||||
if (!games.length) { state.game = null; renderTree(null); return; }
|
||||
var gid = games[0].game_id || games[0].id;
|
||||
api(base() + '/game/api/game_workbench.dspy', {game_id: gid}).then(function (w) {
|
||||
if (!w.success) { msg(w.message, false); return; }
|
||||
state.game = w.data.game; state.data = w.data;
|
||||
document.getElementById('game_name').value = (w.data.game && w.data.game.name) || '';
|
||||
renderTree(w.data);
|
||||
if (!state.sel) selectWorld();
|
||||
else loadSel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderTree(d) {
|
||||
var el = document.getElementById('tree_body');
|
||||
if (!d || !d.world) { el.innerHTML = '<div style="color:#9CA3AF">尚无游戏,先填名称保存</div>'; return; }
|
||||
var h = '';
|
||||
h += '<div class="node' + (state.sel && state.sel.type === 'world' ? ' sel' : '') + '" onclick="wb.selObj(\'world\',\'' + d.world.id + '\',\'' + esc(d.world.name) + '\')">🌍 ' + esc(d.world.name) + '</div>';
|
||||
(d.scenes || []).forEach(function (s) {
|
||||
h += '<div class="node' + (state.sel && state.sel.id === s.id ? ' sel' : '') + '" style="margin-left:12px" onclick="wb.selObj(\'scene\',\'' + s.id + '\',\'' + esc(s.name) + '\')">🎬 ' + esc(s.name) + '</div>';
|
||||
});
|
||||
h += '<div class="node" style="margin-left:12px;color:#2563EB" onclick="wb.addScene()">+ 场景</div>';
|
||||
(d.entities || []).forEach(function (e) {
|
||||
h += '<div class="node' + (state.sel && state.sel.id === e.id ? ' sel' : '') + '" style="margin-left:24px" onclick="wb.selObj(\'entity\',\'' + e.id + '\',\'' + esc(e.name) + '\')">📦 ' + esc(e.name) + (e.scene_id ? '' : '(世界级)') + '</div>';
|
||||
});
|
||||
h += '<div class="node" style="margin-left:24px;color:#2563EB" onclick="wb.addEntity()">+ 实体</div>';
|
||||
el.innerHTML = h;
|
||||
}
|
||||
function esc(s) { return String(s || '').replace(/'/g, "\\'").replace(/</g, '<'); }
|
||||
|
||||
function selectWorld() { if (state.data && state.data.world) selObj('world', state.data.world.id, state.data.world.name); }
|
||||
|
||||
function selObj(type, id, name) {
|
||||
state.sel = {type: type, id: id, name: name};
|
||||
document.getElementById('cur_obj').textContent =
|
||||
({world: '🌍 世界', scene: '🎬 场景', entity: '📦 实体'})[type] + ' · ' + name;
|
||||
renderTree(state.data);
|
||||
loadScript();
|
||||
}
|
||||
|
||||
function bindParams() {
|
||||
var s = state.sel;
|
||||
if (!s) return null;
|
||||
var p = {game_id: state.game.id, trigger_event: document.getElementById('event_sel').value};
|
||||
if (s.type === 'world') p.world_id = s.id;
|
||||
if (s.type === 'scene') p.scene_id = s.id;
|
||||
if (s.type === 'entity') p.entity_id = s.id;
|
||||
return p;
|
||||
}
|
||||
|
||||
function loadScript() {
|
||||
var p = bindParams();
|
||||
if (!p) return;
|
||||
var sc = (state.data.scripts || []).find(function (x) {
|
||||
return (state.sel.type === 'world' && x.world_id === state.sel.id && x.trigger_event === p.trigger_event) ||
|
||||
(state.sel.type === 'scene' && x.scene_id === state.sel.id && x.trigger_event === p.trigger_event) ||
|
||||
(state.sel.type === 'entity' && x.entity_id === state.sel.id && x.trigger_event === p.trigger_event);
|
||||
});
|
||||
document.getElementById('code').value = sc ? (sc.content || '') : '';
|
||||
state.cur_script = sc || null;
|
||||
}
|
||||
|
||||
function saveScript() {
|
||||
var p = bindParams();
|
||||
if (!p) { msg('先选择对象', false); return; }
|
||||
p.name = state.sel.name + '_' + p.trigger_event;
|
||||
p.code = 'SC_' + (state.cur_script ? state.cur_script.code : (state.sel.id.slice(0, 8) + '_' + p.trigger_event));
|
||||
p.content = document.getElementById('code').value;
|
||||
p.script_type = '0';
|
||||
if (state.cur_script) p.id = state.cur_script.id;
|
||||
var url = state.cur_script ? '/script_engine/api/script_engine_update.dspy' : '/script_engine/api/script_engine_create.dspy';
|
||||
api(url, p).then(function (r) {
|
||||
if (r && r.success === false) { msg(r.error || r.message || '保存失败', false); return; }
|
||||
msg(state.cur_script ? '脚本已更新' : '脚本已保存', true);
|
||||
reload();
|
||||
}).catch(function (e) { msg('保存失败: ' + e, false); });
|
||||
}
|
||||
|
||||
function validate() {
|
||||
api('/script_engine/api/validate_script.dspy', {content: document.getElementById('code').value})
|
||||
.then(function (r) { msg(JSON.stringify(r).slice(0, 200), true); });
|
||||
}
|
||||
|
||||
function addScene() {
|
||||
var name = prompt('场景名称');
|
||||
if (!name) return;
|
||||
api(base() + '/game/api/scene_add.dspy', {game_id: state.game.id, name: name})
|
||||
.then(function (r) { r.success ? (msg('场景已加', true), reload()) : msg(r.message, false); });
|
||||
}
|
||||
|
||||
function addEntity() {
|
||||
var name = prompt('实体名称(留空场景ID=世界级实体)');
|
||||
if (!name) return;
|
||||
var sceneId = state.sel && state.sel.type === 'scene' ? state.sel.id : '';
|
||||
api(base() + '/game/api/entity_add.dspy', {game_id: state.game.id, scene_id: sceneId, name: name})
|
||||
.then(function (r) { r.success ? (msg('实体已加', true), reload()) : msg(r.message, false); });
|
||||
}
|
||||
|
||||
function test() {
|
||||
if (!state.game) { msg('先保存游戏', false); return; }
|
||||
var wid = state.data && state.data.world ? state.data.world.id : '';
|
||||
window.open('/scense_demo/demo_viewer.ui?world_id=' + wid + '&game_id=' + state.game.id, '_blank');
|
||||
}
|
||||
|
||||
window.wb = {saveGame: saveGame, reload: reload, selObj: selObj, addScene: addScene,
|
||||
addEntity: addEntity, saveScript: saveScript, validate: validate, test: test};
|
||||
reload();
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user