feat: make camp preparation affect first battle
This commit is contained in:
@@ -168,6 +168,11 @@ async function verifyRuntimeIntentState(browserInstance, targetUrl, scenario, op
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle()?.triggeredBattleEvents?.includes('opening') === true,
|
||||
undefined,
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
|
||||
const live = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
const aliveEnemyCount = live?.units?.filter((unit) => unit.faction === 'enemy' && unit.hp > 0).length ?? 0;
|
||||
|
||||
@@ -368,6 +368,18 @@ try {
|
||||
assert.equal(militiaCamp.npcs.length, 5);
|
||||
assert.equal(militiaCamp.objectives.length, 4);
|
||||
assert.equal(militiaCamp.completedObjectiveIds.length, 0);
|
||||
assert.deepEqual(
|
||||
militiaCamp.preparationFeedback.map(({ id, completed, worldCueVisible }) => ({
|
||||
id,
|
||||
completed,
|
||||
worldCueVisible
|
||||
})),
|
||||
[
|
||||
{ id: 'review-scout-report', completed: false, worldCueVisible: false },
|
||||
{ id: 'ready-vanguard', completed: false, worldCueVisible: false },
|
||||
{ id: 'inspect-arms', completed: false, worldCueVisible: false }
|
||||
]
|
||||
);
|
||||
assert.equal(militiaCamp.exitUnlocked, false);
|
||||
assert.equal(
|
||||
militiaCamp.dialogue.active,
|
||||
@@ -440,6 +452,15 @@ try {
|
||||
await advanceMilitiaCampDialogueUntilClosed(page);
|
||||
militiaCamp = await readMilitiaCamp(page);
|
||||
assert(militiaCamp.completedObjectiveIds.includes('inspect-arms'));
|
||||
const supplyFeedback = militiaCamp.preparationFeedback.find(({ id }) => id === 'inspect-arms');
|
||||
assert.equal(supplyFeedback?.worldCueVisible, true);
|
||||
assertBoundsInsideCampMap(supplyFeedback?.worldCueBounds, 'completed supply world cue');
|
||||
assert(
|
||||
militiaCamp.preparationFeedback
|
||||
.filter(({ id }) => id !== 'inspect-arms')
|
||||
.every(({ worldCueVisible }) => worldCueVisible === false),
|
||||
'Completing supplies must not reveal unrelated camp preparation cues.'
|
||||
);
|
||||
|
||||
await teleportMilitiaCampTo(page, 'zou-jing');
|
||||
await page.keyboard.press('e');
|
||||
@@ -465,6 +486,11 @@ try {
|
||||
militiaCamp.completedObjectiveIds.includes('inspect-arms'),
|
||||
'A completed militia camp inspection must survive reload and Continue.'
|
||||
);
|
||||
assert.equal(
|
||||
militiaCamp.preparationFeedback.find(({ id }) => id === 'inspect-arms')?.worldCueVisible,
|
||||
true,
|
||||
'A completed supply world cue must survive reload and Continue.'
|
||||
);
|
||||
assert.equal(
|
||||
militiaCamp.dialogue.active,
|
||||
false,
|
||||
@@ -478,6 +504,22 @@ try {
|
||||
[...militiaCamp.completedObjectiveIds].sort(),
|
||||
['inspect-arms', 'ready-vanguard', 'review-scout-report'].sort()
|
||||
);
|
||||
assert(
|
||||
militiaCamp.preparationFeedback.every(({ completed, worldCueVisible, worldCueBounds }) => {
|
||||
if (!completed || !worldCueVisible) {
|
||||
return false;
|
||||
}
|
||||
assertBoundsInsideCampMap(worldCueBounds, 'completed camp preparation cue');
|
||||
return true;
|
||||
}),
|
||||
'Every completed preparation must remain visible in the camp world.'
|
||||
);
|
||||
assert.equal(
|
||||
militiaCamp.preparationFeedback.find(({ id }) => id === 'ready-vanguard')?.ambientActorCount,
|
||||
4,
|
||||
'Zhang Fei preparation should visibly line up four non-interactive volunteers.'
|
||||
);
|
||||
assert.equal(militiaCamp.npcs.length, 5, 'Ambient formation actors must not become dialogue targets.');
|
||||
assert.equal(militiaCamp.exitUnlocked, true);
|
||||
await captureStableScreenshot(page, 'dist/verification-prologue-militia-camp-ready.png');
|
||||
|
||||
@@ -575,7 +617,7 @@ try {
|
||||
));
|
||||
const commandReaction = await readMilitiaCamp(page);
|
||||
assert.equal(commandReaction.commandChoice.open, false);
|
||||
assert.equal(commandReaction.dialogue.totalLines, 3);
|
||||
assert.equal(commandReaction.dialogue.totalLines, 2);
|
||||
assert.equal(commandReaction.dialogue.portraitTextureKey, 'portrait-zhang-fei-yellow-turban');
|
||||
const savedCommand = await readCampaignSave(page);
|
||||
assert.deepEqual(savedCommand.sortieOrderSelection, {
|
||||
@@ -624,6 +666,89 @@ try {
|
||||
const firstBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.battle?.());
|
||||
assert.equal(firstBattle.sortieOrder.selectedId, 'mobile');
|
||||
assert.equal(firstBattle.sortieOrder.live.orderId, 'mobile');
|
||||
assert.equal(firstBattle.firstBattlePreparation.eligible, true);
|
||||
assert.equal(firstBattle.firstBattlePreparation.allComplete, true);
|
||||
assert.equal(firstBattle.firstBattlePreparation.completedCount, 3);
|
||||
assert.equal(firstBattle.firstBattlePreparation.summaryVisible, true);
|
||||
assert.match(
|
||||
firstBattle.battleHud.deploymentPanel.subtitleText,
|
||||
/막사 준비 3\/3/,
|
||||
'The deployment subtitle should retain the completed camp preparation summary.'
|
||||
);
|
||||
assert.match(
|
||||
firstBattle.battleHud.deploymentPanel.subtitleText,
|
||||
/첫 군령/,
|
||||
'The preparation summary must not replace the first command selected in camp.'
|
||||
);
|
||||
assertBoundsInsideViewport(
|
||||
firstBattle.firstBattlePreparation.summaryBounds,
|
||||
'first-battle preparation summary'
|
||||
);
|
||||
assert.deepEqual(
|
||||
firstBattle.firstBattlePreparation.callbacks.map(({ callback, completed }) => ({
|
||||
callback,
|
||||
completed
|
||||
})),
|
||||
[
|
||||
{ callback: 'enemy-intel', completed: true },
|
||||
{ callback: 'deployment-flex', completed: true },
|
||||
{ callback: 'field-salve', completed: true }
|
||||
]
|
||||
);
|
||||
assert.equal(firstBattle.firstBattlePreparation.enemyIntel.visibleDuringDeployment, true);
|
||||
assert(firstBattle.firstBattlePreparation.enemyIntel.forecastCount > 0);
|
||||
assert(firstBattle.firstBattlePreparation.enemyIntel.visualCount > 0);
|
||||
assert.deepEqual(firstBattle.firstBattlePreparation.deploymentFlex, {
|
||||
enabled: true,
|
||||
baseReserveSlotCount: 2,
|
||||
bonusReserveSlotCount: 2,
|
||||
activeReserveSlotCount: 4
|
||||
});
|
||||
assert.deepEqual(firstBattle.firstBattlePreparation.fieldSalve, {
|
||||
enabled: true,
|
||||
carrierUnitId: 'guan-yu',
|
||||
itemId: 'salve',
|
||||
count: 1
|
||||
});
|
||||
assert.equal(firstBattle.itemStocks['guan-yu'].salve, 1);
|
||||
await captureStableScreenshot(page, 'dist/verification-first-battle-preparation.png');
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
scene?.saveBattleState?.(1);
|
||||
scene?.loadBattleState?.(1);
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.battle?.()?.firstBattlePreparation?.fieldSalve?.count === 1,
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const resumedFirstBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.battle?.());
|
||||
assert.equal(
|
||||
resumedFirstBattle.firstBattlePreparation.fieldSalve.count,
|
||||
1,
|
||||
'Saving and resuming the first battle must not grant the preparation salve twice.'
|
||||
);
|
||||
assert.equal(resumedFirstBattle.itemStocks['guan-yu'].salve, 1);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
scene?.scene.restart({ battleId: 'second-battle-yellow-turban-pursuit' });
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle?.();
|
||||
return (
|
||||
battle?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
battle?.mapBackgroundReady === true
|
||||
);
|
||||
}, undefined, { timeout: 90000 });
|
||||
const secondBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.battle?.());
|
||||
assert.equal(secondBattle.firstBattlePreparation.eligible, false);
|
||||
assert.equal(secondBattle.firstBattlePreparation.completedCount, 0);
|
||||
assert(
|
||||
secondBattle.firstBattlePreparation.callbacks.every(({ completed }) => completed === false),
|
||||
'Militia-camp preparation callbacks must not leak into later battles.'
|
||||
);
|
||||
|
||||
await seedLegacyCompletedVillageSave(page, battleSave);
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
@@ -924,6 +1049,17 @@ function assertBoundsInsideViewport(bounds, label) {
|
||||
);
|
||||
}
|
||||
|
||||
function assertBoundsInsideCampMap(bounds, label) {
|
||||
assert(bounds, `${label}: bounds are required`);
|
||||
assert(
|
||||
bounds.x >= 0 &&
|
||||
bounds.y >= 92 &&
|
||||
bounds.x + bounds.width <= 1488 &&
|
||||
bounds.y + bounds.height <= 958,
|
||||
`${label}: expected bounds inside the militia-camp map, received ${JSON.stringify(bounds)}`
|
||||
);
|
||||
}
|
||||
|
||||
function assertNpcTextureKeys(npcs, expectedTextureKeyByNpcId, sceneLabel) {
|
||||
assert(Array.isArray(npcs), `${sceneLabel}: expected an NPC debug-state array.`);
|
||||
const actualMappings = npcs
|
||||
@@ -975,7 +1111,7 @@ function assertExplorationBackground(background, expectedKey) {
|
||||
}
|
||||
|
||||
async function captureStableScreenshot(page, path) {
|
||||
await page.waitForTimeout(220);
|
||||
await page.waitForTimeout(650);
|
||||
const loopSlept = await page.evaluate(() => {
|
||||
const loop = window.__HEROS_GAME__?.loop;
|
||||
if (!loop || typeof loop.sleep !== 'function') {
|
||||
|
||||
@@ -162,6 +162,24 @@ try {
|
||||
'militia camp objective rows',
|
||||
failures
|
||||
);
|
||||
const preparationPayoffs = militiaCamp.prologueMilitiaCampPreparationPayoffs;
|
||||
assertArrayEquals(
|
||||
preparationPayoffs.map((payoff) => payoff.objectiveId),
|
||||
expectedCampObjectiveIds,
|
||||
'militia camp preparation payoff order',
|
||||
failures
|
||||
);
|
||||
assertArrayEquals(
|
||||
preparationPayoffs.map((payoff) => payoff.battleCallback),
|
||||
['enemy-intel', 'deployment-flex', 'field-salve'],
|
||||
'militia camp preparation battle callbacks',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
preparationPayoffs.every((payoff) => payoff.worldCue.length > 0 && payoff.battleLabel.length > 0),
|
||||
'every militia camp preparation must name its world cue and first-battle callback',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
campObjectives.find((objective) => objective.id === 'review-scout-report')?.label.includes('피난로'),
|
||||
'Guan Yu scout objective must describe the evacuation-route information actually reviewed',
|
||||
@@ -214,7 +232,7 @@ try {
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
choice.reaction.length === 3 &&
|
||||
choice.reaction.length === 2 &&
|
||||
choice.reaction.some((line) => line.speaker === '관우') &&
|
||||
choice.reaction.some((line) => line.speaker === '장비'),
|
||||
`first command ${choice.orderId} must receive distinct Guan Yu and Zhang Fei reactions`,
|
||||
@@ -232,6 +250,13 @@ try {
|
||||
'Zou Jing command setup should be compressed to one line so the new decision adds no extra input',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
['guan-yu', 'zhang-fei', 'quartermaster'].every((npcId) => (
|
||||
campNpcs.find((npc) => npc.id === npcId)?.dialogue.length === 2
|
||||
)),
|
||||
'mandatory camp inspections should each resolve in two lines before their visible world payoff',
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
campNpcs.some((npc) => !npc.objectiveId && !npc.departure),
|
||||
'militia camp should include at least one optional volunteer conversation',
|
||||
|
||||
Reference in New Issue
Block a user