feat: add selectable core resonance pairs
This commit is contained in:
@@ -44,6 +44,7 @@ try {
|
||||
setCampaignReserveTrainingFocus,
|
||||
setCampaignReserveTrainingAssignment,
|
||||
setCampaignSortieOrderSelection,
|
||||
setCampaignSortieResonanceSelection,
|
||||
setFirstBattleReport
|
||||
} = await server.ssrLoadModule('/src/game/state/campaignState.ts');
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
@@ -491,6 +492,174 @@ try {
|
||||
`Expected clearing the matching battle order to remove the scoped selection: ${JSON.stringify(clearedOrderSelection.sortieOrderSelection)}`
|
||||
);
|
||||
|
||||
const coreResonanceFixture = {
|
||||
version: 1,
|
||||
updatedAt: '2026-07-03T11:15:00.000Z',
|
||||
step: 'first-camp',
|
||||
roster: [
|
||||
{ id: 'liu-bei', name: 'Liu Bei', faction: 'ally' },
|
||||
{ id: 'guan-yu', name: 'Guan Yu', faction: 'ally' },
|
||||
{ id: 'zhang-fei', name: 'Zhang Fei', faction: 'ally' }
|
||||
],
|
||||
bonds: [
|
||||
{ id: 'liu-bei__guan-yu', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: 30, exp: 0, battleExp: 0 },
|
||||
{ id: 'liu-bei__zhang-fei', title: 'Young Bond', unitIds: ['liu-bei', 'zhang-fei'], level: 29, exp: 0, battleExp: 0 }
|
||||
],
|
||||
selectedSortieUnitIds: ['liu-bei', 'guan-yu'],
|
||||
sortieResonanceSelection: {
|
||||
battleId: 'first-battle-zhuo-commandery',
|
||||
bondId: 'liu-bei__guan-yu',
|
||||
extra: true
|
||||
}
|
||||
};
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify(coreResonanceFixture));
|
||||
const normalizedCoreResonance = loadCampaignState();
|
||||
assert(
|
||||
JSON.stringify(normalizedCoreResonance.sortieResonanceSelection) === JSON.stringify({
|
||||
battleId: 'first-battle-zhuo-commandery',
|
||||
bondId: 'liu-bei__guan-yu'
|
||||
}),
|
||||
`Expected an eligible deployed Lv30 bond to retain a battle-scoped core resonance selection: ${JSON.stringify(normalizedCoreResonance.sortieResonanceSelection)}`
|
||||
);
|
||||
const detachedCoreResonance = getCampaignState();
|
||||
detachedCoreResonance.sortieResonanceSelection.bondId = 'mutated';
|
||||
assert(
|
||||
getCampaignState().sortieResonanceSelection?.bondId === 'liu-bei__guan-yu',
|
||||
'Expected core resonance selections returned from campaign state to be deep clones.'
|
||||
);
|
||||
const invalidCoreResonanceSet = setCampaignSortieResonanceSelection('first-battle-zhuo-commandery', 'liu-bei__zhang-fei');
|
||||
const unknownCoreResonanceBattle = setCampaignSortieResonanceSelection('unknown-battle', 'liu-bei__guan-yu');
|
||||
const prototypeKeyCoreResonanceBattle = setCampaignSortieResonanceSelection('constructor', 'liu-bei__guan-yu');
|
||||
assert(
|
||||
invalidCoreResonanceSet.sortieResonanceSelection?.bondId === 'liu-bei__guan-yu' &&
|
||||
unknownCoreResonanceBattle.sortieResonanceSelection?.bondId === 'liu-bei__guan-yu' &&
|
||||
prototypeKeyCoreResonanceBattle.sortieResonanceSelection?.bondId === 'liu-bei__guan-yu',
|
||||
`Expected ineligible bonds and unknown/prototype-key battles not to disturb the current core resonance: ${JSON.stringify({ invalidCoreResonanceSet, unknownCoreResonanceBattle, prototypeKeyCoreResonanceBattle })}`
|
||||
);
|
||||
const clearedOtherBattleCoreResonance = setCampaignSortieResonanceSelection('second-battle-yellow-turban-pursuit');
|
||||
assert(
|
||||
clearedOtherBattleCoreResonance.sortieResonanceSelection?.bondId === 'liu-bei__guan-yu',
|
||||
'Expected clearing another battle not to remove the current battle-scoped core resonance.'
|
||||
);
|
||||
const clearedCoreResonance = setCampaignSortieResonanceSelection('first-battle-zhuo-commandery');
|
||||
assert(clearedCoreResonance.sortieResonanceSelection === undefined, 'Expected the matching core resonance selection to clear explicitly.');
|
||||
const selectedCoreResonance = setCampaignSortieResonanceSelection('second-battle-yellow-turban-pursuit', 'liu-bei__guan-yu');
|
||||
const reloadedCoreResonance = loadCampaignState();
|
||||
assert(
|
||||
selectedCoreResonance.sortieResonanceSelection?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
reloadedCoreResonance.sortieResonanceSelection?.bondId === 'liu-bei__guan-yu',
|
||||
`Expected the setter to persist a valid core resonance selection: ${JSON.stringify(reloadedCoreResonance.sortieResonanceSelection)}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify(coreResonanceFixture));
|
||||
loadCampaignState();
|
||||
const memberRemovedCoreResonance = getCampaignState();
|
||||
memberRemovedCoreResonance.selectedSortieUnitIds = ['liu-bei'];
|
||||
const normalizedMemberRemovedCoreResonance = saveCampaignState(memberRemovedCoreResonance);
|
||||
assert(
|
||||
normalizedMemberRemovedCoreResonance.sortieResonanceSelection === undefined,
|
||||
`Expected removing either core member from the sortie to clear the selection: ${JSON.stringify(normalizedMemberRemovedCoreResonance.sortieResonanceSelection)}`
|
||||
);
|
||||
|
||||
const invalidCoreResonanceFixtures = [
|
||||
{
|
||||
label: 'unknown battle',
|
||||
value: { ...coreResonanceFixture, sortieResonanceSelection: { battleId: 'unknown-battle', bondId: 'liu-bei__guan-yu' } }
|
||||
},
|
||||
{
|
||||
label: 'prototype-key battle',
|
||||
value: { ...coreResonanceFixture, sortieResonanceSelection: { battleId: 'constructor', bondId: 'liu-bei__guan-yu' } }
|
||||
},
|
||||
{
|
||||
label: 'unknown bond',
|
||||
value: { ...coreResonanceFixture, sortieResonanceSelection: { battleId: 'first-battle-zhuo-commandery', bondId: 'ghost-bond' } }
|
||||
},
|
||||
{
|
||||
label: 'below Lv30',
|
||||
value: {
|
||||
...coreResonanceFixture,
|
||||
selectedSortieUnitIds: ['liu-bei', 'zhang-fei'],
|
||||
sortieResonanceSelection: { battleId: 'first-battle-zhuo-commandery', bondId: 'liu-bei__zhang-fei' }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'undeployed member',
|
||||
value: { ...coreResonanceFixture, selectedSortieUnitIds: ['liu-bei'] }
|
||||
}
|
||||
];
|
||||
for (const fixture of invalidCoreResonanceFixtures) {
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify(fixture.value));
|
||||
const normalizedInvalidCoreResonance = loadCampaignState();
|
||||
assert(
|
||||
normalizedInvalidCoreResonance.sortieResonanceSelection === undefined,
|
||||
`Expected ${fixture.label} core resonance selection to normalize away: ${JSON.stringify(normalizedInvalidCoreResonance.sortieResonanceSelection)}`
|
||||
);
|
||||
}
|
||||
|
||||
const firstBattleBondIds = new Set(battleScenarios['first-battle-zhuo-commandery'].bonds.map((bond) => bond.id));
|
||||
const crossBattleResonance = Object.entries(battleScenarios)
|
||||
.filter(([battleId]) => battleId !== 'first-battle-zhuo-commandery')
|
||||
.flatMap(([battleId, scenario]) => scenario.bonds.map((bond) => ({ battleId, bond })))
|
||||
.find(({ bond }) => bond.level >= 30 && !firstBattleBondIds.has(bond.id));
|
||||
assert(crossBattleResonance, 'Expected campaign scenarios to expose a later-battle-only eligible bond fixture.');
|
||||
const crossBattleResonanceFixture = {
|
||||
...coreResonanceFixture,
|
||||
roster: crossBattleResonance.bond.unitIds.map((unitId) => ({ id: unitId, name: unitId, faction: 'ally' })),
|
||||
bonds: [{ ...crossBattleResonance.bond, battleExp: 0 }],
|
||||
selectedSortieUnitIds: [...crossBattleResonance.bond.unitIds],
|
||||
sortieResonanceSelection: {
|
||||
battleId: 'first-battle-zhuo-commandery',
|
||||
bondId: crossBattleResonance.bond.id
|
||||
}
|
||||
};
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify(crossBattleResonanceFixture));
|
||||
const normalizedCrossBattleResonance = loadCampaignState();
|
||||
const rejectedCrossBattleResonance = setCampaignSortieResonanceSelection(
|
||||
'first-battle-zhuo-commandery',
|
||||
crossBattleResonance.bond.id
|
||||
);
|
||||
const acceptedOwningBattleResonance = setCampaignSortieResonanceSelection(
|
||||
crossBattleResonance.battleId,
|
||||
crossBattleResonance.bond.id
|
||||
);
|
||||
assert(
|
||||
normalizedCrossBattleResonance.sortieResonanceSelection === undefined &&
|
||||
rejectedCrossBattleResonance.sortieResonanceSelection === undefined &&
|
||||
acceptedOwningBattleResonance.sortieResonanceSelection?.battleId === crossBattleResonance.battleId &&
|
||||
loadCampaignState().sortieResonanceSelection?.bondId === crossBattleResonance.bond.id,
|
||||
`Expected cross-battle bonds to be rejected while the same bond remains valid for its owning battle: ${JSON.stringify({ crossBattleResonance, normalizedCrossBattleResonance, rejectedCrossBattleResonance, acceptedOwningBattleResonance })}`
|
||||
);
|
||||
|
||||
const scenarioOnlyResonanceFixture = {
|
||||
...crossBattleResonanceFixture,
|
||||
bonds: [],
|
||||
sortieResonanceSelection: {
|
||||
battleId: crossBattleResonance.battleId,
|
||||
bondId: crossBattleResonance.bond.id
|
||||
}
|
||||
};
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify(scenarioOnlyResonanceFixture));
|
||||
const normalizedScenarioOnlyResonance = loadCampaignState();
|
||||
setCampaignSortieResonanceSelection(crossBattleResonance.battleId);
|
||||
const selectedScenarioOnlyResonance = setCampaignSortieResonanceSelection(
|
||||
crossBattleResonance.battleId,
|
||||
crossBattleResonance.bond.id
|
||||
);
|
||||
const reloadedScenarioOnlyResonance = loadCampaignState();
|
||||
assert(
|
||||
normalizedScenarioOnlyResonance.bonds.length === 0 &&
|
||||
normalizedScenarioOnlyResonance.sortieResonanceSelection?.bondId === crossBattleResonance.bond.id &&
|
||||
selectedScenarioOnlyResonance.sortieResonanceSelection?.battleId === crossBattleResonance.battleId &&
|
||||
reloadedScenarioOnlyResonance.bonds.length === 0 &&
|
||||
reloadedScenarioOnlyResonance.sortieResonanceSelection?.bondId === crossBattleResonance.bond.id,
|
||||
`Expected a scenario-owned eligible bond to remain selectable and reload safely before campaign progress contains it: ${JSON.stringify({ normalizedScenarioOnlyResonance, selectedScenarioOnlyResonance, reloadedScenarioOnlyResonance })}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(
|
||||
campaignStorageKey,
|
||||
@@ -1251,6 +1420,7 @@ try {
|
||||
typeof legacy.sortieFormationPresets === 'object' &&
|
||||
Object.keys(legacy.sortieFormationPresets).length === 0 &&
|
||||
legacy.sortieOrderSelection === undefined &&
|
||||
legacy.sortieResonanceSelection === undefined &&
|
||||
typeof legacy.sortieOrderHistory === 'object' &&
|
||||
Object.keys(legacy.sortieOrderHistory).length === 0 &&
|
||||
Array.isArray(legacy.claimedSortieOrderRewardIds) &&
|
||||
|
||||
Reference in New Issue
Block a user