feat(draft): implement alternating draft mode with dual-deck UI
- Add alternating draft where player and AI take turns picking commander then cards - Implement shared card pool tracking with "taken" state overlays in card grid - Introduce dual-deck panel showing both sides' budgets, commanders, and card lists - Refactor AI draft logic into state machine with `_playerPick`, `_aiPickCommander`, and `_aiPickOneCard` - Add status banner indicating whose turn it is, with pulsing animation on AI turns - Adjust card skill values in `cards.json` (e.g., rally: 3→4, heal upgrades, strike boosts)
This commit is contained in:
parent
762dc77556
commit
1ca1b5bf80
145
data/cards.json
145
data/cards.json
|
|
@ -27,7 +27,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "rally",
|
||||||
"value": 3,
|
"value": 4,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -40,7 +40,8 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "rally",
|
||||||
"value": 5,
|
"value": 4,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -53,7 +54,8 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "rally",
|
||||||
"value": 7,
|
"value": 6,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -66,8 +68,14 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "rally",
|
||||||
"value": 10,
|
"value": 8,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"value": 10,
|
||||||
|
"trigger": "on_defend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -89,9 +97,8 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "fortify",
|
"name": "heal",
|
||||||
"value": 1,
|
"value": 2,
|
||||||
"all": true,
|
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -103,9 +110,8 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "fortify",
|
"name": "heal",
|
||||||
"value": 2,
|
"value": 5,
|
||||||
"all": true,
|
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -117,7 +123,7 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "fortify",
|
"name": "heal",
|
||||||
"value": 4,
|
"value": 4,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
|
|
@ -131,8 +137,8 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "fortify",
|
"name": "heal",
|
||||||
"value": 6,
|
"value": 8,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
|
|
@ -144,9 +150,15 @@
|
||||||
"armor": 20,
|
"armor": 20,
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "heal",
|
||||||
|
"value": 10,
|
||||||
|
"all": true,
|
||||||
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "fortify",
|
"name": "fortify",
|
||||||
"value": 8,
|
"value": 5,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +183,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "strike",
|
"name": "strike",
|
||||||
"value": 1,
|
"value": 3,
|
||||||
"all": true,
|
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -185,8 +196,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "strike",
|
"name": "strike",
|
||||||
"value": 4,
|
"value": 6,
|
||||||
"all": true,
|
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -230,6 +240,12 @@
|
||||||
"value": 23,
|
"value": 23,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rally",
|
||||||
|
"value": 8,
|
||||||
|
"all": true,
|
||||||
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -250,12 +266,6 @@
|
||||||
"armor": 3,
|
"armor": 3,
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
|
||||||
"name": "rally",
|
|
||||||
"value": 1,
|
|
||||||
"all": true,
|
|
||||||
"trigger": "preBattle"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
"value": 1,
|
"value": 1,
|
||||||
|
|
@ -270,12 +280,6 @@
|
||||||
"armor": 5,
|
"armor": 5,
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
|
||||||
"name": "rally",
|
|
||||||
"value": 2,
|
|
||||||
"all": true,
|
|
||||||
"trigger": "preBattle"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
"value": 2,
|
"value": 2,
|
||||||
|
|
@ -291,10 +295,10 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "cleanse",
|
||||||
"value": 3,
|
"value": 6,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "postBattle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
|
|
@ -311,10 +315,10 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "cleanse",
|
||||||
"value": 5,
|
"value": 9,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "postBattle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
|
|
@ -331,16 +335,21 @@
|
||||||
"delay": 0,
|
"delay": 0,
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "rally",
|
"name": "cleanse",
|
||||||
"value": 7,
|
"value": 13,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "postBattle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
"value": 7,
|
"value": 7,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"value": 10,
|
||||||
|
"trigger": "on_defend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -2046,7 +2055,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "siege",
|
"name": "siege",
|
||||||
"value": 3,
|
"value": 4,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2059,7 +2068,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "siege",
|
"name": "siege",
|
||||||
"value": 5,
|
"value": 10,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2072,7 +2081,13 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "siege",
|
"name": "siege",
|
||||||
|
"value": 20,
|
||||||
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "weaken",
|
||||||
"value": 7,
|
"value": 7,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2085,7 +2100,13 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "siege",
|
"name": "siege",
|
||||||
|
"value": 20,
|
||||||
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "weaken",
|
||||||
"value": 10,
|
"value": 10,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2098,7 +2119,13 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "siege",
|
"name": "siege",
|
||||||
|
"value": 25,
|
||||||
|
"trigger": "preBattle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "weaken",
|
||||||
"value": 15,
|
"value": 15,
|
||||||
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2125,11 +2152,6 @@
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "strike",
|
|
||||||
"value": 2,
|
|
||||||
"trigger": "preAttack"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -2141,14 +2163,9 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "weaken",
|
"name": "weaken",
|
||||||
"value": 3,
|
"value": 4,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "strike",
|
|
||||||
"value": 4,
|
|
||||||
"trigger": "preAttack"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -2166,7 +2183,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "strike",
|
"name": "strike",
|
||||||
"value": 6,
|
"value": 15,
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2185,7 +2202,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "strike",
|
"name": "strike",
|
||||||
"value": 9,
|
"value": 20,
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2204,7 +2221,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "strike",
|
"name": "strike",
|
||||||
"value": 13,
|
"value": 25,
|
||||||
"trigger": "preAttack"
|
"trigger": "preAttack"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -2231,11 +2248,6 @@
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wound",
|
|
||||||
"value": 2,
|
|
||||||
"trigger": "on_attack"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -2250,11 +2262,6 @@
|
||||||
"value": 5,
|
"value": 5,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "wound",
|
|
||||||
"value": 4,
|
|
||||||
"trigger": "on_attack"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -2304,7 +2311,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "enfeeble",
|
"name": "enfeeble",
|
||||||
"value": 15,
|
"value": 13,
|
||||||
"all": true,
|
"all": true,
|
||||||
"trigger": "preBattle"
|
"trigger": "preBattle"
|
||||||
},
|
},
|
||||||
|
|
@ -7234,7 +7241,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "venom",
|
"name": "venom",
|
||||||
"value": 8,
|
"value": 13,
|
||||||
"trigger": "on_attack",
|
"trigger": "on_attack",
|
||||||
"all": true
|
"all": true
|
||||||
}
|
}
|
||||||
|
|
@ -7248,7 +7255,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "venom",
|
"name": "venom",
|
||||||
"value": 11,
|
"value": 16,
|
||||||
"trigger": "on_attack",
|
"trigger": "on_attack",
|
||||||
"all": true
|
"all": true
|
||||||
}
|
}
|
||||||
|
|
@ -7262,7 +7269,7 @@
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "venom",
|
"name": "venom",
|
||||||
"value": 16,
|
"value": 20,
|
||||||
"trigger": "on_attack",
|
"trigger": "on_attack",
|
||||||
"all": true
|
"all": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,6 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
|
|
||||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
|
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.totalBudget = BUDGETS[this.selectedDifficulty] || 24;
|
||||||
this.remainingBudget = this.totalBudget;
|
this.remainingBudget = this.totalBudget;
|
||||||
|
|
||||||
|
|
@ -36,7 +32,10 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
// Track copies in draft: { cardId: count }
|
// Track copies in draft: { cardId: count }
|
||||||
this.draftCopies = {};
|
this.draftCopies = {};
|
||||||
|
|
||||||
// Generate the shared card pool
|
// Alternating draft mode: both sides are drafting
|
||||||
|
this.isAlternatingDraft = (this.playerChoice === 'draft' && this.opponentChoice === 'draft');
|
||||||
|
|
||||||
|
// Generate the shared card pool (larger pool for alternating draft)
|
||||||
this.pool = this._generatePool();
|
this.pool = this._generatePool();
|
||||||
|
|
||||||
// Deck panel elements (right side, rebuilt on changes)
|
// Deck panel elements (right side, rebuilt on changes)
|
||||||
|
|
@ -64,10 +63,35 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const playerNeedsDraft = this.playerChoice === 'draft';
|
const playerNeedsDraft = this.playerChoice === 'draft';
|
||||||
|
|
||||||
if (playerNeedsDraft) {
|
if (playerNeedsDraft) {
|
||||||
this._renderCardGrid();
|
if (this.isAlternatingDraft) {
|
||||||
this._renderDeckPanel();
|
// Alternating draft state
|
||||||
this._setupScrolling();
|
this.draftState = 'PLAYER_COMMANDER';
|
||||||
|
this.inputLocked = false;
|
||||||
|
this.aiCommander = null;
|
||||||
|
this.aiCards = [];
|
||||||
|
this.aiCopies = {};
|
||||||
|
this.aiBudget = this.totalBudget;
|
||||||
|
this.takenCards = {}; // { cardId: 'player' | 'ai' }
|
||||||
|
this.bannerGroup = [];
|
||||||
|
|
||||||
|
this._renderStatusBanner();
|
||||||
|
this._renderCardGrid();
|
||||||
|
this._renderDualDeckPanel();
|
||||||
|
this._setupScrolling();
|
||||||
|
} else {
|
||||||
|
this.add.text(width / 2, 30, 'Draft Cards', {
|
||||||
|
fontSize: '36px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
this._renderCardGrid();
|
||||||
|
this._renderDeckPanel();
|
||||||
|
this._setupScrolling();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this.add.text(width / 2, 30, 'Draft Cards', {
|
||||||
|
fontSize: '36px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
// Player has premade deck — show simplified view
|
// Player has premade deck — show simplified view
|
||||||
this._renderPremadeView();
|
this._renderPremadeView();
|
||||||
}
|
}
|
||||||
|
|
@ -86,9 +110,10 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
||||||
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
||||||
|
|
||||||
|
const fraction = this.isAlternatingDraft ? 2 : 3;
|
||||||
const pick = (arr) => {
|
const pick = (arr) => {
|
||||||
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
||||||
return shuffled.slice(0, Math.ceil(arr.length / 3));
|
return shuffled.slice(0, Math.ceil(arr.length / fraction));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -202,53 +227,131 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
|
|
||||||
const isCommander = card.type === 'commander';
|
const isCommander = card.type === 'commander';
|
||||||
const cost = isCommander ? 0 : (COSTS[card.rarity] || 1);
|
const cost = isCommander ? 0 : (COSTS[card.rarity] || 1);
|
||||||
const maxCopies = isCommander ? 1 : (MAX_COPIES[card.rarity] || 1);
|
|
||||||
const currentCopies = this.draftCopies[card.id] || 0;
|
|
||||||
|
|
||||||
|
// ── Determine if card can be added ──
|
||||||
let canAdd = false;
|
let canAdd = false;
|
||||||
if (isCommander) {
|
let takenBy = null; // 'player' | 'ai' | null
|
||||||
canAdd = !this.draftedCommander;
|
|
||||||
|
if (this.isAlternatingDraft) {
|
||||||
|
takenBy = this.takenCards[card.id] || null;
|
||||||
|
if (takenBy || this.inputLocked) {
|
||||||
|
canAdd = false;
|
||||||
|
} else if (isCommander) {
|
||||||
|
canAdd = this.draftState === 'PLAYER_COMMANDER' && !this.draftedCommander;
|
||||||
|
} else {
|
||||||
|
canAdd = this.draftState === 'PLAYER_CARD' &&
|
||||||
|
cost <= this.remainingBudget &&
|
||||||
|
this.draftedCards.length < 10;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
canAdd = cost <= this.remainingBudget &&
|
const maxCopies = isCommander ? 1 : (MAX_COPIES[card.rarity] || 1);
|
||||||
currentCopies < maxCopies &&
|
const currentCopies = this.draftCopies[card.id] || 0;
|
||||||
this.draftedCards.length < 10;
|
if (isCommander) {
|
||||||
|
canAdd = !this.draftedCommander;
|
||||||
|
} else {
|
||||||
|
canAdd = cost <= this.remainingBudget &&
|
||||||
|
currentCopies < maxCopies &&
|
||||||
|
this.draftedCards.length < 10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card visual
|
// Card visual
|
||||||
const inst = this.cardManager.createInstance(card.id, this.selectedLevel);
|
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 });
|
const cardObj = new CardObject(this, cx, cy, inst, { width: this.CARD_W, height: this.CARD_H });
|
||||||
if (!canAdd) cardObj.setAlpha(0.4);
|
if (!canAdd) cardObj.setAlpha(takenBy ? 0.3 : 0.4);
|
||||||
this.scrollContainer.add(cardObj);
|
this.scrollContainer.add(cardObj);
|
||||||
|
|
||||||
// Cost badge
|
// Taken overlay (alternating draft only)
|
||||||
const costStr = isCommander ? 'FREE' : `Cost: ${cost}`;
|
if (this.isAlternatingDraft && takenBy) {
|
||||||
const costColor = isCommander ? '#44ff44' : (canAdd ? '#ffcc44' : '#666666');
|
const overlayColor = takenBy === 'player' ? 0x225522 : 0x552222;
|
||||||
const costBadge = this.add.text(cx, cy + this.CARD_H / 2 + 4, costStr, {
|
const overlay = this.add.rectangle(cx, cy, this.CARD_W, this.CARD_H, overlayColor, 0.5);
|
||||||
fontSize: '12px', color: costColor, fontFamily: 'Audiowide'
|
this.scrollContainer.add(overlay);
|
||||||
}).setOrigin(0.5, 0);
|
|
||||||
this.scrollContainer.add(costBadge);
|
|
||||||
|
|
||||||
// Copies indicator (for assault cards with copies already drafted)
|
const labelText = takenBy === 'player' ? 'YOURS' : 'TAKEN';
|
||||||
if (!isCommander && currentCopies > 0) {
|
const labelColor = takenBy === 'player' ? '#44ff44' : '#ff4444';
|
||||||
const copiesTxt = this.add.text(cx, cy + this.CARD_H / 2 + 18, `In deck: ${currentCopies}/${maxCopies}`, {
|
const overlayTxt = this.add.text(cx, cy, labelText, {
|
||||||
fontSize: '11px', color: '#88cc88', fontFamily: 'Audiowide'
|
fontSize: '20px', color: labelColor, fontFamily: 'Audiowide', fontStyle: 'bold'
|
||||||
}).setOrigin(0.5, 0);
|
}).setOrigin(0.5);
|
||||||
this.scrollContainer.add(copiesTxt);
|
this.scrollContainer.add(overlayTxt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add button
|
// Cost gem badge (top-left corner of card)
|
||||||
const btnY = cy + this.CARD_H / 2 + (currentCopies > 0 ? 32 : 18);
|
const gemX = cx - this.CARD_W / 2 + 18;
|
||||||
const btnLabel = isCommander
|
const gemY = cy - this.CARD_H / 2 + 18;
|
||||||
? (this.draftedCommander === card.id ? 'SELECTED' : (this.draftedCommander ? 'SLOT TAKEN' : '+ SELECT'))
|
const gemRadius = 16;
|
||||||
: (canAdd ? '+ ADD' : (currentCopies >= maxCopies ? 'MAX COPIES' : (this.draftedCards.length >= 10 ? 'DECK FULL' : 'NO BUDGET')));
|
|
||||||
|
|
||||||
|
const gemFillColors = { 1: 0x1a6632, 2: 0x1a3a88, 4: 0x5a1a88, 6: 0x884400 };
|
||||||
|
const gemStrokeColors = { 1: 0x44ff88, 2: 0x4488ff, 4: 0xbb44ff, 6: 0xffaa22 };
|
||||||
|
const gemTextColors = { 1: '#44ff88', 2: '#88bbff', 4: '#dd88ff', 6: '#ffbb44' };
|
||||||
|
|
||||||
|
const gemGraphics = this.add.graphics();
|
||||||
|
if (isCommander) {
|
||||||
|
// Gold star-like badge for FREE commanders
|
||||||
|
gemGraphics.fillStyle(canAdd ? 0x5a4400 : 0x222200, 1);
|
||||||
|
gemGraphics.fillCircle(gemX, gemY, gemRadius);
|
||||||
|
gemGraphics.lineStyle(2, canAdd ? 0xffd700 : 0x554400, 1);
|
||||||
|
gemGraphics.strokeCircle(gemX, gemY, gemRadius);
|
||||||
|
} else {
|
||||||
|
gemGraphics.fillStyle(canAdd ? (gemFillColors[cost] || 0x333333) : 0x111111, 1);
|
||||||
|
gemGraphics.fillCircle(gemX, gemY, gemRadius);
|
||||||
|
gemGraphics.lineStyle(2, canAdd ? (gemStrokeColors[cost] || 0x666666) : 0x333333, 1);
|
||||||
|
gemGraphics.strokeCircle(gemX, gemY, gemRadius);
|
||||||
|
}
|
||||||
|
this.scrollContainer.add(gemGraphics);
|
||||||
|
|
||||||
|
const gemLabel = isCommander ? '★' : String(cost);
|
||||||
|
const gemLabelColor = isCommander
|
||||||
|
? (canAdd ? '#ffd700' : '#554400')
|
||||||
|
: (canAdd ? (gemTextColors[cost] || '#888888') : '#333333');
|
||||||
|
const gemTxt = this.add.text(gemX, gemY, gemLabel, {
|
||||||
|
fontSize: isCommander ? '16px' : '18px',
|
||||||
|
color: gemLabelColor,
|
||||||
|
fontFamily: 'Audiowide',
|
||||||
|
fontStyle: 'bold'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.scrollContainer.add(gemTxt);
|
||||||
|
|
||||||
|
// Copies indicator (non-alternating only)
|
||||||
|
if (!this.isAlternatingDraft && !isCommander) {
|
||||||
|
const maxCopies = MAX_COPIES[card.rarity] || 1;
|
||||||
|
const currentCopies = this.draftCopies[card.id] || 0;
|
||||||
|
if (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/Pick button
|
||||||
|
let btnLabel;
|
||||||
|
if (this.isAlternatingDraft) {
|
||||||
|
if (takenBy === 'player') btnLabel = 'YOURS';
|
||||||
|
else if (takenBy === 'ai') btnLabel = 'TAKEN';
|
||||||
|
else if (this.inputLocked) btnLabel = 'WAIT...';
|
||||||
|
else if (isCommander && this.draftState !== 'PLAYER_COMMANDER') btnLabel = 'NOT YET';
|
||||||
|
else if (!isCommander && this.draftState !== 'PLAYER_CARD') btnLabel = 'NOT YET';
|
||||||
|
else if (canAdd) btnLabel = '+ PICK';
|
||||||
|
else if (!isCommander && cost > this.remainingBudget) btnLabel = 'NO BUDGET';
|
||||||
|
else if (!isCommander && this.draftedCards.length >= 10) btnLabel = 'DECK FULL';
|
||||||
|
else btnLabel = '—';
|
||||||
|
} else {
|
||||||
|
const maxCopies = isCommander ? 1 : (MAX_COPIES[card.rarity] || 1);
|
||||||
|
const currentCopies = this.draftCopies[card.id] || 0;
|
||||||
|
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 currentCopiesForBtn = this.draftCopies[card.id] || 0;
|
||||||
|
const btnYPos = cy + this.CARD_H / 2 + ((!this.isAlternatingDraft && currentCopiesForBtn > 0) ? 32 : 18);
|
||||||
const btnBgColor = canAdd ? 0x225522 : 0x222222;
|
const btnBgColor = canAdd ? 0x225522 : 0x222222;
|
||||||
const btnStroke = canAdd ? 0x44aa44 : 0x333333;
|
const btnStroke = canAdd ? 0x44aa44 : 0x333333;
|
||||||
const btnTextColor = canAdd ? '#44ff44' : '#444444';
|
const btnTextColor = canAdd ? '#44ff44' : '#444444';
|
||||||
|
|
||||||
const btnBg = this.add.rectangle(cx, btnY, this.CARD_W - 10, this.BTN_H, btnBgColor)
|
const btnBg = this.add.rectangle(cx, btnYPos, this.CARD_W - 10, this.BTN_H, btnBgColor)
|
||||||
.setStrokeStyle(1, btnStroke);
|
.setStrokeStyle(1, btnStroke);
|
||||||
const btnTxt = this.add.text(cx, btnY, btnLabel, {
|
const btnTxt = this.add.text(cx, btnYPos, btnLabel, {
|
||||||
fontSize: '12px', color: btnTextColor, fontFamily: 'Audiowide'
|
fontSize: '12px', color: btnTextColor, fontFamily: 'Audiowide'
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.scrollContainer.add(btnBg);
|
this.scrollContainer.add(btnBg);
|
||||||
|
|
@ -259,16 +362,20 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
|
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
|
||||||
btnBg.on('pointerout', () => btnBg.setFillStyle(0x225522));
|
btnBg.on('pointerout', () => btnBg.setFillStyle(0x225522));
|
||||||
btnBg.on('pointerdown', () => {
|
btnBg.on('pointerdown', () => {
|
||||||
if (isCommander) {
|
if (this.isAlternatingDraft) {
|
||||||
this.draftedCommander = card.id;
|
this._playerPick(card);
|
||||||
} else {
|
} else {
|
||||||
this.draftedCards.push(card.id);
|
if (isCommander) {
|
||||||
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
this.draftedCommander = card.id;
|
||||||
this.remainingBudget -= cost;
|
} 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();
|
||||||
}
|
}
|
||||||
this.scrollY = Math.max(0, this.scrollY); // preserve scroll
|
|
||||||
this._renderCardGrid();
|
|
||||||
this._renderDeckPanel();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -463,8 +570,259 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dual deck panel (alternating draft) ───────────────────────────
|
||||||
|
|
||||||
|
_renderDualDeckPanel() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Fixed vertical zones so sections never overlap
|
||||||
|
const PLAYER_TOP = 70;
|
||||||
|
const DIVIDER_Y = height / 2 - 20;
|
||||||
|
const OPPONENT_TOP = DIVIDER_Y + 14;
|
||||||
|
const BUTTON_Y = height - 55;
|
||||||
|
const ENTRY_H = 26;
|
||||||
|
const barW = PANEL_W - 200;
|
||||||
|
const barX = PANEL_X + 180;
|
||||||
|
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
|
||||||
|
|
||||||
|
// Panel background
|
||||||
|
const panelBg = this.add.rectangle(panelCX, height / 2, PANEL_W, height - 20, 0x0a0f1a)
|
||||||
|
.setStrokeStyle(2, 0x334466);
|
||||||
|
this.deckPanelGroup.push(panelBg);
|
||||||
|
|
||||||
|
// ── YOUR DECK (top half) ──
|
||||||
|
let y = PLAYER_TOP;
|
||||||
|
const yourTitle = this.add.text(panelCX, y, 'YOUR DECK', {
|
||||||
|
fontSize: '18px', color: '#d4af37', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.deckPanelGroup.push(yourTitle);
|
||||||
|
|
||||||
|
// Player budget
|
||||||
|
y += 26;
|
||||||
|
const pBudgetPct = this.remainingBudget / this.totalBudget;
|
||||||
|
const pBudgetColor = pBudgetPct > 0.5 ? '#44ff44' : pBudgetPct > 0.2 ? '#ffaa00' : '#ff4444';
|
||||||
|
const pBudgetTxt = this.add.text(PANEL_X + 20, y, `Budget: ${this.remainingBudget}/${this.totalBudget}`, {
|
||||||
|
fontSize: '14px', color: pBudgetColor, fontFamily: 'Audiowide'
|
||||||
|
});
|
||||||
|
this.deckPanelGroup.push(pBudgetTxt);
|
||||||
|
|
||||||
|
const pBarBg = this.add.rectangle(barX + barW / 2, y + 8, barW, 10, 0x222222).setStrokeStyle(1, 0x444444);
|
||||||
|
const pFillW = Math.max(1, barW * pBudgetPct);
|
||||||
|
const pBarColor = pBudgetPct > 0.5 ? 0x44aa44 : pBudgetPct > 0.2 ? 0xaa8800 : 0xaa3333;
|
||||||
|
const pBarFill = this.add.rectangle(barX + pFillW / 2, y + 8, pFillW, 8, pBarColor);
|
||||||
|
this.deckPanelGroup.push(pBarBg, pBarFill);
|
||||||
|
|
||||||
|
// Player commander slot
|
||||||
|
y += 38;
|
||||||
|
const pCmd = this.draftedCommander ? this.cardManager.getCard(this.draftedCommander) : null;
|
||||||
|
const pCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, pCmd ? 0x1a3a5c : 0x1a1a2e)
|
||||||
|
.setStrokeStyle(2, 0xffd700);
|
||||||
|
const pCmdTxt = this.add.text(panelCX, y, pCmd ? `Commander: ${pCmd.name}` : 'No Commander', {
|
||||||
|
fontSize: '13px', color: pCmd ? '#ffd700' : '#555555', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.deckPanelGroup.push(pCmdBg, pCmdTxt);
|
||||||
|
|
||||||
|
// Player card list
|
||||||
|
y += 26;
|
||||||
|
const pCounts = {};
|
||||||
|
for (const id of this.draftedCards) pCounts[id] = (pCounts[id] || 0) + 1;
|
||||||
|
|
||||||
|
for (const [cardId, count] of Object.entries(pCounts)) {
|
||||||
|
if (y + ENTRY_H > DIVIDER_Y - 30) break; // don't overflow into divider
|
||||||
|
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: '12px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
const cTxt = this.add.text(PANEL_X + 300, y, `x${count}`, {
|
||||||
|
fontSize: '12px', color: '#aaaaaa', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
const costTxt = this.add.text(PANEL_X + 340, y, `(${cost * count} pts)`, {
|
||||||
|
fontSize: '10px', color: '#8888cc', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
this.deckPanelGroup.push(bg, txt, cTxt, costTxt);
|
||||||
|
y += ENTRY_H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player card count
|
||||||
|
const pFilled = this.draftedCards.length;
|
||||||
|
const pCountTxt = this.add.text(PANEL_X + 20, DIVIDER_Y - 22, `Cards: ${pFilled}/10`, {
|
||||||
|
fontSize: '13px', color: pFilled >= 3 ? '#aaaaff' : '#888888', fontFamily: 'Audiowide'
|
||||||
|
});
|
||||||
|
this.deckPanelGroup.push(pCountTxt);
|
||||||
|
|
||||||
|
// ── Divider ──
|
||||||
|
const divider = this.add.rectangle(panelCX, DIVIDER_Y, PANEL_W - 30, 2, 0x334466);
|
||||||
|
this.deckPanelGroup.push(divider);
|
||||||
|
|
||||||
|
// ── OPPONENT DECK (bottom half) ──
|
||||||
|
y = OPPONENT_TOP;
|
||||||
|
const oppTitle = this.add.text(panelCX, y, 'OPPONENT DECK', {
|
||||||
|
fontSize: '18px', color: '#ff6644', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.deckPanelGroup.push(oppTitle);
|
||||||
|
|
||||||
|
// AI budget
|
||||||
|
y += 26;
|
||||||
|
const aBudgetPct = this.aiBudget / this.totalBudget;
|
||||||
|
const aBudgetColor = aBudgetPct > 0.5 ? '#44ff44' : aBudgetPct > 0.2 ? '#ffaa00' : '#ff4444';
|
||||||
|
const aBudgetTxt = this.add.text(PANEL_X + 20, y, `Budget: ${this.aiBudget}/${this.totalBudget}`, {
|
||||||
|
fontSize: '14px', color: aBudgetColor, fontFamily: 'Audiowide'
|
||||||
|
});
|
||||||
|
this.deckPanelGroup.push(aBudgetTxt);
|
||||||
|
|
||||||
|
const aBarBg = this.add.rectangle(barX + barW / 2, y + 8, barW, 10, 0x222222).setStrokeStyle(1, 0x444444);
|
||||||
|
const aFillW = Math.max(1, barW * aBudgetPct);
|
||||||
|
const aBarColor = aBudgetPct > 0.5 ? 0x44aa44 : aBudgetPct > 0.2 ? 0xaa8800 : 0xaa3333;
|
||||||
|
const aBarFill = this.add.rectangle(barX + aFillW / 2, y + 8, aFillW, 8, aBarColor);
|
||||||
|
this.deckPanelGroup.push(aBarBg, aBarFill);
|
||||||
|
|
||||||
|
// AI commander slot
|
||||||
|
y += 38;
|
||||||
|
const aCmd = this.aiCommander ? this.cardManager.getCard(this.aiCommander) : null;
|
||||||
|
const aCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, aCmd ? 0x3a1a1a : 0x1a1a2e)
|
||||||
|
.setStrokeStyle(2, aCmd ? 0xff6644 : 0x444444);
|
||||||
|
const aCmdTxt = this.add.text(panelCX, y, aCmd ? `Commander: ${aCmd.name}` : 'No Commander', {
|
||||||
|
fontSize: '13px', color: aCmd ? '#ff6644' : '#555555', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.deckPanelGroup.push(aCmdBg, aCmdTxt);
|
||||||
|
|
||||||
|
// AI card list
|
||||||
|
y += 26;
|
||||||
|
const aCounts = {};
|
||||||
|
for (const id of this.aiCards) aCounts[id] = (aCounts[id] || 0) + 1;
|
||||||
|
const maxAiListY = BUTTON_Y - 50;
|
||||||
|
|
||||||
|
for (const [cardId, count] of Object.entries(aCounts)) {
|
||||||
|
if (y + ENTRY_H > maxAiListY) break; // don't overflow into button area
|
||||||
|
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, 0x3a1a1a)
|
||||||
|
.setStrokeStyle(1, rarityColors[card.rarity] || 0x663333);
|
||||||
|
const txt = this.add.text(PANEL_X + 30, y, card.name, {
|
||||||
|
fontSize: '12px', color: '#ffcccc', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
const cTxt = this.add.text(PANEL_X + 300, y, `x${count}`, {
|
||||||
|
fontSize: '12px', color: '#aa8888', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
const costTxt = this.add.text(PANEL_X + 340, y, `(${cost * count} pts)`, {
|
||||||
|
fontSize: '10px', color: '#cc8888', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0, 0.5);
|
||||||
|
this.deckPanelGroup.push(bg, txt, cTxt, costTxt);
|
||||||
|
y += ENTRY_H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI card count
|
||||||
|
const aFilled = this.aiCards.length;
|
||||||
|
const aCountTxt = this.add.text(PANEL_X + 20, maxAiListY + 4, `Cards: ${aFilled}/10`, {
|
||||||
|
fontSize: '13px', color: aFilled >= 3 ? '#ffaaaa' : '#888888', fontFamily: 'Audiowide'
|
||||||
|
});
|
||||||
|
this.deckPanelGroup.push(aCountTxt);
|
||||||
|
|
||||||
|
// ── Validation & Start Battle (fixed at bottom) ──
|
||||||
|
const isValid = this.draftedCommander && pFilled >= 3;
|
||||||
|
if (isValid && !this.inputLocked) {
|
||||||
|
const startBg = this.add.rectangle(panelCX, BUTTON_Y, 240, 50, 0x226622)
|
||||||
|
.setInteractive({ useHandCursor: true })
|
||||||
|
.setStrokeStyle(2, 0x44aa44);
|
||||||
|
const startTxt = this.add.text(panelCX, BUTTON_Y, '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._aiFinishDraft();
|
||||||
|
this._startBattle();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.deckPanelGroup.push(startBg, startTxt);
|
||||||
|
} else if (!isValid) {
|
||||||
|
const validMsg = !this.draftedCommander
|
||||||
|
? 'Select a commander first'
|
||||||
|
: `Need at least ${3 - pFilled} more card(s)`;
|
||||||
|
const validTxt = this.add.text(panelCX, BUTTON_Y, validMsg, {
|
||||||
|
fontSize: '14px', color: '#ff6644', fontFamily: 'Audiowide'
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
this.deckPanelGroup.push(validTxt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_aiFinishDraft() {
|
||||||
|
// If AI doesn't have a commander yet, pick one
|
||||||
|
if (!this.aiCommander) {
|
||||||
|
this._aiPickCommander();
|
||||||
|
}
|
||||||
|
// Fill remaining AI cards without delay
|
||||||
|
let safety = 20;
|
||||||
|
while (this.aiCards.length < 10 && this._canSidePick('ai') && safety-- > 0) {
|
||||||
|
if (!this._aiPickOneCard()) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status banner (alternating draft) ────────────────────────────
|
||||||
|
|
||||||
|
_renderStatusBanner() {
|
||||||
|
if (this.bannerGroup) {
|
||||||
|
for (const o of this.bannerGroup) { if (o.scene) o.destroy(); }
|
||||||
|
}
|
||||||
|
this.bannerGroup = [];
|
||||||
|
|
||||||
|
const { width } = this.scale;
|
||||||
|
const messages = {
|
||||||
|
PLAYER_COMMANDER: { text: 'Choose Your Commander', color: '#44aaff', bgColor: 0x112233 },
|
||||||
|
AI_COMMANDER: { text: 'Opponent Choosing Commander...', color: '#ff6644', bgColor: 0x331111 },
|
||||||
|
PLAYER_CARD: { text: 'Your Turn \u2014 Pick a Card', color: '#44aaff', bgColor: 0x112233 },
|
||||||
|
AI_CARD: { text: 'Opponent Picking...', color: '#ff6644', bgColor: 0x331111 },
|
||||||
|
DRAFT_COMPLETE: { text: 'Draft Complete!', color: '#44ff44', bgColor: 0x113311 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = messages[this.draftState] || messages.PLAYER_COMMANDER;
|
||||||
|
|
||||||
|
const bg = this.add.rectangle(width / 2, 30, width, 50, msg.bgColor).setDepth(10);
|
||||||
|
const txt = this.add.text(width / 2, 30, msg.text, {
|
||||||
|
fontSize: '28px', color: msg.color, fontFamily: 'RaiderCrusader'
|
||||||
|
}).setOrigin(0.5).setDepth(11);
|
||||||
|
|
||||||
|
this.bannerGroup = [bg, txt];
|
||||||
|
|
||||||
|
// Pulse effect during AI turns
|
||||||
|
if (this.draftState === 'AI_COMMANDER' || this.draftState === 'AI_CARD') {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: txt,
|
||||||
|
alpha: { from: 1, to: 0.4 },
|
||||||
|
duration: 600,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── AI Draft Algorithm ───────────────────────────────────────────────
|
// ── AI Draft Algorithm ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
_getRarityTargets() {
|
||||||
|
const targets = {
|
||||||
|
Easy: { legendary: 0, epic: 0, rare: 2, common: 8 },
|
||||||
|
Medium: { legendary: 1, epic: 2, rare: 4, common: 3 },
|
||||||
|
Hard: { legendary: 2, epic: 4, rare: 4, common: 0 },
|
||||||
|
Legendary: { legendary: 4, epic: 6, rare: 0, common: 0 }
|
||||||
|
};
|
||||||
|
return targets[this.selectedDifficulty] || targets.Medium;
|
||||||
|
}
|
||||||
|
|
||||||
_aiDraft() {
|
_aiDraft() {
|
||||||
let budget = this.totalBudget;
|
let budget = this.totalBudget;
|
||||||
const taken = new Set();
|
const taken = new Set();
|
||||||
|
|
@ -478,70 +836,242 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const cmdPool = availCmds.length > 0 ? availCmds : this.pool.commanders;
|
const cmdPool = availCmds.length > 0 ? availCmds : this.pool.commanders;
|
||||||
const commander = cmdPool[Math.floor(Math.random() * cmdPool.length)];
|
const commander = cmdPool[Math.floor(Math.random() * cmdPool.length)];
|
||||||
const preferredFaction = commander.faction;
|
const preferredFaction = commander.faction;
|
||||||
|
taken.add(commander.id);
|
||||||
|
|
||||||
// Gather all available assault cards from pool
|
const getAvailable = () => [
|
||||||
const allAssault = [
|
...this.pool.legendary, ...this.pool.epic,
|
||||||
...this.pool.legendary,
|
...this.pool.rare, ...this.pool.common
|
||||||
...this.pool.epic,
|
|
||||||
...this.pool.rare,
|
|
||||||
...this.pool.common
|
|
||||||
].filter(c => !taken.has(c.id));
|
].filter(c => !taken.has(c.id));
|
||||||
|
|
||||||
// Score each card
|
const targets = this._getRarityTargets();
|
||||||
const scored = allAssault.map(card => {
|
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
||||||
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 aiCards = [];
|
||||||
const aiCopies = {};
|
|
||||||
|
|
||||||
for (const entry of scored) {
|
// Fill each rarity tier in order, highest first
|
||||||
if (aiCards.length >= 10) break;
|
for (const rarity of rarityOrder) {
|
||||||
if (entry.cost > budget) continue;
|
const target = targets[rarity] || 0;
|
||||||
|
if (target <= 0) continue;
|
||||||
|
|
||||||
const maxC = MAX_COPIES[entry.card.rarity] || 1;
|
const cost = COSTS[rarity] || 1;
|
||||||
const current = aiCopies[entry.card.id] || 0;
|
const candidates = getAvailable()
|
||||||
if (current >= maxC) continue;
|
.filter(c => c.rarity === rarity && cost <= budget)
|
||||||
|
.map(c => this._scoreCard(c, preferredFaction))
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
aiCards.push(entry.card.id);
|
let filled = 0;
|
||||||
aiCopies[entry.card.id] = current + 1;
|
for (const entry of candidates) {
|
||||||
budget -= entry.cost;
|
if (filled >= target || aiCards.length >= 10) break;
|
||||||
|
if (entry.cost > budget) continue;
|
||||||
|
aiCards.push(entry.card.id);
|
||||||
|
taken.add(entry.card.id);
|
||||||
|
budget -= entry.cost;
|
||||||
|
filled++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-pass: try to fill remaining slots with cheaper cards
|
// Fallback: fill remaining slots if under minimum, cheapest first
|
||||||
if (aiCards.length < 3 && budget > 0) {
|
if (aiCards.length < 3) {
|
||||||
const cheapest = scored
|
const fallback = getAvailable()
|
||||||
.filter(e => e.cost <= budget && (aiCopies[e.card.id] || 0) < (MAX_COPIES[e.card.rarity] || 1))
|
.filter(c => (COSTS[c.rarity] || 1) <= budget)
|
||||||
|
.map(c => this._scoreCard(c, preferredFaction))
|
||||||
.sort((a, b) => a.cost - b.cost);
|
.sort((a, b) => a.cost - b.cost);
|
||||||
for (const entry of cheapest) {
|
for (const entry of fallback) {
|
||||||
if (aiCards.length >= 10) break;
|
if (aiCards.length >= 10 || entry.cost > budget) break;
|
||||||
if (entry.cost > budget) break;
|
|
||||||
aiCards.push(entry.card.id);
|
aiCards.push(entry.card.id);
|
||||||
aiCopies[entry.card.id] = (aiCopies[entry.card.id] || 0) + 1;
|
taken.add(entry.card.id);
|
||||||
budget -= entry.cost;
|
budget -= entry.cost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { commander: commander.id, cards: aiCards };
|
||||||
commander: commander.id,
|
}
|
||||||
cards: aiCards
|
|
||||||
};
|
// ── Alternating Draft: State Machine ──────────────────────────────
|
||||||
|
|
||||||
|
_playerPick(card) {
|
||||||
|
if (this.inputLocked) return;
|
||||||
|
|
||||||
|
const isCommander = card.type === 'commander';
|
||||||
|
if (isCommander) {
|
||||||
|
this.draftedCommander = card.id;
|
||||||
|
} else {
|
||||||
|
const cost = COSTS[card.rarity] || 1;
|
||||||
|
this.draftedCards.push(card.id);
|
||||||
|
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
||||||
|
this.remainingBudget -= cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.takenCards[card.id] = 'player';
|
||||||
|
this.sound.play('sfx_menu_select', { volume: 0.5 });
|
||||||
|
this._advanceDraftState();
|
||||||
|
}
|
||||||
|
|
||||||
|
_canSidePick(side) {
|
||||||
|
const budget = side === 'player' ? this.remainingBudget : this.aiBudget;
|
||||||
|
const cardCount = side === 'player' ? this.draftedCards.length : this.aiCards.length;
|
||||||
|
if (cardCount >= 10) return false;
|
||||||
|
|
||||||
|
// Check if any affordable assault card remains untaken
|
||||||
|
const allAssault = [
|
||||||
|
...this.pool.legendary, ...this.pool.epic,
|
||||||
|
...this.pool.rare, ...this.pool.common
|
||||||
|
];
|
||||||
|
return allAssault.some(c => !this.takenCards[c.id] && (COSTS[c.rarity] || 1) <= budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
_advanceDraftState() {
|
||||||
|
const playerCan = this._canSidePick('player');
|
||||||
|
const aiCan = this._canSidePick('ai');
|
||||||
|
let aiTurn = false;
|
||||||
|
|
||||||
|
if (this.draftState === 'PLAYER_COMMANDER') {
|
||||||
|
this.draftState = 'AI_COMMANDER';
|
||||||
|
aiTurn = true;
|
||||||
|
} else if (this.draftState === 'AI_COMMANDER') {
|
||||||
|
if (playerCan) {
|
||||||
|
this.draftState = 'PLAYER_CARD';
|
||||||
|
this.inputLocked = false;
|
||||||
|
} else if (aiCan) {
|
||||||
|
this.draftState = 'AI_CARD';
|
||||||
|
aiTurn = true;
|
||||||
|
} else {
|
||||||
|
this.draftState = 'DRAFT_COMPLETE';
|
||||||
|
this.inputLocked = false;
|
||||||
|
}
|
||||||
|
} else if (this.draftState === 'PLAYER_CARD') {
|
||||||
|
if (aiCan) {
|
||||||
|
this.draftState = 'AI_CARD';
|
||||||
|
aiTurn = true;
|
||||||
|
} else if (playerCan) {
|
||||||
|
// AI is done, player keeps picking
|
||||||
|
this.draftState = 'PLAYER_CARD';
|
||||||
|
this.inputLocked = false;
|
||||||
|
} else {
|
||||||
|
this.draftState = 'DRAFT_COMPLETE';
|
||||||
|
this.inputLocked = false;
|
||||||
|
}
|
||||||
|
} else if (this.draftState === 'AI_CARD') {
|
||||||
|
if (playerCan) {
|
||||||
|
this.draftState = 'PLAYER_CARD';
|
||||||
|
this.inputLocked = false;
|
||||||
|
} else if (aiCan) {
|
||||||
|
// Player is done, AI keeps picking
|
||||||
|
this.draftState = 'AI_CARD';
|
||||||
|
aiTurn = true;
|
||||||
|
} else {
|
||||||
|
this.draftState = 'DRAFT_COMPLETE';
|
||||||
|
this.inputLocked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aiTurn) {
|
||||||
|
this._scheduleAiTurn();
|
||||||
|
} else {
|
||||||
|
this._renderStatusBanner();
|
||||||
|
this._renderCardGrid();
|
||||||
|
this._renderDualDeckPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleAiTurn() {
|
||||||
|
this.inputLocked = true;
|
||||||
|
this._renderStatusBanner();
|
||||||
|
this._renderCardGrid();
|
||||||
|
this._renderDualDeckPanel();
|
||||||
|
|
||||||
|
this.time.delayedCall(1500, () => {
|
||||||
|
if (!this.scene.isActive()) return;
|
||||||
|
|
||||||
|
if (this.draftState === 'AI_COMMANDER') {
|
||||||
|
this._aiPickCommander();
|
||||||
|
} else {
|
||||||
|
this._aiPickOneCard();
|
||||||
|
}
|
||||||
|
this._advanceDraftState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_aiPickCommander() {
|
||||||
|
const available = this.pool.commanders.filter(c => !this.takenCards[c.id]);
|
||||||
|
const pool = available.length > 0 ? available : this.pool.commanders;
|
||||||
|
const commander = pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
this.aiCommander = commander.id;
|
||||||
|
this.takenCards[commander.id] = 'ai';
|
||||||
|
}
|
||||||
|
|
||||||
|
_aiPickOneCard() {
|
||||||
|
const available = [
|
||||||
|
...this.pool.legendary, ...this.pool.epic,
|
||||||
|
...this.pool.rare, ...this.pool.common
|
||||||
|
].filter(c => !this.takenCards[c.id]);
|
||||||
|
|
||||||
|
const aiCmd = this.cardManager.getCard(this.aiCommander);
|
||||||
|
const preferredFaction = aiCmd ? aiCmd.faction : null;
|
||||||
|
|
||||||
|
// Count what the AI has already picked by rarity
|
||||||
|
const rarityCounts = {};
|
||||||
|
for (const id of this.aiCards) {
|
||||||
|
const c = this.cardManager.getCard(id);
|
||||||
|
if (c) rarityCounts[c.rarity] = (rarityCounts[c.rarity] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = this._getRarityTargets();
|
||||||
|
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
||||||
|
|
||||||
|
// Try to pick the highest-priority rarity whose target isn't met yet
|
||||||
|
for (const rarity of rarityOrder) {
|
||||||
|
const target = targets[rarity] || 0;
|
||||||
|
if (target <= 0 || (rarityCounts[rarity] || 0) >= target) continue;
|
||||||
|
|
||||||
|
const candidates = available
|
||||||
|
.filter(c => c.rarity === rarity && (COSTS[rarity] || 1) <= this.aiBudget)
|
||||||
|
.map(c => this._scoreCard(c, preferredFaction))
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
const pick = candidates[0];
|
||||||
|
this.aiCards.push(pick.card.id);
|
||||||
|
this.aiCopies[pick.card.id] = (this.aiCopies[pick.card.id] || 0) + 1;
|
||||||
|
this.aiBudget -= pick.cost;
|
||||||
|
this.takenCards[pick.card.id] = 'ai';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: all targets met or unavailable — pick best affordable in rarity order
|
||||||
|
for (const rarity of rarityOrder) {
|
||||||
|
const candidates = available
|
||||||
|
.filter(c => c.rarity === rarity && (COSTS[rarity] || 1) <= this.aiBudget)
|
||||||
|
.map(c => this._scoreCard(c, preferredFaction))
|
||||||
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
const pick = candidates[0];
|
||||||
|
this.aiCards.push(pick.card.id);
|
||||||
|
this.aiCopies[pick.card.id] = (this.aiCopies[pick.card.id] || 0) + 1;
|
||||||
|
this.aiBudget -= pick.cost;
|
||||||
|
this.takenCards[pick.card.id] = 'ai';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scoreCard(card, preferredFaction) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Start Battle ─────────────────────────────────────────────────────
|
// ── Start Battle ─────────────────────────────────────────────────────
|
||||||
|
|
@ -563,7 +1093,12 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Build opponent deck
|
// Build opponent deck
|
||||||
let opponentDeck;
|
let opponentDeck;
|
||||||
if (this.opponentChoice === 'draft') {
|
if (this.isAlternatingDraft) {
|
||||||
|
opponentDeck = {
|
||||||
|
commander: this.aiCommander,
|
||||||
|
cards: [...this.aiCards]
|
||||||
|
};
|
||||||
|
} else if (this.opponentChoice === 'draft') {
|
||||||
opponentDeck = this._aiDraft();
|
opponentDeck = this._aiDraft();
|
||||||
} else {
|
} else {
|
||||||
opponentDeck = {
|
opponentDeck = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue