diff --git a/docs/result-feedback-local-map-popup.png b/docs/result-feedback-local-map-popup.png new file mode 100644 index 0000000..101bb1a Binary files /dev/null and b/docs/result-feedback-local-map-popup.png differ diff --git a/docs/result-feedback-local-post-attack.png b/docs/result-feedback-local-post-attack.png new file mode 100644 index 0000000..6034150 Binary files /dev/null and b/docs/result-feedback-local-post-attack.png differ diff --git a/docs/result-feedback-local-target-preview-card.png b/docs/result-feedback-local-target-preview-card.png new file mode 100644 index 0000000..e37d63b Binary files /dev/null and b/docs/result-feedback-local-target-preview-card.png differ diff --git a/docs/result-feedback-prod-map-popup.png b/docs/result-feedback-prod-map-popup.png new file mode 100644 index 0000000..791c630 Binary files /dev/null and b/docs/result-feedback-prod-map-popup.png differ diff --git a/docs/result-feedback-prod-post-attack.png b/docs/result-feedback-prod-post-attack.png new file mode 100644 index 0000000..f79a523 Binary files /dev/null and b/docs/result-feedback-prod-post-attack.png differ diff --git a/docs/result-feedback-prod-target-preview.png b/docs/result-feedback-prod-target-preview.png new file mode 100644 index 0000000..185ffcf Binary files /dev/null and b/docs/result-feedback-prod-target-preview.png differ diff --git a/docs/result-feedback-report.md b/docs/result-feedback-report.md new file mode 100644 index 0000000..16380ed --- /dev/null +++ b/docs/result-feedback-report.md @@ -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. diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 9ea1d6f..93d3e7f 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -6545,6 +6545,11 @@ export class BattleScene extends Phaser.Scene { await this.delay(180); 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)); } @@ -6569,6 +6574,8 @@ export class BattleScene extends Phaser.Scene { const result = this.resolveSupportAction(user, target, usable); this.clearMarkers(); await this.playSupportCutIn(result); + this.showSupportMapResult(result); + await this.delay(160); 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.faceUnitToward(attacker, defender); if (hit) { - this.flashDamage(defender, damage, critical); + this.flashDamage(defender, damage, critical, previousDefenderHp); } else { - this.flashMiss(defender); + this.flashMiss(defender, this.missFeedbackLabel(action, usable)); } if (defender.hp <= 0) { @@ -8684,7 +8691,7 @@ export class BattleScene extends Phaser.Scene { if (healAmount > 0) { target.hp = Math.min(target.maxHp, target.hp + healAmount); this.syncUnitMotion(target); - this.flashSupport(target, `+${healAmount}`, '#a8ffd0'); + this.flashSupport(target, `회복 +${healAmount}`, '#a8ffd0', `HP ${previousTargetHp}→${target.hp}`); this.updateMiniMap(); } @@ -8698,7 +8705,7 @@ export class BattleScene extends Phaser.Scene { criticalBonus: usable.criticalBonus ?? 0 }; 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)); @@ -8893,7 +8900,7 @@ export class BattleScene extends Phaser.Scene { } private formatCombatResult(result: CombatResult) { - const headline = this.combatResultHeadline(result); + const headline = this.combatResultSummaryLine(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(', ')}` : ''; @@ -8907,10 +8914,42 @@ export class BattleScene extends Phaser.Scene { 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) { 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) { const damage = this.combatDamageValue(result); const outcome = !result.hit @@ -8926,11 +8965,11 @@ export class BattleScene extends Phaser.Scene { } private combatResolutionLine(result: CombatResult) { - const successLabel = result.action === 'strategy' ? '성공' : '명중'; + const successLabel = result.action === 'strategy' || result.usable?.command === '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}`; + const verdict = result.hit ? `${successLabel} ${result.hitRate}% 통과` : `${successLabel} ${result.hitRate}% 실패`; + const critical = result.hit ? (result.critical ? '치명 발생' : `치명 ${result.criticalRate}% 미발동`) : '치명 없음'; + return `판정: ${verdict} · ${critical} · 예측 ${result.expectedDamage} / 실제 ${actual}`; } private formatCombatOutcome(result: CombatResult) { @@ -8943,10 +8982,7 @@ export class BattleScene extends Phaser.Scene { } private formatCounterLine(result: CombatResult) { - 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}`; + return `반격 결과: ${this.combatResultSummaryLine(result)}`; } private combatGrowthLines(result: CombatResult) { @@ -8971,15 +9007,36 @@ 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)}`; + ? `결과: 회복 +${result.healAmount} · HP ${result.previousTargetHp}→${result.target.hp}` + : `결과: 강화 ${result.buff?.turns ?? result.usable.duration ?? 1}턴 · ${this.supportBuffCompactText(result.buff)}`; + 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})` - : `${result.target.name} 강화: 공격 +${result.buff?.attackBonus ?? 0} / 명중 +${result.buff?.hitBonus ?? 0} / 치명 +${result.buff?.criticalBonus ?? 0}`; - return `${result.user.name} ${result.usable.name}\n${effect}\n${this.formatCharacterGrowth(result.characterGrowth)}\n${this.formatEquipmentGrowth(result.equipmentGrowth)}`; + } + + private supportResultSummaryLine(result: SupportResult) { + if (result.usable.effect === 'heal') { + 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) { @@ -9922,6 +9979,11 @@ export class BattleScene extends Phaser.Scene { await this.delay(180); 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); } @@ -10179,10 +10241,18 @@ export class BattleScene extends Phaser.Scene { } private formatEnemyCombatResult(result: CombatResult, behavior: EnemyAiBehavior) { - 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}`; - const counter = result.counter ? ` / ${this.formatCounterLine(result.counter)}` : ''; - return `${result.attacker.name} ${this.enemyBehaviorLabel(behavior)} 공격: ${result.defender.name} ${outcome}${defeated}${counter}`; + const counter = result.counter ? `\n${this.formatCounterLine(result.counter)}` : ''; + return `${this.combatResultSummaryLine(result)} · ${this.enemyBehaviorResultLabel(behavior)}${counter}`; + } + + private enemyBehaviorResultLabel(behavior: EnemyAiBehavior) { + if (behavior === 'aggressive') { + return '추격'; + } + if (behavior === 'hold') { + return '고수'; + } + return '경계'; } private async playCombatCutIn(result: CombatResult) { @@ -13397,22 +13467,15 @@ export class BattleScene extends Phaser.Scene { 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); if (!view) { return; } - const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, critical ? `치명 -${damage}` : `-${damage}`, { - fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: critical ? '22px' : '20px', - color: critical ? '#ff8f5f' : '#ffdf7b', - stroke: '#220909', - strokeThickness: 4, - fontStyle: '700' - }); - popup.setOrigin(0.5); - popup.setDepth(30); + const title = unit.hp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`; + const detail = previousHp === undefined ? '' : `HP ${previousHp}→${unit.hp}`; + this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30); view.sprite.setTintFill(0xffe0a3); this.time.delayedCall(120, () => { @@ -13424,33 +13487,15 @@ export class BattleScene extends Phaser.Scene { 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); if (!view) { return; } - const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, '회피', { - 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.showMapResultPopup(unit, [label], '#d9f1ff', '#071623', label.length > 4 ? 17 : 19, 26); this.tweens.add({ targets: [view.sprite, view.label], @@ -13459,33 +13504,15 @@ export class BattleScene extends Phaser.Scene { yoyo: true, 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); if (!view) { return; } - const popup = this.add.text(view.sprite.x, view.sprite.y - this.layout.tileSize * 0.5, label, { - 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.showMapResultPopup(unit, [label, detail], color, '#071623', 18, 28); this.tweens.add({ targets: view.sprite, @@ -13494,12 +13521,91 @@ export class BattleScene extends Phaser.Scene { yoyo: true, 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({ targets: popup, - y: popup.y - 24, + y: popup.y - lift, alpha: 0, - duration: 560, + duration: 680, + delay: 80, ease: 'Sine.easeOut', onComplete: () => popup.destroy() }); @@ -13592,11 +13698,7 @@ export class BattleScene extends Phaser.Scene { } private compactBattleLogEntry(message: string) { - const firstLine = message - .split('\n') - .map((line) => line.trim()) - .find(Boolean); - + const firstLine = this.firstMessageLine(message); if (!firstLine) { return ''; } @@ -13605,6 +13707,31 @@ export class BattleScene extends Phaser.Scene { 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) { 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 } { const { text } = this.battleLogDisplayParts(entry); + if (text.includes('반격')) { + return { icon: 'counter', tone: 0xb64a45 }; + } if (text.includes('퇴각') || text.includes('격파') || text.includes('전투 불능')) { return { icon: 'critical', tone: 0xd8732c }; } if (text.includes('치명')) { return { icon: 'critical', tone: 0xd8732c }; } - if (text.includes('반격')) { - return { icon: 'counter', tone: 0xb64a45 }; + if (text.includes('책략 실패') || text.includes('효과 실패') || text.includes('회피')) { + return { icon: 'move', tone: 0x83d6ff }; } if (text.includes('책략') || text.includes('화계') || text.includes('교란') || text.includes('고무')) { 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 }; } + if (text.includes('강화') || text.includes('상태효과')) { + return { icon: 'focus', tone: palette.gold }; + } if (text.includes('이동') || text.includes('접근')) { return { icon: 'move', tone: 0x83d6ff }; } @@ -13678,7 +13811,7 @@ export class BattleScene extends Phaser.Scene { countText.setOrigin(1, 0); countText.setDepth(31); - const entries = this.battleLog.slice(0, 4); + const entries = this.battleLog.slice(0, 3); if (entries.length === 0) { const emptyText = this.trackSideObject(this.add.text(x + 12, y + 45, '아직 기록된 행동이 없습니다.', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -13690,21 +13823,21 @@ export class BattleScene extends Phaser.Scene { } entries.forEach((entry, index) => { - const rowY = y + 32 + index * 20; + const rowY = y + 32 + index * 28; const { turn, text } = this.battleLogDisplayParts(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.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.setStrokeStyle(1, visual.tone, 0.5); - const icon = this.trackSideIcon(x + 21, rowY + 9, visual.icon, 17); + iconFrame.setStrokeStyle(1, visual.tone, index === 0 ? 0.72 : 0.52); + const icon = this.trackSideIcon(x + 23, rowY + 12, visual.icon, 21); 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', fontSize: '10px', color: '#9fb0bf', @@ -13713,12 +13846,12 @@ export class BattleScene extends Phaser.Scene { turnText.setOrigin(1, 0); 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', - fontSize: '12px', + fontSize: index === 0 ? '13px' : '12px', color: index === 0 ? '#e8dfca' : '#c8d2dd', fontStyle: index === 0 ? '700' : '400', - fixedWidth: width - 84 + fixedWidth: width - 92 })); eventText.setDepth(32); }); @@ -14289,7 +14422,12 @@ export class BattleScene extends Phaser.Scene { } this.renderEquipmentSummary(unit, left, equipmentTop, width, hasEffects); 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); } @@ -14622,6 +14760,43 @@ export class BattleScene extends Phaser.Scene { 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) { const safeY = Math.max(this.sideContentTop(), Math.min(y, this.sideContentBottom() - height)); const compact = height < 60;