tyrants-edge/src/scenes/StoreScene.js

544 lines
18 KiB
JavaScript

import { SaveManager } from '../managers/SaveManager.js';
import { CardObject } from '../objects/CardObject.js';
const FACTION_COLORS = {
imperial: 0x2244aa,
raider: 0xaa2222,
bloodthirsty: 0x882244,
xeno: 0x22aa44,
righteous: 0xaaaa22
};
const RARITY_COLORS = {
common: 0x888888,
rare: 0x4488ff,
epic: 0xaa44ff,
legendary: 0xffaa00
};
const CARD_BACK_KEYS = {
imperial: 'card_back_imperial',
raider: 'card_back_raider',
bloodthirsty: 'card_back_bloodthirsty',
xeno: 'card_back_xeno',
righteous: 'card_back_right'
};
const FACTION_LABELS = {
imperial: 'IMPERIAL',
raider: 'RAIDER',
bloodthirsty: 'BLOODTHIRSTY',
xeno: 'XENO',
righteous: 'RIGHTEOUS'
};
export class StoreScene extends Phaser.Scene {
constructor() { super('StoreScene'); }
preload() {
this.load.video('corridors', 'assets/video/corridors.mp4');
}
create() {
const { width, height } = this.scale;
this.save = this.registry.get('save');
this.packManager = this.registry.get('packManager');
this.cardManager = this.registry.get('cardManager');
this.campaigns = this.registry.get('campaigns');
this.state = 'SHOPPING';
this._revealObjects = [];
this._emitters = [];
this._packElements = [];
// Video background
const bg = this.add.video(width / 2, height / 2, 'corridors');
bg.setMute(true);
bg.on('created', () => {
if (!bg.scene) return;
bg.setDisplaySize(width, height);
const t = this.registry.get('corridors_time');
if (t > 0) bg.setCurrentTime(t);
});
bg.play(true);
this.events.on('shutdown', () => {
if (bg.scene) this.registry.set('corridors_time', bg.getCurrentTime());
});
// Dark tint overlay
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.55);
// Scanlines
const scanlines = this.add.graphics();
for (let y = 0; y < height; y += 3) {
scanlines.fillStyle(0x000000, 0.22);
scanlines.fillRect(0, y, width, 1);
}
// Header
this.add.text(width / 2, 45, 'Card Store', {
fontSize: '42px', color: '#d4af37', fontFamily: 'RaiderCrusader'
}).setOrigin(0.5);
this.add.text(width / 2, 90, 'Buy packs to expand your collection', {
fontSize: '20px', color: '#888888', fontFamily: 'Audiowide'
}).setOrigin(0.5);
this.goldText = this.add.text(width - 20, 20, `Gold: ${this.save.gold}`, {
fontSize: '24px', color: '#ffd700', fontFamily: 'Audiowide'
}).setOrigin(1, 0);
// Generate particle texture
this._generateParticleTexture();
// Render packs
this._renderAllPacks();
// Back button
this._makeBackButton();
}
// ── Pack Grid ─────────────────────────────────────────────────────────────
_renderAllPacks() {
const packs = this.packManager.getAllPacks();
const { width } = this.scale;
const packW = 320;
const gap = 30;
const totalW = packs.length * packW + (packs.length - 1) * gap;
const startX = width / 2 - totalW / 2 + packW / 2;
packs.forEach((pack, i) => {
const x = startX + i * (packW + gap);
this._renderPack(pack, x, 400);
});
}
_renderPack(pack, x, y) {
const unlocked = this.packManager.isPackUnlocked(pack, this.save, this.campaigns);
const factionColor = FACTION_COLORS[pack.faction] || 0x333333;
const elements = [];
// Pack background
const packBg = this.add.rectangle(x, y, 300, 440, 0x0a0f1a)
.setStrokeStyle(3, unlocked ? factionColor : 0x333333);
elements.push(packBg);
// Placeholder pack art
const artBg = this.add.rectangle(x, y - 100, 220, 180, factionColor, unlocked ? 0.8 : 0.2)
.setStrokeStyle(2, unlocked ? 0xffffff : 0x444444, unlocked ? 0.3 : 0.1);
elements.push(artBg);
// Faction icon text on art
const artLabel = this.add.text(x, y - 115, FACTION_LABELS[pack.faction] || '?', {
fontSize: '22px', color: unlocked ? '#ffffff' : '#444444',
fontStyle: 'bold', fontFamily: 'RaiderCrusader'
}).setOrigin(0.5);
elements.push(artLabel);
const artSub = this.add.text(x, y - 80, 'BOOSTER', {
fontSize: '14px', color: unlocked ? '#aaaaaa' : '#333333', fontFamily: 'Audiowide'
}).setOrigin(0.5);
elements.push(artSub);
// Decorative lines on art
const artGfx = this.add.graphics();
const lineColor = unlocked ? factionColor : 0x333333;
const lineAlpha = unlocked ? 0.5 : 0.15;
artGfx.lineStyle(1, lineColor, lineAlpha);
artGfx.strokeRect(x - 100, y - 180, 200, 160);
artGfx.lineStyle(1, lineColor, lineAlpha * 0.5);
artGfx.strokeRect(x - 90, y - 170, 180, 140);
elements.push(artGfx);
// Pack name
const nameText = this.add.text(x, y + 10, pack.name, {
fontSize: '18px', color: unlocked ? '#ffffff' : '#555555',
fontFamily: 'Audiowide', align: 'center',
wordWrap: { width: 280 }
}).setOrigin(0.5);
elements.push(nameText);
// Card count
const countText = this.add.text(x, y + 40, `${pack.slots.length} cards per pack`, {
fontSize: '14px', color: unlocked ? '#aaaaaa' : '#444444', fontFamily: 'Audiowide'
}).setOrigin(0.5);
elements.push(countText);
// Cost
const costText = this.add.text(x, y + 65, `Cost: ${pack.cost} Gold`, {
fontSize: '18px', color: unlocked ? '#ffd700' : '#554400', fontFamily: 'Audiowide'
}).setOrigin(0.5);
elements.push(costText);
// Special slot indicator
const specialCount = pack.slots.filter(s => s.special).length;
if (specialCount > 0) {
const specialText = this.add.text(x, y + 90,
`${specialCount} special chance card${specialCount > 1 ? 's' : ''}`, {
fontSize: '13px', color: unlocked ? '#ffd700' : '#443300', fontFamily: 'Audiowide'
}).setOrigin(0.5);
elements.push(specialText);
}
if (unlocked) {
// Buy button
const btnBg = this.add.rectangle(x, y + 170, 240, 55, 0x224422)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x44aa44);
const btnText = this.add.text(x, y + 170, 'Buy Pack', {
fontSize: '20px', color: '#44ff44', fontFamily: 'Audiowide'
}).setOrigin(0.5);
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
btnBg.on('pointerout', () => btnBg.setFillStyle(0x224422));
btnBg.on('pointerdown', () => {
if (this.state !== 'SHOPPING') return;
this._buyPack(pack);
});
elements.push(btnBg, btnText);
} else {
// Locked overlay
const campaign = this.campaigns.find(c => c.id === pack.unlockCondition);
const campaignName = campaign ? campaign.name : 'previous campaign';
const lockText = this.add.text(x, y + 145, '🔒 LOCKED', {
fontSize: '20px', color: '#aa4444', fontFamily: 'Audiowide'
}).setOrigin(0.5);
const unlockText = this.add.text(x, y + 175,
`Complete "${campaignName}"`, {
fontSize: '13px', color: '#666666', fontFamily: 'Audiowide',
align: 'center', wordWrap: { width: 260 }
}).setOrigin(0.5);
elements.push(lockText, unlockText);
// Dim everything
elements.forEach(el => {
if (el.setAlpha) el.setAlpha(el.alpha * 0.5);
});
}
this._packElements.push(...elements);
}
// ── Purchase + Reveal ─────────────────────────────────────────────────────
_buyPack(pack) {
if (!SaveManager.spendGold(this.save, pack.cost)) {
this._showMsg('Not enough gold!', '#ff4444');
return;
}
const cards = this.packManager.openPack(pack.id, this.save);
if (!cards || cards.length === 0) {
// Refund
this.save.gold += pack.cost;
SaveManager.save(this.save);
this._showMsg('No cards available in this pack!', '#ff8800');
return;
}
// Add cards to collection
cards.forEach(card => SaveManager.addCard(this.save, card.id));
this.registry.set('save', this.save);
this.goldText.setText(`Gold: ${this.save.gold}`);
// Sort: non-special left, special right
cards.sort((a, b) => (a._slotSpecial ? 1 : 0) - (b._slotSpecial ? 1 : 0));
this.state = 'REVEALING';
this._startReveal(cards, pack);
}
_startReveal(cards, pack) {
const { width, height } = this.scale;
const CARD_W = 260;
const CARD_H = 357;
const GAP = 50;
const totalW = cards.length * CARD_W + (cards.length - 1) * GAP;
const startX = width / 2 - totalW / 2 + CARD_W / 2;
const cardY = height / 2 - 40;
const factionColor = FACTION_COLORS[pack.faction] || 0x333333;
// Dark overlay
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0)
.setDepth(100).setInteractive();
this.tweens.add({ targets: overlay, alpha: 0.9, duration: 300 });
this._revealObjects.push(overlay);
// "Opening pack..." text
const openText = this.add.text(width / 2, cardY - CARD_H / 2 - 60,
`Opening ${pack.name}...`, {
fontSize: '32px', color: '#ffd700', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(103).setAlpha(0);
this.tweens.add({ targets: openText, alpha: 1, duration: 300 });
this._revealObjects.push(openText);
// Create card backs
const cardBacks = [];
const cardFronts = [];
cards.forEach((card, i) => {
const cx = startX + i * (CARD_W + GAP);
// Card back container
const backContainer = this.add.container(cx, cardY).setDepth(101);
// Card back image
const backKey = CARD_BACK_KEYS[pack.faction] || 'card_back_imperial';
const backImg = this.add.image(0, 0, backKey).setDisplaySize(CARD_W, CARD_H);
backContainer.add(backImg);
// Special golden border
if (card._slotSpecial) {
const goldBorder = this.add.rectangle(0, 0, CARD_W + 8, CARD_H + 8)
.setStrokeStyle(4, 0xffd700).setFillStyle(0x000000, 0);
backContainer.addAt(goldBorder, 0);
this.tweens.add({
targets: goldBorder,
strokeAlpha: { from: 0.5, to: 1 },
duration: 600, yoyo: true, repeat: -1, ease: 'Sine.InOut'
});
}
// Entrance animation
backContainer.setAlpha(0).setScale(0.8);
this.tweens.add({
targets: backContainer,
alpha: 1, scaleX: 1, scaleY: 1,
duration: 300, delay: i * 100, ease: 'Back.Out'
});
cardBacks.push(backContainer);
this._revealObjects.push(backContainer);
// Prepare card front (hidden initially)
const save = this.save;
const instance = this.cardManager.createInstance(card.id, save.level || 1);
const cardObj = new CardObject(this, cx, cardY, instance, {
width: CARD_W, height: CARD_H, disableTooltip: true
});
cardObj.setDepth(101).setAlpha(0);
cardObj.scaleX = 0;
cardFronts.push({ cardObj, card, x: cx, y: cardY });
this._revealObjects.push(cardObj);
});
// Begin sequential reveal after delay
const initialDelay = 600 + cards.length * 100;
this.time.delayedCall(initialDelay, () => {
this._revealCards(cardBacks, cardFronts, 0);
});
}
_revealCards(backs, fronts, index) {
if (index >= backs.length) {
// All revealed — show collect button after delay
this.time.delayedCall(400, () => this._showCollectButton());
return;
}
const back = backs[index];
const { cardObj, card, x, y } = fronts[index];
const CARD_W = 260;
const CARD_H = 357;
// Flip: back scaleX → 0
this.tweens.add({
targets: back,
scaleX: 0,
duration: 150,
ease: 'Quad.In',
onComplete: () => {
back.setVisible(false);
// Show front, flip in
cardObj.setAlpha(1);
this.tweens.add({
targets: cardObj,
scaleX: 1,
duration: 150,
ease: 'Quad.Out',
onComplete: () => {
// Pop effect
this.tweens.add({
targets: cardObj,
scaleX: 1.2, scaleY: 1.2,
duration: 150,
ease: 'Quad.Out',
onComplete: () => {
this.tweens.add({
targets: cardObj,
scaleX: 1, scaleY: 1,
duration: 200,
ease: 'Back.Out'
});
}
});
// Rarity glow
const glowColor = RARITY_COLORS[card.rarity] || 0x888888;
if (cardObj.postFX) {
cardObj.postFX.addGlow(glowColor, 8, 0, false, 0.2, 12);
}
// Particle emitter
const hw = CARD_W / 2;
const hh = CARD_H / 2;
const emitter = this.add.particles(x, y, 'store_particle', {
speed: { min: 30, max: 80 },
lifespan: 1400,
scale: { start: 0.8, end: 0 },
alpha: { start: 0.8, end: 0 },
blendMode: 'ADD',
frequency: 50,
tint: glowColor,
emitZone: {
type: 'edge',
source: new Phaser.Geom.Rectangle(-hw, -hh, CARD_W, CARD_H),
quantity: 48
}
});
emitter.setDepth(102);
this._emitters.push(emitter);
this._revealObjects.push(emitter);
// "NEW!" badge
const ownedCount = this.save.collection[card.id] || 0;
if (ownedCount === 1) {
const newBadge = this.add.text(x, y - hh - 25, 'NEW!', {
fontSize: '22px', color: '#44ff44', fontStyle: 'bold',
stroke: '#000000', strokeThickness: 4, fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(103).setAlpha(0);
this.tweens.add({ targets: newBadge, alpha: 1, duration: 200 });
this._revealObjects.push(newBadge);
}
// Rarity label below card
const rarityHex = '#' + (glowColor).toString(16).padStart(6, '0');
const rarityLabel = this.add.text(x, y + hh + 18, card.rarity.toUpperCase(), {
fontSize: '16px', color: rarityHex, fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(103).setAlpha(0);
this.tweens.add({ targets: rarityLabel, alpha: 1, duration: 200 });
this._revealObjects.push(rarityLabel);
// Card name below rarity
const cardName = this.add.text(x, y + hh + 42, card.name, {
fontSize: '16px', color: '#ffffff', fontFamily: 'Audiowide',
align: 'center', wordWrap: { width: CARD_W + 20 }
}).setOrigin(0.5).setDepth(103).setAlpha(0);
this.tweens.add({ targets: cardName, alpha: 1, duration: 200 });
this._revealObjects.push(cardName);
}
});
}
});
// Reveal next card after delay
this.time.delayedCall(500, () => {
this._revealCards(backs, fronts, index + 1);
});
}
_showCollectButton() {
const { width, height } = this.scale;
this.state = 'COLLECTING';
const btnBg = this.add.rectangle(width / 2, height - 80, 320, 65, 0x2a2200)
.setStrokeStyle(3, 0xffd700)
.setInteractive({ useHandCursor: true })
.setDepth(103).setAlpha(0);
const btnText = this.add.text(width / 2, height - 80, 'ADD TO COLLECTION', {
fontSize: '22px', color: '#ffd700', fontStyle: 'bold', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(103).setAlpha(0);
this.tweens.add({ targets: [btnBg, btnText], alpha: 1, duration: 300 });
this._revealObjects.push(btnBg, btnText);
btnBg.on('pointerover', () => btnBg.setFillStyle(0x443300));
btnBg.on('pointerout', () => btnBg.setFillStyle(0x2a2200));
btnBg.on('pointerdown', () => this._collectCards());
}
_collectCards() {
if (this.state !== 'COLLECTING') return;
this.state = 'DISMISSING';
// Find all CardObjects and emitters in reveal objects
const cardObjects = this._revealObjects.filter(o => o instanceof CardObject);
const nonCards = this._revealObjects.filter(o => !(o instanceof CardObject));
// Cards fly off screen
cardObjects.forEach((card, i) => {
this.tweens.add({
targets: card,
y: -300,
scaleX: 0.3, scaleY: 0.3,
alpha: 0,
duration: 500,
delay: i * 100,
ease: 'Quad.In'
});
});
// Stop emitters
this._emitters.forEach(e => {
if (e.stop) e.stop();
});
// Fade everything else
const flyDuration = 500 + cardObjects.length * 100;
this.time.delayedCall(200, () => {
nonCards.forEach(obj => {
if (obj.setAlpha && obj !== this._revealObjects[0]) {
this.tweens.add({ targets: obj, alpha: 0, duration: 300 });
}
});
// Fade overlay
this.tweens.add({ targets: this._revealObjects[0], alpha: 0, duration: 400 });
});
// Cleanup after all animations
this.time.delayedCall(flyDuration + 200, () => {
this._revealObjects.forEach(obj => {
if (obj && obj.destroy) obj.destroy();
});
this._revealObjects = [];
this._emitters = [];
this.state = 'SHOPPING';
this.goldText.setText(`Gold: ${this.save.gold}`);
});
}
// ── Helpers ───────────────────────────────────────────────────────────────
_generateParticleTexture() {
if (this.textures.exists('store_particle')) return;
const gfx = this.make.graphics({ add: false });
gfx.fillStyle(0xffffff);
gfx.fillCircle(4, 4, 4);
gfx.generateTexture('store_particle', 8, 8);
gfx.destroy();
}
_showMsg(msg, color) {
const { width } = this.scale;
const t = this.add.text(width / 2, 660, msg, {
fontSize: '24px', color, fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(150);
this.tweens.add({
targets: t, alpha: 0, duration: 500, delay: 1500,
onComplete: () => t.destroy()
});
}
_makeBackButton() {
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 1040, 'Back', {
fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5);
bg.on('pointerdown', () => {
if (this.state !== 'SHOPPING') return;
this.scene.start('MainMenuScene');
});
}
}