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.",
|
||||
"`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",
|
||||
"missing: set `land`/`surface` to the real file name (in `videoBase`) when",
|
||||
|
|
@ -20,11 +22,11 @@
|
|||
"videoBase": "assets/videos/",
|
||||
"volume": 1,
|
||||
"videos": [
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-01.mp4" },
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
||||
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4" },
|
||||
{ "land": "gasgiant-land-02.mp4", "surface": "terran-surface-01.mp4" },
|
||||
{ "land": "gasgiant-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", "takeoff": "terran-takeoff-02.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", "takeoff": "gasgiant-takeoff-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", "takeoff": "gasgiant-takeoff-01.mp4" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"frames": {
|
||||
"terran": [0, 1, 2],
|
||||
"rocky": [0, 1, 2],
|
||||
"gas": [0, 1, 2],
|
||||
"gas": [3, 4, 5],
|
||||
"ice": [0, 1, 2],
|
||||
"lava": [0, 1, 2]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
* 4. force the surface stage (skip the clip's runtime)
|
||||
* 5. assert deck slots (shop/takeoff/menu), save UI, surface clip
|
||||
* 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)
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
|
|
@ -118,9 +119,20 @@ window.game = game;
|
|||
ss.savePanel.close();
|
||||
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();
|
||||
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,
|
||||
{ ssStatus: ss.sys.getStatus(), gsStatus: gs.sys.getStatus() });
|
||||
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);
|
||||
ss2.takeOff();
|
||||
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);
|
||||
|
||||
window.__SURFACE_TEST__ = { pass: true, steps: window.__STEPS__ };
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// driver can grab a screenshot at exactly the moment we want:
|
||||
// __SHOT__ = 'landing' — the one-shot landing clip, full screen
|
||||
// __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.)
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
|
|
@ -47,6 +48,14 @@ import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
|||
ss.surfaceVideo.video.play().catch(() => {});
|
||||
}
|
||||
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 };
|
||||
})().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
|
||||
* (SavePanel `stateScene`).
|
||||
*
|
||||
* TAKE OFF stops this scene — the paused GameScene resumes exactly
|
||||
* where it was (ship, tethers, discovery, standing, everything).
|
||||
* 3. TAKE OFF — the world's one-shot take-off clip (`takeoff`) plays,
|
||||
* 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
|
||||
* 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.landKey = 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. */
|
||||
|
|
@ -55,10 +60,13 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
const entry = (config.get('landing.videos') ?? [])[this.planetFrame] ?? {};
|
||||
this.landKey = `__surf_land_${this.planetFrame}`;
|
||||
this.surfaceKey = `__surf_loop_${this.planetFrame}`;
|
||||
this.takeoffKey = `__surf_takeoff_${this.planetFrame}`;
|
||||
this.landUrl = this.resolveUrl(entry.land ?? 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.surfaceUrl) this.load.video(this.surfaceKey, this.surfaceUrl);
|
||||
if (this.takeoffUrl) this.load.video(this.takeoffKey, this.takeoffUrl);
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -249,13 +257,63 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
/**
|
||||
* TAKE OFF — leave the surface: stop this scene (the shutdown hook
|
||||
* cleans the deck + clips) and wake the sleeping GameScene — the run
|
||||
* resumes exactly where the ship left it.
|
||||
* TAKE OFF — leave the surface. The world's one-shot take-off clip
|
||||
* (`takeoff`) plays first, then the scene stops and the sleeping
|
||||
* GameScene wakes — the run resumes exactly where the ship left it.
|
||||
* No clip for this world → leave immediately.
|
||||
*/
|
||||
takeOff() {
|
||||
this.savePanel?.close();
|
||||
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.wake('GameScene');
|
||||
}
|
||||
|
|
@ -367,8 +425,10 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
shutdown() {
|
||||
this.destroyVideo(this.landVideo);
|
||||
this.destroyVideo(this.surfaceVideo);
|
||||
this.destroyVideo(this.takeoffVideo);
|
||||
this.landVideo = null;
|
||||
this.surfaceVideo = null;
|
||||
this.takeoffVideo = null;
|
||||
this.actionBar?.destroy();
|
||||
this.menuSubBar?.destroy();
|
||||
this.savePanel?.destroy();
|
||||
|
|
|
|||
Loading…
Reference in New Issue