diff --git a/docs/roadmap.md b/docs/roadmap.md index 3506bcf..64d9bbb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -48,16 +48,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - 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 +- Twenty-seventh battle Yi Province relief-road route, using the thirteen-officer roster in sortie selection across a 54x36 mountain-gate battlefield and recruiting Pang Tong after Yang Huai 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-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 +- Flow verification script from title through the twenty-seventh battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province camp states, campaign timeline state, and camp save state ## Next Steps -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 +1. Continue the Yi Province arc into Fu Pass and Luo Castle, with Pang Tong/Zhuge Liang planning pressure and harder officer selection +2. Add more recruitable officers as the campaign moves deeper 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 cca2b51..ce00dc6 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 !== 26 || + progressTabState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postBowangProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postChangbanProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 27 || 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 !== 26 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 27 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2872,7 +2872,7 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 26 || + postWulingProgressState.campaignProgress?.totalKnown !== 27 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' @@ -3021,15 +3021,160 @@ try { if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || - postChangshaProgressState.campaignProgress?.totalKnown !== 26 || + postChangshaProgressState.campaignProgress?.totalKnown !== 27 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || - postChangshaProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + 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)}`); + throw new Error(`Expected post-Changsha progress tab to complete the veteran-audience battle and open Yi Province relief road: ${JSON.stringify(postChangshaProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-changsha-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const yizhouSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yizhouSortieState?.sortieVisible || + !yizhouSortieState.sortiePlan?.objectiveLine?.includes('익주 원군로') || + !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) || + !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) || + !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited && unit.recommended) || + !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) || + !yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'mi-zhu' && unit.recommended) || + yizhouSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong') || + yizhouSortieState.sortieRoster?.length < 13 || + yizhouSortieState.sortiePlan?.maxCount !== 6 + ) { + throw new Error(`Expected twenty-sixth camp sortie prep to target Yi Province relief road with Huang Zhong/Wei Yan selectable and Pang Tong absent before victory: ${JSON.stringify(yizhouSortieState)}`); + } + assertSortieTacticalRoster(yizhouSortieState, [ + 'liu-bei', + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + 'ma-liang', + 'yi-ji', + 'gong-zhi', + 'huang-zhong', + 'wei-yan' + ]); + + const yizhouPriorityUnits = ['zhuge-liang', 'huang-zhong', 'wei-yan', 'zhao-yun', 'mi-zhu']; + for (const unitId of yizhouPriorityUnits) { + 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' && !yizhouPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const yizhouSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !yizhouPriorityUnits.every((unitId) => + yizhouSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + yizhouSortieReadyState.sortiePlan?.selectedCount !== 6 || + yizhouSortieReadyState.sortiePlan?.recommendedSelectedCount < 6 + ) { + throw new Error(`Expected Yi Province sortie to deploy Zhuge Liang, Huang Zhong, Wei Yan, Zhao Yun, and Mi Zhu while preserving six-officer pressure: ${JSON.stringify(yizhouSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-yizhou-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-yizhou-story.png', fullPage: true }); + + for (let i = 0; i < 50; i += 1) { + const enteredTwentySeventhBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'twenty-seventh-battle-yizhou-relief-road'; + }); + if (enteredTwentySeventhBattle) { + 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-seventh-battle-yizhou-relief-road' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-twenty-seventh-battle.png', fullPage: true }); + + const twentySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const twentySeventhEnemies = twentySeventhBattleState.units.filter((unit) => unit.faction === 'enemy'); + const twentySeventhAllies = twentySeventhBattleState.units.filter((unit) => unit.faction === 'ally'); + const twentySeventhEnemyBehaviors = new Set(twentySeventhEnemies.map((unit) => unit.ai)); + if ( + twentySeventhBattleState.camera?.mapWidth !== 54 || + twentySeventhBattleState.camera?.mapHeight !== 36 || + twentySeventhBattleState.victoryConditionLabel !== '양회 퇴각' || + twentySeventhEnemies.length < 19 || + !twentySeventhEnemyBehaviors.has('aggressive') || + !twentySeventhEnemyBehaviors.has('guard') || + !twentySeventhEnemyBehaviors.has('hold') || + !twentySeventhEnemies.some((unit) => unit.id === 'yizhou-leader-yang-huai') || + !twentySeventhAllies.some((unit) => unit.id === 'huang-zhong') || + !twentySeventhAllies.some((unit) => unit.id === 'wei-yan') || + twentySeventhAllies.some((unit) => unit.id === 'pang-tong') + ) { + throw new Error(`Expected twenty-seventh battle to use Yi Province map, Yang Huai objective, Huang Zhong/Wei Yan as selectable allies, mixed AI, and no Pang Tong before victory: ${JSON.stringify(twentySeventhBattleState)}`); + } + + 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 twentySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + twentySeventhCampState?.campBattleId !== 'twenty-seventh-battle-yizhou-relief-road' || + twentySeventhCampState.campTitle !== '익주 원군로 후 군영' || + twentySeventhCampState.availableDialogueIds?.length !== 3 || + !twentySeventhCampState.availableDialogueIds.every((id) => id.endsWith('yizhou')) || + twentySeventhCampState.availableVisitIds?.length !== 2 || + !twentySeventhCampState.campaign?.roster?.some((unit) => unit.id === 'pang-tong') || + !twentySeventhCampState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited) || + twentySeventhCampState.rosterCollection?.total < 14 + ) { + throw new Error(`Expected twenty-seventh camp to recruit Pang Tong and expose Yi Province dialogue/visit sets: ${JSON.stringify(twentySeventhCampState)}`); + } + await page.screenshot({ path: 'dist/verification-yizhou-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postYizhouProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postYizhouProgressState?.activeTab !== 'progress' || + postYizhouProgressState.campaignProgress?.completedKnown !== 27 || + postYizhouProgressState.campaignProgress?.totalKnown !== 27 || + postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || + postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || + postYizhouProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Yi Province progress tab to complete the relief-road battle and pause on Fu Pass preparation: ${JSON.stringify(postYizhouProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-yizhou-progress.png', fullPage: true }); + await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene')); await page.waitForFunction(() => { const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; @@ -3041,7 +3186,7 @@ try { return activeScenes.includes('CampScene'); }); - 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}`); + console.log(`Verified title-to-twenty-seventh-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, and Pang Tong joins, result states, and debug API at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { @@ -3157,7 +3302,8 @@ async function clickSortieRosterUnit(page, unitId) { throw new Error(`Cannot click sortie unit ${unitId}; unit is missing from roster: ${JSON.stringify(state.sortieRoster)}`); } - const rowGap = state.sortieRoster.length > 7 ? 30 : state.sortieRoster.length > 5 ? 38 : state.sortieRoster.length > 4 ? 43 : 48; - await page.mouse.click(254, 314 + 50 + index * rowGap); + const denseRoster = state.sortieRoster.length > 12; + const rowGap = denseRoster ? 17 : state.sortieRoster.length > 7 ? 30 : state.sortieRoster.length > 5 ? 38 : state.sortieRoster.length > 4 ? 43 : 48; + await page.mouse.click(254, 314 + (denseRoster ? 48 : 50) + index * rowGap); await page.waitForTimeout(120); } diff --git a/src/assets/images/battle/twenty-seventh-battle-map.svg b/src/assets/images/battle/twenty-seventh-battle-map.svg new file mode 100644 index 0000000..d936170 --- /dev/null +++ b/src/assets/images/battle/twenty-seventh-battle-map.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index 876ca45..934d989 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -63,6 +63,10 @@ import { twentySixthBattleMap, twentySixthBattleUnits, twentySixthBattleVictoryPages, + twentySeventhBattleBonds, + twentySeventhBattleMap, + twentySeventhBattleUnits, + twentySeventhBattleVictoryPages, twentiethBattleBonds, twentiethBattleMap, twentiethBattleUnits, @@ -135,7 +139,8 @@ export type BattleScenarioId = | 'twenty-third-battle-jingzhou-south-entry' | 'twenty-fourth-battle-guiyang-persuasion' | 'twenty-fifth-battle-wuling-mountain-road' - | 'twenty-sixth-battle-changsha-veteran'; + | 'twenty-sixth-battle-changsha-veteran' + | 'twenty-seventh-battle-yizhou-relief-road'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -1633,6 +1638,69 @@ export const twentySixthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const twentySeventhBattleScenario: BattleScenarioDefinition = { + id: 'twenty-seventh-battle-yizhou-relief-road', + title: '익주 원군로', + victoryConditionLabel: '양회 퇴각', + defeatConditionLabel: '유비 또는 제갈량 퇴각', + openingObjectiveLines: [ + '장사까지 안정시킨 유비군은 유장의 원군 요청을 따라 익주로 향합니다. 원군로에는 양회의 전초 병력이 관문과 마을을 함께 틀어쥐고 있습니다.', + '이번 전투는 길목의 마을을 지키면서 관문 전열을 무너뜨려야 합니다. 황충과 위연까지 합류한 큰 로스터에서 산길, 돌파, 책략 역할을 직접 골라 출전하십시오.', + '유비와 제갈량을 지키며 양회를 물러나게 하면 봉추 방통이 군영에 합류합니다.' + ], + map: twentySeventhBattleMap, + units: twentySeventhBattleUnits, + bonds: twentySeventhBattleBonds, + mapTextureKey: 'battle-map-twenty-seventh', + leaderUnitId: 'yizhou-leader-yang-huai', + quickVictoryTurnLimit: 36, + baseVictoryGold: 3760, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '양회 퇴각', + rewardGold: 2420, + unitId: 'yizhou-leader-yang-huai' + }, + { + id: 'liu-bei', + kind: 'keep-unit-alive', + label: '유비 생존', + rewardGold: 700, + unitId: 'liu-bei' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 980, + unitId: 'zhuge-liang' + }, + { + id: 'yizhou-village', + kind: 'secure-terrain', + label: '익주 마을 안정', + rewardGold: 1160, + terrain: 'village' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '36턴 이내 승리', + rewardGold: 920, + maxTurn: 36 + } + ], + defeatConditions: [ + { kind: 'unit-defeated', unitId: 'liu-bei' }, + { kind: 'unit-defeated', unitId: 'zhuge-liang' } + ], + itemRewards: ['콩 12', '상처약 9', '탁주 5', '익주 길목 지도 +1', '방통 합류'], + victoryPages: twentySeventhBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -1661,7 +1729,8 @@ export const battleScenarios: Record 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario, 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario, 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario, - 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario + 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario, + 'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index eb41a03..c90ec79 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -23,6 +23,7 @@ import { twentyFifthBattleScenario, twentyFourthBattleScenario, twentySixthBattleScenario, + twentySeventhBattleScenario, twentiethBattleScenario, twelfthBattleScenario, thirdBattleScenario, @@ -74,6 +75,8 @@ import { twentyFourthBattleVictoryPages, twentySixthBattleIntroPages, twentySixthBattleVictoryPages, + twentySeventhBattleIntroPages, + twentySeventhBattleVictoryPages, twentiethBattleIntroPages, twentiethBattleVictoryPages, twelfthBattleIntroPages, @@ -361,13 +364,24 @@ const sortieFlows: Record = { }, [twentySixthBattleScenario.id]: { afterBattleId: twentySixthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '익주 진입 준비', + eyebrow: '다음 전장', + title: twentySeventhBattleScenario.title, description: - '장사까지 열리며 형주 남부의 뿌리가 단단해졌습니다. 다음은 익주로 향하는 더 긴 정치와 전장의 길을 준비해야 합니다.', - rewardHint: '다음 장: 익주 진입 준비 중', - pages: twentySixthBattleVictoryPages, - unavailableNotice: '익주 진입 장은 다음 작업에서 이어집니다. 군영에서 황충과 위연을 살피고 다음 대장정의 출전 후보를 정비하십시오.' + '장사까지 열리며 형주 남부의 뿌리가 단단해졌습니다. 이제 유장의 원군 요청을 명분으로 익주 원군로를 열고, 봉추 방통의 마음까지 얻어야 합니다.', + rewardHint: `예상 보상: ${twentySeventhBattleScenario.title} 개방 / 방통 합류`, + nextBattleId: twentySeventhBattleScenario.id, + campaignStep: 'twenty-seventh-battle', + pages: [...twentySixthBattleVictoryPages, ...twentySeventhBattleIntroPages] + }, + [twentySeventhBattleScenario.id]: { + afterBattleId: twentySeventhBattleScenario.id, + eyebrow: '다음 장 준비', + title: '부수관 진입 준비', + description: + '익주 원군로가 열리고 방통이 합류했습니다. 이제 유비군은 촉의 깊은 산문과 부수관으로 들어가기 전, 와룡과 봉추의 계책을 함께 다듬어야 합니다.', + rewardHint: '다음 장: 부수관 진입 준비 중', + pages: twentySeventhBattleVictoryPages, + unavailableNotice: '부수관 이후 익주 심부 장은 다음 작업에서 이어집니다. 군영에서 방통과 기존 무장들의 공명도를 올리고 다음 대장정의 출전 후보를 정비하십시오.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 8e05d6f..70b49b7 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -1704,6 +1704,68 @@ export const twentySixthBattleVictoryPages: StoryPage[] = [ } ]; +export const twentySeventhBattleIntroPages: StoryPage[] = [ + { + id: 'twenty-seventh-yizhou-request', + bgm: 'story-dark', + chapter: '익주에서 온 요청', + background: 'story-three-heroes', + speaker: '손건', + text: '익주의 유장이 한중 장로의 압박을 두려워하여 원군을 청했습니다. 형주 남부를 안정시킨 지금, 이 요청은 촉으로 향하는 첫 문이 될 수 있습니다.' + }, + { + id: 'twenty-seventh-pang-tong-rumor', + bgm: 'battle-prep', + chapter: '봉추의 그림자', + background: 'story-liu-bei', + speaker: '제갈량', + text: '익주로 들어가는 길목에는 봉추라 불리는 방통이 머문다는 소문이 있습니다. 그가 뜻을 함께한다면, 산과 관문으로 막힌 촉의 길도 달리 보일 것입니다.' + }, + { + id: 'twenty-seventh-gate-warning', + bgm: 'story-dark', + chapter: '원군로의 관문', + background: 'story-militia', + speaker: '마량', + text: '양회가 이끄는 전초 병력이 원군로를 막고 있습니다. 길목의 마을을 지켜 명분을 잃지 않고, 산길의 척후를 먼저 끊어야 합니다.' + }, + { + id: 'twenty-seventh-yizhou-sortie', + bgm: 'battle-prep', + chapter: '익주 원군로', + background: 'story-sortie', + speaker: '유비', + portrait: 'liuBei', + text: '우리는 익주를 빼앗으러 가는 것이 아니라, 백성을 지키라는 부름에 응하는 것이오. 군율을 세우고 길을 열어 봉추의 마음까지 얻어 봅시다.' + } +]; + +export const twentySeventhBattleVictoryPages: StoryPage[] = [ + { + id: 'twenty-seventh-victory-road-open', + bgm: 'militia-theme', + chapter: '익주의 길이 열리다', + background: 'story-sortie', + text: '양회의 전초 병력이 물러나고 익주 원군로가 열렸습니다. 유비군은 마을의 창고를 건드리지 않고 길목을 안정시켜, 원군이라는 명분을 분명히 세웠습니다.' + }, + { + id: 'twenty-seventh-pang-tong-joins', + bgm: 'battle-prep', + chapter: '방통의 합류', + background: 'story-liu-bei', + speaker: '방통', + text: '인의를 말로만 내세우는 군은 많으나, 길목의 작은 마을까지 지키며 움직이는 군은 드뭅니다. 이 방통, 촉으로 향하는 험한 길을 함께 보겠습니다.' + }, + { + id: 'twenty-seventh-next-fu-pass', + bgm: 'story-dark', + chapter: '부수관을 바라보다', + background: 'story-three-heroes', + speaker: '제갈량', + text: '봉추가 합류하니 익주의 지세와 사람의 마음을 함께 읽을 수 있게 되었습니다. 다음 길은 부수관과 낙성으로 이어지는 더 깊은 산문입니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -2005,6 +2067,12 @@ export const twentySixthBattleMap: BattleMap = { terrain: createTwentySixthBattleTerrain() }; +export const twentySeventhBattleMap: BattleMap = { + width: 54, + height: 36, + terrain: createTwentySeventhBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -4078,6 +4146,30 @@ export const changshaRecruitUnits: UnitData[] = [ } ]; +export const yizhouRecruitUnits: UnitData[] = [ + { + id: 'pang-tong', + name: '방통', + faction: 'ally', + className: '봉추 책사', + classKey: 'strategist', + level: 16, + exp: 0, + hp: 36, + maxHp: 36, + attack: 9, + move: 4, + stats: { might: 44, intelligence: 96, leadership: 88, agility: 72, luck: 82 }, + equipment: { + weapon: { itemId: 'white-feather-fan', level: 2, exp: 16 }, + armor: { itemId: 'cloth-armor', level: 2, exp: 12 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 8, + y: 24 + } +]; + const eighthBattleAllyPositions: Record = { 'liu-bei': { x: 2, y: 17 }, 'guan-yu': { x: 3, y: 17 }, @@ -10584,6 +10676,435 @@ export const twentySixthBattleUnits: UnitData[] = [ } ]; +const twentySeventhBattleAllyPositions: Record = { + 'liu-bei': { x: 4, y: 30 }, + 'guan-yu': { x: 6, y: 29 }, + 'zhang-fei': { x: 5, y: 32 }, + 'jian-yong': { x: 3, y: 33 }, + 'mi-zhu': { x: 4, y: 34 }, + 'sun-qian': { x: 7, y: 33 }, + 'zhao-yun': { x: 8, y: 30 }, + 'zhuge-liang': { x: 5, y: 30 }, + 'ma-liang': { x: 8, y: 34 }, + 'yi-ji': { x: 9, y: 32 }, + 'gong-zhi': { x: 10, y: 34 }, + 'huang-zhong': { x: 7, y: 31 }, + 'wei-yan': { x: 9, y: 29 } +}; + +export const twentySeventhBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits + ].map((unit) => placeScenarioUnit(unit, twentySeventhBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + { + id: 'yizhou-scout-a', + name: '익주 척후', + faction: 'enemy', + className: '산길 척후', + classKey: 'bandit', + level: 41, + exp: 62, + hp: 106, + maxHp: 106, + attack: 40, + move: 4, + stats: { might: 128, intelligence: 92, leadership: 104, agility: 120, luck: 84 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 42 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 16, + y: 30 + }, + { + id: 'yizhou-scout-b', + name: '익주 척후', + faction: 'enemy', + className: '관문 척후', + classKey: 'bandit', + level: 41, + exp: 62, + hp: 106, + maxHp: 106, + attack: 40, + move: 4, + stats: { might: 128, intelligence: 92, leadership: 104, agility: 120, luck: 84 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 42 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 28 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 20, + y: 28 + }, + { + id: 'yizhou-infantry-a', + name: '익주 보병', + faction: 'enemy', + className: '원군로 보병', + classKey: 'yellowTurban', + level: 42, + exp: 64, + hp: 140, + maxHp: 140, + attack: 44, + move: 3, + stats: { might: 142, intelligence: 92, leadership: 120, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 58 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 24, + y: 27 + }, + { + id: 'yizhou-infantry-b', + name: '익주 보병', + faction: 'enemy', + className: '고개 보병', + classKey: 'yellowTurban', + level: 42, + exp: 64, + hp: 140, + maxHp: 140, + attack: 44, + move: 3, + stats: { might: 142, intelligence: 92, leadership: 120, agility: 100, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 4, exp: 58 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 42 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 28, + y: 24 + }, + { + id: 'yizhou-infantry-c', + name: '익주 보병', + faction: 'enemy', + className: '관문 보병', + classKey: 'yellowTurban', + level: 43, + exp: 66, + hp: 148, + maxHp: 148, + attack: 46, + move: 3, + stats: { might: 146, intelligence: 94, leadership: 124, agility: 102, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 8 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 35, + y: 21 + }, + { + id: 'yizhou-infantry-d', + name: '익주 보병', + faction: 'enemy', + className: '성문 보병', + classKey: 'yellowTurban', + level: 43, + exp: 66, + hp: 148, + maxHp: 148, + attack: 46, + move: 3, + stats: { might: 146, intelligence: 94, leadership: 124, agility: 102, luck: 84 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 8 }, + armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 40, + y: 17 + }, + { + id: 'yizhou-archer-a', + name: '익주 궁병', + faction: 'enemy', + className: '산비탈 궁병', + classKey: 'archer', + level: 42, + exp: 64, + hp: 104, + maxHp: 104, + attack: 42, + move: 3, + stats: { might: 102, intelligence: 102, leadership: 108, agility: 116, luck: 86 }, + equipment: { + weapon: { itemId: 'short-bow', level: 4, exp: 54 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 34 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 30, + y: 21 + }, + { + id: 'yizhou-archer-b', + name: '익주 궁병', + faction: 'enemy', + className: '관문 궁병', + classKey: 'archer', + level: 43, + exp: 66, + hp: 108, + maxHp: 108, + attack: 43, + move: 3, + stats: { might: 104, intelligence: 104, leadership: 110, agility: 118, luck: 86 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 4 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 38 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 42, + y: 15 + }, + { + id: 'yizhou-archer-c', + name: '익주 궁병', + faction: 'enemy', + className: '성루 궁병', + classKey: 'archer', + level: 43, + exp: 66, + hp: 108, + maxHp: 108, + attack: 43, + move: 3, + stats: { might: 104, intelligence: 104, leadership: 110, agility: 118, luck: 86 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 4 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 38 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 47, + y: 19 + }, + { + id: 'yizhou-cavalry-a', + name: '익주 기병', + faction: 'enemy', + className: '원군로 기병', + classKey: 'cavalry', + level: 43, + exp: 66, + hp: 148, + maxHp: 148, + attack: 47, + move: 5, + stats: { might: 146, intelligence: 92, leadership: 120, agility: 120, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 10 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 44 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 22, + y: 31 + }, + { + id: 'yizhou-cavalry-b', + name: '익주 기병', + faction: 'enemy', + className: '산문 기병', + classKey: 'cavalry', + level: 43, + exp: 66, + hp: 148, + maxHp: 148, + attack: 47, + move: 5, + stats: { might: 146, intelligence: 92, leadership: 120, agility: 120, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 10 }, + armor: { itemId: 'lamellar-armor', level: 4, exp: 44 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 37, + y: 28 + }, + { + id: 'yizhou-cavalry-c', + name: '익주 기병', + faction: 'enemy', + className: '성문 기병', + classKey: 'cavalry', + level: 44, + exp: 68, + hp: 154, + maxHp: 154, + attack: 49, + move: 5, + stats: { might: 150, intelligence: 94, leadership: 124, agility: 122, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 18 }, + armor: { itemId: 'lamellar-armor', level: 5, exp: 4 }, + accessory: { itemId: 'bravery-token', level: 1, exp: 0 } + }, + x: 45, + y: 25 + }, + { + id: 'yizhou-guard-a', + name: '양회 친위대', + faction: 'enemy', + className: '관문 친위대', + classKey: 'yellowTurban', + level: 44, + exp: 68, + hp: 158, + maxHp: 158, + attack: 49, + move: 3, + stats: { might: 152, intelligence: 96, leadership: 128, agility: 104, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 20 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 46, + y: 15 + }, + { + id: 'yizhou-guard-b', + name: '양회 친위대', + faction: 'enemy', + className: '성문 친위대', + classKey: 'yellowTurban', + level: 44, + exp: 68, + hp: 158, + maxHp: 158, + attack: 49, + move: 3, + stats: { might: 152, intelligence: 96, leadership: 128, agility: 104, luck: 86 }, + equipment: { + weapon: { itemId: 'iron-spear', level: 5, exp: 20 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 8 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 49, + y: 17 + }, + { + id: 'yizhou-strategist-a', + name: '익주 책사', + faction: 'enemy', + className: '관문 책사', + classKey: 'archer', + level: 43, + exp: 66, + hp: 108, + maxHp: 108, + attack: 43, + move: 3, + stats: { might: 92, intelligence: 120, leadership: 112, agility: 98, luck: 86 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 4 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 38 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 41, + y: 13 + }, + { + id: 'yizhou-strategist-b', + name: '익주 책사', + faction: 'enemy', + className: '성문 책사', + classKey: 'archer', + level: 43, + exp: 66, + hp: 108, + maxHp: 108, + attack: 43, + move: 3, + stats: { might: 92, intelligence: 120, leadership: 112, agility: 98, luck: 86 }, + equipment: { + weapon: { itemId: 'short-bow', level: 5, exp: 4 }, + armor: { itemId: 'cloth-armor', level: 4, exp: 38 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 48, + y: 21 + }, + { + id: 'yizhou-ambusher-a', + name: '복병', + faction: 'enemy', + className: '숲길 복병', + classKey: 'bandit', + level: 42, + exp: 64, + hp: 112, + maxHp: 112, + attack: 42, + move: 4, + stats: { might: 132, intelligence: 94, leadership: 106, agility: 124, luck: 84 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 48 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 32 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 31, + y: 30 + }, + { + id: 'yizhou-ambusher-b', + name: '복병', + faction: 'enemy', + className: '고개 복병', + classKey: 'bandit', + level: 42, + exp: 64, + hp: 112, + maxHp: 112, + attack: 42, + move: 4, + stats: { might: 132, intelligence: 94, leadership: 106, agility: 124, luck: 84 }, + equipment: { + weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 48 }, + armor: { itemId: 'rebel-vest', level: 4, exp: 32 }, + accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } + }, + x: 36, + y: 8 + }, + { + id: 'yizhou-leader-yang-huai', + name: '양회', + faction: 'enemy', + className: '익주 관문장', + classKey: 'rebelLeader', + level: 45, + exp: 72, + hp: 204, + maxHp: 204, + attack: 50, + move: 4, + stats: { might: 150, intelligence: 112, leadership: 136, agility: 106, luck: 88 }, + equipment: { + weapon: { itemId: 'leader-axe', level: 5, exp: 20 }, + armor: { itemId: 'reinforced-lamellar', level: 5, exp: 16 }, + accessory: { itemId: 'war-manual', level: 1, exp: 0 } + }, + x: 50, + y: 16 + } +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -10827,6 +11348,33 @@ export const changshaRecruitBonds: BattleBond[] = [ } ]; +export const yizhouRecruitBonds: BattleBond[] = [ + { + id: 'liu-bei__pang-tong', + unitIds: ['liu-bei', 'pang-tong'], + title: '봉추가 보는 인의', + level: 36, + exp: 0, + description: '유비의 인의와 방통의 날카로운 지략이 만나, 익주의 산문을 명분과 속도로 함께 열어 갑니다.' + }, + { + id: 'zhuge-liang__pang-tong', + unitIds: ['zhuge-liang', 'pang-tong'], + title: '와룡과 봉추', + level: 34, + exp: 0, + description: '제갈량의 큰 판과 방통의 과감한 계책이 맞물려, 어려운 지형에서도 새로운 돌파구를 찾아냅니다.' + }, + { + id: 'huang-zhong__pang-tong', + unitIds: ['huang-zhong', 'pang-tong'], + 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); @@ -10858,6 +11406,7 @@ export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds] export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario); export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario); export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario); +export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds, ...changshaRecruitBonds].map(cloneBattleBondForScenario); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -11823,6 +12372,62 @@ function createTwentySixthBattleTerrain(): TerrainType[][] { ); } +function createTwentySeventhBattleTerrain(): TerrainType[][] { + return Array.from({ length: 36 }, (_, y) => + Array.from({ length: 54 }, (_, x): TerrainType => { + if (x <= 4 && y >= 29) { + return 'camp'; + } + if ((x >= 45 && x <= 52 && y >= 12 && y <= 20) || (x >= 41 && x <= 47 && y >= 9 && y <= 14)) { + return 'fort'; + } + if ((x >= 18 && x <= 20 && y >= 27 && y <= 29) || (x >= 32 && x <= 34 && y >= 22 && y <= 24) || (x >= 38 && x <= 40 && y >= 16 && y <= 18)) { + return 'village'; + } + if ( + (y >= 31 && x >= 4 && x <= 15) || + (x >= 14 && x <= 24 && y >= 27 && y <= 31) || + (x >= 23 && x <= 33 && y >= 23 && y <= 27) || + (x >= 32 && x <= 40 && y >= 18 && y <= 23) || + (x >= 39 && x <= 48 && y >= 14 && y <= 18) || + (x >= 47 && x <= 51 && y >= 15 && y <= 17) + ) { + return 'road'; + } + if ((x === 12 || x === 13) && y >= 0 && y <= 25) { + return 'river'; + } + if ((x === 29 || x === 30) && y >= 4 && y <= 21) { + return 'river'; + } + if ((x === 36 || x === 37) && y >= 25 && y <= 35) { + return 'river'; + } + if ( + (x >= 4 && x <= 16 && y >= 8 && y <= 20) || + (x >= 17 && x <= 28 && y >= 10 && y <= 21) || + (x >= 31 && x <= 42 && y >= 25 && y <= 35) || + (x >= 41 && x <= 53 && y >= 22 && y <= 33) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 10 && y <= 11) || + (x >= 20 && x <= 28 && y <= 9) || + (x >= 31 && x <= 43 && y >= 4 && y <= 12) || + (x >= 47 && x <= 53 && y <= 11) || + (x >= 22 && x <= 31 && y >= 29 && y <= 35) + ) { + return 'hill'; + } + if ((x <= 2 && y <= 28) || (x >= 52 && y >= 20) || (x >= 43 && x <= 45 && y <= 8)) { + 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 e01ea40..c05add4 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -34,6 +34,7 @@ const unitTexture: Record = { 'gong-zhi': 'unit-guan-yu', 'huang-zhong': 'unit-rebel-archer', 'wei-yan': 'unit-guan-yu', + 'pang-tong': 'unit-liu-bei', 'rebel-a': 'unit-rebel', 'rebel-b': 'unit-rebel', 'rebel-c': 'unit-rebel-archer', @@ -316,7 +317,26 @@ const unitTexture: Record = { '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' + 'changsha-leader-han-xuan': 'unit-rebel-leader', + 'yizhou-scout-a': 'unit-rebel', + 'yizhou-scout-b': 'unit-rebel', + 'yizhou-infantry-a': 'unit-rebel', + 'yizhou-infantry-b': 'unit-rebel', + 'yizhou-infantry-c': 'unit-rebel', + 'yizhou-infantry-d': 'unit-rebel', + 'yizhou-archer-a': 'unit-rebel-archer', + 'yizhou-archer-b': 'unit-rebel-archer', + 'yizhou-archer-c': 'unit-rebel-archer', + 'yizhou-cavalry-a': 'unit-rebel-cavalry', + 'yizhou-cavalry-b': 'unit-rebel-cavalry', + 'yizhou-cavalry-c': 'unit-rebel-cavalry', + 'yizhou-guard-a': 'unit-rebel', + 'yizhou-guard-b': 'unit-rebel', + 'yizhou-strategist-a': 'unit-rebel-archer', + 'yizhou-strategist-b': 'unit-rebel-archer', + 'yizhou-ambusher-a': 'unit-rebel', + 'yizhou-ambusher-b': 'unit-rebel', + 'yizhou-leader-yang-huai': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -742,7 +762,8 @@ const unitStrategyIds: Record = { 'yi-ji': ['aid', 'encourage'], 'gong-zhi': ['encourage'], 'huang-zhong': ['encourage'], - 'wei-yan': ['roar'] + 'wei-yan': ['roar'], + 'pang-tong': ['aid', 'encourage', 'fireTactic'] }; const initialItemStocks: Record> = { @@ -758,7 +779,8 @@ const initialItemStocks: Record> = { 'yi-ji': { bean: 2, salve: 1 }, 'gong-zhi': { bean: 2 }, 'huang-zhong': { bean: 1, wine: 1 }, - 'wei-yan': { bean: 2 } + 'wei-yan': { bean: 2 }, + 'pang-tong': { bean: 1, salve: 1, wine: 1 } }; const attackRangeByClass: Partial> = { @@ -1093,7 +1115,26 @@ const enemyAiByUnitId: Record = { 'changsha-strategist-b': 'hold', 'changsha-veteran-huang-zhong': 'hold', 'changsha-officer-wei-yan': 'aggressive', - 'changsha-leader-han-xuan': 'guard' + 'changsha-leader-han-xuan': 'guard', + 'yizhou-scout-a': 'aggressive', + 'yizhou-scout-b': 'aggressive', + 'yizhou-infantry-a': 'guard', + 'yizhou-infantry-b': 'guard', + 'yizhou-infantry-c': 'guard', + 'yizhou-infantry-d': 'guard', + 'yizhou-archer-a': 'hold', + 'yizhou-archer-b': 'hold', + 'yizhou-archer-c': 'hold', + 'yizhou-cavalry-a': 'aggressive', + 'yizhou-cavalry-b': 'aggressive', + 'yizhou-cavalry-c': 'aggressive', + 'yizhou-guard-a': 'guard', + 'yizhou-guard-b': 'guard', + 'yizhou-strategist-a': 'hold', + 'yizhou-strategist-b': 'hold', + 'yizhou-ambusher-a': 'aggressive', + 'yizhou-ambusher-b': 'aggressive', + 'yizhou-leader-yang-huai': 'guard' }; const defaultEnemyAiByClass: Partial> = { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index d44ff93..8791aeb 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -23,6 +23,7 @@ import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-bat 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 twentySeventhBattleMapUrl from '../../assets/images/battle/twenty-seventh-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'; @@ -110,6 +111,7 @@ export class BootScene extends Phaser.Scene { 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('battle-map-twenty-seventh', twentySeventhBattleMapUrl); 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 6cee0b0..007b0ea 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -21,6 +21,8 @@ import { wulingRecruitUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, + yizhouRecruitBonds, + yizhouRecruitUnits, zhugeRecruitBonds, zhugeRecruitUnits, type PortraitKey, @@ -307,9 +309,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'twenty-third-battle-jingzhou-south-entry', 'twenty-fourth-battle-guiyang-persuasion', 'twenty-fifth-battle-wuling-mountain-road', - 'twenty-sixth-battle-changsha-veteran' + 'twenty-sixth-battle-changsha-veteran', + 'twenty-seventh-battle-yizhou-relief-road' ], - nextHints: ['형주 방위', '익주 진입', '장수 대거 합류'] + nextHints: ['형주 방위', '익주 진입', '방통 합류'] }, { id: 'shu-han', @@ -347,7 +350,8 @@ const campBattleIds = { twentyThird: 'twenty-third-battle-jingzhou-south-entry', twentyFourth: 'twenty-fourth-battle-guiyang-persuasion', twentyFifth: 'twenty-fifth-battle-wuling-mountain-road', - twentySixth: 'twenty-sixth-battle-changsha-veteran' + twentySixth: 'twenty-sixth-battle-changsha-veteran', + twentySeventh: 'twenty-seventh-battle-yizhou-relief-road' } as const; const requiredSortieUnitIds = new Set(['liu-bei']); @@ -634,6 +638,18 @@ const sortieRulesByBattleId: Partial 7 ? 30 : allies.length > 5 ? 38 : allies.length > 4 ? 43 : 48; - const rowHeight = allies.length > 7 ? 24 : rowGap - 6; + const denseRoster = allies.length > 12; + const rowGap = denseRoster ? 17 : allies.length > 7 ? 30 : allies.length > 5 ? 38 : allies.length > 4 ? 43 : 48; + const rowHeight = denseRoster ? 15 : allies.length > 7 ? 24 : rowGap - 6; this.trackSortie( this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${maxUnits}`, this.textStyle(18, '#f2e3bf', true)) ).setDepth(depth + 1); this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭해서 출전/대기 전환', this.textStyle(12, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1); allies.forEach((unit, index) => { - const rowY = y + 50 + index * rowGap; + const rowY = y + (denseRoster ? 48 : 50) + index * rowGap; const selected = this.isSortieSelected(unit.id); const required = requiredSortieUnitIds.has(unit.id); const recruited = !foundingSortieUnitIds.has(unit.id); @@ -4070,17 +4260,21 @@ export class CampScene extends Phaser.Scene { const marker = selected ? '출전' : '대기'; const markerLabel = required ? '필수' : recommendation ? '추천' : recruited ? '합류' : marker; - this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2); - this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2); - this.trackSortie(this.add.text(x + 214, rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, 19), this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); - this.trackSortie(this.add.text(x + 424, rowY, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); - this.drawSortieBar(x + 514, rowY + 6, 74, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2); - this.trackSortie(this.add.text(x + 610, rowY, this.compactText(summary.terrainLine, 13), this.textStyle(11, selected ? '#d8b15f' : '#77818c', true))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(denseRoster ? 10 : 12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + (denseRoster ? 78 : 84), rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(denseRoster ? 12 : 14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + (denseRoster ? 194 : 214), rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, denseRoster ? 17 : 19), this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + 424, rowY, denseRoster ? this.compactText(summary.statLine, 23) : `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(denseRoster ? 10 : 12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2); + if (!denseRoster) { + this.drawSortieBar(x + 514, rowY + 6, 74, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2); + } + this.trackSortie(this.add.text(x + 610, rowY, this.compactText(denseRoster ? summary.bondLine : summary.terrainLine, denseRoster ? 15 : 13), this.textStyle(11, selected ? (denseRoster ? '#a8ffd0' : '#d8b15f') : '#77818c', true))).setDepth(depth + 2); - this.renderSortieEquipmentIcons(unit, x + 84, rowY + 19, selected, depth + 2); - this.trackSortie(this.add.text(x + 162, rowY + 18, this.compactText(summary.equipmentLine, 34), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); - this.trackSortie(this.add.text(x + 424, rowY + 18, this.compactText(summary.statLine, 23), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); - this.trackSortie(this.add.text(x + 610, rowY + 18, this.compactText(summary.bondLine, 15), this.textStyle(11, selected ? '#a8ffd0' : '#68727e', selected))).setDepth(depth + 2); + if (!denseRoster) { + this.renderSortieEquipmentIcons(unit, x + 84, rowY + 19, selected, depth + 2); + this.trackSortie(this.add.text(x + 162, rowY + 18, this.compactText(summary.equipmentLine, 34), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + 424, rowY + 18, this.compactText(summary.statLine, 23), this.textStyle(11, selected ? '#c8d2dd' : '#68727e'))).setDepth(depth + 2); + this.trackSortie(this.add.text(x + 610, rowY + 18, this.compactText(summary.bondLine, 15), this.textStyle(11, selected ? '#a8ffd0' : '#68727e', selected))).setDepth(depth + 2); + } }); } diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index 8cd5ba3..d52752b 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -24,6 +24,7 @@ import { twentyFifthBattleScenario, twentyFourthBattleScenario, twentySixthBattleScenario, + twentySeventhBattleScenario, twentiethBattleScenario, twelfthBattleScenario, thirdBattleScenario @@ -351,7 +352,8 @@ export class TitleScene extends Phaser.Scene { campaign.step === 'twenty-third-camp' || campaign.step === 'twenty-fourth-camp' || campaign.step === 'twenty-fifth-camp' || - campaign.step === 'twenty-sixth-camp' + campaign.step === 'twenty-sixth-camp' || + campaign.step === 'twenty-seventh-camp' ) { this.scene.start('CampScene'); return; @@ -487,6 +489,11 @@ export class TitleScene extends Phaser.Scene { return; } + if (campaign.step === 'twenty-seventh-battle') { + this.scene.start('BattleScene', { battleId: twentySeventhBattleScenario.id }); + return; + } + if (campaign.step === 'first-victory-story') { this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' }); return; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index b44686f..4b4b397 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -93,7 +93,9 @@ export type CampaignStep = | 'twenty-fifth-battle' | 'twenty-fifth-camp' | 'twenty-sixth-battle' - | 'twenty-sixth-camp'; + | 'twenty-sixth-camp' + | 'twenty-seventh-battle' + | 'twenty-seventh-camp'; export type CampaignUnitProgressSnapshot = { unitId: string; @@ -181,7 +183,8 @@ const campaignBattleSteps: Record