diff --git a/docs/roadmap.md b/docs/roadmap.md index e98bc61..890ec87 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,12 +31,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province - Tenth battle Xuzhou breakout, covering Zhang Fei's mistake, the loss of Xu Province to Lu Bu, and the retreat toward Cao Cao's shadow - Eleventh battle Xudu refuge road, opening Liu Bei's uneasy service under Cao Cao through a Yuan Shu remnant test battle +- Twelfth battle Xiapi outer siege, putting Liu Bei under Cao Cao's command against Lu Bu while preserving Liu Bei's independent motive and camp tension - Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, and readiness advice for each deployable officer -- Flow verification script from title through the eleventh battle victory, recruit sortie selection, and camp save state +- Flow verification script from title through the twelfth battle victory, recruit sortie selection, and camp save state ## Next Steps -1. Continue into Liu Bei's tense service under Cao Cao, then build toward the break from Cao Cao +1. Continue the Xiapi siege toward Lu Bu's fall, then build toward Liu Bei's break from Cao Cao 2. Add a chapter/progress view so long campaign arcs are easier to read between battles 3. Apply treasure equipment effects to damage, defense, recovery, and command rules 4. Add more recruitable officers as the campaign moves toward Cao Cao, Yuan Shao, Liu Biao, and Zhuge Liang diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 95df283..fda780c 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1179,6 +1179,84 @@ try { throw new Error(`Expected eleventh camp to expose Cao Cao refuge dialogue set and preserve full roster: ${JSON.stringify(eleventhCampState)}`); } + await page.mouse.click(1120, 38); + await page.waitForTimeout(160); + const eleventhCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!eleventhCampSortieState?.sortieVisible || !eleventhCampSortieState.sortiePlan?.objectiveLine?.includes('하비 외곽전')) { + throw new Error(`Expected eleventh camp sortie prep to target Xiapi outer siege: ${JSON.stringify(eleventhCampSortieState)}`); + } + assertSortieTacticalRoster(eleventhCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']); + await page.mouse.click(1068, 646); + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('StoryScene'); + }); + await page.screenshot({ path: 'dist/verification-xiapi-siege-story.png', fullPage: true }); + + for (let i = 0; i < 18; i += 1) { + const enteredTwelfthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege'; + }); + if (enteredTwelfthBattle) { + break; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(320); + } + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twelfth-battle.png', fullPage: true }); + + const twelfthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twelfthEnemies = twelfthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twelfthAllies = twelfthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twelfthEnemyBehaviors = new Set(twelfthEnemies.map((unit) => unit.ai)); + if ( + twelfthBattleState.camera?.mapWidth !== 30 || + twelfthBattleState.camera?.mapHeight !== 24 || + twelfthBattleState.victoryConditionLabel !== '장료 격파' || + twelfthEnemies.length < 13 || + !twelfthEnemyBehaviors.has('aggressive') || + !twelfthEnemyBehaviors.has('guard') || + !twelfthEnemyBehaviors.has('hold') || + !twelfthEnemies.some((unit) => unit.id === 'xiapi-strategist-chen-gong') || + !twelfthAllies.some((unit) => unit.id === 'liu-bei') || + !twelfthAllies.some((unit) => unit.id === 'guan-yu') || + !twelfthAllies.some((unit) => unit.id === 'zhang-fei') || + !twelfthAllies.some((unit) => unit.id === 'jian-yong') || + !twelfthAllies.some((unit) => unit.id === 'mi-zhu') + ) { + throw new Error(`Expected twelfth battle to use Xiapi siege map, mixed AI, and selected full sortie: ${JSON.stringify(twelfthBattleState)}`); + } + + await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory')); + await page.waitForFunction(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.battleOutcome === 'victory' && state?.phase === 'resolved' && state?.resultVisible === true; + }); + + await page.mouse.click(738, 642); + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene'); + }); + + const twelfthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twelfthCampState?.campBattleId !== 'twelfth-battle-xiapi-outer-siege' || + twelfthCampState.campTitle !== '하비 포위 군영' || + twelfthCampState.availableDialogueIds?.length !== 3 || + !twelfthCampState.availableDialogueIds.every((id) => id.endsWith('xiapi-outer')) || + !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || + !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || + !twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') + ) { + throw new Error(`Expected twelfth camp to expose Xiapi siege dialogue set and preserve full roster: ${JSON.stringify(twelfthCampState)}`); + } + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -1190,7 +1268,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-eleventh-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twelfth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twelfth-battle-map.svg b/src/assets/images/battle/twelfth-battle-map.svg new file mode 100644 index 0000000..2a4543a --- /dev/null +++ b/src/assets/images/battle/twelfth-battle-map.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 54fd956..3f3cd8d 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -19,6 +19,10 @@ import { tenthBattleMap, tenthBattleUnits, tenthBattleVictoryPages, + twelfthBattleBonds, + twelfthBattleMap, + twelfthBattleUnits, + twelfthBattleVictoryPages, fifthBattleBonds, fifthBattleMap, fifthBattleUnits, @@ -60,7 +64,8 @@ export type BattleScenarioId = | 'eighth-battle-xiaopei-supply-road' | 'ninth-battle-xuzhou-gate-night-raid' | 'tenth-battle-xuzhou-breakout' - | 'eleventh-battle-xudu-refuge-road'; + | 'eleventh-battle-xudu-refuge-road' + | 'twelfth-battle-xiapi-outer-siege'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -682,6 +687,59 @@ export const eleventhBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twelfthBattleScenario: BattleScenarioDefinition = { + id: 'twelfth-battle-xiapi-outer-siege', + title: '하비 외곽전', + victoryConditionLabel: '장료 격파', + defeatConditionLabel: '유비 퇴각', + openingObjectiveLines: [ + '조조는 하비 포위망의 남쪽 둑길을 유비군에게 맡깁니다. 장료를 격파하면 여포군 선봉은 성 안으로 물러납니다.', + '강줄기와 둑길이 전장을 나눕니다. 여포군 기병은 길을 타고 돌파하고, 성루 궁병과 진궁은 후방에서 압박합니다.', + '둑길 진영을 확보하고 20턴 안에 승리하면 조조 휘하에서의 공적과 유비군의 독자 보급을 함께 지킬 수 있습니다.' + ], + map: twelfthBattleMap, + units: twelfthBattleUnits, + bonds: twelfthBattleBonds, + mapTextureKey: 'battle-map-twelfth', + leaderUnitId: 'xiapi-leader-zhang-liao', + quickVictoryTurnLimit: 20, + baseVictoryGold: 1440, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '장료 격파', + rewardGold: 900, + unitId: 'xiapi-leader-zhang-liao' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 340, + unitId: 'liu-bei' + }, + { + id: 'camp', + kind: 'secure-terrain', + label: '하비 둑길 진영 확보', + rewardGold: 500, + terrain: 'camp' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '20턴 이내 승리', + rewardGold: 390, + maxTurn: 20 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], + itemRewards: ['콩 4', '상처약 3', '탁주 2', '하비 포위 공적 +1'], + victoryPages: twelfthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -695,7 +753,8 @@ export const battleScenarios: Record 'eighth-battle-xiaopei-supply-road': eighthBattleScenario, 'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario, 'tenth-battle-xuzhou-breakout': tenthBattleScenario, - 'eleventh-battle-xudu-refuge-road': eleventhBattleScenario + 'eleventh-battle-xudu-refuge-road': eleventhBattleScenario, + 'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 2b87f0c..79443b9 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -10,6 +10,7 @@ import { seventhBattleScenario, sixthBattleScenario, tenthBattleScenario, + twelfthBattleScenario, thirdBattleScenario, type BattleScenarioId } from './battles'; @@ -33,6 +34,8 @@ import { sixthBattleVictoryPages, tenthBattleIntroPages, tenthBattleVictoryPages, + twelfthBattleIntroPages, + twelfthBattleVictoryPages, thirdBattleIntroPages, thirdBattleVictoryPages, type StoryPage @@ -153,12 +156,22 @@ const sortieFlows: Record = { }, [eleventhBattleScenario.id]: { afterBattleId: eleventhBattleScenario.id, + eyebrow: '다음 전장', + title: twelfthBattleScenario.title, + description: '조조는 유비군을 하비 포위망에 투입합니다. 여포를 다시 마주하는 이 싸움에서 유비군은 조조의 명을 따르되, 서주 백성을 위한 뜻도 지켜야 합니다.', + rewardHint: `예상 보상: ${twelfthBattleScenario.title} 개방`, + nextBattleId: twelfthBattleScenario.id, + campaignStep: 'twelfth-battle', + pages: [...eleventhBattleVictoryPages, ...twelfthBattleIntroPages] + }, + [twelfthBattleScenario.id]: { + afterBattleId: twelfthBattleScenario.id, eyebrow: '다음 장 준비', - title: '조조의 그늘 아래', - description: '허도 입성로를 열어 조조의 장막에 들어섰지만, 유비군은 보호와 감시를 동시에 받게 됩니다. 다음 흐름은 조조 밑에서 기회를 기다리는 장으로 이어집니다.', - rewardHint: '다음 장: 조조 휘하의 긴장 준비 중', - pages: eleventhBattleVictoryPages, - unavailableNotice: '조조 휘하 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.' + title: '하비 포위의 끝', + description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지는 긴장으로 이어집니다.', + rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중', + pages: twelfthBattleVictoryPages, + unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index ae999f8..37b2925 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -759,6 +759,60 @@ export const eleventhBattleVictoryPages: StoryPage[] = [ } ]; +export const twelfthBattleIntroPages: StoryPage[] = [ + { + id: 'twelfth-cao-cao-order', + bgm: 'story-dark', + chapter: '조조의 명', + background: 'story-liu-bei', + text: '허도에 머문 지 오래지 않아 조조는 유비군을 하비 포위전에 부른다. 여포를 치는 명령은 서주를 잃은 한을 풀 기회이자, 조조의 칼끝이 어디를 향하는지 가늠하는 시험이었다.' + }, + { + id: 'twelfth-xiapi-shadow', + bgm: 'militia-theme', + chapter: '하비의 물길', + background: 'story-militia', + speaker: '미축', + text: '하비 외곽은 강과 진흙이 얽혀 있습니다. 보급로를 끊고 둑길을 잡지 못하면, 여포군 기병이 포위망의 빈틈을 찢고 나올 것입니다.' + }, + { + id: 'twelfth-brothers-before-lubu', + bgm: 'story-dark', + chapter: '다시 마주한 여포', + background: 'story-three-heroes', + speaker: '장비', + portrait: 'zhangFei', + text: '형님, 이번엔 여포의 이름만 들어도 창끝이 뜨겁습니다. 허나 서두르지 않겠습니다. 먼저 포위망을 조이고, 놈의 날뛰는 길을 끊겠습니다.' + }, + { + id: 'twelfth-xiapi-sortie', + bgm: 'battle-prep', + chapter: '하비 외곽전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '여포를 치는 싸움이 조조의 명이라 해도, 서주 백성이 다시 짓밟히지 않게 하는 일은 우리의 뜻이다. 물길을 잡고 포위망을 열어라.' + } +]; + +export const twelfthBattleVictoryPages: StoryPage[] = [ + { + id: 'twelfth-victory-outer-camp', + bgm: 'militia-theme', + chapter: '무너진 외곽 진영', + background: 'story-militia', + text: '유비군은 하비 외곽의 둑길과 보급 진영을 장악했다. 여포군 선봉은 성 안으로 물러났고, 조조군은 포위망을 한층 더 좁혔다.' + }, + { + id: 'twelfth-cao-cao-watches', + bgm: 'story-dark', + chapter: '칭찬과 감시', + background: 'story-liu-bei', + speaker: '간옹', + text: '현덕, 조조는 오늘의 공을 칭찬하겠지만 동시에 더 깊이 살필 걸세. 여포를 치는 동안에도, 우리가 조조의 손안에만 있지 않다는 것을 잊지 말아야 하네.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -970,6 +1024,12 @@ export const eleventhBattleMap: BattleMap = { terrain: createEleventhBattleTerrain() }; +export const twelfthBattleMap: BattleMap = { + width: 30, + height: 24, + terrain: createTwelfthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -3834,6 +3894,293 @@ export const eleventhBattleUnits: UnitData[] = [ } ]; +const twelfthBattleAllyPositions: Record = { + 'liu-bei': { x: 3, y: 18 }, + 'guan-yu': { x: 4, y: 17 }, + 'zhang-fei': { x: 4, y: 19 }, + 'jian-yong': { x: 5, y: 20 }, + 'mi-zhu': { x: 3, y: 21 } +}; + +export const twelfthBattleUnits: UnitData[] = [ + ...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) => + placeScenarioUnit(unit, twelfthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y }) + ), + { + id: 'xiapi-cavalry-a', + name: '여포군 돌격기병', + faction: 'enemy', + className: '여포군 기병', + classKey: 'cavalry', + level: 13, + exp: 76, + hp: 58, + maxHp: 58, + attack: 20, + move: 5, + stats: { might: 95, intelligence: 54, leadership: 74, agility: 96, luck: 60 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 54 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 20, + y: 5 + }, + { + id: 'xiapi-cavalry-b', + name: '여포군 돌격기병', + faction: 'enemy', + className: '여포군 기병', + classKey: 'cavalry', + level: 13, + exp: 78, + hp: 59, + maxHp: 59, + attack: 20, + move: 5, + stats: { might: 96, intelligence: 54, leadership: 74, agility: 97, luck: 60 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 23, + y: 15 + }, + { + id: 'xiapi-cavalry-c', + name: '여포군 돌격기병', + faction: 'enemy', + className: '여포군 기병', + classKey: 'cavalry', + level: 13, + exp: 78, + hp: 59, + maxHp: 59, + attack: 20, + move: 5, + stats: { might: 96, intelligence: 55, leadership: 75, agility: 97, luck: 61 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 24 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 25, + y: 10 + }, + { + id: 'xiapi-dike-guard-a', + name: '하비 둑길 수비병', + faction: 'enemy', + className: '여포군 보병', + classKey: 'yellowTurban', + level: 12, + exp: 74, + hp: 52, + maxHp: 52, + attack: 17, + move: 3, + stats: { might: 86, intelligence: 56, leadership: 72, agility: 65, luck: 57 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 22 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 12, + y: 14 + }, + { + id: 'xiapi-dike-guard-b', + name: '하비 둑길 수비병', + faction: 'enemy', + className: '여포군 보병', + classKey: 'yellowTurban', + level: 12, + exp: 76, + hp: 53, + maxHp: 53, + attack: 17, + move: 3, + stats: { might: 87, intelligence: 56, leadership: 73, agility: 66, luck: 57 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 22 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 14, + y: 18 + }, + { + id: 'xiapi-dike-guard-c', + name: '하비 둑길 수비병', + faction: 'enemy', + className: '여포군 보병', + classKey: 'yellowTurban', + level: 12, + exp: 76, + hp: 53, + maxHp: 53, + attack: 17, + move: 3, + stats: { might: 87, intelligence: 57, leadership: 73, agility: 66, luck: 57 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 22 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 17, + y: 9 + }, + { + id: 'xiapi-archer-a', + name: '하비 성루 궁병', + faction: 'enemy', + className: '여포군 궁병', + classKey: 'archer', + level: 12, + exp: 74, + hp: 43, + maxHp: 43, + attack: 18, + move: 3, + stats: { might: 78, intelligence: 68, leadership: 66, agility: 78, luck: 58 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 50 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 20 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 22, + y: 7 + }, + { + id: 'xiapi-archer-b', + name: '하비 성루 궁병', + faction: 'enemy', + className: '여포군 궁병', + classKey: 'archer', + level: 12, + exp: 76, + hp: 44, + maxHp: 44, + attack: 18, + move: 3, + stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 52 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 20 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 24, + y: 18 + }, + { + id: 'xiapi-archer-c', + name: '하비 성루 궁병', + faction: 'enemy', + className: '여포군 궁병', + classKey: 'archer', + level: 12, + exp: 76, + hp: 44, + maxHp: 44, + attack: 18, + move: 3, + stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 52 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 20 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 19, + y: 3 + }, + { + id: 'xiapi-camp-raider-a', + name: '여포군 기습병', + faction: 'enemy', + className: '여포군 기습병', + classKey: 'bandit', + level: 12, + exp: 74, + hp: 48, + maxHp: 48, + attack: 18, + move: 4, + stats: { might: 88, intelligence: 55, leadership: 64, agility: 81, luck: 58 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 9, + y: 8 + }, + { + id: 'xiapi-camp-raider-b', + name: '여포군 기습병', + faction: 'enemy', + className: '여포군 기습병', + classKey: 'bandit', + level: 12, + exp: 76, + hp: 49, + maxHp: 49, + attack: 18, + move: 4, + stats: { might: 89, intelligence: 55, leadership: 64, agility: 82, luck: 58 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 11, + y: 21 + }, + { + id: 'xiapi-strategist-chen-gong', + name: '진궁', + faction: 'enemy', + className: '여포군 책사', + classKey: 'strategist', + level: 13, + exp: 82, + hp: 52, + maxHp: 52, + attack: 18, + move: 3, + stats: { might: 62, intelligence: 96, leadership: 84, agility: 70, luck: 64 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 54 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 22, + y: 12 + }, + { + id: 'xiapi-leader-zhang-liao', + name: '장료', + faction: 'enemy', + className: '여포군 선봉장', + classKey: 'rebelLeader', + level: 14, + exp: 86, + hp: 78, + maxHp: 78, + attack: 21, + move: 5, + stats: { might: 95, intelligence: 78, leadership: 92, agility: 86, luck: 63 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 2, exp: 58 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 26 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 24, + y: 11 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -3890,6 +4237,7 @@ export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRe export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario); export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario); export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario); +export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -4046,6 +4394,44 @@ function createEleventhBattleTerrain(): TerrainType[][] { ); } +function createTwelfthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 24 }, (_, y) => + Array.from({ length: 30 }, (_, x): TerrainType => { + if (x <= 2 && y >= 17 && y <= 22) { + return 'camp'; + } + if ((x >= 23 && x <= 27 && y >= 8 && y <= 13) || (x >= 20 && x <= 24 && y >= 2 && y <= 4)) { + return 'fort'; + } + if ((x >= 10 && x <= 12 && y >= 16 && y <= 18) || (x >= 16 && x <= 18 && y >= 7 && y <= 9)) { + return 'village'; + } + if ((x === 14 || x === 15) && y >= 0 && y <= 22) { + return 'river'; + } + if ((x === 20 || x === 21) && y >= 4 && y <= 20) { + return 'river'; + } + if ((x >= 12 && x <= 22 && y === 12) || (x >= 4 && x <= 13 && y === 18) || (x >= 6 && x <= 24 && y === 10)) { + return 'road'; + } + if ((x >= 2 && x <= 26 && y === x - 2) || (x >= 3 && x <= 15 && y === 21 - x)) { + return 'road'; + } + if ((x <= 8 && y <= 8) || (x >= 4 && x <= 11 && y >= 9 && y <= 13) || (x >= 22 && y >= 16 && y <= 22)) { + return 'forest'; + } + if ((x >= 6 && x <= 10 && y >= 2 && y <= 5) || (x >= 24 && x <= 28 && y >= 4 && y <= 8) || (x >= 9 && x <= 13 && y >= 20)) { + return 'hill'; + } + if ((x >= 27 && y <= 4) || (x >= 27 && y >= 15) || (x <= 1 && y <= 4)) { + return 'cliff'; + } + return 'plain'; + }) + ); +} + function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData { return { ...cloneUnitForScenario(unit), diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index e1dce78..a45c2cf 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -60,7 +60,20 @@ const unitTexture: Record = { 'xuzhou-cavalry-c': 'unit-rebel-cavalry', 'xuzhou-guard-b': 'unit-rebel', 'xuzhou-archer-c': 'unit-rebel-archer', - 'xuzhou-leader-xiahou-dun': 'unit-rebel-leader' + 'xuzhou-leader-xiahou-dun': 'unit-rebel-leader', + 'xiapi-cavalry-a': 'unit-rebel-cavalry', + 'xiapi-cavalry-b': 'unit-rebel-cavalry', + 'xiapi-cavalry-c': 'unit-rebel-cavalry', + 'xiapi-dike-guard-a': 'unit-rebel', + 'xiapi-dike-guard-b': 'unit-rebel', + 'xiapi-dike-guard-c': 'unit-rebel', + 'xiapi-archer-a': 'unit-rebel-archer', + 'xiapi-archer-b': 'unit-rebel-archer', + 'xiapi-archer-c': 'unit-rebel-archer', + 'xiapi-camp-raider-a': 'unit-rebel', + 'xiapi-camp-raider-b': 'unit-rebel', + 'xiapi-strategist-chen-gong': 'unit-rebel-archer', + 'xiapi-leader-zhang-liao': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -568,7 +581,20 @@ const enemyAiByUnitId: Record = { 'xudu-road-cavalry-b': 'aggressive', 'xudu-road-guard-a': 'guard', 'xudu-road-guard-b': 'guard', - 'xudu-road-leader-ji-ling': 'guard' + 'xudu-road-leader-ji-ling': 'guard', + 'xiapi-cavalry-a': 'aggressive', + 'xiapi-cavalry-b': 'aggressive', + 'xiapi-cavalry-c': 'aggressive', + 'xiapi-dike-guard-a': 'guard', + 'xiapi-dike-guard-b': 'guard', + 'xiapi-dike-guard-c': 'guard', + 'xiapi-archer-a': 'hold', + 'xiapi-archer-b': 'hold', + 'xiapi-archer-c': 'hold', + 'xiapi-camp-raider-a': 'aggressive', + 'xiapi-camp-raider-b': 'aggressive', + 'xiapi-strategist-chen-gong': 'hold', + 'xiapi-leader-zhang-liao': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 47f9631..0ed1848 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -10,6 +10,7 @@ import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.s import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg'; import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg'; import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; +import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg'; import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png'; import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png'; @@ -80,6 +81,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-ninth', ninthBattleMapUrl); this.load.image('battle-map-tenth', tenthBattleMapUrl); this.load.image('battle-map-eleventh', eleventhBattleMapUrl); + this.load.image('battle-map-twelfth', twelfthBattleMapUrl); this.load.image('portrait-liu-bei', liuBeiPortraitUrl); this.load.image('portrait-guan-yu', guanYuPortraitUrl); this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl); diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index f3d92de..f97a7c3 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -176,7 +176,8 @@ const campBattleIds = { eighth: 'eighth-battle-xiaopei-supply-road', ninth: 'ninth-battle-xuzhou-gate-night-raid', tenth: 'tenth-battle-xuzhou-breakout', - eleventh: 'eleventh-battle-xudu-refuge-road' + eleventh: 'eleventh-battle-xudu-refuge-road', + twelfth: 'twelfth-battle-xiapi-outer-siege' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -288,6 +289,16 @@ const sortieRulesByBattleId: Partial