From e5706dc71f963a0bd07f076343a9fe59ff53d4f5 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 2 Jul 2026 20:25:45 +0900 Subject: [PATCH] Improve strategy and item UI clarity --- src/game/scenes/BattleScene.ts | 376 +++++++++++++++++++++++++++++---- 1 file changed, 337 insertions(+), 39 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index cfcaf9b..66734dd 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1327,6 +1327,18 @@ type BattleUsable = { description: string; }; +type SupportPreview = { + usable: BattleUsable; + user: UnitData; + target: UnitData; + healAmount: number; + projectedHp: number; + attackBonus: number; + hitBonus: number; + criticalBonus: number; + duration: number; +}; + type SupportResult = { usable: BattleUsable; user: UnitData; @@ -4218,6 +4230,91 @@ 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; + } + + private usableEffectLabel(usable: BattleUsable) { + if (usable.effect === 'heal') { + return '회복'; + } + if (usable.effect === 'focus') { + return '강화'; + } + return '피해'; + } + + private usableTargetLabel(usable: BattleUsable) { + if (usable.target === 'self') { + return '자신'; + } + if (usable.target === 'ally') { + return '아군'; + } + return '적군'; + } + + private usablePowerLabel(usable: BattleUsable) { + if (usable.effect === 'heal') { + return `회복 ${usable.power}+`; + } + if (usable.effect === 'focus') { + return `${usable.duration ?? 1}턴 강화`; + } + return `위력 ${usable.power}`; + } + + private usableTargetCount(user: UnitData, usable: BattleUsable) { + if (usable.effect === 'damage') { + return this.damageableTargets(user, usable.command, usable).length; + } + return this.supportTargets(user, usable).length; + } + + private usableStockLabel(user: UnitData, usable: BattleUsable) { + return usable.command === 'item' ? `보유 ${this.itemStock(user.id, usable.id)}` : '책략'; + } + + private usableDetailText(user: UnitData, usable: BattleUsable) { + const targetCount = this.usableTargetCount(user, usable); + return [ + `${usable.name} / ${this.usableEffectLabel(usable)} / ${this.usableTargetLabel(usable)}`, + `${this.usablePowerLabel(usable)} · 사거리 ${usable.range} · 대상 ${targetCount}명`, + usable.description + ].join('\n'); + } + + private renderUsableMenuPill(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, tone: number) { + const pill = this.add.rectangle(x, y, width, 22, 0x0b1118, 0.92); + pill.setOrigin(0); + pill.setDepth(16); + pill.setStrokeStyle(1, tone, 0.54); + this.commandMenuObjects.push(pill); + + const iconImage = this.createBattleUiIcon(x + 12, y + 11, icon, 16); + iconImage.setDepth(17); + this.commandMenuObjects.push(iconImage); + + const text = this.add.text(x + 25, y + 5, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#d4dce6', + fontStyle: '700', + fixedWidth: width - 29 + }); + text.setDepth(17); + this.commandMenuObjects.push(text); + } + private hideCommandMenu() { this.commandMenuObjects.forEach((object) => object.destroy()); this.commandMenuObjects = []; @@ -4237,12 +4334,13 @@ export class BattleScene extends Phaser.Scene { this.hideCommandMenu(); - const menuWidth = 276; - const titleHeight = 34; - const rowHeight = 58; + const menuWidth = 322; + const titleHeight = 36; + const rowHeight = 72; const padding = 9; const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight; const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); + const titleLabel = command === 'strategy' ? '책략 선택' : '도구 선택'; const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.98); panel.setOrigin(0); @@ -4250,9 +4348,9 @@ export class BattleScene extends Phaser.Scene { panel.setDepth(14); this.commandMenuObjects.push(panel); - const title = this.add.text(left + menuWidth / 2, top + padding + 2, commandLabels[command], { + const title = this.add.text(left + menuWidth / 2, top + padding + 2, titleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '16px', + fontSize: '17px', color: '#d8b15f', fontStyle: '700' }); @@ -4262,40 +4360,60 @@ export class BattleScene extends Phaser.Scene { usables.forEach((usable, index) => { const rowTop = top + padding + titleHeight + index * rowHeight; - const bg = this.add.rectangle(left + 8, rowTop, menuWidth - 16, rowHeight - 6, 0x1a2630, 0.94); + const accent = this.usableAccentColor(usable); + const bg = this.add.rectangle(left + 8, rowTop, menuWidth - 16, rowHeight - 7, 0x1a2630, 0.95); bg.setOrigin(0); bg.setDepth(15); - bg.setStrokeStyle(1, command === 'strategy' ? palette.blue : palette.gold, 0.66); + bg.setStrokeStyle(1, accent, 0.68); bg.setInteractive({ useHandCursor: true }); this.commandMenuObjects.push(bg); - const iconFrame = this.add.rectangle(left + 28, rowTop + 25, 32, 32, 0x0a0f14, 0.88); - iconFrame.setStrokeStyle(1, command === 'strategy' ? palette.blue : palette.gold, 0.58); + const iconFrame = this.add.rectangle(left + 31, rowTop + 32, 38, 38, 0x0a0f14, 0.9); + iconFrame.setStrokeStyle(1, accent, 0.66); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(left + 28, rowTop + 25, this.usableIcon(usable), 27); + const icon = this.createBattleUiIcon(left + 31, rowTop + 32, this.usableIcon(usable), 32); icon.setDepth(17); this.commandMenuObjects.push(icon); - const stock = command === 'item' ? ` x${this.itemStock(unit.id, usable.id)}` : ''; - const name = this.add.text(left + 50, rowTop + 7, `${usable.name}${stock}`, { + const stock = this.usableStockLabel(unit, usable); + const name = this.add.text(left + 56, rowTop + 7, usable.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '16px', + fontSize: '17px', color: '#f2e3bf', fontStyle: '700' }); name.setDepth(16); this.commandMenuObjects.push(name); - const desc = this.add.text(left + 50, rowTop + 29, usable.description, { + const stockText = this.add.text(left + menuWidth - 24, rowTop + 9, stock, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '12px', - color: '#aeb7c2', - wordWrap: { width: menuWidth - 66, useAdvancedWrap: true } + fontSize: '11px', + color: command === 'item' ? '#f4dfad' : '#9fb0bf', + fontStyle: '700' }); - desc.setDepth(16); - this.commandMenuObjects.push(desc); + stockText.setOrigin(1, 0); + stockText.setDepth(16); + this.commandMenuObjects.push(stockText); + + const summary = this.add.text( + left + 56, + rowTop + 31, + `${this.usableEffectLabel(usable)} · ${this.usableTargetLabel(usable)} · 사거리 ${usable.range} · 대상 ${this.usableTargetCount(unit, usable)}명`, + { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#aeb7c2', + fixedWidth: menuWidth - 74 + } + ); + summary.setDepth(16); + this.commandMenuObjects.push(summary); + + this.renderUsableMenuPill(left + 56, rowTop + 43, 78, this.usableIcon(usable), this.usableEffectLabel(usable), accent); + this.renderUsableMenuPill(left + 140, rowTop + 43, 86, 'move', `거리 ${usable.range}`, accent); + this.renderUsableMenuPill(left + 232, rowTop + 43, 82, usable.target === 'enemy' ? 'hit' : 'success', this.usablePowerLabel(usable), accent); const choose = (choicePointer: Phaser.Input.Pointer) => { if (choicePointer.rightButtonDown()) { @@ -4308,14 +4426,12 @@ export class BattleScene extends Phaser.Scene { }; bg.on('pointerover', () => { bg.setFillStyle(0x283947, 0.98); - this.renderUnitDetail(unit, `${usable.name}\n${usable.description}`); + this.renderUnitDetail(unit, this.usableDetailText(unit, usable)); }); bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.94)); bg.on('pointerdown', choose); name.setInteractive({ useHandCursor: true }); name.on('pointerdown', choose); - desc.setInteractive({ useHandCursor: true }); - desc.on('pointerdown', choose); }); } @@ -4535,6 +4651,7 @@ export class BattleScene extends Phaser.Scene { private showSupportTargets(user: UnitData, usable: BattleUsable, targets = this.supportTargets(user, usable)) { targets.forEach((target) => { + const preview = this.supportPreview(user, target, usable); const marker = this.add.rectangle( this.tileTopLeftX(target.x), this.tileTopLeftY(target.y), @@ -4573,7 +4690,7 @@ export class BattleScene extends Phaser.Scene { hitArea.on('pointerover', () => { marker.setFillStyle(usable.effect === 'heal' ? 0x45b875 : 0x4f86d9, 0.34); marker.setStrokeStyle(3, usable.effect === 'heal' ? 0xd9ffe8 : 0xf2f7ff, 1); - this.renderSupportPreview(user, target, usable); + this.renderSupportPreview(preview); }); hitArea.on('pointerout', () => { marker.setFillStyle(usable.effect === 'heal' ? 0x45b875 : 0x4f86d9, 0.2); @@ -4647,17 +4764,24 @@ export class BattleScene extends Phaser.Scene { this.renderCombatPreviewCard(preview); } - private renderSupportPreview(user: UnitData, target: UnitData, usable: BattleUsable) { - if (usable.effect === 'heal') { - const amount = this.supportHealAmount(user, target, usable); - this.renderUnitDetail(target, `${user.name} ${usable.name} 예측\n병력 +${amount} / ${target.hp} -> ${Math.min(target.maxHp, target.hp + amount)}`); - return; - } - - this.renderUnitDetail( + private supportPreview(user: UnitData, target: UnitData, usable: BattleUsable): SupportPreview { + const healAmount = usable.effect === 'heal' ? this.supportHealAmount(user, target, usable) : 0; + return { + usable, + user, target, - `${user.name} ${usable.name} 예측\n공격 +${usable.attackBonus ?? 0} / 명중 +${usable.hitBonus ?? 0} / 치명 +${usable.criticalBonus ?? 0}\n${usable.duration ?? 1}턴 동안 유지` - ); + healAmount, + projectedHp: Math.min(target.maxHp, target.hp + healAmount), + attackBonus: usable.attackBonus ?? 0, + hitBonus: usable.hitBonus ?? 0, + criticalBonus: usable.criticalBonus ?? 0, + duration: usable.duration ?? 1 + }; + } + + private renderSupportPreview(preview: SupportPreview) { + this.renderUnitDetail(preview.target); + this.renderSupportPreviewCard(preview); } private equipmentPreviewBonusText(preview: CombatPreview) { @@ -4758,6 +4882,59 @@ export class BattleScene extends Phaser.Scene { } } + private renderSupportPreviewCard(preview: SupportPreview) { + const { panelX, panelWidth } = this.layout; + const left = panelX + 24; + const width = panelWidth - 48; + const height = 158; + const y = Math.min(this.sideContentTop() + 402, this.sideContentBottom(10) - height); + const accent = this.usableAccentColor(preview.usable); + + const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 0.98)); + bg.setOrigin(0); + bg.setStrokeStyle(2, accent, 0.78); + + const actionFrame = this.trackSideObject(this.add.rectangle(left + 24, y + 27, 42, 42, 0x0a0f14, 0.9)); + actionFrame.setStrokeStyle(1, accent, 0.76); + this.trackSideIcon(left + 24, y + 27, this.usableIcon(preview.usable), 35); + this.trackSideObject(this.add.text(left + 52, y + 9, `${preview.user.name} ${preview.usable.name} 예측`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '16px', + color: '#f4dfad', + fontStyle: '700' + })); + this.trackSideObject(this.add.text(left + 52, y + 30, `${preview.target.name} / ${this.usableEffectLabel(preview.usable)} / ${this.usableTargetLabel(preview.usable)}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#9fb0bf' + })); + + const metricTop = y + 56; + 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 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent); + this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'move', '거리', `${preview.usable.range}`, accent); + } else { + this.renderPreviewMetric(left + 10, metricTop, metricWidth, 'attack', '공격', `+${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); + } + + this.renderSupportForecastRow(preview, left + 10, y + 98, width - 20, accent); + + const badgeY = y + 126; + 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); + if (preview.usable.effect === 'focus') { + this.renderPreviewBadge(badgeX, badgeY, 'leadership', `${preview.duration}턴`, maxBadgeRight); + } + } + private renderPreviewEquipmentChip( x: number, y: number, @@ -4837,6 +5014,45 @@ export class BattleScene extends Phaser.Scene { hpText.setOrigin(1, 0); } + private renderSupportForecastRow(preview: SupportPreview, x: number, y: number, width: number, accent: number) { + const row = this.trackSideObject(this.add.rectangle(x, y, width, 22, 0x0b1118, 0.94)); + row.setOrigin(0); + row.setStrokeStyle(1, accent, 0.48); + const isHeal = preview.usable.effect === 'heal'; + this.trackSideIcon(x + 12, y + 11, isHeal ? 'hp' : 'leadership', 17); + this.trackSideObject(this.add.text(x + 26, y + 5, isHeal ? '적용 후 HP' : '강화 결과', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#9fb0bf', + fontStyle: '700' + })); + + if (isHeal) { + this.drawGauge(x + 100, y + 7, width - 178, 8, preview.projectedHp / preview.target.maxHp, 0x59d18c); + const hpText = this.trackSideObject(this.add.text(x + width - 8, y + 5, `${preview.target.hp} -> ${preview.projectedHp}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#f2e3bf', + fontStyle: '700' + })); + hpText.setOrigin(1, 0); + return; + } + + const buffText = this.trackSideObject(this.add.text( + x + width - 8, + y + 5, + `공+${preview.attackBonus} / 명+${preview.hitBonus} / 치+${preview.criticalBonus} / ${preview.duration}턴`, + { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#f2e3bf', + fontStyle: '700' + } + )); + buffText.setOrigin(1, 0); + } + private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) { const width = Phaser.Math.Clamp(label.length * 8 + 42, 68, 138); if (maxRight && x + width > maxRight) { @@ -7844,22 +8060,23 @@ export class BattleScene extends Phaser.Scene { })); title.setOrigin(0.5, 0); title.setDepth(depth + 2); + this.renderSupportActionRibbon(result, left + panelWidth / 2 - 220, top + 58, 440, depth + 9); const supportPose: UnitActionPose = result.usable.command === 'strategy' ? 'strategy' : 'item'; const userSprite = this.trackCombatObject( - this.add.sprite(left + 160, top + 146, this.unitActionTexture(result.user), this.unitActionFrameIndex('east', supportPose)) + this.add.sprite(left + 160, top + 156, this.unitActionTexture(result.user), this.unitActionFrameIndex('east', supportPose)) ); userSprite.setDepth(depth + 3); this.applyCombatSpriteDisplaySize(userSprite, result.user, true); this.applyActionSpriteBlend(userSprite); void this.playUnitActionFrames(userSprite, result.user, 'east', supportPose, 100, 2); - const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 146, this.unitTextureKey(result.target), this.unitFrameIndex('west'))); + const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 156, this.unitTextureKey(result.target), this.unitFrameIndex('west'))); targetSprite.setDepth(depth + 3); this.applyCombatSpriteDisplaySize(targetSprite, result.target, true); const effectColor = result.usable.effect === 'heal' ? 0x59d18c : 0xd8b15f; - const effect = this.trackCombatObject(this.add.circle(left + panelWidth / 2, top + 145, 30, effectColor, 0.76)); + const effect = this.trackCombatObject(this.add.circle(left + panelWidth / 2, top + 156, 30, effectColor, 0.76)); effect.setDepth(depth + 4); this.tweens.add({ targets: effect, scale: 1.45, alpha: 0.18, duration: 520, yoyo: true }); soundDirector.playEffect(result.usable.command === 'strategy' ? 'strategy-cast' : 'exp-gain', { @@ -7867,7 +8084,7 @@ export class BattleScene extends Phaser.Scene { stopAfterMs: 900 }); - const targetStatus = this.renderCombatStatusPanel(result.target, left + panelWidth / 2 - 196, top + 190, 392); + const targetStatus = this.renderCombatStatusPanel(result.target, left + panelWidth / 2 - 196, top + 202, 392); targetStatus.hpFill.setScale(result.previousTargetHp / result.target.maxHp, 1); targetStatus.hpText.setText(`${result.previousTargetHp} / ${result.target.maxHp}`); const growthEntries = this.supportGrowthEntries(result); @@ -7880,7 +8097,7 @@ export class BattleScene extends Phaser.Scene { result.usable.effect === 'heal' ? `${result.target.name} 병력 +${result.healAmount}` : `${result.target.name} ${result.usable.name}: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`; - const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 106, resultLine, { + const resultText = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 132, resultLine, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '19px', color: result.usable.effect === 'heal' ? '#a8ffd0' : '#ffdf7b', @@ -7890,7 +8107,7 @@ export class BattleScene extends Phaser.Scene { })); resultText.setOrigin(0.5); resultText.setDepth(depth + 5); - void this.playImmediateGrowthPopups(growthEntries, left + panelWidth / 2, top + 156, depth + 8); + void this.playImmediateGrowthPopups(growthEntries, left + panelWidth / 2, top + 178, depth + 8); await this.playGrowthRewardPanel({ x: left + 64, @@ -7906,6 +8123,87 @@ export class BattleScene extends Phaser.Scene { this.hideCombatCutIn(); } + private renderSupportActionRibbon(result: SupportResult, x: number, y: number, width: number, depth: number) { + const accent = this.usableAccentColor(result.usable); + const bg = this.trackCombatObject(this.add.rectangle(x, y, width, 64, 0x0b1118, 0.92)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, accent, 0.66); + + const actionFrame = this.trackCombatObject(this.add.rectangle(x + 24, y + 22, 36, 36, 0x101820, 0.9)); + actionFrame.setDepth(depth + 1); + actionFrame.setStrokeStyle(1, accent, 0.68); + this.trackCombatIcon(x + 24, y + 22, this.usableIcon(result.usable), 31, depth + 2); + + const sourceItem = getItem(result.usable.command === 'strategy' ? result.user.equipment.weapon.itemId : result.user.equipment.accessory.itemId); + const sourceSlot: EquipmentSlot = result.usable.command === 'strategy' ? 'weapon' : 'accessory'; + const sourceFrame = this.trackCombatObject(this.add.rectangle(x + 136, y + 22, 30, 30, 0x16212d, 0.9)); + sourceFrame.setDepth(depth + 1); + sourceFrame.setStrokeStyle(1, 0x647485, 0.58); + this.trackCombatIcon(x + 136, y + 22, this.itemIcon(sourceItem, sourceSlot), 25, depth + 2); + + const targetFrame = this.trackCombatObject(this.add.rectangle(x + 256, y + 22, 30, 30, 0x16212d, 0.9)); + targetFrame.setDepth(depth + 1); + targetFrame.setStrokeStyle(1, 0x647485, 0.58); + this.trackCombatIcon(x + 256, y + 22, result.usable.effect === 'heal' ? 'hp' : 'leadership', 25, depth + 2); + + const actionLabel = this.trackCombatObject(this.add.text(x + 46, y + 9, result.usable.name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: '#f4dfad', + fontStyle: '700', + fixedWidth: 74 + })); + actionLabel.setDepth(depth + 1); + + const sourceLabel = this.trackCombatObject(this.add.text(x + 156, y + 7, sourceItem.name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#d4dce6', + fixedWidth: 90 + })); + sourceLabel.setDepth(depth + 1); + const sourceRole = this.trackCombatObject(this.add.text(x + 156, y + 25, result.usable.command === 'strategy' ? '책략 도구' : '소모 도구', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#9fb0bf', + fixedWidth: 90 + })); + sourceRole.setDepth(depth + 1); + + const targetLabel = this.trackCombatObject(this.add.text(x + 276, y + 7, result.target.name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#d4dce6', + fixedWidth: 74 + })); + targetLabel.setDepth(depth + 1); + const targetRole = this.trackCombatObject(this.add.text(x + 276, y + 25, this.usableTargetLabel(result.usable), { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#9fb0bf', + fixedWidth: 74 + })); + targetRole.setDepth(depth + 1); + + const resultLabel = + result.usable.effect === 'heal' + ? `회복 +${result.healAmount}` + : `강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴`; + const resultText = this.trackCombatObject(this.add.text(x + width - 14, y + 10, resultLabel, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: result.usable.effect === 'heal' ? '#a8ffd0' : '#ffdf7b', + fontStyle: '700' + })); + resultText.setOrigin(1, 0); + resultText.setDepth(depth + 1); + + 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); + } + private renderCombatStatusPanel(unit: UnitData, x: number, y: number, width: number, growth?: CharacterGrowthResult) { const panel = this.trackCombatObject(this.add.rectangle(x, y, width, 116, 0x101820, 0.96)); panel.setOrigin(0);