- 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
115 lines
4.9 KiB
Python
115 lines
4.9 KiB
Python
"""Render full simulation of Hell Tower menu at floors 1, 5, 10."""
|
||
import struct
|
||
from pathlib import Path
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
MENU_BIN = ROOT / "assets/animations/hall/helltower-menu.bin"
|
||
ART_DIR = Path(r"C:\Users\IKER\.gemini\antigravity-cli\brain\c2d69e9d-8822-4f06-923a-36a9194468d6")
|
||
|
||
FLOOR_Y = [205, 186, 167, 148, 129, 110, 91, 72, 53, 34]
|
||
|
||
BOSSES = [
|
||
"БЕС АЗАРТА", "ДЕМОН КОСТЕЙ", "ЧЕРНОКНИЖНИК", "КРУПЬЕ ПРЕИСПОДНЕЙ", "ДЕМОН БЛЕФА",
|
||
"ОБРЕЧЕННЫЙ ГРЕШНИК", "КОЛЕСО СУДЬБЫ", "КАРТОЧНЫЙ ЧЕРТ", "ПАЛАЧ КРЮКА", "ВЛАДЫКА БАШНИ"
|
||
]
|
||
|
||
GAMES = [
|
||
"Орел и решка", "Кости", "Черепа и шарик", "21 / Очко", "Покер",
|
||
"Русская рулетка", "Казино-рулетка", "Дурак", "Крюк: Skill Check", "Buckshot Roulette"
|
||
]
|
||
|
||
DESCS = [
|
||
"Угадай сторону монеты.",
|
||
"Перебрось демона в сумме костей.",
|
||
"Найди душу под черепом.",
|
||
"Собери 21 очко и победи крупье.",
|
||
"Вскрой карты и победи в покер.",
|
||
"1 патрон в барабане: выживи.",
|
||
"Сделай ставку на рулетке.",
|
||
"Оставь чёрта в дураках.",
|
||
"Попади в зону и сорвись с крюка.",
|
||
"Дуэль на дробовиках за жизнь!"
|
||
]
|
||
|
||
|
||
def render_menu_frame(stage: int, callout_anim: int = 100):
|
||
# 1. Load background from ANM1 pack
|
||
data = MENU_BIN.read_bytes()
|
||
magic, w, h, frames, b_len = struct.unpack("<4sIIII", data[:20])
|
||
raw_bgra = data[20:20 + b_len]
|
||
bg = Image.frombytes("RGBA", (w, h), raw_bgra, "raw", "BGRA")
|
||
|
||
draw = ImageDraw.Draw(bg)
|
||
|
||
floor_idx = stage - 1
|
||
target_y = FLOOR_Y[floor_idx]
|
||
|
||
# Calculate sliding card box
|
||
card_h = 208
|
||
card_y = 20
|
||
card_w = max(10, 206 * callout_anim // 100)
|
||
card_x = 214 - card_w
|
||
|
||
# Connector line
|
||
draw.line([(214, target_y), (224, target_y)], fill=(255, 68, 68), width=2)
|
||
# Cursor chevron/diamond
|
||
draw.rectangle([224, target_y - 3, 230, target_y + 3], fill=(255, 51, 0), outline=(255, 221, 68))
|
||
# Pulsing flame pip
|
||
draw.rectangle([234, target_y - 2, 238, target_y + 2], fill=(255, 170, 34))
|
||
|
||
# Outer and inner border of card
|
||
draw.rectangle([card_x, card_y, card_x + card_w, card_y + card_h], outline=(204, 34, 34), width=2)
|
||
draw.rectangle([card_x + 2, card_y + 2, card_x + card_w - 2, card_y + card_h - 2], outline=(85, 17, 17), width=1)
|
||
|
||
if card_w >= 120:
|
||
# Header banner
|
||
draw.rectangle([card_x + 6, 26, card_x + card_w - 6, 44], fill=(34, 4, 4), outline=(255, 68, 68))
|
||
draw.text((card_x + 12, 29), "КАЗИНО 3D - АДСКАЯ БАШНЯ", fill=(255, 221, 102))
|
||
|
||
# Floor indicator
|
||
draw.text((card_x + 10, 48), f"ЭТАЖ {stage} / 10", fill=(255, 102, 102))
|
||
|
||
# Boss & Game sub-card
|
||
draw.rectangle([card_x + 6, 64, card_x + card_w - 6, 126], fill=(30, 4, 4), outline=(136, 24, 24))
|
||
draw.text((card_x + 10, 68), f"ВРАГ: {BOSSES[floor_idx]}", fill=(255, 255, 255))
|
||
draw.text((card_x + 10, 82), f"ИГРА: {GAMES[floor_idx]}", fill=(255, 221, 102))
|
||
draw.text((card_x + 10, 98), DESCS[floor_idx], fill=(204, 204, 204))
|
||
|
||
# Rules & warning
|
||
draw.text((card_x + 10, 132), "10 битв подряд без права на ошибку", fill=(255, 170, 51))
|
||
draw.text((card_x + 10, 146), "Поражение - возврат в холл казино", fill=(238, 68, 68))
|
||
|
||
# Bankroll & Prize
|
||
draw.text((card_x + 10, 162), "БАНК: $10,000", fill=(102, 255, 102))
|
||
draw.text((card_x + 110, 162), "КУШ: $66,600", fill=(255, 221, 68))
|
||
|
||
# Buttons
|
||
draw.rectangle([card_x + 8, 184, card_x + 100, 206], fill=(61, 12, 12), outline=(255, 68, 68))
|
||
draw.text((card_x + 14, 191), "[ПРОБЕЛ] В БОЙ", fill=(255, 255, 255))
|
||
|
||
draw.rectangle([card_x + 106, 184, card_x + 198, 206], fill=(34, 6, 6), outline=(136, 34, 34))
|
||
draw.text((card_x + 114, 191), "[ESC] СБЕЖАТЬ", fill=(170, 170, 170))
|
||
|
||
# Scale 2x for high quality artifact preview (640x480)
|
||
bg_hd = bg.resize((640, 480), Image.Resampling.NEAREST)
|
||
return bg_hd
|
||
|
||
|
||
def main():
|
||
ART_DIR.mkdir(parents=True, exist_ok=True)
|
||
f1 = render_menu_frame(1, 100)
|
||
f1.save(ART_DIR / "helltower_rendered_floor1.png")
|
||
print("[OK] Saved helltower_rendered_floor1.png")
|
||
|
||
f5 = render_menu_frame(5, 100)
|
||
f5.save(ART_DIR / "helltower_rendered_floor5.png")
|
||
print("[OK] Saved helltower_rendered_floor5.png")
|
||
|
||
f10 = render_menu_frame(10, 100)
|
||
f10.save(ART_DIR / "helltower_rendered_floor10.png")
|
||
print("[OK] Saved helltower_rendered_floor10.png")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|