Enlarge battle UI icons

This commit is contained in:
2026-07-02 21:35:48 +09:00
parent e5706dc71f
commit 79e535c2bb
4 changed files with 172 additions and 109 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -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;

View File

@@ -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',