Add camp equipment management tab

This commit is contained in:
2026-07-09 12:01:26 +09:00
parent fd1e1ce495
commit 1da92efc88

View File

@@ -118,7 +118,7 @@ import {
import { palette } from '../ui/palette';
import { startLazyScene } from './lazyScenes';
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'progress';
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'equipment' | 'progress';
type CampDialogue = {
id: string;
@@ -11845,18 +11845,19 @@ export class CampScene extends Phaser.Scene {
}
private renderStaticButtons() {
this.addTabButton('장수', 'status', 654, 38);
this.addTabButton('대화', 'dialogue', 732, 38);
this.addTabButton('방문', 'visit', 810, 38);
this.addTabButton('정비', 'supplies', 888, 38);
this.addTabButton('연표', 'progress', 966, 38);
this.addCommandButton('저장', 1044, 38, 76, () => {
this.addTabButton('장수', 'status', 576, 38);
this.addTabButton('대화', 'dialogue', 650, 38);
this.addTabButton('방문', 'visit', 724, 38);
this.addTabButton('보급', 'supplies', 798, 38);
this.addTabButton('장비', 'equipment', 872, 38);
this.addTabButton('연표', 'progress', 946, 38);
this.addCommandButton('저장', 1030, 38, 76, () => {
soundDirector.playSelect();
this.showCampSaveSlotPanel();
});
const flow = this.currentSortieFlow();
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '다음 이야기';
this.addCommandButton(storyButtonLabel, 1160, 38, 142, () => {
this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
soundDirector.playSelect();
if (this.isFinalEpilogueFlow(flow)) {
this.startVictoryStory();
@@ -12234,6 +12235,11 @@ export class CampScene extends Phaser.Scene {
return;
}
if (this.activeTab === 'equipment') {
this.renderEquipmentPanel();
return;
}
if (this.activeTab === 'progress') {
this.renderProgressPanel();
return;
@@ -14552,6 +14558,155 @@ export class CampScene extends Phaser.Scene {
this.track(this.add.text(x + 14, y + 66, `제갈량 단서 ${insightCount} · 군자금 ${this.campaign?.gold ?? 0}`, this.textStyle(13, '#9fb0bf', true)));
}
private renderEquipmentPanel() {
const unit = this.selectedUnit();
if (!unit) {
return;
}
const x = 394;
const y = 120;
const width = 828;
const height = 464;
const bg = this.track(this.add.rectangle(x, y, width, height, 0x101820, 0.91));
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.gold, 0.58);
this.track(this.add.text(x + 24, y + 22, '장비 관리', this.textStyle(24, '#f2e3bf', true)));
this.track(this.add.text(x + 24, y + 58, '선택 장수의 무기·방어구·보조구 효과와 성장도를 비교하고 바로 교체합니다.', this.textStyle(14, '#d4dce6')));
this.renderEquipmentUnitSummary(unit, x + 24, y + 86, width - 48, 48);
equipmentSlots.forEach((slot, index) => {
this.renderEquipmentCard(unit, slot, x + 24 + index * 260, y + 150, 252, 96);
});
const inventoryEntries = this.equipmentInventoryEntries();
this.track(this.add.text(x + 24, y + 266, `보유 장비 ${inventoryEntries.length}`, this.textStyle(18, '#f2e3bf', true)));
this.track(this.add.text(x + 516, y + 268, '장비 효과 요약', this.textStyle(17, '#f2e3bf', true)));
if (inventoryEntries.length === 0) {
const empty = this.track(this.add.rectangle(x + 24, y + 298, 468, 86, 0x0d141c, 0.9));
empty.setOrigin(0);
empty.setStrokeStyle(1, palette.blue, 0.38);
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) => {
this.renderEquipmentCompareRow(entry, unit, x + 24, y + 298 + index * 40, 468, 36);
});
}
this.renderEquipmentEffectSummary(unit, x + 516, y + 298, 280, 128);
this.renderEquipmentInventorySummary(inventoryEntries, x + 516, y + 432, 280);
}
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);
bg.setStrokeStyle(1, palette.blue, 0.42);
const classInfo = getUnitClass(unit.classKey);
this.track(this.add.text(x + 14, y + 9, `${unit.name} Lv ${unit.level}`, this.textStyle(17, '#f2e3bf', true)));
this.track(this.add.text(x + 14, y + 29, `${unit.className} · ${classInfo.role}`, this.textStyle(11, '#9fb0bf', true)));
const stats = [
`공격 ${this.statWithBonus(unit.attack, this.equipmentAttackBonus(unit))}`,
`지력 ${this.statWithBonus(unit.stats.intelligence, this.equipmentStrategyBonus(unit))}`,
`통솔 ${this.statWithBonus(unit.stats.leadership, this.equipmentDefenseBonus(unit))}`,
`HP ${unit.hp}/${unit.maxHp}`
];
const statText = this.track(this.add.text(x + width - 14, y + 15, stats.join(' '), this.textStyle(13, '#d4dce6', true)));
statText.setOrigin(1, 0);
}
private renderEquipmentCompareRow(
entry: { label: string; amount: number; item: ItemDefinition },
unit: UnitData,
x: number,
y: number,
width: number,
height: number
) {
const currentItem = getItem(unit.equipment[entry.item.slot].itemId);
const sameItem = currentItem.id === entry.item.id;
const deltaScore = this.itemTotalBonus(entry.item) - this.itemTotalBonus(currentItem);
const canEquip = !sameItem;
const rowColor = sameItem ? 0x111922 : deltaScore > 0 ? 0x172a22 : deltaScore < 0 ? 0x241b1b : 0x151f2a;
const strokeColor = sameItem ? 0x53606c : entry.item.rank === 'treasure' ? palette.gold : deltaScore > 0 ? palette.green : palette.blue;
const bg = this.track(this.add.rectangle(x, y, width, height, rowColor, canEquip ? 0.94 : 0.72));
bg.setOrigin(0);
bg.setStrokeStyle(1, strokeColor, canEquip ? 0.58 : 0.3);
const iconFrame = this.track(this.add.rectangle(x + 20, y + height / 2, 28, 28, 0x0a1017, 0.92));
iconFrame.setStrokeStyle(1, entry.item.rank === 'treasure' ? palette.gold : 0x53606c, 0.54);
const icon = this.track(this.add.image(x + 20, y + height / 2, `item-${entry.item.id}`));
icon.setDisplaySize(22, 22);
icon.setAlpha(canEquip ? 1 : 0.52);
const nameColor = sameItem ? '#87919c' : entry.item.rank === 'treasure' ? '#f4dfad' : '#f2e3bf';
this.track(this.add.text(x + 42, y + 5, `${entry.item.name} x${entry.amount}`, this.textStyle(13, nameColor, true)));
this.track(this.add.text(x + 42, y + 22, `${equipmentSlotLabels[entry.item.slot]} · ${this.equipmentDeltaText(entry.item, currentItem)}`, this.textStyle(10, deltaScore > 0 ? '#a8ffd0' : deltaScore < 0 ? '#ff9d7d' : '#9fb0bf', true)));
this.track(this.add.text(x + 244, y + 7, this.compactText(entry.item.effects[0] ?? entry.item.description, 13), this.textStyle(10, '#c8d2dd')));
const button = this.track(this.add.rectangle(x + width - 34, y + height / 2, 52, 24, canEquip ? 0x1a2630 : 0x121922, canEquip ? 0.96 : 0.62));
button.setStrokeStyle(1, canEquip ? palette.gold : 0x53606c, canEquip ? 0.68 : 0.3);
const buttonText = this.track(this.add.text(x + width - 34, y + height / 2 - 7, canEquip ? '교체' : '착용', this.textStyle(11, canEquip ? '#f2e3bf' : '#87919c', true)));
buttonText.setOrigin(0.5, 0);
if (canEquip) {
const action = () => this.swapUnitEquipment(unit.id, entry.item.slot, entry.item.id);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => bg.setFillStyle(deltaScore > 0 ? 0x22342b : 0x283947, 0.98));
bg.on('pointerout', () => bg.setFillStyle(rowColor, 0.94));
bg.on('pointerdown', action);
button.setInteractive({ useHandCursor: true });
button.on('pointerdown', action);
buttonText.setInteractive({ useHandCursor: true });
buttonText.on('pointerdown', action);
}
}
private renderEquipmentEffectSummary(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);
bg.setStrokeStyle(1, palette.gold, 0.46);
this.track(this.add.text(x + 14, y + 12, '착용 효과', this.textStyle(15, '#f2e3bf', true)));
const equipped = equipmentSlots.map((slot) => ({ slot, state: unit.equipment[slot], item: getItem(unit.equipment[slot].itemId) }));
equipped.forEach((entry, index) => {
const rowY = y + 42 + index * 30;
const icon = this.track(this.add.image(x + 20, rowY + 8, `item-${entry.item.id}`));
icon.setDisplaySize(20, 20);
this.track(this.add.text(x + 36, rowY, `${equipmentSlotLabels[entry.slot]} Lv${entry.state.level}`, this.textStyle(11, '#d8b15f', true)));
this.track(this.add.text(x + 108, rowY, this.compactText(this.itemBonusText(entry.item), 18), this.textStyle(11, '#d4dce6', true)));
this.drawBar(x + 36, rowY + 17, width - 56, 5, entry.state.exp / equipmentExpToNext(entry.state.level), entry.item.rank === 'treasure' ? palette.gold : palette.blue);
});
}
private renderEquipmentInventorySummary(entries: { label: string; amount: number; item: ItemDefinition }[], x: number, y: number, width: number) {
const total = entries.reduce((sum, entry) => sum + entry.amount, 0);
const treasure = entries.filter((entry) => entry.item.rank === 'treasure').reduce((sum, entry) => sum + entry.amount, 0);
const bySlot = equipmentSlots
.map((slot) => `${equipmentSlotLabels[slot]} ${entries.filter((entry) => entry.item.slot === slot).reduce((sum, entry) => sum + entry.amount, 0)}`)
.join(' · ');
const bg = this.track(this.add.rectangle(x, y, width, 32, 0x151f2a, 0.82));
bg.setOrigin(0);
bg.setStrokeStyle(1, palette.blue, 0.34);
this.track(this.add.text(x + 14, y + 6, `창고 ${total}개 · 보물 ${treasure}`, this.textStyle(11, '#f2e3bf', true)));
this.track(this.add.text(x + 136, y + 6, this.compactText(bySlot, 24), this.textStyle(11, '#9fb0bf', true)));
}
private equipmentDeltaText(nextItem: ItemDefinition, currentItem: ItemDefinition) {
const rawDeltas: Array<[string, number]> = [
['공격', (nextItem.attackBonus ?? 0) - (currentItem.attackBonus ?? 0)],
['방어', (nextItem.defenseBonus ?? 0) - (currentItem.defenseBonus ?? 0)],
['책략', (nextItem.strategyBonus ?? 0) - (currentItem.strategyBonus ?? 0)]
];
const deltas = rawDeltas.filter(([, delta]) => delta !== 0);
return deltas.map(([label, delta]) => `${label}${delta > 0 ? '+' : ''}${delta}`).join(' / ') || '수치 변화 없음';
}
private renderSuppliesPanel() {
const x = 394;
const y = 120;
@@ -14845,8 +15000,8 @@ export class CampScene extends Phaser.Scene {
const icon = this.track(this.add.image(x + 28, y + 29, `item-${item.id}`));
icon.setDisplaySize(30, 30);
this.track(this.add.text(x + 58, y + 12, `${equipmentSlotLabels[slot]} · ${item.name}`, this.textStyle(14, isTreasure ? '#f4dfad' : '#d4dce6', true)));
this.track(this.add.text(x + 58, y + 34, `${isTreasure ? '보물' : '일반'} Lv ${state.level}`, this.textStyle(12, isTreasure ? '#d8b15f' : '#9fb0bf', true)));
this.track(this.add.text(x + 58, y + 11, this.compactText(item.name, 8), this.textStyle(14, isTreasure ? '#f4dfad' : '#d4dce6', true)));
this.track(this.add.text(x + 58, y + 34, `${equipmentSlotLabels[slot]} · ${isTreasure ? '보물' : '일반'} Lv ${state.level}`, this.textStyle(11, isTreasure ? '#d8b15f' : '#9fb0bf', true)));
const valueText = this.track(this.add.text(x + width - 12, y + 34, `${state.exp}/${next}`, this.textStyle(12, '#f0e4c8', true)));
valueText.setOrigin(1, 0);
this.drawBar(x + 58, y + 57, width - 74, 7, state.exp / next, isTreasure ? palette.gold : palette.blue);
@@ -14867,7 +15022,7 @@ export class CampScene extends Phaser.Scene {
}
this.track(
this.add.text(x + 14, y + 72, `${this.itemBonusText(item)} · ${item.effects[0] ?? item.description}`, {
this.add.text(x + 14, y + 74, this.compactText(`${this.itemBonusText(item)} · ${item.effects[0] ?? item.description}`, 27), {
...this.textStyle(11, '#c8d2dd'),
wordWrap: { width: width - 28, useAdvancedWrap: true }
})
@@ -14994,13 +15149,14 @@ export class CampScene extends Phaser.Scene {
this.dialogueObjects.forEach((object) => object.destroy());
this.dialogueObjects = [];
const depth = this.sortieObjects.length > 0 ? 72 : 30;
const box = this.add.rectangle(this.scale.width / 2, 104, 720, 58, 0x101820, 0.96);
const noticeWidth = Math.min(640, Math.max(360, message.length * 13));
const box = this.add.rectangle(this.scale.width / 2, 104, noticeWidth, 50, 0x101820, 0.96);
box.setStrokeStyle(2, palette.gold, 0.84);
box.setDepth(depth);
const text = this.add.text(this.scale.width / 2, 104, message, {
...this.textStyle(16, '#f2e3bf', true),
...this.textStyle(14, '#f2e3bf', true),
align: 'center',
wordWrap: { width: 670, useAdvancedWrap: true }
wordWrap: { width: noticeWidth - 44, useAdvancedWrap: true }
});
text.setOrigin(0.5);
text.setDepth(depth + 1);
@@ -15515,6 +15671,14 @@ export class CampScene extends Phaser.Scene {
terrainLine: summary.terrainLine
};
}),
equipmentInventory: this.equipmentInventoryEntries().map((entry) => ({
label: entry.label,
amount: entry.amount,
itemId: entry.item.id,
slot: entry.item.slot,
rank: entry.item.rank,
bonusText: this.itemBonusText(entry.item)
})),
sortiePlan: this.sortiePlanSummary(),
rosterCollection: this.rosterCollectionSummary(),
reserveTrainingAwards: this.latestReserveTrainingAwards(),