Revert "Redraw wall/bush maze tiles with richer pixel-art detail"
The user asked to go back to the pre-commit assets. The original
BMP_{1..4}_*.png tiles have been restored. The generator script
introduced in that commit is also removed.
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 993 B After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 991 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 189 B |
|
Before Width: | Height: | Size: 947 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 1013 B After Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 950 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 172 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 162 B |
|
Before Width: | Height: | Size: 1019 B After Width: | Height: | Size: 161 B |
|
Before Width: | Height: | Size: 941 B After Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 214 B |
|
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 970 B After Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 967 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 208 B |
|
Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 208 B |
@@ -1,401 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate the 32x32 wall/bush tile sprites for the maze with more detail.
|
||||
|
||||
Replaces the noisy checkerboard BMP_{theme}_{N,S,E,W,NE,NW,SE,SW,EN,ES,WN,WS}.png
|
||||
tiles with hand-designed pixel art bush clusters, keeping the original
|
||||
palette per theme (or a richer one for theme 1).
|
||||
|
||||
Each tile is 32x32. The wall/bush area is on a specific side(s) of the tile:
|
||||
N = wall on the top side, passage below
|
||||
S = wall on the bottom side, passage above
|
||||
E = wall on the right side, passage left
|
||||
W = wall on the left side, passage right
|
||||
NE = wall on top + right (outer corner)
|
||||
NW = wall on top + left
|
||||
SE = wall on bottom + right
|
||||
SW = wall on bottom + left
|
||||
EN = inner corner: passage top-left, wall bottom-right (L-shape)
|
||||
ES = inner corner: passage top-right, wall bottom-left
|
||||
WN = inner corner: passage bottom-left, wall top-right
|
||||
WS = inner corner: passage bottom-right, wall top-left
|
||||
|
||||
Run from repo root:
|
||||
python3 tools/redraw_walls.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Output directory
|
||||
OUT_DIR = Path(__file__).resolve().parents[1] / "assets" / "Rat"
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SIZE = 32
|
||||
TRANSPARENT = (128, 128, 128, 255) # engine treats this color as transparent
|
||||
|
||||
# Palette per theme. Each theme gets a 5-color bush palette:
|
||||
# (deep_shadow, dark, mid, light, highlight)
|
||||
THEME_PALETTES = {
|
||||
# Theme 1: lush green bushes (the canonical "hedge maze" look).
|
||||
1: {
|
||||
"deep": (0, 70, 0, 255),
|
||||
"dark": (0, 110, 0, 255),
|
||||
"mid": (0, 160, 0, 255),
|
||||
"light": (60, 220, 60, 255),
|
||||
"highlight": (170, 255, 170, 255),
|
||||
},
|
||||
# Theme 2: yellow/golden dry brush.
|
||||
2: {
|
||||
"deep": (90, 90, 0, 255),
|
||||
"dark": (130, 130, 0, 255),
|
||||
"mid": (200, 200, 0, 255),
|
||||
"light": (240, 240, 80, 255),
|
||||
"highlight": (255, 255, 200, 255),
|
||||
},
|
||||
# Theme 3: white/silver snowy brush.
|
||||
3: {
|
||||
"deep": (140, 140, 140, 255),
|
||||
"dark": (180, 180, 180, 255),
|
||||
"mid": (220, 220, 220, 255),
|
||||
"light": (245, 245, 245, 255),
|
||||
"highlight": (255, 255, 255, 255),
|
||||
},
|
||||
# Theme 4: ashen/red lava brush.
|
||||
4: {
|
||||
"deep": (90, 0, 0, 255),
|
||||
"dark": (150, 25, 25, 255),
|
||||
"mid": (210, 50, 50, 255),
|
||||
"light": (240, 110, 90, 255),
|
||||
"highlight": (255, 200, 180, 255),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deterministic pseudo-random helpers.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _hash(x: int, y: int, salt: int = 0) -> int:
|
||||
"""Tiny integer hash in [0, 65536)."""
|
||||
h = (x * 374761393 + y * 668265263 + salt * 2147483647) & 0xFFFFFFFF
|
||||
h = (h ^ (h >> 13)) * 1274126177 & 0xFFFFFFFF
|
||||
return h & 0xFFFF
|
||||
|
||||
|
||||
def _chance(x: int, y: int, threshold: int, salt: int = 0) -> bool:
|
||||
return _hash(x, y, salt) < threshold
|
||||
|
||||
|
||||
def _hash_seed(x: int, y: int, salt: int) -> int:
|
||||
"""Return a deterministic seed in [0, 8) used to pick a cluster variant."""
|
||||
return _hash(x, y, salt) % 8
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cluster patterns: small 3x3 / 4x4 / 5x5 stamps used to add visible "leaves"
|
||||
# inside the bush body. Each cluster is a small relative offset (+dx, +dy) and
|
||||
# a color choice.
|
||||
# ---------------------------------------------------------------------------
|
||||
# A cluster is (dx, dy, color_key). The cluster is placed at integer pixel
|
||||
# positions; each pixel within a small radius is set to that color (overriding
|
||||
# the base body color). This adds visible "leaf clusters" on top of the body.
|
||||
def _leaf_cluster_positions(cx: int, cy: int, salt: int) -> list:
|
||||
"""Return [(dx, dy, color_key), ...] relative to (cx, cy) for one leaf."""
|
||||
# Choose a variant: 0-7
|
||||
variant = _hash_seed(cx, cy, salt)
|
||||
# Variants:
|
||||
# 0: 3 leaves in a triangle
|
||||
# 1: 4 leaves in a small plus
|
||||
# 2: 5 leaves in a + shape
|
||||
# 3: small cluster of 3
|
||||
# 4: scattered 3
|
||||
# 5: 4 leaves diagonal
|
||||
# 6: single + 2 around
|
||||
# 7: tight 5 cluster
|
||||
clusters = {
|
||||
0: [(0, 0, "highlight"), (-2, 1, "light"), (1, 2, "light")],
|
||||
1: [(0, 0, "highlight"), (2, 0, "light"), (-2, 0, "light"), (0, 2, "light")],
|
||||
2: [(0, 0, "highlight"), (2, 0, "light"), (-2, 0, "light"), (0, 2, "light"), (0, -2, "mid")],
|
||||
3: [(0, 0, "light"), (-1, 1, "mid"), (1, -1, "mid")],
|
||||
4: [(0, 0, "highlight"), (2, 2, "light"), (-2, -1, "light")],
|
||||
5: [(0, 0, "highlight"), (2, 2, "light"), (-2, -2, "light"), (1, -1, "mid")],
|
||||
6: [(0, 0, "highlight"), (-1, 0, "light"), (0, -1, "light")],
|
||||
7: [(0, 0, "highlight"), (1, 0, "light"), (-1, 0, "light"), (0, 1, "light"), (0, -1, "light")],
|
||||
}
|
||||
return clusters[variant]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tile layout: where is the wall (muro di cespugli)?
|
||||
# ---------------------------------------------------------------------------
|
||||
def ideal_mask(shape: str) -> list:
|
||||
"""Return a 32x32 list-of-lists mask of the IDEAL wall footprint (True = bush)."""
|
||||
mask = [[False] * SIZE for _ in range(SIZE)]
|
||||
if shape == "N":
|
||||
for y in range(SIZE // 2):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
elif shape == "S":
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
elif shape == "E":
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[y][x] = True
|
||||
elif shape == "W":
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2):
|
||||
mask[y][x] = True
|
||||
elif shape == "NE":
|
||||
for y in range(SIZE // 2):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[y][x] = True
|
||||
elif shape == "NW":
|
||||
for y in range(SIZE // 2):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2):
|
||||
mask[y][x] = True
|
||||
elif shape == "SE":
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[y][x] = True
|
||||
elif shape == "SW":
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
for x in range(SIZE):
|
||||
mask[y][x] = True
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE // 2):
|
||||
mask[y][x] = True
|
||||
elif shape == "EN":
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[y][x] = True
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[SIZE // 2][x] = True
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
mask[y][SIZE // 2] = True
|
||||
elif shape == "ES":
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
for x in range(SIZE // 2):
|
||||
mask[y][x] = True
|
||||
for x in range(SIZE // 2):
|
||||
mask[SIZE // 2][x] = True
|
||||
for y in range(SIZE // 2, SIZE):
|
||||
mask[y][SIZE // 2 - 1] = True
|
||||
elif shape == "WN":
|
||||
for y in range(SIZE // 2):
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[y][x] = True
|
||||
for x in range(SIZE // 2, SIZE):
|
||||
mask[SIZE // 2 - 1][x] = True
|
||||
for y in range(SIZE // 2):
|
||||
mask[y][SIZE // 2] = True
|
||||
elif shape == "WS":
|
||||
for y in range(SIZE // 2):
|
||||
for x in range(SIZE // 2):
|
||||
mask[y][x] = True
|
||||
for x in range(SIZE // 2):
|
||||
mask[SIZE // 2 - 1][x] = True
|
||||
for y in range(SIZE // 2):
|
||||
mask[y][SIZE // 2 - 1] = True
|
||||
else:
|
||||
raise ValueError(f"Unknown shape: {shape}")
|
||||
return mask
|
||||
|
||||
|
||||
def erode_boundary(mask: list, salt: int) -> list:
|
||||
"""Make the wall/passage boundary irregular and natural."""
|
||||
new_mask = [row[:] for row in mask]
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
is_wall = mask[y][x]
|
||||
diffs = 0
|
||||
for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
ny, nx = y + dy, x + dx
|
||||
if not (0 <= ny < SIZE and 0 <= nx < SIZE) or mask[ny][nx] != is_wall:
|
||||
diffs += 1
|
||||
if diffs == 0:
|
||||
continue
|
||||
if is_wall:
|
||||
# carve a notch out of the bush (more aggressive)
|
||||
if _chance(x, y, 7500, salt):
|
||||
new_mask[y][x] = False
|
||||
else:
|
||||
# let a leaf cluster stick out into the passage
|
||||
if _chance(x, y, 5500, salt + 1):
|
||||
near = False
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-2, 3):
|
||||
ny, nx = y + dy, x + dx
|
||||
if 0 <= ny < SIZE and 0 <= nx < SIZE and mask[ny][nx]:
|
||||
near = True
|
||||
break
|
||||
if near:
|
||||
break
|
||||
if near:
|
||||
new_mask[y][x] = True
|
||||
return new_mask
|
||||
|
||||
|
||||
def compute_depth(mask: list) -> list:
|
||||
"""For each wall pixel, return its distance from the wall/passage boundary.
|
||||
|
||||
Used to determine which shade of green to use (deeper = darker).
|
||||
"""
|
||||
depth = [[0] * SIZE for _ in range(SIZE)]
|
||||
# Boundary pixels have depth 1; interior grows with distance from boundary.
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
if not mask[y][x]:
|
||||
continue
|
||||
# find nearest passage pixel via BFS-lite (4-direction)
|
||||
# Use a simple expansion: search outward until we find a False.
|
||||
d = 1
|
||||
found = False
|
||||
while d < SIZE:
|
||||
ring = False
|
||||
for dy in range(-d, d + 1):
|
||||
for dx in range(-d, d + 1):
|
||||
if abs(dy) != d and abs(dx) != d:
|
||||
continue
|
||||
ny, nx = y + dy, x + dx
|
||||
if not (0 <= ny < SIZE and 0 <= nx < SIZE):
|
||||
ring = True
|
||||
break
|
||||
if not mask[ny][nx]:
|
||||
ring = True
|
||||
break
|
||||
if ring:
|
||||
break
|
||||
if ring:
|
||||
found = True
|
||||
break
|
||||
d += 1
|
||||
depth[y][x] = d if found else SIZE
|
||||
return depth
|
||||
|
||||
|
||||
def fill_bush_detail(mask: list, depth: list, pal: dict, salt: int) -> list:
|
||||
"""Assign palette colors to wall pixels, with leaf clusters overlaid."""
|
||||
pixels = [[None] * SIZE for _ in range(SIZE)]
|
||||
# 1) base layer: pick color by depth.
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
if not mask[y][x]:
|
||||
pixels[y][x] = TRANSPARENT
|
||||
continue
|
||||
d = depth[y][x]
|
||||
if d <= 1:
|
||||
pixels[y][x] = pal["dark"]
|
||||
elif d == 2:
|
||||
pixels[y][x] = pal["dark"]
|
||||
elif d == 3:
|
||||
pixels[y][x] = pal["mid"]
|
||||
elif d == 4:
|
||||
pixels[y][x] = pal["mid"]
|
||||
else:
|
||||
pixels[y][x] = pal["deep"]
|
||||
# 2) scattered mid/light/highlight "speckles" to break uniformity.
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
if not mask[y][x]:
|
||||
continue
|
||||
d = depth[y][x]
|
||||
# A light speckle anywhere on the bush
|
||||
if d <= 4 and _chance(x, y, 4500, salt + 11):
|
||||
pixels[y][x] = pal["light"]
|
||||
# Highlight speckle near the edge only
|
||||
if d <= 2 and _chance(x, y, 3000, salt + 23):
|
||||
pixels[y][x] = pal["highlight"]
|
||||
# 3) Overlay leaf clusters at deterministic anchor points.
|
||||
# Anchors are picked from a low-density grid so each tile gets a few clusters.
|
||||
anchor_spacing = 6
|
||||
for ay in range(2, SIZE - 2, anchor_spacing):
|
||||
for ax in range(2, SIZE - 2, anchor_spacing):
|
||||
if not mask[ay][ax]:
|
||||
continue
|
||||
# Some anchors are skipped for variety
|
||||
if not _chance(ax, ay, 22000, salt + 31):
|
||||
continue
|
||||
leaves = _leaf_cluster_positions(ax, ay, salt + 41)
|
||||
for dx, dy, color_key in leaves:
|
||||
nx, ny = ax + dx, ay + dy
|
||||
if 0 <= nx < SIZE and 0 <= ny < SIZE and mask[ny][nx]:
|
||||
pixels[ny][nx] = pal[color_key]
|
||||
# 4) Soft "rim" highlight along the wall/passage boundary on the
|
||||
# passage-facing side (depth=1) — adds a sense of light catching the
|
||||
# outer leaves.
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
if not mask[y][x]:
|
||||
continue
|
||||
if depth[y][x] == 1 and _chance(x, y, 5000, salt + 53):
|
||||
pixels[y][x] = pal["light"]
|
||||
return pixels
|
||||
|
||||
|
||||
def render_tile(shape: str, theme_index: int) -> list:
|
||||
pal = THEME_PALETTES[theme_index]
|
||||
salt = (theme_index * 1000) + sum(ord(c) for c in shape)
|
||||
base = ideal_mask(shape)
|
||||
eroded = erode_boundary(base, salt)
|
||||
depth = compute_depth(eroded)
|
||||
pixels = fill_bush_detail(eroded, depth, pal, salt + 99)
|
||||
return pixels
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PNG writer (PIL mode P matching original BMP_{theme}_{shape}.png format).
|
||||
# ---------------------------------------------------------------------------
|
||||
def save_png_mode_p(pixels: list, path: Path) -> None:
|
||||
from PIL import Image
|
||||
# Build a unique palette
|
||||
used = {}
|
||||
# Reserve index 0 for the transparent gray (engine convention)
|
||||
used[TRANSPARENT] = 0
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
c = pixels[y][x]
|
||||
if c not in used:
|
||||
used[c] = len(used)
|
||||
palette = [0] * (256 * 3)
|
||||
for color, idx in used.items():
|
||||
palette[idx * 3] = color[0]
|
||||
palette[idx * 3 + 1] = color[1]
|
||||
palette[idx * 3 + 2] = color[2]
|
||||
pal_img = Image.new("P", (SIZE, SIZE))
|
||||
pal_img.putpalette(palette)
|
||||
for y in range(SIZE):
|
||||
for x in range(SIZE):
|
||||
pal_img.putpixel((x, y), used[pixels[y][x]])
|
||||
pal_img.save(path, optimize=True)
|
||||
|
||||
|
||||
SHAPES = ["N", "S", "E", "W",
|
||||
"NE", "NW", "SE", "SW",
|
||||
"EN", "ES", "WN", "WS"]
|
||||
|
||||
|
||||
def main():
|
||||
written = []
|
||||
for theme_index in range(1, 5):
|
||||
for shape in SHAPES:
|
||||
pixels = render_tile(shape, theme_index)
|
||||
path = OUT_DIR / f"BMP_{theme_index}_{shape}.png"
|
||||
save_png_mode_p(pixels, path)
|
||||
written.append(str(path.relative_to(OUT_DIR.parents[1])))
|
||||
print(f"Wrote {len(written)} tile sprites.")
|
||||
for w in written:
|
||||
print(f" {w}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||