refactor: lazy-load audience mood clips one at a time instead of pre-warming all three
Replace the eager `ensureAudienceVideos()` warm-up (which loaded all three mood clips per species upfront) with on-demand JIT loading — only the mood about to be shown is fetched. This saves several megabytes per species since most conversations never cycle through all moods. - Remove `ensureAudienceVideos()` and its call in `openAudienceScreen()` - Load each mood clip only when `show()` is first called for that mood - Show a letterboxed placeholder for the very first clip until the fetch lands - Keep whatever is already on screen while a new mood loads in the background - Add `destroy()` guard to prevent in-flight fetches from mutating a torn-down container when the audience screen is closed Also ship the dedicated Kestrelli audience clips (angry, neutral, happy) to replace the previous human fallback videos for that species.
This commit is contained in:
parent
7e7956d86d
commit
df4c1d7b02
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -136,9 +136,9 @@
|
|||
"happy": { "key": "vega-audience-human-happy", "path": "assets/videos/vega/audience-human-happy.mp4" }
|
||||
},
|
||||
"kestrelli": {
|
||||
"angry": { "key": "vega-audience-human-angry", "path": "assets/videos/vega/audience-human-angry.mp4" },
|
||||
"neutral": { "key": "vega-audience-human-neutral", "path": "assets/videos/vega/audience-human-neutral.mp4" },
|
||||
"happy": { "key": "vega-audience-human-happy", "path": "assets/videos/vega/audience-human-happy.mp4" }
|
||||
"angry": { "key": "vega-audience-kestrelli-angry", "path": "assets/videos/vega/audience-kestrelli-angry.mp4" },
|
||||
"neutral": { "key": "vega-audience-kestrelli-neutral", "path": "assets/videos/vega/audience-kestrelli-neutral.mp4" },
|
||||
"happy": { "key": "vega-audience-kestrelli-happy", "path": "assets/videos/vega/audience-kestrelli-happy.mp4" }
|
||||
},
|
||||
"ursaal": {
|
||||
"angry": { "key": "vega-audience-human-angry", "path": "assets/videos/vega/audience-human-angry.mp4" },
|
||||
|
|
|
|||
|
|
@ -22,12 +22,16 @@
|
|||
// VegaArt.sourceWidth's "trap 23" (docs/mastervega-build-plan.md), a fresh
|
||||
// Phaser Video reports a 256px placeholder width — never 0 — until it decodes,
|
||||
// so scale is always derived from sourceWidth(), re-applied on 'created' and
|
||||
// 'playing'. All three mood clips for a species are JIT-loaded (like
|
||||
// colonyVideos, not the eager shipVideos/portraitVideos), warmed the instant
|
||||
// contact is claimed and again here on open. A species with no audience clips
|
||||
// yet falls back to one shared, un-swapping portrait (video/still), letterboxed
|
||||
// into the 3:2 frame with that species' own colour as the bar — so an art-less
|
||||
// species is still fully playable.
|
||||
// 'playing'. Each mood clip is JIT-loaded (like colonyVideos, not the eager
|
||||
// shipVideos/portraitVideos) one at a time, only the moment it is actually
|
||||
// about to be shown — never all three moods at once, since a species' full
|
||||
// set runs several megabytes and most conversations never even change mood.
|
||||
// Whatever is already on screen keeps playing while a newly-needed mood loads
|
||||
// behind it; the very first clip a species ever shows gets a letterboxed
|
||||
// placeholder in its place until that first fetch lands. A species with no
|
||||
// audience clips at all falls back to that same placeholder permanently,
|
||||
// letterboxed into the 3:2 frame with that species' own colour as the bar —
|
||||
// so an art-less species is still fully playable.
|
||||
|
||||
import * as Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
|
|
@ -118,18 +122,6 @@ function ensureOneAudienceVideo(scene, speciesId, mood, onDone) {
|
|||
if (!scene.load.isLoading()) scene.load.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm all three mood clips for a species. Mood can shift mid-conversation, so
|
||||
* all three are worth having ready rather than only the one currently shown.
|
||||
* Called speculatively the instant a fresh contact is claimed, and again here
|
||||
* on open (covers a manual Seek Audience click long after original contact).
|
||||
*/
|
||||
export function ensureAudienceVideos(scene, speciesId, onReady = null) {
|
||||
let remaining = MOODS.length;
|
||||
const settle = () => { remaining -= 1; if (remaining <= 0) onReady?.(); };
|
||||
for (const mood of MOODS) ensureOneAudienceVideo(scene, speciesId, mood, settle);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// The mood visual: three pre-built Video objects (built lazily on first use,
|
||||
// per mood), hide+pause on swap, never destroy+recreate — safe and cheap per
|
||||
|
|
@ -161,7 +153,12 @@ function buildLetterboxedFallback(scene, rules, art, speciesId, x, y, w, h) {
|
|||
// swapping back to an already-finished mood re-arms the badge immediately),
|
||||
// on the video's own 'complete' event, and on an explicit replay() call.
|
||||
function createMoodVisual(scene, rules, art, speciesId, x, y, w, h, parent, onEndedChanged) {
|
||||
const anyClip = MOODS.some((m) => hasAudienceVideo(scene, speciesId, m));
|
||||
// Whether this species HAS clips at all is a property of the artwork
|
||||
// manifest, known synchronously — it must not be read off `hasAudienceVideo`
|
||||
// (whether bytes happen to be loaded already), or the very first audience
|
||||
// this session would always read as clip-less and get stuck on the
|
||||
// placeholder forever, having never asked the loader for anything.
|
||||
const anyClip = MOODS.some((m) => !!audienceVideoPath(scene, speciesId, m));
|
||||
if (!anyClip) {
|
||||
// Nothing to visually swap, but `current` still has to track the real
|
||||
// mood — otherwise afterAction()'s "did the tier change" check compares
|
||||
|
|
@ -173,11 +170,14 @@ function createMoodVisual(scene, rules, art, speciesId, x, y, w, h, parent, onEn
|
|||
parent.add(shared);
|
||||
let current = null;
|
||||
return {
|
||||
show(mood) { current = mood; }, get current() { return current; }, replay() {},
|
||||
show(mood) { current = mood; }, get current() { return current; }, replay() {}, destroy() {},
|
||||
};
|
||||
}
|
||||
const built = {}; // mood -> { obj, ended }
|
||||
let current = null;
|
||||
let current = null; // mood actually on screen right now
|
||||
let wanted = null; // mood show() last asked for — may still be loading
|
||||
let placeholder = null; // letterboxed stand-in, only ever needed before the FIRST clip lands
|
||||
let destroyed = false;
|
||||
|
||||
function build(mood) {
|
||||
const obj = scene.add.video(x, y, audienceVideoKey(speciesId, mood));
|
||||
|
|
@ -202,9 +202,9 @@ function createMoodVisual(scene, rules, art, speciesId, x, y, w, h, parent, onEn
|
|||
return entry;
|
||||
}
|
||||
|
||||
return {
|
||||
show(mood) {
|
||||
if (current === mood) return;
|
||||
/** Swap the visible clip to `mood`, which must already be in the video cache. */
|
||||
function showLoaded(mood) {
|
||||
placeholder?.setVisible(false);
|
||||
if (built[current]) { built[current].obj.setVisible(false); built[current].obj.pause(); }
|
||||
let entry = built[mood];
|
||||
if (!entry) entry = build(mood);
|
||||
|
|
@ -217,6 +217,31 @@ function createMoodVisual(scene, rules, art, speciesId, x, y, w, h, parent, onEn
|
|||
}
|
||||
current = mood;
|
||||
onEndedChanged?.(entry.ended);
|
||||
}
|
||||
|
||||
return {
|
||||
show(mood) {
|
||||
if (wanted === mood) return;
|
||||
wanted = mood;
|
||||
if (hasAudienceVideo(scene, speciesId, mood)) { showLoaded(mood); return; }
|
||||
// Not fetched yet. Whatever is already on screen keeps playing (that's
|
||||
// the whole point of loading one mood at a time instead of all three up
|
||||
// front) — except the very first clip this species ever shows, which
|
||||
// has nothing to keep on screen, so it gets a placeholder instead of a
|
||||
// blank frame.
|
||||
if (!current) {
|
||||
if (!placeholder) {
|
||||
placeholder = buildLetterboxedFallback(scene, rules, art, speciesId, x, y, w, h);
|
||||
parent.add(placeholder);
|
||||
}
|
||||
placeholder.setVisible(true);
|
||||
}
|
||||
ensureOneAudienceVideo(scene, speciesId, mood, () => {
|
||||
// Superseded by a later show() call, or the screen is gone — either
|
||||
// way this fetch's result is no longer wanted.
|
||||
if (destroyed || wanted !== mood) return;
|
||||
if (hasAudienceVideo(scene, speciesId, mood)) showLoaded(mood);
|
||||
});
|
||||
},
|
||||
get current() { return current; },
|
||||
replay() {
|
||||
|
|
@ -230,6 +255,10 @@ function createMoodVisual(scene, rules, art, speciesId, x, y, w, h, parent, onEn
|
|||
// identical replay mechanism).
|
||||
entry.obj.play(false);
|
||||
},
|
||||
// Called from openAudienceScreen's close(), before root.destroy() tears
|
||||
// down whatever WAS built — stops a fetch that lands after the screen is
|
||||
// gone from reaching into a destroyed container.
|
||||
destroy() { destroyed = true; },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -279,8 +308,6 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
const vars = { you: state.empires[me].name, me: other.name };
|
||||
const speciesColor = Phaser.Display.Color.HexStringToColor(spec.color).color;
|
||||
|
||||
ensureAudienceVideos(scene, speciesId);
|
||||
|
||||
// Declared up front — pushChat()/say() both guard on it, and pushChat() is
|
||||
// called synchronously below (the opening dialogue) well before close()'s
|
||||
// own declaration used to sit.
|
||||
|
|
@ -622,6 +649,9 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
stopTyping();
|
||||
indicator?.destroy();
|
||||
chatMaskG.destroy();
|
||||
// Stops a still-in-flight fetch from swapping a clip into a container
|
||||
// that's about to be destroyed out from under it.
|
||||
moodVisual.destroy();
|
||||
// Destroys the mood video(s) with it, which is what stops their
|
||||
// decoders — same contract as VegaColonyIntro's root.destroy().
|
||||
root.destroy();
|
||||
|
|
|
|||
Loading…
Reference in New Issue