From 2d66b1dec3a17ffc70b1faaf6bcdeedfa577f0d4 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 3 Jul 2026 03:35:16 +0900 Subject: [PATCH] Improve combat preview readability --- src/game/scenes/BattleScene.ts | 157 +++++++++++++++++++++++---------- 1 file changed, 109 insertions(+), 48 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index d03efcb..4dc969b 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -5797,74 +5797,65 @@ export class BattleScene extends Phaser.Scene { const { panelX, panelWidth } = this.layout; const left = panelX + 24; const width = panelWidth - 48; - const height = locked ? 394 : 360; - const y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 344, this.sideContentBottom(10) - height)); + const height = locked ? 396 : 360; + const y = Math.max(this.sideContentTop(), Math.min(this.sideContentTop() + 328, this.sideContentBottom(10) - height)); const accent = this.previewAccentColor(preview); const defenderClass = getUnitClass(preview.defender.classKey); - const counterText = preview.counterAvailable ? '반격 있음' : '반격 없음'; const attackerEquipmentLabel = preview.action === 'strategy' ? '책략 장비' : preview.action === 'item' ? '사용 장비' : '공격 장비'; + 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 bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 0.98)); bg.setOrigin(0); bg.setStrokeStyle(2, accent, 0.78); - const actionFrame = this.trackSideObject(this.add.rectangle(left + 36, y + 38, 64, 64, 0x0a0f14, 0.94)); + const actionFrame = this.trackSideObject(this.add.rectangle(left + 38, y + 38, 66, 66, 0x0a0f14, 0.94)); actionFrame.setStrokeStyle(1, accent, 0.76); - this.trackSideIcon(left + 36, y + 38, this.actionIcon(preview), 56); - this.trackSideObject(this.add.text(left + 78, y + 10, `${preview.attacker.name} → ${preview.defender.name}`, { + this.trackSideIcon(left + 38, y + 38, this.actionIcon(preview), 58); + this.trackSideObject(this.add.text(left + 80, y + 9, `${preview.attacker.name} → ${preview.defender.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '20px', color: '#f4dfad', fontStyle: '700', fixedWidth: width - 90 })); - this.trackSideObject(this.add.text(left + 78, y + 38, `${this.previewActionLabel(preview)} · ${defenderClass.name} · ${preview.matchupLabel} · ${counterText}`, { + this.trackSideObject(this.add.text(left + 80, y + 36, `${this.previewActionLabel(preview)} · ${this.previewActionTypeLabel(preview)} · ${defenderClass.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', - color: preview.counterAvailable ? '#ffb6a6' : '#9fb0bf', + color: '#9fb0bf', fixedWidth: width - 90 })); - const attackerWeapon = getItem(preview.attacker.equipment.weapon.itemId); - const defenderArmor = getItem(preview.defender.equipment.armor.itemId); - const participantGap = 8; - const participantWidth = (width - 20 - participantGap) / 2; - this.renderPreviewParticipantBox( - left + 10, - y + 72, - participantWidth, - preview.attacker, - '공격자', - accent - ); - this.renderPreviewParticipantBox( - left + 10 + participantWidth + participantGap, - y + 72, - participantWidth, - preview.defender, - '대상', - 0xb86b55 - ); - const chipGap = 8; const chipWidth = (width - 20 - chipGap) / 2; - this.renderPreviewEquipmentChip(left + 10, y + 134, chipWidth, this.itemIcon(attackerWeapon, 'weapon'), attackerEquipmentLabel, attackerWeapon.name); - this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 134, chipWidth, this.itemIcon(defenderArmor, 'armor'), '방어 장비', defenderArmor.name); + this.renderPreviewEquipmentChip(left + 10, y + 74, chipWidth, sourceIcon, sourceLabel, sourceName); + this.renderPreviewEquipmentChip(left + 10 + chipWidth + chipGap, y + 74, chipWidth, this.itemIcon(defenderArmor, 'armor'), '대상 방어구', defenderArmor.name); - const metricTop = y + 194; - const metricGap = 6; - const metricWidth = (width - 20 - metricGap * 2) / 3; - this.renderPreviewMetric(left + 10, metricTop, metricWidth, this.previewDamageIcon(preview), '피해', `${preview.damage}`, 0xb64a45); - this.renderPreviewMetric( + const metricGap = 8; + const metricWidth = (width - 20 - metricGap) / 2; + this.renderPreviewOutcomeCard(left + 10, y + 134, metricWidth, this.previewDamageIcon(preview), '예상 피해', `${preview.damage}`, 0xb64a45); + this.renderPreviewOutcomeCard( left + 10 + metricWidth + metricGap, - metricTop, + y + 134, metricWidth, preview.action === 'strategy' ? 'success' : 'hit', this.previewSuccessLabel(preview), `${preview.hitRate}%`, accent ); - this.renderPreviewMetric(left + 10 + (metricWidth + metricGap) * 2, metricTop, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c); + this.renderPreviewOutcomeCard(left + 10, y + 198, metricWidth, 'critical', '치명', `${preview.criticalRate}%`, 0xd8732c); + this.renderPreviewOutcomeCard( + left + 10 + metricWidth + metricGap, + y + 198, + metricWidth, + 'counter', + '반격', + preview.counterAvailable ? '주의' : '없음', + preview.counterAvailable ? 0xb64a45 : 0x59d18c + ); this.renderPreviewForecastRow(preview, left + 10, y + 264, width - 20, accent); this.renderPreviewModifierStrip( @@ -5872,7 +5863,7 @@ export class BattleScene extends Phaser.Scene { y + 300, width - 20, 'mastery', - this.compactModifierText(this.previewModifierLabels(preview), 1, 28), + this.compactModifierText(this.previewModifierLabels(preview), 2, 36), accent ); @@ -5881,8 +5872,7 @@ export class BattleScene extends Phaser.Scene { let badgeX = left + 10; badgeX = this.renderPreviewBadge(badgeX, badgeY, this.actionIcon(preview), this.previewActionTypeLabel(preview), maxBadgeRight); badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight); - badgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight); - let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'counter', counterText, maxBadgeRight); + let detailBadgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight); if (preview.bondDamageBonus > 0 && preview.bondLabel) { detailBadgeX = this.renderPreviewBadge(detailBadgeX, badgeY, 'leadership', `공명 +${preview.bondDamageBonus}%`, maxBadgeRight); } @@ -6150,6 +6140,42 @@ export class BattleScene extends Phaser.Scene { valueText.setOrigin(1, 0.5); } + private renderPreviewOutcomeCard( + x: number, + y: number, + width: number, + icon: BattleUiIconKey, + label: string, + value: string, + tone = 0x647485 + ) { + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 58, 0x16212d, 0.96)); + bg.setOrigin(0); + bg.setStrokeStyle(1, tone, 0.68); + + const iconFrame = this.trackSideObject(this.add.rectangle(x + 24, y + 29, 42, 42, 0x0a0f14, 0.9)); + iconFrame.setStrokeStyle(1, tone, 0.54); + this.trackSideIcon(x + 24, y + 29, icon, 38); + + this.trackSideObject(this.add.text(x + 52, y + 8, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#9fb0bf', + fontStyle: '700', + fixedWidth: width - 60 + })); + + const valueHasKorean = /[가-힣]/.test(value); + const valueFontSize = valueHasKorean || value.length > 4 ? '21px' : '25px'; + const valueText = this.trackSideObject(this.add.text(x + width - 10, y + 38, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: valueFontSize, + color: '#f2e3bf', + fontStyle: '700' + })); + valueText.setOrigin(1, 0.5); + } + private renderPreviewForecastRow(preview: CombatPreview, x: number, y: number, width: number, accent: number) { const projectedHp = Math.max(0, preview.defender.hp - preview.damage); const label = preview.action === 'strategy' ? '성공 시 HP' : '명중 시 HP'; @@ -11443,24 +11469,51 @@ export class BattleScene extends Phaser.Scene { view.direction = direction; this.stopUnitIdle(view); view.sprite.setFlipX(false); - view.sprite.play(`${view.textureBase}-walk-${direction}`, true); + const animationKey = `${view.textureBase}-walk-${direction}`; + if (!this.tryPlayUnitAnimation(view, animationKey)) { + this.setUnitDirectionFrame(view, direction, Math.floor(unitBaseFramesPerDirection / 2)); + } } private playUnitIdle(unit: UnitData, view: UnitView, direction: UnitDirection) { view.direction = direction; this.stopUnitIdle(view); view.sprite.setFlipX(false); - view.sprite.play(`${view.textureBase}-idle-${direction}`, true); + const animationKey = `${view.textureBase}-idle-${direction}`; + if (!this.tryPlayUnitAnimation(view, animationKey)) { + this.setUnitDirectionFrame(view, direction); + } this.startUnitIdleMotion(unit, view, direction); } + private tryPlayUnitAnimation(view: UnitView, animationKey: string) { + if (!view.sprite.active || !view.sprite.scene || !this.anims.exists(animationKey)) { + return false; + } + + try { + view.sprite.play(animationKey, true); + return true; + } catch { + return false; + } + } + private stopUnitWalk(view: UnitView, direction: UnitDirection) { view.direction = direction; this.stopUnitIdle(view); - view.sprite.stop(); + this.tryStopUnitAnimation(view); this.setUnitDirectionFrame(view, direction); } + private tryStopUnitAnimation(view: UnitView) { + try { + view.sprite.stop(); + } catch { + // Some hot scene swaps can leave Phaser's animation state half-created. + } + } + private syncUnitMotion(unit: UnitData, view = this.unitViews.get(unit.id), direction = view?.direction) { if (!view || !direction) { return; @@ -11475,10 +11528,18 @@ export class BattleScene extends Phaser.Scene { this.playUnitIdle(unit, view, direction); } - private setUnitDirectionFrame(view: UnitView, direction: UnitDirection) { - view.sprite.setTexture(view.textureBase, this.unitFrameIndex(direction)); - view.sprite.setFlipX(false); - this.applyBaseSpriteBlend(view.sprite); + private setUnitDirectionFrame(view: UnitView, direction: UnitDirection, frame = 0) { + if (!view.sprite.active || !view.sprite.scene || !this.textures.exists(view.textureBase)) { + return; + } + + try { + view.sprite.setTexture(view.textureBase, this.unitFrameIndex(direction, frame)); + view.sprite.setFlipX(false); + this.applyBaseSpriteBlend(view.sprite); + } catch { + // Some hot scene swaps can leave Phaser's sprite texture state half-created. + } } private setUnitBasePosition(view: UnitView, x: number, y: number) {