diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index d437bef..781dab2 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1324,6 +1324,7 @@ type CombatPreview = { }; type CombatResult = CombatPreview & { + expectedDamage: number; previousDefenderHp: number; hit: boolean; critical: boolean; @@ -8280,6 +8281,7 @@ export class BattleScene extends Phaser.Scene { return { ...preview, + expectedDamage: preview.damage, damage, previousDefenderHp, hit, @@ -8539,16 +8541,46 @@ export class BattleScene extends Phaser.Scene { } private formatCombatResult(result: CombatResult) { - 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 headline = this.combatResultHeadline(result); + const resolution = this.combatResolutionLine(result); 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}`; + return `${headline}\n${resolution}${summaryCounter}${summaryBondBonus}${summaryEquipment}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.attackerGrowth)}\n${this.formatEquipmentGrowth(result.defenderGrowth)}${summaryBond}`; } + private combatActionName(result: CombatResult) { + return result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action]; + } + + private combatDamageValue(result: CombatResult) { + return result.hit ? result.damage : 0; + } + + private combatResultHeadline(result: CombatResult) { + const damage = this.combatDamageValue(result); + const outcome = !result.hit + ? result.action === 'strategy' + ? '책략 실패' + : '회피됨' + : result.critical + ? `치명 ${damage} 피해` + : `${damage} 피해`; + const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}→${result.defender.hp}` : ''; + const defeated = result.defeated ? ' · 퇴각' : ''; + return `${result.attacker.name} ${this.combatActionName(result)} → ${result.defender.name}: ${outcome}${hpLine}${defeated}`; + } + + private combatResolutionLine(result: CombatResult) { + const successLabel = result.action === 'strategy' ? '성공' : '명중'; + const actual = this.combatDamageValue(result); + const verdict = result.hit ? `${successLabel} ${result.hitRate}%` : `${successLabel} 실패(${result.hitRate}%)`; + const critical = result.critical ? '치명 발생' : `치명 ${result.criticalRate}%`; + return `판정: ${verdict} · ${critical} · 예상 ${result.expectedDamage}/실제 ${actual}`; + } + private formatCombatOutcome(result: CombatResult) { if (!result.hit) { return `${result.defender.name}이 공격을 회피했습니다.`; @@ -8556,23 +8588,13 @@ export class BattleScene extends Phaser.Scene { 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}`; + const counterOutcome = result.hit ? `${result.critical ? '치명 ' : ''}${result.damage} 피해` : '회피됨'; + const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}→${result.defender.hp}` : ''; + const defeated = result.defeated ? ' · 퇴각' : ''; + return `반격: ${result.attacker.name} → ${result.defender.name}: ${counterOutcome}${hpLine}${defeated}`; } private combatGrowthLines(result: CombatResult) { @@ -8592,23 +8614,6 @@ export class BattleScene extends Phaser.Scene { } return summaryLines; - - const verdict = result.hit ? `${result.critical ? '치명타 / ' : ''}명중 ${result.hitRate}%` : `회피됨 / 명중 ${result.hitRate}%`; - const lines = [ - verdict, - this.formatCharacterGrowth(result.characterGrowth), - this.formatEquipmentGrowth(result.attackerGrowth), - this.formatEquipmentGrowth(result.defenderGrowth) - ]; - - if (result.bondExp > 0) { - lines.push(`공명 경험치 +${result.bondExp}`); - } - if (result.equipmentEffectLabels.length > 0) { - lines.push(`보물 효과: ${result.equipmentEffectLabels.join(', ')}`); - } - - return lines; } private formatSupportResult(result: SupportResult) { @@ -9913,7 +9918,7 @@ export class BattleScene extends Phaser.Scene { const growthEntries = this.combatGrowthEntries(result); if (result.bondDamageBonus > 0 && result.bondLabel) { - await this.showBondCombatEffect(result, left + panelWidth / 2, top + 106, attackerX, groundY - 70, depth + 15); + await this.showBondCombatEffect(result, left + panelWidth / 2, top + 140, attackerX, groundY - 70, depth + 15); } await this.delay(180); @@ -9923,7 +9928,7 @@ export class BattleScene extends Phaser.Scene { } else { this.showCombatMiss(defenderX, groundY - 70, depth + 14); } - const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 226, top + 196, 452, depth + 16); + const outcomeCard = this.renderCombatOutcomeCard(result, left + panelWidth / 2 - 226, top + 184, 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}`); @@ -9959,29 +9964,18 @@ export class BattleScene extends Phaser.Scene { } private renderCombatOutcomeSummaryCard(result: CombatResult, x: number, y: number, width: number, depth: number) { - const height = 84; - const isStrategy = result.action === 'strategy'; + const height = 96; const icon: BattleUiIconKey = result.defeated ? 'critical' : !result.hit ? 'move' : result.critical ? 'critical' - : isStrategy + : result.action === 'strategy' ? '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 title = this.combatOutcomeTitle(result); + const tone = this.combatOutcomeTone(result); const container = this.trackCombatObject(this.add.container(x, y)); container.setDepth(depth); container.setAlpha(0); @@ -9995,7 +9989,7 @@ export class BattleScene extends Phaser.Scene { iconFrame.setStrokeStyle(1, tone, 0.72); const iconImage = this.createBattleUiIcon(42, height / 2, icon, 58); - const titleText = this.add.text(84, 10, title, { + const titleText = this.add.text(84, 9, title, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '23px', color: result.hit ? '#fff2b8' : '#d9f1ff', @@ -10003,8 +9997,12 @@ export class BattleScene extends Phaser.Scene { 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, { + const detail = result.defeated + ? `${result.defender.name} 퇴각` + : result.hit + ? `${result.defender.name} HP ${result.previousDefenderHp} → ${result.defender.hp}` + : `${result.defender.name} HP 유지 ${result.defender.hp}/${result.defender.maxHp}`; + const detailText = this.add.text(86, 39, detail, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: result.defeated ? '#ffb6a6' : '#c8d2dd', @@ -10012,7 +10010,8 @@ export class BattleScene extends Phaser.Scene { fixedWidth: 190 }); - const value = result.hit ? `-${result.damage}` : '0'; + const actualDamage = this.combatDamageValue(result); + const value = result.hit ? `-${actualDamage}` : '0'; const valueText = this.add.text(width - 18, 9, value, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '31px', @@ -10021,7 +10020,7 @@ export class BattleScene extends Phaser.Scene { }); valueText.setOrigin(1, 0); - const valueLabel = this.add.text(width - 18, 47, '피해', { + const valueLabel = this.add.text(width - 18, 45, '실제 피해', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf', @@ -10029,20 +10028,70 @@ export class BattleScene extends Phaser.Scene { }); valueLabel.setOrigin(1, 0); - container.add([bg, iconFrame, iconImage, titleText, detailText, valueText, valueLabel]); + const expectedText = this.add.text(width - 18, 61, `예상 ${result.expectedDamage}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#c8d2dd', + fontStyle: '700' + }); + expectedText.setOrigin(1, 0); - const badgeY = 64; - this.addCombatOutcomeChip(container, 86, badgeY, result.action === 'strategy' ? 'success' : 'hit', result.hit ? `${result.hitRate}%` : '실패', tone); + container.add([bg, iconFrame, iconImage, titleText, detailText, valueText, valueLabel, expectedText]); + + const badgeY = 78; + this.addCombatOutcomeChip(container, 86, badgeY, result.action === 'strategy' ? 'success' : 'hit', this.combatJudgementChipLabel(result), 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); + const extraChip = this.combatOutcomeExtraChip(result); + if (extraChip) { + this.addCombatOutcomeChip(container, width - 114, badgeY, extraChip.icon, extraChip.label, extraChip.tone, 96); } this.tweens.add({ targets: container, alpha: 1, scale: 1, y: y - 4, duration: 160, ease: 'Sine.easeOut' }); return container; } + private combatOutcomeTitle(result: CombatResult) { + if (result.defeated) { + return '퇴각'; + } + if (!result.hit) { + return result.action === 'strategy' ? '책략 실패' : '회피'; + } + if (result.critical) { + return '치명타'; + } + return result.action === 'strategy' ? '책략 성공' : '명중'; + } + + private combatOutcomeTone(result: CombatResult) { + return result.defeated || result.critical ? 0xd8732c : result.hit ? this.previewAccentColor(result) : 0x83d6ff; + } + + private combatJudgementChipLabel(result: CombatResult) { + if (!result.hit) { + return result.action === 'strategy' ? '실패' : '회피'; + } + return result.action === 'strategy' ? `${result.hitRate}%` : `${result.hitRate}%`; + } + + private combatCounterWillTrigger(result: CombatResult) { + return !result.isCounter && result.counterAvailable && result.defender.hp > 0 && result.attacker.hp > 0; + } + + private combatOutcomeExtraChip(result: CombatResult): { icon: BattleUiIconKey; label: string; tone: number } | undefined { + if (result.defeated) { + return { icon: 'counter', label: '퇴각', tone: 0xd8732c }; + } + if (this.combatCounterWillTrigger(result)) { + return { icon: 'counter', label: '반격 예정', tone: 0xb64a45 }; + } + if (result.bondDamageBonus > 0) { + return { icon: 'leadership', label: `공명 +${result.bondDamageBonus}%`, tone: palette.gold }; + } + return undefined; + } + private addCombatOutcomeChip( container: Phaser.GameObjects.Container, x: number, @@ -10209,7 +10258,9 @@ export class BattleScene extends Phaser.Scene { 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) { + if (result.defeated) { + this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '퇴각', depth + 1, 58); + } else if (this.combatCounterWillTrigger(result) || result.counter) { this.renderCombatRibbonBadge(x + 280, y + 38, 'counter', '반격', depth + 1, 58); } }