Add Xiapi final sortie chapter
This commit is contained in:
@@ -32,12 +32,13 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
||||
- 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
|
||||
- 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
|
||||
- Flow verification script from title through the twelfth battle victory, recruit sortie selection, and camp save state
|
||||
- Thirteenth battle Xiapi final clash, ending Lu Bu's arc and opening the tension that will lead toward Liu Bei breaking from Cao Cao
|
||||
- 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, recruited-officer count, reserve roster summary, and readiness advice for each deployable officer
|
||||
- Flow verification script from title through the thirteenth battle victory, recruit sortie selection, and camp save state
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Continue the Xiapi siege toward Lu Bu's fall, then build toward Liu Bei's break from Cao Cao
|
||||
1. Build the Cao Cao break chapter, then move into Yuan Shao and Liu Biao refuge arcs
|
||||
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
|
||||
4. Add more recruitable officers as the campaign moves toward Cao Cao, Yuan Shao, Liu Biao, and Zhuge Liang
|
||||
|
||||
@@ -1257,6 +1257,93 @@ try {
|
||||
throw new Error(`Expected twelfth camp to expose Xiapi siege dialogue set and preserve full roster: ${JSON.stringify(twelfthCampState)}`);
|
||||
}
|
||||
|
||||
await page.mouse.click(1120, 38);
|
||||
await page.waitForTimeout(160);
|
||||
const twelfthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
!twelfthCampSortieState?.sortieVisible ||
|
||||
!twelfthCampSortieState.sortiePlan?.objectiveLine?.includes('하비 결전') ||
|
||||
twelfthCampSortieState.sortiePlan?.selectedRecruitedCount < 1 ||
|
||||
!twelfthCampSortieState.sortiePlan?.recruitedLine?.includes('합류 무장')
|
||||
) {
|
||||
throw new Error(`Expected twelfth camp sortie prep to target Xiapi final battle with recruit selection summary: ${JSON.stringify(twelfthCampSortieState)}`);
|
||||
}
|
||||
assertSortieTacticalRoster(twelfthCampSortieState, ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu']);
|
||||
if (!twelfthCampSortieState.sortieRoster.some((unit) => unit.id === 'jian-yong' && unit.recruited)) {
|
||||
throw new Error(`Expected recruit officers to be marked in sortie roster: ${JSON.stringify(twelfthCampSortieState.sortieRoster)}`);
|
||||
}
|
||||
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-final-story.png', fullPage: true });
|
||||
|
||||
for (let i = 0; i < 18; i += 1) {
|
||||
const enteredThirteenthBattle = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
return state?.scene === 'BattleScene' && state?.battleId === 'thirteenth-battle-xiapi-final';
|
||||
});
|
||||
if (enteredThirteenthBattle) {
|
||||
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 === 'thirteenth-battle-xiapi-final' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||
});
|
||||
await page.screenshot({ path: 'dist/verification-thirteenth-battle.png', fullPage: true });
|
||||
|
||||
const thirteenthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
const thirteenthEnemies = thirteenthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||
const thirteenthAllies = thirteenthBattleState.units.filter((unit) => unit.faction === 'ally');
|
||||
const thirteenthEnemyBehaviors = new Set(thirteenthEnemies.map((unit) => unit.ai));
|
||||
if (
|
||||
thirteenthBattleState.camera?.mapWidth !== 32 ||
|
||||
thirteenthBattleState.camera?.mapHeight !== 24 ||
|
||||
thirteenthBattleState.victoryConditionLabel !== '여포 격파' ||
|
||||
thirteenthEnemies.length < 15 ||
|
||||
!thirteenthEnemyBehaviors.has('aggressive') ||
|
||||
!thirteenthEnemyBehaviors.has('guard') ||
|
||||
!thirteenthEnemyBehaviors.has('hold') ||
|
||||
!thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-leader-lu-bu') ||
|
||||
!thirteenthEnemies.some((unit) => unit.id === 'xiapi-final-strategist-chen-gong') ||
|
||||
!thirteenthAllies.some((unit) => unit.id === 'liu-bei') ||
|
||||
!thirteenthAllies.some((unit) => unit.id === 'guan-yu') ||
|
||||
!thirteenthAllies.some((unit) => unit.id === 'zhang-fei') ||
|
||||
!thirteenthAllies.some((unit) => unit.id === 'jian-yong') ||
|
||||
!thirteenthAllies.some((unit) => unit.id === 'mi-zhu')
|
||||
) {
|
||||
throw new Error(`Expected thirteenth battle to use Xiapi final map, Lu Bu objective, mixed AI, and selected recruit sortie: ${JSON.stringify(thirteenthBattleState)}`);
|
||||
}
|
||||
|
||||
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 thirteenthCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (
|
||||
thirteenthCampState?.campBattleId !== 'thirteenth-battle-xiapi-final' ||
|
||||
thirteenthCampState.campTitle !== '하비 결전 후 군영' ||
|
||||
thirteenthCampState.availableDialogueIds?.length !== 3 ||
|
||||
!thirteenthCampState.availableDialogueIds.every((id) => id.endsWith('lu-bu-fall')) ||
|
||||
!thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'guan-yu') ||
|
||||
!thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'jian-yong') ||
|
||||
!thirteenthCampState.campaign?.roster?.some((unit) => unit.id === 'mi-zhu')
|
||||
) {
|
||||
throw new Error(`Expected thirteenth camp to expose Lu Bu fall dialogue set and preserve full roster: ${JSON.stringify(thirteenthCampState)}`);
|
||||
}
|
||||
|
||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
@@ -1268,7 +1355,7 @@ try {
|
||||
return activeScenes.includes('CampScene');
|
||||
});
|
||||
|
||||
console.log(`Verified title-to-twelfth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
|
||||
console.log(`Verified title-to-thirteenth-battle flow, recruit sortie selection, result states, and debug API at ${targetUrl}`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
|
||||
95
src/assets/images/battle/thirteenth-battle-map.svg
Normal file
95
src/assets/images/battle/thirteenth-battle-map.svg
Normal file
@@ -0,0 +1,95 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="3200" height="2400" viewBox="0 0 3200 2400">
|
||||
<defs>
|
||||
<linearGradient id="skyWash" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b8756"/>
|
||||
<stop offset="0.48" stop-color="#b79a60"/>
|
||||
<stop offset="1" stop-color="#77754f"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="mudLight" cx="55%" cy="44%" r="62%">
|
||||
<stop offset="0" stop-color="#d4bb7a"/>
|
||||
<stop offset="0.62" stop-color="#a98d57"/>
|
||||
<stop offset="1" stop-color="#716b45"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="riverFlow" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#4e8394"/>
|
||||
<stop offset="0.5" stop-color="#6ca8ad"/>
|
||||
<stop offset="1" stop-color="#375e78"/>
|
||||
</linearGradient>
|
||||
<pattern id="grain" width="64" height="64" patternUnits="userSpaceOnUse">
|
||||
<path d="M4 12h18M34 16h24M12 42h22M42 48h16" stroke="#2d2619" stroke-width="5" opacity=".18" fill="none"/>
|
||||
<path d="M18 5l9 8M51 31l8 9M7 56l11-8" stroke="#e5cf8d" stroke-width="4" opacity=".16" fill="none"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="3200" height="2400" fill="url(#skyWash)"/>
|
||||
<rect width="3200" height="2400" fill="url(#mudLight)" opacity=".88"/>
|
||||
<rect width="3200" height="2400" fill="url(#grain)" opacity=".65"/>
|
||||
|
||||
<rect x="1440" y="-80" width="270" height="2560" rx="120" fill="#5f98a6" opacity=".5"/>
|
||||
<rect x="1888" y="30" width="238" height="2450" rx="105" fill="#5a90a2" opacity=".46"/>
|
||||
<rect x="1538" y="-20" width="22" height="2120" rx="10" fill="#a8d5d0" opacity=".22"/>
|
||||
<rect x="1998" y="90" width="18" height="1980" rx="9" fill="#a8d5d0" opacity=".2"/>
|
||||
|
||||
<g opacity=".42">
|
||||
<ellipse cx="430" cy="310" rx="118" ry="72" fill="#718956"/>
|
||||
<ellipse cx="650" cy="410" rx="156" ry="92" fill="#78905b"/>
|
||||
<ellipse cx="918" cy="330" rx="132" ry="78" fill="#748a54"/>
|
||||
<ellipse cx="420" cy="1715" rx="170" ry="92" fill="#708854"/>
|
||||
<ellipse cx="760" cy="1788" rx="210" ry="105" fill="#778d58"/>
|
||||
<ellipse cx="1045" cy="1848" rx="160" ry="86" fill="#718754"/>
|
||||
<ellipse cx="2948" cy="2100" rx="185" ry="112" fill="#748b57"/>
|
||||
</g>
|
||||
|
||||
<path d="M150 1938 L980 1938 L1430 1455 L2065 1455 L2450 1164 L2920 1164" stroke="#cbb27a" stroke-width="78" stroke-linecap="round" opacity=".92" fill="none"/>
|
||||
<path d="M1020 1375 L1840 1375 L2430 895" stroke="#cbb27a" stroke-width="72" stroke-linecap="round" opacity=".88" fill="none"/>
|
||||
<path d="M2450 735 L2450 1830" stroke="#cbb27a" stroke-width="68" stroke-linecap="round" opacity=".84" fill="none"/>
|
||||
<path d="M150 1938 L980 1938 L1430 1455 L2065 1455 L2450 1164 L2920 1164" stroke="#6e4d2e" stroke-width="12" stroke-dasharray="46 48" opacity=".22" fill="none"/>
|
||||
<path d="M1020 1375 L1840 1375 L2430 895" stroke="#6e4d2e" stroke-width="10" stroke-dasharray="44 46" opacity=".2" fill="none"/>
|
||||
|
||||
<g opacity=".58">
|
||||
<rect x="2188" y="826" width="620" height="64" fill="#9e895f"/>
|
||||
<rect x="2188" y="1438" width="620" height="64" fill="#927d58"/>
|
||||
<rect x="2168" y="870" width="64" height="600" fill="#867250"/>
|
||||
<rect x="2768" y="870" width="64" height="600" fill="#867250"/>
|
||||
<rect x="2280" y="1018" width="390" height="300" rx="14" fill="#a58b58"/>
|
||||
<rect x="2378" y="1112" width="196" height="126" fill="#756342" opacity=".5"/>
|
||||
<path d="M2208 858 H2796" stroke="#d5bd81" stroke-width="10" opacity=".36" fill="none"/>
|
||||
<path d="M2212 1470 H2792" stroke="#d5bd81" stroke-width="10" opacity=".3" fill="none"/>
|
||||
<path d="M2242 892 V1458" stroke="#d5bd81" stroke-width="8" opacity=".25" fill="none"/>
|
||||
<path d="M2796 892 V1458" stroke="#d5bd81" stroke-width="8" opacity=".25" fill="none"/>
|
||||
</g>
|
||||
|
||||
<g opacity=".88">
|
||||
<ellipse cx="1120" cy="1812" rx="165" ry="102" fill="#866b3e"/>
|
||||
<rect x="1006" y="1740" width="230" height="130" fill="#a8874b"/>
|
||||
<path d="M1006 1740 L1120 1662 L1236 1740Z" fill="#6b4a31"/>
|
||||
<rect x="1078" y="1792" width="74" height="78" fill="#3b2b20"/>
|
||||
<path d="M934 1905 C1040 1955 1235 1950 1315 1886" stroke="#d8c08a" stroke-width="18" opacity=".45" fill="none"/>
|
||||
</g>
|
||||
|
||||
<g opacity=".78">
|
||||
<circle cx="520" cy="420" r="72" fill="#41512d"/>
|
||||
<circle cx="600" cy="465" r="90" fill="#536536"/>
|
||||
<circle cx="710" cy="380" r="72" fill="#4f6034"/>
|
||||
<circle cx="760" cy="535" r="95" fill="#44552f"/>
|
||||
<circle cx="888" cy="430" r="78" fill="#526436"/>
|
||||
<circle cx="528" cy="1270" r="84" fill="#4b5d33"/>
|
||||
<circle cx="680" cy="1195" r="96" fill="#536a3b"/>
|
||||
<circle cx="842" cy="1250" r="82" fill="#455830"/>
|
||||
<circle cx="2780" cy="2055" r="88" fill="#4b5d33"/>
|
||||
<circle cx="2912" cy="2130" r="104" fill="#536a3b"/>
|
||||
</g>
|
||||
|
||||
<g opacity=".24">
|
||||
<ellipse cx="930" cy="330" rx="190" ry="112" fill="#9b9366"/>
|
||||
<ellipse cx="2920" cy="270" rx="230" ry="140" fill="#999165"/>
|
||||
<ellipse cx="1160" cy="2190" rx="270" ry="150" fill="#9b9366"/>
|
||||
</g>
|
||||
|
||||
<g opacity=".42">
|
||||
<path d="M0 0H3200V2400H0Z" fill="none" stroke="#ead6a2" stroke-width="22"/>
|
||||
<path d="M108 98H3092V2298H108Z" fill="none" stroke="#3a2c1b" stroke-width="12"/>
|
||||
</g>
|
||||
|
||||
<rect width="3200" height="2400" fill="#101015" opacity=".035"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -67,6 +67,15 @@ export const itemCatalog: Record<string, ItemDefinition> = {
|
||||
attackBonus: 5,
|
||||
effects: ['연속 공격 판정 보정']
|
||||
},
|
||||
'sky-piercer-halberd': {
|
||||
id: 'sky-piercer-halberd',
|
||||
name: '방천화극',
|
||||
slot: 'weapon',
|
||||
rank: 'treasure',
|
||||
description: '여포의 상징으로 두려움을 산 긴 화극. 한 번 휘두르면 전열이 크게 흔들린다.',
|
||||
attackBonus: 6,
|
||||
effects: ['단독으로 돌파할 때 피해 보너스']
|
||||
},
|
||||
'yellow-turban-saber': {
|
||||
id: 'yellow-turban-saber',
|
||||
name: '황건도',
|
||||
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
tenthBattleMap,
|
||||
tenthBattleUnits,
|
||||
tenthBattleVictoryPages,
|
||||
thirteenthBattleBonds,
|
||||
thirteenthBattleMap,
|
||||
thirteenthBattleUnits,
|
||||
thirteenthBattleVictoryPages,
|
||||
twelfthBattleBonds,
|
||||
twelfthBattleMap,
|
||||
twelfthBattleUnits,
|
||||
@@ -65,7 +69,8 @@ export type BattleScenarioId =
|
||||
| 'ninth-battle-xuzhou-gate-night-raid'
|
||||
| 'tenth-battle-xuzhou-breakout'
|
||||
| 'eleventh-battle-xudu-refuge-road'
|
||||
| 'twelfth-battle-xiapi-outer-siege';
|
||||
| 'twelfth-battle-xiapi-outer-siege'
|
||||
| 'thirteenth-battle-xiapi-final';
|
||||
|
||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||
|
||||
@@ -740,6 +745,59 @@ export const twelfthBattleScenario: BattleScenarioDefinition = {
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const thirteenthBattleScenario: BattleScenarioDefinition = {
|
||||
id: 'thirteenth-battle-xiapi-final',
|
||||
title: '하비 결전',
|
||||
victoryConditionLabel: '여포 격파',
|
||||
defeatConditionLabel: '유비 퇴각',
|
||||
openingObjectiveLines: [
|
||||
'여포가 하비 성 안에서 마지막 돌파를 준비합니다. 여포를 격파하면 서주를 뒤흔든 난전도 끝을 맞습니다.',
|
||||
'진궁은 성루 궁병과 수문 수비를 이용해 접근로를 끊으려 합니다. 기병 돌파대와 성문 수비대를 나누어 상대하십시오.',
|
||||
'성문 요새를 확보하고 22턴 안에 승리하면 조조 휘하에서도 유비군의 전공과 독자 보급 명분을 모두 남길 수 있습니다.'
|
||||
],
|
||||
map: thirteenthBattleMap,
|
||||
units: thirteenthBattleUnits,
|
||||
bonds: thirteenthBattleBonds,
|
||||
mapTextureKey: 'battle-map-thirteenth',
|
||||
leaderUnitId: 'xiapi-final-leader-lu-bu',
|
||||
quickVictoryTurnLimit: 22,
|
||||
baseVictoryGold: 1560,
|
||||
objectives: [
|
||||
{
|
||||
id: 'leader',
|
||||
kind: 'defeat-leader',
|
||||
label: '여포 격파',
|
||||
rewardGold: 980,
|
||||
unitId: 'xiapi-final-leader-lu-bu'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '유비 생존',
|
||||
rewardGold: 360,
|
||||
unitId: 'liu-bei'
|
||||
},
|
||||
{
|
||||
id: 'gate',
|
||||
kind: 'secure-terrain',
|
||||
label: '하비 성문 확보',
|
||||
rewardGold: 560,
|
||||
terrain: 'fort'
|
||||
},
|
||||
{
|
||||
id: 'quick',
|
||||
kind: 'quick-victory',
|
||||
label: '22턴 이내 승리',
|
||||
rewardGold: 420,
|
||||
maxTurn: 22
|
||||
}
|
||||
],
|
||||
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||
itemRewards: ['콩 5', '상처약 3', '탁주 2', '하비 결전 공적 +1'],
|
||||
victoryPages: thirteenthBattleVictoryPages,
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||
|
||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||
@@ -754,7 +812,8 @@ export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition>
|
||||
'ninth-battle-xuzhou-gate-night-raid': ninthBattleScenario,
|
||||
'tenth-battle-xuzhou-breakout': tenthBattleScenario,
|
||||
'eleventh-battle-xudu-refuge-road': eleventhBattleScenario,
|
||||
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario
|
||||
'twelfth-battle-xiapi-outer-siege': twelfthBattleScenario,
|
||||
'thirteenth-battle-xiapi-final': thirteenthBattleScenario
|
||||
};
|
||||
|
||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
seventhBattleScenario,
|
||||
sixthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
twelfthBattleScenario,
|
||||
thirdBattleScenario,
|
||||
type BattleScenarioId
|
||||
@@ -34,6 +35,8 @@ import {
|
||||
sixthBattleVictoryPages,
|
||||
tenthBattleIntroPages,
|
||||
tenthBattleVictoryPages,
|
||||
thirteenthBattleIntroPages,
|
||||
thirteenthBattleVictoryPages,
|
||||
twelfthBattleIntroPages,
|
||||
twelfthBattleVictoryPages,
|
||||
thirdBattleIntroPages,
|
||||
@@ -166,12 +169,22 @@ const sortieFlows: Record<string, SortieFlow> = {
|
||||
},
|
||||
[twelfthBattleScenario.id]: {
|
||||
afterBattleId: twelfthBattleScenario.id,
|
||||
eyebrow: '다음 전장',
|
||||
title: thirteenthBattleScenario.title,
|
||||
description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 성문과 수문을 뚫고 여포를 격파하면 조조 휘하에서 깊어지는 긴장이 다음 흐름으로 이어집니다.',
|
||||
rewardHint: `예상 보상: ${thirteenthBattleScenario.title} 개방`,
|
||||
nextBattleId: thirteenthBattleScenario.id,
|
||||
campaignStep: 'thirteenth-battle',
|
||||
pages: [...twelfthBattleVictoryPages, ...thirteenthBattleIntroPages]
|
||||
},
|
||||
[thirteenthBattleScenario.id]: {
|
||||
afterBattleId: thirteenthBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '하비 포위의 끝',
|
||||
description: '하비 외곽 포위망이 좁혀졌고, 여포의 마지막 운명도 가까워졌습니다. 다음 흐름은 여포 토벌의 결말과 조조 밑에서 깊어지는 긴장으로 이어집니다.',
|
||||
rewardHint: '다음 장: 하비 결전과 조조 이탈 준비 중',
|
||||
pages: twelfthBattleVictoryPages,
|
||||
unavailableNotice: '하비 결전과 조조 이탈 장은 다음 작업에서 이어집니다. 군영에서 성장과 출전 구성을 정비하십시오.'
|
||||
title: '조조 이탈의 불씨',
|
||||
description: '여포가 무너지고 하비 결전은 끝났지만, 조조의 칭찬과 경계는 동시에 깊어졌습니다. 다음 흐름은 조조 휘하를 벗어날 명분과 때를 준비하는 장으로 이어집니다.',
|
||||
rewardHint: '다음 장: 조조 이탈과 원소 의탁 준비 중',
|
||||
pages: thirteenthBattleVictoryPages,
|
||||
unavailableNotice: '조조 이탈과 원소 의탁 장은 다음 작업에서 이어집니다. 군영에서 성장, 대화, 출전 구성을 정비하십시오.'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -813,6 +813,70 @@ export const twelfthBattleVictoryPages: StoryPage[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const thirteenthBattleIntroPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirteenth-cao-cao-final-order',
|
||||
bgm: 'story-dark',
|
||||
chapter: '하비 결전의 명',
|
||||
background: 'story-liu-bei',
|
||||
text: '하비 외곽이 무너지자 조조는 마지막 총공세를 명했다. 성 안의 여포는 아직 강했고, 진궁은 남은 성루와 수문을 엮어 마지막 반격을 준비하고 있었다.'
|
||||
},
|
||||
{
|
||||
id: 'thirteenth-brothers-before-final',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '여포를 앞두고',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '관우',
|
||||
portrait: 'guanYu',
|
||||
text: '형님, 여포는 용맹하나 믿음이 얕습니다. 다만 그 칼끝이 어지러운 세상의 그림자라면, 오늘 싸움 뒤에 우리 길도 더 뚜렷해질 것입니다.'
|
||||
},
|
||||
{
|
||||
id: 'thirteenth-liu-bei-resolve',
|
||||
bgm: 'story-dark',
|
||||
chapter: '조조의 그늘',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '조조의 명으로 싸우되, 백성의 원망을 덜고 난세의 사슬을 끊기 위해 싸운다. 여포의 끝을 보더라도 우리의 뜻까지 맡기지는 말자.'
|
||||
},
|
||||
{
|
||||
id: 'thirteenth-xiapi-final-sortie',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '하비 결전',
|
||||
background: 'story-sortie',
|
||||
speaker: '장비',
|
||||
portrait: 'zhangFei',
|
||||
text: '좋습니다, 형님. 이번엔 성문을 열고 여포의 깃발까지 밀고 들어가겠습니다. 누가 함께 나설지 정해 주십시오. 대기한 자도 다음 싸움을 위해 힘을 아껴야 합니다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const thirteenthBattleVictoryPages: StoryPage[] = [
|
||||
{
|
||||
id: 'thirteenth-victory-lubu-falls',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '무너진 여포',
|
||||
background: 'story-militia',
|
||||
text: '하비 성내의 마지막 저항이 꺾이고 여포의 깃발이 내려갔다. 뛰어난 무용도 믿음을 잃으면 성 하나를 지키지 못한다는 소문이 병사들 사이에 퍼졌다.'
|
||||
},
|
||||
{
|
||||
id: 'thirteenth-cao-cao-weighs-liu-bei',
|
||||
bgm: 'story-dark',
|
||||
chapter: '칭찬과 경계',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '간옹',
|
||||
text: '조조는 형님의 말을 들은 뒤 오래 웃지 않았네. 여포의 운명은 끝났지만, 이제부터는 조조가 우리를 어디까지 품고 어디서부터 경계할지 살펴야 하네.'
|
||||
},
|
||||
{
|
||||
id: 'thirteenth-road-to-cao-break',
|
||||
bgm: 'story-dark',
|
||||
chapter: '떠날 때를 헤아리다',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '오늘 싸움으로 빚도 공도 쌓였다. 그러나 의로 시작한 길을 남의 장부 속 숫자로 끝낼 수는 없다. 떠날 때가 오면 망설이지 않도록 마음을 단단히 하자.'
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleMap: BattleMap = {
|
||||
width: 20,
|
||||
height: 18,
|
||||
@@ -1030,6 +1094,12 @@ export const twelfthBattleMap: BattleMap = {
|
||||
terrain: createTwelfthBattleTerrain()
|
||||
};
|
||||
|
||||
export const thirteenthBattleMap: BattleMap = {
|
||||
width: 32,
|
||||
height: 24,
|
||||
terrain: createThirteenthBattleTerrain()
|
||||
};
|
||||
|
||||
export const firstBattleUnits: UnitData[] = [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
@@ -4181,6 +4251,335 @@ export const twelfthBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const thirteenthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 3, y: 19 },
|
||||
'guan-yu': { x: 4, y: 18 },
|
||||
'zhang-fei': { x: 4, y: 20 },
|
||||
'jian-yong': { x: 5, y: 19 },
|
||||
'mi-zhu': { x: 3, y: 21 }
|
||||
};
|
||||
|
||||
export const thirteenthBattleUnits: UnitData[] = [
|
||||
...[...firstBattleUnits.filter((unit) => unit.faction === 'ally'), ...xuzhouRecruitUnits].map((unit) =>
|
||||
placeScenarioUnit(unit, thirteenthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })
|
||||
),
|
||||
{
|
||||
id: 'xiapi-final-cavalry-a',
|
||||
name: '여포군 돌격기병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 14,
|
||||
exp: 84,
|
||||
hp: 62,
|
||||
maxHp: 62,
|
||||
attack: 21,
|
||||
move: 5,
|
||||
stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 15,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-cavalry-b',
|
||||
name: '여포군 돌격기병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 14,
|
||||
exp: 84,
|
||||
hp: 62,
|
||||
maxHp: 62,
|
||||
attack: 21,
|
||||
move: 5,
|
||||
stats: { might: 98, intelligence: 55, leadership: 76, agility: 99, luck: 62 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 60 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 20,
|
||||
y: 7
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-cavalry-c',
|
||||
name: '여포군 돌격기병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 14,
|
||||
exp: 86,
|
||||
hp: 63,
|
||||
maxHp: 63,
|
||||
attack: 21,
|
||||
move: 5,
|
||||
stats: { might: 99, intelligence: 56, leadership: 77, agility: 100, luck: 63 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 62 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 26 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 25,
|
||||
y: 17
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-cavalry-d',
|
||||
name: '여포군 친위기병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기병',
|
||||
classKey: 'cavalry',
|
||||
level: 15,
|
||||
exp: 88,
|
||||
hp: 66,
|
||||
maxHp: 66,
|
||||
attack: 22,
|
||||
move: 5,
|
||||
stats: { might: 101, intelligence: 58, leadership: 80, agility: 101, luck: 64 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 64 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 28 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 27,
|
||||
y: 12
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-gate-guard-a',
|
||||
name: '하비 성문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 13,
|
||||
exp: 82,
|
||||
hp: 56,
|
||||
maxHp: 56,
|
||||
attack: 18,
|
||||
move: 3,
|
||||
stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 18,
|
||||
y: 13
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-gate-guard-b',
|
||||
name: '하비 성문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 13,
|
||||
exp: 82,
|
||||
hp: 56,
|
||||
maxHp: 56,
|
||||
attack: 18,
|
||||
move: 3,
|
||||
stats: { might: 89, intelligence: 58, leadership: 75, agility: 68, luck: 59 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 56 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 19,
|
||||
y: 14
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-gate-guard-c',
|
||||
name: '하비 성문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 13,
|
||||
exp: 84,
|
||||
hp: 57,
|
||||
maxHp: 57,
|
||||
attack: 18,
|
||||
move: 3,
|
||||
stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 23,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-gate-guard-d',
|
||||
name: '하비 성문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 13,
|
||||
exp: 84,
|
||||
hp: 57,
|
||||
maxHp: 57,
|
||||
attack: 18,
|
||||
move: 3,
|
||||
stats: { might: 90, intelligence: 58, leadership: 76, agility: 69, luck: 59 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 24,
|
||||
y: 11
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-archer-a',
|
||||
name: '하비 성루 궁병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 궁병',
|
||||
classKey: 'archer',
|
||||
level: 13,
|
||||
exp: 82,
|
||||
hp: 46,
|
||||
maxHp: 46,
|
||||
attack: 19,
|
||||
move: 3,
|
||||
stats: { might: 80, intelligence: 70, leadership: 68, agility: 82, luck: 60 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 56 },
|
||||
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 22,
|
||||
y: 7
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-archer-b',
|
||||
name: '하비 성루 궁병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 궁병',
|
||||
classKey: 'archer',
|
||||
level: 13,
|
||||
exp: 84,
|
||||
hp: 47,
|
||||
maxHp: 47,
|
||||
attack: 19,
|
||||
move: 3,
|
||||
stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 58 },
|
||||
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 25,
|
||||
y: 8
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-archer-c',
|
||||
name: '하비 성루 궁병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 궁병',
|
||||
classKey: 'archer',
|
||||
level: 13,
|
||||
exp: 84,
|
||||
hp: 47,
|
||||
maxHp: 47,
|
||||
attack: 19,
|
||||
move: 3,
|
||||
stats: { might: 81, intelligence: 71, leadership: 69, agility: 83, luck: 60 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 58 },
|
||||
armor: { itemId: 'cloth-armor', level: 2, exp: 22 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 27,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-raider-a',
|
||||
name: '여포군 성내 기습병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기습병',
|
||||
classKey: 'bandit',
|
||||
level: 13,
|
||||
exp: 82,
|
||||
hp: 52,
|
||||
maxHp: 52,
|
||||
attack: 19,
|
||||
move: 4,
|
||||
stats: { might: 91, intelligence: 57, leadership: 67, agility: 85, 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: 11,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-raider-b',
|
||||
name: '여포군 성내 기습병',
|
||||
faction: 'enemy',
|
||||
className: '여포군 기습병',
|
||||
classKey: 'bandit',
|
||||
level: 13,
|
||||
exp: 84,
|
||||
hp: 53,
|
||||
maxHp: 53,
|
||||
attack: 19,
|
||||
move: 4,
|
||||
stats: { might: 92, intelligence: 57, leadership: 67, agility: 86, luck: 61 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 58 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 12,
|
||||
y: 21
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-strategist-chen-gong',
|
||||
name: '진궁',
|
||||
faction: 'enemy',
|
||||
className: '여포군 책사',
|
||||
classKey: 'strategist',
|
||||
level: 14,
|
||||
exp: 90,
|
||||
hp: 56,
|
||||
maxHp: 56,
|
||||
attack: 19,
|
||||
move: 3,
|
||||
stats: { might: 64, intelligence: 98, leadership: 88, agility: 72, luck: 66 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 60 },
|
||||
armor: { itemId: 'cloth-armor', level: 2, exp: 24 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 25,
|
||||
y: 11
|
||||
},
|
||||
{
|
||||
id: 'xiapi-final-leader-lu-bu',
|
||||
name: '여포',
|
||||
faction: 'enemy',
|
||||
className: '비장군',
|
||||
classKey: 'rebelLeader',
|
||||
level: 16,
|
||||
exp: 96,
|
||||
hp: 94,
|
||||
maxHp: 94,
|
||||
attack: 25,
|
||||
move: 5,
|
||||
stats: { might: 108, intelligence: 46, leadership: 88, agility: 102, luck: 65 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'sky-piercer-halberd', level: 3, exp: 20 },
|
||||
armor: { itemId: 'reinforced-lamellar', level: 2, exp: 32 },
|
||||
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||
},
|
||||
x: 28,
|
||||
y: 11
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -4238,6 +4637,7 @@ export const ninthBattleBonds: BattleBond[] = eighthBattleBonds.map(cloneBattleB
|
||||
export const tenthBattleBonds: BattleBond[] = ninthBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const eleventhBattleBonds: BattleBond[] = tenthBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const twelfthBattleBonds: BattleBond[] = eleventhBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const thirteenthBattleBonds: BattleBond[] = twelfthBattleBonds.map(cloneBattleBondForScenario);
|
||||
|
||||
function createEighthBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 22 }, (_, y) =>
|
||||
@@ -4432,6 +4832,62 @@ function createTwelfthBattleTerrain(): TerrainType[][] {
|
||||
);
|
||||
}
|
||||
|
||||
function createThirteenthBattleTerrain(): TerrainType[][] {
|
||||
return Array.from({ length: 24 }, (_, y) =>
|
||||
Array.from({ length: 32 }, (_, x): TerrainType => {
|
||||
if (x <= 2 && y >= 18 && y <= 22) {
|
||||
return 'camp';
|
||||
}
|
||||
if (
|
||||
(x >= 20 && x <= 29 && y >= 8 && y <= 15) ||
|
||||
(x >= 23 && x <= 28 && y >= 5 && y <= 7) ||
|
||||
(x >= 25 && x <= 30 && y >= 16 && y <= 18)
|
||||
) {
|
||||
return 'fort';
|
||||
}
|
||||
if ((x >= 10 && x <= 12 && y >= 17 && y <= 19) || (x >= 17 && x <= 19 && y >= 13 && y <= 15)) {
|
||||
return 'village';
|
||||
}
|
||||
if ((x === 14 || x === 15) && y >= 0 && y <= 22) {
|
||||
return 'river';
|
||||
}
|
||||
if ((x === 18 || x === 19) && y >= 2 && y <= 20) {
|
||||
return 'river';
|
||||
}
|
||||
if (
|
||||
(y === 19 && x >= 2 && x <= 19) ||
|
||||
(y === 14 && x >= 15 && x <= 24) ||
|
||||
(y === 11 && x >= 19 && x <= 29) ||
|
||||
(x === 24 && y >= 7 && y <= 18)
|
||||
) {
|
||||
return 'road';
|
||||
}
|
||||
if ((x >= 4 && x <= 16 && y === 23 - x) || (x >= 10 && x <= 22 && y === x - 3)) {
|
||||
return 'road';
|
||||
}
|
||||
if (
|
||||
(x <= 8 && y <= 8) ||
|
||||
(x >= 4 && x <= 12 && y >= 9 && y <= 13) ||
|
||||
(x >= 2 && x <= 9 && y >= 14 && y <= 17) ||
|
||||
(x >= 28 && y >= 19 && y <= 22)
|
||||
) {
|
||||
return 'forest';
|
||||
}
|
||||
if (
|
||||
(x >= 6 && x <= 10 && y >= 2 && y <= 5) ||
|
||||
(x >= 27 && x <= 31 && y >= 1 && y <= 5) ||
|
||||
(x >= 9 && x <= 13 && y >= 20)
|
||||
) {
|
||||
return 'hill';
|
||||
}
|
||||
if ((x >= 30 && y <= 8) || (x >= 30 && y >= 14) || (x <= 1 && y <= 4)) {
|
||||
return 'cliff';
|
||||
}
|
||||
return 'plain';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||
return {
|
||||
...cloneUnitForScenario(unit),
|
||||
|
||||
@@ -73,7 +73,22 @@ const unitTexture: Record<string, string> = {
|
||||
'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'
|
||||
'xiapi-leader-zhang-liao': 'unit-rebel-leader',
|
||||
'xiapi-final-cavalry-a': 'unit-rebel-cavalry',
|
||||
'xiapi-final-cavalry-b': 'unit-rebel-cavalry',
|
||||
'xiapi-final-cavalry-c': 'unit-rebel-cavalry',
|
||||
'xiapi-final-cavalry-d': 'unit-rebel-cavalry',
|
||||
'xiapi-final-gate-guard-a': 'unit-rebel',
|
||||
'xiapi-final-gate-guard-b': 'unit-rebel',
|
||||
'xiapi-final-gate-guard-c': 'unit-rebel',
|
||||
'xiapi-final-gate-guard-d': 'unit-rebel',
|
||||
'xiapi-final-archer-a': 'unit-rebel-archer',
|
||||
'xiapi-final-archer-b': 'unit-rebel-archer',
|
||||
'xiapi-final-archer-c': 'unit-rebel-archer',
|
||||
'xiapi-final-raider-a': 'unit-rebel',
|
||||
'xiapi-final-raider-b': 'unit-rebel',
|
||||
'xiapi-final-strategist-chen-gong': 'unit-rebel-archer',
|
||||
'xiapi-final-leader-lu-bu': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
|
||||
@@ -594,7 +609,22 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
||||
'xiapi-camp-raider-a': 'aggressive',
|
||||
'xiapi-camp-raider-b': 'aggressive',
|
||||
'xiapi-strategist-chen-gong': 'hold',
|
||||
'xiapi-leader-zhang-liao': 'guard'
|
||||
'xiapi-leader-zhang-liao': 'guard',
|
||||
'xiapi-final-cavalry-a': 'aggressive',
|
||||
'xiapi-final-cavalry-b': 'aggressive',
|
||||
'xiapi-final-cavalry-c': 'aggressive',
|
||||
'xiapi-final-cavalry-d': 'aggressive',
|
||||
'xiapi-final-gate-guard-a': 'guard',
|
||||
'xiapi-final-gate-guard-b': 'guard',
|
||||
'xiapi-final-gate-guard-c': 'guard',
|
||||
'xiapi-final-gate-guard-d': 'guard',
|
||||
'xiapi-final-archer-a': 'hold',
|
||||
'xiapi-final-archer-b': 'hold',
|
||||
'xiapi-final-archer-c': 'hold',
|
||||
'xiapi-final-raider-a': 'aggressive',
|
||||
'xiapi-final-raider-b': 'aggressive',
|
||||
'xiapi-final-strategist-chen-gong': 'hold',
|
||||
'xiapi-final-leader-lu-bu': 'guard'
|
||||
};
|
||||
|
||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg
|
||||
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
|
||||
import sixthBattleMapUrl from '../../assets/images/battle/sixth-battle-map.svg';
|
||||
import tenthBattleMapUrl from '../../assets/images/battle/tenth-battle-map.svg';
|
||||
import thirteenthBattleMapUrl from '../../assets/images/battle/thirteenth-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';
|
||||
@@ -82,6 +83,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('battle-map-tenth', tenthBattleMapUrl);
|
||||
this.load.image('battle-map-eleventh', eleventhBattleMapUrl);
|
||||
this.load.image('battle-map-twelfth', twelfthBattleMapUrl);
|
||||
this.load.image('battle-map-thirteenth', thirteenthBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
@@ -123,6 +125,7 @@ export class BootScene extends Phaser.Scene {
|
||||
});
|
||||
this.createItemIcon('item-green-dragon-glaive', (graphics) => this.drawPolearmIcon(graphics, 0x5fbf8f, 0xd8b15f));
|
||||
this.createItemIcon('item-serpent-spear', (graphics) => this.drawSpearIcon(graphics, 0xe0e8ef, 0xb86b55));
|
||||
this.createItemIcon('item-sky-piercer-halberd', (graphics) => this.drawPolearmIcon(graphics, 0xd8dfe6, 0xd95f4f));
|
||||
this.createItemIcon('item-yellow-turban-saber', (graphics) => this.drawSaberIcon(graphics, 0xd8b15f, 0x9c6b2f));
|
||||
this.createItemIcon('item-short-bow', (graphics) => this.drawBowIcon(graphics, 0xc58a4c, 0xe8dfca));
|
||||
this.createItemIcon('item-leader-axe', (graphics) => this.drawAxeIcon(graphics, 0xd8dfe6, 0x8d5a3a));
|
||||
|
||||
@@ -97,10 +97,15 @@ type SortiePlanSummary = {
|
||||
terrainScore: number;
|
||||
terrainGrade: string;
|
||||
activeBondCount: number;
|
||||
recruitedCount: number;
|
||||
selectedRecruitedCount: number;
|
||||
reserveCount: number;
|
||||
recommendedSelectedCount: number;
|
||||
recommendedTotal: number;
|
||||
deploymentLine: string;
|
||||
recommendationLine: string;
|
||||
recruitedLine: string;
|
||||
reserveLine: string;
|
||||
objectiveLine: string;
|
||||
formationSlots: SortieFormationSlot[];
|
||||
warnings: string[];
|
||||
@@ -177,10 +182,12 @@ const campBattleIds = {
|
||||
ninth: 'ninth-battle-xuzhou-gate-night-raid',
|
||||
tenth: 'tenth-battle-xuzhou-breakout',
|
||||
eleventh: 'eleventh-battle-xudu-refuge-road',
|
||||
twelfth: 'twelfth-battle-xiapi-outer-siege'
|
||||
twelfth: 'twelfth-battle-xiapi-outer-siege',
|
||||
thirteenth: 'thirteenth-battle-xiapi-final'
|
||||
} as const;
|
||||
|
||||
const requiredSortieUnitIds = new Set(['liu-bei']);
|
||||
const foundingSortieUnitIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']);
|
||||
const maxSortieUnits = 6;
|
||||
const sortieFormationSlotDefinitions: Omit<SortieFormationSlot, 'unitNames'>[] = [
|
||||
{ role: 'front', label: '전열', description: '길목을 막고 반격을 받아냄' },
|
||||
@@ -299,6 +306,16 @@ const sortieRulesByBattleId: Partial<Record<BattleScenarioId, SortieRuleDefiniti
|
||||
{ unitId: 'mi-zhu', reason: '강가 포위전의 보급과 회복 운용에 유리합니다.' }
|
||||
],
|
||||
note: '하비 외곽전은 강줄기와 둑길이 전장을 나눕니다. 돌파와 보급, 조조군 보고까지 함께 고려하십시오.'
|
||||
},
|
||||
'thirteenth-battle-xiapi-final': {
|
||||
maxUnits: 5,
|
||||
recommended: [
|
||||
{ unitId: 'liu-bei', reason: '여포의 운명을 두고 조조 앞에서 말해야 할 주장입니다.' },
|
||||
{ unitId: 'guan-yu', reason: '성문 요새의 전열을 안정적으로 밀어낼 수 있습니다.' },
|
||||
{ unitId: 'zhang-fei', reason: '여포군 기병의 돌파를 받아치고 반격하는 역할입니다.' },
|
||||
{ unitId: 'jian-yong', reason: '여포 처분과 조조의 시선을 둘러싼 명분 조율에 필요합니다.' }
|
||||
],
|
||||
note: '하비 결전은 전열과 돌파가 강해야 하지만, 전후 조조와의 긴장을 생각하면 책사 한 명을 챙기는 편성이 좋습니다.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1328,6 +1345,87 @@ const campDialogues: CampDialogue[] = [
|
||||
rewardExp: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-guan-after-lu-bu-fall',
|
||||
title: '무용과 믿음',
|
||||
availableAfterBattleIds: [campBattleIds.thirteenth],
|
||||
unitIds: ['liu-bei', 'guan-yu'],
|
||||
bondId: 'liu-bei__guan-yu',
|
||||
rewardExp: 24,
|
||||
lines: [
|
||||
'관우: 여포의 무용은 천하가 알았으나, 끝내 누구도 오래 믿게 하지 못했습니다.',
|
||||
'유비: 힘만으로 길을 열 수는 있어도 사람 마음을 머물게 하지는 못하오.',
|
||||
'관우: 그렇다면 형님의 길은 더 무거워질 것입니다. 저 또한 그 길을 지키겠습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'honor-trust-over-force',
|
||||
label: '믿음의 무게를 말한다',
|
||||
response: '관우는 여포의 말로를 힘의 패배가 아니라 믿음의 패배로 새기며 결의를 다졌다.',
|
||||
rewardExp: 10
|
||||
},
|
||||
{
|
||||
id: 'prepare-for-cao-watch',
|
||||
label: '조조의 시선을 경계한다',
|
||||
response: '관우는 칼을 거두는 때도 전투라 여기며 조조의 경계를 살피겠다고 답했다.',
|
||||
rewardExp: 9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-jian-yong-after-lu-bu-fall',
|
||||
title: '말 한마디의 무게',
|
||||
availableAfterBattleIds: [campBattleIds.thirteenth],
|
||||
unitIds: ['liu-bei', 'jian-yong'],
|
||||
bondId: 'liu-bei__jian-yong',
|
||||
rewardExp: 22,
|
||||
lines: [
|
||||
'간옹: 현덕, 여포가 사라졌으니 조조는 이제 우리를 더 오래 바라볼 걸세.',
|
||||
'유비: 오늘 한 말이 내일의 의심이 될 수 있겠군.',
|
||||
'간옹: 그러니 말은 짧게, 뜻은 길게 남기세. 떠날 명분은 오늘부터 쌓아야 하네.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'keep-words-measured',
|
||||
label: '말을 아끼게 한다',
|
||||
response: '간옹은 조조 앞에서 드러낼 말과 속으로 남길 뜻을 분리해 두었다.',
|
||||
rewardExp: 9
|
||||
},
|
||||
{
|
||||
id: 'collect-leaving-cause',
|
||||
label: '떠날 명분을 모은다',
|
||||
response: '간옹은 조조 휘하에서 겪는 작은 어긋남도 훗날의 기록으로 남기기 시작했다.',
|
||||
rewardExp: 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-mi-after-lu-bu-fall',
|
||||
title: '남겨 둘 군량',
|
||||
availableAfterBattleIds: [campBattleIds.thirteenth],
|
||||
unitIds: ['liu-bei', 'mi-zhu'],
|
||||
bondId: 'liu-bei__mi-zhu',
|
||||
rewardExp: 20,
|
||||
lines: [
|
||||
'미축: 하비의 보급품은 조조군 장부로 들어가겠지만, 우리 병사들이 떠날 때 쓸 몫도 필요합니다.',
|
||||
'유비: 떠날 길을 생각하는 것이 불충으로 보일 수도 있소.',
|
||||
'미축: 뜻을 지키려면 길도 있어야 합니다. 군량은 의리를 움직이게 하는 바퀴입니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'set-aside-march-grain',
|
||||
label: '행군 군량을 남긴다',
|
||||
response: '미축은 장부 밖으로 새지 않는 선에서 유비군의 독자 행군 물자를 확보했다.',
|
||||
rewardExp: 9
|
||||
},
|
||||
{
|
||||
id: 'protect-xu-refugees',
|
||||
label: '서주 피난민을 챙긴다',
|
||||
response: '미축은 하비 뒤편의 피난민에게 갈 식량을 남겨 유비군의 명분을 조용히 지켰다.',
|
||||
rewardExp: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1412,7 +1510,8 @@ export class CampScene extends Phaser.Scene {
|
||||
battleId !== campBattleIds.ninth &&
|
||||
battleId !== campBattleIds.tenth &&
|
||||
battleId !== campBattleIds.eleventh &&
|
||||
battleId !== campBattleIds.twelfth) ||
|
||||
battleId !== campBattleIds.twelfth &&
|
||||
battleId !== campBattleIds.thirteenth) ||
|
||||
!this.campaign
|
||||
) {
|
||||
return;
|
||||
@@ -1434,6 +1533,9 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private currentCampTitle() {
|
||||
const battleId = this.currentCampBattleId();
|
||||
if (battleId === campBattleIds.thirteenth) {
|
||||
return '하비 결전 후 군영';
|
||||
}
|
||||
if (battleId === campBattleIds.twelfth) {
|
||||
return '하비 포위 군영';
|
||||
}
|
||||
@@ -1682,6 +1784,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const rowY = y + 50 + index * rowGap;
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const required = requiredSortieUnitIds.has(unit.id);
|
||||
const recruited = !foundingSortieUnitIds.has(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
||||
@@ -1692,7 +1795,7 @@ export class CampScene extends Phaser.Scene {
|
||||
row.on('pointerdown', () => this.toggleSortieUnit(unit.id));
|
||||
|
||||
const marker = selected ? '출전' : '대기';
|
||||
const markerLabel = required ? '필수' : recommendation ? '추천' : marker;
|
||||
const markerLabel = required ? '필수' : recommendation ? '추천' : recruited ? '합류' : marker;
|
||||
this.trackSortie(this.add.text(x + 30, rowY, markerLabel, this.textStyle(12, required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(x + 84, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(x + 214, rowY, this.compactText(`${unit.className} · ${getUnitClass(unit.classKey).role}`, 19), this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||
@@ -1724,17 +1827,18 @@ export class CampScene extends Phaser.Scene {
|
||||
const summaryLines = [
|
||||
plan.deploymentLine,
|
||||
`${plan.recommendationLine} · 공명 ${plan.activeBondCount}개`,
|
||||
plan.recruitedLine,
|
||||
plan.objectiveLine
|
||||
];
|
||||
summaryLines.forEach((line, index) => {
|
||||
this.trackSortie(this.add.text(x + 18, y + 150 + index * 18, this.compactText(line, 31), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 146 + index * 16, this.compactText(line, 32), this.textStyle(12, index === 0 ? '#f2e3bf' : '#d4dce6', index === 0))).setDepth(depth + 1);
|
||||
});
|
||||
|
||||
plan.formationSlots.forEach((slot, index) => {
|
||||
const column = index % 2;
|
||||
const row = Math.floor(index / 2);
|
||||
const rowX = x + 18 + column * 176;
|
||||
const rowY = y + 208 + row * 28;
|
||||
const rowY = y + 218 + row * 28;
|
||||
const names = slot.unitNames.length > 0 ? slot.unitNames.join(', ') : '미배치';
|
||||
const color = slot.unitNames.length > 0 ? '#a8ffd0' : '#9fb0bf';
|
||||
this.trackSortie(this.add.text(rowX, rowY, slot.label, this.textStyle(12, color, true))).setDepth(depth + 1);
|
||||
@@ -1742,7 +1846,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.trackSortie(this.add.text(rowX + 42, rowY + 13, this.compactText(slot.description, 14), this.textStyle(10, '#87919c'))).setDepth(depth + 1);
|
||||
});
|
||||
|
||||
const warning = plan.warnings[0] ?? '전열, 돌파, 지원의 균형이 좋습니다.';
|
||||
const warning = plan.warnings[0] ?? plan.reserveLine;
|
||||
this.trackSortie(this.add.text(x + 18, y + height - 28, this.compactText(`조언: ${warning}`, 42), this.textStyle(12, plan.warnings.length > 0 ? '#ffdf7b' : '#a8ffd0', true))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
@@ -1914,13 +2018,18 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private sortiePlanSummary(): SortiePlanSummary {
|
||||
const allAllies = this.sortieAllies();
|
||||
const selectedUnits = this.selectedSortieUnits();
|
||||
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
|
||||
const scenario = this.nextSortieScenario();
|
||||
const rule = this.nextSortieRule(scenario);
|
||||
const availableIds = new Set(this.sortieAllies().map((unit) => unit.id));
|
||||
const availableIds = new Set(allAllies.map((unit) => unit.id));
|
||||
const recommended = rule.recommended.filter((entry) => availableIds.has(entry.unitId));
|
||||
const missingRecommended = recommended.filter((entry) => !selectedIds.has(entry.unitId));
|
||||
const recruitedUnits = allAllies.filter((unit) => !foundingSortieUnitIds.has(unit.id));
|
||||
const selectedRecruitedCount = recruitedUnits.filter((unit) => selectedIds.has(unit.id)).length;
|
||||
const reserveUnits = allAllies.filter((unit) => !selectedIds.has(unit.id));
|
||||
const reserveNames = reserveUnits.map((unit) => unit.name).join(', ');
|
||||
const terrainScores = selectedUnits.map((unit) => this.sortieTerrainScore(unit, scenario));
|
||||
const terrainScore = terrainScores.length
|
||||
? Math.round(terrainScores.reduce((sum, score) => sum + score, 0) / terrainScores.length)
|
||||
@@ -1970,10 +2079,18 @@ export class CampScene extends Phaser.Scene {
|
||||
terrainScore,
|
||||
terrainGrade,
|
||||
activeBondCount,
|
||||
recruitedCount: recruitedUnits.length,
|
||||
selectedRecruitedCount,
|
||||
reserveCount: reserveUnits.length,
|
||||
recommendedSelectedCount,
|
||||
recommendedTotal: recommended.length,
|
||||
deploymentLine: `전열 ${roleCounts.front} · 돌파 ${roleCounts.flank} · 후원 ${roleCounts.support} · 예비 ${roleCounts.reserve}`,
|
||||
recommendationLine: recommended.length > 0 ? `추천 ${recommendedSelectedCount}/${recommended.length}` : '추천 없음',
|
||||
recruitedLine:
|
||||
recruitedUnits.length > 0
|
||||
? `합류 무장 ${selectedRecruitedCount}/${recruitedUnits.length} · 대기 ${reserveUnits.length}`
|
||||
: '합류 무장 없음',
|
||||
reserveLine: reserveUnits.length > 0 ? `대기 무장: ${reserveNames}` : '전원 출전 중입니다.',
|
||||
objectiveLine: scenario ? `${scenario.title} · ${scenario.victoryConditionLabel}` : '다음 전투 정보 없음',
|
||||
formationSlots,
|
||||
warnings
|
||||
@@ -2903,6 +3020,8 @@ export class CampScene extends Phaser.Scene {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
selected: this.isSortieSelected(unit.id),
|
||||
recruited: !foundingSortieUnitIds.has(unit.id),
|
||||
required: requiredSortieUnitIds.has(unit.id),
|
||||
className: unit.className,
|
||||
classRole: getUnitClass(unit.classKey).role,
|
||||
formationRole: this.sortieFormationRole(unit),
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
seventhBattleScenario,
|
||||
sixthBattleScenario,
|
||||
tenthBattleScenario,
|
||||
thirteenthBattleScenario,
|
||||
twelfthBattleScenario,
|
||||
thirdBattleScenario
|
||||
} from '../data/battles';
|
||||
@@ -323,7 +324,8 @@ export class TitleScene extends Phaser.Scene {
|
||||
campaign.step === 'ninth-camp' ||
|
||||
campaign.step === 'tenth-camp' ||
|
||||
campaign.step === 'eleventh-camp' ||
|
||||
campaign.step === 'twelfth-camp'
|
||||
campaign.step === 'twelfth-camp' ||
|
||||
campaign.step === 'thirteenth-camp'
|
||||
) {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
@@ -389,6 +391,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'thirteenth-battle') {
|
||||
this.scene.start('BattleScene', { battleId: thirteenthBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
@@ -63,7 +63,9 @@ export type CampaignStep =
|
||||
| 'eleventh-battle'
|
||||
| 'eleventh-camp'
|
||||
| 'twelfth-battle'
|
||||
| 'twelfth-camp';
|
||||
| 'twelfth-camp'
|
||||
| 'thirteenth-battle'
|
||||
| 'thirteenth-camp';
|
||||
|
||||
export type CampaignUnitProgressSnapshot = {
|
||||
unitId: string;
|
||||
@@ -126,7 +128,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
||||
'ninth-battle-xuzhou-gate-night-raid': { victory: 'ninth-camp', retry: 'ninth-battle' },
|
||||
'tenth-battle-xuzhou-breakout': { victory: 'tenth-camp', retry: 'tenth-battle' },
|
||||
'eleventh-battle-xudu-refuge-road': { victory: 'eleventh-camp', retry: 'eleventh-battle' },
|
||||
'twelfth-battle-xiapi-outer-siege': { victory: 'twelfth-camp', retry: 'twelfth-battle' }
|
||||
'twelfth-battle-xiapi-outer-siege': { victory: 'twelfth-camp', retry: 'twelfth-battle' },
|
||||
'thirteenth-battle-xiapi-final': { victory: 'thirteenth-camp', retry: 'thirteenth-battle' }
|
||||
};
|
||||
|
||||
export type CampaignSaveSlotSummary = {
|
||||
|
||||
Reference in New Issue
Block a user