feat: unlock tactical commands from sortie orders
This commit is contained in:
@@ -63,7 +63,8 @@ try {
|
||||
const tacticalOptions = {
|
||||
...options,
|
||||
expectedBattleId: 'second-battle-yellow-turban-pursuit',
|
||||
allowTacticalCommand: true
|
||||
allowTacticalCommand: true,
|
||||
allowLegacyTacticalCommand: true
|
||||
};
|
||||
const validTacticalBaseState = {
|
||||
...validState,
|
||||
@@ -114,10 +115,62 @@ try {
|
||||
effectLost: true
|
||||
}
|
||||
};
|
||||
const validInitiativeCommandState = {
|
||||
...validFrontCommandState,
|
||||
tacticalCommand: { ...validFrontCommandState.tacticalCommand, source: 'initiative' }
|
||||
};
|
||||
const validUnlockedOrderCommandState = {
|
||||
...validTacticalBaseState,
|
||||
battleStats: validState.battleStats,
|
||||
sortieOrderCommandUnlocked: true,
|
||||
sortieOrderCommandUnlockTurn: 2,
|
||||
tacticalCommand: {
|
||||
...validFrontCommandState.tacticalCommand,
|
||||
source: 'sortie-order'
|
||||
}
|
||||
};
|
||||
assert(isValidBattleSaveState(validFrontCommandState, tacticalOptions), 'Expected pending front tactical command to pass validation.');
|
||||
assert(isValidBattleSaveState(validInitiativeCommandState, tacticalOptions), 'Expected an explicit initiative source to pass validation.');
|
||||
assert(isValidBattleSaveState(validFlankCommandState, tacticalOptions), 'Expected resolved flank tactical command to pass validation.');
|
||||
assert(isValidBattleSaveState(validSupportCommandState, tacticalOptions), 'Expected immediate support tactical command to pass validation.');
|
||||
assert(isValidBattleSaveState(validLostFrontCommandState, tacticalOptions), 'Expected a lost tactical command to retain its result record.');
|
||||
assert(
|
||||
isValidBattleSaveState(validUnlockedOrderCommandState, tacticalOptions),
|
||||
'Expected an unlocked sortie-order command to replace the counterplay requirement.'
|
||||
);
|
||||
assert(
|
||||
isValidBattleSaveState({ ...validUnlockedOrderCommandState, tacticalCommand: undefined }, tacticalOptions),
|
||||
'Expected an unlocked sortie-order command to remain unused in a valid save.'
|
||||
);
|
||||
assert(
|
||||
isValidBattleSaveState({ ...validTacticalBaseState, sortieOrderCommandUnlocked: false }, tacticalOptions),
|
||||
'Expected an explicitly locked sortie-order command without an unlock turn to pass validation.'
|
||||
);
|
||||
const otherSortieOptions = {
|
||||
...options,
|
||||
allowTacticalCommand: true,
|
||||
allowLegacyTacticalCommand: false
|
||||
};
|
||||
const otherSortieLegacyCommandState = {
|
||||
...validFrontCommandState,
|
||||
battleId: options.expectedBattleId,
|
||||
campaignStep: 'first-battle',
|
||||
tacticalCommand: { ...validFrontCommandState.tacticalCommand }
|
||||
};
|
||||
const otherSortieOrderCommandState = {
|
||||
...validUnlockedOrderCommandState,
|
||||
battleId: options.expectedBattleId,
|
||||
campaignStep: 'first-battle'
|
||||
};
|
||||
assert(
|
||||
isValidBattleSaveState(otherSortieOrderCommandState, otherSortieOptions) &&
|
||||
!isValidBattleSaveState(otherSortieLegacyCommandState, otherSortieOptions) &&
|
||||
!isValidBattleSaveState({
|
||||
...otherSortieLegacyCommandState,
|
||||
tacticalCommand: { ...otherSortieLegacyCommandState.tacticalCommand, source: 'initiative' }
|
||||
}, otherSortieOptions),
|
||||
'Expected another sortie battle to allow sortie-order source while rejecting source-less and explicit legacy initiative commands.'
|
||||
);
|
||||
assert(!isValidBattleSaveState(validFrontCommandState, options), 'Expected tactical commands to be rejected when disabled for the battle.');
|
||||
const tacticalRoleOptions = {
|
||||
...tacticalOptions,
|
||||
@@ -131,6 +184,13 @@ try {
|
||||
JSON.stringify(parsedTacticalCommand) === JSON.stringify(validFrontCommandState.tacticalCommand),
|
||||
'Expected every tactical command field to round-trip.'
|
||||
);
|
||||
const parsedOrderCommand = parseBattleSaveState(JSON.stringify(validUnlockedOrderCommandState), tacticalOptions);
|
||||
assert(
|
||||
parsedOrderCommand?.sortieOrderCommandUnlocked === true &&
|
||||
parsedOrderCommand.sortieOrderCommandUnlockTurn === 2 &&
|
||||
parsedOrderCommand.tacticalCommand?.source === 'sortie-order',
|
||||
'Expected sortie-order command unlock and source fields to round-trip.'
|
||||
);
|
||||
assert(parseBattleSaveState(JSON.stringify(validState), options)?.battleId === options.expectedBattleId, 'Expected valid JSON save to parse.');
|
||||
assert(
|
||||
parseBattleSaveState(JSON.stringify(validState), options)?.sortieOrderId === 'elite',
|
||||
@@ -250,6 +310,7 @@ try {
|
||||
const tacticalRejectedCases = [
|
||||
['invalid tactical command shape', { tacticalCommand: 'front' }],
|
||||
['invalid tactical command role', { tacticalCommand: { ...validFrontCommandState.tacticalCommand, role: 'reserve' } }],
|
||||
['invalid tactical command source', { tacticalCommand: { ...validFrontCommandState.tacticalCommand, source: 'morale' } }],
|
||||
['unknown tactical command actor', { tacticalCommand: { ...validFrontCommandState.tacticalCommand, actorId: 'ghost-unit' } }],
|
||||
['enemy tactical command actor', { tacticalCommand: { ...validFrontCommandState.tacticalCommand, actorId: 'rebel-1' } }],
|
||||
['invalid tactical command turn', { tacticalCommand: { ...validFrontCommandState.tacticalCommand, usedTurn: 0 } }],
|
||||
@@ -275,6 +336,68 @@ try {
|
||||
assert(!isValidBattleSaveState(candidate, tacticalOptions), `Expected invalid tactical command save to be rejected: ${label}`);
|
||||
});
|
||||
|
||||
const orderCommandRejectedCases = [
|
||||
['unlocked command missing unlock turn', { sortieOrderCommandUnlockTurn: undefined }],
|
||||
['zero unlock turn', { sortieOrderCommandUnlockTurn: 0 }],
|
||||
['future unlock turn', { sortieOrderCommandUnlockTurn: 3 }],
|
||||
['fractional unlock turn', { sortieOrderCommandUnlockTurn: 1.5 }],
|
||||
['locked command retaining unlock turn', { sortieOrderCommandUnlocked: false }],
|
||||
['unlocked command without allied actions', {
|
||||
battleStats: {
|
||||
'liu-bei': { ...validState.battleStats['liu-bei'], actions: 0 },
|
||||
'rebel-1': { ...validState.battleStats['liu-bei'], actions: 3 }
|
||||
}
|
||||
}],
|
||||
['sortie-order source without unlock', {
|
||||
sortieOrderCommandUnlocked: undefined,
|
||||
sortieOrderCommandUnlockTurn: undefined
|
||||
}]
|
||||
];
|
||||
orderCommandRejectedCases.forEach(([label, patch]) => {
|
||||
const candidate = { ...validUnlockedOrderCommandState, ...patch };
|
||||
assert(!isValidBattleSaveState(candidate, tacticalOptions), `Expected invalid sortie-order command save to be rejected: ${label}`);
|
||||
});
|
||||
assert(
|
||||
!isValidBattleSaveState(validUnlockedOrderCommandState, options),
|
||||
'Expected sortie-order command unlock state to remain gated by allowTacticalCommand.'
|
||||
);
|
||||
assert(
|
||||
!isValidBattleSaveState(
|
||||
{
|
||||
...validUnlockedOrderCommandState,
|
||||
turnNumber: 3,
|
||||
sortieOrderCommandUnlockTurn: 3,
|
||||
tacticalCommand: { ...validUnlockedOrderCommandState.tacticalCommand, usedTurn: 2 }
|
||||
},
|
||||
tacticalOptions
|
||||
),
|
||||
'Expected a sortie-order tactical command used before its unlock turn to be rejected.'
|
||||
);
|
||||
assert(
|
||||
!isValidBattleSaveState(
|
||||
{
|
||||
...validInitiativeCommandState,
|
||||
battleStats: validState.battleStats
|
||||
},
|
||||
tacticalOptions
|
||||
),
|
||||
'Expected an explicit initiative command to retain the legacy counterplay threshold.'
|
||||
);
|
||||
const unlockedInitiativeState = {
|
||||
...validTacticalBaseState,
|
||||
sortieOrderCommandUnlocked: true,
|
||||
sortieOrderCommandUnlockTurn: 2,
|
||||
tacticalCommand: { ...validInitiativeCommandState.tacticalCommand }
|
||||
};
|
||||
assert(
|
||||
!isValidBattleSaveState(unlockedInitiativeState, tacticalOptions) &&
|
||||
!isValidBattleSaveState({
|
||||
...unlockedInitiativeState,
|
||||
tacticalCommand: { ...unlockedInitiativeState.tacticalCommand, source: undefined }
|
||||
}, tacticalOptions),
|
||||
'Expected unlocked sortie-order state to reject explicit and source-less legacy initiative commands.'
|
||||
);
|
||||
|
||||
assert(
|
||||
!isValidBattleSaveState(validState, { ...options, validUnitIds: new Set(['liu-bei', 'guan-yu', 'rebel-1', 'zhang-fei']) }),
|
||||
'Expected save missing a current battle unit to be rejected.'
|
||||
|
||||
Reference in New Issue
Block a user