feat: add post-battle formation review

This commit is contained in:
2026-07-10 22:04:03 +09:00
parent 7ba51d9243
commit 78669d3c6e
4 changed files with 1232 additions and 6 deletions

View File

@@ -44,6 +44,30 @@ export type CampaignRewardSnapshot = {
note?: string;
};
export type CampaignSortieUnitPerformanceSnapshot = {
unitId: string;
hpBefore: number;
maxHpBefore: number;
damageDealt: number;
damageTaken: number;
defeats: number;
actions: number;
support: number;
sortieBonusDamage: number;
sortiePreventedDamage: number;
sortieBonusHealing: number;
sortieExtendedBondTriggers: number;
intentDefeats: number;
intentLineBreaks: number;
intentGuardSuccesses: number;
};
export type CampaignSortiePerformanceSnapshot = {
selectedUnitIds: string[];
formationAssignments: SortieFormationAssignments;
unitStats: CampaignSortieUnitPerformanceSnapshot[];
};
export type FirstBattleReport = {
battleId: string;
battleTitle: string;
@@ -58,6 +82,7 @@ export type FirstBattleReport = {
mvp?: CampMvpSnapshot;
itemRewards: string[];
campaignRewards?: CampaignRewardSnapshot;
sortiePerformance?: CampaignSortiePerformanceSnapshot;
completedCampDialogues: string[];
completedCampVisits: string[];
createdAt: string;
@@ -320,6 +345,7 @@ export type CampaignBattleSettlement = {
units: CampaignUnitProgressSnapshot[];
bonds: CampaignBondProgressSnapshot[];
reserveTraining?: CampaignReserveTrainingSnapshot[];
sortiePerformance?: CampaignSortiePerformanceSnapshot;
completedAt: string;
};
@@ -1117,11 +1143,16 @@ function normalizeLatestBattleId(value: unknown, battleHistory: Record<string, C
}
function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rosterUnitIds: Set<string>): FirstBattleReport {
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(report.sortiePerformance, rosterUnitIds);
const filtered = {
...report,
units: report.units.filter((unit) => unit.faction !== 'ally' || rosterUnitIds.has(unit.id)),
bonds: report.bonds.filter((bond) => bond.unitIds.every((unitId) => rosterUnitIds.has(unitId)))
bonds: report.bonds.filter((bond) => bond.unitIds.every((unitId) => rosterUnitIds.has(unitId))),
...(sortiePerformance ? { sortiePerformance } : {})
};
if (!sortiePerformance) {
delete filtered.sortiePerformance;
}
if (filtered.mvp && !rosterUnitIds.has(filtered.mvp.unitId)) {
delete filtered.mvp;
}
@@ -1134,12 +1165,17 @@ function filterBattleHistoryRosterReferences(
bondIds: Set<string>
): Record<string, CampaignBattleSettlement> {
return Object.entries(history).reduce<Record<string, CampaignBattleSettlement>>((filteredHistory, [battleId, settlement]) => {
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(settlement.sortiePerformance, rosterUnitIds);
filteredHistory[battleId] = {
...settlement,
units: settlement.units.filter((unit) => rosterUnitIds.has(unit.unitId)),
bonds: bondIds.size > 0 ? settlement.bonds.filter((bond) => bondIds.has(bond.id)) : settlement.bonds,
reserveTraining: settlement.reserveTraining?.filter((entry) => rosterUnitIds.has(entry.unitId))
reserveTraining: settlement.reserveTraining?.filter((entry) => rosterUnitIds.has(entry.unitId)),
...(sortiePerformance ? { sortiePerformance } : {})
};
if (!sortiePerformance) {
delete filteredHistory[battleId].sortiePerformance;
}
return filteredHistory;
}, {});
}
@@ -1163,6 +1199,8 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
return undefined;
}
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(settlement.sortiePerformance);
return {
battleId,
battleTitle,
@@ -1174,6 +1212,7 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
units: normalizeLimitedArray(settlement.units, normalizeCampaignUnitProgressSnapshot, maxCampaignBattleUnitEntries),
bonds: normalizeLimitedArray(settlement.bonds, normalizeCampaignBondProgressSnapshot, maxCampaignBattleBondEntries),
reserveTraining: normalizeLimitedArray(settlement.reserveTraining, normalizeReserveTrainingSnapshot, maxCampaignReserveTrainingEntries),
...(sortiePerformance ? { sortiePerformance } : {}),
completedAt
};
}
@@ -1355,6 +1394,60 @@ export function normalizeCampaignSortieFormationPresets(value: unknown, allowedU
}, {});
}
export function normalizeCampaignSortiePerformanceSnapshot(
value: unknown,
allowedUnitIds?: Set<string>
): CampaignSortiePerformanceSnapshot | undefined {
if (!isPlainObject(value)) {
return undefined;
}
const selectedUnitIds = uniqueStrings(value.selectedUnitIds)
.filter((unitId) => !allowedUnitIds || allowedUnitIds.has(unitId))
.slice(0, maxCampaignSortieAssignmentUnits);
if (selectedUnitIds.length === 0) {
return undefined;
}
const selectedIdSet = new Set(selectedUnitIds);
const unitStats = uniqueByKey(
normalizeLimitedArray(value.unitStats, (entry): CampaignSortieUnitPerformanceSnapshot | undefined => {
if (!isPlainObject(entry)) {
return undefined;
}
const unitId = normalizeKeyString(entry.unitId);
if (!unitId || !selectedIdSet.has(unitId)) {
return undefined;
}
const hpBefore = normalizeNonNegativeInteger(entry.hpBefore);
return {
unitId,
hpBefore,
maxHpBefore: Math.max(hpBefore, normalizeNonNegativeInteger(entry.maxHpBefore)),
damageDealt: normalizeNonNegativeInteger(entry.damageDealt),
damageTaken: normalizeNonNegativeInteger(entry.damageTaken),
defeats: normalizeNonNegativeInteger(entry.defeats),
actions: normalizeNonNegativeInteger(entry.actions),
support: normalizeNonNegativeInteger(entry.support),
sortieBonusDamage: normalizeNonNegativeInteger(entry.sortieBonusDamage),
sortiePreventedDamage: normalizeNonNegativeInteger(entry.sortiePreventedDamage),
sortieBonusHealing: normalizeNonNegativeInteger(entry.sortieBonusHealing),
sortieExtendedBondTriggers: normalizeNonNegativeInteger(entry.sortieExtendedBondTriggers),
intentDefeats: normalizeNonNegativeInteger(entry.intentDefeats),
intentLineBreaks: normalizeNonNegativeInteger(entry.intentLineBreaks),
intentGuardSuccesses: normalizeNonNegativeInteger(entry.intentGuardSuccesses)
};
}, maxCampaignSortieAssignmentUnits),
(entry) => entry.unitId
);
return {
selectedUnitIds,
formationAssignments: normalizeSortieFormationAssignments(recordOrEmpty(value.formationAssignments), selectedIdSet),
unitStats
};
}
function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrainingFocusId {
return isReserveTrainingFocusId(focusId)
? focusId as CampaignReserveTrainingFocusId
@@ -1392,6 +1485,8 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
return undefined;
}
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(report.sortiePerformance);
return {
battleId,
battleTitle,
@@ -1409,6 +1504,7 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
isPlainObject(report.campaignRewards) ? report.campaignRewards as CampaignRewardSnapshot : undefined,
battleId
),
...(sortiePerformance ? { sortiePerformance } : {}),
completedCampDialogues: uniqueStrings(report.completedCampDialogues),
completedCampVisits: uniqueStrings(report.completedCampVisits),
createdAt: normalizeCampaignTimestamp(report.createdAt) ?? new Date().toISOString()
@@ -1643,6 +1739,15 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
battleExp: bond.battleExp
})),
reserveTraining: reserveTraining.map((entry) => ({ ...entry })),
...(report.sortiePerformance
? {
sortiePerformance: {
selectedUnitIds: [...report.sortiePerformance.selectedUnitIds],
formationAssignments: { ...report.sortiePerformance.formationAssignments },
unitStats: report.sortiePerformance.unitStats.map((stats) => ({ ...stats }))
}
}
: {}),
completedAt: report.createdAt
};
}