feat(hell_tower): integrate custom hell sprites, redesign tower menu, and fix entrance crash

- 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
This commit is contained in:
Игорь Федосеев 2026-10-26 19:44:06 +03:00
parent 75d594ec79
commit eef2039c7e
54 changed files with 1144 additions and 112 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,68 @@
# Спрайты для адской версии игр Башни (Hell Tower)
В этой папке собраны **только визуальные спрайты (PNG)**, которые непосредственно используются в играх Адской Башни. Вы можете перерисовать их в демоническом / адском стиле (кровь, руны, пламя, черепа, черные металлы).
---
## 1. Этаж 1: Орел и решка (`01_coin`)
*Босс: БЕС АЗАРТА*
- `faces-v1.png` — Лицевая и обратная сторона монеты (анимация вращения / стороны).
- `hands-v1.png` / `hands-v2.png` — Руки игрока / демона, подбрасывающие монету.
- `table-v1.png` — Стол / сукно, на которое падает монета.
---
## 2. Этаж 2: Кости (`02_dice`)
*Босс: ДЕМОН КОСТЕЙ*
- `hands-v1.png` / `hands-v2.png` — Руки, бросающие игральные кости (кубики).
*(Стол используется общий с `01_coin/table-v1.png`)*
---
## 3. Этажи 4, 5, 8: Карточные игры 21, Покер, Дурак (`04_05_08_card_tables`)
*Боссы: КРУПЬЕ ПРЕИСПОДНЕЙ, ДЕМОН БЛЕФА, КАРТОЧНЫЙ ЧЕРТ*
- `blackjack-guests-v1.png` — Силуэты гостей / грешников за карточным столом.
- `blackjack-room-v1.png` — Фон комнаты карточных дуэлей.
- `card-table-v1.png`, `card-table-active-v2.png`, `card-table-pixel-v3.png` — Варианты карточного стола и сукна.
- `sprite_blackjack_table.png` — 3D-биллборд стола блэкджека в холле/башне.
---
## 4. Этаж 6: Русская рулетка (`06_russian_roulette`)
*Босс: ОБРЕЧЕННЫЙ ГРЕШНИК*
- `revolver-actions-v1.png` — Револьвер (барабан, курок, взвод, наведение).
- `hands-v5.png` — Руки от первого лица с револьвером у виска / стола.
- `participants-v1.png` — Сидящие напротив оппоненты русской рулетки.
- `gman-talk-v1.png` — Ведущий / распорядитель игры.
---
## 5. Этаж 7: Казино-рулетка (`07_casino_roulette`)
*Босс: КОЛЕСО СУДЬБЫ*
- `closeup-v1.png` — Крупный план вращающегося колеса рулетки.
- `table-v1.png` — Стол ставок и разметки чисел рулетки.
- `sprite_roulette_table.png` — 3D-биллборд стола рулетки.
---
## 6. Этаж 10: Buckshot Roulette (`10_buckshot`)
*Босс: ВЛАДЫКА БАШНИ*
- `dealer-actions-v3.png` — Спрайты дилера (прицеливание, передача дробовика, действия).
- `dealer-source-v2.png` — Базовый портрет дилера напротив игрока.
- `death-v3.png` — Спрайты смерти / поражения.
- `items-v2.png` — Предметы на столе (ножовка, сигареты, пиво, лупа, наручники).
- `loading-v2.png` — Заряжание боевых и холостых патронов в патронник.
- `medical-dealer-v5.png` / `medical-player-v5.png` / `medical-v3.png` — Дефибрилляторы и реанимация.
- `props-v2.png` — Реквизит стола: патроны, коробка, дробовик.
- `room-v2.png` — Фон комнаты смертельного поединка.
- `self-flash-v4.png` — Вспышка и выстрел в себя.
- `self-v3.png` — Наведение дробовика на себя.
- `shots-v2.png` — Выстрелы в дилера.
- `blood-v3.png` — Брызги крови, повреждения.
---
## 7. Текстуры окружения (`environment`)
- `tex_wall_hell.png` — Текстура адских стен Башни.
*(Примечание: На этажах 3 (Черепа) и 9 (Крюк: Skill Check) используются процедурные векторные элементы 3D-движка (`DrawGothicCup`, разметка шкалы) либо консольная ANSI-анимация `dbd_hook.ans`).*

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Binary file not shown.

120
poker.asm
View file

