Add Luofeng Slope ambush chapter
This commit is contained in:
@@ -51,15 +51,16 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
||||
- Twenty-seventh battle Yi Province relief-road route, using the thirteen-officer roster in sortie selection across a 54x36 mountain-gate battlefield and recruiting Pang Tong after Yang Huai withdraws
|
||||
- Twenty-eighth battle Fu Pass entry route, putting Pang Tong into the sortie choice across a 56x38 fortified mountain pass and recruiting Fa Zheng after Gao Pei withdraws
|
||||
- Twenty-ninth battle Luo Castle outer wall route, putting Fa Zheng into the sortie choice across a 58x40 outer-wall battlefield and recruiting Wu Yi after Zhang Ren withdraws
|
||||
- Thirtieth battle Luofeng Slope ambush route, using Wu Yi's first sortie choice and Pang Tong survival pressure across a 60x42 ambush battlefield
|
||||
- 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-ninth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle camp states, campaign timeline state, and camp save state
|
||||
- Flow verification script from title through the thirtieth battle victory, recruit sortie selection, Liu Biao visit rewards, Zhuge Liang recruitment, Bowang/Changban/Jiangdong/Red Cliffs/Jing Province/Guiyang/Wuling/Changsha/Yi Province/Fu Pass/Luo Castle/Luofeng camp states, campaign timeline state, and camp save state
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Continue the Yi Province arc into Luofeng Slope and Luo Castle proper, with Pang Tong/Fa Zheng/Wu Yi planning pressure and harder officer selection
|
||||
1. Continue the Yi Province arc into Luo Castle proper, with Pang Tong/Fa Zheng/Wu Yi planning pressure and a stronger mix of siege, persuasion, and officer-selection tradeoffs
|
||||
2. Add more recruitable officers as the campaign moves deeper through Yi Province and toward Shu Han
|
||||
3. Expand reserve training into explicit drill choices, class practice, and bond-focused camp assignments
|
||||
4. Add a dedicated treasure/equipment management view so players can compare special effects and growth
|
||||
|
||||
@@ -1736,7 +1736,7 @@ try {
|
||||
if (
|
||||
progressTabState?.activeTab !== 'progress' ||
|
||||
progressTabState.campaignProgress?.completedKnown !== 17 ||
|
||||
progressTabState.campaignProgress?.totalKnown !== 29 ||
|
||||
progressTabState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postBowangProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postChangbanProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postJiangdongProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postRedCliffsProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postFireAttackProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postJingzhouProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
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 !== 29 ||
|
||||
postGuiyangProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postGuiyangProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postGuiyangProgressState.campaignProgress?.latestBattleTitle !== '계양 설득전' ||
|
||||
postGuiyangProgressState.campaignProgress?.nextBattleTitle !== '무릉 산길 확보전'
|
||||
@@ -2872,7 +2872,7 @@ try {
|
||||
if (
|
||||
postWulingProgressState?.activeTab !== 'progress' ||
|
||||
postWulingProgressState.campaignProgress?.completedKnown !== 25 ||
|
||||
postWulingProgressState.campaignProgress?.totalKnown !== 29 ||
|
||||
postWulingProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postWulingProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postWulingProgressState.campaignProgress?.latestBattleTitle !== '무릉 산길 확보전' ||
|
||||
postWulingProgressState.campaignProgress?.nextBattleTitle !== '장사 노장 대면전'
|
||||
@@ -3021,7 +3021,7 @@ try {
|
||||
if (
|
||||
postChangshaProgressState?.activeTab !== 'progress' ||
|
||||
postChangshaProgressState.campaignProgress?.completedKnown !== 26 ||
|
||||
postChangshaProgressState.campaignProgress?.totalKnown !== 29 ||
|
||||
postChangshaProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postChangshaProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postChangshaProgressState.campaignProgress?.latestBattleTitle !== '장사 노장 대면전' ||
|
||||
postChangshaProgressState.campaignProgress?.nextBattleTitle !== '익주 원군로'
|
||||
@@ -3166,7 +3166,7 @@ try {
|
||||
if (
|
||||
postYizhouProgressState?.activeTab !== 'progress' ||
|
||||
postYizhouProgressState.campaignProgress?.completedKnown !== 27 ||
|
||||
postYizhouProgressState.campaignProgress?.totalKnown !== 29 ||
|
||||
postYizhouProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postYizhouProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postYizhouProgressState.campaignProgress?.latestBattleTitle !== '익주 원군로' ||
|
||||
postYizhouProgressState.campaignProgress?.nextBattleTitle !== '부수관 진입전'
|
||||
@@ -3313,7 +3313,7 @@ try {
|
||||
if (
|
||||
postFuPassProgressState?.activeTab !== 'progress' ||
|
||||
postFuPassProgressState.campaignProgress?.completedKnown !== 28 ||
|
||||
postFuPassProgressState.campaignProgress?.totalKnown !== 29 ||
|
||||
postFuPassProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postFuPassProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postFuPassProgressState.campaignProgress?.latestBattleTitle !== '부수관 진입전' ||
|
||||
postFuPassProgressState.campaignProgress?.nextBattleTitle !== '낙성 외곽전'
|
||||
@@ -3462,15 +3462,161 @@ try {
|
||||
if (
|
||||
postLuoProgressState?.activeTab !== 'progress' ||
|
||||
postLuoProgressState.campaignProgress?.completedKnown !== 29 ||
|
||||
postLuoProgressState.campaignProgress?.totalKnown !== 29 ||
|
||||
postLuoProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postLuoProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postLuoProgressState.campaignProgress?.latestBattleTitle !== '낙성 외곽전' ||
|
||||
postLuoProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
|
||||
postLuoProgressState.campaignProgress?.nextBattleTitle !== '낙봉파 매복전'
|
||||
) {
|
||||
throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and pause on Luofeng preparation: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`);
|
||||
throw new Error(`Expected post-Luo progress tab to complete the outer-wall battle and open Luofeng Slope ambush: ${JSON.stringify(postLuoProgressState?.campaignProgress)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-post-luo-progress.png', fullPage: true });
|
||||
|
||||
await page.mouse.click(1120, 38);
|
||||
await page.waitForTimeout(180);
|
||||
const luofengSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
!luofengSortieState?.sortieVisible ||
|
||||
!luofengSortieState.sortiePlan?.objectiveLine?.includes('낙봉파 매복전') ||
|
||||
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'pang-tong' && unit.recruited && unit.recommended) ||
|
||||
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'wu-yi' && unit.recruited && unit.recommended) ||
|
||||
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'fa-zheng' && unit.recruited && unit.recommended) ||
|
||||
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'zhao-yun' && unit.recruited && unit.recommended) ||
|
||||
!luofengSortieState.sortieRoster?.some((unit) => unit.id === 'huang-zhong' && unit.recruited && unit.recommended) ||
|
||||
luofengSortieState.sortieRoster?.length < 16 ||
|
||||
luofengSortieState.sortiePlan?.maxCount !== 6
|
||||
) {
|
||||
throw new Error(`Expected Luo camp sortie prep to target Luofeng with Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong recommended: ${JSON.stringify(luofengSortieState)}`);
|
||||
}
|
||||
assertSortieTacticalRoster(luofengSortieState, [
|
||||
'liu-bei',
|
||||
'guan-yu',
|
||||
'zhang-fei',
|
||||
'jian-yong',
|
||||
'mi-zhu',
|
||||
'sun-qian',
|
||||
'zhao-yun',
|
||||
'zhuge-liang',
|
||||
'ma-liang',
|
||||
'yi-ji',
|
||||
'gong-zhi',
|
||||
'huang-zhong',
|
||||
'wei-yan',
|
||||
'pang-tong',
|
||||
'fa-zheng',
|
||||
'wu-yi'
|
||||
]);
|
||||
|
||||
const luofengPriorityUnits = ['pang-tong', 'wu-yi', 'fa-zheng', 'zhao-yun', 'huang-zhong'];
|
||||
for (const unitId of luofengPriorityUnits) {
|
||||
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' && !luofengPriorityUnits.includes(unit.id)
|
||||
);
|
||||
if (removable) {
|
||||
await clickSortieRosterUnit(page, removable.id);
|
||||
}
|
||||
await clickSortieRosterUnit(page, unitId);
|
||||
}
|
||||
}
|
||||
|
||||
const luofengSortieReadyState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
!luofengPriorityUnits.every((unitId) =>
|
||||
luofengSortieReadyState.sortieRoster?.some((unit) => unit.id === unitId && unit.selected)
|
||||
) ||
|
||||
luofengSortieReadyState.sortiePlan?.selectedCount !== 6 ||
|
||||
luofengSortieReadyState.sortiePlan?.recommendedSelectedCount < 6
|
||||
) {
|
||||
throw new Error(`Expected Luofeng sortie to deploy Pang Tong, Wu Yi, Fa Zheng, Zhao Yun, and Huang Zhong while preserving six-officer pressure: ${JSON.stringify(luofengSortieReadyState)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-luofeng-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-luofeng-story.png', fullPage: true });
|
||||
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
const enteredThirtiethBattle = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
return state?.scene === 'BattleScene' && state?.battleId === 'thirtieth-battle-luofeng-ambush';
|
||||
});
|
||||
if (enteredThirtiethBattle) {
|
||||
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 === 'thirtieth-battle-luofeng-ambush' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||
});
|
||||
await page.screenshot({ path: 'dist/verification-thirtieth-battle.png', fullPage: true });
|
||||
|
||||
const thirtiethBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
const thirtiethEnemies = thirtiethBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||
const thirtiethAllies = thirtiethBattleState.units.filter((unit) => unit.faction === 'ally');
|
||||
const thirtiethEnemyBehaviors = new Set(thirtiethEnemies.map((unit) => unit.ai));
|
||||
if (
|
||||
thirtiethBattleState.camera?.mapWidth !== 60 ||
|
||||
thirtiethBattleState.camera?.mapHeight !== 42 ||
|
||||
thirtiethBattleState.victoryConditionLabel !== '장임 매복 격파' ||
|
||||
thirtiethEnemies.length < 23 ||
|
||||
!thirtiethEnemyBehaviors.has('aggressive') ||
|
||||
!thirtiethEnemyBehaviors.has('guard') ||
|
||||
!thirtiethEnemyBehaviors.has('hold') ||
|
||||
!thirtiethEnemies.some((unit) => unit.id === 'luofeng-leader-zhang-ren') ||
|
||||
!thirtiethAllies.some((unit) => unit.id === 'pang-tong') ||
|
||||
!thirtiethAllies.some((unit) => unit.id === 'wu-yi') ||
|
||||
!thirtiethAllies.some((unit) => unit.id === 'fa-zheng')
|
||||
) {
|
||||
throw new Error(`Expected thirtieth battle to use Luofeng map, Zhang Ren ambush objective, selected allied officers, and mixed AI: ${JSON.stringify(thirtiethBattleState)}`);
|
||||
}
|
||||
|
||||
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 thirtiethCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
thirtiethCampState?.campBattleId !== 'thirtieth-battle-luofeng-ambush' ||
|
||||
thirtiethCampState.campTitle !== '낙봉파 매복전 후 군영' ||
|
||||
thirtiethCampState.availableDialogueIds?.length !== 3 ||
|
||||
!thirtiethCampState.availableDialogueIds.every((id) => id.endsWith('luofeng')) ||
|
||||
thirtiethCampState.availableVisitIds?.length !== 2 ||
|
||||
!thirtiethCampState.campaign?.roster?.some((unit) => unit.id === 'wu-yi') ||
|
||||
thirtiethCampState.rosterCollection?.total < 16
|
||||
) {
|
||||
throw new Error(`Expected thirtieth camp to preserve Wu Yi and expose Luofeng dialogue/visit sets: ${JSON.stringify(thirtiethCampState)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-luofeng-camp.png', fullPage: true });
|
||||
|
||||
await page.mouse.click(966, 38);
|
||||
await page.waitForTimeout(180);
|
||||
const postLuofengProgressState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
postLuofengProgressState?.activeTab !== 'progress' ||
|
||||
postLuofengProgressState.campaignProgress?.completedKnown !== 30 ||
|
||||
postLuofengProgressState.campaignProgress?.totalKnown !== 30 ||
|
||||
postLuofengProgressState.campaignProgress?.activeChapter?.title !== '형주·익주 확보' ||
|
||||
postLuofengProgressState.campaignProgress?.latestBattleTitle !== '낙봉파 매복전' ||
|
||||
postLuofengProgressState.campaignProgress?.nextBattleTitle !== '준비 중'
|
||||
) {
|
||||
throw new Error(`Expected post-Luofeng progress tab to complete Luofeng and pause before Luo Castle proper: ${JSON.stringify(postLuofengProgressState?.campaignProgress)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-post-luofeng-progress.png', fullPage: true });
|
||||
|
||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
@@ -3482,7 +3628,7 @@ try {
|
||||
return activeScenes.includes('CampScene');
|
||||
});
|
||||
|
||||
console.log(`Verified title-to-twenty-ninth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, and Wu Yi joins, result states, and debug API at ${targetUrl}`);
|
||||
console.log(`Verified title-to-thirtieth-battle flow, recruited officer sortie selection, Ma Liang, Yi Ji, Gong Zhi, Huang Zhong, Wei Yan, Pang Tong, Fa Zheng, Wu Yi joins, Luofeng ambush, result states, and debug API at ${targetUrl}`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
|
||||
122
src/assets/images/battle/thirtieth-battle-map.svg
Normal file
122
src/assets/images/battle/thirtieth-battle-map.svg
Normal file
@@ -0,0 +1,122 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="6000" height="4200" viewBox="0 0 6000 4200">
|
||||
<defs>
|
||||
<filter id="grain" x="-5%" y="-5%" width="110%" height="110%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.024" numOctaves="4" seed="30" result="noise"/>
|
||||
<feColorMatrix type="saturate" values="0.5"/>
|
||||
<feBlend in="SourceGraphic" in2="noise" mode="multiply"/>
|
||||
</filter>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="12" flood-color="#1a1009" flood-opacity="0.48"/>
|
||||
</filter>
|
||||
<linearGradient id="plain" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#6d6f49"/>
|
||||
<stop offset="0.5" stop-color="#a08758"/>
|
||||
<stop offset="1" stop-color="#596743"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="road" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b653f"/>
|
||||
<stop offset="0.5" stop-color="#d0b06f"/>
|
||||
<stop offset="1" stop-color="#75563a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="river" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#213a55"/>
|
||||
<stop offset="0.5" stop-color="#6d9fb2"/>
|
||||
<stop offset="1" stop-color="#1e354c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="mountain" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#5a5134"/>
|
||||
<stop offset="0.58" stop-color="#30341f"/>
|
||||
<stop offset="1" stop-color="#141912"/>
|
||||
</linearGradient>
|
||||
<pattern id="scrub" width="240" height="190" patternUnits="userSpaceOnUse">
|
||||
<path d="M-60 155 C25 110 84 90 160 38 M34 205 C104 150 165 128 260 76" fill="none" stroke="#dec384" stroke-width="12" stroke-opacity="0.15"/>
|
||||
<circle cx="48" cy="60" r="8" fill="#38582f" opacity="0.2"/>
|
||||
<circle cx="164" cy="126" r="7" fill="#31522b" opacity="0.18"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="6000" height="4200" fill="url(#plain)"/>
|
||||
<rect width="6000" height="4200" fill="url(#scrub)" opacity="0.82" filter="url(#grain)"/>
|
||||
|
||||
<g opacity="0.55">
|
||||
<path d="M0 0 L920 0 C690 410 650 920 790 1390 C940 1930 820 2590 600 3300 C480 3680 500 3960 650 4200 L0 4200Z" fill="#101711"/>
|
||||
<path d="M6000 0 L5250 0 C5010 410 5050 820 5350 1140 C5690 1505 5815 1970 5630 2480 C5470 2920 5530 3560 5800 4200 L6000 4200Z" fill="#101711"/>
|
||||
<path d="M4600 0 C4490 350 4620 675 4910 955 L5040 0Z" fill="#101711"/>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M1205 0 C1175 420 1250 890 1325 1280 C1410 1820 1350 2440 1220 3150" fill="none" stroke="#1b3147" stroke-width="260" stroke-linecap="round"/>
|
||||
<path d="M1235 0 C1210 425 1280 900 1355 1285 C1435 1815 1370 2425 1245 3140" fill="none" stroke="url(#river)" stroke-width="196" stroke-linecap="round"/>
|
||||
<path d="M1310 100 C1290 545 1375 910 1425 1290 C1480 1770 1425 2320 1325 2850" fill="none" stroke="#a9d6df" stroke-width="22" stroke-linecap="round" opacity="0.5"/>
|
||||
|
||||
<path d="M3220 290 C3330 960 3260 1640 3220 2650" fill="none" stroke="#1b3147" stroke-width="254" stroke-linecap="round"/>
|
||||
<path d="M3250 300 C3355 960 3290 1640 3255 2635" fill="none" stroke="url(#river)" stroke-width="184" stroke-linecap="round"/>
|
||||
<path d="M3320 420 C3375 1030 3325 1620 3290 2430" fill="none" stroke="#a9d6df" stroke-width="20" stroke-linecap="round" opacity="0.48"/>
|
||||
|
||||
<path d="M4190 3180 C4165 3540 4220 3870 4230 4200" fill="none" stroke="#1b3147" stroke-width="235" stroke-linecap="round"/>
|
||||
<path d="M4220 3190 C4200 3550 4250 3880 4255 4200" fill="none" stroke="url(#river)" stroke-width="172" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M310 760 C690 280 1160 90 1660 70 C1950 55 2200 80 2480 165 C2100 560 1820 950 1580 1400 C1240 1190 890 1185 630 1320 C470 1400 335 1180 310 760Z" fill="url(#mountain)"/>
|
||||
<path d="M2030 90 C2460 -50 2840 50 3190 430 C2950 760 2790 1080 2705 1440 C2350 1185 2020 1090 1660 1220 C1695 795 1825 395 2030 90Z" fill="url(#mountain)"/>
|
||||
<path d="M3360 255 C3890 70 4370 260 4770 870 C4440 1105 4240 1470 4130 1860 C3820 1600 3440 1500 3095 1675 C3135 1180 3240 665 3360 255Z" fill="url(#mountain)"/>
|
||||
<path d="M2360 3500 C2780 3160 3310 3190 3710 3460 C3480 3680 3365 3920 3320 4200 L2240 4200 C2250 3920 2290 3685 2360 3500Z" fill="url(#mountain)"/>
|
||||
<path d="M4320 2880 C4825 2650 5380 2800 5740 3230 C5450 3500 5290 3840 5230 4200 L4330 4200 C4495 3730 4475 3310 4320 2880Z" fill="url(#mountain)"/>
|
||||
</g>
|
||||
|
||||
<g opacity="0.98">
|
||||
<path d="M450 3745 C1050 3500 1550 3450 2090 3130 C2590 2835 2990 2650 3650 2570 C4240 2500 4630 2120 5220 1805 C5470 1670 5700 1680 5860 1790" fill="none" stroke="#4c3422" stroke-width="360" stroke-linecap="round" stroke-linejoin="round" opacity="0.43"/>
|
||||
<path d="M470 3705 C1080 3470 1570 3410 2095 3075 C2625 2740 3035 2590 3640 2510 C4200 2440 4580 2085 5185 1760 C5450 1625 5700 1625 5870 1735" fill="none" stroke="url(#road)" stroke-width="228" stroke-linecap="round" stroke-linejoin="round" filter="url(#grain)"/>
|
||||
<path d="M790 3770 C1480 3525 1910 3390 2470 3010 C2890 2725 3230 2625 3670 2620" fill="none" stroke="#f0d99c" stroke-width="34" stroke-linecap="round" stroke-opacity="0.42"/>
|
||||
<path d="M2860 2780 C3200 2470 3420 2290 3530 1950" fill="none" stroke="#3a261a" stroke-width="68" stroke-linecap="round" opacity="0.28"/>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M4790 1230 L5650 1230 L5780 1390 L5780 2490 L4710 2490 L4710 1390Z" fill="#69573e" stroke="#211812" stroke-width="38"/>
|
||||
<path d="M4915 1390 L5570 1390 L5670 1505 L5670 2350 L4825 2350 L4825 1505Z" fill="#96784b" stroke="#2b1b12" stroke-width="25"/>
|
||||
<path d="M5100 1750 L5345 1750 L5345 2350 L5100 2350Z" fill="#241b14"/>
|
||||
<path d="M4730 1210 L4630 1360 L4710 1390 L5780 1390 L5855 1300 L5650 1230Z" fill="#363026"/>
|
||||
<path d="M4960 1150 L5110 1055 L5260 1150 L5260 1310 L4960 1310Z" fill="#444034" stroke="#211912" stroke-width="22"/>
|
||||
<path d="M5375 1165 L5495 1090 L5615 1165 L5615 1310 L5375 1310Z" fill="#444034" stroke="#211912" stroke-width="22"/>
|
||||
</g>
|
||||
|
||||
<g opacity="0.76">
|
||||
<circle cx="760" cy="1900" r="250" fill="#2c512a"/>
|
||||
<circle cx="1120" cy="1660" r="270" fill="#335e32"/>
|
||||
<circle cx="1510" cy="2020" r="310" fill="#294d27"/>
|
||||
<circle cx="2020" cy="1510" r="280" fill="#31572e"/>
|
||||
<circle cx="2400" cy="1830" r="310" fill="#294a26"/>
|
||||
<circle cx="2870" cy="2320" r="278" fill="#355f32"/>
|
||||
<circle cx="3580" cy="3440" r="315" fill="#315c31"/>
|
||||
<circle cx="4130" cy="3560" r="360" fill="#294c2a"/>
|
||||
<circle cx="4850" cy="3080" r="340" fill="#31582f"/>
|
||||
<circle cx="5460" cy="3500" r="370" fill="#294b27"/>
|
||||
</g>
|
||||
|
||||
<g opacity="0.96">
|
||||
<g transform="translate(1810 3295)">
|
||||
<path d="M0 88 L138 0 L276 88 L250 255 L28 255Z" fill="#746640" stroke="#2b2015" stroke-width="18"/>
|
||||
<path d="M42 95 L138 35 L234 95 L214 136 L138 88 L62 136Z" fill="#ce8428"/>
|
||||
<rect x="98" y="136" width="82" height="116" fill="#2d2116"/>
|
||||
<path d="M35 262 C110 294 178 294 250 262" fill="none" stroke="#25301c" stroke-width="20"/>
|
||||
</g>
|
||||
<g transform="translate(3560 2700) scale(0.9)">
|
||||
<path d="M0 88 L138 0 L276 88 L250 255 L28 255Z" fill="#716241" stroke="#2b2015" stroke-width="18"/>
|
||||
<path d="M42 95 L138 35 L234 95 L214 136 L138 88 L62 136Z" fill="#ce8428"/>
|
||||
<rect x="98" y="136" width="82" height="116" fill="#2d2116"/>
|
||||
</g>
|
||||
<g transform="translate(4320 2040) scale(0.86)">
|
||||
<path d="M0 88 L138 0 L276 88 L250 255 L28 255Z" fill="#746340" stroke="#2b2015" stroke-width="18"/>
|
||||
<path d="M42 95 L138 35 L234 95 L214 136 L138 88 L62 136Z" fill="#ce8428"/>
|
||||
<rect x="98" y="136" width="82" height="116" fill="#2d2116"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g opacity="0.28">
|
||||
<path d="M0 3480 C530 3260 910 3275 1330 3400" fill="none" stroke="#ead49c" stroke-width="28"/>
|
||||
<path d="M3700 1650 C4160 1380 4590 1260 5080 1280" fill="none" stroke="#ead49c" stroke-width="24"/>
|
||||
<path d="M1480 3260 C1990 3130 2480 2880 2970 2530" fill="none" stroke="#49321e" stroke-width="18"/>
|
||||
<path d="M3300 2100 C3540 1850 3860 1710 4200 1660" fill="none" stroke="#ead49c" stroke-width="18"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -95,6 +95,10 @@ import {
|
||||
seventhBattleMap,
|
||||
seventhBattleUnits,
|
||||
seventhBattleVictoryPages,
|
||||
thirtiethBattleBonds,
|
||||
thirtiethBattleMap,
|
||||
thirtiethBattleUnits,
|
||||
thirtiethBattleVictoryPages,
|
||||
sixthBattleBonds,
|
||||
sixthBattleMap,
|
||||
sixthBattleUnits,
|
||||
@@ -150,7 +154,8 @@ export type BattleScenarioId =
|
||||
| 'twenty-sixth-battle-changsha-veteran'
|
||||
| 'twenty-seventh-battle-yizhou-relief-road'
|
||||
| 'twenty-eighth-battle-fu-pass-entry'
|
||||
| 'twenty-ninth-battle-luo-outer-wall';
|
||||
| 'twenty-ninth-battle-luo-outer-wall'
|
||||
| 'thirtieth-battle-luofeng-ambush';
|
||||
|
||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||
|
||||
@@ -1837,6 +1842,69 @@ export const twentyNinthBattleScenario: BattleScenarioDefinition = {
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const thirtiethBattleScenario: BattleScenarioDefinition = {
|
||||
id: 'thirtieth-battle-luofeng-ambush',
|
||||
title: '낙봉파 매복전',
|
||||
victoryConditionLabel: '장임 매복 격파',
|
||||
defeatConditionLabel: '유비 또는 방통 퇴각',
|
||||
openingObjectiveLines: [
|
||||
'낙성 본성으로 가는 빠른 길인 낙봉파에 장임의 매복이 숨어 있습니다. 좁은 길과 고지 궁병을 동시에 살피며 진군해야 합니다.',
|
||||
'오의가 새로 출전 후보에 들어왔습니다. 낙성의 길을 아는 오의를 데려가면 매복 위치를 읽기 쉽지만, 여섯 명 제한은 더 빡빡해집니다.',
|
||||
'유비와 방통을 지키며 장임의 매복을 격파하십시오. 낙봉파를 넘으면 낙성 본성 공략이 열립니다.'
|
||||
],
|
||||
map: thirtiethBattleMap,
|
||||
units: thirtiethBattleUnits,
|
||||
bonds: thirtiethBattleBonds,
|
||||
mapTextureKey: 'battle-map-thirtieth',
|
||||
leaderUnitId: 'luofeng-leader-zhang-ren',
|
||||
quickVictoryTurnLimit: 39,
|
||||
baseVictoryGold: 4440,
|
||||
objectives: [
|
||||
{
|
||||
id: 'leader',
|
||||
kind: 'defeat-leader',
|
||||
label: '장임 매복 격파',
|
||||
rewardGold: 2760,
|
||||
unitId: 'luofeng-leader-zhang-ren'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '유비 생존',
|
||||
rewardGold: 820,
|
||||
unitId: 'liu-bei'
|
||||
},
|
||||
{
|
||||
id: 'pang-tong',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '방통 생존',
|
||||
rewardGold: 1180,
|
||||
unitId: 'pang-tong'
|
||||
},
|
||||
{
|
||||
id: 'luofeng-village',
|
||||
kind: 'secure-terrain',
|
||||
label: '낙봉파 길목 확보',
|
||||
rewardGold: 1320,
|
||||
terrain: 'village'
|
||||
},
|
||||
{
|
||||
id: 'quick',
|
||||
kind: 'quick-victory',
|
||||
label: '39턴 이내 승리',
|
||||
rewardGold: 1040,
|
||||
maxTurn: 39
|
||||
}
|
||||
],
|
||||
defeatConditions: [
|
||||
{ kind: 'unit-defeated', unitId: 'liu-bei' },
|
||||
{ kind: 'unit-defeated', unitId: 'pang-tong' }
|
||||
],
|
||||
itemRewards: ['콩 15', '상처약 11', '탁주 6', '낙봉파 정찰표 +1', '봉추 생환 +1'],
|
||||
victoryPages: thirtiethBattleVictoryPages,
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||
|
||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||
@@ -1868,7 +1936,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
|
||||
'twenty-sixth-battle-changsha-veteran': twentySixthBattleScenario,
|
||||
'twenty-seventh-battle-yizhou-relief-road': twentySeventhBattleScenario,
|
||||
'twenty-eighth-battle-fu-pass-entry': twentyEighthBattleScenario,
|
||||
'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario
|
||||
'twenty-ninth-battle-luo-outer-wall': twentyNinthBattleScenario,
|
||||
'thirtieth-battle-luofeng-ambush': thirtiethBattleScenario
|
||||
};
|
||||
|
||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
seventeenthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
thirtiethBattleScenario,
|
||||
twentyEighthBattleScenario,
|
||||
twentyNinthBattleScenario,
|
||||
twentyFirstBattleScenario,
|
||||
@@ -65,6 +66,8 @@ import {
|
||||
tenthBattleVictoryPages,
|
||||
thirteenthBattleIntroPages,
|
||||
thirteenthBattleVictoryPages,
|
||||
thirtiethBattleIntroPages,
|
||||
thirtiethBattleVictoryPages,
|
||||
twentyEighthBattleIntroPages,
|
||||
twentyEighthBattleVictoryPages,
|
||||
twentyNinthBattleIntroPages,
|
||||
@@ -403,13 +406,24 @@ const sortieFlows: Record<string, SortieFlow> = {
|
||||
},
|
||||
[twentyNinthBattleScenario.id]: {
|
||||
afterBattleId: twentyNinthBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '낙봉파 진입 준비',
|
||||
eyebrow: '다음 전장',
|
||||
title: thirtiethBattleScenario.title,
|
||||
description:
|
||||
'낙성 외곽을 넘고 오의가 합류했습니다. 장임은 더 안쪽으로 물러났고, 방통은 낙봉파라는 좁은 길의 불길한 그림자를 살피고 있습니다.',
|
||||
rewardHint: '다음 장: 낙봉파 진입 준비 중',
|
||||
pages: twentyNinthBattleVictoryPages,
|
||||
unavailableNotice: '낙봉파와 낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 새로 합류한 오의를 포함해 출전 조합과 공명 이벤트를 준비하십시오.'
|
||||
'낙성 외곽을 넘고 오의가 합류했습니다. 이제 낙봉파의 좁은 길을 통과해야 하며, 방통의 불길한 직감과 오의의 길 안내를 함께 살펴야 합니다.',
|
||||
rewardHint: `예상 보상: ${thirtiethBattleScenario.title} 개방 / 봉추 생환`,
|
||||
nextBattleId: thirtiethBattleScenario.id,
|
||||
campaignStep: 'thirtieth-battle',
|
||||
pages: [...twentyNinthBattleVictoryPages, ...thirtiethBattleIntroPages]
|
||||
},
|
||||
[thirtiethBattleScenario.id]: {
|
||||
afterBattleId: thirtiethBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '낙성 본성 공략 준비',
|
||||
description:
|
||||
'낙봉파의 매복을 무너뜨리고 방통을 살려 냈습니다. 장임은 낙성 본성으로 물러났고, 익주의 성문과 장수들의 마음을 함께 열어야 할 차례입니다.',
|
||||
rewardHint: '다음 장: 낙성 본성 공략 준비 중',
|
||||
pages: thirtiethBattleVictoryPages,
|
||||
unavailableNotice: '낙성 본성 공략은 다음 작업에서 이어집니다. 군영에서 방통, 법정, 오의의 정보와 출전 조합을 더 다듬어 두십시오.'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1890,6 +1890,68 @@ export const twentyNinthBattleVictoryPages: StoryPage[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const thirtiethBattleIntroPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirtieth-luofeng-shadow',
|
||||
bgm: 'story-dark',
|
||||
chapter: '낙봉파의 좁은 길',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '오의',
|
||||
text: '낙성 안쪽으로 들어가는 빠른 길은 낙봉파입니다. 길은 짧지만 양쪽 산세가 높고, 장임이 기다리기에는 지나치게 좋은 곳입니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirtieth-pang-tong-warning',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '봉추의 불길한 직감',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '방통',
|
||||
text: '좋은 길이 언제나 좋은 길은 아닙니다. 적이 우리를 빠르게 오게 만들었다면, 빠르게 물러날 길도 함께 보아 두어야 합니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirtieth-roster-pressure',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '오의의 첫 출전 선택',
|
||||
background: 'story-militia',
|
||||
speaker: '법정',
|
||||
text: '이번 전투부터 오의도 출전 후보입니다. 낙성의 길을 아는 오의를 데려가면 매복을 읽기 쉬우나, 전열과 보급 후보는 더 좁아집니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirtieth-luofeng-sortie',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '낙봉파 매복전',
|
||||
background: 'story-sortie',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '빠른 길을 택하되 방심하지 않겠소. 방통의 눈, 법정의 정보, 오의의 길 안내를 함께 엮어 장임의 매복을 거꾸로 무너뜨리겠소.'
|
||||
}
|
||||
];
|
||||
|
||||
export const thirtiethBattleVictoryPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirtieth-victory-ambush-broken',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '매복을 꿰뚫다',
|
||||
background: 'story-sortie',
|
||||
text: '장임의 매복은 거칠게 덮쳐 왔으나, 유비군은 길목을 버티며 역으로 활로를 열었습니다. 낙봉파의 좁은 비탈에는 무너진 방책과 흩어진 깃발만 남았습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirtieth-pang-tong-survives',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '봉추의 생환',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '방통',
|
||||
text: '하마터면 길의 이름값을 할 뻔했습니다. 그러나 이번에는 우리 쪽이 한 수 먼저 보았습니다. 이제 낙성 본성은 더 이상 멀지 않습니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirtieth-next-luo-castle',
|
||||
bgm: 'story-dark',
|
||||
chapter: '낙성 본성 앞',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '제갈량',
|
||||
text: '낙봉파를 넘었으니 장임의 방어선은 본성에 몰렸습니다. 다음 전장은 낙성의 문과 익주 장수들의 마음을 함께 여는 싸움이 될 것입니다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleMap: BattleMap = {
|
||||
width: 20,
|
||||
height: 18,
|
||||
@@ -2209,6 +2271,12 @@ export const twentyNinthBattleMap: BattleMap = {
|
||||
terrain: createTwentyNinthBattleTerrain()
|
||||
};
|
||||
|
||||
export const thirtiethBattleMap: BattleMap = {
|
||||
width: 60,
|
||||
height: 42,
|
||||
terrain: createThirtiethBattleTerrain()
|
||||
};
|
||||
|
||||
export const firstBattleUnits: UnitData[] = [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
@@ -12195,6 +12263,525 @@ export const twentyNinthBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const thirtiethBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 4, y: 36 },
|
||||
'guan-yu': { x: 5, y: 34 },
|
||||
'zhang-fei': { x: 4, y: 38 },
|
||||
'jian-yong': { x: 3, y: 39 },
|
||||
'mi-zhu': { x: 5, y: 40 },
|
||||
'sun-qian': { x: 7, y: 39 },
|
||||
'zhao-yun': { x: 8, y: 36 },
|
||||
'zhuge-liang': { x: 6, y: 35 },
|
||||
'ma-liang': { x: 8, y: 40 },
|
||||
'yi-ji': { x: 9, y: 38 },
|
||||
'gong-zhi': { x: 10, y: 40 },
|
||||
'huang-zhong': { x: 7, y: 37 },
|
||||
'wei-yan': { x: 10, y: 35 },
|
||||
'pang-tong': { x: 9, y: 36 },
|
||||
'fa-zheng': { x: 11, y: 38 },
|
||||
'wu-yi': { x: 12, y: 36 }
|
||||
};
|
||||
|
||||
export const thirtiethBattleUnits: UnitData[] = [
|
||||
...[
|
||||
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
|
||||
...xuzhouRecruitUnits,
|
||||
...caoBreakRecruitUnits,
|
||||
...liuBiaoRecruitUnits,
|
||||
...zhugeRecruitUnits,
|
||||
...jingzhouRecruitUnits,
|
||||
...guiyangRecruitUnits,
|
||||
...wulingRecruitUnits,
|
||||
...changshaRecruitUnits,
|
||||
...yizhouRecruitUnits,
|
||||
...fuPassRecruitUnits,
|
||||
...luoCastleRecruitUnits
|
||||
].map((unit) => placeScenarioUnit(unit, thirtiethBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
|
||||
{
|
||||
id: 'luofeng-scout-a',
|
||||
name: '낙봉 척후',
|
||||
faction: 'enemy',
|
||||
className: '비탈 척후',
|
||||
classKey: 'bandit',
|
||||
level: 50,
|
||||
exp: 82,
|
||||
hp: 132,
|
||||
maxHp: 132,
|
||||
attack: 50,
|
||||
move: 4,
|
||||
stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 28 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 18,
|
||||
y: 35
|
||||
},
|
||||
{
|
||||
id: 'luofeng-scout-b',
|
||||
name: '낙봉 척후',
|
||||
faction: 'enemy',
|
||||
className: '동쪽 척후',
|
||||
classKey: 'bandit',
|
||||
level: 50,
|
||||
exp: 82,
|
||||
hp: 132,
|
||||
maxHp: 132,
|
||||
attack: 50,
|
||||
move: 4,
|
||||
stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 28 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 22,
|
||||
y: 33
|
||||
},
|
||||
{
|
||||
id: 'luofeng-scout-c',
|
||||
name: '낙봉 척후',
|
||||
faction: 'enemy',
|
||||
className: '북쪽 척후',
|
||||
classKey: 'bandit',
|
||||
level: 50,
|
||||
exp: 82,
|
||||
hp: 132,
|
||||
maxHp: 132,
|
||||
attack: 50,
|
||||
move: 4,
|
||||
stats: { might: 150, intelligence: 106, leadership: 122, agility: 136, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 42 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 28 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 19,
|
||||
y: 28
|
||||
},
|
||||
{
|
||||
id: 'luofeng-infantry-a',
|
||||
name: '낙봉 보병',
|
||||
faction: 'enemy',
|
||||
className: '비탈 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 184,
|
||||
maxHp: 184,
|
||||
attack: 58,
|
||||
move: 3,
|
||||
stats: { might: 170, intelligence: 108, leadership: 148, agility: 114, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 62 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 48 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 28,
|
||||
y: 31
|
||||
},
|
||||
{
|
||||
id: 'luofeng-infantry-b',
|
||||
name: '낙봉 보병',
|
||||
faction: 'enemy',
|
||||
className: '협로 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 184,
|
||||
maxHp: 184,
|
||||
attack: 58,
|
||||
move: 3,
|
||||
stats: { might: 170, intelligence: 108, leadership: 148, agility: 114, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 62 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 48 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 34,
|
||||
y: 28
|
||||
},
|
||||
{
|
||||
id: 'luofeng-infantry-c',
|
||||
name: '낙봉 보병',
|
||||
faction: 'enemy',
|
||||
className: '산문 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 192,
|
||||
maxHp: 192,
|
||||
attack: 60,
|
||||
move: 3,
|
||||
stats: { might: 174, intelligence: 110, leadership: 152, agility: 116, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 70 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 56 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 40,
|
||||
y: 24
|
||||
},
|
||||
{
|
||||
id: 'luofeng-infantry-d',
|
||||
name: '낙봉 보병',
|
||||
faction: 'enemy',
|
||||
className: '후미 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 192,
|
||||
maxHp: 192,
|
||||
attack: 60,
|
||||
move: 3,
|
||||
stats: { might: 174, intelligence: 110, leadership: 152, agility: 116, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 70 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 56 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 46,
|
||||
y: 20
|
||||
},
|
||||
{
|
||||
id: 'luofeng-archer-a',
|
||||
name: '낙봉 궁병',
|
||||
faction: 'enemy',
|
||||
className: '매복 궁병',
|
||||
classKey: 'archer',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 132,
|
||||
maxHp: 132,
|
||||
attack: 51,
|
||||
move: 3,
|
||||
stats: { might: 118, intelligence: 120, leadership: 126, agility: 132, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 48 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 32 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 31,
|
||||
y: 27
|
||||
},
|
||||
{
|
||||
id: 'luofeng-archer-b',
|
||||
name: '낙봉 궁병',
|
||||
faction: 'enemy',
|
||||
className: '고지 궁병',
|
||||
classKey: 'archer',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 132,
|
||||
maxHp: 132,
|
||||
attack: 51,
|
||||
move: 3,
|
||||
stats: { might: 118, intelligence: 120, leadership: 126, agility: 132, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 48 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 32 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 39,
|
||||
y: 22
|
||||
},
|
||||
{
|
||||
id: 'luofeng-archer-c',
|
||||
name: '낙봉 궁병',
|
||||
faction: 'enemy',
|
||||
className: '성루 궁병',
|
||||
classKey: 'archer',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 136,
|
||||
maxHp: 136,
|
||||
attack: 52,
|
||||
move: 3,
|
||||
stats: { might: 120, intelligence: 122, leadership: 128, agility: 134, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 56 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 40 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 47,
|
||||
y: 17
|
||||
},
|
||||
{
|
||||
id: 'luofeng-archer-d',
|
||||
name: '낙봉 궁병',
|
||||
faction: 'enemy',
|
||||
className: '후방 궁병',
|
||||
classKey: 'archer',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 136,
|
||||
maxHp: 136,
|
||||
attack: 52,
|
||||
move: 3,
|
||||
stats: { might: 120, intelligence: 122, leadership: 128, agility: 134, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 56 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 40 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 52,
|
||||
y: 24
|
||||
},
|
||||
{
|
||||
id: 'luofeng-cavalry-a',
|
||||
name: '낙봉 기병',
|
||||
faction: 'enemy',
|
||||
className: '협곡 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 194,
|
||||
maxHp: 194,
|
||||
attack: 61,
|
||||
move: 5,
|
||||
stats: { might: 176, intelligence: 110, leadership: 148, agility: 138, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 66 },
|
||||
armor: { itemId: 'lamellar-armor', level: 5, exp: 52 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
x: 25,
|
||||
y: 38
|
||||
},
|
||||
{
|
||||
id: 'luofeng-cavalry-b',
|
||||
name: '낙봉 기병',
|
||||
faction: 'enemy',
|
||||
className: '동쪽 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 194,
|
||||
maxHp: 194,
|
||||
attack: 61,
|
||||
move: 5,
|
||||
stats: { might: 176, intelligence: 110, leadership: 148, agility: 138, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 66 },
|
||||
armor: { itemId: 'lamellar-armor', level: 5, exp: 52 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
x: 42,
|
||||
y: 31
|
||||
},
|
||||
{
|
||||
id: 'luofeng-cavalry-c',
|
||||
name: '낙봉 기병',
|
||||
faction: 'enemy',
|
||||
className: '후방 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 53,
|
||||
exp: 88,
|
||||
hp: 202,
|
||||
maxHp: 202,
|
||||
attack: 63,
|
||||
move: 5,
|
||||
stats: { might: 180, intelligence: 112, leadership: 152, agility: 140, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 74 },
|
||||
armor: { itemId: 'lamellar-armor', level: 5, exp: 60 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
x: 51,
|
||||
y: 29
|
||||
},
|
||||
{
|
||||
id: 'luofeng-guard-a',
|
||||
name: '장임 친위대',
|
||||
faction: 'enemy',
|
||||
className: '낙봉 친위대',
|
||||
classKey: 'yellowTurban',
|
||||
level: 53,
|
||||
exp: 88,
|
||||
hp: 204,
|
||||
maxHp: 204,
|
||||
attack: 63,
|
||||
move: 3,
|
||||
stats: { might: 182, intelligence: 112, leadership: 158, agility: 118, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 78 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 64 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 53,
|
||||
y: 18
|
||||
},
|
||||
{
|
||||
id: 'luofeng-guard-b',
|
||||
name: '장임 친위대',
|
||||
faction: 'enemy',
|
||||
className: '성문 친위대',
|
||||
classKey: 'yellowTurban',
|
||||
level: 53,
|
||||
exp: 88,
|
||||
hp: 204,
|
||||
maxHp: 204,
|
||||
attack: 63,
|
||||
move: 3,
|
||||
stats: { might: 182, intelligence: 112, leadership: 158, agility: 118, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'iron-spear', level: 5, exp: 78 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 64 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 55,
|
||||
y: 22
|
||||
},
|
||||
{
|
||||
id: 'luofeng-strategist-a',
|
||||
name: '낙봉 책사',
|
||||
faction: 'enemy',
|
||||
className: '매복 책사',
|
||||
classKey: 'archer',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 136,
|
||||
maxHp: 136,
|
||||
attack: 52,
|
||||
move: 3,
|
||||
stats: { might: 98, intelligence: 136, leadership: 126, agility: 106, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 58 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 42 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 48,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
id: 'luofeng-strategist-b',
|
||||
name: '낙봉 책사',
|
||||
faction: 'enemy',
|
||||
className: '후방 책사',
|
||||
classKey: 'archer',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 136,
|
||||
maxHp: 136,
|
||||
attack: 52,
|
||||
move: 3,
|
||||
stats: { might: 98, intelligence: 136, leadership: 126, agility: 106, luck: 92 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 5, exp: 58 },
|
||||
armor: { itemId: 'cloth-armor', level: 5, exp: 42 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 55,
|
||||
y: 26
|
||||
},
|
||||
{
|
||||
id: 'luofeng-ambusher-a',
|
||||
name: '낙봉 복병',
|
||||
faction: 'enemy',
|
||||
className: '남쪽 복병',
|
||||
classKey: 'bandit',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 140,
|
||||
maxHp: 140,
|
||||
attack: 52,
|
||||
move: 4,
|
||||
stats: { might: 154, intelligence: 108, leadership: 124, agility: 140, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 50 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 36 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 30,
|
||||
y: 36
|
||||
},
|
||||
{
|
||||
id: 'luofeng-ambusher-b',
|
||||
name: '낙봉 복병',
|
||||
faction: 'enemy',
|
||||
className: '북쪽 복병',
|
||||
classKey: 'bandit',
|
||||
level: 51,
|
||||
exp: 84,
|
||||
hp: 140,
|
||||
maxHp: 140,
|
||||
attack: 52,
|
||||
move: 4,
|
||||
stats: { might: 154, intelligence: 108, leadership: 124, agility: 140, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 50 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 36 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 35,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
id: 'luofeng-ambusher-c',
|
||||
name: '낙봉 복병',
|
||||
faction: 'enemy',
|
||||
className: '동쪽 복병',
|
||||
classKey: 'bandit',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 146,
|
||||
maxHp: 146,
|
||||
attack: 54,
|
||||
move: 4,
|
||||
stats: { might: 158, intelligence: 110, leadership: 126, agility: 142, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 58 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 44 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 44,
|
||||
y: 34
|
||||
},
|
||||
{
|
||||
id: 'luofeng-ambusher-d',
|
||||
name: '낙봉 복병',
|
||||
faction: 'enemy',
|
||||
className: '절벽 복병',
|
||||
classKey: 'bandit',
|
||||
level: 52,
|
||||
exp: 86,
|
||||
hp: 146,
|
||||
maxHp: 146,
|
||||
attack: 54,
|
||||
move: 4,
|
||||
stats: { might: 158, intelligence: 110, leadership: 126, agility: 142, luck: 90 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 5, exp: 58 },
|
||||
armor: { itemId: 'rebel-vest', level: 5, exp: 44 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 50,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'luofeng-leader-zhang-ren',
|
||||
name: '장임',
|
||||
faction: 'enemy',
|
||||
className: '낙봉파 주장',
|
||||
classKey: 'rebelLeader',
|
||||
level: 54,
|
||||
exp: 90,
|
||||
hp: 256,
|
||||
maxHp: 256,
|
||||
attack: 66,
|
||||
move: 4,
|
||||
stats: { might: 184, intelligence: 124, leadership: 162, agility: 120, luck: 94 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'leader-axe', level: 5, exp: 66 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 5, exp: 66 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 56,
|
||||
y: 20
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -12553,6 +13140,7 @@ export const twentySixthBattleBonds: BattleBond[] = [...twentyFifthBattleBonds,
|
||||
export const twentySeventhBattleBonds: BattleBond[] = [...twentySixthBattleBonds, ...changshaRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const twentyEighthBattleBonds: BattleBond[] = [...twentySeventhBattleBonds, ...yizhouRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const twentyNinthBattleBonds: BattleBond[] = [...twentyEighthBattleBonds, ...fuPassRecruitBonds].map(cloneBattleBondForScenario);
|
||||
export const thirtiethBattleBonds: BattleBond[] = [...twentyNinthBattleBonds, ...luoCastleRecruitBonds].map(cloneBattleBondForScenario);
|
||||
|
||||
function createEighthBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 22 }, (_, y) =>
|
||||
@@ -13690,6 +14278,66 @@ function createTwentyNinthBattleTerrain(): TerrainType[][] {
|
||||
);
|
||||
}
|
||||
|
||||
function createThirtiethBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 42 }, (_, y) =>
|
||||
Array.from({ length: 60 }, (_, x): TerrainType => {
|
||||
if (x <= 4 && y >= 35) {
|
||||
return 'camp';
|
||||
}
|
||||
if ((x >= 50 && x <= 58 && y >= 14 && y <= 24) || (x >= 46 && x <= 52 && y >= 10 && y <= 16)) {
|
||||
return 'fort';
|
||||
}
|
||||
if (
|
||||
(x >= 17 && x <= 20 && y >= 33 && y <= 35) ||
|
||||
(x >= 36 && x <= 38 && y >= 26 && y <= 28) ||
|
||||
(x >= 43 && x <= 45 && y >= 20 && y <= 22)
|
||||
) {
|
||||
return 'village';
|
||||
}
|
||||
if (
|
||||
(y >= 37 && x >= 4 && x <= 17) ||
|
||||
(x >= 16 && x <= 29 && y >= 33 && y <= 37) ||
|
||||
(x >= 28 && x <= 39 && y >= 28 && y <= 33) ||
|
||||
(x >= 38 && x <= 48 && y >= 23 && y <= 28) ||
|
||||
(x >= 47 && x <= 56 && y >= 18 && y <= 23) ||
|
||||
(x >= 54 && x <= 58 && y >= 19 && y <= 21)
|
||||
) {
|
||||
return 'road';
|
||||
}
|
||||
if ((x === 12 || x === 13) && y >= 0 && y <= 31) {
|
||||
return 'river';
|
||||
}
|
||||
if ((x === 32 || x === 33) && y >= 2 && y <= 26) {
|
||||
return 'river';
|
||||
}
|
||||
if ((x === 42 || x === 43) && y >= 31 && y <= 41) {
|
||||
return 'river';
|
||||
}
|
||||
if (
|
||||
(x >= 4 && x <= 19 && y >= 8 && y <= 26) ||
|
||||
(x >= 20 && x <= 31 && y >= 13 && y <= 27) ||
|
||||
(x >= 34 && x <= 48 && y >= 31 && y <= 41) ||
|
||||
(x >= 46 && x <= 59 && y >= 26 && y <= 40)
|
||||
) {
|
||||
return 'forest';
|
||||
}
|
||||
if (
|
||||
(x >= 0 && x <= 10 && y <= 14) ||
|
||||
(x >= 20 && x <= 31 && y <= 12) ||
|
||||
(x >= 33 && x <= 49 && y >= 3 && y <= 15) ||
|
||||
(x >= 51 && x <= 59 && y <= 13) ||
|
||||
(x >= 24 && x <= 35 && y >= 34 && y <= 41)
|
||||
) {
|
||||
return 'hill';
|
||||
}
|
||||
if ((x <= 2 && y <= 34) || (x >= 58 && y >= 24) || (x >= 46 && x <= 49 && y <= 10)) {
|
||||
return 'cliff';
|
||||
}
|
||||
return 'plain';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||
return {
|
||||
...cloneUnitForScenario(unit),
|
||||
|
||||
@@ -378,7 +378,30 @@ const unitTexture: Record<string, string> = {
|
||||
'luo-ambusher-a': 'unit-rebel',
|
||||
'luo-ambusher-b': 'unit-rebel',
|
||||
'luo-officer-wu-yi': 'unit-guan-yu',
|
||||
'luo-leader-zhang-ren': 'unit-rebel-leader'
|
||||
'luo-leader-zhang-ren': 'unit-rebel-leader',
|
||||
'luofeng-scout-a': 'unit-rebel',
|
||||
'luofeng-scout-b': 'unit-rebel',
|
||||
'luofeng-scout-c': 'unit-rebel',
|
||||
'luofeng-infantry-a': 'unit-rebel',
|
||||
'luofeng-infantry-b': 'unit-rebel',
|
||||
'luofeng-infantry-c': 'unit-rebel',
|
||||
'luofeng-infantry-d': 'unit-rebel',
|
||||
'luofeng-archer-a': 'unit-rebel-archer',
|
||||
'luofeng-archer-b': 'unit-rebel-archer',
|
||||
'luofeng-archer-c': 'unit-rebel-archer',
|
||||
'luofeng-archer-d': 'unit-rebel-archer',
|
||||
'luofeng-cavalry-a': 'unit-rebel-cavalry',
|
||||
'luofeng-cavalry-b': 'unit-rebel-cavalry',
|
||||
'luofeng-cavalry-c': 'unit-rebel-cavalry',
|
||||
'luofeng-guard-a': 'unit-rebel',
|
||||
'luofeng-guard-b': 'unit-rebel',
|
||||
'luofeng-strategist-a': 'unit-rebel-archer',
|
||||
'luofeng-strategist-b': 'unit-rebel-archer',
|
||||
'luofeng-ambusher-a': 'unit-rebel',
|
||||
'luofeng-ambusher-b': 'unit-rebel',
|
||||
'luofeng-ambusher-c': 'unit-rebel',
|
||||
'luofeng-ambusher-d': 'unit-rebel',
|
||||
'luofeng-leader-zhang-ren': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
|
||||
@@ -1220,7 +1243,30 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
||||
'luo-ambusher-a': 'aggressive',
|
||||
'luo-ambusher-b': 'aggressive',
|
||||
'luo-officer-wu-yi': 'guard',
|
||||
'luo-leader-zhang-ren': 'guard'
|
||||
'luo-leader-zhang-ren': 'guard',
|
||||
'luofeng-scout-a': 'aggressive',
|
||||
'luofeng-scout-b': 'aggressive',
|
||||
'luofeng-scout-c': 'aggressive',
|
||||
'luofeng-infantry-a': 'guard',
|
||||
'luofeng-infantry-b': 'guard',
|
||||
'luofeng-infantry-c': 'guard',
|
||||
'luofeng-infantry-d': 'guard',
|
||||
'luofeng-archer-a': 'hold',
|
||||
'luofeng-archer-b': 'hold',
|
||||
'luofeng-archer-c': 'hold',
|
||||
'luofeng-archer-d': 'hold',
|
||||
'luofeng-cavalry-a': 'aggressive',
|
||||
'luofeng-cavalry-b': 'aggressive',
|
||||
'luofeng-cavalry-c': 'aggressive',
|
||||
'luofeng-guard-a': 'guard',
|
||||
'luofeng-guard-b': 'guard',
|
||||
'luofeng-strategist-a': 'hold',
|
||||
'luofeng-strategist-b': 'hold',
|
||||
'luofeng-ambusher-a': 'aggressive',
|
||||
'luofeng-ambusher-b': 'aggressive',
|
||||
'luofeng-ambusher-c': 'aggressive',
|
||||
'luofeng-ambusher-d': 'aggressive',
|
||||
'luofeng-leader-zhang-ren': 'guard'
|
||||
};
|
||||
|
||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||
|
||||
@@ -16,6 +16,7 @@ import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-m
|
||||
import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
|
||||
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
|
||||
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle-map.svg';
|
||||
import thirtiethBattleMapUrl from '../../assets/images/battle/thirtieth-battle-map.svg';
|
||||
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
||||
import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
|
||||
import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
|
||||
@@ -116,6 +117,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('battle-map-twenty-seventh', twentySeventhBattleMapUrl);
|
||||
this.load.image('battle-map-twenty-eighth', twentyEighthBattleMapUrl);
|
||||
this.load.image('battle-map-twenty-ninth', twentyNinthBattleMapUrl);
|
||||
this.load.image('battle-map-thirtieth', thirtiethBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
|
||||
@@ -316,7 +316,8 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
'twenty-sixth-battle-changsha-veteran',
|
||||
'twenty-seventh-battle-yizhou-relief-road',
|
||||
'twenty-eighth-battle-fu-pass-entry',
|
||||
'twenty-ninth-battle-luo-outer-wall'
|
||||
'twenty-ninth-battle-luo-outer-wall',
|
||||
'thirtieth-battle-luofeng-ambush'
|
||||
],
|
||||
nextHints: ['형주 방위', '익주 진입', '방통·법정 합류']
|
||||
},
|
||||
@@ -359,7 +360,8 @@ const campBattleIds = {
|
||||
twentySixth: 'twenty-sixth-battle-changsha-veteran',
|
||||
twentySeventh: 'twenty-seventh-battle-yizhou-relief-road',
|
||||
twentyEighth: 'twenty-eighth-battle-fu-pass-entry',
|
||||
twentyNinth: 'twenty-ninth-battle-luo-outer-wall'
|
||||
twentyNinth: 'twenty-ninth-battle-luo-outer-wall',
|
||||
thirtieth: 'thirtieth-battle-luofeng-ambush'
|
||||
} as const;
|
||||
|
||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||
@@ -682,6 +684,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
{ unitId: 'zhao-yun', reason: '넓어진 외곽 맵에서 정찰과 기병 대응을 맡기 좋습니다.' }
|
||||
],
|
||||
note: '낙성 외곽전은 열다섯 명 중 여섯 명만 출전합니다. 법정을 데려가면 설득과 정보가 강해지고, 무장을 더 넣으면 전열이 단단해지는 선택지가 생깁니다.'
|
||||
},
|
||||
'thirtieth-battle-luofeng-ambush': {
|
||||
maxUnits: 6,
|
||||
recommended: [
|
||||
{ unitId: 'liu-bei', reason: '유비가 살아 남아야 익주 백성에게 명분을 이어갈 수 있습니다.' },
|
||||
{ unitId: 'pang-tong', reason: '낙봉파 매복을 읽는 핵심 책사이며 생존 조건의 중심입니다.' },
|
||||
{ unitId: 'wu-yi', reason: '오의는 낙성 안쪽 길과 고지 매복 위치를 알고 있는 새 합류 장수입니다.' },
|
||||
{ unitId: 'fa-zheng', reason: '법정은 익주 장수들의 심리와 장임의 방어 습관을 꿰뚫습니다.' },
|
||||
{ unitId: 'zhao-yun', reason: '좁은 길에서 급히 방통을 보호하거나 궁병을 끊는 기동력이 좋습니다.' },
|
||||
{ unitId: 'huang-zhong', reason: '고지 궁병과 매복 부대를 안정적으로 견제하는 원거리 전력입니다.' }
|
||||
],
|
||||
note: '낙봉파 매복전은 열여섯 명 중 여섯 명만 출전합니다. 오의가 새로 후보에 들어오며, 방통 생존과 매복 대응 때문에 책사와 기동 무장 사이의 선택이 더 중요해집니다.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3115,6 +3129,87 @@ const campDialogues: CampDialogue[] = [
|
||||
rewardExp: 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-pang-after-luofeng',
|
||||
title: '살아 돌아온 봉추',
|
||||
availableAfterBattleIds: [campBattleIds.thirtieth],
|
||||
unitIds: ['liu-bei', 'pang-tong'],
|
||||
bondId: 'liu-bei__pang-tong',
|
||||
rewardExp: 34,
|
||||
lines: [
|
||||
'유비: 낙봉파의 화살이 그대에게 닿을 뻔했소. 오늘 그대를 잃었다면 익주 길은 물론 내 마음도 크게 꺾였을 것이오.',
|
||||
'방통: 주공께서 길을 의심해 주셨기에 제가 길 위에서 목숨을 붙였습니다. 이제는 빠른 계책보다 오래 버틸 계책을 먼저 보겠습니다.',
|
||||
'유비: 그대의 말이 오늘 군영의 경계가 되었소. 다음 낙성 본성 앞에서도 그 눈을 믿겠소.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'thank-pang-tong-survival-luofeng',
|
||||
label: '방통의 생환을 기뻐한다',
|
||||
response: '유비가 직접 술잔을 낮추자 방통도 장난스러운 미소를 거두고 다음 전장의 방비도를 다시 펼쳤습니다.',
|
||||
rewardExp: 13
|
||||
},
|
||||
{
|
||||
id: 'ask-pang-tong-next-risk-luofeng',
|
||||
label: '다음 위험을 묻는다',
|
||||
response: '방통은 낙성 본성의 성문, 물길, 민심을 한 줄로 엮어 보이며 무리한 돌파를 피해야 한다고 답했습니다.',
|
||||
rewardExp: 12
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'pang-wu-after-luofeng',
|
||||
title: '매복을 읽은 두 사람',
|
||||
availableAfterBattleIds: [campBattleIds.thirtieth],
|
||||
unitIds: ['pang-tong', 'wu-yi'],
|
||||
bondId: 'pang-tong__wu-yi',
|
||||
rewardExp: 32,
|
||||
lines: [
|
||||
'오의: 장임이 숨어들 만한 길을 말씀드렸을 뿐인데, 선생께서는 적의 숨소리까지 짚어 내셨습니다.',
|
||||
'방통: 길을 아는 사람과 마음을 읽는 사람이 같이 보면, 매복도 길 표지처럼 보이는 법이지요.',
|
||||
'오의: 다음 성문 앞에서도 제가 아는 익주의 길을 숨기지 않겠습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'share-luofeng-slope-map',
|
||||
label: '낙봉파 지도를 보완한다',
|
||||
response: '오의가 놓친 샛길을 더하고 방통이 그 길마다 적의 시야를 적어 넣자, 낙성 진입로가 더 분명해졌습니다.',
|
||||
rewardExp: 12
|
||||
},
|
||||
{
|
||||
id: 'honor-wu-yi-guide',
|
||||
label: '오의의 안내를 치하한다',
|
||||
response: '방통이 오의의 공을 군영 앞에서 분명히 말하자, 새로 합류한 장수의 어깨에서 긴장이 조금 풀렸습니다.',
|
||||
rewardExp: 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'zhuge-fa-after-luofeng',
|
||||
title: '낙성 본성의 문',
|
||||
availableAfterBattleIds: [campBattleIds.thirtieth],
|
||||
unitIds: ['zhuge-liang', 'fa-zheng'],
|
||||
bondId: 'zhuge-liang__fa-zheng',
|
||||
rewardExp: 32,
|
||||
lines: [
|
||||
'제갈량: 낙봉파의 매복을 깨뜨렸으니 장임은 성문 뒤에서 더 단단히 버틸 것입니다.',
|
||||
'법정: 성문은 군사만으로 열리지 않습니다. 익주의 장수들이 유비공을 두려워하지 않게 만드는 말도 함께 준비해야 합니다.',
|
||||
'제갈량: 그렇다면 다음 싸움은 성벽을 치는 전투이자 마음을 여는 전투가 되겠군요.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'prepare-luo-castle-parley',
|
||||
label: '항복 권고문을 준비한다',
|
||||
response: '두 책사가 문장을 다듬자 낙성 안쪽으로 보낼 글에는 위협보다 약속이 먼저 놓였습니다.',
|
||||
rewardExp: 12
|
||||
},
|
||||
{
|
||||
id: 'mark-main-gate-weakness',
|
||||
label: '성문 약점을 표시한다',
|
||||
response: '법정의 내부 정보와 제갈량의 배치도가 겹쳐지며, 다음 전장의 첫 돌파 지점이 좁혀졌습니다.',
|
||||
rewardExp: 11
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3984,6 +4079,66 @@ const campVisits: CampVisitDefinition[] = [
|
||||
itemRewards: ['매복 대응 초안 1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'luofeng-slope-shrine',
|
||||
title: '낙봉파 길가의 제단',
|
||||
location: '낙봉파 비탈',
|
||||
availableAfterBattleIds: [campBattleIds.thirtieth],
|
||||
bondId: 'liu-bei__pang-tong',
|
||||
description: '유비와 방통이 매복을 넘긴 비탈의 작은 제단에 들러, 살아 남은 병사들과 다음 길의 마음을 다집니다.',
|
||||
lines: [
|
||||
'방통: 이곳에서 한 걸음만 늦었다면 제 이름은 길가의 이야기가 되었을지도 모릅니다.',
|
||||
'유비: 그러니 살아 남은 오늘을 가볍게 쓰지 맙시다. 다음 성문 앞에서도 백성을 먼저 보겠소.',
|
||||
'제단 앞에 놓인 낡은 향이 꺼지고, 병사들은 낙봉파를 넘었다는 사실을 뒤늦게 실감했습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'comfort-luofeng-wounded',
|
||||
label: '부상병을 위문한다',
|
||||
response: '유비가 부상병의 손을 잡자 방통도 다음 행군표에서 무리한 구간을 줄였습니다.',
|
||||
bondExp: 26,
|
||||
itemRewards: ['상처약 3']
|
||||
},
|
||||
{
|
||||
id: 'offer-luofeng-prayer',
|
||||
label: '전몰자를 기린다',
|
||||
response: '군영의 말소리가 낮아지고, 방통은 다음 계책에 병사들이 돌아올 길을 먼저 적었습니다.',
|
||||
bondExp: 24,
|
||||
gold: 340,
|
||||
itemRewards: ['맑은 향 1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'luo-castle-main-council',
|
||||
title: '낙성 본성 공략 회의',
|
||||
location: '낙성 앞 군영',
|
||||
availableAfterBattleIds: [campBattleIds.thirtieth],
|
||||
bondId: 'fa-zheng__wu-yi',
|
||||
description: '법정과 오의가 낙성 본성의 문, 물길, 수비 장수들의 성향을 대조하며 다음 공략의 밑그림을 만듭니다.',
|
||||
lines: [
|
||||
'법정: 장임은 강하지만, 낙성 안의 모든 장수가 같은 마음으로 버티는 것은 아닙니다.',
|
||||
'오의: 제가 아는 이름들을 적겠습니다. 문을 여는 사람과 끝까지 막을 사람을 나누어 보지요.',
|
||||
'두 사람이 성문 지도를 다시 그리자, 다음 전장의 목표가 단순한 격파가 아니라 설득을 포함한 공략으로 바뀌었습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'sort-luo-officer-names',
|
||||
label: '장수 명단을 분류한다',
|
||||
response: '익주 장수들의 이름 옆에 설득, 견제, 격파라는 표시가 붙으며 다음 전장의 말길이 열렸습니다.',
|
||||
bondExp: 26,
|
||||
itemRewards: ['낙성 장수 명단 1']
|
||||
},
|
||||
{
|
||||
id: 'trace-luo-supply-water',
|
||||
label: '보급 수로를 추적한다',
|
||||
response: '오의가 작은 물길을 짚자 법정은 그 길이 성 안의 불안을 흔들 수 있다고 판단했습니다.',
|
||||
bondExp: 24,
|
||||
gold: 360,
|
||||
itemRewards: ['낙성 수로도 1']
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -4069,7 +4224,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (battleId === campBattleIds.twentyNinth) {
|
||||
if (battleId === campBattleIds.thirtieth || battleId === campBattleIds.twentyNinth) {
|
||||
this.campaign = ensureCampaignRosterUnits(
|
||||
[
|
||||
...xuzhouRecruitUnits,
|
||||
@@ -4310,6 +4465,9 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private currentCampTitle() {
|
||||
const battleId = this.currentCampBattleId();
|
||||
if (battleId === campBattleIds.thirtieth) {
|
||||
return '낙봉파 매복전 후 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.twentyNinth) {
|
||||
return '낙성 외곽전 후 군영';
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
seventeenthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
thirtiethBattleScenario,
|
||||
twentyEighthBattleScenario,
|
||||
twentyFirstBattleScenario,
|
||||
twentySecondBattleScenario,
|
||||
@@ -357,7 +358,8 @@ export class TitleScene extends Phaser.Scene {
|
||||
campaign.step === 'twenty-sixth-camp' ||
|
||||
campaign.step === 'twenty-seventh-camp' ||
|
||||
campaign.step === 'twenty-eighth-camp' ||
|
||||
campaign.step === 'twenty-ninth-camp'
|
||||
campaign.step === 'twenty-ninth-camp' ||
|
||||
campaign.step === 'thirtieth-camp'
|
||||
) {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
@@ -508,6 +510,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'thirtieth-battle') {
|
||||
this.scene.start('BattleScene', { battleId: thirtiethBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
@@ -99,7 +99,9 @@ export type CampaignStep =
|
||||
| 'twenty-eighth-battle'
|
||||
| 'twenty-eighth-camp'
|
||||
| 'twenty-ninth-battle'
|
||||
| 'twenty-ninth-camp';
|
||||
| 'twenty-ninth-camp'
|
||||
| 'thirtieth-battle'
|
||||
| 'thirtieth-camp';
|
||||
|
||||
export type CampaignUnitProgressSnapshot = {
|
||||
unitId: string;
|
||||
@@ -190,7 +192,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
||||
'twenty-sixth-battle-changsha-veteran': { victory: 'twenty-sixth-camp', retry: 'twenty-sixth-battle' },
|
||||
'twenty-seventh-battle-yizhou-relief-road': { victory: 'twenty-seventh-camp', retry: 'twenty-seventh-battle' },
|
||||
'twenty-eighth-battle-fu-pass-entry': { victory: 'twenty-eighth-camp', retry: 'twenty-eighth-battle' },
|
||||
'twenty-ninth-battle-luo-outer-wall': { victory: 'twenty-ninth-camp', retry: 'twenty-ninth-battle' }
|
||||
'twenty-ninth-battle-luo-outer-wall': { victory: 'twenty-ninth-camp', retry: 'twenty-ninth-battle' },
|
||||
'thirtieth-battle-luofeng-ambush': { victory: 'thirtieth-camp', retry: 'thirtieth-battle' }
|
||||
};
|
||||
|
||||
export type CampaignSaveSlotSummary = {
|
||||
|
||||
Reference in New Issue
Block a user