Add third campaign battle path
This commit is contained in:
@@ -2,13 +2,14 @@
|
||||
|
||||
## Initial Goal
|
||||
|
||||
Build a small complete tactical RPG loop:
|
||||
Build a small complete tactical RPG loop that can grow into a longer Romance of the Three Kingdoms campaign:
|
||||
|
||||
1. Title screen
|
||||
2. Prologue story
|
||||
3. First battle briefing
|
||||
4. First battle
|
||||
5. Battle result and save point
|
||||
6. Repeatable story -> camp -> battle expansion path
|
||||
|
||||
## Current Slice
|
||||
|
||||
@@ -17,15 +18,17 @@ Build a small complete tactical RPG loop:
|
||||
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
|
||||
- File-based original soft stereo orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
|
||||
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, class and terrain affinity rules, ally/enemy roster tabs, unit detail stat panels, weapon/armor/accessory equipment growth, terrain-cost movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, right-click tactical menu, turn ending/reset, resonance bond tracking, and grayscale acted-unit feedback
|
||||
- Flow verification script from title to first battle
|
||||
- Second battle expansion with a larger scrollable map, mixed enemy AI behaviors, minimap navigation, camp progression, and save persistence
|
||||
- Third battle expansion path toward Guangzong with a new battlefield, story bridge, campaign step persistence, and title continue support
|
||||
- Flow verification script from title through the third battle victory and camp save state
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement attack range, damage, defeat, victory checks, and real resonance assist attacks
|
||||
2. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||
3. Add strategy and item inventories with usable battle effects
|
||||
4. Add a simple enemy AI turn
|
||||
5. Add battle result scene and local save/load
|
||||
1. Add the next Guangzong main battle and named Yellow Turban commanders
|
||||
2. Broaden camp events so bonds, supplies, and equipment choices matter between battles
|
||||
3. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||
4. Add more distinctive victory/defeat branches and optional objectives
|
||||
5. Keep expanding scenarios through the long campaign instead of treating the current slice as an ending
|
||||
|
||||
## Content Direction
|
||||
|
||||
|
||||
@@ -418,6 +418,78 @@ try {
|
||||
throw new Error(`Expected campaign save to persist second battle victory: ${JSON.stringify(campaignSaveAfterSecondBattle)}`);
|
||||
}
|
||||
|
||||
await page.mouse.click(1120, 38);
|
||||
await page.waitForTimeout(160);
|
||||
const secondCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (!secondCampSortieState?.sortieVisible) {
|
||||
throw new Error(`Expected second camp sortie preparation overlay: ${JSON.stringify(secondCampSortieState)}`);
|
||||
}
|
||||
await page.mouse.click(938, 596);
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
return activeScenes.includes('StoryScene');
|
||||
});
|
||||
|
||||
for (let i = 0; i < 14; i += 1) {
|
||||
const isThirdBattleActive = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road';
|
||||
});
|
||||
|
||||
if (isThirdBattleActive) {
|
||||
break;
|
||||
}
|
||||
|
||||
await page.keyboard.press('Space');
|
||||
await page.waitForTimeout(220);
|
||||
}
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
return state?.scene === 'BattleScene' && state?.battleId === 'third-battle-guangzong-road' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||
});
|
||||
await page.screenshot({ path: 'dist/verification-third-battle.png', fullPage: true });
|
||||
|
||||
const thirdBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
const thirdEnemies = thirdBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||
const thirdEnemyBehaviors = new Set(thirdEnemies.map((unit) => unit.ai));
|
||||
if (
|
||||
thirdBattleState.camera?.mapWidth !== 24 ||
|
||||
thirdBattleState.camera?.mapHeight !== 20 ||
|
||||
thirdBattleState.victoryConditionLabel !== '전령 마원 격파' ||
|
||||
thirdEnemies.length < 10 ||
|
||||
!thirdEnemyBehaviors.has('aggressive') ||
|
||||
!thirdEnemyBehaviors.has('guard') ||
|
||||
!thirdEnemyBehaviors.has('hold')
|
||||
) {
|
||||
throw new Error(`Expected third battle map, objective, and mixed enemy AI: ${JSON.stringify(thirdBattleState)}`);
|
||||
}
|
||||
|
||||
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 campaignSaveAfterThirdBattle = await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem('heros-web:campaign-state');
|
||||
return raw ? JSON.parse(raw) : undefined;
|
||||
});
|
||||
if (
|
||||
!campaignSaveAfterThirdBattle ||
|
||||
campaignSaveAfterThirdBattle.step !== 'third-camp' ||
|
||||
campaignSaveAfterThirdBattle.latestBattleId !== 'third-battle-guangzong-road' ||
|
||||
!campaignSaveAfterThirdBattle.battleHistory?.['third-battle-guangzong-road']
|
||||
) {
|
||||
throw new Error(`Expected campaign save to persist third battle victory: ${JSON.stringify(campaignSaveAfterThirdBattle)}`);
|
||||
}
|
||||
|
||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
@@ -429,7 +501,7 @@ try {
|
||||
return activeScenes.includes('CampScene');
|
||||
});
|
||||
|
||||
console.log(`Verified title-to-second-battle flow, result states, and debug API at ${targetUrl}`);
|
||||
console.log(`Verified title-to-third-battle flow, result states, and debug API at ${targetUrl}`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
|
||||
81
src/assets/images/battle/third-battle-map.svg
Normal file
81
src/assets/images/battle/third-battle-map.svg
Normal file
@@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2000" width="2400" height="2000">
|
||||
<defs>
|
||||
<filter id="paintTexture">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.016" numOctaves="4" seed="41" />
|
||||
<feColorMatrix type="saturate" values="0.48" />
|
||||
<feBlend mode="multiply" in2="SourceGraphic" />
|
||||
</filter>
|
||||
<pattern id="field" width="64" height="64" patternUnits="userSpaceOnUse">
|
||||
<rect width="64" height="64" fill="#64754c" />
|
||||
<path d="M8 19h18M34 42h21M42 13l12 8M16 53l15-11" stroke="#a5af68" stroke-width="3" opacity="0.26" />
|
||||
<path d="M4 41l12-16M28 12l7-10M50 58l8-18" stroke="#344b31" stroke-width="2" opacity="0.28" />
|
||||
</pattern>
|
||||
<pattern id="earth" width="72" height="72" patternUnits="userSpaceOnUse">
|
||||
<rect width="72" height="72" fill="#9a7449" />
|
||||
<path d="M4 16c22-11 42 8 62-3M0 49c24-13 48 11 68-2" stroke="#c99c5d" stroke-width="5" opacity="0.25" />
|
||||
<path d="M17 62l9-14M42 29l15-10M58 58l7-5" stroke="#5d4932" stroke-width="3" opacity="0.33" />
|
||||
</pattern>
|
||||
<pattern id="leaf" width="96" height="82" patternUnits="userSpaceOnUse">
|
||||
<rect width="96" height="82" fill="#31472c" />
|
||||
<circle cx="22" cy="27" r="22" fill="#263b24" />
|
||||
<circle cx="48" cy="23" r="25" fill="#4b6839" />
|
||||
<circle cx="72" cy="34" r="22" fill="#2b4128" />
|
||||
<circle cx="41" cy="57" r="21" fill="#5b7a43" />
|
||||
<path d="M14 45c20 12 50 11 72-5M29 22c15 13 36 12 51-4" stroke="#142516" stroke-width="5" opacity="0.36" />
|
||||
</pattern>
|
||||
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#3b7894" />
|
||||
<stop offset="0.55" stop-color="#24536e" />
|
||||
<stop offset="1" stop-color="#183b52" />
|
||||
</linearGradient>
|
||||
<linearGradient id="cliff" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#71664d" />
|
||||
<stop offset="1" stop-color="#2f2b24" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="2400" height="2000" fill="url(#field)" />
|
||||
<g filter="url(#paintTexture)">
|
||||
<path d="M-80 1560C230 1410 455 1372 710 1210C928 1072 1044 886 1212 736C1412 556 1690 505 1890 340C2050 208 2175 70 2480-18" fill="none" stroke="#a17a47" stroke-width="210" stroke-linecap="round" stroke-linejoin="round" opacity="0.98" />
|
||||
<path d="M-80 1560C230 1410 455 1372 710 1210C928 1072 1044 886 1212 736C1412 556 1690 505 1890 340C2050 208 2175 70 2480-18" fill="none" stroke="url(#earth)" stroke-width="148" stroke-linecap="round" stroke-linejoin="round" opacity="0.94" />
|
||||
<path d="M180 2060C360 1750 555 1502 828 1290C1052 1117 1170 940 1192 720C1212 520 1130 310 1225-90" fill="none" stroke="#9c7448" stroke-width="175" stroke-linecap="round" opacity="0.94" />
|
||||
<path d="M180 2060C360 1750 555 1502 828 1290C1052 1117 1170 940 1192 720C1212 520 1130 310 1225-90" fill="none" stroke="url(#earth)" stroke-width="124" stroke-linecap="round" opacity="0.9" />
|
||||
|
||||
<path d="M1010-90C1020 180 990 410 1068 610C1160 848 1370 1000 1455 1215C1560 1484 1500 1710 1400 2100" fill="none" stroke="#15394f" stroke-width="250" stroke-linecap="round" opacity="0.94" />
|
||||
<path d="M1010-90C1020 180 990 410 1068 610C1160 848 1370 1000 1455 1215C1560 1484 1500 1710 1400 2100" fill="none" stroke="url(#river)" stroke-width="176" stroke-linecap="round" opacity="0.98" />
|
||||
<path d="M970 120c80 82 78 184 28 288M1182 760c120 82 184 190 190 325M1450 1405c42 156 10 292-83 420" fill="none" stroke="#91d0dc" stroke-width="14" opacity="0.34" />
|
||||
|
||||
<path d="M-40 0h520c100 76 142 175 126 294C410 350 207 328-42 276Z" fill="url(#leaf)" opacity="0.93" />
|
||||
<path d="M650 110h325c132 80 180 188 140 315C922 480 744 430 602 316Z" fill="url(#leaf)" opacity="0.86" />
|
||||
<path d="M80 610c275-110 506-72 690 88c-44 190-244 318-594 340C34 928 0 772 80 610Z" fill="url(#leaf)" opacity="0.9" />
|
||||
<path d="M1515 45c240-82 468-42 708 100c34 164-38 310-200 434c-255 20-420-78-548-258Z" fill="url(#leaf)" opacity="0.88" />
|
||||
<path d="M1715 1010c240-78 445-54 640 70v410c-250 54-468 6-650-150c-28-128-24-232 10-330Z" fill="url(#leaf)" opacity="0.86" />
|
||||
<path d="M560 1580c286-126 555-96 760 72c-40 210-236 332-590 350c-142-94-200-236-170-422Z" fill="url(#leaf)" opacity="0.82" />
|
||||
|
||||
<path d="M1650 405c190-95 370-70 542 60c-18 154-120 278-302 370c-182-44-270-180-240-430Z" fill="url(#cliff)" opacity="0.7" />
|
||||
<path d="M0 1645c230-120 400-128 574-62c18 166-56 305-224 417H0Z" fill="url(#cliff)" opacity="0.88" />
|
||||
<path d="M1900 1490c182-36 360 18 520 150v360h-540c-108-146-102-315 20-510Z" fill="url(#cliff)" opacity="0.84" />
|
||||
|
||||
<g transform="translate(1500 540)">
|
||||
<rect x="0" y="0" width="240" height="160" rx="14" fill="#5f4934" />
|
||||
<rect x="30" y="46" width="68" height="82" fill="#98704a" />
|
||||
<rect x="128" y="32" width="74" height="96" fill="#98704a" />
|
||||
<path d="M14 46l52-42l54 42M116 32l52-40l64 40" fill="#c8923e" stroke="#332317" stroke-width="9" />
|
||||
<path d="M0 144h240" stroke="#2d2218" stroke-width="10" />
|
||||
</g>
|
||||
<g transform="translate(1760 150)">
|
||||
<rect x="0" y="0" width="310" height="210" rx="16" fill="#514538" />
|
||||
<rect x="28" y="54" width="250" height="126" fill="#725c42" />
|
||||
<path d="M18 54l70-56l70 56M148 54l70-56l70 56" fill="#8f6d3e" stroke="#2b2118" stroke-width="10" />
|
||||
<path d="M64 178v-56h38v56M198 178v-70h46v70" fill="#211915" />
|
||||
<path d="M-4 188h318" stroke="#1e1814" stroke-width="12" />
|
||||
</g>
|
||||
<g transform="translate(820 1180)">
|
||||
<rect x="0" y="0" width="230" height="170" rx="13" fill="#584432" />
|
||||
<rect x="32" y="48" width="70" height="88" fill="#8a6848" />
|
||||
<rect x="130" y="52" width="64" height="84" fill="#8a6848" />
|
||||
<path d="M16 48l52-42l56 42M120 52l42-34l50 34" fill="#be8431" stroke="#302215" stroke-width="9" />
|
||||
</g>
|
||||
</g>
|
||||
<rect width="2400" height="2000" fill="#0b0d12" opacity="0.08" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -7,13 +7,17 @@ import {
|
||||
secondBattleMap,
|
||||
secondBattleUnits,
|
||||
secondBattleVictoryPages,
|
||||
thirdBattleBonds,
|
||||
thirdBattleMap,
|
||||
thirdBattleUnits,
|
||||
thirdBattleVictoryPages,
|
||||
type BattleBond,
|
||||
type BattleMap,
|
||||
type StoryPage,
|
||||
type UnitData
|
||||
} from './scenario';
|
||||
|
||||
export type BattleScenarioId = 'first-battle-zhuo-commandery' | 'second-battle-yellow-turban-pursuit';
|
||||
export type BattleScenarioId = 'first-battle-zhuo-commandery' | 'second-battle-yellow-turban-pursuit' | 'third-battle-guangzong-road';
|
||||
|
||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||
|
||||
@@ -158,11 +162,65 @@ export const secondBattleScenario: BattleScenarioDefinition = {
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const thirdBattleScenario: BattleScenarioDefinition = {
|
||||
id: 'third-battle-guangzong-road',
|
||||
title: '광종 구원로',
|
||||
victoryConditionLabel: '전령 마원 격파',
|
||||
defeatConditionLabel: '유비 퇴각',
|
||||
openingObjectiveLines: [
|
||||
'황건 전령 마원이 광종 본대로 향하고 있습니다. 마원을 격파하면 적의 연락망이 끊어집니다.',
|
||||
'강가 길목과 요새 주변의 궁병을 방치하면 전선이 오래 묶입니다. 지형 보정을 살펴 진군하십시오.',
|
||||
'14턴 이내에 승리하면 광종 구원로 확보 명성이 오릅니다.'
|
||||
],
|
||||
map: thirdBattleMap,
|
||||
units: thirdBattleUnits,
|
||||
bonds: thirdBattleBonds,
|
||||
mapTextureKey: 'battle-map-third',
|
||||
leaderUnitId: 'guangzong-leader-ma-yuan',
|
||||
quickVictoryTurnLimit: 14,
|
||||
baseVictoryGold: 520,
|
||||
objectives: [
|
||||
{
|
||||
id: 'leader',
|
||||
kind: 'defeat-leader',
|
||||
label: '전령 마원 격파',
|
||||
rewardGold: 320,
|
||||
unitId: 'guangzong-leader-ma-yuan'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '유비 생존',
|
||||
rewardGold: 140,
|
||||
unitId: 'liu-bei'
|
||||
},
|
||||
{
|
||||
id: 'fort',
|
||||
kind: 'secure-terrain',
|
||||
label: '강가 요새 확보',
|
||||
rewardGold: 220,
|
||||
terrain: 'fort'
|
||||
},
|
||||
{
|
||||
id: 'quick',
|
||||
kind: 'quick-victory',
|
||||
label: '14턴 이내 승리',
|
||||
rewardGold: 180,
|
||||
maxTurn: 14
|
||||
}
|
||||
],
|
||||
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||
itemRewards: ['콩 2', '상처약 1', '의용군 명성 +3'],
|
||||
victoryPages: thirdBattleVictoryPages,
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const defaultBattleScenarioId: BattleScenarioId = firstBattleScenario.id;
|
||||
|
||||
export const battleScenarios: Record<BattleScenarioId, BattleScenarioDefinition> = {
|
||||
'first-battle-zhuo-commandery': firstBattleScenario,
|
||||
'second-battle-yellow-turban-pursuit': secondBattleScenario
|
||||
'second-battle-yellow-turban-pursuit': secondBattleScenario,
|
||||
'third-battle-guangzong-road': thirdBattleScenario
|
||||
};
|
||||
|
||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||
|
||||
@@ -248,6 +248,69 @@ export const secondBattleVictoryPages: StoryPage[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const thirdBattleIntroPages: StoryPage[] = [
|
||||
{
|
||||
id: 'third-guangzong-report',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '광종의 급보',
|
||||
background: 'story-militia',
|
||||
text: '탁현 북쪽의 소란을 잠재우자, 장터에는 더 큰 소식이 밀려왔다. 광종 방면에서 황건 본대가 관군을 압박하고 있다는 전갈이었다.'
|
||||
},
|
||||
{
|
||||
id: 'third-liu-bei-decision',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '넓어지는 길',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '작은 마을만 지키고 물러선다면 난세는 끝나지 않는다. 광종으로 향하는 길을 열어, 더 많은 백성이 버틸 시간을 만들어야 한다.'
|
||||
},
|
||||
{
|
||||
id: 'third-guan-yu-bridge',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '강가의 길목',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '관우',
|
||||
portrait: 'guanYu',
|
||||
text: '적 전령이 강가 요새를 거쳐 본대로 향한다면, 그 길을 끊어야 합니다. 좁은 다리와 숲길을 먼저 장악하겠습니다.'
|
||||
},
|
||||
{
|
||||
id: 'third-zhang-fei-vanguard',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '전초전',
|
||||
background: 'story-sortie',
|
||||
speaker: '장비',
|
||||
portrait: 'zhangFei',
|
||||
text: '전령이든 두령이든 내 앞길에 서면 다 똑같소! 형님, 명만 내리시오. 길목을 시원하게 열어 버리겠소!'
|
||||
}
|
||||
];
|
||||
|
||||
export const thirdBattleVictoryPages: StoryPage[] = [
|
||||
{
|
||||
id: 'third-victory-road',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '열린 길목',
|
||||
background: 'story-sortie',
|
||||
text: '황건 전령의 깃발이 꺾이자 광종으로 향하는 길목에 잠시 숨통이 트였다. 흩어진 백성들은 의용군의 이름을 기억하기 시작했다.'
|
||||
},
|
||||
{
|
||||
id: 'third-victory-liu-bei',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '큰 싸움의 그림자',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '우리가 막은 것은 작은 전갈 하나일 뿐이다. 그러나 작은 길 하나가 열리면, 다음 전장에서도 백성을 구할 길이 생긴다.'
|
||||
},
|
||||
{
|
||||
id: 'third-victory-next',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '광종으로',
|
||||
background: 'story-militia',
|
||||
text: '세 형제는 병사를 추스르고 광종 방면으로 눈을 돌렸다. 황건의 큰 물결은 아직 꺾이지 않았다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleMap: BattleMap = {
|
||||
width: 20,
|
||||
height: 18,
|
||||
@@ -300,6 +363,33 @@ export const secondBattleMap: BattleMap = {
|
||||
]
|
||||
};
|
||||
|
||||
export const thirdBattleMap: BattleMap = {
|
||||
width: 24,
|
||||
height: 20,
|
||||
terrain: [
|
||||
['forest', 'forest', 'hill', 'plain', 'plain', 'road', 'plain', 'forest', 'hill', 'plain', 'river', 'river', 'plain', 'forest', 'forest', 'hill', 'hill', 'plain', 'fort', 'fort', 'plain', 'forest', 'forest', 'hill'],
|
||||
['forest', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'plain', 'plain', 'river', 'river', 'road', 'plain', 'forest', 'plain', 'hill', 'plain', 'fort', 'plain', 'plain', 'plain', 'forest', 'forest'],
|
||||
['hill', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'road', 'road', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'forest'],
|
||||
['plain', 'plain', 'forest', 'road', 'plain', 'plain', 'forest', 'plain', 'hill', 'road', 'river', 'river', 'road', 'road', 'plain', 'hill', 'hill', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain'],
|
||||
['plain', 'forest', 'forest', 'road', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'river', 'river', 'plain', 'road', 'plain', 'plain', 'hill', 'cliff', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain'],
|
||||
['plain', 'plain', 'forest', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'road', 'plain', 'river', 'river', 'road', 'plain', 'village', 'plain', 'hill', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain'],
|
||||
['plain', 'plain', 'plain', 'plain', 'road', 'plain', 'hill', 'hill', 'plain', 'road', 'plain', 'river', 'river', 'road', 'plain', 'village', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain'],
|
||||
['plain', 'forest', 'plain', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'plain', 'river', 'river', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain'],
|
||||
['forest', 'forest', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'hill', 'plain', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain'],
|
||||
['forest', 'plain', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'hill', 'plain', 'plain', 'plain', 'plain'],
|
||||
['plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'river', 'river', 'plain', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain', 'plain'],
|
||||
['plain', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'village', 'plain', 'plain', 'plain', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain'],
|
||||
['hill', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'village', 'plain', 'forest', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'plain', 'plain', 'plain', 'plain'],
|
||||
['hill', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'plain'],
|
||||
['plain', 'road', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain'],
|
||||
['camp', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'hill'],
|
||||
['camp', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'hill'],
|
||||
['plain', 'camp', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain'],
|
||||
['plain', 'plain', 'camp', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'plain'],
|
||||
['plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain']
|
||||
]
|
||||
};
|
||||
|
||||
export const firstBattleUnits: UnitData[] = [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
@@ -840,6 +930,270 @@ export const secondBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const thirdBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 2, y: 17 },
|
||||
'guan-yu': { x: 3, y: 17 },
|
||||
'zhang-fei': { x: 2, y: 18 }
|
||||
};
|
||||
|
||||
export const thirdBattleUnits: UnitData[] = [
|
||||
...firstBattleUnits
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => placeScenarioUnit(unit, thirdBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
|
||||
{
|
||||
id: 'guangzong-scout-a',
|
||||
name: '황건 척후병',
|
||||
faction: 'enemy',
|
||||
className: '황건병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 3,
|
||||
exp: 18,
|
||||
hp: 25,
|
||||
maxHp: 25,
|
||||
attack: 8,
|
||||
move: 3,
|
||||
stats: { might: 58, intelligence: 36, leadership: 45, agility: 52, luck: 43 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 18 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 8 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 6,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
id: 'guangzong-bandit-a',
|
||||
name: '숲길 복병',
|
||||
faction: 'enemy',
|
||||
className: '도적',
|
||||
classKey: 'bandit',
|
||||
level: 3,
|
||||
exp: 20,
|
||||
hp: 26,
|
||||
maxHp: 26,
|
||||
attack: 9,
|
||||
move: 3,
|
||||
stats: { might: 62, intelligence: 34, leadership: 42, agility: 58, luck: 46 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 18 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 8 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 8,
|
||||
y: 13
|
||||
},
|
||||
{
|
||||
id: 'guangzong-archer-a',
|
||||
name: '강가 궁병',
|
||||
faction: 'enemy',
|
||||
className: '궁병',
|
||||
classKey: 'archer',
|
||||
level: 3,
|
||||
exp: 20,
|
||||
hp: 22,
|
||||
maxHp: 22,
|
||||
attack: 9,
|
||||
move: 3,
|
||||
stats: { might: 57, intelligence: 45, leadership: 47, agility: 60, luck: 48 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 1, exp: 20 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 8 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 11,
|
||||
y: 14
|
||||
},
|
||||
{
|
||||
id: 'guangzong-cavalry-a',
|
||||
name: '황건 기병',
|
||||
faction: 'enemy',
|
||||
className: '경기병',
|
||||
classKey: 'cavalry',
|
||||
level: 4,
|
||||
exp: 22,
|
||||
hp: 29,
|
||||
maxHp: 29,
|
||||
attack: 10,
|
||||
move: 5,
|
||||
stats: { might: 66, intelligence: 36, leadership: 50, agility: 68, luck: 48 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 22 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 9 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 12,
|
||||
y: 11
|
||||
},
|
||||
{
|
||||
id: 'guangzong-raider-a',
|
||||
name: '황건 장정',
|
||||
faction: 'enemy',
|
||||
className: '황건병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 3,
|
||||
exp: 18,
|
||||
hp: 26,
|
||||
maxHp: 26,
|
||||
attack: 8,
|
||||
move: 3,
|
||||
stats: { might: 59, intelligence: 35, leadership: 46, agility: 51, luck: 44 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 18 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 8 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 10,
|
||||
y: 9
|
||||
},
|
||||
{
|
||||
id: 'guangzong-raider-b',
|
||||
name: '황건 장정',
|
||||
faction: 'enemy',
|
||||
className: '황건병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 3,
|
||||
exp: 18,
|
||||
hp: 26,
|
||||
maxHp: 26,
|
||||
attack: 8,
|
||||
move: 3,
|
||||
stats: { might: 59, intelligence: 35, leadership: 46, agility: 51, luck: 44 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 18 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 8 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 14,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'guangzong-archer-b',
|
||||
name: '요새 궁병',
|
||||
faction: 'enemy',
|
||||
className: '궁병',
|
||||
classKey: 'archer',
|
||||
level: 4,
|
||||
exp: 24,
|
||||
hp: 23,
|
||||
maxHp: 23,
|
||||
attack: 9,
|
||||
move: 3,
|
||||
stats: { might: 58, intelligence: 47, leadership: 48, agility: 61, luck: 49 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 1, exp: 24 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 9 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 15,
|
||||
y: 7
|
||||
},
|
||||
{
|
||||
id: 'guangzong-cavalry-b',
|
||||
name: '전령 기병',
|
||||
faction: 'enemy',
|
||||
className: '경기병',
|
||||
classKey: 'cavalry',
|
||||
level: 4,
|
||||
exp: 25,
|
||||
hp: 30,
|
||||
maxHp: 30,
|
||||
attack: 10,
|
||||
move: 5,
|
||||
stats: { might: 67, intelligence: 38, leadership: 51, agility: 70, luck: 50 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 25 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 17,
|
||||
y: 6
|
||||
},
|
||||
{
|
||||
id: 'guangzong-guard-a',
|
||||
name: '요새 수비병',
|
||||
faction: 'enemy',
|
||||
className: '황건병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 4,
|
||||
exp: 22,
|
||||
hp: 28,
|
||||
maxHp: 28,
|
||||
attack: 9,
|
||||
move: 3,
|
||||
stats: { might: 62, intelligence: 37, leadership: 50, agility: 52, luck: 46 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 22 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 18,
|
||||
y: 4
|
||||
},
|
||||
{
|
||||
id: 'guangzong-guard-b',
|
||||
name: '요새 수비병',
|
||||
faction: 'enemy',
|
||||
className: '황건병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 4,
|
||||
exp: 22,
|
||||
hp: 28,
|
||||
maxHp: 28,
|
||||
attack: 9,
|
||||
move: 3,
|
||||
stats: { might: 62, intelligence: 37, leadership: 50, agility: 52, luck: 46 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 22 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 10 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 20,
|
||||
y: 4
|
||||
},
|
||||
{
|
||||
id: 'guangzong-fort-archer',
|
||||
name: '망루 궁병',
|
||||
faction: 'enemy',
|
||||
className: '궁병',
|
||||
classKey: 'archer',
|
||||
level: 4,
|
||||
exp: 25,
|
||||
hp: 24,
|
||||
maxHp: 24,
|
||||
attack: 10,
|
||||
move: 3,
|
||||
stats: { might: 60, intelligence: 49, leadership: 49, agility: 61, luck: 50 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 1, exp: 25 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 10 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 19,
|
||||
y: 2
|
||||
},
|
||||
{
|
||||
id: 'guangzong-leader-ma-yuan',
|
||||
name: '전령 마원',
|
||||
faction: 'enemy',
|
||||
className: '두령',
|
||||
classKey: 'rebelLeader',
|
||||
level: 5,
|
||||
exp: 30,
|
||||
hp: 42,
|
||||
maxHp: 42,
|
||||
attack: 12,
|
||||
move: 3,
|
||||
stats: { might: 72, intelligence: 50, leadership: 68, agility: 56, luck: 54 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'leader-axe', level: 2, exp: 8 },
|
||||
armor: { itemId: 'lamellar-armor', level: 1, exp: 18 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 20,
|
||||
y: 2
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -868,6 +1222,7 @@ export const firstBattleBonds: BattleBond[] = [
|
||||
];
|
||||
|
||||
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
|
||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||
return {
|
||||
|
||||
@@ -51,6 +51,17 @@ const unitTexture: Record<string, string> = {
|
||||
'pursuit-leader-han-seok': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
|
||||
lord: 'unit-liu-bei',
|
||||
infantry: 'unit-guan-yu',
|
||||
spearman: 'unit-zhang-fei',
|
||||
archer: 'unit-rebel-archer',
|
||||
cavalry: 'unit-rebel-cavalry',
|
||||
bandit: 'unit-rebel',
|
||||
yellowTurban: 'unit-rebel',
|
||||
rebelLeader: 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitFrameRows: Record<UnitDirection, number> = {
|
||||
south: 0,
|
||||
east: 1,
|
||||
@@ -806,7 +817,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private drawUnits() {
|
||||
battleUnits.forEach((unit) => {
|
||||
const sizeRatio = unit.faction === 'ally' ? 1.16 : 1.1;
|
||||
const textureBase = unitTexture[unit.id] ?? 'unit-rebel';
|
||||
const textureBase = unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel';
|
||||
const sprite = this.add.sprite(this.tileCenterX(unit.x), this.tileCenterY(unit.y), textureBase, this.unitFrameIndex('south'));
|
||||
sprite.setName(`unit-${unit.id}`);
|
||||
sprite.setDisplaySize(this.layout.tileSize * sizeRatio, this.layout.tileSize * sizeRatio);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
||||
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
||||
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg';
|
||||
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
||||
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
|
||||
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
|
||||
@@ -62,6 +63,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('story-sortie', storySortieUrl);
|
||||
this.load.image('battle-map-first', firstBattleMapUrl);
|
||||
this.load.image('battle-map-second', secondBattleMapUrl);
|
||||
this.load.image('battle-map-third', thirdBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
|
||||
@@ -2,8 +2,8 @@ import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type EquipmentSlot } from '../data/battleItems';
|
||||
import { getUnitClass } from '../data/battleRules';
|
||||
import { defaultBattleScenario, secondBattleScenario } from '../data/battles';
|
||||
import { firstBattleBonds, firstBattleUnits, secondBattleIntroPages, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import { defaultBattleScenario, secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
||||
import { firstBattleBonds, firstBattleUnits, secondBattleIntroPages, thirdBattleIntroPages, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import {
|
||||
applyCampBondExp,
|
||||
getCampaignState,
|
||||
@@ -398,17 +398,30 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setStrokeStyle(1, palette.gold, 0.4);
|
||||
const completedDialogues = this.completedCampDialogues().length;
|
||||
const inventory = this.inventoryLabels().join(', ');
|
||||
const reward = this.campaign?.latestBattleId === secondBattleScenario.id ? '예상 보상: 다음 장 개방' : '예상 보상: 군자금, 소모품, 의용군 명성';
|
||||
const reward =
|
||||
this.campaign?.latestBattleId === thirdBattleScenario.id
|
||||
? '다음 장: 광종 본전 준비 중'
|
||||
: this.campaign?.latestBattleId === secondBattleScenario.id
|
||||
? `예상 보상: ${thirdBattleScenario.title} 개방`
|
||||
: '예상 보상: 군자금, 소모품, 의용군 명성';
|
||||
this.trackSortie(this.add.text(x + 16, y + 10, reward, this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 16, y + 30, `현재 준비: 대화 ${completedDialogues}/${campDialogues.length} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private nextSortieBriefing() {
|
||||
if (this.campaign?.latestBattleId === thirdBattleScenario.id) {
|
||||
return {
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '광종 본전',
|
||||
description: '광종 구원로는 열렸지만 황건 본대는 아직 건재합니다. 다음 작업에서 광종 본전과 더 큰 전장 목표를 이어 붙일 수 있습니다.'
|
||||
};
|
||||
}
|
||||
|
||||
if (this.campaign?.latestBattleId === secondBattleScenario.id) {
|
||||
return {
|
||||
eyebrow: '다음 이야기',
|
||||
title: '넓어지는 전장',
|
||||
description: '황건 잔당 추격을 마친 의용군은 탁현을 넘어 더 큰 난세의 흐름을 마주합니다. 다음 장으로 향하기 전 전투 결과를 정리합니다.'
|
||||
eyebrow: '다음 전장',
|
||||
title: thirdBattleScenario.title,
|
||||
description: '탁현을 넘어 광종으로 향하는 길목에서 황건 전령이 관군의 움직임을 본대로 전하려 합니다. 강가 요새와 좁은 길을 돌파해야 합니다.'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -471,8 +484,19 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private startVictoryStory() {
|
||||
if (this.campaign?.latestBattleId === thirdBattleScenario.id) {
|
||||
this.hideSortiePrep();
|
||||
this.showCampNotice('광종 본전은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.campaign?.latestBattleId === secondBattleScenario.id) {
|
||||
this.scene.start('StoryScene', { pages: secondBattleScenario.victoryPages, nextScene: 'TitleScene' });
|
||||
markCampaignStep('third-battle');
|
||||
this.scene.start('StoryScene', {
|
||||
pages: [...secondBattleScenario.victoryPages, ...thirdBattleIntroPages],
|
||||
nextScene: 'BattleScene',
|
||||
nextSceneData: { battleId: thirdBattleScenario.id }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { firstBattleVictoryPages } from '../data/scenario';
|
||||
import { secondBattleScenario } from '../data/battles';
|
||||
import { secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
||||
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
||||
import { palette } from '../ui/palette';
|
||||
|
||||
@@ -299,7 +299,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
|
||||
const campaign = loadCampaignState();
|
||||
if (campaign.step === 'first-camp' || campaign.step === 'second-camp') {
|
||||
if (campaign.step === 'first-camp' || campaign.step === 'second-camp' || campaign.step === 'third-camp') {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
}
|
||||
@@ -314,6 +314,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'third-battle') {
|
||||
this.scene.start('BattleScene', { battleId: thirdBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
@@ -36,7 +36,16 @@ export type FirstBattleReport = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type CampaignStep = 'new' | 'prologue' | 'first-battle' | 'first-camp' | 'first-victory-story' | 'second-battle' | 'second-camp';
|
||||
export type CampaignStep =
|
||||
| 'new'
|
||||
| 'prologue'
|
||||
| 'first-battle'
|
||||
| 'first-camp'
|
||||
| 'first-victory-story'
|
||||
| 'second-battle'
|
||||
| 'second-camp'
|
||||
| 'third-battle'
|
||||
| 'third-camp';
|
||||
|
||||
export type CampaignUnitProgressSnapshot = {
|
||||
unitId: string;
|
||||
@@ -86,6 +95,12 @@ export type CampaignState = {
|
||||
export const campaignStorageKey = 'heros-web:campaign-state';
|
||||
export const campaignSaveSlotCount = 3;
|
||||
|
||||
const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: CampaignStep }> = {
|
||||
'first-battle-zhuo-commandery': { victory: 'first-camp', retry: 'first-battle' },
|
||||
'second-battle-yellow-turban-pursuit': { victory: 'second-camp', retry: 'second-battle' },
|
||||
'third-battle-guangzong-road': { victory: 'third-camp', retry: 'third-battle' }
|
||||
};
|
||||
|
||||
export type CampaignSaveSlotSummary = {
|
||||
slot: number;
|
||||
exists: boolean;
|
||||
@@ -191,8 +206,9 @@ export function setFirstBattleReport(report: FirstBattleReport) {
|
||||
|
||||
reportClone.completedCampDialogues = completedCampDialogues;
|
||||
state.firstBattleReport = reportClone;
|
||||
const victoryStep: CampaignStep = battleId === 'second-battle-yellow-turban-pursuit' ? 'second-camp' : 'first-camp';
|
||||
const retryStep: CampaignStep = battleId === 'second-battle-yellow-turban-pursuit' ? 'second-battle' : 'first-battle';
|
||||
const stepRule = campaignBattleSteps[battleId] ?? campaignBattleSteps['first-battle-zhuo-commandery'];
|
||||
const victoryStep: CampaignStep = stepRule.victory;
|
||||
const retryStep: CampaignStep = stepRule.retry;
|
||||
state.step = reportClone.outcome === 'victory' ? victoryStep : retryStep;
|
||||
state.gold = Math.max(0, state.gold - (previousSettlement?.rewardGold ?? 0) + reportClone.rewardGold);
|
||||
state.roster = reportClone.units.filter((unit) => unit.faction === 'ally').map(cloneUnit);
|
||||
|
||||
Reference in New Issue
Block a user