Merge origin/new-newnassets: new LPC rat assets and animations
Cherry-picked from new-newnassets (3 commits): -bdf5a8dAdd new start animation asset for game launch -5f2d09fFix rat animations and restore map tiles -2e2c5bbfeat: 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.
|
After Width: | Height: | Size: 360 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 320 B |
|
After Width: | Height: | Size: 358 B |
|
After Width: | Height: | Size: 291 B |
|
After Width: | Height: | Size: 277 B |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 414 B |
@@ -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)
|
||||
@@ -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}')
|
||||
@@ -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!")
|
||||
@@ -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]}")
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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!
|
||||
@@ -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)}")
|
||||
@@ -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}")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
@@ -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!")
|
||||
@@ -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]}")
|
||||
@@ -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")
|
||||
@@ -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')
|
||||