feat(draft): implement alternating rarity-phase draft system

- Replace budget-based drafting with structured rarity-phase progression (legendary→epic→rare→common)
- Add phase tracking (`currentPhaseRarity`, `playerRemainingPicks`, `aiRemainingPicks`)
- Update card pool generation to respect target counts per rarity
- Refactor UI to show per-rarity pick progress instead of budget bars
- Adjust AI draft logic to prioritize current phase rarity and fallback safely
- Rename "Difficulty" selector to "Deck Strength" with clearer labels (Beginner/Medium/Advanced/Legendary)
This commit is contained in:
Brian Fertig 2026-04-03 16:38:11 -06:00
parent fd78f75c83
commit d4d9b25c72
2 changed files with 272 additions and 162 deletions

View File

@ -70,9 +70,12 @@ export class SkirmishDraftScene extends Phaser.Scene {
this.aiCommander = null;
this.aiCards = [];
this.aiCopies = {};
this.aiBudget = this.totalBudget;
this.takenCards = {}; // { cardId: 'player' | 'ai' }
this.currentPhaseRarity = null;
this.playerRemainingPicks = { legendary: 0, epic: 0, rare: 0, common: 0 };
this.aiRemainingPicks = { legendary: 0, epic: 0, rare: 0, common: 0 };
this.bannerGroup = [];
this.SCROLL_TOP = 80;
this._renderStatusBanner();
this._renderCardGrid();
@ -110,10 +113,25 @@ 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;
if (this.isAlternatingDraft) {
const targets = this._getRarityTargets();
const pick = (arr, minCount) => {
const shuffled = [...arr].sort(() => Math.random() - 0.5);
const n = Math.min(arr.length, Math.max(Math.ceil(arr.length / 2), minCount));
return shuffled.slice(0, n);
};
return {
commanders: pick(commanders, 4),
legendary: pick(legAssault, targets.legendary * 2 + 2),
epic: pick(epicAssault, targets.epic * 2 + 2),
rare: pick(rareAssault, targets.rare * 2 + 2),
common: pick(commonAssault, targets.common * 2 + 2)
};
}
const pick = (arr) => {
const shuffled = [...arr].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.ceil(arr.length / fraction));
return shuffled.slice(0, Math.ceil(arr.length / 3));
};
return {
@ -214,6 +232,11 @@ export class SkirmishDraftScene extends Phaser.Scene {
const sectionLabel = this.add.text(this.GRID_LEFT, cursorY, section.label, {
fontSize: '18px', color: rarityColors[section.label] || '#d4af37', fontFamily: 'Audiowide'
});
if (this.isAlternatingDraft &&
(this.draftState === 'PLAYER_CARD' || this.draftState === 'AI_CARD') &&
section.costKey !== this.currentPhaseRarity) {
sectionLabel.setAlpha(0.25);
}
this.scrollContainer.add(sectionLabel);
cursorY += 30;
@ -240,7 +263,7 @@ export class SkirmishDraftScene extends Phaser.Scene {
canAdd = this.draftState === 'PLAYER_COMMANDER' && !this.draftedCommander;
} else {
canAdd = this.draftState === 'PLAYER_CARD' &&
cost <= this.remainingBudget &&
card.rarity === this.currentPhaseRarity &&
this.draftedCards.length < 10;
}
} else {
@ -258,7 +281,12 @@ export class SkirmishDraftScene extends Phaser.Scene {
// 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(takenBy ? 0.3 : 0.4);
if (!canAdd) {
const inCardPhase = this.isAlternatingDraft &&
(this.draftState === 'PLAYER_CARD' || this.draftState === 'AI_CARD');
const isWrongPhase = inCardPhase && (isCommander || card.rarity !== this.currentPhaseRarity);
cardObj.setAlpha(takenBy ? 0.3 : (isWrongPhase ? 0.15 : 0.4));
}
this.scrollContainer.add(cardObj);
// Taken overlay (alternating draft only)
@ -331,8 +359,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
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 (!isCommander && card.rarity !== this.currentPhaseRarity) 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 {
@ -587,8 +615,6 @@ export class SkirmishDraftScene extends Phaser.Scene {
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
@ -603,23 +629,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
}).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 rarity pick counters
y += 18;
{
const rTargets = this._getRarityTargets();
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
const inCardPhase = ['PLAYER_CARD', 'AI_CARD', 'DRAFT_COMPLETE'].includes(this.draftState);
const currentIdx = this.currentPhaseRarity ? rarityOrder.indexOf(this.currentPhaseRarity) : 999;
const rarityDisplayColors = { legendary: '#886622', epic: '#663388', rare: '#224488', common: '#444444' };
const rarityActiveColors = { legendary: '#ffbb44', epic: '#cc88ff', rare: '#88aaff', common: '#cccccc' };
for (const r of rarityOrder) {
const total = rTargets[r] || 0;
if (total === 0) continue;
const rIdx = rarityOrder.indexOf(r);
const isDone = this.draftState === 'DRAFT_COMPLETE' || (inCardPhase && rIdx < currentIdx);
const isCurrent = r === this.currentPhaseRarity;
const rem = this.playerRemainingPicks[r] || 0;
const label = `${r.charAt(0).toUpperCase() + r.slice(1)}: `;
const status = isDone ? '\u2713' : (inCardPhase ? `${total - rem}/${total}` : `0/${total}`);
const color = isDone ? '#444444' : (isCurrent ? rarityActiveColors[r] : rarityDisplayColors[r]);
const rTxt = this.add.text(PANEL_X + 20, y, label + status, {
fontSize: '12px', color, fontFamily: 'Audiowide'
});
this.deckPanelGroup.push(rTxt);
y += 17;
}
y += 4;
}
// Player commander slot
y += 38;
y += 4;
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);
@ -672,23 +711,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
}).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 rarity pick counters
y += 18;
{
const rTargets = this._getRarityTargets();
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
const inCardPhase = ['PLAYER_CARD', 'AI_CARD', 'DRAFT_COMPLETE'].includes(this.draftState);
const currentIdx = this.currentPhaseRarity ? rarityOrder.indexOf(this.currentPhaseRarity) : 999;
const rarityDisplayColors = { legendary: '#886622', epic: '#663388', rare: '#224488', common: '#444444' };
const rarityActiveColors = { legendary: '#ffbb44', epic: '#cc88ff', rare: '#88aaff', common: '#cccccc' };
for (const r of rarityOrder) {
const total = rTargets[r] || 0;
if (total === 0) continue;
const rIdx = rarityOrder.indexOf(r);
const isDone = this.draftState === 'DRAFT_COMPLETE' || (inCardPhase && rIdx < currentIdx);
const isCurrent = r === this.currentPhaseRarity;
const rem = this.aiRemainingPicks[r] || 0;
const label = `${r.charAt(0).toUpperCase() + r.slice(1)}: `;
const status = isDone ? '\u2713' : (inCardPhase ? `${total - rem}/${total}` : `0/${total}`);
const color = isDone ? '#444444' : (isCurrent ? rarityActiveColors[r] : rarityDisplayColors[r]);
const rTxt = this.add.text(PANEL_X + 20, y, label + status, {
fontSize: '12px', color, fontFamily: 'Audiowide'
});
this.deckPanelGroup.push(rTxt);
y += 17;
}
y += 4;
}
// AI commander slot
y += 38;
y += 4;
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);
@ -731,9 +783,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
});
this.deckPanelGroup.push(aCountTxt);
// ── Validation & Start Battle (fixed at bottom) ──
const isValid = this.draftedCommander && pFilled >= 3;
if (isValid && !this.inputLocked) {
// ── Start Battle (shown when draft is complete) ──
if (this.draftState === 'DRAFT_COMPLETE') {
const startBg = this.add.rectangle(panelCX, BUTTON_Y, 240, 50, 0x226622)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x44aa44);
@ -750,26 +801,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
});
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'
} else {
const phaseMsg = this.draftState === 'PLAYER_COMMANDER' || this.draftState === 'AI_COMMANDER'
? 'Pick your commander to begin'
: 'Draft in progress...';
const phaseTxt = this.add.text(panelCX, BUTTON_Y, phaseMsg, {
fontSize: '13px', color: '#666688', fontFamily: 'Audiowide'
}).setOrigin(0.5);
this.deckPanelGroup.push(validTxt);
this.deckPanelGroup.push(phaseTxt);
}
}
_aiFinishDraft() {
// If AI doesn't have a commander yet, pick one
if (!this.aiCommander) {
this._aiPickCommander();
}
// Fill remaining AI cards without delay
if (!this.aiCommander) this._aiPickCommander();
if (this.aiCards.length >= 10) return;
// Safety net: fill any missing slots with best available cards (ignores phase)
const aiCmd = this.cardManager.getCard(this.aiCommander);
const preferredFaction = aiCmd ? aiCmd.faction : null;
let safety = 20;
while (this.aiCards.length < 10 && this._canSidePick('ai') && safety-- > 0) {
if (!this._aiPickOneCard()) break;
while (this.aiCards.length < 10 && safety-- > 0) {
const available = [
...this.pool.legendary, ...this.pool.epic,
...this.pool.rare, ...this.pool.common
].filter(c => !this.takenCards[c.id]);
if (available.length === 0) break;
const scored = available
.map(c => this._scoreCard(c, preferredFaction))
.sort((a, b) => b.score - a.score);
const pick = scored[0];
this.aiCards.push(pick.card.id);
this.takenCards[pick.card.id] = 'ai';
}
}
@ -782,22 +843,50 @@ export class SkirmishDraftScene extends Phaser.Scene {
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 rarityNames = { legendary: 'LEGENDARY', epic: 'EPIC', rare: 'RARE', common: 'COMMON' };
const rarityAbbrev = { legendary: 'LEG', epic: 'EPIC', rare: 'RARE', common: 'COM' };
const rarityBgColors = { legendary: 0x443300, epic: 0x2a0a44, rare: 0x0a1a44, common: 0x1a1a1a };
const rarityStrokeColors = { legendary: 0xffaa00, epic: 0xbb44ff, rare: 0x4488ff, common: 0x888888 };
const rarityTextColors = { legendary: '#ffbb44', epic: '#cc88ff', rare: '#88aaff', common: '#cccccc' };
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'
// Determine main text
let mainText, mainColor, bgColor;
if (this.draftState === 'PLAYER_COMMANDER') {
mainText = 'Choose Your Commander';
mainColor = '#44aaff'; bgColor = 0x112233;
} else if (this.draftState === 'AI_COMMANDER') {
mainText = 'Opponent Choosing Commander...';
mainColor = '#ff6644'; bgColor = 0x331111;
} else if (this.draftState === 'DRAFT_COMPLETE') {
mainText = 'Draft Complete!';
mainColor = '#44ff44'; bgColor = 0x113311;
} else if (this.currentPhaseRarity) {
const targets = this._getRarityTargets();
const total = targets[this.currentPhaseRarity] || 0;
const rName = rarityNames[this.currentPhaseRarity];
if (this.draftState === 'PLAYER_CARD') {
const rem = this.playerRemainingPicks[this.currentPhaseRarity] || 0;
const pickNum = total - rem + 1;
mainText = `\u2605 ${rName} PHASE \u2014 Your Pick ${pickNum} of ${total} \u2605`;
mainColor = rarityTextColors[this.currentPhaseRarity];
bgColor = rarityBgColors[this.currentPhaseRarity];
} else {
const rem = this.aiRemainingPicks[this.currentPhaseRarity] || 0;
const pickNum = total - rem + 1;
mainText = `\u2605 ${rName} PHASE \u2014 Opponent Pick ${pickNum} of ${total} \u2605`;
mainColor = '#ff6644'; bgColor = 0x331111;
}
} else {
mainText = 'Your Turn \u2014 Pick a Card';
mainColor = '#44aaff'; bgColor = 0x112233;
}
const bannerBg = this.add.rectangle(width / 2, 37, width, 72, bgColor).setDepth(10);
const txt = this.add.text(width / 2, 20, mainText, {
fontSize: '22px', color: mainColor, fontFamily: 'RaiderCrusader'
}).setOrigin(0.5).setDepth(11);
this.bannerGroup = [bg, txt];
this.bannerGroup.push(bannerBg, txt);
// Pulse effect during AI turns
if (this.draftState === 'AI_COMMANDER' || this.draftState === 'AI_CARD') {
@ -809,6 +898,63 @@ export class SkirmishDraftScene extends Phaser.Scene {
repeat: -1
});
}
// Rarity progress pills (shown during card phases)
const inCardPhase = ['PLAYER_CARD', 'AI_CARD', 'DRAFT_COMPLETE'].includes(this.draftState);
if (inCardPhase) {
const targets = this._getRarityTargets();
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
const activeRarities = rarityOrder.filter(r => (targets[r] || 0) > 0);
const currentIdx = this.currentPhaseRarity
? rarityOrder.indexOf(this.currentPhaseRarity)
: 999;
const pillW = 110, pillH = 22, pillGap = 8;
const totalPillW = activeRarities.length * pillW + (activeRarities.length - 1) * pillGap;
let pillX = (width - totalPillW) / 2 + pillW / 2;
const pillY = 54;
for (const r of activeRarities) {
const rIdx = rarityOrder.indexOf(r);
const isDone = this.draftState === 'DRAFT_COMPLETE' || rIdx < currentIdx;
const isCurrent = r === this.currentPhaseRarity;
const total = targets[r];
const rem = this.playerRemainingPicks[r] || 0;
let pillBg, pillStroke, labelText, labelColor;
if (isDone) {
pillBg = 0x222222; pillStroke = 0x444444;
labelText = `${rarityAbbrev[r]} \u2713`; labelColor = '#555555';
} else if (isCurrent) {
pillBg = rarityBgColors[r]; pillStroke = rarityStrokeColors[r];
const picked = total - rem;
labelText = `${rarityAbbrev[r]} ${picked}/${total}`;
labelColor = rarityTextColors[r];
} else {
pillBg = 0x111122; pillStroke = 0x333344;
labelText = `${rarityAbbrev[r]} /${total}`; labelColor = '#444466';
}
const pill = this.add.rectangle(pillX, pillY, pillW, pillH, pillBg)
.setStrokeStyle(1, pillStroke).setDepth(10);
const pillTxt = this.add.text(pillX, pillY, labelText, {
fontSize: '11px', color: labelColor, fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(11);
this.bannerGroup.push(pill, pillTxt);
if (isCurrent && (this.draftState === 'PLAYER_CARD' || this.draftState === 'AI_CARD')) {
this.tweens.add({
targets: pill,
alpha: { from: 1, to: 0.6 },
duration: 800,
yoyo: true,
repeat: -1
});
}
pillX += pillW + pillGap;
}
}
}
// ── AI Draft Algorithm ───────────────────────────────────────────────
@ -816,13 +962,22 @@ export class SkirmishDraftScene extends Phaser.Scene {
_getRarityTargets() {
const targets = {
Easy: { legendary: 0, epic: 0, rare: 2, common: 8 },
Medium: { legendary: 1, epic: 2, rare: 4, common: 3 },
Medium: { legendary: 1, epic: 2, rare: 3, common: 4 },
Hard: { legendary: 2, epic: 4, rare: 4, common: 0 },
Legendary: { legendary: 4, epic: 6, rare: 0, common: 0 }
Legendary: { legendary: 4, epic: 5, rare: 1, common: 0 }
};
return targets[this.selectedDifficulty] || targets.Medium;
}
_getNextRarity(current) {
const order = ['legendary', 'epic', 'rare', 'common'];
const start = current === null ? 0 : order.indexOf(current) + 1;
for (let i = start; i < order.length; i++) {
if ((this.playerRemainingPicks[order[i]] || 0) > 0) return order[i];
}
return null;
}
_aiDraft() {
let budget = this.totalBudget;
const taken = new Set();
@ -895,10 +1050,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
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';
@ -906,61 +1059,52 @@ export class SkirmishDraftScene extends Phaser.Scene {
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) {
// Initialize rarity pick counts from targets
const targets = this._getRarityTargets();
this.playerRemainingPicks = { ...targets };
this.aiRemainingPicks = { ...targets };
this.currentPhaseRarity = this._getNextRarity(null);
if (this.currentPhaseRarity) {
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;
}
this.playerRemainingPicks[this.currentPhaseRarity] =
Math.max(0, (this.playerRemainingPicks[this.currentPhaseRarity] || 0) - 1);
this.draftState = 'AI_CARD';
aiTurn = true;
} else if (this.draftState === 'AI_CARD') {
if (playerCan) {
this.aiRemainingPicks[this.currentPhaseRarity] =
Math.max(0, (this.aiRemainingPicks[this.currentPhaseRarity] || 0) - 1);
if ((this.playerRemainingPicks[this.currentPhaseRarity] || 0) > 0) {
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;
const next = this._getNextRarity(this.currentPhaseRarity);
if (next) {
this.currentPhaseRarity = next;
this.draftState = 'PLAYER_CARD';
this.inputLocked = false;
} else {
this.currentPhaseRarity = null;
this.draftState = 'DRAFT_COMPLETE';
this.inputLocked = false;
}
}
}
@ -1000,62 +1144,25 @@ export class SkirmishDraftScene extends Phaser.Scene {
}
_aiPickOneCard() {
const available = [
...this.pool.legendary, ...this.pool.epic,
...this.pool.rare, ...this.pool.common
].filter(c => !this.takenCards[c.id]);
if (!this.currentPhaseRarity) return false;
const available = (this.pool[this.currentPhaseRarity] || [])
.filter(c => !this.takenCards[c.id]);
if (available.length === 0) return false;
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 candidates = available
.map(c => this._scoreCard(c, preferredFaction))
.sort((a, b) => b.score - a.score);
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;
const pick = candidates[0];
this.aiCards.push(pick.card.id);
this.aiCopies[pick.card.id] = (this.aiCopies[pick.card.id] || 0) + 1;
this.takenCards[pick.card.id] = 'ai';
return true;
}
_scoreCard(card, preferredFaction) {

View File

@ -55,7 +55,7 @@ export class SkirmishSetupScene extends Phaser.Scene {
}
// ── Difficulty selector ───────────────────────────────────────────
this.add.text(width / 2, 380, 'Difficulty', {
this.add.text(width / 2, 380, 'Deck Strength', {
fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5);
@ -64,6 +64,9 @@ export class SkirmishSetupScene extends Phaser.Scene {
}).setOrigin(0.5);
const difficulties = ['Easy', 'Medium', 'Hard', 'Legendary'];
const diffLabels = {
Easy: 'Beginner', Medium: 'Medium', Hard: 'Advanced', Legendary: 'Legendary'
};
const diffColors = {
Easy: 0x226622, Medium: 0x1a3a5c, Hard: 0x663322, Legendary: 0x664400
};
@ -81,7 +84,7 @@ export class SkirmishSetupScene extends Phaser.Scene {
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, isSelected ? diffBorderColors[diff] : 0x444444);
const text = this.add.text(bx, by, diff, {
const text = this.add.text(bx, by, diffLabels[diff], {
fontSize: '20px', color: isSelected ? '#ffffff' : '#aaaaaa', fontFamily: 'Audiowide'
}).setOrigin(0.5);