feat: carry first-battle camaraderie into sortie prep
This commit is contained in:
@@ -2109,6 +2109,191 @@ try {
|
||||
`Expected selecting 재정비 to consume the order command and resolve deterministic healing immediately: ${JSON.stringify(firstBattleCommand)}`
|
||||
);
|
||||
|
||||
const firstBattleCamaraderieAttempt = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const attacker = scene?.debugUnitById?.('liu-bei');
|
||||
const partner = scene?.debugUnitById?.('zhang-fei');
|
||||
const targetState = window.__HEROS_DEBUG__?.battle()?.units?.find(
|
||||
(unit) => unit.faction === 'enemy' && unit.hp > 0
|
||||
);
|
||||
const target = targetState ? scene?.debugUnitById?.(targetState.id) : undefined;
|
||||
if (!scene || !attacker || !partner || !target) {
|
||||
return {
|
||||
ready: false,
|
||||
reason: 'first-battle Liu Bei/Zhang Fei cooperation fixture units were unavailable',
|
||||
targetId: targetState?.id ?? null
|
||||
};
|
||||
}
|
||||
|
||||
const clone = (value) => value === undefined ? undefined : structuredClone(value);
|
||||
const touchedUnits = [attacker, partner, target];
|
||||
const original = {
|
||||
units: touchedUnits.map((unit) => ({
|
||||
id: unit.id,
|
||||
x: unit.x,
|
||||
y: unit.y,
|
||||
hp: unit.hp,
|
||||
maxHp: unit.maxHp,
|
||||
direction: scene.unitViews.get(unit.id)?.direction ?? 'south'
|
||||
})),
|
||||
attackIntents: scene.attackIntents.map((intent) => ({ ...intent })),
|
||||
actedUnitIds: [...scene.actedUnitIds].sort(),
|
||||
battleStats: clone(scene.serializeBattleStats())
|
||||
};
|
||||
let preview;
|
||||
let resolution;
|
||||
try {
|
||||
Object.assign(attacker, { x: 5, y: 5 });
|
||||
Object.assign(partner, { x: 4, y: 5 });
|
||||
Object.assign(target, {
|
||||
x: 6,
|
||||
y: 5,
|
||||
maxHp: Math.max(target.maxHp, 5000),
|
||||
hp: Math.max(target.maxHp, 5000)
|
||||
});
|
||||
scene.attackIntents = [{ attackerId: partner.id, targetId: target.id }];
|
||||
scene.actedUnitIds = new Set([...scene.actedUnitIds, partner.id]);
|
||||
touchedUnits.forEach((unit) => scene.positionUnitView(unit));
|
||||
|
||||
const livePreview = scene.combatPreview(attacker, target, 'attack');
|
||||
const liveResolution = scene.resolveBondChainFollowUp(livePreview, true, true);
|
||||
preview = {
|
||||
bondId: livePreview.bondChainBondId ?? null,
|
||||
partnerId: livePreview.bondChainPartnerId ?? null,
|
||||
rate: livePreview.bondChainRate,
|
||||
damage: livePreview.bondChainDamage,
|
||||
coreResonance: livePreview.bondChainCoreResonance
|
||||
};
|
||||
resolution = liveResolution
|
||||
? {
|
||||
bondId: liveResolution.attempt.bondId,
|
||||
partnerId: liveResolution.attempt.partnerId,
|
||||
rate: liveResolution.attempt.rate,
|
||||
succeeded: liveResolution.attempt.succeeded,
|
||||
coreResonance: liveResolution.attempt.coreResonance,
|
||||
damage: liveResolution.result?.damage ?? 0
|
||||
}
|
||||
: null;
|
||||
} finally {
|
||||
original.units.forEach((snapshot) => {
|
||||
const unit = scene.debugUnitById(snapshot.id);
|
||||
if (!unit) {
|
||||
return;
|
||||
}
|
||||
Object.assign(unit, {
|
||||
x: snapshot.x,
|
||||
y: snapshot.y,
|
||||
hp: snapshot.hp,
|
||||
maxHp: snapshot.maxHp
|
||||
});
|
||||
scene.positionUnitView(unit);
|
||||
scene.syncUnitMotion(unit, scene.unitViews.get(unit.id), snapshot.direction);
|
||||
});
|
||||
scene.battleStats = new Map(
|
||||
Object.entries(original.battleStats).map(([unitId, stats]) => [unitId, clone(stats)])
|
||||
);
|
||||
scene.attackIntents = original.attackIntents.map((intent) => ({ ...intent }));
|
||||
scene.actedUnitIds = new Set(original.actedUnitIds);
|
||||
scene.refreshUnitLegibilityStyles();
|
||||
}
|
||||
|
||||
const restored = {
|
||||
units: original.units.map(({ id }) => {
|
||||
const unit = scene.debugUnitById(id);
|
||||
return unit
|
||||
? {
|
||||
id: unit.id,
|
||||
x: unit.x,
|
||||
y: unit.y,
|
||||
hp: unit.hp,
|
||||
maxHp: unit.maxHp,
|
||||
direction: scene.unitViews.get(unit.id)?.direction ?? null
|
||||
}
|
||||
: null;
|
||||
}),
|
||||
attackIntents: scene.attackIntents.map((intent) => ({ ...intent })),
|
||||
actedUnitIds: [...scene.actedUnitIds].sort(),
|
||||
battleStats: clone(scene.serializeBattleStats())
|
||||
};
|
||||
const tracked = clone(
|
||||
window.__HEROS_DEBUG__?.battle()?.coreSortieResonance?.cooperation ?? null
|
||||
);
|
||||
scene.saveBattleState?.(1);
|
||||
const battleKey = 'heros-web:battle:first-battle-zhuo-commandery';
|
||||
const read = (key) => {
|
||||
const raw = window.localStorage.getItem(key);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
};
|
||||
return {
|
||||
ready: true,
|
||||
preview,
|
||||
resolution,
|
||||
tracked,
|
||||
restored,
|
||||
expectedRestored: original,
|
||||
battleSave: {
|
||||
current: read(battleKey),
|
||||
slot1: read(`${battleKey}:slot-1`),
|
||||
normalizedSlot1: scene.readBattleSaveState?.(1) ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
const expectedFirstBattleCooperation = {
|
||||
version: 1,
|
||||
bondId: 'liu-bei__zhang-fei',
|
||||
unitIds: ['liu-bei', 'zhang-fei'],
|
||||
attempts: 1,
|
||||
successes: 1,
|
||||
additionalDamage: firstBattleCamaraderieAttempt.resolution?.damage
|
||||
};
|
||||
const expectedFirstBattleCooperationStats = {
|
||||
attempts: 1,
|
||||
successes: 1,
|
||||
additionalDamage: firstBattleCamaraderieAttempt.resolution?.damage
|
||||
};
|
||||
assert(
|
||||
firstBattleCamaraderieAttempt.ready === true &&
|
||||
firstBattleCamaraderieAttempt.preview?.bondId === 'liu-bei__zhang-fei' &&
|
||||
firstBattleCamaraderieAttempt.preview.partnerId === 'zhang-fei' &&
|
||||
firstBattleCamaraderieAttempt.preview.rate === 8 &&
|
||||
firstBattleCamaraderieAttempt.preview.damage > 0 &&
|
||||
firstBattleCamaraderieAttempt.preview.coreResonance === false &&
|
||||
firstBattleCamaraderieAttempt.resolution?.bondId === 'liu-bei__zhang-fei' &&
|
||||
firstBattleCamaraderieAttempt.resolution.partnerId === 'zhang-fei' &&
|
||||
firstBattleCamaraderieAttempt.resolution.rate === 8 &&
|
||||
firstBattleCamaraderieAttempt.resolution.succeeded === true &&
|
||||
firstBattleCamaraderieAttempt.resolution.coreResonance === false &&
|
||||
firstBattleCamaraderieAttempt.resolution.damage > 0,
|
||||
`Expected one retained, actual ordinary Liu Bei/Zhang Fei pursuit success before first victory: ${JSON.stringify(firstBattleCamaraderieAttempt)}`
|
||||
);
|
||||
assert(
|
||||
sameJsonValue(
|
||||
firstBattleCamaraderieAttempt.tracked?.statsByBond?.['liu-bei__zhang-fei'],
|
||||
expectedFirstBattleCooperationStats
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstBattleCamaraderieAttempt.tracked?.dominantSnapshot,
|
||||
expectedFirstBattleCooperation
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstBattleCamaraderieAttempt.battleSave.current?.cooperationStatsByBond?.['liu-bei__zhang-fei'],
|
||||
expectedFirstBattleCooperationStats
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstBattleCamaraderieAttempt.battleSave.slot1?.cooperationStatsByBond?.['liu-bei__zhang-fei'],
|
||||
expectedFirstBattleCooperationStats
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstBattleCamaraderieAttempt.battleSave.normalizedSlot1?.cooperationStatsByBond?.['liu-bei__zhang-fei'],
|
||||
expectedFirstBattleCooperationStats
|
||||
),
|
||||
`Expected retained cooperation statistics to synchronize through debug, raw current/slot saves, and the normalized runtime slot reader: ${JSON.stringify(firstBattleCamaraderieAttempt)}`
|
||||
);
|
||||
assert(
|
||||
sameJsonValue(firstBattleCamaraderieAttempt.restored, firstBattleCamaraderieAttempt.expectedRestored),
|
||||
`Expected the retained cooperation fixture to restore positions, HP, battle stats, intents, and acted state: ${JSON.stringify(firstBattleCamaraderieAttempt)}`
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||
await waitForBattleOutcome(page, 'victory');
|
||||
const initialFirstSettlement = await page.evaluate(() => ({
|
||||
@@ -2203,6 +2388,31 @@ try {
|
||||
firstResultSave.slot1?.pendingAftermathBattleId === 'first-battle-zhuo-commandery',
|
||||
`Expected the victory result to persist its unseen aftermath in current and slot saves: ${JSON.stringify(firstResultSave)}`
|
||||
);
|
||||
assert(
|
||||
sameJsonValue(
|
||||
firstResultSave.current?.firstBattleReport?.sortieCooperation,
|
||||
expectedFirstBattleCooperation
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstResultSave.slot1?.firstBattleReport?.sortieCooperation,
|
||||
expectedFirstBattleCooperation
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstResultSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieCooperation,
|
||||
expectedFirstBattleCooperation
|
||||
) &&
|
||||
sameJsonValue(
|
||||
firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieCooperation,
|
||||
expectedFirstBattleCooperation
|
||||
),
|
||||
`Expected the exact first-battle cooperation memory to persist across current/slot reports and battle history: ${JSON.stringify({
|
||||
expected: expectedFirstBattleCooperation,
|
||||
currentReport: firstResultSave.current?.firstBattleReport?.sortieCooperation,
|
||||
slotReport: firstResultSave.slot1?.firstBattleReport?.sortieCooperation,
|
||||
currentHistory: firstResultSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieCooperation,
|
||||
slotHistory: firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieCooperation
|
||||
})}`
|
||||
);
|
||||
const firstSortieOrder = firstResultState?.sortieOperationOrder;
|
||||
const firstSortieOrderResult = firstSortieOrder?.result;
|
||||
const firstSortieOrderCommand = firstSortieOrderResult?.command;
|
||||
@@ -2703,6 +2913,8 @@ try {
|
||||
});
|
||||
const firstVictoryFollowup = firstVictoryDialogueProbe.state?.firstVictoryFollowup;
|
||||
const firstVictorySelectedDialogue = firstVictoryDialogueProbe.state?.selectedDialogue;
|
||||
const pendingFirstBattleCamaraderieMemory =
|
||||
firstVictoryDialogueProbe.state?.firstBattleCamaraderieMemory;
|
||||
assert(
|
||||
firstVictoryDialogueProbe.state?.activeTab === 'dialogue' &&
|
||||
firstVictoryDialogueProbe.state?.campTabs?.find((tab) => tab.id === 'dialogue')?.newBadgeVisible === true &&
|
||||
@@ -2735,6 +2947,37 @@ try {
|
||||
firstVictorySelectedDialogue.choices.every((choice) => boundsWithinFhdViewport(choice.bounds)),
|
||||
`Expected the first-camp dialogue tab to render one marked Liu Bei/Guan Yu follow-up from the persisted elite victory: ${JSON.stringify(firstVictoryDialogueProbe)}`
|
||||
);
|
||||
assert(
|
||||
pendingFirstBattleCamaraderieMemory?.available === true &&
|
||||
pendingFirstBattleCamaraderieMemory.sourceBattleId === 'first-battle-zhuo-commandery' &&
|
||||
pendingFirstBattleCamaraderieMemory.targetBattleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
pendingFirstBattleCamaraderieMemory.bondId === 'liu-bei__zhang-fei' &&
|
||||
sameJsonValue(pendingFirstBattleCamaraderieMemory.unitIds, ['liu-bei', 'zhang-fei']) &&
|
||||
sameJsonValue(pendingFirstBattleCamaraderieMemory.unitNames, ['유비', '장비']) &&
|
||||
pendingFirstBattleCamaraderieMemory.attempts === 1 &&
|
||||
pendingFirstBattleCamaraderieMemory.successes === 1 &&
|
||||
pendingFirstBattleCamaraderieMemory.additionalDamage ===
|
||||
expectedFirstBattleCooperation.additionalDamage &&
|
||||
pendingFirstBattleCamaraderieMemory.rememberedOutcome === 'success' &&
|
||||
pendingFirstBattleCamaraderieMemory.completed === false &&
|
||||
pendingFirstBattleCamaraderieMemory.dialogue?.id === 'first-battle-camaraderie-memory' &&
|
||||
pendingFirstBattleCamaraderieMemory.dialogue.selected === false &&
|
||||
pendingFirstBattleCamaraderieMemory.dialogue.completed === false &&
|
||||
pendingFirstBattleCamaraderieMemory.dialogue.choiceId === null &&
|
||||
isFiniteBounds(pendingFirstBattleCamaraderieMemory.dialogue.rowBounds) &&
|
||||
pendingFirstBattleCamaraderieMemory.perk?.requiresManualSelection === true &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.autoSelected === false &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.exactPairOnly === true &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.unlocked === false &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.expectedBonusRate === 5 &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.selected === false &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.active === false &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.baseChainRate === 18 &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.bonusChainRate === 0 &&
|
||||
pendingFirstBattleCamaraderieMemory.perk.effectiveChainRate === 18 &&
|
||||
firstVictoryDialogueProbe.state?.campaign?.sortieResonanceSelection === null,
|
||||
`Expected the actual Liu Bei/Zhang Fei success to create one incomplete memory dialogue without auto-selecting a core pair: ${JSON.stringify(pendingFirstBattleCamaraderieMemory)}`
|
||||
);
|
||||
await page.screenshot({
|
||||
path: `${screenshotDir}/rc-first-camp-victory-followup-dialogue.png`,
|
||||
fullPage: true
|
||||
@@ -2788,17 +3031,181 @@ try {
|
||||
] === firstVictoryChoice.id &&
|
||||
completedFirstVictoryDialogueProbe.state?.campTabs?.find(
|
||||
(tab) => tab.id === 'dialogue'
|
||||
)?.newBadgeVisible === false &&
|
||||
)?.newBadgeVisible === true &&
|
||||
completedFirstVictoryDialogueProbe.remainingRowMarkerCount === 0 &&
|
||||
firstVictoryDialoguePersisted(completedFirstVictoryDialogueSave.current) &&
|
||||
firstVictoryDialoguePersisted(completedFirstVictoryDialogueSave.slot1),
|
||||
`Expected the actual first-victory choice to persist in current and slot saves and clear both follow-up markers: ${JSON.stringify({
|
||||
`Expected the first-victory choice to persist while the separate camaraderie memory keeps the dialogue NEW state pending: ${JSON.stringify({
|
||||
probe: completedFirstVictoryDialogueProbe,
|
||||
save: completedFirstVictoryDialogueSave,
|
||||
choiceId: firstVictoryChoice.id
|
||||
})}`
|
||||
);
|
||||
|
||||
const firstBattleCamaraderieDialogueRow = await readCampDialogueControlPoint(
|
||||
page,
|
||||
'row',
|
||||
'first-battle-camaraderie-memory'
|
||||
);
|
||||
await page.mouse.click(
|
||||
firstBattleCamaraderieDialogueRow.x,
|
||||
firstBattleCamaraderieDialogueRow.y
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => (
|
||||
window.__HEROS_DEBUG__?.camp()?.selectedDialogue?.id ===
|
||||
'first-battle-camaraderie-memory'
|
||||
),
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const firstBattleCamaraderieDialogueProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const memory = state?.firstBattleCamaraderieMemory;
|
||||
const renderedDialogueText = memory?.dialogue?.lines?.join('\n') ?? null;
|
||||
const activeTexts = (scene?.contentObjects ?? []).filter(
|
||||
(object) => object?.type === 'Text' && object.active && object.visible
|
||||
);
|
||||
const boundsFor = (object) => {
|
||||
const bounds = object?.getBounds?.();
|
||||
return bounds
|
||||
? { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
|
||||
: null;
|
||||
};
|
||||
return {
|
||||
state,
|
||||
noticeObjectCount: (scene?.dialogueObjects ?? []).filter(
|
||||
(object) => object?.active
|
||||
).length,
|
||||
memoryBadgeCount: activeTexts.filter((object) => object.text === '전우 기억').length,
|
||||
renderedDialogueBlocks: renderedDialogueText
|
||||
? activeTexts
|
||||
.filter((object) => object.text === renderedDialogueText)
|
||||
.map((object) => ({ text: object.text, bounds: boundsFor(object) }))
|
||||
: []
|
||||
};
|
||||
});
|
||||
const selectedFirstBattleCamaraderieMemory =
|
||||
firstBattleCamaraderieDialogueProbe.state?.firstBattleCamaraderieMemory;
|
||||
const selectedFirstBattleCamaraderieDialogue =
|
||||
firstBattleCamaraderieDialogueProbe.state?.selectedDialogue;
|
||||
assert(
|
||||
selectedFirstBattleCamaraderieMemory?.available === true &&
|
||||
selectedFirstBattleCamaraderieMemory.completed === false &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue?.selected === true &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.completed === false &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.title.includes('유비') &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.title.includes('장비') &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.lines?.length === 3 &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.lines.some((line) => line.includes('지난 싸움')) &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.lines.some((line) => line.includes('핵심 공명조')) &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices?.length === 2 &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices[0]?.id ===
|
||||
'trust-remembered-rhythm' &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices[1]?.id ===
|
||||
'review-successful-signals' &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices.every(
|
||||
(choice) => isFiniteBounds(choice.bounds) && boundsWithinFhdViewport(choice.bounds)
|
||||
) &&
|
||||
isFiniteBounds(selectedFirstBattleCamaraderieMemory.dialogue.rowBounds) &&
|
||||
isFiniteBounds(selectedFirstBattleCamaraderieMemory.dialogue.bodyBounds) &&
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.bodyBounds.y +
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.bodyBounds.height <=
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices[0]?.bounds?.y - 8 &&
|
||||
selectedFirstBattleCamaraderieDialogue?.id ===
|
||||
'first-battle-camaraderie-memory' &&
|
||||
selectedFirstBattleCamaraderieDialogue.camaraderieMemory === true &&
|
||||
selectedFirstBattleCamaraderieDialogue.completed === false &&
|
||||
sameJsonValue(
|
||||
selectedFirstBattleCamaraderieDialogue.lines,
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.lines
|
||||
) &&
|
||||
firstBattleCamaraderieDialogueProbe.memoryBadgeCount === 1 &&
|
||||
firstBattleCamaraderieDialogueProbe.renderedDialogueBlocks.length === 1 &&
|
||||
selectedFirstBattleCamaraderieMemory.perk?.unlocked === false &&
|
||||
selectedFirstBattleCamaraderieMemory.perk.selected === false &&
|
||||
selectedFirstBattleCamaraderieMemory.perk.active === false &&
|
||||
firstBattleCamaraderieDialogueProbe.noticeObjectCount === 0 &&
|
||||
firstBattleCamaraderieDialogueProbe.state?.campaign?.sortieResonanceSelection === null,
|
||||
`Expected the dynamic first-battle camaraderie row, body, choices, and bounds to render without selecting the next core pair: ${JSON.stringify(firstBattleCamaraderieDialogueProbe)}`
|
||||
);
|
||||
await page.screenshot({
|
||||
path: `${screenshotDir}/rc-first-camp-camaraderie-memory-dialogue.png`,
|
||||
fullPage: true
|
||||
});
|
||||
await assertCanvasPainted(page, 'first camp camaraderie memory dialogue');
|
||||
|
||||
const firstBattleCamaraderieChoice =
|
||||
selectedFirstBattleCamaraderieMemory.dialogue.choices[0];
|
||||
const firstBattleCamaraderieChoicePoint = await readCampDialogueControlPoint(
|
||||
page,
|
||||
'choice',
|
||||
'first-battle-camaraderie-memory',
|
||||
firstBattleCamaraderieChoice.id
|
||||
);
|
||||
await page.mouse.click(
|
||||
firstBattleCamaraderieChoicePoint.x,
|
||||
firstBattleCamaraderieChoicePoint.y
|
||||
);
|
||||
await page.waitForFunction(
|
||||
({ dialogueId, choiceId }) => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp();
|
||||
return (
|
||||
camp?.firstBattleCamaraderieMemory?.completed === true &&
|
||||
camp?.firstBattleCamaraderieMemory?.dialogue?.completed === true &&
|
||||
camp?.firstBattleCamaraderieMemory?.dialogue?.choiceId === choiceId &&
|
||||
camp?.campaign?.campDialogueChoiceIds?.[dialogueId] === choiceId
|
||||
);
|
||||
},
|
||||
{
|
||||
dialogueId: 'first-battle-camaraderie-memory',
|
||||
choiceId: firstBattleCamaraderieChoice.id
|
||||
},
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const completedFirstBattleCamaraderieState =
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const completedFirstBattleCamaraderieMemory =
|
||||
completedFirstBattleCamaraderieState?.firstBattleCamaraderieMemory;
|
||||
const completedFirstBattleCamaraderieSave = await readCampaignSave(page);
|
||||
const firstBattleCamaraderiePersisted = (save) => (
|
||||
save?.completedCampDialogues?.includes('first-battle-camaraderie-memory') &&
|
||||
save?.firstBattleReport?.completedCampDialogues?.includes(
|
||||
'first-battle-camaraderie-memory'
|
||||
) &&
|
||||
save?.campDialogueChoiceIds?.['first-battle-camaraderie-memory'] ===
|
||||
firstBattleCamaraderieChoice.id &&
|
||||
sameJsonValue(
|
||||
save?.battleHistory?.['first-battle-zhuo-commandery']?.sortieCooperation,
|
||||
expectedFirstBattleCooperation
|
||||
)
|
||||
);
|
||||
assert(
|
||||
completedFirstBattleCamaraderieMemory?.completed === true &&
|
||||
completedFirstBattleCamaraderieMemory.dialogue?.completed === true &&
|
||||
completedFirstBattleCamaraderieMemory.dialogue.choiceId ===
|
||||
firstBattleCamaraderieChoice.id &&
|
||||
completedFirstBattleCamaraderieMemory.perk?.unlocked === true &&
|
||||
completedFirstBattleCamaraderieMemory.perk.requiresManualSelection === true &&
|
||||
completedFirstBattleCamaraderieMemory.perk.autoSelected === false &&
|
||||
completedFirstBattleCamaraderieMemory.perk.selected === false &&
|
||||
completedFirstBattleCamaraderieMemory.perk.active === false &&
|
||||
completedFirstBattleCamaraderieMemory.perk.baseChainRate === 18 &&
|
||||
completedFirstBattleCamaraderieMemory.perk.bonusChainRate === 5 &&
|
||||
completedFirstBattleCamaraderieMemory.perk.effectiveChainRate === 23 &&
|
||||
completedFirstBattleCamaraderieState?.campaign?.sortieResonanceSelection === null &&
|
||||
completedFirstBattleCamaraderieState?.campTabs?.find(
|
||||
(tab) => tab.id === 'dialogue'
|
||||
)?.newBadgeVisible === false &&
|
||||
firstBattleCamaraderiePersisted(completedFirstBattleCamaraderieSave.current) &&
|
||||
firstBattleCamaraderiePersisted(completedFirstBattleCamaraderieSave.slot1),
|
||||
`Expected the chosen camaraderie memory to persist in current/slot saves, unlock +5%p, and still avoid auto-selection: ${JSON.stringify({
|
||||
state: completedFirstBattleCamaraderieState,
|
||||
save: completedFirstBattleCamaraderieSave
|
||||
})}`
|
||||
);
|
||||
|
||||
const firstCampEvaluation = firstCampProbe.state?.reportFormationEvaluation;
|
||||
const firstCampHistory = firstCampProbe.state?.reportFormationHistory;
|
||||
const firstCampEvaluationSaveBefore = await readCampaignSave(page);
|
||||
@@ -3737,20 +4144,54 @@ try {
|
||||
id: candidate.id,
|
||||
level: candidate.level,
|
||||
baseChainRate: candidate.baseChainRate,
|
||||
baseCoreChainRate: candidate.baseCoreChainRate,
|
||||
bonusChainRate: candidate.bonusChainRate,
|
||||
effectiveCoreChainRate: candidate.effectiveCoreChainRate,
|
||||
camaraderieRemembered: candidate.camaraderieRemembered,
|
||||
coreChainRate: candidate.coreChainRate,
|
||||
selected: candidate.selected
|
||||
})),
|
||||
[
|
||||
{ id: 'liu-bei__guan-yu', level: 72, baseChainRate: 18, coreChainRate: 28, selected: false },
|
||||
{ id: 'liu-bei__zhang-fei', level: 68, baseChainRate: 8, coreChainRate: 18, selected: false },
|
||||
{ id: 'guan-yu__zhang-fei', level: 64, baseChainRate: 8, coreChainRate: 18, selected: false }
|
||||
{
|
||||
id: 'liu-bei__zhang-fei',
|
||||
level: 68,
|
||||
baseChainRate: 8,
|
||||
baseCoreChainRate: 18,
|
||||
bonusChainRate: 5,
|
||||
effectiveCoreChainRate: 23,
|
||||
camaraderieRemembered: true,
|
||||
coreChainRate: 23,
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
level: 72,
|
||||
baseChainRate: 18,
|
||||
baseCoreChainRate: 28,
|
||||
bonusChainRate: 0,
|
||||
effectiveCoreChainRate: 28,
|
||||
camaraderieRemembered: false,
|
||||
coreChainRate: 28,
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'guan-yu__zhang-fei',
|
||||
level: 64,
|
||||
baseChainRate: 8,
|
||||
baseCoreChainRate: 18,
|
||||
bonusChainRate: 0,
|
||||
effectiveCoreChainRate: 18,
|
||||
camaraderieRemembered: false,
|
||||
coreChainRate: 18,
|
||||
selected: false
|
||||
}
|
||||
]
|
||||
),
|
||||
`Expected the selected founding trio to expose ordered 18/8/8 resonance-pursuit pairs without reserve opportunities: ${JSON.stringify(firstSortiePursuit)}`
|
||||
`Expected the founding trio to retain ordinary pursuits while the remembered Liu Bei/Zhang Fei core candidate previews 18% + 5%p = 23%: ${JSON.stringify(firstSortiePursuit)}`
|
||||
);
|
||||
assert(
|
||||
firstSortiePursuit?.summaryText === '핵심 공명조 · 미지정 · 후보 3조' &&
|
||||
firstSortiePursuit?.ruleText === 'Lv30+ 한 조 · 핵심 추격 8/18/28% · 재클릭 해제' &&
|
||||
firstSortiePursuit?.ruleText === '전우 기억한 조 직접 지정 · 핵심 추격 +5%p · 재클릭 해제' &&
|
||||
isFiniteBounds(firstSortiePursuit.panelBounds) &&
|
||||
isFiniteBounds(firstSortiePursuit.ruleBounds) &&
|
||||
firstSortiePursuit.page === 0 &&
|
||||
@@ -3766,11 +4207,54 @@ try {
|
||||
boundsInside(firstSortiePursuit.nextBounds, firstSortiePursuit.panelBounds) &&
|
||||
boundsInside(firstSortiePursuit.panelBounds, logicalViewportBounds) &&
|
||||
boundsInside(firstSortiePursuit.ruleBounds, firstSortiePursuit.panelBounds) &&
|
||||
sameJsonValue(firstSortiePursuit.rows?.map((row) => ({ bondId: row.bondId, state: row.state, selected: row.selected, chainRate: row.chainRate, text: row.text })), [
|
||||
{ bondId: 'liu-bei__guan-yu', state: 'candidate', selected: false, chainRate: 28, text: '지정 28% 유비↔관우' },
|
||||
{ bondId: 'liu-bei__zhang-fei', state: 'candidate', selected: false, chainRate: 18, text: '지정 18% 유비↔장비' },
|
||||
{ bondId: 'guan-yu__zhang-fei', state: 'candidate', selected: false, chainRate: 18, text: '지정 18% 관우↔장비' }
|
||||
]) &&
|
||||
sameJsonValue(
|
||||
firstSortiePursuit.rows?.map((row) => ({
|
||||
bondId: row.bondId,
|
||||
state: row.state,
|
||||
selected: row.selected,
|
||||
baseChainRate: row.baseChainRate,
|
||||
bonusChainRate: row.bonusChainRate,
|
||||
effectiveChainRate: row.effectiveChainRate,
|
||||
camaraderieRemembered: row.camaraderieRemembered,
|
||||
chainRate: row.chainRate,
|
||||
text: row.text
|
||||
})),
|
||||
[
|
||||
{
|
||||
bondId: 'liu-bei__zhang-fei',
|
||||
state: 'candidate',
|
||||
selected: false,
|
||||
baseChainRate: 18,
|
||||
bonusChainRate: 5,
|
||||
effectiveChainRate: 23,
|
||||
camaraderieRemembered: true,
|
||||
chainRate: 23,
|
||||
text: '지정 18→23% · 유비↔장비'
|
||||
},
|
||||
{
|
||||
bondId: 'liu-bei__guan-yu',
|
||||
state: 'candidate',
|
||||
selected: false,
|
||||
baseChainRate: 28,
|
||||
bonusChainRate: 0,
|
||||
effectiveChainRate: 28,
|
||||
camaraderieRemembered: false,
|
||||
chainRate: 28,
|
||||
text: '지정 28% · 유비↔관우'
|
||||
},
|
||||
{
|
||||
bondId: 'guan-yu__zhang-fei',
|
||||
state: 'candidate',
|
||||
selected: false,
|
||||
baseChainRate: 18,
|
||||
bonusChainRate: 0,
|
||||
effectiveChainRate: 18,
|
||||
camaraderieRemembered: false,
|
||||
chainRate: 18,
|
||||
text: '지정 18% · 관우↔장비'
|
||||
}
|
||||
]
|
||||
) &&
|
||||
firstSortiePursuit.rows.every(
|
||||
(row) =>
|
||||
isFiniteBounds(row.bounds) &&
|
||||
@@ -3799,25 +4283,53 @@ try {
|
||||
return pursuit?.selectedCoreBondId === 'liu-bei__zhang-fei' &&
|
||||
pursuit?.rows?.some((row) => row.bondId === 'liu-bei__zhang-fei' && row.selected === true);
|
||||
}, undefined, { timeout: 30000 });
|
||||
const selectedCoreResonance = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePursuitPreview);
|
||||
const selectedCoreResonanceCampState =
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const selectedCoreResonance = selectedCoreResonanceCampState?.sortiePursuitPreview;
|
||||
const selectedCamaraderiePerk =
|
||||
selectedCoreResonanceCampState?.firstBattleCamaraderieMemory?.perk;
|
||||
const selectedCoreResonanceSave = await readCampaignSave(page);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-core-resonance-selected.png`, fullPage: true });
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-camaraderie-perk-selected.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'first camp selected camaraderie perk');
|
||||
assert(
|
||||
selectedCoreResonance?.selectionBattleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
selectedCoreResonance?.selectedCoreBondId === 'liu-bei__zhang-fei' &&
|
||||
selectedCoreResonance?.summaryText === '핵심 공명조 · 지정 · 후보 3조' &&
|
||||
selectedCoreResonance?.summaryText === '핵심 공명조 · 23% 지정 · 후보 3조' &&
|
||||
selectedCoreResonance?.activePairs?.[0]?.id === 'liu-bei__zhang-fei' &&
|
||||
selectedCoreResonance.activePairs[0].core === true &&
|
||||
selectedCoreResonance.activePairs[0].baseChainRate === 8 &&
|
||||
selectedCoreResonance.activePairs[0].chainRate === 18 &&
|
||||
selectedCoreResonance.activePairs[0].chainRate === 23 &&
|
||||
selectedCoreResonance?.coreCandidates?.[0]?.id === 'liu-bei__zhang-fei' &&
|
||||
selectedCoreResonance.coreCandidates[0].baseCoreChainRate === 18 &&
|
||||
selectedCoreResonance.coreCandidates[0].bonusChainRate === 5 &&
|
||||
selectedCoreResonance.coreCandidates[0].effectiveCoreChainRate === 23 &&
|
||||
selectedCoreResonance.coreCandidates[0].camaraderieRemembered === true &&
|
||||
selectedCoreResonance.coreCandidates[0].selected === true &&
|
||||
selectedCoreResonance?.rows?.[0]?.bondId === 'liu-bei__zhang-fei' &&
|
||||
selectedCoreResonance.rows[0].baseChainRate === 18 &&
|
||||
selectedCoreResonance.rows[0].bonusChainRate === 5 &&
|
||||
selectedCoreResonance.rows[0].effectiveChainRate === 23 &&
|
||||
selectedCoreResonance.rows[0].camaraderieRemembered === true &&
|
||||
selectedCoreResonance.rows[0].state === 'selected' &&
|
||||
selectedCoreResonance.rows[0].text === '핵심 18% 유비↔장비' &&
|
||||
selectedCoreResonance.rows[0].text === '핵심 18→23% · 유비↔장비' &&
|
||||
selectedCamaraderiePerk?.unlocked === true &&
|
||||
selectedCamaraderiePerk.requiresManualSelection === true &&
|
||||
selectedCamaraderiePerk.autoSelected === false &&
|
||||
selectedCamaraderiePerk.exactPairOnly === true &&
|
||||
selectedCamaraderiePerk.selected === true &&
|
||||
selectedCamaraderiePerk.active === true &&
|
||||
selectedCamaraderiePerk.baseChainRate === 18 &&
|
||||
selectedCamaraderiePerk.bonusChainRate === 5 &&
|
||||
selectedCamaraderiePerk.effectiveChainRate === 23 &&
|
||||
selectedCamaraderiePerk.cardText === '핵심 18→23% · 유비↔장비' &&
|
||||
isFiniteBounds(selectedCamaraderiePerk.cardBounds) &&
|
||||
boundsInside(selectedCamaraderiePerk.cardBounds, selectedCoreResonance.panelBounds) &&
|
||||
selectedCoreResonanceSave.current?.sortieResonanceSelection?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
selectedCoreResonanceSave.current.sortieResonanceSelection.bondId === 'liu-bei__zhang-fei',
|
||||
`Expected one clicked pair to become the persisted, prioritized 18% core resonance: ${JSON.stringify({ selectedCoreResonance, save: selectedCoreResonanceSave.current?.sortieResonanceSelection })}`
|
||||
selectedCoreResonanceSave.current.sortieResonanceSelection.bondId === 'liu-bei__zhang-fei' &&
|
||||
selectedCoreResonanceSave.slot1?.sortieResonanceSelection?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
selectedCoreResonanceSave.slot1.sortieResonanceSelection.bondId === 'liu-bei__zhang-fei',
|
||||
`Expected one manual click to activate and visibly persist the remembered pair at 18% + 5%p = 23%: ${JSON.stringify({ selectedCoreResonance, selectedCamaraderiePerk, save: selectedCoreResonanceSave.current?.sortieResonanceSelection })}`
|
||||
);
|
||||
|
||||
const coreResonanceAutoClearFixture = await page.evaluate(() => {
|
||||
@@ -3855,8 +4367,14 @@ try {
|
||||
clearedCoreResonance?.rows?.every((row) => row.selected === false && row.state === 'candidate') &&
|
||||
!clearedCoreCampState?.selectedSortieUnitIds?.includes('zhang-fei') &&
|
||||
clearedCoreCampState?.sortiePlanFeedback?.includes('핵심 공명조 자동 해제') &&
|
||||
clearedCoreResonanceSave.current?.sortieResonanceSelection === undefined,
|
||||
`Expected removing a designated member to clear the persisted core pair and retain ordinary pursuit behavior: ${JSON.stringify({ clearedCoreCampState, clearedCoreResonance, save: clearedCoreResonanceSave.current?.sortieResonanceSelection })}`
|
||||
clearedCoreCampState?.firstBattleCamaraderieMemory?.perk?.unlocked === true &&
|
||||
clearedCoreCampState.firstBattleCamaraderieMemory.perk.autoSelected === false &&
|
||||
clearedCoreCampState.firstBattleCamaraderieMemory.perk.candidateAvailable === false &&
|
||||
clearedCoreCampState.firstBattleCamaraderieMemory.perk.selected === false &&
|
||||
clearedCoreCampState.firstBattleCamaraderieMemory.perk.active === false &&
|
||||
clearedCoreResonanceSave.current?.sortieResonanceSelection === undefined &&
|
||||
clearedCoreResonanceSave.slot1?.sortieResonanceSelection === undefined,
|
||||
`Expected removing Zhang Fei to clear the remembered core pair without converting the unlocked perk into an automatic selection: ${JSON.stringify({ clearedCoreCampState, clearedCoreResonance, save: clearedCoreResonanceSave.current?.sortieResonanceSelection })}`
|
||||
);
|
||||
await page.evaluate((before) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
@@ -4053,7 +4571,17 @@ try {
|
||||
firstSortiePursuitRestored.planFeedback === firstSortiePursuitMutation.before?.planFeedback &&
|
||||
firstSortiePursuitRestored.rosterScroll === firstSortiePursuitMutation.before?.rosterScroll &&
|
||||
sameJsonValue(firstSortiePursuitRestored.deploymentPreview, firstCampDeploymentPreview) &&
|
||||
sameJsonValue(firstSortiePursuitRestored.pursuit?.activePairs, expectedFirstSortiePursuitPairs),
|
||||
sameJsonValue(firstSortiePursuitRestored.pursuit?.activePairs, expectedFirstSortiePursuitPairs) &&
|
||||
firstSortiePursuitRestored.pursuit?.selectedCoreBondId === null &&
|
||||
firstSortiePursuitRestored.pursuit?.rows?.find(
|
||||
(row) => row.bondId === 'liu-bei__zhang-fei'
|
||||
)?.baseChainRate === 18 &&
|
||||
firstSortiePursuitRestored.pursuit?.rows?.find(
|
||||
(row) => row.bondId === 'liu-bei__zhang-fei'
|
||||
)?.bonusChainRate === 5 &&
|
||||
firstSortiePursuitRestored.pursuit?.rows?.find(
|
||||
(row) => row.bondId === 'liu-bei__zhang-fei'
|
||||
)?.effectiveChainRate === 23,
|
||||
`Expected the pursuit RC fixture to restore the exact live formation, roles, supplies, focus, feedback, scroll, and deployment: ${JSON.stringify(firstSortiePursuitRestored)}`
|
||||
);
|
||||
assert(
|
||||
@@ -4104,8 +4632,14 @@ try {
|
||||
assert(
|
||||
launchCoreResonance?.activePairs?.[0]?.id === 'liu-bei__zhang-fei' &&
|
||||
launchCoreResonance.activePairs[0].core === true &&
|
||||
launchCoreResonance.activePairs[0].chainRate === 18,
|
||||
`Expected the lower legacy-rate pair to be promoted and prioritized before battle launch: ${JSON.stringify(launchCoreResonance)}`
|
||||
launchCoreResonance.activePairs[0].baseChainRate === 8 &&
|
||||
launchCoreResonance.activePairs[0].chainRate === 23 &&
|
||||
launchCoreResonance?.rows?.[0]?.bondId === 'liu-bei__zhang-fei' &&
|
||||
launchCoreResonance.rows[0].baseChainRate === 18 &&
|
||||
launchCoreResonance.rows[0].bonusChainRate === 5 &&
|
||||
launchCoreResonance.rows[0].effectiveChainRate === 23 &&
|
||||
launchCoreResonance.rows[0].selected === true,
|
||||
`Expected reselecting the remembered lower-level pair to restore its explicit 18% + 5%p launch rate: ${JSON.stringify(launchCoreResonance)}`
|
||||
);
|
||||
await advanceSortiePrepStep(page, 'loadout');
|
||||
await clickLegacyUi(page, 1116, 656);
|
||||
@@ -4167,7 +4701,10 @@ try {
|
||||
secondBattleProbe.coreSortieResonance.valid === true &&
|
||||
secondBattleProbe.coreSortieResonance.active === true &&
|
||||
secondBattleProbe.coreSortieResonance.level === 68 &&
|
||||
secondBattleProbe.coreSortieResonance.rate === 18 &&
|
||||
secondBattleProbe.coreSortieResonance.baseRate === 18 &&
|
||||
secondBattleProbe.coreSortieResonance.memoryBonus === 5 &&
|
||||
secondBattleProbe.coreSortieResonance.effectiveRate === 23 &&
|
||||
secondBattleProbe.coreSortieResonance.rate === 23 &&
|
||||
sameJsonValue(secondBattleProbe.coreSortieResonance.unitIds, ['liu-bei', 'zhang-fei']) &&
|
||||
sameJsonValue(secondBattleProbe.coreSortieResonance.stats, {
|
||||
attempts: 0,
|
||||
@@ -4176,7 +4713,50 @@ try {
|
||||
additionalDamage: 0
|
||||
}) &&
|
||||
sameJsonValue(secondBattleProbe.sortieResonance, secondBattleProbe.coreSortieResonance),
|
||||
`Expected the camp designation to reach battle as one active 18% core resonance with clean statistics: ${JSON.stringify(secondBattleProbe.coreSortieResonance)}`
|
||||
`Expected the manually designated remembered pair to reach battle at base 18% + memory 5%p = 23% with clean statistics: ${JSON.stringify(secondBattleProbe.coreSortieResonance)}`
|
||||
);
|
||||
const nonRememberedCorePairProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const rawCampaign = window.localStorage.getItem('heros-web:campaign-state');
|
||||
if (
|
||||
!scene ||
|
||||
!rawCampaign ||
|
||||
typeof scene.refreshLaunchCamaraderieMemoryBonus !== 'function'
|
||||
) {
|
||||
return { ready: false };
|
||||
}
|
||||
const campaign = JSON.parse(rawCampaign);
|
||||
const original = {
|
||||
bondId: scene.launchSortieResonanceBondId,
|
||||
memoryBondId: scene.launchCamaraderieMemoryBondId,
|
||||
memoryBonus: scene.launchCamaraderieMemoryBonus
|
||||
};
|
||||
try {
|
||||
scene.launchSortieResonanceBondId = 'liu-bei__guan-yu';
|
||||
scene.refreshLaunchCamaraderieMemoryBonus(campaign);
|
||||
const resonance = window.__HEROS_DEBUG__?.battle()?.coreSortieResonance;
|
||||
return {
|
||||
ready: true,
|
||||
selectedBondId: resonance?.selectedBondId ?? null,
|
||||
baseRate: resonance?.baseRate ?? null,
|
||||
memoryBonus: resonance?.memoryBonus ?? null,
|
||||
effectiveRate: resonance?.effectiveRate ?? null,
|
||||
rate: resonance?.rate ?? null
|
||||
};
|
||||
} finally {
|
||||
scene.launchSortieResonanceBondId = original.bondId;
|
||||
scene.launchCamaraderieMemoryBondId = original.memoryBondId;
|
||||
scene.launchCamaraderieMemoryBonus = original.memoryBonus;
|
||||
}
|
||||
});
|
||||
assert(
|
||||
nonRememberedCorePairProbe.ready === true &&
|
||||
nonRememberedCorePairProbe.selectedBondId === 'liu-bei__guan-yu' &&
|
||||
nonRememberedCorePairProbe.baseRate === 28 &&
|
||||
nonRememberedCorePairProbe.memoryBonus === 0 &&
|
||||
nonRememberedCorePairProbe.effectiveRate === 28 &&
|
||||
nonRememberedCorePairProbe.rate === 28,
|
||||
`Expected a different valid core pair to keep its normal 28% rate without inheriting the Liu Bei/Zhang Fei +5%p memory: ${JSON.stringify(nonRememberedCorePairProbe)}`
|
||||
);
|
||||
const secondBattleCorePriority = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
@@ -4191,7 +4771,12 @@ try {
|
||||
|
||||
const touchedUnits = [attacker, corePartner, ordinaryPartner, target];
|
||||
const original = {
|
||||
units: touchedUnits.map((unit) => ({ unit, x: unit.x, y: unit.y })),
|
||||
units: touchedUnits.map((unit) => ({
|
||||
unit,
|
||||
x: unit.x,
|
||||
y: unit.y,
|
||||
direction: scene.unitViews.get(unit.id)?.direction ?? 'south'
|
||||
})),
|
||||
attackIntents: scene.attackIntents.map((intent) => ({ ...intent })),
|
||||
actedUnitIds: new Set(scene.actedUnitIds),
|
||||
targetHp: target.hp,
|
||||
@@ -4213,7 +4798,7 @@ try {
|
||||
const preview = scene.combatPreview(attacker, target, 'attack');
|
||||
const failedResolution = scene.resolveBondChainFollowUp(preview, true, false);
|
||||
const successfulResolution = scene.resolveBondChainFollowUp(preview, true, true);
|
||||
const coreStats = window.__HEROS_DEBUG__?.battle()?.coreSortieResonance?.stats ?? null;
|
||||
const coreResonance = window.__HEROS_DEBUG__?.battle()?.coreSortieResonance ?? null;
|
||||
const pursuitTriggers = scene.statsFor(attacker.id).sortieExtendedBondTriggers;
|
||||
return {
|
||||
ready: true,
|
||||
@@ -4233,19 +4818,33 @@ try {
|
||||
},
|
||||
failedResolution: failedResolution ? {
|
||||
succeeded: failedResolution.attempt.succeeded,
|
||||
rate: failedResolution.attempt.rate,
|
||||
coreResonance: failedResolution.attempt.coreResonance,
|
||||
hasResult: Boolean(failedResolution.result)
|
||||
} : null,
|
||||
successfulResolution: successfulResolution ? {
|
||||
succeeded: successfulResolution.attempt.succeeded,
|
||||
rate: successfulResolution.attempt.rate,
|
||||
coreResonance: successfulResolution.attempt.coreResonance,
|
||||
damage: successfulResolution.result?.damage ?? 0
|
||||
} : null,
|
||||
coreStats,
|
||||
coreResonance: coreResonance
|
||||
? {
|
||||
selectedBondId: coreResonance.selectedBondId,
|
||||
rate: coreResonance.rate,
|
||||
baseRate: coreResonance.baseRate,
|
||||
memoryBonus: coreResonance.memoryBonus,
|
||||
effectiveRate: coreResonance.effectiveRate,
|
||||
stats: coreResonance.stats
|
||||
}
|
||||
: null,
|
||||
pursuitTriggers
|
||||
};
|
||||
} finally {
|
||||
original.units.forEach(({ unit, x, y }) => Object.assign(unit, { x, y }));
|
||||
original.units.forEach(({ unit, x, y, direction }) => {
|
||||
Object.assign(unit, { x, y });
|
||||
scene.syncUnitMotion(unit, scene.unitViews.get(unit.id), direction);
|
||||
});
|
||||
target.hp = original.targetHp;
|
||||
scene.battleStats = original.battleStats;
|
||||
scene.attackIntents = original.attackIntents;
|
||||
@@ -4256,27 +4855,34 @@ try {
|
||||
secondBattleCorePriority.ready === true &&
|
||||
secondBattleCorePriority.candidate?.bondId === 'liu-bei__zhang-fei' &&
|
||||
secondBattleCorePriority.candidate.partnerId === 'zhang-fei' &&
|
||||
secondBattleCorePriority.candidate.rate === 18 &&
|
||||
secondBattleCorePriority.candidate.rate === 23 &&
|
||||
secondBattleCorePriority.candidate.coreResonance === true &&
|
||||
secondBattleCorePriority.candidate.label.includes('핵심 공명') &&
|
||||
secondBattleCorePriority.preview?.bondId === 'liu-bei__zhang-fei' &&
|
||||
secondBattleCorePriority.preview.partnerId === 'zhang-fei' &&
|
||||
secondBattleCorePriority.preview.rate === 18 &&
|
||||
secondBattleCorePriority.preview.rate === 23 &&
|
||||
secondBattleCorePriority.preview.coreResonance === true &&
|
||||
secondBattleCorePriority.coreResonance?.selectedBondId === 'liu-bei__zhang-fei' &&
|
||||
secondBattleCorePriority.coreResonance.baseRate === 18 &&
|
||||
secondBattleCorePriority.coreResonance.memoryBonus === 5 &&
|
||||
secondBattleCorePriority.coreResonance.effectiveRate === 23 &&
|
||||
secondBattleCorePriority.coreResonance.rate === 23 &&
|
||||
secondBattleCorePriority.failedResolution?.succeeded === false &&
|
||||
secondBattleCorePriority.failedResolution.rate === 23 &&
|
||||
secondBattleCorePriority.failedResolution.coreResonance === true &&
|
||||
secondBattleCorePriority.failedResolution.hasResult === false &&
|
||||
secondBattleCorePriority.successfulResolution?.succeeded === true &&
|
||||
secondBattleCorePriority.successfulResolution.rate === 23 &&
|
||||
secondBattleCorePriority.successfulResolution.coreResonance === true &&
|
||||
secondBattleCorePriority.successfulResolution.damage > 0 &&
|
||||
secondBattleCorePriority.pursuitTriggers === 1 &&
|
||||
sameJsonValue(secondBattleCorePriority.coreStats, {
|
||||
sameJsonValue(secondBattleCorePriority.coreResonance.stats, {
|
||||
attempts: 2,
|
||||
successes: 1,
|
||||
failures: 1,
|
||||
additionalDamage: secondBattleCorePriority.successfulResolution.damage
|
||||
}),
|
||||
`Expected the designated Lv68 pair to outrank Liu Bei's ordinary Lv72 pursuit candidate: ${JSON.stringify(secondBattleCorePriority)}`
|
||||
`Expected forced failure/success to use exactly 23% and retain one non-stacking +5%p memory bonus over the 18% core rate: ${JSON.stringify(secondBattleCorePriority)}`
|
||||
);
|
||||
assert(
|
||||
secondBattleProbe.openingBannerTexts?.some((text) => text.includes('세 형제 생존 중 공명 지원 거리 2칸')) &&
|
||||
@@ -4416,13 +5022,17 @@ try {
|
||||
assert(
|
||||
roleSealResyncProbe?.changedCoreResonance?.selectedBondId === 'liu-bei__zhang-fei' &&
|
||||
roleSealResyncProbe.changedCoreResonance.active === true &&
|
||||
roleSealResyncProbe.changedCoreResonance.baseRate === 18 &&
|
||||
roleSealResyncProbe.changedCoreResonance.memoryBonus === 5 &&
|
||||
roleSealResyncProbe.changedCoreResonance.effectiveRate === 23 &&
|
||||
roleSealResyncProbe.changedCoreResonance.rate === 23 &&
|
||||
sameJsonValue(roleSealResyncProbe.changedCoreResonance.stats, {
|
||||
attempts: 2,
|
||||
successes: 1,
|
||||
failures: 1,
|
||||
additionalDamage: secondBattleCorePriority.successfulResolution.damage
|
||||
}),
|
||||
`Expected battle save data—not the cleared in-memory or campaign fallback—to restore the core designation and totals: ${JSON.stringify(roleSealResyncProbe?.changedCoreResonance)}`
|
||||
`Expected battle save data—not the cleared in-memory selection—to restore the core pair, totals, and one non-stacking 18% + 5%p rate: ${JSON.stringify(roleSealResyncProbe?.changedCoreResonance)}`
|
||||
);
|
||||
assert(
|
||||
roleSealResyncProbe?.allReserve?.doctrineActive === true &&
|
||||
@@ -4441,14 +5051,18 @@ try {
|
||||
assert(
|
||||
roleSealResyncProbe?.restoredCoreResonance?.selectedBondId === 'liu-bei__zhang-fei' &&
|
||||
roleSealResyncProbe.restoredCoreResonance.active === true &&
|
||||
roleSealResyncProbe.restoredCoreResonance.rate === 18 &&
|
||||
roleSealResyncProbe.restoredCoreResonance.baseRate === 18 &&
|
||||
roleSealResyncProbe.restoredCoreResonance.memoryBonus === 5 &&
|
||||
roleSealResyncProbe.restoredCoreResonance.effectiveRate === 23 &&
|
||||
roleSealResyncProbe.restoredCoreResonance.rate === 23 &&
|
||||
roleSealResyncProbe.restoredCoreResonance.rate !== 28 &&
|
||||
sameJsonValue(roleSealResyncProbe.restoredCoreResonance.stats, {
|
||||
attempts: 2,
|
||||
successes: 1,
|
||||
failures: 1,
|
||||
additionalDamage: secondBattleCorePriority.successfulResolution.damage
|
||||
}),
|
||||
`Expected battle-slot reloads to preserve the selected core pair and its accumulated pursuit statistics: ${JSON.stringify(roleSealResyncProbe?.restoredCoreResonance)}`
|
||||
`Expected battle-slot reloads to preserve 23% rather than double-applying memory to 28%, alongside accumulated pursuit statistics: ${JSON.stringify(roleSealResyncProbe?.restoredCoreResonance)}`
|
||||
);
|
||||
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||
@@ -4462,20 +5076,20 @@ try {
|
||||
return {
|
||||
stats: resonance?.stats ?? null,
|
||||
resultSummary: resonance?.resultSummary ?? null,
|
||||
summary: texts.find((text) => text.startsWith('핵심 공명 · 시도')) ?? null
|
||||
summary: texts.find((text) => text.startsWith('핵심 공명 · 판정')) ?? null
|
||||
};
|
||||
});
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-second-battle-core-resonance-result.png`, fullPage: true });
|
||||
assert(
|
||||
secondBattleCoreResult.summary ===
|
||||
`핵심 공명 · 시도 2 · 성공 1 · 추가 피해 +${secondBattleCorePriority.successfulResolution.damage}` &&
|
||||
`핵심 공명 · 판정 23% (전우 기억 +5%p) · 시도 2 · 성공 1 · 추가 피해 +${secondBattleCorePriority.successfulResolution.damage}` &&
|
||||
sameJsonValue(secondBattleCoreResult.stats, roleSealResyncProbe.restoredCoreResonance.stats) &&
|
||||
secondBattleCoreResult.resultSummary?.visible === true &&
|
||||
secondBattleCoreResult.resultSummary.text === secondBattleCoreResult.summary &&
|
||||
isFiniteBounds(secondBattleCoreResult.resultSummary.bounds) &&
|
||||
boundsInside(secondBattleCoreResult.resultSummary.bounds, logicalViewportBounds) &&
|
||||
secondBattleCoreResult.resultSummary.bounds.y + secondBattleCoreResult.resultSummary.bounds.height <= 416 * legacyUiScale,
|
||||
`Expected the victory report to surface persisted core-resonance attempts, successes, and damage: ${JSON.stringify(secondBattleCoreResult)}`
|
||||
`Expected the victory report to close the remembered-pair feedback loop with the applied 23% rate, +5%p source, attempts, successes, and damage: ${JSON.stringify(secondBattleCoreResult)}`
|
||||
);
|
||||
|
||||
const requiredCasualtySaveBefore = await readCampaignSave(page);
|
||||
@@ -10121,6 +10735,60 @@ async function readFirstVictoryFollowupControlPoint(page, control, choiceId) {
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readCampDialogueControlPoint(page, control, dialogueId, choiceId) {
|
||||
const probe = await page.evaluate(({ control, dialogueId, choiceId }) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp?.();
|
||||
const canvas = document.querySelector('canvas');
|
||||
const memory = state?.firstBattleCamaraderieMemory;
|
||||
const dialogue = memory?.dialogue?.id === dialogueId
|
||||
? memory.dialogue
|
||||
: state?.selectedDialogue?.id === dialogueId
|
||||
? state.selectedDialogue
|
||||
: null;
|
||||
const choice = dialogue?.choices?.find((candidate) => candidate.id === choiceId);
|
||||
const bounds = control === 'row' ? dialogue?.rowBounds : choice?.bounds;
|
||||
const interactiveObject = control === 'row'
|
||||
? scene?.campDialogueRowButtons?.[dialogueId]
|
||||
: scene?.campDialogueChoiceButtons?.[`${dialogueId}:${choiceId}`];
|
||||
if (
|
||||
!scene ||
|
||||
!canvas ||
|
||||
!bounds ||
|
||||
!interactiveObject?.input?.enabled
|
||||
) {
|
||||
return {
|
||||
ready: false,
|
||||
control,
|
||||
dialogueId,
|
||||
choiceId: choiceId ?? null,
|
||||
bounds: bounds ?? null,
|
||||
dialogue: dialogue ?? null
|
||||
};
|
||||
}
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
ready: true,
|
||||
control,
|
||||
dialogueId,
|
||||
choiceId: choiceId ?? null,
|
||||
bounds,
|
||||
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, { control, dialogueId, choiceId });
|
||||
assert(
|
||||
probe.ready === true &&
|
||||
Number.isFinite(probe.x) &&
|
||||
Number.isFinite(probe.y) &&
|
||||
boundsWithinFhdViewport(probe.bounds),
|
||||
`Expected an interactive ${control} control for camp dialogue ${dialogueId}: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readSortieTextControlPoint(page, label) {
|
||||
const probe = await page.evaluate((targetLabel) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
|
||||
Reference in New Issue
Block a user