From 2a4070d4f526888dab5e31c051c3d5637ee08546 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 2 Jul 2026 22:39:02 +0900 Subject: [PATCH] Show battle effect modifiers in tactical panels --- src/game/scenes/BattleScene.ts | 164 +++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 9 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index f383288..270eb83 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1292,6 +1292,8 @@ type CombatPreview = { equipmentEffectLabels: string[]; hitRate: number; criticalRate: number; + terrainDefenseBonus: number; + terrainHitPenalty: number; counterAvailable: boolean; terrainLabel: string; matchupLabel: string; @@ -1359,6 +1361,13 @@ type BattleBuffState = { criticalBonus: number; }; +type UnitEffectSummary = { + icon: BattleUiIconKey; + label: string; + value: string; + tone: number; +}; + type SavedUnitState = Pick & { equipment: UnitData['equipment']; direction?: UnitDirection; @@ -5060,6 +5069,66 @@ export class BattleScene extends Phaser.Scene { return parts.join(' / ') || '보정 없음'; } + private battleBuffBonusText(buff: BattleBuffState) { + const parts = [ + buff.attackBonus > 0 ? `공격 +${buff.attackBonus}` : '', + buff.hitBonus > 0 ? `명중 +${buff.hitBonus}` : '', + buff.criticalBonus > 0 ? `치명 +${buff.criticalBonus}` : '' + ].filter(Boolean); + return `${parts.join(' / ') || '효과 유지'} · ${buff.turns}턴`; + } + + private previewModifierLabels(preview: CombatPreview) { + const labels: string[] = []; + const buff = this.battleBuffs.get(preview.attacker.id); + const equipmentText = this.equipmentPreviewBonusText(preview); + + if (buff) { + labels.push(`${buff.label} ${this.battleBuffBonusText(buff)}`); + } + if (preview.bondDamageBonus > 0 && preview.bondLabel) { + labels.push(`공명 피해 +${preview.bondDamageBonus}%`); + } + if (equipmentText !== '보정 없음') { + labels.push(`장비 ${equipmentText}`); + } + if (preview.terrainDefenseBonus > 0) { + labels.push(`${preview.terrainLabel} 방어 +${preview.terrainDefenseBonus}`); + } + if (preview.terrainHitPenalty > 0) { + labels.push(`지형 명중 -${preview.terrainHitPenalty}`); + } + + return labels.length > 0 ? labels : ['추가 보정 없음']; + } + + private supportPreviewModifierLabels(preview: SupportPreview) { + if (preview.usable.effect === 'heal') { + const baseBonus = preview.healAmount - preview.usable.power; + return [ + `${this.usableChannelLabel(preview.usable)} 회복 ${preview.healAmount}`, + baseBonus > 0 ? `능력/장비 +${baseBonus}` : '추가 보정 없음' + ]; + } + + return [ + `공격 +${preview.attackBonus}`, + `명중 +${preview.hitBonus}`, + `치명 +${preview.criticalBonus}`, + `${preview.duration}턴 유지` + ]; + } + + private compactModifierText(labels: string[], limit = 2) { + const text = labels.slice(0, limit).join(' · '); + const suffix = labels.length > limit ? ` · 외 ${labels.length - limit}` : ''; + return this.truncateUiText(`${text}${suffix}`, 48); + } + + private truncateUiText(text: string, maxLength: number) { + return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 3))}...` : text; + } + private previewActionLabel(preview: CombatPreview) { return preview.usable?.name ?? commandLabels[preview.action]; } @@ -5102,7 +5171,7 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; - const height = 238; + const height = 264; const y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 344, this.sideContentBottom(10) - height)); const accent = this.previewAccentColor(preview); const defenderClass = getUnitClass(preview.defender.classKey); @@ -5152,8 +5221,16 @@ export class BattleScene extends Phaser.Scene { this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c); this.renderPreviewForecastRow(preview, left + 10, y + 184, width - 20, accent); + this.renderPreviewModifierStrip( + left + 10, + y + 216, + width - 20, + 'mastery', + this.compactModifierText(this.previewModifierLabels(preview)), + accent + ); - const badgeY = y + 210; + const badgeY = y + 236; const maxBadgeRight = left + width - 10; let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), maxBadgeRight); @@ -5173,7 +5250,7 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; - const height = 238; + const height = 264; const y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 344, this.sideContentBottom(10) - height)); const accent = this.usableAccentColor(preview.usable); @@ -5231,8 +5308,16 @@ export class BattleScene extends Phaser.Scene { } this.renderSupportForecastRow(preview, left + 10, y + 184, width - 20, accent); + this.renderPreviewModifierStrip( + left + 10, + y + 216, + width - 20, + preview.usable.effect === 'heal' ? this.usablePowerIcon(preview.usable) : 'leadership', + this.compactModifierText(this.supportPreviewModifierLabels(preview)), + accent + ); - const badgeY = y + 210; + const badgeY = y + 236; const maxBadgeRight = left + width - 10; let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableIcon(preview.usable), this.usableEffectLabel(preview.usable), maxBadgeRight); @@ -5361,6 +5446,26 @@ export class BattleScene extends Phaser.Scene { buffText.setOrigin(1, 0); } + private renderPreviewModifierStrip(x: number, y: number, width: number, icon: BattleUiIconKey, text: string, accent: number) { + const row = this.trackSideObject(this.add.rectangle(x, y, width, 22, 0x101820, 0.96)); + row.setOrigin(0); + row.setStrokeStyle(1, accent, 0.42); + this.trackSideIcon(x + 14, y + 11, icon, 19); + this.trackSideObject(this.add.text(x + 30, y + 5, '보정', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#9fb0bf', + fontStyle: '700' + })); + this.trackSideObject(this.add.text(x + 66, y + 4, text, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: text === '추가 보정 없음' ? '#aeb7c2' : '#f2e3bf', + fontStyle: '700', + fixedWidth: width - 74 + })); + } + private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) { const width = Phaser.Math.Clamp(label.length * 7 + 44, 68, 136); if (maxRight && x + width > maxRight) { @@ -6545,6 +6650,8 @@ export class BattleScene extends Phaser.Scene { equipmentEffectLabels: equipmentEffect.labels, hitRate, criticalRate, + terrainDefenseBonus: terrainRule.defenseBonus, + terrainHitPenalty, counterAvailable: this.canCounterAttack(defender, attacker, action), terrainLabel: terrainRule.label, matchupLabel: this.combatMatchupLabel(attacker, defender, action) @@ -11536,22 +11643,61 @@ export class BattleScene extends Phaser.Scene { this.renderStatRow(stat.key, stat.label, unit.stats[stat.key], left, statTop + index * 28, width); }); - const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}% / 이동 ${unit.move}${acted ? ' / 행동완료' : ''}`; - this.trackSideIcon(left + 13, top + 294, acted ? 'counter' : this.unitMoveIcon(unit), 24); - this.trackSideObject(this.add.text(left + 28, top + 285, positionText, { + const effects = this.unitEffectSummaries(unit); + const effectText = effects.length > 0 ? `\n효과 ${this.compactUnitEffectText(effects)}` : ''; + const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}% / 이동 ${unit.move}${acted ? ' / 행동완료' : ''}${effectText}`; + this.trackSideIcon(left + 13, top + 294, effects[0]?.icon ?? (acted ? 'counter' : this.unitMoveIcon(unit)), 24); + this.trackSideObject(this.add.text(left + 28, effects.length > 0 ? top + 280 : top + 285, positionText, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: acted ? '#bdbdbd' : '#9fb0bf', wordWrap: { width: width - 24, useAdvancedWrap: true } })); - this.renderEquipmentSummary(unit, left, top + 308, width); + const equipmentTop = effects.length > 0 ? top + 328 : top + 308; + this.renderEquipmentSummary(unit, left, equipmentTop, width); if (message) { - this.renderPanelMessage(message, left, top + 474, width, 40); + this.renderPanelMessage(message, left, equipmentTop + 166, width, 40); } this.showFacingIndicator(unit); } + private unitEffectSummaries(unit: UnitData): UnitEffectSummary[] { + const effects: UnitEffectSummary[] = []; + const buff = this.battleBuffs.get(unit.id); + const terrainRule = getTerrainRule(battleMap.terrain[unit.y][unit.x]); + const turnStartParts = [ + terrainRule.recoveryHp ? `병력 +${terrainRule.recoveryHp}` : '', + terrainRule.moraleBonus ? `사기 +${terrainRule.moraleBonus}` : '' + ].filter(Boolean); + + if (buff) { + effects.push({ + icon: 'leadership', + label: buff.label, + value: this.battleBuffBonusText(buff), + tone: palette.gold + }); + } + if (turnStartParts.length > 0) { + effects.push({ + icon: 'terrain', + label: `${terrainRule.label} 턴 시작`, + value: turnStartParts.join(' / '), + tone: 0x59d18c + }); + } + + return effects; + } + + private compactUnitEffectText(effects: UnitEffectSummary[]) { + return this.compactModifierText( + effects.map((effect) => `${effect.label}: ${effect.value}`), + 2 + ); + } + private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) { this.trackSideIcon(x + 14, y + 11, 'mastery', 26); this.trackSideObject(this.add.text(x + 31, y, '장비 / 숙련도', {