Improve strategy menu readability

This commit is contained in:
2026-07-03 06:28:24 +09:00
parent 86e5847c5a
commit 08b4a62486

View File

@@ -1374,6 +1374,7 @@ type UsableMenuForecast = {
value: string;
detail: string;
secondary: string;
chips: UsableMenuChip[];
};
type LockedTargetPreview = {
@@ -1419,6 +1420,13 @@ type PreviewModifierSummary = {
tone: number;
};
type UsableMenuChip = {
icon: BattleUiIconKey;
label: string;
value: string;
tone: number;
};
type TargetingGuideMetric = {
icon: BattleUiIconKey;
label: string;
@@ -4791,23 +4799,40 @@ export class BattleScene extends Phaser.Scene {
return this.supportTargetUnavailableReason(user, usable);
}
private rateTone(rate: number) {
if (rate >= 80) {
return 0x59d18c;
}
if (rate >= 60) {
return palette.gold;
}
return 0xb64a45;
}
private usableMenuForecast(user: UnitData, usable: BattleUsable): UsableMenuForecast {
const targetCount = this.usableTargetCount(user, usable);
const hasStock = usable.command !== 'item' || this.itemStock(user.id, usable.id) > 0;
const available = targetCount > 0 && hasStock;
const status = !hasStock ? '수량 없음' : targetCount > 0 ? `대상 ${targetCount}` : '대상 없음';
const accent = this.usableAccentColor(usable);
if (usable.effect === 'damage') {
const target = this.damageableTargets(user, usable.command, usable)[0];
if (target) {
const preview = this.combatPreview(user, target, usable.command, usable);
const successLabel = usable.command === 'strategy' ? '성공률' : '명중률';
return {
available,
status,
targetCount,
valueLabel: '피해',
value: `${preview.damage}`,
detail: `${target.name}에게 ${preview.damage} 피해 예상`,
secondary: `${preview.hitRate}% ${usable.command === 'strategy' ? '성공' : '명중'} · 치명 ${preview.criticalRate}%`
detail: `${target.name} · 피해 ${preview.damage} · ${successLabel} ${preview.hitRate}%`,
secondary: `치명 ${preview.criticalRate}% · 지형 ${preview.terrainLabel} · 사거리 ${usable.range}`,
chips: [
{ icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent },
{ icon: usable.command === 'strategy' ? 'success' : 'hit', label: successLabel, value: `${preview.hitRate}%`, tone: this.rateTone(preview.hitRate) },
{ icon: this.usableTargetIcon(usable), label: '대상', value: `${targetCount}`, tone: 0xb64a45 }
]
};
}
return {
@@ -4817,7 +4842,12 @@ export class BattleScene extends Phaser.Scene {
valueLabel: '위력',
value: `${usable.power}`,
detail: this.usableUnavailableReason(user, usable),
secondary: usable.description
secondary: usable.description,
chips: [
{ icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent },
{ icon: 'success', label: '성공률', value: '-', tone: 0x647485 },
{ icon: this.usableTargetIcon(usable), label: '대상', value: '없음', tone: 0xb64a45 }
]
};
}
if (usable.effect === 'heal') {
@@ -4831,7 +4861,12 @@ export class BattleScene extends Phaser.Scene {
valueLabel: '회복',
value: `+${amount}`,
detail: target ? `${target.name} HP ${target.hp}${projectedHp}` : this.usableUnavailableReason(user, usable),
secondary: target ? `기본 ${usable.power} + 능력/장비 보정` : usable.description
secondary: target ? `기본 ${usable.power} + 능력/장비 보정` : usable.description,
chips: [
{ icon: this.usablePowerIcon(usable), label: '회복량', value: `+${amount}`, tone: 0x59d18c },
{ icon: 'success', label: '적용', value: available ? '100%' : '-', tone: available ? 0x59d18c : 0x647485 },
{ icon: this.usableTargetIcon(usable), label: '대상', value: available ? `${targetCount}` : '없음', tone: accent }
]
};
}
return {
@@ -4841,7 +4876,12 @@ export class BattleScene extends Phaser.Scene {
valueLabel: '강화',
value: `${usable.duration ?? 1}`,
detail: `공격 +${usable.attackBonus ?? 0} · 명중 +${usable.hitBonus ?? 0} · 치명 +${usable.criticalBonus ?? 0}`,
secondary: available ? `${this.usableTargetLabel(usable)} ${targetCount}명에게 ${usable.duration ?? 1}턴 적용` : this.usableUnavailableReason(user, usable)
secondary: available ? `${this.usableTargetLabel(usable)} ${targetCount}명에게 ${usable.duration ?? 1}턴 적용` : this.usableUnavailableReason(user, usable),
chips: [
{ icon: 'focus', label: '강화', value: this.usableBuffText(usable), tone: palette.gold },
{ icon: 'leadership', label: '지속', value: `${usable.duration ?? 1}`, tone: 0x83d6ff },
{ icon: this.usableTargetIcon(usable), label: '대상', value: available ? `${targetCount}` : '없음', tone: accent }
]
};
}
@@ -4866,33 +4906,33 @@ export class BattleScene extends Phaser.Scene {
tone: number,
available: boolean
) {
const chip = this.add.rectangle(x, y, width, 34, 0x0b1118, available ? 0.94 : 0.86);
const chip = this.add.rectangle(x, y, width, 40, 0x0b1118, available ? 0.95 : 0.87);
chip.setOrigin(0);
chip.setDepth(16);
chip.setStrokeStyle(1, tone, available ? 0.58 : 0.36);
chip.setStrokeStyle(1, tone, available ? 0.64 : 0.38);
this.commandMenuObjects.push(chip);
const iconImage = this.createBattleUiIcon(x + 17, y + 17, icon, 25);
const iconImage = this.createBattleUiIcon(x + 20, y + 20, icon, 31);
iconImage.setAlpha(available ? 0.98 : 0.72);
iconImage.setDepth(17);
this.commandMenuObjects.push(iconImage);
const labelText = this.add.text(x + 34, y + 4, label, {
const labelText = this.add.text(x + 41, y + 5, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '9px',
fontSize: '10px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 40
fixedWidth: width - 48
});
labelText.setDepth(17);
this.commandMenuObjects.push(labelText);
const valueText = this.add.text(x + 34, y + 18, this.truncateUiText(value, 8), {
const valueText = this.add.text(x + 41, y + 21, this.truncateUiText(value, 10), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
fontSize: '13px',
color: available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700',
fixedWidth: width - 40
fixedWidth: width - 48
});
valueText.setDepth(17);
this.commandMenuObjects.push(valueText);
@@ -4907,23 +4947,23 @@ export class BattleScene extends Phaser.Scene {
forecast: UsableMenuForecast,
tone: number
) {
const valueBox = this.add.rectangle(x, y, width, height, 0x0b1118, forecast.available ? 0.96 : 0.9);
const valueBox = this.add.rectangle(x, y, width, height, 0x0b1118, forecast.available ? 0.97 : 0.9);
valueBox.setOrigin(0);
valueBox.setDepth(16);
valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.76 : 0.46);
valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.82 : 0.46);
this.commandMenuObjects.push(valueBox);
const valueIcon = this.createBattleUiIcon(x + 24, y + height / 2, this.usablePowerIcon(usable), 36);
const valueIcon = this.createBattleUiIcon(x + 30, y + height / 2, this.usablePowerIcon(usable), 46);
valueIcon.setAlpha(forecast.available ? 1 : 0.72);
valueIcon.setDepth(17);
this.commandMenuObjects.push(valueIcon);
const valueLabel = this.add.text(x + 50, y + 10, forecast.valueLabel, {
const valueLabel = this.add.text(x + 62, y + 12, forecast.valueLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
fontSize: '12px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 58
fixedWidth: width - 70
});
valueLabel.setDepth(17);
this.commandMenuObjects.push(valueLabel);
@@ -4931,7 +4971,7 @@ export class BattleScene extends Phaser.Scene {
const valueHasKorean = /[가-힣]/.test(forecast.value);
const valueText = this.add.text(x + width - 10, y + height - 16, forecast.value, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: valueHasKorean || forecast.value.length > 4 ? '18px' : '23px',
fontSize: valueHasKorean || forecast.value.length > 4 ? '19px' : '27px',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700'
});
@@ -4961,9 +5001,9 @@ export class BattleScene extends Phaser.Scene {
const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) }));
const readyCount = forecasts.filter(({ forecast }) => forecast.available).length;
const menuWidth = 486;
const menuWidth = 520;
const titleHeight = 58;
const rowHeight = 130;
const rowHeight = 148;
const padding = 10;
const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
@@ -5016,8 +5056,8 @@ export class BattleScene extends Phaser.Scene {
const rowLeft = left + padding;
const rowWidth = menuWidth - padding * 2;
const rowRight = rowLeft + rowWidth;
const contentLeft = rowLeft + 90;
const sideBoxWidth = 112;
const contentLeft = rowLeft + 102;
const sideBoxWidth = 124;
const sideBoxLeft = rowRight - sideBoxWidth - 10;
const contentWidth = sideBoxLeft - contentLeft - 12;
const baseFill = forecast.available ? 0x1a2630 : 0x151c23;
@@ -5028,12 +5068,12 @@ export class BattleScene extends Phaser.Scene {
bg.setInteractive({ useHandCursor: true });
this.commandMenuObjects.push(bg);
const iconFrame = this.add.rectangle(rowLeft + 43, rowTop + 61, 76, 76, 0x0a0f14, forecast.available ? 0.92 : 0.86);
const iconFrame = this.add.rectangle(rowLeft + 48, rowTop + 70, 86, 86, 0x0a0f14, forecast.available ? 0.94 : 0.87);
iconFrame.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42);
iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame);
const icon = this.createBattleUiIcon(rowLeft + 43, rowTop + 61, this.usableIcon(usable), 68);
const icon = this.createBattleUiIcon(rowLeft + 48, rowTop + 70, this.usableIcon(usable), 78);
icon.setAlpha(forecast.available ? 1 : 0.72);
icon.setDepth(17);
this.commandMenuObjects.push(icon);
@@ -5078,9 +5118,9 @@ export class BattleScene extends Phaser.Scene {
metaText.setDepth(16);
this.commandMenuObjects.push(metaText);
const summary = this.add.text(contentLeft, rowTop + 54, this.truncateUiText(forecast.detail, 32), {
const summary = this.add.text(contentLeft, rowTop + 55, this.truncateUiText(forecast.detail, 34), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
fontSize: '14px',
color: forecast.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700',
fixedWidth: contentWidth
@@ -5088,40 +5128,31 @@ export class BattleScene extends Phaser.Scene {
summary.setDepth(16);
this.commandMenuObjects.push(summary);
const secondary = this.add.text(contentLeft, rowTop + 72, this.truncateUiText(forecast.secondary, 36), {
const secondary = this.add.text(contentLeft, rowTop + 76, this.truncateUiText(forecast.secondary, 38), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
fontSize: '12px',
color: forecast.available ? '#aeb7c2' : '#d8b199',
fixedWidth: contentWidth
});
secondary.setDepth(16);
this.commandMenuObjects.push(secondary);
this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 45, sideBoxWidth, 66, usable, forecast, accent);
this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 52, sideBoxWidth, 76, usable, forecast, accent);
const chipGap = 6;
const chipWidth = (contentWidth - chipGap * 2) / 3;
this.renderUsableMenuInfoChip(contentLeft, rowTop + 88, chipWidth, this.usableIcon(usable), '효과', this.usableEffectLabel(usable), accent, forecast.available);
this.renderUsableMenuInfoChip(
contentLeft + chipWidth + chipGap,
rowTop + 88,
chipWidth,
this.usableTargetIcon(usable),
'범위',
`${this.usableTargetLabel(usable)} ${this.usableRangeText(usable)}`,
accent,
forecast.available
);
this.renderUsableMenuInfoChip(
contentLeft + (chipWidth + chipGap) * 2,
rowTop + 88,
chipWidth,
usable.effect === 'focus' ? 'leadership' : usable.command === 'item' ? 'accessory' : 'success',
usable.effect === 'focus' ? '강화' : usable.command === 'item' ? '수량' : '가능',
usable.effect === 'focus' ? this.usableBuffText(usable) : usable.command === 'item' ? `${this.itemStock(unit.id, usable.id)}` : `${forecast.targetCount}`,
accent,
forecast.available
);
forecast.chips.slice(0, 3).forEach((chip, chipIndex) => {
this.renderUsableMenuInfoChip(
contentLeft + chipIndex * (chipWidth + chipGap),
rowTop + 98,
chipWidth,
chip.icon,
chip.label,
chip.value,
chip.tone,
forecast.available
);
});
const choose = (choicePointer: Phaser.Input.Pointer) => {
if (choicePointer.rightButtonDown()) {
@@ -6292,10 +6323,26 @@ export class BattleScene extends Phaser.Scene {
const buff = this.battleBuffs.get(preview.attacker.id);
const equipmentText = this.equipmentPreviewBonusText(preview);
const terrainParts = [
preview.terrainDefenseBonus > 0 ? `+${preview.terrainDefenseBonus}` : '',
preview.terrainHitPenalty > 0 ? `-${preview.terrainHitPenalty}` : ''
preview.terrainDefenseBonus > 0 ? `${preview.action === 'strategy' ? '저항' : '방'}+${preview.terrainDefenseBonus}` : '',
preview.terrainHitPenalty > 0 ? `${preview.action === 'strategy' ? '성공' : '명'}-${preview.terrainHitPenalty}` : ''
].filter(Boolean);
if (preview.action === 'strategy') {
summaries.push({
icon: 'intelligence',
label: '지력',
value: `${preview.attacker.stats.intelligence} vs ${preview.defender.stats.intelligence}`,
tone: 0x83d6ff
});
if (terrainParts.length > 0) {
summaries.push({
icon: 'terrain',
label: preview.terrainLabel,
value: terrainParts.join(' / '),
tone: 0x59d18c
});
}
}
if (buff) {
summaries.push({
icon: 'focus',
@@ -6320,7 +6367,7 @@ export class BattleScene extends Phaser.Scene {
tone: 0xd8b15f
});
}
if (terrainParts.length > 0) {
if (preview.action !== 'strategy' && terrainParts.length > 0) {
summaries.push({
icon: 'terrain',
label: preview.terrainLabel,
@@ -6426,9 +6473,11 @@ export class BattleScene extends Phaser.Scene {
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 isStrategy = preview.action === 'strategy';
const sourceIcon = preview.usable ? this.usableIcon(preview.usable) : this.itemIcon(attackerWeapon, 'weapon');
const sourceName = preview.usable?.name ?? attackerWeapon.name;
const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : preview.action === 'strategy' ? '책략 장비' : '공격 장비';
const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : isStrategy ? '책략 장비' : '공격 장비';
const defenseLabel = isStrategy ? '대상 방어' : '대상 방어구';
const counterTone = preview.counterAvailable ? 0xb64a45 : 0x59d18c;
const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1));
@@ -6463,13 +6512,13 @@ export class BattleScene extends Phaser.Scene {
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, metricTop, metricWidth, this.previewDamageIcon(preview), isStrategy ? '책략 피해' : '예상 피해', `${preview.damage}`, 0xb64a45);
this.renderPreviewDecisionCard(
left + 10 + metricWidth + metricGap,
metricTop,
metricWidth,
preview.action === 'strategy' ? 'success' : 'hit',
this.previewSuccessLabel(preview),
isStrategy ? 'success' : 'hit',
isStrategy ? '성공률' : this.previewSuccessLabel(preview),
`${preview.hitRate}%`,
accent
);
@@ -6486,7 +6535,7 @@ export class BattleScene extends Phaser.Scene {
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);
this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 184, chipWidth, this.itemIcon(defenderArmor, 'armor'), defenseLabel, defenderArmor.name);
this.renderPreviewForecastRow(preview, left + 10, y + 238, width - 20, accent);
this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.previewModifierSummaries(preview), accent);
@@ -6494,6 +6543,9 @@ export class BattleScene extends Phaser.Scene {
const badgeY = y + 346;
const maxBadgeRight = left + width - 10;
let badgeX = left + 10;
if (isStrategy) {
badgeX = this.renderPreviewBadge(badgeX, badgeY, 'success', `성공 ${preview.hitRate}%`, 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);
@@ -6566,7 +6618,7 @@ export class BattleScene extends Phaser.Scene {
left + 10,
y + 184,
chipWidth,
preview.usable.command === 'item' ? 'accessory' : 'strategy',
this.usableIcon(preview.usable),
this.usableChannelLabel(preview.usable),
preview.usable.name
);
@@ -6769,24 +6821,24 @@ export class BattleScene extends Phaser.Scene {
roleLabel: string,
label: string
) {
const chip = this.trackSideObject(this.add.rectangle(x, y, width, 48, 0x0b1118, 0.96));
const chip = this.trackSideObject(this.add.rectangle(x, y, width, 52, 0x0b1118, 0.97));
chip.setOrigin(0);
chip.setStrokeStyle(1, 0x647485, 0.58);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 24, y + 24, 42, 42, 0x16212d, 0.92));
chip.setStrokeStyle(1, 0x647485, 0.62);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 27, y + 26, 46, 46, 0x16212d, 0.93));
iconFrame.setStrokeStyle(1, 0x53606c, 0.55);
this.trackSideIcon(x + 24, y + 24, icon, 38);
this.trackSideObject(this.add.text(x + 52, y + 5, roleLabel, {
this.trackSideIcon(x + 27, y + 26, icon, 42);
this.trackSideObject(this.add.text(x + 58, y + 6, roleLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px',
color: '#9fb0bf',
fontStyle: '700'
}));
this.trackSideObject(this.add.text(x + 52, y + 22, label, {
this.trackSideObject(this.add.text(x + 58, y + 24, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: '#d4dce6',
fontStyle: '700',
fixedWidth: width - 60
fixedWidth: width - 66
}));
}