feat: carry first-battle camaraderie into sortie prep

This commit is contained in:
2026-07-27 06:57:33 +09:00
parent b963ef34cb
commit 17486efa8b
10 changed files with 2207 additions and 89 deletions

View File

@@ -133,6 +133,15 @@ export type CampaignSortieRecommendationSnapshot = {
unitReasons: Record<string, string>;
};
export type CampaignSortieCooperationSnapshot = {
version: 1;
bondId: string;
unitIds: [string, string];
attempts: number;
successes: number;
additionalDamage: number;
};
export type CampaignSortieOrderResultSnapshot = SortieOrderResultSnapshot;
export type FirstBattleReport = {
@@ -153,6 +162,7 @@ export type FirstBattleReport = {
sortieReview?: CampaignSortieReviewSnapshot;
sortieOrder?: CampaignSortieOrderResultSnapshot;
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
sortieCooperation?: CampaignSortieCooperationSnapshot;
completedCampDialogues: string[];
completedCampVisits: string[];
createdAt: string;
@@ -436,6 +446,7 @@ export type CampaignBattleSettlement = {
sortieReview?: CampaignSortieReviewSnapshot;
sortieOrder?: CampaignSortieOrderResultSnapshot;
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
sortieCooperation?: CampaignSortieCooperationSnapshot;
completedAt: string;
};
@@ -1759,7 +1770,7 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
normalized.battleHistory = filterBattleHistoryRosterReferences(
normalized.battleHistory,
sortieUnitFilter,
new Set(normalized.bonds.map((bond) => bond.id))
normalized.bonds
);
}
normalized.latestBattleId = normalizeLatestBattleId(normalized.latestBattleId, normalized.battleHistory);
@@ -2131,6 +2142,72 @@ export function normalizeCampaignSortieRecommendationSnapshot(
};
}
type CampaignSortieCooperationNormalizationOptions = {
allowedUnitIds?: ReadonlySet<string>;
allowedBondIds?: ReadonlySet<string>;
bonds?: readonly {
id: string;
unitIds: readonly string[];
}[];
};
export function normalizeCampaignSortieCooperationSnapshot(
value: unknown,
options: CampaignSortieCooperationNormalizationOptions = {}
): CampaignSortieCooperationSnapshot | undefined {
if (!isPlainObject(value) || value.version !== 1) {
return undefined;
}
const bondId = normalizeKeyString(value.bondId);
const rawUnitIds = arrayOrEmpty<unknown>(value.unitIds);
if (!bondId || rawUnitIds.length !== 2) {
return undefined;
}
const unitIds = rawUnitIds.map(normalizeKeyString);
if (
!unitIds[0] ||
!unitIds[1] ||
unitIds[0] === unitIds[1] ||
(options.allowedBondIds && !options.allowedBondIds.has(bondId)) ||
unitIds.some((unitId) => options.allowedUnitIds && !options.allowedUnitIds.has(unitId))
) {
return undefined;
}
const matchingBond = options.bonds?.find((bond) => bond.id === bondId);
if (options.bonds && !matchingBond) {
return undefined;
}
if (
matchingBond &&
(
matchingBond.unitIds.length !== 2 ||
!matchingBond.unitIds.every((unitId) => unitIds.includes(unitId))
)
) {
return undefined;
}
const attempts = normalizeNonNegativeInteger(value.attempts);
if (attempts <= 0) {
return undefined;
}
const normalizedUnitIds = matchingBond
? [matchingBond.unitIds[0], matchingBond.unitIds[1]] as [string, string]
: [unitIds[0], unitIds[1]] as [string, string];
const successes = Math.min(attempts, normalizeNonNegativeInteger(value.successes));
return {
version: 1,
bondId,
unitIds: normalizedUnitIds,
attempts,
successes,
additionalDamage: successes > 0
? normalizeNonNegativeInteger(value.additionalDamage)
: 0
};
}
function resolveCampaignSortieResonanceBond(
battleId: string,
bondId: string,
@@ -2398,14 +2475,21 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
allowedUnitIds: rosterUnitIds
})
: undefined;
const filteredBonds = report.bonds.filter((bond) => bond.unitIds.every((unitId) => rosterUnitIds.has(unitId)));
const sortieCooperation = normalizeCampaignSortieCooperationSnapshot(report.sortieCooperation, {
allowedUnitIds: rosterUnitIds,
allowedBondIds: new Set(filteredBonds.map((bond) => bond.id)),
bonds: filteredBonds
});
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: filteredBonds,
...(sortiePerformance ? { sortiePerformance } : {}),
...(sortieReview ? { sortieReview } : {}),
...(sortieOrder ? { sortieOrder } : {}),
...(sortieRecommendation ? { sortieRecommendation } : {})
...(sortieRecommendation ? { sortieRecommendation } : {}),
...(sortieCooperation ? { sortieCooperation } : {})
};
if (!sortiePerformance) {
delete filtered.sortiePerformance;
@@ -2419,6 +2503,9 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
if (!sortieRecommendation) {
delete filtered.sortieRecommendation;
}
if (!sortieCooperation) {
delete filtered.sortieCooperation;
}
if (filtered.mvp && !rosterUnitIds.has(filtered.mvp.unitId)) {
delete filtered.mvp;
}
@@ -2428,8 +2515,9 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
function filterBattleHistoryRosterReferences(
history: Record<string, CampaignBattleSettlement>,
rosterUnitIds: Set<string>,
bondIds: Set<string>
campaignBonds: readonly CampBondSnapshot[]
): Record<string, CampaignBattleSettlement> {
const bondIds = new Set(campaignBonds.map((bond) => bond.id));
return Object.entries(history).reduce<Record<string, CampaignBattleSettlement>>((filteredHistory, [battleId, settlement]) => {
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(settlement.sortiePerformance, rosterUnitIds);
const sortiePerformanceUnchanged = sortiePerformanceSnapshotsShareRoster(settlement.sortiePerformance, sortiePerformance);
@@ -2448,6 +2536,11 @@ function filterBattleHistoryRosterReferences(
allowedUnitIds: rosterUnitIds
})
: undefined;
const sortieCooperation = normalizeCampaignSortieCooperationSnapshot(settlement.sortieCooperation, {
allowedUnitIds: rosterUnitIds,
allowedBondIds: bondIds,
bonds: campaignBonds
});
filteredHistory[battleId] = {
...settlement,
units: settlement.units.filter((unit) => rosterUnitIds.has(unit.unitId)),
@@ -2456,7 +2549,8 @@ function filterBattleHistoryRosterReferences(
...(sortiePerformance ? { sortiePerformance } : {}),
...(sortieReview ? { sortieReview } : {}),
...(sortieOrder ? { sortieOrder } : {}),
...(sortieRecommendation ? { sortieRecommendation } : {})
...(sortieRecommendation ? { sortieRecommendation } : {}),
...(sortieCooperation ? { sortieCooperation } : {})
};
if (!sortiePerformance) {
delete filteredHistory[battleId].sortiePerformance;
@@ -2470,6 +2564,9 @@ function filterBattleHistoryRosterReferences(
if (!sortieRecommendation) {
delete filteredHistory[battleId].sortieRecommendation;
}
if (!sortieCooperation) {
delete filteredHistory[battleId].sortieCooperation;
}
return filteredHistory;
}, {});
}
@@ -2505,6 +2602,20 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
turnNumber
})
: undefined;
const units = normalizeLimitedArray(
settlement.units,
normalizeCampaignUnitProgressSnapshot,
maxCampaignBattleUnitEntries
);
const bonds = normalizeLimitedArray(
settlement.bonds,
normalizeCampaignBondProgressSnapshot,
maxCampaignBattleBondEntries
);
const sortieCooperation = normalizeCampaignSortieCooperationSnapshot(settlement.sortieCooperation, {
allowedUnitIds: new Set(units.map((unit) => unit.unitId)),
allowedBondIds: new Set(bonds.map((bond) => bond.id))
});
return {
battleId,
@@ -2515,13 +2626,14 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
itemRewards: normalizeRewardStrings(settlement.itemRewards),
campaignRewards: cloneCampaignRewardSnapshot(settlement.campaignRewards, battleId),
objectives: normalizeLimitedArray(settlement.objectives, normalizeBattleObjectiveSnapshot, maxCampaignBattleObjectiveEntries),
units: normalizeLimitedArray(settlement.units, normalizeCampaignUnitProgressSnapshot, maxCampaignBattleUnitEntries),
bonds: normalizeLimitedArray(settlement.bonds, normalizeCampaignBondProgressSnapshot, maxCampaignBattleBondEntries),
units,
bonds,
reserveTraining: normalizeLimitedArray(settlement.reserveTraining, normalizeReserveTrainingSnapshot, maxCampaignReserveTrainingEntries),
...(sortiePerformance ? { sortiePerformance } : {}),
...(sortieReview ? { sortieReview } : {}),
...(sortieOrder ? { sortieOrder } : {}),
...(sortieRecommendation ? { sortieRecommendation } : {}),
...(sortieCooperation ? { sortieCooperation } : {}),
completedAt
};
}
@@ -2856,6 +2968,23 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
turnNumber
})
: undefined;
const units = normalizeLimitedArray(
report.units,
normalizeUnitDataSnapshot,
maxCampaignBattleUnitEntries
);
const bonds = normalizeLimitedArray(
report.bonds,
normalizeCampBondSnapshot,
maxCampaignBattleBondEntries
);
const sortieCooperation = normalizeCampaignSortieCooperationSnapshot(report.sortieCooperation, {
allowedUnitIds: new Set(
units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id)
),
allowedBondIds: new Set(bonds.map((bond) => bond.id)),
bonds
});
return {
battleId,
@@ -2866,8 +2995,8 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
defeatedEnemies: normalizeNonNegativeInteger(report.defeatedEnemies),
totalEnemies: normalizeNonNegativeInteger(report.totalEnemies),
objectives: normalizeLimitedArray(report.objectives, normalizeBattleObjectiveSnapshot, maxCampaignBattleObjectiveEntries),
units: normalizeLimitedArray(report.units, normalizeUnitDataSnapshot, maxCampaignBattleUnitEntries),
bonds: normalizeLimitedArray(report.bonds, normalizeCampBondSnapshot, maxCampaignBattleBondEntries),
units,
bonds,
mvp: normalizeCampMvpSnapshot(report.mvp),
itemRewards: normalizeRewardStrings(report.itemRewards),
campaignRewards: cloneCampaignRewardSnapshot(
@@ -2878,6 +3007,7 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
...(sortieReview ? { sortieReview } : {}),
...(sortieOrder ? { sortieOrder } : {}),
...(sortieRecommendation ? { sortieRecommendation } : {}),
...(sortieCooperation ? { sortieCooperation } : {}),
completedCampDialogues: uniqueCampHistoryIds(report.completedCampDialogues),
completedCampVisits: uniqueCampHistoryIds(report.completedCampVisits),
createdAt: normalizeCampaignTimestamp(report.createdAt) ?? new Date().toISOString()
@@ -3142,6 +3272,9 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
...(report.sortieRecommendation
? { sortieRecommendation: cloneCampaignSortieRecommendationSnapshot(report.sortieRecommendation) }
: {}),
...(report.sortieCooperation
? { sortieCooperation: cloneCampaignSortieCooperationSnapshot(report.sortieCooperation) }
: {}),
completedAt: report.createdAt
};
}
@@ -3302,6 +3435,9 @@ function cloneReport(report: FirstBattleReport): FirstBattleReport {
if (cloned.campaignRewards) {
cloned.campaignRewards = cloneCampaignRewardSnapshot(cloned.campaignRewards, cloned.battleId);
}
if (cloned.sortieCooperation) {
cloned.sortieCooperation = cloneCampaignSortieCooperationSnapshot(cloned.sortieCooperation);
}
return cloned;
}
@@ -3314,6 +3450,13 @@ export function cloneCampaignSortieRecommendationSnapshot(snapshot: CampaignSort
} satisfies CampaignSortieRecommendationSnapshot;
}
export function cloneCampaignSortieCooperationSnapshot(snapshot: CampaignSortieCooperationSnapshot) {
return {
...snapshot,
unitIds: [...snapshot.unitIds] as [string, string]
} satisfies CampaignSortieCooperationSnapshot;
}
function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot, battleId?: string): CampaignRewardSnapshot | undefined {
if (!rewards) {
return undefined;