diff --git a/scripts/build-battle-ui-icons.py b/scripts/build-battle-ui-icons.py index 2d4d9d3..6187c69 100644 --- a/scripts/build-battle-ui-icons.py +++ b/scripts/build-battle-ui-icons.py @@ -9,7 +9,8 @@ from PIL import Image, ImageDraw, ImageFilter ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "src" / "assets" / "images" / "ui" / "battle-ui-icons.png" -FRAME = 48 +BASE_FRAME = 48 +FRAME = 64 SCALE = 4 @@ -38,6 +39,10 @@ def main() -> None: ("counter", draw_counter), ("advantage", draw_advantage), ("success", draw_success), + ("heal", draw_heal), + ("focus", draw_focus), + ("fire", draw_fire), + ("shout", draw_shout), ] cols = 5 rows = math.ceil(len(icons) / cols) @@ -63,24 +68,32 @@ def c(value: tuple[int, int, int, int] | tuple[int, int, int]) -> tuple[int, int return value +def s(value: float) -> int: + return round(value * FRAME / BASE_FRAME * SCALE) + + +def sw(width: float) -> int: + return max(1, round(width * FRAME / BASE_FRAME * SCALE)) + + def line(draw: ImageDraw.ImageDraw, points: list[tuple[int, int]], fill: tuple[int, int, int, int], width: int) -> None: - scaled = [(x * SCALE, y * SCALE) for x, y in points] - draw.line(scaled, fill=fill, width=width * SCALE, joint="curve") + scaled = [(s(x), s(y)) for x, y in points] + draw.line(scaled, fill=fill, width=sw(width), joint="curve") def poly(draw: ImageDraw.ImageDraw, points: list[tuple[int, int]], fill, outline=None) -> None: - scaled = [(x * SCALE, y * SCALE) for x, y in points] + scaled = [(s(x), s(y)) for x, y in points] draw.polygon(scaled, fill=c(fill), outline=c(outline) if outline else None) def ellipse(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], fill, outline=None, width=1) -> None: - scaled = tuple(v * SCALE for v in box) - draw.ellipse(scaled, fill=c(fill), outline=c(outline) if outline else None, width=width * SCALE) + scaled = tuple(s(v) for v in box) + draw.ellipse(scaled, fill=c(fill), outline=c(outline) if outline else None, width=sw(width)) def rect(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], fill, outline=None, width=1) -> None: - scaled = tuple(v * SCALE for v in box) - draw.rounded_rectangle(scaled, radius=4 * SCALE, fill=c(fill), outline=c(outline) if outline else None, width=width * SCALE) + scaled = tuple(s(v) for v in box) + draw.rounded_rectangle(scaled, radius=sw(4), fill=c(fill), outline=c(outline) if outline else None, width=sw(width)) def icon_shadow(draw: ImageDraw.ImageDraw) -> None: @@ -279,5 +292,48 @@ def draw_success(draw: ImageDraw.ImageDraw) -> None: ellipse(draw, (18, 36, 30, 43), (180, 63, 58), (73, 27, 27)) +def draw_heal(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + ellipse(draw, (8, 8, 40, 40), (34, 85, 69), (22, 39, 35), 2) + poly(draw, [(24, 39), (10, 25), (10, 15), (16, 10), (24, 15), (32, 10), (38, 15), (38, 25)], (211, 68, 70), (65, 18, 24)) + poly(draw, [(24, 35), (14, 24), (14, 17), (18, 14), (24, 19), (30, 14), (34, 17), (34, 24)], (255, 139, 117)) + rect(draw, (20, 13, 28, 35), (235, 248, 210), (61, 93, 72), 1) + rect(draw, (13, 20, 35, 28), (235, 248, 210), (61, 93, 72), 1) + line(draw, [(14, 35), (34, 35)], (133, 222, 150, 210), 2) + + +def draw_focus(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(14, 40), (14, 8)], (113, 83, 45), 4) + poly(draw, [(17, 10), (39, 14), (31, 23), (39, 31), (17, 28)], (50, 111, 178), (23, 44, 76)) + poly(draw, [(17, 13), (35, 16), (29, 22), (35, 28), (17, 25)], (109, 169, 232)) + points = [] + for i in range(10): + radius = 9 if i % 2 == 0 else 4 + angle = -math.pi / 2 + i * math.pi / 5 + points.append((24 + math.cos(angle) * radius, 23 + math.sin(angle) * radius)) + poly(draw, [(round(x), round(y)) for x, y in points], (244, 205, 91), (87, 55, 23)) + ellipse(draw, (9, 36, 19, 44), (216, 179, 88), (74, 55, 31)) + + +def draw_fire(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(24, 7), (34, 18), (31, 16), (38, 29), (31, 41), (18, 42), (10, 31), (14, 21), (18, 24)], (154, 42, 35), (58, 20, 19)) + poly(draw, [(25, 11), (32, 22), (29, 21), (34, 31), (29, 38), (20, 39), (14, 31), (18, 24), (20, 28)], (229, 82, 49)) + poly(draw, [(25, 20), (30, 29), (27, 36), (21, 36), (18, 30), (21, 25)], (255, 190, 78), (116, 51, 26)) + line(draw, [(13, 39), (37, 39)], (86, 35, 26, 180), 2) + + +def draw_shout(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + ellipse(draw, (10, 15, 31, 36), (125, 72, 46), (49, 28, 24), 2) + poly(draw, [(27, 19), (42, 13), (39, 35), (27, 30)], (218, 176, 96), (72, 50, 27)) + poly(draw, [(31, 20), (38, 18), (36, 30), (31, 28)], (255, 223, 134)) + line(draw, [(35, 10), (43, 6)], (232, 84, 61), 3) + line(draw, [(38, 24), (46, 24)], (232, 84, 61), 3) + line(draw, [(35, 38), (43, 43)], (232, 84, 61), 3) + ellipse(draw, (16, 22, 23, 29), (35, 20, 18), None) + + if __name__ == "__main__": main() diff --git a/src/assets/images/ui/battle-ui-icons.png b/src/assets/images/ui/battle-ui-icons.png index c353f0a..1134065 100644 Binary files a/src/assets/images/ui/battle-ui-icons.png and b/src/assets/images/ui/battle-ui-icons.png differ diff --git a/src/game/data/battleUiIcons.ts b/src/game/data/battleUiIcons.ts index cc6945d..3144b7e 100644 --- a/src/game/data/battleUiIcons.ts +++ b/src/game/data/battleUiIcons.ts @@ -3,7 +3,7 @@ import Phaser from 'phaser'; import battleUiIconsUrl from '../../assets/images/ui/battle-ui-icons.png'; export const battleUiIconTextureKey = 'battle-ui-icons'; -export const battleUiIconFrameSize = 48; +export const battleUiIconFrameSize = 64; export const battleUiIconFrames = { hp: 0, @@ -27,7 +27,11 @@ export const battleUiIconFrames = { terrain: 18, counter: 19, advantage: 20, - success: 21 + success: 21, + heal: 22, + focus: 23, + fire: 24, + shout: 25 } as const; export type BattleUiIconKey = keyof typeof battleUiIconFrames; @@ -38,7 +42,7 @@ export const battleUiIconManifest = { frameWidth: battleUiIconFrameSize, frameHeight: battleUiIconFrameSize, columns: 5, - rows: 5, + rows: 6, frames: battleUiIconFrames } as const; diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 66734dd..7c122fe 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -4170,12 +4170,12 @@ export class BattleScene extends Phaser.Scene { background.setInteractive({ useHandCursor: true }); background.setDepth(15); - const iconFrame = this.add.rectangle(left + 27, centerY, 26, 26, 0x0a0f14, 0.86); + const iconFrame = this.add.rectangle(left + 28, centerY, 30, 30, 0x0a0f14, 0.86); iconFrame.setStrokeStyle(1, command === 'wait' ? palette.gold : palette.blue, 0.58); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(left + 27, centerY, this.commandIcon(command, unit), 22); + const icon = this.createBattleUiIcon(left + 28, centerY, this.commandIcon(command, unit), 26); icon.setDepth(17); this.commandMenuObjects.push(icon); @@ -4294,22 +4294,22 @@ export class BattleScene extends Phaser.Scene { } private renderUsableMenuPill(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, tone: number) { - const pill = this.add.rectangle(x, y, width, 22, 0x0b1118, 0.92); + const pill = this.add.rectangle(x, y, width, 26, 0x0b1118, 0.92); pill.setOrigin(0); pill.setDepth(16); pill.setStrokeStyle(1, tone, 0.54); this.commandMenuObjects.push(pill); - const iconImage = this.createBattleUiIcon(x + 12, y + 11, icon, 16); + const iconImage = this.createBattleUiIcon(x + 14, y + 13, icon, 20); iconImage.setDepth(17); this.commandMenuObjects.push(iconImage); - const text = this.add.text(x + 25, y + 5, label, { + const text = this.add.text(x + 30, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '10px', + fontSize: '11px', color: '#d4dce6', fontStyle: '700', - fixedWidth: width - 29 + fixedWidth: width - 34 }); text.setDepth(17); this.commandMenuObjects.push(text); @@ -4334,9 +4334,9 @@ export class BattleScene extends Phaser.Scene { this.hideCommandMenu(); - const menuWidth = 322; + const menuWidth = 348; const titleHeight = 36; - const rowHeight = 72; + const rowHeight = 82; const padding = 9; const menuHeight = padding * 2 + titleHeight + usables.length * rowHeight; const { left, top } = this.commandMenuPosition(pointerX, pointerY, menuWidth, menuHeight); @@ -4368,26 +4368,26 @@ export class BattleScene extends Phaser.Scene { bg.setInteractive({ useHandCursor: true }); this.commandMenuObjects.push(bg); - const iconFrame = this.add.rectangle(left + 31, rowTop + 32, 38, 38, 0x0a0f14, 0.9); + const iconFrame = this.add.rectangle(left + 36, rowTop + 38, 50, 50, 0x0a0f14, 0.9); iconFrame.setStrokeStyle(1, accent, 0.66); iconFrame.setDepth(16); this.commandMenuObjects.push(iconFrame); - const icon = this.createBattleUiIcon(left + 31, rowTop + 32, this.usableIcon(usable), 32); + const icon = this.createBattleUiIcon(left + 36, rowTop + 38, this.usableIcon(usable), 44); icon.setDepth(17); this.commandMenuObjects.push(icon); const stock = this.usableStockLabel(unit, usable); - const name = this.add.text(left + 56, rowTop + 7, usable.name, { + const name = this.add.text(left + 68, rowTop + 8, usable.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '17px', + fontSize: '18px', color: '#f2e3bf', fontStyle: '700' }); name.setDepth(16); this.commandMenuObjects.push(name); - const stockText = this.add.text(left + menuWidth - 24, rowTop + 9, stock, { + const stockText = this.add.text(left + menuWidth - 24, rowTop + 10, stock, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: command === 'item' ? '#f4dfad' : '#9fb0bf', @@ -4398,22 +4398,22 @@ export class BattleScene extends Phaser.Scene { this.commandMenuObjects.push(stockText); const summary = this.add.text( - left + 56, - rowTop + 31, + left + 68, + rowTop + 34, `${this.usableEffectLabel(usable)} · ${this.usableTargetLabel(usable)} · 사거리 ${usable.range} · 대상 ${this.usableTargetCount(unit, usable)}명`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#aeb7c2', - fixedWidth: menuWidth - 74 + fixedWidth: menuWidth - 88 } ); summary.setDepth(16); this.commandMenuObjects.push(summary); - this.renderUsableMenuPill(left + 56, rowTop + 43, 78, this.usableIcon(usable), this.usableEffectLabel(usable), accent); - this.renderUsableMenuPill(left + 140, rowTop + 43, 86, 'move', `거리 ${usable.range}`, accent); - this.renderUsableMenuPill(left + 232, rowTop + 43, 82, usable.target === 'enemy' ? 'hit' : 'success', this.usablePowerLabel(usable), accent); + this.renderUsableMenuPill(left + 68, rowTop + 49, 82, this.usableIcon(usable), this.usableEffectLabel(usable), accent); + this.renderUsableMenuPill(left + 156, rowTop + 49, 90, 'move', `거리 ${usable.range}`, accent); + this.renderUsableMenuPill(left + 252, rowTop + 49, 86, usable.target === 'enemy' ? 'hit' : 'success', this.usablePowerLabel(usable), accent); const choose = (choicePointer: Phaser.Input.Pointer) => { if (choicePointer.rightButtonDown()) { @@ -4943,24 +4943,24 @@ export class BattleScene extends Phaser.Scene { roleLabel: string, label: string ) { - const chip = this.trackSideObject(this.add.rectangle(x, y, width, 30, 0x0b1118, 0.96)); + const chip = this.trackSideObject(this.add.rectangle(x, y, width, 34, 0x0b1118, 0.96)); chip.setOrigin(0); chip.setStrokeStyle(1, 0x647485, 0.58); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 16, y + 15, 24, 24, 0x16212d, 0.92)); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 18, y + 17, 30, 30, 0x16212d, 0.92)); iconFrame.setStrokeStyle(1, 0x53606c, 0.55); - this.trackSideIcon(x + 16, y + 15, icon, 22); - this.trackSideObject(this.add.text(x + 33, y + 3, roleLabel, { + this.trackSideIcon(x + 18, y + 17, icon, 27); + this.trackSideObject(this.add.text(x + 38, y + 4, roleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '10px', color: '#9fb0bf', fontStyle: '700' })); - this.trackSideObject(this.add.text(x + 33, y + 15, label, { + this.trackSideObject(this.add.text(x + 38, y + 17, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#d4dce6', fontStyle: '700', - wordWrap: { width: width - 42, useAdvancedWrap: true } + wordWrap: { width: width - 48, useAdvancedWrap: true } })); } @@ -4973,16 +4973,16 @@ export class BattleScene extends Phaser.Scene { value: string, tone = 0x647485 ) { - const bg = this.trackSideObject(this.add.rectangle(x, y, width, 36, 0x16212d, 0.94)); + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 40, 0x16212d, 0.94)); bg.setOrigin(0); bg.setStrokeStyle(1, tone, 0.62); - this.trackSideIcon(x + 16, y + 18, icon, 23); - this.trackSideObject(this.add.text(x + 33, y + 5, label, { + this.trackSideIcon(x + 18, y + 20, icon, 28); + this.trackSideObject(this.add.text(x + 38, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf' })); - const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 18, value, { + const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 20, value, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '17px', color: '#f2e3bf', @@ -4994,18 +4994,18 @@ export class BattleScene extends Phaser.Scene { 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'; - const row = this.trackSideObject(this.add.rectangle(x, y, width, 20, 0x0b1118, 0.94)); + const row = this.trackSideObject(this.add.rectangle(x, y, width, 24, 0x0b1118, 0.94)); row.setOrigin(0); row.setStrokeStyle(1, accent, 0.48); - this.trackSideIcon(x + 12, y + 10, 'hp', 17); - this.trackSideObject(this.add.text(x + 26, y + 4, label, { + this.trackSideIcon(x + 14, y + 12, 'hp', 20); + this.trackSideObject(this.add.text(x + 30, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: '#9fb0bf', fontStyle: '700' })); - this.drawGauge(x + 96, y + 6, width - 170, 8, projectedHp / preview.defender.maxHp, projectedHp <= 0 ? 0xb64a45 : 0x59d18c); - const hpText = this.trackSideObject(this.add.text(x + width - 8, y + 4, `${preview.defender.hp} -> ${projectedHp}`, { + this.drawGauge(x + 102, y + 8, width - 176, 8, projectedHp / preview.defender.maxHp, projectedHp <= 0 ? 0xb64a45 : 0x59d18c); + const hpText = this.trackSideObject(this.add.text(x + width - 8, y + 6, `${preview.defender.hp} -> ${projectedHp}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: projectedHp <= 0 ? '#ff8f5f' : '#f2e3bf', @@ -5015,11 +5015,11 @@ export class BattleScene extends Phaser.Scene { } private renderSupportForecastRow(preview: SupportPreview, x: number, y: number, width: number, accent: number) { - const row = this.trackSideObject(this.add.rectangle(x, y, width, 22, 0x0b1118, 0.94)); + const row = this.trackSideObject(this.add.rectangle(x, y, width, 24, 0x0b1118, 0.94)); row.setOrigin(0); row.setStrokeStyle(1, accent, 0.48); const isHeal = preview.usable.effect === 'heal'; - this.trackSideIcon(x + 12, y + 11, isHeal ? 'hp' : 'leadership', 17); + this.trackSideIcon(x + 14, y + 12, isHeal ? 'heal' : 'focus', 20); this.trackSideObject(this.add.text(x + 26, y + 5, isHeal ? '적용 후 HP' : '강화 결과', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', @@ -5028,8 +5028,8 @@ export class BattleScene extends Phaser.Scene { })); if (isHeal) { - this.drawGauge(x + 100, y + 7, width - 178, 8, preview.projectedHp / preview.target.maxHp, 0x59d18c); - const hpText = this.trackSideObject(this.add.text(x + width - 8, y + 5, `${preview.target.hp} -> ${preview.projectedHp}`, { + this.drawGauge(x + 104, y + 8, width - 182, 8, preview.projectedHp / preview.target.maxHp, 0x59d18c); + const hpText = this.trackSideObject(this.add.text(x + width - 8, y + 6, `${preview.target.hp} -> ${preview.projectedHp}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#f2e3bf', @@ -5041,7 +5041,7 @@ export class BattleScene extends Phaser.Scene { const buffText = this.trackSideObject(this.add.text( x + width - 8, - y + 5, + y + 6, `공+${preview.attackBonus} / 명+${preview.hitBonus} / 치+${preview.criticalBonus} / ${preview.duration}턴`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -5054,15 +5054,15 @@ export class BattleScene extends Phaser.Scene { } private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) { - const width = Phaser.Math.Clamp(label.length * 8 + 42, 68, 138); + const width = Phaser.Math.Clamp(label.length * 8 + 48, 76, 146); if (maxRight && x + width > maxRight) { return x; } - const badge = this.trackSideObject(this.add.rectangle(x, y, width, 23, 0x0b1118, 0.95)); + const badge = this.trackSideObject(this.add.rectangle(x, y, width, 26, 0x0b1118, 0.95)); badge.setOrigin(0); badge.setStrokeStyle(1, 0x53606c, 0.58); - this.trackSideIcon(x + 13, y + 11.5, icon, 18); - this.trackSideObject(this.add.text(x + 27, y + 5, label, { + this.trackSideIcon(x + 15, y + 13, icon, 21); + this.trackSideObject(this.add.text(x + 31, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: '#d4dce6' @@ -6025,21 +6025,21 @@ export class BattleScene extends Phaser.Scene { const visibleGains = gains.filter((gain) => gain.amount > 0).slice(0, 5); let cursorX = x; visibleGains.forEach((gain) => { - const chipWidth = Phaser.Math.Clamp(gain.label.length * 12 + 42, 64, 92); + const chipWidth = Phaser.Math.Clamp(gain.label.length * 12 + 50, 72, 102); if (cursorX + chipWidth > x + maxWidth) { return; } - const bg = this.add.rectangle(cursorX, y, chipWidth, 24, 0x0b1118, 0.9); + const bg = this.add.rectangle(cursorX, y, chipWidth, 28, 0x0b1118, 0.9); bg.setOrigin(0); bg.setStrokeStyle(1, 0x53606c, 0.5); - const icon = this.createBattleUiIcon(cursorX + 13, y + 12, gain.icon, 18); - const text = this.add.text(cursorX + 26, y + 5, `+${gain.amount}`, { + const icon = this.createBattleUiIcon(cursorX + 15, y + 14, gain.icon, 22); + const text = this.add.text(cursorX + 31, y + 6, `+${gain.amount}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#a8ffd0', fontStyle: '700' }); - const label = this.add.text(cursorX + 48, y + 5, gain.label, { + const label = this.add.text(cursorX + 54, y + 6, gain.label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: '#d4dce6' @@ -7920,21 +7920,21 @@ export class BattleScene extends Phaser.Scene { bg.setDepth(depth); bg.setStrokeStyle(1, accent, 0.66); - const actionFrame = this.trackCombatObject(this.add.rectangle(x + 24, y + 22, 36, 36, 0x101820, 0.9)); + const actionFrame = this.trackCombatObject(this.add.rectangle(x + 25, y + 23, 42, 42, 0x101820, 0.9)); actionFrame.setDepth(depth + 1); actionFrame.setStrokeStyle(1, accent, 0.68); - this.trackCombatIcon(x + 24, y + 22, this.actionIcon(result), 31, depth + 2); + this.trackCombatIcon(x + 25, y + 23, this.actionIcon(result), 37, depth + 2); const attackerWeapon = getItem(result.attacker.equipment.weapon.itemId); const defenderArmor = getItem(result.defender.equipment.armor.itemId); - const weaponFrame = this.trackCombatObject(this.add.rectangle(x + 134, y + 22, 30, 30, 0x16212d, 0.9)); + const weaponFrame = this.trackCombatObject(this.add.rectangle(x + 134, y + 22, 34, 34, 0x16212d, 0.9)); weaponFrame.setDepth(depth + 1); weaponFrame.setStrokeStyle(1, 0x647485, 0.58); - this.trackCombatIcon(x + 134, y + 22, this.itemIcon(attackerWeapon, 'weapon'), 25, depth + 2); - const armorFrame = this.trackCombatObject(this.add.rectangle(x + 244, y + 22, 30, 30, 0x16212d, 0.9)); + this.trackCombatIcon(x + 134, y + 22, this.itemIcon(attackerWeapon, 'weapon'), 30, depth + 2); + const armorFrame = this.trackCombatObject(this.add.rectangle(x + 244, y + 22, 34, 34, 0x16212d, 0.9)); armorFrame.setDepth(depth + 1); armorFrame.setStrokeStyle(1, 0x647485, 0.58); - this.trackCombatIcon(x + 244, y + 22, this.itemIcon(defenderArmor, 'armor'), 25, depth + 2); + this.trackCombatIcon(x + 244, y + 22, this.itemIcon(defenderArmor, 'armor'), 30, depth + 2); const actionLabel = this.trackCombatObject(this.add.text(x + 46, y + 9, result.usable?.name ?? commandLabels[result.action], { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -7999,13 +7999,13 @@ export class BattleScene extends Phaser.Scene { badge.setOrigin(0); badge.setDepth(depth); badge.setStrokeStyle(1, 0x53606c, 0.48); - this.trackCombatIcon(x + 12, y + 11, icon, 17, depth + 1); - const text = this.trackCombatObject(this.add.text(x + 25, y + 5, label, { + this.trackCombatIcon(x + 13, y + 11, icon, 19, depth + 1); + const text = this.trackCombatObject(this.add.text(x + 28, y + 5, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '10px', color: '#f2e3bf', fontStyle: '700', - fixedWidth: width - 30 + fixedWidth: width - 33 })); text.setDepth(depth + 1); } @@ -8130,22 +8130,22 @@ export class BattleScene extends Phaser.Scene { bg.setDepth(depth); bg.setStrokeStyle(1, accent, 0.66); - const actionFrame = this.trackCombatObject(this.add.rectangle(x + 24, y + 22, 36, 36, 0x101820, 0.9)); + const actionFrame = this.trackCombatObject(this.add.rectangle(x + 25, y + 23, 42, 42, 0x101820, 0.9)); actionFrame.setDepth(depth + 1); actionFrame.setStrokeStyle(1, accent, 0.68); - this.trackCombatIcon(x + 24, y + 22, this.usableIcon(result.usable), 31, depth + 2); + this.trackCombatIcon(x + 25, y + 23, this.usableIcon(result.usable), 37, depth + 2); const sourceItem = getItem(result.usable.command === 'strategy' ? result.user.equipment.weapon.itemId : result.user.equipment.accessory.itemId); const sourceSlot: EquipmentSlot = result.usable.command === 'strategy' ? 'weapon' : 'accessory'; - const sourceFrame = this.trackCombatObject(this.add.rectangle(x + 136, y + 22, 30, 30, 0x16212d, 0.9)); + const sourceFrame = this.trackCombatObject(this.add.rectangle(x + 136, y + 22, 34, 34, 0x16212d, 0.9)); sourceFrame.setDepth(depth + 1); sourceFrame.setStrokeStyle(1, 0x647485, 0.58); - this.trackCombatIcon(x + 136, y + 22, this.itemIcon(sourceItem, sourceSlot), 25, depth + 2); + this.trackCombatIcon(x + 136, y + 22, this.itemIcon(sourceItem, sourceSlot), 30, depth + 2); - const targetFrame = this.trackCombatObject(this.add.rectangle(x + 256, y + 22, 30, 30, 0x16212d, 0.9)); + const targetFrame = this.trackCombatObject(this.add.rectangle(x + 256, y + 22, 34, 34, 0x16212d, 0.9)); targetFrame.setDepth(depth + 1); targetFrame.setStrokeStyle(1, 0x647485, 0.58); - this.trackCombatIcon(x + 256, y + 22, result.usable.effect === 'heal' ? 'hp' : 'leadership', 25, depth + 2); + this.trackCombatIcon(x + 256, y + 22, result.usable.effect === 'heal' ? 'heal' : 'focus', 30, depth + 2); const actionLabel = this.trackCombatObject(this.add.text(x + 46, y + 9, result.usable.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -8573,8 +8573,8 @@ export class BattleScene extends Phaser.Scene { } private renderGrowthGaugeRow(entry: GrowthGaugeEntry, x: number, y: number, width: number, depth: number): GrowthGaugeView { - this.trackCombatIcon(x + 8, y + 8, this.growthEntryIcon(entry), 16, depth); - const label = this.trackCombatObject(this.add.text(x + 22, y - 3, `${entry.owner} ${entry.label}`, { + this.trackCombatIcon(x + 10, y + 8, this.growthEntryIcon(entry), 20, depth); + const label = this.trackCombatObject(this.add.text(x + 28, y - 3, `${entry.owner} ${entry.label}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', @@ -11116,9 +11116,9 @@ export class BattleScene extends Phaser.Scene { header.setOrigin(0); header.setStrokeStyle(1, factionColor, 0.76); - const classIcon = this.trackSideIcon(left + 23, top + 31, this.unitMoveIcon(unit), 30); + const classIcon = this.trackSideIcon(left + 26, top + 31, this.unitMoveIcon(unit), 38); classIcon.setAlpha(acted ? 0.86 : 0.96); - const headerTextX = left + 48; + const headerTextX = left + 58; this.trackSideObject(this.add.text(headerTextX, top + 9, `${rosterLabels[unit.faction]} / ${unitClass.family}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', @@ -11155,7 +11155,7 @@ export class BattleScene extends Phaser.Scene { backText.setOrigin(0.5); } - this.trackSideIcon(left + 10, top + 88, 'hp', 20); + this.trackSideIcon(left + 13, top + 88, 'hp', 26); this.trackSideObject(this.add.text(left + 26, top + 78, '병력', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '17px', @@ -11169,7 +11169,7 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' })); hpValue.setOrigin(1, 0); - this.drawGauge(left + 92, top + 84, width - 176, 10, unit.hp / unit.maxHp, 0x59d18c); + this.drawGauge(left + 102, top + 84, width - 186, 10, unit.hp / unit.maxHp, 0x59d18c); const attackBonus = this.equipmentAttackBonus(unit); const summaryGap = 6; @@ -11191,23 +11191,23 @@ export class BattleScene extends Phaser.Scene { }); const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}% / 이동 ${unit.move}${acted ? ' / 행동완료' : ''}`; - this.trackSideIcon(left + 11, top + 315, acted ? 'counter' : this.unitMoveIcon(unit), 20); - this.trackSideObject(this.add.text(left + 28, top + 306, positionText, { + this.trackSideIcon(left + 13, top + 294, acted ? 'counter' : this.unitMoveIcon(unit), 24); + this.trackSideObject(this.add.text(left + 28, top + 285, positionText, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: acted ? '#bdbdbd' : '#9fb0bf', wordWrap: { width: width - 24, useAdvancedWrap: true } })); - this.renderEquipmentSummary(unit, left, top + 334, width); + this.renderEquipmentSummary(unit, left, top + 308, width); if (message) { - this.renderPanelMessage(message, left, top + 472, width, 48); + this.renderPanelMessage(message, left, top + 474, width, 40); } this.showFacingIndicator(unit); } private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) { - this.trackSideIcon(x + 11, y + 11, 'mastery', 22); + this.trackSideIcon(x + 14, y + 11, 'mastery', 26); this.trackSideObject(this.add.text(x + 31, y, '장비 / 숙련도', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', @@ -11216,7 +11216,7 @@ export class BattleScene extends Phaser.Scene { })); equipmentSlots.forEach((slot, index) => { - this.renderEquipmentRow(unit, slot, x, y + 26 + index * 33, width); + this.renderEquipmentRow(unit, slot, x, y + 26 + index * 37, width); }); } @@ -11225,22 +11225,22 @@ export class BattleScene extends Phaser.Scene { const item = getItem(state.itemId); const next = equipmentExpToNext(state.level); const isTreasure = item.rank === 'treasure'; - const bg = this.trackSideObject(this.add.rectangle(x, y, width, 32, isTreasure ? 0x1f2430 : 0x101820, isTreasure ? 0.97 : 0.94)); + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 36, isTreasure ? 0x1f2430 : 0x101820, isTreasure ? 0.97 : 0.94)); bg.setOrigin(0); bg.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.66 : 0.5); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 17, y + 16, 26, 26, 0x0a0f14, 0.86)); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 20, y + 18, 32, 32, 0x0a0f14, 0.86)); iconFrame.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.78 : 0.54); - this.trackSideIcon(x + 17, y + 16, this.itemIcon(item, slot), 24); + this.trackSideIcon(x + 20, y + 18, this.itemIcon(item, slot), 29); - this.trackSideObject(this.add.text(x + 38, y + 5, equipmentSlotLabels[slot], { + this.trackSideObject(this.add.text(x + 44, y + 3, equipmentSlotLabels[slot], { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf', fontStyle: '700' })); - this.trackSideObject(this.add.text(x + 84, y + 5, item.name, { + this.trackSideObject(this.add.text(x + 88, y + 3, item.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: isTreasure ? '#f4dfad' : '#d4dce6', @@ -11248,21 +11248,21 @@ export class BattleScene extends Phaser.Scene { })); const bonusText = this.itemBonusText(item); - this.trackSideObject(this.add.text(x + width - 130, y + 8, bonusText, { + this.trackSideObject(this.add.text(x + width - 126, y + 6, bonusText, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: '#9fb0bf' })); - this.trackSideIcon(x + width - 73, y + 16, 'mastery', 17); - const expText = this.trackSideObject(this.add.text(x + width - 56, y + 7, `${state.exp}/${next}`, { + this.trackSideIcon(x + width - 78, y + 18, 'mastery', 18); + const expText = this.trackSideObject(this.add.text(x + width - 58, y + 9, `${state.exp}/${next}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', color: '#9fb0bf' })); expText.setOrigin(1, 0); - const levelText = this.trackSideObject(this.add.text(x + width - 8, y + 4, `Lv ${state.level}`, { + const levelText = this.trackSideObject(this.add.text(x + width - 8, y + 6, `Lv ${state.level}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: isTreasure ? '#f2e3bf' : '#d4dce6', @@ -11270,7 +11270,7 @@ export class BattleScene extends Phaser.Scene { })); levelText.setOrigin(1, 0); - this.drawGauge(x + 84, y + 25, width - 194, 5, state.exp / next, isTreasure ? 0xd8b15f : 0x58aee0); + this.drawGauge(x + 88, y + 27, width - 198, 5, state.exp / next, isTreasure ? 0xd8b15f : 0x58aee0); } private createBattleUiIcon(x: number, y: number, icon: BattleUiIconKey, size = 22) { @@ -11349,16 +11349,19 @@ export class BattleScene extends Phaser.Scene { private usableIcon(usable: BattleUsable): BattleUiIconKey { if (usable.command === 'item') { - return usable.effect === 'heal' ? 'hp' : 'accessory'; + return usable.effect === 'heal' ? 'heal' : 'accessory'; } if (usable.effect === 'heal') { - return 'hp'; + return 'heal'; } if (usable.effect === 'focus') { - return 'leadership'; + return 'focus'; + } + if (usable.id === 'fireTactic') { + return 'fire'; } if (usable.id.toLowerCase().includes('roar')) { - return 'might'; + return 'shout'; } return 'strategy'; } @@ -11407,18 +11410,18 @@ export class BattleScene extends Phaser.Scene { } private renderCompactValueBox(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, value: string) { - const bg = this.trackSideObject(this.add.rectangle(x, y, width, 42, 0x16212d, 0.97)); + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 46, 0x16212d, 0.97)); bg.setOrigin(0); bg.setStrokeStyle(1, 0x647485, 0.72); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 15, y + 21, 26, 26, 0x0a0f14, 0.82)); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 18, y + 23, 32, 32, 0x0a0f14, 0.82)); iconFrame.setStrokeStyle(1, 0x53606c, 0.52); - this.trackSideIcon(x + 15, y + 21, icon, 23); - this.trackSideObject(this.add.text(x + 31, y + 7, label, { + this.trackSideIcon(x + 18, y + 23, icon, 29); + this.trackSideObject(this.add.text(x + 38, y + 7, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf' })); - const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 23, value, { + const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 25, value, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '15px', color: '#f2e3bf', @@ -11428,15 +11431,15 @@ export class BattleScene extends Phaser.Scene { } private renderStatRow(key: keyof UnitStats, label: string, value: number, x: number, y: number, width: number) { - this.trackSideIcon(x + 11, y + 10, this.statIcon(key), 22); - this.trackSideObject(this.add.text(x + 30, y, label, { + this.trackSideIcon(x + 13, y + 10, this.statIcon(key), 26); + this.trackSideObject(this.add.text(x + 36, y, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#d4dce6', fontStyle: '700' })); - this.drawGauge(x + 96, y + 8, width - 150, 10, value / 100, value >= 80 ? 0xd8b15f : 0x58aee0); + this.drawGauge(x + 104, y + 8, width - 158, 10, value / 100, value >= 80 ? 0xd8b15f : 0x58aee0); const valueText = this.trackSideObject(this.add.text(x + width, y - 1, `${value}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',