diff --git a/docs/roadmap.md b/docs/roadmap.md index 93e7775..210484f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,16 +71,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of - Forty-fifth battle Yiling vanguard route, turning Jing Province's loss into Liu Bei's revenge march and using a 90x72 river-forest battlefield to establish the first Yiling camp against Lu Xun's waiting line - Forty-sixth battle Yiling fire-attack route, using Lu Xun's delayed fire plan, stretched Shu-Han camps, a 92x74 burning river-forest battlefield, and a seven-officer retreat-route sortie choice to break through toward Baidi Castle - Baidi Castle entrustment story milestone, where Liu Bei's retreat from Yiling becomes a playable camp transition into Zhuge Liang's regency burden and post-defeat recovery planning +- Forty-seventh battle Nanzhong stabilization route, where the Baidi entrustment aftermath opens a Zhuge Liang-led sortie, removes Liu Bei from the deployable roster, and uses a 94x76 southern mountain battlefield with village, warehouse, and rebel command objectives - 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 Baidi entrustment 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 camp states, campaign timeline state, and camp save state +- Flow verification script from title through the Nanzhong stabilization 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 camp states, campaign timeline state, and camp save state ## Next Steps -1. Build the Nanman pacification preparation chapter, using the Baidi entrustment aftermath, southern instability, and Zhuge Liang's state-rebuilding priorities as the next campaign tension -2. Expand post-Baidi camp consequences with grief checks, recovery policy choices, and regency decisions that affect sortie recommendations +1. Build Meng Huo's first main-force battle and the first explicit Nanman reconciliation mechanic after the Nanzhong stabilization route +2. Expand post-Baidi and post-Nanzhong camp consequences with grief checks, recovery policy choices, village reassurance, 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 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 c68ba10..fd3539a 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1739,7 +1739,7 @@ try { if ( progressTabState?.activeTab !== 'progress' || progressTabState.campaignProgress?.completedKnown !== 17 || - progressTabState.campaignProgress?.totalKnown !== 46 || + progressTabState.campaignProgress?.totalKnown !== 47 || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' @@ -1891,7 +1891,7 @@ try { if ( postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState.campaignProgress?.completedKnown !== 18 || - postBowangProgressState.campaignProgress?.totalKnown !== 46 || + postBowangProgressState.campaignProgress?.totalKnown !== 47 || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' @@ -2029,7 +2029,7 @@ try { if ( postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState.campaignProgress?.completedKnown !== 19 || - postChangbanProgressState.campaignProgress?.totalKnown !== 46 || + postChangbanProgressState.campaignProgress?.totalKnown !== 47 || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' @@ -2167,7 +2167,7 @@ try { if ( postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || - postJiangdongProgressState.campaignProgress?.totalKnown !== 46 || + postJiangdongProgressState.campaignProgress?.totalKnown !== 47 || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' @@ -2307,7 +2307,7 @@ try { if ( postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || - postRedCliffsProgressState.campaignProgress?.totalKnown !== 46 || + postRedCliffsProgressState.campaignProgress?.totalKnown !== 47 || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' @@ -2447,7 +2447,7 @@ try { if ( postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || - postFireAttackProgressState.campaignProgress?.totalKnown !== 46 || + postFireAttackProgressState.campaignProgress?.totalKnown !== 47 || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' @@ -2590,7 +2590,7 @@ try { if ( postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || - postJingzhouProgressState.campaignProgress?.totalKnown !== 46 || + postJingzhouProgressState.campaignProgress?.totalKnown !== 47 || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' @@ -2732,7 +2732,7 @@ try { if ( postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || - postGuiyangProgressState.campaignProgress?.totalKnown !== 46 || + postGuiyangProgressState.campaignProgress?.totalKnown !== 47 || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' @@ -2875,7 +2875,7 @@ try { if ( postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState.campaignProgress?.completedKnown !== 25 || - postWulingProgressState.campaignProgress?.totalKnown !== 46 || + postWulingProgressState.campaignProgress?.totalKnown !== 47 || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전' @@ -3024,7 +3024,7 @@ try { if ( postChangshaProgressState?.activeTab !== 'progress' || postChangshaProgressState.campaignProgress?.completedKnown !== 26 || - postChangshaProgressState.campaignProgress?.totalKnown !== 46 || + postChangshaProgressState.campaignProgress?.totalKnown !== 47 || postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' || postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로' @@ -3169,7 +3169,7 @@ try { if ( postYizhouProgressState?.activeTab !== 'progress' || postYizhouProgressState.campaignProgress?.completedKnown !== 27 || - postYizhouProgressState.campaignProgress?.totalKnown !== 46 || + postYizhouProgressState.campaignProgress?.totalKnown !== 47 || postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' || postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전' @@ -3316,7 +3316,7 @@ try { if ( postFuPassProgressState?.activeTab !== 'progress' || postFuPassProgressState.campaignProgress?.completedKnown !== 28 || - postFuPassProgressState.campaignProgress?.totalKnown !== 46 || + postFuPassProgressState.campaignProgress?.totalKnown !== 47 || postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' || postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전' @@ -3465,7 +3465,7 @@ try { if ( postLuoProgressState?.activeTab !== 'progress' || postLuoProgressState.campaignProgress?.completedKnown !== 29 || - postLuoProgressState.campaignProgress?.totalKnown !== 46 || + postLuoProgressState.campaignProgress?.totalKnown !== 47 || postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' || postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전' @@ -3611,7 +3611,7 @@ try { if ( postLuofengProgressState?.activeTab !== 'progress' || postLuofengProgressState.campaignProgress?.completedKnown !== 30 || - postLuofengProgressState.campaignProgress?.totalKnown !== 46 || + postLuofengProgressState.campaignProgress?.totalKnown !== 47 || postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' || postLuofengProgressState.campaignProgress?.nextBattleTitle !== '낙성 본성 공략' @@ -3763,7 +3763,7 @@ try { if ( postLuoMainProgressState?.activeTab !== 'progress' || postLuoMainProgressState.campaignProgress?.completedKnown !== 31 || - postLuoMainProgressState.campaignProgress?.totalKnown !== 46 || + postLuoMainProgressState.campaignProgress?.totalKnown !== 47 || postLuoMainProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postLuoMainProgressState.campaignProgress?.latestBattleTitle !== '낙성 본성 공략' || postLuoMainProgressState.campaignProgress?.nextBattleTitle !== '면죽관 압박전' @@ -3916,7 +3916,7 @@ try { if ( postMianzhuProgressState?.activeTab !== 'progress' || postMianzhuProgressState.campaignProgress?.completedKnown !== 32 || - postMianzhuProgressState.campaignProgress?.totalKnown !== 46 || + postMianzhuProgressState.campaignProgress?.totalKnown !== 47 || postMianzhuProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postMianzhuProgressState.campaignProgress?.latestBattleTitle !== '면죽관 압박전' || postMianzhuProgressState.campaignProgress?.nextBattleTitle !== '성도 항복 권고전' @@ -4070,7 +4070,7 @@ try { if ( postChengduProgressState?.activeTab !== 'progress' || postChengduProgressState.campaignProgress?.completedKnown !== 33 || - postChengduProgressState.campaignProgress?.totalKnown !== 46 || + postChengduProgressState.campaignProgress?.totalKnown !== 47 || postChengduProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postChengduProgressState.campaignProgress?.latestBattleTitle !== '성도 항복 권고전' || postChengduProgressState.campaignProgress?.nextBattleTitle !== '가맹관 마초 대면전' @@ -4225,7 +4225,7 @@ try { if ( postJiamengProgressState?.activeTab !== 'progress' || postJiamengProgressState.campaignProgress?.completedKnown !== 34 || - postJiamengProgressState.campaignProgress?.totalKnown !== 46 || + postJiamengProgressState.campaignProgress?.totalKnown !== 47 || postJiamengProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postJiamengProgressState.campaignProgress?.latestBattleTitle !== '가맹관 마초 대면전' || postJiamengProgressState.campaignProgress?.nextBattleTitle !== '양평관 정찰전' @@ -4381,7 +4381,7 @@ try { if ( postYangpingProgressState?.activeTab !== 'progress' || postYangpingProgressState.campaignProgress?.completedKnown !== 35 || - postYangpingProgressState.campaignProgress?.totalKnown !== 46 || + postYangpingProgressState.campaignProgress?.totalKnown !== 47 || postYangpingProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postYangpingProgressState.campaignProgress?.latestBattleTitle !== '양평관 정찰전' || postYangpingProgressState.campaignProgress?.nextBattleTitle !== '정군산 전초전' @@ -4538,7 +4538,7 @@ try { if ( postDingjunProgressState?.activeTab !== 'progress' || postDingjunProgressState.campaignProgress?.completedKnown !== 36 || - postDingjunProgressState.campaignProgress?.totalKnown !== 46 || + postDingjunProgressState.campaignProgress?.totalKnown !== 47 || postDingjunProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postDingjunProgressState.campaignProgress?.latestBattleTitle !== '정군산 전초전' || postDingjunProgressState.campaignProgress?.nextBattleTitle !== '한중 결전' @@ -4691,7 +4691,7 @@ try { if ( postHanzhongProgressState?.activeTab !== 'progress' || postHanzhongProgressState.campaignProgress?.completedKnown !== 37 || - postHanzhongProgressState.campaignProgress?.totalKnown !== 46 || + postHanzhongProgressState.campaignProgress?.totalKnown !== 47 || postHanzhongProgressState.campaignProgress?.activeChapter?.title !== '촉한 건국' || postHanzhongProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postHanzhongProgressState.campaignProgress?.nextBattleTitle !== '한중왕 즉위 준비' @@ -4752,7 +4752,7 @@ try { if ( postKingCouncilProgressState?.activeTab !== 'progress' || postKingCouncilProgressState.campaignProgress?.completedKnown !== 37 || - postKingCouncilProgressState.campaignProgress?.totalKnown !== 46 || + postKingCouncilProgressState.campaignProgress?.totalKnown !== 47 || postKingCouncilProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postKingCouncilProgressState.campaignProgress?.nextBattleTitle !== '촉한 건국 선포' ) { @@ -4812,7 +4812,7 @@ try { if ( postFoundationProgressState?.activeTab !== 'progress' || postFoundationProgressState.campaignProgress?.completedKnown !== 37 || - postFoundationProgressState.campaignProgress?.totalKnown !== 46 || + postFoundationProgressState.campaignProgress?.totalKnown !== 47 || postFoundationProgressState.campaignProgress?.latestBattleTitle !== '한중 결전' || postFoundationProgressState.campaignProgress?.nextBattleTitle !== '형주 방위 전초전' ) { @@ -4963,7 +4963,7 @@ try { if ( postJingDefenseProgressState?.activeTab !== 'progress' || postJingDefenseProgressState.campaignProgress?.completedKnown !== 38 || - postJingDefenseProgressState.campaignProgress?.totalKnown !== 46 || + postJingDefenseProgressState.campaignProgress?.totalKnown !== 47 || postJingDefenseProgressState.campaignProgress?.latestBattleTitle !== '형주 방위 전초전' || postJingDefenseProgressState.campaignProgress?.nextBattleTitle !== '번성 외곽 압박전' ) { @@ -5116,7 +5116,7 @@ try { if ( postFanCastleProgressState?.activeTab !== 'progress' || postFanCastleProgressState.campaignProgress?.completedKnown !== 39 || - postFanCastleProgressState.campaignProgress?.totalKnown !== 46 || + postFanCastleProgressState.campaignProgress?.totalKnown !== 47 || postFanCastleProgressState.campaignProgress?.latestBattleTitle !== '번성 외곽 압박전' || postFanCastleProgressState.campaignProgress?.nextBattleTitle !== '한수 수공' ) { @@ -5270,7 +5270,7 @@ try { if ( postHanRiverProgressState?.activeTab !== 'progress' || postHanRiverProgressState.campaignProgress?.completedKnown !== 40 || - postHanRiverProgressState.campaignProgress?.totalKnown !== 46 || + postHanRiverProgressState.campaignProgress?.totalKnown !== 47 || postHanRiverProgressState.campaignProgress?.latestBattleTitle !== '한수 수공' || postHanRiverProgressState.campaignProgress?.nextBattleTitle !== '번성 공성전' ) { @@ -5424,7 +5424,7 @@ try { if ( postFanSiegeProgressState?.activeTab !== 'progress' || postFanSiegeProgressState.campaignProgress?.completedKnown !== 41 || - postFanSiegeProgressState.campaignProgress?.totalKnown !== 46 || + postFanSiegeProgressState.campaignProgress?.totalKnown !== 47 || postFanSiegeProgressState.campaignProgress?.latestBattleTitle !== '번성 공성전' || postFanSiegeProgressState.campaignProgress?.nextBattleTitle !== '강릉 나루 경계전' ) { @@ -5586,7 +5586,7 @@ try { if ( postJingRearProgressState?.activeTab !== 'progress' || postJingRearProgressState.campaignProgress?.completedKnown !== 42 || - postJingRearProgressState.campaignProgress?.totalKnown !== 46 || + postJingRearProgressState.campaignProgress?.totalKnown !== 47 || postJingRearProgressState.campaignProgress?.latestBattleTitle !== '강릉 나루 경계전' || postJingRearProgressState.campaignProgress?.nextBattleTitle !== '공안 성문 변고' ) { @@ -5741,7 +5741,7 @@ try { if ( postGonganProgressState?.activeTab !== 'progress' || postGonganProgressState.campaignProgress?.completedKnown !== 43 || - postGonganProgressState.campaignProgress?.totalKnown !== 46 || + postGonganProgressState.campaignProgress?.totalKnown !== 47 || postGonganProgressState.campaignProgress?.latestBattleTitle !== '공안 성문 변고' || postGonganProgressState.campaignProgress?.nextBattleTitle !== '맥성 고립전' ) { @@ -5899,7 +5899,7 @@ try { if ( postMaichengProgressState?.activeTab !== 'progress' || postMaichengProgressState.campaignProgress?.completedKnown !== 44 || - postMaichengProgressState.campaignProgress?.totalKnown !== 46 || + postMaichengProgressState.campaignProgress?.totalKnown !== 47 || postMaichengProgressState.campaignProgress?.latestBattleTitle !== '맥성 고립전' || postMaichengProgressState.campaignProgress?.nextBattleTitle !== '이릉 진격로' ) { @@ -6056,7 +6056,7 @@ try { if ( postYilingProgressState?.activeTab !== 'progress' || postYilingProgressState.campaignProgress?.completedKnown !== 45 || - postYilingProgressState.campaignProgress?.totalKnown !== 46 || + postYilingProgressState.campaignProgress?.totalKnown !== 47 || postYilingProgressState.campaignProgress?.latestBattleTitle !== '이릉 진격로' || postYilingProgressState.campaignProgress?.nextBattleTitle !== '이릉 화공전' ) { @@ -6214,7 +6214,7 @@ try { if ( postYilingFireProgressState?.activeTab !== 'progress' || postYilingFireProgressState.campaignProgress?.completedKnown !== 46 || - postYilingFireProgressState.campaignProgress?.totalKnown !== 46 || + postYilingFireProgressState.campaignProgress?.totalKnown !== 47 || postYilingFireProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || postYilingFireProgressState.campaignProgress?.nextBattleTitle !== '백제성 유탁' ) { @@ -6280,14 +6280,175 @@ try { if ( postBaidiProgressState?.activeTab !== 'progress' || postBaidiProgressState.campaignProgress?.completedKnown !== 46 || - postBaidiProgressState.campaignProgress?.totalKnown !== 46 || + postBaidiProgressState.campaignProgress?.totalKnown !== 47 || postBaidiProgressState.campaignProgress?.latestBattleTitle !== '이릉 화공전' || - postBaidiProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + postBaidiProgressState.campaignProgress?.nextBattleTitle !== '남중 안정로' ) { - throw new Error(`Expected Baidi entrustment progress tab to keep the forty-sixth battle complete and pause before Nanman prep: ${JSON.stringify(postBaidiProgressState?.campaignProgress)}`); + throw new Error(`Expected Baidi entrustment progress tab to keep the forty-sixth battle complete and point to Nanzhong stabilization: ${JSON.stringify(postBaidiProgressState?.campaignProgress)}`); } await page.screenshot({ path: 'dist/verification-post-baidi-progress.png', fullPage: true }); + await page.mouse.click(1120, 38); + await page.waitForTimeout(180); + const nanzhongSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !nanzhongSortieState?.sortieVisible || + nanzhongSortieState.campaign?.step !== 'baidi-entrustment-camp' || + nanzhongSortieState.campTitle !== '백제성 유탁 후 군영' || + nanzhongSortieState.sortiePlan?.objectiveLine !== '남중 안정로 · 남중 반란 지휘선 진정' || + nanzhongSortieState.sortiePlan?.maxCount !== 7 || + nanzhongSortieState.sortiePlan?.recommendedTotal !== 7 || + !nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + !nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recommended) || + !nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'wang-ping' && unit.recommended) || + nanzhongSortieState.sortieRoster?.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected Nanzhong sortie prep to lock Zhuge Liang, recommend southern-route officers, and remove Liu Bei from the deployable roster: ${JSON.stringify(nanzhongSortieState)}`); + } + assertSortieTacticalRoster(nanzhongSortieState, [ + 'guan-yu', + 'zhang-fei', + 'jian-yong', + 'mi-zhu', + 'sun-qian', + 'zhao-yun', + 'zhuge-liang', + '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' + ]); + + const nanzhongPriorityUnits = ['zhao-yun', 'wang-ping', 'ma-liang', 'huang-quan', 'huang-zhong', 'ma-chao']; + for (const unitId of nanzhongPriorityUnits) { + 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.required && !nanzhongPriorityUnits.includes(unit.id) + ); + if (removable) { + await clickSortieRosterUnit(page, removable.id); + } + await clickSortieRosterUnit(page, unitId); + } + } + + const nanzhongSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + !nanzhongPriorityUnits.every((unitId) => + nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected) + ) || + !nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.selected && unit.required) || + nanzhongSortieReadyState.sortieRoster?.some((unit) => unit.id === 'liu-bei') || + nanzhongSortieReadyState.sortiePlan?.selectedCount !== 7 || + nanzhongSortieReadyState.sortiePlan?.recommendedSelectedCount < 7 + ) { + throw new Error(`Expected Nanzhong sortie to deploy Zhuge Liang, Zhao Yun, Wang Ping, Ma Liang, Huang Quan, Huang Zhong, and Ma Chao: ${JSON.stringify(nanzhongSortieReadyState)}`); + } + await page.screenshot({ path: 'dist/verification-nanzhong-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-nanzhong-story.png', fullPage: true }); + + for (let i = 0; i < 55; i += 1) { + const enteredFortySeventhBattle = await page.evaluate(() => { + const state = window.__HEROS_DEBUG__?.battle(); + return state?.scene === 'BattleScene' && state?.battleId === 'forty-seventh-battle-nanzhong-stabilization'; + }); + if (enteredFortySeventhBattle) { + 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 === 'forty-seventh-battle-nanzhong-stabilization' && state?.battleOutcome === null && state?.phase === 'idle'; + }); + await page.screenshot({ path: 'dist/verification-forty-seventh-battle.png', fullPage: true }); + + const fortySeventhBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()); + const fortySeventhEnemies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'enemy'); + const fortySeventhAllies = fortySeventhBattleState.units.filter((unit) => unit.faction === 'ally'); + const fortySeventhEnemyBehaviors = new Set(fortySeventhEnemies.map((unit) => unit.ai)); + if ( + fortySeventhBattleState.camera?.mapWidth !== 94 || + fortySeventhBattleState.camera?.mapHeight !== 76 || + fortySeventhBattleState.victoryConditionLabel !== '남중 반란 지휘선 진정' || + fortySeventhEnemies.length < 34 || + !fortySeventhEnemyBehaviors.has('aggressive') || + !fortySeventhEnemyBehaviors.has('guard') || + !fortySeventhEnemyBehaviors.has('hold') || + !fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-leader-yong-kai') || + !fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-gao-ding') || + !fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-zhu-bao') || + !fortySeventhEnemies.some((unit) => unit.id === 'nanzhong-officer-meng-huo-vanguard') || + !fortySeventhAllies.some((unit) => unit.id === 'zhuge-liang') || + !fortySeventhAllies.some((unit) => unit.id === 'zhao-yun') || + !fortySeventhAllies.some((unit) => unit.id === 'wang-ping') || + !fortySeventhAllies.some((unit) => unit.id === 'ma-liang') || + !fortySeventhAllies.some((unit) => unit.id === 'huang-quan') || + !fortySeventhAllies.some((unit) => unit.id === 'huang-zhong') || + !fortySeventhAllies.some((unit) => unit.id === 'ma-chao') || + fortySeventhAllies.some((unit) => unit.id === 'liu-bei') + ) { + throw new Error(`Expected forty-seventh battle to use Nanzhong map, selected regency-route officers, and mixed southern rebel AI: ${JSON.stringify(fortySeventhBattleState)}`); + } + + 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 fortySeventhCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + fortySeventhCampState?.campBattleId !== 'forty-seventh-battle-nanzhong-stabilization' || + fortySeventhCampState.campTitle !== '남중 안정로 후 군영' || + fortySeventhCampState.availableDialogueIds?.length !== 3 || + !fortySeventhCampState.availableDialogueIds.every((id) => id.includes('nanzhong')) || + fortySeventhCampState.availableVisitIds?.length !== 2 || + !fortySeventhCampState.availableVisitIds.every((id) => id.includes('nanzhong')) || + fortySeventhCampState.rosterCollection?.total < 21 + ) { + throw new Error(`Expected forty-seventh camp to expose Nanzhong dialogue/visit sets and preserve the deployable officer roster: ${JSON.stringify(fortySeventhCampState)}`); + } + await page.screenshot({ path: 'dist/verification-nanzhong-camp.png', fullPage: true }); + + await page.mouse.click(966, 38); + await page.waitForTimeout(180); + const postNanzhongProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + if ( + postNanzhongProgressState?.activeTab !== 'progress' || + postNanzhongProgressState.campaignProgress?.completedKnown !== 47 || + postNanzhongProgressState.campaignProgress?.totalKnown !== 47 || + postNanzhongProgressState.campaignProgress?.latestBattleTitle !== '남중 안정로' || + postNanzhongProgressState.campaignProgress?.nextBattleTitle !== '준비 중' + ) { + throw new Error(`Expected post-Nanzhong progress tab to complete the forty-seventh battle and pause before Meng Huo's main force: ${JSON.stringify(postNanzhongProgressState?.campaignProgress)}`); + } + await page.screenshot({ path: 'dist/verification-post-nanzhong-progress.png', fullPage: true }); + await page.evaluate(() => { const game = window.__HEROS_GAME__; game?.scene.stop('CampScene'); @@ -6319,11 +6480,11 @@ try { activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [], camp: window.__HEROS_DEBUG__?.camp?.() ?? null })); - if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'baidi-entrustment-camp') { - throw new Error(`Expected title continue to reopen the latest Baidi entrustment camp: ${JSON.stringify(titleContinueState)}`); + if (!titleContinueState.activeScenes.includes('CampScene') || titleContinueState.camp?.campaign?.step !== 'forty-seventh-camp') { + throw new Error(`Expected title continue to reopen the latest Nanzhong camp: ${JSON.stringify(titleContinueState)}`); } - console.log(`Verified title-to-Baidi-entrustment 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 Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, and Baidi entrustment state at ${targetUrl}`); + console.log(`Verified title-to-Nanzhong-stabilization 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 Castle siege, Jing rear crisis, Gongan collapse, Maicheng isolation, Yiling vanguard, Yiling fire attack, Baidi entrustment, and Nanzhong stabilization state at ${targetUrl}`); } finally { await browser?.close(); if (serverProcess && !serverProcess.killed) { @@ -6403,7 +6564,7 @@ function assertSortieTacticalRoster(state, requiredUnitIds) { } const liuBei = state.sortieRoster.find((unit) => unit.id === 'liu-bei'); - if (!liuBei?.equipmentLine?.includes('자웅일대검')) { + if (liuBei && !liuBei.equipmentLine?.includes('자웅일대검')) { throw new Error(`Expected Liu Bei sortie row to expose named treasure weapon: ${JSON.stringify(liuBei)}`); } diff --git a/src/assets/images/battle/forty-seventh-battle-map.svg b/src/assets/images/battle/forty-seventh-battle-map.svg new file mode 100644 index 0000000..360624c --- /dev/null +++ b/src/assets/images/battle/forty-seventh-battle-map.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index fe4a0d9..8029494 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -48,6 +48,10 @@ import { fortySixthBattleMap, fortySixthBattleUnits, fortySixthBattleVictoryPages, + fortySeventhBattleBonds, + fortySeventhBattleMap, + fortySeventhBattleUnits, + fortySeventhBattleVictoryPages, firstBattleMap, firstBattleUnits, firstBattleVictoryPages, @@ -235,7 +239,8 @@ export type BattleScenarioId = | 'forty-third-battle-gongan-collapse' | 'forty-fourth-battle-maicheng-isolation' | 'forty-fifth-battle-yiling-vanguard' - | 'forty-sixth-battle-yiling-fire'; + | 'forty-sixth-battle-yiling-fire' + | 'forty-seventh-battle-nanzhong-stabilization'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; @@ -2987,6 +2992,66 @@ export const fortySixthBattleScenario: BattleScenarioDefinition = { nextCampScene: 'CampScene' }; +export const fortySeventhBattleScenario: BattleScenarioDefinition = { + id: 'forty-seventh-battle-nanzhong-stabilization', + title: '남중 안정로', + victoryConditionLabel: '남중 반란 지휘선 진정', + defeatConditionLabel: '제갈량 퇴각', + openingObjectiveLines: [ + '백제성의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고르려 합니다. 그러나 남중의 반란군은 그 틈을 타 성도 남쪽 길과 창고를 흔듭니다.', + '옹개의 지휘선을 흩뜨리되 마을과 창고를 함부로 불태우지 마십시오. 남중 평정은 적을 모두 쓰러뜨리는 싸움이 아니라, 돌아설 마음을 남겨 두는 싸움입니다.', + '제갈량은 반드시 생존해야 합니다. 조운과 왕평으로 산길을 열고, 마량과 황권으로 마을과 보급을 안정시키면 다음 남만 본전의 발판이 생깁니다.' + ], + map: fortySeventhBattleMap, + units: fortySeventhBattleUnits, + bonds: fortySeventhBattleBonds, + mapTextureKey: 'battle-map-forty-seventh', + leaderUnitId: 'nanzhong-leader-yong-kai', + quickVictoryTurnLimit: 78, + baseVictoryGold: 10120, + objectives: [ + { + id: 'leader', + kind: 'defeat-leader', + label: '옹개 지휘선 진정', + rewardGold: 6840, + unitId: 'nanzhong-leader-yong-kai' + }, + { + id: 'zhuge-liang', + kind: 'keep-unit-alive', + label: '제갈량 생존', + rewardGold: 1860, + unitId: 'zhuge-liang' + }, + { + id: 'villages', + kind: 'secure-terrain', + label: '남중 마을 안정', + rewardGold: 3420, + terrain: 'village' + }, + { + id: 'supply-camps', + kind: 'secure-terrain', + label: '남쪽 창고 확보', + rewardGold: 2720, + terrain: 'camp' + }, + { + id: 'quick', + kind: 'quick-victory', + label: '78턴 이내 안정', + rewardGold: 2360, + maxTurn: 78 + } + ], + defeatConditions: [{ kind: 'unit-defeated', unitId: 'zhuge-liang' }], + itemRewards: ['콩 50', '상처약 40', '탁주 17', '남중 산길 지도 1', '마을 회유 문서 1'], + victoryPages: fortySeventhBattleVictoryPages, + nextCampScene: 'CampScene' +}; + export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id; export const battleScenarios: Record = { @@ -3035,7 +3100,8 @@ export const battleScenarios: Record 'forty-third-battle-gongan-collapse': fortyThirdBattleScenario, 'forty-fourth-battle-maicheng-isolation': fortyFourthBattleScenario, 'forty-fifth-battle-yiling-vanguard': fortyFifthBattleScenario, - 'forty-sixth-battle-yiling-fire': fortySixthBattleScenario + 'forty-sixth-battle-yiling-fire': fortySixthBattleScenario, + 'forty-seventh-battle-nanzhong-stabilization': fortySeventhBattleScenario }; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 1b55842..a9928f7 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -11,6 +11,7 @@ import { fortyFourthBattleScenario, fortyFifthBattleScenario, fortySixthBattleScenario, + fortySeventhBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -69,6 +70,8 @@ import { fortyFifthBattleIntroPages, fortyFifthBattleVictoryPages, fortySixthBattleIntroPages, + fortySeventhBattleIntroPages, + fortySeventhBattleVictoryPages, fifthBattleIntroPages, fifthBattleVictoryPages, fifteenthBattleIntroPages, @@ -675,13 +678,24 @@ const sortieFlows: Record = { }, 'baidi-entrustment-camp': { afterBattleId: fortySixthBattleScenario.id, - eyebrow: '다음 장 준비', - title: '남만 평정 준비', + eyebrow: '다음 전장', + title: fortySeventhBattleScenario.title, description: - '유비의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고릅니다. 다음 장에서는 남중의 불안을 정리하고, 북벌을 준비할 내정을 세우는 흐름으로 확장합니다.', - rewardHint: '다음 장: 남만 평정과 촉한 재정비 준비 중', - pages: [], - unavailableNotice: '남만 평정과 촉한 재정비는 다음 작업에서 이어집니다. 지금은 백제성 유탁 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' + '유비의 유탁 뒤 촉한은 제갈량을 중심으로 다시 숨을 고릅니다. 이제 남중의 반란군이 흔드는 산길과 창고를 안정시켜, 북벌보다 먼저 나라 안의 불안을 수습해야 합니다.', + rewardHint: `예상 보상: ${fortySeventhBattleScenario.title} 개방 / 남중 산길 지도`, + nextBattleId: fortySeventhBattleScenario.id, + campaignStep: 'forty-seventh-battle', + pages: fortySeventhBattleIntroPages + }, + [fortySeventhBattleScenario.id]: { + afterBattleId: fortySeventhBattleScenario.id, + eyebrow: '다음 장 준비', + title: '맹획과 남중 본대', + description: + '남중 안정로의 첫 길은 열렸지만 반란의 중심은 더 깊은 산과 마을 사이에 남아 있습니다. 다음 장에서는 맹획의 본대를 상대하며 힘과 회유를 함께 시험해야 합니다.', + rewardHint: '다음 장: 맹획 본대와 남중 회유 준비 중', + pages: fortySeventhBattleVictoryPages, + unavailableNotice: '맹획 본대와 남중 회유는 다음 작업에서 이어집니다. 지금은 남중 안정로 뒤의 성장, 공명도, 보급을 정비할 수 있습니다.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index 7086624..ba95680 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -3077,6 +3077,67 @@ export const baidiEntrustmentPages: StoryPage[] = [ } ]; +export const fortySeventhBattleIntroPages: StoryPage[] = [ + { + id: 'forty-seventh-southern-unrest', + bgm: 'story-dark', + chapter: '남중의 흔들림', + background: 'story-militia', + speaker: '황권', + text: '백제성의 유탁 뒤 촉한은 아직 상처를 다 씻지 못했습니다. 그러나 남중의 여러 호족과 반란군은 바로 그 틈을 보며, 성도 남쪽의 길과 창고를 흔들기 시작했습니다.' + }, + { + id: 'forty-seventh-zhuge-policy', + bgm: 'battle-prep', + chapter: '먼저 안을 다스리다', + background: 'story-three-heroes', + speaker: '제갈량', + text: '북방을 논하기 전에 나라 안의 불안을 먼저 다스려야 합니다. 남중을 힘으로만 누르면 길은 열려도 마음은 닫힙니다. 반란의 길목을 끊고, 마을과 창고는 살려 두십시오.' + }, + { + id: 'forty-seventh-zhao-yun-vanguard', + bgm: 'battle-prep', + chapter: '남쪽 길의 선봉', + background: 'story-sortie', + speaker: '조운', + text: '산길과 숲길이 겹친 전장이라 기병만 앞세울 수 없습니다. 왕평이 길을 보고, 황권이 보급을 재며, 마량이 마을의 말을 들어야 합니다. 저는 뒤처진 부대를 회수하겠습니다.' + }, + { + id: 'forty-seventh-sortie', + bgm: 'battle-prep', + chapter: '남중 안정로', + background: 'story-sortie', + speaker: '제갈량', + text: '옹개와 남중 반란군이 밀림 길목에 진을 쳤습니다. 반란군 지휘선을 흩뜨리고, 마을과 창고를 확보해 남중 평정의 첫 발판을 세우십시오.' + } +]; + +export const fortySeventhBattleVictoryPages: StoryPage[] = [ + { + id: 'forty-seventh-victory-road-opened', + bgm: 'militia-theme', + chapter: '남쪽 길이 열리다', + background: 'story-sortie', + text: '밀림과 산길 사이의 반란군 지휘선이 흔들렸습니다. 촉한군은 마을을 약탈하지 않고 창고를 정리하며, 남중 평정이 단순한 추격전이 아님을 병사들에게 보였습니다.' + }, + { + id: 'forty-seventh-meng-huo-shadow', + bgm: 'story-dark', + chapter: '더 깊은 산', + background: 'story-militia', + speaker: '왕평', + text: '옹개의 선봉은 물러났지만 남중의 산은 아직 깊습니다. 맹획의 이름이 여러 마을 사이에서 오가고 있고, 힘으로 꺾은 길 뒤에는 설득해야 할 마음이 남아 있습니다.' + }, + { + id: 'forty-seventh-next-nanman', + bgm: 'story-dark', + chapter: '남만 평정의 시작', + background: 'story-three-heroes', + speaker: '제갈량', + text: '첫 길은 열렸습니다. 이제 남중의 여러 세력을 모두 적으로 만들지 않으면서, 반란의 중심을 하나씩 마주해야 합니다. 다음 장에서는 맹획의 본대와 처음으로 맞섭니다.' + } +]; + export const firstBattleMap: BattleMap = { width: 20, height: 18, @@ -3498,6 +3559,12 @@ export const fortySixthBattleMap: BattleMap = { terrain: createFortySixthBattleTerrain() }; +export const fortySeventhBattleMap: BattleMap = { + width: 94, + height: 76, + terrain: createFortySeventhBattleTerrain() +}; + export const firstBattleUnits: UnitData[] = [ { id: 'liu-bei', @@ -15474,6 +15541,88 @@ export const fortySixthBattleUnits: UnitData[] = [ createThirtyFirstEnemyUnit('yiling-fire-leader-lu-xun', '육손', '화공 대도독', 'rebelLeader', 122, 812, 198, 86, 31) ]; +const fortySeventhBattleAllyPositions: Record = { + 'guan-yu': { x: 14, y: 49 }, + 'zhang-fei': { x: 18, y: 50 }, + 'jian-yong': { x: 10, y: 55 }, + 'mi-zhu': { x: 12, y: 52 }, + 'sun-qian': { x: 14, y: 57 }, + 'zhao-yun': { x: 20, y: 46 }, + 'zhuge-liang': { x: 12, y: 47 }, + 'ma-liang': { x: 16, y: 44 }, + 'yi-ji': { x: 11, y: 60 }, + 'gong-zhi': { x: 18, y: 58 }, + 'huang-zhong': { x: 23, y: 48 }, + 'wei-yan': { x: 25, y: 52 }, + 'pang-tong': { x: 15, y: 41 }, + 'fa-zheng': { x: 19, y: 42 }, + 'wu-yi': { x: 27, y: 55 }, + 'yan-yan': { x: 29, y: 50 }, + 'li-yan': { x: 22, y: 39 }, + 'huang-quan': { x: 10, y: 45 }, + 'ma-chao': { x: 31, y: 47 }, + 'ma-dai': { x: 33, y: 51 }, + 'wang-ping': { x: 24, y: 43 } +}; + +export const fortySeventhBattleUnits: UnitData[] = [ + ...[ + ...firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.id !== 'liu-bei'), + ...xuzhouRecruitUnits, + ...caoBreakRecruitUnits, + ...liuBiaoRecruitUnits, + ...zhugeRecruitUnits, + ...jingzhouRecruitUnits, + ...guiyangRecruitUnits, + ...wulingRecruitUnits, + ...changshaRecruitUnits, + ...yizhouRecruitUnits, + ...fuPassRecruitUnits, + ...luoCastleRecruitUnits, + ...luoCastleProperRecruitUnits, + ...chengduPressureRecruitUnits, + ...chengduSurrenderRecruitUnits, + ...hanzhongOpeningRecruitUnits, + ...hanzhongScoutRecruitUnits, + ...hanzhongMainRecruitUnits + ].map((unit) => placeScenarioUnit(unit, fortySeventhBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })), + createThirtyFirstEnemyUnit('nanzhong-scout-a', '남중 척후', '밀림 척후', 'bandit', 123, 520, 162, 38, 28), + createThirtyFirstEnemyUnit('nanzhong-scout-b', '남중 척후', '계곡 척후', 'bandit', 123, 520, 162, 47, 41), + createThirtyFirstEnemyUnit('nanzhong-scout-c', '남중 척후', '마을 척후', 'bandit', 124, 528, 164, 58, 52), + createThirtyFirstEnemyUnit('nanzhong-scout-d', '남중 척후', '남쪽 척후', 'bandit', 124, 528, 164, 80, 58), + createThirtyFirstEnemyUnit('nanzhong-infantry-a', '남중 보병', '산길 보병', 'yellowTurban', 123, 730, 188, 42, 33), + createThirtyFirstEnemyUnit('nanzhong-infantry-b', '남중 보병', '창고 보병', 'yellowTurban', 124, 742, 190, 52, 39), + createThirtyFirstEnemyUnit('nanzhong-infantry-c', '남중 보병', '마을 보병', 'yellowTurban', 124, 742, 190, 60, 49), + createThirtyFirstEnemyUnit('nanzhong-infantry-d', '남중 보병', '남로 보병', 'yellowTurban', 125, 754, 192, 70, 57), + createThirtyFirstEnemyUnit('nanzhong-infantry-e', '남중 보병', '고개 보병', 'yellowTurban', 125, 754, 192, 82, 43), + createThirtyFirstEnemyUnit('nanzhong-infantry-f', '남중 보병', '후위 보병', 'yellowTurban', 126, 766, 194, 88, 62), + createThirtyFirstEnemyUnit('nanzhong-archer-a', '남중 궁병', '밀림 궁병', 'archer', 123, 570, 170, 40, 24), + createThirtyFirstEnemyUnit('nanzhong-archer-b', '남중 궁병', '절벽 궁병', 'archer', 124, 578, 172, 55, 30), + createThirtyFirstEnemyUnit('nanzhong-archer-c', '남중 궁병', '마을 궁병', 'archer', 124, 578, 172, 65, 47), + createThirtyFirstEnemyUnit('nanzhong-archer-d', '남중 궁병', '남로 궁병', 'archer', 125, 586, 174, 78, 55), + createThirtyFirstEnemyUnit('nanzhong-archer-e', '남중 궁병', '고지 궁병', 'archer', 125, 586, 174, 84, 31), + createThirtyFirstEnemyUnit('nanzhong-archer-f', '남중 궁병', '퇴로 궁병', 'archer', 126, 594, 176, 90, 50), + createThirtyFirstEnemyUnit('nanzhong-beast-rider-a', '남중 기병', '산짐승 기병', 'cavalry', 125, 748, 192, 45, 20), + createThirtyFirstEnemyUnit('nanzhong-beast-rider-b', '남중 기병', '밀림 기병', 'cavalry', 125, 748, 192, 62, 26), + createThirtyFirstEnemyUnit('nanzhong-beast-rider-c', '남중 기병', '남쪽 기병', 'cavalry', 126, 760, 194, 75, 45), + createThirtyFirstEnemyUnit('nanzhong-beast-rider-d', '남중 기병', '추격 기병', 'cavalry', 126, 760, 194, 88, 36), + createThirtyFirstEnemyUnit('nanzhong-ambusher-a', '남중 복병', '덩굴 복병', 'bandit', 124, 540, 166, 35, 38), + createThirtyFirstEnemyUnit('nanzhong-ambusher-b', '남중 복병', '계곡 복병', 'bandit', 125, 548, 168, 49, 46), + createThirtyFirstEnemyUnit('nanzhong-ambusher-c', '남중 복병', '강가 복병', 'bandit', 125, 548, 168, 61, 60), + createThirtyFirstEnemyUnit('nanzhong-ambusher-d', '남중 복병', '후방 복병', 'bandit', 126, 556, 170, 78, 66), + createThirtyFirstEnemyUnit('nanzhong-ambusher-e', '남중 복병', '서쪽 차단대', 'bandit', 126, 556, 170, 30, 53), + createThirtyFirstEnemyUnit('nanzhong-guard-a', '남중 친위대', '마을 친위대', 'yellowTurban', 126, 782, 198, 66, 35), + createThirtyFirstEnemyUnit('nanzhong-guard-b', '남중 친위대', '창고 친위대', 'yellowTurban', 127, 794, 200, 74, 50), + createThirtyFirstEnemyUnit('nanzhong-guard-c', '남중 친위대', '수장 친위대', 'yellowTurban', 127, 794, 200, 86, 56), + createThirtyFirstEnemyUnit('nanzhong-strategist-a', '남중 주술사', '밀림 주술사', 'strategist', 126, 610, 178, 63, 29), + createThirtyFirstEnemyUnit('nanzhong-strategist-b', '남중 주술사', '독안개 주술사', 'strategist', 127, 618, 180, 72, 47), + createThirtyFirstEnemyUnit('nanzhong-strategist-c', '남중 주술사', '강가 주술사', 'strategist', 127, 618, 180, 57, 64), + createThirtyFirstEnemyUnit('nanzhong-officer-gao-ding', '고정', '남중 산채장', 'rebelLeader', 128, 812, 204, 78, 39), + createThirtyFirstEnemyUnit('nanzhong-officer-zhu-bao', '주포', '남중 창고장', 'rebelLeader', 127, 798, 202, 66, 58), + createThirtyFirstEnemyUnit('nanzhong-officer-meng-huo-vanguard', '맹획 선봉', '밀림 호족장', 'rebelLeader', 128, 820, 206, 82, 28), + createThirtyFirstEnemyUnit('nanzhong-leader-yong-kai', '옹개', '남중 반란 수장', 'rebelLeader', 130, 856, 212, 88, 42) +]; + export const firstBattleBonds: BattleBond[] = [ { id: 'liu-bei__guan-yu', @@ -16293,6 +16442,35 @@ export const yilingFireBonds: BattleBond[] = [ export const fortySixthBattleBonds: BattleBond[] = [...fortyFifthBattleBonds, ...yilingFireBonds].map( cloneBattleBondForScenario ); +export const nanzhongRecoveryBonds: BattleBond[] = [ + { + id: 'zhuge-liang__zhao-yun_nanzhong', + unitIds: ['zhuge-liang', 'zhao-yun'], + title: '섭정의 선봉', + level: 42, + exp: 0, + description: '제갈량의 신중한 남중 방침과 조운의 안정된 선봉은 백제성 이후 흔들린 군심을 붙잡는다.' + }, + { + id: 'zhuge-liang__huang-quan_nanzhong', + unitIds: ['zhuge-liang', 'huang-quan'], + title: '나라 안의 장부', + level: 40, + exp: 0, + description: '제갈량과 황권은 남중의 반란을 단순한 토벌이 아니라 촉한 재정비의 장부로 바라본다.' + }, + { + id: 'ma-liang__wang-ping_nanzhong', + unitIds: ['ma-liang', 'wang-ping'], + title: '마을과 산길', + level: 36, + exp: 0, + description: '마량의 민심 판단과 왕평의 지형 판단은 남중의 마을을 적으로 돌리지 않고 길을 여는 힘이 된다.' + } +]; +export const fortySeventhBattleBonds: BattleBond[] = [...fortySixthBattleBonds, ...nanzhongRecoveryBonds].map( + cloneBattleBondForScenario +); function createEighthBattleTerrain(): TerrainType[][] { return Array.from({ length: 22 }, (_, y) => @@ -18618,6 +18796,67 @@ function createFortySixthBattleTerrain(): TerrainType[][] { ); } +function createFortySeventhBattleTerrain(): TerrainType[][] { + return Array.from({ length: 76 }, (_, y) => + Array.from({ length: 94 }, (_, x): TerrainType => { + if ( + (x >= 0 && x <= 93 && y >= 66) || + (x >= 46 && x <= 58 && y >= 55 && y <= 75) || + (x >= 10 && x <= 20 && y >= 59 && y <= 75) + ) { + return 'river'; + } + if ( + (x >= 1 && x <= 86 && y >= 46 && y <= 51) || + (x >= 20 && x <= 91 && y >= 57 && y <= 61) || + (x >= 28 && x <= 39 && y >= 33 && y <= 57) || + (x >= 61 && x <= 70 && y >= 24 && y <= 58) + ) { + return 'road'; + } + if ( + (x >= 24 && x <= 64 && y >= 18 && y <= 62) || + (x >= 48 && x <= 82 && y >= 27 && y <= 66) || + (x >= 3 && x <= 31 && y >= 45 && y <= 65) || + (x >= 72 && x <= 93 && y >= 38 && y <= 65) + ) { + return 'forest'; + } + if ( + (x >= 0 && x <= 28 && y <= 32) || + (x >= 36 && x <= 62 && y <= 22) || + (x >= 66 && x <= 93 && y <= 30) || + (x >= 82 && x <= 93 && y >= 29 && y <= 46) + ) { + return 'hill'; + } + if ( + (x <= 2 && y <= 65) || + (x >= 91 && y <= 60) || + (x >= 31 && x <= 36 && y >= 0 && y <= 32) || + (x >= 63 && x <= 68 && y >= 0 && y <= 26) || + (x >= 37 && x <= 42 && y >= 56 && y <= 75) + ) { + return 'cliff'; + } + if ( + (x >= 12 && x <= 23 && y >= 43 && y <= 55) || + (x >= 42 && x <= 54 && y >= 38 && y <= 52) || + (x >= 70 && x <= 84 && y >= 34 && y <= 49) + ) { + return 'village'; + } + if ((x >= 16 && x <= 25 && y >= 34 && y <= 42) || (x >= 76 && x <= 88 && y >= 50 && y <= 62)) { + return 'fort'; + } + if ((x >= 54 && x <= 64 && y >= 52 && y <= 61) || (x >= 30 && x <= 40 && y >= 42 && y <= 50)) { + return 'camp'; + } + 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 3efa792..63f55e4 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -844,7 +844,14 @@ const unitTexture: Record = { 'yiling-fire-officer-han-dang': 'unit-rebel-leader', 'yiling-fire-officer-pan-zhang': 'unit-rebel-leader', 'yiling-fire-officer-ma-zhong': 'unit-rebel', - 'yiling-fire-leader-lu-xun': 'unit-rebel-leader' + 'yiling-fire-leader-lu-xun': 'unit-rebel-leader', + 'nanzhong-strategist-a': 'unit-rebel-archer', + 'nanzhong-strategist-b': 'unit-rebel-archer', + 'nanzhong-strategist-c': 'unit-rebel-archer', + 'nanzhong-officer-gao-ding': 'unit-rebel-leader', + 'nanzhong-officer-zhu-bao': 'unit-rebel-leader', + 'nanzhong-officer-meng-huo-vanguard': 'unit-rebel-leader', + 'nanzhong-leader-yong-kai': 'unit-rebel-leader' }; const unitTextureByClass: Partial> = { @@ -2208,7 +2215,42 @@ const enemyAiByUnitId: Record = { 'yiling-fire-officer-han-dang': 'guard', 'yiling-fire-officer-pan-zhang': 'aggressive', 'yiling-fire-officer-ma-zhong': 'aggressive', - 'yiling-fire-leader-lu-xun': 'hold' + 'yiling-fire-leader-lu-xun': 'hold', + 'nanzhong-scout-a': 'aggressive', + 'nanzhong-scout-b': 'aggressive', + 'nanzhong-scout-c': 'aggressive', + 'nanzhong-scout-d': 'aggressive', + 'nanzhong-infantry-a': 'guard', + 'nanzhong-infantry-b': 'guard', + 'nanzhong-infantry-c': 'guard', + 'nanzhong-infantry-d': 'guard', + 'nanzhong-infantry-e': 'guard', + 'nanzhong-infantry-f': 'guard', + 'nanzhong-archer-a': 'hold', + 'nanzhong-archer-b': 'hold', + 'nanzhong-archer-c': 'hold', + 'nanzhong-archer-d': 'hold', + 'nanzhong-archer-e': 'hold', + 'nanzhong-archer-f': 'hold', + 'nanzhong-beast-rider-a': 'aggressive', + 'nanzhong-beast-rider-b': 'aggressive', + 'nanzhong-beast-rider-c': 'aggressive', + 'nanzhong-beast-rider-d': 'aggressive', + 'nanzhong-ambusher-a': 'aggressive', + 'nanzhong-ambusher-b': 'aggressive', + 'nanzhong-ambusher-c': 'aggressive', + 'nanzhong-ambusher-d': 'aggressive', + 'nanzhong-ambusher-e': 'aggressive', + 'nanzhong-guard-a': 'guard', + 'nanzhong-guard-b': 'guard', + 'nanzhong-guard-c': 'guard', + 'nanzhong-strategist-a': 'hold', + 'nanzhong-strategist-b': 'hold', + 'nanzhong-strategist-c': 'hold', + 'nanzhong-officer-gao-ding': 'guard', + 'nanzhong-officer-zhu-bao': 'guard', + 'nanzhong-officer-meng-huo-vanguard': 'aggressive', + 'nanzhong-leader-yong-kai': 'hold' }; const defaultEnemyAiByClass: Partial> = { @@ -2413,9 +2455,14 @@ export class BattleScene extends Phaser.Scene { private resetBattleData(campaign?: CampaignState) { const selected = new Set(campaign?.selectedSortieUnitIds ?? []); + const forcedAllyIds = new Set( + battleScenario.defeatConditions + .filter((condition) => condition.kind === 'unit-defeated') + .map((condition) => condition.unitId) + ); const hasSelection = selected.size > 0; const nextUnits = initialBattleUnits - .filter((unit) => unit.faction === 'enemy' || !hasSelection || selected.has(unit.id) || unit.id === 'liu-bei') + .filter((unit) => unit.faction === 'enemy' || !hasSelection || selected.has(unit.id) || forcedAllyIds.has(unit.id)) .map((unit) => this.applyCampaignUnitProgress(unit, campaign)); battleUnits.splice(0, battleUnits.length, ...nextUnits); battleBonds.splice(0, battleBonds.length, ...initialBattleBonds.map((bond) => this.applyCampaignBondProgress(bond, campaign))); diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 401c764..a43756c 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -12,6 +12,7 @@ import fortyThirdBattleMapUrl from '../../assets/images/battle/forty-third-battl import fortyFourthBattleMapUrl from '../../assets/images/battle/forty-fourth-battle-map.svg'; import fortyFifthBattleMapUrl from '../../assets/images/battle/forty-fifth-battle-map.svg'; import fortySixthBattleMapUrl from '../../assets/images/battle/forty-sixth-battle-map.svg'; +import fortySeventhBattleMapUrl from '../../assets/images/battle/forty-seventh-battle-map.svg'; import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg'; import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg'; import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg'; @@ -150,6 +151,7 @@ export class BootScene extends Phaser.Scene { this.load.image('battle-map-forty-fourth', fortyFourthBattleMapUrl); this.load.image('battle-map-forty-fifth', fortyFifthBattleMapUrl); this.load.image('battle-map-forty-sixth', fortySixthBattleMapUrl); + this.load.image('battle-map-forty-seventh', fortySeventhBattleMapUrl); 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 997b2b7..825cdce 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -41,6 +41,7 @@ import { luoCastleRecruitBonds, luoCastleRecruitUnits, maichengIsolationBonds, + nanzhongRecoveryBonds, wulingRecruitBonds, wulingRecruitUnits, xuzhouRecruitBonds, @@ -405,9 +406,10 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [ 'forty-third-battle-gongan-collapse', 'forty-fourth-battle-maicheng-isolation', 'forty-fifth-battle-yiling-vanguard', - 'forty-sixth-battle-yiling-fire' + 'forty-sixth-battle-yiling-fire', + 'forty-seventh-battle-nanzhong-stabilization' ], - nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공'] + nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정'] } ]; @@ -457,7 +459,8 @@ const campBattleIds = { fortyThird: 'forty-third-battle-gongan-collapse', fortyFourth: 'forty-fourth-battle-maicheng-isolation', fortyFifth: 'forty-fifth-battle-yiling-vanguard', - fortySixth: 'forty-sixth-battle-yiling-fire' + fortySixth: 'forty-sixth-battle-yiling-fire', + fortySeventh: 'forty-seventh-battle-nanzhong-stabilization' } as const; const defaultRequiredSortieUnitIds = ['liu-bei']; @@ -1004,6 +1007,20 @@ const sortieRulesByBattleId: Partial unit.faction === 'ally' && unit.hp > 0); + const scenario = this.nextSortieScenario(); + const scenarioAllyIds = scenario + ? new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id)) + : undefined; + return this.currentUnits().filter( + (unit) => unit.faction === 'ally' && unit.hp > 0 && (!scenarioAllyIds || scenarioAllyIds.has(unit.id)) + ); } private selectedSortieUnits() { diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index 6b96f1b..8b320ec 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -12,6 +12,7 @@ import { fortyFourthBattleScenario, fortyFifthBattleScenario, fortySixthBattleScenario, + fortySeventhBattleScenario, fifthBattleScenario, fifteenthBattleScenario, fourteenthBattleScenario, @@ -392,6 +393,7 @@ export class TitleScene extends Phaser.Scene { campaign.step === 'forty-fourth-camp' || campaign.step === 'forty-fifth-camp' || campaign.step === 'forty-sixth-camp' || + campaign.step === 'forty-seventh-camp' || campaign.step === 'hanzhong-king-camp' || campaign.step === 'shu-han-foundation-camp' || campaign.step === 'baidi-entrustment-camp' @@ -630,6 +632,11 @@ export class TitleScene extends Phaser.Scene { return; } + if (campaign.step === 'forty-seventh-battle') { + this.scene.start('BattleScene', { battleId: fortySeventhBattleScenario.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 c5be44a..946a909 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -134,6 +134,8 @@ export type CampaignStep = | 'forty-fifth-camp' | 'forty-sixth-battle' | 'forty-sixth-camp' + | 'forty-seventh-battle' + | 'forty-seventh-camp' | 'hanzhong-king-camp' | 'shu-han-foundation-camp' | 'baidi-entrustment-camp'; @@ -288,7 +290,8 @@ const campaignBattleSteps: Record