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.
This commit is contained in:
Matteo Benedetto
2026-06-27 17:53:45 +02:00
parent c1662ecf29
commit e26464d843
139 changed files with 857 additions and 7 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

+24
View File
@@ -0,0 +1,24 @@
from PIL import Image
import glob
from collections import Counter
files = glob.glob('/home/enne2/dev/mice/assets/Rat/*BMP_1*.png')
colors = Counter()
for f in files:
img = Image.open(f).convert('RGB')
for pixel in img.getdata():
colors[pixel] += 1
print("Original Colors:")
for c, count in colors.most_common(10):
print(c, count)
lpc_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGB')
lpc_colors = Counter()
for pixel in lpc_img.getdata():
lpc_colors[pixel] += 1
print("\nLPC Colors:")
for c, count in lpc_colors.most_common(20):
print(c, count)
+22
View File
@@ -0,0 +1,22 @@
import glob
from PIL import Image
def analyze_edges(img_path):
img = Image.open(img_path).convert('RGB')
w, h = img.size
pixels = img.load()
top = [pixels[x, 0] for x in range(w)]
bottom = [pixels[x, h-1] for x in range(w)]
left = [pixels[0, y] for y in range(h)]
right = [pixels[w-1, y] for y in range(h)]
print(f"File: {img_path}")
print(f"Top edge unique colors: {set(top)}")
print(f"Bottom edge unique colors: {set(bottom)}")
print(f"Left edge unique colors: {set(left)}")
print(f"Right edge unique colors: {set(right)}")
print("---------------------------------")
for f in ['BMP_1_GRASS_1.png', 'BMP_1_GRASS_2.png', 'BMP_1_N.png']:
analyze_edges(f'/tmp/orig_rat/{f}')
+84
View File
@@ -0,0 +1,84 @@
import os
import math
from PIL import Image
C_PUPIL = (202, 0, 0, 255)
C_WHITE = (255, 255, 255, 255)
C_BODY = (47, 96, 130, 255)
def animate_rat_v5(img, direction):
w, h = img.size
frames = []
pixels = img.load()
bg_color = (125, 125, 125, 255)
for frame_idx in range(4):
frame = Image.new('RGBA', (w, h), bg_color)
fp = frame.load()
offset_mag = 0
bounce_y = 0
is_blink = False
if frame_idx == 1:
offset_mag = -8
bounce_y = -1
elif frame_idx == 3:
offset_mag = 8
bounce_y = -1
is_blink = True
for y in range(h):
for x in range(w):
px = pixels[x, y]
if px != bg_color and px[3] > 0:
is_tail = False
if direction == 'UP' and y > 40: is_tail = True
if direction == 'DOWN' and y < 24: is_tail = True
if direction == 'LEFT' and x > 40: is_tail = True
if direction == 'RIGHT' and x < 24: is_tail = True
nx, ny = x, y
if is_tail:
if direction == 'UP':
factor = (y - 40) / 20.0
nx = int(x + offset_mag * (factor**1.8))
ny = y + bounce_y # FIX: tail bounces too!
elif direction == 'DOWN':
factor = (24 - y) / 20.0
nx = int(x + offset_mag * (factor**1.8))
ny = y + bounce_y # FIX: tail bounces too!
elif direction == 'LEFT':
factor = (x - 40) / 20.0
ny = int(y + offset_mag * (factor**1.8)) + bounce_y
elif direction == 'RIGHT':
factor = (24 - x) / 20.0
ny = int(y + offset_mag * (factor**1.8)) + bounce_y
else:
ny = y + bounce_y
if 0 <= nx < w and 0 <= ny < h:
if is_blink and px in [C_WHITE, C_PUPIL]:
fp[nx, ny] = C_BODY
else:
fp[nx, ny] = px
frames.append(frame)
return frames
os.makedirs('/home/enne2/dev/mice/assets/Rat_Animated', exist_ok=True)
for d in ['UP', 'DOWN', 'LEFT', 'RIGHT']:
img = Image.open(f'/home/enne2/dev/mice/assets/Rat/BMP_MALE_{d}.png').convert('RGBA')
frames = animate_rat_v5(img, d)
w, h = img.size
sheet = Image.new('RGBA', (w * 4, h))
for i, f in enumerate(frames):
sheet.paste(f, (i * w, 0))
sheet.save(f'/home/enne2/dev/mice/assets/Rat_Animated/MALE_{d}_anim.png')
print("Animations fixed: no more gap between tail and body!")
+9
View File
@@ -0,0 +1,9 @@
from PIL import Image
img = Image.open('/home/enne2/dev/mice/assets/Rat_LPC_Style/BMP_1_E.png').convert('RGBA')
pixels = img.load()
w, h = img.size
for x in range(w):
for y in range(h):
if pixels[x, y][:3] == (0, 0, 0):
print(f"Black at {x}, {y}: {pixels[x, y]}")
+14
View File
@@ -0,0 +1,14 @@
from PIL import Image
import os
img = Image.open('/home/enne2/dev/mice/assets/Rat/BMP_MALE_UP.png').convert('RGBA')
pixels = img.load()
w, h = img.size
print(f"Size: {w}x{h}")
colors = set()
for y in range(h):
for x in range(w):
if pixels[x,y][3] > 0:
colors.add(pixels[x,y])
print("Colors:", colors)
+4
View File
@@ -0,0 +1,4 @@
from PIL import Image
for d in ['UP', 'DOWN', 'LEFT', 'RIGHT']:
img = Image.open(f'/home/enne2/dev/mice/assets/Rat/BMP_MALE_{d}.png')
print(f"{d}: {img.size}")
+4
View File
@@ -0,0 +1,4 @@
from PIL import Image
img = Image.open('/home/enne2/dev/gameboy-hello/iso_test/doc/1719484435-screenshot.png').convert('RGB')
# wait, the image is at /home/enne2/.gemini/... no, it's not local. Let me find it!
+11
View File
@@ -0,0 +1,11 @@
from PIL import Image
lpc = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
grass = lpc.crop((4*32, 1*32, 5*32, 2*32))
bush_center = lpc.crop((1*32, 1*32, 2*32, 2*32))
g_data = list(grass.getdata())
b_data = list(bush_center.getdata())
diff = sum(1 for i in range(len(g_data)) if g_data[i] != b_data[i])
print(f"Differences: {diff} pixels out of {len(g_data)}")
+35
View File
@@ -0,0 +1,35 @@
from PIL import Image
# Load the source image
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
# Define a function to extract a 32x32 tile and scale it to 64x64
def get_tile(tx, ty):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
img = src_img.crop(box)
return img.resize((64, 64), Image.NEAREST)
# Create a dictionary of the files to create and their corresponding tiles
# Using some standard LPC terrain coordinates as a guess
tiles = {
'BMP_1_GRASS_1.png': get_tile(4, 1), # center of grass-on-dirt
'BMP_1_GRASS_2.png': get_tile(4, 1),
'BMP_1_GRASS_3.png': get_tile(4, 1),
'BMP_1_GRASS_4.png': get_tile(4, 1),
# Let's extract some path edges for N, S, E, W
'BMP_1_N.png': get_tile(4, 0), # Top edge of grass (dirt above)
'BMP_1_S.png': get_tile(4, 2), # Bottom edge
'BMP_1_E.png': get_tile(5, 1), # Right edge
'BMP_1_W.png': get_tile(3, 1), # Left edge
# Corners
'BMP_1_NW.png': get_tile(3, 0),
'BMP_1_NE.png': get_tile(5, 0),
'BMP_1_SW.png': get_tile(3, 2),
'BMP_1_SE.png': get_tile(5, 2),
}
for name, img in tiles.items():
img.save('/home/enne2/dev/mice/tools/lpc_extract/' + name)
print(f"Saved {name}")
+16
View File
@@ -0,0 +1,16 @@
from PIL import Image
import os
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
os.makedirs('/home/enne2/dev/mice/tools/lpc_extract/grid', exist_ok=True)
cols = src_img.width // 32
rows = src_img.height // 32
for ty in range(min(10, rows)):
for tx in range(min(10, cols)):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
tile = src_img.crop(box)
tile.save(f'/home/enne2/dev/mice/tools/lpc_extract/grid/{tx}_{ty}.png')
print("Created grid tiles")
+21
View File
@@ -0,0 +1,21 @@
from PIL import Image
import os
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
cols = src_img.width // 32
rows = src_img.height // 32
for ty in range(10, rows):
for tx in range(cols):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
tile = src_img.crop(box)
tile.save(f'/home/enne2/dev/mice/tools/lpc_extract/grid/{tx}_{ty}.png')
for ty in range(10):
for tx in range(10, cols):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
tile = src_img.crop(box)
tile.save(f'/home/enne2/dev/mice/tools/lpc_extract/grid/{tx}_{ty}.png')
print("Created rest of grid tiles")
+17
View File
@@ -0,0 +1,17 @@
from PIL import Image
import os
lpc_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
os.makedirs('/home/enne2/dev/mice/tools/lpc_extract/grass_test', exist_ok=True)
def get_lpc_texture(tx, ty):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
return lpc_img.crop(box)
# Let's extract some potential grass tiles. We know (4,1) is grass.
# It's likely part of a 3x3 block from (3,0) to (5,2) maybe?
for y in range(0, 4):
for x in range(3, 7):
tile = get_lpc_texture(x, y)
tile.save(f'/home/enne2/dev/mice/tools/lpc_extract/grass_test/tile_{x}_{y}.png')
+90
View File
@@ -0,0 +1,90 @@
from PIL import Image, ImageDraw
import os
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
out_dir = '/home/enne2/dev/mice/assets/Rat'
def get_tile(tx, ty):
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
return src_img.crop(box)
def scale(img):
return img.resize((64, 64), Image.NEAREST)
# Grass variations (just use the same grass for now)
grass = get_tile(4, 1)
grass_scaled = scale(grass)
grass_scaled.save(os.path.join(out_dir, 'BMP_1_GRASS_1.png'))
grass_scaled.save(os.path.join(out_dir, 'BMP_1_GRASS_2.png'))
grass_scaled.save(os.path.join(out_dir, 'BMP_1_GRASS_3.png'))
grass_scaled.save(os.path.join(out_dir, 'BMP_1_GRASS_4.png'))
# Paths
paths = {
'BMP_1_N.png': (4, 0),
'BMP_1_S.png': (4, 2),
'BMP_1_E.png': (5, 1),
'BMP_1_W.png': (3, 1),
'BMP_1_NE.png': (5, 0),
'BMP_1_NW.png': (3, 0),
'BMP_1_SE.png': (5, 2),
'BMP_1_SW.png': (3, 2),
'BMP_1_WN.png': (3, 3), # dirt bottom-right
'BMP_1_WS.png': (3, 4), # dirt top-right
'BMP_1_EN.png': (4, 3), # dirt bottom-left
'BMP_1_ES.png': (4, 4), # dirt top-left
}
for name, (tx, ty) in paths.items():
scale(get_tile(tx, ty)).save(os.path.join(out_dir, name))
# Flowers - draw simple flowers on grass
def make_flower(color, positions):
img = grass.copy()
draw = ImageDraw.Draw(img)
for px, py in positions:
draw.rectangle([px, py, px+2, py+2], fill=color)
return scale(img)
make_flower((255, 255, 255), [(16, 16), (10, 20), (22, 10)]).save(os.path.join(out_dir, 'BMP_1_FLOWER_1.png')) # white
make_flower((255, 0, 0), [(10, 10), (20, 20), (14, 26)]).save(os.path.join(out_dir, 'BMP_1_FLOWER_2.png')) # red
make_flower((255, 100, 200), [(16, 16)]).save(os.path.join(out_dir, 'BMP_1_FLOWER_3.png')) # pink
make_flower((100, 200, 255), [(8, 8), (24, 24)]).save(os.path.join(out_dir, 'BMP_1_FLOWER_4.png')) # blue
# Caves - draw black hole on dirt (tx=7, ty=1)
dirt = get_tile(7, 1)
def make_cave(direction):
img = dirt.copy()
draw = ImageDraw.Draw(img)
if direction == 'UP':
draw.pieslice([8, -16, 24, 16], 0, 180, fill=(0,0,0))
elif direction == 'DOWN':
draw.pieslice([8, 16, 24, 48], 180, 360, fill=(0,0,0))
elif direction == 'LEFT':
draw.pieslice([-16, 8, 16, 24], 270, 90, fill=(0,0,0))
elif direction == 'RIGHT':
draw.pieslice([16, 8, 48, 24], 90, 270, fill=(0,0,0))
return scale(img)
make_cave('UP').save(os.path.join(out_dir, 'BMP_1_CAVE_UP.png'))
make_cave('DOWN').save(os.path.join(out_dir, 'BMP_1_CAVE_DOWN.png'))
make_cave('LEFT').save(os.path.join(out_dir, 'BMP_1_CAVE_LEFT.png'))
make_cave('RIGHT').save(os.path.join(out_dir, 'BMP_1_CAVE_RIGHT.png'))
# Gas and Explosion
def make_fx(color, is_gas=False):
img = grass.copy()
draw = ImageDraw.Draw(img)
if is_gas:
draw.ellipse([4, 4, 28, 28], fill=(200, 200, 200, 200))
draw.ellipse([8, 8, 20, 20], fill=(255, 255, 255, 220))
else:
draw.ellipse([4, 4, 28, 28], fill=(255, 100, 0, 255))
draw.ellipse([8, 8, 20, 20], fill=(255, 255, 0, 255))
return scale(img)
for d in ['UP', 'DOWN', 'LEFT', 'RIGHT']:
make_fx(None, True).save(os.path.join(out_dir, f'BMP_1_GAS_{d}.png'))
make_fx(None, False).save(os.path.join(out_dir, f'BMP_1_EXPLOSION_{d}.png'))
print("All 32 files generated.")
@@ -0,0 +1,154 @@
import os
import math
from PIL import Image
RAT_CONFIGS = {
'MALE': {
'C_PUPIL': (202, 0, 0, 255),
'C_BODY': (47, 96, 130, 255),
'C_PINK': (254, 0, 241, 255)
},
'FEMALE': {
'C_PUPIL': (255, 0, 0, 255),
'C_BODY': (235, 94, 167, 255),
'C_PINK': (128, 0, 128, 255) # or similar
},
'BABY': {
'C_PUPIL': None,
'C_BODY': (192, 192, 192, 255),
'C_PINK': None
}
}
C_WHITE = (255, 255, 255, 255)
C_BLACK = (0, 0, 0, 255)
BG_COLOR = (125, 125, 125, 255)
def get_color_priority(color, config):
if color[3] == 0 or color == BG_COLOR: return 0
if color == C_BLACK: return 100
if color == config.get('C_PUPIL'): return 90
if color == C_WHITE: return 80
if color == config.get('C_PINK'): return 70
if color == config.get('C_BODY'): return 60
return 50
def pixel_art_scale_down(img, config):
# Scale from 32x64 (or 64x32) down to 5/8 size (20x40 or 40x20)
w, h = img.size
nw, nh = w * 5 // 8, h * 5 // 8
scaled = Image.new('RGBA', (nw, nh), BG_COLOR)
sp = scaled.load()
ip = img.load()
for dy in range(nh):
for dx in range(nw):
# map back to source range
sx1 = int(dx * 8 / 5)
sy1 = int(dy * 8 / 5)
sx2 = int((dx + 1) * 8 / 5)
sy2 = int((dy + 1) * 8 / 5)
# ensure at least 1 pixel
if sx2 == sx1: sx2 += 1
if sy2 == sy1: sy2 += 1
best_color = BG_COLOR
best_pri = 0
for y in range(sy1, min(sy2, h)):
for x in range(sx1, min(sx2, w)):
c = ip[x, y]
pri = get_color_priority(c, config)
if pri > best_pri:
best_pri = pri
best_color = c
sp[dx, dy] = best_color
return scaled
def generate_animation_v8(sex, direction, file_path):
config = RAT_CONFIGS[sex]
c_pupil = config['C_PUPIL']
c_body = config['C_BODY']
orig_img = Image.open(file_path.replace('.png', '_orig.png')).convert('RGBA')
# Custom scale down BEFORE animation
img = pixel_art_scale_down(orig_img, config)
w, h = img.size
frames = []
pixels = img.load()
for frame_idx in range(4):
frame = Image.new('RGBA', (w, h), BG_COLOR)
fp = frame.load()
offset_mag = 0
bounce_y = 0
is_blink = False
if frame_idx == 1:
offset_mag = -5 # reduced since width is smaller (was -8 for 32px, 5/8 * 8 = 5)
bounce_y = -1
elif frame_idx == 3:
offset_mag = 5
bounce_y = -1
is_blink = True
for y in range(h):
for x in range(w):
src_x = x
src_y = y
is_tail = False
# Adjusted thresholds for 20x40 (was 40, now 25. Was 24, now 15)
if direction == 'UP' and y > 25: is_tail = True
if direction == 'DOWN' and y < 15: is_tail = True
if direction == 'LEFT' and x > 25: is_tail = True
if direction == 'RIGHT' and x < 15: is_tail = True
shift_x = 0
shift_y = 0
if is_tail:
if direction == 'UP':
factor = (y - 25) / 12.0
shift_x = int(round(offset_mag * (factor**1.8)))
elif direction == 'DOWN':
factor = (15 - y) / 12.0
shift_x = int(round(offset_mag * (factor**1.8)))
elif direction == 'LEFT':
factor = (x - 25) / 12.0
shift_y = int(round(offset_mag * (factor**1.8)))
elif direction == 'RIGHT':
factor = (15 - x) / 12.0
shift_y = int(round(offset_mag * (factor**1.8)))
src_x = x - shift_x
src_y = y - shift_y - bounce_y
if 0 <= src_x < w and 0 <= src_y < h:
px = pixels[src_x, src_y]
if px != BG_COLOR and px[3] > 0:
if is_blink and px in [C_WHITE, c_pupil]:
fp[x, y] = c_body
else:
fp[x, y] = px
frames.append(frame)
sheet = Image.new('RGBA', (w * 4, h))
for i, f in enumerate(frames):
sheet.paste(f, (i * w, 0))
sheet.save(file_path)
print(f"Generated {file_path}")
for sex in ['MALE', 'FEMALE', 'BABY']:
for direction in ['UP', 'DOWN', 'LEFT', 'RIGHT']:
file_path = f'/home/enne2/dev/mice/assets/Rat/BMP_{sex}_{direction}.png'
if os.path.exists(file_path.replace('.png', '_orig.png')):
generate_animation_v8(sex, direction, file_path)
+10
View File
@@ -0,0 +1,10 @@
import glob
from PIL import Image
gas = Image.open('/home/enne2/dev/mice/assets/Rat/BMP_1_GAS_DOWN.png').convert('RGB')
colors = set(gas.getdata())
print("Colors in GAS_DOWN:", colors)
flower = Image.open('/home/enne2/dev/mice/assets/Rat/BMP_1_FLOWER_1.png').convert('RGB')
colors = set(flower.getdata())
print("Colors in FLOWER_1:", colors)
+11
View File
@@ -0,0 +1,11 @@
from PIL import Image
import subprocess
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
# Extract the 3x4 block from tx=3 to 5, ty=0 to 3
# Also extract ty=4 just in case
box = (3 * 32, 0, 6 * 32, 5 * 32)
block = src_img.crop(box)
block.save('/home/enne2/dev/mice/tools/lpc_extract/lpc_grass_dirt.png')
print("Created lpc_grass_dirt.png")
+18
View File
@@ -0,0 +1,18 @@
import glob
import subprocess
import os
files = sorted(glob.glob('/home/enne2/dev/mice/assets/Rat/*BMP_1*.png'))
# Use ImageMagick montage to create a preview with labels
cmd = [
'montage',
'-label', '%t', # Use the filename (without extension) as label
*files,
'-geometry', '+2+2',
'-background', 'transparent',
'-tile', '6x',
'/home/enne2/dev/mice/tools/lpc_extract/originals_with_labels.png'
]
subprocess.run(cmd)
print("Done creating originals_with_labels.png")
+45
View File
@@ -0,0 +1,45 @@
import glob
import os
from PIL import Image
# Original colors mapped to LPC approximate equivalents
color_map = {
(0, 128, 0): (92, 154, 42), # Dark Green
(0, 255, 0): (124, 184, 47), # Bright Green
(128, 128, 128): (173, 132, 79), # Grey -> Brown dirt
(192, 192, 192): (248, 188, 118), # Light Grey -> Light dirt/sand
(0, 0, 0): (107, 60, 46), # Black -> Dark brown (for softer LPC look)
(255, 255, 0): (224, 156, 76), # Yellow -> LPC Yellow/Orange
(255, 255, 255): (244, 215, 160), # White -> LPC Light bone/sand
(0, 255, 255): (164, 221, 219), # Cyan
(255, 0, 255): (210, 100, 150), # Magenta -> Pink
(255, 0, 0): (180, 50, 50), # Red
}
def recolor_pixel(px):
# px is (r, g, b) or (r, g, b, a)
rgb = px[:3]
if rgb in color_map:
new_rgb = color_map[rgb]
if len(px) == 4:
return new_rgb + (px[3],)
return new_rgb
# If not exact match, find closest by euclidean distance
closest = min(color_map.keys(), key=lambda c: sum((a-b)**2 for a, b in zip(rgb, c)))
new_rgb = color_map[closest]
if len(px) == 4:
return new_rgb + (px[3],)
return new_rgb
input_files = glob.glob('/home/enne2/dev/mice/assets/Rat/*BMP_1*.png')
out_dir = '/home/enne2/dev/mice/assets/Rat_LPC_Style/'
for f in input_files:
img = Image.open(f).convert('RGBA')
new_data = [recolor_pixel(p) for p in img.getdata()]
img.putdata(new_data)
basename = os.path.basename(f)
img.save(os.path.join(out_dir, basename))
print(f"Recolored {basename}")
+215
View File
@@ -0,0 +1,215 @@
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!")
+14
View File
@@ -0,0 +1,14 @@
from PIL import Image
img = Image.open('/home/enne2/dev/mice/tools/lpc_extract/bushes.png').convert('RGBA')
pixels = img.load()
print("Top edge transition:")
for y in range(0, 48):
if pixels[48, y][3] > 0:
print(f"y={y}: {pixels[48, y]}")
print("\nBottom edge transition:")
for y in range(95, 48, -1):
if pixels[48, y][3] > 0:
print(f"y={y}: {pixels[48, y]}")
+22
View File
@@ -0,0 +1,22 @@
from PIL import Image
src_img = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
# The plain grass tile in LPC terrain is typically at col 4, row 1 (0-indexed)
tx, ty = 4, 1
box = (tx * 32, ty * 32, (tx + 1) * 32, (ty + 1) * 32)
tile32 = src_img.crop(box)
# Strategy 1: Tile it 2x2 to make a 64x64 image
img_tiled = Image.new('RGBA', (64, 64))
img_tiled.paste(tile32, (0, 0))
img_tiled.paste(tile32, (32, 0))
img_tiled.paste(tile32, (0, 32))
img_tiled.paste(tile32, (32, 32))
img_tiled.save('/home/enne2/dev/mice/tools/lpc_extract/BMP_1_GRASS_1_tiled.png')
# Strategy 2: Scale it 2x to make a 64x64 image
img_scaled = tile32.resize((64, 64), Image.NEAREST)
img_scaled.save('/home/enne2/dev/mice/tools/lpc_extract/BMP_1_GRASS_1_scaled.png')
print("Created BMP_1_GRASS_1_tiled.png and BMP_1_GRASS_1_scaled.png")
+4
View File
@@ -0,0 +1,4 @@
import sys
from PIL import Image
lpc = Image.open('/home/enne2/dev/asset-maker/assets/poses/sources/lpc-revised/Terrain/terrain_spring.png').convert('RGBA')
lpc.crop((0, 0, 96, 96)).save('/home/enne2/dev/mice/tools/lpc_extract/bushes.png')