Rework battle command selection menus

This commit is contained in:
2026-07-03 17:44:17 +09:00
parent ea6acefe7b
commit 43dab94ba6
8 changed files with 142 additions and 92 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -0,0 +1,23 @@
# Battle command menu rework report
## Summary
- Reworked the battle command menu rows to use larger raster icons, clearer status badges, and short tactical details.
- Reworked strategy/item selection rows to align with the combat preview panel: large action icon, value card, and three compact metric chips.
- Reworked target candidate rows to show icon-based mini metrics for damage/heal/buff, success or hit rate, and counter/status impact.
## Verification
- Build: `pnpm build` succeeded.
- Local browser: command menu, strategy menu, and support target candidate guide opened without console errors.
- Deployed browser: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=command-menu-rework-20260703b` opened in a PC browser without console errors.
- Console errors: 0 during local verification and 0 during deployed verification.
## Captures
- Local command menu: `docs/battle-command-menu-rework-local-command-menu.png`
- Local strategy menu: `docs/battle-command-menu-rework-local-strategy-menu.png`
- Local support targeting guide: `docs/battle-command-menu-rework-local-support-targeting.png`
- Deployed command menu: `docs/battle-command-menu-rework-prod-command-menu.png`
- Deployed strategy menu: `docs/battle-command-menu-rework-prod-strategy-menu.png`
- Deployed support targeting guide: `docs/battle-command-menu-rework-prod-support-targeting.png`

View File

@@ -1441,6 +1441,7 @@ type TargetingGuideCandidate = {
primary: string; primary: string;
secondary: string; secondary: string;
tone: number; tone: number;
metrics?: TargetingGuideMetric[];
}; };
type TargetingGuideNotice = { type TargetingGuideNotice = {
@@ -4470,7 +4471,7 @@ export class BattleScene extends Phaser.Scene {
return { return {
available: true, available: true,
status: `대상 ${targets.length}`, status: `대상 ${targets.length}`,
detail: `${target.name} · 피해 ${preview.damage} · ${preview.hitRate}%` detail: `${target.name} · 피해 ${preview.damage} · ${preview.hitRate}% · ${preview.counterAvailable ? '반격 주의' : '반격 없음'}`
}; };
} }
@@ -4508,10 +4509,10 @@ export class BattleScene extends Phaser.Scene {
this.hideCommandMenu(); this.hideCommandMenu();
const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait']; const commands: BattleCommand[] = ['attack', 'strategy', 'item', 'wait'];
const menuWidth = 300; const menuWidth = 330;
const titleHeight = 46; const titleHeight = 54;
const buttonHeight = 58; const buttonHeight = 70;
const gap = 7; const gap = 8;
const padding = 10; 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);
@@ -4522,16 +4523,16 @@ 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 + 16, top + padding + 2, `${unit.name} 명령`, { const title = this.add.text(left + 16, top + padding + 3, `${unit.name} 명령`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '18px', fontSize: '20px',
color: '#d8b15f', color: '#d8b15f',
fontStyle: '700' fontStyle: '700'
}); });
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}`, { const subtitle = this.add.text(left + 16, top + padding + 31, `HP ${unit.hp}/${unit.maxHp} · ${getUnitClass(unit.classKey).name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: '#aeb7c2', color: '#aeb7c2',
@@ -4553,46 +4554,46 @@ export class BattleScene extends Phaser.Scene {
background.setInteractive({ useHandCursor: true }); background.setInteractive({ useHandCursor: true });
background.setDepth(15); background.setDepth(15);
const iconFrame = this.add.rectangle(left + 40, centerY, 42, 42, 0x0a0f14, 0.9); const iconFrame = this.add.rectangle(left + 46, centerY, 56, 56, 0x0a0f14, 0.93);
iconFrame.setStrokeStyle(1, accent, info.available ? 0.72 : 0.44); 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 + 40, centerY, this.commandIcon(command, unit), 38); const icon = this.createBattleUiIcon(left + 46, centerY, this.commandIcon(command, unit), 50);
icon.setDepth(17); icon.setDepth(17);
this.commandMenuObjects.push(icon); this.commandMenuObjects.push(icon);
const text = this.add.text(left + 68, buttonTop + 8, commandLabels[command], { const text = this.add.text(left + 84, buttonTop + 10, commandLabels[command], {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '19px', fontSize: '20px',
color: info.available ? '#f2e3bf' : '#c8ced6', color: info.available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700', fontStyle: '700',
fixedWidth: 86 fixedWidth: 92
}); });
text.setDepth(16); text.setDepth(16);
const detail = this.add.text(left + 68, buttonTop + 34, info.detail, { const detail = this.add.text(left + 84, buttonTop + 40, info.detail, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px', fontSize: '12px',
color: info.available ? '#aeb7c2' : '#ffcf9a', color: info.available ? '#aeb7c2' : '#ffcf9a',
fixedWidth: menuWidth - 164 fixedWidth: menuWidth - 194
}); });
detail.setDepth(16); detail.setDepth(16);
this.commandMenuObjects.push(detail); this.commandMenuObjects.push(detail);
const badgeLeft = left + menuWidth - 92; const badgeLeft = left + menuWidth - 106;
const badge = this.add.rectangle(badgeLeft, buttonTop + 15, 74, 28, 0x0b1118, 0.94); const badge = this.add.rectangle(badgeLeft, buttonTop + 19, 88, 32, 0x0b1118, 0.95);
badge.setOrigin(0); badge.setOrigin(0);
badge.setDepth(16); badge.setDepth(16);
badge.setStrokeStyle(1, info.available ? accent : 0xb64a45, info.available ? 0.62 : 0.74); badge.setStrokeStyle(1, info.available ? accent : 0xb64a45, info.available ? 0.62 : 0.74);
this.commandMenuObjects.push(badge); this.commandMenuObjects.push(badge);
const badgeText = this.add.text(badgeLeft + 37, buttonTop + 21, info.status, { const badgeText = this.add.text(badgeLeft + 44, buttonTop + 27, info.status, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '11px',
color: info.available ? '#d4dce6' : '#ffcf9a', color: info.available ? '#d4dce6' : '#ffcf9a',
fontStyle: '700', fontStyle: '700',
fixedWidth: 70, fixedWidth: 84,
align: 'center' align: 'center'
}); });
badgeText.setOrigin(0.5, 0); badgeText.setOrigin(0.5, 0);
@@ -4826,12 +4827,12 @@ export class BattleScene extends Phaser.Scene {
targetCount, targetCount,
valueLabel: '피해', valueLabel: '피해',
value: `${preview.damage}`, value: `${preview.damage}`,
detail: `${target.name} · 피해 ${preview.damage} · ${successLabel} ${preview.hitRate}%`, detail: `${target.name} · 피해 ${preview.damage} · ${successLabel} ${preview.hitRate}% · ${preview.counterAvailable ? '반격 주의' : '반격 없음'}`,
secondary: `치명 ${preview.criticalRate}% · 지형 ${preview.terrainLabel} · 사거리 ${usable.range}`, secondary: `치명 ${preview.criticalRate}% · 지형 ${preview.terrainLabel} · 사거리 ${usable.range}`,
chips: [ chips: [
{ icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent }, { icon: this.usablePowerIcon(usable), label: '피해', value: `${preview.damage}`, tone: 0xb64a45 },
{ icon: usable.command === 'strategy' ? 'success' : 'hit', label: successLabel, value: `${preview.hitRate}%`, tone: this.rateTone(preview.hitRate) }, { 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 } { icon: 'counter', label: '반격', value: preview.counterAvailable ? '주의' : '없음', tone: preview.counterAvailable ? 0xd8732c : 0x59d18c }
] ]
}; };
} }
@@ -4846,7 +4847,7 @@ export class BattleScene extends Phaser.Scene {
chips: [ chips: [
{ icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent }, { icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent },
{ icon: 'success', label: '성공률', value: '-', tone: 0x647485 }, { icon: 'success', label: '성공률', value: '-', tone: 0x647485 },
{ icon: this.usableTargetIcon(usable), label: '대상', value: '없음', tone: 0xb64a45 } { icon: 'counter', label: '반격', value: '-', tone: 0x647485 }
] ]
}; };
} }
@@ -4906,33 +4907,33 @@ export class BattleScene extends Phaser.Scene {
tone: number, tone: number,
available: boolean available: boolean
) { ) {
const chip = this.add.rectangle(x, y, width, 40, 0x0b1118, available ? 0.95 : 0.87); const chip = this.add.rectangle(x, y, width, 44, 0x0b1118, available ? 0.96 : 0.88);
chip.setOrigin(0); chip.setOrigin(0);
chip.setDepth(16); chip.setDepth(16);
chip.setStrokeStyle(1, tone, available ? 0.64 : 0.38); chip.setStrokeStyle(1, tone, available ? 0.64 : 0.38);
this.commandMenuObjects.push(chip); this.commandMenuObjects.push(chip);
const iconImage = this.createBattleUiIcon(x + 20, y + 20, icon, 31); const iconImage = this.createBattleUiIcon(x + 22, y + 22, icon, 34);
iconImage.setAlpha(available ? 0.98 : 0.72); iconImage.setAlpha(available ? 0.98 : 0.72);
iconImage.setDepth(17); iconImage.setDepth(17);
this.commandMenuObjects.push(iconImage); this.commandMenuObjects.push(iconImage);
const labelText = this.add.text(x + 41, y + 5, label, { const labelText = this.add.text(x + 44, y + 6, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '10px',
color: '#9fb0bf', color: '#9fb0bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 48 fixedWidth: width - 52
}); });
labelText.setDepth(17); labelText.setDepth(17);
this.commandMenuObjects.push(labelText); this.commandMenuObjects.push(labelText);
const valueText = this.add.text(x + 41, y + 21, this.truncateUiText(value, 10), { const valueText = this.add.text(x + 44, y + 23, this.truncateUiText(value, 10), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px', fontSize: '13px',
color: available ? '#f2e3bf' : '#c8ced6', color: available ? '#f2e3bf' : '#c8ced6',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 48 fixedWidth: width - 52
}); });
valueText.setDepth(17); valueText.setDepth(17);
this.commandMenuObjects.push(valueText); this.commandMenuObjects.push(valueText);
@@ -4953,17 +4954,17 @@ export class BattleScene extends Phaser.Scene {
valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.82 : 0.46); valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.82 : 0.46);
this.commandMenuObjects.push(valueBox); this.commandMenuObjects.push(valueBox);
const valueIcon = this.createBattleUiIcon(x + 30, y + height / 2, this.usablePowerIcon(usable), 46); const valueIcon = this.createBattleUiIcon(x + 32, y + height / 2, this.usablePowerIcon(usable), 52);
valueIcon.setAlpha(forecast.available ? 1 : 0.72); valueIcon.setAlpha(forecast.available ? 1 : 0.72);
valueIcon.setDepth(17); valueIcon.setDepth(17);
this.commandMenuObjects.push(valueIcon); this.commandMenuObjects.push(valueIcon);
const valueLabel = this.add.text(x + 62, y + 12, forecast.valueLabel, { const valueLabel = this.add.text(x + 66, y + 13, forecast.valueLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: '#9fb0bf', color: '#9fb0bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 70 fixedWidth: width - 74
}); });
valueLabel.setDepth(17); valueLabel.setDepth(17);
this.commandMenuObjects.push(valueLabel); this.commandMenuObjects.push(valueLabel);
@@ -5001,9 +5002,9 @@ export class BattleScene extends Phaser.Scene {
const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) })); const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) }));
const readyCount = forecasts.filter(({ forecast }) => forecast.available).length; const readyCount = forecasts.filter(({ forecast }) => forecast.available).length;
const menuWidth = 520; const menuWidth = 540;
const titleHeight = 58; const titleHeight = 58;
const rowHeight = 148; const rowHeight = 154;
const padding = 10; 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);
@@ -5057,7 +5058,7 @@ export class BattleScene extends Phaser.Scene {
const rowWidth = menuWidth - padding * 2; const rowWidth = menuWidth - padding * 2;
const rowRight = rowLeft + rowWidth; const rowRight = rowLeft + rowWidth;
const contentLeft = rowLeft + 102; const contentLeft = rowLeft + 102;
const sideBoxWidth = 124; const sideBoxWidth = 132;
const sideBoxLeft = rowRight - sideBoxWidth - 10; const sideBoxLeft = rowRight - sideBoxWidth - 10;
const contentWidth = sideBoxLeft - contentLeft - 12; const contentWidth = sideBoxLeft - contentLeft - 12;
const baseFill = forecast.available ? 0x1a2630 : 0x151c23; const baseFill = forecast.available ? 0x1a2630 : 0x151c23;
@@ -5068,12 +5069,12 @@ export class BattleScene extends Phaser.Scene {
bg.setInteractive({ useHandCursor: true }); bg.setInteractive({ useHandCursor: true });
this.commandMenuObjects.push(bg); this.commandMenuObjects.push(bg);
const iconFrame = this.add.rectangle(rowLeft + 48, rowTop + 70, 86, 86, 0x0a0f14, forecast.available ? 0.94 : 0.87); const iconFrame = this.add.rectangle(rowLeft + 48, rowTop + 72, 84, 84, 0x0a0f14, forecast.available ? 0.94 : 0.87);
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(rowLeft + 48, rowTop + 70, this.usableIcon(usable), 78); const icon = this.createBattleUiIcon(rowLeft + 48, rowTop + 72, this.usableIcon(usable), 70);
icon.setAlpha(forecast.available ? 1 : 0.72); icon.setAlpha(forecast.available ? 1 : 0.72);
icon.setDepth(17); icon.setDepth(17);
this.commandMenuObjects.push(icon); this.commandMenuObjects.push(icon);
@@ -5137,14 +5138,14 @@ export class BattleScene extends Phaser.Scene {
secondary.setDepth(16); secondary.setDepth(16);
this.commandMenuObjects.push(secondary); this.commandMenuObjects.push(secondary);
this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 52, sideBoxWidth, 76, usable, forecast, accent); this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 54, sideBoxWidth, 82, usable, forecast, accent);
const chipGap = 6; const chipGap = 6;
const chipWidth = (contentWidth - chipGap * 2) / 3; const chipWidth = (contentWidth - chipGap * 2) / 3;
forecast.chips.slice(0, 3).forEach((chip, chipIndex) => { forecast.chips.slice(0, 3).forEach((chip, chipIndex) => {
this.renderUsableMenuInfoChip( this.renderUsableMenuInfoChip(
contentLeft + chipIndex * (chipWidth + chipGap), contentLeft + chipIndex * (chipWidth + chipGap),
rowTop + 98, rowTop + 102,
chipWidth, chipWidth,
chip.icon, chip.icon,
chip.label, chip.label,
@@ -5500,14 +5501,20 @@ export class BattleScene extends Phaser.Scene {
const candidates = targets.slice(0, 3).map((target): TargetingGuideCandidate => { const candidates = targets.slice(0, 3).map((target): TargetingGuideCandidate => {
const preview = this.combatPreview(unit, target, action, usable); const preview = this.combatPreview(unit, target, action, usable);
const distance = this.tileDistance(unit, target); const distance = this.tileDistance(unit, target);
const successLabel = this.previewSuccessLabel(preview);
const counterLabel = preview.counterAvailable ? '반격 주의' : '반격 없음'; const counterLabel = preview.counterAvailable ? '반격 주의' : '반격 없음';
return { return {
icon: this.previewDamageIcon(preview), icon: this.previewDamageIcon(preview),
name: target.name, name: target.name,
meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${range}`, meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${range}`,
primary: `${preview.damage} 피해`, primary: `${preview.damage} 피해`,
secondary: `${preview.hitRate}% ${this.previewSuccessLabel(preview)} · ${counterLabel}`, secondary: `${preview.hitRate}% ${successLabel} · ${counterLabel}`,
tone: this.previewAccentColor(preview) tone: this.previewAccentColor(preview),
metrics: [
{ icon: this.previewDamageIcon(preview), label: '피해', value: `${preview.damage}`, tone: 0xb64a45 },
{ icon: action === 'strategy' ? 'success' : 'hit', label: successLabel, value: `${preview.hitRate}%`, tone: this.rateTone(preview.hitRate) },
{ icon: 'counter', label: '반격', value: preview.counterAvailable ? '주의' : '없음', tone: preview.counterAvailable ? 0xd8732c : 0x59d18c }
]
}; };
}); });
@@ -5546,7 +5553,18 @@ export class BattleScene extends Phaser.Scene {
meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${usable.range}`, meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${usable.range}`,
primary: isHeal ? `+${preview.healAmount} 회복` : `공+${preview.attackBonus}`, primary: isHeal ? `+${preview.healAmount} 회복` : `공+${preview.attackBonus}`,
secondary: isHeal ? `HP ${target.hp}${preview.projectedHp}` : `명+${preview.hitBonus} · 치+${preview.criticalBonus} · ${preview.duration}`, secondary: isHeal ? `HP ${target.hp}${preview.projectedHp}` : `명+${preview.hitBonus} · 치+${preview.criticalBonus} · ${preview.duration}`,
tone: isHeal ? 0x59d18c : accent tone: isHeal ? 0x59d18c : accent,
metrics: isHeal
? [
{ icon: this.usablePowerIcon(usable), label: '회복', value: `+${preview.healAmount}`, tone: 0x59d18c },
{ icon: 'hp', label: 'HP', value: `${preview.projectedHp}`, tone: 0x59d18c },
{ icon: 'success', label: '적용', value: '100%', tone: 0x59d18c }
]
: [
{ icon: 'focus', label: '공격', value: `+${preview.attackBonus}`, tone: palette.gold },
{ icon: 'hit', label: '명중', value: `+${preview.hitBonus}`, tone: palette.blue },
{ icon: 'critical', label: '치명', value: `+${preview.criticalBonus}`, tone: 0xd8732c }
]
}; };
}); });
@@ -5590,37 +5608,37 @@ export class BattleScene extends Phaser.Scene {
const left = panelX + 24; const left = panelX + 24;
const width = panelWidth - 48; const width = panelWidth - 48;
const top = this.sideContentTop(); const top = this.sideContentTop();
const height = 416; const height = 492;
const bg = this.trackSideObject(this.add.rectangle(left, top, width, height, 0x101820, 0.98)); const bg = this.trackSideObject(this.add.rectangle(left, top, width, height, 0x101820, 0.98));
bg.setOrigin(0); bg.setOrigin(0);
bg.setStrokeStyle(2, options.accent, 0.78); bg.setStrokeStyle(2, options.accent, 0.78);
const iconFrame = this.trackSideObject(this.add.rectangle(left + 40, top + 42, 72, 72, 0x0a0f14, 0.94)); const iconFrame = this.trackSideObject(this.add.rectangle(left + 46, top + 46, 84, 84, 0x0a0f14, 0.94));
iconFrame.setStrokeStyle(1, options.accent, 0.76); iconFrame.setStrokeStyle(1, options.accent, 0.76);
this.trackSideIcon(left + 40, top + 42, options.icon, 64); this.trackSideIcon(left + 46, top + 46, options.icon, 70);
this.trackSideObject(this.add.text(left + 86, top + 11, options.title, { this.trackSideObject(this.add.text(left + 98, top + 12, options.title, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '20px', fontSize: '20px',
color: '#f4dfad', color: '#f4dfad',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 96 fixedWidth: width - 108
})); }));
this.trackSideObject(this.add.text(left + 86, top + 39, options.subtitle, { this.trackSideObject(this.add.text(left + 98, top + 40, options.subtitle, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: '#9fb0bf', color: '#9fb0bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 96 fixedWidth: width - 108
})); }));
this.trackSideObject(this.add.text(left + 86, top + 57, `사용자 ${options.unit.name}`, { this.trackSideObject(this.add.text(left + 98, top + 61, `사용자 ${options.unit.name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: '#c8d2dd', color: '#c8d2dd',
fixedWidth: width - 96 fixedWidth: width - 108
})); }));
const metricTop = top + 92; const metricTop = top + 104;
const metricGap = 6; const metricGap = 6;
const metricWidth = (width - metricGap * 2) / 3; const metricWidth = (width - metricGap * 2) / 3;
options.metrics.slice(0, 3).forEach((metric, index) => { options.metrics.slice(0, 3).forEach((metric, index) => {
@@ -5726,28 +5744,28 @@ export class BattleScene extends Phaser.Scene {
this.renderTargetingGuideMetric(left + index * (metricWidth + metricGap), metricTop, metricWidth, metric); this.renderTargetingGuideMetric(left + index * (metricWidth + metricGap), metricTop, metricWidth, metric);
}); });
this.trackSideIcon(left + 17, top + 165, 'success', 26); this.trackSideIcon(left + 17, top + 182, 'success', 28);
this.trackSideObject(this.add.text(left + 36, top + 154, '대상 후보', { this.trackSideObject(this.add.text(left + 38, top + 170, '대상 후보', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '17px', fontSize: '17px',
color: '#f2e3bf', color: '#f2e3bf',
fontStyle: '700' fontStyle: '700'
})); }));
const rowTop = top + 188; const rowTop = top + 200;
options.candidates.forEach((candidate, index) => { options.candidates.forEach((candidate, index) => {
this.renderTargetingCandidateRow(left, rowTop + index * 58, width, candidate); this.renderTargetingCandidateRow(left, rowTop + index * 84, width, candidate);
}); });
const footerTop = top + 368; const footerTop = top + 454;
const footer = this.trackSideObject(this.add.rectangle(left, footerTop, width, 34, 0x0b1118, 0.95)); const footer = this.trackSideObject(this.add.rectangle(left, footerTop, width, 28, 0x0b1118, 0.95));
footer.setOrigin(0); footer.setOrigin(0);
const footerTone = options.notice?.tone ?? options.accent; const footerTone = options.notice?.tone ?? options.accent;
footer.setStrokeStyle(1, footerTone, options.notice ? 0.72 : 0.52); footer.setStrokeStyle(1, footerTone, options.notice ? 0.72 : 0.52);
this.trackSideIcon(left + 18, footerTop + 17, options.notice?.icon ?? 'success', 26); this.trackSideIcon(left + 18, footerTop + 14, options.notice?.icon ?? 'success', 24);
const overflowText = options.overflowCount > 0 ? ` · 외 ${options.overflowCount}` : ''; const overflowText = options.overflowCount > 0 ? ` · 외 ${options.overflowCount}` : '';
const footerText = options.notice?.text ?? `${options.footer}${overflowText}`; const footerText = options.notice?.text ?? `${options.footer}${overflowText}`;
this.trackSideObject(this.add.text(left + 38, footerTop + 8, footerText, { this.trackSideObject(this.add.text(left + 38, footerTop + 6, footerText, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '12px', fontSize: '12px',
color: options.notice ? '#ffcf9a' : '#d4dce6', color: options.notice ? '#ffcf9a' : '#d4dce6',
@@ -5757,62 +5775,71 @@ export class BattleScene extends Phaser.Scene {
} }
private renderTargetingGuideMetric(x: number, y: number, width: number, metric: TargetingGuideMetric) { private renderTargetingGuideMetric(x: number, y: number, width: number, metric: TargetingGuideMetric) {
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 54, 0x16212d, 0.96)); const bg = this.trackSideObject(this.add.rectangle(x, y, width, 62, 0x16212d, 0.97));
bg.setOrigin(0); bg.setOrigin(0);
bg.setStrokeStyle(1, metric.tone, 0.62); bg.setStrokeStyle(1, metric.tone, 0.62);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 20, y + 27, 38, 38, 0x0a0f14, 0.88)); const iconFrame = this.trackSideObject(this.add.rectangle(x + 23, y + 31, 44, 44, 0x0a0f14, 0.9));
iconFrame.setStrokeStyle(1, metric.tone, 0.54); iconFrame.setStrokeStyle(1, metric.tone, 0.54);
this.trackSideIcon(x + 20, y + 27, metric.icon, 35); this.trackSideIcon(x + 23, y + 31, metric.icon, 40);
this.trackSideObject(this.add.text(x + 42, y + 7, metric.label, { this.trackSideObject(this.add.text(x + 49, y + 9, metric.label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '10px',
color: '#9fb0bf', color: '#9fb0bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 50 fixedWidth: width - 56
})); }));
this.trackSideObject(this.add.text(x + 42, y + 26, this.truncateUiText(metric.value, 8), { this.trackSideObject(this.add.text(x + 49, y + 31, this.truncateUiText(metric.value, 8), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: metric.value.length > 4 ? '13px' : '16px', fontSize: metric.value.length > 4 ? '13px' : '16px',
color: '#f2e3bf', color: '#f2e3bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 50 fixedWidth: width - 56
})); }));
} }
private renderTargetingCandidateRow(x: number, y: number, width: number, candidate: TargetingGuideCandidate) { private renderTargetingCandidateRow(x: number, y: number, width: number, candidate: TargetingGuideCandidate) {
const row = this.trackSideObject(this.add.rectangle(x, y, width, 52, 0x121a22, 0.97)); const rowHeight = 78;
const row = this.trackSideObject(this.add.rectangle(x, y, width, rowHeight, 0x121a22, 0.98));
row.setOrigin(0); row.setOrigin(0);
row.setStrokeStyle(1, candidate.tone, 0.58); row.setStrokeStyle(1, candidate.tone, 0.64);
const iconFrame = this.trackSideObject(this.add.rectangle(x + 23, y + 26, 40, 40, 0x0a0f14, 0.9)); const iconFrame = this.trackSideObject(this.add.rectangle(x + 30, y + 39, 56, 56, 0x0a0f14, 0.92));
iconFrame.setStrokeStyle(1, candidate.tone, 0.52); iconFrame.setStrokeStyle(1, candidate.tone, 0.58);
this.trackSideIcon(x + 23, y + 26, candidate.icon, 36); this.trackSideIcon(x + 30, y + 39, candidate.icon, 50);
this.trackSideObject(this.add.text(x + 50, y + 6, candidate.name, { this.trackSideObject(this.add.text(x + 64, y + 7, candidate.name, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '15px', fontSize: '15px',
color: '#f2e3bf', color: '#f2e3bf',
fontStyle: '700', fontStyle: '700',
fixedWidth: width - 158 fixedWidth: width - 74
})); }));
this.trackSideObject(this.add.text(x + 50, y + 27, candidate.meta, { this.trackSideObject(this.add.text(x + 64, y + 29, candidate.meta, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '10px', fontSize: '10px',
color: '#9fb0bf', color: '#9fb0bf',
fixedWidth: width - 158 fixedWidth: width - 74
})); }));
const primary = this.trackSideObject(this.add.text(x + width - 10, y + 7, candidate.primary, { const metrics = candidate.metrics ?? [
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', { icon: candidate.icon, label: '결과', value: candidate.primary, tone: candidate.tone },
fontSize: '15px', { icon: 'success', label: '판정', value: candidate.secondary, tone: 0x647485 }
color: '#f4dfad', ];
fontStyle: '700' const chipGap = 5;
})); const chipLeft = x + 64;
primary.setOrigin(1, 0); const chipTop = y + 49;
const secondary = this.trackSideObject(this.add.text(x + width - 10, y + 29, candidate.secondary, { const chipWidth = (width - 74 - chipGap * 2) / 3;
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', metrics.slice(0, 3).forEach((metric, index) => {
fontSize: '10px', const chipX = chipLeft + index * (chipWidth + chipGap);
color: '#c8d2dd', const chip = this.trackSideObject(this.add.rectangle(chipX, chipTop, chipWidth, 22, 0x0b1118, 0.94));
fontStyle: '700' chip.setOrigin(0);
})); chip.setStrokeStyle(1, metric.tone, 0.54);
secondary.setOrigin(1, 0); this.trackSideIcon(chipX + 12, chipTop + 11, metric.icon, 19);
this.trackSideObject(this.add.text(chipX + 25, chipTop + 5, this.truncateUiText(metric.value, 7), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '11px',
color: '#f2e3bf',
fontStyle: '700',
fixedWidth: chipWidth - 30
}));
});
} }
private showTargetingRangeTiles(unit: UnitData, range: number, fillColor: number, strokeColor: number) { private showTargetingRangeTiles(unit: UnitData, range: number, fillColor: number, strokeColor: number) {