feat(masterofvega): add studio intro video and colony focus misalignment indicators
- Add full-screen intro video (mov-intro.mp4) that plays once on fresh game entry, with skip prompt on first keypress and immediate skip on second - Include intro soundtrack (intro-theme.mp3) and updated menu title image - Skip video on Load-slot resume to avoid interrupting returning players - Add orange highlight to Colony/Allocation Focus pills when their setting disagrees with the advisor's recommendation (recommendColonyFocus/ recommendAllocationFocus)
This commit is contained in:
parent
72f44178b0
commit
e78f626406
Binary file not shown.
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 559 KiB |
Binary file not shown.
Binary file not shown.
|
|
@ -37,6 +37,7 @@ import { openResearchChoiceScreen } from './VegaResearchChoiceScreen.js';
|
|||
import { openTurnReportScreen } from './VegaTurnReportScreen.js';
|
||||
import { NOTABLE_TYPES, isRelevantToHuman } from './VegaTurnReport.js';
|
||||
import { openAudienceScreen } from './VegaAudience.js';
|
||||
import { playIntroVideo } from './VegaIntroVideo.js';
|
||||
import { claimAudienceContacts, claimFleetComplaints, canNegotiate } from './VegaDiplomacy.js';
|
||||
|
||||
const SAVE_KEY = 'mastervega-save';
|
||||
|
|
@ -91,13 +92,25 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
|
||||
try {
|
||||
this.music = new VegaMusic(this, this.cache.json.get('masterofvega-music'));
|
||||
this.music.setCategory('menu');
|
||||
} catch (err) { /* music is optional */ }
|
||||
|
||||
this.events.once('shutdown', () => this.teardown());
|
||||
|
||||
if (this.pendingSavedState) this.beginGame(null, this.pendingSavedState);
|
||||
else this.showLanding();
|
||||
// A Load-slot resume drops straight into a running game and never sees a
|
||||
// landing screen at all, so it skips the intro too — only a fresh entry
|
||||
// plays it, right before showLanding(). The menu track waits until
|
||||
// whichever of those actually runs: the intro clip carries its own
|
||||
// soundtrack, and starting the menu theme underneath it would just be two
|
||||
// tracks fighting for the same speakers.
|
||||
if (this.pendingSavedState) {
|
||||
this.music?.setCategory('menu');
|
||||
this.beginGame(null, this.pendingSavedState);
|
||||
} else {
|
||||
playIntroVideo(this, () => {
|
||||
this.music?.setCategory('menu');
|
||||
this.showLanding();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap a menu handler so it clicks. Every button and card on the front-end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
// Master of Vega — the full-screen studio intro that plays once, ahead of the
|
||||
// landing screen, on every fresh entry into the game (never on a Load-slot
|
||||
// resume, which drops straight into a running game and has no landing screen
|
||||
// to precede).
|
||||
//
|
||||
// Unlike every other video in this game (colonyVideos, audienceVideos, the
|
||||
// commander portrait loops — see VegaColonyIntro.js and VegaShipMedia.js),
|
||||
// this one is loaded with `noAudio: false`: the clip carries its own
|
||||
// soundtrack and there is no separate track to layer under it. It is safe to
|
||||
// autoplay with sound here specifically because reaching this scene already
|
||||
// required a user gesture (clicking the game's icon in the arcade), which is
|
||||
// what the browser's autoplay gate actually checks for.
|
||||
|
||||
import * as Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { FONT } from './VegaScreens.js';
|
||||
|
||||
const INTRO_KEY = 'vega-mov-intro';
|
||||
const INTRO_PATH = 'assets/videos/vega/mov-intro.mp4';
|
||||
// Source resolution, used until the real texture decodes — see VegaArt.js's
|
||||
// sourceWidth() for why a Video's own width/height lie (a square placeholder)
|
||||
// before its first frame.
|
||||
const INTRO_SRC_W = 854;
|
||||
const INTRO_SRC_H = 480;
|
||||
|
||||
// How long "Press any key to skip" stays up after the first press before it
|
||||
// quietly stands down (rather than skipping on its own) and waits for the
|
||||
// next one.
|
||||
const SKIP_HOLD_MS = 5000;
|
||||
|
||||
/**
|
||||
* Play the intro clip full-screen, then call `onDone`. The first key press or
|
||||
* click surfaces a skip prompt; a second one inside SKIP_HOLD_MS jumps
|
||||
* straight to `onDone`. A load failure or playback error also falls straight
|
||||
* through to `onDone` — a missing intro is never allowed to strand the player
|
||||
* in front of a black screen.
|
||||
*/
|
||||
export function playIntroVideo(scene, onDone) {
|
||||
let done = false;
|
||||
let video = null;
|
||||
let skipArmed = false;
|
||||
let skipTimer = null;
|
||||
let skipText = null;
|
||||
|
||||
const root = scene.add.container(0, 0).setDepth(1000);
|
||||
root.add(scene.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x000000, 1).setOrigin(0, 0));
|
||||
|
||||
const clearPrompt = () => {
|
||||
skipTimer?.remove(false);
|
||||
skipTimer = null;
|
||||
skipText?.destroy();
|
||||
skipText = null;
|
||||
skipArmed = false;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
scene.input.keyboard?.off('keydown', onInput);
|
||||
scene.input.off('pointerdown', onInput);
|
||||
clearPrompt();
|
||||
video?.destroy();
|
||||
root.destroy();
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
cleanup();
|
||||
onDone();
|
||||
};
|
||||
|
||||
// Belt-and-braces: if the scene tears down mid-clip (e.g. a hot reload),
|
||||
// drop our own listeners and the video element without firing onDone a
|
||||
// second time behind whatever else shutdown is already doing.
|
||||
scene.events.once('shutdown', () => { if (!done) { done = true; cleanup(); } });
|
||||
|
||||
const armSkipPrompt = () => {
|
||||
skipArmed = true;
|
||||
skipText = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 96, 'Press any key to skip', {
|
||||
fontFamily: FONT, fontSize: '28px', color: '#e8f4ff',
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
root.add(skipText);
|
||||
scene.tweens.add({ targets: skipText, alpha: 1, duration: 200 });
|
||||
skipTimer = scene.time.delayedCall(SKIP_HOLD_MS, clearPrompt);
|
||||
};
|
||||
|
||||
function onInput() {
|
||||
if (done) return;
|
||||
if (skipArmed) { finish(); return; }
|
||||
armSkipPrompt();
|
||||
}
|
||||
|
||||
scene.input.keyboard?.on('keydown', onInput);
|
||||
scene.input.on('pointerdown', onInput);
|
||||
|
||||
const startVideo = () => {
|
||||
video = scene.add.video(GAME_WIDTH / 2, GAME_HEIGHT / 2, INTRO_KEY);
|
||||
video.setLoop(false);
|
||||
video.setMute(false);
|
||||
video.setVolume(1);
|
||||
|
||||
// Cover, not contain: this is meant to fill the screen edge-to-edge, and
|
||||
// the clip's own aspect (854x480) is within a hair of the game's
|
||||
// (1920x1080) anyway, so the crop is imperceptible.
|
||||
const fit = () => {
|
||||
const real = !!video.videoTexture;
|
||||
const w = real ? video.width : INTRO_SRC_W;
|
||||
const h = real ? video.height : INTRO_SRC_H;
|
||||
video.setScale(Math.max(GAME_WIDTH / w, GAME_HEIGHT / h));
|
||||
};
|
||||
fit();
|
||||
video.on('created', fit);
|
||||
video.on('playing', fit);
|
||||
|
||||
video.once('complete', finish);
|
||||
video.once('error', finish);
|
||||
// Same as every other audio-bearing clip in this game (see
|
||||
// VegaColonyIntro.js's startVideo): a browser that refuses to autoplay
|
||||
// sound fires 'locked' and keeps retrying muted from preUpdate.
|
||||
video.once('locked', () => { if (video?.scene) video.setMute(true); });
|
||||
|
||||
video.play(false);
|
||||
root.add(video);
|
||||
};
|
||||
|
||||
if (scene.cache.video?.exists(INTRO_KEY)) {
|
||||
startVideo();
|
||||
return;
|
||||
}
|
||||
|
||||
scene.load.video(INTRO_KEY, INTRO_PATH, false);
|
||||
scene.load.once(`filecomplete-video-${INTRO_KEY}`, () => { if (!done) startVideo(); });
|
||||
scene.load.once(Phaser.Loader.Events.FILE_LOAD_ERROR, (file) => {
|
||||
if (file?.key === INTRO_KEY) finish();
|
||||
});
|
||||
if (!scene.load.isLoading()) scene.load.start();
|
||||
}
|
||||
Loading…
Reference in New Issue