#!/usr/bin/env python3 """Generate spacecraft spritesheet PNG for the JNR engine. Output: assets/sprites/spacecraft.png Layout: 5 frames horizontally, each 80x48 pixels = 400x48 total Frames: landed, landing1, landing2, landing3, landing4 The ship faces RIGHT, uses the game's pixel art color palette. """ from PIL import Image # ── Color palette (RGBA) ───────────────────────────── T = (0, 0, 0, 0) # transparent BLK = (26, 26, 46, 255) # dark/outline WHT = (238, 238, 234, 255) # white GRY = (136, 136, 136, 255) # grey GYD = (85, 85, 85, 255) # grey dark GYL = (187, 187, 187, 255) # grey light BLU = (74, 124, 189, 255) # blue accent BLD = (54, 94, 143, 255) # blue dark BLL = (111, 168, 220, 255) # blue light CYN = (77, 208, 225, 255) # cyan cockpit CYD = (0, 172, 193, 255) # cyan dark ORG = (255, 152, 0, 255) # orange ORD = (204, 122, 0, 255) # orange dark YLW = (255, 213, 79, 255) # yellow RED = (217, 68, 68, 255) # red W, H = 80, 48 # frame size def make_frame(): """Return a blank 80x48 pixel grid.""" return [[T] * W for _ in range(H)] def hline(grid, y, x0, x1, color): """Draw horizontal line from x0 to x1 inclusive.""" if y < 0 or y >= H: return for x in range(max(0, x0), min(W, x1 + 1)): grid[y][x] = color def pixel(grid, x, y, color): """Set a single pixel.""" if 0 <= x < W and 0 <= y < H: grid[y][x] = color def draw_ship_body(grid, oy=0): """Draw the main spacecraft hull, offset vertically by oy pixels. Ship anatomy (facing right): - Engine block: cols 10-18, thick rear - Main fuselage: cols 18-58, ~10px tall - Cockpit: cols 58-66, with CYN canopy - Nose: cols 66-72, pointed tip - Upper fin: cols 22-38, extends upward - Lower fin: cols 22-38, extends downward """ cy = 23 + oy # vertical center of fuselage # ── Main fuselage (rows cy-5 to cy+4 = 10px tall) ── for dy in range(-5, 5): y = cy + dy # Fuselage hull if dy == -5: # Top edge — light highlight hline(grid, y, 20, 60, GYL) elif dy == 4: # Bottom edge — dark shadow hline(grid, y, 20, 60, BLK) elif dy == -4: hline(grid, y, 18, 62, GYL) elif dy == 3: hline(grid, y, 18, 62, GYD) elif dy == -1 or dy == 0: # Blue racing stripe through center hline(grid, y, 16, 64, BLU) elif dy == -2: hline(grid, y, 17, 63, GRY) elif dy == 1: hline(grid, y, 17, 63, GRY) elif dy == 2: hline(grid, y, 18, 62, GYD) elif dy == -3: hline(grid, y, 19, 61, GRY) # ── Engine block (rear, left side) ── for dy in range(-6, 7): y = cy + dy if -6 <= dy <= 6: # Engine nacelle housing if abs(dy) <= 3: hline(grid, y, 10, 18, GYD) elif abs(dy) <= 5: hline(grid, y, 12, 18, GYD) elif abs(dy) == 6: hline(grid, y, 14, 17, GYD) # Engine inner dark (thruster openings) for dy in range(-3, 4): y = cy + dy if abs(dy) <= 2: hline(grid, y, 10, 12, BLK) elif abs(dy) == 3: pixel(grid, 11, y, BLK) pixel(grid, 12, y, BLK) # Engine highlight on top hline(grid, cy - 4, 13, 17, GRY) hline(grid, cy - 5, 14, 17, GRY) # ── Cockpit canopy ── # Canopy frame (CYD) and glass (CYN) for dy in range(-4, 3): y = cy + dy if dy == -4: hline(grid, y, 58, 63, CYD) elif dy == -3: hline(grid, y, 59, 65, CYD) hline(grid, y, 60, 64, CYN) elif dy == -2: hline(grid, y, 60, 66, CYD) hline(grid, y, 61, 65, CYN) pixel(grid, 63, y, WHT) # glint elif dy == -1: hline(grid, y, 61, 67, CYD) hline(grid, y, 62, 66, CYN) elif dy == 0: hline(grid, y, 61, 67, CYD) hline(grid, y, 62, 66, CYN) pixel(grid, 64, y, WHT) # glint elif dy == 1: hline(grid, y, 60, 66, CYD) hline(grid, y, 61, 65, CYN) elif dy == 2: hline(grid, y, 58, 64, CYD) # ── Nose (pointed tip facing right) ── for dy in range(-3, 4): y = cy + dy if dy == 0: hline(grid, y, 66, 73, GYL) # center line elif abs(dy) == 1: hline(grid, y, 66, 71, GRY) elif abs(dy) == 2: hline(grid, y, 65, 69, GYD) elif abs(dy) == 3: hline(grid, y, 65, 67, BLK) # Nose tip highlight pixel(grid, 73, cy, WHT) pixel(grid, 72, cy, WHT) # ── Upper wing fin ── for i in range(8): y = cy - 6 - i x0 = 26 + i x1 = 40 - i if x0 <= x1: hline(grid, y, x0, x1, GYD) # Highlight top edge if i < 7: pixel(grid, x0, y, GYL) # BLU accent if x1 - x0 > 2: pixel(grid, (x0 + x1) // 2, y, BLU) # Wing fin tip accent pixel(grid, 33, cy - 13, BLU) # ── Lower wing fin ── for i in range(8): y = cy + 5 + i x0 = 26 + i x1 = 40 - i if x0 <= x1: hline(grid, y, x0, x1, GYD) # Shadow bottom edge if i < 7: pixel(grid, x0, y, BLK) # BLU accent if x1 - x0 > 2: pixel(grid, (x0 + x1) // 2, y, BLD) # ── Panel detail lines ── for dy in [-3, 2]: y = cy + dy for x in [30, 42, 52]: pixel(grid, x, y, GYD if dy < 0 else BLK) def draw_landing_gear(grid, oy=0, length=4): """Draw two landing gear struts below the hull.""" cy = 23 + oy gear_top = cy + 5 # just below hull bottom # Front gear (col 52) for dy in range(length): y = gear_top + dy pixel(grid, 52, y, GYD) pixel(grid, 53, y, GYD) # Foot hline(grid, gear_top + length, 51, 54, BLK) # Rear gear (col 26) for dy in range(length): y = gear_top + dy pixel(grid, 26, y, GYD) pixel(grid, 27, y, GYD) # Foot hline(grid, gear_top + length, 25, 28, BLK) def draw_thrusters(grid, oy=0, intensity=0): """Draw thruster flames behind the engine. intensity: 0=off, 1=dim, 2=medium, 3=bright """ if intensity == 0: return cy = 23 + oy if intensity >= 3: # Bright: long flame with white core for dy in range(-2, 3): y = cy + dy if abs(dy) == 0: hline(grid, y, 1, 9, YLW) hline(grid, y, 3, 7, WHT) elif abs(dy) == 1: hline(grid, y, 3, 9, ORG) hline(grid, y, 4, 8, YLW) elif abs(dy) == 2: hline(grid, y, 5, 9, ORD) # Flame tip flicker pixel(grid, 1, cy, WHT) pixel(grid, 2, cy, YLW) elif intensity >= 2: # Medium flame for dy in range(-1, 2): y = cy + dy if dy == 0: hline(grid, y, 4, 9, YLW) hline(grid, y, 5, 8, WHT) else: hline(grid, y, 5, 9, ORG) hline(grid, y, 6, 8, YLW) pixel(grid, 4, cy - 2, ORD) pixel(grid, 4, cy + 2, ORD) elif intensity >= 1: # Dim glow for dy in range(-1, 2): y = cy + dy if dy == 0: hline(grid, y, 7, 9, ORD) pixel(grid, 8, y, ORG) else: pixel(grid, 8, y, ORD) pixel(grid, 9, y, ORD) def apply_tilt(grid, tilt_pixels): """Shift the left side of the image down by tilt_pixels to simulate nose-up tilt. This is a crude per-column vertical shift.""" if tilt_pixels == 0: return grid new_grid = make_frame() for x in range(W): # Linear interpolation: left edge shifts down by tilt_pixels, # right edge stays the same shift = int(tilt_pixels * (1.0 - x / W)) for y in range(H): src_y = y - shift if 0 <= src_y < H: new_grid[y][x] = grid[src_y][x] return new_grid def make_landed(): """Frame: ship landed on ground, gear down, no thrust.""" grid = make_frame() draw_ship_body(grid, oy=0) draw_landing_gear(grid, oy=0, length=5) return grid def make_landing1(): """Frame 1: ship high, tilted nose-up, bright thrusters.""" grid = make_frame() draw_ship_body(grid, oy=-8) draw_thrusters(grid, oy=-8, intensity=3) grid = apply_tilt(grid, 3) return grid def make_landing2(): """Frame 2: ship lower, less tilt, medium thrusters.""" grid = make_frame() draw_ship_body(grid, oy=-4) draw_thrusters(grid, oy=-4, intensity=2) grid = apply_tilt(grid, 1) return grid def make_landing3(): """Frame 3: ship nearly level, near ground, gear extending, dim thrusters.""" grid = make_frame() draw_ship_body(grid, oy=-2) draw_landing_gear(grid, oy=-2, length=3) draw_thrusters(grid, oy=-2, intensity=1) return grid def make_landing4(): """Frame 4: ship touching down, gear extended, no thrust.""" grid = make_frame() draw_ship_body(grid, oy=0) draw_landing_gear(grid, oy=0, length=5) # Nearly identical to landed — maybe a very slight glow pixel(grid, 9, 23, ORD) # residual engine heat return grid def grid_to_image(grid): """Convert a pixel grid to a PIL Image.""" img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) for y in range(H): for x in range(W): img.putpixel((x, y), grid[y][x]) return img def main(): frames = [ make_landed(), make_landing1(), make_landing2(), make_landing3(), make_landing4(), ] # Combine into horizontal spritesheet sheet = Image.new("RGBA", (W * len(frames), H), (0, 0, 0, 0)) for i, frame in enumerate(frames): img = grid_to_image(frame) sheet.paste(img, (i * W, 0)) out_path = "assets/sprites/spacecraft.png" sheet.save(out_path) print(f"Saved spritesheet: {out_path} ({sheet.width}x{sheet.height})") if __name__ == "__main__": main()