- Add ResolveHellAssetPath to automatically load *-hell variants during tournamentMode with fallback to vanilla assets - Rebuild Hell Tower briefing menu using HellTower_menu background with smooth floor cursor and sliding callout info card - Fix buffer overrun in RenderHellTower3D by packing helltower-menu at native 640x480 resolution with bounds-checked blit - Temporarily disable tower background music per user request - Add asset packing pipeline, verification tools, and automated 10-floor run-through smoke test
148 lines
6.4 KiB
Python
148 lines
6.4 KiB
Python
"""Compile custom Hell Tower assets and menu art into engine-ready binary packs."""
|
|
import os
|
|
import struct
|
|
from pathlib import Path
|
|
from PIL import Image, ImageOps
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
HELL_DIR = ROOT / "assets/hell_tower"
|
|
|
|
|
|
def write_anm1_pack(dst_path: Path, frames: list):
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
width, height = frames[0].size
|
|
if not (0 < len(frames) <= 64) or not all(f.size == (width, height) for f in frames):
|
|
raise ValueError(f"Invalid frames for {dst_path.name}")
|
|
raw_bgra = b"".join(f.tobytes("raw", "BGRA") for f in frames)
|
|
header = struct.pack("<4sIIII", b"ANM1", width, height, len(frames), len(raw_bgra))
|
|
dst_path.write_bytes(header + raw_bgra)
|
|
print(f"[OK] Wrote ANM1 pack: {dst_path} ({len(frames)} frames, {width}x{height}, {len(header + raw_bgra)} bytes)")
|
|
|
|
|
|
def write_cta1_pack(dst_path: Path, image: Image.Image):
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
image = image.convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
|
|
raw_bgra = image.tobytes("raw", "BGRA")
|
|
header = struct.pack("<4sIII", b"CTA1", 640, 480, len(raw_bgra))
|
|
dst_path.write_bytes(header + raw_bgra)
|
|
print(f"[OK] Wrote CTA1 pack: {dst_path} (640x480, {len(header + raw_bgra)} bytes)")
|
|
|
|
|
|
def slice_sprite_sheet(path: Path, cols: int, rows: int, cell_size: tuple, props: bool = False):
|
|
source = Image.open(path).convert("RGBA")
|
|
cells = []
|
|
for row in range(rows):
|
|
for col in range(cols):
|
|
cell = source.crop((
|
|
col * source.width // cols,
|
|
row * source.height // rows,
|
|
(col + 1) * source.width // cols,
|
|
(row + 1) * source.height // rows
|
|
))
|
|
cell.putalpha(cell.getchannel("A").point(lambda a: 255 if a >= 128 else 0))
|
|
bounds = cell.getchannel("A").getbbox()
|
|
if bounds is None:
|
|
bounds = (0, 0, cell.width, cell.height)
|
|
cells.append(cell.crop(bounds))
|
|
|
|
factor = min((cell_size[0] - 12) / max(c.width for c in cells),
|
|
(cell_size[1] - 12) / max(c.height for c in cells))
|
|
frames = []
|
|
for cell in cells:
|
|
scale = min((cell_size[0] - 12) / cell.width, (cell_size[1] - 12) / cell.height) if props else factor
|
|
cell = cell.resize((round(cell.width * scale), round(cell.height * scale)), Image.Resampling.NEAREST)
|
|
frame = Image.new("RGBA", cell_size)
|
|
y = (cell_size[1] - cell.height) // 2 if props else cell_size[1] - 6 - cell.height
|
|
frame.alpha_composite(cell, ((cell_size[0] - cell.width) // 2, y))
|
|
frames.append(frame)
|
|
return frames
|
|
|
|
|
|
def pack_all():
|
|
print("Packing Hell Tower assets...")
|
|
|
|
# 1. Hell Tower Menu background
|
|
menu_candidates = [
|
|
HELL_DIR / "environment/HellTower_ menu.jpg",
|
|
HELL_DIR / "environment/HellTower_menu.jpg",
|
|
HELL_DIR / "environment/HellTower_ menu.png",
|
|
HELL_DIR / "environment/HellTower_menu.png"
|
|
]
|
|
menu_src = None
|
|
for mc in menu_candidates:
|
|
if mc.exists():
|
|
menu_src = mc
|
|
break
|
|
if menu_src:
|
|
menu_im = Image.open(menu_src).convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
|
|
write_anm1_pack(ROOT / "assets/animations/hall/helltower-menu.bin", [menu_im])
|
|
else:
|
|
print("[WARN] HellTower_menu image not found!")
|
|
|
|
# 2. Coin table hell
|
|
coin_src = HELL_DIR / "01_coin/table-v1-hell.jpg"
|
|
if not coin_src.exists():
|
|
coin_src = HELL_DIR / "01_coin/table-v1-hell.png"
|
|
if coin_src.exists():
|
|
bg = Image.open(coin_src).convert("RGBA").resize((640, 480), Image.Resampling.NEAREST)
|
|
framed = Image.new("RGBA", (640, 480), (20, 25, 29, 255))
|
|
framed.paste(bg.crop((0, 0, 640, 440)), (0, 40))
|
|
framed.putalpha(255)
|
|
write_anm1_pack(ROOT / "assets/animations/coin/table-v1-hell.bin", [framed])
|
|
|
|
# 3. Card table hell (Blackjack, Poker, Durak)
|
|
card_src = HELL_DIR / "04_05_08_card_tables/card-table-pixel-v3-hell.jpg"
|
|
if not card_src.exists():
|
|
card_src = HELL_DIR / "04_05_08_card_tables/card-table-pixel-v3-hell.png"
|
|
if card_src.exists():
|
|
write_cta1_pack(ROOT / "assets/textures/card-table-hell.bin", Image.open(card_src))
|
|
|
|
# 4. Wheel closeup & table hell
|
|
wheel_closeup = HELL_DIR / "07_casino_roulette/closeup-v1-hell.jpg"
|
|
if not wheel_closeup.exists():
|
|
wheel_closeup = HELL_DIR / "07_casino_roulette/closeup-v1-hell.png"
|
|
if wheel_closeup.exists():
|
|
w_img = Image.open(wheel_closeup).convert("RGBA").resize((640, 480), Image.Resampling.NEAREST)
|
|
w_img.putalpha(255)
|
|
write_anm1_pack(ROOT / "assets/animations/wheel/closeup-v1-hell.bin", [w_img])
|
|
|
|
wheel_table = HELL_DIR / "07_casino_roulette/table-v1-hell.jpg"
|
|
if not wheel_table.exists():
|
|
wheel_table = HELL_DIR / "07_casino_roulette/table-v1-hell.png"
|
|
if wheel_table.exists():
|
|
w_img = Image.open(wheel_table).convert("RGBA").resize((640, 480), Image.Resampling.NEAREST)
|
|
w_img.putalpha(255)
|
|
write_anm1_pack(ROOT / "assets/animations/wheel/table-v1-hell.bin", [w_img])
|
|
|
|
# 5. Buckshot (and Russian Roulette) assets
|
|
# Room
|
|
bs_room = HELL_DIR / "10_buckshot/room-v2-hell.jpg"
|
|
if not bs_room.exists():
|
|
bs_room = HELL_DIR / "10_buckshot/room-v2-hell.png"
|
|
if bs_room.exists():
|
|
room_im = ImageOps.fit(Image.open(bs_room).convert("RGBA"), (640, 480), method=Image.Resampling.NEAREST)
|
|
write_anm1_pack(ROOT / "assets/animations/buckshot/room-v2-hell.bin", [room_im])
|
|
|
|
# Dealer base
|
|
bs_dealer = HELL_DIR / "10_buckshot/dealer-source-v2-hell.png"
|
|
if bs_dealer.exists():
|
|
frames = slice_sprite_sheet(bs_dealer, 4, 4, (256, 224))
|
|
write_anm1_pack(ROOT / "assets/animations/buckshot/dealer-v2-hell.bin", frames)
|
|
|
|
# Dealer actions
|
|
bs_actions = HELL_DIR / "10_buckshot/dealer-actions-v3-hell.png"
|
|
if bs_actions.exists():
|
|
frames = slice_sprite_sheet(bs_actions, 4, 4, (256, 224))
|
|
write_anm1_pack(ROOT / "assets/animations/buckshot/dealer-actions-v3-hell.bin", frames)
|
|
|
|
# Death
|
|
bs_death = HELL_DIR / "10_buckshot/death-v3-hell.png"
|
|
if bs_death.exists():
|
|
frames = slice_sprite_sheet(bs_death, 4, 3, (256, 224))
|
|
write_anm1_pack(ROOT / "assets/animations/buckshot/death-v3-hell.bin", frames)
|
|
|
|
print("All Hell Tower asset packs generated successfully!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pack_all()
|