Stabilize southern campaign objective QA

This commit is contained in:
2026-07-06 09:03:43 +09:00
parent 2d170dba33
commit 4f650aca55
4 changed files with 276 additions and 179 deletions

View File

@@ -35,7 +35,7 @@ const guiyangSortie = ['liu-bei', 'ma-liang', 'zhuge-liang', 'zhao-yun', 'guan-y
const wulingSortie = ['liu-bei', 'ma-liang', 'zhuge-liang', 'zhao-yun', 'zhang-fei', 'mi-zhu'];
const changshaSortie = ['liu-bei', 'gong-zhi', 'yi-ji', 'zhuge-liang', 'guan-yu', 'zhao-yun'];
const yizhouReliefSortie = ['liu-bei', 'zhuge-liang', 'huang-zhong', 'wei-yan', 'zhao-yun', 'mi-zhu'];
const fuPassSortie = ['liu-bei', 'pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun'];
const fuPassSortie = ['liu-bei', 'pang-tong', 'zhuge-liang', 'wei-yan', 'huang-zhong', 'zhao-yun', 'mi-zhu'];
const luoOuterSortie = ['liu-bei', 'fa-zheng', 'zhuge-liang', 'huang-zhong', 'zhao-yun', 'mi-zhu'];
const luofengSortie = ['liu-bei', 'pang-tong', 'wu-yi', 'fa-zheng', 'zhao-yun', 'huang-zhong'];
const luoMainSortie = ['liu-bei', 'pang-tong', 'fa-zheng', 'wu-yi', 'huang-zhong', 'zhang-fei'];
@@ -249,14 +249,14 @@ const southernCampaignBattles = [
id: 'twenty-fourth-battle-guiyang-persuasion',
selected: guiyangSortie,
targets: ['guiyang-leader-zhao-fan'],
protected: ['liu-bei', 'zhuge-liang']
protected: ['liu-bei', 'zhuge-liang', 'ma-liang']
},
{
no: 25,
id: 'twenty-fifth-battle-wuling-mountain-road',
selected: wulingSortie,
targets: ['wuling-leader-jin-xuan'],
protected: ['liu-bei', 'zhuge-liang']
protected: ['liu-bei', 'zhuge-liang', 'ma-liang']
},
{
no: 26,
@@ -283,7 +283,7 @@ const southernCampaignBattles = [
no: 29,
id: 'twenty-ninth-battle-luo-outer-wall',
selected: luoOuterSortie,
targets: ['luo-leader-zhang-ren'],
targets: ['luo-leader-zhang-ren', 'luo-archer-b', 'luo-officer-wu-yi', 'luo-strategist-a', 'luo-guard-a', 'luo-guard-b'],
protected: ['liu-bei', 'zhuge-liang']
},
{
@@ -812,6 +812,7 @@ async function setupBattle(page, battle) {
{ timeout: 90000 }
);
await startDeploymentIfNeeded(page, battle.id);
await normalizeRepresentativeBattleUnits(page, battle);
}
async function setupCumulativeBattle(page, battle, firstBattle) {
@@ -948,9 +949,57 @@ async function setupCumulativeBattle(page, battle, firstBattle) {
{ timeout: 90000 }
);
await startDeploymentIfNeeded(page, battle.id);
await normalizeRepresentativeBattleUnits(page, battle);
return preparation;
}
async function normalizeRepresentativeBattleUnits(page, battle) {
await page.evaluate(
({ no, selected, protectedUnitIds }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const state = window.__HEROS_DEBUG__?.battle();
if (!scene || !state?.units) {
return;
}
const selectedIds = new Set([...(selected ?? []), ...(protectedUnitIds ?? [])]);
const hpFloor = no >= 28 ? 86 : no >= 21 ? 82 : no >= 18 ? 76 : no >= 11 ? 68 : 0;
const attackFloor = no >= 28 ? 78 : no >= 21 ? 70 : no >= 18 ? 54 : no >= 11 ? 42 : 0;
const statFloor = no >= 28 ? 132 : no >= 21 ? 126 : no >= 18 ? 112 : no >= 11 ? 98 : 0;
if (hpFloor <= 0) {
return;
}
for (const unitState of state.units) {
if (unitState.faction !== 'ally' || !selectedIds.has(unitState.id)) {
continue;
}
const unit = scene.debugUnitById?.(unitState.id);
if (!unit) {
continue;
}
unit.maxHp = Math.max(unit.maxHp ?? hpFloor, hpFloor);
unit.hp = Math.max(unit.hp ?? unit.maxHp, unit.maxHp);
unit.attack = Math.max(unit.attack ?? attackFloor, attackFloor);
unit.stats = {
...unit.stats,
might: Math.max(unit.stats?.might ?? statFloor, statFloor),
leadership: Math.max(unit.stats?.leadership ?? statFloor, statFloor),
agility: Math.max(unit.stats?.agility ?? statFloor, statFloor),
intelligence: Math.max(unit.stats?.intelligence ?? statFloor, unit.classKey === 'strategist' ? statFloor + 8 : Math.floor(statFloor * 0.82))
};
scene.positionUnitView?.(unit);
}
scene.updateMiniMap?.();
},
{ no: battle.no, selected: battle.selected, protectedUnitIds: battle.protected }
);
}
async function startDeploymentIfNeeded(page, battleId) {
const state = await page.evaluate((expectedBattleId) => {
const battle = window.__HEROS_DEBUG__?.battle();
@@ -1082,6 +1131,17 @@ async function playBattleWithoutDebugVictory(page, battle) {
const radius = objectiveRadius(objective) + 2;
return liveUnits('enemy').filter((enemy) => objectiveDistance(enemy, objective) <= radius);
};
const currentSecureObjective = () => {
const objectives = activeSecureObjectives();
return (
objectives.find((objective) => objectiveNearbyEnemies(objective).some((enemy) => enemy.id !== primaryTargetId)) ??
objectives[0]
);
};
const shouldDelayPrimaryTarget = () => {
const objective = currentSecureObjective();
return Boolean(objective && objectiveNearbyEnemies(objective).some((enemy) => enemy.id !== primaryTargetId));
};
function withUnitAt(unit, tile, callback) {
const original = { x: unit.x, y: unit.y };
@@ -1096,10 +1156,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
function priorityForTarget(enemy) {
const objective = activeSecureObjectives()[0];
const objective = currentSecureObjective();
const objectivePriority = objective && objectiveDistance(enemy, objective) <= objectiveRadius(objective) + 2 ? 1800 : 0;
if (enemy.id === primaryTargetId) {
return 5200 + objectivePriority;
return (shouldDelayPrimaryTarget() ? 800 : 5200) + objectivePriority;
}
if (targetIds.has(enemy.id)) {
return 3400 + objectivePriority;
@@ -1124,7 +1184,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
};
const progressEnemies = enemies.filter(canProgressTo);
const nearestEnemy = progressEnemies.sort((a, b) => distance(unit, a) - distance(unit, b) || priorityForTarget(b) - priorityForTarget(a))[0];
const objective = activeSecureObjectives()[0];
const objective = currentSecureObjective();
if (objective) {
const objectiveEnemies = objectiveNearbyEnemies(objective).filter(canProgressTo);
if (objectiveEnemies.length > 0) {
@@ -1150,8 +1210,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
function attackChoice(unit) {
const delayPrimary = shouldDelayPrimaryTarget();
return liveUnits('enemy')
.filter((enemy) => scene.canAttack(unit, enemy))
.filter((enemy) => !(delayPrimary && enemy.id === primaryTargetId))
.map((enemy) => {
const preview = scene.combatPreview(unit, enemy, 'attack');
const kill = preview.damage >= enemy.hp;
@@ -1264,9 +1326,42 @@ async function playBattleWithoutDebugVictory(page, battle) {
);
}
function protectedAnchorTile(unit, tiles, pressProtected) {
if (!protectedIds.has(unit.id) || pressProtected || liveUnits('enemy').length <= 4) {
return undefined;
}
const allies = liveUnits('ally').filter((ally) => ally.id !== unit.id);
const focus = focusTarget(unit);
const currentFocusDistance = focus ? distance(unit, focus) : 0;
const safeTiles = tiles
.map((tile) => {
const threat = incomingThreat(unit, tile);
const nearestEnemyDistance =
liveUnits('enemy')
.map((enemy) => distance(tile, enemy))
.sort((a, b) => a - b)[0] ?? 99;
const allyDistance =
allies
.map((ally) => distance(tile, ally))
.sort((a, b) => a - b)[0] ?? 99;
const focusDistance = focus ? distance(tile, focus) : currentFocusDistance;
const overextendPenalty = focus && focusDistance < currentFocusDistance - 1 ? (currentFocusDistance - focusDistance) * 260 : 0;
return { tile, threat, nearestEnemyDistance, allyDistance, focusDistance, score: 0 - threat * 16 + nearestEnemyDistance * 38 - allyDistance * 24 - overextendPenalty };
})
.filter((entry) => entry.threat <= Math.floor(unit.hp * 0.24) && entry.nearestEnemyDistance >= 4 && entry.allyDistance <= 5);
return safeTiles.sort((a, b) => b.score - a.score || a.focusDistance - b.focusDistance || (a.tile.cost ?? 0) - (b.tile.cost ?? 0))[0]?.tile;
}
function chooseMove(unit) {
const tiles = [{ x: unit.x, y: unit.y, cost: 0 }, ...scene.reachableTiles(unit)];
const pressProtected = shouldPressProtected(unit);
const anchorTile = protectedAnchorTile(unit, tiles, pressProtected);
if (anchorTile) {
return anchorTile;
}
const attackingTiles = tiles
.map((tile) =>
withUnitAt(unit, tile, () => {