diff --git a/docs/roadmap.md b/docs/roadmap.md
index 9370d8d..3506bcf 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -47,16 +47,17 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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
+- 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
- 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-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
+- 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
## Next Steps
-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
+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
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 f4a7c8c..cca2b51 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 !== 25 ||
+ progressTabState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postBowangProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postChangbanProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postJiangdongProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postRedCliffsProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postFireAttackProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postJingzhouProgressState.campaignProgress?.totalKnown !== 26 ||
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 !== 25 ||
+ postGuiyangProgressState.campaignProgress?.totalKnown !== 26 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
@@ -2872,15 +2872,164 @@ try {
if (
postWulingProgressState?.activeTab !== 'progress' ||
postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
- postWulingProgressState.campaignProgress?.totalKnown !== 25 ||
+ postWulingProgressState.campaignProgress?.totalKnown !== 26 ||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
- postWulingProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
+ 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.mouse.click(1120, 38);
+ await page.waitForTimeout(180);
+ const changshaSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !changshaSortieState?.sortieVisible ||
+ !changshaSortieState.sortiePlan?.objectiveLine?.includes('장사 노장 대면전') ||
+ !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'gong-zhi' && unit.recruited && unit.recommended) ||
+ !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'yi-ji' && unit.recruited && unit.recommended) ||
+ !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhuge-liang' && unit.recruited && unit.recommended) ||
+ !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'guan-yu' && unit.recommended) ||
+ !changshaSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
+ changshaSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong') ||
+ changshaSortieState.sortieRoster?.some((unit) => unit.id === 'wei-yan') ||
+ changshaSortieState.sortieRoster?.length < 11 ||
+ changshaSortieState.sortiePlan?.maxCount !== 6
+ ) {
+ throw new Error(`Expected twenty-fifth camp sortie prep to target Changsha with Gong Zhi recommended and Huang Zhong/Wei Yan absent before victory: ${JSON.stringify(changshaSortieState)}`);
+ }
+ assertSortieTacticalRoster(changshaSortieState, [
+ 'liu-bei',
+ 'guan-yu',
+ 'zhang-fei',
+ 'jian-yong',
+ 'mi-zhu',
+ 'sun-qian',
+ 'zhao-yun',
+ 'zhuge-liang',
+ 'ma-liang',
+ 'yi-ji',
+ 'gong-zhi'
+ ]);
+
+ const changshaPriorityUnits = ['gong-zhi', 'yi-ji', 'zhuge-liang', 'guan-yu', 'zhao-yun'];
+ for (const unitId of changshaPriorityUnits) {
+ 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' && !changshaPriorityUnits.includes(unit.id)
+ );
+ if (removable) {
+ await clickSortieRosterUnit(page, removable.id);
+ }
+ await clickSortieRosterUnit(page, unitId);
+ }
+ }
+
+ const changshaSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ !changshaPriorityUnits.every((unitId) =>
+ changshaSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
+ ) ||
+ changshaSortieReadyState.sortiePlan?.selectedCount !== 6 ||
+ changshaSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
+ ) {
+ throw new Error(`Expected Changsha sortie to deploy Gong Zhi and the veteran-audience priority officers while preserving six-officer pressure: ${JSON.stringify(changshaSortieReadyState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-changsha-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-changsha-story.png', fullPage: true });
+
+ for (let i = 0; i < 50; i += 1) {
+ const enteredTwentySixthBattle = await page.evaluate(() => {
+ const state = window.__HEROS_DEBUG__?.battle();
+ return state?.scene === 'BattleScene' && state?.battleId === 'twenty-sixth-battle-changsha-veteran';
+ });
+ if (enteredTwentySixthBattle) {
+ 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-sixth-battle-changsha-veteran' && state?.battleOutcome === null && state?.phase === 'idle';
+ });
+ await page.screenshot({ path: 'dist/verification-twenty-sixth-battle.png', fullPage: true });
+
+ const twentySixthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
+ const twentySixthEnemies = twentySixthBattleState.units.filter((unit) => unit.faction === 'enemy');
+ const twentySixthAllies = twentySixthBattleState.units.filter((unit) => unit.faction === 'ally');
+ const twentySixthEnemyBehaviors = new Set(twentySixthEnemies.map((unit) => unit.ai));
+ if (
+ twentySixthBattleState.camera?.mapWidth !== 52 ||
+ twentySixthBattleState.camera?.mapHeight !== 34 ||
+ twentySixthBattleState.victoryConditionLabel !== '한현 퇴각' ||
+ twentySixthEnemies.length < 18 ||
+ !twentySixthEnemyBehaviors.has('aggressive') ||
+ !twentySixthEnemyBehaviors.has('guard') ||
+ !twentySixthEnemyBehaviors.has('hold') ||
+ !twentySixthEnemies.some((unit) => unit.id === 'changsha-leader-han-xuan') ||
+ !twentySixthEnemies.some((unit) => unit.id === 'changsha-veteran-huang-zhong') ||
+ !twentySixthEnemies.some((unit) => unit.id === 'changsha-officer-wei-yan') ||
+ !twentySixthAllies.some((unit) => unit.id === 'gong-zhi') ||
+ !twentySixthAllies.some((unit) => unit.id === 'yi-ji') ||
+ twentySixthAllies.some((unit) => unit.id === 'huang-zhong') ||
+ twentySixthAllies.some((unit) => unit.id === 'wei-yan')
+ ) {
+ throw new Error(`Expected twenty-sixth battle to use Changsha map, Han Xuan objective, Huang Zhong/Wei Yan as enemy officers, mixed AI, and no Changsha recruits before victory: ${JSON.stringify(twentySixthBattleState)}`);
+ }
+
+ 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 twentySixthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ twentySixthCampState?.campBattleId !== 'twenty-sixth-battle-changsha-veteran' ||
+ twentySixthCampState.campTitle !== '장사 노장 대면 후 군영' ||
+ twentySixthCampState.availableDialogueIds?.length !== 4 ||
+ !twentySixthCampState.availableDialogueIds.every((id) => id.endsWith('changsha')) ||
+ twentySixthCampState.availableVisitIds?.length !== 2 ||
+ !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'huang-zhong') ||
+ !twentySixthCampState.campaign?.roster?.some((unit) => unit.id === 'wei-yan') ||
+ !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited) ||
+ !twentySixthCampState.sortieRoster?.some((unit) => unit.id === 'wei-yan' && unit.recruited) ||
+ twentySixthCampState.rosterCollection?.total < 13
+ ) {
+ throw new Error(`Expected twenty-sixth camp to recruit Huang Zhong and Wei Yan and expose Changsha dialogue/visit sets: ${JSON.stringify(twentySixthCampState)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-changsha-camp.png', fullPage: true });
+
+ await page.mouse.click(966, 38);
+ await page.waitForTimeout(180);
+ const postChangshaProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
+ if (
+ postChangshaProgressState?.activeTab !== 'progress' ||
+ postChangshaProgressState.campaignProgress?.completedKnown !== 26 ||
+ postChangshaProgressState.campaignProgress?.totalKnown !== 26 ||
+ postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
+ postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' ||
+ 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)}`);
+ }
+ await page.screenshot({ path: 'dist/verification-post-changsha-progress.png', fullPage: true });
+
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -2892,7 +3041,7 @@ try {
return activeScenes.includes('CampScene');
});
- 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}`);
+ 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}`);
} finally {
await browser?.close();
if (serverProcess && !serverProcess.killed) {
diff --git a/src/assets/images/battle/twenty-sixth-battle-map.svg b/src/assets/images/battle/twenty-sixth-battle-map.svg
new file mode 100644
index 0000000..512fc56
--- /dev/null
+++ b/src/assets/images/battle/twenty-sixth-battle-map.svg
@@ -0,0 +1,74 @@
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index 7711908..876ca45 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -59,6 +59,10 @@ import {
twentyFifthBattleMap,
twentyFifthBattleUnits,
twentyFifthBattleVictoryPages,
+ twentySixthBattleBonds,
+ twentySixthBattleMap,
+ twentySixthBattleUnits,
+ twentySixthBattleVictoryPages,
twentiethBattleBonds,
twentiethBattleMap,
twentiethBattleUnits,
@@ -130,7 +134,8 @@ export type BattleScenarioId =
| 'twenty-second-battle-red-cliffs-fire'
| 'twenty-third-battle-jingzhou-south-entry'
| 'twenty-fourth-battle-guiyang-persuasion'
- | 'twenty-fifth-battle-wuling-mountain-road';
+ | 'twenty-fifth-battle-wuling-mountain-road'
+ | 'twenty-sixth-battle-changsha-veteran';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1565,6 +1570,69 @@ export const twentyFifthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
+export const twentySixthBattleScenario: BattleScenarioDefinition = {
+ id: 'twenty-sixth-battle-changsha-veteran',
+ title: '장사 노장 대면전',
+ victoryConditionLabel: '한현 퇴각',
+ defeatConditionLabel: '유비 또는 제갈량 퇴각',
+ openingObjectiveLines: [
+ '무릉 산길을 연 유비군은 장사로 향합니다. 장사에는 노장 황충과 의심 많은 태수 한현이 버티고 있습니다.',
+ '이번 전투는 황충을 욕보이지 않고 한현의 의심을 꺾어야 합니다. 마을을 안정시키고 성문 전열을 흔들어 장사 내부의 길을 여십시오.',
+ '유비와 제갈량을 지키며 한현을 물러나게 하면 황충과 위연이 군영에 합류합니다.'
+ ],
+ map: twentySixthBattleMap,
+ units: twentySixthBattleUnits,
+ bonds: twentySixthBattleBonds,
+ mapTextureKey: 'battle-map-twenty-sixth',
+ leaderUnitId: 'changsha-leader-han-xuan',
+ quickVictoryTurnLimit: 35,
+ baseVictoryGold: 3560,
+ objectives: [
+ {
+ id: 'leader',
+ kind: 'defeat-leader',
+ label: '한현 퇴각',
+ rewardGold: 2320,
+ unitId: 'changsha-leader-han-xuan'
+ },
+ {
+ id: 'liu-bei',
+ kind: 'keep-unit-alive',
+ label: '유비 생존',
+ rewardGold: 660,
+ unitId: 'liu-bei'
+ },
+ {
+ id: 'zhuge-liang',
+ kind: 'keep-unit-alive',
+ label: '제갈량 생존',
+ rewardGold: 940,
+ unitId: 'zhuge-liang'
+ },
+ {
+ id: 'changsha-village',
+ kind: 'secure-terrain',
+ label: '장사 마을 안정',
+ rewardGold: 1120,
+ terrain: 'village'
+ },
+ {
+ id: 'quick',
+ kind: 'quick-victory',
+ label: '35턴 이내 승리',
+ rewardGold: 880,
+ maxTurn: 35
+ }
+ ],
+ defeatConditions: [
+ { kind: 'unit-defeated', unitId: 'liu-bei' },
+ { kind: 'unit-defeated', unitId: 'zhuge-liang' }
+ ],
+ itemRewards: ['콩 12', '상처약 8', '탁주 5', '장사 사절 문서 +1', '황충 합류', '위연 합류'],
+ victoryPages: twentySixthBattleVictoryPages,
+ nextCampScene: 'CampScene'
+};
+
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record = {
@@ -1592,7 +1660,8 @@ export const battleScenarios: Record
'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario,
'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario,
'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario,
- 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario
+ 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario,
+ 'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 61ccfef..eb41a03 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -22,6 +22,7 @@ import {
twentyThirdBattleScenario,
twentyFifthBattleScenario,
twentyFourthBattleScenario,
+ twentySixthBattleScenario,
twentiethBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
@@ -71,6 +72,8 @@ import {
twentyFifthBattleVictoryPages,
twentyFourthBattleIntroPages,
twentyFourthBattleVictoryPages,
+ twentySixthBattleIntroPages,
+ twentySixthBattleVictoryPages,
twentiethBattleIntroPages,
twentiethBattleVictoryPages,
twelfthBattleIntroPages,
@@ -347,13 +350,24 @@ const sortieFlows: Record = {
},
[twentyFifthBattleScenario.id]: {
afterBattleId: twentyFifthBattleScenario.id,
- eyebrow: '다음 장 준비',
- title: '장사 노장 대면 준비',
+ eyebrow: '다음 전장',
+ title: twentySixthBattleScenario.title,
description:
- '무릉 산길이 열리고 공지가 합류했습니다. 다음은 장사의 노장 황충과 태수 한현을 마주하며 형주 남부 확보의 큰 고비를 넘겨야 합니다.',
- rewardHint: '다음 장: 장사 노장 대면 준비 중',
- pages: twentyFifthBattleVictoryPages,
- unavailableNotice: '장사 노장 대면 장은 다음 작업에서 이어집니다. 군영에서 공지와 대화하고 황충을 맞을 준비를 하십시오.'
+ '무릉 산길이 열리고 공지가 합류했습니다. 이제 장사의 노장 황충과 의심 많은 한현을 마주하며 형주 남부 확보의 큰 고비를 넘어야 합니다.',
+ rewardHint: `예상 보상: ${twentySixthBattleScenario.title} 개방 / 황충·위연 합류`,
+ nextBattleId: twentySixthBattleScenario.id,
+ campaignStep: 'twenty-sixth-battle',
+ pages: [...twentyFifthBattleVictoryPages, ...twentySixthBattleIntroPages]
+ },
+ [twentySixthBattleScenario.id]: {
+ afterBattleId: twentySixthBattleScenario.id,
+ eyebrow: '다음 장 준비',
+ title: '익주 진입 준비',
+ description:
+ '장사까지 열리며 형주 남부의 뿌리가 단단해졌습니다. 다음은 익주로 향하는 더 긴 정치와 전장의 길을 준비해야 합니다.',
+ rewardHint: '다음 장: 익주 진입 준비 중',
+ pages: twentySixthBattleVictoryPages,
+ unavailableNotice: '익주 진입 장은 다음 작업에서 이어집니다. 군영에서 황충과 위연을 살피고 다음 대장정의 출전 후보를 정비하십시오.'
}
};
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index 6dbfbb8..8e05d6f 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -1634,6 +1634,76 @@ export const twentyFifthBattleVictoryPages: StoryPage[] = [
}
];
+export const twentySixthBattleIntroPages: StoryPage[] = [
+ {
+ id: 'twenty-sixth-changsha-old-general',
+ bgm: 'story-dark',
+ chapter: '장사의 노장',
+ background: 'story-three-heroes',
+ speaker: '공지',
+ text: '장사에는 황충이라는 노장이 있습니다. 나이는 들었으나 활은 여전히 매섭고, 성 안 병사들은 그 이름을 믿고 버팁니다.'
+ },
+ {
+ id: 'twenty-sixth-han-xuan-suspicion',
+ bgm: 'battle-prep',
+ chapter: '의심 많은 태수',
+ background: 'story-militia',
+ speaker: '이적',
+ text: '한현은 황충의 명망도 의심한다고 합니다. 성을 무너뜨리기보다 황충의 체면을 지키면, 장사 사람들의 마음도 흔들릴 것입니다.'
+ },
+ {
+ id: 'twenty-sixth-wei-yan-rumor',
+ bgm: 'battle-prep',
+ chapter: '성문 안의 불씨',
+ background: 'story-liu-bei',
+ speaker: '마량',
+ text: '위연이라는 장수는 성 안의 부당함을 참지 못한다고 들었습니다. 백성을 해치지 않고 한현의 의심을 꺾으면, 성 안에서 길이 열릴 수 있습니다.'
+ },
+ {
+ id: 'twenty-sixth-changsha-sortie',
+ bgm: 'battle-prep',
+ chapter: '장사 노장 대면전',
+ background: 'story-sortie',
+ speaker: '유비',
+ portrait: 'liuBei',
+ text: '오늘은 노장의 이름을 꺾으러 가는 것이 아니오. 예를 갖추고 한현의 의심을 물리쳐, 장사의 사람을 얻어야 하오.'
+ }
+];
+
+export const twentySixthBattleVictoryPages: StoryPage[] = [
+ {
+ id: 'twenty-sixth-victory-changsha-open',
+ bgm: 'militia-theme',
+ chapter: '장사 성문이 열리다',
+ background: 'story-sortie',
+ text: '한현의 의심이 전장 위에서 무너지자, 장사 성문은 크게 흔들렸습니다. 유비군은 황충의 명예를 욕보이지 않고 병사들에게 길을 열었습니다.'
+ },
+ {
+ id: 'twenty-sixth-huang-zhong-joins',
+ bgm: 'battle-prep',
+ chapter: '황충의 합류',
+ background: 'story-militia',
+ speaker: '황충',
+ text: '늙은 몸이라 하나, 아직 활은 뜻을 향해 날아갈 수 있습니다. 장사를 예로 대하신 주공의 군에 제 활을 맡기겠습니다.'
+ },
+ {
+ id: 'twenty-sixth-wei-yan-joins',
+ bgm: 'battle-prep',
+ chapter: '위연의 결심',
+ background: 'story-liu-bei',
+ speaker: '위연',
+ text: '의심과 두려움으로 닫힌 성문을 더는 따를 수 없습니다. 제 창은 닫힌 문을 여는 데 쓰겠습니다.'
+ },
+ {
+ id: 'twenty-sixth-next-yi-province',
+ bgm: 'story-dark',
+ chapter: '형주 남부의 끝',
+ background: 'story-three-heroes',
+ speaker: '제갈량',
+ text: '장사까지 열리면 형주 남부의 뿌리가 굳어집니다. 이제 익주로 이어질 더 큰 길을 바라볼 때가 다가옵니다.'
+ }
+];
+
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1929,6 +1999,12 @@ export const twentyFifthBattleMap: BattleMap = {
terrain: createTwentyFifthBattleTerrain()
};
+export const twentySixthBattleMap: BattleMap = {
+ width: 52,
+ height: 34,
+ terrain: createTwentySixthBattleTerrain()
+};
+
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -3957,6 +4033,51 @@ export const wulingRecruitUnits: UnitData[] = [
}
];
+export const changshaRecruitUnits: UnitData[] = [
+ {
+ id: 'huang-zhong',
+ name: '황충',
+ faction: 'ally',
+ className: '노장 궁기병',
+ classKey: 'archer',
+ level: 15,
+ exp: 0,
+ hp: 44,
+ maxHp: 44,
+ attack: 15,
+ move: 4,
+ stats: { might: 88, intelligence: 72, leadership: 86, agility: 78, luck: 76 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 3, exp: 12 },
+ armor: { itemId: 'lamellar-armor', level: 2, exp: 14 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 8,
+ y: 23
+ },
+ {
+ id: 'wei-yan',
+ name: '위연',
+ faction: 'ally',
+ className: '장사 돌격장',
+ classKey: 'infantry',
+ level: 15,
+ exp: 0,
+ hp: 48,
+ maxHp: 48,
+ attack: 16,
+ move: 4,
+ stats: { might: 90, intelligence: 64, leadership: 82, agility: 82, luck: 70 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 3, exp: 10 },
+ armor: { itemId: 'reinforced-lamellar', level: 2, exp: 10 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 9,
+ y: 24
+ }
+];
+
const eighthBattleAllyPositions: Record = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -10058,6 +10179,411 @@ export const twentyFifthBattleUnits: UnitData[] = [
}
];
+const twentySixthBattleAllyPositions: Record = {
+ 'liu-bei': { x: 4, y: 27 },
+ 'guan-yu': { x: 6, y: 25 },
+ 'zhang-fei': { x: 6, y: 29 },
+ 'jian-yong': { x: 3, y: 30 },
+ 'mi-zhu': { x: 4, y: 31 },
+ 'sun-qian': { x: 8, y: 28 },
+ 'zhao-yun': { x: 8, y: 25 },
+ 'zhuge-liang': { x: 5, y: 27 },
+ 'ma-liang': { x: 7, y: 30 },
+ 'yi-ji': { x: 9, y: 27 },
+ 'gong-zhi': { x: 9, y: 30 }
+};
+
+export const twentySixthBattleUnits: UnitData[] = [
+ ...[
+ ...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
+ ...xuzhouRecruitUnits,
+ ...caoBreakRecruitUnits,
+ ...liuBiaoRecruitUnits,
+ ...zhugeRecruitUnits,
+ ...jingzhouRecruitUnits,
+ ...guiyangRecruitUnits,
+ ...wulingRecruitUnits
+ ].map((unit) => placeScenarioUnit(unit, twentySixthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
+ {
+ id: 'changsha-scout-a',
+ name: '장사 척후',
+ faction: 'enemy',
+ className: '장사 척후',
+ classKey: 'bandit',
+ level: 37,
+ exp: 56,
+ hp: 100,
+ maxHp: 100,
+ attack: 38,
+ move: 4,
+ stats: { might: 124, intelligence: 90, leadership: 100, agility: 118, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 36 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 22 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 14,
+ y: 25
+ },
+ {
+ id: 'changsha-scout-b',
+ name: '장사 척후',
+ faction: 'enemy',
+ className: '서문 척후',
+ classKey: 'bandit',
+ level: 37,
+ exp: 56,
+ hp: 100,
+ maxHp: 100,
+ attack: 38,
+ move: 4,
+ stats: { might: 124, intelligence: 90, leadership: 100, agility: 118, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 36 },
+ armor: { itemId: 'rebel-vest', level: 4, exp: 22 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 17,
+ y: 30
+ },
+ {
+ id: 'changsha-infantry-a',
+ name: '장사 보병',
+ faction: 'enemy',
+ className: '장사 보병',
+ classKey: 'yellowTurban',
+ level: 38,
+ exp: 58,
+ hp: 130,
+ maxHp: 130,
+ attack: 42,
+ move: 3,
+ stats: { might: 138, intelligence: 90, leadership: 116, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 50 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 34 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 23,
+ y: 24
+ },
+ {
+ id: 'changsha-infantry-b',
+ name: '장사 보병',
+ faction: 'enemy',
+ className: '마을 보병',
+ classKey: 'yellowTurban',
+ level: 38,
+ exp: 58,
+ hp: 130,
+ maxHp: 130,
+ attack: 42,
+ move: 3,
+ stats: { might: 138, intelligence: 90, leadership: 116, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 50 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 34 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 26,
+ y: 28
+ },
+ {
+ id: 'changsha-infantry-c',
+ name: '장사 보병',
+ faction: 'enemy',
+ className: '성문 보병',
+ classKey: 'yellowTurban',
+ level: 39,
+ exp: 60,
+ hp: 136,
+ maxHp: 136,
+ attack: 43,
+ move: 3,
+ stats: { might: 140, intelligence: 90, leadership: 118, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 54 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 38 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 34,
+ y: 20
+ },
+ {
+ id: 'changsha-infantry-d',
+ name: '장사 보병',
+ faction: 'enemy',
+ className: '성내 보병',
+ classKey: 'yellowTurban',
+ level: 39,
+ exp: 60,
+ hp: 136,
+ maxHp: 136,
+ attack: 43,
+ move: 3,
+ stats: { might: 140, intelligence: 90, leadership: 118, agility: 98, luck: 82 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 54 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 38 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 39,
+ y: 17
+ },
+ {
+ id: 'changsha-archer-a',
+ name: '장사 궁병',
+ faction: 'enemy',
+ className: '장사 궁병',
+ classKey: 'archer',
+ level: 38,
+ exp: 58,
+ hp: 98,
+ maxHp: 98,
+ attack: 39,
+ move: 3,
+ stats: { might: 96, intelligence: 98, leadership: 104, agility: 112, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 46 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 28 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 28,
+ y: 20
+ },
+ {
+ id: 'changsha-archer-b',
+ name: '장사 궁병',
+ faction: 'enemy',
+ className: '성루 궁병',
+ classKey: 'archer',
+ level: 38,
+ exp: 58,
+ hp: 98,
+ maxHp: 98,
+ attack: 39,
+ move: 3,
+ stats: { might: 96, intelligence: 98, leadership: 104, agility: 112, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 46 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 28 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 37,
+ y: 13
+ },
+ {
+ id: 'changsha-archer-c',
+ name: '장사 궁병',
+ faction: 'enemy',
+ className: '동문 궁병',
+ classKey: 'archer',
+ level: 39,
+ exp: 60,
+ hp: 102,
+ maxHp: 102,
+ attack: 40,
+ move: 3,
+ stats: { might: 98, intelligence: 100, leadership: 106, agility: 114, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 50 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 30 },
+ accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
+ },
+ x: 44,
+ y: 19
+ },
+ {
+ id: 'changsha-cavalry-a',
+ name: '장사 기병',
+ faction: 'enemy',
+ className: '장사 기병',
+ classKey: 'cavalry',
+ level: 39,
+ exp: 60,
+ hp: 138,
+ maxHp: 138,
+ attack: 44,
+ move: 5,
+ stats: { might: 140, intelligence: 90, leadership: 116, agility: 118, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 56 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 36 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 31,
+ y: 29
+ },
+ {
+ id: 'changsha-cavalry-b',
+ name: '장사 기병',
+ faction: 'enemy',
+ className: '동문 기병',
+ classKey: 'cavalry',
+ level: 39,
+ exp: 60,
+ hp: 138,
+ maxHp: 138,
+ attack: 44,
+ move: 5,
+ stats: { might: 140, intelligence: 90, leadership: 116, agility: 118, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 56 },
+ armor: { itemId: 'lamellar-armor', level: 4, exp: 36 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 42,
+ y: 28
+ },
+ {
+ id: 'changsha-guard-a',
+ name: '한현 친위대',
+ faction: 'enemy',
+ className: '장사 친위대',
+ classKey: 'yellowTurban',
+ level: 40,
+ exp: 62,
+ hp: 144,
+ maxHp: 144,
+ attack: 45,
+ move: 3,
+ stats: { might: 144, intelligence: 92, leadership: 122, agility: 100, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 62 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 42 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 45,
+ y: 15
+ },
+ {
+ id: 'changsha-guard-b',
+ name: '한현 친위대',
+ faction: 'enemy',
+ className: '장사 친위대',
+ classKey: 'yellowTurban',
+ level: 40,
+ exp: 62,
+ hp: 144,
+ maxHp: 144,
+ attack: 45,
+ move: 3,
+ stats: { might: 144, intelligence: 92, leadership: 122, agility: 100, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 4, exp: 62 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 42 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 47,
+ y: 18
+ },
+ {
+ id: 'changsha-strategist-a',
+ name: '장사 책사',
+ faction: 'enemy',
+ className: '장사 책사',
+ classKey: 'archer',
+ level: 39,
+ exp: 60,
+ hp: 102,
+ maxHp: 102,
+ attack: 40,
+ move: 3,
+ stats: { might: 90, intelligence: 116, leadership: 108, agility: 96, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 50 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 30 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 41,
+ y: 14
+ },
+ {
+ id: 'changsha-strategist-b',
+ name: '장사 책사',
+ faction: 'enemy',
+ className: '의심 많은 책사',
+ classKey: 'archer',
+ level: 39,
+ exp: 60,
+ hp: 102,
+ maxHp: 102,
+ attack: 40,
+ move: 3,
+ stats: { might: 90, intelligence: 116, leadership: 108, agility: 96, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 4, exp: 50 },
+ armor: { itemId: 'cloth-armor', level: 4, exp: 30 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 43,
+ y: 16
+ },
+ {
+ id: 'changsha-veteran-huang-zhong',
+ name: '황충',
+ faction: 'enemy',
+ className: '장사 노장',
+ classKey: 'archer',
+ level: 41,
+ exp: 66,
+ hp: 156,
+ maxHp: 156,
+ attack: 48,
+ move: 4,
+ stats: { might: 150, intelligence: 96, leadership: 128, agility: 108, luck: 88 },
+ equipment: {
+ weapon: { itemId: 'short-bow', level: 5, exp: 18 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 48 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 38,
+ y: 17
+ },
+ {
+ id: 'changsha-officer-wei-yan',
+ name: '위연',
+ faction: 'enemy',
+ className: '장사 돌격장',
+ classKey: 'yellowTurban',
+ level: 40,
+ exp: 64,
+ hp: 152,
+ maxHp: 152,
+ attack: 47,
+ move: 4,
+ stats: { might: 148, intelligence: 82, leadership: 120, agility: 112, luck: 80 },
+ equipment: {
+ weapon: { itemId: 'iron-spear', level: 5, exp: 12 },
+ armor: { itemId: 'reinforced-lamellar', level: 4, exp: 44 },
+ accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
+ },
+ x: 36,
+ y: 23
+ },
+ {
+ id: 'changsha-leader-han-xuan',
+ name: '한현',
+ faction: 'enemy',
+ className: '장사 태수',
+ classKey: 'rebelLeader',
+ level: 41,
+ exp: 66,
+ hp: 190,
+ maxHp: 190,
+ attack: 46,
+ move: 4,
+ stats: { might: 138, intelligence: 104, leadership: 130, agility: 100, luck: 84 },
+ equipment: {
+ weapon: { itemId: 'leader-axe', level: 5, exp: 10 },
+ armor: { itemId: 'reinforced-lamellar', level: 5, exp: 8 },
+ accessory: { itemId: 'war-manual', level: 1, exp: 0 }
+ },
+ x: 48,
+ y: 16
+ }
+];
+
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -10266,6 +10792,41 @@ export const wulingRecruitBonds: BattleBond[] = [
}
];
+export const changshaRecruitBonds: BattleBond[] = [
+ {
+ id: 'liu-bei__huang-zhong',
+ unitIds: ['liu-bei', 'huang-zhong'],
+ title: '노장을 예우하다',
+ level: 38,
+ exp: 0,
+ description: '유비의 예우와 황충의 노련한 활이 만나, 오래 버틴 장수의 마음을 군의 힘으로 바꿉니다.'
+ },
+ {
+ id: 'guan-yu__huang-zhong',
+ unitIds: ['guan-yu', 'huang-zhong'],
+ title: '무인의 존중',
+ level: 34,
+ exp: 0,
+ description: '관우와 황충은 서로의 무예와 체면을 인정하며 전열과 원거리 압박을 함께 세웁니다.'
+ },
+ {
+ id: 'liu-bei__wei-yan',
+ unitIds: ['liu-bei', 'wei-yan'],
+ title: '닫힌 문을 여는 창',
+ level: 32,
+ exp: 0,
+ description: '유비의 큰 뜻과 위연의 거친 돌파가 만나, 막힌 성문과 전열을 과감히 흔듭니다.'
+ },
+ {
+ id: 'huang-zhong__wei-yan',
+ unitIds: ['huang-zhong', 'wei-yan'],
+ 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);
@@ -10296,6 +10857,7 @@ export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds]
export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario);
export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario);
export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario);
+export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -11212,6 +11774,55 @@ function createTwentyFifthBattleTerrain(): TerrainType[][] {
);
}
+function createTwentySixthBattleTerrain(): TerrainType[][] {
+ return Array.from({ length: 34 }, (_, y) =>
+ Array.from({ length: 52 }, (_, x): TerrainType => {
+ if (x <= 3 && y >= 26) {
+ return 'camp';
+ }
+ if ((x >= 42 && x <= 50 && y >= 11 && y <= 20) || (x >= 37 && x <= 43 && y >= 8 && y <= 13)) {
+ return 'fort';
+ }
+ if ((x >= 21 && x <= 24 && y >= 26 && y <= 28) || (x >= 30 && x <= 32 && y >= 19 && y <= 21) || (x >= 15 && x <= 17 && y >= 21 && y <= 23)) {
+ return 'village';
+ }
+ if (
+ (y >= 27 && x >= 3 && x <= 18) ||
+ (x >= 18 && x <= 29 && y >= 23 && y <= 27) ||
+ (x >= 29 && x <= 38 && y >= 19 && y <= 23) ||
+ (x >= 37 && x <= 45 && y >= 15 && y <= 19) ||
+ (x >= 44 && x <= 49 && y >= 13 && y <= 17)
+ ) {
+ return 'road';
+ }
+ if ((x === 11 || x === 12) && y >= 0 && y <= 24) {
+ return 'river';
+ }
+ if ((x === 33 || x === 34) && y >= 22 && y <= 33) {
+ return 'river';
+ }
+ if (
+ (x >= 4 && x <= 14 && y >= 7 && y <= 18) ||
+ (x >= 18 && x <= 30 && y >= 8 && y <= 17) ||
+ (x >= 35 && x <= 47 && y >= 23 && y <= 33)
+ ) {
+ return 'forest';
+ }
+ if (
+ (x >= 0 && x <= 9 && y <= 10) ||
+ (x >= 21 && x <= 32 && y >= 1 && y <= 9) ||
+ (x >= 46 && x <= 51 && y >= 0 && y <= 10)
+ ) {
+ return 'hill';
+ }
+ if ((x <= 1 && y <= 25) || (x >= 50 && y >= 21) || (x >= 39 && x <= 41 && y >= 0 && y <= 7)) {
+ 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 9ab4985..e01ea40 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -32,6 +32,8 @@ const unitTexture: Record = {
'ma-liang': 'unit-liu-bei',
'yi-ji': 'unit-liu-bei',
'gong-zhi': 'unit-guan-yu',
+ 'huang-zhong': 'unit-rebel-archer',
+ 'wei-yan': 'unit-guan-yu',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -296,7 +298,25 @@ const unitTexture: Record = {
'wuling-guard-a': 'unit-rebel',
'wuling-guard-b': 'unit-rebel',
'wuling-strategist-a': 'unit-rebel-archer',
- 'wuling-leader-jin-xuan': 'unit-rebel-leader'
+ 'wuling-leader-jin-xuan': 'unit-rebel-leader',
+ 'changsha-scout-a': 'unit-rebel',
+ 'changsha-scout-b': 'unit-rebel',
+ 'changsha-infantry-a': 'unit-rebel',
+ 'changsha-infantry-b': 'unit-rebel',
+ 'changsha-infantry-c': 'unit-rebel',
+ 'changsha-infantry-d': 'unit-rebel',
+ 'changsha-archer-a': 'unit-rebel-archer',
+ 'changsha-archer-b': 'unit-rebel-archer',
+ 'changsha-archer-c': 'unit-rebel-archer',
+ 'changsha-cavalry-a': 'unit-rebel-cavalry',
+ 'changsha-cavalry-b': 'unit-rebel-cavalry',
+ 'changsha-guard-a': 'unit-rebel',
+ 'changsha-guard-b': 'unit-rebel',
+ 'changsha-strategist-a': 'unit-rebel-archer',
+ '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'
};
const unitTextureByClass: Partial> = {
@@ -720,7 +740,9 @@ const unitStrategyIds: Record = {
'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
'ma-liang': ['aid', 'encourage', 'fireTactic'],
'yi-ji': ['aid', 'encourage'],
- 'gong-zhi': ['encourage']
+ 'gong-zhi': ['encourage'],
+ 'huang-zhong': ['encourage'],
+ 'wei-yan': ['roar']
};
const initialItemStocks: Record> = {
@@ -734,7 +756,9 @@ const initialItemStocks: Record> = {
'zhuge-liang': { bean: 1, wine: 1 },
'ma-liang': { bean: 1, salve: 1 },
'yi-ji': { bean: 2, salve: 1 },
- 'gong-zhi': { bean: 2 }
+ 'gong-zhi': { bean: 2 },
+ 'huang-zhong': { bean: 1, wine: 1 },
+ 'wei-yan': { bean: 2 }
};
const attackRangeByClass: Partial> = {
@@ -1051,7 +1075,25 @@ const enemyAiByUnitId: Record = {
'wuling-guard-a': 'guard',
'wuling-guard-b': 'guard',
'wuling-strategist-a': 'hold',
- 'wuling-leader-jin-xuan': 'guard'
+ 'wuling-leader-jin-xuan': 'guard',
+ 'changsha-scout-a': 'aggressive',
+ 'changsha-scout-b': 'aggressive',
+ 'changsha-infantry-a': 'guard',
+ 'changsha-infantry-b': 'guard',
+ 'changsha-infantry-c': 'guard',
+ 'changsha-infantry-d': 'guard',
+ 'changsha-archer-a': 'hold',
+ 'changsha-archer-b': 'hold',
+ 'changsha-archer-c': 'hold',
+ 'changsha-cavalry-a': 'aggressive',
+ 'changsha-cavalry-b': 'aggressive',
+ 'changsha-guard-a': 'guard',
+ 'changsha-guard-b': 'guard',
+ 'changsha-strategist-a': 'hold',
+ 'changsha-strategist-b': 'hold',
+ 'changsha-veteran-huang-zhong': 'hold',
+ 'changsha-officer-wei-yan': 'aggressive',
+ 'changsha-leader-han-xuan': 'guard'
};
const defaultEnemyAiByClass: Partial> = {
diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts
index 76d3e42..d44ff93 100644
--- a/src/game/scenes/BootScene.ts
+++ b/src/game/scenes/BootScene.ts
@@ -22,6 +22,7 @@ import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-b
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 twentySixthBattleMapUrl from '../../assets/images/battle/twenty-sixth-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';
@@ -108,6 +109,7 @@ export class BootScene extends Phaser.Scene {
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('battle-map-twenty-sixth', twentySixthBattleMapUrl);
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 8ef74eb..6cee0b0 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -7,6 +7,8 @@ import { getSortieFlow } from '../data/campaignFlow';
import {
caoBreakRecruitBonds,
caoBreakRecruitUnits,
+ changshaRecruitBonds,
+ changshaRecruitUnits,
firstBattleBonds,
firstBattleUnits,
guiyangRecruitBonds,
@@ -304,7 +306,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
battleIds: [
'twenty-third-battle-jingzhou-south-entry',
'twenty-fourth-battle-guiyang-persuasion',
- 'twenty-fifth-battle-wuling-mountain-road'
+ 'twenty-fifth-battle-wuling-mountain-road',
+ 'twenty-sixth-battle-changsha-veteran'
],
nextHints: ['형주 방위', '익주 진입', '장수 대거 합류']
},
@@ -343,7 +346,8 @@ const campBattleIds = {
twentySecond: 'twenty-second-battle-red-cliffs-fire',
twentyThird: 'twenty-third-battle-jingzhou-south-entry',
twentyFourth: 'twenty-fourth-battle-guiyang-persuasion',
- twentyFifth: 'twenty-fifth-battle-wuling-mountain-road'
+ twentyFifth: 'twenty-fifth-battle-wuling-mountain-road',
+ twentySixth: 'twenty-sixth-battle-changsha-veteran'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -618,6 +622,18 @@ const sortieRulesByBattleId: Partial