feat: make Xuzhou choices shape battle eight
This commit is contained in:
@@ -19,6 +19,16 @@ try {
|
||||
const {
|
||||
cityEquipmentPreparationSelectionRecordId
|
||||
} = await server.ssrLoadModule('/src/game/data/cityEquipmentPreparation.ts');
|
||||
const {
|
||||
xuzhouStayBattlePayoffRecordId,
|
||||
xuzhouStayBattlePayoffSourceBattleId,
|
||||
xuzhouStayBattlePayoffTargetBattleId,
|
||||
xuzhouStayId,
|
||||
xuzhouStayInformationVisitId,
|
||||
xuzhouStayResonanceDialogueId,
|
||||
xuzhouStayShareStorehouseDutyChoiceId,
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
} = await server.ssrLoadModule('/src/game/data/xuzhouStayBattlePayoff.ts');
|
||||
|
||||
const options = {
|
||||
expectedBattleId: 'first-battle-zhuo-commandery',
|
||||
@@ -630,6 +640,280 @@ try {
|
||||
'Expected legacy eighth-battle saves without city-equipment contribution progress to remain valid.'
|
||||
);
|
||||
|
||||
const eighthBattlePayoffOptions = {
|
||||
...eighthBattleOptions,
|
||||
validUnitIds: new Set([
|
||||
...eighthBattleOptions.validUnitIds,
|
||||
'mi-zhu'
|
||||
]),
|
||||
validAllyUnitIds: new Set([
|
||||
...eighthBattleOptions.validAllyUnitIds,
|
||||
'mi-zhu'
|
||||
])
|
||||
};
|
||||
const miZhuBattleSaveUnit = {
|
||||
...eighthBattleUnits.find((unit) => unit.id === 'guan-yu'),
|
||||
id: 'mi-zhu',
|
||||
hp: 28,
|
||||
maxHp: 28,
|
||||
attack: 7,
|
||||
stats: {
|
||||
might: 40,
|
||||
intelligence: 74,
|
||||
leadership: 70,
|
||||
agility: 58,
|
||||
luck: 82
|
||||
},
|
||||
x: 3,
|
||||
y: 2,
|
||||
equipment: createEquipmentSet()
|
||||
};
|
||||
const eighthBattlePayoffBaseState = {
|
||||
...eighthBattleBaseState,
|
||||
units: [
|
||||
...eighthBattleUnits.map((unit) => ({
|
||||
...unit,
|
||||
stats: unit.stats ? { ...unit.stats } : undefined,
|
||||
equipment: {
|
||||
weapon: { ...unit.equipment.weapon },
|
||||
armor: { ...unit.equipment.armor },
|
||||
accessory: { ...unit.equipment.accessory }
|
||||
}
|
||||
})),
|
||||
miZhuBattleSaveUnit
|
||||
]
|
||||
};
|
||||
const xuzhouStayPayoffIdentity = {
|
||||
version: 1,
|
||||
recordId: xuzhouStayBattlePayoffRecordId,
|
||||
cityStayId: xuzhouStayId,
|
||||
sourceBattleId: xuzhouStayBattlePayoffSourceBattleId,
|
||||
targetBattleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationVisitId: xuzhouStayInformationVisitId,
|
||||
dialogueId: xuzhouStayResonanceDialogueId
|
||||
};
|
||||
const validInformationOnlyPayoff = {
|
||||
...xuzhouStayPayoffIdentity,
|
||||
informationCompleted: true,
|
||||
informationCounterplays: 2,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 0,
|
||||
supportUses: 0,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 0
|
||||
};
|
||||
const validSharedDutyPayoff = {
|
||||
...validInformationOnlyPayoff,
|
||||
choiceId: xuzhouStayShareStorehouseDutyChoiceId,
|
||||
supportAttempts: 2,
|
||||
supportUses: 1,
|
||||
bonusHealing: 4
|
||||
};
|
||||
const validEntrustedLedgerPayoff = {
|
||||
...validInformationOnlyPayoff,
|
||||
choiceId: xuzhouStayTrustMiZhuLedgerChoiceId,
|
||||
supportAttempts: 1,
|
||||
supportUses: 1,
|
||||
bonusBuffTurns: 1
|
||||
};
|
||||
|
||||
[
|
||||
['information-only', validInformationOnlyPayoff],
|
||||
['shared-duty choice', validSharedDutyPayoff],
|
||||
['entrusted-ledger choice', validEntrustedLedgerPayoff]
|
||||
].forEach(([label, xuzhouStayBattlePayoff]) => {
|
||||
const candidate = {
|
||||
...eighthBattlePayoffBaseState,
|
||||
xuzhouStayBattlePayoff
|
||||
};
|
||||
const parsed = parseBattleSaveState(
|
||||
JSON.stringify(candidate),
|
||||
eighthBattlePayoffOptions
|
||||
);
|
||||
const parsedPayoff = parsed?.xuzhouStayBattlePayoff;
|
||||
assert(
|
||||
isValidBattleSaveState(candidate, eighthBattlePayoffOptions) &&
|
||||
parsedPayoff &&
|
||||
Object.keys(parsedPayoff).length ===
|
||||
Object.keys(xuzhouStayBattlePayoff).length &&
|
||||
Object.entries(xuzhouStayBattlePayoff).every(
|
||||
([key, value]) => parsedPayoff[key] === value
|
||||
) &&
|
||||
parsedPayoff !== xuzhouStayBattlePayoff,
|
||||
`Expected canonical ${label} Xuzhou payoff progress to round-trip as a deep-cloned battle-save field: ${JSON.stringify({
|
||||
strict: isValidBattleSaveState(
|
||||
candidate,
|
||||
eighthBattlePayoffOptions
|
||||
),
|
||||
expected: xuzhouStayBattlePayoff,
|
||||
actual: parsedPayoff
|
||||
})}`
|
||||
);
|
||||
});
|
||||
|
||||
assert(
|
||||
isValidBattleSaveState(
|
||||
eighthBattlePayoffBaseState,
|
||||
eighthBattlePayoffOptions
|
||||
) &&
|
||||
parseBattleSaveState(
|
||||
JSON.stringify(eighthBattlePayoffBaseState),
|
||||
eighthBattlePayoffOptions
|
||||
)?.xuzhouStayBattlePayoff === undefined,
|
||||
'Expected legacy eighth-battle saves without Xuzhou stay payoff progress to remain valid.'
|
||||
);
|
||||
|
||||
const normalizedGhostMiZhuPayoff = normalizeBattleSaveState(
|
||||
{
|
||||
...eighthBattleBaseState,
|
||||
xuzhouStayBattlePayoff: {
|
||||
...validSharedDutyPayoff,
|
||||
informationCounterplays: 1,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 8,
|
||||
supportUses: 1,
|
||||
bonusHealing: 4
|
||||
}
|
||||
},
|
||||
eighthBattleOptions
|
||||
)?.xuzhouStayBattlePayoff;
|
||||
assert(
|
||||
normalizedGhostMiZhuPayoff?.miZhuDeployed === false &&
|
||||
normalizedGhostMiZhuPayoff.supportAttempts === 0 &&
|
||||
normalizedGhostMiZhuPayoff.supportUses === 0 &&
|
||||
normalizedGhostMiZhuPayoff.bonusHealing === 0 &&
|
||||
normalizedGhostMiZhuPayoff.bonusBuffTurns === 0,
|
||||
`Expected a claimed Mi Zhu deployment absent from the saved roster to clear all support counters: ${JSON.stringify(normalizedGhostMiZhuPayoff)}`
|
||||
);
|
||||
|
||||
const normalizedSharedDutyCrossEffect =
|
||||
normalizeBattleSaveState(
|
||||
{
|
||||
...eighthBattlePayoffBaseState,
|
||||
xuzhouStayBattlePayoff: {
|
||||
...validSharedDutyPayoff,
|
||||
bonusBuffTurns: 999
|
||||
}
|
||||
},
|
||||
eighthBattlePayoffOptions
|
||||
)?.xuzhouStayBattlePayoff;
|
||||
const normalizedEntrustedLedgerCrossEffect =
|
||||
normalizeBattleSaveState(
|
||||
{
|
||||
...eighthBattlePayoffBaseState,
|
||||
xuzhouStayBattlePayoff: {
|
||||
...validEntrustedLedgerPayoff,
|
||||
bonusHealing: 999
|
||||
}
|
||||
},
|
||||
eighthBattlePayoffOptions
|
||||
)?.xuzhouStayBattlePayoff;
|
||||
assert(
|
||||
normalizedSharedDutyCrossEffect?.bonusHealing === 4 &&
|
||||
normalizedSharedDutyCrossEffect.bonusBuffTurns === 0 &&
|
||||
normalizedEntrustedLedgerCrossEffect?.bonusHealing === 0 &&
|
||||
normalizedEntrustedLedgerCrossEffect.bonusBuffTurns === 1,
|
||||
'Expected each Xuzhou resonance choice to remove the other choice effect during normalization.'
|
||||
);
|
||||
|
||||
const normalizedOversizedXuzhouPayoff =
|
||||
normalizeBattleSaveState(
|
||||
{
|
||||
...eighthBattlePayoffBaseState,
|
||||
xuzhouStayBattlePayoff: {
|
||||
...validSharedDutyPayoff,
|
||||
informationCounterplays: Number.MAX_SAFE_INTEGER,
|
||||
supportAttempts: Number.MAX_SAFE_INTEGER,
|
||||
supportUses: Number.MAX_SAFE_INTEGER,
|
||||
bonusHealing: Number.MAX_SAFE_INTEGER,
|
||||
bonusBuffTurns: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
},
|
||||
eighthBattlePayoffOptions
|
||||
)?.xuzhouStayBattlePayoff;
|
||||
assert(
|
||||
normalizedOversizedXuzhouPayoff?.informationCounterplays ===
|
||||
2 &&
|
||||
normalizedOversizedXuzhouPayoff.supportAttempts ===
|
||||
999999 &&
|
||||
normalizedOversizedXuzhouPayoff.supportUses === 1 &&
|
||||
normalizedOversizedXuzhouPayoff.bonusHealing === 4 &&
|
||||
normalizedOversizedXuzhouPayoff.bonusBuffTurns === 0,
|
||||
`Expected oversized Xuzhou payoff counters to clamp to canonical limits: ${JSON.stringify(normalizedOversizedXuzhouPayoff)}`
|
||||
);
|
||||
|
||||
const forgedXuzhouPayoffCases = [
|
||||
{
|
||||
label: 'target battle identity',
|
||||
payoff: {
|
||||
...validSharedDutyPayoff,
|
||||
targetBattleId: 'forged-target-battle'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'source battle identity',
|
||||
payoff: {
|
||||
...validSharedDutyPayoff,
|
||||
sourceBattleId: 'forged-source-battle'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'information identity',
|
||||
payoff: {
|
||||
...validSharedDutyPayoff,
|
||||
informationVisitId: 'forged-information'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'dialogue identity',
|
||||
payoff: {
|
||||
...validSharedDutyPayoff,
|
||||
dialogueId: 'forged-dialogue'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'resonance choice',
|
||||
payoff: {
|
||||
...validSharedDutyPayoff,
|
||||
choiceId: 'forged-choice'
|
||||
}
|
||||
}
|
||||
];
|
||||
forgedXuzhouPayoffCases.forEach(({ label, payoff }) => {
|
||||
const candidate = {
|
||||
...eighthBattlePayoffBaseState,
|
||||
xuzhouStayBattlePayoff: payoff
|
||||
};
|
||||
const normalized = normalizeBattleSaveState(
|
||||
candidate,
|
||||
eighthBattlePayoffOptions
|
||||
);
|
||||
assert(
|
||||
normalized?.xuzhouStayBattlePayoff === undefined &&
|
||||
!isValidBattleSaveState(
|
||||
candidate,
|
||||
eighthBattlePayoffOptions
|
||||
),
|
||||
`Expected forged Xuzhou ${label} progress to be removed and rejected by strict validation.`
|
||||
);
|
||||
});
|
||||
|
||||
const payoffAttachedToWrongBattle = {
|
||||
...validState,
|
||||
xuzhouStayBattlePayoff: validSharedDutyPayoff
|
||||
};
|
||||
assert(
|
||||
normalizeBattleSaveState(
|
||||
payoffAttachedToWrongBattle,
|
||||
options
|
||||
)?.xuzhouStayBattlePayoff === undefined &&
|
||||
!isValidBattleSaveState(
|
||||
payoffAttachedToWrongBattle,
|
||||
options
|
||||
),
|
||||
'Expected Xuzhou payoff progress attached to a non-eighth battle save to be removed and rejected by strict validation.'
|
||||
);
|
||||
|
||||
const validSortieStatsState = {
|
||||
...validState,
|
||||
battleStats: {
|
||||
|
||||
Reference in New Issue
Block a user