Skip to content

Commit 3e26280

Browse files
author
Sorra
committed
Merge wl-CG-0MMLZK4WQ13K0R1P-meta-progression-ui: Surface meta-progression in game-over overlay
2 parents 535614e + d2ebd7c commit 3e26280

4 files changed

Lines changed: 505 additions & 15 deletions

File tree

example-games/main-street/MainStreetCards.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,3 +952,22 @@ export function cardLabel(card: AnyCard): string {
952952
case 'upgrade': return `${card.name} ($${card.cost})`;
953953
}
954954
}
955+
956+
// ---------------------------------------------------------------------------
957+
// Card template ID → display-name lookup
958+
// ---------------------------------------------------------------------------
959+
960+
/**
961+
* Read-only map from card template ID (e.g. `'biz-cafe'`) to its display name
962+
* (e.g. `'Cafe'`). Built once at module load from the private template arrays.
963+
*
964+
* This is used by the meta-progression UI to show which cards a newly unlocked
965+
* tier adds to the player's card pool.
966+
*/
967+
export const CARD_TEMPLATE_NAMES: ReadonlyMap<string, string> = (() => {
968+
const m = new Map<string, string>();
969+
for (const t of BUSINESS_TEMPLATES) m.set(t.id, t.name);
970+
for (const t of EVENT_TEMPLATES) m.set(t.id, t.name);
971+
for (const t of UPGRADE_TEMPLATES) m.set(t.id, t.name);
972+
return m;
973+
})();

example-games/main-street/MainStreetTiers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,18 @@ export function deriveUnlockedCardIds(unlockedTiers: string[]): string[] {
233233
}
234234
return [...allCardIds];
235235
}
236+
237+
/**
238+
* Returns the highest-order `TierDefinition` among the given unlocked tier IDs,
239+
* or `undefined` if `unlockedTiers` is empty / contains no valid IDs.
240+
*/
241+
export function highestUnlockedTier(
242+
unlockedTiers: string[],
243+
): TierDefinition | undefined {
244+
let best: TierDefinition | undefined;
245+
for (const id of unlockedTiers) {
246+
const def = TIER_DEFINITIONS[id];
247+
if (def && (!best || def.order > best.order)) best = def;
248+
}
249+
return best;
250+
}

example-games/main-street/scenes/MainStreetScene.ts

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
GRID_SIZE,
2222
synergyColor,
2323
cardLabel,
24+
CARD_TEMPLATE_NAMES,
2425
MARKET_BUSINESS_SLOTS,
2526
MARKET_INVESTMENT_SLOTS,
2627
INCIDENT_QUEUE_SIZE,
@@ -58,6 +59,11 @@ import {
5859
loadCampaignProgress,
5960
updateCampaignAfterRun,
6061
} from '../MainStreetSaveLoad';
62+
import {
63+
TIER_DEFINITIONS,
64+
ORDERED_TIER_DEFINITIONS,
65+
highestUnlockedTier,
66+
} from '../MainStreetTiers';
6167

6268
// ── Constants ───────────────────────────────────────────────
6369

