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'); }); } }