From 09c1390ba551d6a98a38ed4e26dc38eec3c56a7e Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 04:08:15 +0900 Subject: [PATCH] Add Cao break chapter and Sun Qian sortie --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 90 +++- .../images/battle/fourteenth-battle-map.svg | 59 +++ src/game/data/battles.ts | 63 ++- src/game/data/campaignFlow.ts | 25 +- src/game/data/scenario.ts | 500 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 37 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 126 ++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 900 insertions(+), 23 deletions(-) create mode 100644 src/assets/images/battle/fourteenth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index d904c61..90e2bb5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,12 +33,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 - 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 - 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 thirteenth battle victory, recruit sortie selection, and camp save state +- Flow verification script from title through the fourteenth battle victory, recruit sortie selection, and camp save state ## Next Steps -1. Build the Cao Cao break chapter, then move into Yuan Shao and Liu Biao refuge arcs +1. Build the Yuan Shao refuge chapter, then move into Liu Biao refuge arcs 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 b3c5d42..fe6e8e1 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1339,9 +1339,93 @@ try { !thirteenthCampState.availableDialogueIds.every((id) => id.endsWith('lu-bu-fall')) || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') || !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') || - !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') + !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu') || + !thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') ) { - throw new Error(`Expected thirteenth camp to expose Lu Bu fall dialogue set and preserve full roster: ${JSON.stringify(thirteenthCampState)}`); + throw new Error(`Expected thirteenth camp to expose Lu Bu fall dialogue set, recruit Sun Qian, and preserve full roster: ${JSON.stringify(thirteenthCampState)}`); + } + + await page.mouse.click(1120, 38); + await page.waitForTimeout(160); + const thirteenthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !thirteenthCampSortieState?.sortieVisible || + !thirteenthCampSortieState.sortiePlan?.objectiveLine?.includes('서주 재기') || + !thirteenthCampSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) + ) { + throw new Error(`Expected thirteenth camp sortie prep to target Cao break battle with Sun Qian as a recruit option: ${JSON.stringify(thirteenthCampSortieState)}`); + } + assertSortieTacticalRoster(thirteenthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu', 'sun-qian']); + await clickSortieRosterUnit(page, 'sun-qian'); + const thirteenthCampSunQianSelectedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!thirteenthCampSunQianSelectedState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.selected)) { + throw new Error(`Expected Sun Qian to be selectable for the fourteenth battle sortie: ${JSON.stringify(thirteenthCampSunQianSelectedState)}`); + } + await page.screenshot({ path: 'dist/verification-cao-break-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-cao-break-story.png', fullPage: true }); + + for (let i = 0; i < 18; i += 1) { + const enteredFourteenthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'fourteenth-battle-cao-break'; + }); + if (enteredFourteenthBattle) { + 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 === 'fourteenth-battle-cao-break' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-fourteenth-battle.png', fullPage: true }); + + const fourteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fourteenthEnemies = fourteenthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fourteenthAllies = fourteenthBattleState.units.filter((unit) => unit.faction === 'ally'); + const fourteenthEnemyBehaviors = new Set(fourteenthEnemies.map((unit) => unit.ai)); + if ( + fourteenthBattleState.camera?.mapWidth !== 32 || + fourteenthBattleState.camera?.mapHeight !== 24 || + fourteenthBattleState.victoryConditionLabel !== '차주 격파' || + fourteenthEnemies.length < 15 || + !fourteenthEnemyBehaviors.has('aggressive') || + !fourteenthEnemyBehaviors.has('guard') || + !fourteenthEnemyBehaviors.has('hold') || + !fourteenthEnemies.some((unit) => unit.id === 'cao-break-leader-che-zhou') || + !fourteenthAllies.some((unit) => unit.id === 'sun-qian') + ) { + throw new Error(`Expected fourteenth battle to use Cao break map, Che Zhou objective, mixed AI, and selected Sun Qian sortie: ${JSON.stringify(fourteenthBattleState)}`); + } + + 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 fourteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fourteenthCampState?.campBattleId !== 'fourteenth-battle-cao-break' || + fourteenthCampState.campTitle !== '서주 재기 후 군영' || + fourteenthCampState.availableDialogueIds?.length !== 3 || + !fourteenthCampState.availableDialogueIds.every((id) => id.endsWith('cao-break')) || + !fourteenthCampState.campaign?.roster?.some((unit) => unit.id === 'sun-qian') + ) { + throw new Error(`Expected fourteenth camp to expose Cao break dialogue set and preserve Sun Qian in roster: ${JSON.stringify(fourteenthCampState)}`); } await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); @@ -1355,7 +1439,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-thirteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-fourteenth-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/fourteenth-battle-map.svg b/src/assets/images/battle/fourteenth-battle-map.svg new file mode 100644 index 0000000..7c6a2f8 --- /dev/null +++ b/src/assets/images/battle/fourteenth-battle-map.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 78cb35c..543603a 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -1,5 +1,9 @@ import { firstBattleBonds, + fourteenthBattleBonds, + fourteenthBattleMap, + fourteenthBattleUnits, + fourteenthBattleVictoryPages, eighthBattleBonds, eighthBattleMap, eighthBattleUnits, @@ -70,7 +74,8 @@ export type BattleScenarioId = | 'tenth-battle-xuzhou-breakout' | 'eleventh-battle-xudu-refuge-road' | 'twelfth-battle-xiapi-outer-siege' - | 'thirteenth-battle-xiapi-final'; + | 'thirteenth-battle-xiapi-final' + | 'fourteenth-battle-cao-break'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -798,6 +803,59 @@ export const thirteenthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fourteenthBattleScenario: BattleScenarioDefinition = { + id: 'fourteenth-battle-cao-break', + title: '서주 재기', + victoryConditionLabel: '차주 격파', + defeatConditionLabel: '유비 퇴각', + openingObjectiveLines: [ + '조조의 감시대가 서주 관문과 역참을 틀어쥐고 있습니다. 차주를 격파하면 유비군은 조조 휘하를 벗어날 첫 발판을 세웁니다.', + '조조군 기병은 길을 따라 추격하고, 궁병은 관문 뒤에서 사거리를 잡습니다. 손건의 문서와 간옹의 말로 민심을 흔들며 길목을 열어야 합니다.', + '서주 관문을 확보하고 22턴 안에 승리하면 원소 진영으로 향할 명분과 보급을 더 많이 보존합니다.' + ], + map: fourteenthBattleMap, + units: fourteenthBattleUnits, + bonds: fourteenthBattleBonds, + mapTextureKey: 'battle-map-fourteenth', + leaderUnitId: 'cao-break-leader-che-zhou', + quickVictoryTurnLimit: 22, + baseVictoryGold: 1660, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '차주 격파', + rewardGold: 1040, + unitId: 'cao-break-leader-che-zhou' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 380, + unitId: 'liu-bei' + }, + { + id: 'gate', + kind: 'secure-terrain', + label: '서주 관문 확보', + rewardGold: 600, + terrain: 'fort' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '22턴 이내 승리', + rewardGold: 460, + maxTurn: 22 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }], + itemRewards: ['콩 5', '상처약 3', '탁주 2', '손건 문서 +1'], + victoryPages: fourteenthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -813,7 +871,8 @@ export const battleScenarios: Record 'tenth-battle-xuzhou-breakout': tenthBattleScenario, 'eleventh-battle-xudu-refuge-road': eleventhBattleScenario, 'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario, - 'thirteenth-battle-xiapi-final': thirteenthBattleScenario + 'thirteenth-battle-xiapi-final': thirteenthBattleScenario, + 'fourteenth-battle-cao-break': fourteenthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 8782ddc..4c67fb1 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -4,6 +4,7 @@ import { eighthBattleScenario, eleventhBattleScenario, fifthBattleScenario, + fourteenthBattleScenario, fourthBattleScenario, ninthBattleScenario, secondBattleScenario, @@ -22,6 +23,8 @@ import { eleventhBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, + fourteenthBattleIntroPages, + fourteenthBattleVictoryPages, firstBattleVictoryPages, fourthBattleIntroPages, fourthBattleVictoryPages, @@ -179,12 +182,24 @@ const sortieFlows: Record = { }, [thirteenthBattleScenario.id]: { afterBattleId: thirteenthBattleScenario.id, + eyebrow: '다음 전장', + title: fourteenthBattleScenario.title, + description: + '여포가 무너진 뒤 조조의 경계는 더 깊어졌습니다. 손건이 합류해 서주 길목의 문서와 민심을 준비하고, 유비군은 조조의 장막 밖으로 나갈 첫 전투를 맞습니다.', + rewardHint: `예상 보상: ${fourteenthBattleScenario.title} 개방 / 손건 합류`, + nextBattleId: fourteenthBattleScenario.id, + campaignStep: 'fourteenth-battle', + pages: [...thirteenthBattleVictoryPages, ...fourteenthBattleIntroPages] + }, + [fourteenthBattleScenario.id]: { + afterBattleId: fourteenthBattleScenario.id, eyebrow: '다음 장 준비', - title: '조조 이탈의 불씨', - description: '여포가 무너지고 하비 결전은 끝났지만, 조조의 칭찬과 경계는 동시에 깊어졌습니다. 다음 흐름은 조조 휘하를 벗어날 명분과 때를 준비하는 장으로 이어집니다.', - rewardHint: '다음 장: 조조 이탈과 원소 의탁 준비 중', - pages: thirteenthBattleVictoryPages, - unavailableNotice: '조조 이탈과 원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.' + title: '원소 의탁의 길', + description: + '조조의 장막을 벗어난 유비군은 원소 진영으로 향할 수밖에 없습니다. 다음 흐름은 원소 의탁과 조조의 추격을 피해 북상하는 장으로 이어집니다.', + rewardHint: '다음 장: 원소 의탁 준비 중', + pages: fourteenthBattleVictoryPages, + unavailableNotice: '원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 5bb2ab5..7769872 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -877,6 +877,70 @@ export const thirteenthBattleVictoryPages: StoryPage[] = [ } ]; +export const fourteenthBattleIntroPages: StoryPage[] = [ + { + id: 'fourteenth-secret-edict-shadow', + bgm: 'story-dark', + chapter: '조조의 그늘', + background: 'story-liu-bei', + speaker: '유비', + portrait: 'liuBei', + text: '하비의 공은 컸지만 조조의 시선은 더욱 무거워졌다. 허도의 잔치와 칭찬 사이에서, 유비는 황실의 명분과 자신의 뜻이 더는 같은 장막 안에 머물 수 없음을 느꼈다.' + }, + { + id: 'fourteenth-sun-qian-arrives', + bgm: 'militia-theme', + chapter: '새로 온 책사', + background: 'story-militia', + speaker: '손건', + text: '현덕공, 저는 손건이라 합니다. 서주와 허도 사이의 길을 오가며 민심과 조조군의 움직임을 살펴 왔습니다. 이제 떠날 때를 고르신다면, 그 길의 말과 문서를 제가 맡겠습니다.' + }, + { + id: 'fourteenth-brothers-break-from-cao', + bgm: 'battle-prep', + chapter: '서주로 나아가다', + background: 'story-three-heroes', + speaker: '장비', + portrait: 'zhangFei', + text: '조조의 눈치를 보느라 칼집만 만지고 있을 순 없습니다. 형님, 서주 길목의 감시대를 뚫고 백성들이 우리 깃발을 다시 볼 수 있게 합시다.' + }, + { + id: 'fourteenth-xuzhou-gate-sortie', + bgm: 'battle-prep', + chapter: '서주 재기', + background: 'story-sortie', + speaker: '간옹', + text: '차주는 조조의 명을 앞세워 성문과 역참을 잡고 있습니다. 길목을 끊고 차주를 격파하면, 조조의 장막 밖으로 나갈 첫 명분을 세울 수 있습니다.' + } +]; + +export const fourteenthBattleVictoryPages: StoryPage[] = [ + { + id: 'fourteenth-victory-xuzhou-gate-opens', + bgm: 'militia-theme', + chapter: '다시 열린 서주 길', + background: 'story-militia', + text: '차주의 감시대가 흩어지자 서주로 향하는 관문이 열렸다. 백성들은 유비군의 깃발을 알아보았고, 조조의 이름보다 현덕의 이름을 먼저 속삭이기 시작했다.' + }, + { + id: 'fourteenth-cao-cao-anger', + bgm: 'story-dark', + chapter: '돌이킬 수 없는 길', + background: 'story-liu-bei', + speaker: '손건', + text: '이 승리는 조조에게는 반역의 조짐으로 보일 것입니다. 허도로 돌아갈 길은 사실상 끊겼고, 다음으로 기대야 할 큰 세력은 원소입니다.' + }, + { + id: 'fourteenth-road-to-yuan-shao', + bgm: 'battle-prep', + chapter: '원소를 향하여', + background: 'story-three-heroes', + speaker: '유비', + portrait: 'liuBei', + text: '조조를 떠난 이상, 더 큰 파도 속으로 들어가야 한다. 원소의 진영은 안전한 항구가 아니라 또 다른 시험일 것이다. 그래도 뜻을 이어 가려면 그 길을 지나야 한다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1100,6 +1164,12 @@ export const thirteenthBattleMap: BattleMap = { terrain: createThirteenthBattleTerrain() }; +export const fourteenthBattleMap: BattleMap = { + width: 32, + height: 24, + terrain: createFourteenthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -2984,6 +3054,30 @@ export const xuzhouRecruitUnits: UnitData[] = [ } ]; +export const caoBreakRecruitUnits: UnitData[] = [ + { + id: 'sun-qian', + name: '손건', + faction: 'ally', + className: '책사', + classKey: 'strategist', + level: 5, + exp: 10, + hp: 27, + maxHp: 27, + attack: 7, + move: 4, + stats: { might: 38, intelligence: 82, leadership: 68, agility: 62, luck: 78 }, + equipment: { + weapon: { itemId: 'training-sword', level: 1, exp: 8 }, + armor: { itemId: 'cloth-armor', level: 1, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 5, + y: 20 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -4580,6 +4674,336 @@ export const thirteenthBattleUnits: UnitData[] = [ } ]; +const fourteenthBattleAllyPositions: Record = { + 'liu-bei': { x: 3, y: 19 }, + 'guan-yu': { x: 4, y: 18 }, + 'zhang-fei': { x: 4, y: 20 }, + 'jian-yong': { x: 5, y: 19 }, + 'mi-zhu': { x: 3, y: 21 }, + 'sun-qian': { x: 5, y: 21 } +}; + +export const fourteenthBattleUnits: UnitData[] = [ + ...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits, ...caoBreakRecruitUnits].map((unit) => + placeScenarioUnit(unit, fourteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y }) + ), + { + id: 'cao-break-scout-a', + name: '조조군 척후', + faction: 'enemy', + className: '조조 척후', + classKey: 'bandit', + level: 14, + exp: 86, + hp: 52, + maxHp: 52, + attack: 19, + move: 4, + stats: { might: 88, intelligence: 61, leadership: 66, agility: 86, luck: 62 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 26 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 11, + y: 15 + }, + { + id: 'cao-break-scout-b', + name: '조조군 척후', + faction: 'enemy', + className: '조조 척후', + classKey: 'bandit', + level: 14, + exp: 86, + hp: 52, + maxHp: 52, + attack: 19, + move: 4, + stats: { might: 88, intelligence: 61, leadership: 66, agility: 86, luck: 62 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 26 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 13, + y: 20 + }, + { + id: 'cao-break-infantry-a', + name: '조조 보병', + faction: 'enemy', + className: '조조 보병', + classKey: 'yellowTurban', + level: 14, + exp: 86, + hp: 58, + maxHp: 58, + attack: 18, + move: 3, + stats: { might: 90, intelligence: 60, leadership: 76, agility: 68, luck: 60 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 26 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 15, + y: 13 + }, + { + id: 'cao-break-infantry-b', + name: '조조 보병', + faction: 'enemy', + className: '조조 보병', + classKey: 'yellowTurban', + level: 14, + exp: 88, + hp: 59, + maxHp: 59, + attack: 18, + move: 3, + stats: { might: 91, intelligence: 60, leadership: 77, agility: 69, luck: 60 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 28 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 16, + y: 14 + }, + { + id: 'cao-break-infantry-c', + name: '조조 보병', + faction: 'enemy', + className: '조조 보병', + classKey: 'yellowTurban', + level: 14, + exp: 88, + hp: 59, + maxHp: 59, + attack: 18, + move: 3, + stats: { might: 91, intelligence: 60, leadership: 77, agility: 69, luck: 60 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 28 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 19, + y: 10 + }, + { + id: 'cao-break-infantry-d', + name: '조조 보병', + faction: 'enemy', + className: '조조 보병', + classKey: 'yellowTurban', + level: 15, + exp: 90, + hp: 61, + maxHp: 61, + attack: 19, + move: 3, + stats: { might: 92, intelligence: 61, leadership: 79, agility: 70, luck: 61 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 30 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 22, + y: 15 + }, + { + id: 'cao-break-archer-a', + name: '조조 궁병', + faction: 'enemy', + className: '조조 궁병', + classKey: 'archer', + level: 14, + exp: 86, + hp: 48, + maxHp: 48, + attack: 19, + move: 3, + stats: { might: 81, intelligence: 72, leadership: 70, agility: 82, luck: 61 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 62 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 24 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 18, + y: 8 + }, + { + id: 'cao-break-archer-b', + name: '조조 궁병', + faction: 'enemy', + className: '조조 궁병', + classKey: 'archer', + level: 14, + exp: 88, + hp: 49, + maxHp: 49, + attack: 19, + move: 3, + stats: { might: 82, intelligence: 73, leadership: 71, agility: 83, luck: 61 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 64 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 26 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 24, + y: 9 + }, + { + id: 'cao-break-archer-c', + name: '조조 궁병', + faction: 'enemy', + className: '조조 궁병', + classKey: 'archer', + level: 14, + exp: 88, + hp: 49, + maxHp: 49, + attack: 19, + move: 3, + stats: { might: 82, intelligence: 73, leadership: 71, agility: 83, luck: 61 }, + equipment: { + weapon: { itemId: 'short-bow', level: 2, exp: 64 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 26 }, + accessory: { itemId: 'wind-quiver', level: 1, exp: 0 } + }, + x: 26, + y: 16 + }, + { + id: 'cao-break-cavalry-a', + name: '조조 기병', + faction: 'enemy', + className: '조조 경기병', + classKey: 'cavalry', + level: 15, + exp: 90, + hp: 66, + maxHp: 66, + attack: 22, + move: 5, + stats: { might: 100, intelligence: 58, leadership: 80, agility: 100, luck: 63 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 30 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 20, + y: 6 + }, + { + id: 'cao-break-cavalry-b', + name: '조조 기병', + faction: 'enemy', + className: '조조 경기병', + classKey: 'cavalry', + level: 15, + exp: 90, + hp: 66, + maxHp: 66, + attack: 22, + move: 5, + stats: { might: 100, intelligence: 58, leadership: 80, agility: 100, luck: 63 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 66 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 30 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 25, + y: 13 + }, + { + id: 'cao-break-cavalry-c', + name: '조조 기병', + faction: 'enemy', + className: '조조 경기병', + classKey: 'cavalry', + level: 15, + exp: 92, + hp: 68, + maxHp: 68, + attack: 22, + move: 5, + stats: { might: 101, intelligence: 59, leadership: 81, agility: 101, luck: 64 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 }, + armor: { itemId: 'rebel-vest', level: 2, exp: 32 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 28, + y: 18 + }, + { + id: 'cao-break-guard-a', + name: '서주 관문 수비대', + faction: 'enemy', + className: '조조 수비대', + classKey: 'yellowTurban', + level: 15, + exp: 92, + hp: 64, + maxHp: 64, + attack: 19, + move: 3, + stats: { might: 93, intelligence: 64, leadership: 82, agility: 70, luck: 62 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 25, + y: 11 + }, + { + id: 'cao-break-guard-b', + name: '서주 관문 수비대', + faction: 'enemy', + className: '조조 수비대', + classKey: 'yellowTurban', + level: 15, + exp: 92, + hp: 64, + maxHp: 64, + attack: 19, + move: 3, + stats: { might: 93, intelligence: 64, leadership: 82, agility: 70, luck: 62 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 68 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 }, + accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 } + }, + x: 26, + y: 12 + }, + { + id: 'cao-break-leader-che-zhou', + name: '차주', + faction: 'enemy', + className: '조조군 장수', + classKey: 'rebelLeader', + level: 17, + exp: 98, + hp: 88, + maxHp: 88, + attack: 24, + move: 4, + stats: { might: 102, intelligence: 72, leadership: 92, agility: 78, luck: 65 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 3, exp: 10 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 36 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 27, + y: 11 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -4626,6 +5050,25 @@ export const xuzhouRecruitBonds: BattleBond[] = [ } ]; +export const caoBreakRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__sun-qian', + unitIds: ['liu-bei', 'sun-qian'], + title: '문서와 명분', + level: 40, + exp: 0, + description: '손건은 조조의 감시망 속에서 유비군이 움직일 명분과 길을 정리해 주는 책사입니다.' + }, + { + id: 'jian-yong__sun-qian', + unitIds: ['jian-yong', 'sun-qian'], + title: '말과 글', + level: 34, + exp: 0, + description: '간옹의 말과 손건의 문서는 유비군이 난세의 틈을 지나갈 때 서로를 보완합니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -4638,6 +5081,9 @@ export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBo export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario); export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario); export const thirteenthBattleBonds: BattleBond[] = twelfthBattleBonds.map(cloneBattleBondForScenario); +export const fourteenthBattleBonds: BattleBond[] = [...thirteenthBattleBonds, ...caoBreakRecruitBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -4888,6 +5334,60 @@ function createThirteenthBattleTerrain(): TerrainType[][] { ); } +function createFourteenthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 24 }, (_, y) => + Array.from({ length: 32 }, (_, x): TerrainType => { + if (x <= 2 && y >= 18 && y <= 22) { + return 'camp'; + } + if ( + (x >= 23 && x <= 29 && y >= 8 && y <= 14) || + (x >= 25 && x <= 30 && y >= 15 && y <= 18) + ) { + return 'fort'; + } + if ((x >= 10 && x <= 12 && y >= 16 && y <= 18) || (x >= 17 && x <= 19 && y >= 10 && y <= 12)) { + return 'village'; + } + if ((x === 15 || x === 16) && y >= 0 && y <= 22) { + return 'river'; + } + if ((x === 21 || x === 22) && y >= 4 && y <= 20) { + return 'river'; + } + if ( + (y === 19 && x >= 2 && x <= 20) || + (y === 12 && x >= 12 && x <= 27) || + (x === 24 && y >= 7 && y <= 18) + ) { + return 'road'; + } + if ((x >= 3 && x <= 16 && y === 22 - x) || (x >= 9 && x <= 24 && y === x - 4)) { + return 'road'; + } + if ( + (x <= 8 && y <= 7) || + (x >= 4 && x <= 12 && y >= 8 && y <= 13) || + (x >= 2 && x <= 9 && y >= 14 && y <= 17) || + (x >= 27 && y >= 18 && y <= 22) + ) { + return 'forest'; + } + if ( + (x >= 6 && x <= 11 && y >= 2 && y <= 5) || + (x >= 27 && x <= 31 && y >= 1 && y <= 6) || + (x >= 9 && x <= 13 && y >= 20) + ) { + return 'hill'; + } + if ((x >= 30 && y <= 7) || (x >= 30 && 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 31b0224..df8bff9 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -88,7 +88,22 @@ const unitTexture: Record = { 'xiapi-final-raider-a': 'unit-rebel', 'xiapi-final-raider-b': 'unit-rebel', 'xiapi-final-strategist-chen-gong': 'unit-rebel-archer', - 'xiapi-final-leader-lu-bu': 'unit-rebel-leader' + 'xiapi-final-leader-lu-bu': 'unit-rebel-leader', + 'cao-break-scout-a': 'unit-rebel', + 'cao-break-scout-b': 'unit-rebel', + 'cao-break-infantry-a': 'unit-rebel', + 'cao-break-infantry-b': 'unit-rebel', + 'cao-break-infantry-c': 'unit-rebel', + 'cao-break-infantry-d': 'unit-rebel', + 'cao-break-archer-a': 'unit-rebel-archer', + 'cao-break-archer-b': 'unit-rebel-archer', + 'cao-break-archer-c': 'unit-rebel-archer', + 'cao-break-cavalry-a': 'unit-rebel-cavalry', + 'cao-break-cavalry-b': 'unit-rebel-cavalry', + 'cao-break-cavalry-c': 'unit-rebel-cavalry', + 'cao-break-guard-a': 'unit-rebel', + 'cao-break-guard-b': 'unit-rebel', + 'cao-break-leader-che-zhou': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -510,7 +525,8 @@ const initialItemStocks: Record> = { 'guan-yu': { bean: 1 }, 'zhang-fei': { bean: 1, wine: 1 }, 'jian-yong': { bean: 1 }, - 'mi-zhu': { bean: 2, salve: 1 } + 'mi-zhu': { bean: 2, salve: 1 }, + 'sun-qian': { bean: 1, salve: 1 } }; const attackRangeByClass: Partial> = { @@ -624,7 +640,22 @@ const enemyAiByUnitId: Record = { 'xiapi-final-raider-a': 'aggressive', 'xiapi-final-raider-b': 'aggressive', 'xiapi-final-strategist-chen-gong': 'hold', - 'xiapi-final-leader-lu-bu': 'guard' + 'xiapi-final-leader-lu-bu': 'guard', + 'cao-break-scout-a': 'aggressive', + 'cao-break-scout-b': 'aggressive', + 'cao-break-infantry-a': 'guard', + 'cao-break-infantry-b': 'guard', + 'cao-break-infantry-c': 'guard', + 'cao-break-infantry-d': 'guard', + 'cao-break-archer-a': 'hold', + 'cao-break-archer-b': 'hold', + 'cao-break-archer-c': 'hold', + 'cao-break-cavalry-a': 'aggressive', + 'cao-break-cavalry-b': 'aggressive', + 'cao-break-cavalry-c': 'aggressive', + 'cao-break-guard-a': 'guard', + 'cao-break-guard-b': 'guard', + 'cao-break-leader-che-zhou': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 16ae100..d3f205e 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -3,6 +3,7 @@ import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg'; import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg'; import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png'; +import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg'; import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg'; import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg'; import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg'; @@ -84,6 +85,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-eleventh', eleventhBattleMapUrl); this.load.image('battle-map-twelfth', twelfthBattleMapUrl); this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl); + this.load.image('battle-map-fourteenth', fourteenthBattleMapUrl); 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 4babd3f..a968098 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -4,7 +4,16 @@ import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRules'; import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles'; import { getSortieFlow } from '../data/campaignFlow'; -import { firstBattleBonds, firstBattleUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, type UnitData } from '../data/scenario'; +import { + caoBreakRecruitBonds, + caoBreakRecruitUnits, + firstBattleBonds, + firstBattleUnits, + xuzhouRecruitBonds, + xuzhouRecruitUnits, + type PortraitKey, + type UnitData +} from '../data/scenario'; import { applyCampBondExp, ensureCampaignRosterUnits, @@ -183,7 +192,8 @@ const campBattleIds = { tenth: 'tenth-battle-xuzhou-breakout', eleventh: 'eleventh-battle-xudu-refuge-road', twelfth: 'twelfth-battle-xiapi-outer-siege', - thirteenth: 'thirteenth-battle-xiapi-final' + thirteenth: 'thirteenth-battle-xiapi-final', + fourteenth: 'fourteenth-battle-cao-break' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -316,6 +326,17 @@ const sortieRulesByBattleId: Partial