Add camp sortie preparation overlay
This commit is contained in:
@@ -265,6 +265,21 @@ try {
|
||||
throw new Error(`Expected campaign slot 1 to persist progress: ${JSON.stringify(campaignSlotAfterDialogue)}`);
|
||||
}
|
||||
|
||||
await page.mouse.click(1120, 38);
|
||||
await page.waitForTimeout(180);
|
||||
const campStateWithSortiePrep = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (!campStateWithSortiePrep?.sortieVisible) {
|
||||
throw new Error(`Expected sortie preparation overlay from next story button: ${JSON.stringify(campStateWithSortiePrep)}`);
|
||||
}
|
||||
await page.screenshot({ path: 'dist/verification-camp-sortie.png', fullPage: true });
|
||||
|
||||
await page.mouse.click(792, 596);
|
||||
await page.waitForTimeout(140);
|
||||
const campStateAfterSortieClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
if (campStateAfterSortieClose?.sortieVisible) {
|
||||
throw new Error(`Expected sortie preparation overlay to close: ${JSON.stringify(campStateAfterSortieClose)}`);
|
||||
}
|
||||
|
||||
await page.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||
await page.waitForFunction(() => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
|
||||
@@ -40,6 +40,12 @@ type CampSupplyDefinition = {
|
||||
bondExp: number;
|
||||
};
|
||||
|
||||
type SortieChecklistItem = {
|
||||
label: string;
|
||||
complete: boolean;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
const portraitKeys: Record<PortraitKey, string> = {
|
||||
liuBei: 'portrait-liu-bei',
|
||||
guanYu: 'portrait-guan-yu',
|
||||
@@ -118,7 +124,9 @@ export class CampScene extends Phaser.Scene {
|
||||
private selectedDialogueId = campDialogues[0].id;
|
||||
private contentObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private tabButtons: CampTabButtonView[] = [];
|
||||
private visitedTabs = new Set<CampTab>();
|
||||
|
||||
constructor() {
|
||||
super('CampScene');
|
||||
@@ -127,7 +135,9 @@ export class CampScene extends Phaser.Scene {
|
||||
create() {
|
||||
this.contentObjects = [];
|
||||
this.dialogueObjects = [];
|
||||
this.sortieObjects = [];
|
||||
this.tabButtons = [];
|
||||
this.visitedTabs = new Set(['status']);
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
||||
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
|
||||
@@ -197,8 +207,7 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
this.addCommandButton('다음 이야기', 1120, 38, 142, () => {
|
||||
soundDirector.playSelect();
|
||||
markCampaignStep('first-victory-story');
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
this.showSortiePrep();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,6 +268,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private render() {
|
||||
this.clearContent();
|
||||
this.visitedTabs.add(this.activeTab);
|
||||
this.updateTabButtons();
|
||||
this.renderUnitColumn();
|
||||
this.renderReportPanel();
|
||||
@@ -276,6 +286,196 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderStatusPanel();
|
||||
}
|
||||
|
||||
private showSortiePrep() {
|
||||
this.hideSortiePrep();
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||
|
||||
const depth = 40;
|
||||
const width = 900;
|
||||
const height = 560;
|
||||
const x = Math.floor((this.scale.width - width) / 2);
|
||||
const y = Math.floor((this.scale.height - height) / 2);
|
||||
|
||||
const shade = this.trackSortie(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.68));
|
||||
shade.setOrigin(0);
|
||||
shade.setDepth(depth);
|
||||
shade.setInteractive();
|
||||
|
||||
const panel = this.trackSortie(this.add.rectangle(x, y, width, height, 0x101820, 0.98));
|
||||
panel.setOrigin(0);
|
||||
panel.setDepth(depth + 1);
|
||||
panel.setStrokeStyle(3, palette.gold, 0.92);
|
||||
|
||||
const title = this.trackSortie(this.add.text(x + 34, y + 28, '출진 준비', this.textStyle(30, '#f2e3bf', true)));
|
||||
title.setDepth(depth + 2);
|
||||
const subtitle = this.trackSortie(
|
||||
this.add.text(x + 34, y + 70, '다음 전장으로 향하기 전 부대 상태와 준비 항목을 확인합니다.', this.textStyle(15, '#d4dce6'))
|
||||
);
|
||||
subtitle.setDepth(depth + 2);
|
||||
|
||||
this.renderSortieBriefing(x + 34, y + 112, 396, 152, depth + 2);
|
||||
this.renderSortieChecklist(x + 462, y + 112, 404, 170, depth + 2);
|
||||
this.renderSortieUnitSummary(x + 34, y + 292, 832, 148, depth + 2);
|
||||
this.renderSortieRewardHint(x + 34, y + 462, 548, 52, depth + 2);
|
||||
|
||||
this.addSortieButton('군영으로', x + width - 298, y + height - 44, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
this.hideSortiePrep();
|
||||
}, depth + 3);
|
||||
this.addSortieButton('출진', x + width - 152, y + height - 44, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
this.startVictoryStory();
|
||||
}, depth + 3);
|
||||
}
|
||||
|
||||
private renderSortieBriefing(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||
bg.setOrigin(0);
|
||||
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);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 84, '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.', {
|
||||
...this.textStyle(14, '#d4dce6'),
|
||||
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
||||
lineSpacing: 4
|
||||
})
|
||||
).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderSortieChecklist(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.5);
|
||||
this.trackSortie(this.add.text(x + 18, y + 14, '준비 체크', this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
|
||||
this.sortieChecklist().forEach((item, index) => {
|
||||
const rowY = y + 42 + index * 27;
|
||||
const color = item.complete ? '#a8ffd0' : '#ffdf7b';
|
||||
const status = item.complete ? '완료' : '주의';
|
||||
this.trackSortie(this.add.text(x + 18, rowY, status, this.textStyle(13, color, true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 70, rowY, item.label, this.textStyle(14, '#d4dce6', true))).setDepth(depth + 1);
|
||||
const detail = this.trackSortie(this.add.text(x + 190, rowY, item.detail, this.textStyle(12, item.complete ? '#9fb0bf' : '#d8b15f')));
|
||||
detail.setDepth(depth + 1);
|
||||
});
|
||||
}
|
||||
|
||||
private renderSortieUnitSummary(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||
bg.setOrigin(0);
|
||||
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.currentUnits()
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.forEach((unit, index) => {
|
||||
const rowY = y + 52 + index * 30;
|
||||
this.trackSortie(this.add.text(x + 18, rowY, `${unit.name} Lv ${unit.level}`, this.textStyle(14, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 132, rowY, unit.className, this.textStyle(13, '#9fb0bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 236, rowY, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(13, '#d4dce6', true))).setDepth(depth + 1);
|
||||
this.drawSortieBar(x + 338, rowY + 6, 128, 7, unit.hp / unit.maxHp, palette.green, depth + 1);
|
||||
|
||||
const equipment = equipmentSlots
|
||||
.map((slot) => {
|
||||
const state = unit.equipment[slot];
|
||||
return `${equipmentSlotLabels[slot]} Lv${state.level}`;
|
||||
})
|
||||
.join(' ');
|
||||
this.trackSortie(this.add.text(x + 496, rowY, equipment, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1);
|
||||
});
|
||||
}
|
||||
|
||||
private renderSortieRewardHint(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.9));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
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);
|
||||
this.trackSortie(this.add.text(x + 16, y + 30, `현재 준비: 대화 ${completedDialogues}/${campDialogues.length} · 보유 ${inventory || '없음'}`, this.textStyle(12, '#d4dce6'))).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private sortieChecklist(): SortieChecklistItem[] {
|
||||
const units = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const liuBei = units.find((unit) => unit.id === 'liu-bei');
|
||||
const injured = units.filter((unit) => unit.hp < unit.maxHp);
|
||||
const supplyCount = campSupplies.reduce((total, supply) => total + this.inventoryAmount(supply.label), 0);
|
||||
const completedDialogues = this.completedCampDialogues().length;
|
||||
return [
|
||||
{
|
||||
label: '유비 생존',
|
||||
complete: Boolean(liuBei && liuBei.hp > 0),
|
||||
detail: liuBei ? `병력 ${liuBei.hp}/${liuBei.maxHp}` : '유비 없음'
|
||||
},
|
||||
{
|
||||
label: '부상 장수 확인',
|
||||
complete: injured.length === 0,
|
||||
detail: injured.length === 0 ? '전원 완전한 병력' : `${injured.map((unit) => unit.name).join(', ')} 회복 권장`
|
||||
},
|
||||
{
|
||||
label: '소모품 보유',
|
||||
complete: supplyCount > 0,
|
||||
detail: supplyCount > 0 ? `보유 ${supplyCount}` : '정비 탭에서 보급 확인'
|
||||
},
|
||||
{
|
||||
label: '공명 대화',
|
||||
complete: completedDialogues >= campDialogues.length,
|
||||
detail: `${completedDialogues}/${campDialogues.length} 완료`
|
||||
},
|
||||
{
|
||||
label: '장비 상태',
|
||||
complete: this.visitedTabs.has('status'),
|
||||
detail: this.visitedTabs.has('status') ? '장수 탭 확인됨' : '장수 탭 확인 권장'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private addSortieButton(label: string, x: number, y: number, width: number, action: () => void, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, 36, 0x1a2630, 0.96));
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, label === '출진' ? palette.gold : palette.blue, 0.78);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
|
||||
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
|
||||
bg.on('pointerdown', action);
|
||||
|
||||
const text = this.trackSortie(this.add.text(x, y, label, this.textStyle(15, '#f2e3bf', true)));
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(depth + 1);
|
||||
text.setInteractive({ useHandCursor: true });
|
||||
text.on('pointerdown', action);
|
||||
}
|
||||
|
||||
private startVictoryStory() {
|
||||
markCampaignStep('first-victory-story');
|
||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
||||
}
|
||||
|
||||
private hideSortiePrep() {
|
||||
this.sortieObjects.forEach((object) => object.destroy());
|
||||
this.sortieObjects = [];
|
||||
}
|
||||
|
||||
private trackSortie<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||
this.sortieObjects.push(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
private drawSortieBar(x: number, y: number, width: number, height: number, ratio: number, color: number, depth: number) {
|
||||
const track = this.trackSortie(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
|
||||
track.setOrigin(0);
|
||||
track.setDepth(depth);
|
||||
const fill = this.trackSortie(this.add.rectangle(x, y, Math.max(2, width * Phaser.Math.Clamp(ratio, 0, 1)), height, color, 0.96));
|
||||
fill.setOrigin(0);
|
||||
fill.setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderUnitColumn() {
|
||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
allies.forEach((unit, index) => {
|
||||
@@ -838,6 +1038,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
activeTab: this.activeTab,
|
||||
sortieVisible: this.sortieObjects.length > 0,
|
||||
selectedUnitId: this.selectedUnitId,
|
||||
selectedDialogueId: this.selectedDialogueId,
|
||||
campaign: this.campaign
|
||||
|
||||
Reference in New Issue
Block a user