feat(工作台v3): 纯bricks声明式工作台——Tree懒加载对象树+CodeEditor编程+PopupWindow建游戏/场景/实体;删除裸html+js版
This commit is contained in:
parent
3847a1ec65
commit
833ffdea03
@ -3,6 +3,9 @@
|
|||||||
from scense_game.aggregation import (
|
from scense_game.aggregation import (
|
||||||
game_create, game_workbench, scene_add, entity_add,
|
game_create, game_workbench, scene_add, entity_add,
|
||||||
)
|
)
|
||||||
|
from scense_game.workbench_api import (
|
||||||
|
workbench_tree, workbench_load_script, workbench_save_script,
|
||||||
|
)
|
||||||
from scense_game.core import (
|
from scense_game.core import (
|
||||||
game_list_games, game_list_sessions, game_list_results,
|
game_list_games, game_list_sessions, game_list_results,
|
||||||
game_start_session, game_pause_session, game_resume_session,
|
game_start_session, game_pause_session, game_resume_session,
|
||||||
@ -21,6 +24,9 @@ def load_scense_game(env=None):
|
|||||||
env.game_workbench = game_workbench
|
env.game_workbench = game_workbench
|
||||||
env.scene_add = scene_add
|
env.scene_add = scene_add
|
||||||
env.entity_add = entity_add
|
env.entity_add = entity_add
|
||||||
|
env.workbench_tree = workbench_tree
|
||||||
|
env.workbench_load_script = workbench_load_script
|
||||||
|
env.workbench_save_script = workbench_save_script
|
||||||
env.game_list_games = game_list_games
|
env.game_list_games = game_list_games
|
||||||
env.game_list_sessions = game_list_sessions
|
env.game_list_sessions = game_list_sessions
|
||||||
env.game_list_results = game_list_results
|
env.game_list_results = game_list_results
|
||||||
|
|||||||
161
scense_game/workbench_api.py
Normal file
161
scense_game/workbench_api.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""域模型 v2 聚合 API(补):对象树 + 保存脚本(声明式工作台后端)。"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from appPublic.log import info, error, exception
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dbname():
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
return ServerEnv().get_module_dbname('scense_game')
|
||||||
|
|
||||||
|
|
||||||
|
async def workbench_tree(ns):
|
||||||
|
"""对象树(Tree 懒加载)。id 空=根(全部游戏+其世界);
|
||||||
|
id=game:xx→世界;world:xx→场景+世界级实体;scene:xx→实体。"""
|
||||||
|
gid = (ns or {}).get('game_id') or ''
|
||||||
|
nid = (ns or {}).get('id') or ''
|
||||||
|
nodes = []
|
||||||
|
dbname = _get_dbname()
|
||||||
|
try:
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
if not nid:
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select id, name from game order by created_at', {})
|
||||||
|
for r in rows:
|
||||||
|
nodes.append({'id': 'game:' + r.id, 'parentid': '',
|
||||||
|
'text': '🎮 ' + r.name, 'is_leaf': 0,
|
||||||
|
'obj_type': 'game', 'real_id': r.id})
|
||||||
|
if gid:
|
||||||
|
w = await sor.sqlExe(
|
||||||
|
'select id, name from world where game_id=${gid}$', {'gid': gid})
|
||||||
|
for r in w:
|
||||||
|
nodes.append({'id': 'world:' + r.id, 'parentid': '',
|
||||||
|
'text': '🌍 ' + r.name, 'is_leaf': 0,
|
||||||
|
'obj_type': 'world', 'real_id': r.id, 'game_id': gid})
|
||||||
|
elif nid.startswith('game:'):
|
||||||
|
g_real = nid.split(':', 1)[1]
|
||||||
|
w = await sor.sqlExe(
|
||||||
|
'select id, name from world where game_id=${gid}$', {'gid': g_real})
|
||||||
|
for r in w:
|
||||||
|
nodes.append({'id': 'world:' + r.id, 'parentid': nid,
|
||||||
|
'text': '🌍 ' + r.name, 'is_leaf': 0,
|
||||||
|
'obj_type': 'world', 'real_id': r.id, 'game_id': g_real})
|
||||||
|
elif nid.startswith('world:'):
|
||||||
|
w_real = nid.split(':', 1)[1]
|
||||||
|
sc = await sor.sqlExe(
|
||||||
|
'select id, name from scene where world_id=${wid}$', {'wid': w_real})
|
||||||
|
for r in sc:
|
||||||
|
nodes.append({'id': 'scene:' + r.id, 'parentid': nid,
|
||||||
|
'text': '🎬 ' + r.name, 'is_leaf': 0,
|
||||||
|
'obj_type': 'scene', 'real_id': r.id})
|
||||||
|
ent = await sor.sqlExe(
|
||||||
|
"select id, name from entity where world_id=${wid}$ and (scene_id is null or scene_id='')",
|
||||||
|
{'wid': w_real})
|
||||||
|
for r in ent:
|
||||||
|
nodes.append({'id': 'entity:' + r.id, 'parentid': nid,
|
||||||
|
'text': '📦 ' + r.name + '(世界级)', 'is_leaf': 1,
|
||||||
|
'obj_type': 'entity', 'real_id': r.id})
|
||||||
|
elif nid.startswith('scene:'):
|
||||||
|
s_real = nid.split(':', 1)[1]
|
||||||
|
ent = await sor.sqlExe(
|
||||||
|
'select id, name from entity where scene_id=${sid}$', {'sid': s_real})
|
||||||
|
for r in ent:
|
||||||
|
nodes.append({'id': 'entity:' + r.id, 'parentid': nid,
|
||||||
|
'text': '📦 ' + r.name, 'is_leaf': 1,
|
||||||
|
'obj_type': 'entity', 'real_id': r.id})
|
||||||
|
return {'success': True, 'data': nodes}
|
||||||
|
except Exception as e:
|
||||||
|
exception('workbench_tree: %s' % e)
|
||||||
|
return {'success': False, 'message': '对象树查询失败: %s' % e, 'data': []}
|
||||||
|
|
||||||
|
|
||||||
|
async def workbench_load_script(ns):
|
||||||
|
"""读当前对象+事件的已有脚本。ns: {obj_type, obj_id, game_id, trigger_event}"""
|
||||||
|
obj_type = (ns or {}).get('obj_type') or ''
|
||||||
|
obj_id = (ns or {}).get('obj_id') or ''
|
||||||
|
evt = (ns or {}).get('trigger_event') or ''
|
||||||
|
if not obj_id or not evt:
|
||||||
|
return {'success': False, 'message': 'obj_id/trigger_event 必填'}
|
||||||
|
dbname = _get_dbname()
|
||||||
|
try:
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
col = {'game': 'game_id', 'world': 'world_id',
|
||||||
|
'scene': 'scene_id', 'entity': 'entity_id'}.get(obj_type)
|
||||||
|
if not col:
|
||||||
|
return {'success': False, 'message': 'obj_type 非法'}
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select id, name, content, world_id, entity_id from script_engine '
|
||||||
|
'where ' + col + '=${oid}$ and trigger_event=${evt}$ limit 1',
|
||||||
|
{'oid': obj_id, 'evt': evt})
|
||||||
|
if rows:
|
||||||
|
return {'success': True, 'data': {'id': rows[0].id, 'name': rows[0].name,
|
||||||
|
'content': rows[0].content or ''}}
|
||||||
|
return {'success': True, 'data': None}
|
||||||
|
except Exception as e:
|
||||||
|
exception('workbench_load_script: %s' % e)
|
||||||
|
return {'success': False, 'message': '读取脚本失败: %s' % e}
|
||||||
|
|
||||||
|
|
||||||
|
async def workbench_save_script(ns):
|
||||||
|
"""保存脚本(存在则更新)。ns: {obj_type, obj_id, game_id, trigger_event, content}"""
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
obj_type = (ns or {}).get('obj_type') or ''
|
||||||
|
obj_id = (ns or {}).get('obj_id') or ''
|
||||||
|
gid = (ns or {}).get('game_id') or ''
|
||||||
|
evt = (ns or {}).get('trigger_event') or ''
|
||||||
|
content = (ns or {}).get('content') or ''
|
||||||
|
if not obj_id or not evt:
|
||||||
|
return {'success': False, 'message': 'obj_id/trigger_event 必填'}
|
||||||
|
dbname = _get_dbname()
|
||||||
|
try:
|
||||||
|
async with DBPools().sqlorContext(dbname) as sor:
|
||||||
|
# 解析对象所属世界(world_id 为 NOT NULL 约束)
|
||||||
|
wid = ''
|
||||||
|
if obj_type == 'world':
|
||||||
|
wid = obj_id
|
||||||
|
elif obj_type == 'scene':
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select world_id from scene where id=${oid}$', {'oid': obj_id})
|
||||||
|
wid = rows[0].world_id if rows else ''
|
||||||
|
elif obj_type == 'entity':
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select world_id from entity where id=${oid}$', {'oid': obj_id})
|
||||||
|
wid = rows[0].world_id if rows else ''
|
||||||
|
elif obj_type == 'game':
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select id from world where game_id=${gid}$', {'gid': obj_id})
|
||||||
|
wid = rows[0].id if rows else ''
|
||||||
|
if not gid and wid:
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
'select game_id from world where id=${wid}$', {'wid': wid})
|
||||||
|
gid = (rows[0].game_id if rows and rows[0].game_id else '') or gid
|
||||||
|
if not wid:
|
||||||
|
return {'success': False, 'message': '无法解析对象所属世界'}
|
||||||
|
col = {'game': 'game_id', 'world': 'world_id',
|
||||||
|
'scene': 'scene_id', 'entity': 'entity_id'}[obj_type]
|
||||||
|
old = await sor.sqlExe(
|
||||||
|
'select id from script_engine where ' + col + '=${oid}$ and trigger_event=${evt}$ limit 1',
|
||||||
|
{'oid': obj_id, 'evt': evt})
|
||||||
|
import time
|
||||||
|
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
if old:
|
||||||
|
await sor.sqlExe(
|
||||||
|
'update script_engine set content=${content}$, updated_at=${now}$ where id=${sid}$',
|
||||||
|
{'content': content, 'now': now, 'sid': old[0].id})
|
||||||
|
sid = old[0].id
|
||||||
|
else:
|
||||||
|
sid = getID()
|
||||||
|
fields = {'id': sid, 'name': obj_type + '_' + obj_id[:8] + '_' + evt,
|
||||||
|
'code': 'SC_' + obj_id[:8] + '_' + evt,
|
||||||
|
'script_type': '0', 'trigger_event': evt, 'content': content,
|
||||||
|
'status': 'active', 'world_id': wid, 'game_id': gid,
|
||||||
|
'created_at': now, 'updated_at': now}
|
||||||
|
if col != 'world_id':
|
||||||
|
fields[col] = obj_id
|
||||||
|
await sor.C('script_engine', fields)
|
||||||
|
return {'success': True, 'data': {'script_id': sid}}
|
||||||
|
except Exception as e:
|
||||||
|
exception('workbench_save_script: %s' % e)
|
||||||
|
return {'success': False, 'message': '保存脚本失败: %s' % e}
|
||||||
4
wwwroot/workbench/api/load_script.dspy
Normal file
4
wwwroot/workbench/api/load_script.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# scense_game/workbench/api/load_script.dspy —— 读当前对象+事件的脚本
|
||||||
|
r = await workbench_load_script(dict(params_kw))
|
||||||
|
return json.dumps(r, ensure_ascii=False, default=str)
|
||||||
4
wwwroot/workbench/api/save_script.dspy
Normal file
4
wwwroot/workbench/api/save_script.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# scense_game/workbench/api/save_script.dspy —— 保存脚本
|
||||||
|
r = await workbench_save_script(dict(params_kw))
|
||||||
|
return json.dumps(r, ensure_ascii=False, default=str)
|
||||||
6
wwwroot/workbench/api/tree.dspy
Normal file
6
wwwroot/workbench/api/tree.dspy
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# scense_game/workbench/api/tree.dspy —— 对象树懒加载(Tree dataurl)
|
||||||
|
r = await workbench_tree(dict(params_kw))
|
||||||
|
if not r.get('success'):
|
||||||
|
return []
|
||||||
|
return r.get('data') or []
|
||||||
@ -1,66 +0,0 @@
|
|||||||
<!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>
|
|
||||||
<select id="game_sel" style="padding:6px 10px;border-radius:4px;border:1px solid #374151;background:#1F2937;color:#F9FAFB"></select>
|
|
||||||
<button class="btn-gray" onclick="wb.newGame()">+ 新建游戏</button>
|
|
||||||
<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?v=20260830c"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
/* 元景游戏工作台 v2:game 聚合根单页编辑器(内联输入,无 prompt) */
|
|
||||||
(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, games: [], cur_script: null, inline: null };
|
|
||||||
|
|
||||||
function msg(t, ok) {
|
|
||||||
var el = document.getElementById('msg');
|
|
||||||
el.textContent = t;
|
|
||||||
el.className = ok ? 'ok' : 'err';
|
|
||||||
}
|
|
||||||
|
|
||||||
function newGame() {
|
|
||||||
state.game = null; state.data = null; state.sel = null; state.cur_script = null;
|
|
||||||
document.getElementById('game_name').value = '';
|
|
||||||
document.getElementById('game_sel').value = '';
|
|
||||||
renderTree(null);
|
|
||||||
document.getElementById('cur_obj').textContent = '未选择对象';
|
|
||||||
document.getElementById('code').value = '';
|
|
||||||
msg('填写名称后点保存游戏(将自动创建 1:1 世界)', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveGame() {
|
|
||||||
var name = document.getElementById('game_name').value.trim();
|
|
||||||
if (state.game) { msg('当前游戏: ' + state.game.name + '(' + 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(r.data.game_id); }
|
|
||||||
else msg(r.message || '创建失败', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchGame(gid) {
|
|
||||||
if (!gid) return;
|
|
||||||
state.game = null; state.sel = null;
|
|
||||||
reload(gid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload(forceGid) {
|
|
||||||
api(base() + '/game/api/game_list.dspy', {}).then(function (r) {
|
|
||||||
var games = (r.data && r.data.games) || r || [];
|
|
||||||
if (!Array.isArray(games)) games = [];
|
|
||||||
state.games = games;
|
|
||||||
var sel = document.getElementById('game_sel');
|
|
||||||
sel.innerHTML = '<option value="">— 选择游戏 —</option>' +
|
|
||||||
games.map(function (g) {
|
|
||||||
var gid = g.game_id || g.id;
|
|
||||||
return '<option value="' + gid + '">' + (g.game_name || g.name) + '</option>';
|
|
||||||
}).join('');
|
|
||||||
var gid = forceGid || (state.game && state.game.id) || (games[0] && (games[0].game_id || games[0].id));
|
|
||||||
if (!gid) { state.game = null; renderTree(null); return; }
|
|
||||||
sel.value = gid;
|
|
||||||
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) loadSelKeep(); else selectWorld();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTree(d) {
|
|
||||||
var el = document.getElementById('tree_body');
|
|
||||||
if (!d || !d.world) { el.innerHTML = '<div style="color:#9CA3AF">尚无游戏,先填名称保存</div>'; return; }
|
|
||||||
var h = '';
|
|
||||||
h += node('world', d.world.id, '🌍 ' + esc(d.world.name));
|
|
||||||
(d.scenes || []).forEach(function (s) {
|
|
||||||
h += node('scene', s.id, '🎬 ' + esc(s.name), 12);
|
|
||||||
});
|
|
||||||
h += addRow('scene', '+ 场景', 12);
|
|
||||||
(d.entities || []).forEach(function (e) {
|
|
||||||
h += node('entity', e.id, '📦 ' + esc(e.name) + (e.scene_id ? '' : '(世界级)'), 24);
|
|
||||||
});
|
|
||||||
h += addRow('entity', '+ 实体', 24);
|
|
||||||
el.innerHTML = h;
|
|
||||||
}
|
|
||||||
function node(type, id, label, ml) {
|
|
||||||
var sel = state.sel && state.sel.id === id ? ' sel' : '';
|
|
||||||
return '<div class="node' + sel + '" style="margin-left:' + (ml || 0) + 'px" ' +
|
|
||||||
'onclick="wb.selObj(\'' + type + '\',\'' + id + '\')">' + label + '</div>';
|
|
||||||
}
|
|
||||||
function addRow(type, label, ml) {
|
|
||||||
if (state.inline === type) {
|
|
||||||
return '<div class="node" style="margin-left:' + ml + 'px;flex-direction:column;align-items:stretch">' +
|
|
||||||
'<input type="text" id="inline_name" placeholder="' + (type === 'scene' ? '场景名称' : '实体名称') + '" ' +
|
|
||||||
'style="padding:4px 8px;border:1px solid #93C5FD;border-radius:4px;margin-bottom:4px" ' +
|
|
||||||
'onkeydown="if(event.key===\'Enter\')wb.inlineOk(\'' + type + '\')">' +
|
|
||||||
'<div style="display:flex;gap:6px"><button onclick="wb.inlineOk(\'' + type + '\')" style="flex:1;padding:4px;background:#2563EB;color:#fff;border:none;border-radius:4px;cursor:pointer">添加</button>' +
|
|
||||||
'<button onclick="wb.inlineCancel()" style="flex:1;padding:4px;background:#E5E7EB;border:none;border-radius:4px;cursor:pointer">取消</button></div></div>';
|
|
||||||
}
|
|
||||||
return '<div class="node" style="margin-left:' + ml + 'px;color:#2563EB" onclick="wb.inlineStart(\'' + type + '\')">' + label + '</div>';
|
|
||||||
}
|
|
||||||
function esc(s) { return String(s || '').replace(/</g, '<'); }
|
|
||||||
|
|
||||||
function inlineStart(type) { state.inline = type; renderTree(state.data); setTimeout(function () { var i = document.getElementById('inline_name'); if (i) i.focus(); }, 50); }
|
|
||||||
function inlineCancel() { state.inline = null; renderTree(state.data); }
|
|
||||||
function inlineOk(type) {
|
|
||||||
var name = (document.getElementById('inline_name') || {}).value;
|
|
||||||
if (!name || !name.trim()) { msg('名称不能为空', false); return; }
|
|
||||||
state.inline = null;
|
|
||||||
if (type === 'scene') {
|
|
||||||
api(base() + '/game/api/scene_add.dspy', {game_id: state.game.id, name: name.trim()})
|
|
||||||
.then(function (r) { r.success ? (msg('场景已添加', true), reload()) : msg(r.message, false); });
|
|
||||||
} else {
|
|
||||||
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.trim()})
|
|
||||||
.then(function (r) { r.success ? (msg('实体已添加' + (sceneId ? '' : '(世界级)'), true), reload()) : msg(r.message, false); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectWorld() {
|
|
||||||
if (state.data && state.data.world) selObj('world', state.data.world.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selObj(type, id) {
|
|
||||||
var obj = null;
|
|
||||||
if (type === 'world') obj = state.data.world;
|
|
||||||
if (type === 'scene') obj = (state.data.scenes || []).find(function (x) { return x.id === id; });
|
|
||||||
if (type === 'entity') obj = (state.data.entities || []).find(function (x) { return x.id === id; });
|
|
||||||
if (!obj) return;
|
|
||||||
state.sel = {type: type, id: id, name: obj.name};
|
|
||||||
document.getElementById('cur_obj').textContent =
|
|
||||||
({world: '🌍 世界', scene: '🎬 场景', entity: '📦 实体'})[type] + ' · ' + obj.name;
|
|
||||||
renderTree(state.data);
|
|
||||||
loadScript();
|
|
||||||
}
|
|
||||||
function loadSelKeep() { if (state.sel) selObj(state.sel.type, state.sel.id); }
|
|
||||||
|
|
||||||
function bindParams() {
|
|
||||||
var s = state.sel;
|
|
||||||
if (!s) return null;
|
|
||||||
var p = {game_id: state.game.id, trigger_event: document.getElementById('event_sel').value};
|
|
||||||
// 世界外键:所有级别脚本都归属该游戏的世界(world_id NOT NULL 约束)
|
|
||||||
if (state.data && state.data.world) p.world_id = state.data.world.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;
|
|
||||||
msg(sc ? '已加载现有脚本: ' + sc.name : '新脚本(保存后绑定到该对象+事件)', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveScript() {
|
|
||||||
var p = bindParams();
|
|
||||||
if (!p) { msg('先在左侧选择对象', false); return; }
|
|
||||||
p.name = state.sel.name + '_' + p.trigger_event;
|
|
||||||
p.code = 'SC_' + 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 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 = {newGame: newGame, saveGame: saveGame, switchGame: switchGame, reload: function () { reload(); },
|
|
||||||
selObj: selObj, inlineStart: inlineStart, inlineCancel: inlineCancel, inlineOk: inlineOk,
|
|
||||||
saveScript: saveScript, validate: validate, test: test};
|
|
||||||
|
|
||||||
document.getElementById('game_sel').onchange = function () { switchGame(this.value); };
|
|
||||||
reload();
|
|
||||||
})();
|
|
||||||
285
wwwroot/workbench/workbench.ui
Normal file
285
wwwroot/workbench/workbench.ui
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"css": "filler"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"padding": "8px 12px",
|
||||||
|
"gap": "8px",
|
||||||
|
"alignItems": "center",
|
||||||
|
"borderBottom": "1px solid #e5e7eb"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "🎮 游戏工作台",
|
||||||
|
"cfontsize": 1.2,
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"halign": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"id": "wb_new_game",
|
||||||
|
"options": {
|
||||||
|
"label": "+ 新建游戏"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "var pw=new bricks.PopupWindow({title:'新建游戏',cwidth:32,cheight:10,auto_open:true,id:'wb_ng_popup'});var vb=new bricks.VBox({padding:'12px',gap:'10px'});var inp=new bricks.UiText({name:'wb_ng_name',placeholder:'游戏名称',cwidth:28});var chk=new bricks.UiCheck({name:'wb_ng_shared',value:false,label:'世界共享'});vb.add_widget(inp);vb.add_widget(chk);var bt=new bricks.Button({label:'创建游戏',css:'primary'});bt.bind('click',function(){var n=inp.resultValue();if(!n||!n.trim())return;var b=new URLSearchParams();b.append('name',n.trim());b.append('description',chk.resultValue()?'shared':'independent');fetch('/scense_game/game/api/game_create.dspy',{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){var ms=bricks.getWidgetById('wb_msg',bricks.app);if(ms)ms.set_text(r.success?'✔ 游戏已创建,世界已自动建立':'✘ '+(r.message||'创建失败'));var w=bricks.getWidgetById('wb_ng_popup',bricks.app);if(w){w.dismiss();w.destroy()};var t=bricks.getWidgetById('wb_tree',bricks.app);if(t&&t.container){t.container.clear_widgets();t.get_children_data(t)}})});vb.add_widget(bt);pw.content_w.add_widget(vb);setTimeout(function(){var el=inp.dom_element?inp.dom_element.querySelector('input,textarea'):null;if(el)el.focus()},300)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"id": "wb_msg",
|
||||||
|
"options": {
|
||||||
|
"text": "选择对象树节点开始编程",
|
||||||
|
"color": "#6b7280",
|
||||||
|
"halign": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"id": "wb_test",
|
||||||
|
"options": {
|
||||||
|
"label": "▶ 测试(新窗口运行demo)",
|
||||||
|
"css": "primary"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "var t=bricks.getWidgetById('wb_tree',bricks.app);var wid='';var gid='';if(t&&t.selected_node){var u=t.selected_node.user_data||{};gid=wb_state.game_id||'';if(u.obj_type==='world')wid=u.real_id;else{var w=t.container?null:null;var sel=t.selected_node;while(sel&&sel.parent_widget){if(sel.user_data&&sel.user_data.obj_type==='world'){wid=sel.user_data.real_id;break}sel=sel.parent_widget}}}if(!wid){var ms=bricks.getWidgetById('wb_msg',bricks.app);if(ms)ms.set_text('请先选中一个世界节点再测试');return}window.open('/scense_demo/demo_viewer.ui?world_id='+encodeURIComponent(wid)+'&game_id='+encodeURIComponent(gid),'_blank')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"css": "filler",
|
||||||
|
"width": "100%"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "280px",
|
||||||
|
"borderRight": "1px solid #e5e7eb",
|
||||||
|
"padding": "8px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"gap": "6px",
|
||||||
|
"alignItems": "center",
|
||||||
|
"marginBottom": "6px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "对象树",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"halign": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"label": "+场景",
|
||||||
|
"css": "small"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"params": {
|
||||||
|
"add_type": "scene",
|
||||||
|
"scene_id": "",
|
||||||
|
"hint": "新场景将加入当前游戏的世界下"
|
||||||
|
},
|
||||||
|
"script": "window.wb_state=window.wb_state||{};var wb_state=window.wb_state;window.wb_add_type=params.add_type;window.wb_add_scene_id=params.scene_id||'';var pw=new bricks.PopupWindow({title:params.add_type==='scene'?'添加场景':'添加实体',cwidth:28,cheight:8,auto_open:true,id:'wb_add_popup'});var vb=new bricks.VBox({padding:'12px',gap:'10px'});var inp=new bricks.UiText({name:'wb_add_name',placeholder:'输入名称',cwidth:24});vb.add_widget(new bricks.Text({text:params.hint,halign:'left'}));vb.add_widget(inp);var bt=new bricks.Button({label:'添加',css:'primary'});bt.bind('click',function(){window.wb_state=window.wb_state||{};var wb_state=window.wb_state;var inp=bricks.getWidgetById('wb_add_name',bricks.app);var name=inp?inp.resultValue():'';if(!name||!name.trim()){return}var b=new URLSearchParams();b.append('game_id',wb_state.game_id||'');b.append('scene_id',wb_add_scene_id||'');b.append('name',name.trim());var url=wb_add_type==='scene'?'/scense_game/game/api/scene_add.dspy':'/scense_game/game/api/entity_add.dspy';fetch(url,{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){var ms=bricks.getWidgetById('wb_msg',bricks.app);if(ms)ms.set_text(r.success?'✔ 已添加':'✘ '+(r.message||'添加失败'));var w=bricks.getWidgetById('wb_add_popup',bricks.app);if(w){w.dismiss();w.destroy()};var t=bricks.getWidgetById('wb_tree',bricks.app);if(t&&t.container){t.container.clear_widgets();t.get_children_data(t)}})});vb.add_widget(bt);pw.content_w.add_widget(vb);setTimeout(function(){var el=inp.dom_element?inp.dom_element.querySelector('input,textarea'):null;if(el)el.focus()},300)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"label": "+实体",
|
||||||
|
"css": "small"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"params": {
|
||||||
|
"add_type": "entity",
|
||||||
|
"scene_id": "",
|
||||||
|
"hint": "选中场景节点则加入该场景,否则为世界级实体"
|
||||||
|
},
|
||||||
|
"script": "window.wb_state=window.wb_state||{};var wb_state=window.wb_state;window.wb_add_type=params.add_type;window.wb_add_scene_id=params.scene_id||'';var pw=new bricks.PopupWindow({title:params.add_type==='scene'?'添加场景':'添加实体',cwidth:28,cheight:8,auto_open:true,id:'wb_add_popup'});var vb=new bricks.VBox({padding:'12px',gap:'10px'});var inp=new bricks.UiText({name:'wb_add_name',placeholder:'输入名称',cwidth:24});vb.add_widget(new bricks.Text({text:params.hint,halign:'left'}));vb.add_widget(inp);var bt=new bricks.Button({label:'添加',css:'primary'});bt.bind('click',function(){window.wb_state=window.wb_state||{};var wb_state=window.wb_state;var inp=bricks.getWidgetById('wb_add_name',bricks.app);var name=inp?inp.resultValue():'';if(!name||!name.trim()){return}var b=new URLSearchParams();b.append('game_id',wb_state.game_id||'');b.append('scene_id',wb_add_scene_id||'');b.append('name',name.trim());var url=wb_add_type==='scene'?'/scense_game/game/api/scene_add.dspy':'/scense_game/game/api/entity_add.dspy';fetch(url,{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){var ms=bricks.getWidgetById('wb_msg',bricks.app);if(ms)ms.set_text(r.success?'✔ 已添加':'✘ '+(r.message||'添加失败'));var w=bricks.getWidgetById('wb_add_popup',bricks.app);if(w){w.dismiss();w.destroy()};var t=bricks.getWidgetById('wb_tree',bricks.app);if(t&&t.container){t.container.clear_widgets();t.get_children_data(t)}})});vb.add_widget(bt);pw.content_w.add_widget(vb);setTimeout(function(){var el=inp.dom_element?inp.dom_element.querySelector('input,textarea'):null;if(el)el.focus()},300)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VScrollPanel",
|
||||||
|
"options": {
|
||||||
|
"css": "filler"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Tree",
|
||||||
|
"id": "wb_tree",
|
||||||
|
"options": {
|
||||||
|
"dataurl": "{{entire_url('/scense_game/workbench/api/tree.dspy')}}",
|
||||||
|
"method": "POST",
|
||||||
|
"idField": "id",
|
||||||
|
"textField": "text",
|
||||||
|
"select_only": true,
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "node_selected",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "window.wb_state=window.wb_state||{};var wb_state=window.wb_state;var t=bricks.getWidgetById('wb_tree',bricks.app);if(!event.params||!event.params.id)return;var ot=event.params.obj_type||'';var oid=event.params.real_id||'';var lbl=bricks.getWidgetById('wb_cur_obj',bricks.app);if(lbl)lbl.set_text(({'game':'🎮 游戏','world':'🌍 世界','scene':'🎬 场景','entity':'📦 实体'})[ot]+' · '+event.params.text);wb_state.obj_type=ot;wb_state.obj_id=oid;wb_state.game_id=(ot==='game')?oid:(event.params.game_id||wb_state.game_id);var es=bricks.getWidgetById('wb_event_sel',bricks.app);var evt=es?es.resultValue():'event_start';if(!oid)return;var b=new URLSearchParams();b.append('obj_type',ot);b.append('obj_id',oid);b.append('trigger_event',evt);fetch('/scense_game/workbench/api/load_script.dspy',{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){var ed=bricks.getWidgetById('wb_code',bricks.app);if(ed&&r.success){ed.set_value(r.data?r.data.content:'')}})"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"css": "filler",
|
||||||
|
"padding": "10px",
|
||||||
|
"gap": "8px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"gap": "10px",
|
||||||
|
"alignItems": "center"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"id": "wb_cur_obj",
|
||||||
|
"options": {
|
||||||
|
"text": "未选择对象",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"halign": "left"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "UiCode",
|
||||||
|
"id": "wb_event_sel",
|
||||||
|
"options": {
|
||||||
|
"name": "wb_event_sel",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"value": "event_start",
|
||||||
|
"text": "事件:开始"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "event_click",
|
||||||
|
"text": "事件:点击"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "event_timer",
|
||||||
|
"text": "事件:定时"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "event_collision",
|
||||||
|
"text": "事件:碰撞"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"valueField": "value",
|
||||||
|
"textField": "text",
|
||||||
|
"cwidth": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"label": "校验"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "var ms=bricks.getWidgetById('wb_msg',bricks.app);var ed=bricks.getWidgetById('wb_code',bricks.app);var code=ed?ed.get_value():'';var b=new URLSearchParams();b.append('content',code);fetch('/script_engine/api/validate_script.dspy',{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){if(ms)ms.set_text(r.success!==false?'✔ 语法校验通过':'✘ '+(r.error||r.message||'校验失败'))})"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"label": "💾 保存脚本",
|
||||||
|
"css": "primary"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "window.wb_state=window.wb_state||{};var wb_state=window.wb_state;var ms=bricks.getWidgetById('wb_msg',bricks.app);if(!wb_state.obj_id){if(ms)ms.set_text('先在左侧树选择对象');return}var es=bricks.getWidgetById('wb_event_sel',bricks.app);var evt=es?es.resultValue():'event_start';var ed=bricks.getWidgetById('wb_code',bricks.app);var code=ed?ed.get_value():'';var b=new URLSearchParams();b.append('obj_type',wb_state.obj_type);b.append('obj_id',wb_state.obj_id);b.append('game_id',wb_state.game_id||'');b.append('trigger_event',evt);b.append('content',code);fetch('/scense_game/workbench/api/save_script.dspy',{method:'POST',body:b}).then(function(r){return r.json()}).then(function(r){if(ms)ms.set_text(r.success?'✔ 脚本已保存':'✘ '+(r.message||'保存失败'))})"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "CodeEditor",
|
||||||
|
"id": "wb_code",
|
||||||
|
"options": {
|
||||||
|
"css": "filler",
|
||||||
|
"mode": "python",
|
||||||
|
"value": "# 选择左侧对象后,为它的事件编写逻辑脚本"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user