330 lines
12 KiB
Python
330 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter
|
|
|
|
|
|
FRAME_SIZE = 313
|
|
PALETTE_COLORS = 192
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UNIT_DIR = ROOT / "src" / "assets" / "images" / "units"
|
|
|
|
DIRECTIONS = ("south", "east", "north", "west")
|
|
FRAME_BY_ACTION = {
|
|
"attack": 2,
|
|
"strategy": 0,
|
|
"item": 1,
|
|
"hurt": 0,
|
|
}
|
|
|
|
FORWARD = {
|
|
"south": (0, 15),
|
|
"east": (18, 0),
|
|
"north": (0, -10),
|
|
"west": (-18, 0),
|
|
}
|
|
|
|
BACKWARD = {
|
|
"south": (0, -12),
|
|
"east": (-14, 0),
|
|
"north": (0, 12),
|
|
"west": (14, 0),
|
|
}
|
|
|
|
ITEM_POS = {
|
|
"south": (184, 142),
|
|
"east": (222, 143),
|
|
"north": (134, 136),
|
|
"west": (88, 143),
|
|
}
|
|
|
|
ACTION_ORDER = ("attack", "strategy", "item", "hurt")
|
|
|
|
|
|
def main() -> None:
|
|
for source in sorted(UNIT_DIR.glob("unit-*.png")):
|
|
if source.name.endswith("-actions.png"):
|
|
continue
|
|
sheet = Image.open(source).convert("RGBA")
|
|
output = Image.new("RGBA", (FRAME_SIZE * len(ACTION_ORDER), FRAME_SIZE * len(DIRECTIONS)), (0, 0, 0, 0))
|
|
profile = action_profile(source.stem)
|
|
|
|
for row, direction in enumerate(DIRECTIONS):
|
|
for col, action in enumerate(ACTION_ORDER):
|
|
frame = crop_frame(sheet, row, FRAME_BY_ACTION[action])
|
|
if action == "attack":
|
|
action_frame = make_attack(frame, direction, profile)
|
|
elif action == "strategy":
|
|
action_frame = make_strategy(frame, direction)
|
|
elif action == "item":
|
|
action_frame = make_item(frame, direction)
|
|
else:
|
|
action_frame = make_hurt(frame, direction)
|
|
output.alpha_composite(action_frame, (col * FRAME_SIZE, row * FRAME_SIZE))
|
|
|
|
save_unit_png(output, source.with_name(f"{source.stem}-actions.png"))
|
|
|
|
|
|
def save_unit_png(image: Image.Image, path: Path) -> None:
|
|
image = image.convert("RGBA")
|
|
indexed = image.quantize(colors=PALETTE_COLORS, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE)
|
|
indexed.save(path, optimize=True)
|
|
|
|
|
|
def action_profile(stem: str) -> str:
|
|
lower = stem.lower()
|
|
if any(token in lower for token in ("archer", "crossbow", "longbow")):
|
|
return "ranged"
|
|
if any(token in lower for token in ("strategist", "shaman", "zhuge", "sima", "fa-zheng", "ma-liang", "yi-ji")):
|
|
return "strategy"
|
|
if any(token in lower for token in ("cavalry", "ma-chao", "ma-dai", "lu-bu")):
|
|
return "cavalry"
|
|
if any(token in lower for token in ("spearman", "guan-yu", "zhang-fei", "jiang-wei", "huang-zhong", "zhao-yun")):
|
|
return "polearm"
|
|
return "blade"
|
|
|
|
|
|
def crop_frame(sheet: Image.Image, row: int, col: int) -> Image.Image:
|
|
return sheet.crop((col * FRAME_SIZE, row * FRAME_SIZE, (col + 1) * FRAME_SIZE, (row + 1) * FRAME_SIZE)).copy()
|
|
|
|
|
|
def offset_subject(frame: Image.Image, dx: int, dy: int) -> Image.Image:
|
|
canvas = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
bbox = frame.getbbox()
|
|
if not bbox:
|
|
return frame
|
|
subject = frame.crop(bbox)
|
|
canvas.alpha_composite(subject, (bbox[0] + dx, bbox[1] + dy))
|
|
return canvas
|
|
|
|
|
|
def underlay(frame: Image.Image, color: tuple[int, int, int, int], blur: int, expand: int = 0) -> Image.Image:
|
|
alpha = frame.getchannel("A")
|
|
if expand > 0:
|
|
alpha = alpha.filter(ImageFilter.MaxFilter(expand * 2 + 1))
|
|
alpha = alpha.filter(ImageFilter.GaussianBlur(blur))
|
|
glow = Image.new("RGBA", frame.size, color)
|
|
glow.putalpha(alpha.point(lambda value: min(255, int(value * color[3] / 255))))
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(glow)
|
|
result.alpha_composite(frame)
|
|
return result
|
|
|
|
|
|
def make_attack(frame: Image.Image, direction: str, profile: str) -> Image.Image:
|
|
if profile == "ranged":
|
|
return make_ranged_attack(frame, direction)
|
|
if profile == "strategy":
|
|
return make_strategy_attack(frame, direction)
|
|
if profile == "cavalry":
|
|
return make_charge_attack(frame, direction)
|
|
return make_blade_attack(frame, direction, profile)
|
|
|
|
|
|
def make_blade_attack(frame: Image.Image, direction: str, profile: str) -> Image.Image:
|
|
dx, dy = FORWARD[direction]
|
|
body = offset_subject(frame, dx, dy)
|
|
body = underlay(body, (255, 224, 120, 92), 4, 1)
|
|
slash = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(slash)
|
|
|
|
if direction == "east":
|
|
points = [(166, 76), (264, 126), (234, 220)]
|
|
elif direction == "west":
|
|
points = [(147, 76), (49, 126), (79, 220)]
|
|
elif direction == "south":
|
|
points = [(74, 104), (166, 184), (248, 112)]
|
|
else:
|
|
points = [(74, 202), (166, 112), (248, 198)]
|
|
|
|
width = 18 if profile == "polearm" else 15
|
|
draw.line(points, fill=(255, 247, 214, 224), width=width, joint="curve")
|
|
draw.line(points, fill=(98, 210, 255, 184), width=7 if profile == "polearm" else 6, joint="curve")
|
|
draw.line(points, fill=(255, 178, 86, 166), width=3 if profile == "polearm" else 2, joint="curve")
|
|
slash = slash.filter(ImageFilter.GaussianBlur(0.45))
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(slash)
|
|
result.alpha_composite(body)
|
|
return result
|
|
|
|
|
|
def make_ranged_attack(frame: Image.Image, direction: str) -> Image.Image:
|
|
dx, dy = FORWARD[direction]
|
|
body = underlay(offset_subject(frame, round(dx * 0.45), round(dy * 0.45)), (255, 221, 96, 78), 4, 1)
|
|
effect = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(effect)
|
|
|
|
if direction == "east":
|
|
shaft = (134, 124, 282, 102)
|
|
tip = [(282, 102), (266, 94), (269, 111)]
|
|
fletch = ((132, 124), (111, 118), (115, 132))
|
|
bow = (84, 74, 158, 198, -54, 62)
|
|
elif direction == "west":
|
|
shaft = (179, 124, 31, 102)
|
|
tip = [(31, 102), (47, 94), (44, 111)]
|
|
fletch = ((181, 124), (202, 118), (198, 132))
|
|
bow = (155, 74, 229, 198, 172, 286)
|
|
elif direction == "south":
|
|
shaft = (140, 118, 176, 274)
|
|
tip = [(176, 274), (163, 259), (184, 257)]
|
|
fletch = ((140, 118), (131, 97), (150, 101))
|
|
bow = (91, 80, 213, 176, 24, 145)
|
|
else:
|
|
shaft = (172, 190, 136, 34)
|
|
tip = [(136, 34), (129, 51), (148, 48)]
|
|
fletch = ((172, 190), (182, 211), (163, 207))
|
|
bow = (91, 128, 213, 224, 205, 326)
|
|
|
|
draw.arc(bow[:4], start=bow[4], end=bow[5], fill=(250, 214, 102, 190), width=5)
|
|
draw.line(shaft, fill=(255, 252, 225, 224), width=7)
|
|
draw.line(shaft, fill=(92, 210, 255, 150), width=3)
|
|
draw.polygon(tip, fill=(255, 245, 207, 230))
|
|
draw.polygon(fletch, fill=(255, 196, 88, 196))
|
|
for offset in (-14, 14):
|
|
if direction in ("east", "west"):
|
|
draw.line((shaft[0], shaft[1] + offset, shaft[2], shaft[3] + offset), fill=(255, 230, 128, 70), width=2)
|
|
else:
|
|
draw.line((shaft[0] + offset, shaft[1], shaft[2] + offset, shaft[3]), fill=(255, 230, 128, 70), width=2)
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(effect.filter(ImageFilter.GaussianBlur(0.25)))
|
|
result.alpha_composite(body)
|
|
return result
|
|
|
|
|
|
def make_strategy_attack(frame: Image.Image, direction: str) -> Image.Image:
|
|
dx, dy = FORWARD[direction]
|
|
body = underlay(offset_subject(frame, round(dx * 0.35), round(dy * 0.35)), (92, 180, 255, 92), 6, 2)
|
|
effect = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(effect)
|
|
center = (156 + round(dx * 0.45), 150 + round(dy * 0.45))
|
|
|
|
for radius, alpha in ((78, 82), (48, 126), (22, 180)):
|
|
draw.ellipse(
|
|
(center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius),
|
|
outline=(108, 210, 255, alpha),
|
|
width=5,
|
|
)
|
|
for angle in range(0, 360, 45):
|
|
x = center[0] + int(74 * math.cos(math.radians(angle)))
|
|
y = center[1] + int(74 * math.sin(math.radians(angle)))
|
|
draw.polygon([(x, y - 8), (x + 5, y), (x, y + 8), (x - 5, y)], fill=(255, 230, 132, 156))
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(effect.filter(ImageFilter.GaussianBlur(0.35)))
|
|
result.alpha_composite(body)
|
|
return result
|
|
|
|
|
|
def make_charge_attack(frame: Image.Image, direction: str) -> Image.Image:
|
|
dx, dy = FORWARD[direction]
|
|
body = underlay(offset_subject(frame, round(dx * 1.2), round(dy * 1.2)), (255, 188, 84, 88), 5, 2)
|
|
effect = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(effect)
|
|
|
|
if direction == "east":
|
|
lines = [((38, 96), (188, 82)), ((28, 154), (214, 146)), ((56, 222), (180, 204))]
|
|
spear = (128, 132, 288, 98)
|
|
elif direction == "west":
|
|
lines = [((275, 96), (125, 82)), ((285, 154), (99, 146)), ((257, 222), (133, 204))]
|
|
spear = (185, 132, 25, 98)
|
|
elif direction == "south":
|
|
lines = [((86, 28), (98, 188)), ((154, 24), (148, 214)), ((226, 52), (205, 180))]
|
|
spear = (138, 120, 180, 286)
|
|
else:
|
|
lines = [((86, 285), (98, 125)), ((154, 289), (148, 99)), ((226, 261), (205, 133))]
|
|
spear = (175, 192, 133, 26)
|
|
|
|
for line in lines:
|
|
draw.line(line, fill=(255, 204, 102, 118), width=8)
|
|
draw.line(line, fill=(255, 245, 210, 120), width=3)
|
|
draw.line(spear, fill=(255, 250, 218, 218), width=8)
|
|
draw.line(spear, fill=(255, 152, 58, 166), width=3)
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(effect.filter(ImageFilter.GaussianBlur(0.3)))
|
|
result.alpha_composite(body)
|
|
return result
|
|
|
|
|
|
def make_strategy(frame: Image.Image, direction: str) -> Image.Image:
|
|
aura = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(aura)
|
|
center = (156, 160)
|
|
|
|
for radius, alpha in ((92, 42), (66, 66), (38, 92)):
|
|
draw.ellipse(
|
|
(center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius),
|
|
outline=(98, 195, 255, alpha),
|
|
width=4,
|
|
)
|
|
|
|
for index in range(10):
|
|
x = 72 + (index * 23) % 168
|
|
y = 58 + (index * 47) % 188
|
|
draw.polygon(
|
|
[(x, y - 7), (x + 4, y), (x, y + 7), (x - 4, y)],
|
|
fill=(255, 230, 132, 140),
|
|
)
|
|
|
|
aura = aura.filter(ImageFilter.GaussianBlur(0.35))
|
|
body = underlay(frame, (60, 160, 255, 92), 6, 2)
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(aura)
|
|
result.alpha_composite(body)
|
|
return result
|
|
|
|
|
|
def make_item(frame: Image.Image, direction: str) -> Image.Image:
|
|
body = underlay(frame, (95, 230, 150, 70), 4, 1)
|
|
effect = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(effect)
|
|
x, y = ITEM_POS[direction]
|
|
|
|
draw.rounded_rectangle((x - 18, y - 15, x + 18, y + 17), radius=7, fill=(137, 91, 45, 235), outline=(245, 207, 122, 230), width=3)
|
|
draw.ellipse((x - 10, y - 28, x + 10, y - 9), fill=(95, 221, 142, 210), outline=(224, 255, 219, 230), width=2)
|
|
draw.line((x - 26, y + 2, x + 26, y + 2), fill=(224, 255, 219, 184), width=4)
|
|
draw.line((x, y - 24, x, y + 27), fill=(224, 255, 219, 184), width=4)
|
|
|
|
for px, py in ((x - 38, y - 30), (x + 36, y - 26), (x - 30, y + 36), (x + 28, y + 34)):
|
|
draw.ellipse((px - 4, py - 4, px + 4, py + 4), fill=(159, 255, 200, 160))
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(body)
|
|
result.alpha_composite(effect)
|
|
return result
|
|
|
|
|
|
def make_hurt(frame: Image.Image, direction: str) -> Image.Image:
|
|
dx, dy = BACKWARD[direction]
|
|
body = offset_subject(frame, dx, dy)
|
|
body = ImageEnhance.Color(body).enhance(0.62)
|
|
|
|
alpha = body.getchannel("A")
|
|
red = Image.new("RGBA", body.size, (255, 78, 60, 70))
|
|
red.putalpha(alpha.point(lambda value: min(88, value // 3)))
|
|
body = Image.alpha_composite(body, red)
|
|
|
|
impact = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(impact)
|
|
if direction in ("west", "north"):
|
|
cx, cy = 190, 118
|
|
else:
|
|
cx, cy = 124, 118
|
|
spikes = [(cx, cy - 28), (cx + 9, cy - 8), (cx + 31, cy - 6), (cx + 12, cy + 7), (cx + 20, cy + 28), (cx, cy + 13), (cx - 22, cy + 27), (cx - 12, cy + 6), (cx - 31, cy - 7), (cx - 8, cy - 9)]
|
|
draw.polygon(spikes, fill=(255, 222, 118, 194), outline=(255, 84, 57, 224))
|
|
draw.line((cx - 38, cy - 35, cx + 38, cy + 35), fill=(255, 245, 205, 186), width=5)
|
|
draw.line((cx + 34, cy - 32, cx - 32, cy + 34), fill=(255, 245, 205, 154), width=3)
|
|
|
|
result = Image.new("RGBA", frame.size, (0, 0, 0, 0))
|
|
result.alpha_composite(body)
|
|
result.alpha_composite(impact.filter(ImageFilter.GaussianBlur(0.2)))
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|