joeylib2/examples/spacetaxi/assets/genPlaceholderArt.py

269 lines
11 KiB
Python

#!/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()