feat: add skirmish deck selection and drafting system
- Added `skirmish_decks.json` with 8 premade decks across difficulty levels (Easy to Legendary) - Implemented `SkirmishDeckSelectScene` for choosing player/opponent decks (draft or premade) - Implemented `SkirmishDraftScene` with interactive card drafting, budget system, and AI opponent deck generation - Updated `BattleScene` to support skirmish mode with custom decks and gold rewards (200/400) - Updated `BattleResultScene` to return players to SkirmishSetup for quick replay - Integrated new scenes into game flow via config and BootScene data loading
This commit is contained in:
parent
7513cb2bf5
commit
a5598bfcd0
|
|
@ -0,0 +1,124 @@
|
|||
[
|
||||
{
|
||||
"id": "sk_easy_imp_01",
|
||||
"title": "Imperial Garrison",
|
||||
"summary": "A straightforward Imperial force. Troopers strike from range while Guardians hold the line with counter attacks.",
|
||||
"difficulty": "Easy",
|
||||
"commander": "imp_cmd_1",
|
||||
"cards": [
|
||||
"imp_trooper_1", "imp_trooper_1", "imp_trooper_1",
|
||||
"imp_guardian_1", "imp_guardian_1",
|
||||
"imp_gunner_1", "imp_gunner_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_easy_raider_01",
|
||||
"title": "Raider Warband",
|
||||
"summary": "An aggressive raider force. Grunts grow stronger with berserk while Scouts jam the enemy's defenses.",
|
||||
"difficulty": "Easy",
|
||||
"commander": "raider_cmd_1",
|
||||
"cards": [
|
||||
"raider_grunt_1", "raider_grunt_1", "raider_grunt_1",
|
||||
"raider_brute_1", "raider_brute_1",
|
||||
"raider_scout_1", "raider_scout_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_medium_bt_01",
|
||||
"title": "Blood Covenant",
|
||||
"summary": "A vampiric force that drains enemy strength. Blood magic keeps your units alive while weakening foes.",
|
||||
"difficulty": "Medium",
|
||||
"commander": "bt_cmd_1",
|
||||
"cards": [
|
||||
"bt_thrall_1", "bt_thrall_1",
|
||||
"bt_leech_1", "bt_leech_1",
|
||||
"bt_initiate_1", "bt_initiate_1",
|
||||
"bt_revenant_1",
|
||||
"bt_bloodmage_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_medium_xeno_01",
|
||||
"title": "Xeno Hive",
|
||||
"summary": "An alien swarm that poisons and overwhelms. Carapace provides natural armor while venom eats through foes.",
|
||||
"difficulty": "Medium",
|
||||
"commander": "xeno_cmd_1",
|
||||
"cards": [
|
||||
"xeno_drone_1", "xeno_drone_1",
|
||||
"xeno_spitter_1", "xeno_spitter_1",
|
||||
"xeno_beetle_1", "xeno_beetle_1",
|
||||
"xeno_mantis_1",
|
||||
"xeno_guard_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_hard_right_01",
|
||||
"title": "Righteous Crusade",
|
||||
"summary": "A holy force of smite and fortification. Templars hack enemy defenses while Crusaders deliver divine punishment.",
|
||||
"difficulty": "Hard",
|
||||
"commander": "right_cmd_1",
|
||||
"cards": [
|
||||
"right_zealot_1", "right_zealot_1",
|
||||
"right_crusader_1", "right_crusader_1",
|
||||
"right_priest_1",
|
||||
"right_warden_1",
|
||||
"right_templar_1",
|
||||
"right_purifier_1",
|
||||
"right_inquisitor_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_hard_raider_01",
|
||||
"title": "Raider Devastation",
|
||||
"summary": "A brutal force of berserkers and reavers. Relentless aggression with pierce and berserk tears through defenses.",
|
||||
"difficulty": "Hard",
|
||||
"commander": "raider_cmd_2",
|
||||
"cards": [
|
||||
"raider_berserker_1", "raider_berserker_1",
|
||||
"raider_scavenger_1", "raider_scavenger_1",
|
||||
"raider_marauder_1",
|
||||
"raider_defiler_1",
|
||||
"raider_warlord_1",
|
||||
"raider_reaver_1",
|
||||
"raider_warbandbreaker_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_legendary_imp_01",
|
||||
"title": "Imperial War Machine",
|
||||
"summary": "The Empire's finest. A devastating combination of legendary war machines backed by elite support units.",
|
||||
"difficulty": "Legendary",
|
||||
"commander": "imp_cmd_3",
|
||||
"cards": [
|
||||
"imp_mech_1",
|
||||
"imp_juggernaut_1",
|
||||
"imp_praetorian_1",
|
||||
"imp_warbanner_1",
|
||||
"imp_sentinel_1",
|
||||
"imp_dreadnought_1",
|
||||
"imp_centurion_1",
|
||||
"imp_orbital_1",
|
||||
"imp_sniper_1",
|
||||
"imp_shieldwall_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sk_legendary_bt_01",
|
||||
"title": "Bloodthirsty Dominion",
|
||||
"summary": "An ancient bloodthirsty horde. Blood pacts fuel devastating power while drain and siphon sustain the onslaught.",
|
||||
"difficulty": "Legendary",
|
||||
"commander": "bt_cmd_2",
|
||||
"cards": [
|
||||
"bt_bloodlord_1",
|
||||
"bt_hemomancer_1",
|
||||
"bt_crimsonguard_1",
|
||||
"bt_hemorrhage_1",
|
||||
"bt_archon_1",
|
||||
"bt_crimson_fury_1",
|
||||
"bt_blood_martyr_1",
|
||||
"bt_sanguine_bastion_1",
|
||||
"bt_harbinger_1",
|
||||
"bt_revenant_1"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -4,6 +4,8 @@ import { MainMenuScene } from './scenes/MainMenuScene.js';
|
|||
import { CampaignSelectScene } from './scenes/CampaignSelectScene.js';
|
||||
import { CampaignScene } from './scenes/CampaignScene.js';
|
||||
import { SkirmishSetupScene } from './scenes/SkirmishSetupScene.js';
|
||||
import { SkirmishDeckSelectScene } from './scenes/SkirmishDeckSelectScene.js';
|
||||
import { SkirmishDraftScene } from './scenes/SkirmishDraftScene.js';
|
||||
import { BattleScene } from './scenes/BattleScene.js';
|
||||
import { BattleResultScene } from './scenes/BattleResultScene.js';
|
||||
import { DeckBuilderScene } from './scenes/DeckBuilderScene.js';
|
||||
|
|
@ -28,6 +30,8 @@ const config = {
|
|||
CampaignSelectScene,
|
||||
CampaignScene,
|
||||
SkirmishSetupScene,
|
||||
SkirmishDeckSelectScene,
|
||||
SkirmishDraftScene,
|
||||
BattleScene,
|
||||
BattleResultScene,
|
||||
DeckBuilderScene,
|
||||
|
|
|
|||
|
|
@ -985,8 +985,8 @@ export class BattleResultScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
if (!this.missionData) {
|
||||
// Skirmish → Main Menu
|
||||
this.scene.start('MainMenuScene');
|
||||
// Skirmish → Skirmish Setup for quick replay
|
||||
this.scene.start('SkirmishSetupScene');
|
||||
} else if (this.isFinalMission) {
|
||||
// Final campaign mission → Campaign Select
|
||||
this.scene.start('CampaignSelectScene');
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
|
||||
// Load a random battle background for the opponent's faction (if available)
|
||||
const opponentDeck = this.missionData?.opponent
|
||||
|| this.opponentDeckData
|
||||
|| { commander: 'raider_cmd_1' }; // skirmish fallback
|
||||
const cardManager = this.registry.get('cardManager');
|
||||
const cmdCard = cardManager?.getCard(opponentDeck.commander);
|
||||
|
|
@ -59,6 +60,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
this.playerLevel = data.playerLevel || 1;
|
||||
this.enemyLevel = data.enemyLevel || 1;
|
||||
this.campaignId = data.campaignId || null;
|
||||
this.opponentDeckData = data.opponentDeck || null;
|
||||
this._resultShown = false; // Set this to null when a mission begins.
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +155,11 @@ export class BattleScene extends Phaser.Scene {
|
|||
let opponentDeck;
|
||||
if (this.missionData) {
|
||||
opponentDeck = this.missionData.opponent;
|
||||
} else if (this.opponentDeckData) {
|
||||
// Skirmish: use the deck passed from deck select / draft
|
||||
opponentDeck = this.opponentDeckData;
|
||||
} else {
|
||||
// Skirmish: use a raider deck
|
||||
// Fallback raider deck
|
||||
opponentDeck = {
|
||||
commander: 'raider_cmd_1',
|
||||
cards: [
|
||||
|
|
@ -4723,8 +4728,16 @@ export class BattleScene extends Phaser.Scene {
|
|||
this.registry.set('save', save);
|
||||
}
|
||||
|
||||
// Skirmish gold rewards
|
||||
if (this.isSkirmish) {
|
||||
rewardGold = won ? 400 : 200;
|
||||
SaveManager.addGold(save, rewardGold);
|
||||
this.registry.set('save', save);
|
||||
}
|
||||
|
||||
// ── Determine opponent commander ID ──────────────────────────────────────
|
||||
const opponentCommanderId = this.missionData?.opponent?.commander
|
||||
|| this.opponentDeckData?.commander
|
||||
|| this.engine.opponentCommander?.id
|
||||
|| 'raider_cmd_1';
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export class BootScene extends Phaser.Scene {
|
|||
this.load.json('missions', 'data/missions.json');
|
||||
this.load.json('campaigns', 'data/campaigns.json');
|
||||
this.load.json('skills', 'data/skills.json');
|
||||
this.load.json('skirmish_decks', 'data/skirmish_decks.json');
|
||||
|
||||
this.load.audio('sfx_attack', 'assets/audio/fx/attack.mp3');
|
||||
this.load.audio('sfx_damage', 'assets/audio/fx/damage.mp3');
|
||||
|
|
@ -102,6 +103,7 @@ export class BootScene extends Phaser.Scene {
|
|||
const packsData = this.cache.json.get('packs');
|
||||
const missionsData = this.cache.json.get('missions');
|
||||
const campaignsData = this.cache.json.get('campaigns');
|
||||
const skirmishDecksData = this.cache.json.get('skirmish_decks');
|
||||
|
||||
const cardManager = new CardManager(cardsData);
|
||||
const packManager = new PackManager(packsData, cardManager);
|
||||
|
|
@ -127,6 +129,7 @@ export class BootScene extends Phaser.Scene {
|
|||
this.registry.set('packManager', packManager);
|
||||
this.registry.set('missions', missionsData);
|
||||
this.registry.set('campaigns', campaignsData);
|
||||
this.registry.set('skirmishDecks', skirmishDecksData);
|
||||
this.registry.set('save', save);
|
||||
|
||||
this.scene.start('IntroScene');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
export class SkirmishDeckSelectScene extends Phaser.Scene {
|
||||
constructor() { super('SkirmishDeckSelectScene'); }
|
||||
|
||||
init(data) {
|
||||
this.selectedLevel = data.selectedLevel || 1;
|
||||
this.selectedDifficulty = data.selectedDifficulty || 'Medium';
|
||||
}
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
|
||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
||||
|
||||
this.add.text(width / 2, 40, 'Select Decks', {
|
||||
fontSize: '42px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(width / 2, 90, `${this.selectedDifficulty} Difficulty • Card Level ${this.selectedLevel}`, {
|
||||
fontSize: '18px', color: '#888888', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const cardManager = this.registry.get('cardManager');
|
||||
const allDecks = this.registry.get('skirmishDecks') || [];
|
||||
this.availableDecks = allDecks.filter(d => d.difficulty === this.selectedDifficulty);
|
||||
this.cardManager = cardManager;
|
||||
|
||||
this.playerChoice = 'draft'; // 'draft' or deck id
|
||||
this.opponentChoice = 'draft';
|
||||
|
||||
// ── Left panel: Your Deck ──
|
||||
this._buildPanel(160, 'Your Deck', 'player');
|
||||
|
||||
// ── Right panel: Opponent Deck ──
|
||||
this._buildPanel(1040, 'Opponent Deck', 'opponent');
|
||||
|
||||
// ── Divider ──
|
||||
this.add.rectangle(width / 2, height / 2, 3, height - 200, 0x334466, 0.5);
|
||||
|
||||
// ── Action button ──
|
||||
this.actionBtnBg = null;
|
||||
this.actionBtnTxt = null;
|
||||
this._updateActionButton();
|
||||
|
||||
this._makeBackButton();
|
||||
}
|
||||
|
||||
_buildPanel(startX, title, side) {
|
||||
const panelW = 720;
|
||||
const panelCX = startX + panelW / 2;
|
||||
const { height } = this.scale;
|
||||
|
||||
this.add.text(panelCX, 130, title, {
|
||||
fontSize: '28px', color: '#d4af37', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Draft Deck option
|
||||
const draftY = 200;
|
||||
const draftBg = this.add.rectangle(panelCX, draftY, panelW - 60, 65, 0x1a3a5c)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x4488ff);
|
||||
const draftTxt = this.add.text(panelCX, draftY - 8, 'Draft Deck', {
|
||||
fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
const draftSub = this.add.text(panelCX, draftY + 16, 'Build a custom deck from a random card pool', {
|
||||
fontSize: '13px', color: '#88aacc', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
draftBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
if (side === 'player') this.playerChoice = 'draft';
|
||||
else this.opponentChoice = 'draft';
|
||||
this._refreshPanelHighlights();
|
||||
this._updateActionButton();
|
||||
});
|
||||
|
||||
const panelKey = `${side}Panel`;
|
||||
this[panelKey] = { draftBg, draftTxt, draftSub, deckEntries: [] };
|
||||
|
||||
// ── Premade deck list ──
|
||||
const listTop = 260;
|
||||
const entryH = 140;
|
||||
|
||||
this.availableDecks.forEach((deck, i) => {
|
||||
const y = listTop + i * (entryH + 12) + entryH / 2;
|
||||
if (y + entryH / 2 > height - 100) return; // safety clamp
|
||||
|
||||
const commander = this.cardManager.getCard(deck.commander);
|
||||
const cmdName = commander ? commander.name : deck.commander;
|
||||
|
||||
const bg = this.add.rectangle(panelCX, y, panelW - 60, entryH, 0x112233)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x334466);
|
||||
|
||||
const titleTxt = this.add.text(startX + 50, y - 42, deck.title, {
|
||||
fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
});
|
||||
|
||||
const cmdTxt = this.add.text(startX + 50, y - 16, `Commander: ${cmdName}`, {
|
||||
fontSize: '14px', color: '#88aaff', fontFamily: 'Audiowide'
|
||||
});
|
||||
|
||||
const summaryTxt = this.add.text(startX + 50, y + 8, deck.summary, {
|
||||
fontSize: '12px', color: '#999999', fontFamily: 'Audiowide',
|
||||
wordWrap: { width: panelW - 100 }
|
||||
});
|
||||
|
||||
const countTxt = this.add.text(startX + panelW - 80, y - 42, `${deck.cards.length} cards`, {
|
||||
fontSize: '13px', color: '#aaaaaa', fontFamily: 'Audiowide'
|
||||
}).setOrigin(1, 0);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
const choice = side === 'player' ? this.playerChoice : this.opponentChoice;
|
||||
if (choice !== deck.id) bg.setFillStyle(0x1a3344);
|
||||
});
|
||||
bg.on('pointerout', () => {
|
||||
const choice = side === 'player' ? this.playerChoice : this.opponentChoice;
|
||||
if (choice !== deck.id) bg.setFillStyle(0x112233);
|
||||
});
|
||||
bg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
if (side === 'player') this.playerChoice = deck.id;
|
||||
else this.opponentChoice = deck.id;
|
||||
this._refreshPanelHighlights();
|
||||
this._updateActionButton();
|
||||
});
|
||||
|
||||
this[panelKey].deckEntries.push({ bg, titleTxt, cmdTxt, summaryTxt, countTxt, deckId: deck.id });
|
||||
});
|
||||
|
||||
this._refreshPanelHighlights();
|
||||
}
|
||||
|
||||
_refreshPanelHighlights() {
|
||||
for (const side of ['player', 'opponent']) {
|
||||
const panel = this[`${side}Panel`];
|
||||
const choice = side === 'player' ? this.playerChoice : this.opponentChoice;
|
||||
|
||||
// Draft button highlight
|
||||
const isDraft = choice === 'draft';
|
||||
panel.draftBg.setFillStyle(isDraft ? 0x2a5a8c : 0x1a2a3a);
|
||||
panel.draftBg.setStrokeStyle(2, isDraft ? 0x88ccff : 0x444444);
|
||||
panel.draftTxt.setColor(isDraft ? '#ffffff' : '#888888');
|
||||
panel.draftSub.setColor(isDraft ? '#88aacc' : '#555555');
|
||||
|
||||
// Premade deck highlights
|
||||
for (const entry of panel.deckEntries) {
|
||||
const isSelected = choice === entry.deckId;
|
||||
entry.bg.setFillStyle(isSelected ? 0x1a3a5c : 0x112233);
|
||||
entry.bg.setStrokeStyle(2, isSelected ? 0x88ccff : 0x334466);
|
||||
entry.titleTxt.setColor(isSelected ? '#ffffff' : '#aaaaaa');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateActionButton() {
|
||||
const { width, height } = this.scale;
|
||||
const btnY = height - 60;
|
||||
|
||||
// Destroy existing button
|
||||
if (this.actionBtnBg) { this.actionBtnBg.destroy(); this.actionBtnBg = null; }
|
||||
if (this.actionBtnTxt) { this.actionBtnTxt.destroy(); this.actionBtnTxt = null; }
|
||||
|
||||
const bothPremade = this.playerChoice !== 'draft' && this.opponentChoice !== 'draft';
|
||||
|
||||
if (bothPremade) {
|
||||
// Start Battle
|
||||
this.actionBtnBg = this.add.rectangle(width / 2, btnY, 300, 60, 0x226622)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x44aa44);
|
||||
this.actionBtnTxt = this.add.text(width / 2, btnY, 'Start Battle', {
|
||||
fontSize: '26px', color: '#ffffff', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.actionBtnBg.on('pointerover', () => this.actionBtnBg.setFillStyle(0x338833));
|
||||
this.actionBtnBg.on('pointerout', () => this.actionBtnBg.setFillStyle(0x226622));
|
||||
this.actionBtnBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
const playerDeck = this._getDeckById(this.playerChoice);
|
||||
const opponentDeck = this._getDeckById(this.opponentChoice);
|
||||
this.scene.start('BattleScene', {
|
||||
skirmish: true,
|
||||
deck: { commander: playerDeck.commander, cards: [...playerDeck.cards] },
|
||||
opponentDeck: { commander: opponentDeck.commander, cards: [...opponentDeck.cards] },
|
||||
playerLevel: this.selectedLevel,
|
||||
enemyLevel: this.selectedLevel,
|
||||
difficulty: this.selectedDifficulty
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Draft Cards
|
||||
this.actionBtnBg = this.add.rectangle(width / 2, btnY, 300, 60, 0x224488)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x4488ff);
|
||||
this.actionBtnTxt = this.add.text(width / 2, btnY, 'Draft Cards', {
|
||||
fontSize: '26px', color: '#ffffff', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.actionBtnBg.on('pointerover', () => this.actionBtnBg.setFillStyle(0x3366aa));
|
||||
this.actionBtnBg.on('pointerout', () => this.actionBtnBg.setFillStyle(0x224488));
|
||||
this.actionBtnBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
this.scene.start('SkirmishDraftScene', {
|
||||
selectedLevel: this.selectedLevel,
|
||||
selectedDifficulty: this.selectedDifficulty,
|
||||
playerChoice: this.playerChoice,
|
||||
opponentChoice: this.opponentChoice,
|
||||
playerPremadeDeck: this.playerChoice !== 'draft' ? this._getDeckById(this.playerChoice) : null,
|
||||
opponentPremadeDeck: this.opponentChoice !== 'draft' ? this._getDeckById(this.opponentChoice) : null
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getDeckById(id) {
|
||||
return this.availableDecks.find(d => d.id === id) || null;
|
||||
}
|
||||
|
||||
_makeBackButton() {
|
||||
const bg = this.add.rectangle(80, 45, 160, 50, 0x333333)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(1, 0x888888);
|
||||
this.add.text(80, 45, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
bg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
this.scene.start('SkirmishSetupScene');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,600 @@
|
|||
import { CardObject } from '../objects/CardObject.js';
|
||||
|
||||
const BUDGETS = { Easy: 12, Medium: 24, Hard: 36, Legendary: 48 };
|
||||
const COSTS = { common: 1, rare: 2, epic: 4, legendary: 6 };
|
||||
const MAX_COPIES = { common: 10, rare: 2, epic: 2, legendary: 1 };
|
||||
|
||||
export class SkirmishDraftScene extends Phaser.Scene {
|
||||
constructor() { super('SkirmishDraftScene'); }
|
||||
|
||||
init(data) {
|
||||
this.selectedLevel = data.selectedLevel || 1;
|
||||
this.selectedDifficulty = data.selectedDifficulty || 'Medium';
|
||||
this.playerChoice = data.playerChoice || 'draft'; // 'draft' or premade deck id
|
||||
this.opponentChoice = data.opponentChoice || 'draft';
|
||||
this.playerPremadeDeck = data.playerPremadeDeck || null;
|
||||
this.opponentPremadeDeck = data.opponentPremadeDeck || null;
|
||||
}
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
this.cardManager = this.registry.get('cardManager');
|
||||
|
||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
||||
|
||||
this.add.text(width / 2, 30, 'Draft Cards', {
|
||||
fontSize: '36px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.totalBudget = BUDGETS[this.selectedDifficulty] || 24;
|
||||
this.remainingBudget = this.totalBudget;
|
||||
|
||||
// Player's working deck
|
||||
this.draftedCommander = null;
|
||||
this.draftedCards = []; // array of card IDs
|
||||
|
||||
// Track copies in draft: { cardId: count }
|
||||
this.draftCopies = {};
|
||||
|
||||
// Generate the shared card pool
|
||||
this.pool = this._generatePool();
|
||||
|
||||
// Deck panel elements (right side, rebuilt on changes)
|
||||
this.deckPanelGroup = [];
|
||||
|
||||
// Card grid
|
||||
this.scrollContainer = null;
|
||||
this.scrollMask = null;
|
||||
this.scrollY = 0;
|
||||
this.maxScrollY = 0;
|
||||
|
||||
// Grid layout constants (same as DeckBuilderScene)
|
||||
this.CARD_W = 155;
|
||||
this.CARD_H = 200;
|
||||
this.BTN_H = 28;
|
||||
this.GAP_X = 14;
|
||||
this.GAP_Y = 14;
|
||||
this.CELL_W = this.CARD_W + this.GAP_X;
|
||||
this.CELL_H = this.CARD_H + this.BTN_H + this.GAP_Y + 18;
|
||||
this.COLS = 7;
|
||||
this.GRID_LEFT = 80;
|
||||
this.SCROLL_TOP = 70;
|
||||
this.SCROLL_BOTTOM = height - 60;
|
||||
|
||||
const playerNeedsDraft = this.playerChoice === 'draft';
|
||||
|
||||
if (playerNeedsDraft) {
|
||||
this._renderCardGrid();
|
||||
this._renderDeckPanel();
|
||||
this._setupScrolling();
|
||||
} else {
|
||||
// Player has premade deck — show simplified view
|
||||
this._renderPremadeView();
|
||||
}
|
||||
|
||||
this._makeBackButton();
|
||||
}
|
||||
|
||||
// ── Pool generation ──────────────────────────────────────────────────
|
||||
|
||||
_generatePool() {
|
||||
const allCards = this.cardManager.getAllCards();
|
||||
|
||||
const commanders = allCards.filter(c => c.type === 'commander');
|
||||
const legAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'legendary');
|
||||
const epicAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'epic');
|
||||
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
||||
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
||||
|
||||
const pick = (arr) => {
|
||||
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, Math.ceil(arr.length / 3));
|
||||
};
|
||||
|
||||
return {
|
||||
commanders: pick(commanders),
|
||||
legendary: pick(legAssault),
|
||||
epic: pick(epicAssault),
|
||||
rare: pick(rareAssault),
|
||||
common: pick(commonAssault)
|
||||
};
|
||||
}
|
||||
|
||||
// ── Premade view (player chose a premade deck) ───────────────────────
|
||||
|
||||
_renderPremadeView() {
|
||||
const { width, height } = this.scale;
|
||||
const cx = width / 2;
|
||||
|
||||
this.add.text(cx, 120, 'Your deck is set!', {
|
||||
fontSize: '28px', color: '#88ccff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const deck = this.playerPremadeDeck;
|
||||
const commander = this.cardManager.getCard(deck.commander);
|
||||
|
||||
this.add.text(cx, 180, deck.title, {
|
||||
fontSize: '24px', color: '#d4af37', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, 220, `Commander: ${commander ? commander.name : deck.commander}`, {
|
||||
fontSize: '18px', color: '#88aaff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, 260, deck.summary, {
|
||||
fontSize: '16px', color: '#999999', fontFamily: 'Audiowide',
|
||||
wordWrap: { width: 700 }, align: 'center'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(cx, 310, `${deck.cards.length} cards`, {
|
||||
fontSize: '16px', color: '#aaaaaa', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Card list
|
||||
const counts = {};
|
||||
for (const id of deck.cards) counts[id] = (counts[id] || 0) + 1;
|
||||
|
||||
let y = 370;
|
||||
for (const [cardId, count] of Object.entries(counts)) {
|
||||
const card = this.cardManager.getCard(cardId);
|
||||
if (!card) continue;
|
||||
this.add.text(cx, y, `${card.name} x${count}`, {
|
||||
fontSize: '15px', color: '#cccccc', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
y += 28;
|
||||
}
|
||||
|
||||
// Start Battle button
|
||||
const btnY = height - 80;
|
||||
const startBg = this.add.rectangle(cx, btnY, 300, 60, 0x226622)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x44aa44);
|
||||
this.add.text(cx, btnY, 'Start Battle', {
|
||||
fontSize: '26px', color: '#ffffff', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
startBg.on('pointerover', () => startBg.setFillStyle(0x338833));
|
||||
startBg.on('pointerout', () => startBg.setFillStyle(0x226622));
|
||||
startBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
this._startBattle();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Card grid (left side) ────────────────────────────────────────────
|
||||
|
||||
_renderCardGrid() {
|
||||
if (this.scrollContainer) { this.scrollContainer.destroy(); this.scrollContainer = null; }
|
||||
if (this.scrollMask) { this.scrollMask.destroy(); this.scrollMask = null; }
|
||||
|
||||
this.scrollContainer = this.add.container(0, 0);
|
||||
|
||||
let cursorY = this.SCROLL_TOP;
|
||||
|
||||
const sections = [
|
||||
{ label: 'Commanders', cards: this.pool.commanders, costKey: null },
|
||||
{ label: 'Legendary', cards: this.pool.legendary, costKey: 'legendary' },
|
||||
{ label: 'Epic', cards: this.pool.epic, costKey: 'epic' },
|
||||
{ label: 'Rare', cards: this.pool.rare, costKey: 'rare' },
|
||||
{ label: 'Common', cards: this.pool.common, costKey: 'common' }
|
||||
];
|
||||
|
||||
const rarityColors = {
|
||||
Commanders: '#ffd700', Legendary: '#ffaa00', Epic: '#bb66ff', Rare: '#6699ff', Common: '#999999'
|
||||
};
|
||||
|
||||
for (const section of sections) {
|
||||
if (section.cards.length === 0) continue;
|
||||
|
||||
const sectionLabel = this.add.text(this.GRID_LEFT, cursorY, section.label, {
|
||||
fontSize: '18px', color: rarityColors[section.label] || '#d4af37', fontFamily: 'Audiowide'
|
||||
});
|
||||
this.scrollContainer.add(sectionLabel);
|
||||
|
||||
cursorY += 30;
|
||||
const startY = cursorY;
|
||||
|
||||
section.cards.forEach((card, i) => {
|
||||
const col = i % this.COLS;
|
||||
const row = Math.floor(i / this.COLS);
|
||||
const cx = this.GRID_LEFT + col * this.CELL_W + this.CARD_W / 2;
|
||||
const cy = startY + row * this.CELL_H + this.CARD_H / 2;
|
||||
|
||||
const isCommander = card.type === 'commander';
|
||||
const cost = isCommander ? 0 : (COSTS[card.rarity] || 1);
|
||||
const maxCopies = isCommander ? 1 : (MAX_COPIES[card.rarity] || 1);
|
||||
const currentCopies = this.draftCopies[card.id] || 0;
|
||||
|
||||
let canAdd = false;
|
||||
if (isCommander) {
|
||||
canAdd = !this.draftedCommander;
|
||||
} else {
|
||||
canAdd = cost <= this.remainingBudget &&
|
||||
currentCopies < maxCopies &&
|
||||
this.draftedCards.length < 10;
|
||||
}
|
||||
|
||||
// Card visual
|
||||
const inst = this.cardManager.createInstance(card.id, this.selectedLevel);
|
||||
const cardObj = new CardObject(this, cx, cy, inst, { width: this.CARD_W, height: this.CARD_H });
|
||||
if (!canAdd) cardObj.setAlpha(0.4);
|
||||
this.scrollContainer.add(cardObj);
|
||||
|
||||
// Cost badge
|
||||
const costStr = isCommander ? 'FREE' : `Cost: ${cost}`;
|
||||
const costColor = isCommander ? '#44ff44' : (canAdd ? '#ffcc44' : '#666666');
|
||||
const costBadge = this.add.text(cx, cy + this.CARD_H / 2 + 4, costStr, {
|
||||
fontSize: '12px', color: costColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5, 0);
|
||||
this.scrollContainer.add(costBadge);
|
||||
|
||||
// Copies indicator (for assault cards with copies already drafted)
|
||||
if (!isCommander && currentCopies > 0) {
|
||||
const copiesTxt = this.add.text(cx, cy + this.CARD_H / 2 + 18, `In deck: ${currentCopies}/${maxCopies}`, {
|
||||
fontSize: '11px', color: '#88cc88', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5, 0);
|
||||
this.scrollContainer.add(copiesTxt);
|
||||
}
|
||||
|
||||
// Add button
|
||||
const btnY = cy + this.CARD_H / 2 + (currentCopies > 0 ? 32 : 18);
|
||||
const btnLabel = isCommander
|
||||
? (this.draftedCommander === card.id ? 'SELECTED' : (this.draftedCommander ? 'SLOT TAKEN' : '+ SELECT'))
|
||||
: (canAdd ? '+ ADD' : (currentCopies >= maxCopies ? 'MAX COPIES' : (this.draftedCards.length >= 10 ? 'DECK FULL' : 'NO BUDGET')));
|
||||
|
||||
const btnBgColor = canAdd ? 0x225522 : 0x222222;
|
||||
const btnStroke = canAdd ? 0x44aa44 : 0x333333;
|
||||
const btnTextColor = canAdd ? '#44ff44' : '#444444';
|
||||
|
||||
const btnBg = this.add.rectangle(cx, btnY, this.CARD_W - 10, this.BTN_H, btnBgColor)
|
||||
.setStrokeStyle(1, btnStroke);
|
||||
const btnTxt = this.add.text(cx, btnY, btnLabel, {
|
||||
fontSize: '12px', color: btnTextColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.scrollContainer.add(btnBg);
|
||||
this.scrollContainer.add(btnTxt);
|
||||
|
||||
if (canAdd) {
|
||||
btnBg.setInteractive({ useHandCursor: true });
|
||||
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
|
||||
btnBg.on('pointerout', () => btnBg.setFillStyle(0x225522));
|
||||
btnBg.on('pointerdown', () => {
|
||||
if (isCommander) {
|
||||
this.draftedCommander = card.id;
|
||||
} else {
|
||||
this.draftedCards.push(card.id);
|
||||
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
||||
this.remainingBudget -= cost;
|
||||
}
|
||||
this.scrollY = Math.max(0, this.scrollY); // preserve scroll
|
||||
this._renderCardGrid();
|
||||
this._renderDeckPanel();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const rows = Math.ceil(section.cards.length / this.COLS);
|
||||
cursorY = startY + rows * this.CELL_H + 10;
|
||||
}
|
||||
|
||||
// Scrolling bounds
|
||||
const totalContentH = cursorY;
|
||||
const visibleH = this.SCROLL_BOTTOM - this.SCROLL_TOP;
|
||||
this.maxScrollY = Math.max(0, totalContentH - visibleH);
|
||||
this.scrollY = Phaser.Math.Clamp(this.scrollY, 0, this.maxScrollY);
|
||||
this.scrollContainer.y = -this.scrollY;
|
||||
|
||||
// Mask
|
||||
const maskShape = this.make.graphics({ x: 0, y: 0 });
|
||||
maskShape.fillStyle(0xffffff);
|
||||
maskShape.fillRect(0, this.SCROLL_TOP, 1320, visibleH);
|
||||
this.scrollMask = maskShape;
|
||||
this.scrollContainer.setMask(new Phaser.Display.Masks.GeometryMask(this, maskShape));
|
||||
}
|
||||
|
||||
_setupScrolling() {
|
||||
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY) => {
|
||||
if (pointer.x > 1320) return;
|
||||
this.scrollY = Phaser.Math.Clamp(this.scrollY + deltaY * 0.5, 0, this.maxScrollY);
|
||||
if (this.scrollContainer) this.scrollContainer.y = -this.scrollY;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Deck panel (right side) ──────────────────────────────────────────
|
||||
|
||||
_renderDeckPanel() {
|
||||
for (const o of this.deckPanelGroup) { if (o.scene) o.destroy(); }
|
||||
this.deckPanelGroup = [];
|
||||
|
||||
const PANEL_X = 1340;
|
||||
const PANEL_W = 540;
|
||||
const panelCX = PANEL_X + PANEL_W / 2;
|
||||
const { height } = this.scale;
|
||||
|
||||
// Panel background
|
||||
const panelBg = this.add.rectangle(panelCX, height / 2, PANEL_W, height - 20, 0x0a0f1a)
|
||||
.setStrokeStyle(2, 0x334466);
|
||||
this.deckPanelGroup.push(panelBg);
|
||||
|
||||
// Budget bar
|
||||
const budgetPct = this.remainingBudget / this.totalBudget;
|
||||
const budgetColor = budgetPct > 0.5 ? '#44ff44' : budgetPct > 0.2 ? '#ffaa00' : '#ff4444';
|
||||
|
||||
const budgetTitle = this.add.text(panelCX, 80, 'Draft Budget', {
|
||||
fontSize: '20px', color: '#d4af37', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(budgetTitle);
|
||||
|
||||
const budgetText = this.add.text(panelCX, 108, `${this.remainingBudget} / ${this.totalBudget}`, {
|
||||
fontSize: '28px', color: budgetColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(budgetText);
|
||||
|
||||
// Budget bar visual
|
||||
const barW = PANEL_W - 80;
|
||||
const barH = 14;
|
||||
const barX = panelCX - barW / 2;
|
||||
const barY = 135;
|
||||
const barBg = this.add.rectangle(panelCX, barY, barW, barH, 0x222222).setStrokeStyle(1, 0x444444);
|
||||
const fillW = Math.max(1, barW * budgetPct);
|
||||
const barColor = budgetPct > 0.5 ? 0x44aa44 : budgetPct > 0.2 ? 0xaa8800 : 0xaa3333;
|
||||
const barFill = this.add.rectangle(barX + fillW / 2, barY, fillW, barH - 2, barColor);
|
||||
this.deckPanelGroup.push(barBg, barFill);
|
||||
|
||||
// Commander slot
|
||||
const cmdY = 175;
|
||||
const cmdCard = this.draftedCommander ? this.cardManager.getCard(this.draftedCommander) : null;
|
||||
const cmdBg = this.add.rectangle(panelCX, cmdY, PANEL_W - 40, 46, cmdCard ? 0x1a3a5c : 0x1a1a2e)
|
||||
.setStrokeStyle(2, 0xffd700);
|
||||
const cmdText = this.add.text(panelCX, cmdY, cmdCard ? `Commander: ${cmdCard.name}` : 'No Commander Selected', {
|
||||
fontSize: '17px', color: cmdCard ? '#ffd700' : '#555555', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(cmdBg, cmdText);
|
||||
|
||||
if (cmdCard) {
|
||||
const rmBtn = this.add.rectangle(panelCX + PANEL_W / 2 - 40, cmdY, 34, 28, 0x662222)
|
||||
.setInteractive({ useHandCursor: true }).setStrokeStyle(1, 0xff4444);
|
||||
const rmTxt = this.add.text(panelCX + PANEL_W / 2 - 40, cmdY, 'X', {
|
||||
fontSize: '14px', color: '#ff4444', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
rmBtn.on('pointerdown', () => {
|
||||
this.draftedCommander = null;
|
||||
this._renderCardGrid();
|
||||
this._renderDeckPanel();
|
||||
});
|
||||
this.deckPanelGroup.push(rmBtn, rmTxt);
|
||||
}
|
||||
|
||||
// Assault cards list
|
||||
const ENTRY_H = 40;
|
||||
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
|
||||
const counts = {};
|
||||
for (const id of this.draftedCards) counts[id] = (counts[id] || 0) + 1;
|
||||
|
||||
let y = 225;
|
||||
for (const [cardId, count] of Object.entries(counts)) {
|
||||
const card = this.cardManager.getCard(cardId);
|
||||
if (!card) continue;
|
||||
const cost = COSTS[card.rarity] || 1;
|
||||
|
||||
const bg = this.add.rectangle(panelCX, y, PANEL_W - 40, ENTRY_H - 4, 0x1a3a5c)
|
||||
.setStrokeStyle(1, rarityColors[card.rarity] || 0x336699);
|
||||
const txt = this.add.text(PANEL_X + 30, y, card.name, {
|
||||
fontSize: '15px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0, 0.5);
|
||||
const countTxt = this.add.text(PANEL_X + 300, y, `x${count}`, {
|
||||
fontSize: '15px', color: '#aaaaaa', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0, 0.5);
|
||||
const costTxt = this.add.text(PANEL_X + 360, y, `(${cost * count} pts)`, {
|
||||
fontSize: '13px', color: '#8888cc', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0, 0.5);
|
||||
|
||||
const rmBtn = this.add.rectangle(panelCX + PANEL_W / 2 - 40, y, 34, 28, 0x662222)
|
||||
.setInteractive({ useHandCursor: true }).setStrokeStyle(1, 0xff4444);
|
||||
const rmTxt = this.add.text(panelCX + PANEL_W / 2 - 40, y, '-', {
|
||||
fontSize: '18px', color: '#ff4444', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
rmBtn.on('pointerdown', () => {
|
||||
const idx = this.draftedCards.lastIndexOf(cardId);
|
||||
if (idx !== -1) {
|
||||
this.draftedCards.splice(idx, 1);
|
||||
this.draftCopies[cardId] = (this.draftCopies[cardId] || 1) - 1;
|
||||
if (this.draftCopies[cardId] <= 0) delete this.draftCopies[cardId];
|
||||
this.remainingBudget += cost;
|
||||
this._renderCardGrid();
|
||||
this._renderDeckPanel();
|
||||
}
|
||||
});
|
||||
|
||||
this.deckPanelGroup.push(bg, txt, countTxt, costTxt, rmBtn, rmTxt);
|
||||
y += ENTRY_H;
|
||||
}
|
||||
|
||||
// Empty slots
|
||||
const filled = this.draftedCards.length;
|
||||
for (let s = filled; s < Math.min(10, filled + 3); s++) {
|
||||
const slotBg = this.add.rectangle(panelCX, y, PANEL_W - 40, ENTRY_H - 4, 0x0d0d1a)
|
||||
.setStrokeStyle(1, 0x222244);
|
||||
const slotTxt = this.add.text(panelCX, y, '— empty slot —', {
|
||||
fontSize: '13px', color: '#333355', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(slotBg, slotTxt);
|
||||
y += ENTRY_H;
|
||||
}
|
||||
|
||||
// Card count
|
||||
const countColor = filled >= 3 ? '#aaaaff' : '#888888';
|
||||
const totalTxt = this.add.text(panelCX, y + 16, `Cards: ${filled} / 10`, {
|
||||
fontSize: '18px', color: countColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(totalTxt);
|
||||
|
||||
// Validation
|
||||
const isValid = this.draftedCommander && filled >= 3;
|
||||
const validMsg = !this.draftedCommander
|
||||
? 'Select a commander'
|
||||
: filled < 3
|
||||
? `Need at least ${3 - filled} more card(s)`
|
||||
: 'Deck ready!';
|
||||
const validColor = isValid ? '#44ff44' : '#ff6644';
|
||||
const validTxt = this.add.text(panelCX, y + 44, validMsg, {
|
||||
fontSize: '15px', color: validColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.deckPanelGroup.push(validTxt);
|
||||
|
||||
// Start Battle button
|
||||
if (isValid) {
|
||||
const btnY = height - 60;
|
||||
const startBg = this.add.rectangle(panelCX, btnY, 240, 50, 0x226622)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x44aa44);
|
||||
const startTxt = this.add.text(panelCX, btnY, 'Start Battle', {
|
||||
fontSize: '22px', color: '#ffffff', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
startBg.on('pointerover', () => startBg.setFillStyle(0x338833));
|
||||
startBg.on('pointerout', () => startBg.setFillStyle(0x226622));
|
||||
startBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
this._startBattle();
|
||||
});
|
||||
|
||||
this.deckPanelGroup.push(startBg, startTxt);
|
||||
}
|
||||
}
|
||||
|
||||
// ── AI Draft Algorithm ───────────────────────────────────────────────
|
||||
|
||||
_aiDraft() {
|
||||
let budget = this.totalBudget;
|
||||
const taken = new Set();
|
||||
|
||||
// Track what the player already drafted from the shared pool
|
||||
if (this.draftedCommander) taken.add(this.draftedCommander);
|
||||
for (const id of this.draftedCards) taken.add(id);
|
||||
|
||||
// Pick commander: prefer one not taken by player
|
||||
const availCmds = this.pool.commanders.filter(c => !taken.has(c.id));
|
||||
const cmdPool = availCmds.length > 0 ? availCmds : this.pool.commanders;
|
||||
const commander = cmdPool[Math.floor(Math.random() * cmdPool.length)];
|
||||
const preferredFaction = commander.faction;
|
||||
|
||||
// Gather all available assault cards from pool
|
||||
const allAssault = [
|
||||
...this.pool.legendary,
|
||||
...this.pool.epic,
|
||||
...this.pool.rare,
|
||||
...this.pool.common
|
||||
].filter(c => !taken.has(c.id));
|
||||
|
||||
// Score each card
|
||||
const scored = allAssault.map(card => {
|
||||
const cost = COSTS[card.rarity] || 1;
|
||||
const lv1 = card.levels[0];
|
||||
const baseValue = (lv1.attack + lv1.health / 3 + (lv1.armor || 0) * 2) / cost;
|
||||
const factionBonus = card.faction === preferredFaction ? 3 : 0;
|
||||
|
||||
// Synergy: count useful skills
|
||||
let synergyBonus = 0;
|
||||
const skills = lv1.skills || [];
|
||||
for (const sk of skills) {
|
||||
if (['strike', 'pierce', 'siege', 'swipe', 'mortar'].includes(sk.name)) synergyBonus += 1;
|
||||
if (['heal', 'protect', 'rally', 'fortify'].includes(sk.name)) synergyBonus += 0.5;
|
||||
}
|
||||
|
||||
return { card, cost, score: baseValue + factionBonus + synergyBonus };
|
||||
});
|
||||
|
||||
// Sort by score descending
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
|
||||
const aiCards = [];
|
||||
const aiCopies = {};
|
||||
|
||||
for (const entry of scored) {
|
||||
if (aiCards.length >= 10) break;
|
||||
if (entry.cost > budget) continue;
|
||||
|
||||
const maxC = MAX_COPIES[entry.card.rarity] || 1;
|
||||
const current = aiCopies[entry.card.id] || 0;
|
||||
if (current >= maxC) continue;
|
||||
|
||||
aiCards.push(entry.card.id);
|
||||
aiCopies[entry.card.id] = current + 1;
|
||||
budget -= entry.cost;
|
||||
}
|
||||
|
||||
// Multi-pass: try to fill remaining slots with cheaper cards
|
||||
if (aiCards.length < 3 && budget > 0) {
|
||||
const cheapest = scored
|
||||
.filter(e => e.cost <= budget && (aiCopies[e.card.id] || 0) < (MAX_COPIES[e.card.rarity] || 1))
|
||||
.sort((a, b) => a.cost - b.cost);
|
||||
for (const entry of cheapest) {
|
||||
if (aiCards.length >= 10) break;
|
||||
if (entry.cost > budget) break;
|
||||
aiCards.push(entry.card.id);
|
||||
aiCopies[entry.card.id] = (aiCopies[entry.card.id] || 0) + 1;
|
||||
budget -= entry.cost;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
commander: commander.id,
|
||||
cards: aiCards
|
||||
};
|
||||
}
|
||||
|
||||
// ── Start Battle ─────────────────────────────────────────────────────
|
||||
|
||||
_startBattle() {
|
||||
// Build player deck
|
||||
let playerDeck;
|
||||
if (this.playerChoice === 'draft') {
|
||||
playerDeck = {
|
||||
commander: this.draftedCommander,
|
||||
cards: [...this.draftedCards]
|
||||
};
|
||||
} else {
|
||||
playerDeck = {
|
||||
commander: this.playerPremadeDeck.commander,
|
||||
cards: [...this.playerPremadeDeck.cards]
|
||||
};
|
||||
}
|
||||
|
||||
// Build opponent deck
|
||||
let opponentDeck;
|
||||
if (this.opponentChoice === 'draft') {
|
||||
opponentDeck = this._aiDraft();
|
||||
} else {
|
||||
opponentDeck = {
|
||||
commander: this.opponentPremadeDeck.commander,
|
||||
cards: [...this.opponentPremadeDeck.cards]
|
||||
};
|
||||
}
|
||||
|
||||
this.scene.start('BattleScene', {
|
||||
skirmish: true,
|
||||
deck: playerDeck,
|
||||
opponentDeck,
|
||||
playerLevel: this.selectedLevel,
|
||||
enemyLevel: this.selectedLevel,
|
||||
difficulty: this.selectedDifficulty
|
||||
});
|
||||
}
|
||||
|
||||
// ── Back button ──────────────────────────────────────────────────────
|
||||
|
||||
_makeBackButton() {
|
||||
const bg = this.add.rectangle(80, 45, 160, 50, 0x333333)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(1, 0x888888);
|
||||
this.add.text(80, 45, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
bg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
this.scene.start('SkirmishDeckSelectScene', {
|
||||
selectedLevel: this.selectedLevel,
|
||||
selectedDifficulty: this.selectedDifficulty
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
|||
fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(width / 2, 415, 'Label only — no stat changes yet', {
|
||||
this.add.text(width / 2, 415, 'Affects draft budget and premade deck options', {
|
||||
fontSize: '14px', color: '#888888', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
|
|
@ -94,11 +94,11 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
|||
this.diffButtons.push({ bg, text, diff });
|
||||
});
|
||||
|
||||
// ── Start Battle button ───────────────────────────────────────────
|
||||
// ── Select Decks button ─────────────────────────────────────────
|
||||
const startBg = this.add.rectangle(width / 2, 620, 300, 70, 0x224488)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x4488ff);
|
||||
this.add.text(width / 2, 620, 'Start Battle', {
|
||||
this.add.text(width / 2, 620, 'Select Decks', {
|
||||
fontSize: '28px', color: '#ffffff', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
|
|
@ -106,17 +106,9 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
|||
startBg.on('pointerout', () => startBg.setFillStyle(0x224488));
|
||||
startBg.on('pointerdown', () => {
|
||||
this.sound.play('sfx_menu_select', { volume: 0.7 });
|
||||
const deck = save.decks[0];
|
||||
if (!deck) {
|
||||
this._showError('No deck! Create one in Deck Builder first.');
|
||||
return;
|
||||
}
|
||||
this.scene.start('BattleScene', {
|
||||
skirmish: true,
|
||||
deck,
|
||||
playerLevel: this.selectedLevel,
|
||||
enemyLevel: this.selectedLevel,
|
||||
difficulty: this.selectedDifficulty
|
||||
this.scene.start('SkirmishDeckSelectScene', {
|
||||
selectedLevel: this.selectedLevel,
|
||||
selectedDifficulty: this.selectedDifficulty
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue