Improve combat preview readability

This commit is contained in:
2026-07-03 03:35:16 +09:00
parent 17db165df4
commit 2d66b1dec3

View File

@@ -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) {