diff --git a/docs/roadmap.md b/docs/roadmap.md index d7168ea..0ba146c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -79,15 +79,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Fifty-second battle Meng Huo fifth-capture route, where hardline clans split from wavering clans across a 104x86 bamboo-ravine battlefield, with hardliner-separation, village-protection, and fort-suppression objectives plus post-battle ledger/banner camp events - Fifty-third battle Meng Huo sixth-capture route, where Meng Huo's remaining pride meets a 106x88 deep-ravine battlefield, with village-trust, trust-road, and hardliner-remnant objectives plus post-battle trust-ledger/final-persuasion camp events - Fifty-fourth battle Meng Huo final-capture route, where the Seven Captures arc closes on a 108x90 council-ground battlefield with final pacify, council-road, village-witness, and remnant-fort objectives plus Nanzhong pacification camp events +- Post-Nanzhong northern-campaign preparation milestone, where the final pacification camp advances into a Zhuge Liang-led war council, excludes Liu Bei from the preparation roster, and opens northern sortie planning dialogue, granary checks, and memorial-drafting visits - 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, focused officer analysis, 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, selectable reserve drill focus, 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 final Meng Huo capture and Nanzhong pacification camp, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire/Baidi entrustment/Nanzhong stabilization/Meng Huo pacification camp states, campaign timeline state, and camp save state +- Flow verification script from title through northern-campaign preparation after the final Meng Huo capture, King of Hanzhong council milestone, Shu-Han foundation milestone, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng/Luo main gate/Mianzhu/Chengdu/Jiameng/Yangping/Dingjun/Hanzhong/Jing defense/Fan Castle/Han River/Fan siege/Jing rear/Gongan collapse/Maicheng isolation/Yiling vanguard/Yiling fire/Baidi entrustment/Nanzhong stabilization/Meng Huo pacification camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the post-Nanzhong northern-campaign preparation milestone so the pacified rear opens Zhuge Liang's next long-term objective instead of ending the campaign +1. Build the first northern expedition battle from the new preparation camp, likely around the Qishan/Jieting opening pressure so sortie choices immediately matter 2. Expand post-Baidi and post-Nanzhong camp consequences with grief checks, recovery policy choices, village reassurance, captive handling, and regency decisions that affect sortie recommendations 3. Expand reserve planning into individual officer assignments, including targeted class practice, paired bond training, and camp task slots 4. Add a dedicated treasure/equipment management view so players can compare special effects and growth diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 119cbe2..344f64e 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -7593,13 +7593,87 @@ try { postFinalCaptureProgressState?.activeTab !== 'progress' || postFinalCaptureProgressState.campaignProgress?.completedKnown !== 54 || postFinalCaptureProgressState.campaignProgress?.totalKnown !== 54 || + postFinalCaptureProgressState.campaignProgress?.activeChapter?.title !== '북벌 준비' || postFinalCaptureProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' || - postFinalCaptureProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postFinalCaptureProgressState.campaignProgress?.nextBattleTitle !== '북벌 준비 회의' ) { - throw new Error(`Expected post-final-capture progress tab to complete the fifty-fourth battle and pause before the northern campaign setup: ${JSON.stringify(postFinalCaptureProgressState?.campaignProgress)}`); + throw new Error(`Expected post-final-capture progress tab to complete the fifty-fourth battle and point toward northern campaign preparation: ${JSON.stringify(postFinalCaptureProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-final-capture-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const northernPrepSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !northernPrepSortieState?.sortieVisible || + northernPrepSortieState.campaign?.step !== 'fifty-fourth-camp' || + northernPrepSortieState.campTitle !== '남중 평정 후 군영' || + northernPrepSortieState.sortiePlan?.objectiveLine !== '북벌 준비 회의 · 군영 의정' || + northernPrepSortieState.sortiePlan?.maxCount !== 7 || + northernPrepSortieState.sortiePlan?.recommendedTotal !== 7 || + !northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || + !northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'huang-quan' && unit.recommended) || + !northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recommended) || + northernPrepSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected northern campaign prep sortie panel to center Zhuge Liang, exclude Liu Bei, and expose a seven-officer planning roster: ${JSON.stringify(northernPrepSortieState)}`); + } + await page.screenshot({ path: 'dist/verification-northern-prep-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-northern-prep-story.png', fullPage: true }); + + for (let i = 0; i < 18; i += 1) { + const returnedToCamp = await page.evaluate(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); + }); + if (returnedToCamp) { + break; + } + await page.keyboard.press('Space'); + await page.waitForTimeout(320); + } + await page.waitForFunction(() => { + const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; + return activeScenes.includes('CampScene') && !activeScenes.includes('StoryScene'); + }); + + const northernPrepCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + northernPrepCampState?.campaign?.step !== 'northern-campaign-prep-camp' || + northernPrepCampState.campTitle !== '북벌 준비 군영' || + northernPrepCampState.campBattleId !== 'fifty-fourth-battle-meng-huo-final-capture' || + northernPrepCampState.availableDialogueIds?.length !== 3 || + !northernPrepCampState.availableDialogueIds.every((id) => id.startsWith('northern-prep-')) || + northernPrepCampState.availableVisitIds?.length !== 2 || + !northernPrepCampState.availableVisitIds.every((id) => id.startsWith('northern-prep-')) || + northernPrepCampState.rosterCollection?.total < 21 + ) { + throw new Error(`Expected northern campaign prep story to persist a dedicated camp step with preparation dialogue and visit sets: ${JSON.stringify(northernPrepCampState)}`); + } + await page.screenshot({ path: 'dist/verification-northern-prep-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postNorthernPrepProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postNorthernPrepProgressState?.activeTab !== 'progress' || + postNorthernPrepProgressState.campaignProgress?.completedKnown !== 54 || + postNorthernPrepProgressState.campaignProgress?.totalKnown !== 54 || + postNorthernPrepProgressState.campaignProgress?.activeChapter?.title !== '북벌 준비' || + postNorthernPrepProgressState.campaignProgress?.latestBattleTitle !== '칠종칠금 7차전' || + postNorthernPrepProgressState.campaignProgress?.nextBattleTitle !== '제1차 북벌 출진 준비' + ) { + throw new Error(`Expected northern campaign prep progress tab to pause before the first northern expedition battle: ${JSON.stringify(postNorthernPrepProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-northern-prep-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -7631,11 +7705,11 @@ try { activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [], camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); - if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'fifty-fourth-camp') { - throw new Error(`Expected title continue to reopen the Nanzhong pacification camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'northern-campaign-prep-camp') { + throw new Error(`Expected title continue to reopen the northern campaign prep camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-final-Meng-Huo-capture flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, and Nanzhong pacification state at ${targetUrl}`); + console.log(`Verified title-to-northern-campaign-prep flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi, Yan Yan, Li Yan, Huang Quan, Ma Chao, Ma Dai, Wang Ping joins, Luofeng, Luo main gate, Mianzhu, Chengdu surrender, Jiameng, Yangping, Dingjun, Hanzhong decisive battle, King of Hanzhong council, Shu-Han foundation state, Jing Province defense vanguard, Fan Castle vanguard, Han River flood, Fan siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, Nanzhong stabilization, Meng Huo pacification, second capture, third capture, fourth capture, fifth capture, sixth capture, final capture, and northern campaign preparation state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 6a9e347..7596f47 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -107,6 +107,7 @@ import { ninthBattleVictoryPages, nineteenthBattleIntroPages, nineteenthBattleVictoryPages, + northernCampaignPrepPages, secondBattleIntroPages, secondBattleVictoryPages, seventhBattleIntroPages, @@ -787,13 +788,25 @@ const sortieFlows: Record = { }, [fiftyFourthBattleScenario.id]: { afterBattleId: fiftyFourthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '남중 평정 이후', + eyebrow: '군영 의정', + title: '북벌 준비 회의', description: - '맹획은 일곱 번의 생포 끝에 항복했고, 남중은 촉한의 약속을 받아들였습니다. 이제 제갈량은 뒤를 안정시킨 뒤 북쪽을 향한 다음 장기전을 준비해야 합니다.', - rewardHint: '다음 장: 북벌 준비와 촉한 내부 정비 준비 중', - pages: fiftyFourthBattleVictoryPages, - unavailableNotice: '북벌 준비 장은 다음 작업에서 이어집니다. 지금은 남중 평정 장부와 맹획 항복 서약을 정비할 수 있습니다.' + '맹획은 일곱 번의 생포 끝에 항복했고, 남중은 촉한의 약속을 받아들였습니다. 출진 준비를 통해 새 전투가 아니라 북벌 준비 회의를 진행하고, 한중 창고와 출전 무장 재편을 군영 안에서 정리합니다.', + rewardHint: '군영 목표: 북벌 준비 회의 / 한중 창고와 출전 무장 재편', + campaignStep: 'northern-campaign-prep-camp', + pages: [...fiftyFourthBattleVictoryPages, ...northernCampaignPrepPages], + unavailableNotice: '북벌 준비 회의가 완료되었습니다. 다음 작업에서는 기산 출진로와 제1차 북벌 전투로 이어갑니다.' + }, + 'northern-campaign-prep-camp': { + afterBattleId: fiftyFourthBattleScenario.id, + eyebrow: '다음 전장 준비', + title: '제1차 북벌 출진 준비', + description: + '남중 평정과 북벌 준비 회의를 마쳤습니다. 이제 제갈량은 한중에서 출사표의 뜻을 세우고, 조운의 기동대와 황권·마량의 보급 판단을 묶어 첫 북벌 전장을 준비해야 합니다.', + rewardHint: '다음 전장: 제1차 북벌 출진로 준비 중', + campaignStep: 'northern-campaign-prep-camp', + pages: northernCampaignPrepPages, + unavailableNotice: '제1차 북벌 전장은 다음 작업에서 이어집니다. 지금은 북벌 편성, 대기 훈련, 군영 대화를 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 9f76d81..5b6e456 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3509,6 +3509,33 @@ export const fiftyFourthBattleVictoryPages: StoryPage[] = [ } ]; +export const northernCampaignPrepPages: StoryPage[] = [ + { + id: 'northern-prep-back-secured', + bgm: 'story-dark', + chapter: '북벌 준비', + background: 'story-militia', + speaker: '제갈량', + text: '남중이 안정되었으니 촉한의 뒤는 흔들림을 줄였습니다. 이제 한중의 창고, 익주의 장부, 장수들의 출전 순서를 다시 맞추어 북쪽으로 향할 때를 준비하겠습니다.' + }, + { + id: 'northern-prep-roster-council', + bgm: 'battle-prep', + chapter: '무장 재편', + background: 'story-sortie', + speaker: '조운', + text: '북쪽 길은 남중과 다릅니다. 산길을 읽는 장수, 보급을 세는 문관, 기병을 끊는 전열이 함께 움직여야 합니다. 다음 출진부터는 누가 먼저 나설지 더 신중히 고르겠습니다.' + }, + { + id: 'northern-prep-long-war', + bgm: 'militia-theme', + chapter: '긴 전쟁의 문턱', + background: 'story-three-heroes', + speaker: '황권', + text: '북벌은 한 번의 승리로 끝나는 길이 아닙니다. 군량과 대기 무장의 훈련, 공명 관계까지 모두 장부에 남겨야 오래 버틸 수 있습니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 1ffd830..e17df52 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -191,6 +191,7 @@ type SortieRecommendation = { type SortieRuleDefinition = { maxUnits: number; requiredUnitIds?: string[]; + excludedUnitIds?: string[]; recommended: SortieRecommendation[]; note: string; }; @@ -424,6 +425,14 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'fifty-fourth-battle-meng-huo-final-capture' ], nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금'] + }, + { + id: 'northern-campaign', + title: '북벌 준비', + period: '출사표를 향해', + description: '남중 평정으로 뒤를 안정시킨 뒤, 한중 창고와 출전 무장 편성을 정비해 첫 북벌을 준비하는 장입니다.', + battleIds: [], + nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '장기전 보급'] } ]; @@ -1143,6 +1152,24 @@ const sortieRulesByBattleId: Partial> = { + 'northern-campaign-prep-camp': { + maxUnits: 7, + requiredUnitIds: ['zhuge-liang'], + excludedUnitIds: ['liu-bei'], + recommended: [ + { unitId: 'zhuge-liang', reason: '북벌 준비 회의의 중심입니다. 한중 창고와 전군 편성을 직접 조율해야 합니다.' }, + { unitId: 'zhao-yun', reason: '기산 출진 전 조운의 기동대 운용과 우회로 정찰이 필요합니다.' }, + { unitId: 'huang-quan', reason: '한중과 익주의 군량 장부를 맞춰 장기전 보급선을 안정시킵니다.' }, + { unitId: 'ma-liang', reason: '문서와 민심을 정리해 북벌 명분과 후방 행정의 빈틈을 줄입니다.' }, + { unitId: 'wang-ping', reason: '산길과 진영 배치를 읽어 다음 북방 전장의 지형 선택을 돕습니다.' }, + { unitId: 'ma-dai', reason: '기병 정찰과 추격 차단을 맡아 출진로의 측면 위험을 줄입니다.' }, + { unitId: 'wei-yan', reason: '거친 돌파력을 어디까지 전방에 둘지 미리 판단해야 할 장수입니다.' } + ], + note: '북벌 준비 회의는 전투가 아니지만 다음 장기전의 편성을 정하는 단계입니다. 제갈량을 중심으로 보급, 산길, 기동, 돌파 역할을 균형 있게 고르십시오.' + } +}; + const campDialogues: CampDialogue[] = [ { id: 'liu-guan-after-first-battle', @@ -5682,6 +5709,90 @@ const campDialogues: CampDialogue[] = [ rewardExp: 22 } ] + }, + { + id: 'northern-prep-zhuge-zhao-march', + title: '북쪽으로 놓는 첫 발', + availableAfterBattleIds: [campBattleIds.fiftyFourth], + availableDuringSteps: ['northern-campaign-prep-camp'], + unitIds: ['zhuge-liang', 'zhao-yun'], + bondId: 'zhao-yun__zhuge-liang', + rewardExp: 76, + lines: [ + '조운: 남중의 길은 정리되었지만, 북쪽 산길은 적이 먼저 기다리는 길입니다.', + '제갈량: 그래서 빠른 창보다 먼저 돌아올 수 있는 길을 정해야 합니다. 자룡의 기동대는 승리를 넓히는 손이 아니라, 전군을 잃지 않게 묶는 끈이 되어야 합니다.', + '조운: 알겠습니다. 다음 출진에서는 먼저 달리는 것보다, 모두가 따라올 수 있는 길을 열겠습니다.' + ], + choices: [ + { + id: 'draw-return-routes', + label: '귀환로를 먼저 그린다', + response: '제갈량은 북방 지도 위에 귀환로를 먼저 긋고, 조운은 기병이 회수 신호를 받을 수 있는 고개와 들판을 표시했습니다.', + rewardExp: 25 + }, + { + id: 'assign-mobile-screen', + label: '기동 엄호대를 편성한다', + response: '조운은 빠른 장수들을 묶어 엄호 순서를 맞췄고, 제갈량은 그 부대가 본대와 너무 멀어지지 않도록 신호 체계를 정했습니다.', + rewardExp: 24 + } + ] + }, + { + id: 'northern-prep-huang-ma-ledgers', + title: '군량과 명분', + availableAfterBattleIds: [campBattleIds.fiftyFourth], + availableDuringSteps: ['northern-campaign-prep-camp'], + unitIds: ['huang-quan', 'ma-liang'], + bondId: 'zhuge-liang__huang-quan_final-capture', + rewardExp: 70, + lines: [ + '황권: 북벌은 군량이 먼저 무너지는 싸움이 될 수 있습니다. 익주와 한중의 장부를 따로 보면 길이 끊깁니다.', + '마량: 장부만으로도 부족합니다. 백성에게 왜 이 길을 여는지 말이 남아야 합니다.', + '황권: 그러면 저는 수량을 맞추고, 마량은 그 수량이 원망이 되지 않게 말을 맞추십시오.' + ], + choices: [ + { + id: 'balance-hanzhong-granary', + label: '한중 창고 장부를 맞춘다', + response: '황권은 한중 창고의 군량과 수레 수를 다시 계산했고, 마량은 징발 대신 보상 문서를 함께 붙였습니다.', + rewardExp: 23 + }, + { + id: 'write-campaign-proclamation', + label: '북벌 고시문을 쓴다', + response: '마량은 북벌의 뜻을 짧게 적었고, 황권은 그 글 아래에 각 고을이 감당할 수 있는 보급 한계를 명확히 남겼습니다.', + rewardExp: 24 + } + ] + }, + { + id: 'northern-prep-wang-madai-ridges', + title: '산길과 말발굽', + availableAfterBattleIds: [campBattleIds.fiftyFourth], + availableDuringSteps: ['northern-campaign-prep-camp'], + unitIds: ['wang-ping', 'ma-dai'], + bondId: 'zhao-yun__ma-dai_final-capture', + rewardExp: 66, + lines: [ + '왕평: 북쪽 산길은 지도보다 좁고, 좁은 길은 기병에게도 보병에게도 함정이 됩니다.', + '마대: 그렇다면 말이 달릴 길보다 멈출 곳을 먼저 익히겠습니다.', + '왕평: 산길을 아는 자가 앞을 보고, 말을 아는 자가 뒤를 닫으면 본대가 길을 잃지 않을 것입니다.' + ], + choices: [ + { + id: 'mark-ridge-halts', + label: '고개 정지선을 표시한다', + response: '왕평은 좁은 고개마다 정지선을 표시했고, 마대는 기병이 그 선에서 멈추는 짧은 훈련을 반복했습니다.', + rewardExp: 22 + }, + { + id: 'test-scout-rotation', + label: '정찰 교대 순서를 시험한다', + response: '마대는 기병 정찰의 교대 시간을 줄였고, 왕평은 산길마다 보병이 이어받을 위치를 정했습니다.', + rewardExp: 21 + } + ] } ]; @@ -8106,6 +8217,68 @@ const campVisits: CampVisitDefinition[] = [ itemRewards: ['콩 16', '탁주 4'] } ] + }, + { + id: 'northern-prep-hanzhong-granary', + title: '한중 창고 점검', + location: '한중 군량 창고', + availableAfterBattleIds: [campBattleIds.fiftyFourth], + availableDuringSteps: ['northern-campaign-prep-camp'], + bondId: 'zhuge-liang__huang-quan_final-capture', + description: '제갈량과 황권이 북벌의 첫 출진 전에 한중 창고, 수레, 보급 기한을 다시 점검합니다.', + lines: [ + '황권: 남중 평정 장부가 끝났다고 곧장 북쪽으로 달리면 군량이 먼저 말라 버립니다.', + '제갈량: 북벌은 뜻만으로 움직이지 않습니다. 뜻을 오래 버티게 하는 창고가 있어야 합니다.', + '한중 창고에는 수레 표식과 군량 날짜가 새로 적힌 장부가 펼쳐졌습니다.' + ], + choices: [ + { + id: 'count-northern-wagons', + label: '수레와 군량을 다시 센다', + response: '황권은 수레 수와 군량 날짜를 맞췄고, 제갈량은 먼저 보낼 물자와 남길 물자를 구분했습니다.', + bondExp: 58, + gold: 2140, + itemRewards: ['콩 20', '상처약 12'] + }, + { + id: 'reserve-field-rations', + label: '예비 군량을 따로 둔다', + response: '제갈량은 전공을 서두르는 병사들에게 예비 군량의 의미를 설명했고, 황권은 대기 부대 몫을 따로 봉했습니다.', + bondExp: 56, + itemRewards: ['탁주 6', '상처약 16'] + } + ] + }, + { + id: 'northern-prep-memorial-table', + title: '출사표 초안', + location: '승상부 서안', + availableAfterBattleIds: [campBattleIds.fiftyFourth], + availableDuringSteps: ['northern-campaign-prep-camp'], + bondId: 'zhao-yun__zhuge-liang', + description: '제갈량이 북쪽으로 나아갈 뜻을 정리하고, 조운이 그 뜻을 실제 출진 순서로 옮길 준비를 합니다.', + lines: [ + '제갈량: 글은 군을 움직이지 못하지만, 군이 왜 움직이는지는 잊지 않게 합니다.', + '조운: 그 뜻이 분명하면 장수들도 무리한 공을 다투지 않을 것입니다.', + '서안 위에는 출사표의 초안과 첫 출진 편성표가 함께 놓였습니다.' + ], + choices: [ + { + id: 'draft-memorial-oath', + label: '출사표 초안을 다듬는다', + response: '제갈량은 군주의 은혜와 한실의 뜻을 다시 적었고, 조운은 그 글이 장수들의 출진 순서와 어긋나지 않게 살폈습니다.', + bondExp: 58, + itemRewards: ['북벌 고시문 1'] + }, + { + id: 'match-sortie-order', + label: '출진 순서를 맞춘다', + response: '조운은 선발대와 본대의 거리를 정했고, 제갈량은 무리한 돌파보다 오래 버티는 행군표를 먼저 승인했습니다.', + bondExp: 56, + gold: 1980, + itemRewards: ['콩 18'] + } + ] } ]; @@ -8903,6 +9076,9 @@ export class CampScene extends Phaser.Scene { if (this.campaign?.step === 'baidi-entrustment-camp') { return '백제성 유탁 후 군영'; } + if (this.campaign?.step === 'northern-campaign-prep-camp') { + return '북벌 준비 군영'; + } if (battleId === campBattleIds.fiftyFourth) { return '남중 평정 후 군영'; } @@ -9522,10 +9698,12 @@ export class CampScene extends Phaser.Scene { } private nextSortieRule(scenario = this.nextSortieScenario()) { - if (!scenario) { - return defaultSortieRule; + if (scenario) { + return sortieRulesByBattleId[scenario.id] ?? defaultSortieRule; } - return sortieRulesByBattleId[scenario.id] ?? defaultSortieRule; + const step = this.campaign?.step; + const flowStep = this.currentSortieFlow().campaignStep; + return (step && sortieRulesByCampaignStep[step]) || (flowStep && sortieRulesByCampaignStep[flowStep]) || defaultSortieRule; } private requiredSortieUnitIdsFor(scenario = this.nextSortieScenario()) { @@ -9660,6 +9838,7 @@ export class CampScene extends Phaser.Scene { const selectedIds = new Set(selectedUnits.map((unit) => unit.id)); const scenario = this.nextSortieScenario(); const rule = this.nextSortieRule(scenario); + const flow = this.currentSortieFlow(); const reserveFocus = this.reserveTrainingFocusDefinition(); const availableIds = new Set(allAllies.map((unit) => unit.id)); const recommended = rule.recommended.filter((entry) => availableIds.has(entry.unitId)); @@ -9738,7 +9917,7 @@ export class CampScene extends Phaser.Scene { reserveTrainingExpPreview: reserveFocus.expGained, reserveTrainingEquipmentPreview: reserveFocus.equipmentExpGained, reserveTrainingBondPreview: reserveFocus.bondExpGained, - objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : '다음 전투 정보 없음', + objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : `${flow.title} · 군영 의정`, formationSlots, warnings }; @@ -9874,7 +10053,14 @@ export class CampScene extends Phaser.Scene { private sortieChecklist(): SortieChecklistItem[] { const units = this.currentUnits().filter((unit) => unit.faction === 'ally'); - const liuBei = units.find((unit) => unit.id === 'liu-bei'); + const sortieAllies = this.sortieAllies(); + const requiredUnitIds = [...this.requiredSortieUnitIdsFor()].filter((id) => sortieAllies.some((unit) => unit.id === id)); + const requiredUnits = requiredUnitIds.map((id) => sortieAllies.find((unit) => unit.id === id)).filter(Boolean) as UnitData[]; + const requiredLabel = requiredUnits.length > 0 && !requiredUnitIds.includes('liu-bei') ? '필수 무장 확인' : '유비 생존'; + const requiredComplete = requiredUnits.length > 0 ? requiredUnits.every((unit) => unit.hp > 0) : true; + const requiredDetail = requiredUnits.length > 0 + ? requiredUnits.map((unit) => `${unit.name} ${unit.hp}/${unit.maxHp}`).join(', ') + : '필수 무장 없음'; const injured = units.filter((unit) => unit.hp < unit.maxHp); const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0); const availableDialogues = this.availableCampDialogues(); @@ -9884,9 +10070,9 @@ export class CampScene extends Phaser.Scene { const selected = this.selectedSortieUnits(); return [ { - label: '유비 생존', - complete: Boolean(liuBei && liuBei.hp > 0), - detail: liuBei ? `병력 ${liuBei.hp}/${liuBei.maxHp}` : '유비 없음' + label: requiredLabel, + complete: requiredComplete, + detail: requiredDetail }, { label: '부상 장수 확인', @@ -9895,7 +10081,7 @@ export class CampScene extends Phaser.Scene { }, { label: '출전 구성', - complete: selected.some((unit) => unit.id === 'liu-bei') && selected.length > 0, + complete: requiredUnitIds.every((id) => selected.some((unit) => unit.id === id)) && selected.length > 0, detail: selected.length > 0 ? selected.map((unit) => unit.name).join(', ') : '출전 무장 선택 필요' }, { @@ -9940,7 +10126,15 @@ export class CampScene extends Phaser.Scene { private startVictoryStory() { const flow = this.currentSortieFlow(); if (!this.ensureSortieSelectionSaved()) { - this.showCampNotice('출전할 무장을 선택하세요. 유비는 반드시 출전해야 합니다.'); + const availableIds = new Set(this.sortieAllies().map((unit) => unit.id)); + const requiredNames = [...this.requiredSortieUnitIdsFor()] + .filter((unitId) => availableIds.has(unitId)) + .map((unitId) => this.unitName(unitId)); + this.showCampNotice( + requiredNames.length > 0 + ? `출전할 무장을 선택하세요. ${requiredNames.join(', ')}는 반드시 포함되어야 합니다.` + : '출전할 무장을 선택하세요.' + ); return; } @@ -10256,7 +10450,14 @@ export class CampScene extends Phaser.Scene { private activeTimelineChapterIndex(completedIds = this.completedBattleIds()) { const latestBattleId = this.campaign?.latestBattleId ?? this.report?.battleId; - const nextBattleId = this.currentSortieFlow().nextBattleId; + const flow = this.currentSortieFlow(); + const nextBattleId = flow.nextBattleId; + if (!nextBattleId && flow.campaignStep === 'northern-campaign-prep-camp') { + const northernIndex = campaignTimelineChapters.findIndex((chapter) => chapter.id === 'northern-campaign'); + if (northernIndex >= 0) { + return northernIndex; + } + } if (!nextBattleId && latestBattleId) { const latestChapterIndex = campaignTimelineChapters.findIndex((chapter) => chapter.battleIds.includes(latestBattleId as BattleScenarioId) @@ -11033,11 +11234,13 @@ export class CampScene extends Phaser.Scene { private sortieAllies() { const scenario = this.nextSortieScenario(); + const rule = this.nextSortieRule(scenario); const scenarioAllyIds = scenario ? new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id)) : undefined; + const excludedUnitIds = new Set(rule.excludedUnitIds ?? []); return this.currentUnits().filter( - (unit) => unit.faction === 'ally' && unit.hp > 0 && (!scenarioAllyIds || scenarioAllyIds.has(unit.id)) + (unit) => unit.faction === 'ally' && unit.hp > 0 && !excludedUnitIds.has(unit.id) && (!scenarioAllyIds || scenarioAllyIds.has(unit.id)) ); } diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index 9f8b45e..6652443 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -410,7 +410,8 @@ export class TitleScene extends Phaser.Scene { campaign.step === 'fifty-fourth-camp' || campaign.step === 'hanzhong-king-camp' || campaign.step === 'shu-han-foundation-camp' || - campaign.step === 'baidi-entrustment-camp' + campaign.step === 'baidi-entrustment-camp' || + campaign.step === 'northern-campaign-prep-camp' ) { this.scene.start('CampScene'); return; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index f00d4a7..537181e 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -152,7 +152,8 @@ export type CampaignStep = | 'fifty-fourth-camp' | 'hanzhong-king-camp' | 'shu-han-foundation-camp' - | 'baidi-entrustment-camp'; + | 'baidi-entrustment-camp' + | 'northern-campaign-prep-camp'; export type CampaignUnitProgressSnapshot = { unitId: string;