Add animated background video and still backdrop to Spelling Bee #11
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'),
|
image('bg-jewelquest-menu', 'assets/images/background-jewelquest-menu.png'),
|
||||||
],
|
],
|
||||||
bejeweled: [image('bg-bejeweled', 'assets/images/background-bejeweledblitz.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: [
|
rushhour: [
|
||||||
// Carries its own title, so the level-select screen draws no heading.
|
// Carries its own title, so the level-select screen draws no heading.
|
||||||
// In-play art is procedural (see games/rushhour/RushHourArt.js).
|
// 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 TITLE_GOLD = '#f2c14e';
|
||||||
const PAPER = 0x1e1a12; // panels
|
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 };
|
const DEPTH = { bg: 0, panel: 2, comb: 8, combTxt: 10, word: 12, ui: 20, overlay: 40, overlayUI: 42 };
|
||||||
|
|
||||||
// ── Honeycomb geometry ─────────────────────────────────────────────────────────
|
// ── Honeycomb geometry ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -48,6 +70,9 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
||||||
this.curLetters = []; // per-char text objects for the current word
|
this.curLetters = []; // per-char text objects for the current word
|
||||||
this.foundTexts = [];
|
this.foundTexts = [];
|
||||||
this.hexes = {}; // letter -> { container, gfx, isCenter }
|
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() {
|
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)
|
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT)
|
||||||
.setDepth(DEPTH.bg);
|
.setDepth(DEPTH.bg);
|
||||||
|
|
||||||
|
this.buildVideoBackground();
|
||||||
|
|
||||||
this.add.text(40, 36, 'SPELLING BEE', {
|
this.add.text(40, 36, 'SPELLING BEE', {
|
||||||
fontFamily: 'Righteous', fontSize: '46px', color: TITLE_GOLD,
|
fontFamily: 'Righteous', fontSize: '46px', color: TITLE_GOLD,
|
||||||
}).setDepth(DEPTH.ui);
|
}).setDepth(DEPTH.ui);
|
||||||
|
|
@ -70,6 +97,89 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
||||||
this.input.keyboard.off('keydown', this.keyHandler);
|
this.input.keyboard.off('keydown', this.keyHandler);
|
||||||
this.keyHandler = null;
|
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) ─────────────
|
// ── 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);
|
b.setDepth(DEPTH.ui);
|
||||||
this.startObjs.push(b);
|
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) {
|
async startPuzzle(difficulty) {
|
||||||
|
|
@ -130,6 +265,12 @@ export default class SpellingBeeGame extends Phaser.Scene {
|
||||||
this.maxScore = data.maxScore ?? 0;
|
this.maxScore = data.maxScore ?? 0;
|
||||||
this.tiers = buildTiers(this.maxScore, difficulty);
|
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();
|
this.buildBoard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue