Implement dynamic stage video system with transition sequences and victory animations

This commit introduces a comprehensive stage video system that dynamically updates based on game progress:
- Added loading of all stage videos (1-6) and transition videos (adjacent pairs) in BootScene
- Implemented victory video loading for both lord and sin winners
- Replaced character state management with a proper stage-based video system
- Added _computeStage() method to determine current stage based on fund differences
- Implemented _setVideo() and _playSequence() helper methods for video management
- Created _handleStageChange() method that plays transition videos when stage changes
- Added game over state handling to prevent further video transitions
- Integrated stage changes with spin results to update video display automatically
- Maintained backward compatibility while adding rich visual feedback for game progression
This commit is contained in:
Brian Fertig 2026-02-28 12:06:14 -07:00
parent f8515f207a
commit d30c018100
2 changed files with 105 additions and 24 deletions

View File

@ -6,9 +6,26 @@ export default class BootScene extends Phaser.Scene {
preload() {
this.load.spritesheet('symbols', 'assets/symbol_sprites.png', { frameWidth: 200, frameHeight: 100 });
this.load.image('bg-gates', 'assets/gates.png');
this.load.video('god-idle-01', 'assets/video/god-idle-01.mp4', true);
this.load.video('god-sin-enters-01', 'assets/video/god-sin-enters-01.mp4', true);
this.load.video('sin-idle-02', 'assets/video/sin-idle-02.mp4', true);
// Stage loop videos (stages 16)
for (let i = 1; i <= 6; i++) {
const pad = String(i).padStart(2, '0');
this.load.video(`stage-${pad}`, `assets/video/stage-${pad}.mp4`, true);
}
// Stage transition videos (adjacent pairs in both directions)
const pairs = [
[1,2],[2,1],[2,3],[3,2],[3,4],[4,3],[4,5],[5,4],[5,6],[6,5]
];
pairs.forEach(([a, b]) => {
const pa = String(a).padStart(2, '0');
const pb = String(b).padStart(2, '0');
this.load.video(`stage-${pa}-${pb}`, `assets/video/stage-${pa}-${pb}.mp4`, true);
});
// Victory videos
this.load.video('lord-victory', 'assets/video/lord-victory.mp4', true);
this.load.video('sin-victory', 'assets/video/sin-victory.mp4', true);
}
create() {

View File

@ -13,16 +13,20 @@ export default class GameScene extends Phaser.Scene {
// Background image — stretched to fill the canvas
this.add.image(800, 450, 'bg-gates').setDisplaySize(1600, 900);
// ── Left section: Character video panel ──────────────────────────────────
// ── Left section: Stage video panel ──────────────────────────────────────
// Centered at x=160 (within the 360px left of the slot machine), y=490
// (below the title gradient which ends at y=230). Size: 300x480.
this._vidX = 160;
this._vidY = 490;
this._vidW = 300;
this._vidH = 480;
this._currentStage = 2;
this._activeVideo = null;
this._gameOver = false;
const vidX = this._vidX;
const vidY = this._vidY;
const vidW = 300;
const vidH = 480;
this._characterState = 'god-idle';
const vidW = this._vidW;
const vidH = this._vidH;
const vidFrame = this.add.graphics();
vidFrame.fillStyle(0x0c0620, 0.65);
@ -30,8 +34,8 @@ export default class GameScene extends Phaser.Scene {
vidFrame.lineStyle(1, 0xffd700, 0.3);
vidFrame.strokeRoundedRect(vidX - vidW / 2 - 8, vidY - vidH / 2 - 8, vidW + 16, vidH + 16, 14);
this.characterVideo = this.add.video(vidX, vidY, 'god-idle-01');
this.characterVideo.play(true);
// Start with the stage-02 loop (game begins at stage 2)
this._setVideo('stage-02', true);
// Gradient backdrop behind title — opaque on left, fades to transparent
const titleBg = this.add.graphics();
@ -91,6 +95,13 @@ export default class GameScene extends Phaser.Scene {
// Listen for spin button events from UIScene via global event bus
this.game.events.on('spin', () => this._triggerSpin(), this);
// Victory: play the appropriate one-shot video then stop
this.game.events.on('vial-winner', ({ winner }) => {
this._gameOver = true;
const key = winner.toLowerCase().includes('lord') ? 'lord-victory' : 'sin-victory';
this._setVideo(key, false);
}, this);
}
_triggerSpin() {
@ -132,7 +143,7 @@ export default class GameScene extends Phaser.Scene {
GameState.lordFunds += lordGain;
this.game.events.emit('funds-updated');
this.lordVial.animateUpdate(GameState.lordFunds, lordBox.x, 115, () => {
this._checkCharacterState();
this._handleStageChange(this._computeStage());
GameState.spinning = false;
this.game.events.emit('spin-complete');
});
@ -153,7 +164,7 @@ export default class GameScene extends Phaser.Scene {
GameState.sinTotal += GameState.spinCost;
this.game.events.emit('funds-updated');
this.sinVial.animateUpdate(GameState.sinTotal, sinBox.x, 115, () => {
this._checkCharacterState();
this._handleStageChange(this._computeStage());
GameState.spinning = false;
this.game.events.emit('spin-complete');
});
@ -162,21 +173,74 @@ export default class GameScene extends Phaser.Scene {
}
}
_checkCharacterState() {
if (this._characterState !== 'god-idle') return;
if (GameState.sinTotal < GameState.lordFunds + 200) return;
// ── Stage video helpers ───────────────────────────────────────────────────
this._characterState = 'sin-entering';
this.characterVideo.stop();
this.characterVideo.destroy();
/** Compute which stage the Reckoning is currently in based on lord vs sin. */
_computeStage() {
const diff = GameState.lordFunds - GameState.sinTotal;
if (diff >= 200) return 1;
if (diff > -200) return 2;
if (diff > -350) return 3;
if (diff > -500) return 4;
if (diff > -650) return 5;
return 6;
}
this.characterVideo = this.add.video(this._vidX, this._vidY, 'god-sin-enters-01');
this.characterVideo.play(false);
this.characterVideo.once('complete', () => {
this._characterState = 'sin-idle';
this.characterVideo.destroy();
this.characterVideo = this.add.video(this._vidX, this._vidY, 'sin-idle-02');
this.characterVideo.play(true);
/**
* Switch the panel video.
* loop=true loops indefinitely (no onComplete needed).
* loop=false plays once; calls onComplete when finished (if provided).
*/
_setVideo(key, loop, onComplete) {
if (this._activeVideo) {
this._activeVideo.stop();
this._activeVideo.destroy();
this._activeVideo = null;
}
const vid = this.add.video(this._vidX, this._vidY, key);
//vid.setDisplaySize(this._vidW, this._vidH);
this._activeVideo = vid;
vid.play(loop);
if (!loop && onComplete) {
vid.once('complete', () => {
if (this._activeVideo === vid) onComplete();
});
}
}
/**
* Play a sequence of one-shot videos in order, then call onComplete.
* keys = array of video keys to play in sequence.
*/
_playSequence(keys, onComplete) {
if (keys.length === 0) {
if (onComplete) onComplete();
return;
}
const [first, ...rest] = keys;
this._setVideo(first, false, () => this._playSequence(rest, onComplete));
}
/**
* Called after each spin resolves. Calculates needed transitions and
* plays them in sequence before resuming the looping stage video.
*/
_handleStageChange(newStage) {
if (this._gameOver) return;
if (newStage === this._currentStage) return;
const oldStage = this._currentStage;
this._currentStage = newStage;
const pad = n => String(n).padStart(2, '0');
const direction = newStage > oldStage ? 1 : -1;
const transitions = [];
for (let s = oldStage; s !== newStage; s += direction) {
transitions.push(`stage-${pad(s)}-${pad(s + direction)}`);
}
this._playSequence(transitions, () => {
if (!this._gameOver) this._setVideo(`stage-${pad(newStage)}`, true);
});
}
}