Files
mice/tools/lpc_extract/restyle_v11.py
T
Matteo Benedetto e26464d843 Merge origin/new-newnassets: new LPC rat assets and animations
Cherry-picked from new-newnassets (3 commits):
- bdf5a8d Add new start animation asset for game launch
- 5f2d09f Fix rat animations and restore map tiles
- 2e2c5bb feat: add LPC-style rat assets, sprite generation scripts, and grid extraction tools

Conflicts resolved:
- engine/sdl2.py: kept 'if scale:' wrapper (compatible, default scale=True)
- units/rat.py: kept animation frame computation (partial_move * 4)
- 12 PNG files (BMP_BABY/MALE/FEMALE_*): kept new-newnassets versions

Cleanup applied: removed ~480 working artifacts not appropriate for the
production repo:
- 25 PNG in repo root (male_rats*.png, spritesheet*.png)
- 416 tile extracts in tools/lpc_extract/grid/
- scattered debug PNGs (bushes, preview, montages, etc.)
- early iteration scripts (animate_rat v1-v4, restyle v2-v10, etc.)

Kept: 32 new LPC-style sprites in assets/Rat_LPC_Style/, 4 animation
sprite sheets in assets/Rat_Animated/, 64 updated Rat sprites, 22
final-version generator scripts, plus all .py/.md updates.
2026-06-27 17:53:45 +02:00

216 lines
9.2 KiB
Python

