Virtue-Slots/scenes/GameScene.js

257 lines
8.9 KiB
JavaScript
Raw 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;
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);
// 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);
// Victory: play the victory video 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);
// 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.slotMachine.spin((result) => this._handleResult(result));
}
_handleResult({ win, symbols, payout }) {
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;
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);
});
}
}