Files
gen1recomp/tools/generate_android_icons.py
1jamie 76ede1171e feat: Gen 3 save editor, GBA extraction resilience, and UI/asset polish
- Save Editor (Gen 3):
  - Added full support for EV, IV, Nature, Ability, and Ribbons editing for Gen 3 Pokémon.
  - Implemented stat calculations, personality-based nature derivation, and Gen 3 save data structures.
  - Added comprehensive unit test suite in tests/save_editor_gen3_ev_iv_tests.lua.

- GBA Extraction & Graphics:
  - Added zero-copy ARM-safe ROM accessor and fast LZ77 string decompression.
  - Fixed bag chrome decompression string-buffer indexing preventing solid-color renders.
  - Fixed TM Case HM badge tile offset and palette mapping.
  - Added builtin moves fallback in src/core/game3/battle/builtin_moves.lua.
  - Improved parallel extraction resilience, desktop fallback search paths, and prefix handling.

- Android & Launchers:
  - Added dynamic high-res app launcher shortcuts and color palettes for all supported games.
  - Updated Android icon generator and intent dispatch handlers.
2026-09-25 16:20:40 -05:00

191 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""
Generates the Android adaptive launcher icon from assets/logo/gen1recomp_cover.png
and 3D cartridge shortcut assets for every density bucket
(mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi).
"""
import os
from PIL import Image, ImageDraw, ImageFilter
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RES_DIR = os.path.join(ROOT, "mobile", "android", "app", "src", "main", "res")
COVER = os.path.join(ROOT, "assets", "logo", "gen1recomp_cover.png")
DENSITIES = {
"mdpi": {"shortcut": 48, "adaptive": 108},
"hdpi": {"shortcut": 72, "adaptive": 162},
"xhdpi": {"shortcut": 96, "adaptive": 216},
"xxhdpi": {"shortcut": 144, "adaptive": 324},
"xxxhdpi": {"shortcut": 192, "adaptive": 432},
}
SHELL_COLORS = {
# Gen 1 (GB)
"red": {"main": (230, 45, 55), "dark": (175, 25, 35), "light": (255, 90, 100), "shape": "gb"},
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255), "shape": "gb"},
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80), "shape": "gb"},
# Gen 2 (GBC)
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80), "shape": "gb"},
"silver": {"main": (185, 190, 200), "dark": (130, 135, 145), "light": (225, 230, 240), "shape": "gb"},
"crystal": {"main": (105, 195, 225), "dark": (60, 135, 165), "light": (180, 240, 255), "shape": "gb"},
# Gen 3 (GBA)
"firered": {"main": (235, 65, 35), "dark": (175, 30, 15), "light": (255, 115, 75), "shape": "gba"},
"leafgreen": {"main": (40, 175, 80), "dark": (20, 120, 45), "light": (85, 215, 125), "shape": "gba"},
"ruby": {"main": (195, 30, 50), "dark": (140, 15, 30), "light": (240, 75, 95), "shape": "gba"},
"sapphire": {"main": (25, 70, 185), "dark": (15, 40, 135), "light": (70, 120, 240), "shape": "gba"},
"emerald": {"main": (20, 155, 80), "dark": (10, 105, 50), "light": (65, 205, 125), "shape": "gba"},
}
def create_adaptive_foreground(size):
"""Full-bleed cover. The launcher mask crops the edges; the mark stays centered."""
cover = Image.open(COVER).convert("RGBA")
return cover.resize((size, size), Image.Resampling.LANCZOS)
def render_3d_cartridge(version, size):
conf = SHELL_COLORS[version]
shape = conf.get("shape", "gb")
colors = conf
S = size * 4
# Transparent canvas so the cartridge sits directly on launcher's white circle plate
canvas = Image.new("RGBA", (S, S), (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
if shape == "gba":
pad_x = int(S * 0.04)
cw = S - pad_x * 2
ch = int(cw * 0.64)
pad_y = (S - ch) // 2
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 6, pad_y + 12, pad_x + cw + 6, pad_y + ch + 12],
radius=int(S * 0.04), fill=(0, 0, 0, 95))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
radius = int(S * 0.04)
depth = int(S * 0.025)
# 3D lower bevel
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
# Main shell
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
# Top ridge
top_lip_h = int(ch * 0.12)
draw.rounded_rectangle([pad_x + 4, pad_y + 2, pad_x + cw - 4, pad_y + top_lip_h],
radius=int(radius * 0.5), fill=colors["light"])
draw.rounded_rectangle([pad_x + 6, pad_y + 4, pad_x + cw - 6, pad_y + top_lip_h - 1],
radius=int(radius * 0.4), fill=colors["main"])
# Label area (landscape aspect ratio ~ 2:1)
label_margin_x = int(cw * 0.08)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
# Label border/recess
draw.rounded_rectangle([label_x - 2, label_top_y - 2, label_x + label_w + 2, label_top_y + label_h + 2],
radius=int(radius * 0.5), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.4), fill=255)
canvas.paste(label_img, (label_x, label_top_y), mask)
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.4), fill=(240, 240, 240, 255))
# Outer highlight
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
else:
# Tight padding to maximize cartridge size
pad_x = int(S * 0.05)
pad_y = int(S * 0.03)
cw = S - pad_x * 2
ch = S - pad_y * 2
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 8, pad_y + 14, pad_x + cw + 8, pad_y + ch + 14],
radius=int(S * 0.05), fill=(0, 0, 0, 90))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
radius = int(S * 0.045)
depth = int(S * 0.03)
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
notch_w = int(cw * 0.65)
notch_h = int(ch * 0.07)
notch_x = pad_x + (cw - notch_w) // 2
notch_y = pad_y + int(ch * 0.035)
draw.rounded_rectangle([notch_x, notch_y, notch_x + notch_w, notch_y + notch_h],
radius=int(notch_h * 0.4), fill=colors["dark"])
draw.rounded_rectangle([notch_x, notch_y - 2, notch_x + notch_w, notch_y + notch_h - 2],
radius=int(notch_h * 0.4), fill=colors["light"])
label_margin_x = int(cw * 0.09)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
draw.rounded_rectangle([label_x - 3, label_top_y - 3, label_x + label_w + 3, label_top_y + label_h + 3],
radius=int(radius * 0.7), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.5), fill=255)
canvas.paste(label_img, (label_x, label_top_y), mask)
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.5), fill=(240, 240, 240, 255))
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
return canvas.resize((size, size), Image.Resampling.LANCZOS)
def main():
os.makedirs(os.path.join(RES_DIR, "values"), exist_ok=True)
os.makedirs(os.path.join(RES_DIR, "mipmap-anydpi-v26"), exist_ok=True)
for density, sizes in DENSITIES.items():
drawable_dir = os.path.join(RES_DIR, f"drawable-{density}")
os.makedirs(drawable_dir, exist_ok=True)
fg = create_adaptive_foreground(sizes["adaptive"])
fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG")
for ver in SHELL_COLORS:
cart = render_3d_cartridge(ver, sizes["shortcut"])
cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
print(f"Generated assets for drawable-{density}")
if __name__ == "__main__":
main()