@@ -390,12 +396,16 @@ export class MainStreetScene extends CardGameScene {
390396
/**
391397
* Updates campaign progress after a completed run (win or loss).
392398
* Evaluates tier unlocks and persists the updated campaign.
399+
* Returns a Promise that resolves when the update is done (or
400+
* immediately if no campaign / store is available).
393401
*/
394-
private updateCampaignProgress(): void {
395-
if (!this.campaign || !this.saveStore) return;
396-
updateCampaignAfterRun(this.campaign, this.state, this.saveStore).catch(() => {
397-
// Silently ignore save failures -- campaign will be retried next run
398-
});
402+
private updateCampaignProgress(): Promise<void> {
403+
if (!this.campaign || !this.saveStore) return Promise.resolve();
404+
return updateCampaignAfterRun(this.campaign, this.state, this.saveStore)
405+
.then(() => {}) // discard the returned campaign (already mutated in place)
406+
.catch(() => {
407+
// Silently ignore save failures -- campaign will be retried next run
408+
});
399409
}
400410

401411
// ── Day flow ────────────────────────────────────────────
@@ -421,9 +431,22 @@ export class MainStreetScene extends CardGameScene {
421431
// Brief delay then show result / advance
422432
this.time.delayedCall(400, () => {
423433
if (result.gameResult !== 'playing') {
424-
// Update campaign progress (tier evaluation + persistence)
425-
this.updateCampaignProgress();
426-
this.showGameOverOverlay(result);
434+
// Snapshot tiers before the campaign update mutates them
435+
const tiersBefore = this.campaign
436+
? [...this.campaign.unlockedTiers]
437+
: [];
438+
439+
// Update campaign progress (tier evaluation + persistence),
440+
// then compute newly unlocked tiers and show the overlay.
441+
this.updateCampaignProgress().then(() => {
442+
const tiersAfter = this.campaign
443+
? this.campaign.unlockedTiers
444+
: [];
445+
const newlyUnlockedTiers = tiersAfter.filter(
446+
(t) => !tiersBefore.includes(t),
447+
);
448+
this.showGameOverOverlay(result, newlyUnlockedTiers);
449+
});
427450
} else {
428451
// Show income feedback briefly then start next turn
429452
if (result.income && result.income.total > 0) {
@@ -1483,7 +1506,10 @@ export class MainStreetScene extends CardGameScene {
14831506

14841507
// ── Game Over Overlay ───────────────────────────────────
14851508

1486-
private showGameOverOverlay(result: TurnResult): void {
1509+
private showGameOverOverlay(
1510+
result: TurnResult,
1511+
newlyUnlockedTiers: string[] = [],
1512+
): void {
14871513
this.uiPhase = 'game-over';
14881514
this.refreshAll();
14891515

@@ -1496,7 +1522,23 @@ export class MainStreetScene extends CardGameScene {
14961522
const challengeLineCount = activeChallenges.length;
14971523
// Extra height: section header + one line per challenge
14981524
const challengeExtraH = challengeLineCount > 0 ? 24 + challengeLineCount * 20 : 0;
1499-
const panelH = 360 + challengeExtraH; // extra 40px for difficulty selector
1525+
1526+
// ── Meta-progression section heights ──
1527+
// Tier unlock notifications (conditional)
1528+
let tierUnlockH = 0;
1529+
if (newlyUnlockedTiers.length > 0) {
1530+
tierUnlockH += 26; // section header
1531+
for (const tierId of newlyUnlockedTiers) {
1532+
tierUnlockH += 20; // tier name line
1533+
const def = TIER_DEFINITIONS[tierId];
1534+
if (def) tierUnlockH += def.newCardIds.length * 16; // card list
1535+
}
1536+
tierUnlockH += 8; // bottom padding
1537+
}
1538+
// Current tier + campaign stats (always shown when campaign exists)
1539+
const campaignH = this.campaign ? 80 : 0; // tier indicator + 3 stat lines + spacing
1540+
1541+
const panelH = 360 + challengeExtraH + tierUnlockH + campaignH;
15001542

15011543
// Overlay background
15021544
const overlay = createOverlayBackground(
@@ -1542,30 +1584,103 @@ export class MainStreetScene extends CardGameScene {
15421584
this.overlayObjects.push(breakdown);
15431585

15441586
// Per-challenge breakdown (below score breakdown)
1545-
let challengeBottomY = breakdownY + 100; // approximate height of score breakdown text
1587+
let cursorY = breakdownY + 100; // approximate height of score breakdown text
15461588
if (challengeLineCount > 0) {
15471589
const sectionTitle = this.add.text(
1548-
GAME_W / 2, challengeBottomY,
1590+
GAME_W / 2, cursorY,
15491591
'Challenge Details:',
15501592
{ fontSize: '14px', fontStyle: 'bold', color: '#aa9977', fontFamily: FONT_FAMILY },
15511593
).setOrigin(0.5, 0).setDepth(101);
15521594
this.overlayObjects.push(sectionTitle);
1553-
challengeBottomY += 22;
1595+
cursorY += 22;
15541596

15551597
for (const ac of activeChallenges) {
15561598
const done = ac.completed;
15571599
const icon = done ? '\u2713' : '\u2717'; // checkmark or cross
15581600
const lineColor = done ? '#44ff44' : '#ff6666';
15591601
const challengeLine = this.add.text(
1560-
GAME_W / 2, challengeBottomY,
1602+
GAME_W / 2, cursorY,
15611603
`${icon} ${ac.challenge.title}`,
15621604
{ fontSize: '13px', color: lineColor, fontFamily: FONT_FAMILY },
15631605
).setOrigin(0.5, 0).setDepth(101);
15641606
this.overlayObjects.push(challengeLine);
1565-
challengeBottomY += 20;
1607+
cursorY += 20;
15661608
}
15671609
}
15681610

1611+
// ── Meta-progression: Tier Unlock Notifications ──
1612+
if (newlyUnlockedTiers.length > 0) {
1613+
cursorY += 8;
1614+
const unlockHeader = this.add.text(
1615+
GAME_W / 2, cursorY,
1616+
'Tier Unlocked!',
1617+
{ fontSize: '14px', fontStyle: 'bold', color: '#44ff44', fontFamily: FONT_FAMILY },
1618+
).setOrigin(0.5, 0).setDepth(101);
1619+
this.overlayObjects.push(unlockHeader);
1620+
cursorY += 22;
1621+
1622+
for (const tierId of newlyUnlockedTiers) {
1623+
const def = TIER_DEFINITIONS[tierId];
1624+
if (!def) continue;
1625+
1626+
// Find the milestone record to determine the trigger type
1627+
const milestone = this.campaign?.milestoneHistory.find(
1628+
(m) => m.tierId === tierId,
1629+
);
1630+
const triggerLabel = milestone?.triggerType === 'challenge'
1631+
? '(via challenges)' : '(via reputation)';
1632+
1633+
const tierLine = this.add.text(
1634+
GAME_W / 2, cursorY,
1635+
`NEW: Tier ${def.order} - ${def.name} ${triggerLabel}`,
1636+
{ fontSize: '13px', color: '#88ff88', fontFamily: FONT_FAMILY },
1637+
).setOrigin(0.5, 0).setDepth(101);
1638+
this.overlayObjects.push(tierLine);
1639+
cursorY += 20;
1640+
1641+
// List the new cards added by this tier
1642+
for (const cardId of def.newCardIds) {
1643+
const cardName = CARD_TEMPLATE_NAMES.get(cardId) ?? cardId;
1644+
const cardLine = this.add.text(
1645+
GAME_W / 2, cursorY,
1646+
` + ${cardName}`,
1647+
{ fontSize: '12px', color: '#aaddaa', fontFamily: FONT_FAMILY },
1648+
).setOrigin(0.5, 0).setDepth(101);
1649+
this.overlayObjects.push(cardLine);
1650+
cursorY += 16;
1651+
}
1652+
}
1653+
}
1654+
1655+
// ── Meta-progression: Current Tier + Campaign Stats ──
1656+
if (this.campaign) {
1657+
cursorY += 8;
1658+
const highest = highestUnlockedTier(this.campaign.unlockedTiers);
1659+
const tierCount = ORDERED_TIER_DEFINITIONS.length;
1660+
const tierLabel = highest
1661+
? `Current Tier: ${highest.order} / ${tierCount} - ${highest.name}`
1662+
: 'Current Tier: --';
1663+
const tierIndicator = this.add.text(
1664+
GAME_W / 2, cursorY, tierLabel,
1665+
{ fontSize: '14px', fontStyle: 'bold', color: '#ddbb88', fontFamily: FONT_FAMILY },
1666+
).setOrigin(0.5, 0).setDepth(101);
1667+
this.overlayObjects.push(tierIndicator);
1668+
cursorY += 22;
1669+
1670+
const winRate = this.campaign.totalRuns > 0
1671+
? Math.round((this.campaign.totalWins / this.campaign.totalRuns) * 100)
1672+
: 0;
1673+
const statsLines = [
1674+
`Runs: ${this.campaign.totalRuns} | Wins: ${this.campaign.totalWins} (${winRate}%)`,
1675+
`High Score: ${this.campaign.highestScore} | Best Rep: ${this.campaign.persistentReputation}`,
1676+
];
1677+
const statsText = this.add.text(
1678+
GAME_W / 2, cursorY, statsLines.join('\n'),
1679+
{ fontSize: '13px', color: '#bbaa99', fontFamily: FONT_FAMILY, align: 'center', lineSpacing: 4 },
1680+
).setOrigin(0.5, 0).setDepth(101);
1681+
this.overlayObjects.push(statsText);
1682+
}
1683+
15691684
// Difficulty selector
15701685
const diffY = panelTop + panelH - 80;
15711686
const diffLabel = this.add.text(

0 commit comments

Comments
 (0)