544 lines
18 KiB
JavaScript
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');
|
|
});
|
|
}
|
|
}
|