Prepare reserve drill assignment state

This commit is contained in:
2026-07-09 13:36:59 +09:00
parent 6351c7b285
commit 053536fd20
2 changed files with 86 additions and 2 deletions

View File

@@ -32,6 +32,7 @@ try {
getCampaignState, getCampaignState,
hasCampaignSave, hasCampaignSave,
loadCampaignState, loadCampaignState,
normalizeCampaignReserveTrainingAssignments,
normalizeCampaignSortieItemAssignments, normalizeCampaignSortieItemAssignments,
resetCampaignState, resetCampaignState,
setCampaignState, setCampaignState,
@@ -104,6 +105,35 @@ try {
cappedLaunchFormation['unit-96'] === undefined, cappedLaunchFormation['unit-96'] === undefined,
`Expected launch sortie formation assignments without a roster filter to stay capped: ${JSON.stringify(cappedLaunchFormation)}` `Expected launch sortie formation assignments without a roster filter to stay capped: ${JSON.stringify(cappedLaunchFormation)}`
); );
const normalizedReserveDrills = normalizeCampaignReserveTrainingAssignments(
{
'liu-bei': 'bond-practice',
' liu-bei ': 'class-practice',
'guan-yu': 'class-practice',
'ghost-unit': 'balanced',
'zhang-fei': 'broken-focus',
['x'.repeat(97)]: 'balanced',
'': 'balanced'
},
new Set(['liu-bei', 'guan-yu'])
);
assert(
normalizedReserveDrills['liu-bei'] === 'bond-practice' &&
normalizedReserveDrills['guan-yu'] === 'class-practice' &&
normalizedReserveDrills['ghost-unit'] === undefined &&
normalizedReserveDrills['zhang-fei'] === undefined &&
normalizedReserveDrills['x'.repeat(97)] === undefined,
`Expected reserve drill assignments to trim, filter, and preserve first valid focus ids: ${JSON.stringify(normalizedReserveDrills)}`
);
const cappedReserveDrills = normalizeCampaignReserveTrainingAssignments(
Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'balanced']))
);
assert(
Object.keys(cappedReserveDrills).length === 96 &&
cappedReserveDrills['unit-95'] === 'balanced' &&
cappedReserveDrills['unit-96'] === undefined,
`Expected reserve drill assignments without a roster filter to stay capped: ${JSON.stringify(cappedReserveDrills)}`
);
resetCampaignState(); resetCampaignState();
const firstScenario = battleScenarios['first-battle-zhuo-commandery']; const firstScenario = battleScenarios['first-battle-zhuo-commandery'];
@@ -325,7 +355,8 @@ try {
assert( assert(
Array.isArray(legacy.selectedSortieUnitIds) && Array.isArray(legacy.selectedSortieUnitIds) &&
typeof legacy.sortieFormationAssignments === 'object' && typeof legacy.sortieFormationAssignments === 'object' &&
typeof legacy.sortieItemAssignments === 'object', typeof legacy.sortieItemAssignments === 'object' &&
typeof legacy.reserveTrainingAssignments === 'object',
`Expected modern sortie fields to be restored: ${JSON.stringify(legacy)}` `Expected modern sortie fields to be restored: ${JSON.stringify(legacy)}`
); );
@@ -360,6 +391,13 @@ try {
}, },
selectedSortieUnitIds: { corrupted: true }, selectedSortieUnitIds: { corrupted: true },
sortieFormationAssignments: 'front', sortieFormationAssignments: 'front',
reserveTrainingFocus: 'ghost-focus',
reserveTrainingAssignments: {
'liu-bei': 'bond-practice',
'guan-yu': 'unknown-focus',
['x'.repeat(97)]: 'balanced',
'': 'balanced'
},
sortieItemAssignments: { sortieItemAssignments: {
'liu-bei': { ' bean ': '5', wine: -1, salve: '1', stone: '9', ['x'.repeat(97)]: '1' }, 'liu-bei': { ' bean ': '5', wine: -1, salve: '1', stone: '9', ['x'.repeat(97)]: '1' },
'guan-yu': '2', 'guan-yu': '2',
@@ -407,6 +445,13 @@ try {
Object.keys(malformed.sortieFormationAssignments).length === 0, Object.keys(malformed.sortieFormationAssignments).length === 0,
`Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` `Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}`
); );
assert(
malformed.reserveTrainingFocus === 'balanced' &&
malformed.reserveTrainingAssignments['liu-bei'] === 'bond-practice' &&
malformed.reserveTrainingAssignments['guan-yu'] === undefined &&
malformed.reserveTrainingAssignments['x'.repeat(97)] === undefined,
`Expected malformed reserve drill focus fields to normalize: ${JSON.stringify(malformed)}`
);
storage.clear(); storage.clear();
storage.set( storage.set(
@@ -445,6 +490,11 @@ try {
'guan-yu': 'support', 'guan-yu': 'support',
broken: 'center' broken: 'center'
}, },
reserveTrainingAssignments: {
'liu-bei': 'bond-practice',
'ghost-unit': 'balanced',
'guan-yu': 'class-practice'
},
sortieItemAssignments: { sortieItemAssignments: {
'liu-bei': { bean: '2' }, 'liu-bei': { bean: '2' },
'ghost-unit': { bean: '1' }, 'ghost-unit': { bean: '1' },
@@ -458,6 +508,9 @@ try {
staleSortie.selectedSortieUnitIds[0] === 'liu-bei' && staleSortie.selectedSortieUnitIds[0] === 'liu-bei' &&
staleSortie.sortieFormationAssignments['liu-bei'] === 'front' && staleSortie.sortieFormationAssignments['liu-bei'] === 'front' &&
staleSortie.sortieFormationAssignments['ghost-unit'] === undefined && staleSortie.sortieFormationAssignments['ghost-unit'] === undefined &&
staleSortie.reserveTrainingAssignments['liu-bei'] === 'bond-practice' &&
staleSortie.reserveTrainingAssignments['ghost-unit'] === undefined &&
staleSortie.reserveTrainingAssignments['guan-yu'] === undefined &&
staleSortie.sortieItemAssignments['liu-bei']?.bean === 2 && staleSortie.sortieItemAssignments['liu-bei']?.bean === 2 &&
staleSortie.sortieItemAssignments['ghost-unit'] === undefined && staleSortie.sortieItemAssignments['ghost-unit'] === undefined &&
staleSortie.sortieItemAssignments['guan-yu'] === undefined, staleSortie.sortieItemAssignments['guan-yu'] === undefined,

View File

@@ -246,6 +246,8 @@ export type CampaignReserveTrainingFocusDefinition = {
bondExpGained: number; bondExpGained: number;
}; };
export type CampaignReserveTrainingAssignments = Partial<Record<string, CampaignReserveTrainingFocusId>>;
export const defaultCampaignReserveTrainingFocusId: CampaignReserveTrainingFocusId = 'balanced'; export const defaultCampaignReserveTrainingFocusId: CampaignReserveTrainingFocusId = 'balanced';
const defaultEquipmentBySlot: Record<EquipmentSlot, string> = { const defaultEquipmentBySlot: Record<EquipmentSlot, string> = {
@@ -274,6 +276,7 @@ const maxCampaignBattleObjectiveEntries = 24;
const maxCampaignBattleUnitEntries = 96; const maxCampaignBattleUnitEntries = 96;
const maxCampaignBattleBondEntries = 96; const maxCampaignBattleBondEntries = 96;
const maxCampaignReserveTrainingEntries = 96; const maxCampaignReserveTrainingEntries = 96;
const maxCampaignReserveTrainingAssignmentUnits = 96;
const maxCampaignSortieAssignmentUnits = 96; const maxCampaignSortieAssignmentUnits = 96;
const maxCampaignDisplayTextLength = 180; const maxCampaignDisplayTextLength = 180;
const maxCampaignNumericValue = 999999; const maxCampaignNumericValue = 999999;
@@ -335,6 +338,7 @@ export type CampaignState = {
sortieFormationAssignments: SortieFormationAssignments; sortieFormationAssignments: SortieFormationAssignments;
sortieItemAssignments: CampaignSortieItemAssignments; sortieItemAssignments: CampaignSortieItemAssignments;
reserveTrainingFocus: CampaignReserveTrainingFocusId; reserveTrainingFocus: CampaignReserveTrainingFocusId;
reserveTrainingAssignments: CampaignReserveTrainingAssignments;
completedCampDialogues: string[]; completedCampDialogues: string[];
completedCampVisits: string[]; completedCampVisits: string[];
battleHistory: Record<string, CampaignBattleSettlement>; battleHistory: Record<string, CampaignBattleSettlement>;
@@ -535,6 +539,7 @@ export function createInitialCampaignState(): CampaignState {
sortieFormationAssignments: {}, sortieFormationAssignments: {},
sortieItemAssignments: {}, sortieItemAssignments: {},
reserveTrainingFocus: defaultCampaignReserveTrainingFocusId, reserveTrainingFocus: defaultCampaignReserveTrainingFocusId,
reserveTrainingAssignments: {},
completedCampDialogues: [], completedCampDialogues: [],
completedCampVisits: [], completedCampVisits: [],
battleHistory: {} battleHistory: {}
@@ -623,6 +628,27 @@ export function setCampaignReserveTrainingFocus(focusId: CampaignReserveTraining
return cloneCampaignState(state); return cloneCampaignState(state);
} }
export function normalizeCampaignReserveTrainingAssignments(
assignments?: Record<string, unknown>,
allowedUnitIds?: Set<string>
): CampaignReserveTrainingAssignments {
return Object.entries(assignments ?? {}).reduce<CampaignReserveTrainingAssignments>((next, [unitId, focusId]) => {
const normalizedUnitId = normalizeKeyString(unitId);
if (
!normalizedUnitId ||
next[normalizedUnitId] ||
(allowedUnitIds && !allowedUnitIds.has(normalizedUnitId)) ||
(!allowedUnitIds && Object.keys(next).length >= maxCampaignReserveTrainingAssignmentUnits) ||
!isReserveTrainingFocusId(focusId)
) {
return next;
}
next[normalizedUnitId] = focusId;
return next;
}, {});
}
export function setFirstBattleReport(report: FirstBattleReport) { export function setFirstBattleReport(report: FirstBattleReport) {
const state = ensureCampaignState(); const state = ensureCampaignState();
const reportClone = cloneReport(report); const reportClone = cloneReport(report);
@@ -867,6 +893,7 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
normalized.sortieFormationAssignments = normalizeSortieFormationAssignments(recordOrEmpty(normalized.sortieFormationAssignments), sortieUnitFilter); normalized.sortieFormationAssignments = normalizeSortieFormationAssignments(recordOrEmpty(normalized.sortieFormationAssignments), sortieUnitFilter);
normalized.sortieItemAssignments = normalizeCampaignSortieItemAssignments(recordOrEmpty(normalized.sortieItemAssignments), sortieUnitFilter); normalized.sortieItemAssignments = normalizeCampaignSortieItemAssignments(recordOrEmpty(normalized.sortieItemAssignments), sortieUnitFilter);
normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus); normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
normalized.reserveTrainingAssignments = normalizeCampaignReserveTrainingAssignments(recordOrEmpty(normalized.reserveTrainingAssignments), sortieUnitFilter);
normalized.battleHistory = normalizeBattleHistory(normalized.battleHistory); normalized.battleHistory = normalizeBattleHistory(normalized.battleHistory);
if (sortieUnitFilter) { if (sortieUnitFilter) {
normalized.battleHistory = filterBattleHistoryRosterReferences( normalized.battleHistory = filterBattleHistoryRosterReferences(
@@ -1256,11 +1283,15 @@ export function normalizeCampaignSortieItemAssignments(assignments?: Record<stri
} }
function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrainingFocusId { function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrainingFocusId {
return campaignReserveTrainingFocusDefinitions.some((focus) => focus.id === focusId) return isReserveTrainingFocusId(focusId)
? focusId as CampaignReserveTrainingFocusId ? focusId as CampaignReserveTrainingFocusId
: defaultCampaignReserveTrainingFocusId; : defaultCampaignReserveTrainingFocusId;
} }
function isReserveTrainingFocusId(value: unknown): value is CampaignReserveTrainingFocusId {
return typeof value === 'string' && campaignReserveTrainingFocusDefinitions.some((focus) => focus.id === value);
}
export function isCampaignStep(value: unknown): value is CampaignStep { export function isCampaignStep(value: unknown): value is CampaignStep {
return typeof value === 'string' && campaignStepIds.has(value as CampaignStep); return typeof value === 'string' && campaignStepIds.has(value as CampaignStep);
} }