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: {
|
||||
|
||||
@@ -58,6 +58,16 @@ try {
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
||||
const { normalizeSortieFormationAssignments } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts');
|
||||
const {
|
||||
xuzhouStayBattlePayoffRecordId,
|
||||
xuzhouStayBattlePayoffSourceBattleId,
|
||||
xuzhouStayBattlePayoffTargetBattleId,
|
||||
xuzhouStayId,
|
||||
xuzhouStayInformationVisitId,
|
||||
xuzhouStayResonanceDialogueId,
|
||||
xuzhouStayShareStorehouseDutyChoiceId,
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
} = await server.ssrLoadModule('/src/game/data/xuzhouStayBattlePayoff.ts');
|
||||
const sortieStatsFixture = (unitId, overrides = {}) => ({
|
||||
unitId,
|
||||
hpBefore: 30,
|
||||
@@ -2601,11 +2611,268 @@ try {
|
||||
`Expected explicitly loaded corrupted slot timestamp to be normalized: ${JSON.stringify(corruptedSlot)}`
|
||||
);
|
||||
|
||||
const eighthScenario =
|
||||
battleScenarios[xuzhouStayBattlePayoffTargetBattleId];
|
||||
const eighthAlliedUnits = eighthScenario.units.filter(
|
||||
(unit) => unit.faction === 'ally'
|
||||
);
|
||||
const xuzhouStayPayoffIdentity = {
|
||||
version: 1,
|
||||
recordId: xuzhouStayBattlePayoffRecordId,
|
||||
cityStayId: xuzhouStayId,
|
||||
sourceBattleId: xuzhouStayBattlePayoffSourceBattleId,
|
||||
targetBattleId: xuzhouStayBattlePayoffTargetBattleId,
|
||||
informationVisitId: xuzhouStayInformationVisitId,
|
||||
dialogueId: xuzhouStayResonanceDialogueId
|
||||
};
|
||||
const eighthReportBase = {
|
||||
battleId: eighthScenario.id,
|
||||
battleTitle: eighthScenario.title,
|
||||
outcome: 'victory',
|
||||
turnNumber: 8,
|
||||
rewardGold: 0,
|
||||
defeatedEnemies: 8,
|
||||
totalEnemies: 8,
|
||||
objectives: [],
|
||||
units: eighthAlliedUnits,
|
||||
bonds: eighthScenario.bonds.map((bond) => ({
|
||||
...bond,
|
||||
battleExp: 0
|
||||
})),
|
||||
itemRewards: [],
|
||||
campaignRewards: {
|
||||
supplies: [],
|
||||
equipment: [],
|
||||
reputation: [],
|
||||
recruits: [],
|
||||
unlocks: []
|
||||
},
|
||||
completedCampDialogues: [],
|
||||
completedCampVisits: [],
|
||||
createdAt: '2026-07-27T08:00:00.000Z'
|
||||
};
|
||||
const seedXuzhouStayMemory = (choiceId) => {
|
||||
resetCampaignState();
|
||||
setFirstBattleReport(seventhBattleReport);
|
||||
const sourceState = getCampaignState();
|
||||
sourceState.completedCampVisits = [
|
||||
...new Set([
|
||||
...sourceState.completedCampVisits,
|
||||
xuzhouStayInformationVisitId
|
||||
])
|
||||
];
|
||||
if (choiceId) {
|
||||
sourceState.completedCampDialogues = [
|
||||
...new Set([
|
||||
...sourceState.completedCampDialogues,
|
||||
xuzhouStayResonanceDialogueId
|
||||
])
|
||||
];
|
||||
sourceState.campDialogueChoiceIds = {
|
||||
...sourceState.campDialogueChoiceIds,
|
||||
[xuzhouStayResonanceDialogueId]: choiceId
|
||||
};
|
||||
} else {
|
||||
sourceState.completedCampDialogues =
|
||||
sourceState.completedCampDialogues.filter(
|
||||
(dialogueId) =>
|
||||
dialogueId !== xuzhouStayResonanceDialogueId
|
||||
);
|
||||
delete sourceState.campDialogueChoiceIds[
|
||||
xuzhouStayResonanceDialogueId
|
||||
];
|
||||
}
|
||||
setCampaignState(sourceState);
|
||||
};
|
||||
const campaignPayoffFixtures = [
|
||||
{
|
||||
label: 'information-only',
|
||||
choiceId: undefined,
|
||||
snapshot: {
|
||||
...xuzhouStayPayoffIdentity,
|
||||
informationCompleted: true,
|
||||
informationCounterplays: 2,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 0,
|
||||
supportUses: 0,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'shared-duty choice',
|
||||
choiceId: xuzhouStayShareStorehouseDutyChoiceId,
|
||||
snapshot: {
|
||||
...xuzhouStayPayoffIdentity,
|
||||
informationCompleted: true,
|
||||
choiceId: xuzhouStayShareStorehouseDutyChoiceId,
|
||||
informationCounterplays: 2,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 2,
|
||||
supportUses: 1,
|
||||
bonusHealing: 4,
|
||||
bonusBuffTurns: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'entrusted-ledger choice',
|
||||
choiceId: xuzhouStayTrustMiZhuLedgerChoiceId,
|
||||
snapshot: {
|
||||
...xuzhouStayPayoffIdentity,
|
||||
informationCompleted: true,
|
||||
choiceId: xuzhouStayTrustMiZhuLedgerChoiceId,
|
||||
informationCounterplays: 1,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 1,
|
||||
supportUses: 1,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
seedXuzhouStayMemory(
|
||||
xuzhouStayShareStorehouseDutyChoiceId
|
||||
);
|
||||
setFirstBattleReport({ ...eighthReportBase });
|
||||
const legacyEighthCampaignState = loadCampaignState();
|
||||
assert(
|
||||
legacyEighthCampaignState.firstBattleReport
|
||||
?.xuzhouStayBattlePayoff === undefined &&
|
||||
legacyEighthCampaignState.battleHistory[
|
||||
eighthScenario.id
|
||||
]?.xuzhouStayBattlePayoff === undefined,
|
||||
'Expected legacy eighth-battle campaign reports without a Xuzhou payoff field to remain valid.'
|
||||
);
|
||||
|
||||
for (const fixture of campaignPayoffFixtures) {
|
||||
seedXuzhouStayMemory(fixture.choiceId);
|
||||
const inputSnapshot = { ...fixture.snapshot };
|
||||
const normalizedReport = setFirstBattleReport({
|
||||
...eighthReportBase,
|
||||
xuzhouStayBattlePayoff: inputSnapshot
|
||||
});
|
||||
const recordedCampaign = loadCampaignState();
|
||||
const reportSnapshot =
|
||||
recordedCampaign.firstBattleReport
|
||||
?.xuzhouStayBattlePayoff;
|
||||
const historySnapshot =
|
||||
recordedCampaign.battleHistory[eighthScenario.id]
|
||||
?.xuzhouStayBattlePayoff;
|
||||
assert(
|
||||
flatRecordsEqual(
|
||||
normalizedReport.xuzhouStayBattlePayoff,
|
||||
fixture.snapshot
|
||||
) &&
|
||||
flatRecordsEqual(reportSnapshot, fixture.snapshot) &&
|
||||
flatRecordsEqual(historySnapshot, fixture.snapshot) &&
|
||||
reportSnapshot !== historySnapshot,
|
||||
`Expected canonical ${fixture.label} payoff to copy from the eighth-battle report into battle history: ${JSON.stringify({
|
||||
normalizedReport:
|
||||
normalizedReport.xuzhouStayBattlePayoff,
|
||||
reportSnapshot,
|
||||
historySnapshot
|
||||
})}`
|
||||
);
|
||||
|
||||
inputSnapshot.informationCounterplays = 0;
|
||||
inputSnapshot.supportAttempts = 0;
|
||||
inputSnapshot.supportUses = 0;
|
||||
inputSnapshot.bonusHealing = 0;
|
||||
inputSnapshot.bonusBuffTurns = 0;
|
||||
const detachedCampaign = getCampaignState();
|
||||
assert(
|
||||
flatRecordsEqual(
|
||||
detachedCampaign.battleHistory[eighthScenario.id]
|
||||
?.xuzhouStayBattlePayoff,
|
||||
fixture.snapshot
|
||||
),
|
||||
`Expected later mutations of the report input not to alter the stored ${fixture.label} settlement snapshot.`
|
||||
);
|
||||
|
||||
if (
|
||||
fixture.choiceId ===
|
||||
xuzhouStayShareStorehouseDutyChoiceId
|
||||
) {
|
||||
const changedCurrentState = getCampaignState();
|
||||
changedCurrentState.campDialogueChoiceIds = {
|
||||
...changedCurrentState.campDialogueChoiceIds,
|
||||
[xuzhouStayResonanceDialogueId]:
|
||||
xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
};
|
||||
changedCurrentState.roster =
|
||||
changedCurrentState.roster.map((unit) =>
|
||||
unit.id === 'mi-zhu'
|
||||
? {
|
||||
...unit,
|
||||
hp: 1,
|
||||
equipment: {
|
||||
weapon: {
|
||||
itemId: 'training-sword',
|
||||
level: 1,
|
||||
exp: 0
|
||||
},
|
||||
armor: {
|
||||
itemId: 'cloth-armor',
|
||||
level: 1,
|
||||
exp: 0
|
||||
},
|
||||
accessory: {
|
||||
itemId: 'grain-pouch',
|
||||
level: 1,
|
||||
exp: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
: unit
|
||||
);
|
||||
changedCurrentState.selectedSortieUnitIds =
|
||||
changedCurrentState.selectedSortieUnitIds.filter(
|
||||
(unitId) => unitId !== 'mi-zhu'
|
||||
);
|
||||
setCampaignState(changedCurrentState);
|
||||
const historicalCampaign = loadCampaignState();
|
||||
assert(
|
||||
flatRecordsEqual(
|
||||
historicalCampaign.firstBattleReport
|
||||
?.xuzhouStayBattlePayoff,
|
||||
fixture.snapshot
|
||||
) &&
|
||||
flatRecordsEqual(
|
||||
historicalCampaign.battleHistory[
|
||||
eighthScenario.id
|
||||
]?.xuzhouStayBattlePayoff,
|
||||
fixture.snapshot
|
||||
),
|
||||
`Expected completed Xuzhou payoff history to remain a report/settlement snapshot instead of being recomputed from later resonance or roster state: ${JSON.stringify({
|
||||
report:
|
||||
historicalCampaign.firstBattleReport
|
||||
?.xuzhouStayBattlePayoff,
|
||||
history:
|
||||
historicalCampaign.battleHistory[
|
||||
eighthScenario.id
|
||||
]?.xuzhouStayBattlePayoff
|
||||
})}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/sortie-preset/sortie-performance/sortie-review/sortie-order/latest-history/detail recovery, sortie performance/review/order legacy/deep-clone/retry/reward-idempotence safety, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.');
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
function flatRecordsEqual(left, right) {
|
||||
if (!left || !right) {
|
||||
return left === right;
|
||||
}
|
||||
const leftEntries = Object.entries(left);
|
||||
return (
|
||||
leftEntries.length === Object.keys(right).length &&
|
||||
leftEntries.every(([key, value]) => right[key] === value)
|
||||
);
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
|
||||
@@ -23,6 +23,7 @@ const checks = [
|
||||
'scripts/verify-camp-reward-data.mjs',
|
||||
'scripts/verify-city-stay-data.mjs',
|
||||
'scripts/verify-city-equipment-preparation.mjs',
|
||||
'scripts/verify-xuzhou-stay-battle-payoff.mjs',
|
||||
'scripts/verify-prologue-village-data.mjs',
|
||||
'scripts/verify-first-battle-camp-followup.mjs',
|
||||
'scripts/verify-first-battle-camaraderie-memory.mjs',
|
||||
|
||||
@@ -1936,10 +1936,10 @@ function assertBattleResultContribution(
|
||||
assert(
|
||||
result.visibleResultTexts.some((text) =>
|
||||
text.includes(
|
||||
battle.cityEquipmentPreparation.resultText
|
||||
battle.cityEquipmentPreparation.compactResultText
|
||||
)
|
||||
),
|
||||
`${renderer}: visible result subtitle must contain the persisted contribution feedback: ${JSON.stringify(
|
||||
`${renderer}: visible result subtitle must contain the compact persisted contribution feedback: ${JSON.stringify(
|
||||
result.visibleResultTexts
|
||||
)}`
|
||||
);
|
||||
|
||||
1281
scripts/verify-xuzhou-stay-battle-payoff-browser.mjs
Normal file
1281
scripts/verify-xuzhou-stay-battle-payoff-browser.mjs
Normal file
File diff suppressed because it is too large
Load Diff
575
scripts/verify-xuzhou-stay-battle-payoff.mjs
Normal file
575
scripts/verify-xuzhou-stay-battle-payoff.mjs
Normal file
@@ -0,0 +1,575 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'vite';
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
server: { middlewareMode: true, hmr: false },
|
||||
appType: 'custom'
|
||||
});
|
||||
|
||||
try {
|
||||
const payoff = await server.ssrLoadModule(
|
||||
'/src/game/data/xuzhouStayBattlePayoff.ts'
|
||||
);
|
||||
const cityStays = await server.ssrLoadModule(
|
||||
'/src/game/data/cityStays.ts'
|
||||
);
|
||||
const battleUsables = await server.ssrLoadModule(
|
||||
'/src/game/data/battleUsables.ts'
|
||||
);
|
||||
const scenario = await server.ssrLoadModule(
|
||||
'/src/game/data/scenario.ts'
|
||||
);
|
||||
|
||||
verifyCanonicalDefinition({
|
||||
payoff,
|
||||
cityStays,
|
||||
battleUsables,
|
||||
scenario
|
||||
});
|
||||
const memories = verifyCampaignResolution(payoff);
|
||||
verifyRuntimeAndSnapshotNormalization(payoff, memories);
|
||||
|
||||
console.log(
|
||||
'Xuzhou stay battle payoff verification passed (canonical city provenance, source-victory precedence, independent information/dialogue resolution, target-battle isolation, first-three-turn aid/encourage effects, counterplay bounds, Mi Zhu deployment neutralization, choice-exclusive counters, snapshot cloning, and forged-state rejection).'
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
function verifyCanonicalDefinition({
|
||||
payoff,
|
||||
cityStays,
|
||||
battleUsables,
|
||||
scenario
|
||||
}) {
|
||||
const city = cityStays.getCityStayDefinition('xuzhou');
|
||||
const definition = payoff.xuzhouStayBattlePayoffDefinition;
|
||||
assert.equal(definition.version, 1);
|
||||
assert.equal(definition.recordId, 'xuzhou-stay-battle-payoff');
|
||||
assert.equal(definition.cityStayId, city.id);
|
||||
assert.equal(definition.sourceBattleId, city.afterBattleId);
|
||||
assert.equal(definition.targetBattleId, city.nextBattleId);
|
||||
assert.equal(definition.information.visitId, city.information.id);
|
||||
assert.equal(definition.dialogue.dialogueId, city.dialogue.id);
|
||||
assert.deepEqual(
|
||||
Object.keys(definition.dialogue.choices),
|
||||
city.dialogue.choices.map((choice) => choice.id),
|
||||
'Both canonical Xuzhou dialogue choices must have a battle payoff.'
|
||||
);
|
||||
assert.deepEqual(payoff.xuzhouStayBattlePayoffChoiceIds, [
|
||||
'city-xuzhou-share-storehouse-duty',
|
||||
'city-xuzhou-trust-mi-zhu-ledger'
|
||||
]);
|
||||
|
||||
assert.deepEqual(definition.information.effect, {
|
||||
kind: 'deployment-intent-preview',
|
||||
enemyUnitIds: ['xiaopei-guard-a', 'xiaopei-guard-b'],
|
||||
previewCount: 2
|
||||
});
|
||||
const eighthBattleEnemyIds = new Set(
|
||||
scenario.eighthBattleUnits
|
||||
.filter((unit) => unit.faction === 'enemy')
|
||||
.map((unit) => unit.id)
|
||||
);
|
||||
definition.information.effect.enemyUnitIds.forEach((unitId) => {
|
||||
assert(
|
||||
eighthBattleEnemyIds.has(unitId),
|
||||
`Information preview target is missing from battle 8: ${unitId}`
|
||||
);
|
||||
});
|
||||
|
||||
const aid = payoff.getXuzhouStayResonanceEffect(
|
||||
'city-xuzhou-share-storehouse-duty'
|
||||
);
|
||||
assert.deepEqual(aid, {
|
||||
kind: 'mi-zhu-first-aid-healing',
|
||||
actorUnitId: 'mi-zhu',
|
||||
usableId: 'aid',
|
||||
activeThroughTurn: 3,
|
||||
bonusHealing: 4,
|
||||
maxBattleUses: 1
|
||||
});
|
||||
const encourage = payoff.getXuzhouStayResonanceEffect(
|
||||
'city-xuzhou-trust-mi-zhu-ledger'
|
||||
);
|
||||
assert.deepEqual(encourage, {
|
||||
kind: 'mi-zhu-first-encourage-duration',
|
||||
actorUnitId: 'mi-zhu',
|
||||
usableId: 'encourage',
|
||||
activeThroughTurn: 3,
|
||||
bonusBuffTurns: 1,
|
||||
maxBattleUses: 1
|
||||
});
|
||||
assert.equal(
|
||||
payoff.getXuzhouStayResonanceEffect('forged-choice'),
|
||||
undefined
|
||||
);
|
||||
assert(
|
||||
battleUsables.unitStrategyIds['mi-zhu'].includes(aid.usableId),
|
||||
'Mi Zhu must know aid before his aid payoff can be applied.'
|
||||
);
|
||||
assert(
|
||||
battleUsables.unitStrategyIds['mi-zhu'].includes(
|
||||
encourage.usableId
|
||||
),
|
||||
'Mi Zhu must know encourage before his encourage payoff can be applied.'
|
||||
);
|
||||
assert.equal(
|
||||
battleUsables.usableCatalog.encourage.duration + encourage.bonusBuffTurns,
|
||||
3,
|
||||
'The entrusted ledger choice should extend encourage from two to three turns.'
|
||||
);
|
||||
|
||||
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(1), true);
|
||||
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(3), true);
|
||||
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(0), false);
|
||||
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(4), false);
|
||||
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(2.5), false);
|
||||
}
|
||||
|
||||
function verifyCampaignResolution(payoff) {
|
||||
const sourceBattleId =
|
||||
payoff.xuzhouStayBattlePayoffSourceBattleId;
|
||||
const targetBattleId =
|
||||
payoff.xuzhouStayBattlePayoffTargetBattleId;
|
||||
const victory = {
|
||||
battleId: sourceBattleId,
|
||||
outcome: 'victory'
|
||||
};
|
||||
const baseCampaign = {
|
||||
battleHistory: {
|
||||
[sourceBattleId]: victory
|
||||
},
|
||||
firstBattleReport: null,
|
||||
completedCampVisits: [],
|
||||
completedCampDialogues: [],
|
||||
campDialogueChoiceIds: {}
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff(baseCampaign),
|
||||
undefined,
|
||||
'A victory without either city activity must not create a payoff.'
|
||||
);
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff(
|
||||
{
|
||||
...baseCampaign,
|
||||
completedCampVisits: [payoff.xuzhouStayInformationVisitId]
|
||||
},
|
||||
'ninth-battle-xuzhou-gate-night-raid'
|
||||
),
|
||||
undefined,
|
||||
'Xuzhou preparation must not leak beyond battle 8.'
|
||||
);
|
||||
|
||||
const informationOnly =
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampVisits: [
|
||||
'unrelated-visit',
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
]
|
||||
});
|
||||
assert(informationOnly);
|
||||
assert.equal(informationOnly.recordSource, 'battle-history');
|
||||
assert.equal(informationOnly.informationCompleted, true);
|
||||
assert.equal(informationOnly.choiceId, undefined);
|
||||
assert.deepEqual(informationOnly.informationEffect, {
|
||||
kind: 'deployment-intent-preview',
|
||||
enemyUnitIds: ['xiaopei-guard-a', 'xiaopei-guard-b'],
|
||||
previewCount: 2
|
||||
});
|
||||
assert.notEqual(
|
||||
informationOnly.informationEffect.enemyUnitIds,
|
||||
payoff.xuzhouStayInformationEnemyUnitIds,
|
||||
'Resolved memories must not expose the canonical tuple by reference.'
|
||||
);
|
||||
|
||||
const aidOnly = payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampDialogues: [
|
||||
payoff.xuzhouStayResonanceDialogueId
|
||||
],
|
||||
campDialogueChoiceIds: {
|
||||
[payoff.xuzhouStayResonanceDialogueId]:
|
||||
payoff.xuzhouStayShareStorehouseDutyChoiceId
|
||||
}
|
||||
});
|
||||
assert(aidOnly);
|
||||
assert.equal(aidOnly.informationCompleted, false);
|
||||
assert.equal(
|
||||
aidOnly.choiceId,
|
||||
payoff.xuzhouStayShareStorehouseDutyChoiceId
|
||||
);
|
||||
assert.equal(
|
||||
aidOnly.resonanceEffect.kind,
|
||||
'mi-zhu-first-aid-healing'
|
||||
);
|
||||
|
||||
const bothWithEncourage =
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampVisits: [
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
],
|
||||
completedCampDialogues: [
|
||||
payoff.xuzhouStayResonanceDialogueId
|
||||
],
|
||||
campDialogueChoiceIds: {
|
||||
[payoff.xuzhouStayResonanceDialogueId]:
|
||||
payoff.xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
}
|
||||
});
|
||||
assert(bothWithEncourage);
|
||||
assert.equal(bothWithEncourage.informationCompleted, true);
|
||||
assert.equal(
|
||||
bothWithEncourage.choiceId,
|
||||
payoff.xuzhouStayTrustMiZhuLedgerChoiceId
|
||||
);
|
||||
assert.equal(
|
||||
bothWithEncourage.resonanceEffect.kind,
|
||||
'mi-zhu-first-encourage-duration'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampDialogues: [],
|
||||
campDialogueChoiceIds: {
|
||||
[payoff.xuzhouStayResonanceDialogueId]:
|
||||
payoff.xuzhouStayShareStorehouseDutyChoiceId
|
||||
}
|
||||
}),
|
||||
undefined,
|
||||
'A remembered choice without completed dialogue is not evidence.'
|
||||
);
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampDialogues: [
|
||||
payoff.xuzhouStayResonanceDialogueId
|
||||
],
|
||||
campDialogueChoiceIds: {
|
||||
[payoff.xuzhouStayResonanceDialogueId]: 'forged-choice'
|
||||
}
|
||||
}),
|
||||
undefined,
|
||||
'A completed dialogue with a forged choice must stay neutral.'
|
||||
);
|
||||
const informationSurvivesForgedDialogue =
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
completedCampVisits: [
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
],
|
||||
completedCampDialogues: [
|
||||
payoff.xuzhouStayResonanceDialogueId
|
||||
],
|
||||
campDialogueChoiceIds: {
|
||||
[payoff.xuzhouStayResonanceDialogueId]: 'forged-choice'
|
||||
}
|
||||
});
|
||||
assert(informationSurvivesForgedDialogue);
|
||||
assert.equal(
|
||||
informationSurvivesForgedDialogue.informationCompleted,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
informationSurvivesForgedDialogue.choiceId,
|
||||
undefined,
|
||||
'Invalid dialogue data must not erase independently earned information.'
|
||||
);
|
||||
|
||||
const legacyInformation =
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
battleHistory: {},
|
||||
firstBattleReport: victory,
|
||||
completedCampVisits: [
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
]
|
||||
});
|
||||
assert(legacyInformation);
|
||||
assert.equal(legacyInformation.recordSource, 'legacy-report');
|
||||
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
battleHistory: {
|
||||
[sourceBattleId]: {
|
||||
battleId: sourceBattleId,
|
||||
outcome: 'defeat'
|
||||
}
|
||||
},
|
||||
firstBattleReport: victory,
|
||||
completedCampVisits: [
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
]
|
||||
}),
|
||||
undefined,
|
||||
'An explicit invalid history entry must shadow a stale legacy report.'
|
||||
);
|
||||
assert.equal(
|
||||
payoff.resolveXuzhouStayBattlePayoff({
|
||||
...baseCampaign,
|
||||
battleHistory: {},
|
||||
firstBattleReport: {
|
||||
battleId: targetBattleId,
|
||||
outcome: 'victory'
|
||||
},
|
||||
completedCampVisits: [
|
||||
payoff.xuzhouStayInformationVisitId
|
||||
]
|
||||
}),
|
||||
undefined,
|
||||
'A victory from another battle must not authorize this payoff.'
|
||||
);
|
||||
|
||||
return {
|
||||
informationOnly,
|
||||
aidOnly,
|
||||
bothWithEncourage
|
||||
};
|
||||
}
|
||||
|
||||
function verifyRuntimeAndSnapshotNormalization(payoff, memories) {
|
||||
const aidRuntime =
|
||||
payoff.createXuzhouStayBattlePayoffRuntime(
|
||||
memories.aidOnly,
|
||||
{
|
||||
miZhuDeployed: true,
|
||||
expectedRosterUnitIds: ['liu-bei', 'mi-zhu']
|
||||
}
|
||||
);
|
||||
assert.deepEqual(aidRuntime, {
|
||||
version: 1,
|
||||
recordId: 'xuzhou-stay-battle-payoff',
|
||||
cityStayId: 'xuzhou',
|
||||
sourceBattleId: 'seventh-battle-xuzhou-rescue',
|
||||
targetBattleId: 'eighth-battle-xiaopei-supply-road',
|
||||
informationVisitId: 'city-xuzhou-xiaopei-ledger',
|
||||
dialogueId: 'city-xuzhou-liu-mi-supply-trust',
|
||||
informationCompleted: false,
|
||||
choiceId: 'city-xuzhou-share-storehouse-duty',
|
||||
informationCounterplays: 0,
|
||||
miZhuDeployed: true,
|
||||
supportAttempts: 0,
|
||||
supportUses: 0,
|
||||
bonusHealing: 0,
|
||||
bonusBuffTurns: 0
|
||||
});
|
||||
|
||||
const saturatedAid =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime(
|
||||
{
|
||||
...aidRuntime,
|
||||
informationCounterplays: 99,
|
||||
supportAttempts: 4.9,
|
||||
supportUses: 8,
|
||||
bonusHealing: 99,
|
||||
bonusBuffTurns: 99
|
||||
},
|
||||
{
|
||||
expectedBattleId:
|
||||
payoff.xuzhouStayBattlePayoffTargetBattleId,
|
||||
expectedRosterUnitIds: new Set(['mi-zhu']),
|
||||
expectedMemory: memories.aidOnly
|
||||
}
|
||||
);
|
||||
assert.deepEqual(saturatedAid, {
|
||||
...aidRuntime,
|
||||
supportAttempts: 4,
|
||||
supportUses: 1,
|
||||
bonusHealing: 4
|
||||
});
|
||||
const zeroBenefitAid =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime({
|
||||
...aidRuntime,
|
||||
supportAttempts: 1,
|
||||
supportUses: 1,
|
||||
bonusHealing: 0
|
||||
});
|
||||
assert.equal(
|
||||
zeroBenefitAid.supportUses,
|
||||
0,
|
||||
'Aid cannot be consumed unless it provided at least one point of real bonus healing.'
|
||||
);
|
||||
assert.equal(zeroBenefitAid.bonusHealing, 0);
|
||||
|
||||
const informationRuntime =
|
||||
payoff.createXuzhouStayBattlePayoffRuntime(
|
||||
memories.informationOnly,
|
||||
{
|
||||
miZhuDeployed: false,
|
||||
expectedRosterUnitIds: ['liu-bei']
|
||||
}
|
||||
);
|
||||
const boundedInformation =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime({
|
||||
...informationRuntime,
|
||||
informationCounterplays: 500,
|
||||
supportAttempts: 3,
|
||||
supportUses: 1,
|
||||
bonusHealing: 4,
|
||||
bonusBuffTurns: 1
|
||||
});
|
||||
assert.equal(boundedInformation.informationCounterplays, 2);
|
||||
assert.equal(boundedInformation.supportAttempts, 0);
|
||||
assert.equal(boundedInformation.supportUses, 0);
|
||||
assert.equal(boundedInformation.bonusHealing, 0);
|
||||
assert.equal(boundedInformation.bonusBuffTurns, 0);
|
||||
|
||||
const neutralizedGhost =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime(
|
||||
{
|
||||
...saturatedAid,
|
||||
informationCompleted: true,
|
||||
informationCounterplays: 2,
|
||||
miZhuDeployed: true
|
||||
},
|
||||
{
|
||||
expectedRosterUnitIds: ['liu-bei']
|
||||
}
|
||||
);
|
||||
assert.equal(neutralizedGhost.miZhuDeployed, false);
|
||||
assert.equal(neutralizedGhost.supportAttempts, 0);
|
||||
assert.equal(neutralizedGhost.supportUses, 0);
|
||||
assert.equal(neutralizedGhost.bonusHealing, 0);
|
||||
assert.equal(neutralizedGhost.bonusBuffTurns, 0);
|
||||
assert.equal(
|
||||
neutralizedGhost.informationCounterplays,
|
||||
2,
|
||||
'Mi Zhu deployment must not erase the independent information payoff.'
|
||||
);
|
||||
|
||||
const rosterRepairsDeployment =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime(
|
||||
{
|
||||
...aidRuntime,
|
||||
miZhuDeployed: false,
|
||||
supportAttempts: 2,
|
||||
supportUses: 1,
|
||||
bonusHealing: 3
|
||||
},
|
||||
{
|
||||
expectedRosterUnitIds: ['mi-zhu']
|
||||
}
|
||||
);
|
||||
assert.equal(rosterRepairsDeployment.miZhuDeployed, true);
|
||||
assert.equal(rosterRepairsDeployment.supportAttempts, 2);
|
||||
assert.equal(rosterRepairsDeployment.supportUses, 1);
|
||||
assert.equal(rosterRepairsDeployment.bonusHealing, 3);
|
||||
|
||||
const encourageRuntime =
|
||||
payoff.createXuzhouStayBattlePayoffRuntime(
|
||||
memories.bothWithEncourage,
|
||||
{
|
||||
miZhuDeployed: true,
|
||||
expectedRosterUnitIds: ['mi-zhu']
|
||||
}
|
||||
);
|
||||
const appliedEncourage =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime(
|
||||
{
|
||||
...encourageRuntime,
|
||||
informationCounterplays: 2,
|
||||
supportAttempts: 3,
|
||||
supportUses: 7,
|
||||
bonusHealing: 50,
|
||||
bonusBuffTurns: 50
|
||||
},
|
||||
{
|
||||
expectedMemory: memories.bothWithEncourage
|
||||
}
|
||||
);
|
||||
assert.equal(appliedEncourage.informationCounterplays, 2);
|
||||
assert.equal(appliedEncourage.supportAttempts, 3);
|
||||
assert.equal(appliedEncourage.supportUses, 1);
|
||||
assert.equal(appliedEncourage.bonusHealing, 0);
|
||||
assert.equal(appliedEncourage.bonusBuffTurns, 1);
|
||||
|
||||
const noEncourageUse =
|
||||
payoff.normalizeXuzhouStayBattlePayoffRuntime({
|
||||
...encourageRuntime,
|
||||
supportAttempts: 1,
|
||||
supportUses: 0,
|
||||
bonusBuffTurns: 99
|
||||
});
|
||||
assert.equal(noEncourageUse.supportAttempts, 1);
|
||||
assert.equal(noEncourageUse.supportUses, 0);
|
||||
assert.equal(noEncourageUse.bonusBuffTurns, 0);
|
||||
|
||||
const snapshot =
|
||||
payoff.createXuzhouStayBattlePayoffSnapshot(
|
||||
appliedEncourage,
|
||||
{
|
||||
expectedBattleId:
|
||||
payoff.xuzhouStayBattlePayoffTargetBattleId,
|
||||
expectedRosterUnitIds: ['mi-zhu']
|
||||
}
|
||||
);
|
||||
assert.deepEqual(snapshot, appliedEncourage);
|
||||
assert.notEqual(snapshot, appliedEncourage);
|
||||
const snapshotClone =
|
||||
payoff.cloneXuzhouStayBattlePayoffSnapshot(snapshot);
|
||||
const runtimeClone =
|
||||
payoff.cloneXuzhouStayBattlePayoffRuntime(appliedEncourage);
|
||||
assert.deepEqual(snapshotClone, snapshot);
|
||||
assert.deepEqual(runtimeClone, appliedEncourage);
|
||||
assert.notEqual(snapshotClone, snapshot);
|
||||
assert.notEqual(runtimeClone, appliedEncourage);
|
||||
|
||||
assert.equal(
|
||||
payoff.normalizeXuzhouStayBattlePayoffSnapshot(snapshot, {
|
||||
expectedBattleId: 'ninth-battle-xuzhou-gate-night-raid'
|
||||
}),
|
||||
undefined,
|
||||
'A containing report from another battle must reject the snapshot.'
|
||||
);
|
||||
assert.equal(
|
||||
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
{
|
||||
...snapshot,
|
||||
targetBattleId: 'forged-battle'
|
||||
}
|
||||
),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
{
|
||||
...snapshot,
|
||||
choiceId: 'forged-choice'
|
||||
}
|
||||
),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
{
|
||||
...snapshot,
|
||||
informationCompleted: false
|
||||
},
|
||||
{
|
||||
expectedMemory: memories.bothWithEncourage
|
||||
}
|
||||
),
|
||||
undefined,
|
||||
'Loaded runtime evidence must match the campaign-derived memory.'
|
||||
);
|
||||
assert.equal(
|
||||
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
|
||||
{
|
||||
...snapshot,
|
||||
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId
|
||||
},
|
||||
{
|
||||
expectedMemory: memories.bothWithEncourage
|
||||
}
|
||||
),
|
||||
undefined,
|
||||
'A save cannot exchange one completed resonance choice for another.'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user