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.aiCommander = null;
|
||||||
this.aiCards = [];
|
this.aiCards = [];
|
||||||
this.aiCopies = {};
|
this.aiCopies = {};
|
||||||
this.aiBudget = this.totalBudget;
|
|
||||||
this.takenCards = {}; // { cardId: 'player' | 'ai' }
|
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.bannerGroup = [];
|
||||||
|
this.SCROLL_TOP = 80;
|
||||||
|
|
||||||
this._renderStatusBanner();
|
this._renderStatusBanner();
|
||||||
this._renderCardGrid();
|
this._renderCardGrid();
|
||||||
|
|
@ -110,10 +113,25 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
const rareAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'rare');
|
||||||
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
const commonAssault = allCards.filter(c => c.type === 'assault' && c.rarity === 'common');
|
||||||
|
|
||||||
const fraction = this.isAlternatingDraft ? 2 : 3;
|
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 pick = (arr) => {
|
||||||
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
||||||
return shuffled.slice(0, Math.ceil(arr.length / fraction));
|
return shuffled.slice(0, Math.ceil(arr.length / 3));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -214,6 +232,11 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const sectionLabel = this.add.text(this.GRID_LEFT, cursorY, section.label, {
|
const sectionLabel = this.add.text(this.GRID_LEFT, cursorY, section.label, {
|
||||||
fontSize: '18px', color: rarityColors[section.label] || '#d4af37', fontFamily: 'Audiowide'
|
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);
|
this.scrollContainer.add(sectionLabel);
|
||||||
|
|
||||||
cursorY += 30;
|
cursorY += 30;
|
||||||
|
|
@ -240,7 +263,7 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
canAdd = this.draftState === 'PLAYER_COMMANDER' && !this.draftedCommander;
|
canAdd = this.draftState === 'PLAYER_COMMANDER' && !this.draftedCommander;
|
||||||
} else {
|
} else {
|
||||||
canAdd = this.draftState === 'PLAYER_CARD' &&
|
canAdd = this.draftState === 'PLAYER_CARD' &&
|
||||||
cost <= this.remainingBudget &&
|
card.rarity === this.currentPhaseRarity &&
|
||||||
this.draftedCards.length < 10;
|
this.draftedCards.length < 10;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -258,7 +281,12 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
// Card visual
|
// Card visual
|
||||||
const inst = this.cardManager.createInstance(card.id, this.selectedLevel);
|
const inst = this.cardManager.createInstance(card.id, this.selectedLevel);
|
||||||
const cardObj = new CardObject(this, cx, cy, inst, { width: this.CARD_W, height: this.CARD_H });
|
const cardObj = new CardObject(this, cx, cy, inst, { width: this.CARD_W, height: this.CARD_H });
|
||||||
if (!canAdd) cardObj.setAlpha(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);
|
this.scrollContainer.add(cardObj);
|
||||||
|
|
||||||
// Taken overlay (alternating draft only)
|
// Taken overlay (alternating draft only)
|
||||||
|
|
@ -331,8 +359,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
else if (this.inputLocked) btnLabel = 'WAIT...';
|
else if (this.inputLocked) btnLabel = 'WAIT...';
|
||||||
else if (isCommander && this.draftState !== 'PLAYER_COMMANDER') btnLabel = 'NOT YET';
|
else if (isCommander && this.draftState !== 'PLAYER_COMMANDER') btnLabel = 'NOT YET';
|
||||||
else if (!isCommander && this.draftState !== 'PLAYER_CARD') 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 (canAdd) btnLabel = '+ PICK';
|
||||||
else if (!isCommander && cost > this.remainingBudget) btnLabel = 'NO BUDGET';
|
|
||||||
else if (!isCommander && this.draftedCards.length >= 10) btnLabel = 'DECK FULL';
|
else if (!isCommander && this.draftedCards.length >= 10) btnLabel = 'DECK FULL';
|
||||||
else btnLabel = '—';
|
else btnLabel = '—';
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -587,8 +615,6 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
const OPPONENT_TOP = DIVIDER_Y + 14;
|
const OPPONENT_TOP = DIVIDER_Y + 14;
|
||||||
const BUTTON_Y = height - 55;
|
const BUTTON_Y = height - 55;
|
||||||
const ENTRY_H = 26;
|
const ENTRY_H = 26;
|
||||||
const barW = PANEL_W - 200;
|
|
||||||
const barX = PANEL_X + 180;
|
|
||||||
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
|
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
|
||||||
|
|
||||||
// Panel background
|
// Panel background
|
||||||
|
|
@ -603,23 +629,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.deckPanelGroup.push(yourTitle);
|
this.deckPanelGroup.push(yourTitle);
|
||||||
|
|
||||||
// Player budget
|
// Player rarity pick counters
|
||||||
y += 26;
|
y += 18;
|
||||||
const pBudgetPct = this.remainingBudget / this.totalBudget;
|
{
|
||||||
const pBudgetColor = pBudgetPct > 0.5 ? '#44ff44' : pBudgetPct > 0.2 ? '#ffaa00' : '#ff4444';
|
const rTargets = this._getRarityTargets();
|
||||||
const pBudgetTxt = this.add.text(PANEL_X + 20, y, `Budget: ${this.remainingBudget}/${this.totalBudget}`, {
|
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
||||||
fontSize: '14px', color: pBudgetColor, fontFamily: 'Audiowide'
|
const inCardPhase = ['PLAYER_CARD', 'AI_CARD', 'DRAFT_COMPLETE'].includes(this.draftState);
|
||||||
});
|
const currentIdx = this.currentPhaseRarity ? rarityOrder.indexOf(this.currentPhaseRarity) : 999;
|
||||||
this.deckPanelGroup.push(pBudgetTxt);
|
const rarityDisplayColors = { legendary: '#886622', epic: '#663388', rare: '#224488', common: '#444444' };
|
||||||
|
const rarityActiveColors = { legendary: '#ffbb44', epic: '#cc88ff', rare: '#88aaff', common: '#cccccc' };
|
||||||
const pBarBg = this.add.rectangle(barX + barW / 2, y + 8, barW, 10, 0x222222).setStrokeStyle(1, 0x444444);
|
for (const r of rarityOrder) {
|
||||||
const pFillW = Math.max(1, barW * pBudgetPct);
|
const total = rTargets[r] || 0;
|
||||||
const pBarColor = pBudgetPct > 0.5 ? 0x44aa44 : pBudgetPct > 0.2 ? 0xaa8800 : 0xaa3333;
|
if (total === 0) continue;
|
||||||
const pBarFill = this.add.rectangle(barX + pFillW / 2, y + 8, pFillW, 8, pBarColor);
|
const rIdx = rarityOrder.indexOf(r);
|
||||||
this.deckPanelGroup.push(pBarBg, pBarFill);
|
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
|
// Player commander slot
|
||||||
y += 38;
|
y += 4;
|
||||||
const pCmd = this.draftedCommander ? this.cardManager.getCard(this.draftedCommander) : null;
|
const pCmd = this.draftedCommander ? this.cardManager.getCard(this.draftedCommander) : null;
|
||||||
const pCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, pCmd ? 0x1a3a5c : 0x1a1a2e)
|
const pCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, pCmd ? 0x1a3a5c : 0x1a1a2e)
|
||||||
.setStrokeStyle(2, 0xffd700);
|
.setStrokeStyle(2, 0xffd700);
|
||||||
|
|
@ -672,23 +711,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.deckPanelGroup.push(oppTitle);
|
this.deckPanelGroup.push(oppTitle);
|
||||||
|
|
||||||
// AI budget
|
// AI rarity pick counters
|
||||||
y += 26;
|
y += 18;
|
||||||
const aBudgetPct = this.aiBudget / this.totalBudget;
|
{
|
||||||
const aBudgetColor = aBudgetPct > 0.5 ? '#44ff44' : aBudgetPct > 0.2 ? '#ffaa00' : '#ff4444';
|
const rTargets = this._getRarityTargets();
|
||||||
const aBudgetTxt = this.add.text(PANEL_X + 20, y, `Budget: ${this.aiBudget}/${this.totalBudget}`, {
|
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
||||||
fontSize: '14px', color: aBudgetColor, fontFamily: 'Audiowide'
|
const inCardPhase = ['PLAYER_CARD', 'AI_CARD', 'DRAFT_COMPLETE'].includes(this.draftState);
|
||||||
});
|
const currentIdx = this.currentPhaseRarity ? rarityOrder.indexOf(this.currentPhaseRarity) : 999;
|
||||||
this.deckPanelGroup.push(aBudgetTxt);
|
const rarityDisplayColors = { legendary: '#886622', epic: '#663388', rare: '#224488', common: '#444444' };
|
||||||
|
const rarityActiveColors = { legendary: '#ffbb44', epic: '#cc88ff', rare: '#88aaff', common: '#cccccc' };
|
||||||
const aBarBg = this.add.rectangle(barX + barW / 2, y + 8, barW, 10, 0x222222).setStrokeStyle(1, 0x444444);
|
for (const r of rarityOrder) {
|
||||||
const aFillW = Math.max(1, barW * aBudgetPct);
|
const total = rTargets[r] || 0;
|
||||||
const aBarColor = aBudgetPct > 0.5 ? 0x44aa44 : aBudgetPct > 0.2 ? 0xaa8800 : 0xaa3333;
|
if (total === 0) continue;
|
||||||
const aBarFill = this.add.rectangle(barX + aFillW / 2, y + 8, aFillW, 8, aBarColor);
|
const rIdx = rarityOrder.indexOf(r);
|
||||||
this.deckPanelGroup.push(aBarBg, aBarFill);
|
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
|
// AI commander slot
|
||||||
y += 38;
|
y += 4;
|
||||||
const aCmd = this.aiCommander ? this.cardManager.getCard(this.aiCommander) : null;
|
const aCmd = this.aiCommander ? this.cardManager.getCard(this.aiCommander) : null;
|
||||||
const aCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, aCmd ? 0x3a1a1a : 0x1a1a2e)
|
const aCmdBg = this.add.rectangle(panelCX, y, PANEL_W - 40, 32, aCmd ? 0x3a1a1a : 0x1a1a2e)
|
||||||
.setStrokeStyle(2, aCmd ? 0xff6644 : 0x444444);
|
.setStrokeStyle(2, aCmd ? 0xff6644 : 0x444444);
|
||||||
|
|
@ -731,9 +783,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
this.deckPanelGroup.push(aCountTxt);
|
this.deckPanelGroup.push(aCountTxt);
|
||||||
|
|
||||||
// ── Validation & Start Battle (fixed at bottom) ──
|
// ── Start Battle (shown when draft is complete) ──
|
||||||
const isValid = this.draftedCommander && pFilled >= 3;
|
if (this.draftState === 'DRAFT_COMPLETE') {
|
||||||
if (isValid && !this.inputLocked) {
|
|
||||||
const startBg = this.add.rectangle(panelCX, BUTTON_Y, 240, 50, 0x226622)
|
const startBg = this.add.rectangle(panelCX, BUTTON_Y, 240, 50, 0x226622)
|
||||||
.setInteractive({ useHandCursor: true })
|
.setInteractive({ useHandCursor: true })
|
||||||
.setStrokeStyle(2, 0x44aa44);
|
.setStrokeStyle(2, 0x44aa44);
|
||||||
|
|
@ -750,26 +801,36 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.deckPanelGroup.push(startBg, startTxt);
|
this.deckPanelGroup.push(startBg, startTxt);
|
||||||
} else if (!isValid) {
|
} else {
|
||||||
const validMsg = !this.draftedCommander
|
const phaseMsg = this.draftState === 'PLAYER_COMMANDER' || this.draftState === 'AI_COMMANDER'
|
||||||
? 'Select a commander first'
|
? 'Pick your commander to begin'
|
||||||
: `Need at least ${3 - pFilled} more card(s)`;
|
: 'Draft in progress...';
|
||||||
const validTxt = this.add.text(panelCX, BUTTON_Y, validMsg, {
|
const phaseTxt = this.add.text(panelCX, BUTTON_Y, phaseMsg, {
|
||||||
fontSize: '14px', color: '#ff6644', fontFamily: 'Audiowide'
|
fontSize: '13px', color: '#666688', fontFamily: 'Audiowide'
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.deckPanelGroup.push(validTxt);
|
this.deckPanelGroup.push(phaseTxt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_aiFinishDraft() {
|
_aiFinishDraft() {
|
||||||
// If AI doesn't have a commander yet, pick one
|
if (!this.aiCommander) this._aiPickCommander();
|
||||||
if (!this.aiCommander) {
|
if (this.aiCards.length >= 10) return;
|
||||||
this._aiPickCommander();
|
// Safety net: fill any missing slots with best available cards (ignores phase)
|
||||||
}
|
const aiCmd = this.cardManager.getCard(this.aiCommander);
|
||||||
// Fill remaining AI cards without delay
|
const preferredFaction = aiCmd ? aiCmd.faction : null;
|
||||||
let safety = 20;
|
let safety = 20;
|
||||||
while (this.aiCards.length < 10 && this._canSidePick('ai') && safety-- > 0) {
|
while (this.aiCards.length < 10 && safety-- > 0) {
|
||||||
if (!this._aiPickOneCard()) break;
|
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 = [];
|
this.bannerGroup = [];
|
||||||
|
|
||||||
const { width } = this.scale;
|
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);
|
// Determine main text
|
||||||
const txt = this.add.text(width / 2, 30, msg.text, {
|
let mainText, mainColor, bgColor;
|
||||||
fontSize: '28px', color: msg.color, fontFamily: 'RaiderCrusader'
|
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);
|
}).setOrigin(0.5).setDepth(11);
|
||||||
|
this.bannerGroup.push(bannerBg, txt);
|
||||||
this.bannerGroup = [bg, txt];
|
|
||||||
|
|
||||||
// Pulse effect during AI turns
|
// Pulse effect during AI turns
|
||||||
if (this.draftState === 'AI_COMMANDER' || this.draftState === 'AI_CARD') {
|
if (this.draftState === 'AI_COMMANDER' || this.draftState === 'AI_CARD') {
|
||||||
|
|
@ -809,6 +898,63 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
repeat: -1
|
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 ───────────────────────────────────────────────
|
// ── AI Draft Algorithm ───────────────────────────────────────────────
|
||||||
|
|
@ -816,13 +962,22 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
_getRarityTargets() {
|
_getRarityTargets() {
|
||||||
const targets = {
|
const targets = {
|
||||||
Easy: { legendary: 0, epic: 0, rare: 2, common: 8 },
|
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 },
|
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;
|
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() {
|
_aiDraft() {
|
||||||
let budget = this.totalBudget;
|
let budget = this.totalBudget;
|
||||||
const taken = new Set();
|
const taken = new Set();
|
||||||
|
|
@ -895,10 +1050,8 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
if (isCommander) {
|
if (isCommander) {
|
||||||
this.draftedCommander = card.id;
|
this.draftedCommander = card.id;
|
||||||
} else {
|
} else {
|
||||||
const cost = COSTS[card.rarity] || 1;
|
|
||||||
this.draftedCards.push(card.id);
|
this.draftedCards.push(card.id);
|
||||||
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
this.draftCopies[card.id] = (this.draftCopies[card.id] || 0) + 1;
|
||||||
this.remainingBudget -= cost;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.takenCards[card.id] = 'player';
|
this.takenCards[card.id] = 'player';
|
||||||
|
|
@ -906,61 +1059,52 @@ export class SkirmishDraftScene extends Phaser.Scene {
|
||||||
this._advanceDraftState();
|
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() {
|
_advanceDraftState() {
|
||||||
const playerCan = this._canSidePick('player');
|
|
||||||
const aiCan = this._canSidePick('ai');
|
|
||||||
let aiTurn = false;
|
let aiTurn = false;
|
||||||
|
|
||||||
if (this.draftState === 'PLAYER_COMMANDER') {
|
if (this.draftState === 'PLAYER_COMMANDER') {
|
||||||
this.draftState = 'AI_COMMANDER';
|
this.draftState = 'AI_COMMANDER';
|
||||||
aiTurn = true;
|
aiTurn = true;
|
||||||
|
|
||||||
} else if (this.draftState === 'AI_COMMANDER') {
|
} 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.draftState = 'PLAYER_CARD';
|
||||||
this.inputLocked = false;
|
this.inputLocked = false;
|
||||||
} else if (aiCan) {
|
|
||||||
this.draftState = 'AI_CARD';
|
|
||||||
aiTurn = true;
|
|
||||||
} else {
|
} else {
|
||||||
this.draftState = 'DRAFT_COMPLETE';
|
this.draftState = 'DRAFT_COMPLETE';
|
||||||
this.inputLocked = false;
|
this.inputLocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.draftState === 'PLAYER_CARD') {
|
} else if (this.draftState === 'PLAYER_CARD') {
|
||||||
if (aiCan) {
|
this.playerRemainingPicks[this.currentPhaseRarity] =
|
||||||
this.draftState = 'AI_CARD';
|
Math.max(0, (this.playerRemainingPicks[this.currentPhaseRarity] || 0) - 1);
|
||||||
aiTurn = true;
|
this.draftState = 'AI_CARD';
|
||||||
} else if (playerCan) {
|
aiTurn = true;
|
||||||
// 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') {
|
} 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.draftState = 'PLAYER_CARD';
|
||||||
this.inputLocked = false;
|
this.inputLocked = false;
|
||||||
} else if (aiCan) {
|
|
||||||
// Player is done, AI keeps picking
|
|
||||||
this.draftState = 'AI_CARD';
|
|
||||||
aiTurn = true;
|
|
||||||
} else {
|
} else {
|
||||||
this.draftState = 'DRAFT_COMPLETE';
|
const next = this._getNextRarity(this.currentPhaseRarity);
|
||||||
this.inputLocked = false;
|
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() {
|
_aiPickOneCard() {
|
||||||
const available = [
|
if (!this.currentPhaseRarity) return false;
|
||||||
...this.pool.legendary, ...this.pool.epic,
|
|
||||||
...this.pool.rare, ...this.pool.common
|
const available = (this.pool[this.currentPhaseRarity] || [])
|
||||||
].filter(c => !this.takenCards[c.id]);
|
.filter(c => !this.takenCards[c.id]);
|
||||||
|
|
||||||
|
if (available.length === 0) return false;
|
||||||
|
|
||||||
const aiCmd = this.cardManager.getCard(this.aiCommander);
|
const aiCmd = this.cardManager.getCard(this.aiCommander);
|
||||||
const preferredFaction = aiCmd ? aiCmd.faction : null;
|
const preferredFaction = aiCmd ? aiCmd.faction : null;
|
||||||
|
|
||||||
// Count what the AI has already picked by rarity
|
const candidates = available
|
||||||
const rarityCounts = {};
|
.map(c => this._scoreCard(c, preferredFaction))
|
||||||
for (const id of this.aiCards) {
|
.sort((a, b) => b.score - a.score);
|
||||||
const c = this.cardManager.getCard(id);
|
|
||||||
if (c) rarityCounts[c.rarity] = (rarityCounts[c.rarity] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targets = this._getRarityTargets();
|
const pick = candidates[0];
|
||||||
const rarityOrder = ['legendary', 'epic', 'rare', 'common'];
|
this.aiCards.push(pick.card.id);
|
||||||
|
this.aiCopies[pick.card.id] = (this.aiCopies[pick.card.id] || 0) + 1;
|
||||||
// Try to pick the highest-priority rarity whose target isn't met yet
|
this.takenCards[pick.card.id] = 'ai';
|
||||||
for (const rarity of rarityOrder) {
|
return true;
|
||||||
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) {
|
_scoreCard(card, preferredFaction) {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Difficulty selector ───────────────────────────────────────────
|
// ── Difficulty selector ───────────────────────────────────────────
|
||||||
this.add.text(width / 2, 380, 'Difficulty', {
|
this.add.text(width / 2, 380, 'Deck Strength', {
|
||||||
fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide'
|
fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
|
@ -64,6 +64,9 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
const difficulties = ['Easy', 'Medium', 'Hard', 'Legendary'];
|
const difficulties = ['Easy', 'Medium', 'Hard', 'Legendary'];
|
||||||
|
const diffLabels = {
|
||||||
|
Easy: 'Beginner', Medium: 'Medium', Hard: 'Advanced', Legendary: 'Legendary'
|
||||||
|
};
|
||||||
const diffColors = {
|
const diffColors = {
|
||||||
Easy: 0x226622, Medium: 0x1a3a5c, Hard: 0x663322, Legendary: 0x664400
|
Easy: 0x226622, Medium: 0x1a3a5c, Hard: 0x663322, Legendary: 0x664400
|
||||||
};
|
};
|
||||||
|
|
@ -81,7 +84,7 @@ export class SkirmishSetupScene extends Phaser.Scene {
|
||||||
.setInteractive({ useHandCursor: true })
|
.setInteractive({ useHandCursor: true })
|
||||||
.setStrokeStyle(2, isSelected ? diffBorderColors[diff] : 0x444444);
|
.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'
|
fontSize: '20px', color: isSelected ? '#ffffff' : '#aaaaaa', fontFamily: 'Audiowide'
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue