midnight_mystery / scripts /generate_assets.py
lcpizani's picture
Add full app: pages, assets, backend, config
811d960
Raw
History Blame Contribute Delete
5.69 kB
"""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()