Polish battle result feedback

This commit is contained in:
2026-07-03 21:18:08 +09:00
parent d7bfdfb508
commit cc373f22e4
8 changed files with 304 additions and 98 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -0,0 +1,31 @@
# Battle Result Feedback Verification
Date: 2026-07-03
## Scope
- Reworked post-action combat feedback for attack, strategy, item, heal, buff, miss, critical, counter, and defeat summaries.
- Kept map feedback short and off the unit body.
- Added compact icon result cards in the unit detail panel when the next unit is auto-selected after an action.
## Build
- `pnpm build`: pass
- `pnpm deploy:nas`: pass
## Browser Checks
- Local: `http://127.0.0.1:4195/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-local-card`
- Deployed: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-20260703`
- PC viewport: 1280x720
- Console errors: 0 local, 0 deployed
## Captures
- Local target preview: `docs/result-feedback-local-target-preview-card.png`
- Local map popup: `docs/result-feedback-local-map-popup.png`
- Local post-attack card: `docs/result-feedback-local-post-attack.png`
- Deployed target preview: `docs/result-feedback-prod-target-preview.png`
- Deployed map popup: `docs/result-feedback-prod-map-popup.png`
- Deployed post-attack card: `docs/result-feedback-prod-post-attack.png`
## Result
- Attack result shows `퇴각 -19 / HP 19→0` as a short floating map popup.
- The right detail panel shows a compact icon card with `관우 → 황건병 · 공격` and `피해 19 · 퇴각 · HP 19→0`.
- No selection ring, base, or large badge was added.

View File

@@ -6545,6 +6545,11 @@ export class BattleScene extends Phaser.Scene {
await this.delay(180); await this.delay(180);
await this.playCombatCutIn(result.counter); await this.playCombatCutIn(result.counter);
} }
this.showCombatMapResult(result);
if (result.counter) {
this.showCombatMapResult(result.counter);
}
await this.delay(result.counter ? 220 : 160);
this.finishUnitAction(attacker, this.formatCombatResult(result)); this.finishUnitAction(attacker, this.formatCombatResult(result));
} }
@@ -6569,6 +6574,8 @@ export class BattleScene extends Phaser.Scene {
const result = this.resolveSupportAction(user, target, usable); const result = this.resolveSupportAction(user, target, usable);
this.clearMarkers(); this.clearMarkers();
await this.playSupportCutIn(result); await this.playSupportCutIn(result);
this.showSupportMapResult(result);
await this.delay(160);
this.finishUnitAction(user, this.formatSupportResult(result)); this.finishUnitAction(user, this.formatSupportResult(result));
} }
@@ -8619,9 +8626,9 @@ export class BattleScene extends Phaser.Scene {
this.recordCombatStats(attacker, defender, damage, defender.hp <= 0 && previousDefenderHp > 0); this.recordCombatStats(attacker, defender, damage, defender.hp <= 0 && previousDefenderHp > 0);
this.faceUnitToward(attacker, defender); this.faceUnitToward(attacker, defender);
if (hit) { if (hit) {
this.flashDamage(defender, damage, critical); this.flashDamage(defender, damage, critical, previousDefenderHp);
} else { } else {
this.flashMiss(defender); this.flashMiss(defender, this.missFeedbackLabel(action, usable));
} }
if (defender.hp <= 0) { if (defender.hp <= 0) {
@@ -8684,7 +8691,7 @@ export class BattleScene extends Phaser.Scene {
if (healAmount > 0) { if (healAmount > 0) {
target.hp = Math.min(target.maxHp, target.hp + healAmount); target.hp = Math.min(target.maxHp, target.hp + healAmount);
this.syncUnitMotion(target); this.syncUnitMotion(target);
this.flashSupport(target, `+${healAmount}`, '#a8ffd0'); this.flashSupport(target, `회복 +${healAmount}`, '#a8ffd0', `HP ${previousTargetHp}${target.hp}`);
this.updateMiniMap(); this.updateMiniMap();
} }
@@ -8698,7 +8705,7 @@ export class BattleScene extends Phaser.Scene {
criticalBonus: usable.criticalBonus ?? 0 criticalBonus: usable.criticalBonus ?? 0
}; };
this.battleBuffs.set(target.id, buff); this.battleBuffs.set(target.id, buff);
this.flashSupport(target, usable.name, '#ffdf7b'); this.flashSupport(target, `강화 ${usable.duration ?? 1}`, '#ffdf7b', this.supportBuffCompactText(buff));
} }
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable)); const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
@@ -8893,7 +8900,7 @@ export class BattleScene extends Phaser.Scene {
} }
private formatCombatResult(result: CombatResult) { private formatCombatResult(result: CombatResult) {
const headline = this.combatResultHeadline(result); const headline = this.combatResultSummaryLine(result);
const resolution = this.combatResolutionLine(result); const resolution = this.combatResolutionLine(result);
const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : ''; const summaryBondBonus = result.bondDamageBonus > 0 && result.bondLabel ? `\n공명 효과 ${result.bondLabel}: 피해 +${result.bondDamageBonus}%` : '';
const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : ''; const summaryEquipment = result.equipmentEffectLabels.length > 0 ? `\n장비 효과: ${result.equipmentEffectLabels.join(', ')}` : '';
@@ -8907,10 +8914,42 @@ export class BattleScene extends Phaser.Scene {
return result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action]; return result.isCounter ? '반격' : result.usable?.name ?? commandLabels[result.action];
} }
private combatActionLogName(result: CombatResult) {
if (result.isCounter) {
return '반격';
}
if (result.usable) {
return result.usable.name;
}
if (result.action === 'strategy') {
return '책략';
}
if (result.action === 'item') {
return '도구';
}
return '공격';
}
private combatDamageValue(result: CombatResult) { private combatDamageValue(result: CombatResult) {
return result.hit ? result.damage : 0; return result.hit ? result.damage : 0;
} }
private combatResultSummaryLine(result: CombatResult) {
const hpLine = result.hit ? ` · HP ${result.previousDefenderHp}${result.defender.hp}` : '';
const counter = !result.isCounter && result.counter ? ' · 반격 발생' : '';
return `${result.attacker.name}${result.defender.name} | ${this.combatActionLogName(result)} | ${this.combatOutcomeSummary(result)}${hpLine}${counter}`;
}
private combatOutcomeSummary(result: CombatResult) {
if (!result.hit) {
return this.missFeedbackLabel(result.action, result.usable);
}
const critical = result.critical ? '치명 ' : '';
const defeated = result.defeated ? ' · 퇴각' : '';
return `${critical}피해 ${result.damage}${defeated}`;
}
private combatResultHeadline(result: CombatResult) { private combatResultHeadline(result: CombatResult) {
const damage = this.combatDamageValue(result); const damage = this.combatDamageValue(result);
const outcome = !result.hit const outcome = !result.hit
@@ -8926,11 +8965,11 @@ export class BattleScene extends Phaser.Scene {
} }
private combatResolutionLine(result: CombatResult) { private combatResolutionLine(result: CombatResult) {
const successLabel = result.action === 'strategy' ? '성공' : '명중'; const successLabel = result.action === 'strategy' || result.usable?.command === 'strategy' ? '성공' : '명중';
const actual = this.combatDamageValue(result); const actual = this.combatDamageValue(result);
const verdict = result.hit ? `${successLabel} ${result.hitRate}%` : `${successLabel} 실패(${result.hitRate}%)`; const verdict = result.hit ? `${successLabel} ${result.hitRate}% 통과` : `${successLabel} ${result.hitRate}% 실패`;
const critical = result.critical ? '치명 발생' : `치명 ${result.criticalRate}%`; const critical = result.hit ? (result.critical ? '치명 발생' : `치명 ${result.criticalRate}% 미발동`) : '치명 없음';
return `판정: ${verdict} · ${critical} · 예 ${result.expectedDamage}/실제 ${actual}`; return `판정: ${verdict} · ${critical} · 예 ${result.expectedDamage} / 실제 ${actual}`;
} }
private formatCombatOutcome(result: CombatResult) { private formatCombatOutcome(result: CombatResult) {
@@ -8943,10 +8982,7 @@ export class BattleScene extends Phaser.Scene {
} }
private formatCounterLine(result: CombatResult) { private formatCounterLine(result: CombatResult) {
const counterOutcome = result.hit ? `${result.critical ? '치명 ' : ''}${result.damage} 피해` : '회피됨'; return `반격 결과: ${this.combatResultSummaryLine(result)}`;
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) { private combatGrowthLines(result: CombatResult) {
@@ -8971,15 +9007,36 @@ export class BattleScene extends Phaser.Scene {
private formatSupportResult(result: SupportResult) { private formatSupportResult(result: SupportResult) {
const supportEffect = const supportEffect =
result.usable.effect === 'heal' result.usable.effect === 'heal'
? `${result.target.name} 병력 +${result.healAmount} (${result.target.hp}/${result.target.maxHp})` ? `결과: 회복 +${result.healAmount} · HP ${result.previousTargetHp}${result.target.hp}`
: `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`; : `결과: 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`;
return `${result.user.name} ${result.usable.name}\n${supportEffect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`; return `${this.supportResultSummaryLine(result)}\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})` private supportResultSummaryLine(result: SupportResult) {
: `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`; if (result.usable.effect === 'heal') {
return `${result.user.name} ${result.usable.name}\n${effect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`; return `${result.user.name} ${result.target.name} | ${result.usable.name} | 회복 +${result.healAmount} · HP ${result.previousTargetHp}${result.target.hp}`;
}
return `${result.user.name}${result.target.name} | ${result.usable.name} | 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`;
}
private supportBuffCompactText(buff?: BattleBuffState) {
if (!buff) {
return '강화 유지';
}
return [`공+${buff.attackBonus}`, `명+${buff.hitBonus}`, `치+${buff.criticalBonus}`].join(' ');
}
private missFeedbackLabel(action: DamageCommand, usable?: BattleUsable) {
if (action === 'strategy' || usable?.command === 'strategy') {
return '책략 실패';
}
if (action === 'item' || usable?.command === 'item') {
return '효과 실패';
}
return '회피';
} }
private commandResultMessage(unit: UnitData, command: BattleCommand) { private commandResultMessage(unit: UnitData, command: BattleCommand) {
@@ -9922,6 +9979,11 @@ export class BattleScene extends Phaser.Scene {
await this.delay(180); await this.delay(180);
await this.playCombatCutIn(result.counter); await this.playCombatCutIn(result.counter);
} }
this.showCombatMapResult(result);
if (result.counter) {
this.showCombatMapResult(result.counter);
}
await this.delay(result.counter ? 220 : 160);
return this.formatEnemyCombatResult(result, behavior); return this.formatEnemyCombatResult(result, behavior);
} }
@@ -10179,10 +10241,18 @@ export class BattleScene extends Phaser.Scene {
} }
private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) { private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) {
const outcome = result.hit ? `${result.critical ? '치명타 ' : ''}${result.damage} 피해` : '회피됨'; const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : '';
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`; return `${this.combatResultSummaryLine(result)} · ${this.enemyBehaviorResultLabel(behavior)}${counter}`;
const counter = result.counter ? ` / ${this.formatCounterLine(result.counter)}` : ''; }
return `${result.attacker.name} ${this.enemyBehaviorLabel(behavior)} 공격: ${result.defender.name} ${outcome}${defeated}${counter}`;
private enemyBehaviorResultLabel(behavior: EnemyAiBehavior) {
if (behavior === 'aggressive') {
return '추격';
}
if (behavior === 'hold') {
return '고수';
}
return '경계';
} }
private async playCombatCutIn(result: CombatResult) { private async playCombatCutIn(result: CombatResult) {
@@ -13397,22 +13467,15 @@ export class BattleScene extends Phaser.Scene {
this.syncUnitMotion(unit, view, direction); this.syncUnitMotion(unit, view, direction);
} }
private flashDamage(unit: UnitData, damage: number, critical = false) { private flashDamage(unit: UnitData, damage: number, critical = false, previousHp?: number) {
const view = this.unitViews.get(unit.id); const view = this.unitViews.get(unit.id);
if (!view) { if (!view) {
return; return;
} }
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, critical ? `치명 -${damage}` : `-${damage}`, { const title = unit.hp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`;
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', const detail = previousHp === undefined ? '' : `HP ${previousHp}${unit.hp}`;
fontSize: critical ? '22px' : '20px', this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30);
color: critical ? '#ff8f5f' : '#ffdf7b',
stroke: '#220909',
strokeThickness: 4,
fontStyle: '700'
});
popup.setOrigin(0.5);
popup.setDepth(30);
view.sprite.setTintFill(0xffe0a3); view.sprite.setTintFill(0xffe0a3);
this.time.delayedCall(120, () => { this.time.delayedCall(120, () => {
@@ -13424,33 +13487,15 @@ export class BattleScene extends Phaser.Scene {
this.applyUnitLegibilityStyle(unit, view); this.applyUnitLegibilityStyle(unit, view);
} }
}); });
this.tweens.add({
targets: popup,
y: popup.y - 28,
alpha: 0,
duration: 520,
ease: 'Sine.easeOut',
onComplete: () => popup.destroy()
});
} }
private flashMiss(unit: UnitData) { private flashMiss(unit: UnitData, label = '회피') {
const view = this.unitViews.get(unit.id); const view = this.unitViews.get(unit.id);
if (!view) { if (!view) {
return; return;
} }
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, '회피', { this.showMapResultPopup(unit, [label], '#d9f1ff', '#071623', label.length > 4 ? 17 : 19, 26);
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
color: '#d9f1ff',
stroke: '#071623',
strokeThickness: 4,
fontStyle: '700'
});
popup.setOrigin(0.5);
popup.setDepth(30);
this.tweens.add({ this.tweens.add({
targets: [view.sprite, view.label], targets: [view.sprite, view.label],
@@ -13459,33 +13504,15 @@ export class BattleScene extends Phaser.Scene {
yoyo: true, yoyo: true,
ease: 'Sine.easeOut' ease: 'Sine.easeOut'
}); });
this.tweens.add({
targets: popup,
y: popup.y - 24,
alpha: 0,
duration: 500,
ease: 'Sine.easeOut',
onComplete: () => popup.destroy()
});
} }
private flashSupport(unit: UnitData, label: string, color: string) { private flashSupport(unit: UnitData, label: string, color: string, detail = '') {
const view = this.unitViews.get(unit.id); const view = this.unitViews.get(unit.id);
if (!view) { if (!view) {
return; return;
} }
const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, label, { this.showMapResultPopup(unit, [label, detail], color, '#071623', 18, 28);
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
color,
stroke: '#071623',
strokeThickness: 4,
fontStyle: '700'
});
popup.setOrigin(0.5);
popup.setDepth(30);
this.tweens.add({ this.tweens.add({
targets: view.sprite, targets: view.sprite,
@@ -13494,12 +13521,91 @@ export class BattleScene extends Phaser.Scene {
yoyo: true, yoyo: true,
ease: 'Sine.easeOut' ease: 'Sine.easeOut'
}); });
}
private showCombatMapResult(result: CombatResult) {
if (!result.hit) {
this.showMapResultPopup(
result.defender,
[this.missFeedbackLabel(result.action, result.usable)],
'#d9f1ff',
'#071623',
19,
26
);
return;
}
this.showMapResultPopup(
result.defender,
[this.combatMapDamageTitle(result), `HP ${result.previousDefenderHp}${result.defender.hp}`],
result.critical ? '#ff8f5f' : '#ffdf7b',
'#220909',
result.critical ? 20 : 18,
result.critical ? 34 : 30
);
}
private combatMapDamageTitle(result: CombatResult) {
const damage = `-${result.damage}`;
if (result.defeated) {
return `${result.critical ? '치명 ' : ''}퇴각 ${damage}`;
}
return `${result.critical ? '치명 ' : '피해 '}${damage}`;
}
private showSupportMapResult(result: SupportResult) {
if (result.usable.effect === 'heal') {
this.showMapResultPopup(
result.target,
[`회복 +${result.healAmount}`, `HP ${result.previousTargetHp}${result.target.hp}`],
'#a8ffd0',
'#071623',
18,
28
);
return;
}
this.showMapResultPopup(
result.target,
[`강화 ${result.buff?.turns ?? result.usable.duration ?? 1}`, this.supportBuffCompactText(result.buff)],
'#ffdf7b',
'#071623',
18,
28
);
}
private showMapResultPopup(unit: UnitData, lines: string[], color: string, stroke: string, fontSize: number, lift: number) {
const view = this.unitViews.get(unit.id);
if (!view) {
return;
}
const popup = this.add.text(
view.sprite.x,
view.sprite.y - this.layout.tileSize * 0.64,
lines.filter(Boolean).join('\n'),
{
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: `${fontSize}px`,
color,
stroke,
strokeThickness: 4,
fontStyle: '700',
align: 'center',
lineSpacing: -3
}
);
popup.setOrigin(0.5, 1);
popup.setDepth(35);
this.tweens.add({ this.tweens.add({
targets: popup, targets: popup,
y: popup.y - 24, y: popup.y - lift,
alpha: 0, alpha: 0,
duration: 560, duration: 680,
delay: 80,
ease: 'Sine.easeOut', ease: 'Sine.easeOut',
onComplete: () => popup.destroy() onComplete: () => popup.destroy()
}); });
@@ -13592,11 +13698,7 @@ export class BattleScene extends Phaser.Scene {
} }
private compactBattleLogEntry(message: string) { private compactBattleLogEntry(message: string) {
const firstLine = message const firstLine = this.firstMessageLine(message);
.split('\n')
.map((line) => line.trim())
.find(Boolean);
if (!firstLine) { if (!firstLine) {
return ''; return '';
} }
@@ -13605,6 +13707,31 @@ export class BattleScene extends Phaser.Scene {
return this.truncateBattleLogText(normalized, 42); return this.truncateBattleLogText(normalized, 42);
} }
private firstMessageLine(message: string) {
return (
message
.split('\n')
.map((line) => line.trim())
.find(Boolean) ?? ''
);
}
private actionResultSummaryParts(line: string) {
const parts = line
.split('|')
.map((part) => part.trim())
.filter(Boolean);
if (parts.length < 3 || !parts[0].includes('→')) {
return undefined;
}
return {
subject: parts[0],
action: parts[1],
outcome: parts.slice(2).join(' · ')
};
}
private truncateBattleLogText(text: string, maxLength: number) { private truncateBattleLogText(text: string, maxLength: number) {
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text; return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text;
} }
@@ -13619,21 +13746,27 @@ export class BattleScene extends Phaser.Scene {
private battleLogVisual(entry: string): { icon: BattleUiIconKey; tone: number } { private battleLogVisual(entry: string): { icon: BattleUiIconKey; tone: number } {
const { text } = this.battleLogDisplayParts(entry); const { text } = this.battleLogDisplayParts(entry);
if (text.includes('반격')) {
return { icon: 'counter', tone: 0xb64a45 };
}
if (text.includes('퇴각') || text.includes('격파') || text.includes('전투 불능')) { if (text.includes('퇴각') || text.includes('격파') || text.includes('전투 불능')) {
return { icon: 'critical', tone: 0xd8732c }; return { icon: 'critical', tone: 0xd8732c };
} }
if (text.includes('치명')) { if (text.includes('치명')) {
return { icon: 'critical', tone: 0xd8732c }; return { icon: 'critical', tone: 0xd8732c };
} }
if (text.includes('반격')) { if (text.includes('책략 실패') || text.includes('효과 실패') || text.includes('회피')) {
return { icon: 'counter', tone: 0xb64a45 }; return { icon: 'move', tone: 0x83d6ff };
} }
if (text.includes('책략') || text.includes('화계') || text.includes('교란') || text.includes('고무')) { if (text.includes('책략') || text.includes('화계') || text.includes('교란') || text.includes('고무')) {
return { icon: 'strategy', tone: palette.blue }; return { icon: 'strategy', tone: palette.blue };
} }
if (text.includes('회복') || text.includes('병력') || text.includes('거점') || text.includes('상태효과')) { if (text.includes('회복') || text.includes('병력') || text.includes('거점')) {
return { icon: 'heal', tone: 0x59d18c }; return { icon: 'heal', tone: 0x59d18c };
} }
if (text.includes('강화') || text.includes('상태효과')) {
return { icon: 'focus', tone: palette.gold };
}
if (text.includes('이동') || text.includes('접근')) { if (text.includes('이동') || text.includes('접근')) {
return { icon: 'move', tone: 0x83d6ff }; return { icon: 'move', tone: 0x83d6ff };
} }
@@ -13678,7 +13811,7 @@ export class BattleScene extends Phaser.Scene {
countText.setOrigin(1, 0); countText.setOrigin(1, 0);
countText.setDepth(31); countText.setDepth(31);
const entries = this.battleLog.slice(0, 4); const entries = this.battleLog.slice(0, 3);
if (entries.length === 0) { if (entries.length === 0) {
const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', { const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
@@ -13690,21 +13823,21 @@ export class BattleScene extends Phaser.Scene {
} }
entries.forEach((entry, index) => { entries.forEach((entry, index) => {
const rowY = y + 32 + index * 20; const rowY = y + 32 + index * 28;
const { turn, text } = this.battleLogDisplayParts(entry); const { turn, text } = this.battleLogDisplayParts(entry);
const visual = this.battleLogVisual(entry); const visual = this.battleLogVisual(entry);
const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, 18, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.86)); const row = this.trackSideObject(this.add.rectangle(x + 8, rowY, width - 16, 24, index === 0 ? 0x17232e : 0x101820, index === 0 ? 0.98 : 0.88));
row.setOrigin(0); row.setOrigin(0);
row.setDepth(31); row.setDepth(31);
row.setStrokeStyle(1, visual.tone, index === 0 ? 0.62 : 0.28); row.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.34);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 21, rowY + 9, 18, 18, 0x0a0f14, 0.96)); const iconFrame = this.trackSideObject(this.add.rectangle(x + 23, rowY + 12, 22, 22, 0x0a0f14, 0.96));
iconFrame.setDepth(32); iconFrame.setDepth(32);
iconFrame.setStrokeStyle(1, visual.tone, 0.5); iconFrame.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.52);
const icon = this.trackSideIcon(x + 21, rowY + 9, visual.icon, 17); const icon = this.trackSideIcon(x + 23, rowY + 12, visual.icon, 21);
icon.setDepth(33); icon.setDepth(33);
const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + 3, turn, { const turnText = this.trackSideObject(this.add.text(x + width - 12, rowY + 6, turn, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '10px',
color: '#9fb0bf', color: '#9fb0bf',
@@ -13713,12 +13846,12 @@ export class BattleScene extends Phaser.Scene {
turnText.setOrigin(1, 0); turnText.setOrigin(1, 0);
turnText.setDepth(32); turnText.setDepth(32);
const eventText = this.trackSideObject(this.add.text(x + 36, rowY + 2, this.truncateBattleLogText(text, 32), { const eventText = this.trackSideObject(this.add.text(x + 42, rowY + 5, this.truncateBattleLogText(text, index === 0 ? 39 : 34), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: index === 0 ? '13px' : '12px',
color: index === 0 ? '#e8dfca' : '#c8d2dd', color: index === 0 ? '#e8dfca' : '#c8d2dd',
fontStyle: index === 0 ? '700' : '400', fontStyle: index === 0 ? '700' : '400',
fixedWidth: width - 84 fixedWidth: width - 92
})); }));
eventText.setDepth(32); eventText.setDepth(32);
}); });
@@ -14289,7 +14422,12 @@ export class BattleScene extends Phaser.Scene {
} }
this.renderEquipmentSummary(unit, left, equipmentTop, width, hasEffects); this.renderEquipmentSummary(unit, left, equipmentTop, width, hasEffects);
if (message) { if (message) {
this.renderPanelMessage(message, left, equipmentTop + 156, width, 42); const resultLine = this.actionResultSummaryParts(this.firstMessageLine(message));
if (resultLine) {
this.renderPanelActionResultMessage(resultLine, left, equipmentTop + 150, width);
} else {
this.renderPanelMessage(message, left, equipmentTop + 156, width, 42);
}
} }
this.showFacingIndicator(unit); this.showFacingIndicator(unit);
} }
@@ -14622,6 +14760,43 @@ export class BattleScene extends Phaser.Scene {
fill.setOrigin(0); fill.setOrigin(0);
} }
private renderPanelActionResultMessage(
result: { subject: string; action: string; outcome: string },
x: number,
y: number,
width: number
) {
const height = 54;
const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height));
const visual = this.battleLogVisual(`0T ${result.subject} | ${result.action} | ${result.outcome}`);
const bg = this.trackSideObject(this.add.rectangle(x, safeY, width, height, 0x101820, 0.97));
bg.setOrigin(0);
bg.setStrokeStyle(1, visual.tone, 0.72);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 24, safeY + 27, 34, 34, 0x0a0f14, 0.94));
iconFrame.setStrokeStyle(1, visual.tone, 0.64);
const icon = this.trackSideIcon(x + 24, safeY + 27, visual.icon, 31);
icon.setDepth(33);
const title = this.trackSideObject(this.add.text(x + 48, safeY + 7, this.truncateBattleLogText(`${result.subject} · ${result.action}`, 31), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 62
}));
title.setDepth(33);
const outcome = this.trackSideObject(this.add.text(x + 48, safeY + 27, this.truncateBattleLogText(result.outcome, 34), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: '#f2e3bf',
fontStyle: '700',
fixedWidth: width - 62
}));
outcome.setDepth(33);
}
private renderPanelMessage(text: string, x: number, y: number, width: number, height = 74) { private renderPanelMessage(text: string, x: number, y: number, width: number, height = 74) {
const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height)); const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height));
const compact = height < 60; const compact = height < 60;