Add Changsha veteran chapter

This commit is contained in:
2026-06-23 08:30:43 +09:00
parent 98876673a8
commit 2402e1c38f
11 changed files with 1217 additions and 31 deletions

View File

@@ -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-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-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-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 - 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 - 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 - 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 - 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 ## Next Steps
1. Continue Jing Province south from Wuling into Changsha acquisition, including Huang Zhong and Wei Yan recruitment pressure 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 Changsha, Yi Province, and Shu Han 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 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 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 5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending

View File

@@ -1736,7 +1736,7 @@ try {
if ( if (
progressTabState?.activeTab !== 'progress' || progressTabState?.activeTab !== 'progress' ||
progressTabState.campaignProgress?.completedKnown !== 17 || progressTabState.campaignProgress?.completedKnown !== 17 ||
progressTabState.campaignProgress?.totalKnown !== 25 || progressTabState.campaignProgress?.totalKnown !== 26 ||
progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' || progressTabState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' || progressTabState.campaignProgress?.latestBattleTitle !== '융중 방문로' ||
progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전' progressTabState.campaignProgress?.nextBattleTitle !== '박망파 매복전'
@@ -1888,7 +1888,7 @@ try {
if ( if (
postBowangProgressState?.activeTab !== 'progress' || postBowangProgressState?.activeTab !== 'progress' ||
postBowangProgressState.campaignProgress?.completedKnown !== 18 || postBowangProgressState.campaignProgress?.completedKnown !== 18 ||
postBowangProgressState.campaignProgress?.totalKnown !== 25 || postBowangProgressState.campaignProgress?.totalKnown !== 26 ||
postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postBowangProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' || postBowangProgressState.campaignProgress?.latestBattleTitle !== '박망파 매복전' ||
postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로' postBowangProgressState.campaignProgress?.nextBattleTitle !== '장판파 피난로'
@@ -2026,7 +2026,7 @@ try {
if ( if (
postChangbanProgressState?.activeTab !== 'progress' || postChangbanProgressState?.activeTab !== 'progress' ||
postChangbanProgressState.campaignProgress?.completedKnown !== 19 || postChangbanProgressState.campaignProgress?.completedKnown !== 19 ||
postChangbanProgressState.campaignProgress?.totalKnown !== 25 || postChangbanProgressState.campaignProgress?.totalKnown !== 26 ||
postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postChangbanProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' || postChangbanProgressState.campaignProgress?.latestBattleTitle !== '장판파 피난로' ||
postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로' postChangbanProgressState.campaignProgress?.nextBattleTitle !== '강동 사절로'
@@ -2164,7 +2164,7 @@ try {
if ( if (
postJiangdongProgressState?.activeTab !== 'progress' || postJiangdongProgressState?.activeTab !== 'progress' ||
postJiangdongProgressState.campaignProgress?.completedKnown !== 20 || postJiangdongProgressState.campaignProgress?.completedKnown !== 20 ||
postJiangdongProgressState.campaignProgress?.totalKnown !== 25 || postJiangdongProgressState.campaignProgress?.totalKnown !== 26 ||
postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postJiangdongProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' || postJiangdongProgressState.campaignProgress?.latestBattleTitle !== '강동 사절로' ||
postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전' postJiangdongProgressState.campaignProgress?.nextBattleTitle !== '적벽 전초전'
@@ -2304,7 +2304,7 @@ try {
if ( if (
postRedCliffsProgressState?.activeTab !== 'progress' || postRedCliffsProgressState?.activeTab !== 'progress' ||
postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 || postRedCliffsProgressState.campaignProgress?.completedKnown !== 21 ||
postRedCliffsProgressState.campaignProgress?.totalKnown !== 25 || postRedCliffsProgressState.campaignProgress?.totalKnown !== 26 ||
postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' || postRedCliffsProgressState.campaignProgress?.activeChapter?.title !== '적벽대전' ||
postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' || postRedCliffsProgressState.campaignProgress?.latestBattleTitle !== '적벽 전초전' ||
postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전' postRedCliffsProgressState.campaignProgress?.nextBattleTitle !== '적벽 화공전'
@@ -2444,7 +2444,7 @@ try {
if ( if (
postFireAttackProgressState?.activeTab !== 'progress' || postFireAttackProgressState?.activeTab !== 'progress' ||
postFireAttackProgressState.campaignProgress?.completedKnown !== 22 || postFireAttackProgressState.campaignProgress?.completedKnown !== 22 ||
postFireAttackProgressState.campaignProgress?.totalKnown !== 25 || postFireAttackProgressState.campaignProgress?.totalKnown !== 26 ||
postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postFireAttackProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' || postFireAttackProgressState.campaignProgress?.latestBattleTitle !== '적벽 화공전' ||
postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전' postFireAttackProgressState.campaignProgress?.nextBattleTitle !== '형주 남부 진입전'
@@ -2587,7 +2587,7 @@ try {
if ( if (
postJingzhouProgressState?.activeTab !== 'progress' || postJingzhouProgressState?.activeTab !== 'progress' ||
postJingzhouProgressState.campaignProgress?.completedKnown !== 23 || postJingzhouProgressState.campaignProgress?.completedKnown !== 23 ||
postJingzhouProgressState.campaignProgress?.totalKnown !== 25 || postJingzhouProgressState.campaignProgress?.totalKnown !== 26 ||
postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postJingzhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' || postJingzhouProgressState.campaignProgress?.latestBattleTitle !== '형주 남부 진입전' ||
postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전' postJingzhouProgressState.campaignProgress?.nextBattleTitle !== '계양 설득전'
@@ -2729,7 +2729,7 @@ try {
if ( if (
postGuiyangProgressState?.activeTab !== 'progress' || postGuiyangProgressState?.activeTab !== 'progress' ||
postGuiyangProgressState.campaignProgress?.completedKnown !== 24 || postGuiyangProgressState.campaignProgress?.completedKnown !== 24 ||
postGuiyangProgressState.campaignProgress?.totalKnown !== 25 || postGuiyangProgressState.campaignProgress?.totalKnown !== 26 ||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' || postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전' postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
@@ -2872,15 +2872,164 @@ try {
if ( if (
postWulingProgressState?.activeTab !== 'progress' || postWulingProgressState?.activeTab !== 'progress' ||
postWulingProgressState.campaignProgress?.completedKnown !== 25 || postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
postWulingProgressState.campaignProgress?.totalKnown !== 25 || postWulingProgressState.campaignProgress?.totalKnown !== 26 ||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' || postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' || 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)}`); 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.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.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
await page.waitForFunction(() => { await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? []; const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -2892,7 +3041,7 @@ try {
return activeScenes.includes('CampScene'); 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 { } finally {
await browser?.close(); await browser?.close();
if (serverProcess && !serverProcess.killed) { if (serverProcess && !serverProcess.killed) {

View File

@@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5200 3400" role="img" aria-label="Changsha veteran audience tactical battlefield">
<defs>
<linearGradient id="field" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#8d7a45"/>
<stop offset="0.42" stop-color="#aa955c"/>
<stop offset="1" stop-color="#736c43"/>
</linearGradient>
<linearGradient id="wall" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#8e7b55"/>
<stop offset="1" stop-color="#3d3829"/>
</linearGradient>
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#9bd7e0"/>
<stop offset="0.55" stop-color="#407f8e"/>
<stop offset="1" stop-color="#1f4658"/>
</linearGradient>
<pattern id="dust" width="96" height="96" patternUnits="userSpaceOnUse">
<path d="M12 22h22M58 31h26M28 68h36M8 84h18" stroke="#40331f" stroke-width="5" opacity=".18"/>
<path d="M74 11h14M44 43h16M78 75h10" stroke="#dec582" stroke-width="4" opacity=".22"/>
</pattern>
<pattern id="grid" width="100" height="100" patternUnits="userSpaceOnUse">
<path d="M100 0H0v100" fill="none" stroke="#14100b" stroke-width="6" opacity=".2"/>
</pattern>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="18" dy="26" stdDeviation="13" flood-color="#12100b" flood-opacity=".34"/>
</filter>
</defs>
<rect width="5200" height="3400" fill="url(#field)"/>
<rect width="5200" height="3400" fill="url(#dust)" opacity=".86"/>
<path d="M0 0 C360 210 520 470 470 820 C420 1180 220 1510 280 1940 C330 2320 190 2790 0 3190 Z" fill="#34422d" opacity=".78"/>
<path d="M2020 80 C2450 -20 2960 80 3240 370 C3040 720 2720 900 2280 880 C1940 860 1730 620 1710 340 C1710 210 1830 120 2020 80 Z" fill="#415033" opacity=".76"/>
<path d="M3560 2380 C4020 2250 4660 2320 5200 2690 L5200 3400 L3600 3400 C3440 3040 3380 2600 3560 2380 Z" fill="#38472e" opacity=".78"/>
<path d="M1120 -60 C1260 420 1160 820 1080 1230 C1000 1640 1100 1940 1240 2360" fill="none" stroke="url(#river)" stroke-width="130" opacity=".9"/>
<path d="M3360 3400 C3330 3100 3410 2880 3560 2660 C3700 2440 3760 2220 3700 2000" fill="none" stroke="url(#river)" stroke-width="118" opacity=".88"/>
<path d="M1120 -60 C1260 420 1160 820 1080 1230 C1000 1640 1100 1940 1240 2360" fill="none" stroke="#d9f1ef" stroke-width="18" opacity=".35"/>
<path d="M260 2840 C820 2800 1280 2740 1710 2520 C2130 2300 2490 2420 2910 2290 C3330 2160 3520 1870 3860 1720 C4170 1580 4490 1600 4890 1510" fill="none" stroke="#b28b4a" stroke-width="150" opacity=".78" stroke-linecap="round" filter="url(#shadow)"/>
<path d="M260 2840 C820 2800 1280 2740 1710 2520 C2130 2300 2490 2420 2910 2290 C3330 2160 3520 1870 3860 1720 C4170 1580 4490 1600 4890 1510" fill="none" stroke="#dbc27a" stroke-width="56" opacity=".48" stroke-linecap="round"/>
<path d="M1540 2300 C1850 2050 2040 1780 2210 1430 C2390 1070 2680 750 3030 520" fill="none" stroke="#a68043" stroke-width="92" opacity=".48" stroke-linecap="round"/>
<g opacity=".92">
<ellipse cx="1620" cy="2240" rx="190" ry="112" fill="#6f7e3a"/>
<rect x="1545" y="2165" width="126" height="104" fill="#8d5d2d"/>
<path d="M1522 2165 L1608 2092 L1698 2165 Z" fill="#d99235"/>
<rect x="1742" y="2190" width="98" height="82" fill="#8b6130"/>
<path d="M1728 2190 L1792 2132 L1858 2190 Z" fill="#e1a249"/>
</g>
<g opacity=".9">
<ellipse cx="3060" cy="2050" rx="180" ry="106" fill="#738441"/>
<rect x="2990" y="1978" width="120" height="96" fill="#865a2c"/>
<path d="M2968 1978 L3050 1908 L3132 1978 Z" fill="#d99634"/>
</g>
<g filter="url(#shadow)">
<path d="M4060 910 L4950 960 L5010 1960 L4040 2030 Z" fill="url(#wall)" opacity=".96"/>
<path d="M4200 1080 L4760 1115 L4810 1780 L4200 1830 Z" fill="#7b623b"/>
<path d="M4210 1080 L4480 875 L4770 1115 Z" fill="#b97935"/>
<rect x="4290" y="1220" width="96" height="270" fill="#28281f"/>
<rect x="4580" y="1230" width="92" height="260" fill="#28281f"/>
<path d="M3960 1430 C4180 1370 4410 1400 4620 1490 C4750 1545 4900 1565 5060 1520" fill="none" stroke="#c9ad68" stroke-width="36" opacity=".7"/>
</g>
<g fill="#2f4b2a" opacity=".78">
<circle cx="530" cy="610" r="88"/><circle cx="740" cy="940" r="76"/><circle cx="540" cy="1450" r="92"/>
<circle cx="1940" cy="940" r="82"/><circle cx="2260" cy="1160" r="90"/><circle cx="2760" cy="1420" r="84"/>
<circle cx="3920" cy="2640" r="106"/><circle cx="4380" cy="2860" r="96"/><circle cx="4780" cy="3080" r="84"/>
</g>
<rect width="5200" height="3400" fill="url(#grid)"/>
<rect x="18" y="18" width="5164" height="3364" fill="none" stroke="#2b2012" stroke-width="18" opacity=".58"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -59,6 +59,10 @@ import {
twentyFifthBattleMap, twentyFifthBattleMap,
twentyFifthBattleUnits, twentyFifthBattleUnits,
twentyFifthBattleVictoryPages, twentyFifthBattleVictoryPages,
twentySixthBattleBonds,
twentySixthBattleMap,
twentySixthBattleUnits,
twentySixthBattleVictoryPages,
twentiethBattleBonds, twentiethBattleBonds,
twentiethBattleMap, twentiethBattleMap,
twentiethBattleUnits, twentiethBattleUnits,
@@ -130,7 +134,8 @@ export type BattleScenarioId =
| 'twenty-second-battle-red-cliffs-fire' | 'twenty-second-battle-red-cliffs-fire'
| 'twenty-third-battle-jingzhou-south-entry' | 'twenty-third-battle-jingzhou-south-entry'
| 'twenty-fourth-battle-guiyang-persuasion' | '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'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1565,6 +1570,69 @@ export const twentyFifthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene' 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 defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = { export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -1592,7 +1660,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario, 'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario,
'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario, 'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario,
'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario, 'twenty-fourth-battle-guiyang-persuasion': twentyFourthBattleScenario,
'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario 'twenty-fifth-battle-wuling-mountain-road': twentyFifthBattleScenario,
'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario
}; };
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -22,6 +22,7 @@ import {
twentyThirdBattleScenario, twentyThirdBattleScenario,
twentyFifthBattleScenario, twentyFifthBattleScenario,
twentyFourthBattleScenario, twentyFourthBattleScenario,
twentySixthBattleScenario,
twentiethBattleScenario, twentiethBattleScenario,
twelfthBattleScenario, twelfthBattleScenario,
thirdBattleScenario, thirdBattleScenario,
@@ -71,6 +72,8 @@ import {
twentyFifthBattleVictoryPages, twentyFifthBattleVictoryPages,
twentyFourthBattleIntroPages, twentyFourthBattleIntroPages,
twentyFourthBattleVictoryPages, twentyFourthBattleVictoryPages,
twentySixthBattleIntroPages,
twentySixthBattleVictoryPages,
twentiethBattleIntroPages, twentiethBattleIntroPages,
twentiethBattleVictoryPages, twentiethBattleVictoryPages,
twelfthBattleIntroPages, twelfthBattleIntroPages,
@@ -347,13 +350,24 @@ const sortieFlows: Record<string, SortieFlow> = {
}, },
[twentyFifthBattleScenario.id]: { [twentyFifthBattleScenario.id]: {
afterBattleId: twentyFifthBattleScenario.id, afterBattleId: twentyFifthBattleScenario.id,
eyebrow: '다음 장 준비', eyebrow: '다음 장',
title: '장사 노장 대면 준비', title: twentySixthBattleScenario.title,
description: description:
'무릉 산길이 열리고 공지가 합류했습니다. 다음은 장사의 노장 황충과 태수 한현을 마주하며 형주 남부 확보의 큰 고비를 넘야 합니다.', '무릉 산길이 열리고 공지가 합류했습니다. 이제 장사의 노장 황충과 의심 많은 한현을 마주하며 형주 남부 확보의 큰 고비를 넘야 합니다.',
rewardHint: '다음 장: 장사 노장 대면 준비 중', rewardHint: `예상 보상: ${twentySixthBattleScenario.title} 개방 / 황충·위연 합류`,
pages: twentyFifthBattleVictoryPages, nextBattleId: twentySixthBattleScenario.id,
unavailableNotice: '장사 노장 대면 장은 다음 작업에서 이어집니다. 군영에서 공지와 대화하고 황충을 맞을 준비를 하십시오.' campaignStep: 'twenty-sixth-battle',
pages: [...twentyFifthBattleVictoryPages, ...twentySixthBattleIntroPages]
},
[twentySixthBattleScenario.id]: {
afterBattleId: twentySixthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '익주 진입 준비',
description:
'장사까지 열리며 형주 남부의 뿌리가 단단해졌습니다. 다음은 익주로 향하는 더 긴 정치와 전장의 길을 준비해야 합니다.',
rewardHint: '다음 장: 익주 진입 준비 중',
pages: twentySixthBattleVictoryPages,
unavailableNotice: '익주 진입 장은 다음 작업에서 이어집니다. 군영에서 황충과 위연을 살피고 다음 대장정의 출전 후보를 정비하십시오.'
} }
}; };

View File

@@ -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 = { export const firstBattleMap: BattleMap = {
width: 20, width: 20,
height: 18, height: 18,
@@ -1929,6 +1999,12 @@ export const twentyFifthBattleMap: BattleMap = {
terrain: createTwentyFifthBattleTerrain() terrain: createTwentyFifthBattleTerrain()
}; };
export const twentySixthBattleMap: BattleMap = {
width: 52,
height: 34,
terrain: createTwentySixthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [ export const firstBattleUnits: UnitData[] = [
{ {
id: 'liu-bei', 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<string, { x: number; y: number }> = { const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 }, 'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 }, 'guan-yu': { x: 3, y: 17 },
@@ -10058,6 +10179,411 @@ export const twentyFifthBattleUnits: UnitData[] = [
} }
]; ];
const twentySixthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'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[] = [ export const firstBattleBonds: BattleBond[] = [
{ {
id: 'liu-bei__guan-yu', 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 secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario); export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: 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 twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario);
export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario); export const twentyFourthBattleBonds: BattleBond[] = [...twentyThirdBattleBonds, ...jingzhouRecruitBonds].map(cloneBattleBondForScenario);
export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario); export const twentyFifthBattleBonds: BattleBond[] = [...twentyFourthBattleBonds, ...guiyangRecruitBonds].map(cloneBattleBondForScenario);
export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds, ...wulingRecruitBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] { function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) => 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 { function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return { return {
...cloneUnitForScenario(unit), ...cloneUnitForScenario(unit),

View File

@@ -32,6 +32,8 @@ const unitTexture: Record<string, string> = {
'ma-liang': 'unit-liu-bei', 'ma-liang': 'unit-liu-bei',
'yi-ji': 'unit-liu-bei', 'yi-ji': 'unit-liu-bei',
'gong-zhi': 'unit-guan-yu', 'gong-zhi': 'unit-guan-yu',
'huang-zhong': 'unit-rebel-archer',
'wei-yan': 'unit-guan-yu',
'rebel-a': 'unit-rebel', 'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel', 'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer', 'rebel-c': 'unit-rebel-archer',
@@ -296,7 +298,25 @@ const unitTexture: Record<string, string> = {
'wuling-guard-a': 'unit-rebel', 'wuling-guard-a': 'unit-rebel',
'wuling-guard-b': 'unit-rebel', 'wuling-guard-b': 'unit-rebel',
'wuling-strategist-a': 'unit-rebel-archer', '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<Record<UnitClassKey, string>> = { const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -720,7 +740,9 @@ const unitStrategyIds: Record<string, string[]> = {
'zhuge-liang': ['aid', 'encourage', 'fireTactic'], 'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
'ma-liang': ['aid', 'encourage', 'fireTactic'], 'ma-liang': ['aid', 'encourage', 'fireTactic'],
'yi-ji': ['aid', 'encourage'], 'yi-ji': ['aid', 'encourage'],
'gong-zhi': ['encourage'] 'gong-zhi': ['encourage'],
'huang-zhong': ['encourage'],
'wei-yan': ['roar']
}; };
const initialItemStocks: Record<string, Record<string, number>> = { const initialItemStocks: Record<string, Record<string, number>> = {
@@ -734,7 +756,9 @@ const initialItemStocks: Record<string, Record<string, number>> = {
'zhuge-liang': { bean: 1, wine: 1 }, 'zhuge-liang': { bean: 1, wine: 1 },
'ma-liang': { bean: 1, salve: 1 }, 'ma-liang': { bean: 1, salve: 1 },
'yi-ji': { bean: 2, 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<Record<UnitClassKey, number>> = { const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
@@ -1051,7 +1075,25 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'wuling-guard-a': 'guard', 'wuling-guard-a': 'guard',
'wuling-guard-b': 'guard', 'wuling-guard-b': 'guard',
'wuling-strategist-a': 'hold', '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<Record<UnitClassKey, EnemyAiBehavior>> = { const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -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 twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg';
import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-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 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 twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg'; import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; 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-third', twentyThirdBattleMapUrl);
this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl); this.load.image('battle-map-twenty-fourth', twentyFourthBattleMapUrl);
this.load.image('battle-map-twenty-fifth', twentyFifthBattleMapUrl); 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-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl); this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl); this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -7,6 +7,8 @@ import { getSortieFlow } from '../data/campaignFlow';
import { import {
caoBreakRecruitBonds, caoBreakRecruitBonds,
caoBreakRecruitUnits, caoBreakRecruitUnits,
changshaRecruitBonds,
changshaRecruitUnits,
firstBattleBonds, firstBattleBonds,
firstBattleUnits, firstBattleUnits,
guiyangRecruitBonds, guiyangRecruitBonds,
@@ -304,7 +306,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
battleIds: [ battleIds: [
'twenty-third-battle-jingzhou-south-entry', 'twenty-third-battle-jingzhou-south-entry',
'twenty-fourth-battle-guiyang-persuasion', 'twenty-fourth-battle-guiyang-persuasion',
'twenty-fifth-battle-wuling-mountain-road' 'twenty-fifth-battle-wuling-mountain-road',
'twenty-sixth-battle-changsha-veteran'
], ],
nextHints: ['형주 방위', '익주 진입', '장수 대거 합류'] nextHints: ['형주 방위', '익주 진입', '장수 대거 합류']
}, },
@@ -343,7 +346,8 @@ const campBattleIds = {
twentySecond: 'twenty-second-battle-red-cliffs-fire', twentySecond: 'twenty-second-battle-red-cliffs-fire',
twentyThird: 'twenty-third-battle-jingzhou-south-entry', twentyThird: 'twenty-third-battle-jingzhou-south-entry',
twentyFourth: 'twenty-fourth-battle-guiyang-persuasion', twentyFourth: 'twenty-fourth-battle-guiyang-persuasion',
twentyFifth: 'twenty-fifth-battle-wuling-mountain-road' twentyFifth: 'twenty-fifth-battle-wuling-mountain-road',
twentySixth: 'twenty-sixth-battle-changsha-veteran'
} as const; } as const;
const requiredSortieUnitIds = new Set(['liu-bei']); const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -618,6 +622,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'zhang-fei', reason: '협곡 보병 전열을 밀어붙이는 돌파 역할입니다.' } { unitId: 'zhang-fei', reason: '협곡 보병 전열을 밀어붙이는 돌파 역할입니다.' }
], ],
note: '무릉 산길 확보전은 열 명 중 여섯 명만 고르는 첫 전장입니다. 마량과 이적을 모두 데려가면 설득은 강해지지만, 전열 무장 한 명을 줄여야 합니다.' note: '무릉 산길 확보전은 열 명 중 여섯 명만 고르는 첫 전장입니다. 마량과 이적을 모두 데려가면 설득은 강해지지만, 전열 무장 한 명을 줄여야 합니다.'
},
'twenty-sixth-battle-changsha-veteran': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '장사 노장을 예우하고 한현을 물러나게 할 명분의 중심입니다.' },
{ unitId: 'gong-zhi', reason: '무릉에서 얻은 현지 정보로 장사 접근로와 성 안 소문을 읽습니다.' },
{ unitId: 'yi-ji', reason: '황충을 예우하고 항복 조건을 문장으로 다듬는 핵심 문사입니다.' },
{ unitId: 'zhuge-liang', reason: '한현의 의심과 성문 방어선을 끊는 작전 핵심입니다.' },
{ unitId: 'guan-yu', reason: '황충과 무인의 예를 나누며 전열을 받아낼 주력입니다.' },
{ unitId: 'zhao-yun', reason: '장사 성문 주변 기병과 척후를 빠르게 정리하기 좋습니다.' }
],
note: '장사 노장 대면전은 열한 명 중 여섯 명만 고르는 전장입니다. 황충과 위연은 승리 후 합류하므로, 이번에는 예우와 돌파의 균형을 먼저 준비하십시오.'
} }
}; };
@@ -2700,6 +2716,114 @@ const campDialogues: CampDialogue[] = [
rewardExp: 9 rewardExp: 9
} }
] ]
},
{
id: 'liu-huang-after-changsha',
title: '노장을 예우하다',
availableAfterBattleIds: [campBattleIds.twentySixth],
unitIds: ['liu-bei', 'huang-zhong'],
bondId: 'liu-bei__huang-zhong',
rewardExp: 30,
lines: [
'황충: 늙은 장수의 체면을 지켜 주신 일은 전장의 승리보다 오래 남을 것입니다.',
'유비: 활은 나이를 묻지 않고 뜻을 묻는다고 들었소. 장군의 활이 백성을 지키는 데 쓰인다면 더 바랄 것이 없소.',
'황충: 그 뜻이라면 제 활과 남은 세월을 맡기겠습니다.'
],
choices: [
{
id: 'honor-veteran-bow',
label: '노장의 활을 예우한다',
response: '유비는 황충의 활과 전공을 병사들 앞에서 높였고, 노장은 조용히 고개를 숙였다.',
rewardExp: 12
},
{
id: 'ask-changsha-people',
label: '장사 백성을 묻는다',
response: '황충은 장사 성 안팎의 창고와 마을 사정을 차분히 들려주었고, 유비는 먼저 살필 곳을 정했다.',
rewardExp: 10
}
]
},
{
id: 'guan-huang-after-changsha',
title: '무인의 존중',
availableAfterBattleIds: [campBattleIds.twentySixth],
unitIds: ['guan-yu', 'huang-zhong'],
bondId: 'guan-yu__huang-zhong',
rewardExp: 28,
lines: [
'관우: 장군의 활은 성루 위에서도 흔들리지 않았소. 칼을 든 몸으로도 그 기개를 알 수 있었소.',
'황충: 운장 장군의 칼 역시 병사를 물러서게 하지 않았습니다. 나이는 달라도 무인의 길은 통하는군요.',
'관우: 다음 전장에서는 서로의 거리와 호흡을 맞춰 봅시다.'
],
choices: [
{
id: 'compare-front-and-bow',
label: '전열과 활의 거리를 맞춘다',
response: '관우와 황충은 전열이 버티는 거리와 활이 닿는 거리를 함께 맞추며 장사 병사들의 시선을 모았다.',
rewardExp: 11
},
{
id: 'share-warrior-courtesy',
label: '무인의 예를 나눈다',
response: '두 장수는 말보다 짧은 예로 서로를 인정했고, 군영의 새 긴장감은 단단한 신뢰로 바뀌기 시작했다.',
rewardExp: 10
}
]
},
{
id: 'liu-wei-after-changsha',
title: '닫힌 문을 여는 창',
availableAfterBattleIds: [campBattleIds.twentySixth],
unitIds: ['liu-bei', 'wei-yan'],
bondId: 'liu-bei__wei-yan',
rewardExp: 26,
lines: [
'위연: 저는 성문 안의 답답함을 참지 못했습니다. 주공의 군이라면 닫힌 문을 여는 데 제 창을 쓰겠습니다.',
'유비: 거친 창도 향할 곳이 바르면 백성을 살리는 길이 되오. 다만 급한 마음이 먼저 나서지 않도록 함께 다듬읍시다.',
'위연: 명령이 분명하다면, 제 창도 분명히 따르겠습니다.'
],
choices: [
{
id: 'set-wei-yan-vanguard-rule',
label: '돌파 규칙을 정한다',
response: '유비는 위연에게 돌파 전에 반드시 백성의 길을 살피라 명했고, 위연은 거칠게나마 고개를 끄덕였다.',
rewardExp: 10
},
{
id: 'temper-wei-yan-ambition',
label: '뜻을 먼저 묻는다',
response: '위연은 공을 세우고 싶다는 마음을 숨기지 않았지만, 그 공이 누구를 위한 것인지 다시 생각하게 되었다.',
rewardExp: 9
}
]
},
{
id: 'huang-wei-after-changsha',
title: '장사의 전환',
availableAfterBattleIds: [campBattleIds.twentySixth],
unitIds: ['huang-zhong', 'wei-yan'],
bondId: 'huang-zhong__wei-yan',
rewardExp: 24,
lines: [
'황충: 성문이 열릴 때의 결단은 빠르더군. 다만 빠른 결단은 때로 먼저 상처를 남긴다.',
'위연: 장군처럼 오래 참는 법은 아직 모릅니다. 하지만 닫힌 성문을 그대로 두는 것도 참기 어렵습니다.',
'황충: 그러니 다음에는 내가 거리를 보고, 그대가 길을 열면 되겠군.'
],
choices: [
{
id: 'pair-bow-and-breakthrough',
label: '활과 돌파를 맞춘다',
response: '황충은 위연이 뛰어들 거리의 위험을 짚었고, 위연은 노장의 활이 닿는 길을 익혔다.',
rewardExp: 10
},
{
id: 'settle-changsha-memory',
label: '장사의 기억을 정리한다',
response: '두 사람은 한현 아래에서 서로 달랐던 선택을 말했고, 이제 같은 군에서 다른 역할을 맡기로 했다.',
rewardExp: 9
}
]
} }
]; ];
@@ -3329,6 +3453,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['남부 군현 지도 1'] itemRewards: ['남부 군현 지도 1']
} }
] ]
},
{
id: 'changsha-veteran-honor',
title: '장사 노장 예우',
location: '장사 성루',
availableAfterBattleIds: [campBattleIds.twentySixth],
bondId: 'liu-bei__huang-zhong',
description: '유비와 황충이 장사 성루에 올라 전투 뒤 흔들린 병사들을 달래고, 노장의 명예를 새 군율 안에 세웁니다.',
lines: [
'황충: 병사들은 제가 패한 것이 아니라 버림받은 것이라 여길까 두려워합니다.',
'유비: 장군을 예로 맞는다면 병사들도 자기 자리를 잃지 않을 것이오. 성루에서 함께 군율을 밝힙시다.',
'장사 성루 위에는 아직 화살 자국이 남아 있었지만, 병사들의 시선은 조금씩 아래로 내려왔습니다.'
],
choices: [
{
id: 'honor-changsha-soldiers',
label: '장사 병사를 예우한다',
response: '황충의 이름으로 장사 병사들의 항복 절차가 정리되자, 성 안의 불안은 빠르게 가라앉았다.',
bondExp: 22,
itemRewards: ['장사 병적 기록 1']
},
{
id: 'open-castle-storehouse',
label: '성내 창고를 점검한다',
response: '창고를 함부로 열지 않고 장부부터 확인하자, 장사 상인들이 다시 거래를 시작할 수 있게 되었다.',
bondExp: 18,
gold: 220,
itemRewards: ['콩 4']
}
]
},
{
id: 'yi-province-road-council',
title: '익주 길 논의',
location: '장사 군막',
availableAfterBattleIds: [campBattleIds.twentySixth],
bondId: 'liu-bei__zhuge-liang',
description: '제갈량과 새로 합류한 장수들의 정보를 모아, 형주 남부 이후 익주로 향할 큰 방향을 처음 정리합니다.',
lines: [
'제갈량: 형주 남부는 뿌리가 되었지만, 뿌리만으로 천하를 받칠 수는 없습니다. 익주로 향할 명분과 길이 필요합니다.',
'유비: 길이 멀수록 함께 갈 사람을 더 살펴야 하오. 새로 온 장수들의 뜻도 함께 묶읍시다.',
'군막의 지도에는 형주 남부 표시 옆으로 익주로 향하는 긴 선이 처음 그어졌습니다.'
],
choices: [
{
id: 'draft-yi-province-cause',
label: '익주 명분을 적는다',
response: '제갈량은 형주 남부 안정과 백성 보호의 기록을 바탕으로, 익주로 향할 첫 명분을 정리했다.',
bondExp: 24,
itemRewards: ['익주 진입 초안 1']
},
{
id: 'assign-new-officers',
label: '새 장수 역할을 나눈다',
response: '황충, 위연, 공지의 역할이 정리되자 다음 대장정에서 누가 전열과 후방을 맡을지 윤곽이 생겼다.',
bondExp: 20,
gold: 220,
itemRewards: ['군영 편성 기록 1']
}
]
} }
]; ];
@@ -3414,6 +3598,33 @@ export class CampScene extends Phaser.Scene {
return; return;
} }
if (battleId === campBattleIds.twentySixth) {
this.campaign = ensureCampaignRosterUnits(
[
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits,
...jingzhouRecruitUnits,
...guiyangRecruitUnits,
...wulingRecruitUnits,
...changshaRecruitUnits
],
[
...xuzhouRecruitBonds,
...caoBreakRecruitBonds,
...liuBiaoRecruitBonds,
...zhugeRecruitBonds,
...jingzhouRecruitBonds,
...guiyangRecruitBonds,
...wulingRecruitBonds,
...changshaRecruitBonds
]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (battleId === campBattleIds.twentyFifth) { if (battleId === campBattleIds.twentyFifth) {
this.campaign = ensureCampaignRosterUnits( this.campaign = ensureCampaignRosterUnits(
[ [
@@ -3535,6 +3746,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() { private currentCampTitle() {
const battleId = this.currentCampBattleId(); const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.twentySixth) {
return '장사 노장 대면 후 군영';
}
if (battleId === campBattleIds.twentyFifth) { if (battleId === campBattleIds.twentyFifth) {
return '무릉 산길 확보 후 군영'; return '무릉 산길 확보 후 군영';
} }

View File

@@ -23,6 +23,7 @@ import {
twentyThirdBattleScenario, twentyThirdBattleScenario,
twentyFifthBattleScenario, twentyFifthBattleScenario,
twentyFourthBattleScenario, twentyFourthBattleScenario,
twentySixthBattleScenario,
twentiethBattleScenario, twentiethBattleScenario,
twelfthBattleScenario, twelfthBattleScenario,
thirdBattleScenario thirdBattleScenario
@@ -349,7 +350,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'twenty-second-camp' || campaign.step === 'twenty-second-camp' ||
campaign.step === 'twenty-third-camp' || campaign.step === 'twenty-third-camp' ||
campaign.step === 'twenty-fourth-camp' || campaign.step === 'twenty-fourth-camp' ||
campaign.step === 'twenty-fifth-camp' campaign.step === 'twenty-fifth-camp' ||
campaign.step === 'twenty-sixth-camp'
) { ) {
this.scene.start('CampScene'); this.scene.start('CampScene');
return; return;
@@ -480,6 +482,11 @@ export class TitleScene extends Phaser.Scene {
return; return;
} }
if (campaign.step === 'twenty-sixth-battle') {
this.scene.start('BattleScene', { battleId: twentySixthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') { if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' }); this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return; return;

View File

@@ -91,7 +91,9 @@ export type CampaignStep =
| 'twenty-fourth-battle' | 'twenty-fourth-battle'
| 'twenty-fourth-camp' | 'twenty-fourth-camp'
| 'twenty-fifth-battle' | 'twenty-fifth-battle'
| 'twenty-fifth-camp'; | 'twenty-fifth-camp'
| 'twenty-sixth-battle'
| 'twenty-sixth-camp';
export type CampaignUnitProgressSnapshot = { export type CampaignUnitProgressSnapshot = {
unitId: string; unitId: string;
@@ -178,7 +180,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'twenty-second-battle-red-cliffs-fire': { victory: 'twenty-second-camp', retry: 'twenty-second-battle' }, 'twenty-second-battle-red-cliffs-fire': { victory: 'twenty-second-camp', retry: 'twenty-second-battle' },
'twenty-third-battle-jingzhou-south-entry': { victory: 'twenty-third-camp', retry: 'twenty-third-battle' }, 'twenty-third-battle-jingzhou-south-entry': { victory: 'twenty-third-camp', retry: 'twenty-third-battle' },
'twenty-fourth-battle-guiyang-persuasion': { victory: 'twenty-fourth-camp', retry: 'twenty-fourth-battle' }, 'twenty-fourth-battle-guiyang-persuasion': { victory: 'twenty-fourth-camp', retry: 'twenty-fourth-battle' },
'twenty-fifth-battle-wuling-mountain-road': { victory: 'twenty-fifth-camp', retry: 'twenty-fifth-battle' } 'twenty-fifth-battle-wuling-mountain-road': { victory: 'twenty-fifth-camp', retry: 'twenty-fifth-battle' },
'twenty-sixth-battle-changsha-veteran': { victory: 'twenty-sixth-camp', retry: 'twenty-sixth-battle' }
}; };
export type CampaignSaveSlotSummary = { export type CampaignSaveSlotSummary = {