Add take-off clip to surface scene before resuming flight
- Introduce a `takeoff` video entry in landing.json for each world; missing/ null entries leave the surface instantly (no regression) - SurfaceScene.takeOff now plays the one-shot take-off clip full-screen, hides the deck, and only then stops the scene and wakes GameScene - Guard against a clip that never starts or runs past its duration so the player is never stranded; finishTakeoff is idempotent - Update surface-flow/surface-shot dev harnesses to assert and screenshot the new take-off stage - Re-map gas giant frames in planets.json from [0,1,2] to [3,4,5] to match their sheet positions
This commit is contained in:
parent
69875c1ff0
commit
1cbbf502df
|
|
@ -6,6 +6,8 @@
|
||||||
"",
|
"",
|
||||||
"`land` — the full-screen one-shot clip played when the ship lands.",
|
"`land` — the full-screen one-shot clip played when the ship lands.",
|
||||||
"`surface` — the looping clip shown on the surface deck behind the action bar.",
|
"`surface` — the looping clip shown on the surface deck behind the action bar.",
|
||||||
|
"`takeoff` — the full-screen one-shot clip played on TAKE OFF, before the",
|
||||||
|
" flight world resumes. A null/missing entry takes off instantly.",
|
||||||
"",
|
"",
|
||||||
"Entries are stubbed with the existing videos where a specific clip is",
|
"Entries are stubbed with the existing videos where a specific clip is",
|
||||||
"missing: set `land`/`surface` to the real file name (in `videoBase`) when",
|
"missing: set `land`/`surface` to the real file name (in `videoBase`) when",
|
||||||
|
|
@ -20,11 +22,11 @@
|
||||||
"videoBase": "assets/videos/",
|
"videoBase": "assets/videos/",
|
||||||
"volume": 1,
|
"volume": 1,
|
||||||
"videos": [
|
"videos": [
|
||||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4" },
|
||||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-01.mp4" },
|
{ "land": "terran-land-02.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-02.mp4" },
|
||||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4" },
|
||||||
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4" },
|
||||||
{ "land": "gasgiant-land-02.mp4", "surface": "terran-surface-01.mp4" },
|
{ "land": "gasgiant-land-02.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-02.mp4" },
|
||||||
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4" }
|
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"frames": {
|
"frames": {
|
||||||
"terran": [0, 1, 2],
|
"terran": [0, 1, 2],
|
||||||
"rocky": [0, 1, 2],
|
"rocky": [0, 1, 2],
|
||||||
"gas": [0, 1, 2],
|
"gas": [3, 4, 5],
|
||||||
"ice": [0, 1, 2],
|
"ice": [0, 1, 2],
|
||||||
"lava": [0, 1, 2]
|
"lava": [0, 1, 2]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
* 4. force the surface stage (skip the clip's runtime)
|
* 4. force the surface stage (skip the clip's runtime)
|
||||||
* 5. assert deck slots (shop/takeoff/menu), save UI, surface clip
|
* 5. assert deck slots (shop/takeoff/menu), save UI, surface clip
|
||||||
* 6. menuAction → sub-bar opens; save panel opens
|
* 6. menuAction → sub-bar opens; save panel opens
|
||||||
* 7. takeOff → SurfaceScene stopped, GameScene resumed, ship preserved
|
* 7. takeOff → take-off clip plays, then (forced complete) SurfaceScene
|
||||||
|
* stopped, GameScene resumed, ship preserved
|
||||||
* 8. land again → takeOff again (repeat-launch/re-stop cycle)
|
* 8. land again → takeOff again (repeat-launch/re-stop cycle)
|
||||||
*/
|
*/
|
||||||
import Phaser from '../js/vendor/phaser.js';
|
import Phaser from '../js/vendor/phaser.js';
|
||||||
|
|
@ -118,9 +119,20 @@ window.game = game;
|
||||||
ss.savePanel.close();
|
ss.savePanel.close();
|
||||||
ss.menuSubBar.dismiss();
|
ss.menuSubBar.dismiss();
|
||||||
|
|
||||||
// 7 — TAKE OFF: overlay stopped, flight scene woken, ship where it was.
|
// 7 — TAKE OFF: the take-off clip plays first, then overlay stopped,
|
||||||
|
// flight scene woken, ship where it was.
|
||||||
|
const takeoffKey = `__surf_takeoff_${ss.planetFrame}`;
|
||||||
|
const takeoffCached = ss.cache.video.has(takeoffKey);
|
||||||
ss.takeOff();
|
ss.takeOff();
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
|
if (takeoffCached) {
|
||||||
|
step('takeoff-clip', ss.takeoffVideo !== null && ss.sys.isActive() === true
|
||||||
|
&& ss.surfaceVideo === null && ss.actionBar?.visible === false,
|
||||||
|
{ takeoffKey, surfaceVideo: ss.surfaceVideo, deckVisible: ss.actionBar?.visible });
|
||||||
|
// the test does not wait out the 15–20s clip — force its completion
|
||||||
|
ss.takeoffVideo.emit('complete', ss.takeoffVideo);
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
step('takeoff', ss.sys.isActive() === false && gs.sys.isActive() === true,
|
step('takeoff', ss.sys.isActive() === false && gs.sys.isActive() === true,
|
||||||
{ ssStatus: ss.sys.getStatus(), gsStatus: gs.sys.getStatus() });
|
{ ssStatus: ss.sys.getStatus(), gsStatus: gs.sys.getStatus() });
|
||||||
step('ship-preserved', Math.abs(gs.ship.x - shipBefore.x) < 1e-6
|
step('ship-preserved', Math.abs(gs.ship.x - shipBefore.x) < 1e-6
|
||||||
|
|
@ -137,6 +149,11 @@ window.game = game;
|
||||||
step('re-surface', ss2.phase === 'surface' && ss2.actionBar !== null);
|
step('re-surface', ss2.phase === 'surface' && ss2.actionBar !== null);
|
||||||
ss2.takeOff();
|
ss2.takeOff();
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
|
if (ss2.cache.video.has(`__surf_takeoff_${ss2.planetFrame}`) && ss2.takeoffVideo) {
|
||||||
|
step('re-takeoff-clip', ss2.takeoffVideo !== null && ss2.sys.isActive() === true);
|
||||||
|
ss2.takeoffVideo.emit('complete', ss2.takeoffVideo);
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
step('re-takeoff', ss2.sys.isActive() === false && gs.sys.isActive() === true);
|
step('re-takeoff', ss2.sys.isActive() === false && gs.sys.isActive() === true);
|
||||||
|
|
||||||
window.__SURFACE_TEST__ = { pass: true, steps: window.__STEPS__ };
|
window.__SURFACE_TEST__ = { pass: true, steps: window.__STEPS__ };
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// driver can grab a screenshot at exactly the moment we want:
|
// driver can grab a screenshot at exactly the moment we want:
|
||||||
// __SHOT__ = 'landing' — the one-shot landing clip, full screen
|
// __SHOT__ = 'landing' — the one-shot landing clip, full screen
|
||||||
// __SHOT__ = 'surface' — looping surface clip + the surface deck
|
// __SHOT__ = 'surface' — looping surface clip + the surface deck
|
||||||
|
// __SHOT__ = 'takeoff' — the one-shot take-off clip (deck hidden)
|
||||||
// (dev/surface-flow.mjs is the assertion suite; this one just parks.)
|
// (dev/surface-flow.mjs is the assertion suite; this one just parks.)
|
||||||
import Phaser from '../js/vendor/phaser.js';
|
import Phaser from '../js/vendor/phaser.js';
|
||||||
import { config } from '../js/config/Config.js';
|
import { config } from '../js/config/Config.js';
|
||||||
|
|
@ -47,6 +48,14 @@ import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
||||||
ss.surfaceVideo.video.play().catch(() => {});
|
ss.surfaceVideo.video.play().catch(() => {});
|
||||||
}
|
}
|
||||||
await sleep(1200);
|
await sleep(1200);
|
||||||
window.__SHOT__ = 'surface'; // ← screenshot #2 (loop + deck), stays here
|
window.__SHOT__ = 'surface'; // ← screenshot #2 (loop + deck)
|
||||||
|
await sleep(600);
|
||||||
|
ss.takeOff(); // the take-off clip owns the screen now
|
||||||
|
if (ss.takeoffVideo && ss.takeoffVideo.video) {
|
||||||
|
ss.takeoffVideo.video.muted = true;
|
||||||
|
ss.takeoffVideo.video.play().catch(() => {});
|
||||||
|
}
|
||||||
|
await sleep(1200);
|
||||||
|
window.__SHOT__ = 'takeoff'; // ← screenshot #3 (take-off clip), stays here
|
||||||
window.__SURFACE_TEST__ = { pass: true, parked: true };
|
window.__SURFACE_TEST__ = { pass: true, parked: true };
|
||||||
})().catch((err) => { window.__SHOT__ = 'FAILED: ' + String(err); window.__SURFACE_TEST__ = { pass: false, error: String(err) }; });
|
})().catch((err) => { window.__SHOT__ = 'FAILED: ' + String(err); window.__SURFACE_TEST__ = { pass: false, error: String(err) }; });
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ import { SaveManager } from '../save/SaveManager.js';
|
||||||
* save sub-bar — Save/Load operate on the paused GameScene's state
|
* save sub-bar — Save/Load operate on the paused GameScene's state
|
||||||
* (SavePanel `stateScene`).
|
* (SavePanel `stateScene`).
|
||||||
*
|
*
|
||||||
* TAKE OFF stops this scene — the paused GameScene resumes exactly
|
* 3. TAKE OFF — the world's one-shot take-off clip (`takeoff`) plays,
|
||||||
* where it was (ship, tethers, discovery, standing, everything).
|
* then this scene stops — the paused GameScene resumes exactly where
|
||||||
|
* it was (ship, tethers, discovery, standing, everything). A world
|
||||||
|
* with no `takeoff` clip leaves immediately.
|
||||||
*
|
*
|
||||||
* Video selection is by the planet's planets.png SHEET FRAME (the same
|
* Video selection is by the planet's planets.png SHEET FRAME (the same
|
||||||
* index its sprite uses — frames 0..2 the terran worlds, 3..5 the gas
|
* index its sprite uses — frames 0..2 the terran worlds, 3..5 the gas
|
||||||
|
|
@ -48,6 +50,9 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
this.saveManager = null;
|
this.saveManager = null;
|
||||||
this.landKey = null;
|
this.landKey = null;
|
||||||
this.surfaceKey = null;
|
this.surfaceKey = null;
|
||||||
|
this.takeoffKey = null;
|
||||||
|
this.takeoffVideo = null;
|
||||||
|
this.takeoffDone = false; // finishTakeoff idempotency ('complete' + a guard can both fire)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pick this world's clips (data/landing.json) and queue them. */
|
/** Pick this world's clips (data/landing.json) and queue them. */
|
||||||
|
|
@ -55,10 +60,13 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
const entry = (config.get('landing.videos') ?? [])[this.planetFrame] ?? {};
|
const entry = (config.get('landing.videos') ?? [])[this.planetFrame] ?? {};
|
||||||
this.landKey = `__surf_land_${this.planetFrame}`;
|
this.landKey = `__surf_land_${this.planetFrame}`;
|
||||||
this.surfaceKey = `__surf_loop_${this.planetFrame}`;
|
this.surfaceKey = `__surf_loop_${this.planetFrame}`;
|
||||||
|
this.takeoffKey = `__surf_takeoff_${this.planetFrame}`;
|
||||||
this.landUrl = this.resolveUrl(entry.land ?? null);
|
this.landUrl = this.resolveUrl(entry.land ?? null);
|
||||||
this.surfaceUrl = this.resolveUrl(entry.surface ?? null);
|
this.surfaceUrl = this.resolveUrl(entry.surface ?? null);
|
||||||
|
this.takeoffUrl = this.resolveUrl(entry.takeoff ?? null);
|
||||||
if (this.landUrl) this.load.video(this.landKey, this.landUrl);
|
if (this.landUrl) this.load.video(this.landKey, this.landUrl);
|
||||||
if (this.surfaceUrl) this.load.video(this.surfaceKey, this.surfaceUrl);
|
if (this.surfaceUrl) this.load.video(this.surfaceKey, this.surfaceUrl);
|
||||||
|
if (this.takeoffUrl) this.load.video(this.takeoffKey, this.takeoffUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -249,13 +257,63 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TAKE OFF — leave the surface: stop this scene (the shutdown hook
|
* TAKE OFF — leave the surface. The world's one-shot take-off clip
|
||||||
* cleans the deck + clips) and wake the sleeping GameScene — the run
|
* (`takeoff`) plays first, then the scene stops and the sleeping
|
||||||
* resumes exactly where the ship left it.
|
* GameScene wakes — the run resumes exactly where the ship left it.
|
||||||
|
* No clip for this world → leave immediately.
|
||||||
*/
|
*/
|
||||||
takeOff() {
|
takeOff() {
|
||||||
this.savePanel?.close();
|
this.savePanel?.close();
|
||||||
this.menuSubBar?.dismiss();
|
this.menuSubBar?.dismiss();
|
||||||
|
if (this.takeoffVideo) return; // the take-off clip is already in flight
|
||||||
|
if (this.hasVideo(this.takeoffKey)) {
|
||||||
|
this.playTakeoff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.finishTakeoff();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage 3 — the one-shot take-off clip, then back to the flight world.
|
||||||
|
* Same shape as the landing stage (full-screen clip, no deck over it),
|
||||||
|
* and the same two guards: a clip that never starts, or goes silent
|
||||||
|
* past duration + margin, must not hold the run.
|
||||||
|
*/
|
||||||
|
playTakeoff() {
|
||||||
|
// The deck + loop clip belong to the surface stage — the clip owns
|
||||||
|
// the screen (same "no UI mid-flight" rule as the landing stage).
|
||||||
|
this.actionBar?.setVisible(false);
|
||||||
|
if (this.surfaceVideo) {
|
||||||
|
this.destroyVideo(this.surfaceVideo); // the opaque clip hides it — free the decoder
|
||||||
|
this.surfaceVideo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = this.add.video(0, 0, this.takeoffKey).setOrigin(0.5).setDepth(20);
|
||||||
|
this.attachClip(v);
|
||||||
|
this.takeoffVideo = v;
|
||||||
|
v.play();
|
||||||
|
v.on('complete', () => this.finishTakeoff()); // played once → back to the ship
|
||||||
|
v.on('error', () => this.finishTakeoff()); // broken clip → don't strand the player
|
||||||
|
|
||||||
|
const el = v.video;
|
||||||
|
const playing = () => !!el && (el.currentTime > 0.1 || !el.paused);
|
||||||
|
this.time.delayedCall(5000, () => {
|
||||||
|
if (this.takeoffVideo === v && !playing()) this.finishTakeoff();
|
||||||
|
});
|
||||||
|
const durS = Number(el && el.duration);
|
||||||
|
if (Number.isFinite(durS) && durS > 0) {
|
||||||
|
this.time.delayedCall(durS * 1000 + 5000, () => {
|
||||||
|
if (this.takeoffVideo === v) this.finishTakeoff();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Take-off clip done (or bailed out) — back to the flight world. */
|
||||||
|
finishTakeoff() {
|
||||||
|
if (this.takeoffDone) return;
|
||||||
|
this.takeoffDone = true;
|
||||||
|
this.destroyVideo(this.takeoffVideo);
|
||||||
|
this.takeoffVideo = null;
|
||||||
this.scene.stop('SurfaceScene');
|
this.scene.stop('SurfaceScene');
|
||||||
this.scene.wake('GameScene');
|
this.scene.wake('GameScene');
|
||||||
}
|
}
|
||||||
|
|
@ -367,8 +425,10 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
shutdown() {
|
shutdown() {
|
||||||
this.destroyVideo(this.landVideo);
|
this.destroyVideo(this.landVideo);
|
||||||
this.destroyVideo(this.surfaceVideo);
|
this.destroyVideo(this.surfaceVideo);
|
||||||
|
this.destroyVideo(this.takeoffVideo);
|
||||||
this.landVideo = null;
|
this.landVideo = null;
|
||||||
this.surfaceVideo = null;
|
this.surfaceVideo = null;
|
||||||
|
this.takeoffVideo = null;
|
||||||
this.actionBar?.destroy();
|
this.actionBar?.destroy();
|
||||||
this.menuSubBar?.destroy();
|
this.menuSubBar?.destroy();
|
||||||
this.savePanel?.destroy();
|
this.savePanel?.destroy();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue