diff --git a/docs/battle-preview-ui-rework-local-combat-preview.png b/docs/battle-preview-ui-rework-local-combat-preview.png new file mode 100644 index 0000000..a3e33cc Binary files /dev/null and b/docs/battle-preview-ui-rework-local-combat-preview.png differ diff --git a/docs/battle-preview-ui-rework-local-strategy-preview.png b/docs/battle-preview-ui-rework-local-strategy-preview.png new file mode 100644 index 0000000..a6391c4 Binary files /dev/null and b/docs/battle-preview-ui-rework-local-strategy-preview.png differ diff --git a/docs/battle-preview-ui-rework-prod-combat-preview.png b/docs/battle-preview-ui-rework-prod-combat-preview.png new file mode 100644 index 0000000..3736633 Binary files /dev/null and b/docs/battle-preview-ui-rework-prod-combat-preview.png differ diff --git a/docs/battle-preview-ui-rework-report.md b/docs/battle-preview-ui-rework-report.md new file mode 100644 index 0000000..8ce1c10 --- /dev/null +++ b/docs/battle-preview-ui-rework-report.md @@ -0,0 +1,24 @@ +# 전투 예측 패널 리워크 검증 기록 + +## 작업 내용 + +- 전투 예측 상단의 공격자 -> 방어자 정보와 사용 무기/책략 아이콘을 더 크게 배치했다. +- 중앙 핵심 카드 3개를 예상 피해, 명중/성공률, 반격 여부 중심으로 키웠다. +- 공격/책략/도구 계열 예측이 같은 카드 구조를 쓰도록 전투 예측과 지원 예측 카드의 간격과 높이를 맞췄다. +- 장비 칩의 아이콘을 키우고, 장비 보정 표기를 짧게 압축했다. +- 하단 보정 카드는 항목 수에 따라 폭을 자동 조정해 텍스트가 불필요하게 잘리지 않도록 했다. + +## 로컬 검증 + +- 빌드: `pnpm build` 성공 +- 브라우저: PC 1280x720 인앱 브라우저, `debugBattleSetup=attack-preview` +- 공격 예측: `docs/battle-preview-ui-rework-local-combat-preview.png` +- 책략 예측: `docs/battle-preview-ui-rework-local-strategy-preview.png` +- 콘솔 에러: 없음 + +## 배포 검증 + +- 배포 URL: `https://comtropy.synology.me/heros_web/?v=combat-preview-ui-20260703b` +- 공격 예측: `docs/battle-preview-ui-rework-prod-combat-preview.png` +- 책략 예측: 로컬에서 구조 동일성 확인 +- 콘솔 에러: 없음 diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 86120b6..bdb6325 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -6301,12 +6301,12 @@ export class BattleScene extends Phaser.Scene { private equipmentPreviewBonusText(preview: CombatPreview) { const parts = [ - preview.equipmentDamageBonus > 0 ? `피해 +${preview.equipmentDamageBonus}%` : '', - preview.equipmentDamageReduction > 0 ? `피해 -${preview.equipmentDamageReduction}%` : '', - preview.equipmentHitBonus > 0 ? `명중 +${preview.equipmentHitBonus}` : '', - preview.equipmentCriticalBonus > 0 ? `치명 +${preview.equipmentCriticalBonus}` : '' + preview.equipmentDamageBonus > 0 ? `피해+${preview.equipmentDamageBonus}%` : '', + preview.equipmentDamageReduction > 0 ? `감소-${preview.equipmentDamageReduction}%` : '', + preview.equipmentHitBonus > 0 ? `명중+${preview.equipmentHitBonus}` : '', + preview.equipmentCriticalBonus > 0 ? `치명+${preview.equipmentCriticalBonus}` : '' ].filter(Boolean); - return parts.join(' / ') || '보정 없음'; + return parts.join(' ') || '보정 없음'; } private battleBuffShortText(buff: BattleBuffState) { @@ -6466,7 +6466,7 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; - const height = locked ? 438 : 402; + const height = locked ? 464 : 430; const y = this.sideContentTop(); const accent = this.previewAccentColor(preview); const distance = this.tileDistance(preview.attacker, preview.defender); @@ -6484,34 +6484,34 @@ export class BattleScene extends Phaser.Scene { bg.setOrigin(0); bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78); - const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95)); + const actionFrame = this.trackSideObject(this.add.rectangle(left + 46, y + 46, 84, 84, 0x0a0f14, 0.96)); actionFrame.setStrokeStyle(1, accent, 0.82); - this.trackSideIcon(left + 42, y + 42, this.actionIcon(preview), 66); + this.trackSideIcon(left + 46, y + 46, this.actionIcon(preview), 70); - this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.attacker.name} → ${preview.defender.name}`, { + this.trackSideObject(this.add.text(left + 98, y + 10, `${preview.attacker.name} → ${preview.defender.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f4dfad', fontStyle: '700', - fixedWidth: width - 98 + fixedWidth: width - 108 })); - this.trackSideObject(this.add.text(left + 88, y + 37, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)}`, { + this.trackSideObject(this.add.text(left + 98, y + 39, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 98 + fixedWidth: width - 108 })); - this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${range} · ${locked ? '선택됨' : '대상 후보'}`, { + this.trackSideObject(this.add.text(left + 98, y + 61, `거리 ${distance}/${range} · ${locked ? '선택됨' : '대상 후보'}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: locked ? '#f2e3bf' : '#9fb0bf', - fixedWidth: width - 98 + fixedWidth: width - 108 })); const metricGap = 6; const metricWidth = (width - 20 - metricGap * 2) / 3; - const metricTop = y + 90; + const metricTop = y + 96; this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), isStrategy ? '책략 피해' : '예상 피해', `${preview.damage}`, 0xb64a45); this.renderPreviewDecisionCard( left + 10 + metricWidth + metricGap, @@ -6534,13 +6534,13 @@ 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'), defenseLabel, defenderArmor.name); + this.renderPreviewEquipmentChip(left + 10, y + 214, chipWidth, sourceIcon, sourceLabel, sourceName); + this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 214, 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); + this.renderPreviewForecastRow(preview, left + 10, y + 272, width - 20, accent); + this.renderPreviewModifierCards(left + 10, y + 312, width - 20, this.previewModifierSummaries(preview), accent); - const badgeY = y + 346; + const badgeY = y + 382; const maxBadgeRight = left + width - 10; let badgeX = left + 10; if (isStrategy) { @@ -6556,7 +6556,7 @@ export class BattleScene extends Phaser.Scene { this.renderPreviewBadge(detailBadgeX, badgeY, 'mastery', '장비 보정', maxBadgeRight); } if (locked) { - this.renderPreviewConfirmBar(left + 10, y + 402, width - 20, accent, `${preview.defender.name} 선택됨`); + this.renderPreviewConfirmBar(left + 10, y + 426, width - 20, accent, `${preview.defender.name} 선택됨`); } } @@ -6564,7 +6564,7 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; - const height = locked ? 438 : 402; + const height = locked ? 464 : 430; const y = this.sideContentTop(); const accent = this.usableAccentColor(preview.usable); const isHeal = preview.usable.effect === 'heal'; @@ -6574,34 +6574,34 @@ export class BattleScene extends Phaser.Scene { bg.setOrigin(0); bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78); - const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95)); + const actionFrame = this.trackSideObject(this.add.rectangle(left + 46, y + 46, 84, 84, 0x0a0f14, 0.96)); actionFrame.setStrokeStyle(1, accent, 0.82); - this.trackSideIcon(left + 42, y + 42, this.usableIcon(preview.usable), 66); + this.trackSideIcon(left + 46, y + 46, this.usableIcon(preview.usable), 70); - this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.user.name} → ${preview.target.name}`, { + this.trackSideObject(this.add.text(left + 98, y + 10, `${preview.user.name} → ${preview.target.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f4dfad', fontStyle: '700', - fixedWidth: width - 98 + fixedWidth: width - 108 })); - this.trackSideObject(this.add.text(left + 88, y + 37, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)}`, { + this.trackSideObject(this.add.text(left + 98, y + 39, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 98 + fixedWidth: width - 108 })); - this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, { + this.trackSideObject(this.add.text(left + 98, y + 61, `거리 ${distance}/${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: locked ? '#f2e3bf' : '#9fb0bf', - fixedWidth: width - 98 + fixedWidth: width - 108 })); const metricGap = 6; const metricWidth = (width - 20 - metricGap * 2) / 3; - const metricTop = y + 90; + const metricTop = y + 96; if (isHeal) { this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '회복', `+${preview.healAmount}`, 0x59d18c); this.renderPreviewDecisionCard(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent); @@ -6616,7 +6616,7 @@ export class BattleScene extends Phaser.Scene { const chipWidth = (width - 20 - chipGap) / 2; this.renderPreviewEquipmentChip( left + 10, - y + 184, + y + 214, chipWidth, this.usableIcon(preview.usable), this.usableChannelLabel(preview.usable), @@ -6624,17 +6624,17 @@ export class BattleScene extends Phaser.Scene { ); this.renderPreviewEquipmentChip( left + 10 + chipWidth + chipGap, - y + 184, + y + 214, chipWidth, this.usableTargetIcon(preview.usable), this.usableTargetLabel(preview.usable), preview.target.name ); - this.renderSupportForecastRow(preview, left + 10, y + 238, width - 20, accent); - this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.supportPreviewModifierSummaries(preview), accent); + this.renderSupportForecastRow(preview, left + 10, y + 272, width - 20, accent); + this.renderPreviewModifierCards(left + 10, y + 312, width - 20, this.supportPreviewModifierSummaries(preview), accent); - const badgeY = y + 346; + const badgeY = y + 382; const maxBadgeRight = left + width - 10; let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableIcon(preview.usable), this.usableEffectLabel(preview.usable), maxBadgeRight); @@ -6644,7 +6644,7 @@ export class BattleScene extends Phaser.Scene { detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'critical', `치명 +${preview.criticalBonus}`, maxBadgeRight); } if (locked) { - this.renderPreviewConfirmBar(left + 10, y + 402, width - 20, accent, `${preview.target.name} 선택됨`); + this.renderPreviewConfirmBar(left + 10, y + 426, width - 20, accent, `${preview.target.name} 선택됨`); } } @@ -6657,17 +6657,17 @@ export class BattleScene extends Phaser.Scene { value: string, tone: number ) { - const bg = this.trackSideObject(this.add.rectangle(x, y, width, 84, 0x16212d, 0.97)); + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 104, 0x16212d, 0.98)); bg.setOrigin(0); - bg.setStrokeStyle(1, tone, 0.72); + bg.setStrokeStyle(1, tone, 0.82); - const iconFrame = this.trackSideObject(this.add.rectangle(x + width / 2, y + 25, 44, 44, 0x0a0f14, 0.92)); - iconFrame.setStrokeStyle(1, tone, 0.58); - this.trackSideIcon(x + width / 2, y + 25, icon, 40); + const iconFrame = this.trackSideObject(this.add.rectangle(x + width / 2, y + 34, 60, 60, 0x0a0f14, 0.94)); + iconFrame.setStrokeStyle(1, tone, 0.66); + this.trackSideIcon(x + width / 2, y + 34, icon, 54); - const labelText = this.trackSideObject(this.add.text(x + width / 2, y + 47, label, { + const labelText = this.trackSideObject(this.add.text(x + width / 2, y + 66, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '11px', + fontSize: '12px', color: '#9fb0bf', fontStyle: '700', fixedWidth: width - 8, @@ -6676,9 +6676,9 @@ export class BattleScene extends Phaser.Scene { labelText.setOrigin(0.5, 0); const valueHasKorean = /[가-힣]/.test(value); - const valueText = this.trackSideObject(this.add.text(x + width / 2, y + 62, value, { + const valueText = this.trackSideObject(this.add.text(x + width / 2, y + 82, value, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: valueHasKorean || value.length > 4 ? '17px' : '22px', + fontSize: valueHasKorean || value.length > 4 ? '18px' : '26px', color: '#f2e3bf', fontStyle: '700', fixedWidth: width - 8, @@ -6724,7 +6724,7 @@ export class BattleScene extends Phaser.Scene { } const gap = 6; - const cardWidth = (width - gap * 2) / 3; + const cardWidth = (width - gap * (visible.length - 1)) / visible.length; visible.forEach((summary, index) => { const cardX = x + index * (cardWidth + gap); const card = this.trackSideObject(this.add.rectangle(cardX, y, cardWidth, 58, 0x0b1118, 0.96)); @@ -6821,24 +6821,24 @@ export class BattleScene extends Phaser.Scene { roleLabel: string, label: string ) { - const chip = this.trackSideObject(this.add.rectangle(x, y, width, 52, 0x0b1118, 0.97)); + const chip = this.trackSideObject(this.add.rectangle(x, y, width, 52, 0x0b1118, 0.98)); chip.setOrigin(0); chip.setStrokeStyle(1, 0x647485, 0.62); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 27, y + 26, 46, 46, 0x16212d, 0.93)); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 29, y + 26, 50, 50, 0x16212d, 0.94)); iconFrame.setStrokeStyle(1, 0x53606c, 0.55); - this.trackSideIcon(x + 27, y + 26, icon, 42); - this.trackSideObject(this.add.text(x + 58, y + 6, roleLabel, { + this.trackSideIcon(x + 29, y + 26, icon, 46); + this.trackSideObject(this.add.text(x + 62, y + 6, roleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '10px', color: '#9fb0bf', fontStyle: '700' })); - this.trackSideObject(this.add.text(x + 58, y + 24, label, { + this.trackSideObject(this.add.text(x + 62, y + 24, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 66 + fixedWidth: width - 70 })); }