feat: make Xuzhou choices shape battle eight

This commit is contained in:
2026-07-27 17:39:13 +09:00
parent 2737354e30
commit eec4896002
13 changed files with 4231 additions and 84 deletions

View File

@@ -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: {