Add Xiapi outer siege chapter

This commit is contained in:
2026-06-23 02:55:33 +09:00
parent 83b2bb6361
commit 450842bd28
11 changed files with 778 additions and 17 deletions

View File

@@ -31,12 +31,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
- 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 - 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
- Eleventh battle Xudu refuge road, opening Liu Bei's uneasy service under Cao Cao through a Yuan Shu remnant test battle - Eleventh battle Xudu refuge road, opening Liu Bei's uneasy service under Cao Cao through a Yuan Shu remnant test battle
- Twelfth battle Xiapi outer siege, putting Liu Bei under Cao Cao's command against Lu Bu while preserving Liu Bei's independent motive and camp tension
- Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, and readiness advice for each deployable officer - Tactical sortie preparation panel with battle-specific sortie limits, recommended officers with reasons, class role, named equipment, core stats, bond partner, next-map terrain suitability, deployment preview, formation roles, active bond count, and readiness advice for each deployable officer
- Flow verification script from title through the eleventh battle victory, recruit sortie selection, and camp save state - Flow verification script from title through the twelfth battle victory, recruit sortie selection, and camp save state
## Next Steps ## Next Steps
1. Continue into Liu Bei's tense service under Cao Cao, then build toward the break from Cao Cao 1. Continue the Xiapi siege toward Lu Bu's fall, then build toward Liu Bei's break from Cao Cao
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

View File