@ -514,28 +514,9 @@ StopTowerMusic PROC
ret
StopTowerMusic ENDP
PlayTowerRandomMusic PROC uses eax
PlayTowerRandomMusic PROC
; Tower music disabled for now per user request
call StopTowerMusic
invoke RandomRange, 4
cmp eax, 0
je tower_music_1
cmp eax, 1
je tower_music_2
cmp eax, 2
je tower_music_3
tower_music_4:
invoke mciSendStringA, OFFSET towerMusicOpen4, 0, 0, 0
jmp tower_music_play
tower_music_1:
invoke mciSendStringA, OFFSET towerMusicOpen1, 0, 0, 0
jmp tower_music_play
tower_music_2:
invoke mciSendStringA, OFFSET towerMusicOpen2, 0, 0, 0
jmp tower_music_play
tower_music_3:
invoke mciSendStringA, OFFSET towerMusicOpen3, 0, 0, 0
tower_music_play:
invoke mciSendStringA, OFFSET towerMusicPlay, 0, 0, 0
ret
PlayTowerRandomMusic ENDP
@ -1923,6 +1904,103 @@ CheckDurakEnd ENDP
include casino3d.inc
IFDEF CASINO_ACTION_TEST
RunCasinoActionsTest3D PROC uses ebx esi edi
local stageLoop:DWORD
call InitPaths3D
call Init3DEngine
test eax, eax
jz test_error_init
; Enter Hell Tower
call InitHellTower3D
cmp tournamentMode, 1
jne test_error_tower_mode
mov stageLoop, 1
stage_test_loop:
; 1. Briefing state frame
call HandleHellTowerInput
call RenderHellTower3D
call Present3D
; 2. Launch current floor minigame
call TowerLaunchCurrentFloor
cmp towerFloorState, TOWER_STATE_IN_GAME
jne test_error_launch
; 3. Win the round on current floor
invoke TowerReportRoundOutcome, 1
; 4. Check outcome state
cmp stageLoop, 10
jae stage_check_champ
cmp towerFloorState, TOWER_STATE_CLEAR
jne test_error_clear_state
; Frame in clear state
call HandleHellTowerInput
call RenderHellTower3D
call Present3D
; Advance to next stage
inc towerStage
mov towerFloorState, TOWER_STATE_BRIEFING
mov towerCalloutAnim, 0
inc stageLoop
jmp stage_test_loop
stage_check_champ:
cmp towerFloorState, TOWER_STATE_CHAMPION
jne test_error_champ_state
; Frame in champion state
call HandleHellTowerInput
call RenderHellTower3D
call Present3D
; Exit tower back to hall
call TowerExitToHall
cmp tournamentMode, 0
jne test_error_exit_mode
cmp engineMode, MODE_WALK
jne test_error_exit_engine
; Hall walk frame
call Present3D
; SUCCESS!
mov eax, 0
ret
test_error_init:
mov eax, 101
ret
test_error_tower_mode:
mov eax, 102
ret
test_error_launch:
mov eax, 103
ret
test_error_clear_state:
mov eax, 104
ret
test_error_champ_state:
mov eax, 105
ret
test_error_exit_mode:
mov eax, 106
ret
test_error_exit_engine:
mov eax, 107
ret
RunCasinoActionsTest3D ENDP
ENDIF
main PROC
IFDEF CASINO_ACTION_TEST
call RunCasinoActionsTest3D

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -16,9 +16,11 @@ FreeCardTableAssets3D ENDP
LoadCardTableAssets3D PROC uses ebx
local count:DWORD
local resolvedPath[260]:BYTE
cmp cardTablePixels3D, 0
jne card_load_ranks
invoke CreateFileA, OFFSET cardTablePath3D, 80000000h, 1, 0, 3, 80h, 0
invoke ResolveHellAssetPath, OFFSET cardTablePath3D, ADDR resolvedPath
invoke CreateFileA, eax, 80000000h, 1, 0, 3, 80h, 0
cmp eax, -1
je card_load_ranks
mov ebx, eax

View file

