feat: connect sortie recommendations to battle results
This commit is contained in:
@@ -4,6 +4,10 @@ import { battleScenarios, type BattleScenarioId } from '../data/battles';
|
||||
import { campaignRecruitUnits, type BattleBond, type UnitData } from '../data/scenario';
|
||||
import { normalizeSortieFormationAssignments, type SortieFormationAssignments } from '../data/sortieDeployment';
|
||||
import { coreSortieResonanceMinLevel } from '../data/sortieSynergy';
|
||||
import {
|
||||
sortieRecommendationPlanIds,
|
||||
type SortieRecommendationPlanId
|
||||
} from '../data/sortieRecommendations';
|
||||
import {
|
||||
isSortieOrderId,
|
||||
sortieOrderCheckIdsByOrder,
|
||||
@@ -64,6 +68,8 @@ export type CampaignSortieUnitPerformanceSnapshot = {
|
||||
defeats: number;
|
||||
actions: number;
|
||||
support: number;
|
||||
strategyUses: number;
|
||||
signatureStrategyUses: number;
|
||||
sortieBonusDamage: number;
|
||||
sortiePreventedDamage: number;
|
||||
sortieBonusHealing: number;
|
||||
@@ -90,6 +96,19 @@ export type CampaignSortieReviewSnapshot = {
|
||||
sourcePresetIds: CampaignSortiePresetId[];
|
||||
};
|
||||
|
||||
export type CampaignSortieRecommendationSnapshot = {
|
||||
version: 1;
|
||||
battleId: BattleScenarioId;
|
||||
planId: SortieRecommendationPlanId;
|
||||
label: string;
|
||||
summary: string;
|
||||
sortieOrderId?: SortieOrderId;
|
||||
sortieResonanceBondId?: string;
|
||||
selectedUnitIds: string[];
|
||||
formationAssignments: SortieFormationAssignments;
|
||||
unitReasons: Record<string, string>;
|
||||
};
|
||||
|
||||
export type CampaignSortieOrderResultSnapshot = SortieOrderResultSnapshot;
|
||||
|
||||
export type FirstBattleReport = {
|
||||
@@ -109,6 +128,7 @@ export type FirstBattleReport = {
|
||||
sortiePerformance?: CampaignSortiePerformanceSnapshot;
|
||||
sortieReview?: CampaignSortieReviewSnapshot;
|
||||
sortieOrder?: CampaignSortieOrderResultSnapshot;
|
||||
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
|
||||
completedCampDialogues: string[];
|
||||
completedCampVisits: string[];
|
||||
createdAt: string;
|
||||
@@ -376,6 +396,7 @@ export type CampaignBattleSettlement = {
|
||||
sortiePerformance?: CampaignSortiePerformanceSnapshot;
|
||||
sortieReview?: CampaignSortieReviewSnapshot;
|
||||
sortieOrder?: CampaignSortieOrderResultSnapshot;
|
||||
sortieRecommendation?: CampaignSortieRecommendationSnapshot;
|
||||
completedAt: string;
|
||||
};
|
||||
|
||||
@@ -420,6 +441,7 @@ export type CampaignState = {
|
||||
sortieFormationPresets: CampaignSortieFormationPresets;
|
||||
sortieOrderSelection?: CampaignSortieOrderSelection;
|
||||
sortieResonanceSelection?: CampaignSortieResonanceSelection;
|
||||
sortieRecommendationSelection?: CampaignSortieRecommendationSnapshot;
|
||||
sortieOrderHistory: CampaignSortieOrderHistory;
|
||||
claimedSortieOrderRewardIds: string[];
|
||||
reserveTrainingFocus: CampaignReserveTrainingFocusId;
|
||||
@@ -816,6 +838,14 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
const state = ensureCampaignState();
|
||||
const reportClone = cloneReport(report);
|
||||
const battleId = reportClone.battleId;
|
||||
const normalizedRecommendation = normalizeCampaignSortieRecommendationSnapshot(reportClone.sortieRecommendation, {
|
||||
expectedBattleId: battleId
|
||||
});
|
||||
if (normalizedRecommendation) {
|
||||
reportClone.sortieRecommendation = normalizedRecommendation;
|
||||
} else {
|
||||
delete reportClone.sortieRecommendation;
|
||||
}
|
||||
const previousSettlement = state.battleHistory[battleId];
|
||||
const completedCampDialogues =
|
||||
battleId === 'first-battle-zhuo-commandery' ? [...reportClone.completedCampDialogues] : [...state.completedCampDialogues];
|
||||
@@ -881,6 +911,21 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
orderId: reportClone.sortieOrder.orderId
|
||||
};
|
||||
}
|
||||
if (reportClone.sortieRecommendation) {
|
||||
state.sortieRecommendationSelection = cloneCampaignSortieRecommendationSnapshot(reportClone.sortieRecommendation);
|
||||
const resonanceBondId = reportClone.sortieRecommendation.sortieResonanceBondId;
|
||||
const resonanceBond = resonanceBondId
|
||||
? resolveCampaignSortieResonanceBond(battleId, resonanceBondId, state.bonds)
|
||||
: undefined;
|
||||
const selectedUnitIds = new Set(state.selectedSortieUnitIds);
|
||||
if (
|
||||
resonanceBond &&
|
||||
resonanceBond.level >= coreSortieResonanceMinLevel &&
|
||||
resonanceBond.unitIds.every((unitId) => selectedUnitIds.has(unitId))
|
||||
) {
|
||||
state.sortieResonanceSelection = { battleId: battleId as BattleScenarioId, bondId: resonanceBond.id };
|
||||
}
|
||||
}
|
||||
state.step = retryStep;
|
||||
state.latestBattleId = battleId;
|
||||
state.updatedAt = new Date().toISOString();
|
||||
@@ -926,6 +971,9 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
if (state.sortieResonanceSelection?.battleId === battleId) {
|
||||
delete state.sortieResonanceSelection;
|
||||
}
|
||||
if (state.sortieRecommendationSelection?.battleId === battleId) {
|
||||
delete state.sortieRecommendationSelection;
|
||||
}
|
||||
state.latestBattleId = battleId;
|
||||
state.updatedAt = new Date().toISOString();
|
||||
|
||||
@@ -1133,6 +1181,21 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
||||
if (!normalized.sortieResonanceSelection) {
|
||||
delete normalized.sortieResonanceSelection;
|
||||
}
|
||||
normalized.sortieRecommendationSelection = normalizeCampaignSortieRecommendationSnapshot(
|
||||
normalized.sortieRecommendationSelection,
|
||||
{ allowedUnitIds: sortieUnitFilter }
|
||||
);
|
||||
if (
|
||||
!normalized.sortieRecommendationSelection ||
|
||||
JSON.stringify(normalized.sortieRecommendationSelection.selectedUnitIds) !== JSON.stringify(normalized.selectedSortieUnitIds) ||
|
||||
normalized.sortieRecommendationSelection.selectedUnitIds.some((unitId) => (
|
||||
normalized.sortieRecommendationSelection?.formationAssignments[unitId] !== normalized.sortieFormationAssignments[unitId]
|
||||
)) ||
|
||||
normalized.sortieRecommendationSelection.sortieOrderId !== normalized.sortieOrderSelection?.orderId ||
|
||||
normalized.sortieRecommendationSelection.sortieResonanceBondId !== normalized.sortieResonanceSelection?.bondId
|
||||
) {
|
||||
delete normalized.sortieRecommendationSelection;
|
||||
}
|
||||
normalized.sortieOrderHistory = normalizeCampaignSortieOrderHistory(normalized.sortieOrderHistory);
|
||||
normalized.claimedSortieOrderRewardIds = normalizeCampaignSortieOrderRewardClaims(
|
||||
normalized.claimedSortieOrderRewardIds,
|
||||
@@ -1335,6 +1398,67 @@ function normalizeCampaignSortieResonanceSelection(
|
||||
return { battleId: battleId as BattleScenarioId, bondId };
|
||||
}
|
||||
|
||||
type CampaignSortieRecommendationNormalizationOptions = {
|
||||
expectedBattleId?: string;
|
||||
allowedUnitIds?: ReadonlySet<string>;
|
||||
};
|
||||
|
||||
export function normalizeCampaignSortieRecommendationSnapshot(
|
||||
value: unknown,
|
||||
options: CampaignSortieRecommendationNormalizationOptions = {}
|
||||
): CampaignSortieRecommendationSnapshot | undefined {
|
||||
if (!isPlainObject(value) || value.version !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const battleId = typeof value.battleId === 'string' ? value.battleId : '';
|
||||
const planId = value.planId;
|
||||
if (
|
||||
!hasCampaignBattleScenario(battleId) ||
|
||||
(options.expectedBattleId && options.expectedBattleId !== battleId) ||
|
||||
!sortieRecommendationPlanIds.includes(planId as SortieRecommendationPlanId)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const label = normalizeDisplayString(value.label).slice(0, 48);
|
||||
const summary = normalizeDisplayString(value.summary).slice(0, 180);
|
||||
const selectedUnitIds = uniqueStrings(value.selectedUnitIds)
|
||||
.filter((unitId) => !options.allowedUnitIds || options.allowedUnitIds.has(unitId))
|
||||
.slice(0, maxCampaignSortieAssignmentUnits);
|
||||
if (!label || !summary || selectedUnitIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const selectedUnitIdSet = new Set(selectedUnitIds);
|
||||
const formationAssignments = normalizeSortieFormationAssignments(
|
||||
recordOrEmpty(value.formationAssignments),
|
||||
selectedUnitIdSet
|
||||
);
|
||||
if (selectedUnitIds.some((unitId) => !formationAssignments[unitId])) {
|
||||
return undefined;
|
||||
}
|
||||
const reasonRecord = recordOrEmpty(value.unitReasons);
|
||||
const unitReasons = Object.fromEntries(selectedUnitIds.map((unitId) => [
|
||||
unitId,
|
||||
typeof reasonRecord[unitId] === 'string' ? reasonRecord[unitId].trim().slice(0, 180) : ''
|
||||
]));
|
||||
if (Object.values(unitReasons).some((reason) => reason.length === 0)) {
|
||||
return undefined;
|
||||
}
|
||||
const sortieOrderId = isSortieOrderId(value.sortieOrderId) ? value.sortieOrderId : undefined;
|
||||
const sortieResonanceBondId = normalizeKeyString(value.sortieResonanceBondId);
|
||||
return {
|
||||
version: 1,
|
||||
battleId,
|
||||
planId: planId as SortieRecommendationPlanId,
|
||||
label,
|
||||
summary,
|
||||
...(sortieOrderId ? { sortieOrderId } : {}),
|
||||
...(sortieResonanceBondId ? { sortieResonanceBondId } : {}),
|
||||
selectedUnitIds,
|
||||
formationAssignments,
|
||||
unitReasons
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCampaignSortieResonanceBond(
|
||||
battleId: string,
|
||||
bondId: string,
|
||||
@@ -1596,13 +1720,20 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
|
||||
turnNumber: report.turnNumber
|
||||
})
|
||||
: undefined;
|
||||
const sortieRecommendation = sortiePerformance
|
||||
? normalizeCampaignSortieRecommendationSnapshot(report.sortieRecommendation, {
|
||||
expectedBattleId: report.battleId,
|
||||
allowedUnitIds: rosterUnitIds
|
||||
})
|
||||
: undefined;
|
||||
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))),
|
||||
...(sortiePerformance ? { sortiePerformance } : {}),
|
||||
...(sortieReview ? { sortieReview } : {}),
|
||||
...(sortieOrder ? { sortieOrder } : {})
|
||||
...(sortieOrder ? { sortieOrder } : {}),
|
||||
...(sortieRecommendation ? { sortieRecommendation } : {})
|
||||
};
|
||||
if (!sortiePerformance) {
|
||||
delete filtered.sortiePerformance;
|
||||
@@ -1613,6 +1744,9 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
|
||||
if (!sortieOrder) {
|
||||
delete filtered.sortieOrder;
|
||||
}
|
||||
if (!sortieRecommendation) {
|
||||
delete filtered.sortieRecommendation;
|
||||
}
|
||||
if (filtered.mvp && !rosterUnitIds.has(filtered.mvp.unitId)) {
|
||||
delete filtered.mvp;
|
||||
}
|
||||
@@ -1636,6 +1770,12 @@ function filterBattleHistoryRosterReferences(
|
||||
turnNumber: settlement.turnNumber
|
||||
})
|
||||
: undefined;
|
||||
const sortieRecommendation = sortiePerformance
|
||||
? normalizeCampaignSortieRecommendationSnapshot(settlement.sortieRecommendation, {
|
||||
expectedBattleId: settlement.battleId,
|
||||
allowedUnitIds: rosterUnitIds
|
||||
})
|
||||
: undefined;
|
||||
filteredHistory[battleId] = {
|
||||
...settlement,
|
||||
units: settlement.units.filter((unit) => rosterUnitIds.has(unit.unitId)),
|
||||
@@ -1643,7 +1783,8 @@ function filterBattleHistoryRosterReferences(
|
||||
reserveTraining: settlement.reserveTraining?.filter((entry) => rosterUnitIds.has(entry.unitId)),
|
||||
...(sortiePerformance ? { sortiePerformance } : {}),
|
||||
...(sortieReview ? { sortieReview } : {}),
|
||||
...(sortieOrder ? { sortieOrder } : {})
|
||||
...(sortieOrder ? { sortieOrder } : {}),
|
||||
...(sortieRecommendation ? { sortieRecommendation } : {})
|
||||
};
|
||||
if (!sortiePerformance) {
|
||||
delete filteredHistory[battleId].sortiePerformance;
|
||||
@@ -1654,6 +1795,9 @@ function filterBattleHistoryRosterReferences(
|
||||
if (!sortieOrder) {
|
||||
delete filteredHistory[battleId].sortieOrder;
|
||||
}
|
||||
if (!sortieRecommendation) {
|
||||
delete filteredHistory[battleId].sortieRecommendation;
|
||||
}
|
||||
return filteredHistory;
|
||||
}, {});
|
||||
}
|
||||
@@ -1679,6 +1823,9 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
|
||||
|
||||
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(settlement.sortiePerformance);
|
||||
const sortieReview = normalizeCampaignSortieReviewSnapshot(settlement.sortieReview, sortiePerformance);
|
||||
const sortieRecommendation = normalizeCampaignSortieRecommendationSnapshot(settlement.sortieRecommendation, {
|
||||
expectedBattleId: battleId
|
||||
});
|
||||
const turnNumber = normalizeNonNegativeInteger(settlement.turnNumber);
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(settlement.sortieOrder, settlement.outcome, {
|
||||
@@ -1702,6 +1849,7 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
|
||||
...(sortiePerformance ? { sortiePerformance } : {}),
|
||||
...(sortieReview ? { sortieReview } : {}),
|
||||
...(sortieOrder ? { sortieOrder } : {}),
|
||||
...(sortieRecommendation ? { sortieRecommendation } : {}),
|
||||
completedAt
|
||||
};
|
||||
}
|
||||
@@ -1909,6 +2057,7 @@ export function normalizeCampaignSortiePerformanceSnapshot(
|
||||
return undefined;
|
||||
}
|
||||
const hpBefore = normalizeNonNegativeInteger(entry.hpBefore);
|
||||
const strategyUses = normalizeNonNegativeInteger(entry.strategyUses);
|
||||
return {
|
||||
unitId,
|
||||
hpBefore,
|
||||
@@ -1918,6 +2067,8 @@ export function normalizeCampaignSortiePerformanceSnapshot(
|
||||
defeats: normalizeNonNegativeInteger(entry.defeats),
|
||||
actions: normalizeNonNegativeInteger(entry.actions),
|
||||
support: normalizeNonNegativeInteger(entry.support),
|
||||
strategyUses,
|
||||
signatureStrategyUses: Math.min(strategyUses, normalizeNonNegativeInteger(entry.signatureStrategyUses)),
|
||||
sortieBonusDamage: normalizeNonNegativeInteger(entry.sortieBonusDamage),
|
||||
sortiePreventedDamage: normalizeNonNegativeInteger(entry.sortiePreventedDamage),
|
||||
sortieBonusHealing: normalizeNonNegativeInteger(entry.sortieBonusHealing),
|
||||
@@ -2023,6 +2174,9 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
|
||||
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(report.sortiePerformance);
|
||||
const sortieReview = normalizeCampaignSortieReviewSnapshot(report.sortieReview, sortiePerformance);
|
||||
const sortieRecommendation = normalizeCampaignSortieRecommendationSnapshot(report.sortieRecommendation, {
|
||||
expectedBattleId: battleId
|
||||
});
|
||||
const turnNumber = normalizeNonNegativeInteger(report.turnNumber);
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(report.sortieOrder, report.outcome, {
|
||||
@@ -2051,6 +2205,7 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
...(sortiePerformance ? { sortiePerformance } : {}),
|
||||
...(sortieReview ? { sortieReview } : {}),
|
||||
...(sortieOrder ? { sortieOrder } : {}),
|
||||
...(sortieRecommendation ? { sortieRecommendation } : {}),
|
||||
completedCampDialogues: uniqueStrings(report.completedCampDialogues),
|
||||
completedCampVisits: uniqueStrings(report.completedCampVisits),
|
||||
createdAt: normalizeCampaignTimestamp(report.createdAt) ?? new Date().toISOString()
|
||||
@@ -2310,7 +2465,10 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
|
||||
progress: report.sortieOrder.progress.map((entry) => ({ ...entry })),
|
||||
...(report.sortieOrder.command ? { command: { ...report.sortieOrder.command } } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(report.sortieRecommendation
|
||||
? { sortieRecommendation: cloneCampaignSortieRecommendationSnapshot(report.sortieRecommendation) }
|
||||
: {}),
|
||||
completedAt: report.createdAt
|
||||
};
|
||||
@@ -2454,6 +2612,15 @@ function cloneReport(report: FirstBattleReport): FirstBattleReport {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function cloneCampaignSortieRecommendationSnapshot(snapshot: CampaignSortieRecommendationSnapshot) {
|
||||
return {
|
||||
...snapshot,
|
||||
selectedUnitIds: [...snapshot.selectedUnitIds],
|
||||
formationAssignments: { ...snapshot.formationAssignments },
|
||||
unitReasons: { ...snapshot.unitReasons }
|
||||
} satisfies CampaignSortieRecommendationSnapshot;
|
||||
}
|
||||
|
||||
function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot, battleId?: string): CampaignRewardSnapshot | undefined {
|
||||
if (!rewards) {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user