Add second battle pursuit chapter
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
markCampaignStep,
|
||||
saveCampaignState,
|
||||
setFirstBattleReport,
|
||||
type CampaignState,
|
||||
type CampaignStep
|
||||
} from '../state/campaignState';
|
||||
import { palette } from '../ui/palette';
|
||||
@@ -36,7 +37,18 @@ const unitTexture: Record<string, string> = {
|
||||
'rebel-cavalry-a': 'unit-rebel-cavalry',
|
||||
'rebel-cavalry-b': 'unit-rebel-cavalry',
|
||||
'rebel-archer-b': 'unit-rebel-archer',
|
||||
'rebel-leader': 'unit-rebel-leader'
|
||||
'rebel-leader': 'unit-rebel-leader',
|
||||
'pursuit-raider-a': 'unit-rebel',
|
||||
'pursuit-raider-b': 'unit-rebel',
|
||||
'pursuit-bandit-a': 'unit-rebel',
|
||||
'pursuit-archer-a': 'unit-rebel-archer',
|
||||
'pursuit-cavalry-a': 'unit-rebel-cavalry',
|
||||
'pursuit-raider-c': 'unit-rebel',
|
||||
'pursuit-archer-b': 'unit-rebel-archer',
|
||||
'pursuit-cavalry-b': 'unit-rebel-cavalry',
|
||||
'pursuit-guard-a': 'unit-rebel',
|
||||
'pursuit-guard-b': 'unit-rebel',
|
||||
'pursuit-leader-han-seok': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
const unitFrameRows: Record<UnitDirection, number> = {
|
||||
@@ -459,7 +471,18 @@ const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
||||
'rebel-cavalry-a': 'aggressive',
|
||||
'rebel-cavalry-b': 'aggressive',
|
||||
'rebel-archer-b': 'hold',
|
||||
'rebel-leader': 'guard'
|
||||
'rebel-leader': 'guard',
|
||||
'pursuit-raider-a': 'aggressive',
|
||||
'pursuit-raider-b': 'guard',
|
||||
'pursuit-bandit-a': 'aggressive',
|
||||
'pursuit-archer-a': 'hold',
|
||||
'pursuit-cavalry-a': 'aggressive',
|
||||
'pursuit-raider-c': 'aggressive',
|
||||
'pursuit-archer-b': 'hold',
|
||||
'pursuit-cavalry-b': 'aggressive',
|
||||
'pursuit-guard-a': 'guard',
|
||||
'pursuit-guard-b': 'guard',
|
||||
'pursuit-leader-han-seok': 'guard'
|
||||
};
|
||||
|
||||
const defaultEnemyAiByClass: Partial<Record<UnitClassKey, EnemyAiBehavior>> = {
|
||||
@@ -611,8 +634,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
create() {
|
||||
this.resetBattleData();
|
||||
const campaign = getCampaignState();
|
||||
this.resetBattleData(campaign);
|
||||
if (campaign.step === 'new' || campaign.step === 'prologue' || campaign.step === 'first-battle') {
|
||||
markCampaignStep('first-battle');
|
||||
}
|
||||
@@ -662,9 +685,42 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.renderRosterPanel('ally', '행동할 장수를 선택하세요.');
|
||||
}
|
||||
|
||||
private resetBattleData() {
|
||||
battleUnits.splice(0, battleUnits.length, ...initialBattleUnits.map(cloneUnitData));
|
||||
battleBonds.splice(0, battleBonds.length, ...initialBattleBonds.map(cloneBattleBond));
|
||||
private resetBattleData(campaign?: CampaignState) {
|
||||
battleUnits.splice(0, battleUnits.length, ...initialBattleUnits.map((unit) => this.applyCampaignUnitProgress(unit, campaign)));
|
||||
battleBonds.splice(0, battleBonds.length, ...initialBattleBonds.map((bond) => this.applyCampaignBondProgress(bond, campaign)));
|
||||
initialBattleUnits = battleUnits.map(cloneUnitData);
|
||||
initialBattleBonds = battleBonds.map(cloneBattleBond);
|
||||
}
|
||||
|
||||
private applyCampaignUnitProgress(unit: UnitData, campaign?: CampaignState) {
|
||||
const cloned = cloneUnitData(unit);
|
||||
const progress = campaign?.roster.find((candidate) => candidate.id === unit.id);
|
||||
if (!progress || unit.faction !== 'ally') {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return {
|
||||
...cloned,
|
||||
level: progress.level,
|
||||
exp: progress.exp,
|
||||
hp: Math.max(1, Math.min(progress.hp, progress.maxHp)),
|
||||
maxHp: progress.maxHp,
|
||||
equipment: JSON.parse(JSON.stringify(progress.equipment)) as UnitData['equipment']
|
||||
};
|
||||
}
|
||||
|
||||
private applyCampaignBondProgress(bond: BattleBond, campaign?: CampaignState) {
|
||||
const cloned = cloneBattleBond(bond);
|
||||
const progress = campaign?.bonds.find((candidate) => candidate.id === bond.id);
|
||||
if (!progress) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return {
|
||||
...cloned,
|
||||
level: progress.level,
|
||||
exp: progress.exp
|
||||
};
|
||||
}
|
||||
|
||||
private createLayout(width: number, height: number): BattleLayout {
|
||||
@@ -2054,7 +2110,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
this.addResultButton('다시 하기', left + panelWidth - 276, top + 612, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
this.scene.restart();
|
||||
this.scene.restart({ battleId: battleScenario.id });
|
||||
}, depth + 3);
|
||||
this.addResultButton('타이틀로', left + panelWidth - 140, top + 612, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
@@ -3149,7 +3205,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private showOpeningBattleEvent() {
|
||||
this.triggerBattleEvent('opening', '첫 전투 목표', battleScenario.openingObjectiveLines);
|
||||
this.triggerBattleEvent('opening', '전투 목표', battleScenario.openingObjectiveLines);
|
||||
}
|
||||
|
||||
private triggerBattleEvent(key: string, title: string, lines: string[]) {
|
||||
@@ -6463,8 +6519,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
x: unit.x,
|
||||
y: unit.y,
|
||||
acted: this.actedUnitIds.has(unit.id),
|
||||
animating: this.unitViews.get(unit.id)?.sprite.anims.isPlaying ?? false,
|
||||
animationKey: this.unitViews.get(unit.id)?.sprite.anims.currentAnim?.key ?? null
|
||||
animating: this.unitViews.get(unit.id)?.sprite.anims?.isPlaying ?? false,
|
||||
animationKey: this.unitViews.get(unit.id)?.sprite.anims?.currentAnim?.key ?? null
|
||||
})),
|
||||
bonds: Array.from(this.bondStates.values()).map((bond) => ({
|
||||
id: bond.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 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';
|
||||
@@ -60,6 +61,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('story-militia', storyMilitiaUrl);
|
||||
this.load.image('story-sortie', storySortieUrl);
|
||||
this.load.image('battle-map-first', firstBattleMapUrl);
|
||||
this.load.image('battle-map-second', secondBattleMapUrl);
|
||||
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 } from '../data/battles';
|
||||
import { firstBattleBonds, firstBattleUnits, firstBattleVictoryPages, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import { defaultBattleScenario, secondBattleScenario } from '../data/battles';
|
||||
import { firstBattleBonds, firstBattleUnits, secondBattleIntroPages, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import {
|
||||
applyCampBondExp,
|
||||
getCampaignState,
|
||||
@@ -335,10 +335,11 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.blue, 0.48);
|
||||
|
||||
this.trackSortie(this.add.text(x + 18, y + 14, '다음 전장', this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 48, '황건 잔당 추격', this.textStyle(23, '#d8b15f', true))).setDepth(depth + 1);
|
||||
const briefing = this.nextSortieBriefing();
|
||||
this.trackSortie(this.add.text(x + 18, y + 14, briefing.eyebrow, this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 48, briefing.title, this.textStyle(23, '#d8b15f', true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 84, '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.', {
|
||||
this.add.text(x + 18, y + 84, briefing.description, {
|
||||
...this.textStyle(14, '#d4dce6'),
|
||||
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
||||
lineSpacing: 4
|
||||
@@ -397,10 +398,27 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setStrokeStyle(1, palette.gold, 0.4);
|
||||
const completedDialogues = this.completedCampDialogues().length;
|
||||
const inventory = this.inventoryLabels().join(', ');
|
||||
this.trackSortie(this.add.text(x + 16, y + 10, `예상 보상: 군자금, 소모품, 의용군 명성`, this.textStyle(13, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
const reward = this.campaign?.latestBattleId === secondBattleScenario.id ? '예상 보상: 다음 장 개방' : '예상 보상: 군자금, 소모품, 의용군 명성';
|
||||
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 === secondBattleScenario.id) {
|
||||
return {
|
||||
eyebrow: '다음 이야기',
|
||||
title: '넓어지는 전장',
|
||||
description: '황건 잔당 추격을 마친 의용군은 탁현을 넘어 더 큰 난세의 흐름을 마주합니다. 다음 장으로 향하기 전 전투 결과를 정리합니다.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
eyebrow: '다음 전장',
|
||||
title: secondBattleScenario.title,
|
||||
description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.'
|
||||
};
|
||||
}
|
||||
|
||||
private sortieChecklist(): SortieChecklistItem[] {
|
||||
const units = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const liuBei = units.find((unit) => unit.id === 'liu-bei');
|
||||
@@ -453,8 +471,17 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private startVictoryStory() {
|
||||
markCampaignStep('first-victory-story');
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
if (this.campaign?.latestBattleId === secondBattleScenario.id) {
|
||||
this.scene.start('StoryScene', { pages: secondBattleScenario.victoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
}
|
||||
|
||||
markCampaignStep('second-battle');
|
||||
this.scene.start('StoryScene', {
|
||||
pages: secondBattleIntroPages,
|
||||
nextScene: 'BattleScene',
|
||||
nextSceneData: { battleId: secondBattleScenario.id }
|
||||
});
|
||||
}
|
||||
|
||||
private hideSortiePrep() {
|
||||
|
||||
@@ -14,12 +14,14 @@ type AlphaObject = Phaser.GameObjects.Image | Phaser.GameObjects.Rectangle | Pha
|
||||
type StorySceneData = {
|
||||
pages?: StoryPage[];
|
||||
nextScene?: string;
|
||||
nextSceneData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export class StoryScene extends Phaser.Scene {
|
||||
private pageIndex = 0;
|
||||
private pages: StoryPage[] = prologuePages;
|
||||
private nextScene = 'BattleScene';
|
||||
private nextSceneData?: Record<string, unknown>;
|
||||
private transitioning = false;
|
||||
private background?: Phaser.GameObjects.Image;
|
||||
private chapterText?: Phaser.GameObjects.Text;
|
||||
@@ -37,6 +39,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
init(data?: StorySceneData) {
|
||||
this.pages = data?.pages?.length ? data.pages : prologuePages;
|
||||
this.nextScene = data?.nextScene ?? 'BattleScene';
|
||||
this.nextSceneData = data?.nextSceneData;
|
||||
this.pageIndex = 0;
|
||||
this.transitioning = false;
|
||||
}
|
||||
@@ -223,7 +226,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
if (this.pageIndex >= this.pages.length - 1) {
|
||||
this.scene.start(this.nextScene);
|
||||
this.scene.start(this.nextScene, this.nextSceneData);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import { firstBattleVictoryPages } from '../data/scenario';
|
||||
import { secondBattleScenario } from '../data/battles';
|
||||
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
|
||||
import { palette } from '../ui/palette';
|
||||
|
||||
@@ -298,7 +299,7 @@ export class TitleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
|
||||
const campaign = loadCampaignState();
|
||||
if (campaign.step === 'first-camp') {
|
||||
if (campaign.step === 'first-camp' || campaign.step === 'second-camp') {
|
||||
this.scene.start('CampScene');
|
||||
return;
|
||||
}
|
||||
@@ -308,6 +309,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'second-battle') {
|
||||
this.scene.start('BattleScene', { battleId: secondBattleScenario.id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaign.step === 'first-victory-story') {
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user