@ -1,12 +1,30 @@
; --- hell_tower.inc: Implementation of Hell Tower (10-floor Boss Rush) ---
.code
FreeTowerMinigameAssets PROC
call FreeBuckshotAssets3D
call FreeRussianAssets3D
call FreeCardTableAssets3D
call FreeNativeCoinArt3D
call FreeNativeWheelArt3D
ret
FreeTowerMinigameAssets ENDP
InitHellTower3D PROC
call FreeTowerMinigameAssets
mov tournamentMode, 1
mov towerStage, 1
mov towerFloorState, TOWER_STATE_BRIEFING
mov towerAnimClock, 0
call PlayTowerRandomMusic
mov towerCalloutAnim, 0
mov eax, towerFloorY[0]
mov towerCursorY, eax
mov towerCursorTargetY, eax
invoke LoadAnimPack3D, OFFSET hellTowerMenuArt3D, OFFSET hellTowerMenuPath3D
call StopTowerMusic
mov engineMode, MODE_HELL_TOWER
call ClearKeys3D
ret
@ -14,6 +32,7 @@ InitHellTower3D ENDP
TowerExitToHall PROC
call StopTowerMusic
call FreeTowerMinigameAssets
mov tournamentMode, 0
mov engineMode, MODE_WALK
call ClearKeys3D
@ -110,6 +129,14 @@ TowerReportRoundOutcome PROC uses eax outcome:DWORD
jae report_champion
mov towerFloorState, TOWER_STATE_CLEAR
mov towerCalloutAnim, 0
mov eax, towerStage
dec eax
cmp eax, 10
jae @F
mov edx, towerFloorY[eax*4]
mov towerCursorTargetY, edx
@@:
invoke PlayCasinoSound3D, OFFSET soundRoundWin, 0, 1
mov engineMode, MODE_HELL_TOWER
call ClearKeys3D
@ -194,6 +221,14 @@ input_clear:
advance_stage:
inc towerStage
mov towerFloorState, TOWER_STATE_BRIEFING
mov towerCalloutAnim, 0
mov eax, towerStage
dec eax
cmp eax, 10
jae @F
mov edx, towerFloorY[eax*4]
mov towerCursorTargetY, edx
@@:
call ClearKeys3D
ret
@ -227,132 +262,321 @@ exit_to_hall:
HandleHellTowerInput ENDP
RenderHellTower3D PROC uses ebx esi edi
; 1. Clear background with deep infernal maroon
local cardX:DWORD, cardY:DWORD, cardW:DWORD, cardH:DWORD
local currY:DWORD, tgtY:DWORD
; 1. Background: draw HellTower menu art if loaded, else fallback to maroon
cmp hellTowerMenuArt3D.pixels, 0
je fallback_clear
cmp hellTowerMenuArt3D.imageWidth, SCREEN_W
jne fallback_sprite
cmp hellTowerMenuArt3D.height, SCREEN_H
jne fallback_sprite
mov esi, hellTowerMenuArt3D.pixels
lea edi, screenBuffer
mov ecx, SCREEN_SIZE
rep movsd
jmp bg_done
fallback_sprite:
invoke DrawAnimSprite3D, OFFSET hellTowerMenuArt3D, 0, 0, 0, SCREEN_W, SCREEN_H
jmp bg_done
fallback_clear:
lea edi, screenBuffer
mov ecx, SCREEN_SIZE
mov eax, 00100202h
rep stosd
; 2. Outer and inner border
invoke DrawBoxBorder, 8, 8, 304, 224, 00120202h, 00CC2222h, 2
invoke DrawBoxBorder, 12, 12, 296, 216, 000C0202h, 00881111h, 1
bg_done:
; 2. Update towerAnimClock
inc towerAnimClock
; 3. Header Banner (Centered: 192px wide, (320 - 192)/2 = 64)
invoke DrawBoxBorder, 18, 16, 284, 20, 00220404h, 00FF4444h, 1
invoke DrawString8x8, 64, 22, OFFSET towerHeader, 00FFDD66h
; 3. Smooth cursor Y movement towards towerCursorTargetY (exponential damping)
mov eax, towerCursorTargetY
cmp eax, towerCursorY
je cursor_update_done
jg cursor_move_down
; 4. Floor Header & Segment Progress Bar
invoke DrawString8x8, 26, 42, OFFSET towerFloorPrefix, 00FF6666h
; Moving UP towards tower peak (target < current)
mov ecx, towerCursorY
sub ecx, eax
shr ecx, 2
inc ecx
sub towerCursorY, ecx
mov eax, towerCursorTargetY
cmp towerCursorY, eax
jge cursor_update_done
mov towerCursorY, eax
jmp cursor_update_done
cursor_move_down:
; Moving DOWN towards tower base (target > current)
mov ecx, eax
sub ecx, towerCursorY
shr ecx, 2
inc ecx
add towerCursorY, ecx
mov eax, towerCursorTargetY
cmp towerCursorY, eax
jle cursor_update_done
mov towerCursorY, eax
cursor_update_done:
; 4. Update sliding callout animation (0 to 100%)
cmp towerCalloutAnim, 100
jge callout_step_done
add towerCalloutAnim, 10
cmp towerCalloutAnim, 100
jle callout_step_done
mov towerCalloutAnim, 100
callout_step_done:
; 5. Calculate sliding callout card dimensions occupying free left space:
; Max width = 206, fixed right bound = 214, top = 20, height = 208
mov cardH, 208
mov cardY, 20
; cardW = 206 * towerCalloutAnim / 100
mov eax, 206
imul eax, towerCalloutAnim
xor edx, edx
mov ecx, 100
div ecx
cmp eax, 10
jae @F
mov eax, 10
@@:
mov cardW, eax
; cardX = 214 - cardW
mov edx, 214
sub edx, cardW
mov cardX, edx
; 6. Draw glowing cursor & indicator on the tower (Right side)
; Indicator line from card right edge (214) to cursor on tower (X=224)
mov eax, towerCursorY
invoke DrawBox, 214, eax, 10, 2, 00FF4444h
; Cursor chevron/diamond at X=224
mov eax, towerCursorY
sub eax, 3
invoke DrawBoxBorder, 224, eax, 7, 7, 00FF3300h, 00FFDD44h, 1
; Pulsing flame pip on the tower floor at X=234
mov eax, towerAnimClock
and eax, 8
jz pip_bright
mov edx, 00EE3300h
jmp pip_draw
pip_bright:
mov edx, 00FFAA22h
pip_draw:
mov eax, towerCursorY
sub eax, 2
invoke DrawBox, 234, eax, 5, 5, edx
; 7. Draw Callout Box (outer and inner border with deep infernal tint)
invoke DrawBoxBorder, cardX, cardY, cardW, cardH, 00120202h, 00CC2222h, 2
mov eax, cardX
add eax, 2
mov edx, cardY
add edx, 2
mov ecx, cardW
sub ecx, 4
mov ebx, cardH
sub ebx, 4
invoke DrawBoxBorder, eax, edx, ecx, ebx, 000C0202h, 00551111h, 1
; Only render full content once card has slid out sufficiently (cardW >= 120)
cmp cardW, 120
jb render_tower_done
; Header Banner: "ÊÀÇÈÍÎ 3D - ÀÄÑÊÀß ÁÀØÍß"
mov eax, cardX
add eax, 6
mov ecx, cardW
sub ecx, 12
invoke DrawBoxBorder, eax, 26, ecx, 18, 00220404h, 00FF4444h, 1
mov eax, cardX
add eax, 12
invoke DrawString8x8, eax, 31, OFFSET towerHeader, 00FFDD66h
; Floor indicator: "ÝÒÀÆ N / 10"
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 50, OFFSET towerFloorPrefix, 00FF6666h
invoke FormatNumber, towerStage, OFFSET hudNumBuffer
invoke DrawString8x8, 66, 42, OFFSET hudNumBuffer, 00FFFFFFh
invoke DrawString8x8, 74, 42, OFFSET towerOfTen, 00AAAAAAh
invoke DrawCharmsHUD, 130, 42
mov eax, cardX
add eax, 50
invoke DrawString8x8, eax, 50, OFFSET hudNumBuffer, 00FFFFFFh
mov eax, cardX
add eax, 58
invoke DrawString8x8, eax, 50, OFFSET towerOfTen, 00AAAAAAh
; Draw 10 stage pips across x = 26 .. 293
xor esi, esi
draw_pips_loop:
mov eax, esi
imul eax, 27
add eax, 26 ; x position
mov ebx, eax
; Color based on state
mov ecx, towerStage
dec ecx
cmp esi, ecx
jae pip_future
; Past stage (Cleared): Bright Gold
invoke DrawBoxBorder, ebx, 54, 24, 6, 00DDBB22h, 00FFEE44h, 1
jmp pip_next
pip_future:
je pip_current
; Future stage: Dark
invoke DrawBoxBorder, ebx, 54, 24, 6, 002A0808h, 00441111h, 1
jmp pip_next
pip_current:
; Current stage: Pulsing Fire Red/Orange
invoke DrawBoxBorder, ebx, 54, 24, 6, 00EE3311h, 00FFAA33h, 1
pip_next:
inc esi
cmp esi, 10
jb draw_pips_loop
; 5. Specific State Panels
; Specific state content inside the card
mov eax, towerFloorState
cmp eax, TOWER_STATE_BRIEFING
je render_briefing
je render_card_briefing
cmp eax, TOWER_STATE_CLEAR
je render_clear
je render_card_clear
cmp eax, TOWER_STATE_DEFEAT
je render_defeat
je render_card_defeat
cmp eax, TOWER_STATE_CHAMPION
je render_champion
ret
je render_card_champion
jmp render_tower_done
render_briefing:
; Boss & Game Card
invoke DrawBoxBorder, 20, 66, 280, 58, 001E0404h, 00AA2222h, 1
invoke DrawString8x8, 28, 72, OFFSET towerEnemyLabel, 00FFAA55h
render_card_briefing:
; Boss & Game sub-card
mov eax, cardX
add eax, 6
mov ecx, cardW
sub ecx, 12
invoke DrawBoxBorder, eax, 64, ecx, 62, 001E0404h, 00881818h, 1
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 70, OFFSET towerEnemyLabel, 00FFAA55h
mov eax, towerStage
dec eax
mov edx, towerBossNames[eax*4]
invoke DrawString8x8, 76, 72, edx, 00FFFFFFh
mov eax, cardX
add eax, 56
invoke DrawString8x8, eax, 70, edx, 00FFFFFFh
invoke DrawString8x8, 28, 86, OFFSET towerGameLabel, 00FFAA55h
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 84, OFFSET towerGameLabel, 00FFAA55h
mov eax, towerStage
dec eax
mov edx, towerGameNames[eax*4]
invoke DrawString8x8, 76, 86, edx, 00FFDD66h
mov eax, cardX
add eax, 56
invoke DrawString8x8, eax, 84, edx, 00FFDD66h
mov eax, towerStage
dec eax
mov edx, towerDescriptions[eax*4]
invoke DrawString8x8, 28, 104, edx, 00CCCCCCh
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 100, edx, 00CCCCCCh
; Rules & Warning (fits cleanly inside 300px boundary)
invoke DrawString8x8, 24, 132, OFFSET towerRuleText, 00FFAA33h
invoke DrawString8x8, 24, 146, OFFSET towerLossWarning, 00EE4444h
; Rules & warning
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 134, OFFSET towerRuleText, 00FFAA33h
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 148, OFFSET towerLossWarning, 00EE4444h
; Bankroll Display
invoke DrawString8x8, 24, 164, OFFSET textBank3D, 00AAAAAAh
; Bankroll & Prize
mov eax, cardX
add eax, 10
invoke DrawString8x8, eax, 164, OFFSET textBank3D, 00AAAAAAh
invoke FormatNumber, bankroll, OFFSET hudNumBuffer
invoke DrawString8x8, 76, 164, OFFSET hudNumBuffer, 0066FF66h
mov eax, cardX
add eax, 56
invoke DrawString8x8, eax, 164, OFFSET hudNumBuffer, 0066FF66h
; Action Buttons (Centered text inside buttons)
invoke DrawBoxBorder, 22, 184, 132, 24, 003D0C0Ch, 00FF4444h, 1
invoke DrawString8x8, 32, 192, OFFSET towerStartBtn, 00FFFFFFh
mov eax, cardX
add eax, 110
invoke DrawString8x8, eax, 164, OFFSET towerPrizePrompt, 00FFAA55h
invoke FormatNumber, towerPrizeAmount, OFFSET hudNumBuffer
mov eax, cardX
add eax, 154
invoke DrawString8x8, eax, 164, OFFSET hudNumBuffer, 00FFDD44h
invoke DrawBoxBorder, 164, 184, 132, 24, 00220606h, 00882222h, 1
invoke DrawString8x8, 178, 192, OFFSET towerEscBtn, 00AAAAAAh
ret
; Action Buttons (Start & Escape)
mov eax, cardX
add eax, 8
invoke DrawBoxBorder, eax, 186, 92, 22, 003D0C0Ch, 00FF4444h, 1
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 193, OFFSET towerStartBtn, 00FFFFFFh
render_clear:
invoke DrawBoxBorder, 20, 76, 280, 74, 00082208h, 0033CC33h, 2
invoke DrawString8x8, 48, 90, OFFSET towerClearTitle, 0066FF66h
invoke DrawString8x8, 24, 112, OFFSET towerClearDesc, 00FFFFFFh
mov eax, cardX
add eax, 106
invoke DrawBoxBorder, eax, 186, 92, 22, 00220606h, 00882222h, 1
mov eax, cardX
add eax, 114
invoke DrawString8x8, eax, 193, OFFSET towerEscBtn, 00AAAAAAh
jmp render_tower_done
invoke DrawBoxBorder, 50, 174, 220, 26, 00114411h, 0044FF44h, 1
invoke DrawString8x8, 100, 183, OFFSET towerNextBtn, 00FFFFFFh
ret
render_card_clear:
mov eax, cardX
add eax, 8
mov ecx, cardW
sub ecx, 16
invoke DrawBoxBorder, eax, 74, ecx, 60, 00082208h, 0033CC33h, 2
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 86, OFFSET towerClearTitle, 0066FF66h
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 108, OFFSET towerClearDesc, 00FFFFFFh
render_defeat:
invoke DrawBoxBorder, 20, 76, 280, 74, 002A0404h, 00FF2222h, 2
invoke DrawString8x8, 60, 90, OFFSET towerDefeatTitle, 00FF4444h
invoke DrawString8x8, 36, 112, OFFSET towerDefeatDesc, 00FFFFFFh
mov eax, cardX
add eax, 18
mov ecx, cardW
sub ecx, 36
invoke DrawBoxBorder, eax, 186, ecx, 22, 00114411h, 0044FF44h, 1
mov eax, cardX
add eax, 42
invoke DrawString8x8, eax, 193, OFFSET towerNextBtn, 00FFFFFFh
jmp render_tower_done
invoke DrawBoxBorder, 32, 174, 256, 26, 00441010h, 00FF4444h, 1
invoke DrawString8x8, 48, 183, OFFSET towerReturnBtn, 00FFFFFFh
ret
render_card_defeat:
mov eax, cardX
add eax, 8
mov ecx, cardW
sub ecx, 16
invoke DrawBoxBorder, eax, 74, ecx, 60, 002A0404h, 00FF2222h, 2
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 86, OFFSET towerDefeatTitle, 00FF4444h
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 108, OFFSET towerDefeatDesc, 00FFFFFFh
render_champion:
invoke DrawBoxBorder, 18, 70, 284, 86, 00332200h, 00FFD700h, 2
invoke DrawString8x8, 40, 82, OFFSET towerChampTitle, 00FFE066h
invoke DrawString8x8, 56, 100, OFFSET towerChampSubtitle, 00FFFFFFh
invoke DrawString8x8, 36, 122, OFFSET towerPrizeText, 0066FF66h
mov eax, cardX
add eax, 16
mov ecx, cardW
sub ecx, 32
invoke DrawBoxBorder, eax, 186, ecx, 22, 00441010h, 00FF4444h, 1
mov eax, cardX
add eax, 22
invoke DrawString8x8, eax, 193, OFFSET towerReturnBtn, 00FFFFFFh
jmp render_tower_done
invoke DrawBoxBorder, 32, 174, 256, 26, 00443300h, 00FFCC00h, 1
invoke DrawString8x8, 48, 183, OFFSET towerReturnBtn, 00FFFFFFh
render_card_champion:
mov eax, cardX
add eax, 8
mov ecx, cardW
sub ecx, 16
invoke DrawBoxBorder, eax, 68, ecx, 72, 00332200h, 00FFD700h, 2
mov eax, cardX
add eax, 12
invoke DrawString8x8, eax, 78, OFFSET towerChampTitle, 00FFE066h
mov eax, cardX
add eax, 16
invoke DrawString8x8, eax, 96, OFFSET towerChampSubtitle, 00FFFFFFh
mov eax, cardX
add eax, 14
invoke DrawString8x8, eax, 116, OFFSET towerPrizeText, 0066FF66h
mov eax, cardX
add eax, 16
mov ecx, cardW
sub ecx, 32
invoke DrawBoxBorder, eax, 186, ecx, 22, 00443300h, 00FFCC00h, 1
mov eax, cardX
add eax, 22
invoke DrawString8x8, eax, 193, OFFSET towerReturnBtn, 00FFFFFFh
render_tower_done:
ret
RenderHellTower3D ENDP

View file

@ -10,6 +10,8 @@ InitHellTower3D PROTO
TowerExitToHall PROTO
TowerReportRoundOutcome PROTO :DWORD
RenderTowerInGameBadge PROTO
ResolveHellAssetPath PROTO :DWORD, :DWORD
FreeTowerMinigameAssets PROTO
.data
towerStage dd 1
@ -19,6 +21,16 @@ towerCurrentGame dd 0
towerPrizeAmount dd 66600
towerHudText db 91,32,0
hellTowerMenuArt3D AnimPack3D <0>
hellTowerMenuPath3D db "assets\animations\hall\helltower-menu.bin",0
towerFloorY dd 205, 186, 167, 148, 129, 110, 91, 72, 53, 34
towerCursorY dd 205
towerCursorTargetY dd 205
towerCalloutAnim dd 0
towerPrizePrompt db 202,211,216,58,32,36,0 ; "КУШ: $"
towerFloorModes dd MODE_COIN, MODE_DICE, MODE_SKULLS, MODE_BLACKJACK, MODE_POKER, MODE_RUSSIAN, MODE_ROULETTE, MODE_DURAK, MODE_SKILL, MODE_BUCKSHOT
towerBoss1 db 193,197,209,32,192,199,192,208,210,192,0

View file

@ -1,9 +1,73 @@
ResolveHellAssetPath PROC uses ebx esi edi, inPath:DWORD, outPathBuf:DWORD
cmp tournamentMode, 1
jne resolve_orig
mov esi, inPath
test esi, esi
jz resolve_orig
xor ecx, ecx
xor edx, edx
find_dot_loop:
mov al, byte ptr [esi+edx]
test al, al
jz check_dot
cmp al, '.'
jne next_char
mov ecx, edx
next_char:
inc edx
jmp find_dot_loop
check_dot:
test ecx, ecx
jz resolve_orig
mov edi, outPathBuf
xor edx, edx
copy_base:
cmp edx, ecx
jae append_suffix
mov al, byte ptr [esi+edx]
mov byte ptr [edi], al
inc edi
inc edx
jmp copy_base
append_suffix:
mov byte ptr [edi], '-'
mov byte ptr [edi+1], 'h'
mov byte ptr [edi+2], 'e'
mov byte ptr [edi+3], 'l'
mov byte ptr [edi+4], 'l'
add edi, 5
copy_ext:
mov al, byte ptr [esi+ecx]
mov byte ptr [edi], al
inc edi
inc ecx
test al, al
jnz copy_ext
invoke GetFileAttributesA, outPathBuf
cmp eax, 0FFFFFFFFh
je resolve_orig
mov eax, outPathBuf
ret
resolve_orig:
mov eax, inPath
ret
ResolveHellAssetPath ENDP
LoadAnimPack3D PROC uses ebx esi edi pack:DWORD,path:DWORD
local io:DWORD,heap:DWORD,allocation:DWORD
local resolvedPath[260]:BYTE
mov esi, pack
cmp [esi].AnimPack3D.pixels, 0
jne anim_load_success
invoke CreateFileA, path, 80000000h, 1, 0, 3, 80h, 0
invoke ResolveHellAssetPath, path, ADDR resolvedPath
invoke CreateFileA, eax, 80000000h, 1, 0, 3, 80h, 0
cmp eax, -1
je anim_load_failure
mov ebx, eax

View file

@ -0,0 +1,105 @@
# Copy all assets used by Hell Tower and its 10 games into assets\hell_tower
$ErrorActionPreference = "Stop"
$srcRoot = "assets"
$dstRoot = "assets\hell_tower"
if (-not (Test-Path $dstRoot)) {
New-Item -ItemType Directory -Path $dstRoot -Force | Out-Null
}
function Copy-AssetDir($relSrc, $relDst) {
$src = Join-Path $srcRoot $relSrc
$dst = Join-Path $dstRoot $relDst
if (-not (Test-Path $dst)) {
New-Item -ItemType Directory -Path $dst -Force | Out-Null
}
Write-Host "Copying directory $relSrc -> $relDst..."
Get-ChildItem -Path $src | ForEach-Object {
$destFile = Join-Path $dst $_.Name
Copy-Item -Path $_.FullName -Destination $destFile -Force
}
}
function Copy-AssetFile($relSrc, $relDst) {
$src = Join-Path $srcRoot $relSrc
$dst = Join-Path $dstRoot $relDst
$dstDir = Split-Path $dst -Parent
if (-not (Test-Path $dstDir)) {
New-Item -ItemType Directory -Path $dstDir -Force | Out-Null
}
Write-Host "Copying file $relSrc -> $relDst..."
Copy-Item -Path $src -Destination $dst -Force
}
# 1. Animations
Copy-AssetDir "animations\buckshot" "animations\buckshot"
Copy-AssetDir "animations\coin" "animations\coin"
Copy-AssetDir "animations\dice" "animations\dice"
Copy-AssetDir "animations\roulette" "animations\roulette"
Copy-AssetDir "animations\tables" "animations\tables"
Copy-AssetDir "animations\wheel" "animations\wheel"
Copy-AssetFile "animations\dbd_hook.ans" "animations\dbd_hook.ans"
# 2. Sprites
Copy-AssetDir "sprites\buckshot" "sprites\buckshot"
Copy-AssetDir "sprites\coin" "sprites\coin"
Copy-AssetDir "sprites\dice" "sprites\dice"
Copy-AssetDir "sprites\roulette" "sprites\roulette"
Copy-AssetDir "sprites\tables" "sprites\tables"
Copy-AssetDir "sprites\wheel" "sprites\wheel"
# 3. Music (Tower OST)
Copy-AssetDir "music\tower" "music\tower"
Copy-AssetDir "music\tower" "music"
# 4. Textures
Copy-AssetFile "textures\card-table.bin" "textures\card-table.bin"
Copy-AssetFile "textures\sprite_blackjack_table.png" "textures\sprite_blackjack_table.png"
Copy-AssetFile "textures\sprite_roulette_table.png" "textures\sprite_roulette_table.png"
Copy-AssetFile "textures\tex_wall_hell.png" "textures\tex_wall_hell.png"
# 5. Data
Copy-AssetFile "data\dealer_phrases.txt" "data\dealer_phrases.txt"
Copy-AssetFile "data\poker-ranks.bin" "data\poker-ranks.bin"
# 6. Audio
# Generated SFX
$genSfx = @(
"buckshot-pump.wav",
"dice-roll.wav",
"round-loss.wav",
"round-win.wav",
"skill-hit.wav",
"skill-miss.wav",
"skill-perfect.wav",
"ui-tick.wav"
)
foreach ($sfx in $genSfx) {
Copy-AssetFile "audio\generated\$sfx" "audio\generated\$sfx"
}
# ASCII root audio
$rootAudio = @(
"b05e1ac8be34248.wav",
"hKhoJArfEGAxxMewx0UQiddakTdBjFc.wav",
"moving-stone.wav",
"playing_cards_deck_flick_through_001_20485.wav",
"revolverdryfirec.wav",
"roulette-wheel-throw-2.wav",
"toy-gun-revolver-close-chamber-spin_f1ruc5nd.wav",
"victory-tf2.wav"
)
foreach ($aud in $rootAudio) {
Copy-AssetFile "audio\$aud" "audio\$aud"
}
# Russian audio files matching tower/game sound effects by exact size to avoid codepage issues
$dstAudioDir = Join-Path $dstRoot "audio"
$targetSizes = @(1466446, 228342, 212174, 757838)
Get-ChildItem "assets\audio\*.wav" | Where-Object { $targetSizes -contains $_.Length } | ForEach-Object {
Write-Host "Copying audio file $($_.Name) -> audio\$($_.Name)..."
Copy-Item -Path $_.FullName -Destination (Join-Path $dstAudioDir $_.Name) -Force
}
Write-Host "All Hell Tower assets successfully copied!"

View file

@ -0,0 +1,148 @@
"""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()

View file

@ -0,0 +1,78 @@
# Clean assets\hell_tower to contain ONLY PNG sprites for Hell Tower games
$ErrorActionPreference = "Stop"
$towerDir = "assets\hell_tower"
# 1. Clean out previous contents of assets\hell_tower
if (Test-Path $towerDir) {
Get-ChildItem -Path $towerDir | Remove-Item -Recurse -Force
}
New-Item -ItemType Directory -Path $towerDir -Force | Out-Null
# 2. Define game sprite mappings
$games = @{
"01_coin" = @(
"assets\sprites\coin\faces-v1.png",
"assets\sprites\coin\hands-v1.png",
"assets\sprites\coin\hands-v2.png",
"assets\sprites\coin\table-v1.png"
)
"02_dice" = @(
"assets\sprites\dice\hands-v1.png",
"assets\sprites\dice\hands-v2.png"
)
"04_05_08_card_tables" = @(
"assets\sprites\tables\blackjack-guests-v1.png",
"assets\sprites\tables\blackjack-room-v1.png",
"assets\sprites\tables\card-table-active-v2.png",
"assets\sprites\tables\card-table-pixel-v3.png",
"assets\sprites\tables\card-table-v1.png",
"assets\textures\sprite_blackjack_table.png"
)
"06_russian_roulette" = @(
"assets\sprites\roulette\gman-talk-v1.png",
"assets\sprites\roulette\hands-v5.png",
"assets\sprites\roulette\participants-v1.png",
"assets\sprites\roulette\revolver-actions-v1.png"
)
"07_casino_roulette" = @(
"assets\sprites\wheel\closeup-v1.png",
"assets\sprites\wheel\table-v1.png",
"assets\textures\sprite_roulette_table.png"
)
"10_buckshot" = @(
"assets\sprites\buckshot\blood-v3.png",
"assets\sprites\buckshot\dealer-actions-v3.png",
"assets\sprites\buckshot\dealer-source-v2.png",
"assets\sprites\buckshot\death-v3.png",
"assets\sprites\buckshot\items-v2.png",
"assets\sprites\buckshot\loading-v2.png",
"assets\sprites\buckshot\medical-dealer-v5.png",
"assets\sprites\buckshot\medical-player-v5.png",
"assets\sprites\buckshot\medical-v3.png",
"assets\sprites\buckshot\props-v2.png",
"assets\sprites\buckshot\room-v2.png",
"assets\sprites\buckshot\self-flash-v4.png",
"assets\sprites\buckshot\self-v3.png",
"assets\sprites\buckshot\shots-v2.png"
)
"environment" = @(
"assets\textures\tex_wall_hell.png"
)
}
foreach ($folder in $games.Keys) {
$targetFolder = Join-Path $towerDir $folder
New-Item -ItemType Directory -Path $targetFolder -Force | Out-Null
foreach ($file in $games[$folder]) {
if (Test-Path $file) {
Copy-Item -Path $file -Destination $targetFolder -Force
Write-Host "Copied $file -> $folder"
} else {
Write-Warning "File not found: $file"
}
}
}
Write-Host "Setup finished successfully!"

View file

@ -0,0 +1,93 @@
"""Validation test suite for Hell Tower custom assets and redesigned menu."""
import os
import struct
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_asset_packs():
print("[1] Verifying Hell Tower binary packs...")
anm1_packs = [
(ROOT / "assets/animations/hall/helltower-menu.bin", 640, 480, 1, 1228800),
(ROOT / "assets/animations/coin/table-v1-hell.bin", 640, 480, 1, 1228800),
(ROOT / "assets/animations/wheel/closeup-v1-hell.bin", 640, 480, 1, 1228800),
(ROOT / "assets/animations/wheel/table-v1-hell.bin", 640, 480, 1, 1228800),
(ROOT / "assets/animations/buckshot/room-v2-hell.bin", 640, 480, 1, 1228800),
(ROOT / "assets/animations/buckshot/dealer-v2-hell.bin", 256, 224, 16, 16 * 256 * 224 * 4),
(ROOT / "assets/animations/buckshot/dealer-actions-v3-hell.bin", 256, 224, 16, 16 * 256 * 224 * 4),
(ROOT / "assets/animations/buckshot/death-v3-hell.bin", 256, 224, 12, 12 * 256 * 224 * 4),
]
for path, exp_w, exp_h, exp_frames, exp_bytes in anm1_packs:
assert path.exists(), f"Missing pack: {path}"
data = path.read_bytes()
assert len(data) >= 20, f"File too small: {path}"
magic, w, h, frames, byte_count = struct.unpack("<4sIIII", data[:20])
assert magic == b"ANM1", f"Bad magic {magic} for {path}"
assert w == exp_w, f"Width mismatch: expected {exp_w}, got {w} for {path}"
assert h == exp_h, f"Height mismatch: expected {exp_h}, got {h} for {path}"
assert frames == exp_frames, f"Frames mismatch: expected {exp_frames}, got {frames} for {path}"
assert byte_count == exp_bytes, f"Bytes mismatch: expected {exp_bytes}, got {byte_count} for {path}"
assert len(data) == 20 + exp_bytes, f"File size mismatch: {len(data)} != {20 + exp_bytes}"
print(f" [OK] ANM1 {path.name}: {w}x{h}, {frames} frames, {len(data)} bytes")
# CTA1 card table pack
card_table = ROOT / "assets/textures/card-table-hell.bin"
assert card_table.exists(), "Missing card-table-hell.bin"
c_data = card_table.read_bytes()
c_magic, c_w, c_h, c_bytes = struct.unpack("<4sIII", c_data[:16])
assert c_magic == b"CTA1", f"Bad CTA1 magic: {c_magic}"
assert c_w == 640 and c_h == 480, f"Bad size: {c_w}x{c_h}"
assert c_bytes == 1228800, f"Bad bytes: {c_bytes}"
assert len(c_data) == 16 + 1228800
print(f" [OK] CTA1 card-table-hell.bin: {c_w}x{c_h}, {len(c_data)} bytes")
def test_code_integrations():
print("[2] Verifying ASM code integrations...")
# Check hell_tower_data.inc
ht_data = (ROOT / "src_graphics/hell_tower_data.inc").read_text(encoding="cp1251")
assert "hellTowerMenuArt3D" in ht_data
assert "hellTowerMenuPath3D" in ht_data
assert "towerCursorY" in ht_data
assert "towerCursorTargetY" in ht_data
assert "towerCalloutAnim" in ht_data
assert "ResolveHellAssetPath PROTO" in ht_data
assert "FreeTowerMinigameAssets PROTO" in ht_data
print(" [OK] hell_tower_data.inc verified.")
# Check scene_animation.inc
scene_anim = (ROOT / "src_graphics/scene_animation.inc").read_text(encoding="cp1251")
assert "ResolveHellAssetPath PROC" in scene_anim
assert "invoke ResolveHellAssetPath" in scene_anim
print(" [OK] scene_animation.inc verified.")
# Check card_table_assets.inc
card_assets = (ROOT / "src_graphics/card_table_assets.inc").read_text(encoding="cp1251")
assert "invoke ResolveHellAssetPath" in card_assets
print(" [OK] card_table_assets.inc verified.")
# Check hell_tower.inc
ht_code = (ROOT / "src_graphics/hell_tower.inc").read_text(encoding="cp1251")
assert "FreeTowerMinigameAssets PROC" in ht_code
assert "call FreeTowerMinigameAssets" in ht_code
assert "hellTowerMenuArt3D.pixels" in ht_code
assert "towerCursorTargetY" in ht_code
assert "towerCalloutAnim" in ht_code
print(" [OK] hell_tower.inc verified.")
def test_executable():
print("[3] Verifying poker.exe build...")
exe = ROOT / "poker.exe"
assert exe.exists(), "poker.exe does not exist"
size = exe.stat().st_size
print(f" [OK] poker.exe verified ({size} bytes)")
if __name__ == "__main__":
test_asset_packs()
test_code_integrations()
test_executable()
print("\nALL HELL TOWER CUSTOM ASSET AND MENU CHECKS PASSED SUCCESSFULLY!")

View file

@ -0,0 +1,115 @@
"""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()

