269 lines
11 KiB
Python
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()
|