initial commit!
This commit is contained in:
568
graphics/prepare_shotgun_sprites.py
Normal file
568
graphics/prepare_shotgun_sprites.py
Normal file
@@ -0,0 +1,568 @@
|
||||
"""
|
||||
prepare_shotgun_sprites.py
|
||||
|
||||
Convert shotgun first-person sprite sheet to VB tile format using grit.exe.
|
||||
Source: shotgun_6_sprites/shottygun01_2.png (3x2 grid)
|
||||
|
||||
Generates DUAL-LAYER sprites (red + black), matching the pistol's approach:
|
||||
- Red layer: the weapon body (normal palette GPLT0)
|
||||
- Black layer: 1-pixel outline around the weapon (GPLT2 palette for dark outlines)
|
||||
|
||||
Tiles are deduplicated across all 12 images (6 red + 6 black, including H-flip).
|
||||
Uses grit.exe for tile encoding to match the proven zombie sprite pipeline.
|
||||
|
||||
Frame layout:
|
||||
0: idle (top-left) -- holding shotgun
|
||||
1: fire1 (top-middle) -- small muzzle flash
|
||||
2: fire2 (top-right) -- large muzzle flash
|
||||
3: pump1 (bottom-left) -- pump start
|
||||
4: pump2 (bottom-middle)-- pump mid
|
||||
5: pump3 (bottom-right) -- pump end
|
||||
|
||||
Output: shotgun_sprites.c, shotgun_sprites.h, shotgun_previews/
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import shutil
|
||||
import subprocess
|
||||
from PIL import Image
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SHEET_PATH = os.path.join(SCRIPT_DIR, "shotgun_6_sprites", "shottygun01_2.png")
|
||||
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images")
|
||||
PREVIEW_DIR = os.path.join(SCRIPT_DIR, "shotgun_previews")
|
||||
GRIT_EXE = os.path.join(SCRIPT_DIR, "grit_conversions", "grit.exe")
|
||||
GRIT_DIR = os.path.join(SCRIPT_DIR, "grit_conversions")
|
||||
|
||||
TILE_SIZE = 8
|
||||
MAX_WIDTH_TILES = 8 # max 64px wide
|
||||
MAX_HEIGHT_TILES = 8 # max 64px tall
|
||||
SHOTGUN_CHAR_START = 120 # Uses the free space after faces (120-543)
|
||||
|
||||
MAGENTA_THRESHOLD = 30
|
||||
BAYER_2x2 = [[0.0, 0.5], [0.75, 0.25]]
|
||||
DITHER_STRENGTH = 22
|
||||
|
||||
VB_PAL = {0: (0, 0, 0, 0), 1: (80, 0, 0, 255), 2: (170, 0, 0, 255),
|
||||
3: (255, 0, 0, 255)}
|
||||
|
||||
FRAME_NAMES = ["idle", "fire1", "fire2", "pump1", "pump2", "pump3"]
|
||||
|
||||
GRIT_FLAGS = ['-fh!', '-ftc', '-gB2', '-p!', '-m!']
|
||||
|
||||
|
||||
def luminance(r, g, b):
|
||||
return int(0.299 * r + 0.587 * g + 0.114 * b)
|
||||
|
||||
|
||||
def is_bg(r, g, b, a):
|
||||
if a < 128:
|
||||
return True
|
||||
if r > 220 and g < MAGENTA_THRESHOLD and b > 220:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_sprite_bboxes(img):
|
||||
"""Find 6 sub-sprite bounding boxes in a 3x2 grid layout."""
|
||||
w, h = img.size
|
||||
pix = img.load()
|
||||
|
||||
mask = [[False] * w for _ in range(h)]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pix[x, y]
|
||||
if not is_bg(r, g, b, a):
|
||||
mask[y][x] = True
|
||||
|
||||
row_has_content = [any(mask[y]) for y in range(h)]
|
||||
bands = []
|
||||
in_band = False
|
||||
band_start = 0
|
||||
for y in range(h):
|
||||
if row_has_content[y] and not in_band:
|
||||
band_start = y
|
||||
in_band = True
|
||||
elif not row_has_content[y] and in_band:
|
||||
bands.append((band_start, y - 1))
|
||||
in_band = False
|
||||
if in_band:
|
||||
bands.append((band_start, h - 1))
|
||||
|
||||
if len(bands) < 2:
|
||||
raise ValueError(f"Expected 2 rows of sprites, found {len(bands)} bands")
|
||||
|
||||
bboxes = []
|
||||
for band_top, band_bot in bands[:2]:
|
||||
col_has_content = [False] * w
|
||||
for x in range(w):
|
||||
for y in range(band_top, band_bot + 1):
|
||||
if mask[y][x]:
|
||||
col_has_content[x] = True
|
||||
break
|
||||
|
||||
cols = []
|
||||
in_col = False
|
||||
col_start = 0
|
||||
for x in range(w):
|
||||
if col_has_content[x] and not in_col:
|
||||
col_start = x
|
||||
in_col = True
|
||||
elif not col_has_content[x] and in_col:
|
||||
cols.append((col_start, x - 1))
|
||||
in_col = False
|
||||
if in_col:
|
||||
cols.append((col_start, w - 1))
|
||||
|
||||
if len(cols) < 3:
|
||||
raise ValueError(f"Expected 3 cols in row, found {len(cols)}")
|
||||
|
||||
for col_left, col_right in cols[:3]:
|
||||
min_y = band_bot + 1
|
||||
max_y = band_top - 1
|
||||
for y in range(band_top, band_bot + 1):
|
||||
for x in range(col_left, col_right + 1):
|
||||
if mask[y][x]:
|
||||
min_y = min(min_y, y)
|
||||
max_y = max(max_y, y)
|
||||
break
|
||||
bboxes.append((col_left, min_y, col_right, max_y))
|
||||
|
||||
return bboxes
|
||||
|
||||
|
||||
def convert_frame(img, bbox, max_w, max_h):
|
||||
"""Crop, scale, dither, quantize a single frame. Returns (canvas, cols, rows)."""
|
||||
left, top, right, bottom = bbox
|
||||
crop = img.crop((left, top, right + 1, bottom + 1))
|
||||
cw, ch = crop.size
|
||||
|
||||
scale = min(max_w / cw, max_h / ch)
|
||||
nw = max(1, int(cw * scale))
|
||||
nh = max(1, int(ch * scale))
|
||||
resized = crop.resize((nw, nh), Image.LANCZOS)
|
||||
|
||||
cols = (nw + TILE_SIZE - 1) // TILE_SIZE
|
||||
rows = (nh + TILE_SIZE - 1) // TILE_SIZE
|
||||
canvas_w = cols * TILE_SIZE
|
||||
canvas_h = rows * TILE_SIZE
|
||||
|
||||
gray = [[0.0] * canvas_w for _ in range(canvas_h)]
|
||||
alpha = [[0] * canvas_w for _ in range(canvas_h)]
|
||||
off_x = (canvas_w - nw) // 2
|
||||
off_y = canvas_h - nh # bottom-align
|
||||
|
||||
rpix = resized.load()
|
||||
for y in range(nh):
|
||||
for x in range(nw):
|
||||
r, g, b, a = rpix[x, y]
|
||||
dx, dy = off_x + x, off_y + y
|
||||
if dx < canvas_w and dy < canvas_h:
|
||||
if not is_bg(r, g, b, a):
|
||||
gray[dy][dx] = float(luminance(r, g, b))
|
||||
alpha[dy][dx] = 1
|
||||
|
||||
# Contrast stretch
|
||||
values = [gray[y][x] for y in range(canvas_h)
|
||||
for x in range(canvas_w) if alpha[y][x]]
|
||||
if len(values) > 10:
|
||||
values.sort()
|
||||
lo = values[len(values) * 3 // 100]
|
||||
hi = values[len(values) * 97 // 100]
|
||||
if hi - lo < 20:
|
||||
hi = lo + 20
|
||||
for y in range(canvas_h):
|
||||
for x in range(canvas_w):
|
||||
if alpha[y][x]:
|
||||
v = (gray[y][x] - lo) / (hi - lo) * 255.0
|
||||
gray[y][x] = max(0.0, min(255.0, v))
|
||||
|
||||
# Quantize with Bayer dithering
|
||||
canvas = [[0] * canvas_w for _ in range(canvas_h)]
|
||||
for y in range(canvas_h):
|
||||
for x in range(canvas_w):
|
||||
if not alpha[y][x]:
|
||||
continue
|
||||
g = gray[y][x]
|
||||
g += BAYER_2x2[y % 2][x % 2] * DITHER_STRENGTH - DITHER_STRENGTH * 0.375
|
||||
if g < 72:
|
||||
canvas[y][x] = 1
|
||||
elif g < 158:
|
||||
canvas[y][x] = 2
|
||||
else:
|
||||
canvas[y][x] = 3
|
||||
|
||||
return canvas, cols, rows
|
||||
|
||||
|
||||
def generate_black_canvas(canvas):
|
||||
"""Generate a black-layer canvas from the red canvas.
|
||||
|
||||
The black canvas contains the FULL weapon body (all pixel indices
|
||||
preserved) PLUS a 2-pixel dilated dark border around the weapon
|
||||
silhouette. This matches the pistol's approach:
|
||||
|
||||
When rendered with GPLT2 (0x84):
|
||||
idx 0 -> transparent
|
||||
idx 1 -> dark (visible as black)
|
||||
idx 2 -> transparent (medium pixels disappear!)
|
||||
idx 3 -> medium (bright pixels become dimmer)
|
||||
|
||||
Interior tiles will be identical to red tiles and deduplicate to
|
||||
zero additional cost. Only edge tiles with the dilated border add
|
||||
to the tile count.
|
||||
"""
|
||||
h = len(canvas)
|
||||
w = len(canvas[0]) if h > 0 else 0
|
||||
black = [[0] * w for _ in range(h)]
|
||||
|
||||
# Copy full weapon body
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
black[y][x] = canvas[y][x]
|
||||
|
||||
# Add 1-pixel dilated dark border (8-directional)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if canvas[y][x] != 0:
|
||||
continue # only fill transparent pixels
|
||||
# Check 8 neighbors for any non-transparent pixel
|
||||
has_neighbor = False
|
||||
for dy in (-1, 0, 1):
|
||||
for dx in (-1, 0, 1):
|
||||
if dy == 0 and dx == 0:
|
||||
continue
|
||||
ny, nx = y + dy, x + dx
|
||||
if 0 <= ny < h and 0 <= nx < w and canvas[ny][nx] != 0:
|
||||
has_neighbor = True
|
||||
break
|
||||
if has_neighbor:
|
||||
break
|
||||
if has_neighbor:
|
||||
black[y][x] = 1 # dark outline pixel
|
||||
|
||||
return black
|
||||
|
||||
|
||||
def save_indexed_png(canvas, path):
|
||||
"""Save a canvas as indexed 4-color PNG for grit."""
|
||||
h = len(canvas)
|
||||
w = len(canvas[0]) if h > 0 else 0
|
||||
img = Image.new('P', (w, h))
|
||||
palette = [0, 0, 0, 85, 85, 85, 170, 170, 170, 255, 255, 255]
|
||||
palette += [0] * (768 - len(palette))
|
||||
img.putpalette(palette)
|
||||
pixels = img.load()
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
pixels[x, y] = canvas[y][x]
|
||||
img.save(path)
|
||||
|
||||
|
||||
def run_grit(png_path):
|
||||
"""Run grit on a PNG, return .c filepath (in GRIT_DIR)."""
|
||||
basename = os.path.splitext(os.path.basename(png_path))[0]
|
||||
cmd = [GRIT_EXE, png_path] + GRIT_FLAGS
|
||||
result = subprocess.run(cmd, cwd=GRIT_DIR, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" ERROR: grit failed for {basename}: {result.stderr}")
|
||||
return None
|
||||
c_file = os.path.join(GRIT_DIR, f"{basename}.c")
|
||||
if not os.path.exists(c_file):
|
||||
print(f" ERROR: grit output not found: {c_file}")
|
||||
return None
|
||||
# Clean up any .h
|
||||
h_file = os.path.join(GRIT_DIR, f"{basename}.h")
|
||||
if os.path.exists(h_file):
|
||||
os.remove(h_file)
|
||||
return c_file
|
||||
|
||||
|
||||
def parse_grit_tiles(c_path):
|
||||
"""Parse grit .c output, return list of tiles (each tile = 4 u32 words)."""
|
||||
with open(c_path, 'r') as f:
|
||||
content = f.read()
|
||||
m = re.search(r'const unsigned int \w+Tiles\[\d+\].*?=\s*\{(.*?)\};',
|
||||
content, re.DOTALL)
|
||||
if not m:
|
||||
return []
|
||||
words = [int(x, 16) for x in re.findall(r'0x[0-9A-Fa-f]+', m.group(1))]
|
||||
tiles = []
|
||||
for i in range(0, len(words), 4):
|
||||
tiles.append(tuple(words[i:i+4]))
|
||||
return tiles
|
||||
|
||||
|
||||
def hflip_tile_words(words):
|
||||
"""H-flip a VB 2bpp tile (sequential format) given as 4 u32 words.
|
||||
|
||||
VB 2bpp tiles use sequential packing: each row is a u16 (LE) where
|
||||
bits[15:14]=px0, bits[13:12]=px1, ..., bits[1:0]=px7.
|
||||
H-flip reverses pixel order within each row.
|
||||
Returns tuple of 4 words.
|
||||
"""
|
||||
data = []
|
||||
for w in words:
|
||||
data.append(w & 0xFF)
|
||||
data.append((w >> 8) & 0xFF)
|
||||
data.append((w >> 16) & 0xFF)
|
||||
data.append((w >> 24) & 0xFF)
|
||||
|
||||
flipped = []
|
||||
for row in range(8):
|
||||
byte0 = data[row * 2] # low byte of u16 (pixels 4-7)
|
||||
byte1 = data[row * 2 + 1] # high byte of u16 (pixels 0-3)
|
||||
u16_val = (byte1 << 8) | byte0
|
||||
|
||||
# Extract 8 pixels (px0 at LSB), reverse them, repack
|
||||
pixels = [(u16_val >> (x * 2)) & 3 for x in range(8)]
|
||||
pixels.reverse()
|
||||
new_u16 = 0
|
||||
for x in range(8):
|
||||
new_u16 |= (pixels[x] << (x * 2))
|
||||
|
||||
flipped.append(new_u16 & 0xFF)
|
||||
flipped.append((new_u16 >> 8) & 0xFF)
|
||||
|
||||
result = []
|
||||
for i in range(0, 16, 4):
|
||||
w = (flipped[i] | (flipped[i+1] << 8) |
|
||||
(flipped[i+2] << 16) | (flipped[i+3] << 24))
|
||||
result.append(w)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def save_preview(canvas, path, scale=3):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
h = len(canvas)
|
||||
w = len(canvas[0]) if h > 0 else 0
|
||||
img = Image.new('RGBA', (w * scale, h * scale), (0, 0, 0, 255))
|
||||
pix = img.load()
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c = VB_PAL[canvas[y][x]]
|
||||
for sy in range(scale):
|
||||
for sx in range(scale):
|
||||
pix[x * scale + sx, y * scale + sy] = c
|
||||
img.save(path)
|
||||
|
||||
|
||||
def deduplicate_tiles(tile_list, unique_tiles, unique_tile_data):
|
||||
"""Deduplicate a list of tile word tuples against the shared pool.
|
||||
|
||||
Returns a list of (char_idx | flags) map entries.
|
||||
Modifies unique_tiles and unique_tile_data in-place.
|
||||
"""
|
||||
frame_map = []
|
||||
for tile_words in tile_list:
|
||||
flags = 0
|
||||
|
||||
if tile_words in unique_tiles:
|
||||
idx = unique_tiles[tile_words]
|
||||
else:
|
||||
hflipped = hflip_tile_words(tile_words)
|
||||
if hflipped in unique_tiles:
|
||||
idx = unique_tiles[hflipped]
|
||||
flags = 0x2000 # H-flip
|
||||
else:
|
||||
idx = len(unique_tile_data)
|
||||
unique_tiles[tile_words] = idx
|
||||
unique_tile_data.append(tile_words)
|
||||
|
||||
char_idx = SHOTGUN_CHAR_START + idx
|
||||
frame_map.append(char_idx | flags)
|
||||
|
||||
return frame_map
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
os.makedirs(PREVIEW_DIR, exist_ok=True)
|
||||
|
||||
if not os.path.exists(GRIT_EXE):
|
||||
print(f"ERROR: grit.exe not found at {GRIT_EXE}")
|
||||
return
|
||||
|
||||
img = Image.open(SHEET_PATH).convert('RGBA')
|
||||
w, h = img.size
|
||||
print(f"Loaded {SHEET_PATH}: {w}x{h}")
|
||||
|
||||
bboxes = find_sprite_bboxes(img)
|
||||
print(f"Found {len(bboxes)} sub-sprites")
|
||||
|
||||
max_w = MAX_WIDTH_TILES * TILE_SIZE
|
||||
max_h = MAX_HEIGHT_TILES * TILE_SIZE
|
||||
|
||||
# Temp dir for grit input PNGs
|
||||
grit_input_dir = os.path.join(PREVIEW_DIR, "grit_input")
|
||||
os.makedirs(grit_input_dir, exist_ok=True)
|
||||
|
||||
# Phase 1: process frames, generate red + black canvases, run grit
|
||||
red_grit_tiles = [] # list of lists of tile tuples (red layer)
|
||||
blk_grit_tiles = [] # list of lists of tile tuples (black layer)
|
||||
frame_dims = [] # list of (cols, rows) -- same for red and black
|
||||
|
||||
for fi in range(6):
|
||||
bbox = bboxes[fi]
|
||||
bw = bbox[2] - bbox[0] + 1
|
||||
bh = bbox[3] - bbox[1] + 1
|
||||
canvas, cols, rows = convert_frame(img, bbox, max_w, max_h)
|
||||
|
||||
# Generate black layer: full weapon body + 2px dilated border
|
||||
black = generate_black_canvas(canvas)
|
||||
|
||||
# Save previews
|
||||
save_preview(canvas, os.path.join(PREVIEW_DIR,
|
||||
f"shotgun_{FRAME_NAMES[fi]}_red.png"))
|
||||
save_preview(black, os.path.join(PREVIEW_DIR,
|
||||
f"shotgun_{FRAME_NAMES[fi]}_blk.png"))
|
||||
print(f" Frame {fi} ({FRAME_NAMES[fi]}): {bw}x{bh} -> "
|
||||
f"{cols}x{rows} tiles ({cols*8}x{rows*8} px)")
|
||||
|
||||
# Save indexed PNGs for grit (red + black)
|
||||
red_png = os.path.join(grit_input_dir, f"shotgun_f{fi}_red.png")
|
||||
blk_png = os.path.join(grit_input_dir, f"shotgun_f{fi}_blk.png")
|
||||
save_indexed_png(canvas, red_png)
|
||||
save_indexed_png(black, blk_png)
|
||||
|
||||
# Run grit on red layer
|
||||
red_c = run_grit(red_png)
|
||||
if red_c is None:
|
||||
print(f" FATAL: grit failed for red frame {fi}")
|
||||
return
|
||||
red_tiles = parse_grit_tiles(red_c)
|
||||
os.remove(red_c)
|
||||
|
||||
# Run grit on black layer
|
||||
blk_c = run_grit(blk_png)
|
||||
if blk_c is None:
|
||||
print(f" FATAL: grit failed for black frame {fi}")
|
||||
return
|
||||
blk_tiles = parse_grit_tiles(blk_c)
|
||||
os.remove(blk_c)
|
||||
|
||||
expected = cols * rows
|
||||
if len(red_tiles) != expected:
|
||||
print(f" WARNING: expected {expected} red tiles, got {len(red_tiles)}")
|
||||
if len(blk_tiles) != expected:
|
||||
print(f" WARNING: expected {expected} blk tiles, got {len(blk_tiles)}")
|
||||
|
||||
red_grit_tiles.append(red_tiles)
|
||||
blk_grit_tiles.append(blk_tiles)
|
||||
frame_dims.append((cols, rows))
|
||||
|
||||
# Phase 2: deduplicate ALL tiles (red + black) into one shared pool
|
||||
unique_tiles = {} # tile_words_tuple -> index
|
||||
unique_tile_data = [] # list of tile word tuples
|
||||
red_frame_maps = [] # list of (cols, rows, map_entries)
|
||||
blk_frame_maps = [] # list of (cols, rows, map_entries)
|
||||
|
||||
for fi in range(6):
|
||||
cols, rows = frame_dims[fi]
|
||||
|
||||
# Red layer
|
||||
red_map = deduplicate_tiles(red_grit_tiles[fi],
|
||||
unique_tiles, unique_tile_data)
|
||||
red_frame_maps.append((cols, rows, red_map))
|
||||
|
||||
# Black layer (same tile grid dimensions)
|
||||
blk_map = deduplicate_tiles(blk_grit_tiles[fi],
|
||||
unique_tiles, unique_tile_data)
|
||||
blk_frame_maps.append((cols, rows, blk_map))
|
||||
|
||||
num_tiles = len(unique_tile_data)
|
||||
print(f"\nTotal unique shotgun tiles (red+black): {num_tiles}")
|
||||
print(f"Tile data: {num_tiles * 16} bytes ({num_tiles * 4} u32 words)")
|
||||
|
||||
if num_tiles > 424:
|
||||
print(f"WARNING: {num_tiles} tiles exceeds shotgun slot limit of 424!")
|
||||
|
||||
# --- Write C file ---
|
||||
c_path = os.path.join(OUTPUT_DIR, "shotgun_sprites.c")
|
||||
with open(c_path, 'w') as f:
|
||||
f.write("/* Shotgun weapon sprite tiles (red+black layers) -- "
|
||||
"auto-generated by prepare_shotgun_sprites.py + grit */\n\n")
|
||||
|
||||
all_u32 = []
|
||||
for td in unique_tile_data:
|
||||
all_u32.extend(td)
|
||||
f.write(f"/* {num_tiles} unique shotgun tiles (red+black shared) */\n")
|
||||
f.write(f"const unsigned int shotgunTiles[{len(all_u32)}]"
|
||||
f" __attribute__((aligned(4))) =\n{{\n")
|
||||
for i in range(0, len(all_u32), 8):
|
||||
chunk = all_u32[i:i+8]
|
||||
s = ",".join(f"0x{w:08X}" for w in chunk)
|
||||
comma = "," if i + 8 < len(all_u32) else ""
|
||||
f.write(f"\t{s}{comma}\n")
|
||||
f.write("};\n\n")
|
||||
|
||||
# Red frame maps
|
||||
for fi in range(6):
|
||||
cols, rows, fmap = red_frame_maps[fi]
|
||||
entries = cols * rows
|
||||
f.write(f"/* Red frame {fi} ({FRAME_NAMES[fi]}): "
|
||||
f"{cols} cols x {rows} rows */\n")
|
||||
f.write(f"const unsigned short shotgunFrame{fi}Map[{entries}]"
|
||||
f" __attribute__((aligned(4))) =\n{{\n")
|
||||
for r in range(rows):
|
||||
chunk = fmap[r * cols:(r + 1) * cols]
|
||||
s = ",".join(f"0x{v:04X}" for v in chunk)
|
||||
comma = "," if r < rows - 1 else ""
|
||||
f.write(f"\t{s}{comma}\n")
|
||||
f.write("};\n\n")
|
||||
|
||||
# Black frame maps
|
||||
for fi in range(6):
|
||||
cols, rows, fmap = blk_frame_maps[fi]
|
||||
entries = cols * rows
|
||||
f.write(f"/* Black frame {fi} ({FRAME_NAMES[fi]}): "
|
||||
f"{cols} cols x {rows} rows */\n")
|
||||
f.write(f"const unsigned short shotgunBlkFrame{fi}Map[{entries}]"
|
||||
f" __attribute__((aligned(4))) =\n{{\n")
|
||||
for r in range(rows):
|
||||
chunk = fmap[r * cols:(r + 1) * cols]
|
||||
s = ",".join(f"0x{v:04X}" for v in chunk)
|
||||
comma = "," if r < rows - 1 else ""
|
||||
f.write(f"\t{s}{comma}\n")
|
||||
f.write("};\n\n")
|
||||
|
||||
print(f"\nWrote {c_path}")
|
||||
|
||||
# --- Write H file ---
|
||||
h_path = os.path.join(OUTPUT_DIR, "shotgun_sprites.h")
|
||||
with open(h_path, 'w') as f:
|
||||
f.write("#ifndef __SHOTGUN_SPRITES_H__\n"
|
||||
"#define __SHOTGUN_SPRITES_H__\n\n")
|
||||
f.write(f"#define SHOTGUN_CHAR_START {SHOTGUN_CHAR_START}\n")
|
||||
f.write(f"#define SHOTGUN_TILE_COUNT {num_tiles}\n")
|
||||
f.write(f"#define SHOTGUN_TILE_BYTES {num_tiles * 16}\n\n")
|
||||
f.write(f"extern const unsigned int shotgunTiles[{len(all_u32)}];\n\n")
|
||||
|
||||
f.write("/* Red layer frames */\n")
|
||||
for fi in range(6):
|
||||
cols, rows, fmap = red_frame_maps[fi]
|
||||
entries = cols * rows
|
||||
f.write(f"#define SHOTGUN_F{fi}_XTILES {cols * 2} "
|
||||
f"/* {cols} cols x 2 bytes */\n")
|
||||
f.write(f"#define SHOTGUN_F{fi}_ROWS {rows}\n")
|
||||
f.write(f"extern const unsigned short "
|
||||
f"shotgunFrame{fi}Map[{entries}];\n\n")
|
||||
|
||||
f.write("/* Black layer frames */\n")
|
||||
for fi in range(6):
|
||||
cols, rows, fmap = blk_frame_maps[fi]
|
||||
entries = cols * rows
|
||||
f.write(f"#define SHOTGUN_BLK_F{fi}_XTILES {cols * 2} "
|
||||
f"/* {cols} cols x 2 bytes */\n")
|
||||
f.write(f"#define SHOTGUN_BLK_F{fi}_ROWS {rows}\n")
|
||||
f.write(f"extern const unsigned short "
|
||||
f"shotgunBlkFrame{fi}Map[{entries}];\n\n")
|
||||
|
||||
f.write("#endif\n")
|
||||
print(f"Wrote {h_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user