Recover malformed first battle reports
This commit is contained in:
@@ -207,16 +207,76 @@ try {
|
||||
firstBattleReport: {
|
||||
battleId: 'first-battle-zhuo-commandery',
|
||||
outcome: 'victory',
|
||||
completedCampVisits: { corrupted: true }
|
||||
turnNumber: '4',
|
||||
rewardGold: '120',
|
||||
defeatedEnemies: '3',
|
||||
totalEnemies: '5',
|
||||
objectives: [
|
||||
{ id: 'village', label: 'Village', achieved: true, detail: 12, rewardGold: '80' },
|
||||
{ id: '', label: 'Broken', achieved: true }
|
||||
],
|
||||
units: [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
name: 'Liu Bei',
|
||||
faction: 'ally',
|
||||
className: 'Lord',
|
||||
classKey: 'lord',
|
||||
level: '5',
|
||||
exp: '15',
|
||||
hp: '41',
|
||||
maxHp: '44',
|
||||
attack: '21',
|
||||
move: '5',
|
||||
stats: { might: '18', intelligence: '17', leadership: '20', agility: '12', luck: '11' },
|
||||
equipment: {},
|
||||
x: '2',
|
||||
y: '3'
|
||||
},
|
||||
{ id: 'broken-unit', name: 'Broken' }
|
||||
],
|
||||
bonds: [
|
||||
{ id: 'liu-bei__guan-yu', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: '2', exp: '30', battleExp: '4' },
|
||||
{ id: 'broken-bond', title: 'Broken Bond', unitIds: ['liu-bei'] }
|
||||
],
|
||||
mvp: { unitId: 'liu-bei', name: 'Liu Bei', damageDealt: '88', defeats: '2' },
|
||||
itemRewards: ['Bean', 'Bean', 10],
|
||||
campaignRewards: {
|
||||
supplies: ['Bean', 'Bean', 20],
|
||||
equipment: ['Iron Sword'],
|
||||
reputation: ['Village Thanks'],
|
||||
recruits: [{ unitId: 'guan-yu', name: 'Guan Yu' }, { unitId: '', name: 'Broken' }],
|
||||
unlocks: [{ battleId: 'second-battle-yellow-turban-pursuit', title: 'Next' }, { battleId: '', title: 'Broken' }]
|
||||
},
|
||||
completedCampDialogues: ['intro', 'intro', 7],
|
||||
completedCampVisits: { corrupted: true },
|
||||
createdAt: 42
|
||||
}
|
||||
})
|
||||
);
|
||||
const malformedReport = loadCampaignState();
|
||||
assert(
|
||||
malformedReport.step === 'fourth-camp' && malformedReport.gold === 320,
|
||||
`Expected malformed report to be dropped without losing campaign progress: ${JSON.stringify(malformedReport)}`
|
||||
`Expected report recovery to keep campaign progress: ${JSON.stringify(malformedReport)}`
|
||||
);
|
||||
assert(
|
||||
malformedReport.firstBattleReport?.battleId === 'first-battle-zhuo-commandery' &&
|
||||
malformedReport.firstBattleReport.battleTitle.length > 0 &&
|
||||
malformedReport.firstBattleReport.turnNumber === 4 &&
|
||||
malformedReport.firstBattleReport.objectives.length === 1 &&
|
||||
malformedReport.firstBattleReport.objectives[0].rewardGold === 80 &&
|
||||
malformedReport.firstBattleReport.units.length === 1 &&
|
||||
malformedReport.firstBattleReport.units[0].level === 5 &&
|
||||
malformedReport.firstBattleReport.bonds.length === 1 &&
|
||||
malformedReport.firstBattleReport.bonds[0].battleExp === 4 &&
|
||||
malformedReport.firstBattleReport.mvp?.damageDealt === 88 &&
|
||||
malformedReport.firstBattleReport.itemRewards.length === 1 &&
|
||||
malformedReport.firstBattleReport.campaignRewards?.supplies.length === 1 &&
|
||||
malformedReport.firstBattleReport.campaignRewards?.recruits.length === 1 &&
|
||||
malformedReport.firstBattleReport.completedCampDialogues.length === 1 &&
|
||||
malformedReport.firstBattleReport.completedCampVisits.length === 0,
|
||||
`Expected malformed report fields to be normalized without discarding the report: ${JSON.stringify(malformedReport)}`
|
||||
);
|
||||
assert(malformedReport.firstBattleReport === undefined, `Expected malformed report to be discarded: ${JSON.stringify(malformedReport)}`);
|
||||
|
||||
storage.clear();
|
||||
storage.set(
|
||||
|
||||
@@ -1051,11 +1051,107 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return cloneReport(report as FirstBattleReport);
|
||||
} catch {
|
||||
if (
|
||||
typeof report.battleId !== 'string' ||
|
||||
report.battleId.length === 0 ||
|
||||
(report.outcome !== 'victory' && report.outcome !== 'defeat')
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const battleTitle =
|
||||
typeof report.battleTitle === 'string' && report.battleTitle.length > 0
|
||||
? report.battleTitle
|
||||
: battleScenarios[report.battleId as BattleScenarioId]?.title;
|
||||
if (!battleTitle) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
battleId: report.battleId,
|
||||
battleTitle,
|
||||
outcome: report.outcome,
|
||||
turnNumber: normalizeNonNegativeInteger(report.turnNumber),
|
||||
rewardGold: normalizeNonNegativeInteger(report.rewardGold),
|
||||
defeatedEnemies: normalizeNonNegativeInteger(report.defeatedEnemies),
|
||||
totalEnemies: normalizeNonNegativeInteger(report.totalEnemies),
|
||||
objectives: arrayOrEmpty<unknown>(report.objectives)
|
||||
.map(normalizeBattleObjectiveSnapshot)
|
||||
.filter((objective): objective is BattleObjectiveSnapshot => Boolean(objective)),
|
||||
units: arrayOrEmpty<unknown>(report.units)
|
||||
.map(normalizeBattleReportUnitSnapshot)
|
||||
.filter((unit): unit is UnitData => Boolean(unit)),
|
||||
bonds: arrayOrEmpty<unknown>(report.bonds)
|
||||
.map(normalizeCampBondSnapshot)
|
||||
.filter((bond): bond is CampBondSnapshot => Boolean(bond)),
|
||||
mvp: normalizeCampMvpSnapshot(report.mvp),
|
||||
itemRewards: uniqueStrings(report.itemRewards),
|
||||
campaignRewards: cloneCampaignRewardSnapshot(
|
||||
isPlainObject(report.campaignRewards) ? report.campaignRewards as CampaignRewardSnapshot : undefined
|
||||
),
|
||||
completedCampDialogues: uniqueStrings(report.completedCampDialogues),
|
||||
completedCampVisits: uniqueStrings(report.completedCampVisits),
|
||||
createdAt: typeof report.createdAt === 'string' && report.createdAt.length > 0 ? report.createdAt : new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBattleReportUnitSnapshot(value: unknown): UnitData | undefined {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
typeof value.id !== 'string' ||
|
||||
value.id.length === 0 ||
|
||||
typeof value.name !== 'string' ||
|
||||
value.name.length === 0 ||
|
||||
(value.faction !== 'ally' && value.faction !== 'enemy')
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const stats = recordOrEmpty(value.stats);
|
||||
const hp = normalizeNonNegativeInteger(value.hp);
|
||||
const maxHp = Math.max(hp, normalizeNonNegativeInteger(value.maxHp));
|
||||
return {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
faction: value.faction,
|
||||
className: typeof value.className === 'string' ? value.className : '',
|
||||
classKey: typeof value.classKey === 'string' ? value.classKey as UnitData['classKey'] : 'infantry',
|
||||
level: normalizeNonNegativeInteger(value.level),
|
||||
exp: normalizeNonNegativeInteger(value.exp),
|
||||
hp,
|
||||
maxHp,
|
||||
attack: normalizeNonNegativeInteger(value.attack),
|
||||
move: normalizeNonNegativeInteger(value.move),
|
||||
stats: {
|
||||
might: normalizeNonNegativeInteger(stats.might),
|
||||
intelligence: normalizeNonNegativeInteger(stats.intelligence),
|
||||
leadership: normalizeNonNegativeInteger(stats.leadership),
|
||||
agility: normalizeNonNegativeInteger(stats.agility),
|
||||
luck: normalizeNonNegativeInteger(stats.luck)
|
||||
},
|
||||
equipment: recordOrEmpty(value.equipment) as UnitData['equipment'],
|
||||
x: normalizeNonNegativeInteger(value.x),
|
||||
y: normalizeNonNegativeInteger(value.y)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCampMvpSnapshot(value: unknown): CampMvpSnapshot | undefined {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
typeof value.unitId !== 'string' ||
|
||||
value.unitId.length === 0 ||
|
||||
typeof value.name !== 'string' ||
|
||||
value.name.length === 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
unitId: value.unitId,
|
||||
name: value.name,
|
||||
damageDealt: normalizeNonNegativeInteger(value.damageDealt),
|
||||
defeats: normalizeNonNegativeInteger(value.defeats)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCampBondSnapshot(value: unknown): CampBondSnapshot | undefined {
|
||||
|
||||
Reference in New Issue
Block a user