feat: unlock tactical commands from sortie orders
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
sortieOrderDefinition,
|
||||
sortieOrderIds,
|
||||
sortieOrderRewardClaimId,
|
||||
type CampaignSortieOrderCommandSnapshot,
|
||||
type SortieOrderId,
|
||||
type SortieOrderResultSnapshot
|
||||
} from '../data/sortieOrders';
|
||||
@@ -363,6 +364,7 @@ export type CampaignBattleSettlement = {
|
||||
battleId: string;
|
||||
battleTitle: string;
|
||||
outcome: FirstBattleReport['outcome'];
|
||||
turnNumber: number;
|
||||
rewardGold: number;
|
||||
itemRewards: string[];
|
||||
campaignRewards?: CampaignRewardSnapshot;
|
||||
@@ -791,7 +793,14 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
reportClone.completedCampVisits = completedCampVisits;
|
||||
let pendingSortieOrderReward: ReturnType<typeof sortieOrderDefinition> | undefined;
|
||||
let pendingSortieOrderClaimId: string | undefined;
|
||||
const sortieOrder = normalizeCampaignSortieOrderResultSnapshot(reportClone.sortieOrder, reportClone.outcome);
|
||||
const sortieOrder = normalizeCampaignSortieOrderResultSnapshot(
|
||||
reportClone.sortieOrder,
|
||||
reportClone.outcome,
|
||||
{
|
||||
performance: normalizeCampaignSortiePerformanceSnapshot(reportClone.sortiePerformance),
|
||||
turnNumber: normalizeNonNegativeInteger(reportClone.turnNumber)
|
||||
}
|
||||
);
|
||||
if (sortieOrder) {
|
||||
if (reportClone.outcome !== 'victory') {
|
||||
const victoryProgress = sortieOrder.progress.find((entry) => entry.id === 'victory');
|
||||
@@ -820,7 +829,8 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
history[sortieOrder.orderId] = {
|
||||
...bestOrderResult,
|
||||
rewardGranted: Boolean(previousOrderResult?.rewardGranted || sortieOrder.rewardGranted),
|
||||
progress: bestOrderResult.progress.map((entry) => ({ ...entry }))
|
||||
progress: bestOrderResult.progress.map((entry) => ({ ...entry })),
|
||||
...(bestOrderResult.command ? { command: { ...bestOrderResult.command } } : {})
|
||||
};
|
||||
state.sortieOrderHistory[battleId as BattleScenarioId] = history;
|
||||
reportClone.sortieOrder = sortieOrder;
|
||||
@@ -1099,6 +1109,11 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
|
||||
if (normalized.firstBattleReport && sortieUnitFilter) {
|
||||
normalized.firstBattleReport = filterFirstBattleReportRosterReferences(normalized.firstBattleReport, sortieUnitFilter);
|
||||
}
|
||||
normalized.sortieOrderHistory = normalizeCampaignSortieOrderHistory(
|
||||
normalized.sortieOrderHistory,
|
||||
normalized.battleHistory,
|
||||
normalized.firstBattleReport
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -1253,7 +1268,8 @@ function normalizeCampaignSortieOrderSelection(value: unknown): CampaignSortieOr
|
||||
|
||||
export function normalizeCampaignSortieOrderResultSnapshot(
|
||||
value: unknown,
|
||||
outcome?: FirstBattleReport['outcome']
|
||||
outcome?: FirstBattleReport['outcome'],
|
||||
commandContext?: CampaignSortieOrderCommandNormalizationContext
|
||||
): CampaignSortieOrderResultSnapshot | undefined {
|
||||
if (!isPlainObject(value) || value.version !== 1 || !isSortieOrderId(value.orderId)) {
|
||||
return undefined;
|
||||
@@ -1284,24 +1300,85 @@ export function normalizeCampaignSortieOrderResultSnapshot(
|
||||
}
|
||||
}
|
||||
const achieved = progress.every((entry) => entry.achieved);
|
||||
const normalizedCommand = normalizeCampaignSortieOrderCommandSnapshot(value.command);
|
||||
const command = normalizedCommand && (
|
||||
!commandContext || campaignSortieOrderCommandMatchesContext(normalizedCommand, commandContext)
|
||||
)
|
||||
? normalizedCommand
|
||||
: undefined;
|
||||
return {
|
||||
version: 1,
|
||||
orderId,
|
||||
score: Math.min(100, normalizeNonNegativeInteger(value.score)),
|
||||
achieved,
|
||||
progress,
|
||||
rewardGranted: achieved && value.rewardGranted === true
|
||||
rewardGranted: achieved && value.rewardGranted === true,
|
||||
...(command ? { command } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCampaignSortieOrderHistory(value: unknown): CampaignSortieOrderHistory {
|
||||
type CampaignSortieOrderCommandNormalizationContext = {
|
||||
performance?: CampaignSortiePerformanceSnapshot;
|
||||
turnNumber: number;
|
||||
};
|
||||
|
||||
function campaignSortieOrderCommandMatchesContext(
|
||||
command: CampaignSortieOrderCommandSnapshot,
|
||||
context: CampaignSortieOrderCommandNormalizationContext
|
||||
) {
|
||||
const performance = context.performance;
|
||||
return Boolean(
|
||||
performance &&
|
||||
command.usedTurn <= context.turnNumber &&
|
||||
performance.selectedUnitIds.includes(command.actorId) &&
|
||||
performance.formationAssignments[command.actorId] === command.role
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCampaignSortieOrderCommandSnapshot(value: unknown): CampaignSortieOrderCommandSnapshot | undefined {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
(value.source !== 'sortie-order' && value.source !== 'initiative') ||
|
||||
(value.role !== 'front' && value.role !== 'flank' && value.role !== 'support')
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const actorId = normalizeKeyString(value.actorId);
|
||||
const usedTurn = normalizeNonNegativeInteger(value.usedTurn);
|
||||
if (!actorId || usedTurn <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
source: value.source,
|
||||
role: value.role,
|
||||
actorId,
|
||||
usedTurn,
|
||||
effectPending: value.effectPending === true,
|
||||
effectValue: normalizeNonNegativeInteger(value.effectValue),
|
||||
statusesCleared: normalizeNonNegativeInteger(value.statusesCleared),
|
||||
effectLost: value.effectLost === true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCampaignSortieOrderHistory(
|
||||
value: unknown,
|
||||
battleHistory: Record<string, CampaignBattleSettlement> = {},
|
||||
latestReport?: FirstBattleReport
|
||||
): CampaignSortieOrderHistory {
|
||||
return Object.entries(recordOrEmpty(value)).reduce<CampaignSortieOrderHistory>((history, [battleId, orderResults]) => {
|
||||
if (!(battleId in battleScenarios)) {
|
||||
return history;
|
||||
}
|
||||
const normalizedByOrder = sortieOrderIds.reduce<Partial<Record<CampaignSortiePresetId, CampaignSortieOrderResultSnapshot>>>(
|
||||
(results, orderId) => {
|
||||
const result = normalizeCampaignSortieOrderResultSnapshot(recordOrEmpty(orderResults)[orderId]);
|
||||
const rawResult = recordOrEmpty(orderResults)[orderId];
|
||||
const standaloneResult = normalizeCampaignSortieOrderResultSnapshot(rawResult);
|
||||
const commandContext = standaloneResult
|
||||
? campaignSortieOrderHistoryCommandContext(battleId, standaloneResult, battleHistory, latestReport)
|
||||
: undefined;
|
||||
const result = commandContext
|
||||
? normalizeCampaignSortieOrderResultSnapshot(rawResult, undefined, commandContext)
|
||||
: standaloneResult;
|
||||
if (result?.orderId === orderId) {
|
||||
results[orderId] = result;
|
||||
}
|
||||
@@ -1316,6 +1393,59 @@ function normalizeCampaignSortieOrderHistory(value: unknown): CampaignSortieOrde
|
||||
}, {});
|
||||
}
|
||||
|
||||
function campaignSortieOrderHistoryCommandContext(
|
||||
battleId: string,
|
||||
result: CampaignSortieOrderResultSnapshot,
|
||||
battleHistory: Record<string, CampaignBattleSettlement>,
|
||||
latestReport?: FirstBattleReport
|
||||
): CampaignSortieOrderCommandNormalizationContext | undefined {
|
||||
const settlement = battleHistory[battleId];
|
||||
const candidates = [
|
||||
settlement?.sortiePerformance && settlement.sortieOrder
|
||||
? {
|
||||
performance: settlement.sortiePerformance,
|
||||
turnNumber: settlement.turnNumber,
|
||||
result: settlement.sortieOrder
|
||||
}
|
||||
: undefined,
|
||||
latestReport?.battleId === battleId && latestReport.sortiePerformance && latestReport.sortieOrder
|
||||
? {
|
||||
performance: latestReport.sortiePerformance,
|
||||
turnNumber: latestReport.turnNumber,
|
||||
result: latestReport.sortieOrder
|
||||
}
|
||||
: undefined
|
||||
];
|
||||
const matching = candidates.find((candidate) => (
|
||||
candidate && campaignSortieOrderResultsShareEvaluation(candidate.result, result)
|
||||
));
|
||||
return matching
|
||||
? { performance: matching.performance, turnNumber: matching.turnNumber }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function campaignSortieOrderResultsShareEvaluation(
|
||||
left: CampaignSortieOrderResultSnapshot,
|
||||
right: CampaignSortieOrderResultSnapshot
|
||||
) {
|
||||
return (
|
||||
left.orderId === right.orderId &&
|
||||
left.score === right.score &&
|
||||
left.achieved === right.achieved &&
|
||||
left.progress.length === right.progress.length &&
|
||||
left.progress.every((entry, index) => {
|
||||
const other = right.progress[index];
|
||||
return Boolean(
|
||||
other &&
|
||||
entry.id === other.id &&
|
||||
entry.achieved === other.achieved &&
|
||||
entry.value === other.value &&
|
||||
entry.target === other.target
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCampaignSortieOrderRewardClaims(
|
||||
value: unknown,
|
||||
history: CampaignSortieOrderHistory
|
||||
@@ -1370,7 +1500,10 @@ function filterFirstBattleReportRosterReferences(report: FirstBattleReport, rost
|
||||
? normalizeCampaignSortieReviewSnapshot(report.sortieReview, sortiePerformance)
|
||||
: undefined;
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(report.sortieOrder, report.outcome)
|
||||
? normalizeCampaignSortieOrderResultSnapshot(report.sortieOrder, report.outcome, {
|
||||
performance: sortiePerformance,
|
||||
turnNumber: report.turnNumber
|
||||
})
|
||||
: undefined;
|
||||
const filtered = {
|
||||
...report,
|
||||
@@ -1407,7 +1540,10 @@ function filterBattleHistoryRosterReferences(
|
||||
? normalizeCampaignSortieReviewSnapshot(settlement.sortieReview, sortiePerformance)
|
||||
: undefined;
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(settlement.sortieOrder, settlement.outcome)
|
||||
? normalizeCampaignSortieOrderResultSnapshot(settlement.sortieOrder, settlement.outcome, {
|
||||
performance: sortiePerformance,
|
||||
turnNumber: settlement.turnNumber
|
||||
})
|
||||
: undefined;
|
||||
filteredHistory[battleId] = {
|
||||
...settlement,
|
||||
@@ -1452,14 +1588,19 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle
|
||||
|
||||
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(settlement.sortiePerformance);
|
||||
const sortieReview = normalizeCampaignSortieReviewSnapshot(settlement.sortieReview, sortiePerformance);
|
||||
const turnNumber = normalizeNonNegativeInteger(settlement.turnNumber);
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(settlement.sortieOrder, settlement.outcome)
|
||||
? normalizeCampaignSortieOrderResultSnapshot(settlement.sortieOrder, settlement.outcome, {
|
||||
performance: sortiePerformance,
|
||||
turnNumber
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
battleId,
|
||||
battleTitle,
|
||||
outcome: settlement.outcome,
|
||||
turnNumber,
|
||||
rewardGold: normalizeNonNegativeInteger(settlement.rewardGold),
|
||||
itemRewards: normalizeRewardStrings(settlement.itemRewards),
|
||||
campaignRewards: cloneCampaignRewardSnapshot(settlement.campaignRewards, battleId),
|
||||
@@ -1791,15 +1932,19 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi
|
||||
|
||||
const sortiePerformance = normalizeCampaignSortiePerformanceSnapshot(report.sortiePerformance);
|
||||
const sortieReview = normalizeCampaignSortieReviewSnapshot(report.sortieReview, sortiePerformance);
|
||||
const turnNumber = normalizeNonNegativeInteger(report.turnNumber);
|
||||
const sortieOrder = sortieReview
|
||||
? normalizeCampaignSortieOrderResultSnapshot(report.sortieOrder, report.outcome)
|
||||
? normalizeCampaignSortieOrderResultSnapshot(report.sortieOrder, report.outcome, {
|
||||
performance: sortiePerformance,
|
||||
turnNumber
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
battleId,
|
||||
battleTitle,
|
||||
outcome: report.outcome,
|
||||
turnNumber: normalizeNonNegativeInteger(report.turnNumber),
|
||||
turnNumber,
|
||||
rewardGold: normalizeNonNegativeInteger(report.rewardGold),
|
||||
defeatedEnemies: normalizeNonNegativeInteger(report.defeatedEnemies),
|
||||
totalEnemies: normalizeNonNegativeInteger(report.totalEnemies),
|
||||
@@ -2026,6 +2171,7 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
|
||||
battleId: report.battleId,
|
||||
battleTitle: report.battleTitle,
|
||||
outcome: report.outcome,
|
||||
turnNumber: report.turnNumber,
|
||||
rewardGold: report.rewardGold,
|
||||
itemRewards: [...report.itemRewards],
|
||||
campaignRewards: cloneCampaignRewardSnapshot(report.campaignRewards, report.battleId),
|
||||
@@ -2070,7 +2216,8 @@ function createBattleSettlement(report: FirstBattleReport, reserveTraining: Camp
|
||||
? {
|
||||
sortieOrder: {
|
||||
...report.sortieOrder,
|
||||
progress: report.sortieOrder.progress.map((entry) => ({ ...entry }))
|
||||
progress: report.sortieOrder.progress.map((entry) => ({ ...entry })),
|
||||
...(report.sortieOrder.command ? { command: { ...report.sortieOrder.command } } : {})
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user