Virtue-Slots/scenes/GameScene.js

329 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { GameState } from '../state/GameState.js';
import { SlotMachine } from '../objects/SlotMachine.js';
import { WinAnimation } from '../objects/WinAnimation.js';
import { LossAnimation } from '../objects/LossAnimation.js';
import { VialDisplay } from '../objects/VialDisplay.js';
export default class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
}
create() {
// Background image — stretched to fill the canvas
this.add.image(800, 450, 'bg-gates').setDisplaySize(1600, 900);
// ── 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;
this._currentMusicKey = null;
this._musicSound = null;
const vidX = this._vidX;
const vidY = this._vidY;
const vidW = this._vidW;
const vidH = this._vidH;
const vidFrame = this.add.graphics();
vidFrame.fillStyle(0x0c0620, 0.65);
vidFrame.fillRoundedRect(vidX - vidW / 2 - 8, vidY - vidH / 2 - 8, vidW + 16, vidH + 16, 14);
vidFrame.lineStyle(1, 0xffd700, 0.3);
vidFrame.strokeRoundedRect(vidX - vidW / 2 - 8, vidY - vidH / 2 - 8, vidW + 16, vidH + 16, 14);
// Start with the stage-02 loop (game begins at stage 2)
this._setVideo('stage-02', true);
this._switchMusic(this._getMusicKey(2));
// Gradient backdrop behind title — opaque on left, fades to transparent
const titleBg = this.add.graphics();
titleBg.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.62, 0, 0.62, 0);
titleBg.fillRect(0, 110, 700, 120);
// Title above the machine — left aligned with padding
this.add.text(40, 150, 'VIRTUE SLOTS', {
fontSize: '42px',
fontFamily: 'Georgia, serif',
color: '#ffd700',
stroke: '#5a3000',
strokeThickness: 4,
shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 6, fill: true }
}).setOrigin(0, 0.5);
this.add.text(40, 195, '✝ May Fortune Favor the Faithful ✝', {
fontSize: '18px',
fontFamily: 'Georgia, serif',
color: '#c8a87e',
alpha: 0.8
}).setOrigin(0, 0.5);
// Slot machine — vertically centered to match The Reckoning panel (y=202690)
this.slotMachine = new SlotMachine(this, 710, 446);
this.winAnim = new WinAnimation();
this.lossAnim = new LossAnimation();
// ── Right section: The Reckoning vials ──────────────────────────────────
// Panel vertically centered in play area (y=100790), with ~87px right margin
const sectionBg = this.add.graphics();
sectionBg.fillStyle(0x0c0620, 0.65);
sectionBg.fillRoundedRect(1085, 202, 428, 488, 14);
sectionBg.lineStyle(1, 0xffd700, 0.3);
sectionBg.strokeRoundedRect(1085, 202, 428, 488, 14);
this.add.text(1299, 220, 'THE RECKONING', {
fontSize: '13px', fontFamily: 'Georgia, serif',
color: '#c8a87e', letterSpacing: 5,
}).setOrigin(0.5, 0.5);
this.add.text(1299, 238, 'First to $2,000 wins', {
fontSize: '10px', fontFamily: 'Georgia, serif', color: '#4a5a6a',
}).setOrigin(0.5, 0.5);
this.add.text(1295, 443, 'VS', {
fontSize: '20px', fontFamily: 'Georgia, serif', fontStyle: 'bold',
color: '#2a1a4a', stroke: '#000000', strokeThickness: 3,
}).setOrigin(0.5, 0.5);
this.lordVial = new VialDisplay(this, 1193, 258, 'The Lord', 0xffd700, 0xc8a87e);
this.sinVial = new VialDisplay(this, 1398, 258, 'Sin', 0xff4444, 0xff6666);
// Keyboard: Space to spin
this.input.keyboard.on('keydown-SPACE', () => this._triggerSpin());
// Listen for spin button events from UIScene via global event bus
this.game.events.on('spin', () => this._triggerSpin(), this);
// Restore the slot machine once animations finish
this.game.events.on('spin-complete', () => {
if (!this._gameOver) this.slotMachine.setEnabled(true);
}, this);
// Insufficient funds — Sin wins by default; play its victory video and music
this.game.events.on('insufficient-funds', () => {
if (this._gameOver) return;
this._gameOver = true;
this._setVideo('sin-victory', false);
if (this._musicSound) {
const outgoing = this._musicSound;
this._musicSound = null;
this._currentMusicKey = null;
this.tweens.add({
targets: outgoing, volume: 0, duration: 1000,
onComplete: () => { outgoing.stop(); outgoing.destroy(); },
});
}
this.sound.play('music-sin-victory');
}, this);
// Victory: play the victory video, music, and shatter the losing vial
this.game.events.on('vial-winner', ({ winner }) => {
this._gameOver = true;
const lordWins = winner.toLowerCase().includes('lord');
const key = lordWins ? 'lord-victory' : 'sin-victory';
this._setVideo(key, false);
// Fade out background music and play the victory track once
if (this._musicSound) {
const outgoing = this._musicSound;
this._musicSound = null;
this._currentMusicKey = null;
this.tweens.add({
targets: outgoing, volume: 0, duration: 1000,
onComplete: () => { outgoing.stop(); outgoing.destroy(); },
});
}
this.sound.play(lordWins ? 'music-lord-victory' : 'music-sin-victory');
// Shatter the losing vial after a brief dramatic pause
this.time.delayedCall(550, () => {
if (lordWins) {
this.sinVial.shatterEvil();
} else {
this.lordVial.shatterHoly();
}
});
}, this);
}
_triggerSpin() {
if (GameState.spinning) return;
if (GameState.playerFunds < GameState.spinCost) {
this.game.events.emit('insufficient-funds');
return;
}
GameState.playerFunds -= GameState.spinCost;
GameState.spinning = true;
this.game.events.emit('funds-updated');
this.game.events.emit('spinning-started');
this.slotMachine.spin((result) => this._handleResult(result));
}
_handleResult({ win, symbols, payout }) {
// Dim the slot machine half a second after the reels stop
this.time.delayedCall(500, () => this.slotMachine.setEnabled(false));
if (win) {
const playerGain = Math.round(payout * 0.6);
const lordGain = payout - playerGain;
this.game.events.emit('win', { playerGain, lordGain, symbol: symbols[0] });
// Resolve UI box positions from UIScene
const uiScene = this.scene.get('UIScene');
const playerBox = uiScene ? uiScene.getPlayerBoxCenter() : { x: 267, y: 60 };
const lordBox = uiScene ? uiScene.getLordBoxCenter() : { x: 800, y: 60 };
this.winAnim.play(
this,
this.slotMachine.getCenterX(),
this.slotMachine.getCenterY(),
playerBox,
lordBox,
symbols[0],
() => {
GameState.playerFunds += playerGain;
GameState.lordFunds += lordGain;
this.game.events.emit('funds-updated');
this.lordVial.animateUpdate(GameState.lordFunds, lordBox.x, 115, () => {
this._handleStageChange(this._computeStage());
GameState.spinning = false;
this.game.events.emit('spin-complete');
});
}
);
} else {
this.game.events.emit('loss', { sinAdded: GameState.spinCost });
const uiScene = this.scene.get('UIScene');
const sinBox = uiScene ? uiScene.getSinBoxCenter() : { x: 1333, y: 60 };
this.lossAnim.play(
uiScene || this,
this.slotMachine.getCenterX(),
this.slotMachine.getCenterY(),
sinBox,
() => {
GameState.sinTotal += GameState.spinCost;
this.game.events.emit('funds-updated');
this.sinVial.animateUpdate(GameState.sinTotal, sinBox.x, 115, () => {
this._handleStageChange(this._computeStage());
GameState.spinning = false;
this.game.events.emit('spin-complete');
});
}
);
}
}
// ── Stage video helpers ───────────────────────────────────────────────────
/** 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;
}
/**
* 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;
this._switchMusic(this._getMusicKey(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);
});
}
/** Map a stage number to its music track key. */
_getMusicKey(stage) {
if (stage <= 2) return 'music-01-02';
if (stage <= 4) return 'music-03-04';
return 'music-05-06';
}
/** Crossfade from the current music track to newKey (no-op if already playing). */
_switchMusic(newKey) {
if (newKey === this._currentMusicKey) return;
this._currentMusicKey = newKey;
// Fade out and destroy the outgoing track
if (this._musicSound) {
const outgoing = this._musicSound;
this.tweens.add({
targets: outgoing,
volume: 0,
duration: 1000,
onComplete: () => { outgoing.stop(); outgoing.destroy(); },
});
}
// Fade in the incoming track from silence
this._musicSound = this.sound.add(newKey, { loop: true, volume: 0 });
this._musicSound.play();
this.tweens.add({ targets: this._musicSound, volume: 1, duration: 1000 });
}
}