|
|
|
|
@ -45,8 +45,19 @@
|
|
|
|
|
.live-ws-bar-actions .btn { margin-top: 0; margin-right: 0; }
|
|
|
|
|
details.live-details > summary { cursor: pointer; color: #cfe3ff; font-weight: 600; list-style-position: outside; }
|
|
|
|
|
details.live-details > summary::-webkit-details-marker { color: #9eb0d6; }
|
|
|
|
|
.live-json-pre { max-height: 320px; overflow: auto; font-size: 11px; margin: 8px 0 0; padding: 10px; background: #0f1522; border: 1px solid #2f3b5a; border-radius: 6px; white-space: pre-wrap; word-break: break-word; }
|
|
|
|
|
.quest-world-panel { margin-top: 12px; padding-top: 12px; border-top: 1px solid #2a3551; }
|
|
|
|
|
.jv-root { margin-top: 8px; font-family: ui-monospace, Consolas, monospace; font-size: 12px; line-height: 1.45; }
|
|
|
|
|
.jv-node { margin: 2px 0 2px 0; border-left: 1px solid #2f3b5a; padding-left: 8px; }
|
|
|
|
|
.jv-node > .jv-sum { cursor: pointer; color: #9eb0d6; user-select: none; }
|
|
|
|
|
.jv-node > .jv-sum:hover { color: #cfe3ff; }
|
|
|
|
|
.jv-ch { margin-top: 4px; padding-left: 4px; }
|
|
|
|
|
.jv-row { margin: 2px 0; }
|
|
|
|
|
.jv-key { color: #7eb8ff; margin-right: 4px; }
|
|
|
|
|
.jv-idx { color: #b8a0ff; margin-right: 4px; }
|
|
|
|
|
.jv-str { color: #7de29f; }
|
|
|
|
|
.jv-lit { color: #ffb86c; }
|
|
|
|
|
.jv-null { color: #9eb0d6; font-style: italic; }
|
|
|
|
|
.jv-empty { color: #6a7a9e; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
@ -96,6 +107,8 @@
|
|
|
|
|
_heroLiveWsLastAt: null,
|
|
|
|
|
_liveSnapshotOpen: false,
|
|
|
|
|
_heroQuestWorldOpen: false,
|
|
|
|
|
_heroLiveSnapshot: null,
|
|
|
|
|
_jsonViewerOpenPaths: null,
|
|
|
|
|
};
|
|
|
|
|
state._confirmAction = null;
|
|
|
|
|
|
|
|
|
|
@ -612,6 +625,8 @@
|
|
|
|
|
state._heroLiveWsStatus = "disconnected";
|
|
|
|
|
state._heroLiveWsError = "";
|
|
|
|
|
state._heroLiveWsLastAt = null;
|
|
|
|
|
state._heroLiveSnapshot = null;
|
|
|
|
|
state._jsonViewerOpenPaths = null;
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
function startHeroMovementPoll(durationSec = 55) {
|
|
|
|
|
@ -649,6 +664,8 @@
|
|
|
|
|
state._heroLiveWsStatus = "connecting";
|
|
|
|
|
state._heroLiveWsError = "";
|
|
|
|
|
state._heroLiveWsLastAt = null;
|
|
|
|
|
state._heroLiveSnapshot = null;
|
|
|
|
|
state._jsonViewerOpenPaths = Object.create(null);
|
|
|
|
|
render();
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
state._heroLiveWsStatus = "connected";
|
|
|
|
|
@ -672,8 +689,18 @@
|
|
|
|
|
render();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (data && data.id && state.selectedHeroId === data.id) {
|
|
|
|
|
state.selectedHero = data;
|
|
|
|
|
let hero = null;
|
|
|
|
|
let snap = null;
|
|
|
|
|
if (data && data.hero && typeof data.hero === "object" && data.hero.id != null) {
|
|
|
|
|
hero = data.hero;
|
|
|
|
|
snap = data;
|
|
|
|
|
} else if (data && data.id != null) {
|
|
|
|
|
hero = data;
|
|
|
|
|
snap = { hero: data, heroMove: null };
|
|
|
|
|
}
|
|
|
|
|
if (hero && state.selectedHeroId === hero.id) {
|
|
|
|
|
state.selectedHero = hero;
|
|
|
|
|
state._heroLiveSnapshot = snap;
|
|
|
|
|
state._heroLiveWsLastAt = Date.now();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
@ -684,6 +711,53 @@
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
function jsonChildPath(parent, segment) {
|
|
|
|
|
if (parent === "$") return `$.${segment}`;
|
|
|
|
|
return `${parent}.${segment}`;
|
|
|
|
|
}
|
|
|
|
|
function jsonViewerToggle(path, ev) {
|
|
|
|
|
if (ev) ev.preventDefault();
|
|
|
|
|
if (!state._jsonViewerOpenPaths) state._jsonViewerOpenPaths = Object.create(null);
|
|
|
|
|
if (state._jsonViewerOpenPaths[path]) delete state._jsonViewerOpenPaths[path];
|
|
|
|
|
else state._jsonViewerOpenPaths[path] = true;
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
function jsonTreeHtml(value, path) {
|
|
|
|
|
const pathArg = JSON.stringify(path);
|
|
|
|
|
const open = state._jsonViewerOpenPaths && state._jsonViewerOpenPaths[path];
|
|
|
|
|
const openAttr = open ? " open" : "";
|
|
|
|
|
if (value === null) return `<span class="jv-null">null</span>`;
|
|
|
|
|
if (value === undefined) return `<span class="jv-null">undefined</span>`;
|
|
|
|
|
const t = typeof value;
|
|
|
|
|
if (t === "boolean" || t === "number") return `<span class="jv-lit">${e(String(value))}</span>`;
|
|
|
|
|
if (t === "string") return `<span class="jv-str">"${e(value)}"</span>`;
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
if (value.length === 0) return `<span class="jv-empty">[]</span>`;
|
|
|
|
|
const inner = value.map((item, i) => {
|
|
|
|
|
const cp = `${path}[${i}]`;
|
|
|
|
|
return `<div class="jv-row"><span class="jv-idx">${i}:</span> ${jsonTreeHtml(item, cp)}</div>`;
|
|
|
|
|
}).join("");
|
|
|
|
|
return `<details class="jv-node"${openAttr}><summary class="jv-sum" onclick="jsonViewerToggle(${pathArg}, event)">[${value.length} эл.]</summary><div class="jv-ch">${inner}</div></details>`;
|
|
|
|
|
}
|
|
|
|
|
if (t === "object") {
|
|
|
|
|
const keys = Object.keys(value);
|
|
|
|
|
if (keys.length === 0) return `<span class="jv-empty">{}</span>`;
|
|
|
|
|
keys.sort();
|
|
|
|
|
const inner = keys.map(k => {
|
|
|
|
|
const cp = jsonChildPath(path, k);
|
|
|
|
|
return `<div class="jv-row"><span class="jv-key">${e(k)}:</span> ${jsonTreeHtml(value[k], cp)}</div>`;
|
|
|
|
|
}).join("");
|
|
|
|
|
return `<details class="jv-node"${openAttr}><summary class="jv-sum" onclick="jsonViewerToggle(${pathArg}, event)">{${keys.length} полей}</summary><div class="jv-ch">${inner}</div></details>`;
|
|
|
|
|
}
|
|
|
|
|
return `<span>${e(String(value))}</span>`;
|
|
|
|
|
}
|
|
|
|
|
function heroSnapshotTreeHtml() {
|
|
|
|
|
const snap = state._heroLiveSnapshot;
|
|
|
|
|
if (!snap || typeof snap !== "object") {
|
|
|
|
|
return `<p class="muted" style="margin-top:8px">Нет данных — подключите live или дождитесь сообщения.</p>`;
|
|
|
|
|
}
|
|
|
|
|
return `<div class="jv-root">${jsonTreeHtml(snap, "$")}</div>`;
|
|
|
|
|
}
|
|
|
|
|
function formatRemainingMs(ms) {
|
|
|
|
|
if (ms == null || !Number.isFinite(ms)) return "—";
|
|
|
|
|
if (ms <= 0) return "истекло";
|
|
|
|
|
@ -740,10 +814,6 @@
|
|
|
|
|
const last = state._heroLiveWsLastAt ? new Date(state._heroLiveWsLastAt).toLocaleTimeString() : "—";
|
|
|
|
|
const heroId = state._heroLiveWsHeroId;
|
|
|
|
|
const err = state._heroLiveWsError;
|
|
|
|
|
const h = state.selectedHero;
|
|
|
|
|
const jsonSnap = h && h.id
|
|
|
|
|
? `<pre class="live-json-pre">${e(JSON.stringify(h, null, 2))}</pre>`
|
|
|
|
|
: `<p class="muted" style="margin-top:8px">Нет JSON — подключите live или дождитесь первого сообщения.</p>`;
|
|
|
|
|
const openAttr = state._liveSnapshotOpen ? " open" : "";
|
|
|
|
|
return `
|
|
|
|
|
<div class="card" style="margin-top:10px">
|
|
|
|
|
@ -756,8 +826,8 @@
|
|
|
|
|
<button type="button" class="btn" onclick="connectHeroLiveWS()">Подключить</button>
|
|
|
|
|
<button type="button" class="btn" onclick="stopHeroLiveWS()">Отключить</button>
|
|
|
|
|
<details class="live-details"${openAttr}>
|
|
|
|
|
<summary onclick="toggleLiveSnapshotOpen(event)">Снимок героя (JSON)</summary>
|
|
|
|
|
${jsonSnap}
|
|
|
|
|
<summary onclick="toggleLiveSnapshotOpen(event)">Снимок: hero + heroMove (дерево)</summary>
|
|
|
|
|
${heroSnapshotTreeHtml()}
|
|
|
|
|
</details>
|
|
|
|
|
<p class="muted" style="margin-top:8px"><kbd>/admin-ws/hero/{heroId}</kbd>, авторизация как у API.</p>
|
|
|
|
|
</div>
|
|
|
|
|
@ -1620,6 +1690,7 @@
|
|
|
|
|
window.stopHeroLiveWS = stopHeroLiveWS;
|
|
|
|
|
window.toggleLiveSnapshotOpen = toggleLiveSnapshotOpen;
|
|
|
|
|
window.toggleHeroQuestWorldOpen = toggleHeroQuestWorldOpen;
|
|
|
|
|
window.jsonViewerToggle = jsonViewerToggle;
|
|
|
|
|
render();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
|