Harden early campaign objective QA
This commit is contained in:
@@ -109,7 +109,20 @@ const earlyCampaignBattles = [
|
||||
no: 6,
|
||||
id: 'sixth-battle-jieqiao-relief',
|
||||
selected: coreBrothers,
|
||||
targets: ['jieqiao-guard-a', 'jieqiao-archer-c', 'jieqiao-leader-qu-yi'],
|
||||
targets: [
|
||||
'jieqiao-skirmisher-a',
|
||||
'jieqiao-infantry-a',
|
||||
'jieqiao-cavalry-a',
|
||||
'jieqiao-skirmisher-b',
|
||||
'jieqiao-archer-a',
|
||||
'jieqiao-archer-b',
|
||||
'jieqiao-cavalry-b',
|
||||
'jieqiao-guard-a',
|
||||
'jieqiao-cavalry-c',
|
||||
'jieqiao-guard-b',
|
||||
'jieqiao-archer-c',
|
||||
'jieqiao-leader-qu-yi'
|
||||
],
|
||||
protected: ['liu-bei']
|
||||
},
|
||||
{
|
||||
@@ -162,10 +175,19 @@ const refugeCampaignBattles = [
|
||||
id: 'thirteenth-battle-xiapi-final',
|
||||
selected: caoRefugeSortie,
|
||||
targets: [
|
||||
'xiapi-final-gate-guard-a',
|
||||
'xiapi-final-gate-guard-b',
|
||||
'xiapi-final-gate-guard-c',
|
||||
'xiapi-final-gate-guard-d',
|
||||
'xiapi-final-archer-a',
|
||||
'xiapi-final-archer-b',
|
||||
'xiapi-final-archer-c',
|
||||
'xiapi-final-cavalry-a',
|
||||
'xiapi-final-cavalry-b',
|
||||
'xiapi-final-cavalry-c',
|
||||
'xiapi-final-cavalry-d',
|
||||
'xiapi-final-raider-a',
|
||||
'xiapi-final-raider-b',
|
||||
'xiapi-final-strategist-chen-gong',
|
||||
'xiapi-final-leader-lu-bu'
|
||||
],
|
||||
@@ -704,6 +726,19 @@ try {
|
||||
}
|
||||
|
||||
const failed = results.filter((result) => result.outcome !== 'victory');
|
||||
const missedObjectives = results.flatMap((result) =>
|
||||
(result.objectiveDetails ?? [])
|
||||
.filter((objective) => !objective.achieved)
|
||||
.map((objective) => ({
|
||||
no: result.no,
|
||||
id: result.id,
|
||||
objectiveId: objective.id,
|
||||
category: objective.category,
|
||||
summary: objective.summary,
|
||||
detail: objective.detail,
|
||||
failureReason: objective.failureReason ?? ''
|
||||
}))
|
||||
);
|
||||
console.table(
|
||||
results.map((result) => ({
|
||||
no: result.no,
|
||||
@@ -761,6 +796,13 @@ try {
|
||||
throw new Error(`Representative battle QA failed: ${failed.map((result) => `${result.no}:${result.id}`).join(', ')}`);
|
||||
}
|
||||
|
||||
if (missedObjectives.length > 0) {
|
||||
console.dir(missedObjectives, { depth: null });
|
||||
throw new Error(
|
||||
`Representative battle QA missed objectives: ${missedObjectives.map((objective) => `${objective.no}:${objective.objectiveId}`).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Representative battle QA passed without debug victory for ${results.length} battles at ${targetUrl}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
@@ -963,9 +1005,9 @@ async function normalizeRepresentativeBattleUnits(page, battle) {
|
||||
}
|
||||
|
||||
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;
|
||||
const hpFloor = no >= 28 ? 86 : no >= 21 ? 82 : no >= 18 ? 76 : no >= 11 ? 68 : no >= 2 ? 56 : 0;
|
||||
const attackFloor = no >= 28 ? 78 : no >= 21 ? 70 : no >= 18 ? 54 : no >= 11 ? 42 : no >= 2 ? 24 : 0;
|
||||
const statFloor = no >= 28 ? 132 : no >= 21 ? 126 : no >= 18 ? 112 : no >= 11 ? 98 : no >= 2 ? 88 : 0;
|
||||
|
||||
if (hpFloor <= 0) {
|
||||
return;
|
||||
@@ -1142,6 +1184,13 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
const objective = currentSecureObjective();
|
||||
return Boolean(objective && objectiveNearbyEnemies(objective).some((enemy) => enemy.id !== primaryTargetId));
|
||||
};
|
||||
const shouldHoldBattleEndingTarget = (enemy, objective = currentSecureObjective()) => {
|
||||
return Boolean(
|
||||
objective &&
|
||||
enemy.id.includes('leader') &&
|
||||
(!isObjectiveHeldByAlly(objective) || objectiveNearbyEnemies(objective).some((nearbyEnemy) => !nearbyEnemy.id.includes('leader')))
|
||||
);
|
||||
};
|
||||
|
||||
function withUnitAt(unit, tile, callback) {
|
||||
const original = { x: unit.x, y: unit.y };
|
||||
@@ -1158,6 +1207,9 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
function priorityForTarget(enemy) {
|
||||
const objective = currentSecureObjective();
|
||||
const objectivePriority = objective && objectiveDistance(enemy, objective) <= objectiveRadius(objective) + 2 ? 1800 : 0;
|
||||
if (shouldHoldBattleEndingTarget(enemy, objective)) {
|
||||
return 420 + objectivePriority;
|
||||
}
|
||||
if (enemy.id === primaryTargetId) {
|
||||
return (shouldDelayPrimaryTarget() ? 800 : 5200) + objectivePriority;
|
||||
}
|
||||
@@ -1189,8 +1241,8 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
const objectiveEnemies = objectiveNearbyEnemies(objective).filter(canProgressTo);
|
||||
if (objectiveEnemies.length > 0) {
|
||||
return objectiveEnemies.sort((a, b) => {
|
||||
const aLeaderPenalty = a.id === primaryTargetId ? 4 : 0;
|
||||
const bLeaderPenalty = b.id === primaryTargetId ? 4 : 0;
|
||||
const aLeaderPenalty = shouldHoldBattleEndingTarget(a, objective) ? 12 : a.id === primaryTargetId ? 4 : 0;
|
||||
const bLeaderPenalty = shouldHoldBattleEndingTarget(b, objective) ? 12 : b.id === primaryTargetId ? 4 : 0;
|
||||
return objectiveDistance(a, objective) + aLeaderPenalty - (objectiveDistance(b, objective) + bLeaderPenalty) || distance(unit, a) - distance(unit, b);
|
||||
})[0];
|
||||
}
|
||||
@@ -1210,10 +1262,26 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
}
|
||||
|
||||
function attackChoice(unit) {
|
||||
const activeObjective = currentSecureObjective();
|
||||
const delayPrimary = shouldDelayPrimaryTarget();
|
||||
return liveUnits('enemy')
|
||||
const delayedObjective = delayPrimary ? activeObjective : undefined;
|
||||
const endingHoldObjective = delayedObjective ?? activeObjective;
|
||||
const candidates = liveUnits('enemy')
|
||||
.filter((enemy) => scene.canAttack(unit, enemy))
|
||||
.filter((enemy) => !(delayPrimary && enemy.id === primaryTargetId))
|
||||
.filter((enemy) => {
|
||||
if (endingHoldObjective && shouldHoldBattleEndingTarget(enemy, endingHoldObjective)) {
|
||||
return false;
|
||||
}
|
||||
if (delayPrimary && delayedObjective) {
|
||||
return objectiveDistance(enemy, delayedObjective) <= objectiveRadius(delayedObjective) + 2;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const preferredCandidates =
|
||||
delayedObjective && !isObjectiveHeldByAlly(delayedObjective)
|
||||
? candidates.filter((enemy) => !shouldHoldBattleEndingTarget(enemy, delayedObjective))
|
||||
: candidates;
|
||||
return (preferredCandidates.length > 0 ? preferredCandidates : candidates)
|
||||
.map((enemy) => {
|
||||
const preview = scene.combatPreview(unit, enemy, 'attack');
|
||||
const kill = preview.damage >= enemy.hp;
|
||||
@@ -1265,7 +1333,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
const vanguards = liveUnits('ally').filter((ally) => ally.id !== unit.id && !protectedIds.has(ally.id));
|
||||
const vanguardInPosition = vanguards.some((ally) => !focus || distance(ally, focus) <= 10);
|
||||
if (vanguards.length === 0) {
|
||||
return unit.hp >= Math.ceil(unit.maxHp * 0.8);
|
||||
return unit.hp >= Math.ceil(unit.maxHp * 0.45);
|
||||
}
|
||||
const targetPressThreshold = protectedIds.size > 1 ? 0.12 : 0.3;
|
||||
return Boolean(
|
||||
@@ -1277,6 +1345,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
);
|
||||
}
|
||||
|
||||
const isLastProtectedFighter = (unit) => {
|
||||
return protectedIds.has(unit.id) && liveUnits('ally').every((ally) => protectedIds.has(ally.id));
|
||||
};
|
||||
|
||||
function canTakeAttack(unit, attack, threat = protectedIds.has(unit.id) ? incomingThreat(unit, unit) : 0) {
|
||||
if (!protectedIds.has(unit.id)) {
|
||||
const kill = attack.preview.damage >= attack.target.hp;
|
||||
@@ -1295,6 +1367,16 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
const pressProtected = shouldPressProtected(unit);
|
||||
const isTarget = targetIds.has(attack.target.id);
|
||||
const kill = attack.preview.damage >= attack.target.hp;
|
||||
if (isLastProtectedFighter(unit)) {
|
||||
const hpRate = unit.hp / Math.max(1, unit.maxHp);
|
||||
if (isTarget && hpRate >= 0.45) {
|
||||
return true;
|
||||
}
|
||||
if (kill && hpRate >= 0.35) {
|
||||
return true;
|
||||
}
|
||||
return threat <= Math.floor(unit.hp * 1.05);
|
||||
}
|
||||
if (protectedIds.size > 1 && !isTarget && unit.id === 'sun-qian') {
|
||||
return kill && liveUnits('enemy').length <= 3 && threat <= Math.floor(unit.hp * 0.12);
|
||||
}
|
||||
@@ -1315,6 +1397,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
|
||||
function canPressProtectedTile(unit, tile, focus, currentDistance) {
|
||||
const threat = incomingThreat(unit, tile);
|
||||
if (isLastProtectedFighter(unit)) {
|
||||
const hpRate = unit.hp / Math.max(1, unit.maxHp);
|
||||
return distance(tile, focus) < currentDistance && (hpRate >= 0.5 || threat <= Math.floor(unit.hp * 1.1));
|
||||
}
|
||||
if (threat <= Math.floor(unit.hp * 0.55)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1362,6 +1448,18 @@ async function playBattleWithoutDebugVictory(page, battle) {
|
||||
return anchorTile;
|
||||
}
|
||||
|
||||
if (isLastProtectedFighter(unit)) {
|
||||
const nearestEnemy = liveUnits('enemy').sort((a, b) => distance(unit, a) - distance(unit, b))[0];
|
||||
const aggressiveTile = nearestEnemy
|
||||
? tiles
|
||||
.filter((tile) => tile.x !== unit.x || tile.y !== unit.y)
|
||||
.sort((a, b) => distance(a, nearestEnemy) - distance(b, nearestEnemy) || (a.cost ?? 0) - (b.cost ?? 0))[0]
|
||||
: undefined;
|
||||
if (aggressiveTile && (!nearestEnemy || distance(aggressiveTile, nearestEnemy) <= distance(unit, nearestEnemy) + 2)) {
|
||||
return aggressiveTile;
|
||||
}
|
||||
}
|
||||
|
||||
const attackingTiles = tiles
|
||||
.map((tile) =>
|
||||
withUnitAt(unit, tile, () => {
|
||||
|
||||
Reference in New Issue
Block a user