"""Generate placeholder sprite assets using Pillow.""" from pathlib import Path from PIL import Image, ImageDraw, ImageFont ASSETS = Path(__file__).parent.parent / "app" / "static" / "assets" SPECIES_COLORS = { "cat": (180, 140, 90), "dog": (160, 120, 70), "rabbit": (220, 200, 180), "bird": (80, 140, 200), "fox": (200, 100, 40), "hamster": (220, 180, 130), "turtle": (80, 140, 80), "corgi": (200, 150, 80), "hound": (120, 100, 80), } EXPRESSION_TINTS = { "neutral": (0, 0, 0, 0), "nervous": (40, 0, 0, 30), "angry": (80, 0, 0, 60), "guilty": (0, 0, 60, 40), } def _darken(color, amount=40): return tuple(max(0, c - amount) for c in color) def make_animal_sprite(species: str, size: int = 32) -> Image.Image: color = SPECIES_COLORS.get(species, (150, 150, 150)) img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Body ellipse draw.ellipse([4, 8, size - 4, size - 4], fill=color + (255,)) # Head circle head_size = size // 3 draw.ellipse([size // 2 - head_size, 1, size // 2 + head_size, head_size * 2 + 1], fill=_darken(color, 20) + (255,)) # Eyes ey = head_size // 2 + 1 draw.ellipse([size // 2 - head_size // 2, ey, size // 2 - head_size // 2 + 3, ey + 3], fill=(20, 20, 20, 255)) draw.ellipse([size // 2 + head_size // 2 - 4, ey, size // 2 + head_size // 2 - 1, ey + 3], fill=(20, 20, 20, 255)) return img def make_portrait(species: str, expression: str, size: int = 128) -> Image.Image: color = SPECIES_COLORS.get(species, (150, 150, 150)) tint = EXPRESSION_TINTS.get(expression, (0, 0, 0, 0)) img = Image.new("RGBA", (size, size), (13, 27, 42, 255)) draw = ImageDraw.Draw(img) # Body body_color = tuple(min(255, c + tint[i]) for i, c in enumerate(color)) draw.ellipse([size // 6, size // 3, size * 5 // 6, size - 4], fill=body_color + (255,)) # Head head_r = size // 4 cx = size // 2 cy = size // 3 draw.ellipse([cx - head_r, cy - head_r, cx + head_r, cy + head_r], fill=_darken(body_color, 15) + (255,)) # Eyes ex_off = head_r // 2 ey_off = head_r // 6 eye_r = max(3, head_r // 5) for ex in [cx - ex_off, cx + ex_off]: draw.ellipse([ex - eye_r, cy - ey_off - eye_r, ex + eye_r, cy - ey_off + eye_r], fill=(255, 255, 255, 255)) inner = eye_r // 2 draw.ellipse([ex - inner, cy - ey_off - inner, ex + inner, cy - ey_off + inner], fill=(20, 20, 20, 255)) # Expression-specific mouth if expression in ("nervous", "guilty"): draw.arc([cx - head_r // 3, cy + head_r // 4, cx + head_r // 3, cy + head_r // 2], 0, 180, fill=(20, 20, 20, 255), width=2) elif expression == "angry": draw.line([cx - head_r // 3, cy + head_r // 3, cx + head_r // 3, cy + head_r // 3], fill=(20, 20, 20, 255), width=2) # Angry brows draw.line([cx - ex_off - eye_r, cy - ey_off - eye_r * 2, cx - ex_off + eye_r, cy - ey_off - eye_r], fill=(80, 20, 20, 255), width=2) draw.line([cx + ex_off - eye_r, cy - ey_off - eye_r, cx + ex_off + eye_r, cy - ey_off - eye_r * 2], fill=(80, 20, 20, 255), width=2) else: draw.arc([cx - head_r // 3, cy + head_r // 8, cx + head_r // 3, cy + head_r // 2], 180, 360, fill=(20, 20, 20, 255), width=2) # Name label at bottom draw.rectangle([0, size - 22, size, size], fill=(13, 27, 42, 200)) draw.text((4, size - 18), f"{species} ยท {expression}", fill=(228, 213, 163, 255)) return img def make_tile(name: str, color: tuple, size: int = 32) -> Image.Image: img = Image.new("RGBA", (size, size), color + (255,)) draw = ImageDraw.Draw(img) draw.rectangle([0, 0, size - 1, size - 1], outline=(0, 0, 0, 60), width=1) return img def generate_all(): animals_dir = ASSETS / "animals" portraits_dir = ASSETS / "portraits" tiles_dir = ASSETS / "tiles" for d in [animals_dir, portraits_dir, tiles_dir]: d.mkdir(parents=True, exist_ok=True) for species in SPECIES_COLORS: sprite = make_animal_sprite(species) sprite.save(animals_dir / f"{species}_idle.png") for i in range(4): sprite.save(animals_dir / f"{species}_walk_{i}.png") for species in SPECIES_COLORS: for expression in EXPRESSION_TINTS: portrait = make_portrait(species, expression) portrait.save(portraits_dir / f"{species}_{expression}.png") tile_defs = { "floor_dark": (20, 20, 30), "floor_wood": (80, 55, 30), "wall_brick": (60, 35, 25), "wall_plaster": (45, 45, 50), "grass": (25, 55, 25), "path": (50, 45, 35), "door": (90, 60, 30), "furniture_bed": (70, 50, 80), "furniture_sofa":(50, 40, 70), "furniture_table":(65, 45, 25), "tree": (20, 60, 20), "bush": (30, 70, 30), "fence": (70, 55, 35), } for name, color in tile_defs.items(): tile = make_tile(name, color) tile.save(tiles_dir / f"{name}.png") print(f"Generated assets in {ASSETS}") print(f" animals/: {len(list(animals_dir.iterdir()))} files") print(f" portraits/: {len(list(portraits_dir.iterdir()))} files") print(f" tiles/: {len(list(tiles_dir.iterdir()))} files") if __name__ == "__main__": generate_all()