diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index eeaae99..19dc821 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -5337,12 +5337,13 @@ export class BattleScene extends Phaser.Scene { const candidates = targets.slice(0, 3).map((target): TargetingGuideCandidate => { const preview = this.combatPreview(unit, target, action, usable); const distance = this.tileDistance(unit, target); + const counterLabel = preview.counterAvailable ? '반격 주의' : '반격 없음'; return { icon: this.previewDamageIcon(preview), name: target.name, meta: `${getUnitClass(target.classKey).name} · 거리 ${distance}/${range}`, primary: `${preview.damage} 피해`, - secondary: `${preview.hitRate}% ${this.previewSuccessLabel(preview)} · 치명 ${preview.criticalRate}%`, + secondary: `${preview.hitRate}% ${this.previewSuccessLabel(preview)} · ${counterLabel}`, tone: this.previewAccentColor(preview) }; }); @@ -6178,85 +6179,86 @@ export class BattleScene extends Phaser.Scene { return '도구'; } - private renderCombatPreviewCard(preview: CombatPreview, locked = false) { + private renderCombatPreviewSummaryCard(preview: CombatPreview, locked = false) { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; const height = locked ? 438 : 402; const y = this.sideContentTop(); const accent = this.previewAccentColor(preview); - const defenderClass = getUnitClass(preview.defender.classKey); - const attackerEquipmentLabel = preview.action === 'strategy' ? '책략 장비' : preview.action === 'item' ? '사용 장비' : '공격 장비'; + const distance = this.tileDistance(preview.attacker, preview.defender); + 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 sourceIcon = preview.usable ? this.usableIcon(preview.usable) : this.itemIcon(attackerWeapon, 'weapon'); - const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : attackerEquipmentLabel; const sourceName = preview.usable?.name ?? attackerWeapon.name; + const sourceLabel = preview.usable ? this.usableChannelLabel(preview.usable) : preview.action === 'strategy' ? '책략 장비' : '공격 장비'; + const counterTone = preview.counterAvailable ? 0xb64a45 : 0x59d18c; const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1)); bg.setOrigin(0); - bg.setStrokeStyle(2, accent, 0.78); + bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78); - const actionFrame = this.trackSideObject(this.add.rectangle(left + 40, y + 40, 72, 72, 0x0a0f14, 0.94)); - actionFrame.setStrokeStyle(1, accent, 0.76); - this.trackSideIcon(left + 40, y + 40, this.actionIcon(preview), 64); - this.trackSideObject(this.add.text(left + 86, y + 8, `${preview.attacker.name} → ${preview.defender.name}`, { + const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95)); + actionFrame.setStrokeStyle(1, accent, 0.82); + this.trackSideIcon(left + 42, y + 42, this.actionIcon(preview), 66); + + this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.attacker.name} → ${preview.defender.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f4dfad', fontStyle: '700', - fixedWidth: width - 96 + fixedWidth: width - 98 })); - this.trackSideObject(this.add.text(left + 86, y + 36, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)} · ${defenderClass.name}`, { + this.trackSideObject(this.add.text(left + 88, y + 37, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', - color: '#9fb0bf', + color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 96 + fixedWidth: width - 98 })); - - this.trackSideObject(this.add.text(left + 86, y + 56, `거리 ${this.tileDistance(preview.attacker, preview.defender)} / 사거리 ${this.actionRange(preview.attacker, preview.action, preview.usable)} · ${locked ? '선택됨' : '대상 후보'}`, { + this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${range} · ${locked ? '선택됨' : '대상 후보'}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', - color: locked ? '#f2e3bf' : '#c8d2dd', - fixedWidth: width - 96 + color: locked ? '#f2e3bf' : '#9fb0bf', + fixedWidth: width - 98 })); - const chipGap = 8; - const chipWidth = (width - 20 - chipGap) / 2; - this.renderPreviewEquipmentChip(left + 10, y + 82, chipWidth, sourceIcon, sourceLabel, sourceName); - this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 82, chipWidth, this.itemIcon(defenderArmor, 'armor'), '대상 방어구', defenderArmor.name); - - const metricGap = 8; - const metricWidth = (width - 20 - metricGap) / 2; - this.renderPreviewOutcomeCard(left + 10, y + 142, metricWidth, this.previewDamageIcon(preview), '예상 피해', `${preview.damage}`, 0xb64a45); - this.renderPreviewOutcomeCard( + 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 + metricWidth + metricGap, - y + 142, + metricTop, metricWidth, preview.action === 'strategy' ? 'success' : 'hit', this.previewSuccessLabel(preview), `${preview.hitRate}%`, accent ); - this.renderPreviewOutcomeCard(left + 10, y + 206, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c); - this.renderPreviewOutcomeCard( - left + 10 + metricWidth + metricGap, - y + 206, + this.renderPreviewDecisionCard( + left + 10 + (metricWidth + metricGap) * 2, + metricTop, metricWidth, 'counter', '반격', preview.counterAvailable ? '주의' : '없음', - preview.counterAvailable ? 0xb64a45 : 0x59d18c + counterTone ); - this.renderPreviewForecastRow(preview, left + 10, y + 272, width - 20, accent); - this.renderPreviewModifierCards(left + 10, y + 308, width - 20, this.previewModifierSummaries(preview), accent); + 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); - const badgeY = y + 374; + this.renderPreviewForecastRow(preview, left + 10, y + 238, width - 20, accent); + this.renderPreviewModifierCards(left + 10, y + 278, width - 20, this.previewModifierSummaries(preview), accent); + + const badgeY = y + 346; const maxBadgeRight = left + width - 10; let badgeX = left + 10; - badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), 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); if (preview.bondDamageBonus > 0 && preview.bondLabel) { @@ -6270,66 +6272,63 @@ export class BattleScene extends Phaser.Scene { } } - private renderSupportPreviewCard(preview: SupportPreview, locked = false) { + private renderSupportPreviewSummaryCard(preview: SupportPreview, locked = false) { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; const height = locked ? 438 : 402; const y = this.sideContentTop(); const accent = this.usableAccentColor(preview.usable); + const isHeal = preview.usable.effect === 'heal'; + const distance = this.tileDistance(preview.user, preview.target); const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 1)); bg.setOrigin(0); - bg.setStrokeStyle(2, accent, 0.78); + bg.setStrokeStyle(2, accent, locked ? 0.95 : 0.78); - const actionFrame = this.trackSideObject(this.add.rectangle(left + 40, y + 40, 72, 72, 0x0a0f14, 0.94)); - actionFrame.setStrokeStyle(1, accent, 0.76); - this.trackSideIcon(left + 40, y + 40, this.usableIcon(preview.usable), 64); - this.trackSideObject(this.add.text(left + 86, y + 8, `${preview.user.name} → ${preview.target.name}`, { + const actionFrame = this.trackSideObject(this.add.rectangle(left + 42, y + 42, 74, 74, 0x0a0f14, 0.95)); + actionFrame.setStrokeStyle(1, accent, 0.82); + this.trackSideIcon(left + 42, y + 42, this.usableIcon(preview.usable), 66); + + this.trackSideObject(this.add.text(left + 88, y + 9, `${preview.user.name} → ${preview.target.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f4dfad', fontStyle: '700', - fixedWidth: width - 96 + fixedWidth: width - 98 })); - this.trackSideObject(this.add.text(left + 86, y + 36, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)} · ${this.usableTargetLabel(preview.usable)} ${preview.usable.range}칸`, { + this.trackSideObject(this.add.text(left + 88, y + 37, `${preview.usable.name} · ${this.usableEffectLabel(preview.usable)}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', - color: '#9fb0bf', + color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 96 + fixedWidth: width - 98 })); - this.trackSideObject(this.add.text(left + 86, y + 56, `거리 ${this.tileDistance(preview.user, preview.target)} / 사거리 ${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, { + this.trackSideObject(this.add.text(left + 88, y + 57, `거리 ${distance}/${preview.usable.range} · ${locked ? '선택됨' : '대상 후보'}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', - color: locked ? '#f2e3bf' : '#c8d2dd', - fixedWidth: width - 96 + color: locked ? '#f2e3bf' : '#9fb0bf', + fixedWidth: width - 98 })); - const participantGap = 8; - const participantWidth = (width - 20 - participantGap) / 2; - this.renderPreviewParticipantBox( - left + 10, - y + 82, - participantWidth, - preview.user, - '사용자', - accent - ); - this.renderPreviewParticipantBox( - left + 10 + participantWidth + participantGap, - y + 82, - participantWidth, - preview.target, - '대상', - preview.target.faction === preview.user.faction ? palette.blue : 0xb86b55 - ); + const metricGap = 6; + const metricWidth = (width - 20 - metricGap * 2) / 3; + const metricTop = y + 90; + 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); + this.renderPreviewDecisionCard(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'hp', '병력', `${preview.projectedHp}`, 0x59d18c); + } else { + this.renderPreviewDecisionCard(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '강화', `+${preview.attackBonus}`, palette.gold); + this.renderPreviewDecisionCard(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'hit', '명중', `+${preview.hitBonus}`, accent); + this.renderPreviewDecisionCard(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'leadership', '지속', `${preview.duration}턴`, 0x83d6ff); + } const chipGap = 8; const chipWidth = (width - 20 - chipGap) / 2; this.renderPreviewEquipmentChip( left + 10, - y + 146, + y + 184, chipWidth, preview.usable.command === 'item' ? 'accessory' : 'strategy', this.usableChannelLabel(preview.usable), @@ -6337,43 +6336,77 @@ export class BattleScene extends Phaser.Scene { ); this.renderPreviewEquipmentChip( left + 10 + chipWidth + chipGap, - y + 146, + y + 184, chipWidth, this.usableTargetIcon(preview.usable), - '대상/사거리', - `${preview.target.name} · ${preview.usable.range}칸` + this.usableTargetLabel(preview.usable), + preview.target.name ); - const metricTop = y + 204; - const metricGap = 6; - const metricWidth = (width - 20 - metricGap * 2) / 3; - if (preview.usable.effect === 'heal') { - this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '회복', `+${preview.healAmount}`, 0x59d18c); - this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'success', '적용', '100%', accent); - this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, this.usableTargetIcon(preview.usable), '대상', `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}`, accent); - } else { - this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.usablePowerIcon(preview.usable), '강화', `+${preview.attackBonus}`, 0xd8b15f); - this.renderPreviewMetric(left + 10 + metricWidth + metricGap, metricTop, metricWidth, 'hit', '명중', `+${preview.hitBonus}`, accent); - this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `+${preview.criticalBonus}`, 0xd8732c); - } + 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 + 308, width - 20, this.supportPreviewModifierSummaries(preview), accent); - - const badgeY = y + 374; + const badgeY = y + 346; const maxBadgeRight = left + width - 10; let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableIcon(preview.usable), this.usableEffectLabel(preview.usable), maxBadgeRight); badgeX = this.renderPreviewBadge(badgeX, badgeY, this.usableTargetIcon(preview.usable), `${this.usableTargetLabel(preview.usable)} ${preview.usable.range}칸`, maxBadgeRight); let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, preview.usable.command === 'item' ? 'accessory' : 'strategy', this.usableChannelLabel(preview.usable), maxBadgeRight); - if (preview.usable.effect === 'focus') { - this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `${preview.duration}턴`, maxBadgeRight); + if (!isHeal) { + detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'critical', `치명 +${preview.criticalBonus}`, maxBadgeRight); } if (locked) { this.renderPreviewConfirmBar(left + 10, y + 402, width - 20, accent, `${preview.target.name} 선택됨`); } } + private renderPreviewDecisionCard( + x: number, + y: number, + width: number, + icon: BattleUiIconKey, + label: string, + value: string, + tone: number + ) { + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 84, 0x16212d, 0.97)); + bg.setOrigin(0); + bg.setStrokeStyle(1, tone, 0.72); + + 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 labelText = this.trackSideObject(this.add.text(x + width / 2, y + 47, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#9fb0bf', + fontStyle: '700', + fixedWidth: width - 8, + align: 'center' + })); + labelText.setOrigin(0.5, 0); + + const valueHasKorean = /[가-힣]/.test(value); + const valueText = this.trackSideObject(this.add.text(x + width / 2, y + 62, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: valueHasKorean || value.length > 4 ? '17px' : '22px', + color: '#f2e3bf', + fontStyle: '700', + fixedWidth: width - 8, + align: 'center' + })); + valueText.setOrigin(0.5, 0); + } + + private renderCombatPreviewCard(preview: CombatPreview, locked = false) { + return this.renderCombatPreviewSummaryCard(preview, locked); + } + + private renderSupportPreviewCard(preview: SupportPreview, locked = false) { + return this.renderSupportPreviewSummaryCard(preview, locked); + } + private renderPreviewModifierCards( x: number, y: number,