Add Changban refuge sortie chapter

This commit is contained in:
2026-06-23 06:33:37 +09:00
parent 2d4b08558d
commit 697ce8837d
11 changed files with 1047 additions and 23 deletions

View File

@@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" width="3800" height="2800" viewBox="0 0 3800 2800">
<defs>
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.014" numOctaves="5" seed="19"/>
<feColorMatrix type="saturate" values="0.46"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
<linearGradient id="plain" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#b7a163"/>
<stop offset="0.52" stop-color="#847748"/>
<stop offset="1" stop-color="#5f6944"/>
</linearGradient>
<linearGradient id="river" x1="0" x2="1">
<stop offset="0" stop-color="#17314d"/>
<stop offset="0.5" stop-color="#2c738f"/>
<stop offset="1" stop-color="#152c45"/>
</linearGradient>
<linearGradient id="road" x1="0" x2="1">
<stop offset="0" stop-color="#d8c17a"/>
<stop offset="0.55" stop-color="#a88550"/>
<stop offset="1" stop-color="#78603c"/>
</linearGradient>
<linearGradient id="camp" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#6f7254"/>
<stop offset="1" stop-color="#3f5360"/>
</linearGradient>
<radialGradient id="torch" cx="0.5" cy="0.5" r="0.55">
<stop offset="0" stop-color="#f1c66b" stop-opacity="0.7"/>
<stop offset="0.42" stop-color="#d66f2e" stop-opacity="0.44"/>
<stop offset="1" stop-color="#251a12" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="3800" height="2800" fill="url(#plain)"/>
<g filter="url(#grain)" opacity="0.5">
<rect width="3800" height="2800" fill="#8e7f52"/>
</g>
<path d="M1550 -160 C1470 330 1600 670 1530 1010 C1460 1330 1510 1540 1460 1780" fill="none" stroke="url(#river)" stroke-width="230" opacity="0.95"/>
<path d="M2380 650 C2310 1040 2470 1410 2380 1780 C2310 2110 2390 2460 2300 2960" fill="none" stroke="url(#river)" stroke-width="220" opacity="0.94"/>
<path d="M280 2220 C720 2200 1080 2210 1400 2190 C1680 2180 1830 1920 2270 1900 C2630 1880 3000 1750 3500 1710" fill="none" stroke="url(#road)" stroke-width="190" stroke-linecap="round" opacity="0.76"/>
<path d="M520 2470 C820 2320 1020 2160 1320 2050 C1670 1920 2040 1890 2380 1830 C2700 1780 3020 1680 3330 1440" fill="none" stroke="#b99055" stroke-width="112" stroke-linecap="round" opacity="0.62"/>
<path d="M3050 1300 C3150 1500 3150 1900 3060 2140" fill="none" stroke="url(#road)" stroke-width="130" stroke-linecap="round" opacity="0.62"/>
<g opacity="0.9">
<path d="M520 820 C860 720 1220 860 1460 1110 C1230 1340 810 1290 560 1160 C470 1050 460 930 520 820Z" fill="#2e5639"/>
<path d="M1660 2020 C1910 1900 2240 2020 2340 2280 C2140 2530 1760 2570 1520 2380Z" fill="#2f5d39"/>
<path d="M60 60 C420 10 780 120 960 370 C680 550 300 530 50 330Z" fill="#315d3e"/>
<path d="M2650 90 C3020 20 3510 120 3800 410 L3800 880 C3440 920 3040 770 2700 560Z" fill="#2d5637"/>
</g>
<g opacity="0.72">
<ellipse cx="1180" cy="380" rx="390" ry="190" fill="#5d553b"/>
<ellipse cx="2850" cy="2500" rx="520" ry="210" fill="#615b3e"/>
<ellipse cx="3270" cy="520" rx="450" ry="190" fill="#60563a"/>
</g>
<g opacity="0.9">
<rect x="40" y="2120" width="360" height="560" rx="26" fill="url(#camp)"/>
<rect x="420" y="2320" width="360" height="330" rx="24" fill="url(#camp)" opacity="0.86"/>
<path d="M80 2220 L210 2110 L340 2220Z" fill="#c58b3b"/>
<path d="M480 2390 L610 2290 L740 2390Z" fill="#c58b3b"/>
<rect x="3020" y="1360" width="600" height="720" rx="24" fill="#5a5541"/>
<path d="M3080 1410 L3580 1410 L3580 2030 L3080 2030Z" fill="none" stroke="#d7b56b" stroke-width="18" opacity="0.52"/>
</g>
<g opacity="0.75">
<rect x="820" y="2030" width="260" height="210" rx="18" fill="#8b6c3c"/>
<path d="M810 2035 L950 1900 L1100 2035Z" fill="#c58a3a"/>
<rect x="2020" y="1730" width="270" height="220" rx="18" fill="#806237"/>
<path d="M2010 1735 L2150 1600 L2310 1735Z" fill="#c58a3a"/>
</g>
<g opacity="0.58">
<rect x="1450" y="1740" width="270" height="120" rx="18" fill="#73664a"/>
<rect x="2260" y="1880" width="260" height="120" rx="18" fill="#73664a"/>
<path d="M1450 1790 H1720 M2260 1940 H2520" stroke="#e6d093" stroke-width="18" opacity="0.55"/>
</g>
<g opacity="0.64">
<circle cx="1150" cy="2150" r="120" fill="url(#torch)"/>
<circle cx="1820" cy="1900" r="110" fill="url(#torch)"/>
<circle cx="2780" cy="1700" r="130" fill="url(#torch)"/>
<circle cx="3300" cy="1550" r="105" fill="url(#torch)"/>
</g>
<g opacity="0.22" fill="#e7d59a">
<circle cx="650" cy="2190" r="18"/>
<circle cx="720" cy="2220" r="14"/>
<circle cx="780" cy="2185" r="16"/>
<circle cx="1260" cy="2100" r="16"/>
<circle cx="1350" cy="2070" r="14"/>
<circle cx="1880" cy="1900" r="15"/>
<circle cx="1970" cy="1870" r="13"/>
</g>
<g opacity="0.2">
<path d="M0 0H3800V2800H0Z" fill="none" stroke="#f2d386" stroke-width="28"/>
<path d="M150 150H3650V2650H150Z" fill="none" stroke="#111820" stroke-width="7"/>
</g>
<g opacity="0.14" stroke="#121a18" stroke-width="8">
<path d="M0 500H3800M0 1000H3800M0 1500H3800M0 2000H3800M0 2500H3800"/>
<path d="M500 0V2800M1000 0V2800M1500 0V2800M2000 0V2800M2500 0V2800M3000 0V2800M3500 0V2800"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -27,6 +27,10 @@ import {
ninthBattleMap,
ninthBattleUnits,
ninthBattleVictoryPages,
nineteenthBattleBonds,
nineteenthBattleMap,
nineteenthBattleUnits,
nineteenthBattleVictoryPages,
tenthBattleBonds,
tenthBattleMap,
tenthBattleUnits,
@@ -95,7 +99,8 @@ export type BattleScenarioId =
| 'fifteenth-battle-yuan-refuge-road'
| 'sixteenth-battle-liu-biao-refuge'
| 'seventeenth-battle-wolong-visit-road'
| 'eighteenth-battle-bowang-ambush';
| 'eighteenth-battle-bowang-ambush'
| 'nineteenth-battle-changban-refuge';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -1088,6 +1093,69 @@ export const eighteenthBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene'
};
export const nineteenthBattleScenario: BattleScenarioDefinition = {
id: 'nineteenth-battle-changban-refuge',
title: '장판파 피난로',
victoryConditionLabel: '조순 퇴각',
defeatConditionLabel: '유비 또는 조운 퇴각',
openingObjectiveLines: [
'조조군 호표기가 피난 행렬을 따라 장판파로 들이칩니다. 백성을 버리지 않으려면 남쪽 피난로와 다리목을 동시에 지켜야 합니다.',
'조운은 넓은 길을 오가며 흩어진 병력을 모을 수 있고, 장비는 다리목을 막아 추격 기병을 늦추기 좋습니다. 제갈량과 손건은 전후 강동 사절 명분을 정리합니다.',
'조순을 퇴각시키고 유비와 조운을 지키십시오. 피난민 마을을 확보하면 다음 강동 사절 준비가 안정됩니다.'
],
map: nineteenthBattleMap,
units: nineteenthBattleUnits,
bonds: nineteenthBattleBonds,
mapTextureKey: 'battle-map-nineteenth',
leaderUnitId: 'changban-leader-cao-chun',
quickVictoryTurnLimit: 28,
baseVictoryGold: 2220,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '조순 퇴각',
rewardGold: 1440,
unitId: 'changban-leader-cao-chun'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 480,
unitId: 'liu-bei'
},
{
id: 'zhao-yun',
kind: 'keep-unit-alive',
label: '조운 생존',
rewardGold: 620,
unitId: 'zhao-yun'
},
{
id: 'refugee-village',
kind: 'secure-terrain',
label: '피난민 마을 확보',
rewardGold: 820,
terrain: 'village'
},
{
id: 'quick',
kind: 'quick-victory',
label: '28턴 이내 승리',
rewardGold: 620,
maxTurn: 28
}
],
defeatConditions: [
{ kind: 'unit-defeated', unitId: 'liu-bei' },
{ kind: 'unit-defeated', unitId: 'zhao-yun' }
],
itemRewards: ['콩 7', '상처약 5', '탁주 2', '피난로 지도 +1', '강동 풍문 +1'],
victoryPages: nineteenthBattleVictoryPages,
nextCampScene: 'CampScene'
};
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
@@ -1108,7 +1176,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'fifteenth-battle-yuan-refuge-road': fifteenthBattleScenario,
'sixteenth-battle-liu-biao-refuge': sixteenthBattleScenario,
'seventeenth-battle-wolong-visit-road': seventeenthBattleScenario,
'eighteenth-battle-bowang-ambush': eighteenthBattleScenario
'eighteenth-battle-bowang-ambush': eighteenthBattleScenario,
'nineteenth-battle-changban-refuge': nineteenthBattleScenario
};
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -9,6 +9,7 @@ import {
fourteenthBattleScenario,
fourthBattleScenario,
ninthBattleScenario,
nineteenthBattleScenario,
secondBattleScenario,
seventhBattleScenario,
sixthBattleScenario,
@@ -38,6 +39,8 @@ import {
fourthBattleVictoryPages,
ninthBattleIntroPages,
ninthBattleVictoryPages,
nineteenthBattleIntroPages,
nineteenthBattleVictoryPages,
secondBattleIntroPages,
secondBattleVictoryPages,
seventhBattleIntroPages,
@@ -249,13 +252,24 @@ const sortieFlows: Record<string, SortieFlow> = {
},
[eighteenthBattleScenario.id]: {
afterBattleId: eighteenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '강동으로 부는 바람',
eyebrow: '다음 장',
title: nineteenthBattleScenario.title,
description:
'박망파에서 조조군 선봉을 늦춘 유비군은 더 큰 남하를 막기 위해 강동과 손잡을 길을 찾아야 합니다. 다음 흐름은 장판파와 강동 사절, 그리고 적벽대전으로 이어집니다.',
rewardHint: '다음 장: 장판파와 강동 사절 준비 중',
pages: eighteenthBattleVictoryPages,
unavailableNotice: '장판파와 강동 사절 장은 다음 작업에서 이어집니다. 군영에서 제갈량의 책략과 장수들의 출전 구성을 정비하십시오.'
'박망파에서 조조군 선봉을 늦췄지만 대군의 남하는 계속됩니다. 신야의 백성을 데리고 장판파 피난로를 열어야 강동 사절로 이어질 시간을 벌 수 있습니다.',
rewardHint: `예상 보상: ${nineteenthBattleScenario.title} 개방 / 장판파 피난로 확보`,
nextBattleId: nineteenthBattleScenario.id,
campaignStep: 'nineteenth-battle',
pages: [...eighteenthBattleVictoryPages, ...nineteenthBattleIntroPages]
},
[nineteenthBattleScenario.id]: {
afterBattleId: nineteenthBattleScenario.id,
eyebrow: '다음 장 준비',
title: '강동 사절 준비',
description:
'장판파에서 피난로를 열어 백성과 군을 다시 모았습니다. 이제 제갈량과 손건은 강동으로 보낼 말과 증거를 정리하고, 손권과 마주할 준비를 해야 합니다.',
rewardHint: '다음 장: 강동 사절과 적벽대전 준비 중',
pages: nineteenthBattleVictoryPages,
unavailableNotice: '강동 사절과 적벽대전 장은 다음 작업에서 이어집니다. 군영에서 출전 후보, 공명, 보급을 정비하십시오.'
}
};

View File

@@ -1197,6 +1197,69 @@ export const eighteenthBattleVictoryPages: StoryPage[] = [
}
];
export const nineteenthBattleIntroPages: StoryPage[] = [
{
id: 'nineteenth-cao-army-south',
bgm: 'story-dark',
chapter: '조조의 남하',
background: 'story-militia',
speaker: '손건',
text: '박망파에서 시간을 벌었지만 조조의 대군은 멈추지 않았습니다. 신야의 백성들이 남쪽으로 몰리고, 장판파 길목에는 추격 기병이 먼저 닿고 있습니다.'
},
{
id: 'nineteenth-zhao-yun-rescue',
bgm: 'battle-prep',
chapter: '백마의 구원',
background: 'story-three-heroes',
speaker: '조운',
text: '주공, 피난 행렬이 흩어지면 백성도 군도 모두 잃습니다. 제가 앞뒤를 오가며 길을 묶겠습니다. 다리와 마을을 먼저 지켜 주십시오.'
},
{
id: 'nineteenth-zhang-fei-bridge',
bgm: 'battle-prep',
chapter: '장판교의 호령',
background: 'story-sortie',
speaker: '장비',
portrait: 'zhangFei',
text: '형님은 백성을 이끌고 남쪽 길을 보시오. 뒤쫓는 놈들은 제가 다리목에서 한 번 꺾어 놓겠습니다. 다만 누가 옆을 받쳐 줄지는 잘 골라야겠소!'
},
{
id: 'nineteenth-changban-sortie',
bgm: 'battle-prep',
chapter: '장판파 피난로',
background: 'story-liu-bei',
speaker: '유비',
portrait: 'liuBei',
text: '오늘의 싸움은 이기는 것만으로 끝나지 않소. 백성이 지나갈 길을 열고, 조운과 익덕이 무너지지 않게 서로 받쳐야 하오. 함께할 무장을 신중히 정합시다.'
}
];
export const nineteenthBattleVictoryPages: StoryPage[] = [
{
id: 'nineteenth-victory-refuge-road',
bgm: 'militia-theme',
chapter: '피난로가 열리다',
background: 'story-militia',
text: '장판파의 먼지 속에서 유비군은 피난 행렬을 가까스로 남쪽 길로 흘려보냈습니다. 조운의 창과 장비의 호령이 무너질 뻔한 길목을 붙들었습니다.'
},
{
id: 'nineteenth-zhao-yun-returns',
bgm: 'battle-prep',
chapter: '흩어진 깃발을 모아',
background: 'story-three-heroes',
speaker: '조운',
text: '흩어진 병사와 백성을 모두 되찾지는 못했습니다. 그러나 주공의 깃발이 뒤돌아보았기에 사람들은 다시 모일 곳을 알게 되었습니다.'
},
{
id: 'nineteenth-jiangdong-envoy',
bgm: 'story-dark',
chapter: '강동으로 보낼 사절',
background: 'story-liu-bei',
speaker: '제갈량',
text: '조조의 남하는 이제 강동도 외면할 수 없습니다. 장판파에서 지켜낸 백성의 길과 조조군의 속도를 함께 전하면, 손권과 마주 앉을 명분이 생깁니다.'
}
];
export const firstBattleMap: BattleMap = {
width: 20,
height: 18,
@@ -1450,6 +1513,12 @@ export const eighteenthBattleMap: BattleMap = {
terrain: createEighteenthBattleTerrain()
};
export const nineteenthBattleMap: BattleMap = {
width: 38,
height: 28,
terrain: createNineteenthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [
{
id: 'liu-bei',
@@ -6792,6 +6861,384 @@ export const eighteenthBattleUnits: UnitData[] = [
}
];
const nineteenthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 4, y: 22 },
'guan-yu': { x: 5, y: 20 },
'zhang-fei': { x: 6, y: 22 },
'jian-yong': { x: 3, y: 23 },
'mi-zhu': { x: 4, y: 24 },
'sun-qian': { x: 6, y: 24 },
'zhao-yun': { x: 8, y: 20 },
'zhuge-liang': { x: 5, y: 23 }
};
export const nineteenthBattleUnits: UnitData[] = [
...[
...firstBattleUnits.filter((unit) => unit.faction === 'ally'),
...xuzhouRecruitUnits,
...caoBreakRecruitUnits,
...liuBiaoRecruitUnits,
...zhugeRecruitUnits
].map((unit) => placeScenarioUnit(unit, nineteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
{
id: 'changban-scout-a',
name: '조조군 척후',
faction: 'enemy',
className: '추격 척후',
classKey: 'bandit',
level: 22,
exp: 30,
hp: 66,
maxHp: 66,
attack: 25,
move: 4,
stats: { might: 98, intelligence: 72, leadership: 78, agility: 98, luck: 68 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 22 },
armor: { itemId: 'rebel-vest', level: 2, exp: 70 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 13,
y: 21
},
{
id: 'changban-scout-b',
name: '조조군 척후',
faction: 'enemy',
className: '측면 척후',
classKey: 'bandit',
level: 22,
exp: 30,
hp: 66,
maxHp: 66,
attack: 25,
move: 4,
stats: { might: 98, intelligence: 72, leadership: 78, agility: 98, luck: 68 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 3, exp: 22 },
armor: { itemId: 'rebel-vest', level: 2, exp: 70 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 12,
y: 24
},
{
id: 'changban-cavalry-a',
name: '호표 추격기',
faction: 'enemy',
className: '조조 경기병',
classKey: 'cavalry',
level: 23,
exp: 32,
hp: 88,
maxHp: 88,
attack: 30,
move: 5,
stats: { might: 112, intelligence: 72, leadership: 90, agility: 106, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 28 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 4 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 16,
y: 18
},
{
id: 'changban-cavalry-b',
name: '호표 추격기',
faction: 'enemy',
className: '조조 경기병',
classKey: 'cavalry',
level: 23,
exp: 32,
hp: 88,
maxHp: 88,
attack: 30,
move: 5,
stats: { might: 112, intelligence: 72, leadership: 90, agility: 106, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 28 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 4 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 17,
y: 22
},
{
id: 'changban-cavalry-c',
name: '조조군 선봉기',
faction: 'enemy',
className: '선봉 기병',
classKey: 'cavalry',
level: 24,
exp: 34,
hp: 92,
maxHp: 92,
attack: 31,
move: 5,
stats: { might: 114, intelligence: 74, leadership: 92, agility: 108, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 34 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 10 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 22,
y: 17
},
{
id: 'changban-cavalry-d',
name: '조조군 선봉기',
faction: 'enemy',
className: '선봉 기병',
classKey: 'cavalry',
level: 24,
exp: 34,
hp: 92,
maxHp: 92,
attack: 31,
move: 5,
stats: { might: 114, intelligence: 74, leadership: 92, agility: 108, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 34 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 10 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 25,
y: 21
},
{
id: 'changban-infantry-a',
name: '조조 보병',
faction: 'enemy',
className: '추격 보병',
classKey: 'yellowTurban',
level: 22,
exp: 30,
hp: 82,
maxHp: 82,
attack: 27,
move: 3,
stats: { might: 106, intelligence: 74, leadership: 92, agility: 78, luck: 68 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 26 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 2 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 18,
y: 16
},
{
id: 'changban-infantry-b',
name: '조조 보병',
faction: 'enemy',
className: '추격 보병',
classKey: 'yellowTurban',
level: 22,
exp: 30,
hp: 82,
maxHp: 82,
attack: 27,
move: 3,
stats: { might: 106, intelligence: 74, leadership: 92, agility: 78, luck: 68 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 26 },
armor: { itemId: 'lamellar-armor', level: 3, exp: 2 },
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
},
x: 20,
y: 20
},
{
id: 'changban-infantry-c',
name: '장판 보병',
faction: 'enemy',
className: '다리목 보병',
classKey: 'yellowTurban',
level: 23,
exp: 32,
hp: 86,
maxHp: 86,
attack: 28,
move: 3,
stats: { might: 108, intelligence: 76, leadership: 94, agility: 80, luck: 69 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 30 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 6 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 26,
y: 16
},
{
id: 'changban-infantry-d',
name: '장판 보병',
faction: 'enemy',
className: '다리목 보병',
classKey: 'yellowTurban',
level: 23,
exp: 32,
hp: 86,
maxHp: 86,
attack: 28,
move: 3,
stats: { might: 108, intelligence: 76, leadership: 94, agility: 80, luck: 69 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 30 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 6 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 28,
y: 20
},
{
id: 'changban-archer-a',
name: '조조 궁병',
faction: 'enemy',
className: '언덕 궁병',
classKey: 'archer',
level: 22,
exp: 30,
hp: 64,
maxHp: 64,
attack: 27,
move: 3,
stats: { might: 94, intelligence: 86, leadership: 84, agility: 92, luck: 70 },
equipment: {
weapon: { itemId: 'short-bow', level: 3, exp: 24 },
armor: { itemId: 'cloth-armor', level: 3, exp: 0 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 21,
y: 14
},
{
id: 'changban-archer-b',
name: '조조 궁병',
faction: 'enemy',
className: '강변 궁병',
classKey: 'archer',
level: 22,
exp: 30,
hp: 64,
maxHp: 64,
attack: 27,
move: 3,
stats: { might: 94, intelligence: 86, leadership: 84, agility: 92, luck: 70 },
equipment: {
weapon: { itemId: 'short-bow', level: 3, exp: 24 },
armor: { itemId: 'cloth-armor', level: 3, exp: 0 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 24,
y: 23
},
{
id: 'changban-archer-c',
name: '조조 궁병',
faction: 'enemy',
className: '후방 궁병',
classKey: 'archer',
level: 23,
exp: 32,
hp: 66,
maxHp: 66,
attack: 28,
move: 3,
stats: { might: 96, intelligence: 88, leadership: 86, agility: 94, luck: 70 },
equipment: {
weapon: { itemId: 'short-bow', level: 3, exp: 30 },
armor: { itemId: 'cloth-armor', level: 3, exp: 4 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 31,
y: 18
},
{
id: 'changban-guard-a',
name: '조조 친위',
faction: 'enemy',
className: '호표 친위',
classKey: 'yellowTurban',
level: 24,
exp: 34,
hp: 94,
maxHp: 94,
attack: 30,
move: 3,
stats: { might: 114, intelligence: 78, leadership: 102, agility: 84, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 38 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 14 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 33,
y: 16
},
{
id: 'changban-guard-b',
name: '조조 친위',
faction: 'enemy',
className: '호표 친위',
classKey: 'yellowTurban',
level: 24,
exp: 34,
hp: 94,
maxHp: 94,
attack: 30,
move: 3,
stats: { might: 114, intelligence: 78, leadership: 102, agility: 84, luck: 70 },
equipment: {
weapon: { itemId: 'iron-spear', level: 3, exp: 38 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 14 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 33,
y: 20
},
{
id: 'changban-strategist-a',
name: '조조군 군리',
faction: 'enemy',
className: '군리',
classKey: 'archer',
level: 23,
exp: 32,
hp: 68,
maxHp: 68,
attack: 27,
move: 3,
stats: { might: 76, intelligence: 96, leadership: 88, agility: 84, luck: 72 },
equipment: {
weapon: { itemId: 'short-bow', level: 3, exp: 34 },
armor: { itemId: 'cloth-armor', level: 3, exp: 8 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 31,
y: 14
},
{
id: 'changban-leader-cao-chun',
name: '조순',
faction: 'enemy',
className: '호표기장',
classKey: 'rebelLeader',
level: 25,
exp: 38,
hp: 126,
maxHp: 126,
attack: 33,
move: 5,
stats: { might: 118, intelligence: 82, leadership: 110, agility: 96, luck: 74 },
equipment: {
weapon: { itemId: 'leader-axe', level: 3, exp: 52 },
armor: { itemId: 'reinforced-lamellar', level: 3, exp: 22 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 35,
y: 18
}
];
export const firstBattleBonds: BattleBond[] = [
{
id: 'liu-bei__guan-yu',
@@ -6942,6 +7389,7 @@ export const seventeenthBattleBonds: BattleBond[] = sixteenthBattleBonds.map(clo
export const eighteenthBattleBonds: BattleBond[] = [...sixteenthBattleBonds, ...zhugeRecruitBonds].map(
cloneBattleBondForScenario
);
export const nineteenthBattleBonds: BattleBond[] = [...eighteenthBattleBonds].map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) =>
@@ -7450,6 +7898,62 @@ function createEighteenthBattleTerrain(): TerrainType[][] {
);
}
function createNineteenthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 28 }, (_, y) =>
Array.from({ length: 38 }, (_, x): TerrainType => {
if ((x <= 3 && y >= 21 && y <= 26) || (x >= 4 && x <= 7 && y >= 23 && y <= 26)) {
return 'camp';
}
if ((x >= 29 && x <= 36 && y >= 15 && y <= 22) || (x >= 32 && x <= 37 && y >= 12 && y <= 14)) {
return 'fort';
}
if ((x >= 8 && x <= 10 && y >= 20 && y <= 22) || (x >= 20 && x <= 22 && y >= 17 && y <= 19)) {
return 'village';
}
if ((x === 15 || x === 16) && y >= 0 && y <= 17) {
return 'river';
}
if ((x === 23 || x === 24) && y >= 7 && y <= 27) {
return 'river';
}
if (
(y === 22 && x >= 3 && x <= 14) ||
(y === 19 && x >= 13 && x <= 23) ||
(y === 17 && x >= 23 && x <= 35) ||
(x === 31 && y >= 13 && y <= 21)
) {
return 'road';
}
if (
(x >= 4 && x <= 14 && y === 25 - Math.floor(x / 2)) ||
(x >= 16 && x <= 30 && y === Math.floor(x / 2) + 8) ||
(x >= 7 && x <= 13 && y === 22)
) {
return 'road';
}
if (
(x >= 5 && x <= 14 && y >= 9 && y <= 18) ||
(x >= 16 && x <= 22 && y >= 20 && y <= 27) ||
(x <= 9 && y <= 8) ||
(x >= 26 && x <= 35 && y <= 9)
) {
return 'forest';
}
if (
(x >= 9 && x <= 14 && y >= 2 && y <= 6) ||
(x >= 25 && x <= 31 && y >= 22 && y <= 27) ||
(x >= 29 && x <= 37 && y >= 3 && y <= 7)
) {
return 'hill';
}
if ((x <= 1 && y <= 13) || (x >= 36 && y <= 11) || (x >= 36 && y >= 23)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return {
...cloneUnitForScenario(unit),

View File

@@ -171,7 +171,24 @@ const unitTexture: Record<string, string> = {
'bowang-guard-a': 'unit-rebel',
'bowang-guard-b': 'unit-rebel',
'bowang-strategist-a': 'unit-rebel-archer',
'bowang-leader-xiahou-dun': 'unit-rebel-leader'
'bowang-leader-xiahou-dun': 'unit-rebel-leader',
'changban-scout-a': 'unit-rebel',
'changban-scout-b': 'unit-rebel',
'changban-cavalry-a': 'unit-rebel-cavalry',
'changban-cavalry-b': 'unit-rebel-cavalry',
'changban-cavalry-c': 'unit-rebel-cavalry',
'changban-cavalry-d': 'unit-rebel-cavalry',
'changban-infantry-a': 'unit-rebel',
'changban-infantry-b': 'unit-rebel',
'changban-infantry-c': 'unit-rebel',
'changban-infantry-d': 'unit-rebel',
'changban-archer-a': 'unit-rebel-archer',
'changban-archer-b': 'unit-rebel-archer',
'changban-archer-c': 'unit-rebel-archer',
'changban-guard-a': 'unit-rebel',
'changban-guard-b': 'unit-rebel',
'changban-strategist-a': 'unit-rebel-archer',
'changban-leader-cao-chun': 'unit-rebel-leader'
};
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -798,7 +815,24 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'bowang-guard-a': 'guard',
'bowang-guard-b': 'guard',
'bowang-strategist-a': 'hold',
'bowang-leader-xiahou-dun': 'guard'
'bowang-leader-xiahou-dun': 'guard',
'changban-scout-a': 'aggressive',
'changban-scout-b': 'aggressive',
'changban-cavalry-a': 'aggressive',
'changban-cavalry-b': 'aggressive',
'changban-cavalry-c': 'aggressive',
'changban-cavalry-d': 'aggressive',
'changban-infantry-a': 'guard',
'changban-infantry-b': 'guard',
'changban-infantry-c': 'guard',
'changban-infantry-d': 'guard',
'changban-archer-a': 'hold',
'changban-archer-b': 'hold',
'changban-archer-c': 'hold',
'changban-guard-a': 'guard',
'changban-guard-b': 'guard',
'changban-strategist-a': 'hold',
'changban-leader-cao-chun': 'guard'
};
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -8,6 +8,7 @@ import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle-map.svg';
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg';
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
@@ -94,6 +95,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-sixteenth', sixteenthBattleMapUrl);
this.load.image('battle-map-seventeenth', seventeenthBattleMapUrl);
this.load.image('battle-map-eighteenth', eighteenthBattleMapUrl);
this.load.image('battle-map-nineteenth', nineteenthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -281,7 +281,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
title: '적벽대전',
period: '강동의 바람',
description: '제갈량의 계책과 강동의 힘이 맞물려 조조의 대군을 막아서는 큰 전환 장입니다.',
battleIds: ['eighteenth-battle-bowang-ambush'],
battleIds: ['eighteenth-battle-bowang-ambush', 'nineteenth-battle-changban-refuge'],
nextHints: ['강동 사절', '화공 준비', '조조 남하']
},
{
@@ -320,7 +320,8 @@ const campBattleIds = {
fifteenth: 'fifteenth-battle-yuan-refuge-road',
sixteenth: 'sixteenth-battle-liu-biao-refuge',
seventeenth: 'seventeenth-battle-wolong-visit-road',
eighteenth: 'eighteenth-battle-bowang-ambush'
eighteenth: 'eighteenth-battle-bowang-ambush',
nineteenth: 'nineteenth-battle-changban-refuge'
} as const;
const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -511,6 +512,18 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'sun-qian', reason: '강동 사절로 이어질 명분과 전후 보고를 정리합니다.' }
],
note: '제갈량의 첫 실전 계책입니다. 책략 담당을 포함해 전열, 기동, 보급 중 무엇을 남길지 결정해야 합니다.'
},
'nineteenth-battle-changban-refuge': {
maxUnits: 6,
recommended: [
{ unitId: 'liu-bei', reason: '피난 행렬의 중심이며 패배 조건입니다.' },
{ unitId: 'zhao-yun', reason: '피난로를 오가며 흩어진 병력과 백성을 붙잡는 핵심 기동 장수입니다.' },
{ unitId: 'zhang-fei', reason: '장판교와 좁은 길목에서 추격 기병을 늦추는 역할입니다.' },
{ unitId: 'zhuge-liang', reason: '피난로 판단과 강동 사절 명분을 전투 뒤로 연결합니다.' },
{ unitId: 'guan-yu', reason: '다리목 전열과 친위 보병을 받아낼 안정적인 전열입니다.' },
{ unitId: 'sun-qian', reason: '강동에 보낼 기록과 외교 명분을 정리합니다.' }
],
note: '장판파는 출전 제한보다 역할이 많은 전장입니다. 조운과 장비를 중심으로 기동, 전열, 책략, 보급 중 무엇을 우선할지 직접 선택하십시오.'
}
};
@@ -2026,6 +2039,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 9
}
]
},
{
id: 'liu-zhao-after-changban-refuge',
title: '백마가 지킨 길',
availableAfterBattleIds: [campBattleIds.nineteenth],
unitIds: ['liu-bei', 'zhao-yun'],
bondId: 'liu-bei__zhao-yun',
rewardExp: 30,
lines: [
'유비: 자룡, 장판파의 먼지 속에서도 그대의 창은 백성을 향해 길을 열었소.',
'조운: 주공께서 백성을 버리지 않으셨기에 저도 물러서지 않을 수 있었습니다.',
'유비: 오늘의 공은 전공보다 무겁소. 다음 길에서도 그 마음을 함께 지켜 주시오.'
],
choices: [
{
id: 'entrust-refugee-guard',
label: '피난 행렬 호위를 맡긴다',
response: '조운은 말없이 창을 세웠고, 유비군의 피난길은 더 단단한 호위를 얻었다.',
rewardExp: 12
},
{
id: 'praise-white-horse-loyalty',
label: '백마의 의리를 칭찬한다',
response: '주공의 말에 조운은 깊이 고개를 숙였다. 흩어진 군영 안에서도 두 사람의 신뢰가 또렷해졌다.',
rewardExp: 10
}
]
},
{
id: 'zhang-zhuge-after-changban-refuge',
title: '다리목의 호령',
availableAfterBattleIds: [campBattleIds.nineteenth],
unitIds: ['zhang-fei', 'zhuge-liang'],
bondId: 'zhang-fei__zhuge-liang',
rewardExp: 22,
lines: [
'장비: 군사, 다리목에서 한 번 호통치니 놈들이 눈치를 보더군. 이번엔 계책이 아니라 목청이 이긴 것 아니오?',
'제갈량: 익덕 장군의 호령이 먹힌 것은 설 곳을 정확히 골랐기 때문입니다. 그 자리까지 가는 길도 계책의 일부입니다.',
'장비: 하하, 그렇다면 다음에도 어디서 소리치면 되는지만 알려 주시오. 나머지는 내 창이 알아서 하겠소.'
],
choices: [
{
id: 'fix-bridge-ambush-signals',
label: '다리목 신호를 정한다',
response: '제갈량은 장비가 나서야 할 순간을 짧은 깃발 신호로 정했고, 장비는 복잡한 설명보다 마음에 든다고 웃었다.',
rewardExp: 9
},
{
id: 'let-roar-lead',
label: '호령을 먼저 세운다',
response: '장비의 호령이 적의 발을 묶고, 제갈량의 손짓이 그 뒤를 받치는 새 호흡이 생겼다.',
rewardExp: 8
}
]
},
{
id: 'liu-zhuge-after-changban-refuge',
title: '피난길과 강동',
availableAfterBattleIds: [campBattleIds.nineteenth],
unitIds: ['liu-bei', 'zhuge-liang'],
bondId: 'liu-bei__zhuge-liang',
rewardExp: 28,
lines: [
'유비: 오늘 지킨 것은 길 하나였지만, 그 길 위의 사람들은 천하보다 가벼울 수 없었소.',
'제갈량: 바로 그 점이 강동에 전할 말입니다. 조조의 힘만이 아니라 주공이 무엇을 지키려 하는지를 보여야 합니다.',
'유비: 그렇다면 손권에게 갈 말도 이 피난길에서 시작해야겠구려.'
],
choices: [
{
id: 'write-refugee-road-record',
label: '피난로 기록을 남긴다',
response: '제갈량은 장판파의 피난로와 조조군의 추격 속도를 함께 적어 강동 사절 문서의 근거로 삼았다.',
rewardExp: 11
},
{
id: 'align-people-and-strategy',
label: '백성과 대계를 함께 본다',
response: '유비와 제갈량은 백성을 지키는 마음과 천하를 보는 계책이 서로 멀지 않음을 확인했다.',
rewardExp: 10
}
]
}
];
@@ -2235,6 +2329,66 @@ const campVisits: CampVisitDefinition[] = [
itemRewards: ['탁주 1']
}
]
},
{
id: 'changban-refugee-camp',
title: '장판파 피난민 수습',
location: '남쪽 피난 진영',
availableAfterBattleIds: [campBattleIds.nineteenth],
bondId: 'liu-bei__zhao-yun',
description: '장판파를 벗어난 피난민을 돌보며 흩어진 병사와 민심을 다시 모읍니다.',
lines: [
'조운: 피난민 중에는 가족을 잃고 주저앉은 사람도 많습니다. 군이 먼저 손을 내밀어야 길이 이어집니다.',
'유비: 우리가 지킨 길이 사람을 버리는 길이 되어서는 안 되오. 군량을 나누되, 다음 행군도 버틸 수 있게 살피시오.',
'남쪽 진영에는 먼지와 상처가 가득하지만, 유비군의 깃발 아래 다시 줄이 만들어집니다.'
],
choices: [
{
id: 'share-field-medicine',
label: '상처 입은 피난민을 돌본다',
response: '조운은 약을 나누며 질서를 세웠고, 피난민들은 유비군이 자신들을 버리지 않았음을 기억했다.',
bondExp: 22,
itemRewards: ['민심 기록 1']
},
{
id: 'gather-scattered-soldiers',
label: '흩어진 병사를 모은다',
response: '장판파에서 흩어진 병사들이 하나둘 돌아왔고, 군영은 다음 이동을 버틸 힘을 되찾았다.',
bondExp: 18,
gold: 140,
itemRewards: ['콩 2']
}
]
},
{
id: 'jiangdong-envoy-after-changban',
title: '강동 사절 문서',
location: '피난 진영 막사',
availableAfterBattleIds: [campBattleIds.nineteenth],
bondId: 'sun-qian__zhuge-liang',
description: '제갈량과 손건이 장판파 전황을 강동에 보낼 문서로 정리합니다.',
lines: [
'손건: 장판파의 일은 처참하지만, 강동이 조조의 남하를 실감할 증거이기도 합니다.',
'제갈량: 두려움만 적으면 사람은 굳고, 계산만 적으면 마음이 움직이지 않습니다. 주공이 지킨 길을 함께 써야 합니다.',
'작은 막사 안에서 피난로 지도와 조조군의 진격 보고가 하나의 문서로 묶입니다.'
],
choices: [
{
id: 'write-sun-quan-brief',
label: '손권에게 보낼 요지를 쓴다',
response: '손건은 제갈량의 형세 판단을 짧은 문장으로 정리했고, 강동 사절의 첫 말이 뚜렷해졌다.',
bondExp: 20,
itemRewards: ['강동 풍문 1']
},
{
id: 'prepare-envoy-supplies',
label: '사절 보급을 챙긴다',
response: '강동까지 갈 사절단의 노자와 약품을 따로 묶어 두었다. 긴 길을 위한 준비가 조금 더 단단해졌다.',
bondExp: 16,
gold: 180,
itemRewards: ['상처약 1', '탁주 1']
}
]
}
];
@@ -2320,7 +2474,7 @@ export class CampScene extends Phaser.Scene {
return;
}
if (battleId === campBattleIds.seventeenth || battleId === campBattleIds.eighteenth) {
if (battleId === campBattleIds.seventeenth || battleId === campBattleIds.eighteenth || battleId === campBattleIds.nineteenth) {
this.campaign = ensureCampaignRosterUnits(
[...xuzhouRecruitUnits, ...caoBreakRecruitUnits, ...liuBiaoRecruitUnits, ...zhugeRecruitUnits],
[...xuzhouRecruitBonds, ...caoBreakRecruitBonds, ...liuBiaoRecruitBonds, ...zhugeRecruitBonds]
@@ -2377,6 +2531,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() {
const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.nineteenth) {
return '장판파 돌파 후 군영';
}
if (battleId === campBattleIds.eighteenth) {
return '박망파 승전 후 군영';
}

View File

@@ -10,6 +10,7 @@ import {
fourteenthBattleScenario,
fourthBattleScenario,
ninthBattleScenario,
nineteenthBattleScenario,
secondBattleScenario,
seventhBattleScenario,
sixthBattleScenario,
@@ -335,7 +336,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'fifteenth-camp' ||
campaign.step === 'sixteenth-camp' ||
campaign.step === 'seventeenth-camp' ||
campaign.step === 'eighteenth-camp'
campaign.step === 'eighteenth-camp' ||
campaign.step === 'nineteenth-camp'
) {
this.scene.start('CampScene');
return;
@@ -431,6 +433,11 @@ export class TitleScene extends Phaser.Scene {
return;
}
if (campaign.step === 'nineteenth-battle') {
this.scene.start('BattleScene', { battleId: nineteenthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return;

View File

@@ -77,7 +77,9 @@ export type CampaignStep =
| 'seventeenth-battle'
| 'seventeenth-camp'
| 'eighteenth-battle'
| 'eighteenth-camp';
| 'eighteenth-camp'
| 'nineteenth-battle'
| 'nineteenth-camp';
export type CampaignUnitProgressSnapshot = {
unitId: string;
@@ -157,7 +159,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'fifteenth-battle-yuan-refuge-road': { victory: 'fifteenth-camp', retry: 'fifteenth-battle' },
'sixteenth-battle-liu-biao-refuge': { victory: 'sixteenth-camp', retry: 'sixteenth-battle' },
'seventeenth-battle-wolong-visit-road': { victory: 'seventeenth-camp', retry: 'seventeenth-battle' },
'eighteenth-battle-bowang-ambush': { victory: 'eighteenth-camp', retry: 'eighteenth-battle' }
'eighteenth-battle-bowang-ambush': { victory: 'eighteenth-camp', retry: 'eighteenth-battle' },
'nineteenth-battle-changban-refuge': { victory: 'nineteenth-camp', retry: 'nineteenth-battle' }
};
export type CampaignSaveSlotSummary = {