Show battle effect modifiers in tactical panels
This commit is contained in:
@@ -1292,6 +1292,8 @@ type CombatPreview = {
|
|||||||
equipmentEffectLabels: string[];
|
equipmentEffectLabels: string[];
|
||||||
hitRate: number;
|
hitRate: number;
|
||||||
criticalRate: number;
|
criticalRate: number;
|
||||||
|
terrainDefenseBonus: number;
|
||||||
|
terrainHitPenalty: number;
|
||||||
counterAvailable: boolean;
|
counterAvailable: boolean;
|
||||||
terrainLabel: string;
|
terrainLabel: string;
|
||||||
matchupLabel: string;
|
matchupLabel: string;
|
||||||
@@ -1359,6 +1361,13 @@ type BattleBuffState = {
|
|||||||
criticalBonus: number;
|
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'> & {
|
type SavedUnitState = Pick<UnitData, 'id' | 'level' | 'exp' | 'hp' | 'maxHp' | 'attack' | 'move' | 'x' | 'y'> & {
|
||||||
equipment: UnitData['equipment'];
|
equipment: UnitData['equipment'];
|
||||||
direction?: UnitDirection;
|
direction?: UnitDirection;
|
||||||
@@ -5060,6 +5069,66 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return parts.join(' / ') || '보정 없음';
|
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) {
|
private previewActionLabel(preview: CombatPreview) {
|
||||||
return preview.usable?.name ?? commandLabels[preview.action];
|
return preview.usable?.name ?? commandLabels[preview.action];
|
||||||
}
|
}
|
||||||
@@ -5102,7 +5171,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const { panelX, panelWidth } = this.layout;
|
const { panelX, panelWidth } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
const width = panelWidth - 48;
|
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 y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 344, this.sideContentBottom(10) - height));
|
||||||
const accent = this.previewAccentColor(preview);
|
const accent = this.previewAccentColor(preview);
|
||||||
const defenderClass = getUnitClass(preview.defender.classKey);
|
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.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c);
|
||||||
|
|
||||||
this.renderPreviewForecastRow(preview, left + 10, y + 184, width - 20, accent);
|
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;
|
const maxBadgeRight = left + width - 10;
|
||||||
let badgeX = left + 10;
|
let badgeX = left + 10;
|
||||||
badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), maxBadgeRight);
|
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 { panelX, panelWidth } = this.layout;
|
||||||
const left = panelX + 24;
|
const left = panelX + 24;
|
||||||
const width = panelWidth - 48;
|
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 y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 344, this.sideContentBottom(10) - height));
|
||||||
const accent = this.usableAccentColor(preview.usable);
|
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.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;
|
const maxBadgeRight = left + width - 10;
|
||||||
let badgeX = left + 10;
|
let badgeX = left + 10;
|
||||||
badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableIcon(preview.usable), this.usableEffectLabel(preview.usable), maxBadgeRight);
|
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);
|
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) {
|
private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) {
|
||||||
const width = Phaser.Math.Clamp(label.length * 7 + 44, 68, 136);
|
const width = Phaser.Math.Clamp(label.length * 7 + 44, 68, 136);
|
||||||
if (maxRight && x + width > maxRight) {
|
if (maxRight && x + width > maxRight) {
|
||||||
@@ -6545,6 +6650,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
equipmentEffectLabels: equipmentEffect.labels,
|
equipmentEffectLabels: equipmentEffect.labels,
|
||||||
hitRate,
|
hitRate,
|
||||||
criticalRate,
|
criticalRate,
|
||||||
|
terrainDefenseBonus: terrainRule.defenseBonus,
|
||||||
|
terrainHitPenalty,
|
||||||
counterAvailable: this.canCounterAttack(defender, attacker, action),
|
counterAvailable: this.canCounterAttack(defender, attacker, action),
|
||||||
terrainLabel: terrainRule.label,
|
terrainLabel: terrainRule.label,
|
||||||
matchupLabel: this.combatMatchupLabel(attacker, defender, action)
|
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);
|
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 ? ' / 행동완료' : ''}`;
|
const effects = this.unitEffectSummaries(unit);
|
||||||
this.trackSideIcon(left + 13, top + 294, acted ? 'counter' : this.unitMoveIcon(unit), 24);
|
const effectText = effects.length > 0 ? `\n효과 ${this.compactUnitEffectText(effects)}` : '';
|
||||||
this.trackSideObject(this.add.text(left + 28, top + 285, positionText, {
|
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',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: acted ? '#bdbdbd' : '#9fb0bf',
|
color: acted ? '#bdbdbd' : '#9fb0bf',
|
||||||
wordWrap: { width: width - 24, useAdvancedWrap: true }
|
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) {
|
if (message) {
|
||||||
this.renderPanelMessage(message, left, top + 474, width, 40);
|
this.renderPanelMessage(message, left, equipmentTop + 166, width, 40);
|
||||||
}
|
}
|
||||||
this.showFacingIndicator(unit);
|
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) {
|
private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) {
|
||||||
this.trackSideIcon(x + 14, y + 11, 'mastery', 26);
|
this.trackSideIcon(x + 14, y + 11, 'mastery', 26);
|
||||||
this.trackSideObject(this.add.text(x + 31, y, '장비 / 숙련도', {
|
this.trackSideObject(this.add.text(x + 31, y, '장비 / 숙련도', {
|
||||||
|
|||||||
Reference in New Issue
Block a user