feat: refine battle feedback and early guidance

This commit is contained in:
2026-07-18 03:16:05 +09:00
parent 470516bfd9
commit af2f5dbf06
4 changed files with 650 additions and 87 deletions

View File

@@ -774,7 +774,7 @@ const campSupplies: CampSupplyDefinition[] = [
label: '탁주',
usableId: 'wine',
title: '탁주',
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
description: '선택 장수의 병력을 8 회복하고 연결된 모든 공명 경험치를 각각 2 올립니다.',
healHp: 8,
bondExp: 2
},
@@ -789,6 +789,7 @@ const campSupplies: CampSupplyDefinition[] = [
];
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
const equipmentInventoryPageSize = 4;
const merchantItems: MerchantItemDefinition[] = [
{
@@ -11234,6 +11235,15 @@ export class CampScene extends Phaser.Scene {
private activeTab: CampTab = 'status';
private selectedDialogueId = campDialogues[0].id;
private selectedVisitId = campVisits[0].id;
private equipmentInventoryPage = 0;
private equipmentPanelBackground?: Phaser.GameObjects.Rectangle;
private equipmentInventoryPreviousButton?: Phaser.GameObjects.Rectangle;
private equipmentInventoryNextButton?: Phaser.GameObjects.Rectangle;
private equipmentInventoryVisibleItemIds: string[] = [];
private suppliesPanelBackground?: Phaser.GameObjects.Rectangle;
private suppliesEquipmentBoxes: Phaser.GameObjects.Rectangle[] = [];
private suppliesTrophyBoxes: Phaser.GameObjects.Rectangle[] = [];
private reportPanelBackground?: Phaser.GameObjects.Rectangle;
private contentObjects: Phaser.GameObjects.GameObject[] = [];
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
@@ -19409,6 +19419,7 @@ export class CampScene extends Phaser.Scene {
const x = 42;
const y = 590;
const bg = this.track(this.add.rectangle(x, y, 1180, 84, 0x101820, 0.86));
this.reportPanelBackground = bg;
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.gold, 0.48);
this.track(this.add.text(x + 18, y + 14, '전투 보고', this.textStyle(18, '#f2e3bf', true)));
@@ -21470,6 +21481,9 @@ export class CampScene extends Phaser.Scene {
const width = 828;
const height = 464;
const bg = this.track(this.add.rectangle(x, y, width, height, 0x101820, 0.91));
this.equipmentPanelBackground = bg;
this.equipmentInventoryPreviousButton = undefined;
this.equipmentInventoryNextButton = undefined;
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.gold, 0.58);
@@ -21483,7 +21497,39 @@ export class CampScene extends Phaser.Scene {
});
const inventoryEntries = this.equipmentInventoryEntries();
this.track(this.add.text(x + 24, y + 266, `보유 장비 ${inventoryEntries.length}`, this.textStyle(18, '#f2e3bf', true)));
const inventoryPageCount = Math.max(1, Math.ceil(inventoryEntries.length / equipmentInventoryPageSize));
this.equipmentInventoryPage = Phaser.Math.Clamp(this.equipmentInventoryPage, 0, inventoryPageCount - 1);
const inventoryStart = this.equipmentInventoryPage * equipmentInventoryPageSize;
const visibleInventoryEntries = inventoryEntries.slice(inventoryStart, inventoryStart + equipmentInventoryPageSize);
this.equipmentInventoryVisibleItemIds = visibleInventoryEntries.map((entry) => entry.item.id);
this.track(this.add.text(
x + 24,
y + 266,
`보유 장비 ${inventoryEntries.length}${inventoryPageCount > 1 ? ` · ${this.equipmentInventoryPage + 1}/${inventoryPageCount}` : ''}`,
this.textStyle(18, '#f2e3bf', true)
));
if (inventoryPageCount > 1) {
this.equipmentInventoryPreviousButton = this.renderEquipmentInventoryPageButton(
x + 424,
y + 278,
'',
this.equipmentInventoryPage > 0,
() => {
this.equipmentInventoryPage -= 1;
this.render();
}
);
this.equipmentInventoryNextButton = this.renderEquipmentInventoryPageButton(
x + 462,
y + 278,
'',
this.equipmentInventoryPage < inventoryPageCount - 1,
() => {
this.equipmentInventoryPage += 1;
this.render();
}
);
}
this.track(this.add.text(x + 516, y + 268, '장비 효과 요약', this.textStyle(17, '#f2e3bf', true)));
if (inventoryEntries.length === 0) {
@@ -21493,7 +21539,7 @@ export class CampScene extends Phaser.Scene {
this.track(this.add.text(x + 44, y + 324, '교체 가능한 보유 장비가 없습니다.', this.textStyle(15, '#9fb0bf', true)));
this.track(this.add.text(x + 44, y + 352, '전투 보상이나 군영 상인을 통해 장비를 확보하면 이곳에서 비교할 수 있습니다.', this.textStyle(12, '#77818c')));
} else {
inventoryEntries.slice(0, 4).forEach((entry, index) => {
visibleInventoryEntries.forEach((entry, index) => {
this.renderEquipmentCompareRow(entry, unit, x + 24, y + 298 + index * 40, 468, 36);
});
}
@@ -21502,6 +21548,31 @@ export class CampScene extends Phaser.Scene {
this.renderEquipmentInventorySummary(inventoryEntries, x + 516, y + 432, 280);
}
private renderEquipmentInventoryPageButton(
x: number,
y: number,
label: string,
enabled: boolean,
action: () => void
) {
const bg = this.track(this.add.rectangle(x, y, 30, 24, enabled ? 0x1a2630 : 0x111820, enabled ? 0.96 : 0.68));
bg.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.72 : 0.34);
const text = this.track(this.add.text(x, y - 1, label, this.textStyle(18, enabled ? '#f2e3bf' : '#6f7882', true)));
text.setOrigin(0.5);
if (!enabled) {
return bg;
}
const setHovered = (hovered: boolean) => bg.setFillStyle(hovered ? 0x654c25 : 0x1a2630, hovered ? 0.99 : 0.96);
[bg, text].forEach((target) => {
target.setInteractive({ useHandCursor: true });
target.on('pointerover', () => setHovered(true));
target.on('pointerout', () => setHovered(false));
target.on('pointerdown', action);
});
return bg;
}
private renderEquipmentUnitSummary(unit: UnitData, x: number, y: number, width: number, height: number) {
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
bg.setOrigin(0);
@@ -21611,7 +21682,10 @@ export class CampScene extends Phaser.Scene {
private renderSuppliesPanel() {
const x = 394;
const y = 120;
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
const bg = this.track(this.add.rectangle(x, y, 828, 466, 0x101820, 0.9));
this.suppliesPanelBackground = bg;
this.suppliesEquipmentBoxes = [];
this.suppliesTrophyBoxes = [];
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.blue, 0.56);
this.track(this.add.text(x + 24, y + 22, '정비와 보급', this.textStyle(24, '#f2e3bf', true)));
@@ -21628,23 +21702,23 @@ export class CampScene extends Phaser.Scene {
this.renderMerchantPanel(x + 24, y + 202, 342);
this.track(this.add.text(x + 390, y + 318, '보유 장비', this.textStyle(18, '#f2e3bf', true)));
this.track(this.add.text(x + 390, y + 312, '보유 장비', this.textStyle(18, '#f2e3bf', true)));
const equipmentEntries = this.equipmentInventoryEntries();
if (equipmentEntries.length === 0) {
this.track(this.add.text(x + 390, y + 346, '교체 가능한 장비 없음', this.textStyle(12, '#9fb0bf')));
} else {
equipmentEntries.slice(0, 3).forEach((entry, index) => {
this.renderEquipmentInventoryBox(entry, x + 390 + index * 128, y + 344);
this.suppliesEquipmentBoxes.push(this.renderEquipmentInventoryBox(entry, x + 390 + index * 128, y + 336));
});
}
this.track(this.add.text(x + 390, y + 410, '전리품과 명성', this.textStyle(17, '#f2e3bf', true)));
this.track(this.add.text(x + 390, y + 394, '전리품과 명성', this.textStyle(17, '#f2e3bf', true)));
const trophies = this.nonEquipmentInventoryLabels();
if (trophies.length === 0) {
this.track(this.add.text(x + 390, y + 438, '장부/명성 전리품 없음', this.textStyle(12, '#9fb0bf')));
} else {
trophies.slice(0, 3).forEach((reward, index) => {
this.renderSupplyBox(reward, x + 390 + index * 128, y + 436);
this.suppliesTrophyBoxes.push(this.renderSupplyBox(reward, x + 390 + index * 128, y + 418));
});
}
}
@@ -21708,6 +21782,7 @@ export class CampScene extends Phaser.Scene {
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.gold, 0.52);
this.track(this.add.text(x + 10, y + 14, this.compactText(label, 9), this.textStyle(13, '#f2e3bf', true)));
return bg;
}
private renderEquipmentInventoryBox(entry: { label: string; amount: number; item: ItemDefinition }, x: number, y: number) {
@@ -21734,6 +21809,7 @@ export class CampScene extends Phaser.Scene {
actionText.setInteractive({ useHandCursor: true });
actionText.on('pointerdown', action);
}
return bg;
}
private renderSupplyTargetCard(unit: UnitData, x: number, y: number, width: number, height: number) {
@@ -21823,7 +21899,7 @@ export class CampScene extends Phaser.Scene {
soundDirector.playEffect('exp-gain', { volume: 0.24, stopAfterMs: 620 });
const hpText = hpGain > 0 ? `병력 +${hpGain}` : '';
const bondText = bondGain > 0 ? `공명 +${supply.bondExp}` : '';
const bondText = bondGain > 0 ? `연결 공명 총 +${bondGain}` : '';
this.showCampNotice(`${target.name} ${supply.title} 사용 · ${[hpText, bondText].filter(Boolean).join(' / ')}`);
this.render();
}
@@ -22052,7 +22128,8 @@ export class CampScene extends Phaser.Scene {
}
private showCampNotice(message: string) {
this.dialogueObjects.forEach((object) => object.destroy());
this.tweens.killTweensOf(this.dialogueObjects);
this.dialogueObjects.forEach((object) => object.active && object.destroy());
this.dialogueObjects = [];
const depth = this.sortieObjects.length > 0 ? 72 : 30;
const noticeWidth = Math.min(640, Math.max(360, message.length * 13));
@@ -22066,15 +22143,18 @@ export class CampScene extends Phaser.Scene {
}));
text.setOrigin(0.5);
text.setDepth(depth + 1);
this.dialogueObjects.push(box, text);
const noticeObjects: Phaser.GameObjects.GameObject[] = [box, text];
this.dialogueObjects = noticeObjects;
this.tweens.add({
targets: this.dialogueObjects,
targets: noticeObjects,
alpha: 0,
delay: 1300,
duration: 320,
onComplete: () => {
this.dialogueObjects.forEach((object) => object.destroy());
this.dialogueObjects = [];
noticeObjects.forEach((object) => object.active && object.destroy());
if (this.dialogueObjects === noticeObjects) {
this.dialogueObjects = [];
}
}
});
}
@@ -22610,6 +22690,14 @@ export class CampScene extends Phaser.Scene {
this.contentObjects.forEach((object) => object.destroy());
this.contentObjects = [];
this.campRosterLayout = undefined;
this.equipmentPanelBackground = undefined;
this.equipmentInventoryPreviousButton = undefined;
this.equipmentInventoryNextButton = undefined;
this.equipmentInventoryVisibleItemIds = [];
this.suppliesPanelBackground = undefined;
this.suppliesEquipmentBoxes = [];
this.suppliesTrophyBoxes = [];
this.reportPanelBackground = undefined;
this.reportFormationReviewToggleButton = undefined;
this.reportFormationHistoryToggleButton = undefined;
this.reportSortieOrderRewardBadge = undefined;
@@ -22728,6 +22816,8 @@ export class CampScene extends Phaser.Scene {
1,
Math.ceil(reportFormationHistoryRecords.length / reportFormationHistoryPageSize)
);
const equipmentInventory = this.equipmentInventoryEntries();
const equipmentInventoryPageCount = Math.max(1, Math.ceil(equipmentInventory.length / equipmentInventoryPageSize));
return {
scene: this.scene.key,
activeTab: this.activeTab,
@@ -23422,7 +23512,35 @@ export class CampScene extends Phaser.Scene {
strategyLine: summary.strategyLine
};
}),
equipmentInventory: this.equipmentInventoryEntries().map((entry) => ({
secondaryPanels: {
report: {
visible: Boolean(this.reportPanelBackground?.active),
bounds: this.sortieObjectBoundsDebug(this.reportPanelBackground)
},
supplies: {
visible: this.activeTab === 'supplies' && Boolean(this.suppliesPanelBackground?.active),
bounds: this.sortieObjectBoundsDebug(this.suppliesPanelBackground),
equipmentBoxBounds: this.suppliesEquipmentBoxes
.map((box) => this.sortieObjectBoundsDebug(box))
.filter((bounds) => bounds !== null),
trophyBoxBounds: this.suppliesTrophyBoxes
.map((box) => this.sortieObjectBoundsDebug(box))
.filter((bounds) => bounds !== null)
},
equipment: {
visible: this.activeTab === 'equipment' && Boolean(this.equipmentPanelBackground?.active),
bounds: this.sortieObjectBoundsDebug(this.equipmentPanelBackground),
page: this.equipmentInventoryPage,
pageCount: equipmentInventoryPageCount,
pageSize: equipmentInventoryPageSize,
visibleItemIds: [...this.equipmentInventoryVisibleItemIds],
previousButtonBounds: this.sortieObjectBoundsDebug(this.equipmentInventoryPreviousButton),
nextButtonBounds: this.sortieObjectBoundsDebug(this.equipmentInventoryNextButton),
previousEnabled: Boolean(this.equipmentInventoryPreviousButton?.input?.enabled),
nextEnabled: Boolean(this.equipmentInventoryNextButton?.input?.enabled)
}
},
equipmentInventory: equipmentInventory.map((entry) => ({
label: entry.label,
amount: entry.amount,
itemId: entry.item.id,