From fe93b70a565b9a237518cc99592e4f268c271040 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Mon, 22 Jun 2026 23:57:49 +0900 Subject: [PATCH] Add Sishui vanguard campaign battle --- docs/roadmap.md | 5 +- scripts/verify-flow.mjs | 74 +++- src/assets/images/battle/fifth-battle-map.svg | 87 +++++ src/game/data/battles.ts | 63 +++- src/game/data/campaignFlow.ts | 31 +- src/game/data/scenario.ts | 325 ++++++++++++++++++ src/game/scenes/BootScene.ts | 2 + src/game/scenes/TitleScene.ts | 15 +- src/game/state/campaignState.ts | 7 +- 9 files changed, 594 insertions(+), 15 deletions(-) create mode 100644 src/assets/images/battle/fifth-battle-map.svg diff --git a/docs/roadmap.md b/docs/roadmap.md index a94c392..9c92838 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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 diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 75b26ea..d9f2baa 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -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) { diff --git a/src/assets/images/battle/fifth-battle-map.svg b/src/assets/images/battle/fifth-battle-map.svg new file mode 100644 index 0000000..2a4e4e0 --- /dev/null +++ b/src/assets/images/battle/fifth-battle-map.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index e33c066..1291d1a 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -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 = { '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]; diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts index 50a60ab..54e0c55 100644 --- a/src/game/data/campaignFlow.ts +++ b/src/game/data/campaignFlow.ts @@ -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 = { }, [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: '공손찬 의탁 흐름은 다음 장으로 이어집니다. 군영에서 성장 상태를 정비하세요.' } }; diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts index e9c5839..6b7ceb8 100644 --- a/src/game/data/scenario.ts +++ b/src/game/data/scenario.ts @@ -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 = { + '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 { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 8bb4697..2236782 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -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); diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index b0cfcc4..d295f75 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -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; diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index d730230..6d64b36 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -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