diff --git a/docs/roadmap.md b/docs/roadmap.md
index c0aac6a..9370d8d 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -46,16 +46,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- Twenty-second battle Red Cliffs fire attack, turning Huang Gai's bitter-flesh plan and Zhuge Liang's wind timing into the decisive fire battle against Cao Cao's chained fleet
- Twenty-third battle Jing Province south entry route, opening the post-Red-Cliffs land campaign with Xing Daorong, village stabilization, a larger 46x32 battlefield, and Ma Liang recruitment after victory
- Twenty-fourth battle Guiyang persuasion route, using Ma Liang's local knowledge in sortie selection, expanding the southern-commandery map to 48x32, and recruiting Yi Ji after Zhao Fan yields
+- 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
- 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-fourth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang camp states, campaign timeline state, and camp save state
+- Flow verification script from title through the twenty-fifth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling camp states, campaign timeline state, and camp save state
## Next Steps
-1. Continue Jing Province south from Guiyang into Wuling and Changsha acquisition
-2. Add more recruitable officers as the campaign moves through Jing Province toward Huang Zhong, Wei Yan, Yi Province, and Shu Han
+1. Continue Jing Province south from Wuling into Changsha acquisition, including Huang Zhong and Wei Yan recruitment pressure
+2. Add more recruitable officers as the campaign moves through Changsha, Yi Province, and Shu Han
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 549e10c..f4a7c8c 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 !== 24 ||
+ progressTabState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 25 ||
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 !== 24 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 25 ||
postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' ||
postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전'
@@ -2729,15 +2729,158 @@ try {
if (
postGuiyangProgressState?.activeTab !== 'progress' ||
postGuiyangProgressState.campaignProgress?.completedKnown !== 24 ||
- postGuiyangProgressState.campaignProgress?.totalKnown !== 24 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 25 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
- postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
) {
throw new Error(`Expected post-Guiyang progress tab to complete the new commandery battle and pause on Wuling preparation: ${JSON.stringify(postGuiyangProgressState?.campaignProgress)}`);
}
await page.screenshot({ path: 'dist/verification-post-guiyang-progress.png', fullPage: true });
+ await page.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const wulingSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !wulingSortieState?.sortieVisible ||
+ !wulingSortieState.sortiePlan?.objectiveLine?.includes('무릉 산길 확보전') ||
+ !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'ma-liang' && unit.recruited && unit.recommended) ||
+ !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) ||
+ !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
+ !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
+ !wulingSortieState.sortieRoster?.some((unit) => unit.id === 'zhang-fei' && unit.recommended) ||
+ wulingSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi') ||
+ wulingSortieState.sortieRoster?.length < 10 ||
+ wulingSortieState.sortiePlan?.maxCount !== 6
+ ) {
+ throw new Error(`Expected twenty-fourth camp sortie prep to target Wuling with Yi Ji recommended and Gong Zhi absent before victory: ${JSON.stringify(wulingSortieState)}`);
+ }
+ assertSortieTacticalRoster(wulingSortieState, [
+ 'liu-bei',
+ 'guan-yu',
+ 'zhang-fei',
+ 'jian-yong',
+ 'mi-zhu',
+ 'sun-qian',
+ 'zhao-yun',
+ 'zhuge-liang',
+ 'ma-liang',
+ 'yi-ji'
+ ]);
+
+ const wulingPriorityUnits = ['ma-liang', 'yi-ji', 'zhuge-liang', 'zhao-yun', 'zhang-fei'];
+ for (const unitId of wulingPriorityUnits) {
+ 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' && !wulingPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const wulingSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !wulingPriorityUnits.every((unitId) =>
+ wulingSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ wulingSortieReadyState.sortiePlan?.selectedCount !== 6 ||
+ wulingSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
+ ) {
+ throw new Error(`Expected Wuling sortie to deploy Ma Liang, Yi Ji, and the mountain-road priority officers while preserving six-officer pressure: ${JSON.stringify(wulingSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-wuling-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-wuling-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredTwentyFifthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'twenty-fifth-battle-wuling-mountain-road';
+ });
+ if (enteredTwentyFifthBattle) {
+ 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-fifth-battle-wuling-mountain-road' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-twenty-fifth-battle.png', fullPage: true });
+
+ const twentyFifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const twentyFifthEnemies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const twentyFifthAllies = twentyFifthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const twentyFifthEnemyBehaviors = new Set(twentyFifthEnemies.map((unit) => unit.ai));
+ if (
+ twentyFifthBattleState.camera?.mapWidth !== 50 ||
+ twentyFifthBattleState.camera?.mapHeight !== 34 ||
+ twentyFifthBattleState.victoryConditionLabel !== '김선 퇴각' ||
+ twentyFifthEnemies.length < 19 ||
+ !twentyFifthEnemyBehaviors.has('aggressive') ||
+ !twentyFifthEnemyBehaviors.has('guard') ||
+ !twentyFifthEnemyBehaviors.has('hold') ||
+ !twentyFifthEnemies.some((unit) => unit.id === 'wuling-leader-jin-xuan') ||
+ !twentyFifthAllies.some((unit) => unit.id === 'ma-liang') ||
+ !twentyFifthAllies.some((unit) => unit.id === 'yi-ji') ||
+ !twentyFifthAllies.some((unit) => unit.id === 'zhao-yun') ||
+ twentyFifthAllies.some((unit) => unit.id === 'gong-zhi')
+ ) {
+ throw new Error(`Expected twenty-fifth battle to use Wuling map, Jin Xuan objective, mixed AI, selected allied officers, and no Gong Zhi before victory: ${JSON.stringify(twentyFifthBattleState)}`);
+ }
+
+ 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 twentyFifthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ twentyFifthCampState?.campBattleId !== 'twenty-fifth-battle-wuling-mountain-road' ||
+ twentyFifthCampState.campTitle !== '무릉 산길 확보 후 군영' ||
+ twentyFifthCampState.availableDialogueIds?.length !== 3 ||
+ !twentyFifthCampState.availableDialogueIds.every((id) => id.endsWith('wuling')) ||
+ twentyFifthCampState.availableVisitIds?.length !== 2 ||
+ !twentyFifthCampState.campaign?.roster?.some((unit) => unit.id === 'gong-zhi') ||
+ !twentyFifthCampState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited) ||
+ twentyFifthCampState.rosterCollection?.total < 11
+ ) {
+ throw new Error(`Expected twenty-fifth camp to recruit Gong Zhi and expose Wuling dialogue/visit sets: ${JSON.stringify(twentyFifthCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-wuling-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postWulingProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postWulingProgressState?.activeTab !== 'progress' ||
+ postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 25 ||
+ postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
+ postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
+ postWulingProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ ) {
+ throw new Error(`Expected post-Wuling progress tab to complete the mountain-road battle and pause on Changsha preparation: ${JSON.stringify(postWulingProgressState?.campaignProgress)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-wuling-progress.png', fullPage: true });
+
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -2749,7 +2892,7 @@ try {
return activeScenes.includes('CampScene');
});
- console.log(`Verified title-to-twenty-fourth-battle flow, recruited officer sortie selection, Ma Liang and Yi Ji joins, result states, and debug API at ${targetUrl}`);
+ console.log(`Verified title-to-twenty-fifth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, and Gong Zhi joins, result states, and debug API at ${targetUrl}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/twenty-fifth-battle-map.svg b/src/assets/images/battle/twenty-fifth-battle-map.svg
new file mode 100644
index 0000000..d96bcbd
--- /dev/null
+++ b/src/assets/images/battle/twenty-fifth-battle-map.svg
@@ -0,0 +1,85 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 0467051..7711908 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -55,6 +55,10 @@ import {
twentyFourthBattleMap,
twentyFourthBattleUnits,
twentyFourthBattleVictoryPages,
+ twentyFifthBattleBonds,
+ twentyFifthBattleMap,
+ twentyFifthBattleUnits,
+ twentyFifthBattleVictoryPages,
twentiethBattleBonds,
twentiethBattleMap,
twentiethBattleUnits,
@@ -125,7 +129,8 @@ export type BattleScenarioId =
| 'twenty-first-battle-red-cliffs-vanguard'
| 'twenty-second-battle-red-cliffs-fire'
| 'twenty-third-battle-jingzhou-south-entry'
- | 'twenty-fourth-battle-guiyang-persuasion';
+ | 'twenty-fourth-battle-guiyang-persuasion'
+ | 'twenty-fifth-battle-wuling-mountain-road';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1497,6 +1502,69 @@ export const twentyFourthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const twentyFifthBattleScenario: BattleScenarioDefinition = {
+ id: 'twenty-fifth-battle-wuling-mountain-road',
+ title: '무릉 산길 확보전',
+ victoryConditionLabel: '김선 퇴각',
+ defeatConditionLabel: '유비 또는 제갈량 퇴각',
+ openingObjectiveLines: [
+ '계양이 열린 뒤 유비군은 무릉의 산길로 들어섭니다. 좁은 길과 흩어진 마을 때문에 단순 돌파만으로는 근거지를 얻을 수 없습니다.',
+ '마량과 이적은 무릉의 내부 동요를 읽고, 김선 아래 장수 공지가 백성 피해를 염려한다고 보고합니다.',
+ '유비와 제갈량을 지키며 김선을 물러나게 하고 산길 마을을 안정시키면, 공지가 군영에 합류합니다.'
+ ],
+ map: twentyFifthBattleMap,
+ units: twentyFifthBattleUnits,
+ bonds: twentyFifthBattleBonds,
+ mapTextureKey: 'battle-map-twenty-fifth',
+ leaderUnitId: 'wuling-leader-jin-xuan',
+ quickVictoryTurnLimit: 34,
+ baseVictoryGold: 3360,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '김선 퇴각',
+ rewardGold: 2200,
+ unitId: 'wuling-leader-jin-xuan'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 640,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 900,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'wuling-village',
+ kind: 'secure-terrain',
+ label: '무릉 마을 안정',
+ rewardGold: 1080,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '34턴 이내 승리',
+ rewardGold: 840,
+ maxTurn: 34
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'zhuge-liang' }
+ ],
+ itemRewards: ['콩 11', '상처약 8', '탁주 4', '무릉 산길 지도 +1', '공지 합류'],
+ victoryPages: twentyFifthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -1523,7 +1591,8 @@ export const battleScenarios: Record
'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario,
'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario,
'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario,
- 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario
+ 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario,
+ 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 475d3e4..61ccfef 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -20,6 +20,7 @@ import {
twentyFirstBattleScenario,
twentySecondBattleScenario,
twentyThirdBattleScenario,
+ twentyFifthBattleScenario,
twentyFourthBattleScenario,
twentiethBattleScenario,
twelfthBattleScenario,
@@ -66,6 +67,8 @@ import {
twentySecondBattleVictoryPages,
twentyThirdBattleIntroPages,
twentyThirdBattleVictoryPages,
+ twentyFifthBattleIntroPages,
+ twentyFifthBattleVictoryPages,
twentyFourthBattleIntroPages,
twentyFourthBattleVictoryPages,
twentiethBattleIntroPages,
@@ -333,13 +336,24 @@ const sortieFlows: Record = {
},
[twentyFourthBattleScenario.id]: {
afterBattleId: twentyFourthBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '무릉 산길 준비',
+ eyebrow: '다음 전장',
+ title: twentyFifthBattleScenario.title,
description:
- '계양이 열리고 이적이 합류했습니다. 다음은 험한 산길과 흩어진 마을이 기다리는 무릉으로 향해야 합니다.',
- rewardHint: '다음 장: 무릉 산길 확보 준비 중',
- pages: twentyFourthBattleVictoryPages,
- unavailableNotice: '무릉 산길 확보 장은 다음 작업에서 이어집니다. 군영에서 이적과 대화하고 산길 전투를 준비하십시오.'
+ '계양이 열리고 이적이 합류했습니다. 이제 마량과 이적의 형주 지식을 앞세워 험한 무릉 산길과 흩어진 마을을 이어야 합니다.',
+ rewardHint: `예상 보상: ${twentyFifthBattleScenario.title} 개방 / 공지 합류`,
+ nextBattleId: twentyFifthBattleScenario.id,
+ campaignStep: 'twenty-fifth-battle',
+ pages: [...twentyFourthBattleVictoryPages, ...twentyFifthBattleIntroPages]
+ },
+ [twentyFifthBattleScenario.id]: {
+ afterBattleId: twentyFifthBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '장사 노장 대면 준비',
+ description:
+ '무릉 산길이 열리고 공지가 합류했습니다. 다음은 장사의 노장 황충과 태수 한현을 마주하며 형주 남부 확보의 큰 고비를 넘겨야 합니다.',
+ rewardHint: '다음 장: 장사 노장 대면 준비 중',
+ pages: twentyFifthBattleVictoryPages,
+ unavailableNotice: '장사 노장 대면 장은 다음 작업에서 이어집니다. 군영에서 공지와 대화하고 황충을 맞을 준비를 하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 293f5f3..6dbfbb8 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -1572,6 +1572,68 @@ export const twentyFourthBattleVictoryPages: StoryPage[] = [
}
];
+export const twentyFifthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'twenty-fifth-wuling-road',
+ bgm: 'story-dark',
+ chapter: '무릉의 닫힌 산길',
+ background: 'story-three-heroes',
+ speaker: '마량',
+ text: '무릉은 계양처럼 성문 하나만 바라보는 땅이 아닙니다. 산길과 작은 마을이 흩어져 있어, 길을 잃으면 군량도 마음도 끊어집니다.'
+ },
+ {
+ id: 'twenty-fifth-yi-ji-letter',
+ bgm: 'battle-prep',
+ chapter: '먼저 보내는 예',
+ background: 'story-militia',
+ speaker: '이적',
+ text: '김선은 강하게 버티려 하지만, 아래 장수 공지는 백성의 피해를 걱정한다고 들었습니다. 산길 마을을 지키면 그에게 말이 닿을 것입니다.'
+ },
+ {
+ id: 'twenty-fifth-zhuge-formation',
+ bgm: 'battle-prep',
+ chapter: '좁은 길의 진형',
+ background: 'story-liu-bei',
+ speaker: '제갈량',
+ text: '좁은 산길에서는 무장이 많다고 곧 힘이 되는 것이 아닙니다. 앞길을 여는 자, 마을을 달래는 자, 보급을 잇는 자를 골라야 합니다.'
+ },
+ {
+ id: 'twenty-fifth-wuling-sortie',
+ bgm: 'battle-prep',
+ chapter: '무릉 산길 확보전',
+ background: 'story-sortie',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '산길을 불태우지 말고 이어야 하오. 무릉의 길과 마을을 지켜 내면, 닫힌 마음도 함께 열릴 것이오.'
+ }
+];
+
+export const twentyFifthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'twenty-fifth-victory-wuling-open',
+ bgm: 'militia-theme',
+ chapter: '무릉의 길이 이어지다',
+ background: 'story-sortie',
+ text: '김선의 산길 방어선이 무너지자 무릉의 마을들은 먼저 불안한 문을 열었습니다. 유비군은 산길 창고를 봉하고 백성의 길을 지켰습니다.'
+ },
+ {
+ id: 'twenty-fifth-gong-zhi-joins',
+ bgm: 'battle-prep',
+ chapter: '공지의 결심',
+ background: 'story-militia',
+ speaker: '공지',
+ text: '무릉을 힘으로만 누르지 않으셨군요. 백성을 앞세우는 군이라면, 저도 산길과 고을의 형세를 바쳐 따르겠습니다.'
+ },
+ {
+ id: 'twenty-fifth-changsha-shadow',
+ bgm: 'story-dark',
+ chapter: '장사의 노장',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '무릉까지 길이 이어졌습니다. 남은 장사에는 황충이라는 노장이 있습니다. 다음 싸움은 힘보다 예우와 인내를 시험할 것입니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1861,6 +1923,12 @@ export const twentyFourthBattleMap: BattleMap = {
terrain: createTwentyFourthBattleTerrain()
};
+export const twentyFifthBattleMap: BattleMap = {
+ width: 50,
+ height: 34,
+ terrain: createTwentyFifthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -3865,6 +3933,30 @@ export const guiyangRecruitUnits: UnitData[] = [
}
];
+export const wulingRecruitUnits: UnitData[] = [
+ {
+ id: 'gong-zhi',
+ name: '공지',
+ faction: 'ally',
+ className: '무릉 의병장',
+ classKey: 'infantry',
+ level: 13,
+ exp: 0,
+ hp: 40,
+ maxHp: 40,
+ attack: 12,
+ move: 4,
+ stats: { might: 76, intelligence: 72, leadership: 82, agility: 74, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 2, exp: 6 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 8 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 8,
+ y: 23
+ }
+];
+
const eighthBattleAllyPositions: Record = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -9542,6 +9634,430 @@ export const twentyFourthBattleUnits: UnitData[] = [
}
];
+const twentyFifthBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 28 },
+ 'guan-yu': { x: 6, y: 26 },
+ 'zhang-fei': { x: 6, y: 30 },
+ 'jian-yong': { x: 3, y: 31 },
+ 'mi-zhu': { x: 4, y: 32 },
+ 'sun-qian': { x: 8, y: 29 },
+ 'zhao-yun': { x: 8, y: 26 },
+ 'zhuge-liang': { x: 5, y: 28 },
+ 'ma-liang': { x: 7, y: 31 },
+ 'yi-ji': { x: 9, y: 28 }
+};
+
+export const twentyFifthBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits,
+ ...jingzhouRecruitUnits,
+ ...guiyangRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, twentyFifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ {
+ id: 'wuling-scout-a',
+ name: '무릉 산길 척후',
+ faction: 'enemy',
+ className: '산길 척후',
+ classKey: 'bandit',
+ level: 34,
+ exp: 52,
+ hp: 92,
+ maxHp: 92,
+ attack: 36,
+ move: 4,
+ stats: { might: 120, intelligence: 88, leadership: 96, agility: 116, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 28 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 16 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 13,
+ y: 27
+ },
+ {
+ id: 'wuling-scout-b',
+ name: '무릉 산길 척후',
+ faction: 'enemy',
+ className: '북릉 척후',
+ classKey: 'bandit',
+ level: 34,
+ exp: 52,
+ hp: 92,
+ maxHp: 92,
+ attack: 36,
+ move: 4,
+ stats: { might: 120, intelligence: 88, leadership: 96, agility: 116, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 28 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 16 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 16,
+ y: 23
+ },
+ {
+ id: 'wuling-scout-c',
+ name: '무릉 산길 척후',
+ faction: 'enemy',
+ className: '서곡 척후',
+ classKey: 'bandit',
+ level: 35,
+ exp: 54,
+ hp: 96,
+ maxHp: 96,
+ attack: 37,
+ move: 4,
+ stats: { might: 122, intelligence: 88, leadership: 98, agility: 118, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 18 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 20,
+ y: 29
+ },
+ {
+ id: 'wuling-bandit-a',
+ name: '무릉 산적',
+ faction: 'enemy',
+ className: '산적',
+ classKey: 'bandit',
+ level: 34,
+ exp: 52,
+ hp: 102,
+ maxHp: 102,
+ attack: 37,
+ move: 4,
+ stats: { might: 126, intelligence: 80, leadership: 92, agility: 112, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 18 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 18,
+ y: 17
+ },
+ {
+ id: 'wuling-bandit-b',
+ name: '무릉 산적',
+ faction: 'enemy',
+ className: '숲길 산적',
+ classKey: 'bandit',
+ level: 34,
+ exp: 52,
+ hp: 102,
+ maxHp: 102,
+ attack: 37,
+ move: 4,
+ stats: { might: 126, intelligence: 80, leadership: 92, agility: 112, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 30 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 18 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 23,
+ y: 20
+ },
+ {
+ id: 'wuling-bandit-c',
+ name: '무릉 산적',
+ faction: 'enemy',
+ className: '협곡 산적',
+ classKey: 'bandit',
+ level: 35,
+ exp: 54,
+ hp: 106,
+ maxHp: 106,
+ attack: 38,
+ move: 4,
+ stats: { might: 128, intelligence: 82, leadership: 94, agility: 114, luck: 78 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 32 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 20 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 25,
+ y: 13
+ },
+ {
+ id: 'wuling-infantry-a',
+ name: '무릉 보병',
+ faction: 'enemy',
+ className: '무릉 보병',
+ classKey: 'yellowTurban',
+ level: 35,
+ exp: 54,
+ hp: 120,
+ maxHp: 120,
+ attack: 39,
+ move: 3,
+ stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 42 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 26 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 27,
+ y: 23
+ },
+ {
+ id: 'wuling-infantry-b',
+ name: '무릉 보병',
+ faction: 'enemy',
+ className: '마을 보병',
+ classKey: 'yellowTurban',
+ level: 35,
+ exp: 54,
+ hp: 120,
+ maxHp: 120,
+ attack: 39,
+ move: 3,
+ stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 42 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 26 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 31,
+ y: 26
+ },
+ {
+ id: 'wuling-infantry-c',
+ name: '무릉 보병',
+ faction: 'enemy',
+ className: '산문 보병',
+ classKey: 'yellowTurban',
+ level: 36,
+ exp: 56,
+ hp: 126,
+ maxHp: 126,
+ attack: 40,
+ move: 3,
+ stats: { might: 134, intelligence: 88, leadership: 114, agility: 96, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 46 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 37,
+ y: 18
+ },
+ {
+ id: 'wuling-infantry-d',
+ name: '무릉 보병',
+ faction: 'enemy',
+ className: '성채 보병',
+ classKey: 'yellowTurban',
+ level: 36,
+ exp: 56,
+ hp: 126,
+ maxHp: 126,
+ attack: 40,
+ move: 3,
+ stats: { might: 134, intelligence: 88, leadership: 114, agility: 96, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 46 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 30 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 41,
+ y: 14
+ },
+ {
+ id: 'wuling-archer-a',
+ name: '무릉 궁병',
+ faction: 'enemy',
+ className: '능선 궁병',
+ classKey: 'archer',
+ level: 35,
+ exp: 54,
+ hp: 90,
+ maxHp: 90,
+ attack: 36,
+ move: 3,
+ stats: { might: 92, intelligence: 96, leadership: 100, agility: 108, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 38 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 22 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 24,
+ y: 16
+ },
+ {
+ id: 'wuling-archer-b',
+ name: '무릉 궁병',
+ faction: 'enemy',
+ className: '산채 궁병',
+ classKey: 'archer',
+ level: 35,
+ exp: 54,
+ hp: 90,
+ maxHp: 90,
+ attack: 36,
+ move: 3,
+ stats: { might: 92, intelligence: 96, leadership: 100, agility: 108, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 38 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 22 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 34,
+ y: 21
+ },
+ {
+ id: 'wuling-archer-c',
+ name: '무릉 궁병',
+ faction: 'enemy',
+ className: '성루 궁병',
+ classKey: 'archer',
+ level: 36,
+ exp: 56,
+ hp: 94,
+ maxHp: 94,
+ attack: 37,
+ move: 3,
+ stats: { might: 94, intelligence: 98, leadership: 102, agility: 110, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 42 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 24 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 43,
+ y: 16
+ },
+ {
+ id: 'wuling-cavalry-a',
+ name: '무릉 기병',
+ faction: 'enemy',
+ className: '산길 기병',
+ classKey: 'cavalry',
+ level: 36,
+ exp: 56,
+ hp: 130,
+ maxHp: 130,
+ attack: 41,
+ move: 5,
+ stats: { might: 136, intelligence: 88, leadership: 112, agility: 116, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 48 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 30 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 30,
+ y: 29
+ },
+ {
+ id: 'wuling-cavalry-b',
+ name: '무릉 기병',
+ faction: 'enemy',
+ className: '동릉 기병',
+ classKey: 'cavalry',
+ level: 36,
+ exp: 56,
+ hp: 130,
+ maxHp: 130,
+ attack: 41,
+ move: 5,
+ stats: { might: 136, intelligence: 88, leadership: 112, agility: 116, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 48 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 30 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 36,
+ y: 27
+ },
+ {
+ id: 'wuling-guard-a',
+ name: '김선 친위대',
+ faction: 'enemy',
+ className: '무릉 친위대',
+ classKey: 'yellowTurban',
+ level: 37,
+ exp: 58,
+ hp: 132,
+ maxHp: 132,
+ attack: 42,
+ move: 3,
+ stats: { might: 138, intelligence: 90, leadership: 118, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 52 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 44,
+ y: 13
+ },
+ {
+ id: 'wuling-guard-b',
+ name: '김선 친위대',
+ faction: 'enemy',
+ className: '무릉 친위대',
+ classKey: 'yellowTurban',
+ level: 37,
+ exp: 58,
+ hp: 132,
+ maxHp: 132,
+ attack: 42,
+ move: 3,
+ stats: { might: 138, intelligence: 90, leadership: 118, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 52 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 45,
+ y: 16
+ },
+ {
+ id: 'wuling-strategist-a',
+ name: '무릉 책사',
+ faction: 'enemy',
+ className: '무릉 책사',
+ classKey: 'archer',
+ level: 36,
+ exp: 56,
+ hp: 94,
+ maxHp: 94,
+ attack: 37,
+ move: 3,
+ stats: { might: 88, intelligence: 112, leadership: 104, agility: 94, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 42 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 24 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 42,
+ y: 12
+ },
+ {
+ id: 'wuling-leader-jin-xuan',
+ name: '김선',
+ faction: 'enemy',
+ className: '무릉 태수',
+ classKey: 'rebelLeader',
+ level: 38,
+ exp: 62,
+ hp: 184,
+ maxHp: 184,
+ attack: 44,
+ move: 4,
+ stats: { might: 140, intelligence: 98, leadership: 126, agility: 100, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'leader-axe', level: 4, exp: 68 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 46,
+ y: 14
+ }
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -9723,6 +10239,33 @@ export const guiyangRecruitBonds: BattleBond[] = [
}
];
+export const wulingRecruitBonds: BattleBond[] = [
+ {
+ id: 'liu-bei__gong-zhi',
+ unitIds: ['liu-bei', 'gong-zhi'],
+ title: '산길의 귀순',
+ level: 32,
+ exp: 0,
+ description: '유비의 군율과 공지의 무릉 지리가 맞물려 산길 마을을 해치지 않는 진군로를 엽니다.'
+ },
+ {
+ id: 'ma-liang__gong-zhi',
+ unitIds: ['ma-liang', 'gong-zhi'],
+ title: '무릉 기록',
+ level: 30,
+ exp: 0,
+ description: '마량의 형주 기록과 공지의 현지 경험이 더해져 험한 길의 위험을 미리 읽어냅니다.'
+ },
+ {
+ id: 'yi-ji__gong-zhi',
+ unitIds: ['yi-ji', 'gong-zhi'],
+ 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);
@@ -9752,6 +10295,7 @@ export const twentyFirstBattleBonds: BattleBond[] = [...twentiethBattleBonds].ma
export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds].map(cloneBattleBondForScenario);
export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario);
export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario);
+export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -10612,6 +11156,62 @@ function createTwentyFourthBattleTerrain(): TerrainType[][] {
);
}
+function createTwentyFifthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 34 }, (_, y) =>
+ Array.from({ length: 50 }, (_, x): TerrainType => {
+ if (x <= 3 && y >= 27) {
+ return 'camp';
+ }
+ if ((x >= 43 && x <= 48 && y >= 11 && y <= 17) || (x >= 40 && x <= 45 && y >= 8 && y <= 12)) {
+ return 'fort';
+ }
+ if ((x >= 28 && x <= 30 && y >= 25 && y <= 27) || (x >= 17 && x <= 19 && y >= 18 && y <= 20) || (x >= 37 && x <= 39 && y >= 23 && y <= 25)) {
+ return 'village';
+ }
+ if (
+ (y >= 28 && x >= 3 && x <= 13) ||
+ (x >= 12 && x <= 16 && y >= 22 && y <= 28) ||
+ (x >= 16 && x <= 23 && y >= 20 && y <= 22) ||
+ (x >= 23 && x <= 31 && y >= 23 && y <= 26) ||
+ (x >= 31 && x <= 39 && y >= 20 && y <= 23) ||
+ (x >= 38 && x <= 44 && y >= 16 && y <= 20) ||
+ (x >= 42 && x <= 47 && y >= 12 && y <= 16)
+ ) {
+ return 'road';
+ }
+ if ((x === 9 || x === 10) && y >= 0 && y <= 22) {
+ return 'river';
+ }
+ if ((x === 33 || x === 34) && y >= 1 && y <= 19) {
+ return 'river';
+ }
+ if ((x === 27 || x === 28) && y >= 27 && y <= 33) {
+ return 'river';
+ }
+ if (
+ (x <= 7 && y <= 14) ||
+ (x >= 13 && x <= 23 && y >= 4 && y <= 13) ||
+ (x >= 18 && x <= 31 && y >= 14 && y <= 19) ||
+ (x >= 35 && x <= 48 && y >= 21 && y <= 32)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 12 && y >= 15 && y <= 25) ||
+ (x >= 21 && x <= 33 && y >= 6 && y <= 15) ||
+ (x >= 36 && x <= 49 && y <= 10) ||
+ (x >= 41 && x <= 49 && y >= 18 && y <= 24)
+ ) {
+ return 'hill';
+ }
+ if ((x <= 2 && y <= 25) || (x >= 48 && y <= 20) || (x >= 30 && x <= 32 && y >= 0 && y <= 9)) {
+ 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 701be35..9ab4985 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -31,6 +31,7 @@ const unitTexture: Record = {
'zhuge-liang': 'unit-liu-bei',
'ma-liang': 'unit-liu-bei',
'yi-ji': 'unit-liu-bei',
+ 'gong-zhi': 'unit-guan-yu',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -276,7 +277,26 @@ const unitTexture: Record = {
'guiyang-guard-b': 'unit-rebel',
'guiyang-strategist-a': 'unit-rebel-archer',
'guiyang-strategist-b': 'unit-rebel-archer',
- 'guiyang-leader-zhao-fan': 'unit-rebel-leader'
+ 'guiyang-leader-zhao-fan': 'unit-rebel-leader',
+ 'wuling-scout-a': 'unit-rebel',
+ 'wuling-scout-b': 'unit-rebel',
+ 'wuling-scout-c': 'unit-rebel',
+ 'wuling-bandit-a': 'unit-rebel',
+ 'wuling-bandit-b': 'unit-rebel',
+ 'wuling-bandit-c': 'unit-rebel',
+ 'wuling-infantry-a': 'unit-rebel',
+ 'wuling-infantry-b': 'unit-rebel',
+ 'wuling-infantry-c': 'unit-rebel',
+ 'wuling-infantry-d': 'unit-rebel',
+ 'wuling-archer-a': 'unit-rebel-archer',
+ 'wuling-archer-b': 'unit-rebel-archer',
+ 'wuling-archer-c': 'unit-rebel-archer',
+ 'wuling-cavalry-a': 'unit-rebel-cavalry',
+ 'wuling-cavalry-b': 'unit-rebel-cavalry',
+ 'wuling-guard-a': 'unit-rebel',
+ 'wuling-guard-b': 'unit-rebel',
+ 'wuling-strategist-a': 'unit-rebel-archer',
+ 'wuling-leader-jin-xuan': 'unit-rebel-leader'
};
const unitTextureByClass: Partial> = {
@@ -699,7 +719,8 @@ const unitStrategyIds: Record = {
'sun-qian': ['aid', 'encourage', 'fireTactic'],
'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
'ma-liang': ['aid', 'encourage', 'fireTactic'],
- 'yi-ji': ['aid', 'encourage']
+ 'yi-ji': ['aid', 'encourage'],
+ 'gong-zhi': ['encourage']
};
const initialItemStocks: Record> = {
@@ -712,7 +733,8 @@ const initialItemStocks: Record> = {
'zhao-yun': { bean: 1, wine: 1 },
'zhuge-liang': { bean: 1, wine: 1 },
'ma-liang': { bean: 1, salve: 1 },
- 'yi-ji': { bean: 2, salve: 1 }
+ 'yi-ji': { bean: 2, salve: 1 },
+ 'gong-zhi': { bean: 2 }
};
const attackRangeByClass: Partial> = {
@@ -1010,7 +1032,26 @@ const enemyAiByUnitId: Record = {
'guiyang-guard-b': 'guard',
'guiyang-strategist-a': 'hold',
'guiyang-strategist-b': 'hold',
- 'guiyang-leader-zhao-fan': 'guard'
+ 'guiyang-leader-zhao-fan': 'guard',
+ 'wuling-scout-a': 'aggressive',
+ 'wuling-scout-b': 'aggressive',
+ 'wuling-scout-c': 'aggressive',
+ 'wuling-bandit-a': 'aggressive',
+ 'wuling-bandit-b': 'aggressive',
+ 'wuling-bandit-c': 'aggressive',
+ 'wuling-infantry-a': 'guard',
+ 'wuling-infantry-b': 'guard',
+ 'wuling-infantry-c': 'guard',
+ 'wuling-infantry-d': 'guard',
+ 'wuling-archer-a': 'hold',
+ 'wuling-archer-b': 'hold',
+ 'wuling-archer-c': 'hold',
+ 'wuling-cavalry-a': 'aggressive',
+ 'wuling-cavalry-b': 'aggressive',
+ 'wuling-guard-a': 'guard',
+ 'wuling-guard-b': 'guard',
+ 'wuling-strategist-a': 'hold',
+ 'wuling-leader-jin-xuan': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 6f36016..76d3e42 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -20,6 +20,7 @@ import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg';
import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg';
+import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg';
import twentyFourthBattleMapUrl from '../../assets/images/battle/twenty-fourth-battle-map.svg';
import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
@@ -106,6 +107,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl);
this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl);
this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl);
+ this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl);
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 ecfddca..8ef74eb 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -15,6 +15,8 @@ import {
jingzhouRecruitUnits,
liuBiaoRecruitBonds,
liuBiaoRecruitUnits,
+ wulingRecruitBonds,
+ wulingRecruitUnits,
xuzhouRecruitBonds,
xuzhouRecruitUnits,
zhugeRecruitBonds,
@@ -299,7 +301,11 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '형주·익주 확보',
period: '근본을 세우다',
description: '형주와 익주를 기반으로 삼아 유비군이 유랑군에서 국가의 뿌리로 바뀌는 장입니다.',
- battleIds: ['twenty-third-battle-jingzhou-south-entry', 'twenty-fourth-battle-guiyang-persuasion'],
+ battleIds: [
+ 'twenty-third-battle-jingzhou-south-entry',
+ 'twenty-fourth-battle-guiyang-persuasion',
+ 'twenty-fifth-battle-wuling-mountain-road'
+ ],
nextHints: ['형주 방위', '익주 진입', '장수 대거 합류']
},
{
@@ -336,7 +342,8 @@ const campBattleIds = {
twentyFirst: 'twenty-first-battle-red-cliffs-vanguard',
twentySecond: 'twenty-second-battle-red-cliffs-fire',
twentyThird: 'twenty-third-battle-jingzhou-south-entry',
- twentyFourth: 'twenty-fourth-battle-guiyang-persuasion'
+ twentyFourth: 'twenty-fourth-battle-guiyang-persuasion',
+ twentyFifth: 'twenty-fifth-battle-wuling-mountain-road'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -599,6 +606,18 @@ const sortieRulesByBattleId: Partial