diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 692de11..48fe11f 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -4346,15 +4346,78 @@ export class BattleScene extends Phaser.Scene { return 0x9e3e58; } + private commandAccentColor(command: BattleCommand) { + if (command === 'attack') { + return 0xd8b15f; + } + if (command === 'strategy') { + return palette.blue; + } + if (command === 'item') { + return palette.green; + } + return 0x9fb0bf; + } + + private commandMenuInfo(unit: UnitData, command: BattleCommand) { + if (command === 'attack') { + const targets = this.damageableTargets(unit, 'attack'); + const target = targets[0]; + if (!target) { + return { + available: false, + status: '대상 없음', + detail: '현재 사거리 안 적 없음' + }; + } + + const preview = this.combatPreview(unit, target, 'attack'); + return { + available: true, + status: `대상 ${targets.length}`, + detail: `${target.name} · 피해 ${preview.damage} · ${preview.hitRate}%` + }; + } + + if (command === 'strategy' || command === 'item') { + const usables = this.availableUsables(unit, command); + if (usables.length === 0) { + return { + available: false, + status: command === 'strategy' ? '책략 없음' : '도구 없음', + detail: command === 'strategy' ? '사용 가능한 책략이 없습니다.' : '보유한 도구가 없습니다.' + }; + } + + const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) })); + const availableCount = forecasts.filter(({ forecast }) => forecast.available).length; + const firstReady = forecasts.find(({ forecast }) => forecast.available) ?? forecasts[0]; + return { + available: availableCount > 0, + status: availableCount > 0 ? `가능 ${availableCount}` : '대상 없음', + detail: + availableCount > 0 + ? `${firstReady.usable.name} · ${firstReady.forecast.valueLabel} ${firstReady.forecast.value}` + : `${firstReady.usable.name} · ${firstReady.forecast.status}` + }; + } + + return { + available: true, + status: '종료', + detail: '이 유닛 행동 종료' + }; + } + private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) { this.hideCommandMenu(); const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait']; - const menuWidth = 152; - const titleHeight = 31; - const buttonHeight = 34; - const gap = 5; - const padding = 8; + const menuWidth = 300; + const titleHeight = 46; + const buttonHeight = 58; + const gap = 7; + const padding = 10; const menuHeight = padding * 2 + titleHeight + commands.length * buttonHeight + (commands.length - 1) * gap; const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); @@ -4364,43 +4427,83 @@ export class BattleScene extends Phaser.Scene { panel.setDepth(14); this.commandMenuObjects.push(panel); - const title = this.add.text(left + menuWidth / 2, top + padding + 2, '명령', { + const title = this.add.text(left + 16, top + padding + 2, `${unit.name} 명령`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '16px', + fontSize: '18px', color: '#d8b15f', fontStyle: '700' }); - title.setOrigin(0.5, 0); title.setDepth(15); this.commandMenuObjects.push(title); + const subtitle = this.add.text(left + 16, top + padding + 26, `HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#aeb7c2', + fontStyle: '700' + }); + subtitle.setDepth(15); + this.commandMenuObjects.push(subtitle); + commands.forEach((command, index) => { const buttonTop = top + padding + titleHeight + index * (buttonHeight + gap); - const centerX = left + menuWidth / 2; + const accent = this.commandAccentColor(command); + const info = this.commandMenuInfo(unit, command); + const baseFill = info.available ? 0x1a2630 : 0x151c23; const centerY = buttonTop + buttonHeight / 2; - const background = this.add.rectangle(centerX, centerY, menuWidth - padding * 2, buttonHeight, 0x1a2630, 0.94); - background.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7); + const buttonLeft = left + padding; + const background = this.add.rectangle(buttonLeft, buttonTop, menuWidth - padding * 2, buttonHeight, baseFill, 0.95); + background.setOrigin(0); + background.setStrokeStyle(1, accent, info.available ? 0.74 : 0.46); background.setInteractive({ useHandCursor: true }); background.setDepth(15); - const iconFrame = this.add.rectangle(left + 28, centerY, 30, 30, 0x0a0f14, 0.86); - iconFrame.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.58); + const iconFrame = this.add.rectangle(left + 40, centerY, 42, 42, 0x0a0f14, 0.9); + iconFrame.setStrokeStyle(1, accent, info.available ? 0.72 : 0.44); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(left + 28, centerY, this.commandIcon(command, unit), 26); + const icon = this.createBattleUiIcon(left + 40, centerY, this.commandIcon(command, unit), 38); icon.setDepth(17); this.commandMenuObjects.push(icon); - const text = this.add.text(centerX + 13, centerY, commandLabels[command], { + const text = this.add.text(left + 68, buttonTop + 8, commandLabels[command], { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '18px', - color: '#f2e3bf', - fontStyle: '700' + fontSize: '19px', + color: info.available ? '#f2e3bf' : '#c8ced6', + fontStyle: '700', + fixedWidth: 86 }); - text.setOrigin(0.5); text.setDepth(16); + const detail = this.add.text(left + 68, buttonTop + 34, info.detail, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: info.available ? '#aeb7c2' : '#ffcf9a', + fixedWidth: menuWidth - 164 + }); + detail.setDepth(16); + this.commandMenuObjects.push(detail); + + const badgeLeft = left + menuWidth - 92; + const badge = this.add.rectangle(badgeLeft, buttonTop + 15, 74, 28, 0x0b1118, 0.94); + badge.setOrigin(0); + badge.setDepth(16); + badge.setStrokeStyle(1, info.available ? accent : 0xb64a45, info.available ? 0.62 : 0.74); + this.commandMenuObjects.push(badge); + + const badgeText = this.add.text(badgeLeft + 37, buttonTop + 21, info.status, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: info.available ? '#d4dce6' : '#ffcf9a', + fontStyle: '700', + fixedWidth: 70, + align: 'center' + }); + badgeText.setOrigin(0.5, 0); + badgeText.setDepth(17); + this.commandMenuObjects.push(badgeText); + const chooseCommand = (choicePointer: Phaser.Input.Pointer) => { if (choicePointer.rightButtonDown()) { this.cancelPendingMove(); @@ -4422,10 +4525,17 @@ export class BattleScene extends Phaser.Scene { this.completeUnitAction(unit, command); } }; - background.on('pointerover', () => background.setFillStyle(0x283947, 0.98)); - background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94)); + const showDetail = () => { + background.setFillStyle(info.available ? 0x283947 : 0x27303a, 0.98); + this.renderUnitDetail(unit, `${commandLabels[command]}\n${info.detail}`); + }; + const clearHover = () => background.setFillStyle(baseFill, 0.95); + background.on('pointerover', showDetail); + background.on('pointerout', clearHover); background.on('pointerdown', chooseCommand); text.setInteractive({ useHandCursor: true }); + text.on('pointerover', showDetail); + text.on('pointerout', clearHover); text.on('pointerdown', chooseCommand); this.commandButtons.push({ command, background, text }); @@ -4640,22 +4750,22 @@ export class BattleScene extends Phaser.Scene { } private renderUsableMenuPill(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, tone: number) { - const pill = this.add.rectangle(x, y, width, 28, 0x0b1118, 0.92); + const pill = this.add.rectangle(x, y, width, 30, 0x0b1118, 0.92); pill.setOrigin(0); pill.setDepth(16); pill.setStrokeStyle(1, tone, 0.54); this.commandMenuObjects.push(pill); - const iconImage = this.createBattleUiIcon(x + 15, y + 14, icon, 22); + const iconImage = this.createBattleUiIcon(x + 16, y + 15, icon, 24); iconImage.setDepth(17); this.commandMenuObjects.push(iconImage); - const text = this.add.text(x + 33, y + 6, label, { + const text = this.add.text(x + 35, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 38 + fixedWidth: width - 40 }); text.setDepth(17); this.commandMenuObjects.push(text); @@ -4680,56 +4790,71 @@ export class BattleScene extends Phaser.Scene { this.hideCommandMenu(); - const menuWidth = 384; - const titleHeight = 38; - const rowHeight = 96; - const padding = 9; + const menuWidth = 432; + const titleHeight = 52; + const rowHeight = 112; + const padding = 10; const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight; const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); - const titleLabel = command === 'strategy' ? `책략 선택 ${usables.length}` : `도구 선택 ${usables.length}`; + const accentColor = command === 'strategy' ? palette.blue : palette.gold; + const titleLabel = `${unit.name} · ${commandLabels[command]}`; const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.98); panel.setOrigin(0); - panel.setStrokeStyle(2, command === 'strategy' ? palette.blue : palette.gold, 0.78); + panel.setStrokeStyle(2, accentColor, 0.78); panel.setDepth(14); this.commandMenuObjects.push(panel); - const title = this.add.text(left + menuWidth / 2, top + padding + 2, titleLabel, { + const title = this.add.text(left + 16, top + padding + 2, titleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '17px', + fontSize: '19px', color: '#d8b15f', fontStyle: '700' }); - title.setOrigin(0.5, 0); title.setDepth(15); this.commandMenuObjects.push(title); + const subtitle = this.add.text(left + 16, top + padding + 29, `목록 ${usables.length} · HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#aeb7c2', + fontStyle: '700' + }); + subtitle.setDepth(15); + this.commandMenuObjects.push(subtitle); + usables.forEach((usable, index) => { const rowTop = top + padding + titleHeight + index * rowHeight; const accent = this.usableAccentColor(usable); const forecast = this.usableMenuForecast(unit, usable); - const bg = this.add.rectangle(left + 8, rowTop, menuWidth - 16, rowHeight - 8, 0x1a2630, 0.95); + const rowLeft = left + padding; + const rowWidth = menuWidth - padding * 2; + const contentLeft = left + 92; + const sideBoxLeft = left + menuWidth - 112; + const baseFill = forecast.available ? 0x1a2630 : 0x151c23; + const bg = this.add.rectangle(rowLeft, rowTop, rowWidth, rowHeight - 8, baseFill, 0.95); bg.setOrigin(0); bg.setDepth(15); bg.setStrokeStyle(1, forecast.available ? accent : 0x6c7480, forecast.available ? 0.72 : 0.52); bg.setInteractive({ useHandCursor: true }); this.commandMenuObjects.push(bg); - const iconFrame = this.add.rectangle(left + 42, rowTop + 44, 58, 58, 0x0a0f14, 0.9); + const iconFrame = this.add.rectangle(left + 52, rowTop + 52, 66, 66, 0x0a0f14, 0.9); iconFrame.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(left + 42, rowTop + 44, this.usableIcon(usable), 52); + const icon = this.createBattleUiIcon(left + 52, rowTop + 52, this.usableIcon(usable), 58); icon.setDepth(17); this.commandMenuObjects.push(icon); const stock = this.usableStockLabel(unit, usable); - const name = this.add.text(left + 78, rowTop + 9, usable.name, { + const name = this.add.text(contentLeft, rowTop + 9, usable.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '20px', + fontSize: '21px', color: forecast.available ? '#f2e3bf' : '#c8ced6', - fontStyle: '700' + fontStyle: '700', + fixedWidth: sideBoxLeft - contentLeft - 10 }); name.setDepth(16); this.commandMenuObjects.push(name); @@ -4745,51 +4870,52 @@ export class BattleScene extends Phaser.Scene { this.commandMenuObjects.push(stockText); const statusTone = forecast.available ? accent : 0xb64a45; - const sideBoxLeft = left + menuWidth - 94; - const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 34, 74, 24, 0x0b1118, 0.94); + const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 36, 92, 28, 0x0b1118, 0.94); statusBg.setOrigin(0); statusBg.setDepth(16); statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.62 : 0.76); this.commandMenuObjects.push(statusBg); - const statusText = this.add.text(sideBoxLeft + 37, rowTop + 40, forecast.status, { + const statusText = this.add.text(sideBoxLeft + 46, rowTop + 42, forecast.status, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '12px', color: forecast.available ? '#d4dce6' : '#ffcf9a', - fontStyle: '700' + fontStyle: '700', + fixedWidth: 88, + align: 'center' }); statusText.setOrigin(0.5, 0); statusText.setDepth(17); this.commandMenuObjects.push(statusText); const summary = this.add.text( - left + 78, - rowTop + 36, + contentLeft, + rowTop + 39, forecast.detail, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: forecast.available ? '#aeb7c2' : '#ffcf9a', - fixedWidth: menuWidth - 186 + fixedWidth: sideBoxLeft - contentLeft - 12 } ); summary.setDepth(16); this.commandMenuObjects.push(summary); - this.renderUsableMenuPill(left + 78, rowTop + 60, 90, this.usableIcon(usable), this.usableEffectLabel(usable), accent); - this.renderUsableMenuPill(left + 174, rowTop + 60, 104, this.usableTargetIcon(usable), `${this.usableTargetLabel(usable)} ${usable.range}칸`, accent); + this.renderUsableMenuPill(contentLeft, rowTop + 70, 96, this.usableIcon(usable), this.usableEffectLabel(usable), accent); + this.renderUsableMenuPill(contentLeft + 104, rowTop + 70, 116, this.usableTargetIcon(usable), `${this.usableTargetLabel(usable)} ${usable.range}칸`, accent); - const valueBox = this.add.rectangle(sideBoxLeft, rowTop + 58, 74, 30, 0x0b1118, 0.94); + const valueBox = this.add.rectangle(sideBoxLeft, rowTop + 68, 92, 34, 0x0b1118, 0.94); valueBox.setOrigin(0); valueBox.setDepth(16); valueBox.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.44); this.commandMenuObjects.push(valueBox); - const valueIcon = this.createBattleUiIcon(sideBoxLeft + 14, rowTop + 73, this.usablePowerIcon(usable), 22); + const valueIcon = this.createBattleUiIcon(sideBoxLeft + 16, rowTop + 85, this.usablePowerIcon(usable), 24); valueIcon.setDepth(17); this.commandMenuObjects.push(valueIcon); - const valueText = this.add.text(sideBoxLeft + 70, rowTop + 71, forecast.value, { + const valueText = this.add.text(sideBoxLeft + 86, rowTop + 83, forecast.value, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: forecast.available ? '#f2e3bf' : '#c8ced6', @@ -4799,7 +4925,7 @@ export class BattleScene extends Phaser.Scene { valueText.setDepth(17); this.commandMenuObjects.push(valueText); - const valueLabel = this.add.text(sideBoxLeft + 29, rowTop + 62, forecast.valueLabel, { + const valueLabel = this.add.text(sideBoxLeft + 34, rowTop + 73, forecast.valueLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '10px', color: '#9fb0bf', @@ -4818,13 +4944,21 @@ export class BattleScene extends Phaser.Scene { this.beginUsableTargeting(unit, usable); } }; - bg.on('pointerover', () => { + const showDetail = () => { bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98); this.renderUnitDetail(unit, this.usableDetailText(unit, usable)); + }; + const clearHover = () => bg.setFillStyle(baseFill, 0.95); + bg.on('pointerover', () => { + showDetail(); + }); + bg.on('pointerout', () => { + clearHover(); }); - bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.94)); bg.on('pointerdown', choose); name.setInteractive({ useHandCursor: true }); + name.on('pointerover', showDetail); + name.on('pointerout', clearHover); name.on('pointerdown', choose); }); }