Distance-based sound effects with stereo panning for explosions, impacts, and pickups. Spacecraft entity with full state machine (fly-in, land, take off, fly-out), engine/synth sound loops, thruster particles, and PNG spritesheet. Moon level intro defers player spawn until ship lands. Also untrack build/ objects that were committed by mistake.
361 lines
10 KiB
Python
361 lines
10 KiB
Python
#!/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()
|