| """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) |
| |
| draw.ellipse([4, 8, size - 4, size - 4], fill=color + (255,)) |
| |
| head_size = size // 3 |
| draw.ellipse([size // 2 - head_size, 1, size // 2 + head_size, head_size * 2 + 1], |
| fill=_darken(color, 20) + (255,)) |
| |
| 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_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_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,)) |
| |
| 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)) |
|
|
| |
| 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) |
| |
| 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) |
|
|
| |
| 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() |
|
|