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:
parent
fd78f75c83
commit
d4d9b25c72
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue