Add Sishui vanguard campaign battle
This commit is contained in:
@@ -21,11 +21,12 @@ Build a small complete tactical RPG loop that can grow into a longer Romance of
|
||||
- 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
|
||||
- Fourth battle Guangzong main camp confrontation with Zhang Jue, expanded enemy formation, campaign persistence, and anti-Dong Zhuo setup
|
||||
- Flow verification script from title through the fourth battle victory and camp save state
|
||||
- Fifth battle Sishui Gate vanguard path starting the anti-Dong Zhuo chapter, with campaign persistence and title continue support
|
||||
- Flow verification script from title through the fifth battle victory and camp save state
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Start the anti-Dong Zhuo chapter with a story bridge and first coalition-era battle
|
||||
1. Build the Gongsun Zan camp chapter after the Sishui Gate vanguard battle
|
||||
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
|
||||
|
||||
@@ -574,6 +574,78 @@ try {
|
||||
throw new Error(`Expected campaign save to persist fourth battle victory: ${JSON.stringify(campaignSaveAfterFourthBattle)}`);
|
||||
}
|
||||
|
||||
await page.mouse.click(1120, 38);
|
||||
await page.waitForTimeout(160);
|
||||
const fourthCampSortieState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (!fourthCampSortieState?.sortieVisible) {
|
||||
throw new Error(`Expected fourth camp sortie preparation overlay: ${JSON.stringify(fourthCampSortieState)}`);
|
||||
}
|
||||
await page.mouse.click(938, 596);
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
return activeScenes.includes('StoryScene');
|
||||
});
|
||||
|
||||
for (let i = 0; i < 26; i += 1) {
|
||||
const isFifthBattleActive = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
return state?.scene === 'BattleScene' && state?.battleId === 'fifth-battle-sishui-vanguard';
|
||||
});
|
||||
|
||||
if (isFifthBattleActive) {
|
||||
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 === 'fifth-battle-sishui-vanguard' && state?.battleOutcome === null && state?.phase === 'idle';
|
||||
});
|
||||
await page.screenshot({ path: 'dist/verification-fifth-battle.png', fullPage: true });
|
||||
|
||||
const fifthBattleState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
const fifthEnemies = fifthBattleState.units.filter((unit) => unit.faction === 'enemy');
|
||||
const fifthEnemyBehaviors = new Set(fifthEnemies.map((unit) => unit.ai));
|
||||
if (
|
||||
fifthBattleState.camera?.mapWidth !== 24 ||
|
||||
fifthBattleState.camera?.mapHeight !== 20 ||
|
||||
fifthBattleState.victoryConditionLabel !== '호진 격파' ||
|
||||
fifthEnemies.length < 10 ||
|
||||
!fifthEnemyBehaviors.has('aggressive') ||
|
||||
!fifthEnemyBehaviors.has('guard') ||
|
||||
!fifthEnemyBehaviors.has('hold')
|
||||
) {
|
||||
throw new Error(`Expected fifth battle map, objective, and mixed enemy AI: ${JSON.stringify(fifthBattleState)}`);
|
||||
}
|
||||
|
||||
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 campaignSaveAfterFifthBattle = await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem('heros-web:campaign-state');
|
||||
return raw ? JSON.parse(raw) : undefined;
|
||||
});
|
||||
if (
|
||||
!campaignSaveAfterFifthBattle ||
|
||||
campaignSaveAfterFifthBattle.step !== 'fifth-camp' ||
|
||||
campaignSaveAfterFifthBattle.latestBattleId !== 'fifth-battle-sishui-vanguard' ||
|
||||
!campaignSaveAfterFifthBattle.battleHistory?.['fifth-battle-sishui-vanguard']
|
||||
) {
|
||||
throw new Error(`Expected campaign save to persist fifth battle victory: ${JSON.stringify(campaignSaveAfterFifthBattle)}`);
|
||||
}
|
||||
|
||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
@@ -585,7 +657,7 @@ try {
|
||||
return activeScenes.includes('CampScene');
|
||||
});
|
||||
|
||||
console.log(`Verified title-to-fourth-battle flow, result states, and debug API at ${targetUrl}`);
|
||||
console.log(`Verified title-to-fifth-battle flow, result states, and debug API at ${targetUrl}`);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
|
||||
87
src/assets/images/battle/fifth-battle-map.svg
Normal file
87
src/assets/images/battle/fifth-battle-map.svg
Normal file
@@ -0,0 +1,87 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2000" width="2400" height="2000">
|
||||
<defs>
|
||||
<filter id="grain">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.017" numOctaves="4" seed="85" />
|
||||
<feColorMatrix type="saturate" values="0.55" />
|
||||
<feBlend mode="multiply" in2="SourceGraphic" />
|
||||
</filter>
|
||||
<pattern id="grass" width="72" height="72" patternUnits="userSpaceOnUse">
|
||||
<rect width="72" height="72" fill="#61784c" />
|
||||
<path d="M10 20h20M41 48h18M20 62l14-10M52 17l12 9" stroke="#a3b46b" stroke-width="3" opacity="0.26" />
|
||||
<path d="M8 52l11-18M35 24l10-14M58 66l7-20" stroke="#334e30" stroke-width="2" opacity="0.34" />
|
||||
</pattern>
|
||||
<pattern id="earth" width="82" height="82" patternUnits="userSpaceOnUse">
|
||||
<rect width="82" height="82" fill="#94704a" />
|
||||
<path d="M4 18c28-14 50 9 76-5M0 55c30-16 56 12 80-4" stroke="#c69a5f" stroke-width="6" opacity="0.25" />
|
||||
<path d="M18 70l13-18M48 34l18-13M64 68l10-7" stroke="#57452f" stroke-width="3" opacity="0.32" />
|
||||
</pattern>
|
||||
<pattern id="forest" width="104" height="92" patternUnits="userSpaceOnUse">
|
||||
<rect width="104" height="92" fill="#30472d" />
|
||||
<circle cx="20" cy="32" r="25" fill="#223923" />
|
||||
<circle cx="50" cy="24" r="29" fill="#526f3e" />
|
||||
<circle cx="78" cy="40" r="26" fill="#2b4329" />
|
||||
<circle cx="44" cy="68" r="23" fill="#668548" />
|
||||
<path d="M12 50c24 15 58 14 84-5M32 22c18 16 44 14 60-5" 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="#467f9a" />
|
||||
<stop offset="0.54" stop-color="#23536f" />
|
||||
<stop offset="1" stop-color="#193b52" />
|
||||
</linearGradient>
|
||||
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#80705a" />
|
||||
<stop offset="1" stop-color="#44372a" />
|
||||
</linearGradient>
|
||||
<linearGradient id="roof" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#c88b38" />
|
||||
<stop offset="1" stop-color="#7a4f1f" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="2400" height="2000" fill="url(#grass)" />
|
||||
<g filter="url(#grain)">
|
||||
<path d="M-120 1710C230 1438 416 1288 640 1072C826 892 990 760 1164 646C1374 508 1568 380 1870 300C2060 250 2246 150 2520-30" fill="none" stroke="#a98555" stroke-width="250" stroke-linecap="round" opacity="0.98" />
|
||||
<path d="M-120 1710C230 1438 416 1288 640 1072C826 892 990 760 1164 646C1374 508 1568 380 1870 300C2060 250 2246 150 2520-30" fill="none" stroke="url(#earth)" stroke-width="170" stroke-linecap="round" opacity="0.92" />
|
||||
<path d="M-70 1040C212 945 444 920 666 782C880 650 1036 506 1280 454C1508 405 1668 312 1788 128" fill="none" stroke="#a17a4c" stroke-width="184" stroke-linecap="round" opacity="0.9" />
|
||||
<path d="M-70 1040C212 945 444 920 666 782C880 650 1036 506 1280 454C1508 405 1668 312 1788 128" fill="none" stroke="url(#earth)" stroke-width="116" stroke-linecap="round" opacity="0.86" />
|
||||
|
||||
<path d="M1286 430c-62 224 24 368 168 514c146 146 192 292 136 486c-44 152-18 302 112 458" fill="none" stroke="#173b52" stroke-width="252" stroke-linecap="round" opacity="0.96" />
|
||||
<path d="M1286 430c-62 224 24 368 168 514c146 146 192 292 136 486c-44 152-18 302 112 458" fill="none" stroke="url(#river)" stroke-width="170" stroke-linecap="round" opacity="0.98" />
|
||||
<path d="M1325 546c62 86 70 176 26 272M1480 1048c88 100 105 220 52 360M1642 1630c48 84 64 166 46 250" fill="none" stroke="#a9dbe4" stroke-width="13" opacity="0.34" />
|
||||
|
||||
<path d="M-40 70c228-80 454-52 674 80c42 142-24 266-198 372c-192-16-356-80-492-194Z" fill="url(#forest)" opacity="0.9" />
|
||||
<path d="M562 238c238-104 466-78 684 84c16 142-72 248-264 316c-232-18-372-132-420-400Z" fill="url(#forest)" opacity="0.82" />
|
||||
<path d="M40 618c300-118 568-70 806 146c-80 222-324 342-730 360C-10 980-34 812 40 618Z" fill="url(#forest)" opacity="0.86" />
|
||||
<path d="M1762 470c252-92 454-60 650 94v392c-240 78-468 20-684-172c-36-122-24-226 34-314Z" fill="url(#forest)" opacity="0.84" />
|
||||
<path d="M640 1538c320-126 602-82 846 118c-54 220-284 336-690 344c-158-106-210-250-156-462Z" fill="url(#forest)" opacity="0.84" />
|
||||
|
||||
<g transform="translate(1338 58)">
|
||||
<rect x="-34" y="-12" width="560" height="324" rx="18" fill="#2f281e" opacity="0.6" />
|
||||
<rect x="0" y="0" width="492" height="278" rx="16" fill="url(#wall)" />
|
||||
<rect x="40" y="50" width="412" height="184" fill="#756044" stroke="#251d15" stroke-width="12" />
|
||||
<path d="M40 50h412M40 234h412M40 50v184M452 50v184" stroke="#35291e" stroke-width="10" />
|
||||
<rect x="76" y="100" width="82" height="96" fill="#98724a" />
|
||||
<rect x="204" y="72" width="100" height="124" fill="#9d7650" />
|
||||
<rect x="346" y="98" width="68" height="98" fill="#8c6946" />
|
||||
<path d="M58 100l60-48l62 48M190 72l64-52l76 52M330 98l50-42l58 42" fill="url(#roof)" stroke="#302316" stroke-width="9" />
|
||||
<path d="M229 196v-66h50v66" fill="#1f1712" />
|
||||
<path d="M18 278h456" stroke="#c0a369" stroke-width="8" opacity="0.42" />
|
||||
</g>
|
||||
|
||||
<g transform="translate(1760 520)">
|
||||
<rect x="0" y="0" width="220" height="158" rx="12" fill="#5f4b36" />
|
||||
<rect x="34" y="46" width="66" height="82" fill="#98714d" />
|
||||
<rect x="124" y="36" width="70" height="92" fill="#96704a" />
|
||||
<path d="M18 46l50-40l54 40M112 36l48-38l58 38" fill="url(#roof)" stroke="#322317" stroke-width="9" />
|
||||
</g>
|
||||
|
||||
<g transform="translate(80 1418)">
|
||||
<path d="M0 88c84-70 182-92 294-66c80 74 92 162 36 264c-116 24-222 0-318-74c-34-48-38-90-12-124Z" fill="#4d5d5f" opacity="0.48" />
|
||||
<rect x="74" y="104" width="178" height="136" rx="10" fill="#604b36" />
|
||||
<rect x="112" y="148" width="52" height="70" fill="#98724a" />
|
||||
<rect x="184" y="136" width="44" height="82" fill="#96704a" />
|
||||
<path d="M92 148l46-36l48 36M174 136l34-28l42 28" fill="url(#roof)" stroke="#2f2218" stroke-width="8" />
|
||||
</g>
|
||||
</g>
|
||||
<rect width="2400" height="2000" fill="#0b0d12" opacity="0.07" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -3,6 +3,10 @@ import {
|
||||
firstBattleMap,
|
||||
firstBattleUnits,
|
||||
firstBattleVictoryPages,
|
||||
fifthBattleBonds,
|
||||
fifthBattleMap,
|
||||
fifthBattleUnits,
|
||||
fifthBattleVictoryPages,
|
||||
secondBattleBonds,
|
||||
secondBattleMap,
|
||||
secondBattleUnits,
|
||||
@@ -25,7 +29,8 @@ export type BattleScenarioId =
|
||||
| 'first-battle-zhuo-commandery'
|
||||
| 'second-battle-yellow-turban-pursuit'
|
||||
| 'third-battle-guangzong-road'
|
||||
| 'fourth-battle-guangzong-camp';
|
||||
| 'fourth-battle-guangzong-camp'
|
||||
| 'fifth-battle-sishui-vanguard';
|
||||
|
||||
export type BattleObjectiveKind = 'defeat-leader' | 'keep-unit-alive' | 'secure-terrain' | 'quick-victory';
|
||||
|
||||
@@ -276,13 +281,67 @@ export const fourthBattleScenario: BattleScenarioDefinition = {
|
||||
nextCampScene: 'CampScene'
|
||||
};
|
||||
|
||||
export const fifthBattleScenario: BattleScenarioDefinition = {
|
||||
id: 'fifth-battle-sishui-vanguard',
|
||||
title: '사수관 전초전',
|
||||
victoryConditionLabel: '호진 격파',
|
||||
defeatConditionLabel: '유비 퇴각',
|
||||
openingObjectiveLines: [
|
||||
'반동탁 연합으로 향하는 길목을 동탁군 장수 호진이 막고 있습니다. 호진을 격파하면 사수관 앞 전초 진지가 흔들립니다.',
|
||||
'중앙 길은 빠르지만 궁병과 기병의 반격을 받기 쉽습니다. 숲길 척후를 처리하고 요새 주변을 차근히 압박하십시오.',
|
||||
'14턴 이내에 승리하면 공손찬 진영에서 세 형제의 무공이 더 크게 알려집니다.'
|
||||
],
|
||||
map: fifthBattleMap,
|
||||
units: fifthBattleUnits,
|
||||
bonds: fifthBattleBonds,
|
||||
mapTextureKey: 'battle-map-fifth',
|
||||
leaderUnitId: 'sishui-leader-hu-zhen',
|
||||
quickVictoryTurnLimit: 14,
|
||||
baseVictoryGold: 760,
|
||||
objectives: [
|
||||
{
|
||||
id: 'leader',
|
||||
kind: 'defeat-leader',
|
||||
label: '호진 격파',
|
||||
rewardGold: 460,
|
||||
unitId: 'sishui-leader-hu-zhen'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei',
|
||||
kind: 'keep-unit-alive',
|
||||
label: '유비 생존',
|
||||
rewardGold: 190,
|
||||
unitId: 'liu-bei'
|
||||
},
|
||||
{
|
||||
id: 'fort',
|
||||
kind: 'secure-terrain',
|
||||
label: '사수관 전초 요새 확보',
|
||||
rewardGold: 280,
|
||||
terrain: 'fort'
|
||||
},
|
||||
{
|
||||
id: 'quick',
|
||||
kind: 'quick-victory',
|
||||
label: '14턴 이내 승리',
|
||||
rewardGold: 230,
|
||||
maxTurn: 14
|
||||
}
|
||||
],
|
||||
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
|
||||
itemRewards: ['콩 2', '상처약 1', '탁주 1', '반동탁 공적 +1'],
|
||||
victoryPages: fifthBattleVictoryPages,
|
||||
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,
|
||||
'third-battle-guangzong-road': thirdBattleScenario,
|
||||
'fourth-battle-guangzong-camp': fourthBattleScenario
|
||||
'fourth-battle-guangzong-camp': fourthBattleScenario,
|
||||
'fifth-battle-sishui-vanguard': fifthBattleScenario
|
||||
};
|
||||
|
||||
export const defaultBattleScenario = battleScenarios[defaultBattleScenarioId];
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { CampaignStep } from '../state/campaignState';
|
||||
import { defaultBattleScenario, fourthBattleScenario, secondBattleScenario, thirdBattleScenario, type BattleScenarioId } from './battles';
|
||||
import {
|
||||
defaultBattleScenario,
|
||||
fifthBattleScenario,
|
||||
fourthBattleScenario,
|
||||
secondBattleScenario,
|
||||
thirdBattleScenario,
|
||||
type BattleScenarioId
|
||||
} from './battles';
|
||||
import {
|
||||
fifthBattleIntroPages,
|
||||
fifthBattleVictoryPages,
|
||||
firstBattleVictoryPages,
|
||||
fourthBattleIntroPages,
|
||||
fourthBattleVictoryPages,
|
||||
@@ -56,12 +65,22 @@ const sortieFlows: Record<string, SortieFlow> = {
|
||||
},
|
||||
[fourthBattleScenario.id]: {
|
||||
afterBattleId: fourthBattleScenario.id,
|
||||
eyebrow: '다음 전장',
|
||||
title: fifthBattleScenario.title,
|
||||
description: '황건적 토벌로 이름을 알린 세 형제는 반동탁 연합의 흐름에 몸을 싣습니다. 공손찬의 진영으로 향하는 길목에서 사수관 전초 병력을 상대해야 합니다.',
|
||||
rewardHint: `예상 보상: ${fifthBattleScenario.title} 개방`,
|
||||
nextBattleId: fifthBattleScenario.id,
|
||||
campaignStep: 'fifth-battle',
|
||||
pages: [...fourthBattleVictoryPages, ...fifthBattleIntroPages]
|
||||
},
|
||||
[fifthBattleScenario.id]: {
|
||||
afterBattleId: fifthBattleScenario.id,
|
||||
eyebrow: '다음 장 준비',
|
||||
title: '반동탁의 흐름',
|
||||
description: '황건적 토벌로 이름을 알린 세 형제 앞에 조정의 새로운 혼란이 다가옵니다. 다음 큰 줄기는 반동탁 흐름으로 이어집니다.',
|
||||
rewardHint: '다음 장: 반동탁 흐름 준비 중',
|
||||
pages: [],
|
||||
unavailableNotice: '반동탁 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
|
||||
title: '공손찬 의탁',
|
||||
description: '사수관 앞 전초전을 넘긴 유비군은 공손찬 진영에서 연합군의 큰 흐름을 살피게 됩니다. 다음 줄기는 공손찬 의탁과 그 뒤의 서주로 이어집니다.',
|
||||
rewardHint: '다음 장: 공손찬 의탁 준비 중',
|
||||
pages: fifthBattleVictoryPages,
|
||||
unavailableNotice: '공손찬 의탁 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -374,6 +374,60 @@ export const fourthBattleVictoryPages: StoryPage[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const fifthBattleIntroPages: StoryPage[] = [
|
||||
{
|
||||
id: 'fifth-coalition-rumor',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '반동탁의 격문',
|
||||
background: 'story-militia',
|
||||
text: '황건의 불길이 잦아들자 조정에는 또 다른 폭풍이 일었다. 동탁이 낙양을 장악했다는 소식과 함께, 각지의 제후들이 반동탁의 격문에 응하기 시작했다.'
|
||||
},
|
||||
{
|
||||
id: 'fifth-liu-bei-choice',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '새로운 대의',
|
||||
background: 'story-liu-bei',
|
||||
speaker: '유비',
|
||||
portrait: 'liuBei',
|
||||
text: '백성을 괴롭히는 난을 막았다 하나, 조정이 어지러우면 천하는 다시 피를 흘린다. 우리는 더 큰 흐름 속으로 들어가야 한다.'
|
||||
},
|
||||
{
|
||||
id: 'fifth-gongsun-zan',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '공손찬의 진영',
|
||||
background: 'story-three-heroes',
|
||||
speaker: '관우',
|
||||
portrait: 'guanYu',
|
||||
text: '공손찬의 군영에 합류하면 연합군의 움직임을 가까이서 볼 수 있습니다. 그러나 동탁군도 사수관 앞길을 막고 있습니다.'
|
||||
},
|
||||
{
|
||||
id: 'fifth-sishui-sortie',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '사수관 전초전',
|
||||
background: 'story-sortie',
|
||||
speaker: '장비',
|
||||
portrait: 'zhangFei',
|
||||
text: '동탁군이든 황건적이든 백성을 짓밟는 놈들은 똑같소. 사수관 앞길부터 뚫어 봅시다!'
|
||||
}
|
||||
];
|
||||
|
||||
export const fifthBattleVictoryPages: StoryPage[] = [
|
||||
{
|
||||
id: 'fifth-victory-sishui',
|
||||
bgm: 'militia-theme',
|
||||
chapter: '사수관의 문턱',
|
||||
background: 'story-sortie',
|
||||
text: '동탁군의 전초 부대가 물러서자 연합군 진영에 세 형제의 이름이 다시 한 번 전해졌다.'
|
||||
},
|
||||
{
|
||||
id: 'fifth-victory-next',
|
||||
bgm: 'battle-prep',
|
||||
chapter: '공손찬 의탁',
|
||||
background: 'story-militia',
|
||||
text: '사수관 앞 전초전은 끝났지만, 반동탁의 싸움은 이제 막 시작되었다. 유비는 공손찬의 군영에서 다음 기회를 기다린다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleMap: BattleMap = {
|
||||
width: 20,
|
||||
height: 18,
|
||||
@@ -480,6 +534,33 @@ export const fourthBattleMap: BattleMap = {
|
||||
]
|
||||
};
|
||||
|
||||
export const fifthBattleMap: BattleMap = {
|
||||
width: 24,
|
||||
height: 20,
|
||||
terrain: [
|
||||
['hill', 'hill', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'hill', 'hill', 'forest', 'plain', 'plain', 'fort', 'fort', 'plain', 'hill', 'hill', 'forest', 'forest', 'plain', 'plain', 'hill', 'hill'],
|
||||
['hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'fort', 'fort', 'road', 'plain', 'hill', 'forest', 'plain', 'plain', 'plain', 'plain', 'hill'],
|
||||
['plain', 'plain', 'forest', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain', 'hill', 'plain', 'plain'],
|
||||
['plain', 'forest', 'forest', 'plain', 'road', 'plain', 'hill', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'plain', 'road', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain'],
|
||||
['plain', 'plain', 'forest', 'road', 'road', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill'],
|
||||
['plain', 'plain', 'plain', 'road', 'plain', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'road', 'plain', 'village', 'plain', 'forest', 'forest', 'plain', 'hill'],
|
||||
['forest', 'plain', 'plain', 'road', 'plain', 'hill', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'plain', 'village', 'plain', 'plain', 'forest', 'plain', 'plain'],
|
||||
['forest', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain'],
|
||||
['plain', 'plain', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain'],
|
||||
['plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'road', 'road', 'plain', 'plain', 'plain', 'forest', 'plain'],
|
||||
['road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'road', 'plain', 'plain', 'hill', 'plain', 'plain'],
|
||||
['plain', 'road', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'hill', 'plain', 'plain'],
|
||||
['plain', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'plain', 'plain'],
|
||||
['plain', 'road', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'hill', 'plain'],
|
||||
['camp', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain'],
|
||||
['camp', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'forest', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain'],
|
||||
['plain', 'camp', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'plain'],
|
||||
['plain', 'plain', 'camp', 'road', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'plain'],
|
||||
['plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'plain'],
|
||||
['plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'hill', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain']
|
||||
]
|
||||
};
|
||||
|
||||
export const firstBattleUnits: UnitData[] = [
|
||||
{
|
||||
id: 'liu-bei',
|
||||
@@ -1548,6 +1629,249 @@ export const fourthBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const fifthBattleAllyPositions: Record<string, { x: number; y: number }> = {
|
||||
'liu-bei': { x: 2, y: 16 },
|
||||
'guan-yu': { x: 3, y: 16 },
|
||||
'zhang-fei': { x: 2, y: 17 }
|
||||
};
|
||||
|
||||
export const fifthBattleUnits: UnitData[] = [
|
||||
...firstBattleUnits
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => placeScenarioUnit(unit, fifthBattleAllyPositions[unit.id] ?? { x: unit.x, y: unit.y })),
|
||||
{
|
||||
id: 'sishui-vanguard-a',
|
||||
name: '서량 선봉',
|
||||
faction: 'enemy',
|
||||
className: '동탁 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 5,
|
||||
exp: 30,
|
||||
hp: 32,
|
||||
maxHp: 32,
|
||||
attack: 10,
|
||||
move: 3,
|
||||
stats: { might: 68, intelligence: 38, leadership: 52, agility: 55, luck: 44 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 4 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 16 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 6,
|
||||
y: 14
|
||||
},
|
||||
{
|
||||
id: 'sishui-vanguard-b',
|
||||
name: '서량 선봉',
|
||||
faction: 'enemy',
|
||||
className: '동탁 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 5,
|
||||
exp: 30,
|
||||
hp: 32,
|
||||
maxHp: 32,
|
||||
attack: 10,
|
||||
move: 3,
|
||||
stats: { might: 68, intelligence: 37, leadership: 52, agility: 54, luck: 44 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 4 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 16 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 8,
|
||||
y: 12
|
||||
},
|
||||
{
|
||||
id: 'sishui-raider-a',
|
||||
name: '산길 척후',
|
||||
faction: 'enemy',
|
||||
className: '척후병',
|
||||
classKey: 'bandit',
|
||||
level: 5,
|
||||
exp: 32,
|
||||
hp: 30,
|
||||
maxHp: 30,
|
||||
attack: 11,
|
||||
move: 4,
|
||||
stats: { might: 70, intelligence: 42, leadership: 48, agility: 66, luck: 52 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 8 },
|
||||
armor: { itemId: 'rebel-vest', level: 1, exp: 16 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 5,
|
||||
y: 9
|
||||
},
|
||||
{
|
||||
id: 'sishui-archer-a',
|
||||
name: '사수관 궁병',
|
||||
faction: 'enemy',
|
||||
className: '서량 궁병',
|
||||
classKey: 'archer',
|
||||
level: 5,
|
||||
exp: 32,
|
||||
hp: 27,
|
||||
maxHp: 27,
|
||||
attack: 11,
|
||||
move: 3,
|
||||
stats: { might: 63, intelligence: 50, leadership: 51, agility: 64, luck: 49 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 8 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 16 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 10,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'sishui-cavalry-a',
|
||||
name: '서량 기병',
|
||||
faction: 'enemy',
|
||||
className: '서량 경기병',
|
||||
classKey: 'cavalry',
|
||||
level: 6,
|
||||
exp: 34,
|
||||
hp: 36,
|
||||
maxHp: 36,
|
||||
attack: 12,
|
||||
move: 5,
|
||||
stats: { might: 74, intelligence: 38, leadership: 54, agility: 76, luck: 50 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 10 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 2 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 12,
|
||||
y: 9
|
||||
},
|
||||
{
|
||||
id: 'sishui-guard-a',
|
||||
name: '관문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '동탁 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 6,
|
||||
exp: 34,
|
||||
hp: 35,
|
||||
maxHp: 35,
|
||||
attack: 11,
|
||||
move: 3,
|
||||
stats: { might: 72, intelligence: 40, leadership: 56, agility: 56, luck: 48 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 12 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 2 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 14,
|
||||
y: 7
|
||||
},
|
||||
{
|
||||
id: 'sishui-archer-b',
|
||||
name: '관문 궁병',
|
||||
faction: 'enemy',
|
||||
className: '서량 궁병',
|
||||
classKey: 'archer',
|
||||
level: 6,
|
||||
exp: 36,
|
||||
hp: 28,
|
||||
maxHp: 28,
|
||||
attack: 12,
|
||||
move: 3,
|
||||
stats: { might: 65, intelligence: 52, leadership: 53, agility: 65, luck: 50 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'short-bow', level: 2, exp: 12 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 18 },
|
||||
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||
},
|
||||
x: 16,
|
||||
y: 6
|
||||
},
|
||||
{
|
||||
id: 'sishui-cavalry-b',
|
||||
name: '서량 기병',
|
||||
faction: 'enemy',
|
||||
className: '서량 경기병',
|
||||
classKey: 'cavalry',
|
||||
level: 6,
|
||||
exp: 36,
|
||||
hp: 37,
|
||||
maxHp: 37,
|
||||
attack: 12,
|
||||
move: 5,
|
||||
stats: { might: 75, intelligence: 39, leadership: 55, agility: 77, luck: 51 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 12 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 4 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 18,
|
||||
y: 10
|
||||
},
|
||||
{
|
||||
id: 'sishui-guard-b',
|
||||
name: '관문 수비병',
|
||||
faction: 'enemy',
|
||||
className: '동탁 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 6,
|
||||
exp: 36,
|
||||
hp: 36,
|
||||
maxHp: 36,
|
||||
attack: 11,
|
||||
move: 3,
|
||||
stats: { might: 73, intelligence: 40, leadership: 57, agility: 56, luck: 49 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 14 },
|
||||
armor: { itemId: 'rebel-vest', level: 2, exp: 4 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 16,
|
||||
y: 3
|
||||
},
|
||||
{
|
||||
id: 'sishui-gate-guard',
|
||||
name: '관문 친위병',
|
||||
faction: 'enemy',
|
||||
className: '동탁 보병',
|
||||
classKey: 'yellowTurban',
|
||||
level: 6,
|
||||
exp: 38,
|
||||
hp: 38,
|
||||
maxHp: 38,
|
||||
attack: 12,
|
||||
move: 3,
|
||||
stats: { might: 76, intelligence: 42, leadership: 60, agility: 57, luck: 50 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'yellow-turban-saber', level: 2, exp: 18 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 8 },
|
||||
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||
},
|
||||
x: 14,
|
||||
y: 1
|
||||
},
|
||||
{
|
||||
id: 'sishui-leader-hu-zhen',
|
||||
name: '호진',
|
||||
faction: 'enemy',
|
||||
className: '동탁 장수',
|
||||
classKey: 'rebelLeader',
|
||||
level: 7,
|
||||
exp: 44,
|
||||
hp: 50,
|
||||
maxHp: 50,
|
||||
attack: 14,
|
||||
move: 3,
|
||||
stats: { might: 80, intelligence: 55, leadership: 72, agility: 60, luck: 54 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'leader-axe', level: 2, exp: 20 },
|
||||
armor: { itemId: 'lamellar-armor', level: 2, exp: 10 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 15,
|
||||
y: 1
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -1578,6 +1902,7 @@ export const firstBattleBonds: BattleBond[] = [
|
||||
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const fifthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
|
||||
function placeScenarioUnit(unit: UnitData, position: { x: number; y: number }): UnitData {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Phaser from 'phaser';
|
||||
import fifthBattleMapUrl from '../../assets/images/battle/fifth-battle-map.svg';
|
||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
||||
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
|
||||
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg';
|
||||
@@ -66,6 +67,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('battle-map-second', secondBattleMapUrl);
|
||||
this.load.image('battle-map-third', thirdBattleMapUrl);
|
||||
this.load.image('battle-map-fourth', fourthBattleMapUrl);
|
||||
this.load.image('battle-map-fifth', fifthBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { firstBattleVictoryPages } from '../data/scenario';
|
||||
import { fourthBattleScenario, secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
||||
import { fifthBattleScenario, fourthBattleScenario, secondBattleScenario, thirdBattleScenario } from '../data/battles';
|
||||
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
||||
import { palette } from '../ui/palette';
|
||||
|
||||
@@ -299,7 +299,13 @@ export class TitleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
|
||||
const campaign = loadCampaignState();
|
||||
if (campaign.step === 'first-camp' || campaign.step === 'second-camp' || campaign.step === 'third-camp' || campaign.step === 'fourth-camp') {
|
||||
if (
|
||||
campaign.step === 'first-camp' ||
|
||||
campaign.step === 'second-camp' ||
|
||||
campaign.step === 'third-camp' ||
|
||||
campaign.step === 'fourth-camp' ||
|
||||
campaign.step === 'fifth-camp'
|
||||
) {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
}
|
||||
@@ -324,6 +330,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'fifth-battle') {
|
||||
this.scene.start('BattleScene', { battleId: fifthBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
@@ -47,7 +47,9 @@ export type CampaignStep =
|
||||
| 'third-battle'
|
||||
| 'third-camp'
|
||||
| 'fourth-battle'
|
||||
| 'fourth-camp';
|
||||
| 'fourth-camp'
|
||||
| 'fifth-battle'
|
||||
| 'fifth-camp';
|
||||
|
||||
export type CampaignUnitProgressSnapshot = {
|
||||
unitId: string;
|
||||
@@ -101,7 +103,8 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
|
||||
'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' },
|
||||
'fourth-battle-guangzong-camp': { victory: 'fourth-camp', retry: 'fourth-battle' }
|
||||
'fourth-battle-guangzong-camp': { victory: 'fourth-camp', retry: 'fourth-battle' },
|
||||
'fifth-battle-sishui-vanguard': { victory: 'fifth-camp', retry: 'fifth-battle' }
|
||||
};
|
||||
|
||||
export type CampaignSaveSlotSummary = {
|
||||
|
||||
Reference in New Issue
Block a user