Add camp equipment management tab
This commit is contained in:
@@ -118,7 +118,7 @@ import {
|
|||||||
import { palette } from '../ui/palette';
|
import { palette } from '../ui/palette';
|
||||||
import { startLazyScene } from './lazyScenes';
|
import { startLazyScene } from './lazyScenes';
|
||||||
|
|
||||||
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'progress';
|
type CampTab = 'status' | 'dialogue' | 'visit' | 'supplies' | 'equipment' | 'progress';
|
||||||
|
|
||||||
type CampDialogue = {
|
type CampDialogue = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11845,18 +11845,19 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderStaticButtons() {
|
private renderStaticButtons() {
|
||||||
this.addTabButton('장수', 'status', 654, 38);
|
this.addTabButton('장수', 'status', 576, 38);
|
||||||
this.addTabButton('대화', 'dialogue', 732, 38);
|
this.addTabButton('대화', 'dialogue', 650, 38);
|
||||||
this.addTabButton('방문', 'visit', 810, 38);
|
this.addTabButton('방문', 'visit', 724, 38);
|
||||||
this.addTabButton('정비', 'supplies', 888, 38);
|
this.addTabButton('보급', 'supplies', 798, 38);
|
||||||
this.addTabButton('연표', 'progress', 966, 38);
|
this.addTabButton('장비', 'equipment', 872, 38);
|
||||||
this.addCommandButton('저장', 1044, 38, 76, () => {
|
this.addTabButton('연표', 'progress', 946, 38);
|
||||||
|
this.addCommandButton('저장', 1030, 38, 76, () => {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
this.showCampSaveSlotPanel();
|
this.showCampSaveSlotPanel();
|
||||||
});
|
});
|
||||||
const flow = this.currentSortieFlow();
|
const flow = this.currentSortieFlow();
|
||||||
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '다음 이야기';
|
const storyButtonLabel = this.isFinalEpilogueFlow(flow) && this.campaign?.step === flow.campaignStep ? '엔딩 보기' : '다음 이야기';
|
||||||
this.addCommandButton(storyButtonLabel, 1160, 38, 142, () => {
|
this.addCommandButton(storyButtonLabel, 1152, 38, 142, () => {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
if (this.isFinalEpilogueFlow(flow)) {
|
if (this.isFinalEpilogueFlow(flow)) {
|
||||||
this.startVictoryStory();
|
this.startVictoryStory();
|
||||||
@@ -12234,6 +12235,11 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.activeTab === 'equipment') {
|
||||||
|
this.renderEquipmentPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.activeTab === 'progress') {
|
if (this.activeTab === 'progress') {
|
||||||
this.renderProgressPanel();
|
this.renderProgressPanel();
|
||||||
return;
|
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)));
|
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() {
|
private renderSuppliesPanel() {
|
||||||
const x = 394;
|
const x = 394;
|
||||||
const y = 120;
|
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}`));
|
const icon = this.track(this.add.image(x + 28, y + 29, `item-${item.id}`));
|
||||||
icon.setDisplaySize(30, 30);
|
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 + 11, this.compactText(item.name, 8), 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 + 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)));
|
const valueText = this.track(this.add.text(x + width - 12, y + 34, `${state.exp}/${next}`, this.textStyle(12, '#f0e4c8', true)));
|
||||||
valueText.setOrigin(1, 0);
|
valueText.setOrigin(1, 0);
|
||||||
this.drawBar(x + 58, y + 57, width - 74, 7, state.exp / next, isTreasure ? palette.gold : palette.blue);
|
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.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'),
|
...this.textStyle(11, '#c8d2dd'),
|
||||||
wordWrap: { width: width - 28, useAdvancedWrap: true }
|
wordWrap: { width: width - 28, useAdvancedWrap: true }
|
||||||
})
|
})
|
||||||
@@ -14994,13 +15149,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.dialogueObjects.forEach((object) => object.destroy());
|
this.dialogueObjects.forEach((object) => object.destroy());
|
||||||
this.dialogueObjects = [];
|
this.dialogueObjects = [];
|
||||||
const depth = this.sortieObjects.length > 0 ? 72 : 30;
|
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.setStrokeStyle(2, palette.gold, 0.84);
|
||||||
box.setDepth(depth);
|
box.setDepth(depth);
|
||||||
const text = this.add.text(this.scale.width / 2, 104, message, {
|
const text = this.add.text(this.scale.width / 2, 104, message, {
|
||||||
...this.textStyle(16, '#f2e3bf', true),
|
...this.textStyle(14, '#f2e3bf', true),
|
||||||
align: 'center',
|
align: 'center',
|
||||||
wordWrap: { width: 670, useAdvancedWrap: true }
|
wordWrap: { width: noticeWidth - 44, useAdvancedWrap: true }
|
||||||
});
|
});
|
||||||
text.setOrigin(0.5);
|
text.setOrigin(0.5);
|
||||||
text.setDepth(depth + 1);
|
text.setDepth(depth + 1);
|
||||||
@@ -15515,6 +15671,14 @@ export class CampScene extends Phaser.Scene {
|
|||||||
terrainLine: summary.terrainLine
|
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(),
|
sortiePlan: this.sortiePlanSummary(),
|
||||||
rosterCollection: this.rosterCollectionSummary(),
|
rosterCollection: this.rosterCollectionSummary(),
|
||||||
reserveTrainingAwards: this.latestReserveTrainingAwards(),
|
reserveTrainingAwards: this.latestReserveTrainingAwards(),
|
||||||
|
|||||||
Reference in New Issue
Block a user