From f8cc2f329cc3ea415c7f1070e7b2247e380e5398 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 07:58:54 +0900 Subject: [PATCH] Add Guiyang persuasion chapter --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 160 ++++- .../battle/twenty-fourth-battle-map.svg | 86 +++ src/game/data/battles.ts | 73 ++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 563 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 45 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 186 +++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1134 insertions(+), 28 deletions(-) create mode 100644 src/assets/images/battle/twenty-fourth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 58bf091..c0aac6a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -45,15 +45,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Twenty-first battle Red Cliffs vanguard route, opening the first allied river-line clash with Cai Mao, six-officer sortie pressure, and post-battle fire-attack preparation events - Twenty-second battle Red Cliffs fire attack, turning Huang Gai's bitter-flesh plan and Zhuge Liang's wind timing into the decisive fire battle against Cao Cao's chained fleet - Twenty-third battle Jing Province south entry route, opening the post-Red-Cliffs land campaign with Xing Daorong, village stabilization, a larger 46x32 battlefield, and Ma Liang recruitment after victory +- Twenty-fourth battle Guiyang persuasion route, using Ma Liang's local knowledge in sortie selection, expanding the southern-commandery map to 48x32, and recruiting Yi Ji after Zhao Fan yields - Camp progress timeline tab that summarizes Liu Bei's long campaign arc, completed battles, current chapter, latest battle, and next major chapter - 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 - Officer collection support in camp, including full roster status, selected/reserve/recommended markers, and post-battle reserve training growth for benched officers - Treasure equipment effects wired into battle previews and resolution, including named weapon damage bonuses, defensive treasure mitigation, support recovery bonuses, turn-start charm recovery, and equipment-specific growth bonuses -- Flow verification script from title through the twenty-third battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province camp states, campaign timeline state, and camp save state +- Flow verification script from title through the twenty-fourth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue Jing Province south from the first entry battle into Guiyang, Wuling, and Changsha acquisition +1. Continue Jing Province south from Guiyang into Wuling and Changsha acquisition 2. Add more recruitable officers as the campaign moves through Jing Province toward Huang Zhong, Wei Yan, Yi Province, and Shu Han 3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 2dfb144..549e10c 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1736,7 +1736,7 @@ try { if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || - progressTabState.campaignProgress?.totalKnown !== 23 || + progressTabState.campaignProgress?.totalKnown !== 24 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' @@ -1888,7 +1888,7 @@ try { if ( postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState.campaignProgress?.completedKnown !== 18 || - postBowangProgressState.campaignProgress?.totalKnown !== 23 || + postBowangProgressState.campaignProgress?.totalKnown !== 24 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' @@ -2026,7 +2026,7 @@ try { if ( postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState.campaignProgress?.completedKnown !== 19 || - postChangbanProgressState.campaignProgress?.totalKnown !== 23 || + postChangbanProgressState.campaignProgress?.totalKnown !== 24 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' @@ -2164,7 +2164,7 @@ try { if ( postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || - postJiangdongProgressState.campaignProgress?.totalKnown !== 23 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 24 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' @@ -2304,7 +2304,7 @@ try { if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || - postRedCliffsProgressState.campaignProgress?.totalKnown !== 23 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 24 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' @@ -2444,7 +2444,7 @@ try { if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || - postFireAttackProgressState.campaignProgress?.totalKnown !== 23 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 24 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2587,15 +2587,157 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 23 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 24 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || - postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' ) { throw new Error(`Expected post-Jing Province progress tab to complete the new chapter opening and pause on next commandery preparation: ${JSON.stringify(postJingzhouProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-jingzhou-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const guiyangSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !guiyangSortieState?.sortieVisible || + !guiyangSortieState.sortiePlan?.objectiveLine?.includes('계양 설득전') || + !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited && unit.recommended) || + !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || + !guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'sun-qian' && unit.recruited && unit.recommended) || + guiyangSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji') || + guiyangSortieState.sortieRoster?.length < 9 || + guiyangSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-third camp sortie prep to target Guiyang with Ma Liang recommended and Yi Ji absent before victory: ${JSON.stringify(guiyangSortieState)}`); + } + assertSortieTacticalRoster(guiyangSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang' + ]); + + const guiyangPriorityUnits = ['ma-liang', 'zhuge-liang', 'zhao-yun', 'guan-yu', 'sun-qian']; + for (const unitId of guiyangPriorityUnits) { + const currentSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if (!currentSortieState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)) { + const removable = currentSortieState.sortieRoster?.find( + (unit) => unit.selected && unit.id !== 'liu-bei' && !guiyangPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const guiyangSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !guiyangPriorityUnits.every((unitId) => + guiyangSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + guiyangSortieReadyState.sortiePlan?.selectedCount !== 6 || + guiyangSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Guiyang sortie to deploy Ma Liang and other recommended officers while preserving six-officer pressure: ${JSON.stringify(guiyangSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-guiyang-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-guiyang-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentyFourthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fourth-battle-guiyang-persuasion'; + }); + if (enteredTwentyFourthBattle) { + 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 === 'twenty-fourth-battle-guiyang-persuasion' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-fourth-battle.png', fullPage: true }); + + const twentyFourthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentyFourthEnemies = twentyFourthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentyFourthAllies = twentyFourthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentyFourthEnemyBehaviors = new Set(twentyFourthEnemies.map((unit) => unit.ai)); + if ( + twentyFourthBattleState.camera?.mapWidth !== 48 || + twentyFourthBattleState.camera?.mapHeight !== 32 || + twentyFourthBattleState.victoryConditionLabel !== '조범 항복' || + twentyFourthEnemies.length < 17 || + !twentyFourthEnemyBehaviors.has('aggressive') || + !twentyFourthEnemyBehaviors.has('guard') || + !twentyFourthEnemyBehaviors.has('hold') || + !twentyFourthEnemies.some((unit) => unit.id === 'guiyang-leader-zhao-fan') || + !twentyFourthAllies.some((unit) => unit.id === 'ma-liang') || + !twentyFourthAllies.some((unit) => unit.id === 'zhuge-liang') || + !twentyFourthAllies.some((unit) => unit.id === 'sun-qian') || + twentyFourthAllies.some((unit) => unit.id === 'yi-ji') + ) { + throw new Error(`Expected twenty-fourth battle to use Guiyang map, Zhao Fan objective, mixed AI, selected allied officers, and no Yi Ji before victory: ${JSON.stringify(twentyFourthBattleState)}`); + } + + 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 twentyFourthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentyFourthCampState?.campBattleId !== 'twenty-fourth-battle-guiyang-persuasion' || + twentyFourthCampState.campTitle !== '계양 설득전 후 군영' || + twentyFourthCampState.availableDialogueIds?.length !== 3 || + !twentyFourthCampState.availableDialogueIds.every((id) => id.endsWith('guiyang')) || + twentyFourthCampState.availableVisitIds?.length !== 2 || + !twentyFourthCampState.campaign?.roster?.some((unit) => unit.id === 'yi-ji') || + !twentyFourthCampState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited) || + twentyFourthCampState.rosterCollection?.total < 10 + ) { + throw new Error(`Expected twenty-fourth camp to recruit Yi Ji and expose Guiyang dialogue/visit sets: ${JSON.stringify(twentyFourthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-guiyang-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postGuiyangProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postGuiyangProgressState?.activeTab !== 'progress' || + postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 24 || + postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || + postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Guiyang progress tab to complete the new commandery battle and pause on Wuling preparation: ${JSON.stringify(postGuiyangProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-guiyang-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -2607,7 +2749,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-twenty-third-battle flow, recruited officer sortie selection, Ma Liang join, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twenty-fourth-battle flow, recruited officer sortie selection, Ma Liang and Yi Ji joins, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twenty-fourth-battle-map.svg b/src/assets/images/battle/twenty-fourth-battle-map.svg new file mode 100644 index 0000000..ca557cc --- /dev/null +++ b/src/assets/images/battle/twenty-fourth-battle-map.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 573bbe9..0467051 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -51,6 +51,10 @@ import { twentyThirdBattleMap, twentyThirdBattleUnits, twentyThirdBattleVictoryPages, + twentyFourthBattleBonds, + twentyFourthBattleMap, + twentyFourthBattleUnits, + twentyFourthBattleVictoryPages, twentiethBattleBonds, twentiethBattleMap, twentiethBattleUnits, @@ -120,7 +124,8 @@ export type BattleScenarioId = | 'twentieth-battle-jiangdong-envoy' | 'twenty-first-battle-red-cliffs-vanguard' | 'twenty-second-battle-red-cliffs-fire' - | 'twenty-third-battle-jingzhou-south-entry'; + | 'twenty-third-battle-jingzhou-south-entry' + | 'twenty-fourth-battle-guiyang-persuasion'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1429,6 +1434,69 @@ export const twentyThirdBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentyFourthBattleScenario: BattleScenarioDefinition = { + id: 'twenty-fourth-battle-guiyang-persuasion', + title: '계양 설득전', + victoryConditionLabel: '조범 항복', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '형주 남부의 첫 길목을 연 유비군은 계양으로 향합니다. 이번 전투는 성채를 불태우는 싸움이 아니라 조범의 마음을 굽히는 싸움입니다.', + '마량의 서신과 제갈량의 계책이 맞물리려면 마을을 안정시키고, 성채 주변 친위대를 물리쳐야 합니다.', + '유비와 제갈량을 지키며 조범을 항복시키면, 형주의 문사 이적이 군영에 합류합니다.' + ], + map: twentyFourthBattleMap, + units: twentyFourthBattleUnits, + bonds: twentyFourthBattleBonds, + mapTextureKey: 'battle-map-twenty-fourth', + leaderUnitId: 'guiyang-leader-zhao-fan', + quickVictoryTurnLimit: 33, + baseVictoryGold: 3180, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '조범 항복', + rewardGold: 2100, + unitId: 'guiyang-leader-zhao-fan' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 620, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 880, + unitId: 'zhuge-liang' + }, + { + id: 'guiyang-village', + kind: 'secure-terrain', + label: '계양 마을 안정', + rewardGold: 1040, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '33턴 이내 승리', + rewardGold: 800, + maxTurn: 33 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 10', '상처약 7', '탁주 4', '계양 설득 문서 +1', '이적 합류'], + victoryPages: twentyFourthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1454,7 +1522,8 @@ export const battleScenarios: Record 'twentieth-battle-jiangdong-envoy': twentiethBattleScenario, 'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario, 'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario, - 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario + 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario, + 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 469220a..475d3e4 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -20,6 +20,7 @@ import { twentyFirstBattleScenario, twentySecondBattleScenario, twentyThirdBattleScenario, + twentyFourthBattleScenario, twentiethBattleScenario, twelfthBattleScenario, thirdBattleScenario, @@ -65,6 +66,8 @@ import { twentySecondBattleVictoryPages, twentyThirdBattleIntroPages, twentyThirdBattleVictoryPages, + twentyFourthBattleIntroPages, + twentyFourthBattleVictoryPages, twentiethBattleIntroPages, twentiethBattleVictoryPages, twelfthBattleIntroPages, @@ -319,13 +322,24 @@ const sortieFlows: Record = { }, [twentyThirdBattleScenario.id]: { afterBattleId: twentyThirdBattleScenario.id, - eyebrow: '다음 장 준비', - title: '영릉 안정과 다음 군현', + eyebrow: '다음 전장', + title: twentyFourthBattleScenario.title, description: - '형주 남부의 첫 길목을 열고 마량이 합류했습니다. 이제 계양, 무릉, 장사까지 차례로 설득하고 지켜야 형주·익주 확보의 기반이 단단해집니다.', - rewardHint: '다음 장: 계양·무릉·장사 확보 준비 중', - pages: twentyThirdBattleVictoryPages, - unavailableNotice: '다음 형주 남부 군현 확보 장은 다음 작업에서 이어집니다. 군영에서 마량과 대화하고 출전 후보를 정비하십시오.' + '형주 남부의 첫 길목을 열고 마량이 합류했습니다. 이제 마량의 현지 정보와 유비군의 군율을 앞세워 계양의 문을 열어야 합니다.', + rewardHint: `예상 보상: ${twentyFourthBattleScenario.title} 개방 / 이적 합류`, + nextBattleId: twentyFourthBattleScenario.id, + campaignStep: 'twenty-fourth-battle', + pages: [...twentyThirdBattleVictoryPages, ...twentyFourthBattleIntroPages] + }, + [twentyFourthBattleScenario.id]: { + afterBattleId: twentyFourthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '무릉 산길 준비', + description: + '계양이 열리고 이적이 합류했습니다. 다음은 험한 산길과 흩어진 마을이 기다리는 무릉으로 향해야 합니다.', + rewardHint: '다음 장: 무릉 산길 확보 준비 중', + pages: twentyFourthBattleVictoryPages, + unavailableNotice: '무릉 산길 확보 장은 다음 작업에서 이어집니다. 군영에서 이적과 대화하고 산길 전투를 준비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 6695f33..293f5f3 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1510,6 +1510,68 @@ export const twentyThirdBattleVictoryPages: StoryPage[] = [ } ]; +export const twentyFourthBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-fourth-guiyang-cause', + bgm: 'militia-theme', + chapter: '계양으로 가는 서신', + background: 'story-militia', + speaker: '마량', + text: '계양의 조범은 항복과 저항 사이에서 눈치를 보고 있습니다. 먼저 서신을 보내되, 길목의 군사들이 백성을 겁박하지 못하게 막아야 합니다.' + }, + { + id: 'twenty-fourth-zhuge-plan', + bgm: 'battle-prep', + chapter: '칼보다 먼저 놓는 글', + background: 'story-three-heroes', + speaker: '제갈량', + text: '이번 전투는 성을 불태우는 싸움이 아닙니다. 마을을 안정시키고 조범의 친위대를 물리치면, 계양은 스스로 문을 열 것입니다.' + }, + { + id: 'twenty-fourth-yi-ji-rumor', + bgm: 'story-dark', + chapter: '이적의 이름', + background: 'story-liu-bei', + speaker: '손건', + text: '형주의 문사 이적이 계양의 움직임을 지켜보고 있다고 합니다. 무리한 점령보다 예를 갖춘 설득을 보이면, 그 역시 길을 함께할지 모릅니다.' + }, + { + id: 'twenty-fourth-guiyang-sortie', + bgm: 'battle-prep', + chapter: '계양 설득전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '오늘의 목적은 계양을 꺾는 것이 아니라 열어 보이는 것이오. 마을을 지키고 성채를 압박하되, 백성이 다시 문을 닫지 않게 하시오.' + } +]; + +export const twentyFourthBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-fourth-victory-guiyang-open', + bgm: 'militia-theme', + chapter: '계양의 문이 열리다', + background: 'story-sortie', + text: '조범의 친위대가 물러나자 계양의 성문은 불길이 아니라 서신 앞에서 열렸습니다. 유비군은 군율을 세우고 마을의 창고를 함부로 건드리지 않았습니다.' + }, + { + id: 'twenty-fourth-yi-ji-joins', + bgm: 'battle-prep', + chapter: '이적의 합류', + background: 'story-militia', + speaker: '이적', + text: '주공께서 계양을 힘으로만 다루지 않으셨다는 말을 들었습니다. 형주의 글과 예가 필요하다면, 저도 붓과 길을 보태겠습니다.' + }, + { + id: 'twenty-fourth-next-commandery', + bgm: 'story-dark', + chapter: '험한 무릉의 길', + background: 'story-three-heroes', + speaker: '제갈량', + text: '계양은 열렸지만 무릉의 산길은 아직 닫혀 있습니다. 다음에는 험한 길을 뚫고 흩어진 마을을 잇는 싸움이 될 것입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1793,6 +1855,12 @@ export const twentyThirdBattleMap: BattleMap = { terrain: createTwentyThirdBattleTerrain() }; +export const twentyFourthBattleMap: BattleMap = { + width: 48, + height: 32, + terrain: createTwentyFourthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -3773,6 +3841,30 @@ export const jingzhouRecruitUnits: UnitData[] = [ } ]; +export const guiyangRecruitUnits: UnitData[] = [ + { + id: 'yi-ji', + name: '이적', + faction: 'ally', + className: '형주 문사', + classKey: 'quartermaster', + level: 12, + exp: 0, + hp: 32, + maxHp: 32, + attack: 8, + move: 4, + stats: { might: 38, intelligence: 88, leadership: 82, agility: 68, luck: 84 }, + equipment: { + weapon: { itemId: 'white-feather-fan', level: 1, exp: 8 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 10 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 8, + y: 22 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -9070,6 +9162,386 @@ export const twentyThirdBattleUnits: UnitData[] = [ } ]; +const twentyFourthBattleAllyPositions: Record = { + 'liu-bei': { x: 5, y: 24 }, + 'guan-yu': { x: 7, y: 22 }, + 'zhang-fei': { x: 7, y: 27 }, + 'jian-yong': { x: 4, y: 28 }, + 'mi-zhu': { x: 5, y: 29 }, + 'sun-qian': { x: 9, y: 25 }, + 'zhao-yun': { x: 9, y: 22 }, + 'zhuge-liang': { x: 6, y: 24 }, + 'ma-liang': { x: 8, y: 26 } +}; + +export const twentyFourthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentyFourthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'guiyang-scout-a', + name: '계양 산길 척후', + faction: 'enemy', + className: '산길 척후', + classKey: 'bandit', + level: 31, + exp: 50, + hp: 86, + maxHp: 86, + attack: 34, + move: 4, + stats: { might: 116, intelligence: 86, leadership: 92, agility: 112, luck: 78 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 22 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 15, + y: 23 + }, + { + id: 'guiyang-scout-b', + name: '계양 산길 척후', + faction: 'enemy', + className: '서문 척후', + classKey: 'bandit', + level: 31, + exp: 50, + hp: 86, + maxHp: 86, + attack: 34, + move: 4, + stats: { might: 116, intelligence: 86, leadership: 92, agility: 112, luck: 78 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 22 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 10 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 17, + y: 28 + }, + { + id: 'guiyang-infantry-a', + name: '계양 보병', + faction: 'enemy', + className: '계양 보병', + classKey: 'yellowTurban', + level: 32, + exp: 52, + hp: 112, + maxHp: 112, + attack: 36, + move: 3, + stats: { might: 126, intelligence: 86, leadership: 108, agility: 94, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 34 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 20 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 22, + y: 21 + }, + { + id: 'guiyang-infantry-b', + name: '계양 보병', + faction: 'enemy', + className: '마을 보병', + classKey: 'yellowTurban', + level: 32, + exp: 52, + hp: 112, + maxHp: 112, + attack: 36, + move: 3, + stats: { might: 126, intelligence: 86, leadership: 108, agility: 94, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 34 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 20 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 24, + y: 25 + }, + { + id: 'guiyang-infantry-c', + name: '계양 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 33, + exp: 54, + hp: 116, + maxHp: 116, + attack: 37, + move: 3, + stats: { might: 128, intelligence: 86, leadership: 110, agility: 94, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 38 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 35, + y: 18 + }, + { + id: 'guiyang-infantry-d', + name: '계양 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 33, + exp: 54, + hp: 116, + maxHp: 116, + attack: 37, + move: 3, + stats: { might: 128, intelligence: 86, leadership: 110, agility: 94, luck: 78 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 38 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 22 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 38, + y: 21 + }, + { + id: 'guiyang-archer-a', + name: '계양 궁병', + faction: 'enemy', + className: '계양 궁병', + classKey: 'archer', + level: 32, + exp: 50, + hp: 84, + maxHp: 84, + attack: 34, + move: 3, + stats: { might: 88, intelligence: 92, leadership: 96, agility: 104, luck: 80 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 32 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 18 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 28, + y: 17 + }, + { + id: 'guiyang-archer-b', + name: '계양 궁병', + faction: 'enemy', + className: '언덕 궁병', + classKey: 'archer', + level: 32, + exp: 50, + hp: 84, + maxHp: 84, + attack: 34, + move: 3, + stats: { might: 88, intelligence: 92, leadership: 96, agility: 104, luck: 80 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 32 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 18 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 31, + y: 22 + }, + { + id: 'guiyang-archer-c', + name: '계양 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 33, + exp: 52, + hp: 86, + maxHp: 86, + attack: 35, + move: 3, + stats: { might: 90, intelligence: 94, leadership: 98, agility: 104, luck: 80 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 36 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 20 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 41, + y: 16 + }, + { + id: 'guiyang-cavalry-a', + name: '계양 기병', + faction: 'enemy', + className: '계양 기병', + classKey: 'cavalry', + level: 33, + exp: 54, + hp: 122, + maxHp: 122, + attack: 38, + move: 5, + stats: { might: 130, intelligence: 86, leadership: 108, agility: 112, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 40 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 24 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 26, + y: 28 + }, + { + id: 'guiyang-cavalry-b', + name: '계양 기병', + faction: 'enemy', + className: '남문 기병', + classKey: 'cavalry', + level: 33, + exp: 54, + hp: 122, + maxHp: 122, + attack: 38, + move: 5, + stats: { might: 130, intelligence: 86, leadership: 108, agility: 112, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 40 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 24 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 34, + y: 26 + }, + { + id: 'guiyang-cavalry-c', + name: '계양 기병', + faction: 'enemy', + className: '성채 기병', + classKey: 'cavalry', + level: 34, + exp: 56, + hp: 126, + maxHp: 126, + attack: 39, + move: 5, + stats: { might: 132, intelligence: 88, leadership: 110, agility: 114, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 42 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 26 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 39, + y: 24 + }, + { + id: 'guiyang-guard-a', + name: '조범 친위대', + faction: 'enemy', + className: '계양 친위대', + classKey: 'yellowTurban', + level: 34, + exp: 58, + hp: 124, + maxHp: 124, + attack: 39, + move: 3, + stats: { might: 132, intelligence: 88, leadership: 114, agility: 96, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 44 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 42, + y: 18 + }, + { + id: 'guiyang-guard-b', + name: '조범 친위대', + faction: 'enemy', + className: '계양 친위대', + classKey: 'yellowTurban', + level: 34, + exp: 58, + hp: 124, + maxHp: 124, + attack: 39, + move: 3, + stats: { might: 132, intelligence: 88, leadership: 114, agility: 96, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 44 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 43, + y: 21 + }, + { + id: 'guiyang-strategist-a', + name: '계양 책사', + faction: 'enemy', + className: '계양 책사', + classKey: 'archer', + level: 33, + exp: 54, + hp: 88, + maxHp: 88, + attack: 35, + move: 3, + stats: { might: 86, intelligence: 108, leadership: 100, agility: 92, luck: 82 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 34 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 18 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 37, + y: 16 + }, + { + id: 'guiyang-strategist-b', + name: '계양 책사', + faction: 'enemy', + className: '성내 책사', + classKey: 'archer', + level: 33, + exp: 54, + hp: 88, + maxHp: 88, + attack: 35, + move: 3, + stats: { might: 86, intelligence: 108, leadership: 100, agility: 92, luck: 82 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 34 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 18 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 40, + y: 19 + }, + { + id: 'guiyang-leader-zhao-fan', + name: '조범', + faction: 'enemy', + className: '계양 태수', + classKey: 'rebelLeader', + level: 35, + exp: 60, + hp: 172, + maxHp: 172, + attack: 42, + move: 4, + stats: { might: 134, intelligence: 96, leadership: 122, agility: 98, luck: 84 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 4, exp: 62 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 44 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 44, + y: 19 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -9224,6 +9696,33 @@ export const jingzhouRecruitBonds: BattleBond[] = [ } ]; +export const guiyangRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__yi-ji', + unitIds: ['liu-bei', 'yi-ji'], + title: '예로 여는 문', + level: 34, + exp: 0, + description: '유비의 인의와 이적의 예법이 맞물려 형주의 닫힌 성문을 말과 글로 열어 갑니다.' + }, + { + id: 'ma-liang__yi-ji', + unitIds: ['ma-liang', 'yi-ji'], + title: '백미와 문사', + level: 32, + exp: 0, + description: '마량의 현지 기록과 이적의 의전 감각이 더해져 남부 군현의 설득력이 깊어집니다.' + }, + { + id: 'sun-qian__yi-ji', + unitIds: ['sun-qian', 'yi-ji'], + title: '외교 문안', + level: 30, + exp: 0, + description: '손건의 외교 문서와 이적의 형주 예법이 만나 싸우지 않고 길을 여는 선택지를 늘립니다.' + } +]; + export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); @@ -9252,6 +9751,7 @@ export const twentiethBattleBonds: BattleBond[] = [...nineteenthBattleBonds].map export const twentyFirstBattleBonds: BattleBond[] = [...twentiethBattleBonds].map(cloneBattleBondForScenario); export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds].map(cloneBattleBondForScenario); export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario); +export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -10049,6 +10549,69 @@ function createTwentyThirdBattleTerrain(): TerrainType[][] { ); } +function createTwentyFourthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 32 }, (_, y) => + Array.from({ length: 48 }, (_, x): TerrainType => { + if ((x <= 5 && y >= 23 && y <= 30) || (x >= 6 && x <= 10 && y >= 27 && y <= 31)) { + return 'camp'; + } + if ((x >= 40 && x <= 47 && y >= 15 && y <= 23) || (x >= 36 && x <= 40 && y >= 17 && y <= 21)) { + return 'fort'; + } + if ( + (x >= 14 && x <= 17 && y >= 22 && y <= 25) || + (x >= 27 && x <= 30 && y >= 17 && y <= 19) || + (x >= 35 && x <= 37 && y >= 13 && y <= 15) + ) { + return 'village'; + } + if ( + (y === 24 && x >= 4 && x <= 20) || + (y === 21 && x >= 18 && x <= 32) || + (y === 18 && x >= 31 && x <= 43) || + (x === 42 && y >= 15 && y <= 25) + ) { + return 'road'; + } + if ( + (x >= 8 && x <= 20 && y === 29 - Math.floor(x / 2)) || + (x >= 22 && x <= 38 && y === 28 - Math.floor(x / 3)) || + (x >= 32 && x <= 42 && y === 17) + ) { + return 'road'; + } + if ((x === 22 || x === 23) && y >= 0 && y <= 31) { + return 'river'; + } + if ((x === 32 || x === 33) && y >= 0 && y <= 22) { + return 'river'; + } + if ((x === 28 || x === 29) && y >= 25 && y <= 31) { + return 'river'; + } + if ( + (x >= 4 && x <= 16 && y >= 7 && y <= 18) || + (x >= 15 && x <= 22 && y >= 25 && y <= 31) || + (x <= 10 && y <= 8) || + (x >= 31 && x <= 45 && y <= 10) + ) { + return 'forest'; + } + if ( + (x >= 10 && x <= 20 && y >= 2 && y <= 7) || + (x >= 31 && x <= 40 && y >= 24 && y <= 31) || + (x >= 38 && x <= 47 && y >= 4 && y <= 9) + ) { + return 'hill'; + } + if ((x <= 1 && y <= 15) || (x >= 46 && y <= 12) || (x >= 46 && y >= 24)) { + 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 2e3e09d..701be35 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -30,6 +30,7 @@ const unitTexture: Record = { 'zhao-yun': 'unit-rebel-cavalry', 'zhuge-liang': 'unit-liu-bei', 'ma-liang': 'unit-liu-bei', + 'yi-ji': 'unit-liu-bei', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -258,7 +259,24 @@ const unitTexture: Record = { 'jing-south-guard-b': 'unit-rebel', 'jing-south-strategist-a': 'unit-rebel-archer', 'jing-south-strategist-b': 'unit-rebel-archer', - 'jing-south-leader-xing-daorong': 'unit-rebel-leader' + 'jing-south-leader-xing-daorong': 'unit-rebel-leader', + 'guiyang-scout-a': 'unit-rebel', + 'guiyang-scout-b': 'unit-rebel', + 'guiyang-infantry-a': 'unit-rebel', + 'guiyang-infantry-b': 'unit-rebel', + 'guiyang-infantry-c': 'unit-rebel', + 'guiyang-infantry-d': 'unit-rebel', + 'guiyang-archer-a': 'unit-rebel-archer', + 'guiyang-archer-b': 'unit-rebel-archer', + 'guiyang-archer-c': 'unit-rebel-archer', + 'guiyang-cavalry-a': 'unit-rebel-cavalry', + 'guiyang-cavalry-b': 'unit-rebel-cavalry', + 'guiyang-cavalry-c': 'unit-rebel-cavalry', + 'guiyang-guard-a': 'unit-rebel', + 'guiyang-guard-b': 'unit-rebel', + 'guiyang-strategist-a': 'unit-rebel-archer', + 'guiyang-strategist-b': 'unit-rebel-archer', + 'guiyang-leader-zhao-fan': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -680,7 +698,8 @@ const unitStrategyIds: Record = { 'mi-zhu': ['aid', 'encourage'], 'sun-qian': ['aid', 'encourage', 'fireTactic'], 'zhuge-liang': ['aid', 'encourage', 'fireTactic'], - 'ma-liang': ['aid', 'encourage', 'fireTactic'] + 'ma-liang': ['aid', 'encourage', 'fireTactic'], + 'yi-ji': ['aid', 'encourage'] }; const initialItemStocks: Record> = { @@ -692,7 +711,8 @@ const initialItemStocks: Record> = { 'sun-qian': { bean: 1, salve: 1 }, 'zhao-yun': { bean: 1, wine: 1 }, 'zhuge-liang': { bean: 1, wine: 1 }, - 'ma-liang': { bean: 1, salve: 1 } + 'ma-liang': { bean: 1, salve: 1 }, + 'yi-ji': { bean: 2, salve: 1 } }; const attackRangeByClass: Partial> = { @@ -973,7 +993,24 @@ const enemyAiByUnitId: Record = { 'jing-south-guard-b': 'guard', 'jing-south-strategist-a': 'hold', 'jing-south-strategist-b': 'hold', - 'jing-south-leader-xing-daorong': 'guard' + 'jing-south-leader-xing-daorong': 'guard', + 'guiyang-scout-a': 'aggressive', + 'guiyang-scout-b': 'aggressive', + 'guiyang-infantry-a': 'guard', + 'guiyang-infantry-b': 'guard', + 'guiyang-infantry-c': 'guard', + 'guiyang-infantry-d': 'guard', + 'guiyang-archer-a': 'hold', + 'guiyang-archer-b': 'hold', + 'guiyang-archer-c': 'hold', + 'guiyang-cavalry-a': 'aggressive', + 'guiyang-cavalry-b': 'aggressive', + 'guiyang-cavalry-c': 'aggressive', + 'guiyang-guard-a': 'guard', + 'guiyang-guard-b': 'guard', + 'guiyang-strategist-a': 'hold', + 'guiyang-strategist-b': 'hold', + 'guiyang-leader-zhao-fan': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index ef7249c..6f36016 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -20,6 +20,7 @@ import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg'; import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg'; import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg'; +import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg'; import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg'; import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg'; import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; @@ -104,6 +105,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-twenty-first', twentyFirstBattleMapUrl); this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl); this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl); + this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl); 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 238e7c5..ecfddca 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -9,6 +9,8 @@ import { caoBreakRecruitUnits, firstBattleBonds, firstBattleUnits, + guiyangRecruitBonds, + guiyangRecruitUnits, jingzhouRecruitBonds, jingzhouRecruitUnits, liuBiaoRecruitBonds, @@ -297,7 +299,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ title: '형주·익주 확보', period: '근본을 세우다', description: '형주와 익주를 기반으로 삼아 유비군이 유랑군에서 국가의 뿌리로 바뀌는 장입니다.', - battleIds: ['twenty-third-battle-jingzhou-south-entry'], + battleIds: ['twenty-third-battle-jingzhou-south-entry', 'twenty-fourth-battle-guiyang-persuasion'], nextHints: ['형주 방위', '익주 진입', '장수 대거 합류'] }, { @@ -333,7 +335,8 @@ const campBattleIds = { twentieth: 'twentieth-battle-jiangdong-envoy', twentyFirst: 'twenty-first-battle-red-cliffs-vanguard', twentySecond: 'twenty-second-battle-red-cliffs-fire', - twentyThird: 'twenty-third-battle-jingzhou-south-entry' + twentyThird: 'twenty-third-battle-jingzhou-south-entry', + twentyFourth: 'twenty-fourth-battle-guiyang-persuasion' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -584,6 +587,18 @@ const sortieRulesByBattleId: Partial