@@ -9,9 +9,14 @@ import {
99 serializeMainStreetState ,
1010 deserializeMainStreetState ,
1111} from './MainStreetState' ;
12+ import {
13+ TIER_DEFINITIONS ,
14+ ORDERED_TIER_DEFINITIONS ,
15+ deriveUnlockedCardIds ,
16+ } from './MainStreetTiers' ;
1217
1318export const MAIN_STREET_SAVE_SCHEMA_VERSION = 1 ;
14- export const MAIN_STREET_CAMPAIGN_SCHEMA_VERSION = 1 ;
19+ export const MAIN_STREET_CAMPAIGN_SCHEMA_VERSION = 2 ;
1520export const MAIN_STREET_GAME_TYPE = 'main-street' ;
1621export const MAIN_STREET_RUN_SLOT = 'turn-start' ;
1722export const MAIN_STREET_CAMPAIGN_SLOT = 'campaign-default' ;
@@ -31,12 +36,26 @@ export const mainStreetCampaignSerializer: SaveSerializer<
3136> = {
3237 schemaVersion : MAIN_STREET_CAMPAIGN_SCHEMA_VERSION ,
3338 serialize : ( state ) => structuredClone ( state ) ,
34- deserialize : ( data ) => structuredClone ( data ) ,
39+ deserialize : ( data ) => {
40+ // v1 -> v2 migration: add schemaVersion, unlockedCardIds, milestoneHistory
41+ if ( ! data . schemaVersion || data . schemaVersion === 1 ) {
42+ return {
43+ ...data ,
44+ schemaVersion : 2 ,
45+ unlockedCardIds : deriveUnlockedCardIds ( data . unlockedTiers ) ,
46+ milestoneHistory : [ ] ,
47+ } ;
48+ }
49+ return structuredClone ( data ) ;
50+ } ,
3551} ;
3652
3753export function createDefaultCampaignProgress ( ) : MainStreetCampaignProgress {
3854 return {
55+ schemaVersion : MAIN_STREET_CAMPAIGN_SCHEMA_VERSION ,
3956 unlockedTiers : [ 'tier-1' ] ,
57+ unlockedCardIds : TIER_DEFINITIONS [ 'tier-1' ] . cumulativeCardIds . slice ( ) ,
58+ milestoneHistory : [ ] ,
4059 persistentReputation : 0 ,
4160 highestScore : 0 ,
4261 totalRuns : 0 ,
@@ -45,6 +64,74 @@ export function createDefaultCampaignProgress(): MainStreetCampaignProgress {
4564 } ;
4665}
4766
67+ /**
68+ * Evaluates tier unlocks and updates campaign progress after a completed run.
69+ *
70+ * Called after EndCheck phase determines the game result.
71+ * Mutates the campaign progress in place, then persists it.
72+ *
73+ * @param campaign Current campaign progress (loaded from storage).
74+ * @param state Completed game state (after EndCheck).
75+ * @param store Save/Load store for persistence (optional; if provided, saves automatically).
76+ * @returns The updated campaign progress.
77+ */
78+ export async function updateCampaignAfterRun (
79+ campaign : MainStreetCampaignProgress ,
80+ state : MainStreetState ,
81+ store ?: SaveLoadStore ,
82+ ) : Promise < MainStreetCampaignProgress > {
83+ const now = new Date ( ) . toISOString ( ) ;
84+
85+ // Update statistics
86+ campaign . totalRuns += 1 ;
87+ if ( state . gameResult === 'win' ) campaign . totalWins += 1 ;
88+ if ( state . finalScore > campaign . highestScore ) {
89+ campaign . highestScore = state . finalScore ;
90+ }
91+ if ( state . resourceBank . reputation > campaign . persistentReputation ) {
92+ campaign . persistentReputation = state . resourceBank . reputation ;
93+ }
94+
95+ // Evaluate tier unlocks (ordered by tier number)
96+ for ( const tierDef of ORDERED_TIER_DEFINITIONS ) {
97+ // Skip tier-1 (always unlocked) and already-unlocked tiers
98+ if ( tierDef . id === 'tier-1' ) continue ;
99+ if ( campaign . unlockedTiers . includes ( tierDef . id ) ) continue ;
100+
101+ const reputationMet =
102+ state . resourceBank . reputation >= tierDef . reputationThreshold ;
103+ const challengeMet = tierDef . challengeCondition ( state ) ;
104+
105+ if ( reputationMet || challengeMet ) {
106+ campaign . unlockedTiers . push ( tierDef . id ) ;
107+ campaign . milestoneHistory . push ( {
108+ tierId : tierDef . id ,
109+ triggerType : reputationMet ? 'reputation' : 'challenge' ,
110+ reputationAtUnlock : reputationMet
111+ ? state . resourceBank . reputation
112+ : null ,
113+ challengeIdsAtUnlock : challengeMet
114+ ? [ ...state . challengesCompleted ]
115+ : null ,
116+ runFinalScore : state . finalScore ,
117+ runSeed : state . seed ,
118+ unlockedAt : now ,
119+ } ) ;
120+ }
121+ }
122+
123+ // Derive updated card list from all unlocked tiers
124+ campaign . unlockedCardIds = deriveUnlockedCardIds ( campaign . unlockedTiers ) ;
125+ campaign . lastUpdatedAt = now ;
126+
127+ // Persist if store is provided
128+ if ( store ) {
129+ await saveCampaignProgress ( store , campaign ) ;
130+ }
131+
132+ return campaign ;
133+ }
134+
48135export async function saveTurnStartCheckpoint (
49136 store : SaveLoadStore ,
50137 state : MainStreetState ,
0 commit comments