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)}`);
|
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.evaluate(() => window.__HEROS_GAME__?.scene.start('TitleScene'));
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ type CampSupplyDefinition = {
|
|||||||
bondExp: number;
|
bondExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SortieChecklistItem = {
|
||||||
|
label: string;
|
||||||
|
complete: boolean;
|
||||||
|
detail: string;
|
||||||
|
};
|
||||||
|
|
||||||
const portraitKeys: Record<PortraitKey, string> = {
|
const portraitKeys: Record<PortraitKey, string> = {
|
||||||
liuBei: 'portrait-liu-bei',
|
liuBei: 'portrait-liu-bei',
|
||||||
guanYu: 'portrait-guan-yu',
|
guanYu: 'portrait-guan-yu',
|
||||||
@@ -118,7 +124,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private selectedDialogueId = campDialogues[0].id;
|
private selectedDialogueId = campDialogues[0].id;
|
||||||
private contentObjects: Phaser.GameObjects.GameObject[] = [];
|
private contentObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
|
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
|
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private tabButtons: CampTabButtonView[] = [];
|
private tabButtons: CampTabButtonView[] = [];
|
||||||
|
private visitedTabs = new Set<CampTab>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('CampScene');
|
super('CampScene');
|
||||||
@@ -127,7 +135,9 @@ export class CampScene extends Phaser.Scene {
|
|||||||
create() {
|
create() {
|
||||||
this.contentObjects = [];
|
this.contentObjects = [];
|
||||||
this.dialogueObjects = [];
|
this.dialogueObjects = [];
|
||||||
|
this.sortieObjects = [];
|
||||||
this.tabButtons = [];
|
this.tabButtons = [];
|
||||||
|
this.visitedTabs = new Set(['status']);
|
||||||
this.campaign = getCampaignState();
|
this.campaign = getCampaignState();
|
||||||
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
||||||
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
|
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, () => {
|
this.addCommandButton('다음 이야기', 1120, 38, 142, () => {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
markCampaignStep('first-victory-story');
|
this.showSortiePrep();
|
||||||
this.scene.start('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +268,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private render() {
|
private render() {
|
||||||
this.clearContent();
|
this.clearContent();
|
||||||
|
this.visitedTabs.add(this.activeTab);
|
||||||
this.updateTabButtons();
|
this.updateTabButtons();
|
||||||
this.renderUnitColumn();
|
this.renderUnitColumn();
|
||||||
this.renderReportPanel();
|
this.renderReportPanel();
|
||||||
@@ -276,6 +286,196 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.renderStatusPanel();
|
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() {
|
private renderUnitColumn() {
|
||||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||||
allies.forEach((unit, index) => {
|
allies.forEach((unit, index) => {
|
||||||
@@ -838,6 +1038,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return {
|
return {
|
||||||
scene: this.scene.key,
|
scene: this.scene.key,
|
||||||
activeTab: this.activeTab,
|
activeTab: this.activeTab,
|
||||||
|
sortieVisible: this.sortieObjects.length > 0,
|
||||||
selectedUnitId: this.selectedUnitId,
|
selectedUnitId: this.selectedUnitId,
|
||||||
selectedDialogueId: this.selectedDialogueId,
|
selectedDialogueId: this.selectedDialogueId,
|
||||||
campaign: this.campaign
|
campaign: this.campaign
|
||||||
|
|||||||
Reference in New Issue
Block a user