Add Jing Province entry chapter

This commit is contained in:
2026-06-23 07:42:11 +09:00
parent 302f0e0a76
commit d26a16d2a1
11 changed files with 1120 additions and 29 deletions

View File

@@ -0,0 +1,86 @@
<svg xmlns="http://www.w3.org/2000/svg" width="4600" height="3200" viewBox="0 0 4600 3200">
<defs>
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="5" seed="23"/>
<feColorMatrix type="saturate" values="0.6"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
<linearGradient id="plain" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#aaa05f"/>
<stop offset="0.55" stop-color="#76845a"/>
<stop offset="1" stop-color="#526949"/>
</linearGradient>
<linearGradient id="river" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#142a40"/>
<stop offset="0.45" stop-color="#2b7591"/>
<stop offset="1" stop-color="#102238"/>
</linearGradient>
<linearGradient id="road" x1="0" x2="1">
<stop offset="0" stop-color="#987042"/>
<stop offset="0.5" stop-color="#c7aa67"/>
<stop offset="1" stop-color="#74512e"/>
</linearGradient>
<pattern id="grass" width="130" height="130" patternUnits="userSpaceOnUse">
<rect width="130" height="130" fill="url(#plain)"/>
<path d="M18 92 L44 70 M70 104 L104 78 M24 34 L48 20 M94 38 L122 22" stroke="#2a4628" stroke-opacity="0.35" stroke-width="8"/>
<circle cx="28" cy="110" r="8" fill="#b7ad66" opacity="0.38"/>
<circle cx="92" cy="28" r="7" fill="#344e2c" opacity="0.4"/>
</pattern>
<pattern id="waves" width="170" height="96" patternUnits="userSpaceOnUse">
<path d="M0 42 C42 18 83 72 128 44 C145 34 156 32 170 38" fill="none" stroke="#c3edf1" stroke-opacity="0.23" stroke-width="9"/>
<path d="M0 82 C38 58 84 108 130 82 C148 72 158 68 170 72" fill="none" stroke="#0b314b" stroke-opacity="0.34" stroke-width="10"/>
</pattern>
<radialGradient id="hill" cx="50%" cy="45%" r="60%">
<stop offset="0" stop-color="#9f9867"/>
<stop offset="0.55" stop-color="#626938"/>
<stop offset="1" stop-color="#303a24"/>
</radialGradient>
<radialGradient id="forest" cx="50%" cy="50%" r="62%">
<stop offset="0" stop-color="#536834"/>
<stop offset="0.58" stop-color="#243b25"/>
<stop offset="1" stop-color="#142417"/>
</radialGradient>
</defs>
<rect width="4600" height="3200" fill="#151714"/>
<rect width="4600" height="3200" fill="url(#grass)" filter="url(#grain)"/>
<path d="M1980 -100 C2140 520 2120 1150 2010 1590 C1900 2060 2060 2540 1940 3300 L2200 3300 C2310 2580 2160 2110 2260 1660 C2380 1120 2370 510 2180 -100 Z" fill="url(#river)"/>
<path d="M2870 -80 C3040 510 3060 930 2970 1320 C2880 1700 2940 1980 3040 2130 L3210 2130 C3120 1740 3240 1390 3180 1050 C3120 640 3190 300 3060 -80 Z" fill="url(#river)"/>
<path d="M2460 2380 C2600 2680 2670 2920 2630 3300 L2810 3300 C2890 2880 2810 2600 2660 2380 Z" fill="url(#river)"/>
<path d="M1980 -100 C2140 520 2120 1150 2010 1590 C1900 2060 2060 2540 1940 3300 L2200 3300 C2310 2580 2160 2110 2260 1660 C2380 1120 2370 510 2180 -100 Z" fill="url(#waves)" opacity="0.75"/>
<path d="M2870 -80 C3040 510 3060 930 2970 1320 C2880 1700 2940 1980 3040 2130 L3210 2130 C3120 1740 3240 1390 3180 1050 C3120 640 3190 300 3060 -80 Z" fill="url(#waves)" opacity="0.72"/>
<path d="M290 2510 C700 2480 1260 2520 1690 2370 C2160 2210 2510 2020 3040 1880 C3460 1768 3880 1760 4210 1790" fill="none" stroke="url(#road)" stroke-width="150" stroke-linecap="round"/>
<path d="M900 2550 C1180 2300 1430 2100 1840 2040 C2260 1980 2590 1850 2860 1720" fill="none" stroke="#6f4c2c" stroke-opacity="0.34" stroke-width="48" stroke-dasharray="90 80"/>
<path d="M3800 1450 L3820 2390" stroke="url(#road)" stroke-width="125" stroke-linecap="round"/>
<g opacity="0.92">
<path d="M250 920 C580 650 960 540 1430 570 C1500 900 1380 1260 1070 1460 C720 1430 430 1220 250 920 Z" fill="url(#forest)"/>
<path d="M380 90 C760 30 1020 90 1210 310 C1020 590 690 720 260 690 C130 420 160 240 380 90 Z" fill="url(#forest)"/>
<path d="M3180 180 C3580 30 4090 80 4460 260 C4300 600 3970 780 3560 770 C3290 660 3180 430 3180 180 Z" fill="url(#forest)"/>
<path d="M1570 2540 C1870 2440 2110 2560 2280 2840 C2100 3110 1780 3190 1510 3060 C1420 2840 1450 2670 1570 2540 Z" fill="url(#forest)"/>
</g>
<g opacity="0.9">
<ellipse cx="1410" cy="470" rx="510" ry="260" fill="url(#hill)"/>
<ellipse cx="3630" cy="2790" rx="620" ry="330" fill="url(#hill)"/>
<ellipse cx="4150" cy="590" rx="420" ry="250" fill="url(#hill)"/>
<path d="M80 0 C180 500 160 980 0 1550 L0 0 Z" fill="#2f3327"/>
<path d="M4510 0 C4420 420 4450 800 4600 1220 L4600 0 Z" fill="#2f3327"/>
<path d="M4470 2440 C4500 2790 4540 3020 4600 3200 L4600 2440 Z" fill="#2f3327"/>
</g>
<g opacity="0.94">
<rect x="90" y="2430" width="590" height="520" rx="20" fill="#2f4234" stroke="#d7bb72" stroke-width="20"/>
<path d="M170 2630 L350 2490 L530 2630 Z" fill="#d9a13d"/>
<rect x="205" y="2630" width="300" height="190" fill="#6f8348"/>
<rect x="1210" y="2300" width="330" height="270" fill="#695738" stroke="#e2c575" stroke-width="16"/>
<path d="M1210 2300 L1375 2180 L1540 2300" fill="#d59b34"/>
<rect x="2460" y="1780" width="350" height="260" fill="#5f6f43" stroke="#dfc171" stroke-width="16"/>
<path d="M2460 1780 L2635 1655 L2810 1780" fill="#c98932"/>
<rect x="3360" y="1490" width="310" height="260" fill="#596840" stroke="#d9bd72" stroke-width="16"/>
<path d="M3360 1490 L3515 1380 L3670 1490" fill="#d19637"/>
<path d="M3820 1370 L4500 1370 L4500 2230 L3820 2230 Z" fill="#263236" stroke="#c89f53" stroke-width="32"/>
<path d="M3930 1510 L4390 1510 M3930 1680 L4390 1680 M3930 1850 L4390 1850 M3930 2020 L4390 2020" stroke="#775630" stroke-width="40"/>
<path d="M3860 1325 L4140 1210 L4500 1340" fill="#8c6c3f"/>
</g>
<g opacity="0.14">
<path d="M0 0 H4600 M0 100 H4600 M0 200 H4600 M0 300 H4600 M0 400 H4600 M0 500 H4600 M0 600 H4600 M0 700 H4600 M0 800 H4600 M0 900 H4600 M0 1000 H4600 M0 1100 H4600 M0 1200 H4600 M0 1300 H4600 M0 1400 H4600 M0 1500 H4600 M0 1600 H4600 M0 1700 H4600 M0 1800 H4600 M0 1900 H4600 M0 2000 H4600 M0 2100 H4600 M0 2200 H4600 M0 2300 H4600 M0 2400 H4600 M0 2500 H4600 M0 2600 H4600 M0 2700 H4600 M0 2800 H4600 M0 2900 H4600 M0 3000 H4600 M0 3100 H4600" stroke="#0c0f10" stroke-width="3"/>
<path d="M0 0 V3200 M100 0 V3200 M200 0 V3200 M300 0 V3200 M400 0 V3200 M500 0 V3200 M600 0 V3200 M700 0 V3200 M800 0 V3200 M900 0 V3200 M1000 0 V3200 M1100 0 V3200 M1200 0 V3200 M1300 0 V3200 M1400 0 V3200 M1500 0 V3200 M1600 0 V3200 M1700 0 V3200 M1800 0 V3200 M1900 0 V3200 M2000 0 V3200 M2100 0 V3200 M2200 0 V3200 M2300 0 V3200 M2400 0 V3200 M2500 0 V3200 M2600 0 V3200 M2700 0 V3200 M2800 0 V3200 M2900 0 V3200 M3000 0 V3200 M3100 0 V3200 M3200 0 V3200 M3300 0 V3200 M3400 0 V3200 M3500 0 V3200 M3600 0 V3200 M3700 0 V3200 M3800 0 V3200 M3900 0 V3200 M4000 0 V3200 M4100 0 V3200 M4200 0 V3200 M4300 0 V3200 M4400 0 V3200 M4500 0 V3200" stroke="#0c0f10" stroke-width="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -47,6 +47,10 @@ import {
twentySecondBattleMap,
twentySecondBattleUnits,
twentySecondBattleVictoryPages,
twentyThirdBattleBonds,
twentyThirdBattleMap,
twentyThirdBattleUnits,
twentyThirdBattleVictoryPages,
twentiethBattleBonds,
twentiethBattleMap,
twentiethBattleUnits,
@@ -115,7 +119,8 @@ export type BattleScenarioId =
| 'nineteenth-battle-changban-refuge'
| 'twentieth-battle-jiangdong-envoy'
| 'twenty-first-battle-red-cliffs-vanguard'
| 'twenty-second-battle-red-cliffs-fire';
| 'twenty-second-battle-red-cliffs-fire'
| 'twenty-third-battle-jingzhou-south-entry';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1361,6 +1366,69 @@ export const twentySecondBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const twentyThirdBattleScenario: BattleScenarioDefinition = {
id: 'twenty-third-battle-jingzhou-south-entry',
title: '형주 남부 진입전',
victoryConditionLabel: '형도영 퇴각',
defeatConditionLabel: '유비 또는 제갈량 퇴각',
openingObjectiveLines: [
'적벽의 불길로 조조의 기세가 꺾인 지금, 유비군은 형주 남부의 길과 민심을 열어야 합니다.',
'형도영의 가병은 영릉으로 이어지는 길목과 마을을 붙잡고 있습니다. 성채만 몰아치면 마을이 흔들리니 보급 거점을 함께 지키십시오.',
'유비와 제갈량을 지키며 형도영을 물리치고, 남부 마을을 안정시키면 마량이 군영에 합류합니다.'
],
map: twentyThirdBattleMap,
units: twentyThirdBattleUnits,
bonds: twentyThirdBattleBonds,
mapTextureKey: 'battle-map-twenty-third',
leaderUnitId: 'jing-south-leader-xing-daorong',
quickVictoryTurnLimit: 32,
baseVictoryGold: 3020,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '형도영 퇴각',
rewardGold: 2000,
unitId: 'jing-south-leader-xing-daorong'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 600,
unitId: 'liu-bei'
},
{
id: 'zhuge-liang',
kind: 'keep-unit-alive',
label: '제갈량 생존',
rewardGold: 900,
unitId: 'zhuge-liang'
},
{
id: 'southern-village',
kind: 'secure-terrain',
label: '남부 마을 안정',
rewardGold: 980,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '32턴 이내 승리',
rewardGold: 760,
maxTurn: 32
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'zhuge-liang' }
],
itemRewards: ['콩 9', '상처약 7', '탁주 4', '형주 진입 문서 +1', '마량 합류'],
victoryPages: twentyThirdBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -1385,7 +1453,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'nineteenth-battle-changban-refuge': nineteenthBattleScenario,
'twentieth-battle-jiangdong-envoy': twentiethBattleScenario,
'twenty-first-battle-red-cliffs-vanguard': twentyFirstBattleScenario,
'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario
'twenty-second-battle-red-cliffs-fire': twentySecondBattleScenario,
'twenty-third-battle-jingzhou-south-entry': twentyThirdBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -19,6 +19,7 @@ import {
thirteenthBattleScenario,
twentyFirstBattleScenario,
twentySecondBattleScenario,
twentyThirdBattleScenario,
twentiethBattleScenario,
twelfthBattleScenario,
thirdBattleScenario,
@@ -62,6 +63,8 @@ import {
twentyFirstBattleVictoryPages,
twentySecondBattleIntroPages,
twentySecondBattleVictoryPages,
twentyThirdBattleIntroPages,
twentyThirdBattleVictoryPages,
twentiethBattleIntroPages,
twentiethBattleVictoryPages,
twelfthBattleIntroPages,
@@ -305,13 +308,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[twentySecondBattleScenario.id]: {
afterBattleId: twentySecondBattleScenario.id,
eyebrow: '다음 장 준비',
title: '형주 확보 준비',
eyebrow: '다음 장',
title: twentyThirdBattleScenario.title,
description:
'적벽의 불길로 조조의 남하가 꺾였습니다. 이제 유비군은 머물 땅을 마련하기 위해 형주 남부와 익주로 이어질 근거지를 준비해야 합니다.',
rewardHint: '다음 장: 형주 남부 확보 준비 중',
pages: twentySecondBattleVictoryPages,
unavailableNotice: '형주 남부 확보 장은 다음 작업에서 이어집니다. 군영에서 전후 수습, 공명, 보급을 정비하십시오.'
'적벽의 불길로 조조의 남하가 꺾였습니다. 유비군은 이제 형주 남부의 길목을 열고, 백성을 먼저 안정시키며 머물 땅을 마련해야 합니다.',
rewardHint: `예상 보상: ${twentyThirdBattleScenario.title} 개방 / 마량 합류`,
nextBattleId: twentyThirdBattleScenario.id,
campaignStep: 'twenty-third-battle',
pages: [...twentySecondBattleVictoryPages, ...twentyThirdBattleIntroPages]
},
[twentyThirdBattleScenario.id]: {
afterBattleId: twentyThirdBattleScenario.id,
eyebrow: '다음 장 준비',
title: '영릉 안정과 다음 군현',
description:
'형주 남부의 첫 길목을 열고 마량이 합류했습니다. 이제 계양, 무릉, 장사까지 차례로 설득하고 지켜야 형주·익주 확보의 기반이 단단해집니다.',
rewardHint: '다음 장: 계양·무릉·장사 확보 준비 중',
pages: twentyThirdBattleVictoryPages,
unavailableNotice: '다음 형주 남부 군현 확보 장은 다음 작업에서 이어집니다. 군영에서 마량과 대화하고 출전 후보를 정비하십시오.'
}
};

View File

@@ -1448,6 +1448,68 @@ export const twentySecondBattleVictoryPages: StoryPage[] = [
}
];
export const twentyThirdBattleIntroPages: StoryPage[] = [
{
id: 'twenty-third-jingzhou-cause',
bgm: 'militia-theme',
chapter: '형주 남부의 문',
background: 'story-militia',
speaker: '유비',
portrait: 'liuBei',
text: '적벽의 불길은 조조를 막았지만 백성이 기대어 살 땅은 아직 없소. 형주 남부의 군현을 힘으로 짓밟지 말고, 먼저 길과 민심을 열어야 하오.'
},
{
id: 'twenty-third-zhuge-plan',
bgm: 'battle-prep',
chapter: '군현을 여는 계책',
background: 'story-three-heroes',
speaker: '제갈량',
text: '영릉의 유도는 아직 마음을 정하지 못했고, 형도영은 길목을 막아 자신의 힘을 보이려 합니다. 그를 물리치되 백성을 다치게 하지 않으면 형주 남부가 움직일 것입니다.'
},
{
id: 'twenty-third-ma-liang-rumor',
bgm: 'story-dark',
chapter: '백미의 소문',
background: 'story-liu-bei',
speaker: '손건',
text: '형주 선비들 사이에 마량이라는 이름이 오갑니다. 눈썹이 희어 백미라 불리는 사람인데, 남부 군현의 민심과 길을 누구보다 잘 안다고 합니다.'
},
{
id: 'twenty-third-jingzhou-sortie',
bgm: 'battle-prep',
chapter: '형주 남부 진입전',
background: 'story-sortie',
speaker: '제갈량',
text: '형도영의 가병이 남부 길목과 마을을 막고 있습니다. 성급히 몰아붙이면 백성이 흩어지니, 전열을 세우고 마을을 지키며 길목을 확보하십시오.'
}
];
export const twentyThirdBattleVictoryPages: StoryPage[] = [
{
id: 'twenty-third-victory-southern-road',
bgm: 'militia-theme',
chapter: '남부 길목이 열리다',
background: 'story-sortie',
text: '형도영이 물러서자 남부 군현으로 이어지는 길이 열렸습니다. 유비군은 약탈 없이 마을을 안정시키며 적벽 이후 처음으로 머물 땅의 윤곽을 얻었습니다.'
},
{
id: 'twenty-third-ma-liang-joins',
bgm: 'battle-prep',
chapter: '마량의 합류',
background: 'story-militia',
speaker: '마량',
text: '주공께서 백성을 먼저 살피셨다는 말을 들었습니다. 형주 남부의 길과 사람을 잇는 일이라면, 제 작은 글과 말도 보탬이 될 것입니다.'
},
{
id: 'twenty-third-next-commandery',
bgm: 'story-dark',
chapter: '군현을 차례로',
background: 'story-three-heroes',
speaker: '제갈량',
text: '영릉의 문은 열렸지만 형주 남부 전체가 움직인 것은 아닙니다. 계양, 무릉, 장사까지 차례로 설득하고 지켜야 비로소 근거지가 됩니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1725,6 +1787,12 @@ export const twentySecondBattleMap: BattleMap = {
terrain: createTwentySecondBattleTerrain()
};
export const twentyThirdBattleMap: BattleMap = {
width: 46,
height: 32,
terrain: createTwentyThirdBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -3681,6 +3749,30 @@ export const zhugeRecruitUnits: UnitData[] = [
}
];
export const jingzhouRecruitUnits: UnitData[] = [
{
id: 'ma-liang',
name: '마량',
faction: 'ally',
className: '백미 책사',
classKey: 'strategist',
level: 11,
exp: 0,
hp: 30,
maxHp: 30,
attack: 8,
move: 4,
stats: { might: 36, intelligence: 92, leadership: 80, agility: 70, luck: 88 },
equipment: {
weapon: { itemId: 'white-feather-fan', level: 1, exp: 10 },
armor: { itemId: 'cloth-armor', level: 2, exp: 8 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 7,
y: 22
}
];
const eighthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 2, y: 17 },
'guan-yu': { x: 3, y: 17 },
@@ -8600,6 +8692,384 @@ export const twentySecondBattleUnits: UnitData[] = [
}
];
const twentyThirdBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 4, y: 25 },
'guan-yu': { x: 6, y: 23 },
'zhang-fei': { x: 6, y: 27 },
'jian-yong': { x: 3, y: 28 },
'mi-zhu': { x: 5, y: 29 },
'sun-qian': { x: 8, y: 25 },
'zhao-yun': { x: 8, y: 22 },
'zhuge-liang': { x: 6, y: 24 }
};
export const twentyThirdBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits
].map((unit) => placeScenarioUnit(unit, twentyThirdBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
{
id: 'jing-south-scout-a',
name: '형주 남부 척후',
faction: 'enemy',
className: '남부 척후',
classKey: 'bandit',
level: 30,
exp: 48,
hp: 82,
maxHp: 82,
attack: 33,
move: 4,
stats: { might: 114, intelligence: 84, leadership: 90, agility: 110, luck: 76 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 18 },
armor: { itemId: 'rebel-vest', level: 4, exp: 8 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 13,
y: 24
},
{
id: 'jing-south-scout-b',
name: '형주 남부 척후',
faction: 'enemy',
className: '마을 척후',
classKey: 'bandit',
level: 30,
exp: 48,
hp: 82,
maxHp: 82,
attack: 33,
move: 4,
stats: { might: 114, intelligence: 84, leadership: 90, agility: 110, luck: 76 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 4, exp: 18 },
armor: { itemId: 'rebel-vest', level: 4, exp: 8 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 14,
y: 28
},
{
id: 'jing-south-infantry-a',
name: '영릉 가병',
faction: 'enemy',
className: '영릉 가병',
classKey: 'yellowTurban',
level: 31,
exp: 50,
hp: 108,
maxHp: 108,
attack: 35,
move: 3,
stats: { might: 124, intelligence: 84, leadership: 104, agility: 92, luck: 76 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 30 },
armor: { itemId: 'lamellar-armor', level: 4, exp: 18 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 18,
y: 22
},
{
id: 'jing-south-infantry-b',
name: '영릉 가병',
faction: 'enemy',
className: '마을 가병',
classKey: 'yellowTurban',
level: 31,
exp: 50,
hp: 108,
maxHp: 108,
attack: 35,
move: 3,
stats: { might: 124, intelligence: 84, leadership: 104, agility: 92, luck: 76 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 30 },
armor: { itemId: 'lamellar-armor', level: 4, exp: 18 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 20,
y: 25
},
{
id: 'jing-south-infantry-c',
name: '영릉 가병',
faction: 'enemy',
className: '관문 가병',
classKey: 'yellowTurban',
level: 31,
exp: 52,
hp: 110,
maxHp: 110,
attack: 35,
move: 3,
stats: { might: 125, intelligence: 85, leadership: 106, agility: 92, luck: 76 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 34 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 20 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 28,
y: 18
},
{
id: 'jing-south-infantry-d',
name: '영릉 가병',
faction: 'enemy',
className: '관문 가병',
classKey: 'yellowTurban',
level: 31,
exp: 52,
hp: 110,
maxHp: 110,
attack: 35,
move: 3,
stats: { might: 125, intelligence: 85, leadership: 106, agility: 92, luck: 76 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 34 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 20 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 30,
y: 21
},
{
id: 'jing-south-archer-a',
name: '영릉 궁병',
faction: 'enemy',
className: '마을 궁병',
classKey: 'archer',
level: 30,
exp: 48,
hp: 80,
maxHp: 80,
attack: 34,
move: 3,
stats: { might: 108, intelligence: 98, leadership: 94, agility: 106, luck: 78 },
equipment: {
weapon: { itemId: 'short-bow', level: 4, exp: 26 },
armor: { itemId: 'cloth-armor', level: 4, exp: 16 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 18,
y: 18
},
{
id: 'jing-south-archer-b',
name: '영릉 궁병',
faction: 'enemy',
className: '언덕 궁병',
classKey: 'archer',
level: 31,
exp: 50,
hp: 82,
maxHp: 82,
attack: 35,
move: 3,
stats: { might: 110, intelligence: 100, leadership: 96, agility: 108, luck: 78 },
equipment: {
weapon: { itemId: 'short-bow', level: 4, exp: 32 },
armor: { itemId: 'cloth-armor', level: 4, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 25,
y: 16
},
{
id: 'jing-south-archer-c',
name: '영릉 궁병',
faction: 'enemy',
className: '성채 궁병',
classKey: 'archer',
level: 31,
exp: 52,
hp: 84,
maxHp: 84,
attack: 35,
move: 3,
stats: { might: 110, intelligence: 100, leadership: 96, agility: 108, luck: 78 },
equipment: {
weapon: { itemId: 'short-bow', level: 4, exp: 36 },
armor: { itemId: 'cloth-armor', level: 4, exp: 22 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 36,
y: 14
},
{
id: 'jing-south-cavalry-a',
name: '영릉 기병',
faction: 'enemy',
className: '남부 기병',
classKey: 'cavalry',
level: 31,
exp: 52,
hp: 116,
maxHp: 116,
attack: 37,
move: 5,
stats: { might: 130, intelligence: 84, leadership: 104, agility: 122, luck: 78 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 40 },
armor: { itemId: 'lamellar-armor', level: 4, exp: 24 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 17,
y: 29
},
{
id: 'jing-south-cavalry-b',
name: '영릉 기병',
faction: 'enemy',
className: '남부 기병',
classKey: 'cavalry',
level: 31,
exp: 52,
hp: 116,
maxHp: 116,
attack: 37,
move: 5,
stats: { might: 130, intelligence: 84, leadership: 104, agility: 122, luck: 78 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 40 },
armor: { itemId: 'lamellar-armor', level: 4, exp: 24 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 26,
y: 27
},
{
id: 'jing-south-cavalry-c',
name: '영릉 기병',
faction: 'enemy',
className: '성채 기병',
classKey: 'cavalry',
level: 32,
exp: 54,
hp: 120,
maxHp: 120,
attack: 38,
move: 5,
stats: { might: 132, intelligence: 86, leadership: 106, agility: 124, luck: 79 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 46 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 28 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 35,
y: 23
},
{
id: 'jing-south-guard-a',
name: '형도영 친위',
faction: 'enemy',
className: '영릉 친위',
classKey: 'yellowTurban',
level: 32,
exp: 56,
hp: 122,
maxHp: 122,
attack: 38,
move: 3,
stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 52 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 38,
y: 16
},
{
id: 'jing-south-guard-b',
name: '형도영 친위',
faction: 'enemy',
className: '영릉 친위',
classKey: 'yellowTurban',
level: 32,
exp: 56,
hp: 122,
maxHp: 122,
attack: 38,
move: 3,
stats: { might: 132, intelligence: 88, leadership: 112, agility: 96, luck: 80 },
equipment: {
weapon: { itemId: 'iron-spear', level: 4, exp: 52 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 34 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 39,
y: 19
},
{
id: 'jing-south-strategist-a',
name: '영릉 문객',
faction: 'enemy',
className: '남부 문객',
classKey: 'archer',
level: 31,
exp: 52,
hp: 86,
maxHp: 86,
attack: 34,
move: 3,
stats: { might: 84, intelligence: 106, leadership: 100, agility: 92, luck: 80 },
equipment: {
weapon: { itemId: 'short-bow', level: 4, exp: 44 },
armor: { itemId: 'cloth-armor', level: 4, exp: 28 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 36,
y: 13
},
{
id: 'jing-south-strategist-b',
name: '영릉 문객',
faction: 'enemy',
className: '남부 문객',
classKey: 'archer',
level: 31,
exp: 52,
hp: 86,
maxHp: 86,
attack: 34,
move: 3,
stats: { might: 84, intelligence: 106, leadership: 100, agility: 92, luck: 80 },
equipment: {
weapon: { itemId: 'short-bow', level: 4, exp: 44 },
armor: { itemId: 'cloth-armor', level: 4, exp: 28 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 40,
y: 14
},
{
id: 'jing-south-leader-xing-daorong',
name: '형도영',
faction: 'enemy',
className: '영릉 맹장',
classKey: 'rebelLeader',
level: 33,
exp: 58,
hp: 164,
maxHp: 164,
attack: 41,
move: 4,
stats: { might: 136, intelligence: 76, leadership: 118, agility: 96, luck: 80 },
equipment: {
weapon: { itemId: 'leader-axe', level: 4, exp: 58 },
armor: { itemId: 'reinforced-lamellar', level: 4, exp: 40 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 42,
y: 17
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -8727,6 +9197,33 @@ export const zhugeRecruitBonds: BattleBond[] = [
}
];
export const jingzhouRecruitBonds: BattleBond[] = [
{
id: 'liu-bei__ma-liang',
unitIds: ['liu-bei', 'ma-liang'],
title: '백미의 조언',
level: 36,
exp: 0,
description: '유비의 인의와 마량의 형주 식견이 만나 남부 군현의 민심을 차분히 묶어 냅니다.'
},
{
id: 'zhuge-liang__ma-liang',
unitIds: ['zhuge-liang', 'ma-liang'],
title: '형주 문맥',
level: 34,
exp: 0,
description: '제갈량의 큰 계책과 마량의 현지 정보가 맞물려 형주 남부의 길과 사람을 읽어냅니다.'
},
{
id: 'sun-qian__ma-liang',
unitIds: ['sun-qian', 'ma-liang'],
title: '군현 문서',
level: 30,
exp: 0,
description: '손건의 외교 문서와 마량의 남부 군현 기록이 합쳐져 설득과 보급의 실마리를 만듭니다.'
}
];
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
@@ -8754,6 +9251,7 @@ export const nineteenthBattleBonds: BattleBond[] = [...eighteenthBattleBonds].ma
export const twentiethBattleBonds: BattleBond[] = [...nineteenthBattleBonds].map(cloneBattleBondForScenario);
export const twentyFirstBattleBonds: BattleBond[] = [...twentiethBattleBonds].map(cloneBattleBondForScenario);
export const twentySecondBattleBonds: BattleBond[] = [...twentyFirstBattleBonds].map(cloneBattleBondForScenario);
export const twentyThirdBattleBonds: BattleBond[] = [...twentySecondBattleBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -9488,6 +9986,69 @@ function createTwentySecondBattleTerrain(): TerrainType[][] {
);
}
function createTwentyThirdBattleTerrain(): TerrainType[][] {
return Array.from({ length: 32 }, (_, y) =>
Array.from({ length: 46 }, (_, x): TerrainType => {
if ((x <= 4 && y >= 24 && y <= 30) || (x >= 5 && x <= 9 && y >= 27 && y <= 31)) {
return 'camp';
}
if ((x >= 38 && x <= 45 && y >= 14 && y <= 22) || (x >= 34 && x <= 38 && y >= 16 && y <= 20)) {
return 'fort';
}
if (
(x >= 12 && x <= 15 && y >= 23 && y <= 25) ||
(x >= 24 && x <= 27 && y >= 18 && y <= 20) ||
(x >= 33 && x <= 35 && y >= 15 && y <= 17)
) {
return 'village';
}
if (
(y === 25 && x >= 3 && x <= 18) ||
(y === 22 && x >= 16 && x <= 29) ||
(y === 18 && x >= 28 && x <= 40) ||
(x === 38 && y >= 14 && y <= 24)
) {
return 'road';
}
if (
(x >= 7 && x <= 18 && y === 29 - Math.floor(x / 2)) ||
(x >= 19 && x <= 36 && y === Math.floor(x / 2) + 8) ||
(x >= 31 && x <= 39 && y === 18)
) {
return 'road';
}
if ((x === 20 || x === 21) && y >= 0 && y <= 31) {
return 'river';
}
if ((x === 29 || x === 30) && y >= 0 && y <= 20) {
return 'river';
}
if ((x === 25 || x === 26) && y >= 24 && y <= 31) {
return 'river';
}
if (
(x >= 4 && x <= 15 && y >= 8 && y <= 19) ||
(x >= 15 && x <= 21 && y >= 25 && y <= 31) ||
(x <= 9 && y <= 8) ||
(x >= 31 && x <= 44 && y <= 10)
) {
return 'forest';
}
if (
(x >= 10 && x <= 19 && y >= 2 && y <= 7) ||
(x >= 30 && x <= 39 && y >= 24 && y <= 31) ||
(x >= 37 && x <= 45 && y >= 4 && y <= 9)
) {
return 'hill';
}
if ((x <= 1 && y <= 15) || (x >= 44 && y <= 12) || (x >= 44 && y >= 24)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -29,6 +29,7 @@ const unitTexture: Record<string, string> = {
'zhang-fei': 'unit-zhang-fei',
'zhao-yun': 'unit-rebel-cavalry',
'zhuge-liang': 'unit-liu-bei',
'ma-liang': 'unit-liu-bei',
'rebel-a': 'unit-rebel',
'rebel-b': 'unit-rebel',
'rebel-c': 'unit-rebel-archer',
@@ -240,7 +241,24 @@ const unitTexture: Record<string, string> = {
'redcliff-fire-guard-b': 'unit-rebel',
'redcliff-fire-strategist-a': 'unit-rebel-archer',
'redcliff-fire-strategist-b': 'unit-rebel-archer',
'redcliff-fire-leader-cao-cao': 'unit-rebel-leader'
'redcliff-fire-leader-cao-cao': 'unit-rebel-leader',
'jing-south-scout-a': 'unit-rebel',
'jing-south-scout-b': 'unit-rebel',
'jing-south-infantry-a': 'unit-rebel',
'jing-south-infantry-b': 'unit-rebel',
'jing-south-infantry-c': 'unit-rebel',
'jing-south-infantry-d': 'unit-rebel',
'jing-south-archer-a': 'unit-rebel-archer',
'jing-south-archer-b': 'unit-rebel-archer',
'jing-south-archer-c': 'unit-rebel-archer',
'jing-south-cavalry-a': 'unit-rebel-cavalry',
'jing-south-cavalry-b': 'unit-rebel-cavalry',
'jing-south-cavalry-c': 'unit-rebel-cavalry',
'jing-south-guard-a': 'unit-rebel',
'jing-south-guard-b': 'unit-rebel',
'jing-south-strategist-a': 'unit-rebel-archer',
'jing-south-strategist-b': 'unit-rebel-archer',
'jing-south-leader-xing-daorong': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -661,7 +679,8 @@ const unitStrategyIds: Record<string, string[]> = {
'jian-yong': ['aid', 'encourage', 'fireTactic'],
'mi-zhu': ['aid', 'encourage'],
'sun-qian': ['aid', 'encourage', 'fireTactic'],
'zhuge-liang': ['aid', 'encourage', 'fireTactic']
'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
'ma-liang': ['aid', 'encourage', 'fireTactic']
};
const initialItemStocks: Record<string, Record<string, number>> = {
@@ -672,7 +691,8 @@ const initialItemStocks: Record<string, Record<string, number>> = {
'mi-zhu': { bean: 2, salve: 1 },
'sun-qian': { bean: 1, salve: 1 },
'zhao-yun': { bean: 1, wine: 1 },
'zhuge-liang': { bean: 1, wine: 1 }
'zhuge-liang': { bean: 1, wine: 1 },
'ma-liang': { bean: 1, salve: 1 }
};
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
@@ -936,7 +956,24 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'redcliff-fire-guard-b': 'guard',
'redcliff-fire-strategist-a': 'hold',
'redcliff-fire-strategist-b': 'hold',
'redcliff-fire-leader-cao-cao': 'guard'
'redcliff-fire-leader-cao-cao': 'guard',
'jing-south-scout-a': 'aggressive',
'jing-south-scout-b': 'aggressive',
'jing-south-infantry-a': 'guard',
'jing-south-infantry-b': 'guard',
'jing-south-infantry-c': 'guard',
'jing-south-infantry-d': 'guard',
'jing-south-archer-a': 'hold',
'jing-south-archer-b': 'hold',
'jing-south-archer-c': 'hold',
'jing-south-cavalry-a': 'aggressive',
'jing-south-cavalry-b': 'aggressive',
'jing-south-cavalry-c': 'aggressive',
'jing-south-guard-a': 'guard',
'jing-south-guard-b': 'guard',
'jing-south-strategist-a': 'hold',
'jing-south-strategist-b': 'hold',
'jing-south-leader-xing-daorong': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -19,6 +19,7 @@ import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-battle
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
import twentyFirstBattleMapUrl from '../../assets/images/battle/twenty-first-battle-map.svg';
import twentySecondBattleMapUrl from '../../assets/images/battle/twenty-second-battle-map.svg';
import twentyThirdBattleMapUrl from '../../assets/images/battle/twenty-third-battle-map.svg';
import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
import twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
@@ -102,6 +103,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-twentieth', twentiethBattleMapUrl);
this.load.image('battle-map-twenty-first', twentyFirstBattleMapUrl);
this.load.image('battle-map-twenty-second', twentySecondBattleMapUrl);
this.load.image('battle-map-twenty-third', twentyThirdBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -9,6 +9,8 @@ import {
caoBreakRecruitUnits,
firstBattleBonds,
firstBattleUnits,
jingzhouRecruitBonds,
jingzhouRecruitUnits,
liuBiaoRecruitBonds,
liuBiaoRecruitUnits,
xuzhouRecruitBonds,
@@ -295,7 +297,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '형주·익주 확보',
period: '근본을 세우다',
description: '형주와 익주를 기반으로 삼아 유비군이 유랑군에서 국가의 뿌리로 바뀌는 장입니다.',
battleIds: [],
battleIds: ['twenty-third-battle-jingzhou-south-entry'],
nextHints: ['형주 방위', '익주 진입', '장수 대거 합류']
},
{
@@ -330,7 +332,8 @@ const campBattleIds = {
nineteenth: 'nineteenth-battle-changban-refuge',
twentieth: 'twentieth-battle-jiangdong-envoy',
twentyFirst: 'twenty-first-battle-red-cliffs-vanguard',
twentySecond: 'twenty-second-battle-red-cliffs-fire'
twentySecond: 'twenty-second-battle-red-cliffs-fire',
twentyThird: 'twenty-third-battle-jingzhou-south-entry'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -569,6 +572,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '화공전의 장기 피해를 버티기 위한 회복과 보급을 맡습니다.' }
],
note: '적벽 화공전은 강 위의 결전입니다. 제갈량의 책략과 전열 유지, 화선 접안로 확보를 중심으로 여섯 명을 고르십시오.'
},
'twenty-third-battle-jingzhou-south-entry': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '형주 남부 진입의 명분이자 패배 조건의 중심입니다.' },
{ unitId: 'zhuge-liang', reason: '마을을 해치지 않고 길목을 여는 작전 핵심입니다.' },
{ unitId: 'guan-yu', reason: '형도영의 보병 전열을 정면에서 받아낼 주력입니다.' },
{ unitId: 'zhao-yun', reason: '척후와 기병을 빠르게 끊어 마을을 안정시키기 좋습니다.' },
{ unitId: 'sun-qian', reason: '마량과 형주 선비층으로 이어질 외교 명분을 정리합니다.' },
{ unitId: 'mi-zhu', reason: '긴 남부 길목에서 회복과 보급을 맡기 좋습니다.' }
],
note: '형주 남부 진입전은 마을 안정과 길목 돌파가 동시에 필요한 전장입니다. 마량은 승리 후 합류하므로 이번 전투 뒤 다음 출전 후보로 늘어납니다.'
}
};
@@ -2408,6 +2423,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 9
}
]
},
{
id: 'liu-ma-after-jingzhou-south-entry',
title: '백미를 맞이하다',
availableAfterBattleIds: [campBattleIds.twentyThird],
unitIds: ['liu-bei', 'ma-liang'],
bondId: 'liu-bei__ma-liang',
rewardExp: 28,
lines: [
'마량: 주공께서 형주 남부의 마을을 먼저 살피셨다는 말을 들었습니다. 그래서 저도 붓을 들고 찾아왔습니다.',
'유비: 땅을 얻는 일보다 사람을 잃지 않는 일이 먼저요. 선생이 남부의 길과 민심을 알려 준다면 큰 힘이 되겠소.',
'마량: 칼로 열 수 없는 문은 말과 문서로 열겠습니다. 다음 군현부터는 제가 앞서 백성의 사정을 살피겠습니다.'
],
choices: [
{
id: 'ask-ma-liang-local-voices',
label: '군현의 민심을 묻는다',
response: '마량은 영릉과 계양의 토호, 마을 장로, 상인 길목을 차분히 짚어 주었고 유비군의 다음 행군 명분이 선명해졌다.',
rewardExp: 12
},
{
id: 'entrust-scholar-letters',
label: '선비들에게 보낼 글을 맡긴다',
response: '유비는 형주 선비들에게 보낼 첫 문장을 마량에게 맡겼고, 백미의 이름은 군영 안에서 조용히 힘을 얻기 시작했다.',
rewardExp: 10
}
]
},
{
id: 'zhuge-ma-after-jingzhou-south-entry',
title: '형주의 지맥',
availableAfterBattleIds: [campBattleIds.twentyThird],
unitIds: ['zhuge-liang', 'ma-liang'],
bondId: 'zhuge-liang__ma-liang',
rewardExp: 26,
lines: [
'제갈량: 큰 계책은 지도를 보지만, 실제 길은 그곳에 사는 사람의 입에서 나옵니다. 마량 선생의 기록이 필요합니다.',
'마량: 형주 남부는 길 하나가 성 하나와 같습니다. 어디를 먼저 설득하고 어디를 피해야 할지 적어 두겠습니다.',
'제갈량: 좋습니다. 이번부터 출전 명단을 고를 때도 전열만이 아니라 문서와 설득을 함께 보아야 합니다.'
],
choices: [
{
id: 'map-commandery-roads',
label: '남부 군현의 길을 표시한다',
response: '마량이 좁은 산길과 마을 우회로를 표시하자, 제갈량의 다음 전장 배치안이 훨씬 촘촘해졌다.',
rewardExp: 11
},
{
id: 'compare-scholar-networks',
label: '선비들의 관계망을 맞춰 본다',
response: '두 사람은 선비층의 관계를 병력 배치처럼 정리했고, 싸우지 않고 열 수 있는 문이 몇 곳 보이기 시작했다.',
rewardExp: 10
}
]
},
{
id: 'sun-ma-after-jingzhou-south-entry',
title: '문서와 백미',
availableAfterBattleIds: [campBattleIds.twentyThird],
unitIds: ['sun-qian', 'ma-liang'],
bondId: 'sun-qian__ma-liang',
rewardExp: 24,
lines: [
'손건: 외교 문서는 말이 곧 병사입니다. 마량 선생이 남부의 이름과 체면을 바로잡아 주면 설득이 쉬워질 겁니다.',
'마량: 손건 선생의 문장은 군영 밖에서도 곧게 들립니다. 거기에 현지의 호칭과 예법을 붙이면 문이 열릴 것입니다.',
'손건: 다음 출전부터는 칼을 드는 장수뿐 아니라, 문서를 들고 갈 장수도 생각해야겠군요.'
],
choices: [
{
id: 'draft-commandery-letters',
label: '군현에 보낼 서신을 쓴다',
response: '손건과 마량은 군현별 서신 초안을 만들었고, 전투 사이 선택지가 더 넓어질 준비가 갖춰졌다.',
rewardExp: 10
},
{
id: 'sort-local-titles',
label: '토호들의 호칭을 정리한다',
response: '사소한 호칭 하나가 문을 닫기도 연다. 두 사람은 남부 토호들의 이름과 예를 새로 정리했다.',
rewardExp: 9
}
]
}
];
@@ -2857,6 +2953,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['민심 기록 1']
}
]
},
{
id: 'lingling-village-relief',
title: '영릉 마을 구호',
location: '영릉 외곽 마을',
availableAfterBattleIds: [campBattleIds.twentyThird],
bondId: 'liu-bei__ma-liang',
description: '형도영의 가병이 물러난 뒤, 유비와 마량이 마을을 돌며 남은 상처와 민심을 살핍니다.',
lines: [
'마량: 이 마을은 길목이라 어느 군대가 지나가도 먼저 흔들립니다. 오늘은 곡식보다 안심할 말이 필요합니다.',
'유비: 우리가 지나간 뒤에도 마을이 버틸 수 있어야 하오. 남은 약과 곡식을 나누고, 군율을 다시 세우시오.',
'마을 장로들은 처음에는 문을 반쯤만 열었지만, 약과 곡식이 먼저 들어오자 조심스레 군영의 이름을 물었습니다.'
],
choices: [
{
id: 'leave-village-medicine',
label: '상처약을 남긴다',
response: '마량은 약을 받은 집마다 이름을 적었고, 유비군이 약탈군이 아니라는 소문이 영릉 주변으로 퍼졌다.',
bondExp: 22,
itemRewards: ['상처약 2']
},
{
id: 'write-relief-record',
label: '구호 기록을 남긴다',
response: '유비와 마량은 마을별 필요 물자를 문서로 남겼고, 다음 보급 계획이 한결 정확해졌다.',
bondExp: 18,
gold: 160,
itemRewards: ['형주 민심 기록 1']
}
]
},
{
id: 'southern-commandery-ledger',
title: '남부 군현 문서',
location: '임시 군막',
availableAfterBattleIds: [campBattleIds.twentyThird],
bondId: 'zhuge-liang__ma-liang',
description: '제갈량과 마량이 영릉 이후 차례로 열어야 할 군현의 길, 토호, 마을 사정을 정리합니다.',
lines: [
'제갈량: 전투로 이긴 곳은 하루면 흔들리고, 문서로 묶은 곳은 다음 계절까지 버팁니다.',
'마량: 계양은 먼저 설득할 사람이 있고, 무릉은 길이 험합니다. 장사는 장수의 이름부터 살펴야 합니다.',
'두 사람의 지도가 펼쳐지자 형주 남부는 막연한 땅이 아니라, 차례로 준비해야 할 선택지로 보이기 시작했습니다.'
],
choices: [
{
id: 'mark-guiyang-route',
label: '계양으로 가는 길을 표시한다',
response: '마량이 계양 길목과 설득 대상을 적자, 제갈량은 다음 군현 확보 순서를 새로 정리했다.',
bondExp: 20,
itemRewards: ['남부 군현 지도 1']
},
{
id: 'estimate-wuling-risk',
label: '무릉의 위험을 계산한다',
response: '험한 길과 분산된 마을이 지도에 표시되며, 다음 전장에는 기동 장수와 보급 장수의 선택이 더 중요해졌다.',
bondExp: 18,
gold: 180,
itemRewards: ['군현 문서 1']
}
]
}
];
@@ -2942,6 +3098,15 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.twentyThird) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits, ...jingzhouRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds, ...jingzhouRecruitBonds]
);
this.report = this.campaign.firstBattleReport ?? this.report;
return;
}
if (
battleId === campBattleIds.seventeenth ||
battleId === campBattleIds.eighteenth ||
@@ -3006,6 +3171,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.twentyThird) {
return '형주 남부 진입 후 군영';
}
if (battleId === campBattleIds.twentySecond) {
return '적벽 화공전 후 군영';
}

View File

@@ -20,6 +20,7 @@ import {
thirteenthBattleScenario,
twentyFirstBattleScenario,
twentySecondBattleScenario,
twentyThirdBattleScenario,
twentiethBattleScenario,
twelfthBattleScenario,
thirdBattleScenario
@@ -343,7 +344,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'nineteenth-camp' ||
campaign.step === 'twentieth-camp' ||
campaign.step === 'twenty-first-camp' ||
campaign.step === 'twenty-second-camp'
campaign.step === 'twenty-second-camp' ||
campaign.step === 'twenty-third-camp'
) {
this.scene.start('CampScene');
return;
@@ -459,6 +461,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'twenty-third-battle') {
this.scene.start('BattleScene', { battleId: twentyThirdBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -85,7 +85,9 @@ export type CampaignStep =
| 'twenty-first-battle'
| 'twenty-first-camp'
| 'twenty-second-battle'
| 'twenty-second-camp';
| 'twenty-second-camp'
| 'twenty-third-battle'
| 'twenty-third-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -169,7 +171,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'nineteenth-battle-changban-refuge': { victory: 'nineteenth-camp', retry: 'nineteenth-battle' },
'twentieth-battle-jiangdong-envoy': { victory: 'twentieth-camp', retry: 'twentieth-battle' },
'twenty-first-battle-red-cliffs-vanguard': { victory: 'twenty-first-camp', retry: 'twenty-first-battle' },
'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' }
};
export type CampaignSaveSlotSummary = {