// Master of Vega — the colony-founding vignette. // // Planting a colony used to be a line in the next turn's "New Turn" report, // which is the same weight the game gives a finished refit. It is the single // most consequential thing a player does on the map, so it now stops the game // instead: the soundtrack ducks, the world fills the screen, a clip of THIS // world being settled plays in a window over it, and the founding numbers are // read out underneath. // // Two pictures of the same world are on screen at once. The BACKDROP is always // the 1920x1080 still (`worldBackgrounds[typeId]`, gradient-painted from the // type's colour when that art does not exist), and the CLIP // (`colonyVideos[typeId]`, 960x544) plays in a framed window between the // masthead and the congratulation line, right-aligned so its edge lands on the // centre line of the planet disc in the top corner. The clip is optional; the // window is simply not built for a type that has none, and the vignette reads // as a still-backed report card instead of looking broken. // // The clip is scaled by WIDTH alone, never `setDisplaySize` — 960x544 is // 1.765:1 and the window is cut to match, so anything that fits the two // independently would skew every horizon in the set. // // It plays ONCE, with its own audio at 0.9 over the founding cue, and holds its // last frame — then a replay badge lights in the corner of the window and a // click anywhere on the picture runs it again. That is why // `ensureColonyVideo()` loads it with `noAudio: false`, unlike every other // video in this game, and why `applyAudio()` exists. // // THE CLIPS ARE NOT IN THE ASSET MANIFEST — but NOT for the reason it looks // like. Phaser's video loader downloads nothing: `VideoFile.load()` records the // URL, marks itself complete and returns ("we don't actually load anything, the // Video Game Object does that"). So the manifest was never pulling 19 MB on // entering the game room; those bytes arrive when a `Video` sets `el.src`, // which is the moment the vignette opens. Keeping the block out of the manifest // is what lets THIS file own that timing instead: `ensureColonyVideo()` // registers the URL *and* warms the bytes through a detached element, and the // system view calls it the moment a colony ship is in orbit over a settleable // world — the strongest signal we ever get that the vignette is about to be // needed. If the clip has not landed by the time it opens, the window says so // and the picture fades into it on arrival, so the network never holds the // moment up. import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; import { Button } from './VegaButton.js'; import { TextInput } from '../../ui/TextInput.js'; import { FONT, D, ORBIT, uiClick } from './VegaScreens.js'; import { turnToYear } from './VegaRules.js'; import { colonyVideoKey, hasColonyVideo, worldBackground, planetFrame, sourceWidth, } from './VegaArt.js'; import { colonyMaxPop, colonyProduction, colonyFactoryCap, colonyBuildRate, empireColonies, } from './VegaLogic.js'; /** Source resolution of every colony clip. The window below is cut to match. */ const SRC_W = 960; const SRC_H = 544; /** The founding cue (assets/music/vega/colony.mp3), loaded by the manifest. */ const CUE_KEY = 'vega-colony-cue'; const CUE_VOLUME = 0.7; /** The clip's own soundtrack, played over the cue. */ const CLIP_VOLUME = 0.9; const ACCENT = 0x6fc4ff; const PANEL = 0x0b1220; const PANEL_X = 64; const PANEL_Y = 838; const PANEL_W = 1180; const PANEL_H = 176; /** The orrery disc in the top corner. Its centre line is also the clip's right edge. */ const DISC_CX = GAME_WIDTH - 150; const DISC_CY = 158; const DISC_SIZE = 150; // The clip window: right edge on the disc's centre line, centred in the band // between the masthead (which ends around y=283, under the empire/year line) // and the congratulation line at y=700, leaving ~19px of air at each end. Cut // to the clip's own 960x544 so the picture is never distorted to fit it. const VIDEO_W = 640; const VIDEO_H = Math.round((VIDEO_W * SRC_H) / SRC_W); const VIDEO_CX = DISC_CX - VIDEO_W / 2; const VIDEO_CY = 491; /** How far the frame stands off the picture on every side. */ const VIDEO_INSET = 8; /** The replay badge, inset from the picture's bottom-right corner. */ const REPLAY_R = 26; const REPLAY_PAD = 18; // --------------------------------------------------------------------------- // Just-in-time clip loading /** * Clips handed to a scene's loader but not yet arrived, per scene. * * A WeakMap rather than one module-level Set because the loader is per-scene: * leaving Master of Vega mid-fetch and coming back must not leave a key stuck * in a set that the NEW scene's loader knows nothing about, which would wedge * that clip on the still fallback forever. */ const inFlight = new WeakMap(); function pendingFor(scene) { let set = inFlight.get(scene); if (!set) { set = new Set(); inFlight.set(scene, set); } return set; } /** The path is declared in the artwork manifest, which is eager-preloaded. */ function colonyVideoPath(scene, typeId) { return scene.cache.json.get('mastervega-artwork')?.colonyVideos?.[typeId]?.path ?? null; } /** * Detached elements holding a warmed clip, per scene. See warmBytes(). */ const warmed = new WeakMap(); /** * Actually pull the bytes down. * * `scene.load.video()` does NOT do this. Phaser's `VideoFile.load()` records * the URL, marks itself complete and returns — its own comment reads "we don't * actually load anything (the Video Game Object does that)". The download * starts when a `Video` sets `el.src`, which for this file is the moment the * vignette opens. So the loader alone buys nothing but a cache entry, and a * "warm-up" through it would leave the first megabyte arriving over the * player's shoulder. * * A detached `