#!/usr/bin/env python3 # genPlaceholderArt.py -- generate generic geometric placeholder art # for the Space Taxi port. Output is 320x200 indexed PNG, 16-color. # Replace these with hand-authored art any time; the build pipeline # picks up whichever PNG sits in assets/. import os from PIL import Image, ImageDraw ROOT = os.path.dirname(os.path.abspath(__file__)) # 16-color palette matching the engine's kDefaultPalette (see stRender.c). # Each entry is (R, G, B) in 0..255. The .jas format will store the # 4-bit-per-channel quantization automatically via joeyasset. PALETTE_RGB = [ (0x00, 0x00, 0x00), # 0 black (0x00, 0x00, 0x77), # 1 dark blue (0xCC, 0x00, 0x00), # 2 red (0x00, 0x77, 0x00), # 3 dark green (0x66, 0x44, 0x00), # 4 brown (0xEE, 0xEE, 0x00), # 5 yellow (0x88, 0x88, 0x88), # 6 mid gray (0xBB, 0xBB, 0xBB), # 7 light gray (0x44, 0x66, 0xFF), # 8 sky blue (0x77, 0x88, 0xFF), # 9 light blue (0xFF, 0x88, 0x88), # 10 light red (0x88, 0xFF, 0x44), # 11 light green (0xFF, 0x88, 0x00), # 12 orange (0xFF, 0x00, 0xFF), # 13 magenta (0x00, 0xFF, 0xFF), # 14 cyan (0xFF, 0xFF, 0xFF), # 15 white ] def new_canvas(): """Returns a fresh 320x200 indexed PIL image with the palette installed.""" img = Image.new('P', (320, 200), 0) flat = [] for r, g, b in PALETTE_RGB: flat.extend([r, g, b]) flat.extend([0] * (768 - len(flat))) # pad to 256 entries img.putpalette(flat) return img def write_indexed_png(img, path): """Save with mode='P' so PNG output is indexed (joeyasset wants this when converted through ppm; ImageMagick's `convert` preserves indexed mode when writing PPM).""" img.save(path, 'PNG', optimize=True) print(f" wrote {path}") def gen_tilebank(): """tilebank0.png: 320x200, 40x25 cells of 8x8 each (= 1000 tile slots). The engine convention is: index 0 = empty (non-solid) index 1..63 = solid walls index 64..127 = landing-pad surfaces index 128..255 = decorative non-solid We fill a few representative slots; the rest stay black so they show as "empty" if a level references them. """ img = new_canvas() px = img.load() def draw_tile(idx, painter): bx = (idx % 40) * 8 by = (idx // 40) * 8 painter(bx, by) def block(color): return lambda x, y: [px.__setitem__((x+dx, y+dy), color) for dx in range(8) for dy in range(8)] def bricks(color_a, color_b): def paint(x, y): for dy in range(8): row_offset = 4 if (y // 8 + dy // 4) % 2 == 0 else 0 for dx in range(8): is_grout = (dx == (row_offset % 8)) or (dy == 3 or dy == 7) px[x + dx, y + dy] = color_b if is_grout else color_a return paint def stripes(color_a, color_b): def paint(x, y): for dy in range(8): c = color_a if (dy + (x // 8)) % 2 == 0 else color_b for dx in range(8): px[x + dx, y + dy] = c return paint def landing_pad(color_a, color_b): # diagonal stripes signal a landing-pad surface def paint(x, y): for dy in range(8): for dx in range(8): px[x + dx, y + dy] = color_a if ((dx + dy) // 2) % 2 == 0 else color_b return paint # tile 0 -> already black (empty); leave as-is # walls 1..7: a variety of brick/stripe patterns draw_tile(1, bricks(6, 0)) # gray brick draw_tile(2, bricks(4, 0)) # brown brick draw_tile(3, block(6)) # solid gray draw_tile(4, block(4)) # solid brown draw_tile(5, stripes(6, 7)) # gray stripes (ceiling) draw_tile(6, stripes(1, 8)) # blue stripes (sky deco) draw_tile(7, block(7)) # solid light gray # landing pad tiles 64..67 draw_tile(64, landing_pad(5, 12)) # yellow/orange pad draw_tile(65, landing_pad(11, 3)) # green pad draw_tile(66, landing_pad(9, 8)) # blue pad draw_tile(67, landing_pad(15, 7)) # white pad write_indexed_png(img, os.path.join(ROOT, 'tiles', 'tilebank0.png')) def gen_sprites(): """sprites.png: 320x200, layout: y= 0..23 : taxi cels (4 frames of 24x24 starting at x=0) y= 24..39 : passenger walk cels (4 frames of 16x16 at x=0) Remaining rows free for future content. """ img = new_canvas() d = ImageDraw.Draw(img) # ---- Taxi cels: simple side-view "pod with thruster" ---- # Cel 0 = idle (thruster off) # Cel 1..3 = thrust-on, alternating flame frames for cel in range(4): x = cel * 24 # Pod body: rounded rectangle in gray d.rectangle([x+2, 4, x+21, 16], fill=7, outline=15) # Cockpit window in blue d.rectangle([x+13, 6, x+19, 10], fill=8, outline=15) # Landing skid d.line([(x+3, 17), (x+20, 17)], fill=6) d.line([(x+3, 17), (x+3, 19)], fill=6) d.line([(x+20,17), (x+20, 19)], fill=6) # Thruster flame (cels 1..3 only, alternating shape) if cel >= 1: flame_color = [2, 12, 5][(cel - 1) % 3] flame_top = 18 flame_bot = 22 if cel != 2 else 23 d.polygon([(x+8, flame_top), (x+15, flame_top), (x+12, flame_bot)], fill=flame_color) # ---- Passenger cels: small stick figure, walk cycle ---- # Centered in 16x16, walking right. for cel in range(4): x = cel * 16 y = 24 # Head d.ellipse([x+5, y+1, x+10, y+6], fill=10, outline=15) # Body d.line([(x+7, y+7), (x+7, y+12)], fill=15) # Arms (swing alternately based on cel) if cel == 0 or cel == 2: d.line([(x+4, y+10), (x+10, y+10)], fill=15) else: d.line([(x+3, y+9), (x+11, y+11)], fill=15) # Legs (alternating stride) if cel == 0 or cel == 2: d.line([(x+7, y+12), (x+4, y+15)], fill=15) d.line([(x+7, y+12), (x+10,y+15)], fill=15) else: d.line([(x+7, y+12), (x+5, y+15)], fill=15) d.line([(x+7, y+12), (x+9, y+15)], fill=15) write_indexed_png(img, os.path.join(ROOT, 'sprites', 'sprites.png')) def gen_font(): """font.png: 320x200 (40x25 grid of 8x8 ASCII glyphs). Authored manually for the printable subset (0x20..0x7E). Each glyph cell is at (ascii%40, ascii/40) when ascii<128. Unfilled cells stay black (treated as TILE_NO_GLYPH at runtime). """ # A compact 5-pixel-wide bitmap font, drawn in 7 rows with 1px # padding on all sides (so 5x7 active inside an 8x8 cell, baseline # at row 6). The font supports digits, uppercase letters, space, # and basic punctuation -- enough for HUD text. Lowercase letters # map to the same glyphs as uppercase (room to add later). GLYPHS = { ' ': [], '!': ['..#..', '..#..', '..#..', '..#..', '.....', '..#..', '.....'], '"': ['.#.#.', '.#.#.', '.....', '.....', '.....', '.....', '.....'], '0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'], '1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'], '2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'], '3': ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'], '4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'], '5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'], '6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'], '7': ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'], '8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'], '9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'], ':': ['.....', '..#..', '.....', '.....', '.....', '..#..', '.....'], '.': ['.....', '.....', '.....', '.....', '.....', '..#..', '.....'], '-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'], '/': ['....#', '...#.', '...#.', '..#..', '.#...', '.#...', '#....'], 'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], 'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'], 'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'], 'D': ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'], 'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'], 'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'], 'G': ['.###.', '#...#', '#....', '#..##', '#...#', '#...#', '.###.'], 'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'], 'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'], 'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'], 'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'], 'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'], 'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'], 'N': ['#...#', '##..#', '#.#.#', '#.#.#', '#..##', '#...#', '#...#'], 'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], 'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'], 'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'], 'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'], 'S': ['.###.', '#...#', '#....', '.###.', '....#', '#...#', '.###.'], 'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'], 'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'], 'V': ['#...#', '#...#', '#...#', '#...#', '.#.#.', '.#.#.', '..#..'], 'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '##.##', '#...#'], 'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'], 'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'], 'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'], } img = new_canvas() px = img.load() def paint_glyph(ascii_code, glyph_rows): col = ascii_code % 40 row = ascii_code // 40 bx = col * 8 by = row * 8 # active 5x7 area: cell-x 1..5, cell-y 0..6 (top row, 1px left pad) for ry, line in enumerate(glyph_rows): for rx, ch in enumerate(line): if ch == '#': px[bx + 1 + rx, by + ry] = 15 # white for ascii_code in range(32, 127): c = chr(ascii_code) if c in GLYPHS: paint_glyph(ascii_code, GLYPHS[c]) elif 'a' <= c <= 'z': paint_glyph(ascii_code, GLYPHS[c.upper()]) write_indexed_png(img, os.path.join(ROOT, 'font.png')) def main(): os.makedirs(os.path.join(ROOT, 'tiles'), exist_ok=True) os.makedirs(os.path.join(ROOT, 'sprites'), exist_ok=True) print("genPlaceholderArt: emitting 320x200 indexed PNGs...") gen_tilebank() gen_sprites() gen_font() print("done.") if __name__ == '__main__': main()