import glob
import os
import random
import math
from PIL import Image
# Vibrant colors closer to the original 8-bit sprites, but rich and organic
C_DARK = (15, 110, 35, 255)
C_MED = (30, 155, 45, 255)
C_LIGHT = (45, 195, 60, 255)
C_BRIGHT = (70, 230, 80, 255)
C_SHADOW = (20, 50, 30, 100) # Softer, greener shadow instead of black/purple
C_SHADOW_SOFT = (20, 50, 30, 50)
C_DIRT = (128, 128, 128, 255)
def draw_wrapped_circle(pixels, w, h, cx, cy, r, color):
for y in range(int(cy - r - 1), int(cy + r + 2)):
for x in range(int(cx - r - 1), int(cx + r + 2)):
d = math.hypot(x - cx, y - cy)
if d <= r:
pixels[x % w, y % h] = color
def generate_seamless_hedge(w, h):
img = Image.new('RGBA', (w, h), C_MED)
pixels = img.load()
# Large clusters of base grass color
for _ in range((w * h) // 16):
cx, cy = random.uniform(0, w), random.uniform(0, h)
draw_wrapped_circle(pixels, w, h, cx, cy, random.uniform(2.0, 3.5), C_LIGHT)
# Highlights
for _ in range((w * h) // 32):
cx, cy = random.uniform(0, w), random.uniform(0, h)
draw_wrapped_circle(pixels, w, h, cx, cy, random.uniform(1.0, 2.0), C_BRIGHT)
# Tiny dark gaps
for _ in range((w * h) // 80):
cx, cy = random.uniform(0, w), random.uniform(0, h)
draw_wrapped_circle(pixels, w, h, cx, cy, random.uniform(0.5, 1.2), C_DARK)
return img
def get_class(rgb):
if rgb in [(0, 128, 0), (0, 255, 0)]:
return 'GRASS'
return 'DIRT'
def alpha_blend(c1, c2):
a = c1[3] / 255.0
r = int(c1[0] * a + c2[0] * (1 - a))
g = int(c1[1] * a + c2[1] * (1 - a))
b = int(c1[2] * a + c2[2] * (1 - a))
return (r, g, b, 255)
input_files = glob.glob('/home/enne2/dev/mice/assets/Rat/*BMP_1*.png')
out_dir = '/home/enne2/dev/mice/assets/Rat_LPC_Style/'
os.makedirs(out_dir, exist_ok=True)
base_hedge_64 = generate_seamless_hedge(64, 64)
for f in input_files:
basename = os.path.basename(f)
img = Image.open(f).convert('RGBA')
width, height = img.size
pixels = img.load()
out_img = Image.new('RGBA', (width, height))
out_pixels = out_img.load()
is_fx = 'EXPLOSION' in basename or 'GAS' in basename
is_flower = 'FLOWER' in basename
mask = {}
for y in range(height):
for x in range(width):
px = pixels[x, y][:3]
cls = get_class(px)
if is_flower:
mask[(x, y)] = 'GRASS'
elif is_fx:
if cls == 'GRASS':
mask[(x, y)] = 'GRASS'
else:
mask[(x, y)] = 'DIRT'
else:
mask[(x, y)] = cls
tile_hedge = base_hedge_64.copy()
tile_pixels = tile_hedge.load()
if basename == 'BMP_1_GRASS_2.png':
for _ in range(8):
cx, cy = random.randint(16, 48), random.randint(16, 48)
tile_pixels[cx, cy] = (220, 50, 50, 255)
elif basename == 'BMP_1_GRASS_3.png':
for _ in range(12):
cx, cy = random.randint(16, 48), random.randint(16, 48)
tile_pixels[cx, cy] = (255, 255, 255, 255)
elif basename == 'BMP_1_GRASS_4.png':
for _ in range(20):
cx, cy = random.randint(20, 44), random.randint(20, 44)
draw_wrapped_circle(tile_pixels, 64, 64, cx, cy, random.uniform(1.0, 2.5), C_MED)
for y in range(height):
for x in range(width):
if mask[(x, y)] == 'GRASS':
dist_to_dirt = 999
for dy in range(-3, 4):
for dx in range(-3, 4):
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height:
if mask[(nx, ny)] == 'DIRT':
d = math.hypot(dx, dy)
if d < dist_to_dirt:
dist_to_dirt = d
if dist_to_dirt <= 1.5:
dirt_below = (y+1 < height and mask[(x, y+1)] == 'DIRT') or (y+2 < height and mask[(x, y+2)] == 'DIRT')
if dirt_below:
# Solid dark green outline on the bottom (no more dashes!)
out_pixels[x, y] = C_DARK
else:
out_pixels[x, y] = C_MED
elif dist_to_dirt <= 2.5:
out_pixels[x, y] = C_MED
else:
out_pixels[x, y] = tile_pixels[x % 64, y % 64]
else:
orig_color = pixels[x, y][:3]
if orig_color == (0, 0, 0) and not is_fx:
# Replace debug dashed lines with dirt
orig_color = C_DIRT[:3]
if orig_color == (0, 0, 0):
out_pixels[x, y] = (0, 0, 0, 255)
elif orig_color in [(128,128,128), (192,192,192)]:
dist_to_grass = 999
for dy in range(-2, 3):
for dx in range(-2, 3):
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height:
if mask[(nx, ny)] == 'GRASS':
d = math.hypot(dx, dy)
if d < dist_to_grass:
dist_to_grass = d
base_c = orig_color
if dist_to_grass <= 1.5:
out_pixels[x, y] = alpha_blend(C_SHADOW, base_c)
elif dist_to_grass <= 2.5:
out_pixels[x, y] = alpha_blend(C_SHADOW_SOFT, base_c)
else:
out_pixels[x, y] = base_c + (255,)
else:
out_pixels[x, y] = orig_color + (255,)
# Flowers logic
if is_flower:
def draw_circle(cx, cy, r, color, outline=None):
for fy in range(cy - r - 1, cy + r + 2):
for fx in range(cx - r - 1, cx + r + 2):
if 0 <= fx < width and 0 <= fy < height:
d = math.hypot(fx - cx, fy - cy)
if d <= r:
out_pixels[fx, fy] = color
elif outline and r < d <= r + 1.2:
if out_pixels[fx, fy] != color:
out_pixels[fx, fy] = outline
def draw_petal(cx, cy, angle, length, pw, color, outline):
cos_a, sin_a = math.cos(angle), math.sin(angle)
size = int(length + pw)
for fy in range(cy - size, cy + size + 1):
for fx in range(cx - size, cx + size + 1):
if 0 <= fx < width and 0 <= fy < height:
dx, dy = fx - cx, fy - cy
rx = dx * cos_a + dy * sin_a
ry = -dx * sin_a + dy * cos_a
if length > 0 and pw > 0:
val = (rx / length)**2 + (ry / pw)**2
if val <= 1.0:
out_pixels[fx, fy] = color
elif val <= 1.5:
if out_pixels[fx, fy] != color:
out_pixels[fx, fy] = outline
def draw_large_flower(cx, cy, petal_color, outline_color, center_color, num_petals, petal_length, petal_width):
draw_circle(cx, cy + 3, petal_length + 1, (20, 50, 20, 200))
for i in range(num_petals):
angle = i * (2 * math.pi / num_petals)
pcolor = (int(petal_color[0]*0.8), int(petal_color[1]*0.8), int(petal_color[2]*0.8), 255) if math.sin(angle) > 0 else petal_color
draw_petal(cx, cy, angle, petal_length, petal_width, pcolor, outline_color)
draw_circle(cx, cy, petal_width + 1, center_color, (int(center_color[0]*0.6), int(center_color[1]*0.6), int(center_color[2]*0.6), 255))
if 'FLOWER_1' in basename:
draw_large_flower(32, 32, (240, 240, 240, 255), (100, 100, 110, 255), (240, 200, 50, 255), 10, 14, 5)
elif 'FLOWER_2' in basename:
draw_large_flower(16, 16, (200, 40, 40, 255), (80, 20, 20, 255), (240, 200, 50, 255), 5, 7, 4)
draw_large_flower(48, 20, (200, 40, 40, 255), (80, 20, 20, 255), (240, 200, 50, 255), 5, 7, 4)
draw_large_flower(32, 48, (200, 40, 40, 255), (80, 20, 20, 255), (240, 200, 50, 255), 5, 7, 4)
elif 'FLOWER_3' in basename:
draw_large_flower(32, 32, (220, 80, 180, 255), (100, 30, 80, 255), (240, 200, 50, 255), 8, 12, 6)
elif 'FLOWER_4' in basename:
draw_large_flower(32, 32, (80, 150, 230, 255), (30, 70, 120, 255), (240, 200, 50, 255), 8, 12, 6)
if is_fx:
for y in range(height):
for x in range(width):
px = pixels[x, y][:3]
if get_class(px) != 'GRASS' and px not in [(128,128,128), (192,192,192), (0,0,0)]:
out_pixels[x, y] = px + (255,)
out_img.save(os.path.join(out_dir, basename))
print("V11 - Vibrant 8-bit-like colors and NO dashed lines!")