265 lines
9.2 KiB
JavaScript
265 lines
9.2 KiB
JavaScript
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=202–690)
|
||
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=100–790), 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);
|
||
|
||
// 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.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;
|
||
|
||
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);
|
||
});
|
||
}
|
||
}
|