Add camp equipment management tab
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user