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": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 3,
|
||||
"value": 4,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -40,7 +40,8 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 5,
|
||||
"value": 4,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -53,7 +54,8 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 7,
|
||||
"value": 6,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -66,8 +68,14 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 10,
|
||||
"value": 8,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "counter",
|
||||
"value": 10,
|
||||
"trigger": "on_defend"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -89,9 +97,8 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "fortify",
|
||||
"value": 1,
|
||||
"all": true,
|
||||
"name": "heal",
|
||||
"value": 2,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -103,9 +110,8 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "fortify",
|
||||
"value": 2,
|
||||
"all": true,
|
||||
"name": "heal",
|
||||
"value": 5,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -117,7 +123,7 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "fortify",
|
||||
"name": "heal",
|
||||
"value": 4,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
|
|
@ -131,8 +137,8 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "fortify",
|
||||
"value": 6,
|
||||
"name": "heal",
|
||||
"value": 8,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
|
|
@ -144,9 +150,15 @@
|
|||
"armor": 20,
|
||||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "heal",
|
||||
"value": 10,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "fortify",
|
||||
"value": 8,
|
||||
"value": 5,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
|
|
@ -171,8 +183,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 1,
|
||||
"all": true,
|
||||
"value": 3,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
|
|
@ -185,8 +196,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 4,
|
||||
"all": true,
|
||||
"value": 6,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
|
|
@ -230,6 +240,12 @@
|
|||
"value": 23,
|
||||
"all": true,
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 8,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -250,12 +266,6 @@
|
|||
"armor": 3,
|
||||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 1,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "enfeeble",
|
||||
"value": 1,
|
||||
|
|
@ -270,12 +280,6 @@
|
|||
"armor": 5,
|
||||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 2,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "enfeeble",
|
||||
"value": 2,
|
||||
|
|
@ -291,10 +295,10 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 3,
|
||||
"name": "cleanse",
|
||||
"value": 6,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "postBattle"
|
||||
},
|
||||
{
|
||||
"name": "enfeeble",
|
||||
|
|
@ -311,10 +315,10 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 5,
|
||||
"name": "cleanse",
|
||||
"value": 9,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "postBattle"
|
||||
},
|
||||
{
|
||||
"name": "enfeeble",
|
||||
|
|
@ -331,16 +335,21 @@
|
|||
"delay": 0,
|
||||
"skills": [
|
||||
{
|
||||
"name": "rally",
|
||||
"value": 7,
|
||||
"name": "cleanse",
|
||||
"value": 13,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "postBattle"
|
||||
},
|
||||
{
|
||||
"name": "enfeeble",
|
||||
"value": 7,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "counter",
|
||||
"value": 10,
|
||||
"trigger": "on_defend"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2046,7 +2055,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "siege",
|
||||
"value": 3,
|
||||
"value": 4,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -2059,7 +2068,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "siege",
|
||||
"value": 5,
|
||||
"value": 10,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -2072,7 +2081,13 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "siege",
|
||||
"value": 20,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "weaken",
|
||||
"value": 7,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -2085,7 +2100,13 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "siege",
|
||||
"value": 20,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "weaken",
|
||||
"value": 10,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -2098,7 +2119,13 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "siege",
|
||||
"value": 25,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "weaken",
|
||||
"value": 15,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
}
|
||||
]
|
||||
|
|
@ -2125,11 +2152,6 @@
|
|||
"value": 1,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 2,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2141,14 +2163,9 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "weaken",
|
||||
"value": 3,
|
||||
"value": 4,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 4,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2166,7 +2183,7 @@
|
|||
},
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 6,
|
||||
"value": 15,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
|
|
@ -2185,7 +2202,7 @@
|
|||
},
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 9,
|
||||
"value": 20,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
|
|
@ -2204,7 +2221,7 @@
|
|||
},
|
||||
{
|
||||
"name": "strike",
|
||||
"value": 13,
|
||||
"value": 25,
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
|
|
@ -2231,11 +2248,6 @@
|
|||
"value": 1,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "wound",
|
||||
"value": 2,
|
||||
"trigger": "on_attack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2250,11 +2262,6 @@
|
|||
"value": 5,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
{
|
||||
"name": "wound",
|
||||
"value": 4,
|
||||
"trigger": "on_attack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -2304,7 +2311,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "enfeeble",
|
||||
"value": 15,
|
||||
"value": 13,
|
||||
"all": true,
|
||||
"trigger": "preBattle"
|
||||
},
|
||||
|
|
@ -7234,7 +7241,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "venom",
|
||||
"value": 8,
|
||||
"value": 13,
|
||||
"trigger": "on_attack",
|
||||
"all": true
|
||||
}
|
||||
|
|
@ -7248,7 +7255,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "venom",
|
||||
"value": 11,
|
||||
"value": 16,
|
||||
"trigger": "on_attack",
|
||||
"all": true
|
||||
}
|
||||
|
|
@ -7262,7 +7269,7 @@
|
|||
"skills": [
|
||||
{
|
||||
"name": "venom",
|
||||
"value": 16,
|
||||
"value": 20,
|
||||
"trigger": "on_attack",
|
||||
"all": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
|
||||
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;
|
||||
|
||||
|
|
@ -36,7 +32,10 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
// Track copies in draft: { cardId: count }
|
||||
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();
|
||||
|
||||
// Deck panel elements (right side, rebuilt on changes)
|
||||
|
|
@ -64,10 +63,35 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
const playerNeedsDraft = this.playerChoice === 'draft';
|
||||
|
||||
if (playerNeedsDraft) {
|
||||
this._renderCardGrid();
|
||||
this._renderDeckPanel();
|
||||
this._setupScrolling();
|
||||
if (this.isAlternatingDraft) {
|
||||
// Alternating draft state
|
||||
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 {
|
||||
this.add.text(width / 2, 30, 'Draft Cards', {
|
||||
fontSize: '36px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Player has premade deck — show simplified view
|
||||
this._renderPremadeView();
|
||||
}
|
||||
|
|
@ -86,9 +110,10 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
||||
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
||||
|
||||
const fraction = this.isAlternatingDraft ? 2 : 3;
|
||||
const pick = (arr) => {
|
||||
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 {
|
||||
|
|
@ -202,53 +227,131 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
|
||||
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;
|
||||
|
||||
// ── Determine if card can be added ──
|
||||
let canAdd = false;
|
||||
if (isCommander) {
|
||||
canAdd = !this.draftedCommander;
|
||||
let takenBy = null; // 'player' | 'ai' | null
|
||||
|
||||
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 {
|
||||
canAdd = cost <= this.remainingBudget &&
|
||||
currentCopies < maxCopies &&
|
||||
this.draftedCards.length < 10;
|
||||
const maxCopies = isCommander ? 1 : (MAX_COPIES[card.rarity] || 1);
|
||||
const currentCopies = this.draftCopies[card.id] || 0;
|
||||
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);
|
||||
if (!canAdd) cardObj.setAlpha(takenBy ? 0.3 : 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);
|
||||
// Taken overlay (alternating draft only)
|
||||
if (this.isAlternatingDraft && takenBy) {
|
||||
const overlayColor = takenBy === 'player' ? 0x225522 : 0x552222;
|
||||
const overlay = this.add.rectangle(cx, cy, this.CARD_W, this.CARD_H, overlayColor, 0.5);
|
||||
this.scrollContainer.add(overlay);
|
||||
|
||||
// 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);
|
||||
const labelText = takenBy === 'player' ? 'YOURS' : 'TAKEN';
|
||||
const labelColor = takenBy === 'player' ? '#44ff44' : '#ff4444';
|
||||
const overlayTxt = this.add.text(cx, cy, labelText, {
|
||||
fontSize: '20px', color: labelColor, fontFamily: 'Audiowide', fontStyle: 'bold'
|
||||
}).setOrigin(0.5);
|
||||
this.scrollContainer.add(overlayTxt);
|
||||
}
|
||||
|
||||
// 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')));
|
||||
// Cost gem badge (top-left corner of card)
|
||||
const gemX = cx - this.CARD_W / 2 + 18;
|
||||
const gemY = cy - this.CARD_H / 2 + 18;
|
||||
const gemRadius = 16;
|
||||
|
||||
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 btnStroke = canAdd ? 0x44aa44 : 0x333333;
|
||||
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);
|
||||
const btnTxt = this.add.text(cx, btnY, btnLabel, {
|
||||
const btnTxt = this.add.text(cx, btnYPos, btnLabel, {
|
||||
fontSize: '12px', color: btnTextColor, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
this.scrollContainer.add(btnBg);
|
||||
|
|
@ -259,16 +362,20 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
|
||||
btnBg.on('pointerout', () => btnBg.setFillStyle(0x225522));
|
||||
btnBg.on('pointerdown', () => {
|
||||
if (isCommander) {
|
||||
this.draftedCommander = card.id;
|
||||
if (this.isAlternatingDraft) {
|
||||
this._playerPick(card);
|
||||
} else {
|
||||
this.draftedCards.push(card.id);
|
||||
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
||||
this.remainingBudget -= cost;
|
||||
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();
|
||||
}
|
||||
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 ───────────────────────────────────────────────
|
||||
|
||||
_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() {
|
||||
let budget = this.totalBudget;
|
||||
const taken = new Set();
|
||||
|
|
@ -478,70 +836,242 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
const cmdPool = availCmds.length > 0 ? availCmds : this.pool.commanders;
|
||||
const commander = cmdPool[Math.floor(Math.random() * cmdPool.length)];
|
||||
const preferredFaction = commander.faction;
|
||||
taken.add(commander.id);
|
||||
|
||||
// Gather all available assault cards from pool
|
||||
const allAssault = [
|
||||
...this.pool.legendary,
|
||||
...this.pool.epic,
|
||||
...this.pool.rare,
|
||||
...this.pool.common
|
||||
const getAvailable = () => [
|
||||
...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 targets = this._getRarityTargets();
|
||||
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
||||
const aiCards = [];
|
||||
const aiCopies = {};
|
||||
|
||||
for (const entry of scored) {
|
||||
if (aiCards.length >= 10) break;
|
||||
if (entry.cost > budget) continue;
|
||||
// Fill each rarity tier in order, highest first
|
||||
for (const rarity of rarityOrder) {
|
||||
const target = targets[rarity] || 0;
|
||||
if (target <= 0) continue;
|
||||
|
||||
const maxC = MAX_COPIES[entry.card.rarity] || 1;
|
||||
const current = aiCopies[entry.card.id] || 0;
|
||||
if (current >= maxC) continue;
|
||||
const cost = COSTS[rarity] || 1;
|
||||
const candidates = getAvailable()
|
||||
.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);
|
||||
aiCopies[entry.card.id] = current + 1;
|
||||
budget -= entry.cost;
|
||||
let filled = 0;
|
||||
for (const entry of candidates) {
|
||||
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
|
||||
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))
|
||||
// Fallback: fill remaining slots if under minimum, cheapest first
|
||||
if (aiCards.length < 3) {
|
||||
const fallback = getAvailable()
|
||||
.filter(c => (COSTS[c.rarity] || 1) <= budget)
|
||||
.map(c => this._scoreCard(c, preferredFaction))
|
||||
.sort((a, b) => a.cost - b.cost);
|
||||
for (const entry of cheapest) {
|
||||
if (aiCards.length >= 10) break;
|
||||
if (entry.cost > budget) break;
|
||||
for (const entry of fallback) {
|
||||
if (aiCards.length >= 10 || entry.cost > budget) break;
|
||||
aiCards.push(entry.card.id);
|
||||
aiCopies[entry.card.id] = (aiCopies[entry.card.id] || 0) + 1;
|
||||
taken.add(entry.card.id);
|
||||
budget -= entry.cost;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
commander: commander.id,
|
||||
cards: aiCards
|
||||
};
|
||||
return { 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 ─────────────────────────────────────────────────────
|
||||
|
|
@ -563,7 +1093,12 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
|||
|
||||
// Build opponent deck
|
||||
let opponentDeck;
|
||||
if (this.opponentChoice === 'draft') {
|
||||
if (this.isAlternatingDraft) {
|
||||
opponentDeck = {
|
||||
commander: this.aiCommander,
|
||||
cards: [...this.aiCards]
|
||||
};
|
||||
} else if (this.opponentChoice === 'draft') {
|
||||
opponentDeck = this._aiDraft();
|
||||
} else {
|
||||
opponentDeck = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue