KAZIK/tools/tests/render_hell_tower_preview.py
Игорь Федосеев 3c8465bdad fix(hell_tower): calibrate menu to 6 floors, fix UI text overflow, and remove casino prefix
- 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
2026-10-26 20:43:19 +03:00

173 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()