Improve usable action menu readability

This commit is contained in:
2026-07-03 04:39:51 +09:00
parent 302d0e5f2d
commit ef030b88b7

View File

@@ -1365,6 +1365,16 @@ type SupportPreview = {
duration: number;
};
type UsableMenuForecast = {
available: boolean;
status: string;
targetCount: number;
valueLabel: string;
value: string;
detail: string;
secondary: string;
};
type LockedTargetPreview = {
kind: 'damage' | 'support';
userId: string;
@@ -4713,7 +4723,7 @@ export class BattleScene extends Phaser.Scene {
return this.supportTargetUnavailableReason(user, usable);
}
private usableMenuForecast(user: UnitData, usable: BattleUsable) {
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;
@@ -4725,59 +4735,141 @@ export class BattleScene extends Phaser.Scene {
return {
available,
status,
targetCount,
valueLabel: '피해',
value: `${preview.damage}`,
detail: `${preview.hitRate}% ${usable.command === 'strategy' ? '성공' : '명중'} · 치명 ${preview.criticalRate}%`
detail: `${target.name}에게 ${preview.damage} 피해 예상`,
secondary: `${preview.hitRate}% ${usable.command === 'strategy' ? '성공' : '명중'} · 치명 ${preview.criticalRate}%`
};
}
return {
available,
status,
targetCount,
valueLabel: '위력',
value: `${usable.power}`,
detail: this.usableUnavailableReason(user, usable)
detail: this.usableUnavailableReason(user, usable),
secondary: usable.description
};
}
if (usable.effect === 'heal') {
const target = this.supportTargets(user, usable)[0];
const amount = target ? this.supportHealAmount(user, target, usable) : usable.power;
const projectedHp = target ? Math.min(target.maxHp, target.hp + amount) : undefined;
return {
available,
status,
targetCount,
valueLabel: '회복',
value: `+${amount}`,
detail: target ? `${target.name} 기준 HP 회복` : this.usableUnavailableReason(user, usable)
detail: target ? `${target.name} HP ${target.hp}${projectedHp}` : this.usableUnavailableReason(user, usable),
secondary: target ? `기본 ${usable.power} + 능력/장비 보정` : usable.description
};
}
return {
available,
status,
targetCount,
valueLabel: '강화',
value: `${usable.duration ?? 1}`,
detail: `공격 +${usable.attackBonus ?? 0} · 명중 +${usable.hitBonus ?? 0} · 치명 +${usable.criticalBonus ?? 0}`
detail: `공격 +${usable.attackBonus ?? 0} · 명중 +${usable.hitBonus ?? 0} · 치명 +${usable.criticalBonus ?? 0}`,
secondary: available ? `${this.usableTargetLabel(usable)} ${targetCount}명에게 ${usable.duration ?? 1}턴 적용` : this.usableUnavailableReason(user, usable)
};
}
private renderUsableMenuPill(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, tone: number) {
const pill = this.add.rectangle(x, y, width, 30, 0x0b1118, 0.92);
pill.setOrigin(0);
pill.setDepth(16);
pill.setStrokeStyle(1, tone, 0.54);
this.commandMenuObjects.push(pill);
private usableRangeText(usable: BattleUsable) {
return usable.range === 0 ? '자신' : `${usable.range}`;
}
const iconImage = this.createBattleUiIcon(x + 16, y + 15, icon, 24);
private usableBuffText(usable: BattleUsable) {
if (usable.effect !== 'focus') {
return this.usablePowerLabel(usable);
}
return `공+${usable.attackBonus ?? 0} 명+${usable.hitBonus ?? 0} 치+${usable.criticalBonus ?? 0}`;
}
private renderUsableMenuInfoChip(
x: number,
y: number,
width: number,
icon: BattleUiIconKey,
label: string,
value: string,
tone: number,
available: boolean
) {
const chip = this.add.rectangle(x, y, width, 34, 0x0b1118, available ? 0.94 : 0.86);
chip.setOrigin(0);
chip.setDepth(16);
chip.setStrokeStyle(1, tone, available ? 0.58 : 0.36);
this.commandMenuObjects.push(chip);
const iconImage = this.createBattleUiIcon(x + 17, y + 17, icon, 25);
iconImage.setAlpha(available ? 0.98 : 0.72);
iconImage.setDepth(17);
this.commandMenuObjects.push(iconImage);
const text = this.add.text(x + 35, y + 6, label, {
const labelText = this.add.text(x + 34, y + 4, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#d4dce6',
fontSize: '9px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 40
});
text.setDepth(17);
this.commandMenuObjects.push(text);
labelText.setDepth(17);
this.commandMenuObjects.push(labelText);
const valueText = this.add.text(x + 34, y + 18, this.truncateUiText(value, 8), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
color: available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700',
fixedWidth: width - 40
});
valueText.setDepth(17);
this.commandMenuObjects.push(valueText);
}
private renderUsableMenuValueCard(
x: number,
y: number,
width: number,
height: number,
usable: BattleUsable,
forecast: UsableMenuForecast,
tone: number
) {
const valueBox = this.add.rectangle(x, y, width, height, 0x0b1118, forecast.available ? 0.96 : 0.9);
valueBox.setOrigin(0);
valueBox.setDepth(16);
valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.76 : 0.46);
this.commandMenuObjects.push(valueBox);
const valueIcon = this.createBattleUiIcon(x + 24, y + height / 2, this.usablePowerIcon(usable), 36);
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, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: width - 58
});
valueLabel.setDepth(17);
this.commandMenuObjects.push(valueLabel);
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',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700'
});
valueText.setOrigin(1, 0.5);
valueText.setDepth(17);
this.commandMenuObjects.push(valueText);
}
private hideCommandMenu() {
@@ -4799,9 +4891,11 @@ export class BattleScene extends Phaser.Scene {
this.hideCommandMenu();
const menuWidth = 432;
const titleHeight = 52;
const rowHeight = 112;
const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) }));
const readyCount = forecasts.filter(({ forecast }) => forecast.available).length;
const menuWidth = 486;
const titleHeight = 58;
const rowHeight = 130;
const padding = 10;
const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
@@ -4823,7 +4917,7 @@ export class BattleScene extends Phaser.Scene {
title.setDepth(15);
this.commandMenuObjects.push(title);
const subtitle = this.add.text(left + 16, top + padding + 29, `목록 ${usables.length} · HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name}`, {
const subtitle = this.add.text(left + 16, top + padding + 31, `HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name} · ${command === 'strategy' ? '책략 선택' : '도구 선택'}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#aeb7c2',
@@ -4832,28 +4926,47 @@ export class BattleScene extends Phaser.Scene {
subtitle.setDepth(15);
this.commandMenuObjects.push(subtitle);
usables.forEach((usable, index) => {
const countBadge = this.add.rectangle(left + menuWidth - 92, top + padding + 12, 70, 28, 0x0b1118, 0.94);
countBadge.setOrigin(0);
countBadge.setDepth(16);
countBadge.setStrokeStyle(1, readyCount > 0 ? accentColor : 0xb64a45, readyCount > 0 ? 0.64 : 0.76);
this.commandMenuObjects.push(countBadge);
const countText = this.add.text(left + menuWidth - 57, top + padding + 19, `${readyCount}/${usables.length}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: readyCount > 0 ? '#f2e3bf' : '#ffcf9a',
fontStyle: '700'
});
countText.setOrigin(0.5, 0);
countText.setDepth(17);
this.commandMenuObjects.push(countText);
forecasts.forEach(({ usable, forecast }, index) => {
const rowTop = top + padding + titleHeight + index * rowHeight;
const accent = this.usableAccentColor(usable);
const forecast = this.usableMenuForecast(unit, usable);
const rowLeft = left + padding;
const rowWidth = menuWidth - padding * 2;
const contentLeft = left + 92;
const sideBoxLeft = left + menuWidth - 112;
const rowRight = rowLeft + rowWidth;
const contentLeft = rowLeft + 90;
const sideBoxWidth = 112;
const sideBoxLeft = rowRight - sideBoxWidth - 10;
const contentWidth = sideBoxLeft - contentLeft - 12;
const baseFill = forecast.available ? 0x1a2630 : 0x151c23;
const bg = this.add.rectangle(rowLeft, rowTop, rowWidth, rowHeight - 8, baseFill, 0.95);
const bg = this.add.rectangle(rowLeft, rowTop, rowWidth, rowHeight - 8, baseFill, forecast.available ? 0.97 : 0.92);
bg.setOrigin(0);
bg.setDepth(15);
bg.setStrokeStyle(1, forecast.available ? accent : 0x6c7480, forecast.available ? 0.72 : 0.52);
bg.setInteractive({ useHandCursor: true });
this.commandMenuObjects.push(bg);
const iconFrame = this.add.rectangle(left + 52, rowTop + 52, 66, 66, 0x0a0f14, 0.9);
const iconFrame = this.add.rectangle(rowLeft + 43, rowTop + 61, 76, 76, 0x0a0f14, forecast.available ? 0.92 : 0.86);
iconFrame.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42);
iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame);
const icon = this.createBattleUiIcon(left + 52, rowTop + 52, this.usableIcon(usable), 58);
const icon = this.createBattleUiIcon(rowLeft + 43, rowTop + 61, this.usableIcon(usable), 68);
icon.setAlpha(forecast.available ? 1 : 0.72);
icon.setDepth(17);
this.commandMenuObjects.push(icon);
@@ -4863,85 +4976,84 @@ export class BattleScene extends Phaser.Scene {
fontSize: '21px',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700',
fixedWidth: sideBoxLeft - contentLeft - 10
fixedWidth: contentWidth
});
name.setDepth(16);
this.commandMenuObjects.push(name);
const stockText = this.add.text(left + menuWidth - 20, rowTop + 11, stock, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: command === 'item' ? '#f4dfad' : '#9fb0bf',
fontStyle: '700'
});
stockText.setOrigin(1, 0);
stockText.setDepth(16);
this.commandMenuObjects.push(stockText);
const statusTone = forecast.available ? accent : 0xb64a45;
const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 36, 92, 28, 0x0b1118, 0.94);
const statusBg = this.add.rectangle(rowRight - 98, rowTop + 9, 88, 28, 0x0b1118, 0.95);
statusBg.setOrigin(0);
statusBg.setDepth(16);
statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.62 : 0.76);
statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.66 : 0.78);
this.commandMenuObjects.push(statusBg);
const statusText = this.add.text(sideBoxLeft + 46, rowTop + 42, forecast.status, {
const statusText = this.add.text(rowRight - 54, rowTop + 16, forecast.status, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: forecast.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700',
fixedWidth: 88,
fixedWidth: 84,
align: 'center'
});
statusText.setOrigin(0.5, 0);
statusText.setDepth(17);
this.commandMenuObjects.push(statusText);
const summary = this.add.text(
contentLeft,
rowTop + 39,
forecast.detail,
{
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: forecast.available ? '#aeb7c2' : '#ffcf9a',
fixedWidth: sideBoxLeft - contentLeft - 12
}
);
const metaText = this.add.text(contentLeft, rowTop + 35, `${this.usableChannelLabel(usable)} · ${this.usableEffectLabel(usable)} · ${this.usableTargetLabel(usable)} ${this.usableRangeText(usable)} · ${stock}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#9fb0bf',
fontStyle: '700',
fixedWidth: contentWidth
});
metaText.setDepth(16);
this.commandMenuObjects.push(metaText);
const summary = this.add.text(contentLeft, rowTop + 54, this.truncateUiText(forecast.detail, 32), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: forecast.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700',
fixedWidth: contentWidth
});
summary.setDepth(16);
this.commandMenuObjects.push(summary);
this.renderUsableMenuPill(contentLeft, rowTop + 70, 96, this.usableIcon(usable), this.usableEffectLabel(usable), accent);
this.renderUsableMenuPill(contentLeft + 104, rowTop + 70, 116, this.usableTargetIcon(usable), `${this.usableTargetLabel(usable)} ${usable.range}`, accent);
const valueBox = this.add.rectangle(sideBoxLeft, rowTop + 68, 92, 34, 0x0b1118, 0.94);
valueBox.setOrigin(0);
valueBox.setDepth(16);
valueBox.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.44);
this.commandMenuObjects.push(valueBox);
const valueIcon = this.createBattleUiIcon(sideBoxLeft + 16, rowTop + 85, this.usablePowerIcon(usable), 24);
valueIcon.setDepth(17);
this.commandMenuObjects.push(valueIcon);
const valueText = this.add.text(sideBoxLeft + 86, rowTop + 83, forecast.value, {
const secondary = this.add.text(contentLeft, rowTop + 72, this.truncateUiText(forecast.secondary, 36), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700'
fontSize: '11px',
color: forecast.available ? '#aeb7c2' : '#d8b199',
fixedWidth: contentWidth
});
valueText.setOrigin(1, 0);
valueText.setDepth(17);
this.commandMenuObjects.push(valueText);
secondary.setDepth(16);
this.commandMenuObjects.push(secondary);
const valueLabel = this.add.text(sideBoxLeft + 34, rowTop + 73, forecast.valueLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px',
color: '#9fb0bf',
fontStyle: '700'
});
valueLabel.setDepth(17);
this.commandMenuObjects.push(valueLabel);
this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 45, sideBoxWidth, 66, 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
);
const choose = (choicePointer: Phaser.Input.Pointer) => {
if (choicePointer.rightButtonDown()) {
@@ -4950,6 +5062,10 @@ export class BattleScene extends Phaser.Scene {
}
if (choicePointer.leftButtonDown()) {
this.suppressNextLeftClick = true;
if (!forecast.available) {
showDetail();
return;
}
this.beginUsableTargeting(unit, usable);
}
};
@@ -4957,7 +5073,7 @@ export class BattleScene extends Phaser.Scene {
bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98);
this.renderUnitDetail(unit, this.usableDetailText(unit, usable));
};
const clearHover = () => bg.setFillStyle(baseFill, 0.95);
const clearHover = () => bg.setFillStyle(baseFill, forecast.available ? 0.97 : 0.92);
bg.on('pointerover', () => {
showDetail();
});