Improve combat preview readability
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user