Improve battle action menu readability

This commit is contained in:
2026-07-03 02:53:04 +09:00
parent 0f8c9df853
commit ba6bcc6b2d

View File

@@ -4346,15 +4346,78 @@ export class BattleScene extends Phaser.Scene {
return 0x9e3e58;
}
private commandAccentColor(command: BattleCommand) {
if (command === 'attack') {
return 0xd8b15f;
}
if (command === 'strategy') {
return palette.blue;
}
if (command === 'item') {
return palette.green;
}
return 0x9fb0bf;
}
private commandMenuInfo(unit: UnitData, command: BattleCommand) {
if (command === 'attack') {
const targets = this.damageableTargets(unit, 'attack');
const target = targets[0];
if (!target) {
return {
available: false,
status: '대상 없음',
detail: '현재 사거리 안 적 없음'
};
}
const preview = this.combatPreview(unit, target, 'attack');
return {
available: true,
status: `대상 ${targets.length}`,
detail: `${target.name} · 피해 ${preview.damage} · ${preview.hitRate}%`
};
}
if (command === 'strategy' || command === 'item') {
const usables = this.availableUsables(unit, command);
if (usables.length === 0) {
return {
available: false,
status: command === 'strategy' ? '책략 없음' : '도구 없음',
detail: command === 'strategy' ? '사용 가능한 책략이 없습니다.' : '보유한 도구가 없습니다.'
};
}
const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) }));
const availableCount = forecasts.filter(({ forecast }) => forecast.available).length;
const firstReady = forecasts.find(({ forecast }) => forecast.available) ?? forecasts[0];
return {
available: availableCount > 0,
status: availableCount > 0 ? `가능 ${availableCount}` : '대상 없음',
detail:
availableCount > 0
? `${firstReady.usable.name} · ${firstReady.forecast.valueLabel} ${firstReady.forecast.value}`
: `${firstReady.usable.name} · ${firstReady.forecast.status}`
};
}
return {
available: true,
status: '종료',
detail: '이 유닛 행동 종료'
};
}
private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) {
this.hideCommandMenu();
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
const menuWidth = 152;
const titleHeight = 31;
const buttonHeight = 34;
const gap = 5;
const padding = 8;
const menuWidth = 300;
const titleHeight = 46;
const buttonHeight = 58;
const gap = 7;
const padding = 10;
const menuHeight = padding * 2 + titleHeight + commands.length * buttonHeight + (commands.length - 1) * gap;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
@@ -4364,43 +4427,83 @@ export class BattleScene extends Phaser.Scene {
panel.setDepth(14);
this.commandMenuObjects.push(panel);
const title = this.add.text(left + menuWidth / 2, top + padding + 2, '명령', {
const title = this.add.text(left + 16, top + padding + 2, `${unit.name} 명령`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '16px',
fontSize: '18px',
color: '#d8b15f',
fontStyle: '700'
});
title.setOrigin(0.5, 0);
title.setDepth(15);
this.commandMenuObjects.push(title);
const subtitle = this.add.text(left + 16, top + padding + 26, `HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#aeb7c2',
fontStyle: '700'
});
subtitle.setDepth(15);
this.commandMenuObjects.push(subtitle);
commands.forEach((command, index) => {
const buttonTop = top + padding + titleHeight + index * (buttonHeight + gap);
const centerX = left + menuWidth / 2;
const accent = this.commandAccentColor(command);
const info = this.commandMenuInfo(unit, command);
const baseFill = info.available ? 0x1a2630 : 0x151c23;
const centerY = buttonTop + buttonHeight / 2;
const background = this.add.rectangle(centerX, centerY, menuWidth - padding * 2, buttonHeight, 0x1a2630, 0.94);
background.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7);
const buttonLeft = left + padding;
const background = this.add.rectangle(buttonLeft, buttonTop, menuWidth - padding * 2, buttonHeight, baseFill, 0.95);
background.setOrigin(0);
background.setStrokeStyle(1, accent, info.available ? 0.74 : 0.46);
background.setInteractive({ useHandCursor: true });
background.setDepth(15);
const iconFrame = this.add.rectangle(left + 28, centerY, 30, 30, 0x0a0f14, 0.86);
iconFrame.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.58);
const iconFrame = this.add.rectangle(left + 40, centerY, 42, 42, 0x0a0f14, 0.9);
iconFrame.setStrokeStyle(1, accent, info.available ? 0.72 : 0.44);
iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame);
const icon = this.createBattleUiIcon(left + 28, centerY, this.commandIcon(command, unit), 26);
const icon = this.createBattleUiIcon(left + 40, centerY, this.commandIcon(command, unit), 38);
icon.setDepth(17);
this.commandMenuObjects.push(icon);
const text = this.add.text(centerX + 13, centerY, commandLabels[command], {
const text = this.add.text(left + 68, buttonTop + 8, commandLabels[command], {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px',
color: '#f2e3bf',
fontStyle: '700'
fontSize: '19px',
color: info.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700',
fixedWidth: 86
});
text.setOrigin(0.5);
text.setDepth(16);
const detail = this.add.text(left + 68, buttonTop + 34, info.detail, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
color: info.available ? '#aeb7c2' : '#ffcf9a',
fixedWidth: menuWidth - 164
});
detail.setDepth(16);
this.commandMenuObjects.push(detail);
const badgeLeft = left + menuWidth - 92;
const badge = this.add.rectangle(badgeLeft, buttonTop + 15, 74, 28, 0x0b1118, 0.94);
badge.setOrigin(0);
badge.setDepth(16);
badge.setStrokeStyle(1, info.available ? accent : 0xb64a45, info.available ? 0.62 : 0.74);
this.commandMenuObjects.push(badge);
const badgeText = this.add.text(badgeLeft + 37, buttonTop + 21, info.status, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px',
color: info.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700',
fixedWidth: 70,
align: 'center'
});
badgeText.setOrigin(0.5, 0);
badgeText.setDepth(17);
this.commandMenuObjects.push(badgeText);
const chooseCommand = (choicePointer: Phaser.Input.Pointer) => {
if (choicePointer.rightButtonDown()) {
this.cancelPendingMove();
@@ -4422,10 +4525,17 @@ export class BattleScene extends Phaser.Scene {
this.completeUnitAction(unit, command);
}
};
background.on('pointerover', () => background.setFillStyle(0x283947, 0.98));
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94));
const showDetail = () => {
background.setFillStyle(info.available ? 0x283947 : 0x27303a, 0.98);
this.renderUnitDetail(unit, `${commandLabels[command]}\n${info.detail}`);
};
const clearHover = () => background.setFillStyle(baseFill, 0.95);
background.on('pointerover', showDetail);
background.on('pointerout', clearHover);
background.on('pointerdown', chooseCommand);
text.setInteractive({ useHandCursor: true });
text.on('pointerover', showDetail);
text.on('pointerout', clearHover);
text.on('pointerdown', chooseCommand);
this.commandButtons.push({ command, background, text });
@@ -4640,22 +4750,22 @@ export class BattleScene extends Phaser.Scene {
}
private renderUsableMenuPill(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, tone: number) {
const pill = this.add.rectangle(x, y, width, 28, 0x0b1118, 0.92);
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);
const iconImage = this.createBattleUiIcon(x + 15, y + 14, icon, 22);
const iconImage = this.createBattleUiIcon(x + 16, y + 15, icon, 24);
iconImage.setDepth(17);
this.commandMenuObjects.push(iconImage);
const text = this.add.text(x + 33, y + 6, label, {
const text = this.add.text(x + 35, y + 6, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#d4dce6',
fontStyle: '700',
fixedWidth: width - 38
fixedWidth: width - 40
});
text.setDepth(17);
this.commandMenuObjects.push(text);
@@ -4680,56 +4790,71 @@ export class BattleScene extends Phaser.Scene {
this.hideCommandMenu();
const menuWidth = 384;
const titleHeight = 38;
const rowHeight = 96;
const padding = 9;
const menuWidth = 432;
const titleHeight = 52;
const rowHeight = 112;
const padding = 10;
const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
const titleLabel = command === 'strategy' ? `책략 선택 ${usables.length}` : `도구 선택 ${usables.length}`;
const accentColor = command === 'strategy' ? palette.blue : palette.gold;
const titleLabel = `${unit.name} · ${commandLabels[command]}`;
const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.98);
panel.setOrigin(0);
panel.setStrokeStyle(2, command === 'strategy' ? palette.blue : palette.gold, 0.78);
panel.setStrokeStyle(2, accentColor, 0.78);
panel.setDepth(14);
this.commandMenuObjects.push(panel);
const title = this.add.text(left + menuWidth / 2, top + padding + 2, titleLabel, {
const title = this.add.text(left + 16, top + padding + 2, titleLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '17px',
fontSize: '19px',
color: '#d8b15f',
fontStyle: '700'
});
title.setOrigin(0.5, 0);
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}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px',
color: '#aeb7c2',
fontStyle: '700'
});
subtitle.setDepth(15);
this.commandMenuObjects.push(subtitle);
usables.forEach((usable, index) => {
const rowTop = top + padding + titleHeight + index * rowHeight;
const accent = this.usableAccentColor(usable);
const forecast = this.usableMenuForecast(unit, usable);
const bg = this.add.rectangle(left + 8, rowTop, menuWidth - 16, rowHeight - 8, 0x1a2630, 0.95);
const rowLeft = left + padding;
const rowWidth = menuWidth - padding * 2;
const contentLeft = left + 92;
const sideBoxLeft = left + menuWidth - 112;
const baseFill = forecast.available ? 0x1a2630 : 0x151c23;
const bg = this.add.rectangle(rowLeft, rowTop, rowWidth, rowHeight - 8, baseFill, 0.95);
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 + 42, rowTop + 44, 58, 58, 0x0a0f14, 0.9);
const iconFrame = this.add.rectangle(left + 52, rowTop + 52, 66, 66, 0x0a0f14, 0.9);
iconFrame.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42);
iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame);
const icon = this.createBattleUiIcon(left + 42, rowTop + 44, this.usableIcon(usable), 52);
const icon = this.createBattleUiIcon(left + 52, rowTop + 52, this.usableIcon(usable), 58);
icon.setDepth(17);
this.commandMenuObjects.push(icon);
const stock = this.usableStockLabel(unit, usable);
const name = this.add.text(left + 78, rowTop + 9, usable.name, {
const name = this.add.text(contentLeft, rowTop + 9, usable.name, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px',
fontSize: '21px',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700'
fontStyle: '700',
fixedWidth: sideBoxLeft - contentLeft - 10
});
name.setDepth(16);
this.commandMenuObjects.push(name);
@@ -4745,51 +4870,52 @@ export class BattleScene extends Phaser.Scene {
this.commandMenuObjects.push(stockText);
const statusTone = forecast.available ? accent : 0xb64a45;
const sideBoxLeft = left + menuWidth - 94;
const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 34, 74, 24, 0x0b1118, 0.94);
const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 36, 92, 28, 0x0b1118, 0.94);
statusBg.setOrigin(0);
statusBg.setDepth(16);
statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.62 : 0.76);
this.commandMenuObjects.push(statusBg);
const statusText = this.add.text(sideBoxLeft + 37, rowTop + 40, forecast.status, {
const statusText = this.add.text(sideBoxLeft + 46, rowTop + 42, forecast.status, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
fontSize: '12px',
color: forecast.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700'
fontStyle: '700',
fixedWidth: 88,
align: 'center'
});
statusText.setOrigin(0.5, 0);
statusText.setDepth(17);
this.commandMenuObjects.push(statusText);
const summary = this.add.text(
left + 78,
rowTop + 36,
contentLeft,
rowTop + 39,
forecast.detail,
{
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: forecast.available ? '#aeb7c2' : '#ffcf9a',
fixedWidth: menuWidth - 186
fixedWidth: sideBoxLeft - contentLeft - 12
}
);
summary.setDepth(16);
this.commandMenuObjects.push(summary);
this.renderUsableMenuPill(left + 78, rowTop + 60, 90, this.usableIcon(usable), this.usableEffectLabel(usable), accent);
this.renderUsableMenuPill(left + 174, rowTop + 60, 104, this.usableTargetIcon(usable), `${this.usableTargetLabel(usable)} ${usable.range}`, accent);
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 + 58, 74, 30, 0x0b1118, 0.94);
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 + 14, rowTop + 73, this.usablePowerIcon(usable), 22);
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 + 70, rowTop + 71, forecast.value, {
const valueText = this.add.text(sideBoxLeft + 86, rowTop + 83, forecast.value, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: forecast.available ? '#f2e3bf' : '#c8ced6',
@@ -4799,7 +4925,7 @@ export class BattleScene extends Phaser.Scene {
valueText.setDepth(17);
this.commandMenuObjects.push(valueText);
const valueLabel = this.add.text(sideBoxLeft + 29, rowTop + 62, forecast.valueLabel, {
const valueLabel = this.add.text(sideBoxLeft + 34, rowTop + 73, forecast.valueLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px',
color: '#9fb0bf',
@@ -4818,13 +4944,21 @@ export class BattleScene extends Phaser.Scene {
this.beginUsableTargeting(unit, usable);
}
};
bg.on('pointerover', () => {
const showDetail = () => {
bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98);
this.renderUnitDetail(unit, this.usableDetailText(unit, usable));
};
const clearHover = () => bg.setFillStyle(baseFill, 0.95);
bg.on('pointerover', () => {
showDetail();
});
bg.on('pointerout', () => {
clearHover();
});
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.94));
bg.on('pointerdown', choose);
name.setInteractive({ useHandCursor: true });
name.on('pointerover', showDetail);
name.on('pointerout', clearHover);
name.on('pointerdown', choose);
});
}