beyond-simulator-web 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/public/app.js +6 -0
- package/dist/public/editor.js +13 -12
- package/dist/public/index.html +1 -0
- package/dist/public/play-renderer.js +71 -70
- package/dist/public/play.html +1 -1
- package/dist/public/play.js +78 -4
- package/dist/server.js +15 -13
- package/dist/worker.js +8 -6
- package/package.json +1 -1
package/dist/public/play.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<header class="bar">
|
|
12
12
|
<div class="brand"><span class="icon">UI</span><strong>模拟器</strong><select id="deviceSel" class="devsel hidden" aria-label="切换设备画布" title="切换设备画布:以新画布重建运行时,GetUICanvasSize 与预设像素一致"></select><select id="playerCountSel" class="playersel hidden" aria-label="试玩人数" title="试玩人数:最多 8 人,每人一份独立客户端"></select><select id="playerViewSel" class="viewsel hidden" aria-label="当前玩家视角" title="切换当前玩家视角:输入、画面和 PlayerSelf 都跟随该玩家"></select><span class="device">设备 <b id="resolution">准备中…</b></span></div>
|
|
13
13
|
<div class="view-tabs" role="tablist" aria-label="试玩页面"><button class="active" data-view="play" role="tab">试玩</button><button data-view="log" role="tab">日志</button></div>
|
|
14
|
-
<div class="actions"><button id="pause">Ⅱ 暂停运行</button><button id="step" disabled title="暂停后可单帧调试">▷ 单帧调试</button><button class="stop" id="stop">■ 结束试玩</button><button class="primary hidden" id="restart">↻ 重新开始</button><button id="theme" title="切换亮暗主题">◐</button><button id="close">返回编辑</button></div>
|
|
14
|
+
<div class="actions"><button id="audio" data-audio-toggle title="点击解锁浏览器音效">开启声音</button><button id="pause">Ⅱ 暂停运行</button><button id="step" disabled title="暂停后可单帧调试">▷ 单帧调试</button><button class="stop" id="stop">■ 结束试玩</button><button class="primary hidden" id="restart">↻ 重新开始</button><button id="theme" title="切换亮暗主题">◐</button><button id="close">返回编辑</button></div>
|
|
15
15
|
</header>
|
|
16
16
|
<div class="views">
|
|
17
17
|
<main class="workspace play-view" id="workspace"><div class="stage-shell veiled" id="shell"><div class="stage-label" id="stageLabel"></div><div class="stage" id="stage"><canvas id="cv" class="stage-canvas"></canvas></div></div><div class="loading" id="loading"><span class="spin"></span>正在准备试玩…</div><div class="fatal hidden" id="fatal"></div></main>
|
package/dist/public/play.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
'use strict'
|
|
6
6
|
const API = '/editor/api/play'
|
|
7
7
|
const sessionId = decodeURIComponent(location.hash.slice(1))
|
|
8
|
+
const embedded = window.parent !== window && new URLSearchParams(location.search).get('embedded') === '1'
|
|
9
|
+
let closing = false
|
|
10
|
+
let stopped = false
|
|
11
|
+
let embeddedApi = null
|
|
8
12
|
const $ = (id) => document.getElementById(id)
|
|
9
13
|
const stage = $('stage')
|
|
10
14
|
const cv = $('cv')
|
|
@@ -27,11 +31,16 @@
|
|
|
27
31
|
if (!showLog) requestAnimationFrame(() => fitStage(true))
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
function api(action, args = {}) {
|
|
35
|
+
if (closing && action !== 'stop') return Promise.reject(new Error('试玩正在关闭'))
|
|
36
|
+
return embeddedApi ? embeddedApi(action, args) : request(action, args)
|
|
37
|
+
}
|
|
38
|
+
async function request(action, args) {
|
|
31
39
|
const response = await fetch(API, {
|
|
32
40
|
method: 'POST',
|
|
33
41
|
headers: { 'content-type': 'application/json' },
|
|
34
42
|
body: JSON.stringify({ sessionId, action, args }),
|
|
43
|
+
signal: AbortSignal.timeout(15_000),
|
|
35
44
|
})
|
|
36
45
|
const envelope = await response.json().catch(() => null)
|
|
37
46
|
if (!response.ok || !envelope || envelope.ok !== true) throw new Error(envelope?.error || '试玩请求失败')
|
|
@@ -188,6 +197,7 @@
|
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
function showError(error) {
|
|
200
|
+
if (closing) return
|
|
191
201
|
$('loading').classList.add('hidden')
|
|
192
202
|
$('fatal').classList.remove('hidden')
|
|
193
203
|
$('fatal').textContent = error?.message || String(error)
|
|
@@ -201,8 +211,14 @@
|
|
|
201
211
|
onSnapshot: renderChrome,
|
|
202
212
|
onStatus: applyStatus,
|
|
203
213
|
onError: showError,
|
|
214
|
+
onAudioStatus: status => {
|
|
215
|
+
$('audio').textContent = !status.unlocked ? '开启声音' : status.muted ? '声音已关闭' : '声音已开启'
|
|
216
|
+
$('audio').title = status.error || (!status.unlocked ? '点击或在画布内操作以解锁音效' : '切换静音')
|
|
217
|
+
},
|
|
218
|
+
isActive: () => !closing,
|
|
204
219
|
})
|
|
205
220
|
play.bindInput(stage, { keysOn: window })
|
|
221
|
+
$('audio').addEventListener('click', () => play.audio?.toggle())
|
|
206
222
|
|
|
207
223
|
$('step').addEventListener('click', () => void play.act('step', { dt: 1 / 30 }))
|
|
208
224
|
$('pause').addEventListener('click', () => void play.act(play.snapshot?.paused ? 'resume' : 'pause'))
|
|
@@ -263,7 +279,40 @@
|
|
|
263
279
|
revealStage()
|
|
264
280
|
} catch (error) { showError(error) }
|
|
265
281
|
})
|
|
266
|
-
|
|
282
|
+
function quiesce() {
|
|
283
|
+
closing = true
|
|
284
|
+
document.querySelector('.app').inert = true
|
|
285
|
+
play.reset()
|
|
286
|
+
}
|
|
287
|
+
function focusStage() {
|
|
288
|
+
stage.tabIndex = -1
|
|
289
|
+
stage.focus({ preventScroll: true })
|
|
290
|
+
}
|
|
291
|
+
stage.addEventListener('pointerdown', focusStage)
|
|
292
|
+
function requestEmbeddedClose() {
|
|
293
|
+
// Custom desktop schemes can serialize their origin as "null".
|
|
294
|
+
// The parent checks the exact iframe WindowProxy and session id.
|
|
295
|
+
parent.postMessage({ type: 'qxqy-play-close', sessionId }, location.origin === 'null' ? '*' : location.origin)
|
|
296
|
+
}
|
|
297
|
+
addEventListener('keydown', event => {
|
|
298
|
+
if (embedded && event.key === 'Escape') { event.preventDefault(); requestEmbeddedClose() }
|
|
299
|
+
})
|
|
300
|
+
$('close').addEventListener('click', async () => {
|
|
301
|
+
if (embedded) {
|
|
302
|
+
requestEmbeddedClose()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
quiesce()
|
|
307
|
+
await api('stop')
|
|
308
|
+
stopped = true
|
|
309
|
+
window.close()
|
|
310
|
+
} catch (error) {
|
|
311
|
+
closing = false
|
|
312
|
+
document.querySelector('.app').inert = false
|
|
313
|
+
showError(error)
|
|
314
|
+
}
|
|
315
|
+
})
|
|
267
316
|
$('theme').addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; applyTheme() })
|
|
268
317
|
let lastCase = null
|
|
269
318
|
function parseAsserts() {
|
|
@@ -290,11 +339,36 @@
|
|
|
290
339
|
for (const button of document.querySelectorAll('[data-view]')) button.addEventListener('click', () => setView(button.dataset.view))
|
|
291
340
|
addEventListener('resize', () => fitStage(true))
|
|
292
341
|
addEventListener('pagehide', () => {
|
|
342
|
+
closing = true
|
|
293
343
|
play.destroy()
|
|
294
|
-
|
|
344
|
+
// Embedded close/unmount is owned by the parent. A late keepalive stop
|
|
345
|
+
// from the old frame must never terminate the next play session.
|
|
346
|
+
if (!embedded && !stopped) fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
|
|
295
347
|
})
|
|
348
|
+
if (embedded) {
|
|
349
|
+
// Closing the whole application document has no awaitable teardown.
|
|
350
|
+
// This fallback is separate from normal modal close, which awaits stop.
|
|
351
|
+
const parentPagehide = () => {
|
|
352
|
+
quiesce()
|
|
353
|
+
fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId, action: 'stop', args: {} }), keepalive: true }).catch(() => {})
|
|
354
|
+
}
|
|
355
|
+
parent.addEventListener('pagehide', parentPagehide)
|
|
356
|
+
addEventListener('pagehide', () => parent.removeEventListener('pagehide', parentPagehide))
|
|
357
|
+
}
|
|
296
358
|
|
|
297
359
|
if (!sessionId) showError(new Error('缺少会话 ID'))
|
|
298
|
-
else
|
|
360
|
+
else if (embedded) {
|
|
361
|
+
// Wait for the parent's load handler: closing a still-loading iframe
|
|
362
|
+
// must not race an automatic start from this document.
|
|
363
|
+
window.qxqyEmbeddedPlay = {
|
|
364
|
+
start: request => {
|
|
365
|
+
if (closing) return Promise.resolve()
|
|
366
|
+
embeddedApi = request
|
|
367
|
+
return play.start().then(() => { if (closing) play.reset(); else { revealStage(); focusStage() } })
|
|
368
|
+
},
|
|
369
|
+
quiesce,
|
|
370
|
+
focus: focusStage,
|
|
371
|
+
}
|
|
372
|
+
} else play.start().then(() => setTimeout(revealStage, 0)).catch(showError)
|
|
299
373
|
})()
|
|
300
374
|
|