feat: unlock tactical commands from sortie orders

This commit is contained in:
2026-07-11 02:42:04 +09:00
parent 9f49069db2
commit 2b9e33f4c0
8 changed files with 1060 additions and 70 deletions

View File

@@ -360,6 +360,62 @@ try {
normalizedStringBooleans.progress.every((entry) => entry.achieved === false),
`Expected malformed string booleans to stay false instead of restoring an unearned reward claim: ${JSON.stringify(normalizedStringBooleans)}`
);
const normalizedOrderCommand = normalizeCampaignSortieOrderResultSnapshot({
...normalizedEliteOrder,
command: {
source: 'sortie-order',
role: 'support',
actorId: ' liu-bei ',
usedTurn: '3.9',
effectPending: 'true',
effectValue: '1000001',
statusesCleared: '2.9',
effectLost: true,
extra: 'discard'
}
});
assert(
normalizedOrderCommand?.command?.source === 'sortie-order' &&
normalizedOrderCommand.command.role === 'support' &&
normalizedOrderCommand.command.actorId === 'liu-bei' &&
normalizedOrderCommand.command.usedTurn === 3 &&
normalizedOrderCommand.command.effectPending === false &&
normalizedOrderCommand.command.effectValue === 999999 &&
normalizedOrderCommand.command.statusesCleared === 2 &&
normalizedOrderCommand.command.effectLost === true &&
!Object.prototype.hasOwnProperty.call(normalizedOrderCommand.command, 'extra') &&
!Object.prototype.hasOwnProperty.call(normalizedEliteOrder, 'command'),
`Expected sortie-order commands to sanitize ids, numbers, booleans, and preserve command-less legacy snapshots: ${JSON.stringify(normalizedOrderCommand)}`
);
const invalidOrderCommands = [
{ ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, source: 'morale' } },
{ ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, role: 'reserve' } },
{ ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, actorId: 'x'.repeat(97) } },
{ ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, usedTurn: 0 } }
];
assert(
invalidOrderCommands.every((snapshot) => (
!Object.prototype.hasOwnProperty.call(normalizeCampaignSortieOrderResultSnapshot(snapshot) ?? {}, 'command')
)),
'Expected malformed optional command records to be discarded without losing the sortie-order result.'
);
const normalizedInitiativeCommand = normalizeCampaignSortieOrderResultSnapshot({
...normalizedEliteOrder,
command: {
...normalizedOrderCommand.command,
source: 'initiative',
role: 'flank',
effectPending: true,
effectLost: 'false'
}
})?.command;
assert(
normalizedInitiativeCommand?.source === 'initiative' &&
normalizedInitiativeCommand.role === 'flank' &&
normalizedInitiativeCommand.effectPending === true &&
normalizedInitiativeCommand.effectLost === false,
`Expected explicit initiative command sources and strict command booleans to normalize: ${JSON.stringify(normalizedInitiativeCommand)}`
);
storage.clear();
storage.set(
@@ -900,7 +956,17 @@ try {
{ id: 'victory', achieved: true, value: 1, target: 1 },
{ id: 'elite-grade', achieved: true, value: 88, target: 75 },
{ id: 'elite-survival', achieved: true, value: 1, target: 1 }
]
],
command: {
source: 'sortie-order',
role: 'front',
actorId: performanceUnit.id,
usedTurn: 3,
effectPending: false,
effectValue: 8,
statusesCleared: 0,
effectLost: false
}
};
const successfulMobileOrder = {
version: 1,
@@ -964,7 +1030,9 @@ try {
firstOrderSettlement.claimedSortieOrderRewardIds[0] === `${firstScenario.id}:elite` &&
firstOrderSettlement.sortieOrderSelection === undefined &&
firstOrderSettlement.firstBattleReport?.sortieOrder?.rewardGranted === true &&
firstOrderSettlement.firstBattleReport.sortieOrder.command?.source === 'sortie-order' &&
firstOrderSettlement.battleHistory[firstScenario.id]?.sortieOrder?.rewardGranted === true &&
firstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.command?.effectValue === 8 &&
firstOrderSettlement.sortieOrderHistory[firstScenario.id]?.elite?.rewardGranted === true,
`Expected the first battle/order success to grant its independent reward once, persist every snapshot, and clear the matching selection: ${JSON.stringify(firstOrderSettlement)}`
);
@@ -972,23 +1040,55 @@ try {
assert(
reloadedFirstOrderSettlement.firstBattleReport?.sortieOrder?.achieved === true &&
reloadedFirstOrderSettlement.firstBattleReport.sortieOrder.rewardGranted === true &&
reloadedFirstOrderSettlement.firstBattleReport.sortieOrder.command?.actorId === performanceUnit.id &&
reloadedFirstOrderSettlement.battleHistory[firstScenario.id]?.sortieOrder?.achieved === true &&
reloadedFirstOrderSettlement.battleHistory[firstScenario.id].turnNumber === orderBattleReport.turnNumber &&
reloadedFirstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.rewardGranted === true &&
reloadedFirstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.command?.effectValue === 8 &&
reloadedFirstOrderSettlement.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true &&
reloadedFirstOrderSettlement.sortieOrderHistory[firstScenario.id].elite.command?.source === 'sortie-order' &&
reloadedFirstOrderSettlement.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`),
`Expected report, settlement, independent history, and reward claim to survive save normalization together: ${JSON.stringify(reloadedFirstOrderSettlement)}`
);
const validContextualCommandStateRaw = storage.get(campaignStorageKey);
assert(validContextualCommandStateRaw, 'Expected a persisted command state for contextual corruption checks.');
const corruptedContextualCommandState = JSON.parse(validContextualCommandStateRaw);
corruptedContextualCommandState.firstBattleReport.sortieOrder.command.actorId = 'ghost-unit';
corruptedContextualCommandState.battleHistory[firstScenario.id].sortieOrder.command.role = 'support';
corruptedContextualCommandState.sortieOrderHistory[firstScenario.id].elite.command.usedTurn = orderBattleReport.turnNumber + 1;
storage.set(campaignStorageKey, JSON.stringify(corruptedContextualCommandState));
const normalizedContextualCommandState = loadCampaignState();
assert(
normalizedContextualCommandState.firstBattleReport?.sortieOrder?.command === undefined &&
normalizedContextualCommandState.battleHistory[firstScenario.id]?.sortieOrder?.command === undefined &&
normalizedContextualCommandState.sortieOrderHistory[firstScenario.id]?.elite?.command === undefined &&
normalizedContextualCommandState.firstBattleReport?.sortieOrder?.achieved === true &&
normalizedContextualCommandState.battleHistory[firstScenario.id]?.sortieOrder?.achieved === true &&
normalizedContextualCommandState.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true,
`Expected contextual normalization to remove ghost actors, role mismatches, and future command turns without dropping their order results: ${JSON.stringify(normalizedContextualCommandState)}`
);
storage.set(campaignStorageKey, validContextualCommandStateRaw);
loadCampaignState();
orderBattleReport.sortieOrder.progress[0].achieved = false;
orderBattleReport.sortieOrder.command.effectValue = 999;
firstOrderCanonicalReport.sortieOrder.progress[1].achieved = false;
firstOrderCanonicalReport.sortieOrder.command.actorId = 'ghost-unit';
const deepCopyOrderState = getCampaignState();
const detachedOrderState = getCampaignState();
detachedOrderState.firstBattleReport.sortieOrder.progress[2].achieved = false;
detachedOrderState.firstBattleReport.sortieOrder.command.effectLost = true;
assert(
deepCopyOrderState.firstBattleReport?.sortieOrder?.progress.every((entry) => entry.achieved) &&
deepCopyOrderState.firstBattleReport.sortieOrder.command?.effectValue === 8 &&
deepCopyOrderState.firstBattleReport.sortieOrder.command.actorId === performanceUnit.id &&
deepCopyOrderState.battleHistory[firstScenario.id]?.sortieOrder?.progress.every((entry) => entry.achieved) &&
deepCopyOrderState.battleHistory[firstScenario.id].sortieOrder.command?.effectLost === false &&
deepCopyOrderState.sortieOrderHistory[firstScenario.id]?.elite?.progress.every((entry) => entry.achieved) &&
deepCopyOrderState.sortieOrderHistory[firstScenario.id].elite.command?.effectValue === 8 &&
detachedOrderState.battleHistory[firstScenario.id]?.sortieOrder?.progress[2].achieved === true &&
detachedOrderState.battleHistory[firstScenario.id].sortieOrder.command?.effectLost === false &&
getCampaignState().firstBattleReport?.sortieOrder?.progress[2].achieved === true,
`Expected input, returned report, report/history, and returned campaign sortie-order progress to be deeply detached: ${JSON.stringify(deepCopyOrderState)}`
);