From 08b4a624861ed05e2ce905040a815445419119bd Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 3 Jul 2026 06:28:24 +0900 Subject: [PATCH] Improve strategy menu readability --- src/game/scenes/BattleScene.ts | 190 +++++++++++++++++++++------------ 1 file changed, 121 insertions(+), 69 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 776ec09..8f7097a 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1374,6 +1374,7 @@ type UsableMenuForecast = { value: string; detail: string; secondary: string; + chips: UsableMenuChip[]; }; type LockedTargetPreview = { @@ -1419,6 +1420,13 @@ type PreviewModifierSummary = { tone: number; }; +type UsableMenuChip = { + icon: BattleUiIconKey; + label: string; + value: string; + tone: number; +}; + type TargetingGuideMetric = { icon: BattleUiIconKey; label: string; @@ -4791,23 +4799,40 @@ export class BattleScene extends Phaser.Scene { return this.supportTargetUnavailableReason(user, usable); } + private rateTone(rate: number) { + if (rate >= 80) { + return 0x59d18c; + } + if (rate >= 60) { + return palette.gold; + } + return 0xb64a45; + } + 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; const status = !hasStock ? '수량 없음' : targetCount > 0 ? `대상 ${targetCount}` : '대상 없음'; + const accent = this.usableAccentColor(usable); if (usable.effect === 'damage') { const target = this.damageableTargets(user, usable.command, usable)[0]; if (target) { const preview = this.combatPreview(user, target, usable.command, usable); + const successLabel = usable.command === 'strategy' ? '성공률' : '명중률'; return { available, status, targetCount, valueLabel: '피해', value: `${preview.damage}`, - detail: `${target.name}에게 ${preview.damage} 피해 예상`, - secondary: `${preview.hitRate}% ${usable.command === 'strategy' ? '성공' : '명중'} · 치명 ${preview.criticalRate}%` + detail: `${target.name} · 피해 ${preview.damage} · ${successLabel} ${preview.hitRate}%`, + secondary: `치명 ${preview.criticalRate}% · 지형 ${preview.terrainLabel} · 사거리 ${usable.range}칸`, + chips: [ + { icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent }, + { 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 } + ] }; } return { @@ -4817,7 +4842,12 @@ export class BattleScene extends Phaser.Scene { valueLabel: '위력', value: `${usable.power}`, detail: this.usableUnavailableReason(user, usable), - secondary: usable.description + secondary: usable.description, + chips: [ + { icon: this.usablePowerIcon(usable), label: '효과', value: this.usableEffectLabel(usable), tone: accent }, + { icon: 'success', label: '성공률', value: '-', tone: 0x647485 }, + { icon: this.usableTargetIcon(usable), label: '대상', value: '없음', tone: 0xb64a45 } + ] }; } if (usable.effect === 'heal') { @@ -4831,7 +4861,12 @@ export class BattleScene extends Phaser.Scene { valueLabel: '회복', value: `+${amount}`, detail: target ? `${target.name} HP ${target.hp} → ${projectedHp}` : this.usableUnavailableReason(user, usable), - secondary: target ? `기본 ${usable.power} + 능력/장비 보정` : usable.description + secondary: target ? `기본 ${usable.power} + 능력/장비 보정` : usable.description, + chips: [ + { icon: this.usablePowerIcon(usable), label: '회복량', value: `+${amount}`, tone: 0x59d18c }, + { icon: 'success', label: '적용', value: available ? '100%' : '-', tone: available ? 0x59d18c : 0x647485 }, + { icon: this.usableTargetIcon(usable), label: '대상', value: available ? `${targetCount}명` : '없음', tone: accent } + ] }; } return { @@ -4841,7 +4876,12 @@ export class BattleScene extends Phaser.Scene { valueLabel: '강화', value: `${usable.duration ?? 1}턴`, detail: `공격 +${usable.attackBonus ?? 0} · 명중 +${usable.hitBonus ?? 0} · 치명 +${usable.criticalBonus ?? 0}`, - secondary: available ? `${this.usableTargetLabel(usable)} ${targetCount}명에게 ${usable.duration ?? 1}턴 적용` : this.usableUnavailableReason(user, usable) + secondary: available ? `${this.usableTargetLabel(usable)} ${targetCount}명에게 ${usable.duration ?? 1}턴 적용` : this.usableUnavailableReason(user, usable), + chips: [ + { icon: 'focus', label: '강화', value: this.usableBuffText(usable), tone: palette.gold }, + { icon: 'leadership', label: '지속', value: `${usable.duration ?? 1}턴`, tone: 0x83d6ff }, + { icon: this.usableTargetIcon(usable), label: '대상', value: available ? `${targetCount}명` : '없음', tone: accent } + ] }; } @@ -4866,33 +4906,33 @@ export class BattleScene extends Phaser.Scene { tone: number, available: boolean ) { - const chip = this.add.rectangle(x, y, width, 34, 0x0b1118, available ? 0.94 : 0.86); + const chip = this.add.rectangle(x, y, width, 40, 0x0b1118, available ? 0.95 : 0.87); chip.setOrigin(0); chip.setDepth(16); - chip.setStrokeStyle(1, tone, available ? 0.58 : 0.36); + chip.setStrokeStyle(1, tone, available ? 0.64 : 0.38); this.commandMenuObjects.push(chip); - const iconImage = this.createBattleUiIcon(x + 17, y + 17, icon, 25); + const iconImage = this.createBattleUiIcon(x + 20, y + 20, icon, 31); iconImage.setAlpha(available ? 0.98 : 0.72); iconImage.setDepth(17); this.commandMenuObjects.push(iconImage); - const labelText = this.add.text(x + 34, y + 4, label, { + const labelText = this.add.text(x + 41, y + 5, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '9px', + fontSize: '10px', color: '#9fb0bf', fontStyle: '700', - fixedWidth: width - 40 + fixedWidth: width - 48 }); labelText.setDepth(17); this.commandMenuObjects.push(labelText); - const valueText = this.add.text(x + 34, y + 18, this.truncateUiText(value, 8), { + const valueText = this.add.text(x + 41, y + 21, this.truncateUiText(value, 10), { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '13px', color: available ? '#f2e3bf' : '#c8ced6', fontStyle: '700', - fixedWidth: width - 40 + fixedWidth: width - 48 }); valueText.setDepth(17); this.commandMenuObjects.push(valueText); @@ -4907,23 +4947,23 @@ export class BattleScene extends Phaser.Scene { forecast: UsableMenuForecast, tone: number ) { - const valueBox = this.add.rectangle(x, y, width, height, 0x0b1118, forecast.available ? 0.96 : 0.9); + const valueBox = this.add.rectangle(x, y, width, height, 0x0b1118, forecast.available ? 0.97 : 0.9); valueBox.setOrigin(0); valueBox.setDepth(16); - valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.76 : 0.46); + valueBox.setStrokeStyle(1, forecast.available ? tone : 0x6c7480, forecast.available ? 0.82 : 0.46); this.commandMenuObjects.push(valueBox); - const valueIcon = this.createBattleUiIcon(x + 24, y + height / 2, this.usablePowerIcon(usable), 36); + const valueIcon = this.createBattleUiIcon(x + 30, y + height / 2, this.usablePowerIcon(usable), 46); 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, { + const valueLabel = this.add.text(x + 62, y + 12, forecast.valueLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '12px', color: '#9fb0bf', fontStyle: '700', - fixedWidth: width - 58 + fixedWidth: width - 70 }); valueLabel.setDepth(17); this.commandMenuObjects.push(valueLabel); @@ -4931,7 +4971,7 @@ export class BattleScene extends Phaser.Scene { 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', + fontSize: valueHasKorean || forecast.value.length > 4 ? '19px' : '27px', color: forecast.available ? '#f2e3bf' : '#c8ced6', fontStyle: '700' }); @@ -4961,9 +5001,9 @@ export class BattleScene extends Phaser.Scene { const forecasts = usables.map((usable) => ({ usable, forecast: this.usableMenuForecast(unit, usable) })); const readyCount = forecasts.filter(({ forecast }) => forecast.available).length; - const menuWidth = 486; + const menuWidth = 520; const titleHeight = 58; - const rowHeight = 130; + const rowHeight = 148; const padding = 10; const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight; const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); @@ -5016,8 +5056,8 @@ export class BattleScene extends Phaser.Scene { const rowLeft = left + padding; const rowWidth = menuWidth - padding * 2; const rowRight = rowLeft + rowWidth; - const contentLeft = rowLeft + 90; - const sideBoxWidth = 112; + const contentLeft = rowLeft + 102; + const sideBoxWidth = 124; const sideBoxLeft = rowRight - sideBoxWidth - 10; const contentWidth = sideBoxLeft - contentLeft - 12; const baseFill = forecast.available ? 0x1a2630 : 0x151c23; @@ -5028,12 +5068,12 @@ export class BattleScene extends Phaser.Scene { bg.setInteractive({ useHandCursor: true }); this.commandMenuObjects.push(bg); - const iconFrame = this.add.rectangle(rowLeft + 43, rowTop + 61, 76, 76, 0x0a0f14, forecast.available ? 0.92 : 0.86); + const iconFrame = this.add.rectangle(rowLeft + 48, rowTop + 70, 86, 86, 0x0a0f14, forecast.available ? 0.94 : 0.87); iconFrame.setStrokeStyle(1, accent, forecast.available ? 0.72 : 0.42); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(rowLeft + 43, rowTop + 61, this.usableIcon(usable), 68); + const icon = this.createBattleUiIcon(rowLeft + 48, rowTop + 70, this.usableIcon(usable), 78); icon.setAlpha(forecast.available ? 1 : 0.72); icon.setDepth(17); this.commandMenuObjects.push(icon); @@ -5078,9 +5118,9 @@ export class BattleScene extends Phaser.Scene { metaText.setDepth(16); this.commandMenuObjects.push(metaText); - const summary = this.add.text(contentLeft, rowTop + 54, this.truncateUiText(forecast.detail, 32), { + const summary = this.add.text(contentLeft, rowTop + 55, this.truncateUiText(forecast.detail, 34), { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '13px', + fontSize: '14px', color: forecast.available ? '#d4dce6' : '#ffcf9a', fontStyle: '700', fixedWidth: contentWidth @@ -5088,40 +5128,31 @@ export class BattleScene extends Phaser.Scene { summary.setDepth(16); this.commandMenuObjects.push(summary); - const secondary = this.add.text(contentLeft, rowTop + 72, this.truncateUiText(forecast.secondary, 36), { + const secondary = this.add.text(contentLeft, rowTop + 76, this.truncateUiText(forecast.secondary, 38), { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '12px', color: forecast.available ? '#aeb7c2' : '#d8b199', fixedWidth: contentWidth }); secondary.setDepth(16); this.commandMenuObjects.push(secondary); - this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 45, sideBoxWidth, 66, usable, forecast, accent); + this.renderUsableMenuValueCard(sideBoxLeft, rowTop + 52, sideBoxWidth, 76, 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 - ); + forecast.chips.slice(0, 3).forEach((chip, chipIndex) => { + this.renderUsableMenuInfoChip( + contentLeft + chipIndex * (chipWidth + chipGap), + rowTop + 98, + chipWidth, + chip.icon, + chip.label, + chip.value, + chip.tone, + forecast.available + ); + }); const choose = (choicePointer: Phaser.Input.Pointer) => { if (choicePointer.rightButtonDown()) { @@ -6292,10 +6323,26 @@ export class BattleScene extends Phaser.Scene { const buff = this.battleBuffs.get(preview.attacker.id); const equipmentText = this.equipmentPreviewBonusText(preview); const terrainParts = [ - preview.terrainDefenseBonus > 0 ? `방+${preview.terrainDefenseBonus}` : '', - preview.terrainHitPenalty > 0 ? `명-${preview.terrainHitPenalty}` : '' + preview.terrainDefenseBonus > 0 ? `${preview.action === 'strategy' ? '저항' : '방'}+${preview.terrainDefenseBonus}` : '', + preview.terrainHitPenalty > 0 ? `${preview.action === 'strategy' ? '성공' : '명'}-${preview.terrainHitPenalty}` : '' ].filter(Boolean); + if (preview.action === 'strategy') { + summaries.push({ + icon: 'intelligence', + label: '지력', + value: `${preview.attacker.stats.intelligence} vs ${preview.defender.stats.intelligence}`, + tone: 0x83d6ff + }); + if (terrainParts.length > 0) { + summaries.push({ + icon: 'terrain', + label: preview.terrainLabel, + value: terrainParts.join(' / '), + tone: 0x59d18c + }); + } + } if (buff) { summaries.push({ icon: 'focus', @@ -6320,7 +6367,7 @@ export class BattleScene extends Phaser.Scene { tone: 0xd8b15f }); } - if (terrainParts.length > 0) { + if (preview.action !== 'strategy' && terrainParts.length > 0) { summaries.push({ icon: 'terrain', label: preview.terrainLabel, @@ -6426,9 +6473,11 @@ export class BattleScene extends Phaser.Scene { const range = this.actionRange(preview.attacker, preview.action, preview.usable); const attackerWeapon = getItem(preview.attacker.equipment.weapon.itemId); const defenderArmor = getItem(preview.defender.equipment.armor.itemId); + const isStrategy = preview.action === 'strategy'; const sourceIcon = preview.usable ? this.usableIcon(preview.usable) : this.itemIcon(attackerWeapon, 'weapon'); const sourceName = preview.usable?.name ?? attackerWeapon.name; - const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : preview.action === 'strategy' ? '책략 장비' : '공격 장비'; + const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : isStrategy ? '책략 장비' : '공격 장비'; + const defenseLabel = isStrategy ? '대상 방어' : '대상 방어구'; const counterTone = preview.counterAvailable ? 0xb64a45 : 0x59d18c; const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1)); @@ -6463,13 +6512,13 @@ export class BattleScene extends Phaser.Scene { const metricGap = 6; const metricWidth = (width - 20 - metricGap * 2) / 3; const metricTop = y + 90; - this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), '예상 피해', `${preview.damage}`, 0xb64a45); + this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), isStrategy ? '책략 피해' : '예상 피해', `${preview.damage}`, 0xb64a45); this.renderPreviewDecisionCard( left + 10 + metricWidth + metricGap, metricTop, metricWidth, - preview.action === 'strategy' ? 'success' : 'hit', - this.previewSuccessLabel(preview), + isStrategy ? 'success' : 'hit', + isStrategy ? '성공률' : this.previewSuccessLabel(preview), `${preview.hitRate}%`, accent ); @@ -6486,7 +6535,7 @@ export class BattleScene extends Phaser.Scene { const chipGap = 8; const chipWidth = (width - 20 - chipGap) / 2; this.renderPreviewEquipmentChip(left + 10, y + 184, chipWidth, sourceIcon, sourceLabel, sourceName); - this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 184, chipWidth, this.itemIcon(defenderArmor, 'armor'), '대상 방어구', defenderArmor.name); + this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 184, chipWidth, this.itemIcon(defenderArmor, 'armor'), defenseLabel, defenderArmor.name); this.renderPreviewForecastRow(preview, left + 10, y + 238, width - 20, accent); this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.previewModifierSummaries(preview), accent); @@ -6494,6 +6543,9 @@ export class BattleScene extends Phaser.Scene { const badgeY = y + 346; const maxBadgeRight = left + width - 10; let badgeX = left + 10; + if (isStrategy) { + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'success', `성공 ${preview.hitRate}%`, maxBadgeRight); + } badgeX = this.renderPreviewBadge(badgeX, badgeY, 'critical', `치명 ${preview.criticalRate}%`, maxBadgeRight); badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight); let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight); @@ -6566,7 +6618,7 @@ export class BattleScene extends Phaser.Scene { left + 10, y + 184, chipWidth, - preview.usable.command === 'item' ? 'accessory' : 'strategy', + this.usableIcon(preview.usable), this.usableChannelLabel(preview.usable), preview.usable.name ); @@ -6769,24 +6821,24 @@ export class BattleScene extends Phaser.Scene { roleLabel: string, label: string ) { - const chip = this.trackSideObject(this.add.rectangle(x, y, width, 48, 0x0b1118, 0.96)); + const chip = this.trackSideObject(this.add.rectangle(x, y, width, 52, 0x0b1118, 0.97)); chip.setOrigin(0); - chip.setStrokeStyle(1, 0x647485, 0.58); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 24, y + 24, 42, 42, 0x16212d, 0.92)); + chip.setStrokeStyle(1, 0x647485, 0.62); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 27, y + 26, 46, 46, 0x16212d, 0.93)); iconFrame.setStrokeStyle(1, 0x53606c, 0.55); - this.trackSideIcon(x + 24, y + 24, icon, 38); - this.trackSideObject(this.add.text(x + 52, y + 5, roleLabel, { + this.trackSideIcon(x + 27, y + 26, icon, 42); + this.trackSideObject(this.add.text(x + 58, y + 6, roleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '10px', color: '#9fb0bf', fontStyle: '700' })); - this.trackSideObject(this.add.text(x + 52, y + 22, label, { + this.trackSideObject(this.add.text(x + 58, y + 24, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 60 + fixedWidth: width - 66 })); }