From 02578e2f2883c3f71e4391c6db0354adb4b9e49c Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 9 Jul 2026 13:38:20 +0900 Subject: [PATCH] Add reserve drill assignment API --- .../verify-campaign-save-normalization.mjs | 21 +++++++++++++++++++ src/game/state/campaignState.ts | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index ec09a57..bcad6ae 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -36,6 +36,7 @@ try { normalizeCampaignSortieItemAssignments, resetCampaignState, setCampaignState, + setCampaignReserveTrainingAssignment, setFirstBattleReport } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); @@ -134,6 +135,26 @@ try { cappedReserveDrills['unit-96'] === undefined, `Expected reserve drill assignments without a roster filter to stay capped: ${JSON.stringify(cappedReserveDrills)}` ); + storage.clear(); + storage.set( + campaignStorageKey, + JSON.stringify({ + version: 1, + updatedAt: '2026-07-03T11:30:00.000Z', + step: 'third-camp', + roster: [{ id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }] + }) + ); + loadCampaignState(); + const assignedReserveDrill = setCampaignReserveTrainingAssignment(' liu-bei ', 'class-practice'); + const ignoredReserveDrill = setCampaignReserveTrainingAssignment('ghost-unit', 'bond-practice'); + const clearedReserveDrill = setCampaignReserveTrainingAssignment('liu-bei'); + assert( + assignedReserveDrill.reserveTrainingAssignments['liu-bei'] === 'class-practice' && + ignoredReserveDrill.reserveTrainingAssignments['ghost-unit'] === undefined && + clearedReserveDrill.reserveTrainingAssignments['liu-bei'] === undefined, + `Expected reserve drill assignment API to trim, filter, and clear per-officer focus ids: ${JSON.stringify({ assignedReserveDrill, ignoredReserveDrill, clearedReserveDrill })}` + ); resetCampaignState(); const firstScenario = battleScenarios['first-battle-zhuo-commandery']; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 002daa2..3f03591 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -628,6 +628,27 @@ export function setCampaignReserveTrainingFocus(focusId: CampaignReserveTraining return cloneCampaignState(state); } +export function setCampaignReserveTrainingAssignment(unitId: string, focusId?: CampaignReserveTrainingFocusId) { + const state = ensureCampaignState(); + const normalizedUnitId = normalizeKeyString(unitId); + const rosterUnitIds = normalizedRosterUnitIds(state.roster); + const allowedUnitIds = rosterUnitIds.size > 0 ? rosterUnitIds : undefined; + if (!normalizedUnitId || (allowedUnitIds && !allowedUnitIds.has(normalizedUnitId))) { + return cloneCampaignState(state); + } + + const assignments = normalizeCampaignReserveTrainingAssignments(state.reserveTrainingAssignments, allowedUnitIds); + if (focusId && isReserveTrainingFocusId(focusId)) { + assignments[normalizedUnitId] = focusId; + } else { + delete assignments[normalizedUnitId]; + } + state.reserveTrainingAssignments = assignments; + state.updatedAt = new Date().toISOString(); + persistCampaignState(state); + return cloneCampaignState(state); +} + export function normalizeCampaignReserveTrainingAssignments( assignments?: Record, allowedUnitIds?: Set