diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 8d09cb0..eeaae99 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -7729,27 +7729,27 @@ export class BattleScene extends Phaser.Scene { const visibleGains = gains.filter((gain) => gain.amount > 0).slice(0, 5); let cursorX = x; visibleGains.forEach((gain) => { - const chipWidth = Phaser.Math.Clamp(gain.label.length * 10 + 52, 72, 96); + const chipWidth = Phaser.Math.Clamp(gain.label.length * 11 + 62, 86, 116); if (cursorX + chipWidth > x + maxWidth) { return; } - const bg = this.add.rectangle(cursorX, y, chipWidth, 30, 0x0b1118, 0.92); + const bg = this.add.rectangle(cursorX, y, chipWidth, 34, 0x0b1118, 0.94); bg.setOrigin(0); bg.setStrokeStyle(1, 0x53606c, 0.62); - const icon = this.createBattleUiIcon(cursorX + 16, y + 15, gain.icon, 24); - const text = this.add.text(cursorX + 32, y + 7, `+${gain.amount}`, { + const icon = this.createBattleUiIcon(cursorX + 18, y + 17, gain.icon, 28); + const text = this.add.text(cursorX + 36, y + 7, `+${gain.amount}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '13px', + fontSize: '15px', color: '#a8ffd0', fontStyle: '700' }); - const label = this.add.text(cursorX + 55, y + 8, gain.label, { + const label = this.add.text(cursorX + 65, y + 9, gain.label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '12px', color: '#d4dce6' }); container.add([bg, icon, text, label]); - cursorX += chipWidth + 5; + cursorX += chipWidth + 6; }); } @@ -8271,13 +8271,14 @@ export class BattleScene extends Phaser.Scene { } private formatCombatResult(result: CombatResult) { - const outcome = this.formatCombatOutcome(result); - const defeated = result.defeated ? `\n${result.defender.name} 퇴각!` : `\n${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; - const bondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : ''; - const equipment = result.equipmentEffectLabels.length > 0 ? `\n보물 효과: ${result.equipmentEffectLabels.join(', ')}` : ''; - const bond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : ''; - const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : ''; - return `${result.attacker.name} ${result.usable?.name ?? commandLabels[result.action]}\n${outcome}${defeated}${bondBonus}${equipment}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${bond}${counter}`; + const summaryOutcome = this.formatCombatOutcome(result); + const summaryDefeated = result.defeated ? `\n${result.defender.name} 격파!` : `\n${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; + const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : ''; + const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : ''; + const summaryBond = result.bondExp > 0 ? `\n공명 경험치 +${result.bondExp}` : ''; + const summaryCounter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : ''; + return `${result.attacker.name} ${result.usable?.name ?? commandLabels[result.action]}\n${summaryOutcome}${summaryDefeated}${summaryBondBonus}${summaryEquipment}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${summaryBond}${summaryCounter}`; + } private formatCombatOutcome(result: CombatResult) { @@ -8285,17 +8286,45 @@ export class BattleScene extends Phaser.Scene { return `${result.defender.name}이 공격을 회피했습니다.`; } + const summaryCritical = result.critical ? '치명타! ' : ''; + return `${summaryCritical}${result.defender.name}에게 ${result.damage} 피해`; + + if (!result.hit) { + return `${result.defender.name}이 공격을 회피했습니다.`; + } + const critical = result.critical ? '치명타! ' : ''; return `${critical}${result.defender.name}에게 ${result.damage} 피해`; } private formatCounterLine(result: CombatResult) { + const counterOutcome = result.hit ? `${result.critical ? '치명타로 ' : ''}${result.damage} 피해` : '회피'; + const counterDefeated = result.defeated ? ` / ${result.defender.name} 격파` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; + return `반격: ${result.attacker.name} -> ${result.defender.name} ${counterOutcome}${counterDefeated}`; + const outcome = result.hit ? `${result.critical ? '치명타로 ' : ''}${result.damage} 피해` : '회피됨'; const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; return `반격(감소): ${result.attacker.name} -> ${result.defender.name} ${outcome}${defeated}`; } private combatGrowthLines(result: CombatResult) { + const summaryVerdict = result.hit ? `${result.critical ? '치명타 / ' : ''}명중 ${result.hitRate}%` : `회피 / 명중 ${result.hitRate}%`; + const summaryLines = [ + summaryVerdict, + this.formatCharacterGrowth(result.characterGrowth), + this.formatEquipmentGrowth(result.attackerGrowth), + this.formatEquipmentGrowth(result.defenderGrowth) + ]; + + if (result.bondExp > 0) { + summaryLines.push(`공명 경험치 +${result.bondExp}`); + } + if (result.equipmentEffectLabels.length > 0) { + summaryLines.push(`장비 효과: ${result.equipmentEffectLabels.join(', ')}`); + } + + return summaryLines; + const verdict = result.hit ? `${result.critical ? '치명타 / ' : ''}명중 ${result.hitRate}%` : `회피됨 / 명중 ${result.hitRate}%`; const lines = [ verdict, @@ -8315,6 +8344,12 @@ export class BattleScene extends Phaser.Scene { } private formatSupportResult(result: SupportResult) { + const supportEffect = + result.usable.effect === 'heal' + ? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})` + : `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`; + return `${result.user.name} ${result.usable.name}\n${supportEffect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`; + const effect = result.usable.effect === 'heal' ? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})` @@ -9574,7 +9609,8 @@ export class BattleScene extends Phaser.Scene { stage.fillRect(left + 18, top + 82, panelWidth - 36, 18); const titleText = result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action]; - const title = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 24, titleText, { + const displayTitleText = result.isCounter ? '반격' : titleText; + const title = this.trackCombatObject(this.add.text(left + panelWidth / 2, top + 24, displayTitleText, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '24px', color: '#f4dfad', @@ -9625,7 +9661,7 @@ export class BattleScene extends Phaser.Scene { } else { this.showCombatMiss(defenderX, groundY - 70, depth + 14); } - const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 198, top + 214, 396, depth + 16); + const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 226, top + 196, 452, depth + 16); await this.animateCombatGauge(defenderStatus.hpFill, result.previousDefenderHp / result.defender.maxHp, result.defender.hp / result.defender.maxHp, 420); defenderStatus.hpText.setText(`${result.defender.hp} / ${result.defender.maxHp}`); @@ -9660,7 +9696,117 @@ export class BattleScene extends Phaser.Scene { this.hideCombatCutIn(); } + private renderCombatOutcomeSummaryCard(result: CombatResult, x: number, y: number, width: number, depth: number) { + const height = 84; + const isStrategy = result.action === 'strategy'; + const icon: BattleUiIconKey = result.defeated + ? 'critical' + : !result.hit + ? 'move' + : result.critical + ? 'critical' + : isStrategy + ? 'success' + : 'hit'; + const title = result.defeated + ? '격파' + : !result.hit + ? isStrategy + ? '책략 실패' + : '회피' + : result.critical + ? '치명타' + : isStrategy + ? '책략 성공' + : '명중'; + const tone = result.defeated || result.critical ? 0xd8732c : result.hit ? this.previewAccentColor(result) : 0x83d6ff; + const container = this.trackCombatObject(this.add.container(x, y)); + container.setDepth(depth); + container.setAlpha(0); + container.setScale(0.96); + + const bg = this.add.rectangle(0, 0, width, height, 0x0b121a, 0.98); + bg.setOrigin(0); + bg.setStrokeStyle(2, tone, 0.88); + + const iconFrame = this.add.rectangle(42, height / 2, 64, 64, 0x101820, 0.94); + iconFrame.setStrokeStyle(1, tone, 0.72); + const iconImage = this.createBattleUiIcon(42, height / 2, icon, 58); + + const titleText = this.add.text(84, 10, title, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '23px', + color: result.hit ? '#fff2b8' : '#d9f1ff', + fontStyle: '700', + fixedWidth: 150 + }); + + const detail = result.defeated ? `${result.defender.name} 전투 불능` : `${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; + const detailText = this.add.text(86, 42, detail, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: result.defeated ? '#ffb6a6' : '#c8d2dd', + fontStyle: '700', + fixedWidth: 190 + }); + + const value = result.hit ? `-${result.damage}` : '0'; + const valueText = this.add.text(width - 18, 9, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '31px', + color: result.hit ? '#ffdf7b' : '#9fb0bf', + fontStyle: '700' + }); + valueText.setOrigin(1, 0); + + const valueLabel = this.add.text(width - 18, 47, '피해', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#9fb0bf', + fontStyle: '700' + }); + valueLabel.setOrigin(1, 0); + + container.add([bg, iconFrame, iconImage, titleText, detailText, valueText, valueLabel]); + + const badgeY = 64; + this.addCombatOutcomeChip(container, 86, badgeY, result.action === 'strategy' ? 'success' : 'hit', result.hit ? `${result.hitRate}%` : '실패', tone); + this.addCombatOutcomeChip(container, 154, badgeY, 'critical', result.critical ? '치명' : `${result.criticalRate}%`, 0xd8732c); + this.addCombatOutcomeChip(container, 222, badgeY, 'advantage', result.matchupLabel, palette.blue, 86); + if (result.bondDamageBonus > 0) { + this.addCombatOutcomeChip(container, width - 114, badgeY, 'leadership', `공명 +${result.bondDamageBonus}%`, palette.gold, 96); + } + + this.tweens.add({ targets: container, alpha: 1, scale: 1, y: y - 4, duration: 160, ease: 'Sine.easeOut' }); + return container; + } + + private addCombatOutcomeChip( + container: Phaser.GameObjects.Container, + x: number, + y: number, + icon: BattleUiIconKey, + label: string, + tone: number, + width = 62 + ) { + const bg = this.add.rectangle(x, y, width, 22, 0x101820, 0.94); + bg.setOrigin(0, 0.5); + bg.setStrokeStyle(1, tone, 0.54); + const chipIcon = this.createBattleUiIcon(x + 13, y, icon, 19); + const text = this.add.text(x + 27, y - 6, this.truncateUiText(label, width > 70 ? 8 : 5), { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#f2e3bf', + fontStyle: '700', + fixedWidth: width - 30 + }); + container.add([bg, chipIcon, text]); + } + private renderCombatOutcomeCard(result: CombatResult, x: number, y: number, width: number, depth: number) { + return this.renderCombatOutcomeSummaryCard(result, x, y, width, depth); + const height = 66; const isStrategy = result.action === 'strategy'; const icon: BattleUiIconKey = result.defeated @@ -9759,7 +9905,86 @@ export class BattleScene extends Phaser.Scene { await this.delay(170); } + private renderCombatActionSummaryRibbon(result: CombatResult, x: number, y: number, width: number, depth: number) { + const accent = this.previewAccentColor(result); + const bg = this.trackCombatObject(this.add.rectangle(x, y, width, 64, 0x0b1118, 0.94)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, accent, 0.72); + + const actionFrame = this.trackCombatObject(this.add.rectangle(x + 25, y + 23, 42, 42, 0x101820, 0.9)); + actionFrame.setDepth(depth + 1); + actionFrame.setStrokeStyle(1, accent, 0.68); + this.trackCombatIcon(x + 25, y + 23, this.actionIcon(result), 37, depth + 2); + + const attackerWeapon = getItem(result.attacker.equipment.weapon.itemId); + const defenderArmor = getItem(result.defender.equipment.armor.itemId); + const dealtDamage = result.hit ? result.damage : 0; + + this.renderCombatRibbonEquipment(x + 120, y + 8, this.itemIcon(attackerWeapon, 'weapon'), attackerWeapon.name, '공격 장비', depth + 1); + this.renderCombatRibbonEquipment(x + 250, y + 8, this.itemIcon(defenderArmor, 'armor'), defenderArmor.name, '방어 장비', depth + 1); + + const actionLabel = this.trackCombatObject(this.add.text(x + 46, y + 9, result.usable?.name ?? commandLabels[result.action], { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: '#f4dfad', + fontStyle: '700', + fixedWidth: 72 + })); + actionLabel.setDepth(depth + 1); + + const damageText = this.trackCombatObject(this.add.text(x + width - 14, y + 8, `피해 ${dealtDamage}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: result.hit ? '#ffcf91' : '#9fb0bf', + fontStyle: '700' + })); + damageText.setOrigin(1, 0); + damageText.setDepth(depth + 1); + + const outcomeIcon = result.action === 'strategy' ? 'success' : result.critical ? 'critical' : result.hit ? 'hit' : 'move'; + const outcomeLabel = result.action === 'strategy' ? (result.hit ? '성공' : '실패') : result.hit ? (result.critical ? '치명' : '명중') : '회피'; + 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 + 280, y + 38, 'counter', '반격', depth + 1, 58); + } + } + + private renderCombatRibbonEquipment( + x: number, + y: number, + icon: BattleUiIconKey, + itemName: string, + role: string, + depth: number + ) { + const frame = this.trackCombatObject(this.add.rectangle(x + 14, y + 14, 34, 34, 0x16212d, 0.9)); + frame.setDepth(depth); + frame.setStrokeStyle(1, 0x647485, 0.58); + this.trackCombatIcon(x + 14, y + 14, icon, 30, depth + 1); + + const label = this.trackCombatObject(this.add.text(x + 34, y - 1, this.truncateUiText(itemName, 7), { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#d4dce6', + fixedWidth: 84 + })); + label.setDepth(depth); + const roleText = this.trackCombatObject(this.add.text(x + 34, y + 17, role, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#9fb0bf', + fixedWidth: 84 + })); + roleText.setDepth(depth); + } + private renderCombatActionRibbon(result: CombatResult, x: number, y: number, width: number, depth: number) { + this.renderCombatActionSummaryRibbon(result, x, y, width, depth); + return; + const accent = this.previewAccentColor(result); const bg = this.trackCombatObject(this.add.rectangle(x, y, width, 64, 0x0b1118, 0.92)); bg.setOrigin(0); @@ -9937,7 +10162,7 @@ export class BattleScene extends Phaser.Scene { const growthEntries = this.supportGrowthEntries(result); await this.delay(250); - const outcomeCard = this.renderSupportOutcomeCard(result, left + panelWidth / 2 - 190, top + 116, 380, depth + 5); + const outcomeCard = this.renderSupportOutcomeCard(result, left + panelWidth / 2 - 216, top + 108, 432, depth + 5); await this.animateCombatGauge(targetStatus.hpFill, result.previousTargetHp / result.target.maxHp, result.target.hp / result.target.maxHp, 480); targetStatus.hpText.setText(`${result.target.hp} / ${result.target.maxHp}`); @@ -9956,7 +10181,72 @@ export class BattleScene extends Phaser.Scene { this.hideCombatCutIn(); } + private renderSupportOutcomeSummaryCard(result: SupportResult, x: number, y: number, width: number, depth: number) { + const height = 84; + const heal = result.usable.effect === 'heal'; + const tone = heal ? 0x59d18c : 0xd8b15f; + const container = this.trackCombatObject(this.add.container(x, y)); + container.setDepth(depth); + container.setAlpha(0); + container.setScale(0.96); + + const bg = this.add.rectangle(0, 0, width, height, 0x0b121a, 0.98); + bg.setOrigin(0); + bg.setStrokeStyle(2, tone, 0.88); + + const iconFrame = this.add.rectangle(42, height / 2, 64, 64, 0x101820, 0.94); + iconFrame.setStrokeStyle(1, tone, 0.72); + const iconImage = this.createBattleUiIcon(42, height / 2, this.usablePowerIcon(result.usable), 58); + + const titleText = this.add.text(84, 10, heal ? '병력 회복' : '강화 적용', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '23px', + color: heal ? '#a8ffd0' : '#fff2b8', + fontStyle: '700', + fixedWidth: 156 + }); + + const detail = heal + ? `${result.target.name} HP ${result.previousTargetHp} → ${result.target.hp}` + : `${result.target.name} 공+${result.buff?.attackBonus ?? 0} 명+${result.buff?.hitBonus ?? 0} 치+${result.buff?.criticalBonus ?? 0}`; + const detailText = this.add.text(86, 42, detail, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: '#d4dce6', + fontStyle: '700', + fixedWidth: 218 + }); + + const value = heal ? `+${result.healAmount}` : `${result.buff?.turns ?? result.usable.duration ?? 1}`; + const valueText = this.add.text(width - 18, 9, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '31px', + color: heal ? '#a8ffd0' : '#ffdf7b', + fontStyle: '700' + }); + valueText.setOrigin(1, 0); + + const valueLabel = this.add.text(width - 18, 47, heal ? '회복' : '턴', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#9fb0bf', + fontStyle: '700' + }); + valueLabel.setOrigin(1, 0); + + container.add([bg, iconFrame, iconImage, titleText, detailText, valueText, valueLabel]); + const badgeY = 64; + this.addCombatOutcomeChip(container, 86, badgeY, this.usableIcon(result.usable), this.usableEffectLabel(result.usable), tone, 78); + this.addCombatOutcomeChip(container, 170, badgeY, 'success', '적용', 0x59d18c, 62); + this.addCombatOutcomeChip(container, 238, badgeY, this.usableTargetIcon(result.usable), `${this.usableTargetLabel(result.usable)} ${result.usable.range}칸`, palette.blue, 92); + + this.tweens.add({ targets: container, alpha: 1, scale: 1, y: y - 4, duration: 160, ease: 'Sine.easeOut' }); + return container; + } + private renderSupportOutcomeCard(result: SupportResult, x: number, y: number, width: number, depth: number) { + return this.renderSupportOutcomeSummaryCard(result, x, y, width, depth); + const height = 66; const heal = result.usable.effect === 'heal'; const tone = heal ? 0x59d18c : 0xd8b15f; @@ -10007,7 +10297,61 @@ export class BattleScene extends Phaser.Scene { return container; } + private renderSupportActionSummaryRibbon(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.94)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, accent, 0.72); + + const actionFrame = this.trackCombatObject(this.add.rectangle(x + 25, y + 23, 42, 42, 0x101820, 0.9)); + actionFrame.setDepth(depth + 1); + actionFrame.setStrokeStyle(1, accent, 0.68); + this.trackCombatIcon(x + 25, y + 23, this.usableIcon(result.usable), 37, 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'; + this.renderCombatRibbonEquipment( + x + 120, + y + 8, + this.itemIcon(sourceItem, sourceSlot), + sourceItem.name, + result.usable.command === 'strategy' ? '책략 도구' : '소모 도구', + depth + 1 + ); + this.renderCombatRibbonEquipment(x + 254, y + 8, this.usablePowerIcon(result.usable), result.target.name, this.usableTargetLabel(result.usable), depth + 1); + + 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: 72 + })); + actionLabel.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 + 8, resultLabel, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + 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, this.usableTargetIcon(result.usable), `${this.usableTargetLabel(result.usable)} ${result.usable.range}칸`, depth + 1, 78); + } + private renderSupportActionRibbon(result: SupportResult, x: number, y: number, width: number, depth: number) { + this.renderSupportActionSummaryRibbon(result, x, y, width, depth); + return; + const accent = this.usableAccentColor(result.usable); const bg = this.trackCombatObject(this.add.rectangle(x, y, width, 64, 0x0b1118, 0.92)); bg.setOrigin(0); @@ -10121,6 +10465,7 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' })); hpLabel.setDepth(85); + hpLabel.setText('병력'); const hpTrack = this.trackCombatObject(this.add.rectangle(x + 148, y + 54, width - 232, 9, 0x0a0f14, 0.9)); hpTrack.setOrigin(0, 0.5); hpTrack.setDepth(85); @@ -10145,6 +10490,7 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' })); expLabel.setDepth(85); + expLabel.setText('경험'); const expTrack = this.trackCombatObject(this.add.rectangle(x + 148, y + 84, width - 232, 9, 0x0a0f14, 0.9)); expTrack.setOrigin(0, 0.5); expTrack.setDepth(85); @@ -10199,6 +10545,22 @@ export class BattleScene extends Phaser.Scene { color: 0xd8b15f, statGains: growth.statGains }; + + return { + kind: 'character', + label: '장수 경험치', + owner: unit.name, + amountLabel: `+${growth.amount}`, + previousLevel: growth.previousLevel, + previousExp: growth.previousExp, + previousNext: 100, + level: growth.level, + exp: growth.exp, + next: growth.next, + leveled: growth.leveled, + color: 0xd8b15f, + statGains: growth.statGains + }; } private equipmentGrowthEntry(unit: UnitData, growth: EquipmentGrowthResult): GrowthGaugeEntry { @@ -10234,6 +10596,21 @@ export class BattleScene extends Phaser.Scene { leveled: growth.leveled, color: 0x83d6ff }; + + return { + kind: 'bond', + label: '공명 경험치', + owner: growth.label, + amountLabel: `+${growth.amount}`, + previousLevel: growth.previousLevel, + previousExp: growth.previousExp, + previousNext: 100, + level: growth.level, + exp: growth.exp, + next: growth.next, + leveled: growth.leveled, + color: 0x83d6ff + }; } private growthCelebrant(result: CombatResult) { @@ -10273,7 +10650,8 @@ export class BattleScene extends Phaser.Scene { panel.setDepth(config.depth); panel.setStrokeStyle(2, palette.gold, 0.82); - const title = this.trackCombatObject(this.add.text(config.x + 16, config.y + 12, config.title, { + const displayTitle = config.title === '성장 결과' ? config.title : '성장 결과'; + const title = this.trackCombatObject(this.add.text(config.x + 16, config.y + 12, displayTitle, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#f4dfad', @@ -10342,6 +10720,7 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' }); levelText.setOrigin(celebrant ? 0 : 0.5, 0); + levelText.setText(`Lv ${entry.previousLevel} → Lv ${entry.level}`); const statText = this.add.text(celebrant ? contentX : 0, 16, entry.statGains ? '능력 상승' : `${entry.label} 효과 강화`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', @@ -10351,6 +10730,7 @@ export class BattleScene extends Phaser.Scene { strokeThickness: 3 }); statText.setOrigin(celebrant ? 0 : 0.5, 0); + statText.setText(entry.statGains ? '능력 상승' : `${entry.label} 강화`); container.add([bg, visualFrame, glow, title, levelText, statText]); if (entry.statGains) { this.addStatGainChipsToContainer(container, entry.statGains, celebrant ? contentX : -panelWidth / 2 + 44, 50, panelWidth - 132); @@ -11024,6 +11404,7 @@ export class BattleScene extends Phaser.Scene { lineSpacing: 2 })); text.setOrigin(0.5); + text.setText(`공명 효과 ${result.bondLabel}\n피해 +${result.bondDamageBonus}%`); text.setDepth(depth + 2); this.tweens.add({ targets: aura, scale: 1.7, alpha: 0, duration: 560, ease: 'Sine.easeOut' }); @@ -11065,6 +11446,7 @@ export class BattleScene extends Phaser.Scene { strokeThickness: 4 })); miss.setOrigin(0.5); + miss.setText('회피'); miss.setDepth(depth); this.tweens.add({ targets: miss,