mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
76ede1171e
- 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.
191 lines
8.8 KiB
Python
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()
|