Show battle effect modifiers in tactical panels
This commit is contained in:
@@ -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, '장비 / 숙련도', {
|
||||
|
||||
Reference in New Issue
Block a user