From 1da92efc88d89b966f60b8eb303425400e5a43d4 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 9 Jul 2026 12:01:26 +0900 Subject: [PATCH] Add camp equipment management tab --- src/game/scenes/CampScene.ts | 192 ++++++++++++++++++++++++++++++++--- 1 file changed, 178 insertions(+), 14 deletions(-) diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 1020ef3..c5abf9c 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -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(),