Show battle effect modifiers in tactical panels

This commit is contained in:
2026-07-02 22:39:02 +09:00
parent c408b57c10
commit 2a4070d4f5

View File

@@ -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<UnitData, 'id' | 'level' | 'exp' | 'hp' | 'maxHp' | 'attack' | 'move' | 'x' | 'y'> & {
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, '장비 / 숙련도', {