View file

@ -0,0 +1,45 @@
"""Automated smoke test for Hell Tower 10-floor run-through."""
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
POKER_ASM = ROOT / "poker.asm"
def main():
print("[1/4] Preparing test harness in poker.asm...")
code = POKER_ASM.read_text(encoding="cp1251")
target = "\nIFDEF CASINO_ACTION_TEST\n"
if "CASINO_ACTION_TEST equ 1\n" not in code:
code = code.replace(target, "\nCASINO_ACTION_TEST equ 1\nIFDEF CASINO_ACTION_TEST\n")
POKER_ASM.write_text(code, encoding="cp1251")
try:
print("[2/4] Building poker.exe with test harness...")
build_res = subprocess.run(["cmd.exe", "/c", "build.bat"], cwd=ROOT, capture_output=True, text=True)
if build_res.returncode != 0:
print("[FAIL] Build failed:", build_res.stdout, build_res.stderr)
return 1
print("[3/4] Running Hell Tower 10-floor run-through...")
run_res = subprocess.run([str(ROOT / "poker.exe")], cwd=ROOT, capture_output=True, timeout=15)
print(f"Hell Tower test completed with exit code: {run_res.returncode}")
if run_res.returncode != 0:
print(f"[FAIL] Hell Tower run-through failed with code {run_res.returncode}")
return run_res.returncode
print("[SUCCESS] All 10 floors of Hell Tower completed without errors or crashes!")
finally:
print("[4/4] Restoring normal poker.asm and rebuilding...")
restored = POKER_ASM.read_text(encoding="cp1251")
if "CASINO_ACTION_TEST equ 1\n" in restored:
restored = restored.replace("CASINO_ACTION_TEST equ 1\n", "")
POKER_ASM.write_text(restored, encoding="cp1251")
subprocess.run(["cmd.exe", "/c", "build.bat"], cwd=ROOT, capture_output=True, text=True)
print("[OK] Production poker.exe rebuilt cleanly.")
return 0
if __name__ == "__main__":
sys.exit(main())