Refine Xiaopei supply road sortie

This commit is contained in:
2026-07-04 14:58:48 +09:00
parent 836b53586e
commit 62e2c58608
5 changed files with 100 additions and 17 deletions

View File

@@ -3,10 +3,10 @@ import { existsSync } from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { chromium } from 'playwright'; import { chromium } from 'playwright';
const targetUrl = process.env.QA_URL ?? 'http://localhost:5173/'; const defaultQaPort = process.env.QA_PORT ?? '41737';
const targetUrl = process.env.QA_URL ?? `http://127.0.0.1:${defaultQaPort}/`;
const headless = process.env.QA_HEADLESS !== '0'; const headless = process.env.QA_HEADLESS !== '0';
const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80); const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80);
const pnpmCommand = process.env.PNPM_CMD ?? 'pnpm';
const cumulativeMode = process.env.QA_CUMULATIVE === '1'; const cumulativeMode = process.env.QA_CUMULATIVE === '1';
const campaignStorageKey = 'heros-web:campaign-state'; const campaignStorageKey = 'heros-web:campaign-state';
const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1'; const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1';
@@ -1478,9 +1478,13 @@ async function ensureLocalServer(url) {
return undefined; return undefined;
} }
const child = spawn(pnpmCommand, ['dev'], { const viteArgs = localViteServerArgs(url);
if (!viteArgs) {
throw new Error(`QA target is not reachable and cannot be started locally: ${url}`);
}
const child = spawn(process.execPath, viteArgs, {
cwd: process.cwd(), cwd: process.cwd(),
shell: true,
stdio: 'ignore', stdio: 'ignore',
windowsHide: true windowsHide: true
}); });
@@ -1496,6 +1500,28 @@ async function ensureLocalServer(url) {
throw new Error(`Could not start dev server at ${url}`); throw new Error(`Could not start dev server at ${url}`);
} }
function localViteServerArgs(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
return undefined;
}
const localHosts = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]', '::1']);
if (!localHosts.has(parsed.hostname) || !parsed.port) {
return undefined;
}
const viteCliPath = 'node_modules/vite/bin/vite.js';
if (!existsSync(viteCliPath)) {
return undefined;
}
const host = parsed.hostname === 'localhost' || parsed.hostname === '0.0.0.0' ? '127.0.0.1' : parsed.hostname;
return [viteCliPath, '--host', host, '--port', parsed.port, '--strictPort'];
}
async function isServerReachable(url) { async function isServerReachable(url) {
try { try {
const response = await fetch(url); const response = await fetch(url);

View File

@@ -1091,10 +1091,55 @@ export const eighthBattleScenario: BattleScenarioDefinition = {
victoryConditionLabel: '고순 격파', victoryConditionLabel: '고순 격파',
defeatConditionLabel: '유비 퇴각', defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [ openingObjectiveLines: [
'여포군의 척후와 습격병이 소패 보급로를 흔들고 있습니다. 고순을 격파하면 서주의 첫 방위선을 지킬 수 있습니다.', '여포군의 척후와 습격병이 소패 보급로의 창고를 노리고 있습니다. 고순을 격파하면 서주의 첫 방위선을 지킬 수 있습니다.',
'기병은 길을 타고 빠르게 파고들고, 궁병은 강 건너와 언덕에서 사거리를 잡습니다. 마을과 창고 지형을 이용해 버티십시오.', '간옹은 병법서와 책략으로 길목 압박을 늦추고, 미축은 창고 주변 보급과 회복을 맡기 좋습니다. 마을과 창고 지형을 중심으로 전열을 잡으십시오.',
'18턴 이내 승리하면 서주 보급망과 새 합류 무장의 명성이 크게 오릅니다.' '18턴 이내 승리하면 서주 보급망과 새 합류 무장의 명성이 크게 오릅니다.'
], ],
tacticalGuide: {
summary: '중앙 창고 마을을 먼저 안정시키고, 관우가 길목을 버티는 동안 간옹과 미축이 후방에서 회복과 책략 보조를 이어갑니다.',
route: '아군 진영 → 중앙 창고 마을 → 동쪽 길목 → 고순 본대',
focus: '창고 습격병을 먼저 정리해 미축의 보급 위치를 확보하고, 간옹은 화계와 격려로 기병의 접근을 늦추십시오.',
roles: ['관우: 보급로 전열', '장비: 남쪽 길 압박', '간옹: 책략/격려 보조', '미축: 창고 보급/회복', '유비: 후방 지휘'],
events: {
'village-approach': {
title: '창고 마을 접근',
lines: ['소패 창고 주변에 습격병이 붙어 있습니다.', '미축을 후방에 두고 간옹의 책략으로 길목 적을 약화시키면 보급선을 안정시키기 쉽습니다.']
},
'enemy-posture': {
title: '보급로 압박',
lines: ['여포군 기병은 동쪽 길을 타고 빠르게 들어오고, 궁병은 언덕에서 전열을 끊으려 합니다.', '관우는 길목을 잡고 장비는 남쪽 압박, 간옹과 미축은 뒤에서 회복과 책략을 이어가십시오.']
},
'cavalry-approach': {
title: '기병 돌입',
lines: ['여포군 기병이 보급로 안쪽으로 파고듭니다.', '장비로 측면을 끊고 간옹의 격려 또는 화계로 돌파 속도를 늦추십시오.']
}
}
},
sortie: {
sortieLimit: 5,
requiredUnits: ['liu-bei'],
recommendedUnits: [
{ unitId: 'guan-yu', role: 'front', reason: '보급로 앞 길목을 버티며 고순 본대와 기병 압박을 받아냅니다.' },
{ unitId: 'jian-yong', role: 'support', reason: '병법서와 잘 맞는 책략 보조로 화계, 응급, 격려를 모두 활용할 수 있습니다.' },
{ unitId: 'mi-zhu', role: 'reserve', reason: '군량 주머니와 회복품 운용으로 창고 주변 장기전을 안정시킵니다.' },
{ unitId: 'zhang-fei', role: 'flank', reason: '남쪽 길로 들어오는 기병과 습격병을 빠르게 압박합니다.' },
{ unitId: 'liu-bei', role: 'support', reason: '후방에서 생존을 지키며 새 합류 무장과 전열의 균형을 잡습니다.' }
],
recommendedClasses: [
{ label: '보급로 전열', classKeys: ['infantry'], reason: '마을과 길목 방어 보정을 살려 기병과 보병의 첫 압박을 받아냅니다.' },
{ label: '책략 보조', classKeys: ['strategist'], reason: '화계와 격려로 원거리 궁병과 기병 돌입을 늦추는 역할이 중요합니다.' },
{ label: '창고 보급', classKeys: ['quartermaster'], reason: '회복품과 마을 보정을 묶어 보급로 장기전을 안정시킵니다.' },
{ label: '측면 압박', classKeys: ['spearman'], reason: '남쪽 길의 기병 진입과 창고 습격병을 끊어냅니다.' }
],
deploymentSlots: [
{ role: 'front', unitId: 'guan-yu', x: 3, y: 17, label: '보급로 전열' },
{ role: 'support', unitId: 'jian-yong', x: 3, y: 18, label: '책략 보조' },
{ role: 'reserve', unitId: 'mi-zhu', x: 1, y: 18, label: '창고 보급' },
{ role: 'flank', unitId: 'zhang-fei', x: 2, y: 18, label: '남쪽 압박' },
{ role: 'support', unitId: 'liu-bei', x: 2, y: 17, label: '후방 지휘' }
],
note: '소패 보급로는 새로 합류한 간옹과 미축을 시험하기 좋은 전장입니다. 관우가 길목을 막고, 간옹은 책략, 미축은 보급을 맡는 전열이 기본입니다.'
},
map: eighthBattleMap, map: eighthBattleMap,
units: eighthBattleUnits, units: eighthBattleUnits,
bonds: eighthBattleBonds, bonds: eighthBattleBonds,
@@ -1122,7 +1167,9 @@ export const eighthBattleScenario: BattleScenarioDefinition = {
kind: 'secure-terrain', kind: 'secure-terrain',
label: '소패 보급촌 확보', label: '소패 보급촌 확보',
rewardGold: 360, rewardGold: 360,
terrain: 'village' terrain: 'village',
targetTile: { x: 12, y: 10, radius: 5 },
threatRadius: 0
}, },
{ {
id: 'quick', id: 'quick',
@@ -1134,6 +1181,14 @@ export const eighthBattleScenario: BattleScenarioDefinition = {
], ],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 3', '상처약 2', '탁주 1', '서주 보급망 +1'], itemRewards: ['콩 3', '상처약 2', '탁주 1', '서주 보급망 +1'],
campaignReward: {
supplies: ['콩 3', '상처약 2', '탁주 1'],
equipment: ['군량 주머니 1'],
reputation: ['서주 보급망 +1'],
unlockBattleId: 'ninth-battle-xuzhou-gate-night-raid',
unlockLabel: '서주 성문 야습 개방',
note: '소패 보급로를 지켜 미축의 보급 운용과 간옹의 책략 지원이 서주 방위 편성에 자리 잡습니다.'
},
victoryPages: eighthBattleVictoryPages, victoryPages: eighthBattleVictoryPages,
nextCampScene: 'CampScene' nextCampScene: 'CampScene'
}; };

View File

@@ -282,7 +282,7 @@ const sortieFlows: Record<string, SortieFlow> = {
afterBattleId: seventhBattleScenario.id, afterBattleId: seventhBattleScenario.id,
eyebrow: '다음 전장', eyebrow: '다음 전장',
title: eighthBattleScenario.title, title: eighthBattleScenario.title,
description: '도겸에게 서주를 맡은 뒤 첫 과제는 소패 보급로를 지키는 일입니다. 여포군의 그림자가 드리우기 전에 새로 합류한 장수들과 방위선워야 합니다.', description: '도겸에게 서주를 맡은 뒤 첫 과제는 소패 보급로를 지키는 일입니다. 간옹은 전황 판단을, 미축은 창고 보급을 맡아 새 방위선의 빈틈워야 합니다.',
rewardHint: `예상 보상: ${eighthBattleScenario.title} 개방`, rewardHint: `예상 보상: ${eighthBattleScenario.title} 개방`,
nextBattleId: eighthBattleScenario.id, nextBattleId: eighthBattleScenario.id,
campaignStep: 'eighth-battle', campaignStep: 'eighth-battle',
@@ -292,7 +292,7 @@ const sortieFlows: Record<string, SortieFlow> = {
afterBattleId: eighthBattleScenario.id, afterBattleId: eighthBattleScenario.id,
eyebrow: '다음 전장', eyebrow: '다음 전장',
title: ninthBattleScenario.title, title: ninthBattleScenario.title,
description: '여포를 의탁시킨 뒤 서주 성문 안팎의 불안 터집니다. 조표의 야습을 막아 성문을 지켜내야 합니다.', description: '소패 보급로를 지켰지만 서주 성문 안팎의 불안은 곧 터집니다. 보급을 정돈한 미축과 책략 보조를 익힌 간옹을 다음 방위 편성에 넣을지 판단해야 합니다.',
rewardHint: `예상 보상: ${ninthBattleScenario.title} 개방`, rewardHint: `예상 보상: ${ninthBattleScenario.title} 개방`,
nextBattleId: ninthBattleScenario.id, nextBattleId: ninthBattleScenario.id,
campaignStep: 'ninth-battle', campaignStep: 'ninth-battle',

View File

@@ -1731,14 +1731,14 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'xuzhou-guard-b': 'guard', 'xuzhou-guard-b': 'guard',
'xuzhou-archer-c': 'hold', 'xuzhou-archer-c': 'hold',
'xuzhou-leader-xiahou-dun': 'guard', 'xuzhou-leader-xiahou-dun': 'guard',
'xiaopei-raider-a': 'aggressive', 'xiaopei-raider-a': 'guard',
'xiaopei-raider-b': 'aggressive', 'xiaopei-raider-b': 'guard',
'xiaopei-infantry-a': 'guard', 'xiaopei-infantry-a': 'guard',
'xiaopei-infantry-b': 'guard', 'xiaopei-infantry-b': 'guard',
'xiaopei-archer-a': 'hold', 'xiaopei-archer-a': 'hold',
'xiaopei-archer-b': 'hold', 'xiaopei-archer-b': 'hold',
'xiaopei-guard-a': 'aggressive', 'xiaopei-guard-a': 'guard',
'xiaopei-guard-b': 'aggressive', 'xiaopei-guard-b': 'guard',
'xiaopei-cavalry-a': 'aggressive', 'xiaopei-cavalry-a': 'aggressive',
'xiaopei-cavalry-b': 'aggressive', 'xiaopei-cavalry-b': 'aggressive',
'xiaopei-leader-gao-shun': 'guard', 'xiaopei-leader-gao-shun': 'guard',

View File

@@ -651,11 +651,13 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
'eighth-battle-xiaopei-supply-road': { 'eighth-battle-xiaopei-supply-road': {
maxUnits: 5, maxUnits: 5,
recommended: [ recommended: [
{ unitId: 'liu-bei', reason: '서주 방위선의 사기를 붙잡습니다.' }, { unitId: 'guan-yu', reason: '보급로 앞 길목에서 고순 본대와 기병 압박을 받아냅니다.' },
{ unitId: 'jian-yong', reason: '보급로 전투에서 책략 보조가 빛납니다.' }, { unitId: 'jian-yong', reason: '병법서와 책략 보조가 잘 맞아 화계, 응급, 격려를 모두 활용하기 좋습니다.' },
{ unitId: 'mi-zhu', reason: '소패 보급과 회복 운용에 어울립니다.' } { unitId: 'mi-zhu', reason: '창고 주변 보급과 회복 운용으로 장기전을 안정시킵니다.' },
{ unitId: 'zhang-fei', reason: '남쪽 길의 습격병과 기병을 빠르게 끊어냅니다.' },
{ unitId: 'liu-bei', reason: '후방에서 새 합류 무장과 전열의 균형을 잡습니다.' }
], ],
note: '새로 합류한 서주 인재를 시험하기 좋은 전장입니다. 전투력, 책략, 보급 추천 사유를 비교해 다섯 명을 채워 보십시오.' note: '새로 합류한 간옹과 미축을 바로 시험하기 좋은 전장입니다. 간옹은 병법서와 책략, 미축은 보급과 회복 역할로 비교해 편성하십시오.'
}, },
'ninth-battle-xuzhou-gate-night-raid': { 'ninth-battle-xuzhou-gate-night-raid': {
maxUnits: 5, maxUnits: 5,