""" prepare_rocket_projectile.py Convert rocket projectile directional sprite strip to VB tile format. Source: new_weapons/rocket[towards_us,away_from_us,...].png (5 frames, 29x20 each) Produces 5 frame tile arrays. At runtime, H-flip provides the other 3 directions. Direction mapping: 0: front (towards_us) = frame 0 1: front-right = frame 4 (towards_us_but_alittle_right) 2: right = frame 3 (right) 3: back-right = frame 2 (away_from_us_but_alittle_to_right) 4: back (away_from_us) = frame 1 5: back-left = frame 2 H-flipped 6: left = frame 3 H-flipped 7: front-left = frame 4 H-flipped Output: rocket_projectile_sprites.c/.h in src/vbdoom/assets/images/ """ import os import re import subprocess import shutil from PIL import Image SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) STRIP_PATH = os.path.join(SCRIPT_DIR, "new_weapons", "rocket[towards_us,away_from_us,away_from_us_but_alittle_to_right,right,towards_us_but_alittle_right].png") OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images") GRIT_EXE = os.path.join(SCRIPT_DIR, "grit_conversions", "grit.exe") GRIT_DIR = os.path.join(SCRIPT_DIR, "grit_conversions") TILE_SIZE = 8 GRIT_FLAGS = ['-fh!', '-ftc', '-gB2', '-p!', '-m!'] NUM_FRAMES = 5 FRAME_NAMES = ["towards_us", "away_from_us", "away_alittle_right", "right", "towards_alittle_right"] 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)} def luminance(r, g, b): return int(0.299 * r + 0.587 * g + 0.114 * b) def is_bg(r, g, b, a, bg_color=(128, 0, 128, 255)): """Check if pixel is background (transparent). Uses sampled bg_color with tolerance.""" if a < 128: return True br, bg_g, bb = bg_color[0], bg_color[1], bg_color[2] if abs(r - br) < 15 and abs(g - bg_g) < 15 and abs(b - bb) < 15: return True if r < 10 and g < 10 and b < 10: return True return False def crop_to_content(img, target_w, target_h, bg_color): """Crop to bounding box of non-background content, pad to target size (centered).""" pix = img.load() w, h = img.size min_x, min_y, max_x, max_y = w, h, 0, 0 has_content = False 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, bg_color): min_x = min(min_x, x) min_y = min(min_y, y) max_x = max(max_x, x) max_y = max(max_y, y) has_content = True if not has_content: return Image.new('RGBA', (target_w, target_h), (0, 0, 0, 0)) cw = max_x - min_x + 1 ch = max_y - min_y + 1 cropped = img.crop((min_x, min_y, max_x + 1, max_y + 1)) result = Image.new('RGBA', (target_w, target_h), (0, 0, 0, 0)) ox = (target_w - cw) // 2 oy = (target_h - ch) // 2 result.paste(cropped, (ox, oy)) return result def quantize_vb(img, bg_color=(128, 0, 128, 255)): pix = img.load() w, h = img.size out = Image.new('RGBA', (w, h), (0,0,0,0)) opix = out.load() for y in range(h): for x in range(w): r, g, b, a = pix[x, y] if is_bg(r, g, b, a, bg_color): opix[x, y] = (0,0,0,0) continue lum = luminance(r, g, b) bayer = BAYER_2x2[y%2][x%2] lum_d = max(0, min(255, lum + (bayer - 0.5) * DITHER_STRENGTH)) if lum_d < 42: vb = 1 elif lum_d < 128: vb = 2 else: vb = 3 opix[x, y] = VB_PAL[vb] return out def run_grit(png_path): 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" GRIT ERROR: {result.stderr}") return [] basename = os.path.splitext(os.path.basename(png_path))[0] c_file = os.path.join(GRIT_DIR, f'{basename}.c') if not os.path.exists(c_file): return [] with open(c_file) as f: text = f.read() os.remove(c_file) m = re.search(r'const unsigned int \w+\[\d+\][^=]*=\s*\{([^}]+)\}', text, re.DOTALL) if not m: return [] raw = m.group(1).replace('\n', ' ').replace('\t', ' ') return [int(x.strip(), 0) for x in raw.split(',') if x.strip()] def main(): print("=== Rocket Projectile Sprite Converter ===\n") if not os.path.exists(STRIP_PATH): print(f"ERROR: Strip not found: {STRIP_PATH}") return img = Image.open(STRIP_PATH).convert('RGBA') print(f"Strip: {img.size[0]}x{img.size[1]}") # Sample background from top-left corner bg_color = img.getpixel((0, 0)) print(f"Background color (top-left): RGBA{bg_color}") fw = img.size[0] // NUM_FRAMES fh = img.size[1] # Pad each frame to 16x16 (2x2 tiles) - fits 15x14 first frame target_w = 16 target_h = 16 temp_dir = os.path.join(GRIT_DIR, "_rocket_proj_temp") os.makedirs(temp_dir, exist_ok=True) all_data = [] for i in range(NUM_FRAMES): frame = img.crop((i * fw, 0, (i + 1) * fw, fh)) # Crop to content, remove bg, pad to 16x16 centered padded = crop_to_content(frame, target_w, target_h, bg_color) quantized = quantize_vb(padded, bg_color) path = os.path.join(temp_dir, f"rocket_proj_{i}.png") quantized.save(path) data = run_grit(path) all_data.append(data) tiles = len(data) // 4 print(f"Frame {i} ({FRAME_NAMES[i]}): {tiles} tiles, {len(data)*4} bytes") shutil.rmtree(temp_dir, ignore_errors=True) tiles_per_frame = len(all_data[0]) // 4 # 4 (2x2 tiles) tw = target_w // TILE_SIZE # 2 th = target_h // TILE_SIZE # 2 # Write C source c_path = os.path.join(OUTPUT_DIR, "rocket_projectile_sprites.c") h_path = os.path.join(OUTPUT_DIR, "rocket_projectile_sprites.h") with open(c_path, 'w') as f: f.write("/* Rocket projectile directional sprites -- auto-generated */\n\n") for i, data in enumerate(all_data): f.write(f"const unsigned int rocketProjFrame{i}Tiles[{len(data)}] __attribute__((aligned(4))) = {{\n") for j in range(0, len(data), 4): chunk = data[j:j+4] vals = ', '.join(f'0x{v:08X}' for v in chunk) comma = ',' if j + 4 < len(data) else '' f.write(f' {vals}{comma}\n') f.write("};\n\n") with open(h_path, 'w') as f: f.write("#ifndef __ROCKET_PROJECTILE_SPRITES_H__\n") f.write("#define __ROCKET_PROJECTILE_SPRITES_H__\n\n") f.write(f"#define ROCKET_PROJ_TILE_W {tw}\n") f.write(f"#define ROCKET_PROJ_TILE_H {th}\n") f.write(f"#define ROCKET_PROJ_TILES {tiles_per_frame}\n") f.write(f"#define ROCKET_PROJ_FRAME_BYTES {tiles_per_frame * 16}\n") f.write(f"#define ROCKET_PROJ_FRAME_COUNT {NUM_FRAMES}\n\n") for i in range(NUM_FRAMES): f.write(f"extern const unsigned int rocketProjFrame{i}Tiles[{len(all_data[i])}];\n") f.write(f"\n/* Frame pointer table (5 unique views) */\n") f.write(f"static const unsigned int* const ROCKET_PROJ_FRAMES[{NUM_FRAMES}] = {{\n") for i in range(NUM_FRAMES): comma = ',' if i < NUM_FRAMES - 1 else '' f.write(f" rocketProjFrame{i}Tiles{comma}\n") f.write("};\n\n") f.write("/*\n") f.write(" * Direction mapping (8 directions, 0=front/towards player):\n") f.write(" * 0: front = frame 0\n") f.write(" * 1: front-right = frame 4\n") f.write(" * 2: right = frame 3\n") f.write(" * 3: back-right = frame 2\n") f.write(" * 4: back = frame 1\n") f.write(" * 5: back-left = frame 2 (H-flip)\n") f.write(" * 6: left = frame 3 (H-flip)\n") f.write(" * 7: front-left = frame 4 (H-flip)\n") f.write(" */\n") f.write("static const u8 ROCKET_PROJ_DIR_FRAME[8] = {0, 4, 3, 2, 1, 2, 3, 4};\n") f.write("static const u8 ROCKET_PROJ_DIR_HFLIP[8] = {0, 0, 0, 0, 0, 1, 1, 1};\n\n") f.write("#endif\n") print(f"\nWritten: {c_path}") print(f"Written: {h_path}") if __name__ == '__main__': main()