diff --git a/docs/unit-sprite-direction-preview.png b/docs/unit-sprite-direction-preview.png new file mode 100644 index 0000000..56f8ed7 Binary files /dev/null and b/docs/unit-sprite-direction-preview.png differ diff --git a/scripts/redraw-unit-sprites.py b/scripts/redraw-unit-sprites.py index fc64aef..5f6141a 100644 --- a/scripts/redraw-unit-sprites.py +++ b/scripts/redraw-unit-sprites.py @@ -7,21 +7,22 @@ import math from dataclasses import dataclass from pathlib import Path -from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter +from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageFont -FRAME_SIZE = 313 -GRID_SIZE = 4 -PALETTE_COLORS = 224 ROOT = Path(__file__).resolve().parents[1] UNIT_DIR = ROOT / "src" / "assets" / "images" / "units" -SOURCE_ATLAS = ROOT / "src" / "assets" / "images" / "unit-sources" / "three-kingdoms-unit-atlas.png" +PREVIEW_PATH = ROOT / "docs" / "unit-sprite-direction-preview.png" -DIRECTIONS = ("south", "east", "north", "west") -ACTION_ORDER = ("attack", "strategy", "item", "hurt", "celebrate") +FRAME_SIZE = 313 +DRAW_SCALE = 2 IDLE_FRAME_COUNT = 8 MOVE_FRAME_COUNT = 8 BASE_FRAMES_PER_DIRECTION = IDLE_FRAME_COUNT + MOVE_FRAME_COUNT +PALETTE_COLORS = 208 + +DIRECTIONS = ("south", "east", "north", "west") +ACTION_ORDER = ("attack", "strategy", "item", "hurt", "celebrate") ACTION_FRAME_COUNTS = { "attack": 10, "strategy": 8, @@ -29,590 +30,1164 @@ ACTION_FRAME_COUNTS = { "hurt": 4, "celebrate": 6, } -ACTION_FRAME_OFFSETS = { +ACTION_COLUMN_OFFSETS = { action: sum(ACTION_FRAME_COUNTS[previous] for previous in ACTION_ORDER[:index]) for index, action in enumerate(ACTION_ORDER) } ACTION_FRAMES_PER_DIRECTION = sum(ACTION_FRAME_COUNTS.values()) +REPRESENTATIVE_UNITS = ( + "unit-shu-infantry", + "unit-shu-spearman", + "unit-shu-archer", + "unit-shu-strategist", + "unit-shu-cavalry", + "unit-zhao-yun", +) + +Color = tuple[int, int, int] +Rgba = tuple[int, int, int, int] +INK: Rgba = (12, 9, 8, 235) +DEEP_INK: Rgba = (5, 4, 4, 245) +GROUND_SHADOW: Rgba = (23, 19, 15, 80) + @dataclass(frozen=True) -class Template: - atlas_index: int - role: str - target_height: int - target_width: int +class Palette: + primary: Color + secondary: Color + accent: Color + cloth: Color + armor: Color + horse: Color + trim: Color @dataclass(frozen=True) class UnitPlan: - template: str - tint: tuple[int, int, int] - tint_strength: float - saturation: float = 1.0 - brightness: float = 1.0 - scale: float = 1.0 + stem: str + faction: str + role: str + weapon: str + palette: Palette + skin: Color + hair: Color + metal: Color + cape: bool + helmet: bool + banner: bool + scale: float + bulk: float + rank: int @dataclass(frozen=True) -class RoleSpec: - height: int - max_width: int - bottom_padding: int - idle_scale_x: tuple[float, float, float, float] - idle_scale_y: tuple[float, float, float, float] - idle_dx: tuple[int, int, int, int] - - -TEMPLATES: dict[str, Template] = { - "shu-lord": Template(0, "infantry", 286, 274), - "green-polearm": Template(1, "polearm", 292, 282), - "red-spear": Template(2, "polearm", 292, 282), - "white-cavalry": Template(3, "cavalry", 292, 304), - "wei-lord": Template(4, "infantry", 286, 274), - "red-cavalry": Template(5, "cavalry", 292, 304), - "strategist": Template(6, "strategist", 276, 250), - "rebel-infantry": Template(7, "infantry", 286, 274), - "rebel-archer": Template(8, "archer", 282, 276), - "rebel-cavalry": Template(9, "cavalry", 292, 304), - "wu-officer": Template(10, "infantry", 286, 274), - "nanman-officer": Template(11, "polearm", 292, 282), -} - -ROLE_SPECS: dict[str, RoleSpec] = { - "infantry": RoleSpec(286, 274, 4, (1.0, 0.992, 0.986, 0.996, 1.008, 1.015, 1.006, 1.0), (1.0, 1.012, 1.02, 1.012, 0.998, 0.99, 0.997, 1.0), (0, 0, -1, 0, 0, 1, 0, 0)), - "polearm": RoleSpec(292, 282, 4, (1.0, 0.992, 0.986, 0.996, 1.008, 1.014, 1.006, 1.0), (1.0, 1.012, 1.02, 1.012, 0.998, 0.99, 0.997, 1.0), (0, 0, -1, 0, 0, 1, 0, 0)), - "cavalry": RoleSpec(292, 304, 3, (1.0, 1.008, 1.014, 1.006, 0.996, 0.988, 0.996, 1.0), (1.0, 1.01, 1.016, 1.008, 0.998, 0.99, 0.998, 1.0), (0, 1, 1, 0, 0, -1, -1, 0)), - "strategist": RoleSpec(276, 250, 5, (1.0, 0.993, 0.988, 0.997, 1.006, 1.012, 1.005, 1.0), (1.0, 1.012, 1.019, 1.012, 0.998, 0.994, 0.998, 1.0), (0, 0, -1, 0, 0, 1, 0, 0)), - "archer": RoleSpec(282, 276, 4, (1.0, 0.993, 0.987, 0.996, 1.006, 1.012, 1.004, 1.0), (1.0, 1.011, 1.018, 1.01, 0.998, 0.992, 0.998, 1.0), (0, 0, -1, 0, 0, 1, 0, 0)), -} - -SIGNATURE_PLANS: dict[str, UnitPlan] = { - "unit-liu-bei": UnitPlan("shu-lord", (42, 142, 72), 0.04, 1.04, 1.02, 1.0), - "unit-guan-yu": UnitPlan("green-polearm", (34, 132, 70), 0.04, 1.03, 1.02, 1.0), - "unit-zhang-fei": UnitPlan("red-spear", (152, 42, 42), 0.05, 1.04, 1.02, 1.0), - "unit-zhao-yun": UnitPlan("white-cavalry", (42, 116, 184), 0.06, 1.04, 1.03, 1.0), - "unit-cao-cao": UnitPlan("wei-lord", (52, 58, 122), 0.06, 1.02, 1.01, 1.0), - "unit-lu-bu": UnitPlan("red-cavalry", (140, 36, 54), 0.05, 1.05, 1.02, 1.0), - "unit-zhuge-liang": UnitPlan("strategist", (42, 126, 118), 0.05, 1.02, 1.03, 1.0), - "unit-pang-tong": UnitPlan("strategist", (80, 126, 68), 0.09, 1.02, 1.0, 1.0), - "unit-sima-yi": UnitPlan("strategist", (58, 62, 132), 0.16, 1.04, 0.96, 1.0), - "unit-huang-zhong": UnitPlan("rebel-archer", (54, 124, 64), 0.28, 1.04, 1.02, 1.0), - "unit-ma-chao": UnitPlan("white-cavalry", (54, 118, 194), 0.1, 1.04, 1.04, 1.0), - "unit-ma-dai": UnitPlan("white-cavalry", (98, 122, 156), 0.22, 1.02, 0.98, 1.0), - "unit-meng-huo": UnitPlan("nanman-officer", (192, 88, 42), 0.1, 1.04, 1.02, 1.0), - "unit-jiang-wei": UnitPlan("green-polearm", (44, 140, 124), 0.18, 1.04, 1.02, 1.0), - "unit-wei-yan": UnitPlan("green-polearm", (132, 42, 58), 0.28, 1.05, 0.98, 1.0), -} - -FACTION_TINTS = { - "shu": (42, 142, 76), - "wei": (64, 72, 150), - "wu": (28, 132, 142), - "nanman": (188, 88, 42), - "rebel": (202, 150, 36), -} +class Motion: + body_dx: float = 0 + body_dy: float = 0 + lean: float = 0 + leg: float = 0 + arm: float = 0 + weapon: float = 0 + raise_arm: float = 0 + lunge: float = 0 + impact: float = 0 + horse_leg: float = 0 + squash_x: float = 1 + squash_y: float = 1 def main() -> None: - parser = argparse.ArgumentParser(description="Redraw unit sprite sheets from a high-quality generated source atlas.") + parser = argparse.ArgumentParser( + description=( + "Draw original four-direction frame-by-frame tactical RPG unit sprite sheets. " + "This does not transform the previous unit art or reuse the source atlas." + ) + ) parser.add_argument("--only", help="Comma-separated unit stems or filenames to redraw.") - parser.add_argument("--out-dir", default=str(UNIT_DIR), help="Output directory. Defaults to the project unit asset directory.") + parser.add_argument("--out-dir", default=str(UNIT_DIR), help="Output directory for generated sprite sheets.") + parser.add_argument("--preview", default=str(PREVIEW_PATH), help="Preview contact sheet path.") + parser.add_argument("--preview-only", action="store_true", help="Write only the representative preview sheet.") args = parser.parse_args() - if not SOURCE_ATLAS.exists(): - raise FileNotFoundError(f"Missing source atlas: {SOURCE_ATLAS}") - output_dir = Path(args.out_dir).resolve() output_dir.mkdir(parents=True, exist_ok=True) - only = {entry.strip().removesuffix(".png") for entry in args.only.split(",") if entry.strip()} if args.only else set() - atlas_sprites = extract_source_sprites(Image.open(SOURCE_ATLAS).convert("RGBA")) + preview_path = Path(args.preview).resolve() + preview_path.parent.mkdir(parents=True, exist_ok=True) + only = normalize_only(args.only) + stems = discover_unit_stems(only) + preview_cache: dict[str, tuple[Image.Image, Image.Image]] = {} + + written = 0 + for stem in stems: + plan = unit_plan(stem) + base_sheet = draw_base_sheet(plan) + action_sheet = draw_action_sheet(plan) + + if stem in REPRESENTATIVE_UNITS: + preview_cache[stem] = (base_sheet, action_sheet) + + if not args.preview_only: + save_unit_png(base_sheet, output_dir / f"{stem}.png") + save_unit_png(action_sheet, output_dir / f"{stem}-actions.png") + written += 2 + + missing_preview = [stem for stem in REPRESENTATIVE_UNITS if stem not in preview_cache] + for stem in missing_preview: + plan = unit_plan(stem) + preview_cache[stem] = (draw_base_sheet(plan), draw_action_sheet(plan)) + + write_preview_sheet(preview_cache, preview_path) + + action = "Previewed" if args.preview_only else "Redrew" + print(f"{action} {len(stems)} unit sprite definitions; wrote {written} sprite sheets.") + print(f"Preview contact sheet: {preview_path}") + + +def normalize_only(raw: str | None) -> set[str]: + if not raw: + return set() + return { + entry.strip().removesuffix(".png").removesuffix("-actions") + for entry in raw.split(",") + if entry.strip() + } + + +def discover_unit_stems(only: set[str]) -> list[str]: stems = [ path.stem for path in sorted(UNIT_DIR.glob("unit-*.png")) if not path.name.endswith("-actions.png") ] - - written = 0 - for stem in stems: - if only and stem not in only: - continue - plan = unit_plan(stem) - template = TEMPLATES[plan.template] - source = atlas_sprites[template.atlas_index] - source = tint_sprite(source, plan) - sheet = draw_base_sheet(source, template, stem, plan) - save_unit_png(sheet, output_dir / f"{stem}.png") - save_unit_png(draw_action_sheet(sheet, template, stem), output_dir / f"{stem}-actions.png") - written += 2 - - print(f"Redrew {written} unit sprite sheets from {SOURCE_ATLAS}") + if only: + stems = [stem for stem in stems if stem in only] + if not stems: + raise ValueError("No unit sprite sheets matched the requested selection.") + return stems -def extract_source_sprites(atlas: Image.Image) -> list[Image.Image]: - cell_width = atlas.width // 4 - cell_height = atlas.height // 3 - sprites: list[Image.Image] = [] - for index in range(12): - col = index % 4 - row = index // 4 - cell = atlas.crop((col * cell_width, row * cell_height, (col + 1) * cell_width, (row + 1) * cell_height)) - sprite = remove_chroma_key(cell) - bbox = sprite.getbbox() - if not bbox: - raise ValueError(f"Could not extract atlas sprite {index}") - sprite = sprite.crop(expand_box(bbox, sprite.size, 10)) - sprites.append(sprite) - return sprites - - -def remove_chroma_key(image: Image.Image) -> Image.Image: - image = image.convert("RGBA") - pixels = image.load() - width, height = image.size - for y in range(height): - for x in range(width): - red, green, blue, alpha = pixels[x, y] - green_dominance = green - max(red, blue) - is_key_green = green > 158 and red < 118 and blue < 118 and green_dominance > 58 - if is_key_green: - fade = min(255, max(0, round((green_dominance - 58) * 4.8))) - alpha = max(0, alpha - fade) - if alpha <= 18: - pixels[x, y] = (red, green, blue, 0) - continue - if alpha > 0 and green > 150 and red < 132 and blue < 132 and green_dominance > 48: - green = round(max(red, blue) + green_dominance * 0.38) - pixels[x, y] = (red, green, blue, alpha) - - alpha = image.getchannel("A") - alpha = alpha.filter(ImageFilter.MinFilter(3)).filter(ImageFilter.MaxFilter(3)) - red, green, blue, _ = image.split() - cleaned = Image.merge("RGBA", (red, green, blue, alpha.point(lambda value: 0 if value < 9 else value))) - cleaned = despill_key_edges(cleaned) - return remove_detached_artifacts(cleaned) - - -def despill_key_edges(image: Image.Image) -> Image.Image: - image = image.convert("RGBA") - source = image.copy() - src = source.load() - pixels = image.load() - width, height = image.size - for y in range(height): - for x in range(width): - red, green, blue, alpha = src[x, y] - if alpha <= 0: - continue - green_dominance = green - max(red, blue) - if green <= 120 or green_dominance <= 34: - continue - touches_transparency = False - for ny in range(max(0, y - 1), min(height, y + 2)): - for nx in range(max(0, x - 1), min(width, x + 2)): - if src[nx, ny][3] <= 8: - touches_transparency = True - break - if touches_transparency: - break - if not touches_transparency: - continue - green = min(green, max(red, blue) + 18) - pixels[x, y] = (red, green, blue, alpha) - return image - - -def draw_base_sheet(source: Image.Image, template: Template, stem: str, plan: UnitPlan) -> Image.Image: +def draw_base_sheet(plan: UnitPlan) -> Image.Image: sheet = Image.new("RGBA", (FRAME_SIZE * BASE_FRAMES_PER_DIRECTION, FRAME_SIZE * len(DIRECTIONS)), (0, 0, 0, 0)) - fitted = fit_sprite(source, template, plan) for row, direction in enumerate(DIRECTIONS): - directed = orient_sprite(fitted, direction) - for col in range(IDLE_FRAME_COUNT): - frame = frame_variant(directed, template, stem, direction, "idle", col) - sheet.alpha_composite(frame, (col * FRAME_SIZE, row * FRAME_SIZE)) + for frame_index in range(IDLE_FRAME_COUNT): + frame = render_frame(plan, direction, "idle", frame_index, IDLE_FRAME_COUNT) + sheet.alpha_composite(frame, (frame_index * FRAME_SIZE, row * FRAME_SIZE)) for frame_index in range(MOVE_FRAME_COUNT): - col = IDLE_FRAME_COUNT + frame_index - frame = frame_variant(directed, template, stem, direction, "move", frame_index) - sheet.alpha_composite(frame, (col * FRAME_SIZE, row * FRAME_SIZE)) + frame = render_frame(plan, direction, "move", frame_index, MOVE_FRAME_COUNT) + sheet.alpha_composite(frame, ((IDLE_FRAME_COUNT + frame_index) * FRAME_SIZE, row * FRAME_SIZE)) return sheet -def fit_sprite(source: Image.Image, template: Template, plan: UnitPlan) -> Image.Image: - bbox = source.getbbox() - if not bbox: - return source - subject = source.crop(bbox) - width, height = subject.size - spec = ROLE_SPECS[template.role] - target_height = round(spec.height * plan.scale) - proportional_width = round(width * (target_height / height)) - target_width = min(round(spec.max_width * plan.scale), max(1, proportional_width)) - new_size = (max(1, target_width), max(1, target_height)) - subject = subject.resize(new_size, Image.Resampling.LANCZOS) - subject = subject.filter(ImageFilter.UnsharpMask(radius=0.65, percent=95, threshold=2)) - return subject - - -def orient_sprite(sprite: Image.Image, direction: str) -> Image.Image: - if direction == "east": - return side_variant(sprite) - if direction == "west": - return ImageOps_mirror(side_variant(sprite)) - if direction == "north": - return back_variant(sprite) - return sprite - - -def frame_variant(sprite: Image.Image, template: Template, stem: str, direction: str, pose: str, frame_index: int) -> Image.Image: - canvas = Image.new("RGBA", (FRAME_SIZE, FRAME_SIZE), (0, 0, 0, 0)) - seed = stable_int(f"{stem}:{direction}") - spec = ROLE_SPECS[template.role] - if pose == "move": - frame = move_pose(sprite, spec, template.role, direction, frame_index) - dx = move_body_dx(template.role, direction, frame_index) - else: - frame = idle_pose(sprite, spec, direction, frame_index) - dx = spec.idle_dx[frame_index % len(spec.idle_dx)] - if direction == "west": - dx = -dx - if direction == "north": - dx = round(dx * 0.5) - dx += seed % 3 - 1 - bottom = FRAME_SIZE - spec.bottom_padding - left = round((FRAME_SIZE - frame.width) / 2 + dx) - top = round(bottom - frame.height) - canvas.alpha_composite(frame, (left, top)) - if pose == "move": - draw_step_contact(canvas, template.role, direction, frame_index) - return finish_frame(canvas) - - -def idle_pose(sprite: Image.Image, spec: RoleSpec, direction: str, frame_index: int) -> Image.Image: - index = frame_index % IDLE_FRAME_COUNT - sx = spec.idle_scale_x[index] - sy = spec.idle_scale_y[index] - width = max(1, round(sprite.width * sx)) - height = max(1, round(sprite.height * sy)) - resized = sprite.resize((width, height), Image.Resampling.LANCZOS) - sway = 1.2 if direction in ("south", "north") else 1.8 - return row_wave(resized, sway, index / IDLE_FRAME_COUNT * math.tau, upper_bias=0.88) - - -def move_pose(sprite: Image.Image, spec: RoleSpec, role: str, direction: str, frame_index: int) -> Image.Image: - index = frame_index % MOVE_FRAME_COUNT - phase = index / MOVE_FRAME_COUNT * math.tau - stride = math.sin(phase) - contact = abs(math.cos(phase)) - mounted = role == "cavalry" - sx = 1.0 + (0.025 if mounted else 0.018) * math.cos(phase) - sy = 1.0 + (0.018 if mounted else 0.012) * contact - width = max(1, round(sprite.width * sx)) - height = max(1, round(sprite.height * sy)) - resized = sprite.resize((width, height), Image.Resampling.LANCZOS) - upper, lower = split_subject(resized, 0.58 if mounted else 0.62) - upper_shift = round(stride * (3 if mounted else 2)) - lower_shift = -round(stride * (5 if mounted else 4)) - if direction == "west": - upper_shift = -upper_shift - lower_shift = -lower_shift - if direction == "north": - upper_shift = round(upper_shift * 0.5) - lower_shift = round(lower_shift * 0.5) - composed = Image.new("RGBA", (resized.width + 16, resized.height), (0, 0, 0, 0)) - composed.alpha_composite(upper, (8 + upper_shift, 0)) - composed.alpha_composite(row_wave(lower, 2.6 if mounted else 2.0, phase + math.pi, upper_bias=0.4), (8 + lower_shift, upper.height)) - return row_wave(composed, 3.4 if mounted else 2.4, phase, upper_bias=0.7) - - -def split_subject(sprite: Image.Image, split_ratio: float) -> tuple[Image.Image, Image.Image]: - split_y = max(1, min(sprite.height - 1, round(sprite.height * split_ratio))) - return sprite.crop((0, 0, sprite.width, split_y)), sprite.crop((0, split_y, sprite.width, sprite.height)) - - -def row_wave(sprite: Image.Image, amplitude: float, phase: float, upper_bias: float) -> Image.Image: - if amplitude <= 0: - return sprite - padding = math.ceil(abs(amplitude)) + 3 - output = Image.new("RGBA", (sprite.width + padding * 2, sprite.height), (0, 0, 0, 0)) - denominator = max(1, sprite.height - 1) - for y in range(sprite.height): - vertical = y / denominator - weight = (1 - vertical) * upper_bias + vertical * (1 - upper_bias) - dx = round(math.sin(phase + vertical * math.pi * 1.6) * amplitude * weight) - output.alpha_composite(sprite.crop((0, y, sprite.width, y + 1)), (padding + dx, y)) - bbox = output.getbbox() - return output.crop(bbox) if bbox else output - - -def wave_frame_subject(frame: Image.Image, amplitude: float, phase: float, upper_bias: float) -> Image.Image: - bbox = frame.getbbox() - if not bbox: - return frame - subject = frame.crop(bbox) - waved = row_wave(subject, amplitude, phase, upper_bias) - canvas = Image.new("RGBA", frame.size, (0, 0, 0, 0)) - left = bbox[0] - max(0, (waved.width - subject.width) // 2) - top = bbox[3] - waved.height - canvas.alpha_composite(waved, (left, top)) - return canvas - - -def move_body_dx(role: str, direction: str, frame_index: int) -> int: - if direction in ("north", "south"): - return 0 - mounted = role == "cavalry" - stride = math.sin(frame_index / MOVE_FRAME_COUNT * math.tau) - return round(stride * (4 if mounted else 3)) - - -def draw_step_contact(canvas: Image.Image, role: str, direction: str, frame_index: int) -> None: - draw = ImageDraw.Draw(canvas) - phase = frame_index / MOVE_FRAME_COUNT * math.tau - contact = abs(math.cos(phase)) - if contact < 0.42: - return - y = FRAME_SIZE - 7 - center = FRAME_SIZE // 2 - mounted = role == "cavalry" - spread = 46 if mounted else 28 - alpha = round(54 + contact * 52) - color = (54, 45, 34, alpha) - if direction in ("east", "west"): - draw.ellipse((center - spread, y - 5, center - spread + 26, y + 2), fill=color) - draw.ellipse((center + spread - 26, y - 4, center + spread, y + 2), fill=color) - else: - draw.ellipse((center - spread // 2, y - 5, center + spread // 2, y + 2), fill=color) - - -def draw_action_sheet(base_sheet: Image.Image, template: Template, stem: str) -> Image.Image: - output = Image.new("RGBA", (FRAME_SIZE * ACTION_FRAMES_PER_DIRECTION, FRAME_SIZE * len(DIRECTIONS)), (0, 0, 0, 0)) +def draw_action_sheet(plan: UnitPlan) -> Image.Image: + sheet = Image.new("RGBA", (FRAME_SIZE * ACTION_FRAMES_PER_DIRECTION, FRAME_SIZE * len(DIRECTIONS)), (0, 0, 0, 0)) for row, direction in enumerate(DIRECTIONS): - for action_index, action in enumerate(ACTION_ORDER): + for action in ACTION_ORDER: frame_count = ACTION_FRAME_COUNTS[action] - start_col = ACTION_FRAME_OFFSETS[action] + start_col = ACTION_COLUMN_OFFSETS[action] for frame_index in range(frame_count): - idle_index = min(IDLE_FRAME_COUNT - 1, round(frame_index * (IDLE_FRAME_COUNT - 1) / max(1, frame_count - 1))) - frame = base_sheet.crop((idle_index * FRAME_SIZE, row * FRAME_SIZE, (idle_index + 1) * FRAME_SIZE, (row + 1) * FRAME_SIZE)).convert("RGBA") - output.alpha_composite( - action_frame(frame, template, stem, direction, action, frame_index, frame_count), - ((start_col + frame_index) * FRAME_SIZE, row * FRAME_SIZE) - ) - return output + frame = render_frame(plan, direction, action, frame_index, frame_count) + sheet.alpha_composite(frame, ((start_col + frame_index) * FRAME_SIZE, row * FRAME_SIZE)) + return sheet -def action_frame(frame: Image.Image, template: Template, stem: str, direction: str, action: str, frame_index: int, frame_count: int) -> Image.Image: - if action == "hurt": - return hurt_frame(frame, direction, frame_index) - if action == "celebrate": - return celebrate_frame(frame, direction, frame_index, frame_count) +def render_frame(plan: UnitPlan, direction: str, action: str, frame_index: int, frame_count: int) -> Image.Image: + image = Image.new("RGBA", (FRAME_SIZE * DRAW_SCALE, FRAME_SIZE * DRAW_SCALE), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + motion = motion_for(plan, direction, action, frame_index, frame_count) + body_center_x = 156 + motion.body_dx + bottom_y = 280 + motion.body_dy - effect = Image.new("RGBA", frame.size, (0, 0, 0, 0)) - draw = ImageDraw.Draw(effect) - if action == "attack": - impact_start = max(3, round(frame_count * 0.42)) - impact_end = min(frame_count - 1, impact_start + 3) - if impact_start <= frame_index <= impact_end: - draw_attack_effect(draw, template, direction, frame_index - impact_start + 2) - lunge_values = (0, -3, -6, 2, 11, 19, 15, 9, 4, 0) - mounted_lunge_values = (0, -4, -8, 4, 16, 26, 20, 12, 5, 0) - lift_values = (0, 0, -2, -5, -6, -4, -2, -1, 0, 0) - lunge = sequence_value(mounted_lunge_values if template.role == "cavalry" else lunge_values, frame_index, frame_count) - lift = sequence_value(lift_values, frame_index, frame_count) - dx, dy = direction_offset(direction, lunge) - body = offset_subject(wave_frame_subject(frame, 3.0 if template.role == "cavalry" else 2.2, frame_index / frame_count * math.tau, 0.82), dx, dy + lift) - result = Image.alpha_composite(effect, body) - elif action == "strategy": - draw_strategy_effect(draw, frame_index, frame_count) - lift = sequence_value((0, -2, -5, -7, -5, -3, -1, 0), frame_index, frame_count) - result = Image.alpha_composite(effect, offset_subject(frame, 0, lift)) + draw_ground_shadow(draw, body_center_x, bottom_y, plan, direction, motion) + + if action in ("strategy", "item"): + draw_support_effect(draw, action, direction, frame_index, frame_count, body_center_x, bottom_y) + + if plan.role == "cavalry": + draw_cavalry(draw, plan, direction, body_center_x, bottom_y, motion, action) else: - draw_item_effect(draw, direction, frame_index, frame_count) - lift = sequence_value((0, -1, -3, -5, -4, -2, -1, 0), frame_index, frame_count) - result = Image.alpha_composite(offset_subject(frame, 0, lift), effect) - return finish_frame(result) + draw_human(draw, plan, direction, body_center_x, bottom_y, motion, action) + + if action == "attack": + draw_attack_effect(draw, plan, direction, body_center_x, bottom_y, frame_index, frame_count) + elif action == "hurt": + draw_hurt_effect(draw, direction, body_center_x, bottom_y, frame_index) + elif action == "celebrate": + draw_celebrate_effect(draw, body_center_x, bottom_y, frame_index, frame_count) + + image = image.resize((FRAME_SIZE, FRAME_SIZE), Image.Resampling.LANCZOS) + if action == "hurt": + image = tint_alpha(image, (188, 38, 32), 0.22) + image = finish_frame(image) + return image -def draw_attack_effect(draw: ImageDraw.ImageDraw, template: Template, direction: str, frame_index: int = 2) -> None: - warm = (239, 177, 68, 160) - bright = (255, 237, 184, 205) - red = (178, 52, 42, 132) - if template.role == "archer": - shafts = { - "east": ((122, 132), (286, 98)), - "west": ((191, 132), (27, 98)), - "north": ((170, 179), (135, 35)), - "south": ((143, 112), (180, 283)), - } - start, end = shafts[direction] - line(draw, [start, end], bright, 6 if frame_index == 2 else 4) - line(draw, [start, end], red, 3 if frame_index == 2 else 2) - draw.polygon(arrow_tip(end, start, 19), fill=bright, outline=(25, 17, 14, 210)) +def motion_for(plan: UnitPlan, direction: str, action: str, frame_index: int, frame_count: int) -> Motion: + phase = frame_index / max(1, frame_count) * math.tau + side_sign = 1 if direction == "east" else -1 if direction == "west" else 0 + + if action == "move": + step = math.sin(phase) + lift = abs(math.sin(phase)) + mounted = plan.role == "cavalry" + return Motion( + body_dx=side_sign * step * (3.8 if mounted else 2.1), + body_dy=-(2.4 if mounted else 1.7) * lift, + lean=side_sign * step * (2.2 if mounted else 1.4), + leg=step, + arm=-step, + weapon=math.cos(phase) * (4.8 if mounted else 3.2), + horse_leg=step, + squash_x=1 + (0.014 if mounted else 0.008) * math.cos(phase), + squash_y=1 - (0.01 if mounted else 0.006) * math.cos(phase), + ) + + if action == "attack": + t = frame_index / max(1, frame_count - 1) + windup = math.sin(min(1, t / 0.38) * math.pi) if t < 0.38 else 0 + strike = math.sin(max(0, min(1, (t - 0.28) / 0.42)) * math.pi) + recover = math.sin(max(0, min(1, (t - 0.68) / 0.32)) * math.pi) + dx, dy = direction_vector(direction) + lunge = strike * (25 if plan.role == "cavalry" else 17) - windup * 6 - recover * 3 + return Motion( + body_dx=dx * lunge + side_sign * windup * -4, + body_dy=dy * lunge - strike * 4, + lean=side_sign * (strike * 6 - windup * 4), + leg=strike * 0.8 - recover * 0.35, + arm=strike, + weapon=strike * 12 - windup * 8, + lunge=strike, + impact=1 if 0.43 <= t <= 0.66 else 0, + horse_leg=strike, + ) + + if action == "hurt": + values = (0.0, 1.0, 0.72, 0.24) + power = values[min(frame_index, len(values) - 1)] + dx, dy = direction_vector(direction) + return Motion( + body_dx=-dx * 18 * power, + body_dy=-dy * 14 * power - 3 * power, + lean=-side_sign * 7 * power, + leg=-0.45 * power, + arm=0.5 * power, + weapon=-4 * power, + impact=power, + ) + + if action == "celebrate": + lift = max(0.0, math.sin(frame_index / max(1, frame_count - 1) * math.pi)) + sway = math.sin(frame_index / max(1, frame_count - 1) * math.tau) + return Motion( + body_dx=side_sign * sway * 2, + body_dy=-lift * (13 if plan.role == "cavalry" else 10), + lean=side_sign * sway * 2, + leg=sway * 0.35, + arm=sway * 0.3, + raise_arm=lift, + weapon=lift * 8, + horse_leg=sway * 0.25, + squash_y=1 + lift * 0.025, + ) + + if action == "strategy": + pulse = math.sin(phase) + return Motion(body_dy=-3 - abs(pulse) * 2, arm=0.15, raise_arm=0.55 + abs(pulse) * 0.35, weapon=pulse * 3) + + if action == "item": + lift = math.sin(min(1, frame_index / max(1, frame_count - 1)) * math.pi) + return Motion(body_dy=-lift * 5, raise_arm=lift * 0.9, arm=lift * 0.4, weapon=lift * 4) + + breathe = math.sin(phase) + mounted = plan.role == "cavalry" + return Motion( + body_dx=side_sign * breathe * (0.9 if mounted else 0.5), + body_dy=-abs(breathe) * (1.6 if mounted else 1.0), + lean=side_sign * breathe * (0.6 if mounted else 0.4), + leg=breathe * 0.15, + arm=breathe * 0.18, + weapon=breathe * 2.2, + horse_leg=breathe * 0.18, + squash_x=1 + 0.004 * breathe, + squash_y=1 - 0.006 * breathe, + ) + + +def draw_human( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + direction: str, + cx: float, + bottom: float, + motion: Motion, + action: str, + mounted: bool = False, +) -> None: + scale = plan.scale * (0.82 if mounted else 1.0) + bulk = plan.bulk + foot_y = bottom + leg_top_y = bottom - 56 * scale + hip_y = bottom - 72 * scale + chest_y = bottom - 122 * scale + shoulder_y = bottom - 134 * scale + head_y = bottom - 165 * scale + side_sign = 1 if direction == "east" else -1 if direction == "west" else 0 + facing_front = direction == "south" + facing_back = direction == "north" + side_view = direction in ("east", "west") + leg_stride = motion.leg * (23 if side_view else 9) * scale + arm_stride = motion.arm * (15 if side_view else 8) * scale + raise_arm = motion.raise_arm + lean = motion.lean + + if action == "celebrate": + raise_arm = max(raise_arm, 0.75) + + if plan.cape and not side_view: + cape_top = shoulder_y - 1 + cape_bottom = bottom - 12 * scale + cape_w = 38 * scale * bulk + cape_color = with_alpha(darken(plan.palette.secondary, 0.76), 210 if facing_back else 155) + polygon( + draw, + [ + (cx - cape_w, cape_top), + (cx + cape_w, cape_top), + (cx + cape_w * 0.78 + math.sin(motion.weapon) * 1.5, cape_bottom), + (cx - cape_w * 0.78 + math.cos(motion.weapon) * 1.5, cape_bottom), + ], + cape_color, + outline=INK, + width=3, + ) + + if side_view: + draw_human_side(draw, plan, direction, cx, bottom, scale, bulk, leg_stride, arm_stride, raise_arm, lean) + elif facing_back: + draw_human_back(draw, plan, cx, bottom, scale, bulk, leg_stride, arm_stride, raise_arm, lean) + else: + draw_human_front(draw, plan, cx, bottom, scale, bulk, leg_stride, arm_stride, raise_arm, lean) + + draw_weapon(draw, plan, direction, cx, bottom, scale, motion, action, mounted) + + +def draw_human_front( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + cx: float, + bottom: float, + scale: float, + bulk: float, + leg_stride: float, + arm_stride: float, + raise_arm: float, + lean: float, +) -> None: + hip_y = bottom - 72 * scale + knee_y = bottom - 35 * scale + shoulder_y = bottom - 134 * scale + chest_y = bottom - 116 * scale + head_y = bottom - 165 * scale + stance = 16 * scale + left_foot = (cx - stance - leg_stride * 0.18, bottom) + right_foot = (cx + stance + leg_stride * 0.18, bottom - 2 * abs(math.sin(leg_stride / 20))) + limb(draw, (cx - 12 * scale, hip_y), (cx - 17 * scale - leg_stride * 0.25, knee_y), left_foot, plan.palette.secondary, 14 * scale) + limb(draw, (cx + 12 * scale, hip_y), (cx + 17 * scale + leg_stride * 0.25, knee_y), right_foot, plan.palette.secondary, 14 * scale) + boot(draw, left_foot, 17 * scale) + boot(draw, right_foot, 17 * scale) + + body_w = 62 * scale * bulk + waist_w = 42 * scale * bulk + polygon( + draw, + [ + (cx - body_w / 2 + lean * 0.25, shoulder_y), + (cx + body_w / 2 + lean * 0.25, shoulder_y), + (cx + waist_w / 2 - lean * 0.15, hip_y), + (cx - waist_w / 2 - lean * 0.15, hip_y), + ], + with_alpha(plan.palette.armor, 255), + outline=INK, + width=4, + ) + polygon( + draw, + [ + (cx - 24 * scale, shoulder_y + 13 * scale), + (cx, chest_y + 34 * scale), + (cx + 24 * scale, shoulder_y + 13 * scale), + (cx + 14 * scale, hip_y - 3 * scale), + (cx - 14 * scale, hip_y - 3 * scale), + ], + with_alpha(plan.palette.primary, 235), + outline=with_alpha(darken(plan.palette.primary, 0.48), 190), + width=2, + ) + belt(draw, cx, hip_y, 50 * scale, plan.palette.accent) + + left_hand = (cx - 42 * scale - arm_stride * 0.18, bottom - (91 + 38 * raise_arm) * scale) + right_hand = (cx + 42 * scale + arm_stride * 0.18, bottom - (92 + 40 * raise_arm) * scale) + arm(draw, (cx - 30 * scale, shoulder_y + 10 * scale), left_hand, plan.palette.cloth, 13 * scale) + arm(draw, (cx + 30 * scale, shoulder_y + 10 * scale), right_hand, plan.palette.cloth, 13 * scale) + hand(draw, left_hand, plan.skin, 7 * scale) + hand(draw, right_hand, plan.skin, 7 * scale) + + neck(draw, cx, head_y + 21 * scale, plan.skin, scale) + head_front(draw, plan, cx + lean * 0.18, head_y, scale) + + +def draw_human_back( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + cx: float, + bottom: float, + scale: float, + bulk: float, + leg_stride: float, + arm_stride: float, + raise_arm: float, + lean: float, +) -> None: + hip_y = bottom - 72 * scale + knee_y = bottom - 35 * scale + shoulder_y = bottom - 134 * scale + head_y = bottom - 165 * scale + stance = 15 * scale + left_foot = (cx - stance + leg_stride * 0.12, bottom) + right_foot = (cx + stance - leg_stride * 0.12, bottom) + limb(draw, (cx - 11 * scale, hip_y), (cx - 16 * scale + leg_stride * 0.18, knee_y), left_foot, darken(plan.palette.secondary, 0.84), 14 * scale) + limb(draw, (cx + 11 * scale, hip_y), (cx + 16 * scale - leg_stride * 0.18, knee_y), right_foot, darken(plan.palette.secondary, 0.84), 14 * scale) + boot(draw, left_foot, 16 * scale) + boot(draw, right_foot, 16 * scale) + + body_w = 64 * scale * bulk + waist_w = 44 * scale * bulk + polygon( + draw, + [ + (cx - body_w / 2 + lean * 0.16, shoulder_y), + (cx + body_w / 2 + lean * 0.16, shoulder_y), + (cx + waist_w / 2 - lean * 0.1, hip_y), + (cx - waist_w / 2 - lean * 0.1, hip_y), + ], + with_alpha(darken(plan.palette.armor, 0.76), 255), + outline=INK, + width=4, + ) + line(draw, [(cx, shoulder_y + 8 * scale), (cx, hip_y - 6 * scale)], with_alpha(plan.palette.accent, 165), 3) + belt(draw, cx, hip_y, 48 * scale, darken(plan.palette.accent, 0.82)) + + left_hand = (cx - 42 * scale + arm_stride * 0.12, bottom - (91 + 34 * raise_arm) * scale) + right_hand = (cx + 42 * scale - arm_stride * 0.12, bottom - (92 + 36 * raise_arm) * scale) + arm(draw, (cx - 31 * scale, shoulder_y + 9 * scale), left_hand, darken(plan.palette.cloth, 0.78), 13 * scale) + arm(draw, (cx + 31 * scale, shoulder_y + 9 * scale), right_hand, darken(plan.palette.cloth, 0.78), 13 * scale) + hand(draw, left_hand, darken(plan.skin, 0.82), 7 * scale) + hand(draw, right_hand, darken(plan.skin, 0.82), 7 * scale) + + neck(draw, cx, head_y + 23 * scale, darken(plan.skin, 0.75), scale) + head_back(draw, plan, cx + lean * 0.08, head_y, scale) + + +def draw_human_side( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + direction: str, + cx: float, + bottom: float, + scale: float, + bulk: float, + leg_stride: float, + arm_stride: float, + raise_arm: float, + lean: float, +) -> None: + sign = 1 if direction == "east" else -1 + hip_y = bottom - 72 * scale + shoulder_y = bottom - 134 * scale + head_y = bottom - 165 * scale + rear_foot = (cx - sign * (14 * scale + leg_stride * 0.35), bottom) + front_foot = (cx + sign * (17 * scale + leg_stride * 0.55), bottom - 2 * abs(math.sin(leg_stride / 22))) + limb(draw, (cx - sign * 8 * scale, hip_y), (cx - sign * (15 * scale + leg_stride * 0.2), bottom - 38 * scale), rear_foot, darken(plan.palette.secondary, 0.76), 13 * scale) + limb(draw, (cx + sign * 8 * scale, hip_y), (cx + sign * (18 * scale + leg_stride * 0.3), bottom - 36 * scale), front_foot, plan.palette.secondary, 14 * scale) + boot(draw, rear_foot, 16 * scale) + boot(draw, front_foot, 18 * scale, sign) + + torso_color = plan.palette.armor + polygon( + draw, + [ + (cx - sign * 22 * scale + lean * 0.1, shoulder_y + 2 * scale), + (cx + sign * 31 * scale + lean * 0.4, shoulder_y + 9 * scale), + (cx + sign * 22 * scale - lean * 0.18, hip_y), + (cx - sign * 18 * scale - lean * 0.18, hip_y - 3 * scale), + ], + with_alpha(torso_color, 255), + outline=INK, + width=4, + ) + polygon( + draw, + [ + (cx - sign * 12 * scale, shoulder_y + 12 * scale), + (cx + sign * 23 * scale, shoulder_y + 16 * scale), + (cx + sign * 13 * scale, hip_y - 5 * scale), + (cx - sign * 12 * scale, hip_y - 6 * scale), + ], + with_alpha(plan.palette.primary, 222), + outline=with_alpha(darken(plan.palette.primary, 0.48), 175), + width=2, + ) + belt(draw, cx + sign * 3 * scale, hip_y - 2 * scale, 43 * scale, plan.palette.accent) + + rear_hand = (cx - sign * (31 * scale + arm_stride * 0.28), bottom - (98 + 26 * raise_arm) * scale) + front_hand = (cx + sign * (47 * scale + arm_stride * 0.44), bottom - (96 + 41 * raise_arm) * scale) + arm(draw, (cx - sign * 20 * scale, shoulder_y + 12 * scale), rear_hand, darken(plan.palette.cloth, 0.72), 11 * scale) + arm(draw, (cx + sign * 25 * scale, shoulder_y + 13 * scale), front_hand, plan.palette.cloth, 12 * scale) + hand(draw, rear_hand, darken(plan.skin, 0.85), 6 * scale) + hand(draw, front_hand, plan.skin, 7 * scale) + + neck(draw, cx + sign * 10 * scale, head_y + 22 * scale, plan.skin, scale) + head_side(draw, plan, direction, cx + sign * 13 * scale + lean * 0.22, head_y + 1 * scale, scale) + + +def draw_cavalry( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + direction: str, + cx: float, + bottom: float, + motion: Motion, + action: str, +) -> None: + scale = plan.scale + if direction in ("east", "west"): + draw_horse_side(draw, plan, direction, cx, bottom, scale, motion) + rider_bottom = bottom - 72 * scale + draw_human(draw, plan, direction, cx - (8 if direction == "east" else -8) * scale, rider_bottom, motion, action, mounted=True) + else: + draw_horse_front_back(draw, plan, direction, cx, bottom, scale, motion) + rider_bottom = bottom - 78 * scale + draw_human(draw, plan, direction, cx, rider_bottom, motion, action, mounted=True) + + +def draw_horse_side(draw: ImageDraw.ImageDraw, plan: UnitPlan, direction: str, cx: float, bottom: float, scale: float, motion: Motion) -> None: + sign = 1 if direction == "east" else -1 + horse = plan.palette.horse + phase = motion.horse_leg + body_y = bottom - 50 * scale + body_w = 125 * scale + body_h = 48 * scale + ellipse(draw, (cx - body_w / 2, body_y - body_h / 2, cx + body_w / 2, body_y + body_h / 2), with_alpha(horse, 255), outline=INK, width=4) + ellipse(draw, (cx - body_w * 0.45, body_y - body_h * 0.35, cx - body_w * 0.05, body_y + body_h * 0.42), with_alpha(darken(horse, 0.82), 235), outline=with_alpha(DEEP_INK[:3], 160), width=2) + + neck = [ + (cx + sign * 43 * scale, body_y - 24 * scale), + (cx + sign * 76 * scale, body_y - 58 * scale), + (cx + sign * 91 * scale, body_y - 48 * scale), + (cx + sign * 61 * scale, body_y - 11 * scale), + ] + polygon(draw, neck, with_alpha(horse, 255), outline=INK, width=4) + ellipse(draw, (cx + sign * 73 * scale, body_y - 73 * scale, cx + sign * 115 * scale, body_y - 35 * scale), with_alpha(lighten(horse, 1.06), 255), outline=INK, width=4) + ellipse(draw, (cx + sign * 101 * scale, body_y - 54 * scale, cx + sign * 128 * scale, body_y - 36 * scale), with_alpha(lighten(horse, 1.02), 255), outline=INK, width=3) + polygon(draw, [(cx + sign * 83 * scale, body_y - 72 * scale), (cx + sign * 91 * scale, body_y - 91 * scale), (cx + sign * 98 * scale, body_y - 68 * scale)], with_alpha(darken(horse, 0.82), 255), outline=INK, width=2) + ellipse(draw, (cx + sign * 111 * scale - 3 * scale, body_y - 51 * scale - 2 * scale, cx + sign * 111 * scale + 3 * scale, body_y - 51 * scale + 2 * scale), with_alpha((20, 15, 12), 255)) + line(draw, [(cx + sign * 51 * scale, body_y - 38 * scale), (cx + sign * 79 * scale, body_y - 69 * scale)], with_alpha(darken(plan.hair, 0.65), 230), 8) + line(draw, [(cx - sign * 64 * scale, body_y - 14 * scale), (cx - sign * 91 * scale, body_y - 35 * scale), (cx - sign * 103 * scale, body_y - 20 * scale)], with_alpha(darken(horse, 0.64), 230), 8) + + leg_roots = (-43, -17, 18, 44) + for index, root in enumerate(leg_roots): + stride = math.sin(math.asin(max(-1, min(1, phase))) + index * math.pi * 0.7) + far = index in (0, 3) + color = darken(horse, 0.74 if far else 0.92) + root_x = cx + root * scale + knee = (root_x + sign * stride * (13 if far else 17) * scale, bottom - 30 * scale) + hoof = (root_x - sign * stride * (18 if far else 22) * scale, bottom) + line(draw, [(root_x, body_y + 14 * scale), knee, hoof], with_alpha(color, 255), 11 if not far else 9) + ellipse(draw, (hoof[0] - 8 * scale, hoof[1] - 5 * scale, hoof[0] + 10 * scale, hoof[1] + 2 * scale), with_alpha((25, 18, 13), 255), outline=INK, width=2) + + saddle_w = 55 * scale + polygon(draw, [(cx - saddle_w / 2, body_y - 29 * scale), (cx + saddle_w / 2, body_y - 29 * scale), (cx + 31 * scale, body_y - 2 * scale), (cx - 31 * scale, body_y - 2 * scale)], with_alpha(plan.palette.secondary, 245), outline=INK, width=3) + line(draw, [(cx - 26 * scale, body_y - 1 * scale), (cx - 10 * scale, body_y + 26 * scale)], with_alpha(plan.palette.accent, 210), 3) + line(draw, [(cx + 24 * scale, body_y - 1 * scale), (cx + 8 * scale, body_y + 26 * scale)], with_alpha(plan.palette.accent, 210), 3) + + +def draw_horse_front_back(draw: ImageDraw.ImageDraw, plan: UnitPlan, direction: str, cx: float, bottom: float, scale: float, motion: Motion) -> None: + horse = plan.palette.horse + front = direction == "south" + body_y = bottom - 44 * scale + ellipse(draw, (cx - 48 * scale, body_y - 35 * scale, cx + 48 * scale, body_y + 32 * scale), with_alpha(horse, 255), outline=INK, width=4) + if front: + ellipse(draw, (cx - 28 * scale, body_y - 76 * scale, cx + 28 * scale, body_y - 22 * scale), with_alpha(lighten(horse, 1.05), 255), outline=INK, width=4) + polygon(draw, [(cx - 18 * scale, body_y - 72 * scale), (cx - 30 * scale, body_y - 94 * scale), (cx - 5 * scale, body_y - 76 * scale)], with_alpha(darken(horse, 0.82), 255), outline=INK, width=2) + polygon(draw, [(cx + 18 * scale, body_y - 72 * scale), (cx + 30 * scale, body_y - 94 * scale), (cx + 5 * scale, body_y - 76 * scale)], with_alpha(darken(horse, 0.82), 255), outline=INK, width=2) + ellipse(draw, (cx - 14 * scale, body_y - 52 * scale, cx - 8 * scale, body_y - 46 * scale), with_alpha((15, 12, 10), 255)) + ellipse(draw, (cx + 8 * scale, body_y - 52 * scale, cx + 14 * scale, body_y - 46 * scale), with_alpha((15, 12, 10), 255)) + else: + ellipse(draw, (cx - 32 * scale, body_y - 2 * scale, cx + 32 * scale, body_y + 41 * scale), with_alpha(darken(horse, 0.88), 255), outline=INK, width=3) + line(draw, [(cx, body_y + 19 * scale), (cx, body_y + 54 * scale)], with_alpha(darken(horse, 0.58), 235), 9) + + phase = motion.horse_leg + for index, xoff in enumerate((-31, -12, 12, 31)): + stride = math.sin(math.asin(max(-1, min(1, phase))) + index * math.pi * 0.75) + root = (cx + xoff * scale, body_y + 8 * scale) + knee = (cx + (xoff + stride * 7) * scale, bottom - 29 * scale) + hoof = (cx + (xoff - stride * 9) * scale, bottom) + color = horse if index in (1, 2) else darken(horse, 0.72) + line(draw, [root, knee, hoof], with_alpha(color, 255), 10 if index in (1, 2) else 8) + ellipse(draw, (hoof[0] - 8 * scale, hoof[1] - 5 * scale, hoof[0] + 8 * scale, hoof[1] + 2 * scale), with_alpha((24, 17, 13), 255), outline=INK, width=2) + + polygon(draw, [(cx - 36 * scale, body_y - 33 * scale), (cx + 36 * scale, body_y - 33 * scale), (cx + 28 * scale, body_y - 8 * scale), (cx - 28 * scale, body_y - 8 * scale)], with_alpha(plan.palette.secondary, 245), outline=INK, width=3) + + +def draw_weapon( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + direction: str, + cx: float, + bottom: float, + scale: float, + motion: Motion, + action: str, + mounted: bool, +) -> None: + sign = 1 if direction == "east" else -1 if direction == "west" else 0 + dx, dy = direction_vector(direction) + attack = action == "attack" + celebrate = action == "celebrate" + forward = motion.lunge + weapon = plan.weapon + hand_y = bottom - (104 if mounted else 102) * scale - motion.raise_arm * 34 * scale + hand_x = cx + (sign * (31 if mounted else 42) * scale if sign else 27 * scale) + + if weapon in ("spear", "lance", "halberd"): + length = (112 if mounted else 102) * scale + if direction == "south": + start = (hand_x - 10 * scale, hand_y - 34 * scale + motion.weapon * 0.08) + end = (hand_x + dx * 4 * scale, hand_y + length * (0.66 + 0.28 * forward)) + elif direction == "north": + start = (hand_x - 18 * scale, hand_y + 38 * scale) + end = (hand_x - 24 * scale, hand_y - length * (0.73 + 0.22 * forward)) + else: + start = (hand_x - sign * 43 * scale, hand_y + 15 * scale) + end = (hand_x + sign * length * (0.88 + 0.32 * forward), hand_y - 19 * scale - motion.weapon * 0.25) + line(draw, [start, end], with_alpha((101, 66, 36), 255), 7 if not mounted else 8) + line(draw, [start, end], with_alpha(lighten(plan.palette.accent, 1.12), 190), 2) + spear_tip(draw, start, end, plan.metal, 18 * scale if weapon != "halberd" else 24 * scale) + if weapon == "halberd": + draw_halberd_hook(draw, start, end, plan.palette.accent, scale) return - if template.role == "strategist": - draw_strategy_effect(draw, frame_index) + if weapon == "bow": + bow_x = hand_x + sign * 9 * scale if sign else cx + 34 * scale + bow_y = hand_y + 2 * scale + if direction in ("east", "west"): + arc_box = (bow_x - sign * 32 * scale, bow_y - 48 * scale, bow_x + sign * 26 * scale, bow_y + 49 * scale) + normalized = normalize_box(arc_box) + start, end = (-72, 72) if direction == "east" else (108, 252) + arc(draw, normalized, start, end, with_alpha(plan.palette.accent, 255), 6) + line(draw, [(bow_x, bow_y - 45 * scale), (bow_x, bow_y + 45 * scale)], with_alpha((233, 222, 186), 220), 2) + if attack: + line(draw, [(bow_x - sign * 22 * scale, bow_y), (bow_x + sign * 92 * scale, bow_y - 14 * scale)], with_alpha(plan.metal, 240), 4) + spear_tip(draw, (bow_x, bow_y), (bow_x + sign * 102 * scale, bow_y - 15 * scale), plan.metal, 11 * scale) + else: + arc(draw, (cx - 50 * scale, hand_y - 22 * scale, cx + 50 * scale, hand_y + 42 * scale), 15 if direction == "south" else 195, 165 if direction == "south" else 345, with_alpha(plan.palette.accent, 255), 6) + if attack: + end = (cx + dx * 10 * scale, hand_y + dy * 92 * scale) + line(draw, [(cx, hand_y), end], with_alpha(plan.metal, 240), 4) + spear_tip(draw, (cx, hand_y), end, plan.metal, 11 * scale) return - arcs = { - "east": ((132, 58, 303, 224), -55, 55), - "west": ((10, 58, 181, 224), 125, 235), - "north": ((50, 14, 267, 182), 204, 336), - "south": ((45, 113, 269, 295), 25, 155), - } - box, start, end = arcs[direction] - width = 17 if frame_index == 2 else 11 - draw.arc(box, start=start, end=end, fill=warm, width=width) - inset = (box[0] + 15, box[1] + 15, box[2] - 15, box[3] - 15) - draw.arc(inset, start=start + 4, end=end - 4, fill=bright, width=max(4, width - 10)) + if weapon == "fan": + fan_cx = hand_x + (sign * 8 * scale if sign else 0) + fan_cy = hand_y - 4 * scale + fan_r = (21 + 5 * motion.raise_arm) * scale + for blade in range(5): + angle = -68 + blade * 34 + if direction == "north": + angle += 180 + elif direction == "east": + angle -= 22 + elif direction == "west": + angle = 180 - angle + 22 + end = (fan_cx + math.cos(math.radians(angle)) * fan_r, fan_cy + math.sin(math.radians(angle)) * fan_r) + line(draw, [(fan_cx, fan_cy), end], with_alpha(plan.palette.accent, 230), 3) + pieslice(draw, (fan_cx - fan_r, fan_cy - fan_r, fan_cx + fan_r, fan_cy + fan_r), 205 if direction != "west" else -25, 335 if direction != "west" else 155, with_alpha((237, 232, 207), 215), outline=INK, width=2) + return + + blade_len = 58 * scale + if direction == "south": + start = (hand_x, hand_y + 2 * scale) + end = (hand_x + 28 * scale + motion.weapon * 0.2, hand_y + blade_len * (0.58 + forward * 0.45)) + elif direction == "north": + start = (hand_x - 18 * scale, hand_y + 4 * scale) + end = (hand_x - 40 * scale - motion.weapon * 0.12, hand_y - blade_len * (0.72 + forward * 0.38)) + else: + start = (hand_x - sign * 22 * scale, hand_y + 6 * scale) + end = (hand_x + sign * blade_len * (0.82 + forward * 0.65), hand_y - 17 * scale) + line(draw, [start, end], with_alpha(plan.metal, 255), 9) + line(draw, [start, end], with_alpha((249, 247, 227), 170), 3) + line(draw, [(start[0] - 11 * scale, start[1] + 5 * scale), (start[0] + 11 * scale, start[1] - 5 * scale)], with_alpha(plan.palette.accent, 245), 5) -def draw_strategy_effect(draw: ImageDraw.ImageDraw, frame_index: int = 0, frame_count: int = 4) -> None: - center = (156, 151) - pulse = round((1 - math.cos(frame_index / max(1, frame_count - 1) * math.tau)) * 9) - for radius, color, width in ((88 + pulse, (39, 94, 156, 66), 4), (58 + pulse // 2, (46, 142, 174, 106), 4), (28 + pulse // 3, (236, 184, 74, 134), 5)): - draw.ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), outline=color, width=width) - for angle in range(0, 360, 60): - x = center[0] + math.cos(math.radians(angle)) * 72 - y = center[1] + math.sin(math.radians(angle)) * 72 - draw.polygon([(x, y - 7), (x + 5, y), (x, y + 7), (x - 5, y)], fill=(236, 184, 74, 126)) +def draw_attack_effect( + draw: ImageDraw.ImageDraw, + plan: UnitPlan, + direction: str, + cx: float, + bottom: float, + frame_index: int, + frame_count: int, +) -> None: + t = frame_index / max(1, frame_count - 1) + if t < 0.38 or t > 0.78: + return + alpha = round(210 * math.sin((t - 0.38) / 0.4 * math.pi)) + warm = with_alpha(lighten(plan.palette.accent, 1.25), alpha) + white = (255, 245, 211, max(0, alpha - 30)) + dx, dy = direction_vector(direction) + if plan.weapon == "bow": + end = (cx + dx * 126 * DRAW_SCALE, bottom - 106 * DRAW_SCALE + dy * 96 * DRAW_SCALE) + start = (cx + dx * 28 * DRAW_SCALE, bottom - 104 * DRAW_SCALE + dy * 18 * DRAW_SCALE) + line_raw(draw, [start, end], white, 6 * DRAW_SCALE) + line_raw(draw, [start, end], warm, 2 * DRAW_SCALE) + return + if plan.weapon == "fan": + center = (cx * DRAW_SCALE + dx * 22 * DRAW_SCALE, (bottom - 122) * DRAW_SCALE + dy * 30 * DRAW_SCALE) + for radius in (29, 48, 69): + ellipse_raw(draw, (center[0] - radius * DRAW_SCALE, center[1] - radius * DRAW_SCALE, center[0] + radius * DRAW_SCALE, center[1] + radius * DRAW_SCALE), None, outline=with_alpha((96, 184, 222), max(40, alpha // 2)), width=3 * DRAW_SCALE) + return + if direction == "east": + box = (cx + 3, bottom - 213, cx + 164, bottom - 45) + start, end = -48, 68 + elif direction == "west": + box = (cx - 164, bottom - 213, cx - 3, bottom - 45) + start, end = 112, 228 + elif direction == "north": + box = (cx - 104, bottom - 274, cx + 104, bottom - 76) + start, end = 205, 335 + else: + box = (cx - 103, bottom - 166, cx + 103, bottom + 12) + start, end = 25, 155 + arc(draw, box, start, end, warm, 18) + inset = (box[0] + 13, box[1] + 13, box[2] - 13, box[3] - 13) + arc(draw, inset, start + 5, end - 5, white, 8) -def draw_item_effect(draw: ImageDraw.ImageDraw, direction: str, frame_index: int = 0, frame_count: int = 4) -> None: - positions = { - "south": (184, 145), - "east": (220, 146), - "north": (126, 137), - "west": (92, 146), - } - x, y = positions[direction] - y += sequence_value((0, -5, -11, -15, -12, -7, -2, 0), frame_index, frame_count) - draw.rounded_rectangle((x - 20, y - 16, x + 20, y + 20), radius=7, fill=(126, 79, 39, 235), outline=(25, 18, 14, 235), width=3) - line(draw, [(x - 27, y + 1), (x + 27, y + 1)], (226, 178, 72, 196), 4) - draw.ellipse((x - 11, y - 31, x + 11, y - 9), fill=(76, 178, 112, 200), outline=(24, 18, 14, 220), width=2) +def draw_support_effect( + draw: ImageDraw.ImageDraw, + action: str, + direction: str, + frame_index: int, + frame_count: int, + cx: float, + bottom: float, +) -> None: + t = frame_index / max(1, frame_count - 1) + pulse = math.sin(t * math.pi) + center = (cx, bottom - 114) + if action == "strategy": + for radius, color in ((72 + pulse * 11, (75, 160, 213, 78)), (44 + pulse * 8, (221, 186, 93, 115)), (22 + pulse * 5, (244, 237, 191, 142))): + ellipse(draw, (center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), None, outline=color, width=4) + for angle in range(0, 360, 72): + x = center[0] + math.cos(math.radians(angle + frame_index * 13)) * (62 + pulse * 6) + y = center[1] + math.sin(math.radians(angle + frame_index * 13)) * (44 + pulse * 6) + diamond(draw, x, y, 7, (239, 214, 128, 125)) + elif action == "item": + x = center[0] + direction_vector(direction)[0] * 27 + y = center[1] - 8 - pulse * 22 + rounded_rect(draw, (x - 19, y - 17, x + 19, y + 21), 6, (125, 80, 39, 230), outline=INK, width=3) + line(draw, [(x - 26, y + 2), (x + 26, y + 2)], (230, 190, 82, 210), 4) + ellipse(draw, (x - 10, y - 33, x + 10, y - 12), (76, 174, 112, 210), outline=(235, 242, 207, 220), width=2) -def hurt_frame(frame: Image.Image, direction: str, frame_index: int = 0) -> Image.Image: - knockback = [0, -10, -15, -5][frame_index] - dx, dy = direction_offset(direction, knockback) - body = offset_subject(frame, dx, dy + [0, -2, 2, 0][frame_index]) - body = ImageEnhance.Color(body).enhance(0.74) - alpha = body.getchannel("A") - red = Image.new("RGBA", body.size, (188, 42, 36, 0)) - red.putalpha(alpha.point(lambda value: min(86, value // 3))) - body = Image.alpha_composite(body, red) - effect = Image.new("RGBA", frame.size, (0, 0, 0, 0)) - draw = ImageDraw.Draw(effect) - if frame_index == 0: - return finish_frame(body) - cx, cy = (194, 122) if direction in ("west", "north") else (120, 122) - cy += -4 if frame_index == 1 else 4 - spikes = [(cx, cy - 31), (cx + 9, cy - 9), (cx + 34, cy - 6), (cx + 13, cy + 8), (cx + 23, cy + 31), (cx, cy + 15), (cx - 24, cy + 30), (cx - 14, cy + 7), (cx - 34, cy - 6), (cx - 9, cy - 9)] - draw.polygon(spikes, fill=(232, 176, 72, 190), outline=(138, 31, 31, 220)) - line(draw, [(cx - 38, cy - 34), (cx + 36, cy + 34)], (255, 231, 181, 166), 5) - return finish_frame(Image.alpha_composite(body, effect)) +def draw_hurt_effect(draw: ImageDraw.ImageDraw, direction: str, cx: float, bottom: float, frame_index: int) -> None: + if frame_index <= 0: + return + dx, dy = direction_vector(direction) + center = (cx + dx * 34, bottom - 123 + dy * 26) + spikes = [] + for index in range(12): + angle = index / 12 * math.tau + radius = 36 if index % 2 == 0 else 15 + spikes.append((center[0] + math.cos(angle) * radius, center[1] + math.sin(angle) * radius)) + polygon(draw, spikes, (234, 176, 72, 178), outline=(138, 32, 28, 230), width=2) + line(draw, [(center[0] - 38, center[1] - 34), (center[0] + 38, center[1] + 34)], (255, 237, 199, 170), 5) -def celebrate_frame(frame: Image.Image, direction: str, frame_index: int = 0, frame_count: int = 4) -> Image.Image: - lift = sequence_value((0, -6, -11, -5, -9, 0), frame_index, frame_count) - sway = sequence_value((0, -2, 1, 3, -1, 0), frame_index, frame_count) - if direction == "west": - sway = -sway - body = offset_subject(frame, sway, lift) - effect = Image.new("RGBA", frame.size, (0, 0, 0, 0)) - draw = ImageDraw.Draw(effect) - cx = 156 + sway - cy = 92 + lift +def draw_celebrate_effect(draw: ImageDraw.ImageDraw, cx: float, bottom: float, frame_index: int, frame_count: int) -> None: + t = frame_index / max(1, frame_count - 1) if frame_index in (1, 2, 4): - draw.ellipse((cx - 42, cy - 26, cx + 42, cy + 58), outline=(255, 220, 114, 118), width=5) + center = (cx, bottom - 166 - math.sin(t * math.pi) * 10) + ellipse(draw, (center[0] - 46, center[1] - 27, center[0] + 46, center[1] + 57), None, outline=(255, 220, 115, 120), width=5) for angle in range(20, 360, 72): - x = cx + math.cos(math.radians(angle)) * 54 - y = cy + math.sin(math.radians(angle)) * 40 - draw.polygon([(x, y - 8), (x + 5, y), (x, y + 8), (x - 5, y)], fill=(255, 226, 132, 148)) - return finish_frame(Image.alpha_composite(body, effect)) + x = center[0] + math.cos(math.radians(angle + frame_index * 9)) * 58 + y = center[1] + math.sin(math.radians(angle + frame_index * 9)) * 42 + diamond(draw, x, y, 8, (255, 226, 132, 150)) -def sequence_value(values: tuple[int, ...], frame_index: int, frame_count: int) -> int: - if frame_count <= 1 or len(values) <= 1: - return values[0] - position = frame_index * (len(values) - 1) / max(1, frame_count - 1) - left = math.floor(position) - right = min(len(values) - 1, left + 1) - mix = position - left - return round(values[left] * (1 - mix) + values[right] * mix) +def draw_ground_shadow(draw: ImageDraw.ImageDraw, cx: float, bottom: float, plan: UnitPlan, direction: str, motion: Motion) -> None: + width = 118 if plan.role == "cavalry" and direction in ("east", "west") else 72 if plan.role != "cavalry" else 88 + height = 17 if plan.role != "cavalry" else 20 + ellipse(draw, (cx - width / 2, bottom - height / 2 + 3, cx + width / 2, bottom + height / 2 + 3), GROUND_SHADOW) + + +def limb(draw: ImageDraw.ImageDraw, start: tuple[float, float], mid: tuple[float, float], end: tuple[float, float], color: Color, width: float) -> None: + line(draw, [start, mid, end], with_alpha(darken(color, 0.72), 255), width + 4) + line(draw, [start, mid, end], with_alpha(color, 255), width) + + +def arm(draw: ImageDraw.ImageDraw, shoulder: tuple[float, float], hand_pos: tuple[float, float], color: Color, width: float) -> None: + elbow = ((shoulder[0] + hand_pos[0]) / 2, (shoulder[1] + hand_pos[1]) / 2 + 7 * DRAW_SCALE / DRAW_SCALE) + line(draw, [shoulder, elbow, hand_pos], with_alpha(darken(color, 0.55), 255), width + 4) + line(draw, [shoulder, elbow, hand_pos], with_alpha(color, 255), width) + + +def hand(draw: ImageDraw.ImageDraw, pos: tuple[float, float], skin: Color, radius: float) -> None: + ellipse(draw, (pos[0] - radius, pos[1] - radius, pos[0] + radius, pos[1] + radius), with_alpha(skin, 255), outline=INK, width=2) + + +def boot(draw: ImageDraw.ImageDraw, pos: tuple[float, float], width: float, sign: int = 0) -> None: + x, y = pos + forward = sign * width * 0.28 + ellipse(draw, (x - width / 2 + forward, y - width * 0.28, x + width / 2 + forward, y + width * 0.14), with_alpha((31, 22, 16), 255), outline=INK, width=2) + + +def belt(draw: ImageDraw.ImageDraw, cx: float, y: float, width: float, color: Color) -> None: + rounded_rect(draw, (cx - width / 2, y - 7, cx + width / 2, y + 4), 3, with_alpha((55, 33, 18), 238), outline=INK, width=2) + rounded_rect(draw, (cx - 8, y - 8, cx + 8, y + 5), 2, with_alpha(color, 245), outline=DEEP_INK, width=1) + + +def neck(draw: ImageDraw.ImageDraw, cx: float, y: float, skin: Color, scale: float) -> None: + rounded_rect(draw, (cx - 9 * scale, y - 7 * scale, cx + 9 * scale, y + 13 * scale), 5 * scale, with_alpha(darken(skin, 0.9), 255), outline=INK, width=2) + + +def head_front(draw: ImageDraw.ImageDraw, plan: UnitPlan, cx: float, cy: float, scale: float) -> None: + ellipse(draw, (cx - 22 * scale, cy - 25 * scale, cx + 22 * scale, cy + 23 * scale), with_alpha(plan.skin, 255), outline=INK, width=3) + if plan.helmet: + helmet(draw, plan, cx, cy - 8 * scale, scale, "front") + else: + ellipse(draw, (cx - 24 * scale, cy - 29 * scale, cx + 24 * scale, cy - 1 * scale), with_alpha(plan.hair, 255), outline=INK, width=2) + eye(draw, cx - 8 * scale, cy - 4 * scale, scale) + eye(draw, cx + 8 * scale, cy - 4 * scale, scale) + line(draw, [(cx - 7 * scale, cy + 11 * scale), (cx + 7 * scale, cy + 11 * scale)], with_alpha((73, 35, 25), 210), 2) + if "guan-yu" in plan.stem or "zhang-fei" in plan.stem or plan.rank >= 2: + line(draw, [(cx - 11 * scale, cy + 15 * scale), (cx, cy + 29 * scale), (cx + 11 * scale, cy + 15 * scale)], with_alpha(darken(plan.hair, 0.72), 220), 5) + + +def head_back(draw: ImageDraw.ImageDraw, plan: UnitPlan, cx: float, cy: float, scale: float) -> None: + ellipse(draw, (cx - 22 * scale, cy - 25 * scale, cx + 22 * scale, cy + 23 * scale), with_alpha(darken(plan.skin, 0.82), 255), outline=INK, width=3) + if plan.helmet: + helmet(draw, plan, cx, cy - 8 * scale, scale, "back") + else: + ellipse(draw, (cx - 26 * scale, cy - 30 * scale, cx + 26 * scale, cy + 10 * scale), with_alpha(plan.hair, 255), outline=INK, width=2) + ellipse(draw, (cx - 10 * scale, cy - 38 * scale, cx + 10 * scale, cy - 20 * scale), with_alpha(darken(plan.hair, 0.72), 255), outline=INK, width=2) + line(draw, [(cx - 13 * scale, cy + 2 * scale), (cx + 13 * scale, cy + 2 * scale)], with_alpha(darken(plan.palette.accent, 0.84), 160), 2) + + +def head_side(draw: ImageDraw.ImageDraw, plan: UnitPlan, direction: str, cx: float, cy: float, scale: float) -> None: + sign = 1 if direction == "east" else -1 + ellipse(draw, (cx - 21 * scale, cy - 24 * scale, cx + 21 * scale, cy + 23 * scale), with_alpha(plan.skin, 255), outline=INK, width=3) + polygon(draw, [(cx + sign * 15 * scale, cy - 6 * scale), (cx + sign * 32 * scale, cy + 1 * scale), (cx + sign * 14 * scale, cy + 7 * scale)], with_alpha(plan.skin, 255), outline=INK, width=2) + if plan.helmet: + helmet(draw, plan, cx - sign * 2 * scale, cy - 9 * scale, scale, "side", sign) + else: + ellipse(draw, (cx - 23 * scale, cy - 29 * scale, cx + 20 * scale, cy + 1 * scale), with_alpha(plan.hair, 255), outline=INK, width=2) + ellipse(draw, (cx - sign * 22 * scale, cy - 3 * scale, cx - sign * 4 * scale, cy + 19 * scale), with_alpha(darken(plan.hair, 0.78), 245), outline=INK, width=2) + eye(draw, cx + sign * 9 * scale, cy - 4 * scale, scale) + line(draw, [(cx + sign * 5 * scale, cy + 12 * scale), (cx + sign * 16 * scale, cy + 12 * scale)], with_alpha((73, 35, 25), 210), 2) + + +def helmet(draw: ImageDraw.ImageDraw, plan: UnitPlan, cx: float, cy: float, scale: float, view: str, sign: int = 1) -> None: + metal = darken(plan.metal, 0.78) + ellipse(draw, (cx - 25 * scale, cy - 18 * scale, cx + 25 * scale, cy + 17 * scale), with_alpha(metal, 255), outline=INK, width=3) + rounded_rect(draw, (cx - 21 * scale, cy + 2 * scale, cx + 21 * scale, cy + 16 * scale), 3 * scale, with_alpha(plan.palette.accent, 235), outline=INK, width=2) + if view == "side": + line(draw, [(cx - sign * 3 * scale, cy - 19 * scale), (cx + sign * 23 * scale, cy - 35 * scale)], with_alpha(plan.palette.accent, 240), 4) + else: + line(draw, [(cx, cy - 21 * scale), (cx, cy - 42 * scale)], with_alpha(plan.palette.accent, 240), 5) + + +def eye(draw: ImageDraw.ImageDraw, x: float, y: float, scale: float) -> None: + ellipse(draw, (x - 2.2 * scale, y - 2.2 * scale, x + 2.2 * scale, y + 2.2 * scale), with_alpha((20, 16, 13), 255)) + + +def spear_tip(draw: ImageDraw.ImageDraw, start: tuple[float, float], end: tuple[float, float], color: Color, size: float) -> None: + sx, sy = start + ex, ey = end + angle = math.atan2(ey - sy, ex - sx) + left = angle + math.radians(150) + right = angle - math.radians(150) + points = [ + (ex, ey), + (ex + math.cos(left) * size, ey + math.sin(left) * size), + (ex - math.cos(angle) * size * 0.34, ey - math.sin(angle) * size * 0.34), + (ex + math.cos(right) * size, ey + math.sin(right) * size), + ] + polygon(draw, points, with_alpha(color, 255), outline=INK, width=2) + + +def draw_halberd_hook(draw: ImageDraw.ImageDraw, start: tuple[float, float], end: tuple[float, float], color: Color, scale: float) -> None: + sx, sy = start + ex, ey = end + angle = math.atan2(ey - sy, ex - sx) + base_x = ex - math.cos(angle) * 18 * scale + base_y = ey - math.sin(angle) * 18 * scale + normal = angle + math.pi / 2 + hook = [ + (base_x, base_y), + (base_x + math.cos(normal) * 22 * scale, base_y + math.sin(normal) * 22 * scale), + (base_x + math.cos(angle) * 11 * scale + math.cos(normal) * 8 * scale, base_y + math.sin(angle) * 11 * scale + math.sin(normal) * 8 * scale), + ] + polygon(draw, hook, with_alpha(color, 235), outline=INK, width=2) + + +def finish_frame(frame: Image.Image) -> Image.Image: + frame = clean_alpha(frame) + alpha = frame.getchannel("A") + if not alpha.getbbox(): + return frame + outer = ImageChops.subtract(alpha.filter(ImageFilter.MaxFilter(5)), alpha) + inner = ImageChops.subtract(alpha, alpha.filter(ImageFilter.MinFilter(3))) + rim = Image.new("RGBA", frame.size, (8, 6, 6, 0)) + rim.putalpha(outer.point(lambda value: min(190, round(value * 0.72)))) + inner_ink = Image.new("RGBA", frame.size, (24, 17, 13, 0)) + inner_ink.putalpha(inner.point(lambda value: min(58, round(value * 0.2)))) + result = Image.new("RGBA", frame.size, (0, 0, 0, 0)) + result.alpha_composite(rim) + result.alpha_composite(frame) + result.alpha_composite(inner_ink) + result = ImageEnhance.Contrast(result).enhance(1.04) + return clean_alpha(result) + + +def tint_alpha(image: Image.Image, color: Color, strength: float) -> Image.Image: + image = image.convert("RGBA") + alpha = image.getchannel("A") + overlay = Image.new("RGBA", image.size, (*color, 0)) + overlay.putalpha(alpha.point(lambda value: min(255, round(value * strength)))) + return Image.alpha_composite(image, overlay) + + +def clean_alpha(image: Image.Image) -> Image.Image: + image = image.convert("RGBA") + red, green, blue, alpha = image.split() + alpha = alpha.point(lambda value: 0 if value < 4 else 255 if value > 250 else value) + return Image.merge("RGBA", (red, green, blue, alpha)) + + +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 write_preview_sheet(cache: dict[str, tuple[Image.Image, Image.Image]], path: Path) -> None: + thumb = 72 + label_w = 230 + gap = 6 + row_h = thumb + 16 + header_h = 34 + actions = ("idle", "move", "attack") + rows_per_unit = len(DIRECTIONS) * len(actions) + width = label_w + 10 * (thumb + gap) + 24 + height = len(REPRESENTATIVE_UNITS) * (header_h + rows_per_unit * row_h + 18) + 18 + preview = Image.new("RGB", (width, height), (31, 34, 28)) + draw = ImageDraw.Draw(preview) + font = load_font(16) + small_font = load_font(13) + title_font = load_font(18) + y = 12 + for stem in REPRESENTATIVE_UNITS: + base, actions_sheet = cache[stem] + plan = unit_plan(stem) + draw.rectangle((10, y, width - 10, y + header_h - 4), fill=(45, 51, 40), outline=(116, 96, 56)) + draw.text((22, y + 6), f"{stem} role={plan.role} weapon={plan.weapon}", fill=(239, 223, 184), font=title_font) + y += header_h + for action in actions: + for direction in DIRECTIONS: + draw.text((22, y + 22), f"{action:<6} {direction}", fill=(218, 218, 201), font=small_font) + frames = preview_frames(base, actions_sheet, action, direction) + for index, frame in enumerate(frames): + x = label_w + index * (thumb + gap) + tile = checker_thumb(frame, thumb) + preview.paste(tile, (x, y + 4)) + draw.text((x + 2, y + thumb + 3), str(index), fill=(178, 171, 144), font=small_font) + y += row_h + y += 18 + preview.save(path, optimize=True) + + +def preview_frames(base: Image.Image, actions_sheet: Image.Image, action: str, direction: str) -> list[Image.Image]: + row = DIRECTIONS.index(direction) + if action == "idle": + return [crop_frame(base, row, col) for col in range(IDLE_FRAME_COUNT)] + if action == "move": + return [crop_frame(base, row, IDLE_FRAME_COUNT + col) for col in range(MOVE_FRAME_COUNT)] + start = ACTION_COLUMN_OFFSETS[action] + count = ACTION_FRAME_COUNTS[action] + return [crop_frame(actions_sheet, row, start + col) for col in range(count)] + + +def checker_thumb(frame: Image.Image, size: int) -> Image.Image: + bg = Image.new("RGB", (size, size), (47, 52, 42)) + draw = ImageDraw.Draw(bg) + cell = 12 + for y in range(0, size, cell): + for x in range(0, size, cell): + if (x // cell + y // cell) % 2 == 0: + draw.rectangle((x, y, x + cell - 1, y + cell - 1), fill=(39, 43, 35)) + frame = frame.resize((size, size), Image.Resampling.LANCZOS) + bg.paste(frame, (0, 0), frame) + draw.rectangle((0, 0, size - 1, size - 1), outline=(100, 91, 65)) + return bg + + +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)).convert("RGBA") + + +def load_font(size: int) -> ImageFont.ImageFont: + candidates = ( + Path("C:/Windows/Fonts/arial.ttf"), + Path("C:/Windows/Fonts/malgun.ttf"), + Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), + ) + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size) + return ImageFont.load_default() def unit_plan(stem: str) -> UnitPlan: - if stem in SIGNATURE_PLANS: - return SIGNATURE_PLANS[stem] - - faction = infer_faction(stem) - role = infer_role(stem) - tint = FACTION_TINTS[faction] - strength = 0.16 - if faction == "rebel": - strength = 0.12 - if faction == "nanman": - strength = 0.16 - if faction == "shu": - strength = 0.08 - if faction == "wei": - strength = 0.2 - if faction == "wu": - strength = 0.18 - - template_key = template_for(faction, role, stem) - saturation = 1.03 if "veteran" in stem or "elite" in stem or "warlord" in stem else 1.0 - brightness = 1.03 if "white" in stem else 1.0 - return UnitPlan(template_key, tint, strength, saturation, brightness, 1.0) - - -def template_for(faction: str, role: str, stem: str) -> str: - if faction == "nanman": - if role == "strategist" or "shaman" in stem: - return "nanman-officer" - return "nanman-officer" if role in {"officer", "cavalry", "polearm"} else "rebel-infantry" - if faction == "wu": - if role == "strategist": - return "strategist" - if role == "cavalry": - return "rebel-cavalry" - if role == "archer": - return "rebel-archer" - return "wu-officer" - if faction == "wei": - if role == "strategist": - return "strategist" - if role == "cavalry": - return "red-cavalry" - if role == "archer": - return "rebel-archer" - return "wei-lord" if role == "officer" else "rebel-infantry" - if faction == "rebel": - if role == "cavalry": - return "rebel-cavalry" - if role == "archer": - return "rebel-archer" - if role == "officer" or role == "polearm": - return "rebel-infantry" - return "rebel-infantry" - - if role == "cavalry": - return "white-cavalry" - if role == "archer": - return "rebel-archer" - if role == "strategist": - return "strategist" - if role == "polearm": - return "green-polearm" - return "shu-lord" if role == "officer" else "green-polearm" - - -def infer_faction(stem: str) -> str: lower = stem.lower() + faction = infer_faction(lower) + role = infer_role(lower) + palette = faction_palette(faction, stable_int(stem)) + skin = varied_skin(stem) + hair = varied_hair(stem) + metal = varied_metal(stem) + weapon = weapon_for(role, lower) + cape = role in {"lord", "officer", "strategist"} or any(token in lower for token in ("liu-bei", "cao-cao", "guan-yu", "lu-bu", "zhao-yun", "ma-chao")) + helmet = role in {"lord", "officer", "spearman", "cavalry"} or any(token in lower for token in ("guard", "veteran", "elite", "leader")) + banner = any(token in lower for token in ("banner", "leader", "warlord")) + rank = 2 if any(token in lower for token in ("liu-bei", "guan-yu", "zhang-fei", "zhao-yun", "cao-cao", "lu-bu", "zhuge", "sima", "ma-chao", "meng-huo")) else 1 if any(token in lower for token in ("officer", "leader", "veteran", "elite", "guard")) else 0 + scale = 1.18 + bulk = 1.0 + + signature = signature_plan(stem) + if signature: + palette = signature.get("palette", palette) + role = signature.get("role", role) + weapon = signature.get("weapon", weapon) + cape = signature.get("cape", cape) + helmet = signature.get("helmet", helmet) + scale = signature.get("scale", scale) + bulk = signature.get("bulk", bulk) + rank = max(rank, signature.get("rank", rank)) + + if role == "strategist": + scale *= 0.96 + bulk *= 0.88 + elif role == "archer": + scale *= 0.98 + bulk *= 0.92 + elif role == "cavalry": + scale *= 1.02 + bulk *= 0.98 + elif role in {"spearman", "officer", "lord"}: + scale *= 1.02 + bulk *= 1.02 + if "nanman" in lower or "meng-huo" in lower: + bulk *= 1.08 + if "ragged" in lower or "scout" in lower: + scale *= 0.97 + bulk *= 0.94 + + return UnitPlan(stem, faction, role, weapon, palette, skin, hair, metal, cape, helmet, banner, scale, bulk, rank) + + +def signature_plan(stem: str) -> dict[str, object] | None: + plans: dict[str, dict[str, object]] = { + "unit-liu-bei": { + "role": "lord", + "weapon": "sword", + "palette": Palette((50, 122, 76), (39, 79, 72), (215, 174, 79), (78, 126, 92), (91, 108, 98), (176, 156, 118), (234, 213, 142)), + "cape": True, + "helmet": True, + "rank": 2, + }, + "unit-guan-yu": { + "role": "spearman", + "weapon": "halberd", + "palette": Palette((34, 112, 63), (32, 74, 53), (222, 179, 75), (69, 130, 79), (78, 110, 88), (132, 104, 76), (229, 212, 141)), + "cape": True, + "helmet": True, + "scale": 1.04, + "bulk": 1.06, + "rank": 2, + }, + "unit-zhang-fei": { + "role": "spearman", + "weapon": "spear", + "palette": Palette((138, 45, 43), (73, 41, 46), (222, 168, 69), (137, 67, 53), (95, 91, 88), (116, 82, 62), (234, 211, 141)), + "helmet": True, + "scale": 1.04, + "bulk": 1.1, + "rank": 2, + }, + "unit-zhao-yun": { + "role": "cavalry", + "weapon": "lance", + "palette": Palette((75, 132, 174), (238, 236, 218), (213, 178, 84), (90, 142, 183), (165, 174, 180), (213, 205, 185), (236, 221, 158)), + "cape": True, + "helmet": True, + "rank": 2, + }, + "unit-cao-cao": { + "role": "lord", + "weapon": "sword", + "palette": Palette((56, 68, 143), (42, 43, 77), (214, 174, 78), (70, 78, 133), (92, 100, 128), (83, 70, 65), (228, 211, 145)), + "cape": True, + "helmet": True, + "rank": 2, + }, + "unit-lu-bu": { + "role": "cavalry", + "weapon": "halberd", + "palette": Palette((146, 37, 49), (41, 37, 48), (224, 164, 66), (142, 60, 58), (98, 92, 95), (68, 58, 54), (235, 205, 133)), + "cape": True, + "helmet": True, + "rank": 2, + }, + "unit-zhuge-liang": { + "role": "strategist", + "weapon": "fan", + "palette": Palette((67, 132, 124), (225, 225, 203), (204, 171, 93), (82, 145, 133), (116, 136, 126), (167, 152, 123), (236, 226, 175)), + "cape": True, + "helmet": False, + "rank": 2, + }, + "unit-huang-zhong": {"role": "archer", "weapon": "bow", "helmet": True, "rank": 2}, + "unit-ma-chao": {"role": "cavalry", "weapon": "lance", "helmet": True, "rank": 2}, + "unit-ma-dai": {"role": "cavalry", "weapon": "lance", "helmet": True, "rank": 1}, + "unit-meng-huo": { + "role": "officer", + "weapon": "spear", + "palette": Palette((171, 83, 43), (86, 64, 40), (218, 158, 66), (148, 91, 50), (102, 91, 72), (91, 68, 48), (229, 202, 127)), + "helmet": True, + "scale": 1.05, + "bulk": 1.16, + "rank": 2, + }, + } + return plans.get(stem) + + +def infer_faction(lower: str) -> str: if "nanman" in lower or "meng-huo" in lower: return "nanman" - if lower.startswith("unit-wu-") or "lu-meng" in lower: + if lower.startswith("unit-wu-") or "lu-meng" in lower or "zhou-yu" in lower: return "wu" if lower.startswith("unit-wei-") or any(token in lower for token in ("cao-cao", "sima-yi")): return "wei" @@ -621,202 +1196,172 @@ def infer_faction(stem: str) -> str: return "shu" -def infer_role(stem: str) -> str: - lower = stem.lower() +def infer_role(lower: str) -> str: if "cavalry" in lower or any(token in lower for token in ("zhao-yun", "ma-chao", "ma-dai", "lu-bu")): return "cavalry" - if "archer" in lower or "huang-zhong" in lower or "crossbow" in lower or "longbow" in lower: + if "archer" in lower or "crossbow" in lower or "longbow" in lower or "huang-zhong" in lower: return "archer" - if any(token in lower for token in ("strategist", "shaman", "zhuge", "sima", "fa-zheng", "ma-liang", "yi-ji", "pang-tong")): + if any(token in lower for token in ("strategist", "shaman", "quartermaster", "zhuge", "sima", "fa-zheng", "ma-liang", "yi-ji", "pang-tong")): return "strategist" if "spearman" in lower or any(token in lower for token in ("guan-yu", "zhang-fei", "jiang-wei", "yan-yan")): - return "polearm" - if "leader" in lower or "officer" in lower or any(token in lower for token in ("liu-bei", "cao-cao", "meng-huo", "wei-yan", "wang-ping", "wu-yi", "li-yan", "huang-quan", "gong-zhi")): + return "spearman" + if any(token in lower for token in ("liu-bei", "cao-cao")): + return "lord" + if "leader" in lower or "officer" in lower or any(token in lower for token in ("meng-huo", "wei-yan", "wang-ping", "wu-yi", "li-yan", "huang-quan", "gong-zhi")): return "officer" return "infantry" -def tint_sprite(image: Image.Image, plan: UnitPlan) -> Image.Image: - image = image.convert("RGBA") - if plan.tint_strength <= 0: - return image - pixels = image.load() - tint_h, tint_s, tint_v = colorsys.rgb_to_hsv(plan.tint[0] / 255, plan.tint[1] / 255, plan.tint[2] / 255) - for y in range(image.height): - for x in range(image.width): - red, green, blue, alpha = pixels[x, y] - if alpha <= 8 or is_skin(red, green, blue) or is_neutral_metal(red, green, blue): - continue - hue, sat, val = colorsys.rgb_to_hsv(red / 255, green / 255, blue / 255) - strength = plan.tint_strength * (0.45 + min(0.55, sat)) - hue = hue * (1 - strength) + tint_h * strength - sat = min(1.0, sat * plan.saturation + tint_s * strength * 0.45) - val = min(1.0, val * plan.brightness + tint_v * strength * 0.08) - nr, ng, nb = colorsys.hsv_to_rgb(hue, sat, val) - pixels[x, y] = (round(nr * 255), round(ng * 255), round(nb * 255), alpha) - return image +def weapon_for(role: str, lower: str) -> str: + if role == "cavalry": + return "lance" + if role == "archer": + return "bow" + if role == "strategist": + return "fan" + if role == "spearman": + return "spear" + if "axe" in lower or "nanman" in lower: + return "spear" + return "sword" -def back_variant(sprite: Image.Image) -> Image.Image: - result = sprite.copy() - result = result.resize((max(1, round(result.width * 0.96)), result.height), Image.Resampling.LANCZOS) - overlay = Image.new("RGBA", result.size, (18, 20, 26, 0)) - overlay.putalpha(result.getchannel("A").point(lambda value: min(48, value // 6))) - result = Image.alpha_composite(result, overlay) - return result +def faction_palette(faction: str, seed: int) -> Palette: + palettes = { + "shu": Palette((55, 126, 76), (45, 83, 69), (210, 169, 77), (74, 124, 88), (93, 113, 98), (151, 121, 86), (229, 211, 143)), + "wei": Palette((63, 72, 145), (46, 49, 82), (207, 168, 80), (73, 80, 132), (91, 101, 127), (83, 74, 69), (224, 207, 145)), + "wu": Palette((36, 126, 135), (52, 79, 83), (214, 154, 82), (57, 134, 137), (88, 113, 119), (142, 114, 87), (229, 203, 139)), + "rebel": Palette((185, 132, 44), (89, 71, 42), (202, 154, 63), (145, 107, 54), (101, 89, 70), (109, 82, 59), (222, 190, 116)), + "nanman": Palette((165, 83, 43), (79, 61, 39), (213, 150, 60), (142, 87, 49), (103, 88, 67), (91, 68, 48), (224, 192, 111)), + } + palette = palettes[faction] + jitter = ((seed % 9) - 4) * 0.015 + return Palette( + adjust_color(palette.primary, 1 + jitter, 1.02), + adjust_color(palette.secondary, 1 - jitter * 0.5, 0.98), + adjust_color(palette.accent, 1 + jitter * 0.4, 1.02), + adjust_color(palette.cloth, 1 + jitter, 1.0), + adjust_color(palette.armor, 1 - jitter, 1.0), + adjust_color(palette.horse, 1 + jitter * 0.8, 1.0), + adjust_color(palette.trim, 1 + jitter * 0.2, 1.0), + ) -def side_variant(sprite: Image.Image) -> Image.Image: - result = sprite.resize((max(1, round(sprite.width * 0.9)), sprite.height), Image.Resampling.LANCZOS) - overlay = Image.new("RGBA", result.size, (16, 16, 22, 0)) - overlay.putalpha(result.getchannel("A").point(lambda value: min(18, value // 18))) - return Image.alpha_composite(result, overlay) +def varied_skin(stem: str) -> Color: + skins = ((189, 132, 86), (203, 149, 97), (177, 119, 78), (214, 160, 108)) + return skins[stable_int(stem + ":skin") % len(skins)] -def finish_frame(frame: Image.Image) -> Image.Image: - frame = clean_alpha(frame) - alpha = frame.getchannel("A") - if not alpha.getbbox(): - return frame - ink_alpha = ImageChops.subtract(alpha.filter(ImageFilter.MaxFilter(3)), alpha) - rim = Image.new("RGBA", frame.size, (10, 8, 10, 0)) - rim.putalpha(ink_alpha.point(lambda value: min(180, round(value * 0.65)))) - result = Image.new("RGBA", frame.size, (0, 0, 0, 0)) - result.alpha_composite(rim) - result.alpha_composite(frame) - return clean_alpha(result) +def varied_hair(stem: str) -> Color: + hairs = ((38, 29, 24), (55, 36, 25), (31, 31, 34), (67, 44, 30)) + return hairs[stable_int(stem + ":hair") % len(hairs)] -def clean_alpha(image: Image.Image) -> Image.Image: - image = image.convert("RGBA") - red, green, blue, alpha = image.split() - alpha = alpha.point(lambda value: 0 if value < 5 else (255 if value > 250 else value)) - return Image.merge("RGBA", (red, green, blue, alpha)) - - -def remove_detached_artifacts(image: Image.Image) -> Image.Image: - alpha = image.getchannel("A") - pixels = alpha.load() - width, height = alpha.size - visited = bytearray(width * height) - components: list[tuple[int, tuple[int, int, int, int], list[tuple[int, int]]]] = [] - for y in range(height): - for x in range(width): - index = y * width + x - if visited[index] or pixels[x, y] <= 8: - continue - stack = [(x, y)] - visited[index] = 1 - points: list[tuple[int, int]] = [] - left = right = x - top = bottom = y - while stack: - px, py = stack.pop() - points.append((px, py)) - left = min(left, px) - right = max(right, px) - top = min(top, py) - bottom = max(bottom, py) - for nx, ny in ((px + 1, py), (px - 1, py), (px, py + 1), (px, py - 1)): - if nx < 0 or nx >= width or ny < 0 or ny >= height: - continue - ni = ny * width + nx - if visited[ni] or pixels[nx, ny] <= 8: - continue - visited[ni] = 1 - stack.append((nx, ny)) - components.append((len(points), (left, top, right + 1, bottom + 1), points)) - if not components: - return image - components.sort(key=lambda component: component[0], reverse=True) - keep_points = set(components[0][2]) - largest_area, largest_bbox, _ = components[0] - for area, bbox, points in components[1:]: - if area >= largest_area * 0.08 or bbox_distance(largest_bbox, bbox) < 12: - keep_points.update(points) - cleaned_alpha = Image.new("L", image.size, 0) - cleaned_pixels = cleaned_alpha.load() - source_pixels = alpha.load() - for x, y in keep_points: - cleaned_pixels[x, y] = source_pixels[x, y] - red, green, blue, _ = image.split() - return Image.merge("RGBA", (red, green, blue, cleaned_alpha)) - - -def expand_box(box: tuple[int, int, int, int], size: tuple[int, int], padding: int) -> tuple[int, int, int, int]: - left, top, right, bottom = box - width, height = size - return (max(0, left - padding), max(0, top - padding), min(width, right + padding), min(height, bottom + padding)) - - -def bbox_distance(left_box: tuple[int, int, int, int], right_box: tuple[int, int, int, int]) -> int: - left_a, top_a, right_a, bottom_a = left_box - left_b, top_b, right_b, bottom_b = right_box - dx = max(left_a - right_b, left_b - right_a, 0) - dy = max(top_a - bottom_b, top_b - bottom_a, 0) - return max(dx, dy) - - -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 direction_offset(direction: str, amount: int) -> tuple[int, int]: - if direction == "east": - return amount, 0 - if direction == "west": - return -amount, 0 - if direction == "north": - return 0, -amount - return 0, amount - - -def arrow_tip(end: tuple[float, float], start: tuple[float, float], size: float) -> list[tuple[float, float]]: - ex, ey = end - sx, sy = start - angle = math.atan2(ey - sy, ex - sx) - left = angle + math.radians(150) - right = angle - math.radians(150) - return [ - (ex, ey), - (ex + math.cos(left) * size, ey + math.sin(left) * size), - (ex - math.cos(angle) * size * 0.45, ey - math.sin(angle) * size * 0.45), - (ex + math.cos(right) * size, ey + math.sin(right) * size), - ] - - -def line(draw: ImageDraw.ImageDraw, points: list[tuple[float, float]], fill: tuple[int, int, int, int], width: int) -> None: - draw.line([(round(x), round(y)) for x, y in points], fill=fill, width=width, joint="curve") - - -def is_skin(red: int, green: int, blue: int) -> bool: - return red > 110 and green > 55 and blue > 32 and red > green * 1.1 and green > blue * 1.05 - - -def is_neutral_metal(red: int, green: int, blue: int) -> bool: - return abs(red - green) < 18 and abs(green - blue) < 22 and red > 118 +def varied_metal(stem: str) -> Color: + metals = ((167, 172, 171), (188, 185, 171), (151, 162, 172), (176, 166, 151)) + return metals[stable_int(stem + ":metal") % len(metals)] def stable_int(value: str) -> int: - return int(hashlib.sha256(value.encode("utf-8")).hexdigest()[:8], 16) + return int(hashlib.sha256(value.encode("utf-8")).hexdigest()[:12], 16) -def ImageOps_mirror(image: Image.Image) -> Image.Image: - from PIL import ImageOps - - return ImageOps.mirror(image) +def direction_vector(direction: str) -> tuple[int, int]: + if direction == "east": + return (1, 0) + if direction == "west": + return (-1, 0) + if direction == "north": + return (0, -1) + return (0, 1) -def save_unit_png(image: Image.Image, path: Path) -> None: - image = clean_alpha(image) - indexed = image.quantize(colors=PALETTE_COLORS, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE) - indexed.save(path, optimize=True) +def with_alpha(color: Color | Rgba, alpha: int = 255) -> Rgba: + if len(color) == 4: + return (color[0], color[1], color[2], alpha) + return (color[0], color[1], color[2], alpha) + + +def darken(color: Color, amount: float) -> Color: + return tuple(clamp_channel(channel * amount) for channel in color) + + +def lighten(color: Color, amount: float) -> Color: + return tuple(clamp_channel(channel * amount + (255 * (amount - 1) * 0.12 if amount > 1 else 0)) for channel in color) + + +def adjust_color(color: Color, brightness: float, saturation: float) -> Color: + r, g, b = [channel / 255 for channel in color] + h, s, v = colorsys.rgb_to_hsv(r, g, b) + s = max(0, min(1, s * saturation)) + v = max(0, min(1, v * brightness)) + nr, ng, nb = colorsys.hsv_to_rgb(h, s, v) + return (round(nr * 255), round(ng * 255), round(nb * 255)) + + +def clamp_channel(value: float) -> int: + return max(0, min(255, round(value))) + + +def scale_point(point: tuple[float, float]) -> tuple[int, int]: + return (round(point[0] * DRAW_SCALE), round(point[1] * DRAW_SCALE)) + + +def scale_box(box: tuple[float, float, float, float]) -> tuple[int, int, int, int]: + return ( + round(box[0] * DRAW_SCALE), + round(box[1] * DRAW_SCALE), + round(box[2] * DRAW_SCALE), + round(box[3] * DRAW_SCALE), + ) + + +def scaled_width(width: float) -> int: + return max(1, round(width * DRAW_SCALE)) + + +def polygon(draw: ImageDraw.ImageDraw, points: list[tuple[float, float]], fill: Rgba, outline: Rgba | None = None, width: float = 1) -> None: + scaled = [scale_point(point) for point in points] + draw.polygon(scaled, fill=fill) + if outline: + draw.line(scaled + [scaled[0]], fill=outline, width=scaled_width(width), joint="curve") + + +def line(draw: ImageDraw.ImageDraw, points: list[tuple[float, float]], fill: Rgba, width: float) -> None: + draw.line([scale_point(point) for point in points], fill=fill, width=scaled_width(width), joint="curve") + + +def line_raw(draw: ImageDraw.ImageDraw, points: list[tuple[float, float]], fill: Rgba, width: float) -> None: + draw.line([(round(x), round(y)) for x, y in points], fill=fill, width=max(1, round(width)), joint="curve") + + +def ellipse(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], fill: Rgba | None, outline: Rgba | None = None, width: float = 1) -> None: + draw.ellipse(scale_box(normalize_box(box)), fill=fill, outline=outline, width=scaled_width(width) if outline else 1) + + +def ellipse_raw(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], fill: Rgba | None, outline: Rgba | None = None, width: float = 1) -> None: + draw.ellipse(tuple(round(v) for v in box), fill=fill, outline=outline, width=max(1, round(width)) if outline else 1) + + +def rounded_rect(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], radius: float, fill: Rgba, outline: Rgba | None = None, width: float = 1) -> None: + draw.rounded_rectangle(scale_box(box), radius=scaled_width(radius), fill=fill, outline=outline, width=scaled_width(width) if outline else 1) + + +def arc(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], start: float, end: float, fill: Rgba, width: float) -> None: + draw.arc(scale_box(normalize_box(box)), start=start, end=end, fill=fill, width=scaled_width(width)) + + +def pieslice(draw: ImageDraw.ImageDraw, box: tuple[float, float, float, float], start: float, end: float, fill: Rgba, outline: Rgba | None = None, width: float = 1) -> None: + draw.pieslice(scale_box(normalize_box(box)), start=start, end=end, fill=fill, outline=outline, width=scaled_width(width) if outline else 1) + + +def diamond(draw: ImageDraw.ImageDraw, x: float, y: float, radius: float, fill: Rgba) -> None: + polygon(draw, [(x, y - radius), (x + radius * 0.7, y), (x, y + radius), (x - radius * 0.7, y)], fill) + + +def normalize_box(box: tuple[float, float, float, float]) -> tuple[float, float, float, float]: + left, top, right, bottom = box + return (min(left, right), min(top, bottom), max(left, right), max(top, bottom)) if __name__ == "__main__": diff --git a/src/assets/images/units/unit-cao-cao-actions.png b/src/assets/images/units/unit-cao-cao-actions.png index a198a76..4e427df 100644 Binary files a/src/assets/images/units/unit-cao-cao-actions.png and b/src/assets/images/units/unit-cao-cao-actions.png differ diff --git a/src/assets/images/units/unit-cao-cao.png b/src/assets/images/units/unit-cao-cao.png index c6c3661..1da722a 100644 Binary files a/src/assets/images/units/unit-cao-cao.png and b/src/assets/images/units/unit-cao-cao.png differ diff --git a/src/assets/images/units/unit-fa-zheng-actions.png b/src/assets/images/units/unit-fa-zheng-actions.png index aff6fe0..59f7f16 100644 Binary files a/src/assets/images/units/unit-fa-zheng-actions.png and b/src/assets/images/units/unit-fa-zheng-actions.png differ diff --git a/src/assets/images/units/unit-fa-zheng.png b/src/assets/images/units/unit-fa-zheng.png index de888ef..f42e4e8 100644 Binary files a/src/assets/images/units/unit-fa-zheng.png and b/src/assets/images/units/unit-fa-zheng.png differ diff --git a/src/assets/images/units/unit-gong-zhi-actions.png b/src/assets/images/units/unit-gong-zhi-actions.png index 6242a52..3d7854d 100644 Binary files a/src/assets/images/units/unit-gong-zhi-actions.png and b/src/assets/images/units/unit-gong-zhi-actions.png differ diff --git a/src/assets/images/units/unit-gong-zhi.png b/src/assets/images/units/unit-gong-zhi.png index b176b5e..3937e3a 100644 Binary files a/src/assets/images/units/unit-gong-zhi.png and b/src/assets/images/units/unit-gong-zhi.png differ diff --git a/src/assets/images/units/unit-guan-yu-actions.png b/src/assets/images/units/unit-guan-yu-actions.png index 851c576..9a4dcab 100644 Binary files a/src/assets/images/units/unit-guan-yu-actions.png and b/src/assets/images/units/unit-guan-yu-actions.png differ diff --git a/src/assets/images/units/unit-guan-yu.png b/src/assets/images/units/unit-guan-yu.png index 09d53cf..395874e 100644 Binary files a/src/assets/images/units/unit-guan-yu.png and b/src/assets/images/units/unit-guan-yu.png differ diff --git a/src/assets/images/units/unit-huang-quan-actions.png b/src/assets/images/units/unit-huang-quan-actions.png index 484d9c0..b3e35dd 100644 Binary files a/src/assets/images/units/unit-huang-quan-actions.png and b/src/assets/images/units/unit-huang-quan-actions.png differ diff --git a/src/assets/images/units/unit-huang-quan.png b/src/assets/images/units/unit-huang-quan.png index 45cb695..2570160 100644 Binary files a/src/assets/images/units/unit-huang-quan.png and b/src/assets/images/units/unit-huang-quan.png differ diff --git a/src/assets/images/units/unit-huang-zhong-actions.png b/src/assets/images/units/unit-huang-zhong-actions.png index ff2385e..1e5eff4 100644 Binary files a/src/assets/images/units/unit-huang-zhong-actions.png and b/src/assets/images/units/unit-huang-zhong-actions.png differ diff --git a/src/assets/images/units/unit-huang-zhong.png b/src/assets/images/units/unit-huang-zhong.png index 3a084e9..b5cc612 100644 Binary files a/src/assets/images/units/unit-huang-zhong.png and b/src/assets/images/units/unit-huang-zhong.png differ diff --git a/src/assets/images/units/unit-jiang-wei-actions.png b/src/assets/images/units/unit-jiang-wei-actions.png index bdfa2a0..6962c2b 100644 Binary files a/src/assets/images/units/unit-jiang-wei-actions.png and b/src/assets/images/units/unit-jiang-wei-actions.png differ diff --git a/src/assets/images/units/unit-jiang-wei.png b/src/assets/images/units/unit-jiang-wei.png index 30f9d86..869c9eb 100644 Binary files a/src/assets/images/units/unit-jiang-wei.png and b/src/assets/images/units/unit-jiang-wei.png differ diff --git a/src/assets/images/units/unit-li-yan-actions.png b/src/assets/images/units/unit-li-yan-actions.png index d0dc6a2..ed01f71 100644 Binary files a/src/assets/images/units/unit-li-yan-actions.png and b/src/assets/images/units/unit-li-yan-actions.png differ diff --git a/src/assets/images/units/unit-li-yan.png b/src/assets/images/units/unit-li-yan.png index 4e9167d..e1504b0 100644 Binary files a/src/assets/images/units/unit-li-yan.png and b/src/assets/images/units/unit-li-yan.png differ diff --git a/src/assets/images/units/unit-liu-bei-actions.png b/src/assets/images/units/unit-liu-bei-actions.png index 93bd455..50f5855 100644 Binary files a/src/assets/images/units/unit-liu-bei-actions.png and b/src/assets/images/units/unit-liu-bei-actions.png differ diff --git a/src/assets/images/units/unit-liu-bei.png b/src/assets/images/units/unit-liu-bei.png index 118e651..9428380 100644 Binary files a/src/assets/images/units/unit-liu-bei.png and b/src/assets/images/units/unit-liu-bei.png differ diff --git a/src/assets/images/units/unit-lu-bu-actions.png b/src/assets/images/units/unit-lu-bu-actions.png index 5ac6abd..e518167 100644 Binary files a/src/assets/images/units/unit-lu-bu-actions.png and b/src/assets/images/units/unit-lu-bu-actions.png differ diff --git a/src/assets/images/units/unit-lu-bu.png b/src/assets/images/units/unit-lu-bu.png index 7b29996..b3e3cee 100644 Binary files a/src/assets/images/units/unit-lu-bu.png and b/src/assets/images/units/unit-lu-bu.png differ diff --git a/src/assets/images/units/unit-lu-meng-actions.png b/src/assets/images/units/unit-lu-meng-actions.png index 16504a9..24aef4d 100644 Binary files a/src/assets/images/units/unit-lu-meng-actions.png and b/src/assets/images/units/unit-lu-meng-actions.png differ diff --git a/src/assets/images/units/unit-lu-meng.png b/src/assets/images/units/unit-lu-meng.png index 5537613..4d8fb5e 100644 Binary files a/src/assets/images/units/unit-lu-meng.png and b/src/assets/images/units/unit-lu-meng.png differ diff --git a/src/assets/images/units/unit-ma-chao-actions.png b/src/assets/images/units/unit-ma-chao-actions.png index 280b1d1..222b1fa 100644 Binary files a/src/assets/images/units/unit-ma-chao-actions.png and b/src/assets/images/units/unit-ma-chao-actions.png differ diff --git a/src/assets/images/units/unit-ma-chao.png b/src/assets/images/units/unit-ma-chao.png index 697ba5c..e3d6107 100644 Binary files a/src/assets/images/units/unit-ma-chao.png and b/src/assets/images/units/unit-ma-chao.png differ diff --git a/src/assets/images/units/unit-ma-dai-actions.png b/src/assets/images/units/unit-ma-dai-actions.png index c540c34..2175bc2 100644 Binary files a/src/assets/images/units/unit-ma-dai-actions.png and b/src/assets/images/units/unit-ma-dai-actions.png differ diff --git a/src/assets/images/units/unit-ma-dai.png b/src/assets/images/units/unit-ma-dai.png index 3c3c4c2..7858f25 100644 Binary files a/src/assets/images/units/unit-ma-dai.png and b/src/assets/images/units/unit-ma-dai.png differ diff --git a/src/assets/images/units/unit-ma-liang-actions.png b/src/assets/images/units/unit-ma-liang-actions.png index 26fafa9..d8920a1 100644 Binary files a/src/assets/images/units/unit-ma-liang-actions.png and b/src/assets/images/units/unit-ma-liang-actions.png differ diff --git a/src/assets/images/units/unit-ma-liang.png b/src/assets/images/units/unit-ma-liang.png index 9d67de5..a778acc 100644 Binary files a/src/assets/images/units/unit-ma-liang.png and b/src/assets/images/units/unit-ma-liang.png differ diff --git a/src/assets/images/units/unit-meng-huo-actions.png b/src/assets/images/units/unit-meng-huo-actions.png index b28f534..a5c2ee9 100644 Binary files a/src/assets/images/units/unit-meng-huo-actions.png and b/src/assets/images/units/unit-meng-huo-actions.png differ diff --git a/src/assets/images/units/unit-meng-huo.png b/src/assets/images/units/unit-meng-huo.png index 29fddb5..0512c38 100644 Binary files a/src/assets/images/units/unit-meng-huo.png and b/src/assets/images/units/unit-meng-huo.png differ diff --git a/src/assets/images/units/unit-nanman-infantry-actions.png b/src/assets/images/units/unit-nanman-infantry-actions.png index 579b98a..c7d7e89 100644 Binary files a/src/assets/images/units/unit-nanman-infantry-actions.png and b/src/assets/images/units/unit-nanman-infantry-actions.png differ diff --git a/src/assets/images/units/unit-nanman-infantry-jungle-actions.png b/src/assets/images/units/unit-nanman-infantry-jungle-actions.png index bd3573b..0ab844f 100644 Binary files a/src/assets/images/units/unit-nanman-infantry-jungle-actions.png and b/src/assets/images/units/unit-nanman-infantry-jungle-actions.png differ diff --git a/src/assets/images/units/unit-nanman-infantry-jungle.png b/src/assets/images/units/unit-nanman-infantry-jungle.png index 55149f7..90cac11 100644 Binary files a/src/assets/images/units/unit-nanman-infantry-jungle.png and b/src/assets/images/units/unit-nanman-infantry-jungle.png differ diff --git a/src/assets/images/units/unit-nanman-infantry-spear-actions.png b/src/assets/images/units/unit-nanman-infantry-spear-actions.png index a1205b8..3e3c46e 100644 Binary files a/src/assets/images/units/unit-nanman-infantry-spear-actions.png and b/src/assets/images/units/unit-nanman-infantry-spear-actions.png differ diff --git a/src/assets/images/units/unit-nanman-infantry-spear.png b/src/assets/images/units/unit-nanman-infantry-spear.png index 462b199..10b98ee 100644 Binary files a/src/assets/images/units/unit-nanman-infantry-spear.png and b/src/assets/images/units/unit-nanman-infantry-spear.png differ diff --git a/src/assets/images/units/unit-nanman-infantry.png b/src/assets/images/units/unit-nanman-infantry.png index 2a647ec..0b4a682 100644 Binary files a/src/assets/images/units/unit-nanman-infantry.png and b/src/assets/images/units/unit-nanman-infantry.png differ diff --git a/src/assets/images/units/unit-nanman-officer-actions.png b/src/assets/images/units/unit-nanman-officer-actions.png index 81e68aa..4b75b50 100644 Binary files a/src/assets/images/units/unit-nanman-officer-actions.png and b/src/assets/images/units/unit-nanman-officer-actions.png differ diff --git a/src/assets/images/units/unit-nanman-officer-bronze-actions.png b/src/assets/images/units/unit-nanman-officer-bronze-actions.png index b84f03a..5621ac9 100644 Binary files a/src/assets/images/units/unit-nanman-officer-bronze-actions.png and b/src/assets/images/units/unit-nanman-officer-bronze-actions.png differ diff --git a/src/assets/images/units/unit-nanman-officer-bronze.png b/src/assets/images/units/unit-nanman-officer-bronze.png index cdb5b37..5aeb7cf 100644 Binary files a/src/assets/images/units/unit-nanman-officer-bronze.png and b/src/assets/images/units/unit-nanman-officer-bronze.png differ diff --git a/src/assets/images/units/unit-nanman-officer-warlord-actions.png b/src/assets/images/units/unit-nanman-officer-warlord-actions.png index 54bb77c..fecc3a3 100644 Binary files a/src/assets/images/units/unit-nanman-officer-warlord-actions.png and b/src/assets/images/units/unit-nanman-officer-warlord-actions.png differ diff --git a/src/assets/images/units/unit-nanman-officer-warlord.png b/src/assets/images/units/unit-nanman-officer-warlord.png index 5fa59d4..07eb97d 100644 Binary files a/src/assets/images/units/unit-nanman-officer-warlord.png and b/src/assets/images/units/unit-nanman-officer-warlord.png differ diff --git a/src/assets/images/units/unit-nanman-officer.png b/src/assets/images/units/unit-nanman-officer.png index fe65fda..8cfecb6 100644 Binary files a/src/assets/images/units/unit-nanman-officer.png and b/src/assets/images/units/unit-nanman-officer.png differ diff --git a/src/assets/images/units/unit-nanman-shaman-actions.png b/src/assets/images/units/unit-nanman-shaman-actions.png index 32f5070..d25e817 100644 Binary files a/src/assets/images/units/unit-nanman-shaman-actions.png and b/src/assets/images/units/unit-nanman-shaman-actions.png differ diff --git a/src/assets/images/units/unit-nanman-shaman-ember-actions.png b/src/assets/images/units/unit-nanman-shaman-ember-actions.png index fee0bba..d348a75 100644 Binary files a/src/assets/images/units/unit-nanman-shaman-ember-actions.png and b/src/assets/images/units/unit-nanman-shaman-ember-actions.png differ diff --git a/src/assets/images/units/unit-nanman-shaman-ember.png b/src/assets/images/units/unit-nanman-shaman-ember.png index 9e66ecc..d6adcb2 100644 Binary files a/src/assets/images/units/unit-nanman-shaman-ember.png and b/src/assets/images/units/unit-nanman-shaman-ember.png differ diff --git a/src/assets/images/units/unit-nanman-shaman-storm-actions.png b/src/assets/images/units/unit-nanman-shaman-storm-actions.png index 796ed30..9ca1967 100644 Binary files a/src/assets/images/units/unit-nanman-shaman-storm-actions.png and b/src/assets/images/units/unit-nanman-shaman-storm-actions.png differ diff --git a/src/assets/images/units/unit-nanman-shaman-storm.png b/src/assets/images/units/unit-nanman-shaman-storm.png index 36e4f17..d742747 100644 Binary files a/src/assets/images/units/unit-nanman-shaman-storm.png and b/src/assets/images/units/unit-nanman-shaman-storm.png differ diff --git a/src/assets/images/units/unit-nanman-shaman.png b/src/assets/images/units/unit-nanman-shaman.png index bbbfb58..5b2083a 100644 Binary files a/src/assets/images/units/unit-nanman-shaman.png and b/src/assets/images/units/unit-nanman-shaman.png differ diff --git a/src/assets/images/units/unit-pang-tong-actions.png b/src/assets/images/units/unit-pang-tong-actions.png index cddf5b8..a9a09cf 100644 Binary files a/src/assets/images/units/unit-pang-tong-actions.png and b/src/assets/images/units/unit-pang-tong-actions.png differ diff --git a/src/assets/images/units/unit-pang-tong.png b/src/assets/images/units/unit-pang-tong.png index c2216e0..230a22e 100644 Binary files a/src/assets/images/units/unit-pang-tong.png and b/src/assets/images/units/unit-pang-tong.png differ diff --git a/src/assets/images/units/unit-rebel-actions.png b/src/assets/images/units/unit-rebel-actions.png index 622b2e3..269ff7a 100644 Binary files a/src/assets/images/units/unit-rebel-actions.png and b/src/assets/images/units/unit-rebel-actions.png differ diff --git a/src/assets/images/units/unit-rebel-archer-actions.png b/src/assets/images/units/unit-rebel-archer-actions.png index c412705..077d922 100644 Binary files a/src/assets/images/units/unit-rebel-archer-actions.png and b/src/assets/images/units/unit-rebel-archer-actions.png differ diff --git a/src/assets/images/units/unit-rebel-archer-fire-actions.png b/src/assets/images/units/unit-rebel-archer-fire-actions.png index 034e198..ff7ff83 100644 Binary files a/src/assets/images/units/unit-rebel-archer-fire-actions.png and b/src/assets/images/units/unit-rebel-archer-fire-actions.png differ diff --git a/src/assets/images/units/unit-rebel-archer-fire.png b/src/assets/images/units/unit-rebel-archer-fire.png index e977f7b..8cd24a0 100644 Binary files a/src/assets/images/units/unit-rebel-archer-fire.png and b/src/assets/images/units/unit-rebel-archer-fire.png differ diff --git a/src/assets/images/units/unit-rebel-archer-veteran-actions.png b/src/assets/images/units/unit-rebel-archer-veteran-actions.png index 79b70f5..0b45f84 100644 Binary files a/src/assets/images/units/unit-rebel-archer-veteran-actions.png and b/src/assets/images/units/unit-rebel-archer-veteran-actions.png differ diff --git a/src/assets/images/units/unit-rebel-archer-veteran.png b/src/assets/images/units/unit-rebel-archer-veteran.png index eb188b5..4596dce 100644 Binary files a/src/assets/images/units/unit-rebel-archer-veteran.png and b/src/assets/images/units/unit-rebel-archer-veteran.png differ diff --git a/src/assets/images/units/unit-rebel-archer.png b/src/assets/images/units/unit-rebel-archer.png index 2c8f3f6..dc81661 100644 Binary files a/src/assets/images/units/unit-rebel-archer.png and b/src/assets/images/units/unit-rebel-archer.png differ diff --git a/src/assets/images/units/unit-rebel-cavalry-actions.png b/src/assets/images/units/unit-rebel-cavalry-actions.png index b4e8b41..df00687 100644 Binary files a/src/assets/images/units/unit-rebel-cavalry-actions.png and b/src/assets/images/units/unit-rebel-cavalry-actions.png differ diff --git a/src/assets/images/units/unit-rebel-cavalry-veteran-actions.png b/src/assets/images/units/unit-rebel-cavalry-veteran-actions.png index cd502b6..3c5c3c8 100644 Binary files a/src/assets/images/units/unit-rebel-cavalry-veteran-actions.png and b/src/assets/images/units/unit-rebel-cavalry-veteran-actions.png differ diff --git a/src/assets/images/units/unit-rebel-cavalry-veteran.png b/src/assets/images/units/unit-rebel-cavalry-veteran.png index 8c97384..3f52c4f 100644 Binary files a/src/assets/images/units/unit-rebel-cavalry-veteran.png and b/src/assets/images/units/unit-rebel-cavalry-veteran.png differ diff --git a/src/assets/images/units/unit-rebel-cavalry.png b/src/assets/images/units/unit-rebel-cavalry.png index 8610b15..8ea3f9b 100644 Binary files a/src/assets/images/units/unit-rebel-cavalry.png and b/src/assets/images/units/unit-rebel-cavalry.png differ diff --git a/src/assets/images/units/unit-rebel-leader-actions.png b/src/assets/images/units/unit-rebel-leader-actions.png index 34f1975..28384b2 100644 Binary files a/src/assets/images/units/unit-rebel-leader-actions.png and b/src/assets/images/units/unit-rebel-leader-actions.png differ diff --git a/src/assets/images/units/unit-rebel-leader-warlord-actions.png b/src/assets/images/units/unit-rebel-leader-warlord-actions.png index 975b0ce..d40f9a0 100644 Binary files a/src/assets/images/units/unit-rebel-leader-warlord-actions.png and b/src/assets/images/units/unit-rebel-leader-warlord-actions.png differ diff --git a/src/assets/images/units/unit-rebel-leader-warlord.png b/src/assets/images/units/unit-rebel-leader-warlord.png index 880b8d5..65085af 100644 Binary files a/src/assets/images/units/unit-rebel-leader-warlord.png and b/src/assets/images/units/unit-rebel-leader-warlord.png differ diff --git a/src/assets/images/units/unit-rebel-leader.png b/src/assets/images/units/unit-rebel-leader.png index 0abe685..9931a59 100644 Binary files a/src/assets/images/units/unit-rebel-leader.png and b/src/assets/images/units/unit-rebel-leader.png differ diff --git a/src/assets/images/units/unit-rebel-ragged-actions.png b/src/assets/images/units/unit-rebel-ragged-actions.png index 6ef58d6..8301b24 100644 Binary files a/src/assets/images/units/unit-rebel-ragged-actions.png and b/src/assets/images/units/unit-rebel-ragged-actions.png differ diff --git a/src/assets/images/units/unit-rebel-ragged.png b/src/assets/images/units/unit-rebel-ragged.png index b537052..08521c5 100644 Binary files a/src/assets/images/units/unit-rebel-ragged.png and b/src/assets/images/units/unit-rebel-ragged.png differ diff --git a/src/assets/images/units/unit-rebel.png b/src/assets/images/units/unit-rebel.png index df9118e..beef145 100644 Binary files a/src/assets/images/units/unit-rebel.png and b/src/assets/images/units/unit-rebel.png differ diff --git a/src/assets/images/units/unit-shu-archer-actions.png b/src/assets/images/units/unit-shu-archer-actions.png index be026de..56146c0 100644 Binary files a/src/assets/images/units/unit-shu-archer-actions.png and b/src/assets/images/units/unit-shu-archer-actions.png differ diff --git a/src/assets/images/units/unit-shu-archer-longbow-actions.png b/src/assets/images/units/unit-shu-archer-longbow-actions.png index b024d7e..c5258c3 100644 Binary files a/src/assets/images/units/unit-shu-archer-longbow-actions.png and b/src/assets/images/units/unit-shu-archer-longbow-actions.png differ diff --git a/src/assets/images/units/unit-shu-archer-longbow.png b/src/assets/images/units/unit-shu-archer-longbow.png index afce75e..1fc881a 100644 Binary files a/src/assets/images/units/unit-shu-archer-longbow.png and b/src/assets/images/units/unit-shu-archer-longbow.png differ diff --git a/src/assets/images/units/unit-shu-archer-veteran-actions.png b/src/assets/images/units/unit-shu-archer-veteran-actions.png index 23d31a4..7d3b789 100644 Binary files a/src/assets/images/units/unit-shu-archer-veteran-actions.png and b/src/assets/images/units/unit-shu-archer-veteran-actions.png differ diff --git a/src/assets/images/units/unit-shu-archer-veteran.png b/src/assets/images/units/unit-shu-archer-veteran.png index c4c9f7f..0527560 100644 Binary files a/src/assets/images/units/unit-shu-archer-veteran.png and b/src/assets/images/units/unit-shu-archer-veteran.png differ diff --git a/src/assets/images/units/unit-shu-archer.png b/src/assets/images/units/unit-shu-archer.png index 6fbd0c8..88cf638 100644 Binary files a/src/assets/images/units/unit-shu-archer.png and b/src/assets/images/units/unit-shu-archer.png differ diff --git a/src/assets/images/units/unit-shu-cavalry-actions.png b/src/assets/images/units/unit-shu-cavalry-actions.png index b06a38b..c54dc2c 100644 Binary files a/src/assets/images/units/unit-shu-cavalry-actions.png and b/src/assets/images/units/unit-shu-cavalry-actions.png differ diff --git a/src/assets/images/units/unit-shu-cavalry-scout-actions.png b/src/assets/images/units/unit-shu-cavalry-scout-actions.png index b5af520..a20c880 100644 Binary files a/src/assets/images/units/unit-shu-cavalry-scout-actions.png and b/src/assets/images/units/unit-shu-cavalry-scout-actions.png differ diff --git a/src/assets/images/units/unit-shu-cavalry-scout.png b/src/assets/images/units/unit-shu-cavalry-scout.png index 75d1e80..5024863 100644 Binary files a/src/assets/images/units/unit-shu-cavalry-scout.png and b/src/assets/images/units/unit-shu-cavalry-scout.png differ diff --git a/src/assets/images/units/unit-shu-cavalry-white-actions.png b/src/assets/images/units/unit-shu-cavalry-white-actions.png index 6950d2a..969d993 100644 Binary files a/src/assets/images/units/unit-shu-cavalry-white-actions.png and b/src/assets/images/units/unit-shu-cavalry-white-actions.png differ diff --git a/src/assets/images/units/unit-shu-cavalry-white.png b/src/assets/images/units/unit-shu-cavalry-white.png index 1df8f4a..029be2e 100644 Binary files a/src/assets/images/units/unit-shu-cavalry-white.png and b/src/assets/images/units/unit-shu-cavalry-white.png differ diff --git a/src/assets/images/units/unit-shu-cavalry.png b/src/assets/images/units/unit-shu-cavalry.png index 3de400f..b2ce18c 100644 Binary files a/src/assets/images/units/unit-shu-cavalry.png and b/src/assets/images/units/unit-shu-cavalry.png differ diff --git a/src/assets/images/units/unit-shu-infantry-actions.png b/src/assets/images/units/unit-shu-infantry-actions.png index 26c0964..49b4966 100644 Binary files a/src/assets/images/units/unit-shu-infantry-actions.png and b/src/assets/images/units/unit-shu-infantry-actions.png differ diff --git a/src/assets/images/units/unit-shu-infantry-banner-actions.png b/src/assets/images/units/unit-shu-infantry-banner-actions.png index 9cbe357..5e4e6f5 100644 Binary files a/src/assets/images/units/unit-shu-infantry-banner-actions.png and b/src/assets/images/units/unit-shu-infantry-banner-actions.png differ diff --git a/src/assets/images/units/unit-shu-infantry-banner.png b/src/assets/images/units/unit-shu-infantry-banner.png index fe3a683..d8ddaf1 100644 Binary files a/src/assets/images/units/unit-shu-infantry-banner.png and b/src/assets/images/units/unit-shu-infantry-banner.png differ diff --git a/src/assets/images/units/unit-shu-infantry-guard-actions.png b/src/assets/images/units/unit-shu-infantry-guard-actions.png index 2d8a441..2874e8c 100644 Binary files a/src/assets/images/units/unit-shu-infantry-guard-actions.png and b/src/assets/images/units/unit-shu-infantry-guard-actions.png differ diff --git a/src/assets/images/units/unit-shu-infantry-guard.png b/src/assets/images/units/unit-shu-infantry-guard.png index ed26810..fd9f3de 100644 Binary files a/src/assets/images/units/unit-shu-infantry-guard.png and b/src/assets/images/units/unit-shu-infantry-guard.png differ diff --git a/src/assets/images/units/unit-shu-infantry.png b/src/assets/images/units/unit-shu-infantry.png index c35dcaf..6f33710 100644 Binary files a/src/assets/images/units/unit-shu-infantry.png and b/src/assets/images/units/unit-shu-infantry.png differ diff --git a/src/assets/images/units/unit-shu-officer-actions.png b/src/assets/images/units/unit-shu-officer-actions.png index f3458d5..f4bf6cc 100644 Binary files a/src/assets/images/units/unit-shu-officer-actions.png and b/src/assets/images/units/unit-shu-officer-actions.png differ diff --git a/src/assets/images/units/unit-shu-officer-council-actions.png b/src/assets/images/units/unit-shu-officer-council-actions.png index 9d707f8..8a70598 100644 Binary files a/src/assets/images/units/unit-shu-officer-council-actions.png and b/src/assets/images/units/unit-shu-officer-council-actions.png differ diff --git a/src/assets/images/units/unit-shu-officer-council.png b/src/assets/images/units/unit-shu-officer-council.png index f831e38..92aeb59 100644 Binary files a/src/assets/images/units/unit-shu-officer-council.png and b/src/assets/images/units/unit-shu-officer-council.png differ diff --git a/src/assets/images/units/unit-shu-officer-vanguard-actions.png b/src/assets/images/units/unit-shu-officer-vanguard-actions.png index b4708bd..aef6709 100644 Binary files a/src/assets/images/units/unit-shu-officer-vanguard-actions.png and b/src/assets/images/units/unit-shu-officer-vanguard-actions.png differ diff --git a/src/assets/images/units/unit-shu-officer-vanguard.png b/src/assets/images/units/unit-shu-officer-vanguard.png index bac899b..508c241 100644 Binary files a/src/assets/images/units/unit-shu-officer-vanguard.png and b/src/assets/images/units/unit-shu-officer-vanguard.png differ diff --git a/src/assets/images/units/unit-shu-officer.png b/src/assets/images/units/unit-shu-officer.png index 0718e7d..a0b4f59 100644 Binary files a/src/assets/images/units/unit-shu-officer.png and b/src/assets/images/units/unit-shu-officer.png differ diff --git a/src/assets/images/units/unit-shu-spearman-actions.png b/src/assets/images/units/unit-shu-spearman-actions.png index 591cdcd..94841dc 100644 Binary files a/src/assets/images/units/unit-shu-spearman-actions.png and b/src/assets/images/units/unit-shu-spearman-actions.png differ diff --git a/src/assets/images/units/unit-shu-spearman-guard-actions.png b/src/assets/images/units/unit-shu-spearman-guard-actions.png index 160e901..db98b10 100644 Binary files a/src/assets/images/units/unit-shu-spearman-guard-actions.png and b/src/assets/images/units/unit-shu-spearman-guard-actions.png differ diff --git a/src/assets/images/units/unit-shu-spearman-guard.png b/src/assets/images/units/unit-shu-spearman-guard.png index 90934c1..e6ec794 100644 Binary files a/src/assets/images/units/unit-shu-spearman-guard.png and b/src/assets/images/units/unit-shu-spearman-guard.png differ diff --git a/src/assets/images/units/unit-shu-spearman-veteran-actions.png b/src/assets/images/units/unit-shu-spearman-veteran-actions.png index 45420a8..c22ddfd 100644 Binary files a/src/assets/images/units/unit-shu-spearman-veteran-actions.png and b/src/assets/images/units/unit-shu-spearman-veteran-actions.png differ diff --git a/src/assets/images/units/unit-shu-spearman-veteran.png b/src/assets/images/units/unit-shu-spearman-veteran.png index 168c3d5..47a1cd8 100644 Binary files a/src/assets/images/units/unit-shu-spearman-veteran.png and b/src/assets/images/units/unit-shu-spearman-veteran.png differ diff --git a/src/assets/images/units/unit-shu-spearman.png b/src/assets/images/units/unit-shu-spearman.png index a2f3175..c04b97b 100644 Binary files a/src/assets/images/units/unit-shu-spearman.png and b/src/assets/images/units/unit-shu-spearman.png differ diff --git a/src/assets/images/units/unit-shu-strategist-actions.png b/src/assets/images/units/unit-shu-strategist-actions.png index 983d1e8..7bb78b0 100644 Binary files a/src/assets/images/units/unit-shu-strategist-actions.png and b/src/assets/images/units/unit-shu-strategist-actions.png differ diff --git a/src/assets/images/units/unit-shu-strategist-field-actions.png b/src/assets/images/units/unit-shu-strategist-field-actions.png index b867b9a..9723cdb 100644 Binary files a/src/assets/images/units/unit-shu-strategist-field-actions.png and b/src/assets/images/units/unit-shu-strategist-field-actions.png differ diff --git a/src/assets/images/units/unit-shu-strategist-field.png b/src/assets/images/units/unit-shu-strategist-field.png index 62599f0..6d8bb4f 100644 Binary files a/src/assets/images/units/unit-shu-strategist-field.png and b/src/assets/images/units/unit-shu-strategist-field.png differ diff --git a/src/assets/images/units/unit-shu-strategist-white-actions.png b/src/assets/images/units/unit-shu-strategist-white-actions.png index 7d9344f..4874082 100644 Binary files a/src/assets/images/units/unit-shu-strategist-white-actions.png and b/src/assets/images/units/unit-shu-strategist-white-actions.png differ diff --git a/src/assets/images/units/unit-shu-strategist-white.png b/src/assets/images/units/unit-shu-strategist-white.png index 26b6f3d..4a21738 100644 Binary files a/src/assets/images/units/unit-shu-strategist-white.png and b/src/assets/images/units/unit-shu-strategist-white.png differ diff --git a/src/assets/images/units/unit-shu-strategist.png b/src/assets/images/units/unit-shu-strategist.png index a31dd73..7bdd019 100644 Binary files a/src/assets/images/units/unit-shu-strategist.png and b/src/assets/images/units/unit-shu-strategist.png differ diff --git a/src/assets/images/units/unit-sima-yi-actions.png b/src/assets/images/units/unit-sima-yi-actions.png index 31e98df..9b697f8 100644 Binary files a/src/assets/images/units/unit-sima-yi-actions.png and b/src/assets/images/units/unit-sima-yi-actions.png differ diff --git a/src/assets/images/units/unit-sima-yi.png b/src/assets/images/units/unit-sima-yi.png index 9390fa4..88344de 100644 Binary files a/src/assets/images/units/unit-sima-yi.png and b/src/assets/images/units/unit-sima-yi.png differ diff --git a/src/assets/images/units/unit-wang-ping-actions.png b/src/assets/images/units/unit-wang-ping-actions.png index ac6c7af..3a2b1fa 100644 Binary files a/src/assets/images/units/unit-wang-ping-actions.png and b/src/assets/images/units/unit-wang-ping-actions.png differ diff --git a/src/assets/images/units/unit-wang-ping.png b/src/assets/images/units/unit-wang-ping.png index 6f8eb49..05bf157 100644 Binary files a/src/assets/images/units/unit-wang-ping.png and b/src/assets/images/units/unit-wang-ping.png differ diff --git a/src/assets/images/units/unit-wei-archer-actions.png b/src/assets/images/units/unit-wei-archer-actions.png index f62ca46..ad7178c 100644 Binary files a/src/assets/images/units/unit-wei-archer-actions.png and b/src/assets/images/units/unit-wei-archer-actions.png differ diff --git a/src/assets/images/units/unit-wei-archer-crossbow-actions.png b/src/assets/images/units/unit-wei-archer-crossbow-actions.png index 9f1f13b..bbadb21 100644 Binary files a/src/assets/images/units/unit-wei-archer-crossbow-actions.png and b/src/assets/images/units/unit-wei-archer-crossbow-actions.png differ diff --git a/src/assets/images/units/unit-wei-archer-crossbow.png b/src/assets/images/units/unit-wei-archer-crossbow.png index 5dd8776..79c1c6d 100644 Binary files a/src/assets/images/units/unit-wei-archer-crossbow.png and b/src/assets/images/units/unit-wei-archer-crossbow.png differ diff --git a/src/assets/images/units/unit-wei-archer-veteran-actions.png b/src/assets/images/units/unit-wei-archer-veteran-actions.png index 03b4cd7..8510e35 100644 Binary files a/src/assets/images/units/unit-wei-archer-veteran-actions.png and b/src/assets/images/units/unit-wei-archer-veteran-actions.png differ diff --git a/src/assets/images/units/unit-wei-archer-veteran.png b/src/assets/images/units/unit-wei-archer-veteran.png index c44b1bf..1a3a3bd 100644 Binary files a/src/assets/images/units/unit-wei-archer-veteran.png and b/src/assets/images/units/unit-wei-archer-veteran.png differ diff --git a/src/assets/images/units/unit-wei-archer.png b/src/assets/images/units/unit-wei-archer.png index d43ce6a..22c92b6 100644 Binary files a/src/assets/images/units/unit-wei-archer.png and b/src/assets/images/units/unit-wei-archer.png differ diff --git a/src/assets/images/units/unit-wei-cavalry-actions.png b/src/assets/images/units/unit-wei-cavalry-actions.png index 4ad0716..038ff85 100644 Binary files a/src/assets/images/units/unit-wei-cavalry-actions.png and b/src/assets/images/units/unit-wei-cavalry-actions.png differ diff --git a/src/assets/images/units/unit-wei-cavalry-elite-actions.png b/src/assets/images/units/unit-wei-cavalry-elite-actions.png index 251679a..e445820 100644 Binary files a/src/assets/images/units/unit-wei-cavalry-elite-actions.png and b/src/assets/images/units/unit-wei-cavalry-elite-actions.png differ diff --git a/src/assets/images/units/unit-wei-cavalry-elite.png b/src/assets/images/units/unit-wei-cavalry-elite.png index da563fd..a501d72 100644 Binary files a/src/assets/images/units/unit-wei-cavalry-elite.png and b/src/assets/images/units/unit-wei-cavalry-elite.png differ diff --git a/src/assets/images/units/unit-wei-cavalry-iron-actions.png b/src/assets/images/units/unit-wei-cavalry-iron-actions.png index bc7428b..2f5538d 100644 Binary files a/src/assets/images/units/unit-wei-cavalry-iron-actions.png and b/src/assets/images/units/unit-wei-cavalry-iron-actions.png differ diff --git a/src/assets/images/units/unit-wei-cavalry-iron.png b/src/assets/images/units/unit-wei-cavalry-iron.png index 68d4f7f..3beaf12 100644 Binary files a/src/assets/images/units/unit-wei-cavalry-iron.png and b/src/assets/images/units/unit-wei-cavalry-iron.png differ diff --git a/src/assets/images/units/unit-wei-cavalry.png b/src/assets/images/units/unit-wei-cavalry.png index fcc58a8..19c7a04 100644 Binary files a/src/assets/images/units/unit-wei-cavalry.png and b/src/assets/images/units/unit-wei-cavalry.png differ diff --git a/src/assets/images/units/unit-wei-infantry-actions.png b/src/assets/images/units/unit-wei-infantry-actions.png index d8fa456..bc59a69 100644 Binary files a/src/assets/images/units/unit-wei-infantry-actions.png and b/src/assets/images/units/unit-wei-infantry-actions.png differ diff --git a/src/assets/images/units/unit-wei-infantry-shield-actions.png b/src/assets/images/units/unit-wei-infantry-shield-actions.png index d43e4fe..de44694 100644 Binary files a/src/assets/images/units/unit-wei-infantry-shield-actions.png and b/src/assets/images/units/unit-wei-infantry-shield-actions.png differ diff --git a/src/assets/images/units/unit-wei-infantry-shield.png b/src/assets/images/units/unit-wei-infantry-shield.png index 39b16c5..fa9e727 100644 Binary files a/src/assets/images/units/unit-wei-infantry-shield.png and b/src/assets/images/units/unit-wei-infantry-shield.png differ diff --git a/src/assets/images/units/unit-wei-infantry-veteran-actions.png b/src/assets/images/units/unit-wei-infantry-veteran-actions.png index 785e23d..8608790 100644 Binary files a/src/assets/images/units/unit-wei-infantry-veteran-actions.png and b/src/assets/images/units/unit-wei-infantry-veteran-actions.png differ diff --git a/src/assets/images/units/unit-wei-infantry-veteran.png b/src/assets/images/units/unit-wei-infantry-veteran.png index 3b2e8a2..1daf51d 100644 Binary files a/src/assets/images/units/unit-wei-infantry-veteran.png and b/src/assets/images/units/unit-wei-infantry-veteran.png differ diff --git a/src/assets/images/units/unit-wei-infantry.png b/src/assets/images/units/unit-wei-infantry.png index 92e044a..0a59566 100644 Binary files a/src/assets/images/units/unit-wei-infantry.png and b/src/assets/images/units/unit-wei-infantry.png differ diff --git a/src/assets/images/units/unit-wei-officer-actions.png b/src/assets/images/units/unit-wei-officer-actions.png index c070944..cebd874 100644 Binary files a/src/assets/images/units/unit-wei-officer-actions.png and b/src/assets/images/units/unit-wei-officer-actions.png differ diff --git a/src/assets/images/units/unit-wei-officer-gate-actions.png b/src/assets/images/units/unit-wei-officer-gate-actions.png index 647911e..b1be2d6 100644 Binary files a/src/assets/images/units/unit-wei-officer-gate-actions.png and b/src/assets/images/units/unit-wei-officer-gate-actions.png differ diff --git a/src/assets/images/units/unit-wei-officer-gate.png b/src/assets/images/units/unit-wei-officer-gate.png index 354c8d8..be808de 100644 Binary files a/src/assets/images/units/unit-wei-officer-gate.png and b/src/assets/images/units/unit-wei-officer-gate.png differ diff --git a/src/assets/images/units/unit-wei-officer-iron-actions.png b/src/assets/images/units/unit-wei-officer-iron-actions.png index 1011cd1..ca1cb2d 100644 Binary files a/src/assets/images/units/unit-wei-officer-iron-actions.png and b/src/assets/images/units/unit-wei-officer-iron-actions.png differ diff --git a/src/assets/images/units/unit-wei-officer-iron.png b/src/assets/images/units/unit-wei-officer-iron.png index 726046f..8c7b160 100644 Binary files a/src/assets/images/units/unit-wei-officer-iron.png and b/src/assets/images/units/unit-wei-officer-iron.png differ diff --git a/src/assets/images/units/unit-wei-officer.png b/src/assets/images/units/unit-wei-officer.png index 9a0b6a3..f1c9cf9 100644 Binary files a/src/assets/images/units/unit-wei-officer.png and b/src/assets/images/units/unit-wei-officer.png differ diff --git a/src/assets/images/units/unit-wei-strategist-actions.png b/src/assets/images/units/unit-wei-strategist-actions.png index 51f5c76..71817e4 100644 Binary files a/src/assets/images/units/unit-wei-strategist-actions.png and b/src/assets/images/units/unit-wei-strategist-actions.png differ diff --git a/src/assets/images/units/unit-wei-strategist-black-actions.png b/src/assets/images/units/unit-wei-strategist-black-actions.png index 3c7065a..bfd1c5b 100644 Binary files a/src/assets/images/units/unit-wei-strategist-black-actions.png and b/src/assets/images/units/unit-wei-strategist-black-actions.png differ diff --git a/src/assets/images/units/unit-wei-strategist-black.png b/src/assets/images/units/unit-wei-strategist-black.png index cbf7ac8..d15e5eb 100644 Binary files a/src/assets/images/units/unit-wei-strategist-black.png and b/src/assets/images/units/unit-wei-strategist-black.png differ diff --git a/src/assets/images/units/unit-wei-strategist-blue-actions.png b/src/assets/images/units/unit-wei-strategist-blue-actions.png index c54f9b6..ee6812b 100644 Binary files a/src/assets/images/units/unit-wei-strategist-blue-actions.png and b/src/assets/images/units/unit-wei-strategist-blue-actions.png differ diff --git a/src/assets/images/units/unit-wei-strategist-blue.png b/src/assets/images/units/unit-wei-strategist-blue.png index 9f66379..518561c 100644 Binary files a/src/assets/images/units/unit-wei-strategist-blue.png and b/src/assets/images/units/unit-wei-strategist-blue.png differ diff --git a/src/assets/images/units/unit-wei-strategist.png b/src/assets/images/units/unit-wei-strategist.png index e5efee2..7515488 100644 Binary files a/src/assets/images/units/unit-wei-strategist.png and b/src/assets/images/units/unit-wei-strategist.png differ diff --git a/src/assets/images/units/unit-wei-yan-actions.png b/src/assets/images/units/unit-wei-yan-actions.png index 9d3100d..5e74bd2 100644 Binary files a/src/assets/images/units/unit-wei-yan-actions.png and b/src/assets/images/units/unit-wei-yan-actions.png differ diff --git a/src/assets/images/units/unit-wei-yan.png b/src/assets/images/units/unit-wei-yan.png index aa791dc..b84be73 100644 Binary files a/src/assets/images/units/unit-wei-yan.png and b/src/assets/images/units/unit-wei-yan.png differ diff --git a/src/assets/images/units/unit-wu-archer-actions.png b/src/assets/images/units/unit-wu-archer-actions.png index 5d21187..ba1a126 100644 Binary files a/src/assets/images/units/unit-wu-archer-actions.png and b/src/assets/images/units/unit-wu-archer-actions.png differ diff --git a/src/assets/images/units/unit-wu-archer-naval-actions.png b/src/assets/images/units/unit-wu-archer-naval-actions.png index 47f3762..87a83dd 100644 Binary files a/src/assets/images/units/unit-wu-archer-naval-actions.png and b/src/assets/images/units/unit-wu-archer-naval-actions.png differ diff --git a/src/assets/images/units/unit-wu-archer-naval.png b/src/assets/images/units/unit-wu-archer-naval.png index f01b5cb..17bb1ec 100644 Binary files a/src/assets/images/units/unit-wu-archer-naval.png and b/src/assets/images/units/unit-wu-archer-naval.png differ diff --git a/src/assets/images/units/unit-wu-archer-river-actions.png b/src/assets/images/units/unit-wu-archer-river-actions.png index 6c3cb09..9c8758d 100644 Binary files a/src/assets/images/units/unit-wu-archer-river-actions.png and b/src/assets/images/units/unit-wu-archer-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-archer-river.png b/src/assets/images/units/unit-wu-archer-river.png index 6e0af13..8bf22a5 100644 Binary files a/src/assets/images/units/unit-wu-archer-river.png and b/src/assets/images/units/unit-wu-archer-river.png differ diff --git a/src/assets/images/units/unit-wu-archer.png b/src/assets/images/units/unit-wu-archer.png index 1d8958c..bcbe97a 100644 Binary files a/src/assets/images/units/unit-wu-archer.png and b/src/assets/images/units/unit-wu-archer.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-actions.png b/src/assets/images/units/unit-wu-cavalry-actions.png index 6455793..3df9468 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-actions.png and b/src/assets/images/units/unit-wu-cavalry-actions.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-elite-actions.png b/src/assets/images/units/unit-wu-cavalry-elite-actions.png index c9837ec..7a18a45 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-elite-actions.png and b/src/assets/images/units/unit-wu-cavalry-elite-actions.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-elite.png b/src/assets/images/units/unit-wu-cavalry-elite.png index f828c62..7b9f4ab 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-elite.png and b/src/assets/images/units/unit-wu-cavalry-elite.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-river-actions.png b/src/assets/images/units/unit-wu-cavalry-river-actions.png index e7345c3..0b3f14c 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-river-actions.png and b/src/assets/images/units/unit-wu-cavalry-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-river.png b/src/assets/images/units/unit-wu-cavalry-river.png index b17f2f3..6ed4bd0 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-river.png and b/src/assets/images/units/unit-wu-cavalry-river.png differ diff --git a/src/assets/images/units/unit-wu-cavalry.png b/src/assets/images/units/unit-wu-cavalry.png index 5596200..39aa78a 100644 Binary files a/src/assets/images/units/unit-wu-cavalry.png and b/src/assets/images/units/unit-wu-cavalry.png differ diff --git a/src/assets/images/units/unit-wu-infantry-actions.png b/src/assets/images/units/unit-wu-infantry-actions.png index 38917ba..af3f625 100644 Binary files a/src/assets/images/units/unit-wu-infantry-actions.png and b/src/assets/images/units/unit-wu-infantry-actions.png differ diff --git a/src/assets/images/units/unit-wu-infantry-marine-actions.png b/src/assets/images/units/unit-wu-infantry-marine-actions.png index 8a1292b..10a46e8 100644 Binary files a/src/assets/images/units/unit-wu-infantry-marine-actions.png and b/src/assets/images/units/unit-wu-infantry-marine-actions.png differ diff --git a/src/assets/images/units/unit-wu-infantry-marine.png b/src/assets/images/units/unit-wu-infantry-marine.png index 0d7150f..d221a44 100644 Binary files a/src/assets/images/units/unit-wu-infantry-marine.png and b/src/assets/images/units/unit-wu-infantry-marine.png differ diff --git a/src/assets/images/units/unit-wu-infantry-river-actions.png b/src/assets/images/units/unit-wu-infantry-river-actions.png index aedf307..00132d5 100644 Binary files a/src/assets/images/units/unit-wu-infantry-river-actions.png and b/src/assets/images/units/unit-wu-infantry-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-infantry-river.png b/src/assets/images/units/unit-wu-infantry-river.png index 029b00d..382f1e4 100644 Binary files a/src/assets/images/units/unit-wu-infantry-river.png and b/src/assets/images/units/unit-wu-infantry-river.png differ diff --git a/src/assets/images/units/unit-wu-infantry.png b/src/assets/images/units/unit-wu-infantry.png index 5e39a1d..e15f55c 100644 Binary files a/src/assets/images/units/unit-wu-infantry.png and b/src/assets/images/units/unit-wu-infantry.png differ diff --git a/src/assets/images/units/unit-wu-officer-actions.png b/src/assets/images/units/unit-wu-officer-actions.png index 16504a9..421a759 100644 Binary files a/src/assets/images/units/unit-wu-officer-actions.png and b/src/assets/images/units/unit-wu-officer-actions.png differ diff --git a/src/assets/images/units/unit-wu-officer-harbor-actions.png b/src/assets/images/units/unit-wu-officer-harbor-actions.png index 6d5b847..cb32174 100644 Binary files a/src/assets/images/units/unit-wu-officer-harbor-actions.png and b/src/assets/images/units/unit-wu-officer-harbor-actions.png differ diff --git a/src/assets/images/units/unit-wu-officer-harbor.png b/src/assets/images/units/unit-wu-officer-harbor.png index 395c811..09d93d4 100644 Binary files a/src/assets/images/units/unit-wu-officer-harbor.png and b/src/assets/images/units/unit-wu-officer-harbor.png differ diff --git a/src/assets/images/units/unit-wu-officer-river-actions.png b/src/assets/images/units/unit-wu-officer-river-actions.png index 3e202d0..960bb65 100644 Binary files a/src/assets/images/units/unit-wu-officer-river-actions.png and b/src/assets/images/units/unit-wu-officer-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-officer-river.png b/src/assets/images/units/unit-wu-officer-river.png index bc1fb5c..ad9fbca 100644 Binary files a/src/assets/images/units/unit-wu-officer-river.png and b/src/assets/images/units/unit-wu-officer-river.png differ diff --git a/src/assets/images/units/unit-wu-officer.png b/src/assets/images/units/unit-wu-officer.png index 5537613..14fee46 100644 Binary files a/src/assets/images/units/unit-wu-officer.png and b/src/assets/images/units/unit-wu-officer.png differ diff --git a/src/assets/images/units/unit-wu-strategist-actions.png b/src/assets/images/units/unit-wu-strategist-actions.png index 682af93..0ff9adf 100644 Binary files a/src/assets/images/units/unit-wu-strategist-actions.png and b/src/assets/images/units/unit-wu-strategist-actions.png differ diff --git a/src/assets/images/units/unit-wu-strategist-fire-actions.png b/src/assets/images/units/unit-wu-strategist-fire-actions.png index ddc860e..536ccc4 100644 Binary files a/src/assets/images/units/unit-wu-strategist-fire-actions.png and b/src/assets/images/units/unit-wu-strategist-fire-actions.png differ diff --git a/src/assets/images/units/unit-wu-strategist-fire.png b/src/assets/images/units/unit-wu-strategist-fire.png index 1b20a19..dd6049c 100644 Binary files a/src/assets/images/units/unit-wu-strategist-fire.png and b/src/assets/images/units/unit-wu-strategist-fire.png differ diff --git a/src/assets/images/units/unit-wu-strategist-river-actions.png b/src/assets/images/units/unit-wu-strategist-river-actions.png index ef2bca6..53948c8 100644 Binary files a/src/assets/images/units/unit-wu-strategist-river-actions.png and b/src/assets/images/units/unit-wu-strategist-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-strategist-river.png b/src/assets/images/units/unit-wu-strategist-river.png index 10b6e18..731bb3b 100644 Binary files a/src/assets/images/units/unit-wu-strategist-river.png and b/src/assets/images/units/unit-wu-strategist-river.png differ diff --git a/src/assets/images/units/unit-wu-strategist.png b/src/assets/images/units/unit-wu-strategist.png index b57d5ef..e8db00e 100644 Binary files a/src/assets/images/units/unit-wu-strategist.png and b/src/assets/images/units/unit-wu-strategist.png differ diff --git a/src/assets/images/units/unit-wu-yi-actions.png b/src/assets/images/units/unit-wu-yi-actions.png index 4a438d3..8cfd277 100644 Binary files a/src/assets/images/units/unit-wu-yi-actions.png and b/src/assets/images/units/unit-wu-yi-actions.png differ diff --git a/src/assets/images/units/unit-wu-yi.png b/src/assets/images/units/unit-wu-yi.png index 40d7068..5bf8779 100644 Binary files a/src/assets/images/units/unit-wu-yi.png and b/src/assets/images/units/unit-wu-yi.png differ diff --git a/src/assets/images/units/unit-yan-yan-actions.png b/src/assets/images/units/unit-yan-yan-actions.png index a72cd87..5bf5493 100644 Binary files a/src/assets/images/units/unit-yan-yan-actions.png and b/src/assets/images/units/unit-yan-yan-actions.png differ diff --git a/src/assets/images/units/unit-yan-yan.png b/src/assets/images/units/unit-yan-yan.png index d298448..eb590c8 100644 Binary files a/src/assets/images/units/unit-yan-yan.png and b/src/assets/images/units/unit-yan-yan.png differ diff --git a/src/assets/images/units/unit-yi-ji-actions.png b/src/assets/images/units/unit-yi-ji-actions.png index ecc3a84..561e362 100644 Binary files a/src/assets/images/units/unit-yi-ji-actions.png and b/src/assets/images/units/unit-yi-ji-actions.png differ diff --git a/src/assets/images/units/unit-yi-ji.png b/src/assets/images/units/unit-yi-ji.png index c0e6009..e496375 100644 Binary files a/src/assets/images/units/unit-yi-ji.png and b/src/assets/images/units/unit-yi-ji.png differ diff --git a/src/assets/images/units/unit-zhang-fei-actions.png b/src/assets/images/units/unit-zhang-fei-actions.png index 8ee67f4..aa0f238 100644 Binary files a/src/assets/images/units/unit-zhang-fei-actions.png and b/src/assets/images/units/unit-zhang-fei-actions.png differ diff --git a/src/assets/images/units/unit-zhang-fei.png b/src/assets/images/units/unit-zhang-fei.png index 1074141..1d5c647 100644 Binary files a/src/assets/images/units/unit-zhang-fei.png and b/src/assets/images/units/unit-zhang-fei.png differ diff --git a/src/assets/images/units/unit-zhao-yun-actions.png b/src/assets/images/units/unit-zhao-yun-actions.png index 3fa3b86..01d78e4 100644 Binary files a/src/assets/images/units/unit-zhao-yun-actions.png and b/src/assets/images/units/unit-zhao-yun-actions.png differ diff --git a/src/assets/images/units/unit-zhao-yun.png b/src/assets/images/units/unit-zhao-yun.png index bd7eb85..39f3b95 100644 Binary files a/src/assets/images/units/unit-zhao-yun.png and b/src/assets/images/units/unit-zhao-yun.png differ diff --git a/src/assets/images/units/unit-zhuge-liang-actions.png b/src/assets/images/units/unit-zhuge-liang-actions.png index 33f511e..5689f21 100644 Binary files a/src/assets/images/units/unit-zhuge-liang-actions.png and b/src/assets/images/units/unit-zhuge-liang-actions.png differ diff --git a/src/assets/images/units/unit-zhuge-liang.png b/src/assets/images/units/unit-zhuge-liang.png index 3d1c7f3..fc27036 100644 Binary files a/src/assets/images/units/unit-zhuge-liang.png and b/src/assets/images/units/unit-zhuge-liang.png differ diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index b3b5978..d2b4c89 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -9504,7 +9504,7 @@ export class BattleScene extends Phaser.Scene { } private applyActionSpriteBlend(sprite: Phaser.GameObjects.Sprite) { - sprite.setBlendMode(Phaser.BlendModes.SCREEN); + sprite.setBlendMode(Phaser.BlendModes.NORMAL); } private applyBaseSpriteBlend(sprite: Phaser.GameObjects.Sprite) {