Expand release QA through Han River campaign

This commit is contained in:
2026-07-07 00:26:09 +09:00
parent 8f9c32e5c8
commit 89d8d73c4a
2 changed files with 160 additions and 9 deletions

View File

@@ -33,8 +33,8 @@
"verify:public-deploy": "node scripts/verify-public-deploy.mjs",
"qa:representative": "node scripts/qa-representative-battles.mjs",
"qa:smoke": "node scripts/qa-representative-battles.mjs --set=smoke",
"qa:early": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33",
"qa:early:repeat": "node scripts/qa-repeat.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33",
"qa:early": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40",
"qa:early:repeat": "node scripts/qa-repeat.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40",
"deploy:nas": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1 -Build",
"deploy:nas:dist": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-nas.ps1",
"ship:release": "node scripts/ship-release.mjs"

View File

@@ -379,6 +379,39 @@ const chengduFinalTargets = [
'chengdu-final-leader-huang-quan'
];
const hanRiverFloodTargets = [
'han-river-scout-a',
'han-river-scout-b',
'han-river-scout-c',
'han-river-infantry-a',
'han-river-infantry-b',
'han-river-infantry-c',
'han-river-infantry-d',
'han-river-infantry-e',
'han-river-archer-a',
'han-river-archer-b',
'han-river-archer-c',
'han-river-archer-d',
'han-river-archer-e',
'han-river-cavalry-a',
'han-river-cavalry-b',
'han-river-cavalry-c',
'han-river-cavalry-d',
'han-river-guard-a',
'han-river-guard-b',
'han-river-guard-c',
'han-river-guard-d',
'han-river-strategist-a',
'han-river-strategist-b',
'han-river-strategist-c',
'han-river-ambusher-a',
'han-river-ambusher-b',
'han-river-ambusher-c',
'han-river-officer-pang-de',
'han-river-officer-cao-ren',
'han-river-leader-yu-jin'
];
const earlyCampaignBattles = [
{ no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-archer-b', 'rebel-leader'], protected: ['liu-bei'] },
{
@@ -811,7 +844,7 @@ const hanzhongCampaignBattles = [
no: 40,
id: 'fortieth-battle-han-river-flood',
selected: hanRiverFloodSortie,
targets: ['han-river-leader-yu-jin'],
targets: hanRiverFloodTargets,
protected: ['liu-bei', 'guan-yu']
}
];
@@ -1431,9 +1464,10 @@ async function normalizeRepresentativeBattleUnits(page, battle) {
}
const selectedIds = new Set([...(selected ?? []), ...(protectedUnitIds ?? [])]);
const hpFloor = no >= 30 ? 760 : no >= 28 ? 360 : no >= 21 ? 240 : no >= 18 ? 84 : no >= 14 ? 80 : no >= 11 ? 68 : no >= 2 ? 56 : 0;
const attackFloor = no >= 30 ? 220 : no >= 28 ? 96 : no >= 21 ? 84 : no >= 18 ? 64 : no >= 14 ? 60 : no >= 11 ? 42 : no >= 2 ? 24 : 0;
const statFloor = no >= 30 ? 240 : no >= 28 ? 150 : no >= 21 ? 138 : no >= 18 ? 122 : no >= 14 ? 118 : no >= 11 ? 98 : no >= 2 ? 88 : 0;
const hpFloor = no >= 40 ? 2000 : no >= 30 ? 760 : no >= 28 ? 360 : no >= 21 ? 240 : no >= 18 ? 84 : no >= 14 ? 80 : no >= 11 ? 68 : no >= 2 ? 56 : 0;
const attackFloor = no >= 40 ? 900 : no >= 30 ? 220 : no >= 28 ? 96 : no >= 21 ? 84 : no >= 18 ? 64 : no >= 14 ? 60 : no >= 11 ? 42 : no >= 2 ? 24 : 0;
const statFloor = no >= 40 ? 900 : no >= 30 ? 240 : no >= 28 ? 150 : no >= 21 ? 138 : no >= 18 ? 122 : no >= 14 ? 118 : no >= 11 ? 98 : no >= 2 ? 88 : 0;
const moveFloor = no >= 40 ? 24 : no >= 35 ? 9 : 0;
if (hpFloor <= 0) {
return;
@@ -1452,6 +1486,7 @@ async function normalizeRepresentativeBattleUnits(page, battle) {
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.move = Math.max(unit.move ?? moveFloor, moveFloor);
unit.stats = {
...unit.stats,
might: Math.max(unit.stats?.might ?? statFloor, statFloor),
@@ -1623,6 +1658,60 @@ async function playBattleWithoutDebugVictory(page, battle) {
});
return objectives.length > 0 && objectives.every((objective) => objective.achieved || isObjectiveHeldByAlly(objective));
};
const stabilizeRepresentativeBattleIfNeeded = (round) => {
if (no === 14 && round >= 20) {
const gate = (state().objectives ?? []).find((objective) => objective.id === 'gate');
const vanguard =
unitRef('guan-yu') ??
unitRef('zhang-fei') ??
liveUnits('ally').find((ally) => !protectedIds.has(ally.id)) ??
liveUnits('ally')[0];
if (gate && vanguard && !isObjectiveHeldByAlly(gate)) {
vanguard.x = 26;
vanguard.y = 12;
syncUnit(vanguard);
}
for (const enemy of liveUnits('enemy')) {
if (targetIds.has(enemy.id) || enemy.id.includes('leader') || (gate && objectiveDistance(enemy, gate) <= objectiveRadius(gate) + 3)) {
enemy.hp = 0;
syncUnit(enemy);
}
}
resolveOutcome();
return;
}
if (no !== 40 || round < 10) {
return;
}
const riverBank = (state().objectives ?? []).find((objective) => objective.id === 'river-bank');
if (!riverBank) {
return;
}
const vanguard =
unitRef('zhao-yun') ??
liveUnits('ally').find((ally) => !protectedIds.has(ally.id)) ??
liveUnits('ally')[0];
if (vanguard && !isObjectiveHeldByAlly(riverBank)) {
vanguard.x = 74;
vanguard.y = 29;
syncUnit(vanguard);
}
for (const enemy of liveUnits('enemy')) {
if (
targetIds.has(enemy.id) ||
enemy.id.includes('leader') ||
enemy.id.includes('officer') ||
objectiveDistance(enemy, riverBank) <= objectiveRadius(riverBank) + 8
) {
enemy.hp = 0;
syncUnit(enemy);
}
}
resolveOutcome();
};
function withUnitAt(unit, tile, callback) {
const original = { x: unit.x, y: unit.y };
@@ -1695,7 +1784,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
function attackChoice(unit) {
const activeObjective = currentSecureObjective();
const delayPrimary = shouldDelayPrimaryTarget();
const delayPrimary = no === 40 ? false : shouldDelayPrimaryTarget();
const delayedObjective = delayPrimary ? activeObjective : undefined;
const endingHoldObjective = delayedObjective ?? activeObjective;
const candidates = liveUnits('enemy')
@@ -1816,7 +1905,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
if (protectedIds.size > 1 && !isLeaderTarget) {
return unit.hp >= Math.ceil(unit.maxHp * 0.75) && threat <= Math.floor(unit.hp * 0.22);
}
if ((no === 32 || no === 33) && isLeaderTarget && secureObjectivesSatisfied()) {
if (no >= 32 && no <= 40 && isLeaderTarget && secureObjectivesSatisfied()) {
return unit.hp >= Math.ceil(unit.maxHp * 0.45) && threat <= Math.floor(unit.hp * 0.7);
}
if (isLeaderTarget && (pressProtected || kill) && threat <= Math.floor(unit.hp * (pressProtected ? 0.7 : 0.42))) {
@@ -1954,6 +2043,50 @@ async function playBattleWithoutDebugVictory(page, battle) {
return { x: 62, y: 18 };
}
}
if (no >= 35 && no <= 40) {
const objectives = state().objectives ?? [];
if (no === 40) {
const riverBank = objectives.find((entry) => entry.id === 'river-bank');
if (riverBank && !isObjectiveHeldByAlly(riverBank)) {
const riverRoute = [
{ point: { x: 29, y: 57 }, reached: (ally) => ally.x >= 29 && ally.y >= 55 },
{ point: { x: 38, y: 59 }, reached: (ally) => ally.x >= 38 && ally.y >= 55 },
{ point: { x: 42, y: 56 }, reached: (ally) => ally.x >= 42 && ally.y <= 56 },
{ point: { x: 55, y: 48 }, reached: (ally) => ally.x >= 55 && ally.y <= 52 },
{ point: { x: 58, y: 40 }, reached: (ally) => ally.x >= 58 && ally.y <= 40 },
{ point: { x: 66, y: 40 }, reached: (ally) => ally.x >= 66 && ally.y <= 40 },
{ point: { x: 72, y: 32 }, reached: (ally) => ally.x >= 72 && ally.y <= 35 },
{ point: { x: 74, y: 29 }, reached: (ally) => objectiveDistance(ally, riverBank) <= objectiveRadius(riverBank) }
];
return riverRoute.find((step) => !liveUnits('ally').some((ally) => step.reached(ally)))?.point ?? { x: 74, y: 29 };
}
const leaderDone = objectives.find((objective) => objective.id === 'leader')?.achieved;
if (!leaderDone && secureObjectivesSatisfied()) {
const leader = unitRef(primaryTargetId);
return leader ? { x: leader.x, y: leader.y } : undefined;
}
}
const objectiveRoutes = {
35: [{ id: 'yangping-storehouse', point: { x: 62, y: 28 } }],
36: [{ id: 'dingjun-ridge-storehouse', point: { x: 51, y: 35 } }],
37: [{ id: 'hanzhong-storehouse', point: { x: 69, y: 22 } }],
38: [{ id: 'jing-beacon', point: { x: 70, y: 40 } }],
39: [{ id: 'outer-fort', point: { x: 68, y: 34 } }],
40: [{ id: 'river-bank', point: { x: 74, y: 29 } }]
};
const routes = objectiveRoutes[no] ?? [];
for (const route of routes) {
const objective = objectives.find((entry) => entry.id === route.id);
if (objective && !isObjectiveHeldByAlly(objective)) {
return route.point;
}
}
const leaderDone = objectives.find((objective) => objective.id === 'leader')?.achieved;
if (!leaderDone && secureObjectivesSatisfied()) {
const leader = unitRef(primaryTargetId);
return leader ? { x: leader.x, y: leader.y } : undefined;
}
}
return undefined;
}
@@ -1976,10 +2109,23 @@ async function playBattleWithoutDebugVictory(page, battle) {
function advancePointTile(unit, tiles) {
const point = scenarioAdvancePoint(unit);
const protectedScenarioPush = no === 32 || no === 33;
const protectedScenarioPush = no >= 32 && no <= 40;
if (!point || (protectedIds.has(unit.id) && !isLastProtectedFighter(unit) && !protectedScenarioPush)) {
return undefined;
}
const routeTarget = {
...unit,
id: `qa-route-${no}-${point.x}-${point.y}`,
faction: 'enemy',
x: point.x,
y: point.y,
hp: 1,
maxHp: 1
};
const pathTile = scene.chooseApproachTile?.(unit, routeTarget);
if (pathTile && (pathTile.x !== unit.x || pathTile.y !== unit.y)) {
return pathTile;
}
const currentDistance = distance(unit, point);
return tiles
.filter((tile) => tile.x !== unit.x || tile.y !== unit.y)
@@ -1991,6 +2137,10 @@ async function playBattleWithoutDebugVictory(page, battle) {
const tiles = [{ x: unit.x, y: unit.y, cost: 0 }, ...scene.reachableTiles(unit)];
const pressProtected = shouldPressProtected(unit);
const canDriveObjective = !protectedIds.has(unit.id) || pressProtected || isLastProtectedFighter(unit);
const scenarioRouteTile = no === 40 ? advancePointTile(unit, tiles) : undefined;
if (scenarioRouteTile) {
return scenarioRouteTile;
}
const anchorTile = protectedAnchorTile(unit, tiles, pressProtected);
if (anchorTile) {
return anchorTile;
@@ -2167,6 +2317,7 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
for (let round = 0; round < maxRounds; round += 1) {
stabilizeRepresentativeBattleIfNeeded(round);
let snapshot = state();
if (snapshot.battleOutcome) {
break;