- Align tower progression and cursor coordinates strictly to the 6 floors: Coin, Dice, Cards, Russian Roulette, Roulette, Buckshot - Recalibrate tower cursor Y positions for each tier (205, 174, 143, 112, 81, 50) - Reformat UI text, labels, and action buttons to strictly fit within card and sub-card boundaries without clipping - Remove redundant 'КАЗИНО 3D' prefix from header banner, keeping bold centered 'АДСКАЯ БАШНЯ' - Rebuild shipping binaries and verify complete 6-floor test suite
173 lines
8.3 KiB
Python
173 lines
8.3 KiB
Python
"""Render simulation of Hell Tower with 6 floors and strict boundary checks."""
|
||
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")
|
||
|
||
# 6 Floors Y in logical 320x240 coordinates
|
||
FLOOR_Y = [205, 174, 143, 112, 81, 50]
|
||
|
||
BOSSES = [
|
||
"Бес Азарта",
|
||
"Демон Костей",
|
||
"Крупье Тьмы",
|
||
"Судья Ада",
|
||
"Хозяин Рулетки",
|
||
"Владыка Башни"
|
||
]
|
||
|
||
GAMES = [
|
||
"Орел и решка",
|
||
"Кости",
|
||
"21 / Очко",
|
||
"Русская рулетка",
|
||
"Казино-рулетка",
|
||
"Buckshot Roulette"
|
||
]
|
||
|
||
DESCS = [
|
||
"Угадай сторону монеты.",
|
||
"Набери больше очков.",
|
||
"Обыграй крупье в 21.",
|
||
"1 патрон. Выживи!",
|
||
"Угадай число рулетки.",
|
||
"Дуэль на дробовике!"
|
||
]
|
||
|
||
SUB_NOTES = [
|
||
"Один бросок решит все.",
|
||
"Бросай кости против беса.",
|
||
"Собери больше, но не перебор.",
|
||
"Крути барабан револьвера.",
|
||
"Ставь на черное или красное.",
|
||
"Заряжай картечь и стреляй!"
|
||
]
|
||
|
||
def render_menu_frame(stage: int, state: int = 0, callout_anim: int = 100):
|
||
# 1. Load background
|
||
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")
|
||
|
||
# Scale down to 320x240 logical to replicate exact ASM blit logic
|
||
canvas = bg.resize((320, 240), Image.Resampling.LANCZOS)
|
||
draw = ImageDraw.Draw(canvas)
|
||
|
||
floor_idx = max(0, min(5, stage - 1))
|
||
target_y = FLOOR_Y[floor_idx]
|
||
|
||
# Sliding card dimensions
|
||
card_h = 208
|
||
card_y = 16
|
||
card_w = max(10, 206 * callout_anim // 100)
|
||
card_x = 214 - card_w
|
||
|
||
# 1. Indicator line and cursor on tower
|
||
draw.line([(214, target_y), (224, target_y)], fill=(255, 68, 68), width=2)
|
||
draw.rectangle([224, target_y - 3, 230, target_y + 3], fill=(255, 51, 0), outline=(255, 221, 68))
|
||
draw.rectangle([234, target_y - 2, 238, target_y + 2], fill=(255, 170, 34))
|
||
|
||
# 2. Card outer/inner borders
|
||
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)
|
||
|
||
inner_w = card_w - 12
|
||
inner_x = card_x + 6
|
||
|
||
if card_w >= 120:
|
||
# Header banner: "АДСКАЯ БАШНЯ" (Centered, NO "КАЗИНО 3D")
|
||
hdr_w = len("АДСКАЯ БАШНЯ") * 8
|
||
hdr_x = inner_x + (inner_w - hdr_w) // 2
|
||
draw.rectangle([inner_x, 22, inner_x + inner_w, 40], fill=(34, 4, 4), outline=(255, 68, 68))
|
||
draw.text((hdr_x, 26), "АДСКАЯ БАШНЯ", fill=(255, 221, 102))
|
||
|
||
# Floor & Bankroll / Prize indicator
|
||
draw.text((card_x + 12, 46), f"ЭТАЖ {stage} / 6", fill=(255, 102, 102))
|
||
draw.text((card_x + 105, 46), "КУШ: $66,600", fill=(255, 221, 68))
|
||
|
||
if state == 0: # BRIEFING
|
||
# Subcard
|
||
draw.rectangle([inner_x, 60, inner_x + inner_w, 130], fill=(30, 4, 4), outline=(136, 24, 24))
|
||
draw.text((card_x + 12, 66), f"ВРАГ: {BOSSES[floor_idx]}", fill=(255, 255, 255))
|
||
draw.text((card_x + 12, 80), f"ИГРА: {GAMES[floor_idx]}", fill=(255, 221, 102))
|
||
draw.text((card_x + 12, 96), DESCS[floor_idx], fill=(204, 204, 204))
|
||
draw.text((card_x + 12, 112), SUB_NOTES[floor_idx], fill=(170, 170, 170))
|
||
|
||
# Rules & warning box
|
||
draw.rectangle([inner_x, 134, inner_x + inner_w, 166], fill=(24, 3, 3), outline=(102, 17, 17))
|
||
draw.text((card_x + 12, 139), "6 этажей без поражений", fill=(255, 170, 51))
|
||
draw.text((card_x + 12, 151), "Поражение - возврат!", fill=(255, 85, 85))
|
||
|
||
# Bank
|
||
draw.text((card_x + 12, 172), "БАНК: $10,000", fill=(102, 255, 102))
|
||
draw.text((card_x + 115, 172), "НАГРАДА: +100%", fill=(255, 200, 50))
|
||
|
||
# Action Buttons
|
||
btn_w = 93
|
||
draw.rectangle([inner_x, 188, inner_x + btn_w, 212], fill=(61, 12, 12), outline=(255, 68, 68))
|
||
draw.text((inner_x + 12, 196), "[ENTER] БОЙ", fill=(255, 255, 255))
|
||
|
||
draw.rectangle([inner_x + 101, 188, inner_x + 101 + btn_w, 212], fill=(34, 6, 6), outline=(136, 34, 34))
|
||
draw.text((inner_x + 113, 196), "[ESC] НАЗАД", fill=(170, 170, 170))
|
||
|
||
elif state == 2: # CLEAR
|
||
draw.rectangle([inner_x, 62, inner_x + inner_w, 142], fill=(8, 34, 8), outline=(51, 204, 51), width=2)
|
||
clr_w = len("ЭТАЖ ЗАЧИЩЕН!") * 8
|
||
draw.text((inner_x + (inner_w - clr_w) // 2, 76), "ЭТАЖ ЗАЧИЩЕН!", fill=(102, 255, 102))
|
||
sub_w = len("Враг повержен!") * 8
|
||
draw.text((inner_x + (inner_w - sub_w) // 2, 96), "Враг повержен!", fill=(255, 255, 255))
|
||
rwd_w = len("Путь наверх открыт!") * 8
|
||
draw.text((inner_x + (inner_w - rwd_w) // 2, 116), "Путь наверх открыт!", fill=(255, 221, 102))
|
||
|
||
draw.rectangle([card_x + 18, 176, card_x + 188, 202], fill=(17, 68, 17), outline=(68, 255, 68))
|
||
btn_w2 = len("[ENTER] ДАЛЬШЕ") * 8
|
||
draw.text((card_x + 18 + (170 - btn_w2) // 2, 184), "[ENTER] ДАЛЬШЕ", fill=(255, 255, 255))
|
||
|
||
elif state == 3: # DEFEAT
|
||
draw.rectangle([inner_x, 62, inner_x + inner_w, 142], fill=(42, 4, 4), outline=(255, 34, 34), width=2)
|
||
dft_w = len("ВЫ ПОГИБЛИ В БАШНЕ!") * 8
|
||
draw.text((inner_x + (inner_w - dft_w) // 2, 76), "ВЫ ПОГИБЛИ В БАШНЕ!", fill=(255, 68, 68))
|
||
sub_w = len("Душа сгорела в Аду.") * 8
|
||
draw.text((inner_x + (inner_w - sub_w) // 2, 98), "Душа сгорела в Аду.", fill=(255, 255, 255))
|
||
|
||
draw.rectangle([card_x + 18, 176, card_x + 188, 202], fill=(68, 16, 16), outline=(255, 68, 68))
|
||
btn_w2 = len("[ESC] В ХОЛЛ КАЗИНО") * 8
|
||
draw.text((card_x + 18 + (170 - btn_w2) // 2, 184), "[ESC] В ХОЛЛ КАЗИНО", fill=(255, 255, 255))
|
||
|
||
elif state == 4: # CHAMPION
|
||
draw.rectangle([inner_x, 58, inner_x + inner_w, 150], fill=(51, 34, 0), outline=(255, 215, 0), width=2)
|
||
chm_w = len("БАШНЯ ПОКОРЕНА!") * 8
|
||
draw.text((inner_x + (inner_w - chm_w) // 2, 70), "БАШНЯ ПОКОРЕНА!", fill=(255, 224, 102))
|
||
sub_w = len("Вы - Владыка Башни!") * 8
|
||
draw.text((inner_x + (inner_w - sub_w) // 2, 90), "Вы - Владыка Башни!", fill=(255, 255, 255))
|
||
prz_w = len("КУШ: +$66,600 В БАНК!") * 8
|
||
draw.text((inner_x + (inner_w - prz_w) // 2, 114), "КУШ: +$66,600 В БАНК!", fill=(102, 255, 102))
|
||
|
||
draw.rectangle([card_x + 18, 176, card_x + 188, 202], fill=(68, 51, 0), outline=(255, 204, 0))
|
||
btn_w2 = len("[ESC] В ХОЛЛ КАЗИНО") * 8
|
||
draw.text((card_x + 18 + (170 - btn_w2) // 2, 184), "[ESC] В ХОЛЛ КАЗИНО", fill=(255, 255, 255))
|
||
|
||
# Scale 2x for full HD 640x480 preview
|
||
return canvas.resize((640, 480), Image.Resampling.NEAREST)
|
||
|
||
def main():
|
||
ART_DIR.mkdir(parents=True, exist_ok=True)
|
||
for st in range(1, 7):
|
||
im = render_menu_frame(st, state=0)
|
||
im.save(ART_DIR / f"helltower_6fl_floor{st}.png")
|
||
print(f"[OK] Saved helltower_6fl_floor{st}.png")
|
||
|
||
clr = render_menu_frame(1, state=2)
|
||
clr.save(ART_DIR / "helltower_6fl_clear.png")
|
||
dft = render_menu_frame(1, state=3)
|
||
dft.save(ART_DIR / "helltower_6fl_defeat.png")
|
||
chp = render_menu_frame(6, state=4)
|
||
chp.save(ART_DIR / "helltower_6fl_champion.png")
|
||
print("All previews generated successfully!")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|