Improve usable action menu readability
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user