diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 7c122fe..a697bd0 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -4231,26 +4231,94 @@ export class BattleScene extends Phaser.Scene { } private usableAccentColor(usable: BattleUsable) { - if (usable.command === 'item') { - return palette.gold; - } - if (usable.effect === 'heal') { - return palette.green; - } - if (usable.effect === 'focus') { - return palette.blue; - } - return 0xb64a45; + return this.usableVisualStyle(usable).accent; } private usableEffectLabel(usable: BattleUsable) { + return this.usableVisualStyle(usable).effectLabel; + } + + private usableChannelLabel(usable: BattleUsable) { + return usable.command === 'item' ? '도구' : '책략'; + } + + private usableTargetIcon(usable: BattleUsable): BattleUiIconKey { + if (usable.target === 'self') { + return 'counter'; + } + if (usable.target === 'ally') { + return 'leadership'; + } + return 'hit'; + } + + private usablePowerIcon(usable: BattleUsable): BattleUiIconKey { + return this.usableVisualStyle(usable).powerIcon; + } + + private usableVisualStyle(usable: BattleUsable) { + const id = usable.id.toLowerCase(); + if (usable.command === 'item') { + if (usable.effect === 'heal') { + return { + icon: 'heal' as BattleUiIconKey, + powerIcon: 'heal' as BattleUiIconKey, + accent: palette.gold, + effectLabel: '회복', + powerLabel: `회복 ${usable.power}+` + }; + } + return { + icon: 'accessory' as BattleUiIconKey, + powerIcon: 'focus' as BattleUiIconKey, + accent: palette.gold, + effectLabel: '강화', + powerLabel: `${usable.duration ?? 1}턴 강화` + }; + } if (usable.effect === 'heal') { - return '회복'; + return { + icon: 'heal' as BattleUiIconKey, + powerIcon: 'heal' as BattleUiIconKey, + accent: palette.green, + effectLabel: '회복', + powerLabel: `회복 ${usable.power}+` + }; } if (usable.effect === 'focus') { - return '강화'; + return { + icon: 'focus' as BattleUiIconKey, + powerIcon: 'leadership' as BattleUiIconKey, + accent: palette.blue, + effectLabel: '격려', + powerLabel: `${usable.duration ?? 1}턴 강화` + }; } - return '피해'; + if (id.includes('fire')) { + return { + icon: 'fire' as BattleUiIconKey, + powerIcon: 'fire' as BattleUiIconKey, + accent: 0xd8732c, + effectLabel: '화염', + powerLabel: `화염 ${usable.power}` + }; + } + if (id.includes('roar') || id.includes('shout')) { + return { + icon: 'shout' as BattleUiIconKey, + powerIcon: 'shout' as BattleUiIconKey, + accent: 0xc96b4a, + effectLabel: '위압', + powerLabel: `위압 ${usable.power}` + }; + } + return { + icon: 'strategy' as BattleUiIconKey, + powerIcon: 'strategy' as BattleUiIconKey, + accent: 0xb64a45, + effectLabel: '피해', + powerLabel: `위력 ${usable.power}` + }; } private usableTargetLabel(usable: BattleUsable) { @@ -4264,13 +4332,7 @@ export class BattleScene extends Phaser.Scene { } private usablePowerLabel(usable: BattleUsable) { - if (usable.effect === 'heal') { - return `회복 ${usable.power}+`; - } - if (usable.effect === 'focus') { - return `${usable.duration ?? 1}턴 강화`; - } - return `위력 ${usable.power}`; + return this.usableVisualStyle(usable).powerLabel; } private usableTargetCount(user: UnitData, usable: BattleUsable) { @@ -4287,7 +4349,7 @@ export class BattleScene extends Phaser.Scene { private usableDetailText(user: UnitData, usable: BattleUsable) { const targetCount = this.usableTargetCount(user, usable); return [ - `${usable.name} / ${this.usableEffectLabel(usable)} / ${this.usableTargetLabel(usable)}`, + `${usable.name} / ${this.usableChannelLabel(usable)} / ${this.usableEffectLabel(usable)} / ${this.usableTargetLabel(usable)}`, `${this.usablePowerLabel(usable)} · 사거리 ${usable.range} · 대상 ${targetCount}명`, usable.description ].join('\n'); @@ -4400,7 +4462,7 @@ export class BattleScene extends Phaser.Scene { const summary = this.add.text( left + 68, rowTop + 34, - `${this.usableEffectLabel(usable)} · ${this.usableTargetLabel(usable)} · 사거리 ${usable.range} · 대상 ${this.usableTargetCount(unit, usable)}명`, + `${this.usableChannelLabel(usable)} · ${this.usableEffectLabel(usable)} · ${this.usableTargetLabel(usable)} · 대상 ${this.usableTargetCount(unit, usable)}명`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', @@ -4412,8 +4474,8 @@ export class BattleScene extends Phaser.Scene { this.commandMenuObjects.push(summary); this.renderUsableMenuPill(left + 68, rowTop + 49, 82, this.usableIcon(usable), this.usableEffectLabel(usable), accent); - this.renderUsableMenuPill(left + 156, rowTop + 49, 90, 'move', `거리 ${usable.range}`, accent); - this.renderUsableMenuPill(left + 252, rowTop + 49, 86, usable.target === 'enemy' ? 'hit' : 'success', this.usablePowerLabel(usable), accent); + this.renderUsableMenuPill(left + 156, rowTop + 49, 90, this.usableTargetIcon(usable), `${this.usableTargetLabel(usable)} ${usable.range}칸`, accent); + this.renderUsableMenuPill(left + 252, rowTop + 49, 86, this.usablePowerIcon(usable), this.usablePowerLabel(usable), accent); const choose = (choicePointer: Phaser.Input.Pointer) => { if (choicePointer.rightButtonDown()) { @@ -4799,6 +4861,9 @@ export class BattleScene extends Phaser.Scene { } private previewAccentColor(preview: CombatPreview) { + if (preview.usable) { + return this.usableAccentColor(preview.usable); + } if (preview.action === 'strategy') { return palette.blue; } @@ -4812,6 +4877,23 @@ export class BattleScene extends Phaser.Scene { return preview.action === 'strategy' ? '성공' : '명중'; } + private previewDamageIcon(preview: CombatPreview): BattleUiIconKey { + return preview.usable ? this.usablePowerIcon(preview.usable) : 'attack'; + } + + private previewActionTypeLabel(preview: CombatPreview) { + if (preview.usable) { + return this.usableEffectLabel(preview.usable); + } + if (preview.action === 'attack') { + return '무기 공격'; + } + if (preview.action === 'strategy') { + return '책략'; + } + return '도구'; + } + private renderCombatPreviewCard(preview: CombatPreview) { const { panelX, panelWidth } = this.layout; const left = panelX + 24; @@ -4849,7 +4931,7 @@ export class BattleScene extends Phaser.Scene { const metricTop = y + 86; const metricGap = 6; const metricWidth = (width - 20 - metricGap * 2) / 3; - this.renderPreviewMetric(left + 10, metricTop, metricWidth, 'attack', '피해', `${preview.damage}`, 0xb64a45); + this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), '피해', `${preview.damage}`, 0xb64a45); this.renderPreviewMetric( left + 10 + metricWidth + metricGap, metricTop, @@ -4866,10 +4948,9 @@ export class BattleScene extends Phaser.Scene { const badgeY = y + 150; const maxBadgeRight = left + width - 10; let badgeX = left + 10; + badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), maxBadgeRight); + badgeX = this.renderPreviewBadge(badgeX, badgeY, preview.action === 'strategy' ? 'success' : 'hit', `${preview.hitRate}% ${this.previewSuccessLabel(preview)}`, maxBadgeRight); badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight); - if (preview.action === 'strategy') { - badgeX = this.renderPreviewBadge(badgeX, badgeY, 'success', `${preview.hitRate}% 성공`, maxBadgeRight); - } badgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight); if (preview.counterAvailable) { badgeX = this.renderPreviewBadge(badgeX, badgeY, 'counter', '반격 가능', maxBadgeRight); @@ -4913,11 +4994,11 @@ export class BattleScene extends Phaser.Scene { const metricGap = 6; const metricWidth = (width - 20 - metricGap * 2) / 3; if (preview.usable.effect === 'heal') { - this.renderPreviewMetric(left + 10, metricTop, metricWidth, 'hp', '회복', `+${preview.healAmount}`, 0x59d18c); + this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '회복', `+${preview.healAmount}`, 0x59d18c); this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent); - this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'move', '거리', `${preview.usable.range}`, accent); + this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, this.usableTargetIcon(preview.usable), '대상', `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}`, accent); } else { - this.renderPreviewMetric(left + 10, metricTop, metricWidth, 'attack', '공격', `+${preview.attackBonus}`, 0xd8b15f); + this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '강화', `+${preview.attackBonus}`, 0xd8b15f); this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'hit', '명중', `+${preview.hitBonus}`, accent); this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `+${preview.criticalBonus}`, 0xd8732c); } @@ -4928,8 +5009,8 @@ export class BattleScene extends Phaser.Scene { const maxBadgeRight = left + width - 10; let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableIcon(preview.usable), this.usableEffectLabel(preview.usable), maxBadgeRight); - badgeX = this.renderPreviewBadge(badgeX, badgeY, 'move', `사거리 ${preview.usable.range}`, maxBadgeRight); - badgeX = this.renderPreviewBadge(badgeX, badgeY, 'success', preview.usable.command === 'item' ? '도구 사용' : '책략 사용', maxBadgeRight); + badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableTargetIcon(preview.usable), `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}칸`, maxBadgeRight); + badgeX = this.renderPreviewBadge(badgeX, badgeY, preview.usable.command === 'item' ? 'accessory' : 'strategy', this.usableChannelLabel(preview.usable), maxBadgeRight); if (preview.usable.effect === 'focus') { this.renderPreviewBadge(badgeX, badgeY, 'leadership', `${preview.duration}턴`, maxBadgeRight); } @@ -5019,7 +5100,7 @@ export class BattleScene extends Phaser.Scene { row.setOrigin(0); row.setStrokeStyle(1, accent, 0.48); const isHeal = preview.usable.effect === 'heal'; - this.trackSideIcon(x + 14, y + 12, isHeal ? 'heal' : 'focus', 20); + this.trackSideIcon(x + 14, y + 12, this.usablePowerIcon(preview.usable), 20); this.trackSideObject(this.add.text(x + 26, y + 5, isHeal ? '적용 후 HP' : '강화 결과', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', @@ -5054,7 +5135,7 @@ export class BattleScene extends Phaser.Scene { } private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) { - const width = Phaser.Math.Clamp(label.length * 8 + 48, 76, 146); + const width = Phaser.Math.Clamp(label.length * 7 + 44, 68, 136); if (maxRight && x + width > maxRight) { return x; } @@ -7987,10 +8068,11 @@ export class BattleScene extends Phaser.Scene { const outcomeIcon = result.action === 'strategy' ? 'success' : result.critical ? 'critical' : 'hit'; const outcomeLabel = result.action === 'strategy' ? (result.hit ? '성공' : '실패') : result.hit ? (result.critical ? '치명' : '명중') : '회피'; - this.renderCombatRibbonBadge(x + 46, y + 38, 'advantage', result.matchupLabel, depth + 1, 78); + this.renderCombatRibbonBadge(x + 46, y + 38, this.actionIcon(result), this.previewActionTypeLabel(result), depth + 1, 78); this.renderCombatRibbonBadge(x + 130, y + 38, outcomeIcon, outcomeLabel, depth + 1, 62); + this.renderCombatRibbonBadge(x + 198, y + 38, 'advantage', result.matchupLabel, depth + 1, 76); if (result.counter) { - this.renderCombatRibbonBadge(x + 198, y + 38, 'counter', '반격', depth + 1, 58); + this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '반격', depth + 1, 58); } } @@ -8145,7 +8227,7 @@ export class BattleScene extends Phaser.Scene { const targetFrame = this.trackCombatObject(this.add.rectangle(x + 256, y + 22, 34, 34, 0x16212d, 0.9)); targetFrame.setDepth(depth + 1); targetFrame.setStrokeStyle(1, 0x647485, 0.58); - this.trackCombatIcon(x + 256, y + 22, result.usable.effect === 'heal' ? 'heal' : 'focus', 30, depth + 2); + this.trackCombatIcon(x + 256, y + 22, this.usablePowerIcon(result.usable), 30, depth + 2); const actionLabel = this.trackCombatObject(this.add.text(x + 46, y + 9, result.usable.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -8201,7 +8283,7 @@ export class BattleScene extends Phaser.Scene { this.renderCombatRibbonBadge(x + 46, y + 38, this.usableIcon(result.usable), this.usableEffectLabel(result.usable), depth + 1, 68); this.renderCombatRibbonBadge(x + 120, y + 38, 'success', '적용', depth + 1, 58); - this.renderCombatRibbonBadge(x + 184, y + 38, 'move', `거리 ${result.usable.range}`, depth + 1, 66); + this.renderCombatRibbonBadge(x + 184, y + 38, this.usableTargetIcon(result.usable), `${this.usableTargetLabel(result.usable)} ${result.usable.range}칸`, depth + 1, 78); } private renderCombatStatusPanel(unit: UnitData, x: number, y: number, width: number, growth?: CharacterGrowthResult) { @@ -11244,7 +11326,8 @@ export class BattleScene extends Phaser.Scene { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: isTreasure ? '#f4dfad' : '#d4dce6', - fontStyle: isTreasure ? '700' : '400' + fontStyle: isTreasure ? '700' : '400', + fixedWidth: width - 214 })); const bonusText = this.itemBonusText(item); @@ -11348,22 +11431,7 @@ export class BattleScene extends Phaser.Scene { } private usableIcon(usable: BattleUsable): BattleUiIconKey { - if (usable.command === 'item') { - return usable.effect === 'heal' ? 'heal' : 'accessory'; - } - if (usable.effect === 'heal') { - return 'heal'; - } - if (usable.effect === 'focus') { - return 'focus'; - } - if (usable.id === 'fireTactic') { - return 'fire'; - } - if (usable.id.toLowerCase().includes('roar')) { - return 'shout'; - } - return 'strategy'; + return this.usableVisualStyle(usable).icon; } private actionIcon(preview: CombatPreview): BattleUiIconKey { @@ -11378,9 +11446,9 @@ export class BattleScene extends Phaser.Scene { private itemBonusText(item: ReturnType) { const parts = [ - item.attackBonus ? `공+${item.attackBonus}` : '', - item.defenseBonus ? `방+${item.defenseBonus}` : '', - item.strategyBonus ? `책+${item.strategyBonus}` : '' + item.attackBonus ? `공격+${item.attackBonus}` : '', + item.defenseBonus ? `방어+${item.defenseBonus}` : '', + item.strategyBonus ? `책략+${item.strategyBonus}` : '' ].filter(Boolean); return parts.length > 0 ? parts.join(' ') : '-'; }