Add animated background video and still backdrop to Spelling Bee
- Loop a muted bee-on-flower clip behind the board during the intro, fading it in over the felt base with a dim layer for readability. - Reveal the difficulty panel on the first full loop of the clip, with a ~20s fallback timer and an immediate reveal on video load/playback error. - Swap the intro clip for a preloaded honeycomb PNG once a puzzle starts; fall back to the felt rectangle if the image is unavailable. - Register the still background in the asset manifest so GameRoomScene preloads it before the scene launches.
This commit is contained in:
parent
52eaff8b96
commit
5a5f30bca7
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
|
@ -256,6 +256,9 @@ export const MANIFEST = {
|
|||
image('bg-jewelquest-menu', 'assets/images/background-jewelquest-menu.png'),
|
||||
],
|
||||
bejeweled: [image('bg-bejeweled', 'assets/images/background-bejeweledblitz.png')],
|
||||
// Still gameplay background for Spelling Bee — the intro clip is swapped for
|
||||
// this once a puzzle starts (SpellingBeeGame.showImageBackground).
|
||||
spellingbee: [image('spellingbee-bg', 'assets/images/background-spellingbee.png')],
|
||||
rushhour: [
|
||||
// Carries its own title, so the level-select screen draws no heading.
|
||||
// In-play art is procedural (see games/rushhour/RushHourArt.js).
|
||||
|
|
|
|||
|
|
@ -17,6 +17,28 @@ const INK_DARK = '#1a1208'; // dark letter on the honey center hex
|
|||
const TITLE_GOLD = '#f2c14e';
|
||||
const PAPER = 0x1e1a12; // panels
|
||||
|
||||
// ── Background video ─────────────────────────────────────────────────────────
|
||||
// Animated bee-on-a-flower clip used as a live backdrop behind the board.
|
||||
// Loaded on demand (not in the asset manifest) and muted — the game has its
|
||||
// own soundtrack and SFX, so the clip is purely visual. Source resolution is
|
||||
// used to scale until the real texture decodes (a Video's own width/height
|
||||
// report a square placeholder before its first frame).
|
||||
const BG_VIDEO_KEY = 'spellingbee-bg-video';
|
||||
const BG_VIDEO_PATH = 'assets/videos/games/spellingBee.mp4';
|
||||
const BG_VIDEO_SRC_W = 864;
|
||||
const BG_VIDEO_SRC_H = 480;
|
||||
// The clip is bright, so dim it a touch to keep the title and board readable
|
||||
// without killing the color.
|
||||
const BG_DIM_ALPHA = 0.38;
|
||||
// Fallback for revealing the start panel: if the intro clip never finishes its
|
||||
// first loop (stalled download, unsupported codec, ...), bring the panel up
|
||||
// anyway. Just past the clip's ~15 s runtime.
|
||||
const INTRO_REVEAL_FALLBACK_MS = 20000;
|
||||
// Still gameplay backdrop, preloaded by GameRoomScene via data/assetManifest.js.
|
||||
// Replaces the intro clip the moment a puzzle actually starts.
|
||||
const BG_IMAGE_KEY = 'spellingbee-bg';
|
||||
const BG_IMAGE_PATH = 'assets/images/background-spellingbee.png';
|
||||
|
||||
const DEPTH = { bg: 0, panel: 2, comb: 8, combTxt: 10, word: 12, ui: 20, overlay: 40, overlayUI: 42 };
|
||||
|
||||
// ── Honeycomb geometry ─────────────────────────────────────────────────────────
|
||||
|
|
@ -48,6 +70,9 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
|||
this.curLetters = []; // per-char text objects for the current word
|
||||
this.foundTexts = [];
|
||||
this.hexes = {}; // letter -> { container, gfx, isCenter }
|
||||
this.bgVideo = null;
|
||||
this.bgImage = null; // still gameplay background (replaces the video)
|
||||
this.panelRevealed = false; // difficulty panel shown (intro finished)
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -57,6 +82,8 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
|||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT)
|
||||
.setDepth(DEPTH.bg);
|
||||
|
||||
this.buildVideoBackground();
|
||||
|
||||
this.add.text(40, 36, 'SPELLING BEE', {
|
||||
fontFamily: 'Righteous', fontSize: '46px', color: TITLE_GOLD,
|
||||
}).setDepth(DEPTH.ui);
|
||||
|
|
@ -70,6 +97,89 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
|||
this.input.keyboard.off('keydown', this.keyHandler);
|
||||
this.keyHandler = null;
|
||||
}
|
||||
// The video may already be gone (e.g. the error handler destroyed it).
|
||||
if (this.bgVideo?.scene) this.bgVideo.destroy();
|
||||
this.bgVideo = null;
|
||||
if (this.bgImage?.scene) this.bgImage.destroy();
|
||||
this.bgImage = null;
|
||||
}
|
||||
|
||||
// ── Background video ───────────────────────────────────────────────────────
|
||||
|
||||
// Bring up the looping background clip on top of the felt base. In this
|
||||
// Phaser version a VideoFile "load" is a synchronous no-op — it only records
|
||||
// the URL in the video cache (the real fetch happens when the Video object
|
||||
// is created, in showBackgroundVideo) — so there is no async window to wait
|
||||
// out: queue it if needed and show it immediately. A fetch/playback failure
|
||||
// is handled by the video's 'error' event, leaving the felt base visible.
|
||||
buildVideoBackground() {
|
||||
if (!this.cache.video?.exists(BG_VIDEO_KEY)) {
|
||||
this.load.video(BG_VIDEO_KEY, BG_VIDEO_PATH, true); // noAudio — visual only
|
||||
if (!this.load.isLoading()) this.load.start();
|
||||
}
|
||||
this.showBackgroundVideo();
|
||||
}
|
||||
|
||||
showBackgroundVideo() {
|
||||
const v = this.add.video(GAME_WIDTH / 2, GAME_HEIGHT / 2, BG_VIDEO_KEY)
|
||||
.setDepth(DEPTH.bg);
|
||||
v.setLoop(true);
|
||||
v.setMute(true);
|
||||
|
||||
// Cover, not contain: fill edge-to-edge. The clip's aspect (864x480) is
|
||||
// within a hair of the game's (1920x1080), so the crop is imperceptible.
|
||||
const fit = () => {
|
||||
const w = v.videoTexture ? v.width : BG_VIDEO_SRC_W;
|
||||
const h = v.videoTexture ? v.height : BG_VIDEO_SRC_H;
|
||||
v.setScale(Math.max(GAME_WIDTH / w, GAME_HEIGHT / h));
|
||||
};
|
||||
fit();
|
||||
v.on('created', fit);
|
||||
v.on('playing', fit);
|
||||
|
||||
// A load failure mid-playback (or a browser refusing the element) falls
|
||||
// back to the felt rectangle already underneath — and reveals the start
|
||||
// panel, which otherwise waits for the intro loop.
|
||||
v.once('error', () => {
|
||||
this.revealStartPanel();
|
||||
if (v.scene) v.destroy();
|
||||
});
|
||||
|
||||
this.bgVideo = v;
|
||||
v.setAlpha(0);
|
||||
v.play(true);
|
||||
this.tweens.add({ targets: v, alpha: 1, duration: 700, ease: 'Sine.easeOut' });
|
||||
|
||||
// Dim layer on top of the footage so the title and board stay readable.
|
||||
// Fixed alpha: it only darkens the felt base while the video fades in, so
|
||||
// the footage never flashes bright before settling. (It also dims the
|
||||
// still gameplay backdrop once the video is swapped out — see
|
||||
// showImageBackground.)
|
||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT,
|
||||
0x000000, BG_DIM_ALPHA).setDepth(DEPTH.bg + 1);
|
||||
}
|
||||
|
||||
// Swap the intro clip for the still gameplay backdrop (honeycomb PNG).
|
||||
// Preloaded via the asset manifest, so the texture is almost always ready;
|
||||
// the else branch covers a scene started without that preload. The felt
|
||||
// rectangle stays underneath as the ultimate fallback.
|
||||
showImageBackground() {
|
||||
if (this.bgImage) return;
|
||||
if (!this.textures.exists(BG_IMAGE_KEY)) {
|
||||
this.load.image(BG_IMAGE_KEY, BG_IMAGE_PATH);
|
||||
if (!this.load.isLoading()) this.load.start();
|
||||
}
|
||||
if (this.textures.exists(BG_IMAGE_KEY)) {
|
||||
this.bgImage = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, BG_IMAGE_KEY)
|
||||
.setOrigin(0.5).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(DEPTH.bg);
|
||||
} else {
|
||||
this.load.once('filecomplete', (type, key) => {
|
||||
if (type !== 'texture' || key !== BG_IMAGE_KEY) return;
|
||||
if (!this.sys.isActive() || this.bgImage) return;
|
||||
this.bgImage = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, BG_IMAGE_KEY)
|
||||
.setOrigin(0.5).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(DEPTH.bg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start panel (opponent-select is skipped for this solo game) ─────────────
|
||||
|
|
@ -107,6 +217,31 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
|||
b.setDepth(DEPTH.ui);
|
||||
this.startObjs.push(b);
|
||||
});
|
||||
|
||||
// Hidden until the intro clip has played a full loop (see revealStartPanel).
|
||||
this.startObjs.forEach((o) => o.setAlpha(0));
|
||||
|
||||
// The first 'loop' event of the background video means the clip has just
|
||||
// played through once — the natural moment to surface the difficulty
|
||||
// choice. The safety timer covers a clip that never gets that far; the
|
||||
// video's 'error' handler (showBackgroundVideo) does the same immediately.
|
||||
const reveal = () => this.revealStartPanel();
|
||||
this.bgVideo?.once(Phaser.GameObjects.Events.VIDEO_LOOP, reveal);
|
||||
this.time.delayedCall(INTRO_REVEAL_FALLBACK_MS, reveal);
|
||||
}
|
||||
|
||||
// Fade the difficulty panel in. Whichever trigger fires first (intro loop,
|
||||
// video error, fallback timer) wins; the rest are no-ops.
|
||||
revealStartPanel() {
|
||||
if (this.panelRevealed) return;
|
||||
// Panel not built yet — bail without consuming the reveal; a later trigger
|
||||
// (loop timer / fallback) will fire once it exists.
|
||||
if (!this.startObjs.length) return;
|
||||
this.panelRevealed = true;
|
||||
this.startObjs.forEach((o) => {
|
||||
if (!o.scene) return;
|
||||
this.tweens.add({ targets: o, alpha: 1, duration: 700, ease: 'Sine.easeOut' });
|
||||
});
|
||||
}
|
||||
|
||||
async startPuzzle(difficulty) {
|
||||
|
|
@ -130,6 +265,12 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
|||
this.maxScore = data.maxScore ?? 0;
|
||||
this.tiers = buildTiers(this.maxScore, difficulty);
|
||||
|
||||
// Difficulty screen is over: retire the intro clip and bring up the still
|
||||
// honeycomb backdrop. (If the fetch above failed we never get here, and
|
||||
// the player stays on the difficulty screen with the clip still playing.)
|
||||
if (this.bgVideo?.scene) { this.bgVideo.destroy(); this.bgVideo = null; }
|
||||
this.showImageBackground();
|
||||
|
||||
this.buildBoard();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue