Improve battle preview readability

This commit is contained in:
2026-07-03 05:32:34 +09:00
parent 9b6dad4016
commit 4c3fd6d435

View File

@@ -5337,12 +5337,13 @@ export class BattleScene extends Phaser.Scene {
const candidates = targets.slice(0, 3).map((target): TargetingGuideCandidate => {
const preview = this.combatPreview(unit, target, action, usable);
const distance = this.tileDistance(unit, target);
const counterLabel = preview.counterAvailable ? '반격 주의' : '반격 없음';
return {
icon: this.previewDamageIcon(preview),
name: target.name,
meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${range}`,
primary: `${preview.damage} 피해`,
secondary: `${preview.hitRate}% ${this.previewSuccessLabel(preview)} · 치명 ${preview.criticalRate}%`,
secondary: `${preview.hitRate}% ${this.previewSuccessLabel(preview)} · ${counterLabel}`,
tone: this.previewAccentColor(preview)
};
});
@@ -6178,85 +6179,86 @@ export class BattleScene extends Phaser.Scene {
return '도구';
}
private renderCombatPreviewCard(preview: CombatPreview, locked = false) {
private renderCombatPreviewSummaryCard(preview: CombatPreview, locked = false) {
const { panelX, panelWidth } = this.layout;
const left = panelX + 24;
const width = panelWidth - 48;
const height = locked ? 438 : 402;
const y = this.sideContentTop();
const accent = this.previewAccentColor(preview);
const defenderClass = getUnitClass(preview.defender.classKey);
const attackerEquipmentLabel = preview.action === 'strategy' ? '책략 장비' : preview.action === 'item' ? '사용 장비' : '공격 장비';
const distance = this.tileDistance(preview.attacker, preview.defender);
const range = this.actionRange(preview.attacker, preview.action, preview.usable);
const attackerWeapon = getItem(preview.attacker.equipment.weapon.itemId);
const defenderArmor = getItem(preview.defender.equipment.armor.itemId);
const sourceIcon = preview.usable ? this.usableIcon(preview.usable) : this.itemIcon(attackerWeapon, 'weapon');
const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : attackerEquipmentLabel;
const sourceName = preview.usable?.name ?? attackerWeapon.name;
const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : preview.action === 'strategy' ? '책략 장비' : '공격 장비';
const counterTone = preview.counterAvailable ? 0xb64a45 : 0x59d18c;
const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1));
bg.setOrigin(0);
bg.setStrokeStyle(2, accent, 0.78);
bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78);
const actionFrame = this.trackSideObject(this.add.rectangle(left + 40, y + 40, 72, 72, 0x0a0f14, 0.94));
actionFrame.setStrokeStyle(1, accent, 0.76);
this.trackSideIcon(left + 40, y + 40, this.actionIcon(preview), 64);
this.trackSideObject(this.add.text(left + 86, y + 8, `${preview.attacker.name}${preview.defender.name}`, {
const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95));
actionFrame.setStrokeStyle(1, accent, 0.82);
this.trackSideIcon(left + 42, y + 42, this.actionIcon(preview), 66);
this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.attacker.name}${preview.defender.name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
color: '#f4dfad',
fontStyle: '700',
fixedWidth: width - 96
fixedWidth: width - 98
}));
this.trackSideObject(this.add.text(left + 86, y + 36, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)} · ${defenderClass.name}`, {
this.trackSideObject(this.add.text(left + 88, y + 37, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: '#9fb0bf',
color: '#d4dce6',
fontStyle: '700',
fixedWidth: width - 96
fixedWidth: width - 98
}));
this.trackSideObject(this.add.text(left + 86, y + 56, `거리 ${this.tileDistance(preview.attacker, preview.defender)} / 사거리 ${this.actionRange(preview.attacker, preview.action, preview.usable)} · ${locked ? '선택됨' : '대상 후보'}`, {
this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${range} · ${locked ? '선택됨' : '대상 후보'}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: locked ? '#f2e3bf' : '#c8d2dd',
fixedWidth: width - 96
color: locked ? '#f2e3bf' : '#9fb0bf',
fixedWidth: width - 98
}));
const chipGap = 8;
const chipWidth = (width - 20 - chipGap) / 2;
this.renderPreviewEquipmentChip(left + 10, y + 82, chipWidth, sourceIcon, sourceLabel, sourceName);
this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 82, chipWidth, this.itemIcon(defenderArmor, 'armor'), '방어구', defenderArmor.name);
const metricGap = 8;
const metricWidth = (width - 20 - metricGap) / 2;
this.renderPreviewOutcomeCard(left + 10, y + 142, metricWidth, this.previewDamageIcon(preview), '예상 피해', `${preview.damage}`, 0xb64a45);
this.renderPreviewOutcomeCard(
const metricGap = 6;
const metricWidth = (width - 20 - metricGap * 2) / 3;
const metricTop = y + 90;
this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), '피해', `${preview.damage}`, 0xb64a45);
this.renderPreviewDecisionCard(
left + 10 + metricWidth + metricGap,
y + 142,
metricTop,
metricWidth,
preview.action === 'strategy' ? 'success' : 'hit',
this.previewSuccessLabel(preview),
`${preview.hitRate}%`,
accent
);
this.renderPreviewOutcomeCard(left + 10, y + 206, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c);
this.renderPreviewOutcomeCard(
left + 10 + metricWidth + metricGap,
y + 206,
this.renderPreviewDecisionCard(
left + 10 + (metricWidth + metricGap) * 2,
metricTop,
metricWidth,
'counter',
'반격',
preview.counterAvailable ? '주의' : '없음',
preview.counterAvailable ? 0xb64a45 : 0x59d18c
counterTone
);
this.renderPreviewForecastRow(preview, left + 10, y + 272, width - 20, accent);
this.renderPreviewModifierCards(left + 10, y + 308, width - 20, this.previewModifierSummaries(preview), accent);
const chipGap = 8;
const chipWidth = (width - 20 - chipGap) / 2;
this.renderPreviewEquipmentChip(left + 10, y + 184, chipWidth, sourceIcon, sourceLabel, sourceName);
this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 184, chipWidth, this.itemIcon(defenderArmor, 'armor'), '대상 방어구', defenderArmor.name);
const badgeY = y + 374;
this.renderPreviewForecastRow(preview, left + 10, y + 238, width - 20, accent);
this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.previewModifierSummaries(preview), accent);
const badgeY = y + 346;
const maxBadgeRight = left + width - 10;
let badgeX = left + 10;
badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), maxBadgeRight);
badgeX = this.renderPreviewBadge(badgeX, badgeY, 'critical', `치명 ${preview.criticalRate}%`, maxBadgeRight);
badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight);
let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight);
if (preview.bondDamageBonus > 0 && preview.bondLabel) {
@@ -6270,66 +6272,63 @@ export class BattleScene extends Phaser.Scene {
}
}
private renderSupportPreviewCard(preview: SupportPreview, locked = false) {
private renderSupportPreviewSummaryCard(preview: SupportPreview, locked = false) {
const { panelX, panelWidth } = this.layout;
const left = panelX + 24;
const width = panelWidth - 48;
const height = locked ? 438 : 402;
const y = this.sideContentTop();
const accent = this.usableAccentColor(preview.usable);
const isHeal = preview.usable.effect === 'heal';
const distance = this.tileDistance(preview.user, preview.target);
const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1));
bg.setOrigin(0);
bg.setStrokeStyle(2, accent, 0.78);
bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78);
const actionFrame = this.trackSideObject(this.add.rectangle(left + 40, y + 40, 72, 72, 0x0a0f14, 0.94));
actionFrame.setStrokeStyle(1, accent, 0.76);
this.trackSideIcon(left + 40, y + 40, this.usableIcon(preview.usable), 64);
this.trackSideObject(this.add.text(left + 86, y + 8, `${preview.user.name}${preview.target.name}`, {
const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95));
actionFrame.setStrokeStyle(1, accent, 0.82);
this.trackSideIcon(left + 42, y + 42, this.usableIcon(preview.usable), 66);
this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.user.name}${preview.target.name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
color: '#f4dfad',
fontStyle: '700',
fixedWidth: width - 96
fixedWidth: width - 98
}));
this.trackSideObject(this.add.text(left + 86, y + 36, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)} · ${this.usableTargetLabel(preview.usable)} ${preview.usable.range}`, {
this.trackSideObject(this.add.text(left + 88, y + 37, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: '#9fb0bf',
color: '#d4dce6',
fontStyle: '700',
fixedWidth: width - 96
fixedWidth: width - 98
}));
this.trackSideObject(this.add.text(left + 86, y + 56, `거리 ${this.tileDistance(preview.user, preview.target)} / 사거리 ${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, {
this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: locked ? '#f2e3bf' : '#c8d2dd',
fixedWidth: width - 96
color: locked ? '#f2e3bf' : '#9fb0bf',
fixedWidth: width - 98
}));
const participantGap = 8;
const participantWidth = (width - 20 - participantGap) / 2;
this.renderPreviewParticipantBox(
left + 10,
y + 82,
participantWidth,
preview.user,
'사용자',
accent
);
this.renderPreviewParticipantBox(
left + 10 + participantWidth + participantGap,
y + 82,
participantWidth,
preview.target,
'대상',
preview.target.faction === preview.user.faction ? palette.blue : 0xb86b55
);
const metricGap = 6;
const metricWidth = (width - 20 - metricGap * 2) / 3;
const metricTop = y + 90;
if (isHeal) {
this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '회복', `+${preview.healAmount}`, 0x59d18c);
this.renderPreviewDecisionCard(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent);
this.renderPreviewDecisionCard(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'hp', '병력', `${preview.projectedHp}`, 0x59d18c);
} else {
this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '강화', `+${preview.attackBonus}`, palette.gold);
this.renderPreviewDecisionCard(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'hit', '명중', `+${preview.hitBonus}`, accent);
this.renderPreviewDecisionCard(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'leadership', '지속', `${preview.duration}`, 0x83d6ff);
}
const chipGap = 8;
const chipWidth = (width - 20 - chipGap) / 2;
this.renderPreviewEquipmentChip(
left + 10,
y + 146,
y + 184,
chipWidth,
preview.usable.command === 'item' ? 'accessory' : 'strategy',
this.usableChannelLabel(preview.usable),
@@ -6337,43 +6336,77 @@ export class BattleScene extends Phaser.Scene {
);
this.renderPreviewEquipmentChip(
left + 10 + chipWidth + chipGap,
y + 146,
y + 184,
chipWidth,
this.usableTargetIcon(preview.usable),
'대상/사거리',
`${preview.target.name} · ${preview.usable.range}`
this.usableTargetLabel(preview.usable),
preview.target.name
);
const metricTop = y + 204;
const metricGap = 6;
const metricWidth = (width - 20 - metricGap * 2) / 3;
if (preview.usable.effect === 'heal') {
this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '회복', `+${preview.healAmount}`, 0x59d18c);
this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent);
this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, this.usableTargetIcon(preview.usable), '대상', `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}`, accent);
} else {
this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '강화', `+${preview.attackBonus}`, 0xd8b15f);
this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'hit', '명중', `+${preview.hitBonus}`, accent);
this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `+${preview.criticalBonus}`, 0xd8732c);
}
this.renderSupportForecastRow(preview, left + 10, y + 238, width - 20, accent);
this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.supportPreviewModifierSummaries(preview), accent);
this.renderSupportForecastRow(preview, left + 10, y + 272, width - 20, accent);
this.renderPreviewModifierCards(left + 10, y + 308, width - 20, this.supportPreviewModifierSummaries(preview), accent);
const badgeY = y + 374;
const badgeY = y + 346;
const maxBadgeRight = left + width - 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.usableTargetIcon(preview.usable), `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}`, maxBadgeRight);
let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, preview.usable.command === 'item' ? 'accessory' : 'strategy', this.usableChannelLabel(preview.usable), maxBadgeRight);
if (preview.usable.effect === 'focus') {
this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `${preview.duration}`, maxBadgeRight);
if (!isHeal) {
detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'critical', `치명 +${preview.criticalBonus}`, maxBadgeRight);
}
if (locked) {
this.renderPreviewConfirmBar(left + 10, y + 402, width - 20, accent, `${preview.target.name} 선택됨`);
}
}
private renderPreviewDecisionCard(
x: number,
y: number,
width: number,
icon: BattleUiIconKey,
label: string,
value: string,
tone: number
) {
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 84, 0x16212d, 0.97));
bg.setOrigin(0);
bg.setStrokeStyle(1, tone, 0.72);
const iconFrame = this.trackSideObject(this.add.rectangle(x + width / 2, y + 25, 44, 44, 0x0a0f14, 0.92));
iconFrame.setStrokeStyle(1, tone, 0.58);
this.trackSideIcon(x + width / 2, y + 25, icon, 40);
const labelText = this.trackSideObject(this.add.text(x + width / 2, y + 47, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 8,
align: 'center'
}));
labelText.setOrigin(0.5, 0);
const valueHasKorean = /[가-힣]/.test(value);
const valueText = this.trackSideObject(this.add.text(x + width / 2, y + 62, value, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: valueHasKorean || value.length > 4 ? '17px' : '22px',
color: '#f2e3bf',
fontStyle: '700',
fixedWidth: width - 8,
align: 'center'
}));
valueText.setOrigin(0.5, 0);
}
private renderCombatPreviewCard(preview: CombatPreview, locked = false) {
return this.renderCombatPreviewSummaryCard(preview, locked);
}
private renderSupportPreviewCard(preview: SupportPreview, locked = false) {
return this.renderSupportPreviewSummaryCard(preview, locked);
}
private renderPreviewModifierCards(
x: number,
y: number,