From 8f2479e5fc3d90bff9e69b7ea27e88b2027ee710 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 04:39:49 +0900 Subject: [PATCH] Add Liu Biao refuge chapter and Zhao Yun recruit --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 112 +++- .../images/battle/sixteenth-battle-map.svg | 89 +++ src/game/data/battles.ts | 63 +- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 564 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 44 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 113 +++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1012 insertions(+), 22 deletions(-) create mode 100644 src/assets/images/battle/sixteenth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 93b88c9..8922aca 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -35,12 +35,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Thirteenth battle Xiapi final clash, ending Lu Bu's arc and opening the tension that will lead toward Liu Bei breaking from Cao Cao - Fourteenth battle Cao Cao break route, adding Sun Qian as a recruitable strategist and making expanded sortie selection matter before Liu Bei heads toward Yuan Shao - Fifteenth battle Yuan Shao refuge road, pushing Liu Bei through Cao Cao's pursuit into Yuan Shao's camp while preserving the six-officer sortie roster +- Sixteenth battle Liu Biao refuge road, adding Zhao Yun as a recruitable cavalry officer and forcing a seven-officer roster into a six-officer sortie choice - 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, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer -- Flow verification script from title through the fifteenth battle victory, recruit sortie selection, and camp save state +- Flow verification script from title through the sixteenth battle victory, recruit sortie selection, and camp save state ## Next Steps -1. Build the Liu Biao refuge chapter, then move toward Zhuge Liang's recruitment arc +1. Build the Zhuge Liang recruitment chapter and add stronger between-battle visit/dialogue choices in Liu Biao's territory 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 a62dda8..1ee81f5 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1501,9 +1501,115 @@ try { fifteenthCampState.campTitle !== '원소 의탁 후 군영' || fifteenthCampState.availableDialogueIds?.length !== 3 || !fifteenthCampState.availableDialogueIds.every((id) => id.endsWith('yuan-refuge')) || - !fifteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') + !fifteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') || + !fifteenthCampState.campaign?.roster?.some((unit) => unit.id === 'zhao-yun') ) { - throw new Error(`Expected fifteenth camp to expose Yuan refuge dialogue set and preserve expanded roster: ${JSON.stringify(fifteenthCampState)}`); + throw new Error(`Expected fifteenth camp to expose Yuan refuge dialogue set, preserve expanded roster, and recruit Zhao Yun: ${JSON.stringify(fifteenthCampState)}`); + } + + await page.mouse.click(1120, 38); + await page.waitForTimeout(160); + const fifteenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fifteenthCampSortieState?.sortieVisible || + !fifteenthCampSortieState.sortiePlan?.objectiveLine?.includes('유표 의탁로') || + !fifteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + fifteenthCampSortieState.sortieRoster?.length < 7 || + fifteenthCampSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected fifteenth camp sortie prep to target Liu Biao refuge road with Zhao Yun as a selectable recruit: ${JSON.stringify(fifteenthCampSortieState)}`); + } + assertSortieTacticalRoster(fifteenthCampSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun' + ]); + if (fifteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected)) { + await clickSortieRosterUnit(page, 'zhao-yun'); + } + const fifteenthCampBeforeSwapState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (fifteenthCampBeforeSwapState.sortieRoster?.some((unit) => unit.id === 'jian-yong' && unit.selected)) { + await clickSortieRosterUnit(page, 'jian-yong'); + } + await clickSortieRosterUnit(page, 'zhao-yun'); + const fifteenthCampZhaoYunSelectedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !fifteenthCampZhaoYunSelectedState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.selected) || + fifteenthCampZhaoYunSelectedState.sortieRoster?.some((unit) => unit.id === 'jian-yong' && unit.selected) || + fifteenthCampZhaoYunSelectedState.sortiePlan?.selectedCount !== 6 + ) { + throw new Error(`Expected Zhao Yun to replace Jian Yong in sixteenth battle sortie: ${JSON.stringify(fifteenthCampZhaoYunSelectedState)}`); + } + await page.screenshot({ path: 'dist/verification-liu-biao-sortie.png', fullPage: true }); + + 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-liu-biao-story.png', fullPage: true }); + + for (let i = 0; i < 22; i += 1) { + const enteredSixteenthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'sixteenth-battle-liu-biao-refuge'; + }); + if (enteredSixteenthBattle) { + 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 === 'sixteenth-battle-liu-biao-refuge' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-sixteenth-battle.png', fullPage: true }); + + const sixteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const sixteenthEnemies = sixteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const sixteenthAllies = sixteenthBattleState.units.filter((unit) => unit.faction === 'ally'); + const sixteenthEnemyBehaviors = new Set(sixteenthEnemies.map((unit) => unit.ai)); + if ( + sixteenthBattleState.camera?.mapWidth !== 34 || + sixteenthBattleState.camera?.mapHeight !== 26 || + sixteenthBattleState.victoryConditionLabel !== '조인 격파' || + sixteenthEnemies.length < 18 || + !sixteenthEnemyBehaviors.has('aggressive') || + !sixteenthEnemyBehaviors.has('guard') || + !sixteenthEnemyBehaviors.has('hold') || + !sixteenthEnemies.some((unit) => unit.id === 'liu-biao-road-leader-cao-ren') || + !sixteenthAllies.some((unit) => unit.id === 'zhao-yun') || + sixteenthAllies.some((unit) => unit.id === 'jian-yong') + ) { + throw new Error(`Expected sixteenth battle to use Liu Biao refuge map, Cao Ren objective, mixed AI, and selected Zhao Yun sortie: ${JSON.stringify(sixteenthBattleState)}`); + } + + 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 sixteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + sixteenthCampState?.campBattleId !== 'sixteenth-battle-liu-biao-refuge' || + sixteenthCampState.campTitle !== '형주 의탁 후 군영' || + sixteenthCampState.availableDialogueIds?.length !== 3 || + !sixteenthCampState.availableDialogueIds.every((id) => id.endsWith('liu-biao-refuge')) || + !sixteenthCampState.campaign?.roster?.some((unit) => unit.id === 'zhao-yun') + ) { + throw new Error(`Expected sixteenth camp to expose Liu Biao refuge dialogue set and preserve Zhao Yun in roster: ${JSON.stringify(sixteenthCampState)}`); } await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); @@ -1517,7 +1623,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-fifteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-sixteenth-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/sixteenth-battle-map.svg b/src/assets/images/battle/sixteenth-battle-map.svg new file mode 100644 index 0000000..06475dd --- /dev/null +++ b/src/assets/images/battle/sixteenth-battle-map.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 2081088..590fb6a 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -56,6 +56,10 @@ import { fourthBattleMap, fourthBattleUnits, fourthBattleVictoryPages, + sixteenthBattleBonds, + sixteenthBattleMap, + sixteenthBattleUnits, + sixteenthBattleVictoryPages, thirdBattleMap, thirdBattleUnits, thirdBattleVictoryPages, @@ -80,7 +84,8 @@ export type BattleScenarioId = | 'twelfth-battle-xiapi-outer-siege' | 'thirteenth-battle-xiapi-final' | 'fourteenth-battle-cao-break' - | 'fifteenth-battle-yuan-refuge-road'; + | 'fifteenth-battle-yuan-refuge-road' + | 'sixteenth-battle-liu-biao-refuge'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -914,6 +919,59 @@ export const fifteenthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const sixteenthBattleScenario: BattleScenarioDefinition = { + id: 'sixteenth-battle-liu-biao-refuge', + title: '유표 의탁로', + victoryConditionLabel: '조인 격파', + defeatConditionLabel: '유비 퇴각', + openingObjectiveLines: [ + '원소 진영을 떠난 유비군은 형주의 유표에게 의탁하기 위해 남쪽 길을 엽니다. 조운이 합류했지만 조조군 추격대도 형주 관문 앞에 따라붙었습니다.', + '중앙 강줄기와 숲길은 행군을 늦추고, 동쪽 관문은 수비병과 궁병이 지키고 있습니다. 조운의 기동력과 책사들의 지원을 함께 활용하십시오.', + '조인을 격파하고 형주 관문을 확보하십시오. 25턴 안에 승리하면 유비군의 의탁 명분과 군자금을 더 단단히 챙길 수 있습니다.' + ], + map: sixteenthBattleMap, + units: sixteenthBattleUnits, + bonds: sixteenthBattleBonds, + mapTextureKey: 'battle-map-sixteenth', + leaderUnitId: 'liu-biao-road-leader-cao-ren', + quickVictoryTurnLimit: 25, + baseVictoryGold: 1880, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '조인 격파', + rewardGold: 1200, + unitId: 'liu-biao-road-leader-cao-ren' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 420, + unitId: 'liu-bei' + }, + { + id: 'jingzhou-gate', + kind: 'secure-terrain', + label: '형주 관문 확보', + rewardGold: 680, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '25턴 이내 승리', + rewardGold: 540, + maxTurn: 25 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], + itemRewards: ['콩 6', '상처약 4', '탁주 2', '형주 접선 문서 +1'], + victoryPages: sixteenthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -931,7 +989,8 @@ export const battleScenarios: Record 'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario, 'thirteenth-battle-xiapi-final': thirteenthBattleScenario, 'fourteenth-battle-cao-break': fourteenthBattleScenario, - 'fifteenth-battle-yuan-refuge-road': fifteenthBattleScenario + 'fifteenth-battle-yuan-refuge-road': fifteenthBattleScenario, + 'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 7844d10..4652850 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -11,6 +11,7 @@ import { secondBattleScenario, seventhBattleScenario, sixthBattleScenario, + sixteenthBattleScenario, tenthBattleScenario, thirteenthBattleScenario, twelfthBattleScenario, @@ -39,6 +40,8 @@ import { seventhBattleVictoryPages, sixthBattleIntroPages, sixthBattleVictoryPages, + sixteenthBattleIntroPages, + sixteenthBattleVictoryPages, tenthBattleIntroPages, tenthBattleVictoryPages, thirteenthBattleIntroPages, @@ -207,13 +210,24 @@ const sortieFlows: Record = { }, [fifteenthBattleScenario.id]: { afterBattleId: fifteenthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '유표 의탁의 길', + eyebrow: '다음 전장', + title: sixteenthBattleScenario.title, description: - '원소 진영에 몸을 의탁했지만, 큰 세력의 틈은 오래 머물 곳이 되기 어렵습니다. 다음 흐름은 원소 진영을 떠나 유표에게 의탁하는 장으로 이어집니다.', - rewardHint: '다음 장: 유표 의탁 준비 중', - pages: fifteenthBattleVictoryPages, - unavailableNotice: '유표 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.' + '원소 진영의 형세가 흔들리자 유비군은 다시 몸을 옮겨 형주로 향합니다. 조운이 재합류하며 기동력이 늘지만, 조조군 추격대가 형주 관문 앞을 막아섭니다.', + rewardHint: `예상 보상: ${sixteenthBattleScenario.title} 개방 / 조운 합류`, + nextBattleId: sixteenthBattleScenario.id, + campaignStep: 'sixteenth-battle', + pages: [...fifteenthBattleVictoryPages, ...sixteenthBattleIntroPages] + }, + [sixteenthBattleScenario.id]: { + afterBattleId: sixteenthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '제갈량을 향한 길', + description: + '유표에게 의탁한 유비군은 형주에서 숨을 돌리지만, 손님으로 머무는 것만으로 천하의 길을 열 수는 없습니다. 다음 흐름은 와룡의 이름을 찾아가는 제갈량 영입 장으로 이어집니다.', + rewardHint: '다음 장: 제갈량 영입 준비 중', + pages: sixteenthBattleVictoryPages, + unavailableNotice: '제갈량 영입 장은 다음 작업에서 이어집니다. 군영에서 새로 합류한 조운과 공명도를 다지고 출전 구성을 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index e15d314..a95009e 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1006,6 +1006,69 @@ export const fifteenthBattleVictoryPages: StoryPage[] = [ } ]; +export const sixteenthBattleIntroPages: StoryPage[] = [ + { + id: 'sixteenth-yuan-camp-shifts', + bgm: 'story-dark', + chapter: '원소 진영의 그림자', + background: 'story-militia', + speaker: '간옹', + text: '원소의 군막은 넓지만, 그 안에서는 말 한마디가 칼보다 빠르게 옮겨집니다. 조조를 떠난 유비군을 오래 품으려는 자도, 조조에게 빌미를 주지 않으려는 자도 모두 눈치를 보고 있습니다.' + }, + { + id: 'sixteenth-zhao-yun-returns', + bgm: 'militia-theme', + chapter: '백마의 재회', + background: 'story-three-heroes', + speaker: '조운', + text: '공손찬 장군의 군세가 흩어진 뒤에도 주공의 의리를 잊지 않았습니다. 오늘부터 이 조자룡의 창은 백성을 버리지 않는 군주를 위해 달리겠습니다.' + }, + { + id: 'sixteenth-liu-biao-road', + bgm: 'battle-prep', + chapter: '형주로 가는 길', + background: 'story-liu-bei', + speaker: '유비', + portrait: 'liuBei', + text: '원소의 장막 아래 머무는 것은 잠시 몸을 숨기는 일일 뿐, 뜻을 세울 땅은 되지 못한다. 형주의 유표에게 의탁해 백성을 거둘 길을 다시 찾아야 하오.' + }, + { + id: 'sixteenth-jingzhou-road-sortie', + bgm: 'battle-prep', + chapter: '형주 의탁로', + background: 'story-sortie', + speaker: '장비', + portrait: 'zhangFei', + text: '형님, 조조군 추격대가 또 길목을 막고 있소. 이번에는 조운도 합류했으니, 데려갈 장수를 잘 골라 한 번에 길을 열어 봅시다.' + } +]; + +export const sixteenthBattleVictoryPages: StoryPage[] = [ + { + id: 'sixteenth-victory-jingzhou-gate', + bgm: 'militia-theme', + chapter: '형주 관문', + background: 'story-sortie', + text: '조인의 추격대가 물러서자 형주로 향하는 관문이 열렸다. 유비군은 원소 진영의 무거운 시선에서 벗어나, 다시 스스로 길을 고를 수 있는 숨을 얻었다.' + }, + { + id: 'sixteenth-liu-biao-welcome', + bgm: 'story-dark', + chapter: '유표의 객장', + background: 'story-militia', + speaker: '손건', + text: '유표는 주공을 객장으로 맞이하겠다고 합니다. 환대는 분명하지만 형주 안에도 각자의 계산이 있습니다. 이제 싸움만큼이나 사람의 마음을 읽어야 할 때입니다.' + }, + { + id: 'sixteenth-next-zhuge-shadow', + bgm: 'battle-prep', + chapter: '와룡의 소문', + background: 'story-liu-bei', + speaker: '간옹', + text: '형주 선비들 사이에 융중의 와룡이라는 이름이 오갑니다. 주공의 뜻을 천하의 길로 펼칠 사람이라면, 다음 걸음은 그 사람을 찾아가는 일이 되겠습니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1241,6 +1304,12 @@ export const fifteenthBattleMap: BattleMap = { terrain: createFifteenthBattleTerrain() }; +export const sixteenthBattleMap: BattleMap = { + width: 34, + height: 26, + terrain: createSixteenthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -3149,6 +3218,30 @@ export const caoBreakRecruitUnits: UnitData[] = [ } ]; +export const liuBiaoRecruitUnits: UnitData[] = [ + { + id: 'zhao-yun', + name: '조운', + faction: 'ally', + className: '백마 기병', + classKey: 'cavalry', + level: 7, + exp: 18, + hp: 36, + maxHp: 36, + attack: 12, + move: 5, + stats: { might: 88, intelligence: 76, leadership: 82, agility: 92, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 1, exp: 18 }, + armor: { itemId: 'lamellar-armor', level: 1, exp: 14 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 6, + y: 21 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -5447,6 +5540,403 @@ export const fifteenthBattleUnits: UnitData[] = [ } ]; +const sixteenthBattleAllyPositions: Record = { + 'liu-bei': { x: 3, y: 20 }, + 'guan-yu': { x: 4, y: 19 }, + 'zhang-fei': { x: 4, y: 21 }, + 'jian-yong': { x: 5, y: 20 }, + 'mi-zhu': { x: 3, y: 22 }, + 'sun-qian': { x: 5, y: 22 }, + 'zhao-yun': { x: 6, y: 21 } +}; + +export const sixteenthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits + ].map((unit) => placeScenarioUnit(unit, sixteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'liu-biao-road-scout-a', + name: '조조군 척후', + faction: 'enemy', + className: '추격 척후', + classKey: 'bandit', + level: 16, + exp: 8, + hp: 56, + maxHp: 56, + attack: 21, + move: 4, + stats: { might: 92, intelligence: 62, leadership: 70, agility: 90, luck: 63 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 72 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 34 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 10, + y: 20 + }, + { + id: 'liu-biao-road-scout-b', + name: '조조군 척후', + faction: 'enemy', + className: '추격 척후', + classKey: 'bandit', + level: 16, + exp: 8, + hp: 56, + maxHp: 56, + attack: 21, + move: 4, + stats: { might: 92, intelligence: 62, leadership: 70, agility: 90, luck: 63 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 72 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 34 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 12, + y: 22 + }, + { + id: 'liu-biao-road-infantry-a', + name: '조조 보병', + faction: 'enemy', + className: '추격 보병', + classKey: 'yellowTurban', + level: 16, + exp: 10, + hp: 66, + maxHp: 66, + attack: 22, + move: 3, + stats: { might: 96, intelligence: 64, leadership: 82, agility: 72, luck: 62 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 74 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 36 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 15, + y: 16 + }, + { + id: 'liu-biao-road-infantry-b', + name: '조조 보병', + faction: 'enemy', + className: '추격 보병', + classKey: 'yellowTurban', + level: 16, + exp: 10, + hp: 66, + maxHp: 66, + attack: 22, + move: 3, + stats: { might: 96, intelligence: 64, leadership: 82, agility: 72, luck: 62 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 74 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 36 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 17, + y: 18 + }, + { + id: 'liu-biao-road-infantry-c', + name: '조조 보병', + faction: 'enemy', + className: '관문 보병', + classKey: 'yellowTurban', + level: 17, + exp: 12, + hp: 68, + maxHp: 68, + attack: 23, + move: 3, + stats: { might: 98, intelligence: 65, leadership: 84, agility: 72, luck: 63 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 78 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 38 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 24, + y: 12 + }, + { + id: 'liu-biao-road-infantry-d', + name: '조조 보병', + faction: 'enemy', + className: '관문 보병', + classKey: 'yellowTurban', + level: 17, + exp: 12, + hp: 68, + maxHp: 68, + attack: 23, + move: 3, + stats: { might: 98, intelligence: 65, leadership: 84, agility: 72, luck: 63 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 78 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 38 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 25, + y: 15 + }, + { + id: 'liu-biao-road-archer-a', + name: '조조 궁병', + faction: 'enemy', + className: '추격 궁병', + classKey: 'archer', + level: 16, + exp: 10, + hp: 52, + maxHp: 52, + attack: 21, + move: 3, + stats: { might: 84, intelligence: 76, leadership: 74, agility: 86, luck: 64 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 72 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 34 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 17, + y: 12 + }, + { + id: 'liu-biao-road-archer-b', + name: '조조 궁병', + faction: 'enemy', + className: '관문 궁병', + classKey: 'archer', + level: 17, + exp: 12, + hp: 54, + maxHp: 54, + attack: 22, + move: 3, + stats: { might: 86, intelligence: 78, leadership: 76, agility: 86, luck: 64 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 76 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 36 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 25, + y: 9 + }, + { + id: 'liu-biao-road-archer-c', + name: '조조 궁병', + faction: 'enemy', + className: '관문 궁병', + classKey: 'archer', + level: 17, + exp: 12, + hp: 54, + maxHp: 54, + attack: 22, + move: 3, + stats: { might: 86, intelligence: 78, leadership: 76, agility: 86, luck: 64 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 76 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 36 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 29, + y: 12 + }, + { + id: 'liu-biao-road-archer-d', + name: '조조 궁병', + faction: 'enemy', + className: '관문 궁병', + classKey: 'archer', + level: 17, + exp: 12, + hp: 54, + maxHp: 54, + attack: 22, + move: 3, + stats: { might: 86, intelligence: 78, leadership: 76, agility: 86, luck: 64 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 76 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 36 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 28, + y: 17 + }, + { + id: 'liu-biao-road-cavalry-a', + name: '조조 기병', + faction: 'enemy', + className: '추격 기병', + classKey: 'cavalry', + level: 17, + exp: 12, + hp: 70, + maxHp: 70, + attack: 24, + move: 5, + stats: { might: 100, intelligence: 60, leadership: 78, agility: 94, luck: 65 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 80 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 38 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 13, + y: 19 + }, + { + id: 'liu-biao-road-cavalry-b', + name: '조조 기병', + faction: 'enemy', + className: '추격 기병', + classKey: 'cavalry', + level: 17, + exp: 12, + hp: 70, + maxHp: 70, + attack: 24, + move: 5, + stats: { might: 100, intelligence: 60, leadership: 78, agility: 94, luck: 65 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 80 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 38 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 19, + y: 21 + }, + { + id: 'liu-biao-road-cavalry-c', + name: '조조 기병', + faction: 'enemy', + className: '추격 기병', + classKey: 'cavalry', + level: 17, + exp: 12, + hp: 70, + maxHp: 70, + attack: 24, + move: 5, + stats: { might: 100, intelligence: 60, leadership: 78, agility: 94, luck: 65 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 80 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 38 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 21, + y: 17 + }, + { + id: 'liu-biao-road-cavalry-d', + name: '조조 기병', + faction: 'enemy', + className: '관문 기병', + classKey: 'cavalry', + level: 18, + exp: 14, + hp: 74, + maxHp: 74, + attack: 25, + move: 5, + stats: { might: 102, intelligence: 62, leadership: 80, agility: 95, luck: 66 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 84 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 40 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 28, + y: 20 + }, + { + id: 'liu-biao-road-guard-a', + name: '형주 관문 수비', + faction: 'enemy', + className: '관문 수비대', + classKey: 'yellowTurban', + level: 18, + exp: 14, + hp: 74, + maxHp: 74, + attack: 24, + move: 3, + stats: { might: 100, intelligence: 66, leadership: 88, agility: 72, luck: 64 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 84 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 42 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 30, + y: 13 + }, + { + id: 'liu-biao-road-guard-b', + name: '형주 관문 수비', + faction: 'enemy', + className: '관문 수비대', + classKey: 'yellowTurban', + level: 18, + exp: 14, + hp: 74, + maxHp: 74, + attack: 24, + move: 3, + stats: { might: 100, intelligence: 66, leadership: 88, agility: 72, luck: 64 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 2, exp: 84 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 42 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 31, + y: 16 + }, + { + id: 'liu-biao-road-strategist-a', + name: '조조군 참모', + faction: 'enemy', + className: '추격 참모', + classKey: 'archer', + level: 18, + exp: 14, + hp: 58, + maxHp: 58, + attack: 23, + move: 3, + stats: { might: 72, intelligence: 90, leadership: 80, agility: 78, luck: 66 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 80 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 38 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 29, + y: 15 + }, + { + id: 'liu-biao-road-leader-cao-ren', + name: '조인', + faction: 'enemy', + className: '조조군 추격장', + classKey: 'rebelLeader', + level: 19, + exp: 18, + hp: 98, + maxHp: 98, + attack: 27, + move: 4, + stats: { might: 106, intelligence: 74, leadership: 96, agility: 80, luck: 66 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 3, exp: 24 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 46 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 31, + y: 14 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -5512,6 +6002,25 @@ export const caoBreakRecruitBonds: BattleBond[] = [ } ]; +export const liuBiaoRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__zhao-yun', + unitIds: ['liu-bei', 'zhao-yun'], + title: '백마의 의리', + level: 52, + exp: 0, + description: '조운은 난세 속에서도 유비의 백성을 향한 뜻을 믿고 다시 창을 맡깁니다.' + }, + { + id: 'guan-yu__zhao-yun', + unitIds: ['guan-yu', 'zhao-yun'], + title: '충의의 칼끝', + level: 38, + exp: 0, + description: '관우와 조운은 서로의 충의를 알아보고 같은 전선에서 흔들림 없는 압박을 만듭니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -5528,6 +6037,9 @@ export const fourteenthBattleBonds: BattleBond[] = [...thirteenthBattleBonds, .. cloneBattleBondForScenario ); export const fifteenthBattleBonds: BattleBond[] = fourteenthBattleBonds.map(cloneBattleBondForScenario); +export const sixteenthBattleBonds: BattleBond[] = [...fifteenthBattleBonds, ...liuBiaoRecruitBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -5884,6 +6396,58 @@ function createFifteenthBattleTerrain(): TerrainType[][] { ); } +function createSixteenthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 26 }, (_, y) => + Array.from({ length: 34 }, (_, x): TerrainType => { + if ((x <= 2 && y >= 19 && y <= 23) || (x === 3 && y >= 21 && y <= 23)) { + return 'camp'; + } + if ((x >= 29 && x <= 33 && y >= 11 && y <= 17) || (x >= 30 && x <= 33 && y >= 8 && y <= 10)) { + return 'fort'; + } + if ((x >= 12 && x <= 14 && y >= 17 && y <= 19) || (x >= 21 && x <= 23 && y >= 9 && y <= 11)) { + return 'village'; + } + if ((x === 16 || x === 17) && y >= 2 && y <= 23) { + return 'river'; + } + if ((x === 25 || x === 26) && y >= 4 && y <= 20) { + return 'river'; + } + if ( + (y === 20 && x >= 2 && x <= 18) || + (y === 16 && x >= 14 && x <= 30) || + (y === 12 && x >= 22 && x <= 32) || + (x === 30 && y >= 10 && y <= 18) + ) { + return 'road'; + } + if ((x >= 5 && x <= 16 && y === 25 - x) || (x >= 15 && x <= 29 && y === x - 9)) { + return 'road'; + } + if ( + (x <= 8 && y <= 8) || + (x >= 5 && x <= 13 && y >= 10 && y <= 15) || + (x >= 3 && x <= 9 && y >= 16 && y <= 18) || + (x >= 24 && x <= 31 && y >= 19 && y <= 24) + ) { + return 'forest'; + } + if ( + (x >= 7 && x <= 13 && y >= 2 && y <= 6) || + (x >= 18 && x <= 22 && y >= 20 && y <= 24) || + (x >= 28 && x <= 33 && y >= 1 && y <= 5) + ) { + return 'hill'; + } + if ((x >= 32 && y <= 8) || (x >= 32 && y >= 18) || (x <= 1 && y <= 5)) { + 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 e0c4450..fcb3fe4 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -27,6 +27,7 @@ const unitTexture: Record = { 'liu-bei': 'unit-liu-bei', 'guan-yu': 'unit-guan-yu', 'zhang-fei': 'unit-zhang-fei', + 'zhao-yun': 'unit-rebel-cavalry', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -120,7 +121,25 @@ const unitTexture: Record = { 'yuan-refuge-guard-b': 'unit-rebel', 'yuan-refuge-guard-c': 'unit-rebel', 'yuan-refuge-strategist-a': 'unit-rebel-archer', - 'yuan-refuge-leader-cai-yang': 'unit-rebel-leader' + 'yuan-refuge-leader-cai-yang': 'unit-rebel-leader', + 'liu-biao-road-scout-a': 'unit-rebel', + 'liu-biao-road-scout-b': 'unit-rebel', + 'liu-biao-road-infantry-a': 'unit-rebel', + 'liu-biao-road-infantry-b': 'unit-rebel', + 'liu-biao-road-infantry-c': 'unit-rebel', + 'liu-biao-road-infantry-d': 'unit-rebel', + 'liu-biao-road-archer-a': 'unit-rebel-archer', + 'liu-biao-road-archer-b': 'unit-rebel-archer', + 'liu-biao-road-archer-c': 'unit-rebel-archer', + 'liu-biao-road-archer-d': 'unit-rebel-archer', + 'liu-biao-road-cavalry-a': 'unit-rebel-cavalry', + 'liu-biao-road-cavalry-b': 'unit-rebel-cavalry', + 'liu-biao-road-cavalry-c': 'unit-rebel-cavalry', + 'liu-biao-road-cavalry-d': 'unit-rebel-cavalry', + 'liu-biao-road-guard-a': 'unit-rebel', + 'liu-biao-road-guard-b': 'unit-rebel', + 'liu-biao-road-strategist-a': 'unit-rebel-archer', + 'liu-biao-road-leader-cao-ren': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -543,7 +562,8 @@ const initialItemStocks: Record> = { 'zhang-fei': { bean: 1, wine: 1 }, 'jian-yong': { bean: 1 }, 'mi-zhu': { bean: 2, salve: 1 }, - 'sun-qian': { bean: 1, salve: 1 } + 'sun-qian': { bean: 1, salve: 1 }, + 'zhao-yun': { bean: 1, wine: 1 } }; const attackRangeByClass: Partial> = { @@ -689,7 +709,25 @@ const enemyAiByUnitId: Record = { 'yuan-refuge-guard-b': 'guard', 'yuan-refuge-guard-c': 'guard', 'yuan-refuge-strategist-a': 'hold', - 'yuan-refuge-leader-cai-yang': 'guard' + 'yuan-refuge-leader-cai-yang': 'guard', + 'liu-biao-road-scout-a': 'aggressive', + 'liu-biao-road-scout-b': 'aggressive', + 'liu-biao-road-infantry-a': 'guard', + 'liu-biao-road-infantry-b': 'guard', + 'liu-biao-road-infantry-c': 'guard', + 'liu-biao-road-infantry-d': 'guard', + 'liu-biao-road-archer-a': 'hold', + 'liu-biao-road-archer-b': 'hold', + 'liu-biao-road-archer-c': 'hold', + 'liu-biao-road-archer-d': 'hold', + 'liu-biao-road-cavalry-a': 'aggressive', + 'liu-biao-road-cavalry-b': 'aggressive', + 'liu-biao-road-cavalry-c': 'aggressive', + 'liu-biao-road-cavalry-d': 'aggressive', + 'liu-biao-road-guard-a': 'guard', + 'liu-biao-road-guard-b': 'guard', + 'liu-biao-road-strategist-a': 'hold', + 'liu-biao-road-leader-cao-ren': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 172de23..840969c 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -10,6 +10,7 @@ import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg'; import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg'; import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg'; import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg'; +import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg'; import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg'; import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg'; import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; @@ -88,6 +89,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl); this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl); this.load.image('battle-map-fifteenth', fifteenthBattleMapUrl); + this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl); 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 2a5fafc..99f4819 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -9,6 +9,8 @@ import { caoBreakRecruitUnits, firstBattleBonds, firstBattleUnits, + liuBiaoRecruitBonds, + liuBiaoRecruitUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, @@ -194,7 +196,8 @@ const campBattleIds = { twelfth: 'twelfth-battle-xiapi-outer-siege', thirteenth: 'thirteenth-battle-xiapi-final', fourteenth: 'fourteenth-battle-cao-break', - fifteenth: 'fifteenth-battle-yuan-refuge-road' + fifteenth: 'fifteenth-battle-yuan-refuge-road', + sixteenth: 'sixteenth-battle-liu-biao-refuge' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -349,6 +352,18 @@ const sortieRulesByBattleId: Partial