@@ -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