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 { openTurnReportScreen } from './VegaTurnReportScreen.js';
|
||||||
import { NOTABLE_TYPES, isRelevantToHuman } from './VegaTurnReport.js';
|
import { NOTABLE_TYPES, isRelevantToHuman } from './VegaTurnReport.js';
|
||||||
import { openAudienceScreen } from './VegaAudience.js';
|
import { openAudienceScreen } from './VegaAudience.js';
|
||||||
|
import { playIntroVideo } from './VegaIntroVideo.js';
|
||||||
import { claimAudienceContacts, claimFleetComplaints, canNegotiate } from './VegaDiplomacy.js';
|
import { claimAudienceContacts, claimFleetComplaints, canNegotiate } from './VegaDiplomacy.js';
|
||||||
|
|
||||||
const SAVE_KEY = 'mastervega-save';
|
const SAVE_KEY = 'mastervega-save';
|
||||||
|
|
@ -91,13 +92,25 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.music = new VegaMusic(this, this.cache.json.get('masterofvega-music'));
|
this.music = new VegaMusic(this, this.cache.json.get('masterofvega-music'));
|
||||||
this.music.setCategory('menu');
|
|
||||||
} catch (err) { /* music is optional */ }
|
} catch (err) { /* music is optional */ }
|
||||||
|
|
||||||
this.events.once('shutdown', () => this.teardown());
|
this.events.once('shutdown', () => this.teardown());
|
||||||
|
|
||||||
if (this.pendingSavedState) this.beginGame(null, this.pendingSavedState);
|
// A Load-slot resume drops straight into a running game and never sees a
|
||||||
else this.showLanding();
|
// 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
|
// 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