feat: unify victory reward experience

This commit is contained in:
2026-07-18 14:33:29 +09:00
parent 1df324ecd2
commit 134a509938
6 changed files with 1385 additions and 129 deletions

View File

@@ -134,6 +134,18 @@ export type FirstBattleReport = {
createdAt: string;
};
export type CampaignVictoryRewardPresentation = {
baseRewardGold: number;
rewardGold: number;
itemRewards: string[];
sortieOrderBonus?: {
orderId: SortieOrderId;
label: string;
rewardGold: number;
rewardItems: string[];
};
};
export type CampaignStep =
| 'new'
| 'prologue'
@@ -448,6 +460,7 @@ export type CampaignState = {
reserveTrainingAssignments: CampaignReserveTrainingAssignments;
completedCampDialogues: string[];
completedCampVisits: string[];
acknowledgedVictoryRewardBattleIds: string[];
battleHistory: Record<string, CampaignBattleSettlement>;
latestBattleId?: string;
firstBattleReport?: FirstBattleReport;
@@ -652,6 +665,7 @@ export function createInitialCampaignState(): CampaignState {
reserveTrainingAssignments: {},
completedCampDialogues: [],
completedCampVisits: [],
acknowledgedVictoryRewardBattleIds: [],
battleHistory: {}
};
}
@@ -986,6 +1000,66 @@ export function getFirstBattleReport() {
return report ? cloneReport(report) : undefined;
}
export function campaignVictoryRewardPresentation(report: FirstBattleReport): CampaignVictoryRewardPresentation {
const definition = report.outcome === 'victory' && report.sortieOrder?.rewardGranted
? sortieOrderDefinition(report.sortieOrder.orderId)
: undefined;
const sortieOrderBonus = definition
? {
orderId: definition.id,
label: definition.label,
rewardGold: definition.rewardGold,
rewardItems: [...definition.rewardItems]
}
: undefined;
return {
baseRewardGold: report.rewardGold,
rewardGold: report.rewardGold + (sortieOrderBonus?.rewardGold ?? 0),
itemRewards: [...report.itemRewards, ...(sortieOrderBonus?.rewardItems ?? [])],
sortieOrderBonus
};
}
export function getPendingCampaignVictoryRewardReport() {
const state = ensureCampaignState();
const report = state.firstBattleReport;
if (
!report ||
report.outcome !== 'victory' ||
state.acknowledgedVictoryRewardBattleIds.includes(report.battleId)
) {
return undefined;
}
return cloneReport(report);
}
export function acknowledgeCampaignVictoryReward(battleId: string) {
const state = ensureCampaignState();
const normalizedBattleId = normalizeKeyString(battleId);
const report = state.firstBattleReport;
const settlement = state.battleHistory[normalizedBattleId];
const hasVictory = settlement?.outcome === 'victory' || (
report?.battleId === normalizedBattleId && report.outcome === 'victory'
);
if (
!normalizedBattleId ||
!(normalizedBattleId in battleScenarios) ||
!hasVictory
) {
return cloneCampaignState(state);
}
if (!state.acknowledgedVictoryRewardBattleIds.includes(normalizedBattleId)) {
state.acknowledgedVictoryRewardBattleIds = [
...state.acknowledgedVictoryRewardBattleIds,
normalizedBattleId
];
state.updatedAt = new Date().toISOString();
persistCampaignState(state);
}
return cloneCampaignState(state);
}
export function applyCampBondExp(dialogueId: string, bondId: string, amount: number) {
const state = ensureCampaignState();
if (!state.firstBattleReport) {
@@ -1158,6 +1232,8 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
.filter((bond): bond is CampBondSnapshot => Boolean(bond));
normalized.completedCampDialogues = uniqueStrings(normalized.completedCampDialogues);
normalized.completedCampVisits = uniqueStrings(normalized.completedCampVisits);
normalized.acknowledgedVictoryRewardBattleIds = uniqueStrings(normalized.acknowledgedVictoryRewardBattleIds)
.filter((battleId) => battleId in battleScenarios);
normalized.inventory = normalizeInventory(normalized.inventory);
const rosterUnitIds = normalizedRosterUnitIds(normalized.roster);
const sortieUnitFilter = rosterUnitIds.size > 0 ? rosterUnitIds : undefined;
@@ -1216,6 +1292,16 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
if (normalized.firstBattleReport && sortieUnitFilter) {
normalized.firstBattleReport = filterFirstBattleReportRosterReferences(normalized.firstBattleReport, sortieUnitFilter);
}
const recordedVictoryBattleIds = new Set(
Object.entries(normalized.battleHistory)
.filter(([, settlement]) => settlement.outcome === 'victory')
.map(([battleId]) => battleId)
);
if (normalized.firstBattleReport?.outcome === 'victory') {
recordedVictoryBattleIds.add(normalized.firstBattleReport.battleId);
}
normalized.acknowledgedVictoryRewardBattleIds = normalized.acknowledgedVictoryRewardBattleIds
.filter((battleId) => recordedVictoryBattleIds.has(battleId));
normalized.sortieOrderHistory = normalizeCampaignSortieOrderHistory(
normalized.sortieOrderHistory,
normalized.battleHistory,