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