From 52612b498ccfb4da30a786f4b134de81669c509f Mon Sep 17 00:00:00 2001 From: Wickedness Date: Mon, 29 Jun 2026 13:33:47 +0900 Subject: [PATCH] Add tactical battle UI icons --- scripts/build-battle-ui-icons.py | 283 +++++++++++++ src/assets/images/ui/battle-ui-icons.png | Bin 0 -> 46537 bytes src/game/data/battleUiIcons.ts | 57 +++ src/game/scenes/BattleScene.ts | 514 ++++++++++++++++++++--- 4 files changed, 796 insertions(+), 58 deletions(-) create mode 100644 scripts/build-battle-ui-icons.py create mode 100644 src/assets/images/ui/battle-ui-icons.png create mode 100644 src/game/data/battleUiIcons.ts diff --git a/scripts/build-battle-ui-icons.py b/scripts/build-battle-ui-icons.py new file mode 100644 index 0000000..2d4d9d3 --- /dev/null +++ b/scripts/build-battle-ui-icons.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import math +from pathlib import Path +from typing import Callable + +from PIL import Image, ImageDraw, ImageFilter + + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "src" / "assets" / "images" / "ui" / "battle-ui-icons.png" +FRAME = 48 +SCALE = 4 + + +def main() -> None: + OUT.parent.mkdir(parents=True, exist_ok=True) + icons: list[tuple[str, Callable[[ImageDraw.ImageDraw], None]]] = [ + ("hp", draw_hp), + ("might", draw_might), + ("intelligence", draw_intelligence), + ("leadership", draw_leadership), + ("attack", draw_attack), + ("defense", draw_defense), + ("move", draw_move), + ("mastery", draw_mastery), + ("sword", draw_sword), + ("spear", draw_spear), + ("axe", draw_axe), + ("bow", draw_bow), + ("strategy", draw_strategy), + ("horse", draw_horse), + ("armor", draw_armor), + ("accessory", draw_accessory), + ("hit", draw_hit), + ("critical", draw_critical), + ("terrain", draw_terrain), + ("counter", draw_counter), + ("advantage", draw_advantage), + ("success", draw_success), + ] + cols = 5 + rows = math.ceil(len(icons) / cols) + atlas = Image.new("RGBA", (cols * FRAME, rows * FRAME), (0, 0, 0, 0)) + for index, (_name, draw_icon) in enumerate(icons): + tile = make_icon(draw_icon) + atlas.alpha_composite(tile, ((index % cols) * FRAME, (index // cols) * FRAME)) + atlas.save(OUT, optimize=True) + print(f"Wrote {OUT}") + + +def make_icon(draw_icon: Callable[[ImageDraw.ImageDraw], None]) -> Image.Image: + image = Image.new("RGBA", (FRAME * SCALE, FRAME * SCALE), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image, "RGBA") + draw_icon(draw) + image = image.filter(ImageFilter.UnsharpMask(radius=1.2, percent=90, threshold=3)) + return image.resize((FRAME, FRAME), Image.Resampling.LANCZOS) + + +def c(value: tuple[int, int, int, int] | tuple[int, int, int]) -> tuple[int, int, int, int]: + if len(value) == 3: + return (value[0], value[1], value[2], 255) + return value + + +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") + + +def poly(draw: ImageDraw.ImageDraw, points: list[tuple[int, int]], fill, outline=None) -> None: + scaled = [(x * SCALE, y * SCALE) 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) + + +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) + + +def icon_shadow(draw: ImageDraw.ImageDraw) -> None: + ellipse(draw, (7, 35, 41, 42), (0, 0, 0, 80)) + + +def draw_hp(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(24, 39), (10, 25), (10, 15), (16, 10), (24, 15), (32, 10), (38, 15), (38, 25)], (210, 64, 65), (72, 21, 28)) + poly(draw, [(24, 35), (13, 24), (13, 16), (17, 13), (24, 18), (31, 13), (35, 16), (35, 24)], (255, 130, 111)) + line(draw, [(18, 21), (30, 21)], (255, 235, 188, 240), 2) + line(draw, [(24, 15), (24, 28)], (255, 235, 188, 240), 2) + + +def draw_might(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(13, 35), (31, 17)], (112, 78, 48), 4) + poly(draw, [(33, 10), (39, 15), (27, 25), (23, 21)], (228, 230, 219), (72, 78, 80)) + line(draw, [(35, 36), (17, 18)], (112, 78, 48), 4) + poly(draw, [(15, 11), (10, 18), (22, 23), (25, 18)], (217, 221, 211), (65, 70, 73)) + ellipse(draw, (19, 19, 29, 29), (190, 60, 52), (74, 31, 29)) + line(draw, [(18, 30), (24, 36), (30, 30)], (239, 188, 83), 2) + + +def draw_intelligence(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(13, 13), (25, 9), (38, 13), (38, 34), (25, 38), (13, 34)], (74, 103, 116), (30, 41, 50)) + poly(draw, [(16, 16), (25, 13), (35, 16), (35, 31), (25, 34), (16, 31)], (212, 204, 158)) + line(draw, [(20, 18), (31, 16)], (89, 75, 52, 210), 1) + line(draw, [(20, 23), (31, 21)], (89, 75, 52, 210), 1) + line(draw, [(20, 28), (31, 26)], (89, 75, 52, 210), 1) + ellipse(draw, (9, 26, 17, 36), (145, 95, 64), (67, 37, 31)) + line(draw, [(14, 30), (10, 37)], (231, 206, 150, 230), 2) + + +def draw_leadership(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(15, 39), (15, 9)], (118, 92, 58), 3) + poly(draw, [(17, 11), (38, 15), (31, 23), (38, 31), (17, 27)], (185, 55, 50), (77, 27, 30)) + poly(draw, [(17, 13), (34, 16), (29, 22), (34, 28), (17, 25)], (229, 157, 73)) + ellipse(draw, (11, 35, 20, 43), (208, 176, 88), (73, 55, 31)) + + +def draw_attack(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(13, 34), (33, 14)], (108, 80, 49), 4) + poly(draw, [(35, 9), (39, 13), (25, 28), (21, 24)], (225, 226, 217), (70, 76, 78)) + poly(draw, [(25, 25), (33, 32), (30, 36), (21, 28)], (216, 151, 73), (79, 46, 28)) + line(draw, [(14, 34), (21, 41)], (244, 199, 104), 3) + + +def draw_defense(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(24, 7), (38, 13), (35, 31), (24, 41), (13, 31), (10, 13)], (77, 109, 132), (29, 38, 50)) + poly(draw, [(24, 11), (34, 15), (32, 29), (24, 36), (16, 29), (14, 15)], (113, 154, 183)) + line(draw, [(24, 12), (24, 36)], (225, 206, 138, 230), 2) + line(draw, [(15, 22), (33, 22)], (225, 206, 138, 220), 2) + + +def draw_move(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(15, 29), (24, 31), (36, 35), (35, 40), (22, 40), (12, 35)], (72, 98, 96), (25, 38, 38)) + poly(draw, [(19, 10), (28, 10), (27, 28), (18, 29)], (148, 108, 72), (65, 43, 30)) + poly(draw, [(20, 27), (31, 30), (36, 34), (23, 34), (15, 31)], (213, 167, 94)) + line(draw, [(32, 14), (39, 10)], (220, 204, 144, 170), 2) + line(draw, [(34, 22), (42, 21)], (220, 204, 144, 150), 2) + + +def draw_mastery(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + points = [] + for i in range(10): + radius = 15 if i % 2 == 0 else 6 + angle = -math.pi / 2 + i * math.pi / 5 + points.append((24 + math.cos(angle) * radius, 22 + math.sin(angle) * radius)) + poly(draw, [(round(x), round(y)) for x, y in points], (237, 185, 72), (88, 58, 24)) + rect(draw, (12, 33, 36, 39), (61, 73, 69), (30, 38, 37)) + rect(draw, (14, 35, 27, 37), (129, 206, 120), None) + + +def draw_sword(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(32, 6), (38, 11), (21, 31), (17, 27)], (226, 229, 220), (73, 78, 80)) + poly(draw, [(17, 28), (23, 34), (20, 38), (13, 31)], (201, 135, 67), (72, 44, 31)) + line(draw, [(12, 37), (17, 32)], (235, 205, 112), 3) + line(draw, [(18, 25), (25, 32)], (75, 43, 33), 2) + + +def draw_spear(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(14, 39), (31, 12)], (117, 82, 45), 3) + poly(draw, [(33, 6), (39, 18), (29, 17)], (226, 229, 220), (71, 77, 78)) + poly(draw, [(29, 18), (35, 20), (30, 24)], (187, 61, 51), (75, 31, 30)) + + +def draw_axe(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(17, 39), (30, 10)], (112, 78, 48), 4) + poly(draw, [(27, 8), (42, 13), (39, 25), (28, 21), (24, 15)], (217, 221, 211), (65, 70, 73)) + poly(draw, [(24, 15), (17, 19), (24, 23), (29, 20)], (183, 130, 64), (75, 45, 29)) + + +def draw_bow(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(29, 8), (34, 14), (35, 25), (30, 37)], (184, 121, 62), 4) + line(draw, [(29, 8), (18, 22), (30, 37)], (217, 199, 139, 220), 1) + line(draw, [(15, 24), (35, 24)], (220, 225, 214), 2) + poly(draw, [(36, 24), (30, 20), (30, 28)], (219, 83, 66), (75, 29, 26)) + + +def draw_strategy(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(11, 30), (18, 13), (30, 10), (38, 22), (34, 36), (22, 39)], (223, 221, 202), (78, 77, 65)) + line(draw, [(18, 16), (34, 23)], (107, 121, 129, 180), 1) + line(draw, [(16, 22), (33, 28)], (107, 121, 129, 180), 1) + line(draw, [(14, 29), (30, 34)], (107, 121, 129, 180), 1) + line(draw, [(25, 17), (17, 40)], (124, 84, 49), 2) + + +def draw_horse(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(11, 25), (18, 15), (31, 15), (39, 23), (35, 31), (22, 31)], (150, 114, 80), (60, 43, 33)) + poly(draw, [(30, 15), (40, 9), (42, 17), (36, 21)], (150, 114, 80), (60, 43, 33)) + ellipse(draw, (38, 12, 41, 15), (20, 18, 14)) + line(draw, [(17, 30), (14, 40)], (90, 62, 42), 3) + line(draw, [(29, 30), (33, 40)], (90, 62, 42), 3) + line(draw, [(13, 20), (8, 16)], (92, 57, 35), 3) + + +def draw_armor(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(17, 9), (31, 9), (38, 18), (34, 39), (14, 39), (10, 18)], (79, 98, 116), (28, 38, 48)) + poly(draw, [(20, 12), (28, 12), (34, 20), (31, 35), (17, 35), (14, 20)], (121, 143, 158)) + line(draw, [(24, 13), (24, 36)], (216, 178, 94, 190), 2) + line(draw, [(16, 23), (32, 23)], (43, 55, 66, 200), 1) + line(draw, [(15, 29), (33, 29)], (43, 55, 66, 200), 1) + + +def draw_accessory(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + ellipse(draw, (13, 12, 35, 34), (42, 82, 90), (218, 187, 96), 2) + ellipse(draw, (19, 18, 29, 28), (84, 174, 166), (236, 223, 155), 1) + line(draw, [(24, 34), (24, 40)], (218, 187, 96), 2) + ellipse(draw, (21, 38, 27, 44), (204, 65, 59), (80, 29, 29)) + + +def draw_hit(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + ellipse(draw, (9, 9, 39, 39), (32, 50, 58), (94, 149, 164), 2) + ellipse(draw, (16, 16, 32, 32), (83, 134, 140), (211, 218, 172), 2) + ellipse(draw, (21, 21, 27, 27), (241, 208, 96), (73, 56, 28), 1) + line(draw, [(31, 17), (40, 8)], (225, 229, 219), 2) + + +def draw_critical(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + points = [] + for i in range(14): + radius = 18 if i % 2 == 0 else 7 + angle = -math.pi / 2 + i * math.pi / 7 + points.append((24 + math.cos(angle) * radius, 24 + math.sin(angle) * radius)) + poly(draw, [(round(x), round(y)) for x, y in points], (230, 78, 61), (83, 32, 27)) + line(draw, [(17, 16), (31, 32)], (255, 229, 139), 3) + line(draw, [(31, 16), (17, 32)], (255, 229, 139), 3) + + +def draw_terrain(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + poly(draw, [(7, 35), (17, 20), (24, 32), (32, 14), (42, 35)], (81, 117, 69), (33, 54, 34)) + poly(draw, [(7, 35), (18, 28), (30, 33), (42, 35), (42, 40), (7, 40)], (122, 102, 63), (56, 45, 31)) + line(draw, [(17, 23), (19, 27)], (215, 206, 158), 1) + line(draw, [(32, 17), (34, 22)], (215, 206, 158), 1) + + +def draw_counter(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + line(draw, [(13, 30), (30, 13)], (220, 224, 214), 3) + line(draw, [(35, 18), (18, 35)], (220, 224, 214), 3) + poly(draw, [(31, 9), (39, 12), (33, 18)], (216, 151, 73), (75, 41, 28)) + poly(draw, [(17, 39), (9, 36), (15, 30)], (216, 151, 73), (75, 41, 28)) + + +def draw_advantage(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + ellipse(draw, (9, 9, 39, 39), (32, 50, 58), (94, 149, 164), 2) + poly(draw, [(15, 17), (28, 17), (28, 12), (38, 22), (28, 32), (28, 27), (15, 27)], (229, 157, 73), (76, 48, 30)) + poly(draw, [(33, 31), (20, 31), (20, 36), (10, 26), (20, 16), (20, 21), (33, 21)], (113, 154, 183), (29, 38, 50)) + ellipse(draw, (20, 20, 28, 28), (237, 185, 72), (79, 52, 24)) + + +def draw_success(draw: ImageDraw.ImageDraw) -> None: + icon_shadow(draw) + rect(draw, (11, 11, 37, 35), (76, 103, 91), (29, 43, 39), 2) + poly(draw, [(14, 14), (34, 14), (31, 23), (34, 32), (14, 32), (17, 23)], (184, 145, 83), (77, 55, 31)) + line(draw, [(17, 24), (22, 29), (32, 17)], (225, 245, 202), 3) + ellipse(draw, (18, 36, 30, 43), (180, 63, 58), (73, 27, 27)) + + +if __name__ == "__main__": + main() diff --git a/src/assets/images/ui/battle-ui-icons.png b/src/assets/images/ui/battle-ui-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..c353f0af8ee326956cab8f53732ef306e16da069 GIT binary patch literal 46537 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⁡D$!l6h<8)T5K5roa8vyqPa(p{_d! z4$cIaYTq&3RZAHUj~b4r-Ee3r4z#sXOjMgZ_frcVDe-lt530~Q@_E-~}-yCVV?POb${JDt6u7p+aRj9)4w+zQdkjtvDw;&X7@{vxc2!wXDJg0q4MnKJ zU$hvmTpoY+naTw1^bn&JjyUzz!IBxSMWbFcnk~d6v>dK!O>ba^4%O$=7u(rvv_3vCT2OY7rskJy{CLn*K-S12IFh(R|0;nQ zI*TKJIK;>yhb!$#L&$vfX*ac;!qanPW-Pv`hS7D{ORxH+ek{9IoL8%*EdyGEyGrP0 zJ6$hVA6ddbkse~GZx>8;@lXW1Y+t)wX=3eIx-H}EhmRrW@EeH{DobjuRrkIw&fb;w zyFVL!e3cYEWdE|`0;@(}Fkyet*RSkqE^jF|X&0}wen5AWHAhryyAv=fBIntzV1UIR z7{Kvf6G~}?%_ORCmx+LIDAAhvud5lR>-)U~=vmd$9x)Z|^a9Gk0dK5d2a#(PZjZ-5 zV1iS*er3Sr^iy9G!@`V|;cQ(ow{_&OW{dMV3{JR8#Tvs&rY}Jv1rz`{MNd%6$vnzgwOwn~C z<(;Xqbm*GzhR@gamA~xowoa>+zCE&$V42kiDN~OY;}|Sd>8adn>UAi@0hyF5HD zrQ;>z{%dsqVDTpL=~Jr>o59JW&6d3i(^iBgZbFfa9#YX&MRTkAl1y$FWyb8q)@xh*^qeUU>RD_ycOJ@-29NfI*7F0FJ$Rh8O$qvBKkT)egy zPLu+@iz<8`MN!zW)z+LA97TFV4J6mHsRpl!$>_2U1}(XYT;A9|t~-7GclwXj=ItOU z=saZ7a;bqm{OwE;h5SowK(efYwLJzV!9h(TVsv9O(>g~7Yx^QPbkNGHpe}>gtO9D_ z`_msVHC!C&7Oy<*Ov;ude!W){^>ux6o+dfQWDjpK&il^@=)asA-4xsEPIA}btW#tD zAf?r@af~*HzBIwxDQQnq3LOvL51y(f)#~(#lM=NY*P)U2H8G1IRZVnlxNne_l3h)b z=aB?z+GO1b$5~W0Bn>G=n03dw?sp-R=X1H`?lSiwTYU9L?Z(U6_y-JeuK~du`70|A zX+=x+&iilO1NwZ{pUdJp^I;G}@-cm%lOJqs!2kiD$zQsPPTG1(u%xRY76z@_N+CN6 zVNhh+cZ^bgUPO?$l8SU_l8V&n=g^PGcb2R<`XDN~$H|YZa3z0nEI&^Pul9sL0yd`# zlq9Z&PF9%q+8CMiJaGRVYyrX zowb4H2b_jhl`qM_gh==o$gDYk(e!?;7~>d;glfP7s81`q3Qu7}n~ zB7SPNu+_gsc{DOgz%MjU4`kRdLnO20-eP}6Zxu93Tmfhi zxP(GbzyBAnF8XC~9w_9r6?B;&`x!<7OhEΞqRkR)+abr-_I>3PKI#e*7+!r9wyS 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 void) { + if (scene.textures.exists(battleUiIconTextureKey)) { + onReady(); + return; + } + + scene.load.once('complete', onReady); + scene.load.spritesheet(battleUiIconTextureKey, battleUiIconsUrl, { + frameWidth: battleUiIconFrameSize, + frameHeight: battleUiIconFrameSize + }); + scene.load.start(); +} diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index d2b4c89..5cf3e99 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -11,12 +11,19 @@ import { unitTextureVariantKeys as unitAssetTextureVariantKeys, type UnitDirection } from '../data/unitAssets'; +import { + battleUiIconFrames, + battleUiIconTextureKey, + loadBattleUiIcons, + type BattleUiIconKey +} from '../data/battleUiIcons'; import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, - type EquipmentSlot + type EquipmentSlot, + type ItemDefinition } from '../data/battleItems'; import { campaignSaveSlotCount, @@ -1194,6 +1201,7 @@ type EquipmentGrowthResult = { type LevelUpStatGain = { label: string; amount: number; + icon: BattleUiIconKey; }; type CharacterGrowthResult = { @@ -1285,6 +1293,7 @@ type CombatPreview = { criticalRate: number; counterAvailable: boolean; terrainLabel: string; + matchupLabel: string; }; type CombatResult = CombatPreview & { @@ -2992,7 +3001,7 @@ export class BattleScene extends Phaser.Scene { } private ensureScenarioAssets(onReady: () => void) { - this.ensureScenarioMapTexture(() => this.ensureScenarioUnitTextures(onReady)); + this.ensureScenarioMapTexture(() => loadBattleUiIcons(this, () => this.ensureScenarioUnitTextures(onReady))); } private ensureScenarioMapTexture(onReady: () => void) { @@ -4453,15 +4462,8 @@ export class BattleScene extends Phaser.Scene { } private renderAttackPreview(preview: CombatPreview) { - const counter = preview.counterAvailable ? ' / 반격 가능' : ''; - const bond = preview.bondDamageBonus > 0 && preview.bondLabel ? `\n공명 ${preview.bondLabel} 피해 +${preview.bondDamageBonus}%` : ''; - const equipment = preview.equipmentEffectLabels.length > 0 - ? `\n보물 ${preview.equipmentEffectLabels.join(', ')} (${this.equipmentPreviewBonusText(preview)})` - : ''; - this.renderUnitDetail( - preview.defender, - `${preview.attacker.name} ${commandLabels[preview.action]} 예측\n피해 ${preview.damage} / 명중 ${preview.hitRate}% / 치명 ${preview.criticalRate}%\n지형 ${preview.terrainLabel}${counter}${bond}${equipment}` - ); + this.renderUnitDetail(preview.defender); + this.renderCombatPreviewCard(preview); } private renderSupportPreview(user: UnitData, target: UnitData, usable: BattleUsable) { @@ -4487,6 +4489,101 @@ export class BattleScene extends Phaser.Scene { return parts.join(' / ') || '보정 없음'; } + private renderCombatPreviewCard(preview: CombatPreview) { + const { panelX, panelWidth } = this.layout; + const left = panelX + 24; + const width = panelWidth - 48; + const height = 124; + const y = Math.min(this.sideContentTop() + 430, this.sideContentBottom(10) - height); + + const bg = this.trackSideObject(this.add.rectangle(left, y, width, height, 0x121a22, 0.96)); + bg.setOrigin(0); + bg.setStrokeStyle(2, palette.gold, 0.72); + + this.trackSideIcon(left + 22, y + 25, this.actionIcon(preview), 32); + this.trackSideObject(this.add.text(left + 44, y + 11, `${preview.attacker.name} ${commandLabels[preview.action]} 예측`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '16px', + color: '#f4dfad', + fontStyle: '700' + })); + + const attackerWeapon = getItem(preview.attacker.equipment.weapon.itemId); + const defenderArmor = getItem(preview.defender.equipment.armor.itemId); + this.renderPreviewEquipmentChip(left + 44, y + 38, 126, this.itemIcon(attackerWeapon, 'weapon'), attackerWeapon.name); + this.renderPreviewEquipmentChip(left + 178, y + 38, 126, this.itemIcon(defenderArmor, 'armor'), defenderArmor.name); + + const metricTop = y + 70; + const metricWidth = (width - 24) / 3; + this.renderPreviewMetric(left + 8, metricTop, metricWidth, 'attack', '피해', `${preview.damage}`); + this.renderPreviewMetric(left + 8 + metricWidth, metricTop, metricWidth, preview.action === 'strategy' ? 'success' : 'hit', preview.action === 'strategy' ? '성공' : '명중', `${preview.hitRate}%`); + this.renderPreviewMetric(left + 8 + metricWidth * 2, metricTop, metricWidth, 'critical', '치명', `${preview.criticalRate}%`); + + const badgeY = y + 101; + const maxBadgeRight = left + width - 10; + let badgeX = left + 10; + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'advantage', preview.matchupLabel, maxBadgeRight); + if (preview.action === 'strategy') { + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'success', `${preview.hitRate}% 성공`, maxBadgeRight); + } + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'terrain', preview.terrainLabel, maxBadgeRight); + if (preview.counterAvailable) { + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'counter', '반격 가능', maxBadgeRight); + } + if (preview.bondDamageBonus > 0 && preview.bondLabel) { + badgeX = this.renderPreviewBadge(badgeX, badgeY, 'leadership', `공명 +${preview.bondDamageBonus}%`, maxBadgeRight); + } + if (preview.equipmentEffectLabels.length > 0) { + this.renderPreviewBadge(badgeX, badgeY, 'mastery', '장비 보정', maxBadgeRight); + } + } + + private renderPreviewEquipmentChip(x: number, y: number, width: number, icon: BattleUiIconKey, label: string) { + const chip = this.trackSideObject(this.add.rectangle(x, y, width, 22, 0x0b1118, 0.86)); + chip.setOrigin(0); + chip.setStrokeStyle(1, 0x53606c, 0.42); + this.trackSideIcon(x + 12, y + 11, icon, 18); + this.trackSideObject(this.add.text(x + 25, y + 4, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#d4dce6', + wordWrap: { width: width - 32, useAdvancedWrap: true } + })); + } + + private renderPreviewMetric(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, value: string) { + this.trackSideIcon(x + 13, y + 11, icon, 18); + this.trackSideObject(this.add.text(x + 26, y + 2, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#9fb0bf' + })); + const valueText = this.trackSideObject(this.add.text(x + width - 8, y + 1, value, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: '#f2e3bf', + fontStyle: '700' + })); + valueText.setOrigin(1, 0); + } + + private renderPreviewBadge(x: number, y: number, icon: BattleUiIconKey, label: string, maxRight?: number) { + const width = Phaser.Math.Clamp(label.length * 8 + 34, 58, 120); + if (maxRight && x + width > maxRight) { + return x; + } + const badge = this.trackSideObject(this.add.rectangle(x, y, width, 20, 0x0b1118, 0.88)); + badge.setOrigin(0); + badge.setStrokeStyle(1, 0x53606c, 0.44); + this.trackSideIcon(x + 11, y + 10, icon, 15); + this.trackSideObject(this.add.text(x + 22, y + 4, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#d4dce6' + })); + return x + width + 6; + } + private finishUnitAction(unit: UnitData, message: string) { this.actedUnitIds.add(unit.id); this.phase = 'idle'; @@ -5104,7 +5201,7 @@ export class BattleScene extends Phaser.Scene { exp: unit.exp, next: 100, leveled: unit.level > previousLevel, - statGains: this.characterLevelUpStatGains(unit.level - previousLevel), + statGains: this.characterLevelUpStatGains(unit, unit.level - previousLevel), flashArea: { x, y, width, height: 68, depth: depth + 3 } }); } @@ -5397,7 +5494,7 @@ export class BattleScene extends Phaser.Scene { color: '#f4dfad', fontStyle: '700' }); - const statText = this.add.text(-width / 2 + 110, 26, stats ?? '숙련 효과 강화', { + const statText = this.add.text(-width / 2 + 110, 22, stats ? '능력 상승' : '숙련 효과 강화', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: stats ? '#a8ffd0' : '#d8b15f', @@ -5406,6 +5503,9 @@ export class BattleScene extends Phaser.Scene { strokeThickness: 3 }); container.add([bg, burst, title, levelText, statText]); + if (animation.statGains) { + this.addStatGainChipsToContainer(container, animation.statGains, -width / 2 + 110, 46, width - 124); + } if (animation.unit) { const sprite = this.add.sprite(-width / 2 + 56, 10, this.unitActionTexture(animation.unit), this.unitActionFrameIndex('south', this.celebrationPose(animation.unit))); @@ -5428,6 +5528,40 @@ export class BattleScene extends Phaser.Scene { await this.delay(260); } + private addStatGainChipsToContainer( + container: Phaser.GameObjects.Container, + gains: LevelUpStatGain[], + x: number, + y: number, + maxWidth: number + ) { + 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); + if (cursorX + chipWidth > x + maxWidth) { + return; + } + const bg = this.add.rectangle(cursorX, y, chipWidth, 24, 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}`, { + 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, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '11px', + color: '#d4dce6' + }); + container.add([bg, icon, text, label]); + cursorX += chipWidth + 6; + }); + } + private createResultLevelUpBurst(x: number, y: number, depth: number) { const ring = this.trackResultObject(this.add.circle(x, y + 2, 33)); ring.setStrokeStyle(3, 0xffdf7b, 0.84); @@ -5617,7 +5751,8 @@ export class BattleScene extends Phaser.Scene { hitRate, criticalRate, counterAvailable: this.canCounterAttack(defender, attacker, action), - terrainLabel: terrainRule.label + terrainLabel: terrainRule.label, + matchupLabel: this.combatMatchupLabel(attacker, defender, action) }; } @@ -5886,6 +6021,43 @@ export class BattleScene extends Phaser.Scene { return this.equipmentDefenseBonus(unit) + Math.floor(unit.stats.leadership / 18) + Math.floor(terrainDefenseBonus / 3); } + private combatMatchupLabel(attacker: UnitData, defender: UnitData, action: DamageCommand) { + if (action === 'strategy') { + const gap = attacker.stats.intelligence - defender.stats.intelligence; + return gap >= 10 ? '책략 우위' : gap <= -10 ? '책략 불리' : '책략 균형'; + } + + if (action === 'item') { + const gap = attacker.stats.luck - defender.stats.luck; + return gap >= 12 ? '도구 우위' : gap <= -12 ? '도구 불리' : '도구 균형'; + } + + if (this.hasClassAdvantage(attacker, defender)) { + return '상성 우위'; + } + if (this.hasClassAdvantage(defender, attacker)) { + return '상성 불리'; + } + return '상성 보통'; + } + + private hasClassAdvantage(attacker: UnitData, defender: UnitData) { + const distance = this.tileDistance(attacker, defender); + if (attacker.classKey === 'spearman' && defender.classKey === 'cavalry') { + return true; + } + if (attacker.classKey === 'cavalry' && (defender.classKey === 'archer' || defender.classKey === 'strategist' || defender.classKey === 'quartermaster')) { + return true; + } + if (attacker.classKey === 'archer' && distance > 1 && (defender.classKey === 'infantry' || defender.classKey === 'spearman' || defender.classKey === 'yellowTurban')) { + return true; + } + if ((attacker.classKey === 'strategist' || attacker.classKey === 'quartermaster') && defender.stats.intelligence + 12 <= attacker.stats.intelligence) { + return true; + } + return false; + } + private attackerEquipmentExpGain(unit: UnitData, action: DamageCommand) { if (action === 'attack') { return this.weaponExpGain(unit); @@ -7163,6 +7335,7 @@ export class BattleScene extends Phaser.Scene { })); title.setOrigin(0.5, 0); title.setDepth(depth + 8); + this.renderCombatActionRibbon(result, left + panelWidth / 2 - 220, top + 54, 440, depth + 9); const attackerDirection = 'east'; const defenderDirection = 'west'; @@ -7249,6 +7422,68 @@ export class BattleScene extends Phaser.Scene { this.hideCombatCutIn(); } + private renderCombatActionRibbon(result: CombatResult, x: number, y: number, width: number, depth: number) { + const bg = this.trackCombatObject(this.add.rectangle(x, y, width, 58, 0x0b1118, 0.88)); + bg.setOrigin(0); + bg.setDepth(depth); + bg.setStrokeStyle(1, palette.gold, 0.58); + + this.trackCombatIcon(x + 22, y + 19, this.actionIcon(result), 28, depth + 1); + const attackerWeapon = getItem(result.attacker.equipment.weapon.itemId); + const defenderArmor = getItem(result.defender.equipment.armor.itemId); + this.trackCombatIcon(x + 132, y + 19, this.itemIcon(attackerWeapon, 'weapon'), 22, depth + 1); + this.trackCombatIcon(x + 248, y + 19, this.itemIcon(defenderArmor, 'armor'), 22, depth + 1); + + const actionLabel = this.trackCombatObject(this.add.text(x + 42, y + 8, result.usable?.name ?? commandLabels[result.action], { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: '#f4dfad', + fontStyle: '700', + fixedWidth: 74 + })); + actionLabel.setDepth(depth + 1); + + const weaponLabel = this.trackCombatObject(this.add.text(x + 148, y + 9, attackerWeapon.name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#d4dce6', + fixedWidth: 82 + })); + weaponLabel.setDepth(depth + 1); + + const armorLabel = this.trackCombatObject(this.add.text(x + 264, y + 9, defenderArmor.name, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#d4dce6', + fixedWidth: 74 + })); + armorLabel.setDepth(depth + 1); + + const outcomeIcon = result.action === 'strategy' ? 'success' : result.critical ? 'critical' : 'hit'; + const outcomeLabel = result.action === 'strategy' ? (result.hit ? '성공' : '실패') : result.hit ? (result.critical ? '치명' : '명중') : '회피'; + this.renderCombatRibbonBadge(x + 42, y + 33, 'advantage', result.matchupLabel, depth + 1, 74); + this.renderCombatRibbonBadge(x + 122, y + 33, outcomeIcon, outcomeLabel, depth + 1, 58); + if (result.counter) { + this.renderCombatRibbonBadge(x + 186, y + 33, 'counter', '반격', depth + 1, 54); + } + } + + private renderCombatRibbonBadge(x: number, y: number, icon: BattleUiIconKey, label: string, depth: number, width = 52) { + const badge = this.trackCombatObject(this.add.rectangle(x, y, width, 20, 0x101820, 0.92)); + badge.setOrigin(0); + badge.setDepth(depth); + badge.setStrokeStyle(1, 0x53606c, 0.48); + this.trackCombatIcon(x + 11, y + 10, icon, 15, depth + 1); + const text = this.trackCombatObject(this.add.text(x + 22, y + 4, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#f2e3bf', + fontStyle: '700', + fixedWidth: width - 26 + })); + text.setDepth(depth + 1); + } + private applyCombatSpriteDisplaySize(sprite: Phaser.GameObjects.Sprite, unit: UnitData, compact = false) { const size = this.combatSpriteDisplaySize(unit, compact); sprite.setDisplaySize(size, size); @@ -7386,17 +7621,18 @@ export class BattleScene extends Phaser.Scene { })); nameText.setDepth(85); - const hpLabel = this.trackCombatObject(this.add.text(x + 78, y + 45, '병력', { + this.trackCombatIcon(x + 88, y + 54, 'hp', 18, 85); + const hpLabel = this.trackCombatObject(this.add.text(x + 104, y + 45, '병력', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#9fb0bf', fontStyle: '700' })); hpLabel.setDepth(85); - const hpTrack = this.trackCombatObject(this.add.rectangle(x + 126, y + 54, width - 210, 9, 0x0a0f14, 0.9)); + const hpTrack = this.trackCombatObject(this.add.rectangle(x + 148, y + 54, width - 232, 9, 0x0a0f14, 0.9)); hpTrack.setOrigin(0, 0.5); hpTrack.setDepth(85); - const hpFill = this.trackCombatObject(this.add.rectangle(x + 126, y + 54, width - 210, 7, 0x59d18c, 0.96)); + const hpFill = this.trackCombatObject(this.add.rectangle(x + 148, y + 54, width - 232, 7, 0x59d18c, 0.96)); hpFill.setOrigin(0, 0.5); hpFill.setDepth(86); hpFill.setScale(unit.hp / unit.maxHp, 1); @@ -7409,17 +7645,18 @@ export class BattleScene extends Phaser.Scene { hpText.setOrigin(1, 0); hpText.setDepth(85); - const expLabel = this.trackCombatObject(this.add.text(x + 78, y + 75, '경험', { + this.trackCombatIcon(x + 88, y + 84, 'mastery', 18, 85); + const expLabel = this.trackCombatObject(this.add.text(x + 104, y + 75, '경험', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#9fb0bf', fontStyle: '700' })); expLabel.setDepth(85); - const expTrack = this.trackCombatObject(this.add.rectangle(x + 126, y + 84, width - 210, 9, 0x0a0f14, 0.9)); + const expTrack = this.trackCombatObject(this.add.rectangle(x + 148, y + 84, width - 232, 9, 0x0a0f14, 0.9)); expTrack.setOrigin(0, 0.5); expTrack.setDepth(85); - const expFill = this.trackCombatObject(this.add.rectangle(x + 126, y + 84, width - 210, 7, 0xd8b15f, 0.96)); + const expFill = this.trackCombatObject(this.add.rectangle(x + 148, y + 84, width - 232, 7, 0xd8b15f, 0.96)); expFill.setOrigin(0, 0.5); expFill.setDepth(86); expFill.setScale((growth?.previousExp ?? unit.exp) / 100, 1); @@ -7666,7 +7903,7 @@ export class BattleScene extends Phaser.Scene { private async playCombatLevelUpSpotlight(entry: GrowthGaugeEntry, celebrant: UnitData | undefined, x: number, y: number, width: number, depth: number) { const panelWidth = Math.min(540, width - 34); - const panelHeight = 104; + const panelHeight = 116; const container = this.trackCombatObject(this.add.container(x, y)); container.setDepth(depth); container.setAlpha(0); @@ -7691,7 +7928,7 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' }); levelText.setOrigin(celebrant ? 0 : 0.5, 0); - const statText = this.add.text(celebrant ? -panelWidth / 2 + 112 : 0, 25, this.statGainLine(entry.statGains) ?? `${entry.label} 효과 강화`, { + const statText = this.add.text(celebrant ? -panelWidth / 2 + 112 : 0, 25, entry.statGains ? '능력 상승' : `${entry.label} 효과 강화`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#a8ffd0', @@ -7701,6 +7938,9 @@ export class BattleScene extends Phaser.Scene { }); statText.setOrigin(celebrant ? 0 : 0.5, 0); container.add([bg, glow, title, levelText, statText]); + if (entry.statGains) { + this.addStatGainChipsToContainer(container, entry.statGains, celebrant ? -panelWidth / 2 + 112 : -panelWidth / 2 + 40, 51, panelWidth - 140); + } if (celebrant) { const sprite = this.add.sprite(-panelWidth / 2 + 56, 6, this.unitActionTexture(celebrant), this.unitActionFrameIndex('south', this.celebrationPose(celebrant))); @@ -7725,7 +7965,8 @@ export class BattleScene extends Phaser.Scene { } private renderGrowthGaugeRow(entry: GrowthGaugeEntry, x: number, y: number, width: number, depth: number): GrowthGaugeView { - const label = this.trackCombatObject(this.add.text(x, y - 3, `${entry.owner} ${entry.label}`, { + 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}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: '#d4dce6', @@ -7804,6 +8045,22 @@ export class BattleScene extends Phaser.Scene { return stats ? `Lv ${entry.level} 달성! ${stats}` : `Lv ${entry.level} 달성!`; } + private growthEntryIcon(entry: GrowthGaugeEntry): BattleUiIconKey { + if (entry.kind === 'character') { + return entry.statGains?.[0]?.icon ?? 'hp'; + } + if (entry.kind === 'bond') { + return 'leadership'; + } + if (entry.slot === 'armor') { + return 'armor'; + } + if (entry.slot === 'accessory') { + return 'accessory'; + } + return 'sword'; + } + private showGrowthEventText(text: Phaser.GameObjects.Text, label: string) { text.setText(label); text.setAlpha(1); @@ -8874,13 +9131,11 @@ export class BattleScene extends Phaser.Scene { while (unit.level < maxCharacterLevel && exp >= 100) { exp -= 100; unit.level += 1; - unit.maxHp += 2; - unit.hp = Math.min(unit.maxHp, unit.hp + 2); - unit.attack += 1; leveled = true; levelUps += 1; } + const statGains = this.applyCharacterLevelUpStats(unit, levelUps); unit.exp = unit.level >= maxCharacterLevel ? Math.min(exp, 100) : exp; return { @@ -8892,21 +9147,90 @@ export class BattleScene extends Phaser.Scene { next: 100, leveled, levelUps, - statGains: this.characterLevelUpStatGains(levelUps) + statGains }; } - private characterLevelUpStatGains(levelUps: number): LevelUpStatGain[] { + private applyCharacterLevelUpStats(unit: UnitData, levelUps: number) { + const gains = this.characterLevelUpStatGains(unit, levelUps); + gains.forEach((gain) => { + switch (gain.icon) { + case 'hp': + unit.maxHp += gain.amount; + unit.hp = Math.min(unit.maxHp, unit.hp + gain.amount); + return; + case 'attack': + unit.attack += gain.amount; + return; + case 'might': + unit.stats.might = Math.min(255, unit.stats.might + gain.amount); + return; + case 'intelligence': + unit.stats.intelligence = Math.min(255, unit.stats.intelligence + gain.amount); + return; + case 'leadership': + unit.stats.leadership = Math.min(255, unit.stats.leadership + gain.amount); + return; + case 'move': + unit.stats.agility = Math.min(255, unit.stats.agility + gain.amount); + return; + default: + return; + } + }); + return gains; + } + + private characterLevelUpStatGains(unit: UnitData, levelUps: number): LevelUpStatGain[] { if (levelUps <= 0) { return []; } + const profile = this.levelUpStatProfile(unit.classKey); return [ - { label: '병력', amount: levelUps * 2 }, - { label: '공격', amount: levelUps } + { label: '병력', amount: levelUps * 2, icon: 'hp' }, + { label: '공격', amount: levelUps, icon: 'attack' }, + ...profile.map((icon) => ({ + label: this.levelUpStatLabel(icon), + amount: levelUps, + icon + })) ]; } + private levelUpStatProfile(classKey: UnitClassKey): BattleUiIconKey[] { + switch (classKey) { + case 'strategist': + case 'quartermaster': + return ['intelligence', 'leadership']; + case 'archer': + return ['might', 'intelligence']; + case 'cavalry': + return ['might', 'leadership', 'move']; + case 'lord': + case 'rebelLeader': + case 'yellowTurban': + return ['might', 'intelligence', 'leadership']; + default: + return ['might', 'leadership']; + } + } + + private levelUpStatLabel(icon: BattleUiIconKey) { + switch (icon) { + case 'might': + return '무력'; + case 'intelligence': + return '지력'; + case 'leadership': + return '통솔'; + case 'move': + return '민첩'; + default: + return '능력'; + } + } + private formatEquipmentGrowth(result: EquipmentGrowthResult) { const levelUp = result.leveled ? ` / Lv ${result.level} 달성` : ''; return `${equipmentSlotLabels[result.slot]} ${result.itemName} 경험치 +${result.amount} (${result.exp}/${result.next})${levelUp}`; @@ -9782,7 +10106,8 @@ export class BattleScene extends Phaser.Scene { } private sideContentBottom(padding = 16) { - return (this.miniMapLayout?.y ?? this.layout.panelY + this.layout.panelHeight - 140) - padding; + const panelBottom = this.layout.panelY + this.layout.panelHeight - padding; + return this.miniMapVisible && this.miniMapLayout ? this.miniMapLayout.y - padding : panelBottom; } private renderBondPanel(message?: string) { @@ -10230,13 +10555,16 @@ export class BattleScene extends Phaser.Scene { header.setOrigin(0); header.setStrokeStyle(1, factionColor, 0.76); - this.trackSideObject(this.add.text(left + 14, top + 9, `${rosterLabels[unit.faction]} / ${unitClass.family}`, { + const classIcon = this.trackSideIcon(left + 23, top + 31, this.unitMoveIcon(unit), 30); + classIcon.setAlpha(acted ? 0.55 : 0.94); + const headerTextX = left + 48; + this.trackSideObject(this.add.text(headerTextX, top + 9, `${rosterLabels[unit.faction]} / ${unitClass.family}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: unit.faction === 'ally' ? '#9fd0ff' : '#ffb2a4', fontStyle: '700' })); - this.trackSideObject(this.add.text(left + 14, top + 29, `${unit.name} ${unitClass.name}`, { + this.trackSideObject(this.add.text(headerTextX, top + 29, `${unit.name} ${unitClass.name}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '22px', color: acted ? '#bdbdbd' : '#f2e3bf', @@ -10266,7 +10594,8 @@ export class BattleScene extends Phaser.Scene { backText.setOrigin(0.5); } - this.trackSideObject(this.add.text(left, top + 78, '병력', { + this.trackSideIcon(left + 10, top + 88, 'hp', 20); + this.trackSideObject(this.add.text(left + 26, top + 78, '병력', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '17px', color: '#d4dce6', @@ -10279,43 +10608,46 @@ export class BattleScene extends Phaser.Scene { fontStyle: '700' })); hpValue.setOrigin(1, 0); - this.drawGauge(left + 76, top + 84, width - 160, 10, unit.hp / unit.maxHp, 0x59d18c); + this.drawGauge(left + 92, top + 84, width - 176, 10, unit.hp / unit.maxHp, 0x59d18c); const attackBonus = this.equipmentAttackBonus(unit); const summaryGap = 6; const summaryBoxWidth = (width - summaryGap * 2) / 3; - this.renderCompactValueBox(left, top + 112, summaryBoxWidth, '공격', `${unit.attack + attackBonus}`); - this.renderCompactValueBox(left + summaryBoxWidth + summaryGap, top + 112, summaryBoxWidth, '방어+', `${this.equipmentDefenseBonus(unit)}`); + this.renderCompactValueBox(left, top + 112, summaryBoxWidth, 'attack', '공격', `${unit.attack + attackBonus}`); + this.renderCompactValueBox(left + summaryBoxWidth + summaryGap, top + 112, summaryBoxWidth, 'defense', '방어+', `${this.equipmentDefenseBonus(unit)}`); this.renderCompactValueBox( left + (summaryBoxWidth + summaryGap) * 2, top + 112, summaryBoxWidth, + this.unitMoveIcon(unit), '방향', `${this.directionSymbol(direction)} ${this.unitDirectionLabel(direction)}` ); const statTop = top + 158; statLabels.forEach((stat, index) => { - this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 28, width); + this.renderStatRow(stat.key, stat.label, unit.stats[stat.key], left, statTop + index * 28, width); }); const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}% / 이동 ${unit.move}${acted ? ' / 행동완료' : ''}`; - this.trackSideObject(this.add.text(left, top + 306, positionText, { + this.trackSideIcon(left + 9, top + 315, acted ? 'counter' : this.unitMoveIcon(unit), 18); + this.trackSideObject(this.add.text(left + 24, top + 306, positionText, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', color: acted ? '#bdbdbd' : '#9fb0bf', - wordWrap: { width, useAdvancedWrap: true } + wordWrap: { width: width - 24, useAdvancedWrap: true } })); this.renderEquipmentSummary(unit, left, top + 330, width); if (message) { - this.renderPanelMessage(message, left, top + 444, width, 66); + this.renderPanelMessage(message, left, top + 460, width, 58); } this.showFacingIndicator(unit); } private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) { - this.trackSideObject(this.add.text(x, y, '장비 / 숙련도', { + this.trackSideIcon(x + 9, y + 10, 'mastery', 18); + this.trackSideObject(this.add.text(x + 24, y, '장비 / 숙련도', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#f2e3bf', @@ -10323,7 +10655,7 @@ export class BattleScene extends Phaser.Scene { })); equipmentSlots.forEach((slot, index) => { - this.renderEquipmentRow(unit, slot, x, y + 24 + index * 28, width); + this.renderEquipmentRow(unit, slot, x, y + 24 + index * 31, width); }); } @@ -10332,23 +10664,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, 26, isTreasure ? 0x1f2430 : 0x101820, isTreasure ? 0.94 : 0.86)); + const bg = this.trackSideObject(this.add.rectangle(x, y, width, 29, isTreasure ? 0x1f2430 : 0x101820, isTreasure ? 0.94 : 0.86)); bg.setOrigin(0); bg.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.58 : 0.42); - const iconFrame = this.trackSideObject(this.add.rectangle(x + 14, y + 13, 22, 22, 0x0a0f14, 0.82)); + const iconFrame = this.trackSideObject(this.add.rectangle(x + 15, y + 14.5, 24, 24, 0x0a0f14, 0.82)); iconFrame.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.72 : 0.48); - const icon = this.trackSideObject(this.add.image(x + 14, y + 13, this.itemIconKey(item.id))); - icon.setDisplaySize(22, 22); + this.trackSideIcon(x + 15, y + 14.5, this.itemIcon(item, slot), 22); - this.trackSideObject(this.add.text(x + 31, y + 6, equipmentSlotLabels[slot], { + this.trackSideObject(this.add.text(x + 33, y + 4, equipmentSlotLabels[slot], { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf', fontStyle: '700' })); - this.trackSideObject(this.add.text(x + 78, y + 4, item.name, { + this.trackSideObject(this.add.text(x + 78, y + 3, item.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px', color: isTreasure ? '#f4dfad' : '#d4dce6', @@ -10362,6 +10693,7 @@ export class BattleScene extends Phaser.Scene { color: '#9fb0bf' })); + this.trackSideIcon(x + width - 69, y + 14, 'mastery', 15); const expText = this.trackSideObject(this.add.text(x + width - 54, y + 6, `${state.exp}/${next}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', @@ -10377,12 +10709,76 @@ export class BattleScene extends Phaser.Scene { })); levelText.setOrigin(1, 0); - this.drawGauge(x + 78, y + 20, width - 178, 4, state.exp / next, isTreasure ? 0xd8b15f : 0x58aee0); + this.drawGauge(x + 78, y + 22, width - 178, 4, state.exp / next, isTreasure ? 0xd8b15f : 0x58aee0); } - private itemIconKey(itemId: string) { - const key = `item-${itemId}`; - return this.textures.exists(key) ? key : 'item-training-sword'; + private createBattleUiIcon(x: number, y: number, icon: BattleUiIconKey, size = 22) { + const image = this.add.image(x, y, battleUiIconTextureKey, battleUiIconFrames[icon]); + image.setDisplaySize(size, size); + return image; + } + + private trackSideIcon(x: number, y: number, icon: BattleUiIconKey, size = 22) { + return this.trackSideObject(this.createBattleUiIcon(x, y, icon, size)); + } + + private trackCombatIcon(x: number, y: number, icon: BattleUiIconKey, size = 22, depth = 85) { + const image = this.trackCombatObject(this.createBattleUiIcon(x, y, icon, size)); + image.setDepth(depth); + return image; + } + + private itemIcon(item: ItemDefinition, slot: EquipmentSlot = item.slot): BattleUiIconKey { + if (slot === 'armor') { + return 'armor'; + } + if (slot === 'accessory') { + return item.id.includes('quiver') ? 'bow' : 'accessory'; + } + if (item.id.includes('bow') || item.id.includes('quiver')) { + return 'bow'; + } + if (item.id.includes('spear') || item.id.includes('glaive') || item.id.includes('halberd') || item.id.includes('piercer')) { + return 'spear'; + } + if (item.id.includes('axe')) { + return 'axe'; + } + if (item.id.includes('fan') || item.id.includes('manual') || item.id.includes('scroll') || (item.strategyBonus ?? 0) > (item.attackBonus ?? 0)) { + return 'strategy'; + } + return 'sword'; + } + + private unitMoveIcon(unit: UnitData): BattleUiIconKey { + return unit.classKey === 'cavalry' ? 'horse' : 'move'; + } + + private statIcon(key: keyof UnitStats): BattleUiIconKey { + switch (key) { + case 'might': + return 'might'; + case 'intelligence': + return 'intelligence'; + case 'leadership': + return 'leadership'; + case 'agility': + return 'move'; + case 'luck': + return 'mastery'; + default: + return 'might'; + } + } + + private actionIcon(preview: CombatPreview): BattleUiIconKey { + if (preview.action === 'attack') { + return this.itemIcon(getItem(preview.attacker.equipment.weapon.itemId), 'weapon'); + } + if (preview.action === 'strategy') { + return 'strategy'; + } + return preview.usable ? this.itemIcon(getItem(preview.attacker.equipment.accessory.itemId), 'accessory') : 'accessory'; } private itemBonusText(item: ReturnType) { @@ -10418,11 +10814,12 @@ export class BattleScene extends Phaser.Scene { }, 0); } - private renderCompactValueBox(x: number, y: number, width: number, label: string, value: string) { + private renderCompactValueBox(x: number, y: number, width: number, icon: BattleUiIconKey, label: string, value: string) { const bg = this.trackSideObject(this.add.rectangle(x, y, width, 38, 0x16212d, 0.92)); bg.setOrigin(0); bg.setStrokeStyle(1, 0x53606c, 0.56); - this.trackSideObject(this.add.text(x + 8, y + 6, label, { + this.trackSideIcon(x + 13, y + 17, icon, 18); + this.trackSideObject(this.add.text(x + 25, y + 6, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', color: '#9fb0bf' @@ -10436,15 +10833,16 @@ export class BattleScene extends Phaser.Scene { valueText.setOrigin(1, 0.5); } - private renderStatRow(label: string, value: number, x: number, y: number, width: number) { - this.trackSideObject(this.add.text(x, y, label, { + private renderStatRow(key: keyof UnitStats, label: string, value: number, x: number, y: number, width: number) { + this.trackSideIcon(x + 9, y + 10, this.statIcon(key), 18); + this.trackSideObject(this.add.text(x + 24, y, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '16px', color: '#d4dce6', fontStyle: '700' })); - this.drawGauge(x + 76, y + 8, width - 130, 9, value / 100, value >= 80 ? 0xd8b15f : 0x58aee0); + this.drawGauge(x + 92, y + 8, width - 146, 9, 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',