Improve combat result feedback
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user