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; 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) { private showCommandMenu(unit: UnitData, pointerX: number, pointerY: number) {
this.hideCommandMenu(); this.hideCommandMenu();
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait']; const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
const menuWidth = 152; const menuWidth = 300;
const titleHeight = 31; const titleHeight = 46;
const buttonHeight = 34; const buttonHeight = 58;
const gap = 5; const gap = 7;
const padding = 8; const padding = 10;
const menuHeight = padding * 2 + titleHeight + commands.length * buttonHeight + (commands.length - 1) * gap; const menuHeight = padding * 2 + titleHeight + commands.length * buttonHeight + (commands.length - 1) * gap;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight);
@@ -4364,43 +4427,83 @@ export class BattleScene extends Phaser.Scene {
panel.setDepth(14); panel.setDepth(14);
this.commandMenuObjects.push(panel); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '16px', fontSize: '18px',
color: '#d8b15f', color: '#d8b15f',
fontStyle: '700' fontStyle: '700'
}); });
title.setOrigin(0.5, 0);
title.setDepth(15); title.setDepth(15);
this.commandMenuObjects.push(title); 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) => { commands.forEach((command, index) => {
const buttonTop = top + padding + titleHeight + index * (buttonHeight + gap); 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 centerY = buttonTop + buttonHeight / 2;
const background = this.add.rectangle(centerX, centerY, menuWidth - padding * 2, buttonHeight, 0x1a2630, 0.94); const buttonLeft = left + padding;
background.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.7); 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.setInteractive({ useHandCursor: true });
background.setDepth(15); background.setDepth(15);
const iconFrame = this.add.rectangle(left + 28, centerY, 30, 30, 0x0a0f14, 0.86); const iconFrame = this.add.rectangle(left + 40, centerY, 42, 42, 0x0a0f14, 0.9);
iconFrame.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.58); iconFrame.setStrokeStyle(1, accent, info.available ? 0.72 : 0.44);
iconFrame.setDepth(16); iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame); 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); icon.setDepth(17);
this.commandMenuObjects.push(icon); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px', fontSize: '19px',
color: '#f2e3bf', color: info.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700' fontStyle: '700',
fixedWidth: 86
}); });
text.setOrigin(0.5);
text.setDepth(16); 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) => { const chooseCommand = (choicePointer: Phaser.Input.Pointer) => {
if (choicePointer.rightButtonDown()) { if (choicePointer.rightButtonDown()) {
this.cancelPendingMove(); this.cancelPendingMove();
@@ -4422,10 +4525,17 @@ export class BattleScene extends Phaser.Scene {
this.completeUnitAction(unit, command); this.completeUnitAction(unit, command);
} }
}; };
background.on('pointerover', () => background.setFillStyle(0x283947, 0.98)); const showDetail = () => {
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.94)); 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); background.on('pointerdown', chooseCommand);
text.setInteractive({ useHandCursor: true }); text.setInteractive({ useHandCursor: true });
text.on('pointerover', showDetail);
text.on('pointerout', clearHover);
text.on('pointerdown', chooseCommand); text.on('pointerdown', chooseCommand);
this.commandButtons.push({ command, background, text }); 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) { 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.setOrigin(0);
pill.setDepth(16); pill.setDepth(16);
pill.setStrokeStyle(1, tone, 0.54); pill.setStrokeStyle(1, tone, 0.54);
this.commandMenuObjects.push(pill); 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); iconImage.setDepth(17);
this.commandMenuObjects.push(iconImage); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: '#d4dce6', color: '#d4dce6',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 38 fixedWidth: width - 40
}); });
text.setDepth(17); text.setDepth(17);
this.commandMenuObjects.push(text); this.commandMenuObjects.push(text);
@@ -4680,56 +4790,71 @@ export class BattleScene extends Phaser.Scene {
this.hideCommandMenu(); this.hideCommandMenu();
const menuWidth = 384; const menuWidth = 432;
const titleHeight = 38; const titleHeight = 52;
const rowHeight = 96; const rowHeight = 112;
const padding = 9; const padding = 10;
const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight; const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight;
const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); 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); const panel = this.add.rectangle(left, top, menuWidth, menuHeight, 0x101821, 0.98);
panel.setOrigin(0); panel.setOrigin(0);
panel.setStrokeStyle(2, command === 'strategy' ? palette.blue : palette.gold, 0.78); panel.setStrokeStyle(2, accentColor, 0.78);
panel.setDepth(14); panel.setDepth(14);
this.commandMenuObjects.push(panel); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '17px', fontSize: '19px',
color: '#d8b15f', color: '#d8b15f',
fontStyle: '700' fontStyle: '700'
}); });
title.setOrigin(0.5, 0);
title.setDepth(15); title.setDepth(15);
this.commandMenuObjects.push(title); 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) => { usables.forEach((usable, index) => {
const rowTop = top + padding + titleHeight + index * rowHeight; const rowTop = top + padding + titleHeight + index * rowHeight;
const accent = this.usableAccentColor(usable); const accent = this.usableAccentColor(usable);
const forecast = this.usableMenuForecast(unit, 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.setOrigin(0);
bg.setDepth(15); bg.setDepth(15);
bg.setStrokeStyle(1, forecast.available ? accent : 0x6c7480, forecast.available ? 0.72 : 0.52); bg.setStrokeStyle(1, forecast.available ? accent : 0x6c7480, forecast.available ? 0.72 : 0.52);
bg.setInteractive({ useHandCursor: true }); bg.setInteractive({ useHandCursor: true });
this.commandMenuObjects.push(bg); 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.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42);
iconFrame.setDepth(16); iconFrame.setDepth(16);
this.commandMenuObjects.push(iconFrame); 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); icon.setDepth(17);
this.commandMenuObjects.push(icon); this.commandMenuObjects.push(icon);
const stock = this.usableStockLabel(unit, usable); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px', fontSize: '21px',
color: forecast.available ? '#f2e3bf' : '#c8ced6', color: forecast.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700' fontStyle: '700',
fixedWidth: sideBoxLeft - contentLeft - 10
}); });
name.setDepth(16); name.setDepth(16);
this.commandMenuObjects.push(name); this.commandMenuObjects.push(name);
@@ -4745,51 +4870,52 @@ export class BattleScene extends Phaser.Scene {
this.commandMenuObjects.push(stockText); this.commandMenuObjects.push(stockText);
const statusTone = forecast.available ? accent : 0xb64a45; const statusTone = forecast.available ? accent : 0xb64a45;
const sideBoxLeft = left + menuWidth - 94; const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 36, 92, 28, 0x0b1118, 0.94);
const statusBg = this.add.rectangle(sideBoxLeft, rowTop + 34, 74, 24, 0x0b1118, 0.94);
statusBg.setOrigin(0); statusBg.setOrigin(0);
statusBg.setDepth(16); statusBg.setDepth(16);
statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.62 : 0.76); statusBg.setStrokeStyle(1, statusTone, forecast.available ? 0.62 : 0.76);
this.commandMenuObjects.push(statusBg); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px', fontSize: '12px',
color: forecast.available ? '#d4dce6' : '#ffcf9a', color: forecast.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700' fontStyle: '700',
fixedWidth: 88,
align: 'center'
}); });
statusText.setOrigin(0.5, 0); statusText.setOrigin(0.5, 0);
statusText.setDepth(17); statusText.setDepth(17);
this.commandMenuObjects.push(statusText); this.commandMenuObjects.push(statusText);
const summary = this.add.text( const summary = this.add.text(
left + 78, contentLeft,
rowTop + 36, rowTop + 39,
forecast.detail, forecast.detail,
{ {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px', fontSize: '13px',
color: forecast.available ? '#aeb7c2' : '#ffcf9a', color: forecast.available ? '#aeb7c2' : '#ffcf9a',
fixedWidth: menuWidth - 186 fixedWidth: sideBoxLeft - contentLeft - 12
} }
); );
summary.setDepth(16); summary.setDepth(16);
this.commandMenuObjects.push(summary); this.commandMenuObjects.push(summary);
this.renderUsableMenuPill(left + 78, rowTop + 60, 90, this.usableIcon(usable), this.usableEffectLabel(usable), accent); this.renderUsableMenuPill(contentLeft, rowTop + 70, 96, 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 + 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.setOrigin(0);
valueBox.setDepth(16); valueBox.setDepth(16);
valueBox.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.44); valueBox.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.44);
this.commandMenuObjects.push(valueBox); 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); valueIcon.setDepth(17);
this.commandMenuObjects.push(valueIcon); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px', fontSize: '14px',
color: forecast.available ? '#f2e3bf' : '#c8ced6', color: forecast.available ? '#f2e3bf' : '#c8ced6',
@@ -4799,7 +4925,7 @@ export class BattleScene extends Phaser.Scene {
valueText.setDepth(17); valueText.setDepth(17);
this.commandMenuObjects.push(valueText); 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', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '10px',
color: '#9fb0bf', color: '#9fb0bf',
@@ -4818,13 +4944,21 @@ export class BattleScene extends Phaser.Scene {
this.beginUsableTargeting(unit, usable); this.beginUsableTargeting(unit, usable);
} }
}; };
bg.on('pointerover', () => { const showDetail = () => {
bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98); bg.setFillStyle(forecast.available ? 0x283947 : 0x27303a, 0.98);
this.renderUnitDetail(unit, this.usableDetailText(unit, usable)); 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); bg.on('pointerdown', choose);
name.setInteractive({ useHandCursor: true }); name.setInteractive({ useHandCursor: true });
name.on('pointerover', showDetail);
name.on('pointerout', clearHover);
name.on('pointerdown', choose); name.on('pointerdown', choose);
}); });
} }