Compare commits
2 Commits
9b7bf334e4
...
5efc5dfe61
| Author | SHA1 | Date |
|---|---|---|
|
|
5efc5dfe61 | |
|
|
ec68fa142c |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="../" />
|
||||
<title>Orbit — video reopen regression (dev)</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/video-reopen.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Dev-only: the EXACT player-reported flow against the real Research
|
||||
* window — open the console, close it, open it again: the feed must be
|
||||
* PLAYING every time it opens (it used to stay paused on reopen, because
|
||||
* Phaser 4's video play() is a no-op after pause(); the window now
|
||||
* resumes via video.resume()).
|
||||
*
|
||||
* node dev/server.mjs 8123
|
||||
* node dev/shot-firefox.mjs \
|
||||
* "http://127.0.0.1:8123/dev/video-reopen.html" \
|
||||
* "window.__VIDEO_REOPEN ? '1' : null" VIDEO_REOPEN.png 45000
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||
import { GameScene } from '../js/scenes/GameScene.js';
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
|
||||
const errors = [];
|
||||
const origErr = console.error.bind(console);
|
||||
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
|
||||
window.addEventListener('error', (e) => errors.push(String(e.message)));
|
||||
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
|
||||
|
||||
const report = document.createElement('pre');
|
||||
report.id = 'report';
|
||||
report.style.cssText =
|
||||
'position:fixed;left:10px;top:10px;z-index:9999;margin:0;padding:10px 14px;font:13px/1.55 monospace;color:#eaf6ff;background:rgba(6,20,16,0.94);border:1px solid #1b3a5a;white-space:pre-wrap;max-width:60%;';
|
||||
document.body.appendChild(report);
|
||||
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
|
||||
setReport('booting…');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const done = (lines) => {
|
||||
setReport(lines);
|
||||
window.__VIDEO_REOPEN = { ready: true, lines, errors };
|
||||
};
|
||||
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [GameScene]; // main.js does the same
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
|
||||
async function waitScene() {
|
||||
for (let i = 0; i < 150; i++) {
|
||||
const s = game.scene.getScene('GameScene');
|
||||
if (s && s.ship && s.researchWindow) return s;
|
||||
await sleep(100);
|
||||
}
|
||||
throw new Error('GameScene never booted');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const line = (ok, tag, detail) => results.push(`${ok ? 'PASS' : 'FAIL'} ${tag} — ${detail}`);
|
||||
const vstate = (w) => {
|
||||
const el = w.video && w.video.video;
|
||||
return el ? `state=${w.openState} video:${el.paused ? 'paused' : 'playing'} t=${(el.currentTime || 0).toFixed(2)}s` : 'NO SIGNAL';
|
||||
};
|
||||
|
||||
try {
|
||||
const s = await waitScene();
|
||||
const w = s.researchWindow;
|
||||
await sleep(800); // boot reveal settles
|
||||
|
||||
// ── open #1 (deck RESEARCH button) ─────────────────────────────────
|
||||
s.deckAction('research');
|
||||
await sleep(1500);
|
||||
const el = w.video.video;
|
||||
line(w.isOpen && el && !el.paused, 'open #1', vstate(w));
|
||||
|
||||
// ── close (the ✕ / deck toggle) ────────────────────────────────────
|
||||
w.close();
|
||||
await sleep(700);
|
||||
line(!w.isOpen && el.paused, 'close', vstate(w));
|
||||
|
||||
// ── open #2 — the reported bug: the feed must play again ───────────
|
||||
s.deckAction('research');
|
||||
await sleep(1500);
|
||||
const t2 = el.currentTime;
|
||||
line(w.isOpen && !el.paused, 'open #2 (reopen — was stuck paused before the fix)', vstate(w));
|
||||
await sleep(900);
|
||||
line(el.currentTime > t2, 'open #2 time advancing', `t=${t2.toFixed(2)}s → ${el.currentTime.toFixed(2)}s`);
|
||||
|
||||
// ── close → open #3: repeated toggles keep working ─────────────────
|
||||
w.close();
|
||||
await sleep(700);
|
||||
line(el.paused, 'close #2', vstate(w));
|
||||
s.deckAction('research');
|
||||
await sleep(1500);
|
||||
line(w.isOpen && !el.paused, 'open #3', vstate(w));
|
||||
|
||||
const allPass = results.every((r) => r.startsWith('PASS'));
|
||||
const lines = [
|
||||
allPass ? 'VIDEO REOPEN (RESEARCH WINDOW) — ALL PASS' : 'VIDEO REOPEN (RESEARCH WINDOW) — FAILURES',
|
||||
'─'.repeat(64),
|
||||
...results,
|
||||
errors.length ? `errors:\n${errors.slice(0, 5).join('\n')}` : 'no console errors',
|
||||
];
|
||||
done(lines);
|
||||
} catch (err) {
|
||||
errors.push(`FATAL: ${err.message}`);
|
||||
done(['VIDEO REOPEN — ERROR', String(err.message), ...errors.slice(0, 5)]);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="../" />
|
||||
<title>Orbit — video replay regression (dev)</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { position: absolute; right: 10px; bottom: 10px; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/video-replay.mjs"></script>
|
||||
<!-- Holds the `load` event (when --screenshot fires) until the test
|
||||
sequences have all run and the report has painted. -->
|
||||
<script defer src="/sleep?ms=11000"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Dev-only regression test: feed-window video play/pause/replay.
|
||||
*
|
||||
* Symptom under test (Build/Research/Map/Quests windows): open the
|
||||
* window, close it, open it again — the feed stays PAUSED.
|
||||
*
|
||||
* Root cause lives in the vendored Phaser 4 Video object: play() only
|
||||
* issues the native <video>.play() while its internal _playCalled flag
|
||||
* is false; pause() leaves the flag true, so a later play() call is a
|
||||
* silent no-op. resume() is the code path that re-issues native
|
||||
* playback for a previously-played, now-paused clip.
|
||||
*
|
||||
* This page exercises exactly those call sequences on real Phaser
|
||||
* Video objects (assets/videos/build.mp4) and reports:
|
||||
* A play → pause → play (old open() path — EXPECTED to stay paused)
|
||||
* B play → pause → resume (new open() path — EXPECTED to play)
|
||||
* C resume (first open) (new open() path on a fresh clip)
|
||||
*
|
||||
* node dev/server.mjs 8123 # any static server that also answers /sleep?ms=N
|
||||
* firefox --headless --screenshot VIDEO_REPLAY.png \
|
||||
* --window-size=960,520 "http://127.0.0.1:8123/dev/video-replay.html"
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
|
||||
const errors = [];
|
||||
const origErr = console.error.bind(console);
|
||||
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
|
||||
window.addEventListener('error', (e) => errors.push(String(e.message)));
|
||||
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
|
||||
|
||||
const report = document.createElement('pre');
|
||||
report.id = 'report';
|
||||
report.style.cssText =
|
||||
'position:fixed;left:10px;top:10px;z-index:9999;margin:0;padding:10px 14px;font:13px/1.55 monospace;color:#eaf6ff;background:rgba(6,20,16,0.94);border:1px solid #1b3a5a;white-space:pre-wrap;';
|
||||
document.body.appendChild(report);
|
||||
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
|
||||
setReport('booting…');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
class VideoTestScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super({ key: 'VideoTestScene' }); // v4: scenes carry their key explicitly
|
||||
}
|
||||
preload() {
|
||||
this.load.video('feedA', 'assets/videos/build.mp4');
|
||||
this.load.video('feedB', 'assets/videos/build.mp4');
|
||||
this.load.video('feedC', 'assets/videos/build.mp4');
|
||||
}
|
||||
create() {
|
||||
this.__ready = true; // create() runs after the loader completes
|
||||
}
|
||||
}
|
||||
|
||||
const game = new Phaser.Game({
|
||||
type: Phaser.AUTO,
|
||||
width: 300,
|
||||
height: 200,
|
||||
parent: 'game',
|
||||
backgroundColor: '#04060d',
|
||||
scene: VideoTestScene,
|
||||
});
|
||||
window.game = game;
|
||||
|
||||
const results = [];
|
||||
const line = (ok, tag, detail) => results.push(`${ok ? 'PASS' : 'FAIL'} ${tag} — ${detail}`);
|
||||
|
||||
function snapshot(v, label) {
|
||||
const el = v && v.video;
|
||||
const s = el
|
||||
? `paused=${el.paused} t=${(el.currentTime || 0).toFixed(2)}s ended=${el.ended}`
|
||||
: 'no <video> element';
|
||||
return `${label}: ${s}`;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
// the scene manager resolves by name only after boot — poll (same as
|
||||
// the other dev harnesses, e.g. research-shot.mjs)
|
||||
let scene = null;
|
||||
for (let i = 0; i < 200; i++) {
|
||||
scene = game.scene.getScene('VideoTestScene');
|
||||
if (scene) break;
|
||||
await sleep(100);
|
||||
}
|
||||
if (!scene) {
|
||||
const dump = JSON.stringify({
|
||||
booted: game.isBooted,
|
||||
renderer: game.renderer && game.renderer.type,
|
||||
scenes: (game.scene && game.scene.scenes || []).map((sc) => (sc.sys && (sc.sys.settings && sc.sys.settings.key || sc.sys.key)) || sc.key),
|
||||
}, null, 0);
|
||||
throw new Error('scene never registered — ' + dump);
|
||||
}
|
||||
// wait for the scene's create() (loader done)
|
||||
for (let i = 0; i < 200 && !scene.__ready; i++) await sleep(100);
|
||||
if (!scene.__ready) throw new Error('scene never reached create()');
|
||||
await sleep(300);
|
||||
|
||||
const mk = (key) => {
|
||||
const v = scene.add.video(0, 0, key);
|
||||
v.setLoop(true);
|
||||
if (v.video) v.video.muted = true;
|
||||
return v;
|
||||
};
|
||||
|
||||
// ── A: the OLD open() path — play() after pause() ─────────────────
|
||||
const a = mk('feedA');
|
||||
a.play(); // open #1
|
||||
await sleep(1200);
|
||||
const a1 = a.video;
|
||||
line(!a1.paused, 'A-open#1 play()', snapshot(a, 'after play()'));
|
||||
a.pause(); // close
|
||||
await sleep(400);
|
||||
line(a1.paused, 'A-close pause()', snapshot(a, 'after pause()'));
|
||||
a.play(); // open #2 — the old code path
|
||||
await sleep(1200);
|
||||
line(
|
||||
a1.paused,
|
||||
'A-open#2 play() (old path — reproduces the bug: stays paused)',
|
||||
snapshot(a, 'after 2nd play()')
|
||||
);
|
||||
|
||||
// ── B: the NEW open() path — resume() after pause() ────────────────
|
||||
const b = mk('feedB');
|
||||
b.resume(); // open #1 (resume() delegates to play() on a fresh clip)
|
||||
await sleep(1200);
|
||||
const b1 = b.video;
|
||||
line(!b1.paused, 'B-open#1 resume()', snapshot(b, 'after first resume()'));
|
||||
b.pause(); // close
|
||||
await sleep(400);
|
||||
const tBefore = b1.currentTime;
|
||||
b.resume(); // open #2 — the new code path
|
||||
await sleep(1200);
|
||||
line(
|
||||
!b1.paused && b1.currentTime > tBefore,
|
||||
'B-open#2 resume() (new path — fixed)',
|
||||
snapshot(b, `after resume() (was t=${tBefore.toFixed(2)}s)`)
|
||||
);
|
||||
|
||||
// ── C: first-ever open uses resume() (never played before) ────────
|
||||
const c = mk('feedC');
|
||||
c.resume(); // open — never played
|
||||
await sleep(1200);
|
||||
const c1 = c.video;
|
||||
line(!c1.paused && c1.currentTime > 0, 'C-open#1 resume() (fresh clip)', snapshot(c, 'after resume()'));
|
||||
|
||||
// ── verdict ───────────────────────────────────────────────────────
|
||||
const allPass = results.every((r) => r.startsWith('PASS'));
|
||||
const lines = [
|
||||
`Phaser ${Phaser.VERSION}`,
|
||||
allPass ? 'VIDEO REPLAY TEST — ALL PASS' : 'VIDEO REPLAY TEST — FAILURES',
|
||||
'─'.repeat(64),
|
||||
...results,
|
||||
'',
|
||||
'A-open#2 staying paused is EXPECTED: it is the Phaser 4 play()-after-',
|
||||
'pause() no-op that the window fix (resume()) works around.',
|
||||
errors.length ? `errors:\n${errors.slice(0, 5).join('\n')}` : 'no console errors',
|
||||
];
|
||||
setReport(lines);
|
||||
window.__done = true; // dev/shot-firefox.mjs polls this
|
||||
a.pause(); // stop the repro clip; B/C keep looping (ambience, muted)
|
||||
setReport(lines);
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
window.__done = true;
|
||||
setReport(['VIDEO REPLAY TEST — ERROR', String((e && e.message) || e), ...errors.slice(0, 5)]);
|
||||
});
|
||||
|
|
@ -446,6 +446,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
if (!this.hudLines) return;
|
||||
if (this.hudT0 === null) {
|
||||
this.hudT0 = time;
|
||||
this.playSfx('construct'); // the planet info starts typing in (data/sfx.json → construct)
|
||||
for (const ln of this.hudLines) ln.dec = new ScrambleDecode(ln.value, time + ln.delay, ln.dur);
|
||||
}
|
||||
let done = true;
|
||||
|
|
|
|||
|
|
@ -1233,7 +1233,12 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this._paintAll(this.activeCat);
|
||||
this._paintStatusStrip();
|
||||
|
||||
this.video?.play?.();
|
||||
// resume() not play(): v4's play() is a no-op after pause() (its
|
||||
// _playCalled flag stays true, so the native play() is never
|
||||
// re-issued) — the feed would stay frozen after close → reopen.
|
||||
// resume() handles both a first open (delegates to play()) and a
|
||||
// re-open of a paused clip.
|
||||
this.video?.resume?.();
|
||||
this.sfx('ui_window');
|
||||
this._startReveal();
|
||||
|
||||
|
|
|
|||
|
|
@ -2171,7 +2171,12 @@ export class MapWindow extends Phaser.GameObjects.Container {
|
|||
if (this.openState === 'open' || this.openState === 'opening') return;
|
||||
this.openState = 'opening';
|
||||
this.setAlpha(0);
|
||||
this.video?.play?.();
|
||||
// resume() not play(): v4's play() is a no-op after pause() (its
|
||||
// _playCalled flag stays true, so the native play() is never
|
||||
// re-issued) — the feed would stay frozen after close → reopen.
|
||||
// resume() handles both a first open (delegates to play()) and a
|
||||
// re-open of a paused clip.
|
||||
this.video?.resume?.();
|
||||
this.sfx('ui_window');
|
||||
const now = this.scene.time.now;
|
||||
this._startReveal(now);
|
||||
|
|
|
|||
|
|
@ -1065,7 +1065,12 @@ export class QuestWindow extends Phaser.GameObjects.Container {
|
|||
if (this.openState === 'open' || this.openState === 'opening') return;
|
||||
this.openState = 'opening';
|
||||
this.setAlpha(0);
|
||||
this.video?.play?.();
|
||||
// resume() not play(): v4's play() is a no-op after pause() (its
|
||||
// _playCalled flag stays true, so the native play() is never
|
||||
// re-issued) — the feed would stay frozen after close → reopen.
|
||||
// resume() handles both a first open (delegates to play()) and a
|
||||
// re-open of a paused clip.
|
||||
this.video?.resume?.();
|
||||
this.sfx('ui_window');
|
||||
this._startReveal(this.scene.time.now);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1269,7 +1269,12 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
|
|||
this._paintAll(this.activeCat);
|
||||
this._paintStatusStrip();
|
||||
|
||||
this.video?.play?.();
|
||||
// resume() not play(): v4's play() is a no-op after pause() (its
|
||||
// _playCalled flag stays true, so the native play() is never
|
||||
// re-issued) — the feed would stay frozen after close → reopen.
|
||||
// resume() handles both a first open (delegates to play()) and a
|
||||
// re-open of a paused clip.
|
||||
this.video?.resume?.();
|
||||
this.sfx('ui_window');
|
||||
this._startReveal();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue