Files
heros_web/scripts/build-battle-ui-icons.py

568 lines
26 KiB
Python

from __future__ import annotations
import math
from pathlib import Path
from typing import Callable
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "src" / "assets" / "images" / "ui" / "battle-ui-icons.png"
MICRO_OUT = ROOT / "src" / "assets" / "images" / "ui" / "battle-ui-icons-micro.png"
PROOF_OUT = ROOT / "docs" / "battle-ui-icons-v2-action-item-contact.png"
SEED_ATLAS = ROOT / "docs" / "battle-ui-icons-hq-gapfix-v1-atlas.png"
TERRAIN_SOURCE = ROOT / "src" / "assets" / "images" / "battle" / "first-battle-map.webp"
SOURCE_DIR = ROOT / "src" / "assets" / "images" / "ui" / "icon-sources" / "battle-ui-v2"
BASE_FRAME = 48
FRAME = 128
MICRO_FRAME = 32
SCALE = 4
COLS = 5
SEED_ROWS = 6
ROWS = 8
SOURCE_FRAMES = {
4: ("attack", "attack-v2.png"),
16: ("hit", "hit-v2.png"),
17: ("critical", "critical-v2.png"),
19: ("counter", "counter-v2.png"),
24: ("fire", "fire-v2.png"),
27: ("bean", "bean-v2.png"),
28: ("salve", "salve-v2.png"),
29: ("wine", "wine-v2.png"),
30: ("confusion", "confusion-v2.png"),
31: ("benevolent-command", "benevolent-command-v2.png"),
32: ("azure-dragon-strike", "azure-dragon-strike-v2.png"),
33: ("changban-roar", "changban-roar-v2.png"),
34: ("single-rider-rescue", "single-rider-rescue-v2.png"),
35: ("east-wind-fire", "east-wind-fire-v2.png"),
36: ("hundred-pace-pierce", "hundred-pace-pierce-v2.png"),
37: ("western-cavalry-charge", "western-cavalry-charge-v2.png"),
38: ("qilin-stratagem", "qilin-stratagem-v2.png"),
39: ("burn", "burn-v2.png"),
}
REPLACEMENT_FRAMES: dict[int, tuple[str, Callable[[Image.Image], Image.Image]]] = {
10: ("axe", lambda atlas: compose_axe_icon(atlas)),
}
def main() -> None:
OUT.parent.mkdir(parents=True, exist_ok=True)
PROOF_OUT.parent.mkdir(parents=True, exist_ok=True)
seed_path = SEED_ATLAS if SEED_ATLAS.exists() else OUT
if not seed_path.exists():
raise FileNotFoundError(f"missing seed atlas: {seed_path}")
seed_atlas = Image.open(seed_path).convert("RGBA")
expected_seed_size = (COLS * FRAME, SEED_ROWS * FRAME)
if seed_atlas.size != expected_seed_size:
raise ValueError(f"seed atlas must be {expected_seed_size}, got {seed_atlas.size}: {seed_path}")
atlas = Image.new("RGBA", (COLS * FRAME, ROWS * FRAME), (0, 0, 0, 0))
atlas.alpha_composite(seed_atlas, (0, 0))
for index, (name, filename) in SOURCE_FRAMES.items():
tile = source_icon(SOURCE_DIR / filename)
replace_frame(atlas, index, tile)
print(f"Applied frame {index:02d} {name} from {filename}")
for index, (_name, build_icon) in REPLACEMENT_FRAMES.items():
tile = build_icon(atlas)
replace_frame(atlas, index, tile)
validate_source_frames(atlas)
atlas.save(OUT, optimize=True)
micro_atlas = build_micro_atlas(atlas)
validate_micro_frames(micro_atlas)
micro_atlas.save(MICRO_OUT, optimize=True)
write_contact_sheet(seed_atlas, atlas, micro_atlas)
print(f"Wrote {OUT}")
print(f"Wrote {MICRO_OUT}")
print(f"Wrote {PROOF_OUT}")
def replace_frame(atlas: Image.Image, index: int, tile: Image.Image) -> None:
frame_box = ((index % COLS) * FRAME, (index // COLS) * FRAME)
atlas.paste(Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0)), frame_box)
atlas.alpha_composite(tile.convert("RGBA"), frame_box)
def source_icon(path: Path) -> Image.Image:
if not path.exists():
raise FileNotFoundError(f"missing V2 icon source: {path}")
image = Image.open(path).convert("RGBA")
alpha = image.getchannel("A")
corner_alpha = [
alpha.getpixel((0, 0)),
alpha.getpixel((image.width - 1, 0)),
alpha.getpixel((0, image.height - 1)),
alpha.getpixel((image.width - 1, image.height - 1)),
]
if max(corner_alpha) > 8:
raise ValueError(f"V2 icon source must have transparent corners: {path}")
bounds = alpha.getbbox()
if bounds is None:
raise ValueError(f"V2 icon source is fully transparent: {path}")
cropped = image.crop(bounds)
content_size = 104
scale = min(content_size / cropped.width, content_size / cropped.height)
width = max(1, round(cropped.width * scale))
height = max(1, round(cropped.height * scale))
resized = cropped.resize((width, height), Image.Resampling.LANCZOS)
resized = resized.filter(ImageFilter.UnsharpMask(radius=0.7, percent=85, threshold=2))
tile = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
tile.alpha_composite(resized, ((FRAME - width) // 2, (FRAME - height) // 2))
return tile
def build_micro_atlas(atlas: Image.Image) -> Image.Image:
micro = Image.new("RGBA", (COLS * MICRO_FRAME, ROWS * MICRO_FRAME), (0, 0, 0, 0))
for index in range(COLS * ROWS):
micro_content_size = MICRO_FRAME - 2
frame = atlas_frame(atlas, index).resize((micro_content_size, micro_content_size), Image.Resampling.LANCZOS)
alpha = frame.getchannel("A")
frame = frame.filter(ImageFilter.UnsharpMask(radius=0.45, percent=120, threshold=1))
frame.putalpha(alpha)
micro.alpha_composite(frame, ((index % COLS) * MICRO_FRAME + 1, (index // COLS) * MICRO_FRAME + 1))
return micro
def validate_source_frames(atlas: Image.Image) -> None:
for index, (name, _filename) in SOURCE_FRAMES.items():
bounds = atlas_frame(atlas, index).getchannel("A").getbbox()
if bounds is None:
raise ValueError(f"V2 atlas frame {index:02d} {name} is fully transparent")
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
if min(width, height) < 48:
raise ValueError(f"V2 atlas frame {index:02d} {name} is too narrow for runtime readability: {bounds}")
if bounds[0] < 8 or bounds[1] < 8 or bounds[2] > FRAME - 8 or bounds[3] > FRAME - 8:
raise ValueError(f"V2 atlas frame {index:02d} {name} violates the 8px safe edge: {bounds}")
def validate_micro_frames(atlas: Image.Image) -> None:
for index, (name, _filename) in SOURCE_FRAMES.items():
bounds = micro_atlas_frame(atlas, index).getchannel("A").getbbox()
if bounds is None:
raise ValueError(f"micro atlas frame {index:02d} {name} is fully transparent")
if bounds[0] == 0 or bounds[1] == 0 or bounds[2] == MICRO_FRAME or bounds[3] == MICRO_FRAME:
raise ValueError(f"micro atlas frame {index:02d} {name} touches an outer edge: {bounds}")
def micro_atlas_frame(atlas: Image.Image, index: int) -> Image.Image:
x = (index % COLS) * MICRO_FRAME
y = (index // COLS) * MICRO_FRAME
return atlas.crop((x, y, x + MICRO_FRAME, y + MICRO_FRAME)).convert("RGBA")
def write_contact_sheet(seed_atlas: Image.Image, atlas: Image.Image, micro_atlas: Image.Image) -> None:
card_width = 500
card_height = 220
labels = list(SOURCE_FRAMES.items())
sheet_rows = math.ceil(len(labels) / 4)
sheet = Image.new("RGB", (card_width * 4, 40 + card_height * sheet_rows), (8, 14, 20))
draw = ImageDraw.Draw(sheet)
draw.text((14, 13), "Battle UI V2 action/item/signature/status icons - before, HQ, runtime sizes", fill=(232, 220, 183))
runtime_sizes = (14, 20, 30, 34, 52, 70)
for position, (index, (name, _filename)) in enumerate(labels):
column = position % 4
row = position // 4
left = column * card_width
top = 40 + row * card_height
draw.rounded_rectangle((left + 6, top + 6, left + card_width - 6, top + card_height - 6), radius=8, fill=(15, 25, 34), outline=(64, 83, 98), width=1)
draw.text((left + 16, top + 14), f"{index:02d} {name}", fill=(236, 221, 176))
before = atlas_frame(seed_atlas, index).resize((72, 72), Image.Resampling.LANCZOS)
after = atlas_frame(atlas, index).resize((72, 72), Image.Resampling.LANCZOS)
sheet.paste(before, (left + 16, top + 46), before)
sheet.paste(after, (left + 104, top + 46), after)
draw.text((left + 29, top + 124), "before", fill=(125, 145, 159))
draw.text((left + 121, top + 124), "HQ", fill=(125, 145, 159))
cursor_x = left + 194
micro_frame = micro_atlas_frame(micro_atlas, index)
hq_frame = atlas_frame(atlas, index)
for size in runtime_sizes:
source = micro_frame if size <= MICRO_FRAME else hq_frame
sample = source.resize((size, size), Image.Resampling.LANCZOS)
sample_y = top + 78 - size // 2
sheet.paste(sample, (cursor_x, sample_y), sample)
draw.text((cursor_x, top + 124), str(size), fill=(125, 145, 159))
cursor_x += max(size, 28) + 8
bounds = atlas_frame(atlas, index).getchannel("A").getbbox()
occupancy = 0 if bounds is None else round(100 * max(bounds[2] - bounds[0], bounds[3] - bounds[1]) / FRAME)
draw.text((left + 16, top + 164), f"frame {FRAME}px / micro {MICRO_FRAME}px / max span {occupancy}%", fill=(143, 164, 176))
sheet.save(PROOF_OUT, optimize=True)
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 atlas_frame(atlas: Image.Image, index: int) -> Image.Image:
x = (index % COLS) * FRAME
y = (index // COLS) * FRAME
return atlas.crop((x, y, x + FRAME, y + FRAME)).convert("RGBA")
def alpha_paste(base: Image.Image, overlay: Image.Image, x: int, y: int) -> None:
base.alpha_composite(overlay.convert("RGBA"), (x, y))
def scaled_overlay(image: Image.Image, size: int, angle: float = 0) -> Image.Image:
overlay = image.resize((size, size), Image.Resampling.LANCZOS)
if angle:
overlay = overlay.rotate(angle, resample=Image.Resampling.BICUBIC, expand=True)
return overlay
def compose_attack_icon(atlas: Image.Image) -> Image.Image:
tile = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
burst = scaled_overlay(atlas_frame(atlas, 17), 100)
burst_alpha = burst.getchannel("A").point(lambda value: int(value * 0.72))
burst.putalpha(burst_alpha)
alpha_paste(tile, burst, 6, 24)
sword = scaled_overlay(atlas_frame(atlas, 8), 122, -8)
alpha_paste(tile, sword, (FRAME - sword.width) // 2 + 3, (FRAME - sword.height) // 2 - 1)
draw = ImageDraw.Draw(tile, "RGBA")
draw.line([(24, 92), (50, 68), (83, 41), (108, 22)], fill=(207, 66, 43, 210), width=7)
draw.line([(28, 96), (58, 72), (95, 48), (116, 34)], fill=(255, 215, 111, 170), width=3)
return tile.filter(ImageFilter.UnsharpMask(radius=0.9, percent=80, threshold=2))
def compose_axe_icon(atlas: Image.Image) -> Image.Image:
tile = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
axe_source = atlas_frame(atlas, 1).transpose(Image.Transpose.FLIP_LEFT_RIGHT)
axe = scaled_overlay(axe_source, 124, 4)
alpha_paste(tile, axe, (FRAME - axe.width) // 2, (FRAME - axe.height) // 2 + 2)
return tile.filter(ImageFilter.UnsharpMask(radius=0.9, percent=80, threshold=2))
def compose_terrain_icon() -> Image.Image:
if not TERRAIN_SOURCE.exists():
return make_icon(draw_terrain)
source = Image.open(TERRAIN_SOURCE).convert("RGBA")
crop = source.crop((2440, 720, 3630, 1910)).resize((104, 104), Image.Resampling.LANCZOS)
crop = ImageEnhance.Color(crop).enhance(1.18)
crop = ImageEnhance.Contrast(crop).enhance(1.1)
crop = ImageEnhance.Brightness(crop).enhance(1.05)
tile = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
mask = Image.new("L", (104, 104), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rounded_rectangle((0, 0, 103, 103), radius=22, fill=255)
tile.paste(crop, (12, 12), mask)
draw = ImageDraw.Draw(tile, "RGBA")
draw.rounded_rectangle((12, 12, 116, 116), radius=22, outline=(48, 35, 23, 235), width=5)
draw.rounded_rectangle((16, 16, 112, 112), radius=18, outline=(216, 183, 92, 215), width=3)
draw.line([(21, 93), (42, 83), (63, 88), (86, 79), (107, 85)], fill=(231, 193, 101, 170), width=4)
draw.line([(69, 97), (79, 90), (97, 94)], fill=(111, 190, 192, 150), width=4)
return tile.filter(ImageFilter.UnsharpMask(radius=1.0, percent=95, threshold=2))
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 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 = [(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 = [(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(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(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:
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, [(8, 35), (18, 28), (30, 18), (41, 8)], (113, 38, 30, 90), 9)
poly(draw, [(36, 6), (42, 12), (26, 30), (21, 25)], (227, 230, 220), (50, 54, 55))
poly(draw, [(34, 10), (39, 12), (26, 26), (24, 24)], (255, 255, 245, 170))
poly(draw, [(22, 27), (31, 35), (27, 40), (17, 31)], (210, 142, 66), (67, 37, 26))
line(draw, [(15, 39), (21, 32)], (246, 208, 111), 4)
line(draw, [(15, 34), (25, 42)], (83, 45, 31), 2)
poly(draw, [(13, 18), (20, 16), (18, 22), (28, 20), (20, 27), (22, 34), (15, 29), (7, 32), (12, 24)], (232, 84, 54, 190), (86, 30, 24))
line(draw, [(11, 20), (27, 35), (37, 38)], (255, 204, 85, 190), 2)
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, [(16, 40), (30, 9)], (102, 70, 42), 5)
line(draw, [(19, 38), (31, 12)], (190, 126, 62, 210), 2)
poly(draw, [(28, 8), (43, 12), (42, 22), (34, 27), (29, 21), (24, 16)], (213, 218, 213), (50, 54, 55))
poly(draw, [(28, 8), (17, 13), (17, 23), (27, 22), (31, 16)], (218, 223, 216), (50, 54, 55))
poly(draw, [(31, 11), (40, 14), (39, 20), (32, 23), (30, 18)], (255, 255, 240, 135))
poly(draw, [(25, 13), (19, 16), (20, 21), (27, 20), (29, 16)], (255, 255, 240, 120))
ellipse(draw, (24, 14, 33, 23), (202, 148, 69), (69, 42, 25))
line(draw, [(33, 24), (41, 31)], (154, 42, 35, 210), 3)
line(draw, [(29, 25), (33, 35)], (229, 82, 49, 190), 2)
line(draw, [(21, 31), (13, 39)], (62, 36, 26, 180), 2)
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, [(6, 37), (16, 21), (24, 34), (34, 14), (43, 37), (36, 43), (13, 43)], (42, 63, 47), (24, 34, 28))
poly(draw, [(8, 36), (17, 23), (24, 34), (18, 39)], (86, 125, 74), (30, 51, 35))
poly(draw, [(24, 34), (34, 16), (42, 36), (35, 40)], (67, 112, 74), (30, 51, 35))
poly(draw, [(15, 23), (18, 28), (21, 26), (17, 20)], (229, 218, 176, 230))
poly(draw, [(33, 17), (36, 25), (39, 24), (34, 14)], (235, 225, 188, 220))
poly(draw, [(6, 38), (17, 33), (25, 36), (37, 33), (44, 37), (44, 42), (6, 42)], (137, 106, 62), (55, 42, 29))
line(draw, [(13, 39), (20, 36), (30, 39), (39, 36)], (203, 168, 92, 200), 2)
line(draw, [(11, 35), (18, 32), (27, 34), (36, 32)], (88, 146, 96, 190), 2)
line(draw, [(27, 41), (32, 38), (39, 39)], (97, 169, 171, 190), 2)
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))
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()