@@ -1179,6 +1179,84 @@ try {
throw new Error(`Expected eleventh camp to expose Cao Cao refuge dialogue set and preserve full roster: ${JSON.stringify(eleventhCampState)}`); throw new Error(`Expected eleventh camp to expose Cao Cao refuge dialogue set and preserve full roster: ${JSON.stringify(eleventhCampState)}`);
} }
await page.mouse.click(1120, 38);
await page.waitForTimeout(160);
const eleventhCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (!eleventhCampSortieState?.sortieVisible || !eleventhCampSortieState.sortiePlan?.objectiveLine?.includes('하비 외곽전')) {
throw new Error(`Expected eleventh camp sortie prep to target Xiapi outer siege: ${JSON.stringify(eleventhCampSortieState)}`);
}
assertSortieTacticalRoster(eleventhCampSortieState, ['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-xiapi-siege-story.png', fullPage: true });
for (let i = 0; i < 18; i += 1) {
const enteredTwelfthBattle = await page.evaluate(() => {
const state = window.__HEROS_DEBUG__?.battle();
return state?.scene === 'BattleScene' && state?.battleId === 'twelfth-battle-xiapi-outer-siege';
});
if (enteredTwelfthBattle) {
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 === 'twelfth-battle-xiapi-outer-siege' && state?.battleOutcome === null && state?.phase === 'idle';
});
await page.screenshot({ path: 'dist/verification-twelfth-battle.png', fullPage: true });
const twelfthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
const twelfthEnemies = twelfthBattleState.units.filter((unit) => unit.faction === 'enemy');
const twelfthAllies = twelfthBattleState.units.filter((unit) => unit.faction === 'ally');
const twelfthEnemyBehaviors = new Set(twelfthEnemies.map((unit) => unit.ai));
if (
twelfthBattleState.camera?.mapWidth !== 30 ||
twelfthBattleState.camera?.mapHeight !== 24 ||
twelfthBattleState.victoryConditionLabel !== '장료 격파' ||
twelfthEnemies.length < 13 ||
!twelfthEnemyBehaviors.has('aggressive') ||
!twelfthEnemyBehaviors.has('guard') ||
!twelfthEnemyBehaviors.has('hold') ||
!twelfthEnemies.some((unit) => unit.id === 'xiapi-strategist-chen-gong') ||
!twelfthAllies.some((unit) => unit.id === 'liu-bei') ||
!twelfthAllies.some((unit) => unit.id === 'guan-yu') ||
!twelfthAllies.some((unit) => unit.id === 'zhang-fei') ||
!twelfthAllies.some((unit) => unit.id === 'jian-yong') ||
!twelfthAllies.some((unit) => unit.id === 'mi-zhu')
) {
throw new Error(`Expected twelfth battle to use Xiapi siege map, mixed AI, and selected full sortie: ${JSON.stringify(twelfthBattleState)}`);
}
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 twelfthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
twelfthCampState?.campBattleId !== 'twelfth-battle-xiapi-outer-siege' ||
twelfthCampState.campTitle !== '하비 포위 군영' ||
twelfthCampState.availableDialogueIds?.length !== 3 ||
!twelfthCampState.availableDialogueIds.every((id) => id.endsWith('xiapi-outer')) ||
!twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') ||
!twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') ||
!twelfthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu')
) {
throw new Error(`Expected twelfth camp to expose Xiapi siege dialogue set and preserve full roster: ${JSON.stringify(twelfthCampState)}`);
}
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() ?? [];
@@ -1190,7 +1268,7 @@ try {
return activeScenes.includes('CampScene'); return activeScenes.includes('CampScene');
}); });
console.log(`Verified title-to-eleventh-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`); console.log(`Verified title-to-twelfth-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) {

View File

@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3000 2400" width="3000" height="2400">
<defs>
<filter id="xiapiGrain">
<feTurbulence type="fractalNoise" baseFrequency="0.017" numOctaves="4" seed="1212" />
<feColorMatrix type="saturate" values="0.68" />
<feBlend mode="multiply" in2="SourceGraphic" />
</filter>
<pattern id="field" width="120" height="120" patternUnits="userSpaceOnUse">
<rect width="120" height="120" fill="#5f704d" />
<path d="M12 34h34M52 80h42M78 20l24 16M22 104l30-20" stroke="#adc077" stroke-width="4" opacity="0.18" />
<path d="M20 68l18-30M62 42l22-20M90 98l14-30" stroke="#283b21" stroke-width="3" opacity="0.34" />
</pattern>
<pattern id="road" width="126" height="126" patternUnits="userSpaceOnUse">
<rect width="126" height="126" fill="#8d7047" />
<path d="M0 36c46-20 88 14 126-10M0 90c52-24 86 12 126-4" stroke="#c7a771" stroke-width="9" opacity="0.24" />
<path d="M24 100l24-32M70 52l30-22M98 98l14-12" stroke="#3a2c20" stroke-width="4" opacity="0.32" />
</pattern>
<pattern id="forest" width="140" height="120" patternUnits="userSpaceOnUse">
<rect width="140" height="120" fill="#1f3825" />
<circle cx="28" cy="52" r="34" fill="#122717" />
<circle cx="68" cy="34" r="40" fill="#456842" />
<circle cx="112" cy="58" r="34" fill="#213a24" />
<circle cx="56" cy="90" r="30" fill="#59774b" />
<path d="M12 72c38 18 84 16 118-12M38 26c26 22 62 20 80-8" stroke="#09180d" stroke-width="5" opacity="0.34" />
</pattern>
<pattern id="hill" width="140" height="110" patternUnits="userSpaceOnUse">
<rect width="140" height="110" fill="#766d4b" />
<path d="M-16 98c52-70 100-88 146-68c38 18 62 50 82 92" fill="#4b4933" opacity="0.72" />
<path d="M20 88c42-46 80-58 118-34" fill="none" stroke="#a39a64" stroke-width="11" opacity="0.26" />
</pattern>
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6e9bad" />
<stop offset="0.55" stop-color="#2d637c" />
<stop offset="1" stop-color="#153448" />
</linearGradient>
<linearGradient id="fortWall" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#8b7661" />
<stop offset="1" stop-color="#3c3026" />
</linearGradient>
<radialGradient id="fire" cx="0.5" cy="0.44" r="0.58">
<stop offset="0" stop-color="#ffe6a0" />
<stop offset="0.48" stop-color="#d6752f" />
<stop offset="1" stop-color="#4b1e14" stop-opacity="0" />
</radialGradient>
</defs>
<rect width="3000" height="2400" fill="#111a1f" />
<rect width="3000" height="2400" fill="url(#field)" opacity="0.96" />
<g filter="url(#xiapiGrain)">
<path d="M1500-110c-70 344-42 620 84 828c122 202 96 468-76 796c-118 226-140 544-66 954" fill="none" stroke="#13354c" stroke-width="310" stroke-linecap="round" opacity="0.94" />
<path d="M1500-110c-70 344-42 620 84 828c122 202 96 468-76 796c-118 226-140 544-66 954" fill="none" stroke="url(#river)" stroke-width="194" stroke-linecap="round" opacity="0.98" />
<path d="M2110 250c-88 350-92 710-16 1080c72 350 70 676-32 1030" fill="none" stroke="#13354c" stroke-width="260" stroke-linecap="round" opacity="0.92" />
<path d="M2110 250c-88 350-92 710-16 1080c72 350 70 676-32 1030" fill="none" stroke="url(#river)" stroke-width="158" stroke-linecap="round" opacity="0.95" />
<path d="M-80 1840C384 1780 802 1782 1240 1816c334 26 636 16 948-60c260-64 528-72 906-16" fill="none" stroke="#94754a" stroke-width="250" stroke-linecap="round" opacity="0.82" />
<path d="M-80 1840C384 1780 802 1782 1240 1816c334 26 636 16 948-60c260-64 528-72 906-16" fill="none" stroke="url(#road)" stroke-width="138" stroke-linecap="round" opacity="0.9" />
<path d="M430 2320C668 1946 956 1640 1294 1402c392-276 790-440 1200-522" fill="none" stroke="#94754a" stroke-width="190" stroke-linecap="round" opacity="0.76" />
<path d="M430 2320C668 1946 956 1640 1294 1402c392-276 790-440 1200-522" fill="none" stroke="url(#road)" stroke-width="100" stroke-linecap="round" opacity="0.84" />
<path d="M650 1010c360 14 722 12 1086-8c344-18 718-4 1110 40" fill="none" stroke="#94754a" stroke-width="210" stroke-linecap="round" opacity="0.78" />
<path d="M650 1010c360 14 722 12 1086-8c344-18 718-4 1110 40" fill="none" stroke="url(#road)" stroke-width="108" stroke-linecap="round" opacity="0.84" />
<path d="M20 10c308-76 588-22 842 164c-24 222-170 364-438 426C190 536 36 406 20 10Z" fill="url(#forest)" opacity="0.82" />
<path d="M360 850c298-122 574-72 828 150c-106 216-382 324-826 324c-116-160-116-318-2-474Z" fill="url(#forest)" opacity="0.82" />
<path d="M2230 1650c214-70 418-38 612 96c-44 246-214 414-510 504c-154-108-188-308-102-600Z" fill="url(#forest)" opacity="0.76" />
<path d="M620 198c240-76 452-36 636 120c-42 144-168 238-378 282c-168-74-254-208-258-402Z" fill="url(#hill)" opacity="0.82" />
<path d="M2420 350c206-66 386-34 540 96c-18 210-126 342-324 396c-154-86-226-250-216-492Z" fill="url(#hill)" opacity="0.82" />
<path d="M900 2060c210-62 400-26 570 108c-28 126-144 202-348 228c-142-68-216-180-222-336Z" fill="url(#hill)" opacity="0.76" />
<g transform="translate(2180 760)">
<rect x="-82" y="-66" width="690" height="650" rx="20" fill="#0f0a07" opacity="0.45" />
<rect x="0" y="0" width="540" height="520" rx="14" fill="url(#fortWall)" stroke="#211711" stroke-width="20" />
<path d="M0 112h540M0 226h540M0 340h540M0 450h540" stroke="#2f251c" stroke-width="14" opacity="0.76" />
<path d="M112 0v520M270 0v520M428 0v520" stroke="#67523e" stroke-width="10" opacity="0.48" />
<rect x="164" y="218" width="210" height="134" fill="#1f1713" stroke="#0e0907" stroke-width="12" />
<path d="M164 218l104-72l106 72" fill="#744923" stroke="#21160e" stroke-width="12" />
<path d="M232 352v-82h74v82" fill="#0d0907" />
<circle cx="84" cy="144" r="76" fill="url(#fire)" opacity="0.68" />
<circle cx="462" cy="374" r="76" fill="url(#fire)" opacity="0.64" />
</g>
<g transform="translate(70 1780)">
<rect x="0" y="0" width="245" height="178" rx="10" fill="#594836" stroke="#201914" stroke-width="12" />
<path d="M-26 22L122-70l170 92Z" fill="#986230" stroke="#2a1c12" stroke-width="12" />
<path d="M76 178v-74h74v74" fill="#201713" />
<circle cx="210" cy="30" r="58" fill="url(#fire)" opacity="0.55" />
</g>
</g>
<rect width="3000" height="2400" fill="#000" opacity="0.1" />
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -19,6 +19,10 @@ import {
tenthBattleMap, tenthBattleMap,
tenthBattleUnits, tenthBattleUnits,
tenthBattleVictoryPages, tenthBattleVictoryPages,
twelfthBattleBonds,
twelfthBattleMap,
twelfthBattleUnits,
twelfthBattleVictoryPages,
fifthBattleBonds, fifthBattleBonds,
fifthBattleMap, fifthBattleMap,
fifthBattleUnits, fifthBattleUnits,
@@ -60,7 +64,8 @@ export type BattleScenarioId =
| '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' | 'tenth-battle-xuzhou-breakout'
| 'eleventh-battle-xudu-refuge-road'; | 'eleventh-battle-xudu-refuge-road'
| 'twelfth-battle-xiapi-outer-siege';
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory'; export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
@@ -682,6 +687,59 @@ export const eleventhBattleScenario: BattleScenarioDefinition = {
nextCampScene: 'CampScene' nextCampScene: 'CampScene'
}; };
export const twelfthBattleScenario: BattleScenarioDefinition = {
id: 'twelfth-battle-xiapi-outer-siege',
title: '하비 외곽전',
victoryConditionLabel: '장료 격파',
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'조조는 하비 포위망의 남쪽 둑길을 유비군에게 맡깁니다. 장료를 격파하면 여포군 선봉은 성 안으로 물러납니다.',
'강줄기와 둑길이 전장을 나눕니다. 여포군 기병은 길을 타고 돌파하고, 성루 궁병과 진궁은 후방에서 압박합니다.',
'둑길 진영을 확보하고 20턴 안에 승리하면 조조 휘하에서의 공적과 유비군의 독자 보급을 함께 지킬 수 있습니다.'
],
map: twelfthBattleMap,
units: twelfthBattleUnits,
bonds: twelfthBattleBonds,
mapTextureKey: 'battle-map-twelfth',
leaderUnitId: 'xiapi-leader-zhang-liao',
quickVictoryTurnLimit: 20,
baseVictoryGold: 1440,
objectives: [
{
id: 'leader',
kind: 'defeat-leader',
label: '장료 격파',
rewardGold: 900,
unitId: 'xiapi-leader-zhang-liao'
},
{
id: 'liu-bei',
kind: 'keep-unit-alive',
label: '유비 생존',
rewardGold: 340,
unitId: 'liu-bei'
},
{
id: 'camp',
kind: 'secure-terrain',
label: '하비 둑길 진영 확보',
rewardGold: 500,
terrain: 'camp'
},
{
id: 'quick',
kind: 'quick-victory',
label: '20턴 이내 승리',
rewardGold: 390,
maxTurn: 20
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
itemRewards: ['콩 4', '상처약 3', '탁주 2', '하비 포위 공적 +1'],
victoryPages: twelfthBattleVictoryPages,
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> = {
@@ -695,7 +753,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
'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, 'tenth-battle-xuzhou-breakout': tenthBattleScenario,
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario 'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario
}; };
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId]; export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];

View File

@@ -10,6 +10,7 @@ import {
seventhBattleScenario, seventhBattleScenario,
sixthBattleScenario, sixthBattleScenario,
tenthBattleScenario, tenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario, thirdBattleScenario,
type BattleScenarioId type BattleScenarioId
} from './battles'; } from './battles';
@@ -33,6 +34,8 @@ import {
sixthBattleVictoryPages, sixthBattleVictoryPages,
tenthBattleIntroPages, tenthBattleIntroPages,
tenthBattleVictoryPages, tenthBattleVictoryPages,
twelfthBattleIntroPages,
twelfthBattleVictoryPages,
thirdBattleIntroPages, thirdBattleIntroPages,
thirdBattleVictoryPages, thirdBattleVictoryPages,
type StoryPage type StoryPage
@@ -153,12 +156,22 @@ const sortieFlows: Record<string, SortieFlow> = {
}, },
[eleventhBattleScenario.id]: { [eleventhBattleScenario.id]: {
afterBattleId: eleventhBattleScenario.id, afterBattleId: eleventhBattleScenario.id,
eyebrow: '다음 전장',
title: twelfthBattleScenario.title,
description: '조조는 유비군을 하비 포위망에 투입합니다. 여포를 다시 마주하는 이 싸움에서 유비군은 조조의 명을 따르되, 서주 백성을 위한 뜻도 지켜야 합니다.',
rewardHint: `예상 보상: ${twelfthBattleScenario.title} 개방`,
nextBattleId: twelfthBattleScenario.id,
campaignStep: 'twelfth-battle',
pages: [...eleventhBattleVictoryPages, ...twelfthBattleIntroPages]
},
[twelfthBattleScenario.id]: {
afterBattleId: twelfthBattleScenario.id,
eyebrow: '다음 장 준비', eyebrow: '다음 장 준비',
title: '조조의 그늘 아래', title: '하비 포위의 끝',
description: '허도 입성로를 열어 조조의 장막에 들어섰지만, 유비군은 보호와 감시를 동시에 받게 됩니다. 다음 흐름은 조조 밑에서 기회를 기다리는 장으로 이어집니다.', description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지장으로 이어집니다.',
rewardHint: '다음 장: 조조 휘하의 긴장 준비 중', rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중',
pages: eleventhBattleVictoryPages, pages: twelfthBattleVictoryPages,
unavailableNotice: '조조 휘하 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.' unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
} }
}; };

View File

@@ -759,6 +759,60 @@ export const eleventhBattleVictoryPages: StoryPage[] = [
} }
]; ];
export const twelfthBattleIntroPages: StoryPage[] = [
{
id: 'twelfth-cao-cao-order',
bgm: 'story-dark',
chapter: '조조의 명',
background: 'story-liu-bei',
text: '허도에 머문 지 오래지 않아 조조는 유비군을 하비 포위전에 부른다. 여포를 치는 명령은 서주를 잃은 한을 풀 기회이자, 조조의 칼끝이 어디를 향하는지 가늠하는 시험이었다.'
},
{
id: 'twelfth-xiapi-shadow',
bgm: 'militia-theme',
chapter: '하비의 물길',
background: 'story-militia',
speaker: '미축',
text: '하비 외곽은 강과 진흙이 얽혀 있습니다. 보급로를 끊고 둑길을 잡지 못하면, 여포군 기병이 포위망의 빈틈을 찢고 나올 것입니다.'
},
{
id: 'twelfth-brothers-before-lubu',
bgm: 'story-dark',
chapter: '다시 마주한 여포',
background: 'story-three-heroes',
speaker: '장비',
portrait: 'zhangFei',
text: '형님, 이번엔 여포의 이름만 들어도 창끝이 뜨겁습니다. 허나 서두르지 않겠습니다. 먼저 포위망을 조이고, 놈의 날뛰는 길을 끊겠습니다.'
},
{
id: 'twelfth-xiapi-sortie',
bgm: 'battle-prep',
chapter: '하비 외곽전',
background: 'story-sortie',
speaker: '유비',
portrait: 'liuBei',
text: '여포를 치는 싸움이 조조의 명이라 해도, 서주 백성이 다시 짓밟히지 않게 하는 일은 우리의 뜻이다. 물길을 잡고 포위망을 열어라.'
}
];
export const twelfthBattleVictoryPages: StoryPage[] = [
{
id: 'twelfth-victory-outer-camp',
bgm: 'militia-theme',
chapter: '무너진 외곽 진영',
background: 'story-militia',
text: '유비군은 하비 외곽의 둑길과 보급 진영을 장악했다. 여포군 선봉은 성 안으로 물러났고, 조조군은 포위망을 한층 더 좁혔다.'
},
{
id: 'twelfth-cao-cao-watches',
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,
@@ -970,6 +1024,12 @@ export const eleventhBattleMap: BattleMap = {
terrain: createEleventhBattleTerrain() terrain: createEleventhBattleTerrain()
}; };
export const twelfthBattleMap: BattleMap = {
width: 30,
height: 24,
terrain: createTwelfthBattleTerrain()
};
export const firstBattleUnits: UnitData[] = [ export const firstBattleUnits: UnitData[] = [
{ {
id: 'liu-bei', id: 'liu-bei',
@@ -3834,6 +3894,293 @@ export const eleventhBattleUnits: UnitData[] = [
} }
]; ];
const twelfthBattleAllyPositions: Record<string, { x: number; y: number }> = {
'liu-bei': { x: 3, y: 18 },
'guan-yu': { x: 4, y: 17 },
'zhang-fei': { x: 4, y: 19 },
'jian-yong': { x: 5, y: 20 },
'mi-zhu': { x: 3, y: 21 }
};
export const twelfthBattleUnits: UnitData[] = [
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
placeScenarioUnit(unit, twelfthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
),
{
id: 'xiapi-cavalry-a',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 76,
hp: 58,
maxHp: 58,
attack: 20,
move: 5,
stats: { might: 95, intelligence: 54, leadership: 74, agility: 96, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 54 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 20,
y: 5
},
{
id: 'xiapi-cavalry-b',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 78,
hp: 59,
maxHp: 59,
attack: 20,
move: 5,
stats: { might: 96, intelligence: 54, leadership: 74, agility: 97, luck: 60 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 23,
y: 15
},
{
id: 'xiapi-cavalry-c',
name: '여포군 돌격기병',
faction: 'enemy',
className: '여포군 기병',
classKey: 'cavalry',
level: 13,
exp: 78,
hp: 59,
maxHp: 59,
attack: 20,
move: 5,
stats: { might: 96, intelligence: 55, leadership: 75, agility: 97, luck: 61 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 25,
y: 10
},
{
id: 'xiapi-dike-guard-a',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 74,
hp: 52,
maxHp: 52,
attack: 17,
move: 3,
stats: { might: 86, intelligence: 56, leadership: 72, agility: 65, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 12,
y: 14
},
{
id: 'xiapi-dike-guard-b',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 76,
hp: 53,
maxHp: 53,
attack: 17,
move: 3,
stats: { might: 87, intelligence: 56, leadership: 73, agility: 66, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 14,
y: 18
},
{
id: 'xiapi-dike-guard-c',
name: '하비 둑길 수비병',
faction: 'enemy',
className: '여포군 보병',
classKey: 'yellowTurban',
level: 12,
exp: 76,
hp: 53,
maxHp: 53,
attack: 17,
move: 3,
stats: { might: 87, intelligence: 57, leadership: 73, agility: 66, luck: 57 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'lamellar-armor', level: 2, exp: 22 },
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
},
x: 17,
y: 9
},
{
id: 'xiapi-archer-a',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 74,
hp: 43,
maxHp: 43,
attack: 18,
move: 3,
stats: { might: 78, intelligence: 68, leadership: 66, agility: 78, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 50 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 22,
y: 7
},
{
id: 'xiapi-archer-b',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 76,
hp: 44,
maxHp: 44,
attack: 18,
move: 3,
stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 52 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 24,
y: 18
},
{
id: 'xiapi-archer-c',
name: '하비 성루 궁병',
faction: 'enemy',
className: '여포군 궁병',
classKey: 'archer',
level: 12,
exp: 76,
hp: 44,
maxHp: 44,
attack: 18,
move: 3,
stats: { might: 79, intelligence: 69, leadership: 67, agility: 79, luck: 58 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 52 },
armor: { itemId: 'cloth-armor', level: 2, exp: 20 },
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
},
x: 19,
y: 3
},
{
id: 'xiapi-camp-raider-a',
name: '여포군 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 12,
exp: 74,
hp: 48,
maxHp: 48,
attack: 18,
move: 4,
stats: { might: 88, intelligence: 55, leadership: 64, agility: 81, luck: 58 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 50 },
armor: { itemId: 'rebel-vest', level: 2, exp: 22 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 9,
y: 8
},
{
id: 'xiapi-camp-raider-b',
name: '여포군 기습병',
faction: 'enemy',
className: '여포군 기습병',
classKey: 'bandit',
level: 12,
exp: 76,
hp: 49,
maxHp: 49,
attack: 18,
move: 4,
stats: { might: 89, intelligence: 55, leadership: 64, agility: 82, luck: 58 },
equipment: {
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 52 },
armor: { itemId: 'rebel-vest', level: 2, exp: 22 },
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
},
x: 11,
y: 21
},
{
id: 'xiapi-strategist-chen-gong',
name: '진궁',
faction: 'enemy',
className: '여포군 책사',
classKey: 'strategist',
level: 13,
exp: 82,
hp: 52,
maxHp: 52,
attack: 18,
move: 3,
stats: { might: 62, intelligence: 96, leadership: 84, agility: 70, luck: 64 },
equipment: {
weapon: { itemId: 'short-bow', level: 2, exp: 54 },
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 22,
y: 12
},
{
id: 'xiapi-leader-zhang-liao',
name: '장료',
faction: 'enemy',
className: '여포군 선봉장',
classKey: 'rebelLeader',
level: 14,
exp: 86,
hp: 78,
maxHp: 78,
attack: 21,
move: 5,
stats: { might: 95, intelligence: 78, leadership: 92, agility: 86, luck: 63 },
equipment: {
weapon: { itemId: 'leader-axe', level: 2, exp: 58 },
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 26 },
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
},
x: 24,
y: 11
}
];
export const firstBattleBonds: BattleBond[] = [ export const firstBattleBonds: BattleBond[] = [
{ {
id: 'liu-bei__guan-yu', id: 'liu-bei__guan-yu',
@@ -3890,6 +4237,7 @@ export const eighthBattleBonds: BattleBond[] = [...firstBattleBonds, ...xuzhouRe
export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario); export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleBondForScenario);
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario); export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario); export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
function createEighthBattleTerrain(): TerrainType[][] { function createEighthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 22 }, (_, y) => return Array.from({ length: 22 }, (_, y) =>
@@ -4046,6 +4394,44 @@ function createEleventhBattleTerrain(): TerrainType[][] {
); );
} }
function createTwelfthBattleTerrain(): TerrainType[][] {
return Array.from({ length: 24 }, (_, y) =>
Array.from({ length: 30 }, (_, x): TerrainType => {
if (x <= 2 && y >= 17 && y <= 22) {
return 'camp';
}
if ((x >= 23 && x <= 27 && y >= 8 && y <= 13) || (x >= 20 && x <= 24 && y >= 2 && y <= 4)) {
return 'fort';
}
if ((x >= 10 && x <= 12 && y >= 16 && y <= 18) || (x >= 16 && x <= 18 && y >= 7 && y <= 9)) {
return 'village';
}
if ((x === 14 || x === 15) && y >= 0 && y <= 22) {
return 'river';
}
if ((x === 20 || x === 21) && y >= 4 && y <= 20) {
return 'river';
}
if ((x >= 12 && x <= 22 && y === 12) || (x >= 4 && x <= 13 && y === 18) || (x >= 6 && x <= 24 && y === 10)) {
return 'road';
}
if ((x >= 2 && x <= 26 && y === x - 2) || (x >= 3 && x <= 15 && y === 21 - x)) {
return 'road';
}
if ((x <= 8 && y <= 8) || (x >= 4 && x <= 11 && y >= 9 && y <= 13) || (x >= 22 && y >= 16 && y <= 22)) {
return 'forest';
}
if ((x >= 6 && x <= 10 && y >= 2 && y <= 5) || (x >= 24 && x <= 28 && y >= 4 && y <= 8) || (x >= 9 && x <= 13 && y >= 20)) {
return 'hill';
}
if ((x >= 27 && y <= 4) || (x >= 27 && y >= 15) || (x <= 1 && y <= 4)) {
return 'cliff';
}
return 'plain';
})
);
}
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData { function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
return { return {
...cloneUnitForScenario(unit), ...cloneUnitForScenario(unit),

View File

@@ -60,7 +60,20 @@ const unitTexture: Record<string, string> = {
'xuzhou-cavalry-c': 'unit-rebel-cavalry', 'xuzhou-cavalry-c': 'unit-rebel-cavalry',
'xuzhou-guard-b': 'unit-rebel', 'xuzhou-guard-b': 'unit-rebel',
'xuzhou-archer-c': 'unit-rebel-archer', 'xuzhou-archer-c': 'unit-rebel-archer',
'xuzhou-leader-xiahou-dun': 'unit-rebel-leader' 'xuzhou-leader-xiahou-dun': 'unit-rebel-leader',
'xiapi-cavalry-a': 'unit-rebel-cavalry',
'xiapi-cavalry-b': 'unit-rebel-cavalry',
'xiapi-cavalry-c': 'unit-rebel-cavalry',
'xiapi-dike-guard-a': 'unit-rebel',
'xiapi-dike-guard-b': 'unit-rebel',
'xiapi-dike-guard-c': 'unit-rebel',
'xiapi-archer-a': 'unit-rebel-archer',
'xiapi-archer-b': 'unit-rebel-archer',
'xiapi-archer-c': 'unit-rebel-archer',
'xiapi-camp-raider-a': 'unit-rebel',
'xiapi-camp-raider-b': 'unit-rebel',
'xiapi-strategist-chen-gong': 'unit-rebel-archer',
'xiapi-leader-zhang-liao': 'unit-rebel-leader'
}; };
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = { const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
@@ -568,7 +581,20 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
'xudu-road-cavalry-b': 'aggressive', 'xudu-road-cavalry-b': 'aggressive',
'xudu-road-guard-a': 'guard', 'xudu-road-guard-a': 'guard',
'xudu-road-guard-b': 'guard', 'xudu-road-guard-b': 'guard',
'xudu-road-leader-ji-ling': 'guard' 'xudu-road-leader-ji-ling': 'guard',
'xiapi-cavalry-a': 'aggressive',
'xiapi-cavalry-b': 'aggressive',
'xiapi-cavalry-c': 'aggressive',
'xiapi-dike-guard-a': 'guard',
'xiapi-dike-guard-b': 'guard',
'xiapi-dike-guard-c': 'guard',
'xiapi-archer-a': 'hold',
'xiapi-archer-b': 'hold',
'xiapi-archer-c': 'hold',
'xiapi-camp-raider-a': 'aggressive',
'xiapi-camp-raider-b': 'aggressive',
'xiapi-strategist-chen-gong': 'hold',
'xiapi-leader-zhang-liao': 'guard'
}; };
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = { const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {

View File

@@ -10,6 +10,7 @@ import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.s
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 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 twelfthBattleMapUrl from '../../assets/images/battle/twelfth-battle-map.svg';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png'; import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png'; import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
@@ -80,6 +81,7 @@ export class BootScene extends Phaser.Scene {
this.load.image('battle-map-ninth', ninthBattleMapUrl); this.load.image('battle-map-ninth', ninthBattleMapUrl);
this.load.image('battle-map-tenth', tenthBattleMapUrl); this.load.image('battle-map-tenth', tenthBattleMapUrl);
this.load.image('battle-map-eleventh', eleventhBattleMapUrl); this.load.image('battle-map-eleventh', eleventhBattleMapUrl);
this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl); this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl); this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl); this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);

View File

@@ -176,7 +176,8 @@ const campBattleIds = {
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', tenth: 'tenth-battle-xuzhou-breakout',
eleventh: 'eleventh-battle-xudu-refuge-road' eleventh: 'eleventh-battle-xudu-refuge-road',
twelfth: 'twelfth-battle-xiapi-outer-siege'
} as const; } as const;
const requiredSortieUnitIds = new Set(['liu-bei']); const requiredSortieUnitIds = new Set(['liu-bei']);
@@ -288,6 +289,16 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
{ unitId: 'mi-zhu', reason: '허도 입성 전 보급 안정에 유리합니다.' } { unitId: 'mi-zhu', reason: '허도 입성 전 보급 안정에 유리합니다.' }
], ],
note: '허도 입성로는 조조군의 감시를 받는 시험입니다. 군율과 보급을 보여줄 무장을 챙기십시오.' note: '허도 입성로는 조조군의 감시를 받는 시험입니다. 군율과 보급을 보여줄 무장을 챙기십시오.'
},
'twelfth-battle-xiapi-outer-siege': {
maxUnits: 5,
recommended: [
{ unitId: 'liu-bei', reason: '조조 휘하 작전에서도 유비군의 명분을 지켜야 합니다.' },
{ unitId: 'zhang-fei', reason: '여포군 기병 돌파를 막고 둑길을 압박합니다.' },
{ unitId: 'jian-yong', reason: '조조군 감시 속 명분과 보고 문서를 조율합니다.' },
{ unitId: 'mi-zhu', reason: '강가 포위전의 보급과 회복 운용에 유리합니다.' }
],
note: '하비 외곽전은 강줄기와 둑길이 전장을 나눕니다. 돌파와 보급, 조조군 보고까지 함께 고려하십시오.'
} }
}; };
@@ -1236,6 +1247,87 @@ const campDialogues: CampDialogue[] = [
rewardExp: 8 rewardExp: 8
} }
] ]
},
{
id: 'liu-zhang-after-xiapi-outer',
title: '창끝을 누르는 법',
availableAfterBattleIds: [campBattleIds.twelfth],
unitIds: ['liu-bei', 'zhang-fei'],
bondId: 'liu-bei__zhang-fei',
rewardExp: 22,
lines: [
'장비: 형님, 여포 놈의 깃발이 눈앞에 있는데도 창을 참는 일이 쉽지 않았습니다.',
'유비: 참았기에 포위망이 무너지지 않았다. 익덕의 창은 분노보다 늦게 나갈 때 더 무겁소.',
'장비: 다음에는 여포의 말발굽보다 먼저 제 마음을 묶어 두겠습니다.'
],
choices: [
{
id: 'praise-restraint',
label: '참은 공을 칭찬한다',
response: '장비는 호통보다 뜻밖의 칭찬에 더 깊이 고개를 숙였고, 다음 싸움에서도 전열을 지키겠다고 다짐했다.',
rewardExp: 10
},
{
id: 'assign-dike-front',
label: '둑길 전열을 맡긴다',
response: '장비는 자신이 앞에서 버티면 여포군 기병도 쉽게 뚫지 못할 것이라며 창대를 움켜쥐었다.',
rewardExp: 9
}
]
},
{
id: 'liu-jian-yong-after-xiapi-outer',
title: '공적을 쓰는 붓',
availableAfterBattleIds: [campBattleIds.twelfth],
unitIds: ['liu-bei', 'jian-yong'],
bondId: 'liu-bei__jian-yong',
rewardExp: 20,
lines: [
'간옹: 오늘 공적은 조조의 장부에도 오르겠지만, 그 장부가 우리 목줄이 되지 않게 써야 하네.',
'유비: 공을 세워도 빚이 되고, 물러서도 의심이 되는 형국이오.',
'간옹: 그래서 말의 끝을 남겨야지. 우리는 조조를 위해서만 싸운 것이 아니라 서주 백성을 위해 싸웠다고.'
],
choices: [
{
id: 'record-people-first',
label: '백성 보호를 먼저 기록한다',
response: '간옹은 전공 보고의 첫 줄에 피난민과 보급로 보호를 적어 유비군의 명분을 분명히 했다.',
rewardExp: 9
},
{
id: 'keep-private-notes',
label: '별도 기록을 남긴다',
response: '간옹은 조조에게 보일 보고와 훗날 유비군이 기억할 기록을 따로 남겼다.',
rewardExp: 8
}
]
},
{
id: 'liu-mi-after-xiapi-outer',
title: '물가의 보급선',
availableAfterBattleIds: [campBattleIds.twelfth],
unitIds: ['liu-bei', 'mi-zhu'],
bondId: 'liu-bei__mi-zhu',
rewardExp: 18,
lines: [
'미축: 강가 포위전은 싸움보다 군량이 먼저 흔들립니다. 조조군 배급만 믿으면 우리 병사 마음도 함께 묶입니다.',
'유비: 독자 보급이 곧 독자적인 마음이오. 작은 양이라도 우리가 스스로 마련할 길을 남깁시다.',
'미축: 그렇다면 하비 외곽에서 거둔 전리품은 공용과 예비로 나누어 두겠습니다.'
],
choices: [
{
id: 'split-siege-supplies',
label: '보급을 둘로 나눈다',
response: '미축은 조조군 공용 보급과 유비군 예비 보급을 나누어, 훗날 떠날 수 있는 여지를 남겼다.',
rewardExp: 8
},
{
id: 'secure-river-cart',
label: '강가 수레를 확보한다',
response: '미축은 강가에서 쓸 수레와 배를 따로 챙겨 다음 포위전의 군량 흐름을 안정시켰다.',
rewardExp: 8
}
]
} }
]; ];
@@ -1319,7 +1411,8 @@ export class CampScene extends Phaser.Scene {
battleId !== campBattleIds.eighth && battleId !== campBattleIds.eighth &&
battleId !== campBattleIds.ninth && battleId !== campBattleIds.ninth &&
battleId !== campBattleIds.tenth && battleId !== campBattleIds.tenth &&
battleId !== campBattleIds.eleventh) || battleId !== campBattleIds.eleventh &&
battleId !== campBattleIds.twelfth) ||
!this.campaign !this.campaign
) { ) {
return; return;
@@ -1341,6 +1434,9 @@ export class CampScene extends Phaser.Scene {
private currentCampTitle() { private currentCampTitle() {
const battleId = this.currentCampBattleId(); const battleId = this.currentCampBattleId();
if (battleId === campBattleIds.twelfth) {
return '하비 포위 군영';
}
if (battleId === campBattleIds.eleventh) { if (battleId === campBattleIds.eleventh) {
return '허도 의탁 군영'; return '허도 의탁 군영';
} }

View File

@@ -11,6 +11,7 @@ import {
seventhBattleScenario, seventhBattleScenario,
sixthBattleScenario, sixthBattleScenario,
tenthBattleScenario, tenthBattleScenario,
twelfthBattleScenario,
thirdBattleScenario thirdBattleScenario
} from '../data/battles'; } from '../data/battles';
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState'; import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
@@ -321,7 +322,8 @@ export class TitleScene extends Phaser.Scene {
campaign.step === 'eighth-camp' || campaign.step === 'eighth-camp' ||
campaign.step === 'ninth-camp' || campaign.step === 'ninth-camp' ||
campaign.step === 'tenth-camp' || campaign.step === 'tenth-camp' ||
campaign.step === 'eleventh-camp' campaign.step === 'eleventh-camp' ||
campaign.step === 'twelfth-camp'
) { ) {
this.scene.start('CampScene'); this.scene.start('CampScene');
return; return;
@@ -382,6 +384,11 @@ export class TitleScene extends Phaser.Scene {
return; return;
} }
if (campaign.step === 'twelfth-battle') {
this.scene.start('BattleScene', { battleId: twelfthBattleScenario.id });
return;
}
if (campaign.step === 'first-victory-story') { if (campaign.step === 'first-victory-story') {
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' }); this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
return; return;

View File

@@ -61,7 +61,9 @@ export type CampaignStep =
| 'tenth-battle' | 'tenth-battle'
| 'tenth-camp' | 'tenth-camp'
| 'eleventh-battle' | 'eleventh-battle'
| 'eleventh-camp'; | 'eleventh-camp'
| 'twelfth-battle'
| 'twelfth-camp';
export type CampaignUnitProgressSnapshot = { export type CampaignUnitProgressSnapshot = {
unitId: string; unitId: string;
@@ -123,7 +125,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'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' }, 'tenth-battle-xuzhou-breakout': { victory: 'tenth-camp', retry: 'tenth-battle' },
'eleventh-battle-xudu-refuge-road': { victory: 'eleventh-camp', retry: 'eleventh-battle' } 'eleventh-battle-xudu-refuge-road': { victory: 'eleventh-camp', retry: 'eleventh-battle' },
'twelfth-battle-xiapi-outer-siege': { victory: 'twelfth-camp', retry: 'twelfth-battle' }
}; };
export type CampaignSaveSlotSummary = { export type CampaignSaveSlotSummary = {