Add Xuzhou gate night raid battle
This commit is contained in:
@@ -28,11 +28,12 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
|||||||
- Sortie preparation selection in camp so Liu Bei is required and optional allied officers can be deployed or benched before battle
|
- Sortie preparation selection in camp so Liu Bei is required and optional allied officers can be deployed or benched before battle
|
||||||
- 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
|
||||||
- Flow verification script from title through the eighth battle victory, recruit sortie selection, and camp save state
|
- Ninth battle Xuzhou gate night raid, opening Lu Bu's refuge and the internal instability that leads toward the loss of Xu Province
|
||||||
|
- Flow verification script from title through the ninth battle victory, recruit sortie selection, and camp save state
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Continue into the Lu Bu refuge and Xu Province loss chapter
|
1. Continue into Zhang Fei's mistake and the full loss of Xu Province to Lu Bu
|
||||||
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
|
||||||
|
|||||||
@@ -930,6 +930,89 @@ try {
|
|||||||
throw new Error(`Expected eighth camp to expose Xiaopei dialogue set and preserve recruits: ${JSON.stringify(eighthCampState)}`);
|
throw new Error(`Expected eighth camp to expose Xiaopei dialogue set and preserve recruits: ${JSON.stringify(eighthCampState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.mouse.click(1120, 38);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
const eighthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (!eighthCampSortieState?.sortieVisible) {
|
||||||
|
throw new Error(`Expected eighth camp sortie prep overlay: ${JSON.stringify(eighthCampSortieState)}`);
|
||||||
|
}
|
||||||
|
if (!eighthCampSortieState.selectedSortieUnitIds?.includes('mi-zhu')) {
|
||||||
|
await page.mouse.click(256, 516);
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
}
|
||||||
|
const eighthCampMiZhuSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (!eighthCampMiZhuSortieState.selectedSortieUnitIds?.includes('mi-zhu')) {
|
||||||
|
throw new Error(`Expected Mi Zhu to be selectable for ninth battle sortie: ${JSON.stringify(eighthCampMiZhuSortieState)}`);
|
||||||
|
}
|
||||||
|
await page.mouse.click(938, 596);
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
|
return activeScenes.includes('StoryScene');
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: 'dist/verification-lubu-refuge-story.png', fullPage: true });
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
const enteredNinthBattle = await page.evaluate(() => {
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
return state?.scene === 'BattleScene' && state?.battleId === 'ninth-battle-xuzhou-gate-night-raid';
|
||||||
|
});
|
||||||
|
if (enteredNinthBattle) {
|
||||||
|
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 === 'ninth-battle-xuzhou-gate-night-raid' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: 'dist/verification-ninth-battle.png', fullPage: true });
|
||||||
|
|
||||||
|
const ninthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||||
|
const ninthEnemies = ninthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||||
|
const ninthAllies = ninthBattleState.units.filter((unit) => unit.faction === 'ally');
|
||||||
|
const ninthEnemyBehaviors = new Set(ninthEnemies.map((unit) => unit.ai));
|
||||||
|
if (
|
||||||
|
ninthBattleState.camera?.mapWidth !== 26 ||
|
||||||
|
ninthBattleState.camera?.mapHeight !== 22 ||
|
||||||
|
ninthBattleState.victoryConditionLabel !== '조표 격파' ||
|
||||||
|
ninthEnemies.length < 10 ||
|
||||||
|
!ninthEnemyBehaviors.has('aggressive') ||
|
||||||
|
!ninthEnemyBehaviors.has('guard') ||
|
||||||
|
!ninthEnemyBehaviors.has('hold') ||
|
||||||
|
ninthAllies.some((unit) => unit.id === 'guan-yu') ||
|
||||||
|
!ninthAllies.some((unit) => unit.id === 'liu-bei') ||
|
||||||
|
!ninthAllies.some((unit) => unit.id === 'zhang-fei') ||
|
||||||
|
!ninthAllies.some((unit) => unit.id === 'jian-yong') ||
|
||||||
|
!ninthAllies.some((unit) => unit.id === 'mi-zhu')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected ninth battle to use gate map, mixed AI, and selected recruit sortie: ${JSON.stringify(ninthBattleState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ninthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
if (
|
||||||
|
ninthCampState?.campBattleId !== 'ninth-battle-xuzhou-gate-night-raid' ||
|
||||||
|
ninthCampState.campTitle !== '서주 성문 군영' ||
|
||||||
|
ninthCampState.availableDialogueIds?.length !== 3 ||
|
||||||
|
!ninthCampState.availableDialogueIds.every((id) => id.endsWith('xuzhou-gate')) ||
|
||||||
|
!ninthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') ||
|
||||||
|
!ninthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu')
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected ninth camp to expose Xuzhou gate dialogue set and preserve recruits: ${JSON.stringify(ninthCampState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
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() ?? [];
|
||||||
@@ -941,7 +1024,7 @@ try {
|
|||||||
return activeScenes.includes('CampScene');
|
return activeScenes.includes('CampScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Verified title-to-eighth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
|
console.log(`Verified title-to-ninth-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) {
|
||||||
|
|||||||
88
src/assets/images/battle/ninth-battle-map.svg
Normal file
88
src/assets/images/battle/ninth-battle-map.svg
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2600 2200" width="2600" height="2200">
|
||||||
|
<defs>
|
||||||
|
<filter id="nightGrain">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.018" numOctaves="4" seed="922" />
|
||||||
|
<feColorMatrix type="saturate" values="0.55" />
|
||||||
|
<feBlend mode="multiply" in2="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
<pattern id="grass" width="96" height="96" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="96" height="96" fill="#4e6547" />
|
||||||
|
<path d="M8 28h28M48 58h30M24 76l20-14M66 18l16 12" stroke="#9fb36f" stroke-width="3" opacity="0.22" />
|
||||||
|
<path d="M12 62l14-24M40 34l13-18M70 80l9-25" stroke="#263f2a" stroke-width="2" opacity="0.38" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="earth" width="104" height="104" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="104" height="104" fill="#816640" />
|
||||||
|
<path d="M0 26c38-18 70 12 104-8M0 70c42-18 72 14 104-4" stroke="#b08b59" stroke-width="7" opacity="0.24" />
|
||||||
|
<path d="M20 86l18-24M62 42l24-16M78 82l14-9" stroke="#423323" stroke-width="3" opacity="0.34" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="forest" width="126" height="110" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="126" height="110" fill="#233a29" />
|
||||||
|
<circle cx="26" cy="42" r="30" fill="#172a1b" />
|
||||||
|
<circle cx="62" cy="32" r="35" fill="#425f3f" />
|
||||||
|
<circle cx="96" cy="50" r="31" fill="#1f3622" />
|
||||||
|
<circle cx="52" cy="82" r="27" fill="#4e6844" />
|
||||||
|
<path d="M14 62c34 18 74 16 104-8M36 26c22 18 54 16 72-6" stroke="#0d1c12" stroke-width="5" opacity="0.38" />
|
||||||
|
</pattern>
|
||||||
|
<pattern id="hill" width="132" height="98" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="132" height="98" fill="#655f44" />
|
||||||
|
<path d="M-10 84c48-62 92-82 134-64c36 15 56 44 74 80" fill="#444530" opacity="0.72" />
|
||||||
|
<path d="M18 76c36-40 72-50 108-32" fill="none" stroke="#928957" stroke-width="10" opacity="0.28" />
|
||||||
|
</pattern>
|
||||||
|
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#4a839b" />
|
||||||
|
<stop offset="0.52" stop-color="#224e68" />
|
||||||
|
<stop offset="1" stop-color="#102b42" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#89735d" />
|
||||||
|
<stop offset="1" stop-color="#3d3025" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="torch" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ffdf82" />
|
||||||
|
<stop offset="1" stop-color="#b24a20" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="2600" height="2200" fill="#101923" />
|
||||||
|
<rect width="2600" height="2200" fill="url(#grass)" opacity="0.92" />
|
||||||
|
<g filter="url(#nightGrain)">
|
||||||
|
<path d="M-120 1050C360 1010 780 1040 1140 1040C1540 1040 1980 980 2700 980" fill="none" stroke="#8e6f47" stroke-width="252" stroke-linecap="round" opacity="0.92" />
|
||||||
|
<path d="M-120 1050C360 1010 780 1040 1140 1040C1540 1040 1980 980 2700 980" fill="none" stroke="url(#earth)" stroke-width="150" stroke-linecap="round" opacity="0.92" />
|
||||||
|
<path d="M780 2180C760 1750 790 1420 810 1160C830 890 812 560 820 140" fill="none" stroke="#8e6f47" stroke-width="180" stroke-linecap="round" opacity="0.86" />
|
||||||
|
<path d="M780 2180C760 1750 790 1420 810 1160C830 890 812 560 820 140" fill="none" stroke="url(#earth)" stroke-width="104" stroke-linecap="round" opacity="0.82" />
|
||||||
|
<path d="M1380 2220c92-294 94-520 10-724c-82-198-70-382 36-552c116-184 126-438 30-892" fill="none" stroke="#102a3e" stroke-width="280" stroke-linecap="round" opacity="0.96" />
|
||||||
|
<path d="M1380 2220c92-294 94-520 10-724c-82-198-70-382 36-552c116-184 126-438 30-892" fill="none" stroke="url(#river)" stroke-width="180" stroke-linecap="round" opacity="0.98" />
|
||||||
|
|
||||||
|
<path d="M0 20c280-80 556-44 820 112c44 154-60 292-310 414c-262-30-430-126-510-292Z" fill="url(#forest)" opacity="0.84" />
|
||||||
|
<path d="M120 760c280-120 560-70 826 158c-84 226-350 354-796 374C18 1122-10 954 120 760Z" fill="url(#forest)" opacity="0.82" />
|
||||||
|
<path d="M870 80c230-82 432-48 606 94c2 150-104 262-318 334c-210-42-310-184-288-428Z" fill="url(#forest)" opacity="0.76" />
|
||||||
|
<path d="M430 1450c270-104 526-64 768 120c-48 208-236 336-566 384c-174-86-240-252-202-504Z" fill="url(#forest)" opacity="0.8" />
|
||||||
|
<path d="M2140 60c190-48 356-20 498 86v230c-174 46-334 16-480-92c-54-76-60-150-18-224Z" fill="url(#hill)" opacity="0.82" />
|
||||||
|
<path d="M2180 1720c176-58 332-34 468 72v350c-192 62-358 28-500-102c-42-122-32-228 32-320Z" fill="url(#hill)" opacity="0.82" />
|
||||||
|
|
||||||
|
<g transform="translate(1800 300)">
|
||||||
|
<rect x="-50" y="-34" width="700" height="1260" rx="22" fill="#1c1713" opacity="0.55" />
|
||||||
|
<rect x="0" y="0" width="600" height="1200" rx="16" fill="url(#wall)" stroke="#241d15" stroke-width="18" />
|
||||||
|
<path d="M0 160h600M0 340h600M0 520h600M0 700h600M0 880h600M0 1060h600" stroke="#30261d" stroke-width="12" opacity="0.72" />
|
||||||
|
<path d="M110 0v1200M300 0v1200M490 0v1200" stroke="#5f4f3d" stroke-width="10" opacity="0.48" />
|
||||||
|
<rect x="150" y="520" width="300" height="180" fill="#211914" stroke="#100b08" stroke-width="12" />
|
||||||
|
<path d="M150 520l150-90l150 90" fill="#6f4822" stroke="#21160e" stroke-width="12" />
|
||||||
|
<path d="M246 700v-108h108v108" fill="#0d0907" />
|
||||||
|
<circle cx="94" cy="520" r="20" fill="url(#torch)" opacity="0.9" />
|
||||||
|
<circle cx="506" cy="700" r="20" fill="url(#torch)" opacity="0.9" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(960 1300)">
|
||||||
|
<rect x="0" y="0" width="240" height="132" rx="10" fill="#5f4a35" stroke="#251d16" stroke-width="10" />
|
||||||
|
<path d="M-18 16L120-70l140 86Z" fill="#9a612d" stroke="#2b1f14" stroke-width="10" />
|
||||||
|
<path d="M72 132v-58h56v58" fill="#201713" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(70 1650)">
|
||||||
|
<rect x="0" y="0" width="360" height="245" rx="16" fill="#47574d" stroke="#1c241d" stroke-width="14" />
|
||||||
|
<path d="M22 60h315M22 124h315M22 188h315M86 22v200M180 22v200M274 22v200" stroke="#243126" stroke-width="9" opacity="0.7" />
|
||||||
|
<rect x="82" y="-38" width="196" height="84" fill="#8d5528" stroke="#24190e" stroke-width="10" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect width="2600" height="2200" fill="#08101c" opacity="0.18" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -7,6 +7,10 @@ import {
|
|||||||
firstBattleMap,
|
firstBattleMap,
|
||||||
firstBattleUnits,
|
firstBattleUnits,
|
||||||
firstBattleVictoryPages,
|
firstBattleVictoryPages,
|
||||||
|
ninthBattleBonds,
|
||||||
|
ninthBattleMap,
|
||||||
|
ninthBattleUnits,
|
||||||
|
ninthBattleVictoryPages,
|
||||||
fifthBattleBonds,
|
fifthBattleBonds,
|
||||||
fifthBattleMap,
|
fifthBattleMap,
|
||||||
fifthBattleUnits,
|
fifthBattleUnits,
|
||||||
@@ -45,7 +49,8 @@ export type BattleScenarioId =
|
|||||||
| 'fifth-battle-sishui-vanguard'
|
| 'fifth-battle-sishui-vanguard'
|
||||||
| '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';
|
||||||
|
|
||||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||||
|
|
||||||
@@ -508,6 +513,59 @@ export const eighthBattleScenario: BattleScenarioDefinition = {
|
|||||||
nextCampScene: 'CampScene'
|
nextCampScene: 'CampScene'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ninthBattleScenario: BattleScenarioDefinition = {
|
||||||
|
id: 'ninth-battle-xuzhou-gate-night-raid',
|
||||||
|
title: '서주 성문 야습',
|
||||||
|
victoryConditionLabel: '조표 격파',
|
||||||
|
defeatConditionLabel: '유비 퇴각',
|
||||||
|
openingObjectiveLines: [
|
||||||
|
'조표가 성문 수비병을 움직이고 여포군 기병이 밤길을 타고 접근합니다. 조표를 격파하면 성문을 다시 장악할 수 있습니다.',
|
||||||
|
'성벽 궁병은 높은 곳을 지키고, 여포군 기병은 길을 따라 빠르게 파고듭니다. 마을과 성문 지형을 이용해 전열을 끊어야 합니다.',
|
||||||
|
'18턴 이내 성문을 안정시키면 서주 내부 불안을 잠시나마 눌러 둘 수 있습니다.'
|
||||||
|
],
|
||||||
|
map: ninthBattleMap,
|
||||||
|
units: ninthBattleUnits,
|
||||||
|
bonds: ninthBattleBonds,
|
||||||
|
mapTextureKey: 'battle-map-ninth',
|
||||||
|
leaderUnitId: 'xuzhou-leader-cao-bao',
|
||||||
|
quickVictoryTurnLimit: 18,
|
||||||
|
baseVictoryGold: 1160,
|
||||||
|
objectives: [
|
||||||
|
{
|
||||||
|
id: 'leader',
|
||||||
|
kind: 'defeat-leader',
|
||||||
|
label: '조표 격파',
|
||||||
|
rewardGold: 700,
|
||||||
|
unitId: 'xuzhou-leader-cao-bao'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-bei',
|
||||||
|
kind: 'keep-unit-alive',
|
||||||
|
label: '유비 생존',
|
||||||
|
rewardGold: 280,
|
||||||
|
unitId: 'liu-bei'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fort',
|
||||||
|
kind: 'secure-terrain',
|
||||||
|
label: '서주 성문 확보',
|
||||||
|
rewardGold: 400,
|
||||||
|
terrain: 'fort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quick',
|
||||||
|
kind: 'quick-victory',
|
||||||
|
label: '18턴 이내 승리',
|
||||||
|
rewardGold: 320,
|
||||||
|
maxTurn: 18
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||||
|
itemRewards: ['콩 3', '상처약 2', '탁주 1', '서주 성문 수비 +1'],
|
||||||
|
victoryPages: ninthBattleVictoryPages,
|
||||||
|
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> = {
|
||||||
@@ -518,7 +576,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
|
|||||||
'fifth-battle-sishui-vanguard': fifthBattleScenario,
|
'fifth-battle-sishui-vanguard': fifthBattleScenario,
|
||||||
'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
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
eighthBattleScenario,
|
eighthBattleScenario,
|
||||||
fifthBattleScenario,
|
fifthBattleScenario,
|
||||||
fourthBattleScenario,
|
fourthBattleScenario,
|
||||||
|
ninthBattleScenario,
|
||||||
secondBattleScenario,
|
secondBattleScenario,
|
||||||
seventhBattleScenario,
|
seventhBattleScenario,
|
||||||
sixthBattleScenario,
|
sixthBattleScenario,
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
firstBattleVictoryPages,
|
firstBattleVictoryPages,
|
||||||
fourthBattleIntroPages,
|
fourthBattleIntroPages,
|
||||||
fourthBattleVictoryPages,
|
fourthBattleVictoryPages,
|
||||||
|
ninthBattleIntroPages,
|
||||||
|
ninthBattleVictoryPages,
|
||||||
secondBattleIntroPages,
|
secondBattleIntroPages,
|
||||||
secondBattleVictoryPages,
|
secondBattleVictoryPages,
|
||||||
seventhBattleIntroPages,
|
seventhBattleIntroPages,
|
||||||
@@ -114,12 +117,22 @@ const sortieFlows: Record<string, SortieFlow> = {
|
|||||||
},
|
},
|
||||||
[eighthBattleScenario.id]: {
|
[eighthBattleScenario.id]: {
|
||||||
afterBattleId: eighthBattleScenario.id,
|
afterBattleId: eighthBattleScenario.id,
|
||||||
|
eyebrow: '다음 전장',
|
||||||
|
title: ninthBattleScenario.title,
|
||||||
|
description: '여포를 의탁시킨 뒤 서주 성문 안팎의 불안이 터집니다. 조표의 야습을 막아 성문을 지켜내야 합니다.',
|
||||||
|
rewardHint: `예상 보상: ${ninthBattleScenario.title} 개방`,
|
||||||
|
nextBattleId: ninthBattleScenario.id,
|
||||||
|
campaignStep: 'ninth-battle',
|
||||||
|
pages: [...eighthBattleVictoryPages, ...ninthBattleIntroPages]
|
||||||
|
},
|
||||||
|
[ninthBattleScenario.id]: {
|
||||||
|
afterBattleId: ninthBattleScenario.id,
|
||||||
eyebrow: '다음 장 준비',
|
eyebrow: '다음 장 준비',
|
||||||
title: '여포의 의탁',
|
title: '서주 상실의 전조',
|
||||||
description: '소패 보급로를 지켜냈지만 여포가 몸을 의탁하겠다는 전갈이 도착합니다. 받아들이는 선택은 곧 서주 상실의 갈등으로 이어질 것입니다.',
|
description: '성문은 지켰지만 여포와 서주 내부의 불씨는 더 커졌습니다. 다음 흐름은 장비의 실책과 서주 상실로 이어집니다.',
|
||||||
rewardHint: '다음 장: 여포 의탁과 서주 내부 불안 준비 중',
|
rewardHint: '다음 장: 장비의 실책과 서주 상실 준비 중',
|
||||||
pages: eighthBattleVictoryPages,
|
pages: ninthBattleVictoryPages,
|
||||||
unavailableNotice: '여포 의탁 이후의 서주 상실 흐름은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
|
unavailableNotice: '서주 상실 전투는 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -594,6 +594,61 @@ export const eighthBattleVictoryPages: StoryPage[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ninthBattleIntroPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'ninth-lu-bu-refuge',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '여포의 의탁',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '유비',
|
||||||
|
portrait: 'liuBei',
|
||||||
|
text: '여포가 몸을 의탁하겠다 청했다. 난세에 갈 곳 없는 이를 내치는 것은 쉽지 않으나, 서주 백성의 불안을 모른 척할 수도 없다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ninth-zhang-fei-suspicion',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '성문 안의 불씨',
|
||||||
|
background: 'story-three-heroes',
|
||||||
|
speaker: '장비',
|
||||||
|
portrait: 'zhangFei',
|
||||||
|
text: '여포 같은 자를 성 안에 들이면 반드시 화근이 됩니다. 도겸의 옛 무장들까지 술렁이니, 성문부터 단단히 지켜야 합니다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ninth-cao-bao-mutiny',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '조표의 야습',
|
||||||
|
background: 'story-sortie',
|
||||||
|
text: '밤이 깊자 조표가 일부 성문 수비병을 움직였다. 바깥의 여포군 기병은 불빛을 신호로 삼아 성문을 향해 몰려들었다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ninth-xuzhou-gate',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '서주 성문 방위',
|
||||||
|
background: 'story-militia',
|
||||||
|
speaker: '간옹',
|
||||||
|
text: '현덕, 이 싸움은 이겨도 상처가 남을 걸세. 하지만 성문이 오늘 밤 무너지면 내일의 선택도 사라진다네.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ninthBattleVictoryPages: StoryPage[] = [
|
||||||
|
{
|
||||||
|
id: 'ninth-victory-gate-held',
|
||||||
|
bgm: 'militia-theme',
|
||||||
|
chapter: '지켜낸 밤',
|
||||||
|
background: 'story-militia',
|
||||||
|
text: '조표의 야습은 물러났고 성문은 다시 닫혔다. 그러나 성 안의 의심과 두려움은 이미 선명한 금이 되어 남았다.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ninth-lu-bu-pretext',
|
||||||
|
bgm: 'battle-prep',
|
||||||
|
chapter: '더 깊어진 그림자',
|
||||||
|
background: 'story-liu-bei',
|
||||||
|
speaker: '유비',
|
||||||
|
portrait: 'liuBei',
|
||||||
|
text: '여포는 서주의 혼란을 핑계 삼아 더 깊이 발을 들이려 했다. 오늘 밤 성문은 지켰으나, 서주를 잃을 싸움은 이제 시작되고 있었다.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleMap: BattleMap = {
|
export const firstBattleMap: BattleMap = {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 18,
|
height: 18,
|
||||||
@@ -787,6 +842,12 @@ export const eighthBattleMap: BattleMap = {
|
|||||||
terrain: createEighthBattleTerrain()
|
terrain: createEighthBattleTerrain()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ninthBattleMap: BattleMap = {
|
||||||
|
width: 26,
|
||||||
|
height: 22,
|
||||||
|
terrain: createNinthBattleTerrain()
|
||||||
|
};
|
||||||
|
|
||||||
export const firstBattleUnits: UnitData[] = [
|
export const firstBattleUnits: UnitData[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei',
|
id: 'liu-bei',
|
||||||
@@ -2916,6 +2977,230 @@ export const eighthBattleUnits: UnitData[] = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ninthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||||
|
'liu-bei': { x: 3, y: 17 },
|
||||||
|
'guan-yu': { x: 4, y: 17 },
|
||||||
|
'zhang-fei': { x: 3, y: 18 },
|
||||||
|
'jian-yong': { x: 5, y: 18 },
|
||||||
|
'mi-zhu': { x: 4, y: 19 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ninthBattleUnits: UnitData[] = [
|
||||||
|
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
|
||||||
|
placeScenarioUnit(unit, ninthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-traitor-a',
|
||||||
|
name: '성문 배반병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '서주 배반병',
|
||||||
|
classKey: 'bandit',
|
||||||
|
level: 9,
|
||||||
|
exp: 56,
|
||||||
|
hp: 40,
|
||||||
|
maxHp: 40,
|
||||||
|
attack: 15,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 80, intelligence: 48, leadership: 58, agility: 74, luck: 52 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 30 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 11,
|
||||||
|
y: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-traitor-b',
|
||||||
|
name: '성문 배반병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '서주 배반병',
|
||||||
|
classKey: 'bandit',
|
||||||
|
level: 9,
|
||||||
|
exp: 58,
|
||||||
|
hp: 41,
|
||||||
|
maxHp: 41,
|
||||||
|
attack: 15,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 81, intelligence: 49, leadership: 59, agility: 74, luck: 52 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 32 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 12,
|
||||||
|
y: 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-infantry-a',
|
||||||
|
name: '성문 수비병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '조표 휘하 보병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 9,
|
||||||
|
exp: 56,
|
||||||
|
hp: 44,
|
||||||
|
maxHp: 44,
|
||||||
|
attack: 14,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 80, intelligence: 52, leadership: 68, agility: 62, luck: 54 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 30 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 17,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-infantry-b',
|
||||||
|
name: '성문 수비병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '조표 휘하 보병',
|
||||||
|
classKey: 'yellowTurban',
|
||||||
|
level: 9,
|
||||||
|
exp: 58,
|
||||||
|
hp: 45,
|
||||||
|
maxHp: 45,
|
||||||
|
attack: 14,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 81, intelligence: 52, leadership: 68, agility: 63, luck: 54 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 32 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 18,
|
||||||
|
y: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-archer-a',
|
||||||
|
name: '성벽 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '성벽 궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 9,
|
||||||
|
exp: 58,
|
||||||
|
hp: 36,
|
||||||
|
maxHp: 36,
|
||||||
|
attack: 15,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 72, intelligence: 64, leadership: 62, agility: 73, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 2, exp: 32 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 20,
|
||||||
|
y: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-gate-archer-b',
|
||||||
|
name: '성벽 궁병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '성벽 궁병',
|
||||||
|
classKey: 'archer',
|
||||||
|
level: 9,
|
||||||
|
exp: 60,
|
||||||
|
hp: 37,
|
||||||
|
maxHp: 37,
|
||||||
|
attack: 15,
|
||||||
|
move: 3,
|
||||||
|
stats: { might: 73, intelligence: 64, leadership: 63, agility: 74, luck: 55 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 2, exp: 34 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 2, exp: 14 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 22,
|
||||||
|
y: 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lubu-cavalry-a',
|
||||||
|
name: '여포군 기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 10,
|
||||||
|
exp: 62,
|
||||||
|
hp: 49,
|
||||||
|
maxHp: 49,
|
||||||
|
attack: 17,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 88, intelligence: 50, leadership: 68, agility: 90, luck: 57 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 36 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 2, exp: 18 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 23,
|
||||||
|
y: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lubu-cavalry-b',
|
||||||
|
name: '여포군 기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 10,
|
||||||
|
exp: 64,
|
||||||
|
hp: 50,
|
||||||
|
maxHp: 50,
|
||||||
|
attack: 17,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 89, intelligence: 50, leadership: 68, agility: 91, luck: 57 },
|
||||||
|
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: 24,
|
||||||
|
y: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lubu-cavalry-c',
|
||||||
|
name: '여포군 기병',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '여포군 기병',
|
||||||
|
classKey: 'cavalry',
|
||||||
|
level: 10,
|
||||||
|
exp: 66,
|
||||||
|
hp: 50,
|
||||||
|
maxHp: 50,
|
||||||
|
attack: 18,
|
||||||
|
move: 5,
|
||||||
|
stats: { might: 90, intelligence: 52, leadership: 70, agility: 91, luck: 58 },
|
||||||
|
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: 21,
|
||||||
|
y: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xuzhou-leader-cao-bao',
|
||||||
|
name: '조표',
|
||||||
|
faction: 'enemy',
|
||||||
|
className: '서주 성문장',
|
||||||
|
classKey: 'rebelLeader',
|
||||||
|
level: 10,
|
||||||
|
exp: 68,
|
||||||
|
hp: 64,
|
||||||
|
maxHp: 64,
|
||||||
|
attack: 17,
|
||||||
|
move: 4,
|
||||||
|
stats: { might: 84, intelligence: 60, leadership: 82, agility: 66, luck: 56 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'leader-axe', level: 2, exp: 38 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 2, exp: 20 },
|
||||||
|
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
|
x: 20,
|
||||||
|
y: 10
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const firstBattleBonds: BattleBond[] = [
|
export const firstBattleBonds: BattleBond[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-bei__guan-yu',
|
id: 'liu-bei__guan-yu',
|
||||||
@@ -2969,6 +3254,7 @@ export const fifthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBo
|
|||||||
export const sixthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
export const sixthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||||
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);
|
||||||
|
|
||||||
function createEighthBattleTerrain(): TerrainType[][] {
|
function createEighthBattleTerrain(): TerrainType[][] {
|
||||||
return Array.from({ length: 22 }, (_, y) =>
|
return Array.from({ length: 22 }, (_, y) =>
|
||||||
@@ -3002,6 +3288,35 @@ function createEighthBattleTerrain(): TerrainType[][] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createNinthBattleTerrain(): TerrainType[][] {
|
||||||
|
return Array.from({ length: 22 }, (_, y) =>
|
||||||
|
Array.from({ length: 26 }, (_, x): TerrainType => {
|
||||||
|
if ((x >= 1 && x <= 4 && y >= 16 && y <= 20) || (x >= 5 && x <= 7 && y >= 18 && y <= 20)) {
|
||||||
|
return 'camp';
|
||||||
|
}
|
||||||
|
if ((x >= 19 && x <= 24 && y >= 3 && y <= 8) || (x >= 18 && x <= 24 && y >= 12 && y <= 16)) {
|
||||||
|
return 'fort';
|
||||||
|
}
|
||||||
|
if ((x >= 20 && x <= 22 && y >= 9 && y <= 11) || (x >= 11 && x <= 12 && y >= 13 && y <= 15)) {
|
||||||
|
return 'village';
|
||||||
|
}
|
||||||
|
if ((x === 14 || x === 15) && y >= 0 && y <= 20) {
|
||||||
|
return 'river';
|
||||||
|
}
|
||||||
|
if ((y === 10 && x >= 2 && x <= 24) || (x === 8 && y >= 5 && y <= 20) || (x === 18 && y >= 2 && y <= 18) || (x + y === 25 && x >= 7 && x <= 17)) {
|
||||||
|
return 'road';
|
||||||
|
}
|
||||||
|
if ((x <= 5 && y <= 5) || (x >= 2 && x <= 7 && y >= 7 && y <= 12) || (x >= 8 && x <= 13 && y >= 1 && y <= 5) || (x >= 4 && x <= 10 && y >= 14 && y <= 17)) {
|
||||||
|
return 'forest';
|
||||||
|
}
|
||||||
|
if ((x >= 21 && y <= 2) || (x >= 22 && y >= 17) || (x >= 10 && x <= 13 && y >= 16) || (x <= 3 && y >= 6 && y <= 9)) {
|
||||||
|
return 'hill';
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
|||||||
@@ -535,7 +535,17 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
|||||||
'xiaopei-guard-b': 'aggressive',
|
'xiaopei-guard-b': 'aggressive',
|
||||||
'xiaopei-cavalry-a': 'aggressive',
|
'xiaopei-cavalry-a': 'aggressive',
|
||||||
'xiaopei-cavalry-b': 'aggressive',
|
'xiaopei-cavalry-b': 'aggressive',
|
||||||
'xiaopei-leader-gao-shun': 'guard'
|
'xiaopei-leader-gao-shun': 'guard',
|
||||||
|
'xuzhou-gate-traitor-a': 'aggressive',
|
||||||
|
'xuzhou-gate-traitor-b': 'aggressive',
|
||||||
|
'xuzhou-gate-infantry-a': 'guard',
|
||||||
|
'xuzhou-gate-infantry-b': 'guard',
|
||||||
|
'xuzhou-gate-archer-a': 'hold',
|
||||||
|
'xuzhou-gate-archer-b': 'hold',
|
||||||
|
'lubu-cavalry-a': 'aggressive',
|
||||||
|
'lubu-cavalry-b': 'aggressive',
|
||||||
|
'lubu-cavalry-c': 'aggressive',
|
||||||
|
'xuzhou-leader-cao-bao': 'guard'
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg
|
|||||||
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
|
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
|
||||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
||||||
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
|
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
|
||||||
|
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';
|
||||||
@@ -74,6 +75,7 @@ export class BootScene extends Phaser.Scene {
|
|||||||
this.load.image('battle-map-sixth', sixthBattleMapUrl);
|
this.load.image('battle-map-sixth', sixthBattleMapUrl);
|
||||||
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('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);
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ const campBattleIds = {
|
|||||||
fifth: 'fifth-battle-sishui-vanguard',
|
fifth: 'fifth-battle-sishui-vanguard',
|
||||||
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'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||||
@@ -839,6 +840,87 @@ const campDialogues: CampDialogue[] = [
|
|||||||
rewardExp: 9
|
rewardExp: 9
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-zhang-after-xuzhou-gate',
|
||||||
|
title: '성문 뒤의 후회',
|
||||||
|
availableAfterBattleIds: [campBattleIds.ninth],
|
||||||
|
unitIds: ['liu-bei', 'zhang-fei'],
|
||||||
|
bondId: 'liu-bei__zhang-fei',
|
||||||
|
rewardExp: 22,
|
||||||
|
lines: [
|
||||||
|
'장비: 형님, 성문은 지켰지만 조표 같은 자들이 움직인 것 자체가 제 불찰입니다.',
|
||||||
|
'유비: 익덕, 성급함은 네 강점이자 약점이다. 하지만 후회로 멈추면 다음 성문도 지키지 못한다.',
|
||||||
|
'장비: 다음에는 술도, 분노도, 먼저 앞세우지 않겠습니다.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'trust-reform',
|
||||||
|
label: '다시 맡기되 군율을 세운다',
|
||||||
|
response: '장비는 꾸짖음보다 믿음을 받은 것에 더 무겁게 고개를 숙였다.',
|
||||||
|
rewardExp: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'share-command',
|
||||||
|
label: '관우와 함께 성문을 맡긴다',
|
||||||
|
response: '장비는 운장 형님과 함께라면 이번 실수를 씻어 보이겠다고 답했다.',
|
||||||
|
rewardExp: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-jian-yong-after-xuzhou-gate',
|
||||||
|
title: '의와 현실 사이',
|
||||||
|
availableAfterBattleIds: [campBattleIds.ninth],
|
||||||
|
unitIds: ['liu-bei', 'jian-yong'],
|
||||||
|
bondId: 'liu-bei__jian-yong',
|
||||||
|
rewardExp: 22,
|
||||||
|
lines: [
|
||||||
|
'간옹: 현덕, 여포를 들인 일은 이제 말 한마디로 되돌릴 수 없네. 사람들은 이미 네 의와 그의 칼을 함께 보고 있어.',
|
||||||
|
'유비: 의로 시작한 일이 백성을 위태롭게 한다면, 나는 무엇을 지켜야 하오?',
|
||||||
|
'간옹: 그래서 더더욱 눈을 뜨고 있어야지. 의도 잠들면 남의 칼집이 된다네.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'watch-lu-bu',
|
||||||
|
label: '여포의 움직임을 감시한다',
|
||||||
|
response: '간옹은 마침내 현실적인 지시가 나왔다며 사람을 붙이겠다고 했다.',
|
||||||
|
rewardExp: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'protect-civilians-first',
|
||||||
|
label: '백성 피난로를 먼저 정한다',
|
||||||
|
response: '간옹은 서주를 잃더라도 사람을 잃지 않겠다는 말에 한숨 섞인 미소를 지었다.',
|
||||||
|
rewardExp: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'liu-mi-after-xuzhou-gate',
|
||||||
|
title: '성문과 창고',
|
||||||
|
availableAfterBattleIds: [campBattleIds.ninth],
|
||||||
|
unitIds: ['liu-bei', 'mi-zhu'],
|
||||||
|
bondId: 'liu-bei__mi-zhu',
|
||||||
|
rewardExp: 18,
|
||||||
|
lines: [
|
||||||
|
'미축: 성문이 흔들리면 창고도 오래 버티지 못합니다. 군량을 지키는 길과 성문을 지키는 길은 다르지 않습니다.',
|
||||||
|
'유비: 그렇다면 창고의 문서와 수레를 성문 수비와 함께 움직입시다. 길이 끊겨도 백성에게 갈 몫은 남겨 두어야 하오.',
|
||||||
|
'미축: 그 뜻이면 충분합니다. 다음 혼란에 대비하겠습니다.'
|
||||||
|
],
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'move-grain-carts',
|
||||||
|
label: '군량 수레를 재배치한다',
|
||||||
|
response: '미축은 군량 수레를 성문 안쪽 길목으로 옮겨 비상시에 대비했다.',
|
||||||
|
rewardExp: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'guard-ledgers',
|
||||||
|
label: '창고 장부를 보호한다',
|
||||||
|
response: '관우는 장부를 지키는 일도 전열을 지키는 일이라며 병사를 배치했다.',
|
||||||
|
rewardExp: 7
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -917,7 +999,7 @@ 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) || !this.campaign) {
|
if ((battleId !== campBattleIds.seventh && battleId !== campBattleIds.eighth && battleId !== campBattleIds.ninth) || !this.campaign) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,6 +1019,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private currentCampTitle() {
|
private currentCampTitle() {
|
||||||
const battleId = this.currentCampBattleId();
|
const battleId = this.currentCampBattleId();
|
||||||
|
if (battleId === campBattleIds.ninth) {
|
||||||
|
return '서주 성문 군영';
|
||||||
|
}
|
||||||
if (battleId === campBattleIds.eighth) {
|
if (battleId === campBattleIds.eighth) {
|
||||||
return '소패 방위 군영';
|
return '소패 방위 군영';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
eighthBattleScenario,
|
eighthBattleScenario,
|
||||||
fifthBattleScenario,
|
fifthBattleScenario,
|
||||||
fourthBattleScenario,
|
fourthBattleScenario,
|
||||||
|
ninthBattleScenario,
|
||||||
secondBattleScenario,
|
secondBattleScenario,
|
||||||
seventhBattleScenario,
|
seventhBattleScenario,
|
||||||
sixthBattleScenario,
|
sixthBattleScenario,
|
||||||
@@ -315,7 +316,8 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
campaign.step === 'fifth-camp' ||
|
campaign.step === 'fifth-camp' ||
|
||||||
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'
|
||||||
) {
|
) {
|
||||||
this.scene.start('CampScene');
|
this.scene.start('CampScene');
|
||||||
return;
|
return;
|
||||||
@@ -361,6 +363,11 @@ export class TitleScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (campaign.step === 'ninth-battle') {
|
||||||
|
this.scene.start('BattleScene', { battleId: ninthBattleScenario.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;
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export type CampaignStep =
|
|||||||
| 'seventh-battle'
|
| 'seventh-battle'
|
||||||
| 'seventh-camp'
|
| 'seventh-camp'
|
||||||
| 'eighth-battle'
|
| 'eighth-battle'
|
||||||
| 'eighth-camp';
|
| 'eighth-camp'
|
||||||
|
| 'ninth-battle'
|
||||||
|
| 'ninth-camp';
|
||||||
|
|
||||||
export type CampaignUnitProgressSnapshot = {
|
export type CampaignUnitProgressSnapshot = {
|
||||||
unitId: string;
|
unitId: string;
|
||||||
@@ -114,7 +116,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
|||||||
'fifth-battle-sishui-vanguard': { victory: 'fifth-camp', retry: 'fifth-battle' },
|
'fifth-battle-sishui-vanguard': { victory: 'fifth-camp', retry: 'fifth-battle' },
|
||||||
'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' }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CampaignSaveSlotSummary = {
|
export type CampaignSaveSlotSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user