Add Xuzhou breakout chapter
This commit is contained in:
@@ -29,12 +29,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
|||||||
- Xu Province camp recruitment for Jian Yong and Mi Zhu, including new strategist/quartermaster roles, bond dialogue, and selectable sortie participation
|
- Xu Province camp recruitment for Jian Yong and Mi Zhu, including new strategist/quartermaster roles, bond dialogue, and selectable sortie participation
|
||||||
- Eighth battle Xiaopei supply-road defense, where the expanded Xu Province roster can be selected into battle and Lu Bu's refuge arc is foreshadowed
|
- Eighth battle Xiaopei supply-road defense, where the expanded Xu Province roster can be selected into battle and Lu Bu's refuge arc is foreshadowed
|
||||||
- Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province
|
- Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province
|
||||||
|
- Tenth battle Xuzhou breakout, covering Zhang Fei's mistake, the loss of Xu Province to Lu Bu, and the retreat toward Cao Cao's shadow
|
||||||
- Tactical sortie preparation panel with class role, named equipment, core stats, bond partner, and next-map terrain suitability for each deployable officer
|
- Tactical sortie preparation panel with class role, named equipment, core stats, bond partner, and next-map terrain suitability for each deployable officer
|
||||||
- Flow verification script from title through the ninth battle victory, recruit sortie selection, and camp save state
|
- Flow verification script from title through the tenth battle victory, recruit sortie selection, and camp save state
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Continue into Zhang Fei's mistake and the full loss of Xu Province to Lu Bu
|
1. Continue into Liu Bei's temporary service under Cao Cao and the tension of surviving in Xu/Hu Du politics
|
||||||
2. Add a chapter/progress view so long campaign arcs are easier to read between battles
|
2. Add a chapter/progress view so long campaign arcs are easier to read between battles
|
||||||
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||||
4. Add more recruitable officers as the campaign moves toward Cao Cao, Yuan Shao, Liu Biao, and Zhuge Liang
|
4. Add more recruitable officers as the campaign moves toward Cao Cao, Yuan Shao, Liu Biao, and Zhuge Liang
|
||||||
|
|||||||
@@ -1017,6 +1017,91 @@ try {
|
|||||||
throw new Error(`Expected ninth camp to expose Xuzhou gate dialogue set and preserve recruits: ${JSON.stringify(ninthCampState)}`);
|
throw new Error(`Expected ninth camp to expose Xuzhou gate dialogue set and preserve recruits: ${JSON.stringify(ninthCampState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.mouse.click(1120, 38);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
const ninthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (!ninthCampSortieState?.sortieVisible) {
|
||||||
|
throw new Error(`Expected ninth camp sortie prep overlay: ${JSON.stringify(ninthCampSortieState)}`);
|
||||||
|
}
|
||||||
|
if (!ninthCampSortieState.selectedSortieUnitIds?.includes('guan-yu')) {
|
||||||
|
await page.mouse.click(256, 420);
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
}
|
||||||
|
const ninthCampGuanYuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (!ninthCampGuanYuSortieState.selectedSortieUnitIds?.includes('guan-yu')) {
|
||||||
|
throw new Error(`Expected Guan Yu to be selectable again for Xuzhou breakout: ${JSON.stringify(ninthCampGuanYuSortieState)}`);
|
||||||
|
}
|
||||||
|
assertSortieTacticalRoster(ninthCampGuanYuSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']);
|
||||||
|
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-xuzhou-loss-story.png', fullPage: true });
|
||||||
|
|
||||||
|
for (let i = 0; i < 14; i += 1) {
|
||||||
|
const enteredTenthBattle = await page.evaluate(() => {
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return state?.scene === 'BattleScene' && state?.battleId === 'tenth-battle-xuzhou-breakout';
|
||||||
|
});
|
||||||
|
if (enteredTenthBattle) {
|
||||||
|
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 === 'tenth-battle-xuzhou-breakout' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: 'dist/verification-tenth-battle.png', fullPage: true });
|
||||||
|
|
||||||
|
const tenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||||
|
const tenthEnemies = tenthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||||
|
const tenthAllies = tenthBattleState.units.filter((unit) => unit.faction === 'ally');
|
||||||
|
const tenthEnemyBehaviors = new Set(tenthEnemies.map((unit) => unit.ai));
|
||||||
|
if (
|
||||||
|
tenthBattleState.camera?.mapWidth !== 28 ||
|
||||||
|
tenthBattleState.camera?.mapHeight !== 22 ||
|
||||||
|
tenthBattleState.victoryConditionLabel !== '송헌 격파' ||
|
||||||
|
tenthEnemies.length < 11 ||
|
||||||
|
!tenthEnemyBehaviors.has('aggressive') ||
|
||||||
|
!tenthEnemyBehaviors.has('guard') ||
|
||||||
|
!tenthEnemyBehaviors.has('hold') ||
|
||||||
|
!tenthAllies.some((unit) => unit.id === 'liu-bei') ||
|
||||||
|
!tenthAllies.some((unit) => unit.id === 'guan-yu') ||
|
||||||
|
!tenthAllies.some((unit) => unit.id === 'zhang-fei') ||
|
||||||
|
!tenthAllies.some((unit) => unit.id === 'jian-yong') ||
|
||||||
|
!tenthAllies.some((unit) => unit.id === 'mi-zhu')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected tenth battle to use breakout map, mixed AI, and selected full Xu Province sortie: ${JSON.stringify(tenthBattleState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 tenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (
|
||||||
|
tenthCampState?.campBattleId !== 'tenth-battle-xuzhou-breakout' ||
|
||||||
|
tenthCampState.campTitle !== '서주 상실 후 군영' ||
|
||||||
|
tenthCampState.availableDialogueIds?.length !== 3 ||
|
||||||
|
!tenthCampState.availableDialogueIds.every((id) => id.endsWith('xuzhou-loss')) ||
|
||||||
|
!tenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') ||
|
||||||
|
!tenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') ||
|
||||||
|
!tenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected tenth camp to expose Xuzhou loss dialogue set and preserve full roster: ${JSON.stringify(tenthCampState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
@@ -1028,7 +1113,7 @@ try {
|
|||||||
return activeScenes.includes('CampScene');
|
return activeScenes.includes('CampScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Verified title-to-ninth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
|
console.log(`Verified title-to-tenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
|
||||||
} finally {
|
} finally {
|
||||||
await browser?.close();
|
await browser?.close();
|
||||||
if (serverProcess && !serverProcess.killed) {
|
if (serverProcess && !serverProcess.killed) {
|
||||||
|
|||||||
97
src/assets/images/battle/tenth-battle-map.svg
Normal file
97
src/assets/images/battle/tenth-battle-map.svg
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2800 2200" width="2800" height="2200">
|
||||||
|
<defs>
|
||||||
|
<filter id="smokeGrain">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.022" numOctaves="4" seed="1010" />
|
||||||
|
<feColorMatrix type="saturate" values="0.5" />
|
||||||
|
<feBlend mode="multiply" in2="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
<pattern id="plain" width="100" height="100" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="100" height="100" fill="#516247" />
|
||||||
|
<path d="M10 28h26M42 68h34M64 18l18 14M20 82l22-15" stroke="#9bad6b" stroke-width="3" opacity="0.18" />
|
||||||
|
<path d="M12 56l14-22M50 38l20-18M76 80l10-24" stroke="#263922" stroke-width="2" opacity="0.36" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="road" width="112" height="112" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="112" height="112" fill="#806943" />
|
||||||
|
<path d="M0 30c42-18 78 14 112-8M0 78c44-20 76 12 112-5" stroke="#b99b66" stroke-width="8" opacity="0.24" />
|
||||||
|
<path d="M18 88l22-28M60 46l26-18M86 86l12-10" stroke="#3a2d20" stroke-width="3" opacity="0.38" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="forest" width="132" height="112" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="132" height="112" fill="#203425" />
|
||||||
|
<circle cx="24" cy="48" r="30" fill="#152616" />
|
||||||
|
<circle cx="64" cy="34" r="38" fill="#3d5a38" />
|
||||||
|
<circle cx="104" cy="54" r="31" fill="#1f3420" />
|
||||||
|
<circle cx="54" cy="84" r="28" fill="#4d6741" />
|
||||||
|
<path d="M12 66c36 18 78 16 110-10M36 26c24 20 58 18 76-8" stroke="#0b190e" stroke-width="5" opacity="0.38" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="hill" width="132" height="100" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="132" height="100" fill="#675f43" />
|
||||||
|
<path d="M-12 86c48-62 92-82 134-64c38 16 58 46 76 82" fill="#43442f" opacity="0.72" />
|
||||||
|
<path d="M18 78c38-42 74-52 110-32" fill="none" stroke="#958b58" stroke-width="10" opacity="0.28" />
|
||||||
|
</pattern>
|
||||||
|
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#426f88" />
|
||||||
|
<stop offset="0.52" stop-color="#1e4963" />
|
||||||
|
<stop offset="1" stop-color="#102a3e" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#8b755f" />
|
||||||
|
<stop offset="1" stop-color="#3e3026" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="fire" cx="0.5" cy="0.38" r="0.58">
|
||||||
|
<stop offset="0" stop-color="#ffe394" />
|
||||||
|
<stop offset="0.48" stop-color="#d76c2f" />
|
||||||
|
<stop offset="1" stop-color="#4c1f16" stop-opacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="2800" height="2200" fill="#10171d" />
|
||||||
|
<rect width="2800" height="2200" fill="url(#plain)" opacity="0.94" />
|
||||||
|
<g filter="url(#smokeGrain)">
|
||||||
|
<path d="M-100 1500C330 1458 720 1480 1090 1496C1460 1512 1950 1500 2920 1460" fill="none" stroke="#92744c" stroke-width="258" stroke-linecap="round" opacity="0.9" />
|
||||||
|
<path d="M-100 1500C330 1458 720 1480 1090 1496C1460 1512 1950 1500 2920 1460" fill="none" stroke="url(#road)" stroke-width="154" stroke-linecap="round" opacity="0.92" />
|
||||||
|
<path d="M600 2180C610 1800 610 1420 612 1040C614 760 604 500 612 180" fill="none" stroke="#92744c" stroke-width="184" stroke-linecap="round" opacity="0.86" />
|
||||||
|
<path d="M600 2180C610 1800 610 1420 612 1040C614 760 604 500 612 180" fill="none" stroke="url(#road)" stroke-width="106" stroke-linecap="round" opacity="0.84" />
|
||||||
|
<path d="M2020 1840C2020 1480 2010 1120 2025 760C2032 574 2052 384 2036 120" fill="none" stroke="#92744c" stroke-width="178" stroke-linecap="round" opacity="0.86" />
|
||||||
|
<path d="M2020 1840C2020 1480 2010 1120 2025 760C2032 574 2052 384 2036 120" fill="none" stroke="url(#road)" stroke-width="100" stroke-linecap="round" opacity="0.82" />
|
||||||
|
<path d="M1420 2220c82-306 78-574-10-806c-78-208-62-422 48-640c86-170 100-360 42-572" fill="none" stroke="#102a3e" stroke-width="286" stroke-linecap="round" opacity="0.96" />
|
||||||
|
<path d="M1420 2220c82-306 78-574-10-806c-78-208-62-422 48-640c86-170 100-360 42-572" fill="none" stroke="url(#river)" stroke-width="182" stroke-linecap="round" opacity="0.98" />
|
||||||
|
|
||||||
|
<path d="M0 20c260-74 520-34 780 120c52 178-76 324-384 438C170 530 34 410-14 246Z" fill="url(#forest)" opacity="0.84" />
|
||||||
|
<path d="M138 760c314-116 606-62 878 158c-118 238-404 364-858 378C14 1124 8 944 138 760Z" fill="url(#forest)" opacity="0.82" />
|
||||||
|
<path d="M720 1840c230-86 450-54 660 94c-44 164-198 260-462 288c-146-70-210-194-198-382Z" fill="url(#forest)" opacity="0.78" />
|
||||||
|
<path d="M2110 36c204-54 384-22 540 92v300c-184 44-356 8-512-110c-64-90-72-184-28-282Z" fill="url(#hill)" opacity="0.82" />
|
||||||
|
<path d="M2380 1780c178-50 330-22 456 84v300c-190 54-354 24-492-90c-42-112-30-210 36-294Z" fill="url(#hill)" opacity="0.82" />
|
||||||
|
|
||||||
|
<g transform="translate(1835 455)">
|
||||||
|
<rect x="-70" y="-70" width="900" height="1120" rx="20" fill="#18120f" opacity="0.58" />
|
||||||
|
<rect x="0" y="0" width="770" height="1010" rx="14" fill="url(#wall)" stroke="#211811" stroke-width="20" />
|
||||||
|
<path d="M0 140h770M0 292h770M0 444h770M0 596h770M0 748h770M0 900h770" stroke="#2e241b" stroke-width="12" opacity="0.72" />
|
||||||
|
<path d="M132 0v1010M330 0v1010M536 0v1010" stroke="#63513e" stroke-width="10" opacity="0.48" />
|
||||||
|
<rect x="250" y="504" width="270" height="168" fill="#1f1713" stroke="#0e0907" stroke-width="12" />
|
||||||
|
<path d="M250 504l135-84l135 84" fill="#734a24" stroke="#21160e" stroke-width="12" />
|
||||||
|
<path d="M336 672v-100h98v100" fill="#0d0907" />
|
||||||
|
<circle cx="120" cy="220" r="72" fill="url(#fire)" opacity="0.74" />
|
||||||
|
<circle cx="645" cy="720" r="78" fill="url(#fire)" opacity="0.7" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(620 865)">
|
||||||
|
<rect x="0" y="0" width="260" height="140" rx="10" fill="#5f4a35" stroke="#251d16" stroke-width="10" />
|
||||||
|
<path d="M-20 18L130-74l152 92Z" fill="#98622d" stroke="#2b1f14" stroke-width="10" />
|
||||||
|
<path d="M78 140v-62h60v62" fill="#201713" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(1265 1640)">
|
||||||
|
<rect x="0" y="0" width="230" height="126" rx="10" fill="#5d4935" stroke="#251d16" stroke-width="9" />
|
||||||
|
<path d="M-18 14L115-62l134 76Z" fill="#8c5a2a" stroke="#2b1f14" stroke-width="9" />
|
||||||
|
<path d="M72 126v-54h54v54" fill="#201713" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path d="M1880 680c210 40 456 28 752-38" fill="none" stroke="#15100d" stroke-width="20" opacity="0.35" />
|
||||||
|
<path d="M1840 1270c214 48 482 34 826-42" fill="none" stroke="#15100d" stroke-width="18" opacity="0.32" />
|
||||||
|
<path d="M160 1510c260 58 550 58 870 0" fill="none" stroke="#d8b15f" stroke-width="10" opacity="0.16" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<rect width="2800" height="2200" fill="#07090c" opacity="0.18" />
|
||||||
|
<path d="M1860 460c-160 240-126 536 102 888" fill="none" stroke="#ffb15f" stroke-width="12" opacity="0.12" />
|
||||||
|
<path d="M1780 470c-102 228-76 470 78 724" fill="none" stroke="#ffe0a0" stroke-width="8" opacity="0.08" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -11,6 +11,10 @@ import {
|
|||||||
ninthBattleMap,
|
ninthBattleMap,
|
||||||
ninthBattleUnits,
|
ninthBattleUnits,
|
||||||
ninthBattleVictoryPages,
|
ninthBattleVictoryPages,
|
||||||
|
tenthBattleBonds,
|
||||||
|
tenthBattleMap,
|
||||||
|
tenthBattleUnits,
|
||||||
|
tenthBattleVictoryPages,
|
||||||
fifthBattleBonds,
|
fifthBattleBonds,
|
||||||
fifthBattleMap,
|
fifthBattleMap,
|
||||||
fifthBattleUnits,
|
fifthBattleUnits,
|
||||||
@@ -50,7 +54,8 @@ export type BattleScenarioId =
|
|||||||
| 'sixth-battle-jieqiao-relief'
|
| 'sixth-battle-jieqiao-relief'
|
||||||
| 'seventh-battle-xuzhou-rescue'
|
| 'seventh-battle-xuzhou-rescue'
|
||||||
| 'eighth-battle-xiaopei-supply-road'
|
| 'eighth-battle-xiaopei-supply-road'
|
||||||
| 'ninth-battle-xuzhou-gate-night-raid';
|
| 'ninth-battle-xuzhou-gate-night-raid'
|
||||||
|
| 'tenth-battle-xuzhou-breakout';
|
||||||
|
|
||||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||||
|
|
||||||
@@ -566,6 +571,59 @@ export const ninthBattleScenario: BattleScenarioDefinition = {
|
|||||||
nextCampScene: 'CampScene'
|
nextCampScene: 'CampScene'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const tenthBattleScenario: BattleScenarioDefinition = {
|
||||||
|
id: 'tenth-battle-xuzhou-breakout',
|
||||||
|
title: '서주 탈출전',
|
||||||
|
victoryConditionLabel: '송헌 격파',
|
||||||
|
defeatConditionLabel: '유비 퇴각',
|
||||||
|
openingObjectiveLines: [
|
||||||
|
'서주는 여포에게 넘어갔고, 유비군은 성 밖으로 빠져나갈 길을 열어야 합니다. 송헌을 격파하면 추격대의 지휘가 무너집니다.',
|
||||||
|
'여포군 기병은 길을 따라 빠르게 압박하고, 성내 배반병은 숲과 마을 근처에서 퇴로를 끊으려 합니다.',
|
||||||
|
'서쪽 진영과 마을을 확보한 채 20턴 안에 돌파하면 조조 의탁으로 이어질 병력과 보급을 더 많이 보존합니다.'
|
||||||
|
],
|
||||||
|
map: tenthBattleMap,
|
||||||
|
units: tenthBattleUnits,
|
||||||
|
bonds: tenthBattleBonds,
|
||||||
|
mapTextureKey: 'battle-map-tenth',
|
||||||
|
leaderUnitId: 'xuzhou-escape-leader-song-xian',
|
||||||
|
quickVictoryTurnLimit: 20,
|
||||||
|
baseVictoryGold: 1240,
|
||||||
|
objectives: [
|
||||||
|
{
|
||||||
|
id: 'leader',
|
||||||
|
kind: 'defeat-leader',
|
||||||
|
label: '송헌 격파',
|
||||||
|
rewardGold: 760,
|
||||||
|
unitId: 'xuzhou-escape-leader-song-xian'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-bei',
|
||||||
|
kind: 'keep-unit-alive',
|
||||||
|
label: '유비 생존',
|
||||||
|
rewardGold: 300,
|
||||||
|
unitId: 'liu-bei'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'village',
|
||||||
|
kind: 'secure-terrain',
|
||||||
|
label: '피난 마을 확보',
|
||||||
|
rewardGold: 420,
|
||||||
|
terrain: 'village'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick',
|
||||||
|
kind: 'quick-victory',
|
||||||
|
label: '20턴 이내 돌파',
|
||||||
|
rewardGold: 340,
|
||||||
|
maxTurn: 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||||
|
itemRewards: ['콩 4', '상처약 2', '탁주 1', '서주 피난민 보호 +1'],
|
||||||
|
victoryPages: tenthBattleVictoryPages,
|
||||||
|
nextCampScene: 'CampScene'
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||||
|
|
||||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||||
@@ -577,7 +635,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
|
|||||||
'sixth-battle-jieqiao-relief': sixthBattleScenario,
|
'sixth-battle-jieqiao-relief': sixthBattleScenario,
|
||||||
'seventh-battle-xuzhou-rescue': seventhBattleScenario,
|
'seventh-battle-xuzhou-rescue': seventhBattleScenario,
|
||||||
'eighth-battle-xiaopei-supply-road': eighthBattleScenario,
|
'eighth-battle-xiaopei-supply-road': eighthBattleScenario,
|
||||||
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario
|
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario,
|
||||||
|
'tenth-battle-xuzhou-breakout': tenthBattleScenario
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
secondBattleScenario,
|
secondBattleScenario,
|
||||||
seventhBattleScenario,
|
seventhBattleScenario,
|
||||||
sixthBattleScenario,
|
sixthBattleScenario,
|
||||||
|
tenthBattleScenario,
|
||||||
thirdBattleScenario,
|
thirdBattleScenario,
|
||||||
type BattleScenarioId
|
type BattleScenarioId
|
||||||
} from './battles';
|
} from './battles';
|
||||||
@@ -27,6 +28,8 @@ import {
|
|||||||
seventhBattleVictoryPages,
|
seventhBattleVictoryPages,
|
||||||
sixthBattleIntroPages,
|
sixthBattleIntroPages,
|
||||||
sixthBattleVictoryPages,
|
sixthBattleVictoryPages,
|
||||||
|
tenthBattleIntroPages,
|
||||||
|
tenthBattleVictoryPages,
|
||||||
thirdBattleIntroPages,
|
thirdBattleIntroPages,
|
||||||
thirdBattleVictoryPages,
|
thirdBattleVictoryPages,
|
||||||
type StoryPage
|
type StoryPage
|
||||||
@@ -127,12 +130,22 @@ const sortieFlows: Record<string, SortieFlow> = {
|
|||||||
},
|
},
|
||||||
[ninthBattleScenario.id]: {
|
[ninthBattleScenario.id]: {
|
||||||
afterBattleId: ninthBattleScenario.id,
|
afterBattleId: ninthBattleScenario.id,
|
||||||
|
eyebrow: '다음 전장',
|
||||||
|
title: tenthBattleScenario.title,
|
||||||
|
description: '장비의 성급함과 여포의 야심이 맞물려 서주는 무너집니다. 성을 되찾기보다 포위망을 뚫고 다음 길을 찾는 전투가 됩니다.',
|
||||||
|
rewardHint: `예상 보상: ${tenthBattleScenario.title} 개방`,
|
||||||
|
nextBattleId: tenthBattleScenario.id,
|
||||||
|
campaignStep: 'tenth-battle',
|
||||||
|
pages: [...ninthBattleVictoryPages, ...tenthBattleIntroPages]
|
||||||
|
},
|
||||||
|
[tenthBattleScenario.id]: {
|
||||||
|
afterBattleId: tenthBattleScenario.id,
|
||||||
eyebrow: '다음 장 준비',
|
eyebrow: '다음 장 준비',
|
||||||
title: '서주 상실의 전조',
|
title: '조조 의탁의 길',
|
||||||
description: '성문은 지켰지만 여포와 서주 내부의 불씨는 더 커졌습니다. 다음 흐름은 장비의 실책과 서주 상실로 이어집니다.',
|
description: '서주를 잃은 유비군은 살아남은 병력과 백성을 추슬러 허도로 향할 길을 찾습니다. 다음 흐름은 조조 의탁과 그 안의 긴장으로 이어집니다.',
|
||||||
rewardHint: '다음 장: 장비의 실책과 서주 상실 준비 중',
|
rewardHint: '다음 장: 조조 의탁 준비 중',
|
||||||
pages: ninthBattleVictoryPages,
|
pages: tenthBattleVictoryPages,
|
||||||
unavailableNotice: '서주 상실 전투는 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
|
unavailableNotice: '조조 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -649,6 +649,61 @@ export const ninthBattleVictoryPages: StoryPage[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const tenthBattleIntroPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'tenth-zhang-fei-fault',
|
||||||
|
bgm: 'story-dark',
|
||||||
|
chapter: '술잔이 부른 균열',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '장비',
|
||||||
|
portrait: 'zhangFei',
|
||||||
|
text: '조표의 잔당을 다그치던 장비는 분노를 이기지 못했다. 말 한마디, 술 한잔, 그리고 성 안의 오래된 원한이 한밤중에 다시 타올랐다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenth-lu-bu-takes-xuzhou',
|
||||||
|
bgm: 'story-dark',
|
||||||
|
chapter: '빼앗긴 서주',
|
||||||
|
background: 'story-militia',
|
||||||
|
text: '여포는 성문이 흔들린 틈을 놓치지 않았다. 서주 곳곳의 수비대가 갈라졌고, 유비가 돌아왔을 때 성루의 깃발은 이미 바뀌어 있었다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenth-brothers-regroup',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '무너진 성 앞에서',
|
||||||
|
background: 'story-three-heroes',
|
||||||
|
speaker: '관우',
|
||||||
|
portrait: 'guanYu',
|
||||||
|
text: '형님, 지금 성을 되찾으려 들면 백성과 군졸을 모두 잃습니다. 익덕을 꾸짖는 일은 살아나간 뒤에 해도 늦지 않습니다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenth-xuzhou-escape',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '서주 탈출',
|
||||||
|
background: 'story-sortie',
|
||||||
|
speaker: '유비',
|
||||||
|
portrait: 'liuBei',
|
||||||
|
text: '서주는 오늘 내 손에서 떠났다. 그러나 백성과 형제와 뜻까지 잃을 수는 없다. 포위망을 뚫고 다음 길을 찾자.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tenthBattleVictoryPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'tenth-victory-breakout',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '잃고도 남은 것',
|
||||||
|
background: 'story-militia',
|
||||||
|
text: '유비군은 여포군 선봉을 뚫고 서주 외곽으로 물러났다. 성은 빼앗겼으나, 세 형제와 새로 모인 장수들의 전열은 아직 흩어지지 않았다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tenth-cao-cao-shadow',
|
||||||
|
bgm: 'story-dark',
|
||||||
|
chapter: '허도로 향하는 그림자',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '간옹',
|
||||||
|
text: '현덕, 이제 서주 안에서 버티는 길은 막혔네. 조조의 휘하로 들어가는 일은 마음에 걸리지만, 살아남아 다시 뜻을 세우려면 그 길을 지나야 할 걸세.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleMap: BattleMap = {
|
export const firstBattleMap: BattleMap = {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 18,
|
height: 18,
|
||||||
@@ -848,6 +903,12 @@ export const ninthBattleMap: BattleMap = {
|
|||||||
terrain: createNinthBattleTerrain()
|
terrain: createNinthBattleTerrain()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const tenthBattleMap: BattleMap = {
|
||||||
|
width: 28,
|
||||||
|
height: 22,
|
||||||
|
terrain: createTenthBattleTerrain()
|
||||||
|
};
|
||||||
|
|
||||||
export const firstBattleUnits: UnitData[] = [
|
export const firstBattleUnits: UnitData[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei',
|
id: 'liu-bei',
|
||||||
@@ -3201,6 +3262,251 @@ export const ninthBattleUnits: UnitData[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tenthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||||
|
'liu-bei': { x: 3, y: 14 },
|
||||||
|
'guan-yu': { x: 4, y: 15 },
|
||||||
|
'zhang-fei': { x: 3, y: 16 },
|
||||||
|
'jian-yong': { x: 5, y: 17 },
|
||||||
|
'mi-zhu': { x: 4, y: 18 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tenthBattleUnits: UnitData[] = [
|
||||||
|
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
|
||||||
|
placeScenarioUnit(unit, tenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-cavalry-a',
|
||||||
|
name: '여포군 추격기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 11,
|
||||||
|
exp: 64,
|
||||||
|
hp: 52,
|
||||||
|
maxHp: 52,
|
||||||
|
attack: 18,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 91, intelligence: 52, leadership: 70, agility: 92, luck: 58 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 42 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 20 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 21,
|
||||||
|
y: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-cavalry-b',
|
||||||
|
name: '여포군 추격기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 11,
|
||||||
|
exp: 66,
|
||||||
|
hp: 53,
|
||||||
|
maxHp: 53,
|
||||||
|
attack: 18,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 92, intelligence: 52, leadership: 70, agility: 93, luck: 58 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 44 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 20 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 23,
|
||||||
|
y: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-cavalry-c',
|
||||||
|
name: '여포군 추격기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 11,
|
||||||
|
exp: 66,
|
||||||
|
hp: 53,
|
||||||
|
maxHp: 53,
|
||||||
|
attack: 18,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 92, intelligence: 53, leadership: 71, agility: 93, luck: 59 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 46 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 20 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 22,
|
||||||
|
y: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-guard-a',
|
||||||
|
name: '서주 점거병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 보병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 10,
|
||||||
|
exp: 62,
|
||||||
|
hp: 47,
|
||||||
|
maxHp: 47,
|
||||||
|
attack: 15,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 82, intelligence: 54, leadership: 69, agility: 63, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 36 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 12,
|
||||||
|
y: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-guard-b',
|
||||||
|
name: '서주 점거병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 보병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 10,
|
||||||
|
exp: 62,
|
||||||
|
hp: 48,
|
||||||
|
maxHp: 48,
|
||||||
|
attack: 15,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 83, intelligence: 54, leadership: 70, agility: 64, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 38 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 13,
|
||||||
|
y: 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-guard-c',
|
||||||
|
name: '서주 점거병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 보병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 10,
|
||||||
|
exp: 64,
|
||||||
|
hp: 48,
|
||||||
|
maxHp: 48,
|
||||||
|
attack: 15,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 84, intelligence: 55, leadership: 70, agility: 64, luck: 56 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 40 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 15,
|
||||||
|
y: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-archer-a',
|
||||||
|
name: '여포군 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 10,
|
||||||
|
exp: 62,
|
||||||
|
hp: 38,
|
||||||
|
maxHp: 38,
|
||||||
|
attack: 16,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 74, intelligence: 65, leadership: 63, agility: 74, luck: 56 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 2, exp: 38 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 2, exp: 16 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 17,
|
||||||
|
y: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-archer-b',
|
||||||
|
name: '여포군 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 10,
|
||||||
|
exp: 64,
|
||||||
|
hp: 39,
|
||||||
|
maxHp: 39,
|
||||||
|
attack: 16,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 75, intelligence: 66, leadership: 64, agility: 75, luck: 56 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 2, exp: 40 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 2, exp: 16 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 18,
|
||||||
|
y: 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-raider-a',
|
||||||
|
name: '성내 배반병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '성내 배반병',
|
||||||
|
classKey: 'bandit',
|
||||||
|
level: 10,
|
||||||
|
exp: 62,
|
||||||
|
hp: 43,
|
||||||
|
maxHp: 43,
|
||||||
|
attack: 16,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 84, intelligence: 52, leadership: 61, agility: 76, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 38 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 8,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-raider-b',
|
||||||
|
name: '성내 배반병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '성내 배반병',
|
||||||
|
classKey: 'bandit',
|
||||||
|
level: 10,
|
||||||
|
exp: 64,
|
||||||
|
hp: 44,
|
||||||
|
maxHp: 44,
|
||||||
|
attack: 16,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 85, intelligence: 52, leadership: 62, agility: 77, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 40 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 10,
|
||||||
|
y: 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-escape-leader-song-xian',
|
||||||
|
name: '송헌',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 선봉장',
|
||||||
|
classKey: 'rebelLeader',
|
||||||
|
level: 11,
|
||||||
|
exp: 70,
|
||||||
|
hp: 68,
|
||||||
|
maxHp: 68,
|
||||||
|
attack: 18,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 88, intelligence: 60, leadership: 84, agility: 72, luck: 58 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'leader-axe', level: 2, exp: 42 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
|
||||||
|
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 20,
|
||||||
|
y: 11
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleBonds: BattleBond[] = [
|
export const firstBattleBonds: BattleBond[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei__guan-yu',
|
id: 'liu-bei__guan-yu',
|
||||||
@@ -3255,6 +3561,7 @@ export const sixthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBo
|
|||||||
export const seventhBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
export const seventhBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||||
export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRecruitBonds].map(cloneBattleBondForScenario);
|
export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRecruitBonds].map(cloneBattleBondForScenario);
|
||||||
export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario);
|
export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario);
|
||||||
|
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
|
||||||
|
|
||||||
function createEighthBattleTerrain(): TerrainType[][] {
|
function createEighthBattleTerrain(): TerrainType[][] {
|
||||||
return Array.from({ length: 22 }, (_, y) =>
|
return Array.from({ length: 22 }, (_, y) =>
|
||||||
@@ -3317,6 +3624,53 @@ function createNinthBattleTerrain(): TerrainType[][] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTenthBattleTerrain(): TerrainType[][] {
|
||||||
|
return Array.from({ length: 22 }, (_, y) =>
|
||||||
|
Array.from({ length: 28 }, (_, x): TerrainType => {
|
||||||
|
if ((x <= 4 && y >= 14 && y <= 20) || (x >= 5 && x <= 7 && y >= 17 && y <= 20)) {
|
||||||
|
return 'camp';
|
||||||
|
}
|
||||||
|
if ((x >= 18 && x <= 25 && y >= 5 && y <= 12) || (x >= 20 && x <= 26 && y >= 13 && y <= 17)) {
|
||||||
|
return 'fort';
|
||||||
|
}
|
||||||
|
if ((x >= 6 && x <= 8 && y >= 9 && y <= 11) || (x >= 13 && x <= 14 && y >= 16 && y <= 18)) {
|
||||||
|
return 'village';
|
||||||
|
}
|
||||||
|
if ((x === 15 || x === 16) && y >= 2 && y <= 21) {
|
||||||
|
return 'river';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(y === 15 && x >= 2 && x <= 26) ||
|
||||||
|
(x === 6 && y >= 7 && y <= 20) ||
|
||||||
|
(x === 20 && y >= 4 && y <= 18) ||
|
||||||
|
(x + y === 28 && x >= 9 && x <= 20)
|
||||||
|
) {
|
||||||
|
return 'road';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(x <= 5 && y <= 6) ||
|
||||||
|
(x >= 7 && x <= 12 && y >= 2 && y <= 7) ||
|
||||||
|
(x >= 2 && x <= 9 && y >= 8 && y <= 13) ||
|
||||||
|
(x >= 8 && x <= 13 && y >= 18 && y <= 21)
|
||||||
|
) {
|
||||||
|
return 'forest';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(x >= 21 && y <= 4) ||
|
||||||
|
(x >= 24 && y >= 18) ||
|
||||||
|
(x >= 10 && x <= 13 && y >= 11 && y <= 14) ||
|
||||||
|
(x <= 3 && y >= 7 && y <= 10)
|
||||||
|
) {
|
||||||
|
return 'hill';
|
||||||
|
}
|
||||||
|
if ((x >= 25 && y >= 6 && y <= 10) || (x >= 18 && x <= 20 && y <= 2)) {
|
||||||
|
return 'cliff';
|
||||||
|
}
|
||||||
|
return 'plain';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||||
return {
|
return {
|
||||||
...cloneUnitForScenario(unit),
|
...cloneUnitForScenario(unit),
|
||||||
|
|||||||
@@ -545,7 +545,18 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
|||||||
'lubu-cavalry-a': 'aggressive',
|
'lubu-cavalry-a': 'aggressive',
|
||||||
'lubu-cavalry-b': 'aggressive',
|
'lubu-cavalry-b': 'aggressive',
|
||||||
'lubu-cavalry-c': 'aggressive',
|
'lubu-cavalry-c': 'aggressive',
|
||||||
'xuzhou-leader-cao-bao': 'guard'
|
'xuzhou-leader-cao-bao': 'guard',
|
||||||
|
'xuzhou-escape-cavalry-a': 'aggressive',
|
||||||
|
'xuzhou-escape-cavalry-b': 'aggressive',
|
||||||
|
'xuzhou-escape-cavalry-c': 'aggressive',
|
||||||
|
'xuzhou-escape-guard-a': 'guard',
|
||||||
|
'xuzhou-escape-guard-b': 'guard',
|
||||||
|
'xuzhou-escape-guard-c': 'guard',
|
||||||
|
'xuzhou-escape-archer-a': 'hold',
|
||||||
|
'xuzhou-escape-archer-b': 'hold',
|
||||||
|
'xuzhou-escape-raider-a': 'aggressive',
|
||||||
|
'xuzhou-escape-raider-b': 'aggressive',
|
||||||
|
'xuzhou-escape-leader-song-xian': 'guard'
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
|
|||||||
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
||||||
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
|
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
|
||||||
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
|
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
|
||||||
|
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
|
||||||
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
||||||
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
||||||
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
|
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
|
||||||
@@ -76,6 +77,7 @@ export class BootScene extends Phaser.Scene {
|
|||||||
this.load.image('battle-map-seventh', seventhBattleMapUrl);
|
this.load.image('battle-map-seventh', seventhBattleMapUrl);
|
||||||
this.load.image('battle-map-eighth', eighthBattleMapUrl);
|
this.load.image('battle-map-eighth', eighthBattleMapUrl);
|
||||||
this.load.image('battle-map-ninth', ninthBattleMapUrl);
|
this.load.image('battle-map-ninth', ninthBattleMapUrl);
|
||||||
|
this.load.image('battle-map-tenth', tenthBattleMapUrl);
|
||||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ const campBattleIds = {
|
|||||||
sixth: 'sixth-battle-jieqiao-relief',
|
sixth: 'sixth-battle-jieqiao-relief',
|
||||||
seventh: 'seventh-battle-xuzhou-rescue',
|
seventh: 'seventh-battle-xuzhou-rescue',
|
||||||
eighth: 'eighth-battle-xiaopei-supply-road',
|
eighth: 'eighth-battle-xiaopei-supply-road',
|
||||||
ninth: 'ninth-battle-xuzhou-gate-night-raid'
|
ninth: 'ninth-battle-xuzhou-gate-night-raid',
|
||||||
|
tenth: 'tenth-battle-xuzhou-breakout'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||||
@@ -928,6 +929,87 @@ const campDialogues: CampDialogue[] = [
|
|||||||
rewardExp: 7
|
rewardExp: 7
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-zhang-after-xuzhou-loss',
|
||||||
|
title: '잃은 성 앞에서',
|
||||||
|
availableAfterBattleIds: [campBattleIds.tenth],
|
||||||
|
unitIds: ['liu-bei', 'zhang-fei'],
|
||||||
|
bondId: 'liu-bei__zhang-fei',
|
||||||
|
rewardExp: 24,
|
||||||
|
lines: [
|
||||||
|
'장비: 형님, 서주를 잃은 죄는 모두 제게 있습니다. 제 목을 베어 군율을 세우십시오.',
|
||||||
|
'유비: 익덕의 목을 베면 서주가 돌아오느냐. 살아서 다시 지키는 것이 네 벌이다.',
|
||||||
|
'장비: 다시는 술과 분노가 창끝보다 먼저 나가지 않게 하겠습니다.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'forgive-and-command',
|
||||||
|
label: '용서하되 군율을 맡긴다',
|
||||||
|
response: '장비는 꾸지람보다 무거운 신뢰를 받고, 다음 전장에서는 먼저 자신을 다스리겠다고 맹세했다.',
|
||||||
|
rewardExp: 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'make-amends-through-action',
|
||||||
|
label: '공으로 갚게 한다',
|
||||||
|
response: '장비는 서주를 잃은 분노를 다음 싸움의 방패로 삼겠다고 답했다.',
|
||||||
|
rewardExp: 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-guan-after-xuzhou-loss',
|
||||||
|
title: '흩어지지 않는 전열',
|
||||||
|
availableAfterBattleIds: [campBattleIds.tenth],
|
||||||
|
unitIds: ['liu-bei', 'guan-yu'],
|
||||||
|
bondId: 'liu-bei__guan-yu',
|
||||||
|
rewardExp: 22,
|
||||||
|
lines: [
|
||||||
|
'관우: 성은 잃었으나 형님의 깃발 아래 모인 마음은 아직 남았습니다.',
|
||||||
|
'유비: 운장, 내가 의를 앞세워 사람을 들였고 그 의가 백성을 흔들었다.',
|
||||||
|
'관우: 의는 버릴 것이 아니라 더 단단히 세울 것입니다. 이번 패배가 그 기둥이 되게 하십시오.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'hold-the-banner',
|
||||||
|
label: '깃발을 다시 세운다',
|
||||||
|
response: '관우는 흩어진 병사들이 볼 수 있도록 유비군의 깃발을 다시 높이 세웠다.',
|
||||||
|
rewardExp: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'protect-the-people',
|
||||||
|
label: '피난민을 먼저 모은다',
|
||||||
|
response: '관우는 패전의 행군에서도 백성을 버리지 않는 것이 형님의 길이라며 고개를 끄덕였다.',
|
||||||
|
rewardExp: 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-jian-yong-after-xuzhou-loss',
|
||||||
|
title: '허도로 가는 말',
|
||||||
|
availableAfterBattleIds: [campBattleIds.tenth],
|
||||||
|
unitIds: ['liu-bei', 'jian-yong'],
|
||||||
|
bondId: 'liu-bei__jian-yong',
|
||||||
|
rewardExp: 20,
|
||||||
|
lines: [
|
||||||
|
'간옹: 현덕, 조조에게 몸을 의탁하는 일은 달갑지 않겠지. 하지만 지금은 그 그늘을 지나야 다음 길이 열린다네.',
|
||||||
|
'유비: 조조의 그늘이 너무 짙어 내 뜻까지 가리면 어찌하오?',
|
||||||
|
'간옹: 그때를 대비해 사람과 말을 잃지 말아야지. 뜻도 살아 있는 사람에게 붙는 법이네.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'send-envoys',
|
||||||
|
label: '먼저 사자를 보낸다',
|
||||||
|
response: '간옹은 허도로 향할 말을 고르고, 유비군이 굴복이 아닌 임시 의탁임을 전할 문장을 다듬었다.',
|
||||||
|
rewardExp: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hide-strength',
|
||||||
|
label: '병력 규모를 감춘다',
|
||||||
|
response: '간옹은 조조가 보는 숫자와 실제로 보존할 숫자를 나누어 생각해야 한다고 조언했다.',
|
||||||
|
rewardExp: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1006,7 +1088,13 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private ensureCurrentCampRecruitment() {
|
private ensureCurrentCampRecruitment() {
|
||||||
const battleId = this.currentCampBattleId();
|
const battleId = this.currentCampBattleId();
|
||||||
if ((battleId !== campBattleIds.seventh && battleId !== campBattleIds.eighth && battleId !== campBattleIds.ninth) || !this.campaign) {
|
if (
|
||||||
|
(battleId !== campBattleIds.seventh &&
|
||||||
|
battleId !== campBattleIds.eighth &&
|
||||||
|
battleId !== campBattleIds.ninth &&
|
||||||
|
battleId !== campBattleIds.tenth) ||
|
||||||
|
!this.campaign
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,6 +1114,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private currentCampTitle() {
|
private currentCampTitle() {
|
||||||
const battleId = this.currentCampBattleId();
|
const battleId = this.currentCampBattleId();
|
||||||
|
if (battleId === campBattleIds.tenth) {
|
||||||
|
return '서주 상실 후 군영';
|
||||||
|
}
|
||||||
if (battleId === campBattleIds.ninth) {
|
if (battleId === campBattleIds.ninth) {
|
||||||
return '서주 성문 군영';
|
return '서주 성문 군영';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
secondBattleScenario,
|
secondBattleScenario,
|
||||||
seventhBattleScenario,
|
seventhBattleScenario,
|
||||||
sixthBattleScenario,
|
sixthBattleScenario,
|
||||||
|
tenthBattleScenario,
|
||||||
thirdBattleScenario
|
thirdBattleScenario
|
||||||
} from '../data/battles';
|
} from '../data/battles';
|
||||||
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
||||||
@@ -317,7 +318,8 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
campaign.step === 'sixth-camp' ||
|
campaign.step === 'sixth-camp' ||
|
||||||
campaign.step === 'seventh-camp' ||
|
campaign.step === 'seventh-camp' ||
|
||||||
campaign.step === 'eighth-camp' ||
|
campaign.step === 'eighth-camp' ||
|
||||||
campaign.step === 'ninth-camp'
|
campaign.step === 'ninth-camp' ||
|
||||||
|
campaign.step === 'tenth-camp'
|
||||||
) {
|
) {
|
||||||
this.scene.start('CampScene');
|
this.scene.start('CampScene');
|
||||||
return;
|
return;
|
||||||
@@ -368,6 +370,11 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (campaign.step === 'tenth-battle') {
|
||||||
|
this.scene.start('BattleScene', { battleId: tenthBattleScenario.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (campaign.step === 'first-victory-story') {
|
if (campaign.step === 'first-victory-story') {
|
||||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export type CampaignStep =
|
|||||||
| 'eighth-battle'
|
| 'eighth-battle'
|
||||||
| 'eighth-camp'
|
| 'eighth-camp'
|
||||||
| 'ninth-battle'
|
| 'ninth-battle'
|
||||||
| 'ninth-camp';
|
| 'ninth-camp'
|
||||||
|
| 'tenth-battle'
|
||||||
|
| 'tenth-camp';
|
||||||
|
|
||||||
export type CampaignUnitProgressSnapshot = {
|
export type CampaignUnitProgressSnapshot = {
|
||||||
unitId: string;
|
unitId: string;
|
||||||
@@ -117,7 +119,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
|||||||
'sixth-battle-jieqiao-relief': { victory: 'sixth-camp', retry: 'sixth-battle' },
|
'sixth-battle-jieqiao-relief': { victory: 'sixth-camp', retry: 'sixth-battle' },
|
||||||
'seventh-battle-xuzhou-rescue': { victory: 'seventh-camp', retry: 'seventh-battle' },
|
'seventh-battle-xuzhou-rescue': { victory: 'seventh-camp', retry: 'seventh-battle' },
|
||||||
'eighth-battle-xiaopei-supply-road': { victory: 'eighth-camp', retry: 'eighth-battle' },
|
'eighth-battle-xiaopei-supply-road': { victory: 'eighth-camp', retry: 'eighth-battle' },
|
||||||
'ninth-battle-xuzhou-gate-night-raid': { victory: 'ninth-camp', retry: 'ninth-battle' }
|
'ninth-battle-xuzhou-gate-night-raid': { victory: 'ninth-camp', retry: 'ninth-battle' },
|
||||||
|
'tenth-battle-xuzhou-breakout': { victory: 'tenth-camp', retry: 'tenth-battle' }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CampaignSaveSlotSummary = {
|
export type CampaignSaveSlotSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user