Improve combat result feedback

This commit is contained in:
2026-07-03 06:01:38 +09:00
parent 6ff336969a
commit 4402c49202

View File

@@ -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);
}
}