From 2402e1c38f2a42502b5bc3b8607534cdb4db3883 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 23 Jun 2026 08:30:43 +0900 Subject: [PATCH] Add Changsha veteran chapter --- docs/roadmap.md | 7 +- scripts/verify-flow.mjs | 171 ++++- .../images/battle/twenty-sixth-battle-map.svg | 74 +++ src/game/data/battles.ts | 73 ++- src/game/data/campaignFlow.ts | 26 +- src/game/data/scenario.ts | 611 ++++++++++++++++++ src/game/scenes/BattleScene.ts | 50 +- src/game/scenes/BootScene.ts | 2 + src/game/scenes/CampScene.ts | 218 ++++++- src/game/scenes/TitleScene.ts | 9 +- src/game/state/campaignState.ts | 7 +- 11 files changed, 1217 insertions(+), 31 deletions(-) create mode 100644 src/assets/images/battle/twenty-sixth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index 9370d8d..3506bcf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -47,16 +47,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 - Twenty-fifth battle Wuling mountain road route, pushing ten recruited officers into a six-officer sortie choice across a 50x34 mountain battlefield and recruiting Gong Zhi after Jin Xuan withdraws +- Twenty-sixth battle Changsha veteran audience route, turning the southern Jing Province finale into a 52x34 castle approach battle and recruiting Huang Zhong plus Wei Yan after Han Xuan withdraws - 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-fifth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling camp states, campaign timeline state, and camp save state +- Flow verification script from title through the twenty-sixth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha camp states, campaign timeline state, and camp save state ## Next Steps -1. Continue Jing Province south from Wuling into Changsha acquisition, including Huang Zhong and Wei Yan recruitment pressure -2. Add more recruitable officers as the campaign moves through Changsha, Yi Province, and Shu Han +1. Start the Yi Province entry arc from the stabilized Jing Province base, with larger roster pressure and political camp events +2. Add more recruitable officers as the campaign moves through Yi Province and toward 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 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index f4a7c8c..cca2b51 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 !== 25 || + progressTabState.campaignProgress?.totalKnown !== 26 || 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 !== 25 || + postBowangProgressState.campaignProgress?.totalKnown !== 26 || 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 !== 25 || + postChangbanProgressState.campaignProgress?.totalKnown !== 26 || 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 !== 25 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 26 || 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 !== 25 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 26 || 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 !== 25 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 26 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2587,7 +2587,7 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 25 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 26 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2729,7 +2729,7 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 25 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 26 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2872,15 +2872,164 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 25 || + postWulingProgressState.campaignProgress?.totalKnown !== 26 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || - postWulingProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' ) { throw new Error(`Expected post-Wuling progress tab to complete the mountain-road battle and pause on Changsha preparation: ${JSON.stringify(postWulingProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-wuling-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const changshaSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !changshaSortieState?.sortieVisible || + !changshaSortieState.sortiePlan?.objectiveLine?.includes('장사 노장 대면전') || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited && unit.recommended) || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) || + !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + changshaSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong') || + changshaSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan') || + changshaSortieState.sortieRoster?.length < 11 || + changshaSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-fifth camp sortie prep to target Changsha with Gong Zhi recommended and Huang Zhong/Wei Yan absent before victory: ${JSON.stringify(changshaSortieState)}`); + } + assertSortieTacticalRoster(changshaSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang', + 'yi-ji', + 'gong-zhi' + ]); + + const changshaPriorityUnits = ['gong-zhi', 'yi-ji', 'zhuge-liang', 'guan-yu', 'zhao-yun']; + for (const unitId of changshaPriorityUnits) { + 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' && !changshaPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const changshaSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !changshaPriorityUnits.every((unitId) => + changshaSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + changshaSortieReadyState.sortiePlan?.selectedCount !== 6 || + changshaSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Changsha sortie to deploy Gong Zhi and the veteran-audience priority officers while preserving six-officer pressure: ${JSON.stringify(changshaSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-changsha-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-changsha-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentySixthBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-sixth-battle-changsha-veteran'; + }); + if (enteredTwentySixthBattle) { + 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-sixth-battle-changsha-veteran' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-sixth-battle.png', fullPage: true }); + + const twentySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentySixthEnemies = twentySixthBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentySixthAllies = twentySixthBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentySixthEnemyBehaviors = new Set(twentySixthEnemies.map((unit) => unit.ai)); + if ( + twentySixthBattleState.camera?.mapWidth !== 52 || + twentySixthBattleState.camera?.mapHeight !== 34 || + twentySixthBattleState.victoryConditionLabel !== '한현 퇴각' || + twentySixthEnemies.length < 18 || + !twentySixthEnemyBehaviors.has('aggressive') || + !twentySixthEnemyBehaviors.has('guard') || + !twentySixthEnemyBehaviors.has('hold') || + !twentySixthEnemies.some((unit) => unit.id === 'changsha-leader-han-xuan') || + !twentySixthEnemies.some((unit) => unit.id === 'changsha-veteran-huang-zhong') || + !twentySixthEnemies.some((unit) => unit.id === 'changsha-officer-wei-yan') || + !twentySixthAllies.some((unit) => unit.id === 'gong-zhi') || + !twentySixthAllies.some((unit) => unit.id === 'yi-ji') || + twentySixthAllies.some((unit) => unit.id === 'huang-zhong') || + twentySixthAllies.some((unit) => unit.id === 'wei-yan') + ) { + throw new Error(`Expected twenty-sixth battle to use Changsha map, Han Xuan objective, Huang Zhong/Wei Yan as enemy officers, mixed AI, and no Changsha recruits before victory: ${JSON.stringify(twentySixthBattleState)}`); + } + + 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 twentySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentySixthCampState?.campBattleId !== 'twenty-sixth-battle-changsha-veteran' || + twentySixthCampState.campTitle !== '장사 노장 대면 후 군영' || + twentySixthCampState.availableDialogueIds?.length !== 4 || + !twentySixthCampState.availableDialogueIds.every((id) => id.endsWith('changsha')) || + twentySixthCampState.availableVisitIds?.length !== 2 || + !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'huang-zhong') || + !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wei-yan') || + !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited) || + !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited) || + twentySixthCampState.rosterCollection?.total < 13 + ) { + throw new Error(`Expected twenty-sixth camp to recruit Huang Zhong and Wei Yan and expose Changsha dialogue/visit sets: ${JSON.stringify(twentySixthCampState)}`); + } + await page.screenshot({ path: 'dist/verification-changsha-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postChangshaProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postChangshaProgressState?.activeTab !== 'progress' || + postChangshaProgressState.campaignProgress?.completedKnown !== 26 || + postChangshaProgressState.campaignProgress?.totalKnown !== 26 || + postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || + postChangshaProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Changsha progress tab to complete the veteran-audience battle and pause on Yi Province preparation: ${JSON.stringify(postChangshaProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-changsha-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -2892,7 +3041,7 @@ try { return activeScenes.includes('CampScene'); }); - console.log(`Verified title-to-twenty-fifth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, and Gong Zhi joins, result states, and debug API at ${targetUrl}`); + console.log(`Verified title-to-twenty-sixth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, and Wei Yan joins, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/assets/images/battle/twenty-sixth-battle-map.svg b/src/assets/images/battle/twenty-sixth-battle-map.svg new file mode 100644 index 0000000..512fc56 --- /dev/null +++ b/src/assets/images/battle/twenty-sixth-battle-map.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 7711908..876ca45 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -59,6 +59,10 @@ import { twentyFifthBattleMap, twentyFifthBattleUnits, twentyFifthBattleVictoryPages, + twentySixthBattleBonds, + twentySixthBattleMap, + twentySixthBattleUnits, + twentySixthBattleVictoryPages, twentiethBattleBonds, twentiethBattleMap, twentiethBattleUnits, @@ -130,7 +134,8 @@ export type BattleScenarioId = | 'twenty-second-battle-red-cliffs-fire' | 'twenty-third-battle-jingzhou-south-entry' | 'twenty-fourth-battle-guiyang-persuasion' - | 'twenty-fifth-battle-wuling-mountain-road'; + | 'twenty-fifth-battle-wuling-mountain-road' + | 'twenty-sixth-battle-changsha-veteran'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1565,6 +1570,69 @@ export const twentyFifthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentySixthBattleScenario: BattleScenarioDefinition = { + id: 'twenty-sixth-battle-changsha-veteran', + title: '장사 노장 대면전', + victoryConditionLabel: '한현 퇴각', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '무릉 산길을 연 유비군은 장사로 향합니다. 장사에는 노장 황충과 의심 많은 태수 한현이 버티고 있습니다.', + '이번 전투는 황충을 욕보이지 않고 한현의 의심을 꺾어야 합니다. 마을을 안정시키고 성문 전열을 흔들어 장사 내부의 길을 여십시오.', + '유비와 제갈량을 지키며 한현을 물러나게 하면 황충과 위연이 군영에 합류합니다.' + ], + map: twentySixthBattleMap, + units: twentySixthBattleUnits, + bonds: twentySixthBattleBonds, + mapTextureKey: 'battle-map-twenty-sixth', + leaderUnitId: 'changsha-leader-han-xuan', + quickVictoryTurnLimit: 35, + baseVictoryGold: 3560, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '한현 퇴각', + rewardGold: 2320, + unitId: 'changsha-leader-han-xuan' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 660, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 940, + unitId: 'zhuge-liang' + }, + { + id: 'changsha-village', + kind: 'secure-terrain', + label: '장사 마을 안정', + rewardGold: 1120, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '35턴 이내 승리', + rewardGold: 880, + maxTurn: 35 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 12', '상처약 8', '탁주 5', '장사 사절 문서 +1', '황충 합류', '위연 합류'], + victoryPages: twentySixthBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1592,7 +1660,8 @@ export const battleScenarios: Record 'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario, 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario, 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario, - 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario + 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario, + 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 61ccfef..eb41a03 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -22,6 +22,7 @@ import { twentyThirdBattleScenario, twentyFifthBattleScenario, twentyFourthBattleScenario, + twentySixthBattleScenario, twentiethBattleScenario, twelfthBattleScenario, thirdBattleScenario, @@ -71,6 +72,8 @@ import { twentyFifthBattleVictoryPages, twentyFourthBattleIntroPages, twentyFourthBattleVictoryPages, + twentySixthBattleIntroPages, + twentySixthBattleVictoryPages, twentiethBattleIntroPages, twentiethBattleVictoryPages, twelfthBattleIntroPages, @@ -347,13 +350,24 @@ const sortieFlows: Record = { }, [twentyFifthBattleScenario.id]: { afterBattleId: twentyFifthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '장사 노장 대면 준비', + eyebrow: '다음 전장', + title: twentySixthBattleScenario.title, description: - '무릉 산길이 열리고 공지가 합류했습니다. 다음은 장사의 노장 황충과 태수 한현을 마주하며 형주 남부 확보의 큰 고비를 넘겨야 합니다.', - rewardHint: '다음 장: 장사 노장 대면 준비 중', - pages: twentyFifthBattleVictoryPages, - unavailableNotice: '장사 노장 대면 장은 다음 작업에서 이어집니다. 군영에서 공지와 대화하고 황충을 맞을 준비를 하십시오.' + '무릉 산길이 열리고 공지가 합류했습니다. 이제 장사의 노장 황충과 의심 많은 한현을 마주하며 형주 남부 확보의 큰 고비를 넘어야 합니다.', + rewardHint: `예상 보상: ${twentySixthBattleScenario.title} 개방 / 황충·위연 합류`, + nextBattleId: twentySixthBattleScenario.id, + campaignStep: 'twenty-sixth-battle', + pages: [...twentyFifthBattleVictoryPages, ...twentySixthBattleIntroPages] + }, + [twentySixthBattleScenario.id]: { + afterBattleId: twentySixthBattleScenario.id, + eyebrow: '다음 장 준비', + title: '익주 진입 준비', + description: + '장사까지 열리며 형주 남부의 뿌리가 단단해졌습니다. 다음은 익주로 향하는 더 긴 정치와 전장의 길을 준비해야 합니다.', + rewardHint: '다음 장: 익주 진입 준비 중', + pages: twentySixthBattleVictoryPages, + unavailableNotice: '익주 진입 장은 다음 작업에서 이어집니다. 군영에서 황충과 위연을 살피고 다음 대장정의 출전 후보를 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 6dbfbb8..8e05d6f 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1634,6 +1634,76 @@ export const twentyFifthBattleVictoryPages: StoryPage[] = [ } ]; +export const twentySixthBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-sixth-changsha-old-general', + bgm: 'story-dark', + chapter: '장사의 노장', + background: 'story-three-heroes', + speaker: '공지', + text: '장사에는 황충이라는 노장이 있습니다. 나이는 들었으나 활은 여전히 매섭고, 성 안 병사들은 그 이름을 믿고 버팁니다.' + }, + { + id: 'twenty-sixth-han-xuan-suspicion', + bgm: 'battle-prep', + chapter: '의심 많은 태수', + background: 'story-militia', + speaker: '이적', + text: '한현은 황충의 명망도 의심한다고 합니다. 성을 무너뜨리기보다 황충의 체면을 지키면, 장사 사람들의 마음도 흔들릴 것입니다.' + }, + { + id: 'twenty-sixth-wei-yan-rumor', + bgm: 'battle-prep', + chapter: '성문 안의 불씨', + background: 'story-liu-bei', + speaker: '마량', + text: '위연이라는 장수는 성 안의 부당함을 참지 못한다고 들었습니다. 백성을 해치지 않고 한현의 의심을 꺾으면, 성 안에서 길이 열릴 수 있습니다.' + }, + { + id: 'twenty-sixth-changsha-sortie', + bgm: 'battle-prep', + chapter: '장사 노장 대면전', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '오늘은 노장의 이름을 꺾으러 가는 것이 아니오. 예를 갖추고 한현의 의심을 물리쳐, 장사의 사람을 얻어야 하오.' + } +]; + +export const twentySixthBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-sixth-victory-changsha-open', + bgm: 'militia-theme', + chapter: '장사 성문이 열리다', + background: 'story-sortie', + text: '한현의 의심이 전장 위에서 무너지자, 장사 성문은 크게 흔들렸습니다. 유비군은 황충의 명예를 욕보이지 않고 병사들에게 길을 열었습니다.' + }, + { + id: 'twenty-sixth-huang-zhong-joins', + bgm: 'battle-prep', + chapter: '황충의 합류', + background: 'story-militia', + speaker: '황충', + text: '늙은 몸이라 하나, 아직 활은 뜻을 향해 날아갈 수 있습니다. 장사를 예로 대하신 주공의 군에 제 활을 맡기겠습니다.' + }, + { + id: 'twenty-sixth-wei-yan-joins', + bgm: 'battle-prep', + chapter: '위연의 결심', + background: 'story-liu-bei', + speaker: '위연', + text: '의심과 두려움으로 닫힌 성문을 더는 따를 수 없습니다. 제 창은 닫힌 문을 여는 데 쓰겠습니다.' + }, + { + id: 'twenty-sixth-next-yi-province', + bgm: 'story-dark', + chapter: '형주 남부의 끝', + background: 'story-three-heroes', + speaker: '제갈량', + text: '장사까지 열리면 형주 남부의 뿌리가 굳어집니다. 이제 익주로 이어질 더 큰 길을 바라볼 때가 다가옵니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -1929,6 +1999,12 @@ export const twentyFifthBattleMap: BattleMap = { terrain: createTwentyFifthBattleTerrain() }; +export const twentySixthBattleMap: BattleMap = { + width: 52, + height: 34, + terrain: createTwentySixthBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -3957,6 +4033,51 @@ export const wulingRecruitUnits: UnitData[] = [ } ]; +export const changshaRecruitUnits: UnitData[] = [ + { + id: 'huang-zhong', + name: '황충', + faction: 'ally', + className: '노장 궁기병', + classKey: 'archer', + level: 15, + exp: 0, + hp: 44, + maxHp: 44, + attack: 15, + move: 4, + stats: { might: 88, intelligence: 72, leadership: 86, agility: 78, luck: 76 }, + equipment: { + weapon: { itemId: 'short-bow', level: 3, exp: 12 }, + armor: { itemId: 'lamellar-armor', level: 2, exp: 14 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 8, + y: 23 + }, + { + id: 'wei-yan', + name: '위연', + faction: 'ally', + className: '장사 돌격장', + classKey: 'infantry', + level: 15, + exp: 0, + hp: 48, + maxHp: 48, + attack: 16, + move: 4, + stats: { might: 90, intelligence: 64, leadership: 82, agility: 82, luck: 70 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 3, exp: 10 }, + armor: { itemId: 'reinforced-lamellar', level: 2, exp: 10 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 9, + y: 24 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -10058,6 +10179,411 @@ export const twentyFifthBattleUnits: UnitData[] = [ } ]; +const twentySixthBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 27 }, + 'guan-yu': { x: 6, y: 25 }, + 'zhang-fei': { x: 6, y: 29 }, + 'jian-yong': { x: 3, y: 30 }, + 'mi-zhu': { x: 4, y: 31 }, + 'sun-qian': { x: 8, y: 28 }, + 'zhao-yun': { x: 8, y: 25 }, + 'zhuge-liang': { x: 5, y: 27 }, + 'ma-liang': { x: 7, y: 30 }, + 'yi-ji': { x: 9, y: 27 }, + 'gong-zhi': { x: 9, y: 30 } +}; + +export const twentySixthBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'changsha-scout-a', + name: '장사 척후', + faction: 'enemy', + className: '장사 척후', + classKey: 'bandit', + level: 37, + exp: 56, + hp: 100, + maxHp: 100, + attack: 38, + move: 4, + stats: { might: 124, intelligence: 90, leadership: 100, agility: 118, luck: 82 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 36 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 14, + y: 25 + }, + { + id: 'changsha-scout-b', + name: '장사 척후', + faction: 'enemy', + className: '서문 척후', + classKey: 'bandit', + level: 37, + exp: 56, + hp: 100, + maxHp: 100, + attack: 38, + move: 4, + stats: { might: 124, intelligence: 90, leadership: 100, agility: 118, luck: 82 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 36 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 22 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 17, + y: 30 + }, + { + id: 'changsha-infantry-a', + name: '장사 보병', + faction: 'enemy', + className: '장사 보병', + classKey: 'yellowTurban', + level: 38, + exp: 58, + hp: 130, + maxHp: 130, + attack: 42, + move: 3, + stats: { might: 138, intelligence: 90, leadership: 116, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 50 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 34 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 23, + y: 24 + }, + { + id: 'changsha-infantry-b', + name: '장사 보병', + faction: 'enemy', + className: '마을 보병', + classKey: 'yellowTurban', + level: 38, + exp: 58, + hp: 130, + maxHp: 130, + attack: 42, + move: 3, + stats: { might: 138, intelligence: 90, leadership: 116, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 50 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 34 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 26, + y: 28 + }, + { + id: 'changsha-infantry-c', + name: '장사 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 39, + exp: 60, + hp: 136, + maxHp: 136, + attack: 43, + move: 3, + stats: { might: 140, intelligence: 90, leadership: 118, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 54 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 38 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 34, + y: 20 + }, + { + id: 'changsha-infantry-d', + name: '장사 보병', + faction: 'enemy', + className: '성내 보병', + classKey: 'yellowTurban', + level: 39, + exp: 60, + hp: 136, + maxHp: 136, + attack: 43, + move: 3, + stats: { might: 140, intelligence: 90, leadership: 118, agility: 98, luck: 82 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 54 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 38 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 39, + y: 17 + }, + { + id: 'changsha-archer-a', + name: '장사 궁병', + faction: 'enemy', + className: '장사 궁병', + classKey: 'archer', + level: 38, + exp: 58, + hp: 98, + maxHp: 98, + attack: 39, + move: 3, + stats: { might: 96, intelligence: 98, leadership: 104, agility: 112, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 46 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 28, + y: 20 + }, + { + id: 'changsha-archer-b', + name: '장사 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 38, + exp: 58, + hp: 98, + maxHp: 98, + attack: 39, + move: 3, + stats: { might: 96, intelligence: 98, leadership: 104, agility: 112, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 46 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 37, + y: 13 + }, + { + id: 'changsha-archer-c', + name: '장사 궁병', + faction: 'enemy', + className: '동문 궁병', + classKey: 'archer', + level: 39, + exp: 60, + hp: 102, + maxHp: 102, + attack: 40, + move: 3, + stats: { might: 98, intelligence: 100, leadership: 106, agility: 114, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 50 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 30 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 44, + y: 19 + }, + { + id: 'changsha-cavalry-a', + name: '장사 기병', + faction: 'enemy', + className: '장사 기병', + classKey: 'cavalry', + level: 39, + exp: 60, + hp: 138, + maxHp: 138, + attack: 44, + move: 5, + stats: { might: 140, intelligence: 90, leadership: 116, agility: 118, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 56 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 36 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 31, + y: 29 + }, + { + id: 'changsha-cavalry-b', + name: '장사 기병', + faction: 'enemy', + className: '동문 기병', + classKey: 'cavalry', + level: 39, + exp: 60, + hp: 138, + maxHp: 138, + attack: 44, + move: 5, + stats: { might: 140, intelligence: 90, leadership: 116, agility: 118, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 56 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 36 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 42, + y: 28 + }, + { + id: 'changsha-guard-a', + name: '한현 친위대', + faction: 'enemy', + className: '장사 친위대', + classKey: 'yellowTurban', + level: 40, + exp: 62, + hp: 144, + maxHp: 144, + attack: 45, + move: 3, + stats: { might: 144, intelligence: 92, leadership: 122, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 62 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 45, + y: 15 + }, + { + id: 'changsha-guard-b', + name: '한현 친위대', + faction: 'enemy', + className: '장사 친위대', + classKey: 'yellowTurban', + level: 40, + exp: 62, + hp: 144, + maxHp: 144, + attack: 45, + move: 3, + stats: { might: 144, intelligence: 92, leadership: 122, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 62 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 47, + y: 18 + }, + { + id: 'changsha-strategist-a', + name: '장사 책사', + faction: 'enemy', + className: '장사 책사', + classKey: 'archer', + level: 39, + exp: 60, + hp: 102, + maxHp: 102, + attack: 40, + move: 3, + stats: { might: 90, intelligence: 116, leadership: 108, agility: 96, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 50 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 41, + y: 14 + }, + { + id: 'changsha-strategist-b', + name: '장사 책사', + faction: 'enemy', + className: '의심 많은 책사', + classKey: 'archer', + level: 39, + exp: 60, + hp: 102, + maxHp: 102, + attack: 40, + move: 3, + stats: { might: 90, intelligence: 116, leadership: 108, agility: 96, luck: 84 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 50 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 30 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 43, + y: 16 + }, + { + id: 'changsha-veteran-huang-zhong', + name: '황충', + faction: 'enemy', + className: '장사 노장', + classKey: 'archer', + level: 41, + exp: 66, + hp: 156, + maxHp: 156, + attack: 48, + move: 4, + stats: { might: 150, intelligence: 96, leadership: 128, agility: 108, luck: 88 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 18 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 38, + y: 17 + }, + { + id: 'changsha-officer-wei-yan', + name: '위연', + faction: 'enemy', + className: '장사 돌격장', + classKey: 'yellowTurban', + level: 40, + exp: 64, + hp: 152, + maxHp: 152, + attack: 47, + move: 4, + stats: { might: 148, intelligence: 82, leadership: 120, agility: 112, luck: 80 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 12 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 44 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 36, + y: 23 + }, + { + id: 'changsha-leader-han-xuan', + name: '한현', + faction: 'enemy', + className: '장사 태수', + classKey: 'rebelLeader', + level: 41, + exp: 66, + hp: 190, + maxHp: 190, + attack: 46, + move: 4, + stats: { might: 138, intelligence: 104, leadership: 130, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 5, exp: 10 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 48, + y: 16 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -10266,6 +10792,41 @@ export const wulingRecruitBonds: BattleBond[] = [ } ]; +export const changshaRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__huang-zhong', + unitIds: ['liu-bei', 'huang-zhong'], + title: '노장을 예우하다', + level: 38, + exp: 0, + description: '유비의 예우와 황충의 노련한 활이 만나, 오래 버틴 장수의 마음을 군의 힘으로 바꿉니다.' + }, + { + id: 'guan-yu__huang-zhong', + unitIds: ['guan-yu', 'huang-zhong'], + title: '무인의 존중', + level: 34, + exp: 0, + description: '관우와 황충은 서로의 무예와 체면을 인정하며 전열과 원거리 압박을 함께 세웁니다.' + }, + { + id: 'liu-bei__wei-yan', + unitIds: ['liu-bei', 'wei-yan'], + title: '닫힌 문을 여는 창', + level: 32, + exp: 0, + description: '유비의 큰 뜻과 위연의 거친 돌파가 만나, 막힌 성문과 전열을 과감히 흔듭니다.' + }, + { + id: 'huang-zhong__wei-yan', + unitIds: ['huang-zhong', 'wei-yan'], + 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); @@ -10296,6 +10857,7 @@ export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds] export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario); export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario); export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario); +export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -11212,6 +11774,55 @@ function createTwentyFifthBattleTerrain(): TerrainType[][] { ); } +function createTwentySixthBattleTerrain(): TerrainType[][] { + return Array.from({ length: 34 }, (_, y) => + Array.from({ length: 52 }, (_, x): TerrainType => { + if (x <= 3 && y >= 26) { + return 'camp'; + } + if ((x >= 42 && x <= 50 && y >= 11 && y <= 20) || (x >= 37 && x <= 43 && y >= 8 && y <= 13)) { + return 'fort'; + } + if ((x >= 21 && x <= 24 && y >= 26 && y <= 28) || (x >= 30 && x <= 32 && y >= 19 && y <= 21) || (x >= 15 && x <= 17 && y >= 21 && y <= 23)) { + return 'village'; + } + if ( + (y >= 27 && x >= 3 && x <= 18) || + (x >= 18 && x <= 29 && y >= 23 && y <= 27) || + (x >= 29 && x <= 38 && y >= 19 && y <= 23) || + (x >= 37 && x <= 45 && y >= 15 && y <= 19) || + (x >= 44 && x <= 49 && y >= 13 && y <= 17) + ) { + return 'road'; + } + if ((x === 11 || x === 12) && y >= 0 && y <= 24) { + return 'river'; + } + if ((x === 33 || x === 34) && y >= 22 && y <= 33) { + return 'river'; + } + if ( + (x >= 4 && x <= 14 && y >= 7 && y <= 18) || + (x >= 18 && x <= 30 && y >= 8 && y <= 17) || + (x >= 35 && x <= 47 && y >= 23 && y <= 33) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 9 && y <= 10) || + (x >= 21 && x <= 32 && y >= 1 && y <= 9) || + (x >= 46 && x <= 51 && y >= 0 && y <= 10) + ) { + return 'hill'; + } + if ((x <= 1 && y <= 25) || (x >= 50 && y >= 21) || (x >= 39 && x <= 41 && y >= 0 && y <= 7)) { + 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 9ab4985..e01ea40 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -32,6 +32,8 @@ const unitTexture: Record = { 'ma-liang': 'unit-liu-bei', 'yi-ji': 'unit-liu-bei', 'gong-zhi': 'unit-guan-yu', + 'huang-zhong': 'unit-rebel-archer', + 'wei-yan': 'unit-guan-yu', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -296,7 +298,25 @@ const unitTexture: Record = { 'wuling-guard-a': 'unit-rebel', 'wuling-guard-b': 'unit-rebel', 'wuling-strategist-a': 'unit-rebel-archer', - 'wuling-leader-jin-xuan': 'unit-rebel-leader' + 'wuling-leader-jin-xuan': 'unit-rebel-leader', + 'changsha-scout-a': 'unit-rebel', + 'changsha-scout-b': 'unit-rebel', + 'changsha-infantry-a': 'unit-rebel', + 'changsha-infantry-b': 'unit-rebel', + 'changsha-infantry-c': 'unit-rebel', + 'changsha-infantry-d': 'unit-rebel', + 'changsha-archer-a': 'unit-rebel-archer', + 'changsha-archer-b': 'unit-rebel-archer', + 'changsha-archer-c': 'unit-rebel-archer', + 'changsha-cavalry-a': 'unit-rebel-cavalry', + 'changsha-cavalry-b': 'unit-rebel-cavalry', + 'changsha-guard-a': 'unit-rebel', + 'changsha-guard-b': 'unit-rebel', + 'changsha-strategist-a': 'unit-rebel-archer', + 'changsha-strategist-b': 'unit-rebel-archer', + 'changsha-veteran-huang-zhong': 'unit-rebel-archer', + 'changsha-officer-wei-yan': 'unit-guan-yu', + 'changsha-leader-han-xuan': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -720,7 +740,9 @@ const unitStrategyIds: Record = { 'zhuge-liang': ['aid', 'encourage', 'fireTactic'], 'ma-liang': ['aid', 'encourage', 'fireTactic'], 'yi-ji': ['aid', 'encourage'], - 'gong-zhi': ['encourage'] + 'gong-zhi': ['encourage'], + 'huang-zhong': ['encourage'], + 'wei-yan': ['roar'] }; const initialItemStocks: Record> = { @@ -734,7 +756,9 @@ const initialItemStocks: Record> = { 'zhuge-liang': { bean: 1, wine: 1 }, 'ma-liang': { bean: 1, salve: 1 }, 'yi-ji': { bean: 2, salve: 1 }, - 'gong-zhi': { bean: 2 } + 'gong-zhi': { bean: 2 }, + 'huang-zhong': { bean: 1, wine: 1 }, + 'wei-yan': { bean: 2 } }; const attackRangeByClass: Partial> = { @@ -1051,7 +1075,25 @@ const enemyAiByUnitId: Record = { 'wuling-guard-a': 'guard', 'wuling-guard-b': 'guard', 'wuling-strategist-a': 'hold', - 'wuling-leader-jin-xuan': 'guard' + 'wuling-leader-jin-xuan': 'guard', + 'changsha-scout-a': 'aggressive', + 'changsha-scout-b': 'aggressive', + 'changsha-infantry-a': 'guard', + 'changsha-infantry-b': 'guard', + 'changsha-infantry-c': 'guard', + 'changsha-infantry-d': 'guard', + 'changsha-archer-a': 'hold', + 'changsha-archer-b': 'hold', + 'changsha-archer-c': 'hold', + 'changsha-cavalry-a': 'aggressive', + 'changsha-cavalry-b': 'aggressive', + 'changsha-guard-a': 'guard', + 'changsha-guard-b': 'guard', + 'changsha-strategist-a': 'hold', + 'changsha-strategist-b': 'hold', + 'changsha-veteran-huang-zhong': 'hold', + 'changsha-officer-wei-yan': 'aggressive', + 'changsha-leader-han-xuan': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 76d3e42..d44ff93 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -22,6 +22,7 @@ import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-b import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg'; import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg'; import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg'; +import twentySixthBattleMapUrl from '../../assets/images/battle/twenty-sixth-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'; @@ -108,6 +109,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl); this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl); this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl); + this.load.image('battle-map-twenty-sixth', twentySixthBattleMapUrl); 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 8ef74eb..6cee0b0 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -7,6 +7,8 @@ import { getSortieFlow } from '../data/campaignFlow'; import { caoBreakRecruitBonds, caoBreakRecruitUnits, + changshaRecruitBonds, + changshaRecruitUnits, firstBattleBonds, firstBattleUnits, guiyangRecruitBonds, @@ -304,7 +306,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ battleIds: [ 'twenty-third-battle-jingzhou-south-entry', 'twenty-fourth-battle-guiyang-persuasion', - 'twenty-fifth-battle-wuling-mountain-road' + 'twenty-fifth-battle-wuling-mountain-road', + 'twenty-sixth-battle-changsha-veteran' ], nextHints: ['형주 방위', '익주 진입', '장수 대거 합류'] }, @@ -343,7 +346,8 @@ const campBattleIds = { twentySecond: 'twenty-second-battle-red-cliffs-fire', twentyThird: 'twenty-third-battle-jingzhou-south-entry', twentyFourth: 'twenty-fourth-battle-guiyang-persuasion', - twentyFifth: 'twenty-fifth-battle-wuling-mountain-road' + twentyFifth: 'twenty-fifth-battle-wuling-mountain-road', + twentySixth: 'twenty-sixth-battle-changsha-veteran' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -618,6 +622,18 @@ const sortieRulesByBattleId: Partial