Compare commits

..

11 commits

Author SHA1 Message Date
eef2039c7e 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
2026-10-26 19:44:06 +03:00
75d594ec79 Implement 3D positional audio across floors and popup volume HUD for Jukebox 2026-10-25 18:27:51 +03:00
d4436674b4 Configure Jukebox to exclusively play tracks from assets/music/jackbox 2026-10-25 18:03:39 +03:00
4c2428c367 feat(jackbox): use terminal wooden cabinet side panels (material 121) for Jukebox flanks 2026-10-25 17:21:40 +03:00
7c262a15ad fix(jackbox): realign UI strictly within CRT monitor sprite and clear wall painting 2026-10-25 17:07:05 +03:00
6bf2a2885c fix(jukebox): make cabinet solid rectangular with zero gaps, relocate to East wall beside billiards 2026-10-25 16:43:43 +03:00
0a85e271e9 fix(jukebox): remove studio grey background, fix 3D texture orientation and cabinet rendering 2026-10-25 16:30:40 +03:00
82f3aa9437 feat(audio): implement interactive 3D jukebox (jackbox) and background music playback 2026-10-25 15:15:02 +03:00
5aec3ded9f feat(fortune): add download_model.py utility with resume and hash validation 2026-10-24 23:01:46 +03:00
2ab7c9ed40 Merge branch 'main' of https://git.silverstone.crazedns.ru/Billi_Bob/KAZIK
# Conflicts:
#	build/graphics/poker.exe
#	poker.exe
#	poker.obj
2026-10-24 23:01:08 +03:00
4ca7912562 fix(build): add fallback detection for MSVC BuildTools alongside VS Community in build.bat 2026-10-21 17:25:12 +03:00
85 changed files with 2970 additions and 115 deletions

2
.gitignore vendored
View file

@ -48,3 +48,5 @@ __pycache__/
/debug_render.png
/render_view.png
/docs/*.zip
/scratch/

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Binary file not shown.

View file

@ -15,6 +15,18 @@ if exist "%VS_COMM_BIN%\ml.exe" (
goto do_build
)
rem Check Visual Studio BuildTools
set "MSVC_BIN=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\bin\Hostx64\x86"
set "VC_LIB=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\lib\x86"
if exist "%MSVC_BIN%\ml.exe" (
set "ML_EXE=%MSVC_BIN%\ml.exe"
set "LINK_EXE=%MSVC_BIN%\link.exe"
set LIB_PATHS=/LIBPATH:"%SDK_LIB%" /LIBPATH:"%UCRT_LIB%" /LIBPATH:"%VC_LIB%"
set "EXTRA_FLAGS=/NOLOGO /SUBSYSTEM:WINDOWS"
goto do_build
)
rem Check standard MASM32 paths
if exist "F:\MASM32\masm32\bin\ml.exe" (
set "ML_EXE=F:\MASM32\masm32\bin\ml.exe"

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "C3D857FDBF7597C976B16C5021D7F07951628E54F86FF2B7BB53F8A83FDE11CA";
const string ExpectedPokerSha256 = "07D474A0223390E7AC4F4F779818D77CC2DAE5123F4BDB0F72DD975473267310";
[STAThread]
static void Main()

Binary file not shown.

View file

@ -172,7 +172,8 @@ include src_graphics\scratch_data.inc
include src_graphics\hell_tower_data.inc
include src_graphics\charms_data.inc
include src_graphics\observer_data.inc
include src_graphics\darts_data.inc
include src_graphics\darts_data.inc
include src_graphics\jackbox_data.inc
.data
ALIGN 4
@ -5197,7 +5198,8 @@ include src_graphics\fortune_dialog.inc
include src_graphics\hell_tower.inc
include src_graphics\charms.inc
include src_graphics\observer.inc
include src_graphics\darts.inc
include src_graphics\darts.inc
include src_graphics\jackbox.inc
; --- Main Loop of the 3D Casino Engine ---
StartHellTower3D PROC uses ebx esi edi
@ -5344,6 +5346,7 @@ check_walk_mode:
call UpdateCamera3D
call CheckInteractions
call HandlePlayerInput
call HandleJukeboxKeys3D
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
call UpdateCamera3D
@ -5356,6 +5359,14 @@ check_walk_mode:
call RenderMinimap
jmp present_frame
check_slots_mode:
cmp engineMode,MODE_JACKBOX
jne check_jackbox_done
call HandleJackbox3D
cmp engineMode,MODE_WALK
je check_walk_mode
call RenderJackbox3D
jmp present_frame
check_jackbox_done:
cmp engineMode,MODE_DARTS
jne check_darts_done
call HandleDarts3D
@ -5423,6 +5434,7 @@ check_darts_done:
cmp engineMode, MODE_STAIRS
jne @F
call HandleStairs3D
call HandleJukeboxKeys3D
call RenderStairs3D
jmp present_frame
@@:
@ -5584,7 +5596,9 @@ pause_low_spec_saved:
call SaveProgress3D
jmp engine_shutdown
present_frame:
call UpdateJukeboxAudio3D
call RenderTowerInGameBadge
call RenderJukeboxVolumeHUD
mov eax, engineMode
cmp eax, lastPersistMode3D
je @F

Binary file not shown.

47
docs/JACKBOX.md Normal file
View file

@ -0,0 +1,47 @@
# Музыкальный автомат (Jukebox / Jackbox) в казино
## Описание
В зале казино (зона бара и лаунжа, координаты `(6.0, 20.0)`) установлен интерактивный ретро-автомат для выбора и воспроизведения фоновой музыки.
## Возможности
1. **Фоновое воспроизведение (Windows MCI)**:
- Музыка воспроизводится асинхронно в фоне через нативный API `winmm.dll` (`mciSendStringA`).
- Музыка **продолжает играть** при выходе из меню автомата обратно в 3D-зал, во время ходьбы, игры за столами (покер, блэкджек, рулетка, дурак, слоты, дартс и др.).
- При завершении трека он автоматически зацикливается (`repeat`).
2. **Интерактивный экран (`MODE_JACKBOX = 28`)**:
- Отрисовывается на базе текстуры `assets/animations/hall/jackbox-ui-v1.bin` (640×480 BGRA).
- Отображает текущий статус («СЕЙЧАС ИГРАЕТ: <Трек>» или «МУЗЫКА ВЫКЛЮЧЕНА»).
- Список из 10 позиций с подсветкой текущего выбора `> ` и индикатором `[ИГРАЕТ]`.
3. **Управление**:
- `Стрелки Вверх / Вниз` или `W / S`: выбор трека в списке.
- `Enter` / `Пробел`: включить выбранный трек.
- `1`–`9`: мгновенный запуск соответствующего трека по номеру.
- `0`: выключить музыку (тишина).
- `S`: остановить воспроизведение.
- `R`: воспроизвести случайный трек.
- `Esc` / `E` / Клик по кнопке «ВЫХОД»: вернуться в 3D-зал.
- `Клик мыши` по строке трека: мгновенный выбор и запуск.
4. **Физическая 3D-модель в зале**:
- Габариты: ширина 0.7м, глубина 0.45м, высота 1.85м.
- Материал 161 (фасад `Jackbox_front.jpg`), материал 162 (боковины `Jackbox_side.jpg`), верхняя крышка (110).
- Коллизия: игрок не может пройти сквозь автомат (`JackboxCanWalk3D`).
## Плейлист
| № | Исполнитель и трек | Формат |
|---|---|---|
| 1 | Keygen Church — Tenebre Rosso Sangue (ULTRAKILL OST) | MP3 |
| 2 | Keygen Church — Hareklavit | MP3 |
| 3 | Keygen Church — Ladikerfos | MP3 |
| 4 | Keygen Church — Tilaskarot | MP3 |
| 5 | Mick Gordon — BFG Division (DOOM) | MP3 |
| 6 | Mick Gordon — Rip & Tear (DOOM) | MP3 |
| 7 | Mick Gordon — The Only Thing They Fear Is You (DOOM Eternal) | MP3 |
| 8 | Hakari Dance (Jackpot Win Theme) | WAV |
| 9 | Team Fortress 2 — Victory Anthem | WAV |
| 0 | *[ ВЫКЛЮЧИТЬ МУЗЫКУ / ТИШИНА ]* | — |
## Файлы реализации
- `src_graphics/jackbox_data.inc`: структуры, таблицы команд MCI, пути, CP1251 строки.
- `src_graphics/jackbox.inc`: загрузка ассетов, 3D-кабинет, коллизии, аудио-контроллер, ввод и рендеринг GUI.
- `tools/generators/pack_jackbox.py`: оффлайн-компилятор текстур из `assets/sprites/jackbox/` в бинарные контейнеры `ANM1`.
- `tools/tests/check_jackbox.py`: тестовый набор верификации ассетов, аудио-файлов и бинарника.

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

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

@ -44,5 +44,6 @@ close_legacy_sound:
cmp esi, legacySoundCount3D
jb close_legacy_sound
call StopTowerMusic
call StopJukeboxTrack3D
ret
CloseLegacySounds3D ENDP

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

@ -5,6 +5,13 @@ cityRayY3D dd SCREEN_W dup(?)
; Exterior-only materials do not consume or overwrite the hall's texture slots.
ResolveCitySurface3D PROC material:DWORD
mov eax,material
cmp eax,161
jb @F
cmp eax,163
ja @F
invoke ResolveJackboxSurface3D,material
ret
@@:
cmp eax,160
jne @F
call ResolveDartsSurface3D

View file

@ -782,6 +782,7 @@ CheckHallInteractions3D PROC
invoke HallTarget3D, playerX,23*FP_1+FP_HALF,INTERACT_BAR,OFFSET hallBarPrompt3D
hall_check_other:
invoke HallTarget3D,FP_1*305/10,28*FP_1,INTERACT_DARTS,OFFSET dtPrompt
invoke HallTarget3D,JACKBOX_X,JACKBOX_Y,INTERACT_JACKBOX,OFFSET jackboxPrompt3D
invoke HallTarget3D,16*FP_1,FP_1*73/10,INTERACT_CABARET_SEAT,OFFSET cabaretSeat3D
invoke HallTarget3D,scrStation3D.posX,scrStation3D.posY,INTERACT_SCRATCH,OFFSET scratchHall3D
invoke HallTarget3D,FP_1*285/10,FP_1*105/10,INTERACT_BASEMENT,OFFSET hallBunkerPrompt3D
@ -798,6 +799,12 @@ hall_check_bunker:
CheckHallInteractions3D ENDP
TriggerHallInteraction3D PROC
cmp interactTargetId,INTERACT_JACKBOX
jne @F
call BeginJackbox3D
mov eax,1
ret
@@:
cmp interactTargetId,INTERACT_DARTS
jne hall_not_darts
call BeginDarts3D
@ -1119,6 +1126,9 @@ fortune_collision_stairs:
invoke TerminalCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
invoke JackboxCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
xor esi, esi
hall_collision_decor:
imul ebx, esi, SIZEOF CityProp3D

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

885
src_graphics/jackbox.inc Normal file
View file

@ -0,0 +1,885 @@
; ==============================================================================
; JACKBOX / JUKEBOX IMPLEMENTATION - MASM32 / CP1251
; ==============================================================================
.code
LoadJackboxArt3D PROC
invoke LoadAnimPack3D, OFFSET jackboxWorldArt3D, OFFSET jackboxWorldPath3D
invoke ValidateCityPack3D, OFFSET jackboxWorldArt3D, 256, 512, 3
invoke LoadAnimPack3D, OFFSET jackboxScreenArt3D, OFFSET jackboxScreenPath3D
invoke ValidateCityPack3D, OFFSET jackboxScreenArt3D, 640, 480, 1
ret
LoadJackboxArt3D ENDP
FreeJackboxArt3D PROC
invoke FreeAnimPack3D, OFFSET jackboxWorldArt3D
invoke FreeAnimPack3D, OFFSET jackboxScreenArt3D
ret
FreeJackboxArt3D ENDP
ResolveJackboxSurface3D PROC material:DWORD
cmp jackboxWorldArt3D.pixels, 0
je jackbox_surface_missing
mov eax, material
cmp eax, MATERIAL_JACKBOX_FRONT
je jackbox_surface_front
cmp eax, MATERIAL_JACKBOX_SIDE
je jackbox_surface_side
cmp eax, MATERIAL_JACKBOX_BACK
jne jackbox_surface_missing
; Frame 2: rear panel (offset 256*512*4*2 = 1048576)
mov eax, jackboxWorldArt3D.pixels
add eax, 1048576
mov ecx, 256
mov edx, 512
ret
jackbox_surface_side:
; Frame 1: side profile (offset 256*512*4 = 524288)
mov eax, jackboxWorldArt3D.pixels
add eax, 524288
mov ecx, 256
mov edx, 512
ret
jackbox_surface_front:
; Frame 0: front face
mov eax, jackboxWorldArt3D.pixels
mov ecx, 256
mov edx, 512
ret
jackbox_surface_missing:
xor eax, eax
ret
ResolveJackboxSurface3D ENDP
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
; Solid rectangular cabinet against East wall beside billiards:
; Depth along X: 0.55m (x0 = 30.40m, x1 = 30.95m against wall at 31.0m)
; Width along Y: 0.85m (y0 = 23.075m, y1 = 23.925m, centered at Y = 23.5m)
; Leaves painting 116 at Y = [25.0 .. 26.5] completely visible and unblocked!
; Height Z: 1.85m
mov x0, FP_1*304/10
mov x1, FP_1*3095/100
mov y0, FP_1*235/10 - FP_1*425/1000
mov y1, FP_1*235/10 + FP_1*425/1000
; 1. Back Face (against East wall: X = x1)
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
; 2. North Side (Y = y0) - Authentic wooden terminal cabinet panel with brass trim
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
; 3. South Side (Y = y1) - Authentic wooden terminal cabinet panel with brass trim
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
; 4. Front Face (facing West towards billiards: X = x0)
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
; 5. Top Lid (horizontal cap at z = 1.85m)
invoke DrawCityHorizontal3D, x0, x1, y0, y1, FP_1*37/20, 110
ret
DrawJackboxCabinet3D ENDP
RenderJackboxCabinet3D PROC
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
ret
RenderJackboxCabinet3D ENDP
JackboxCanWalk3D PROC px:DWORD, py:DWORD
mov eax, px
cmp eax, FP_1*303/10
jb jackbox_walk_clear
cmp eax, 31*FP_1+9830
ja jackbox_walk_clear
mov eax, py
sub eax, FP_1*235/10
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_1*55/100
ja jackbox_walk_clear
xor eax, eax
ret
jackbox_walk_clear:
mov eax, 1
ret
JackboxCanWalk3D ENDP
PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0
mov jboxIsPlaying, 0
mov ebx, trackIndex
cmp ebx, 2
jae jbox_play_stop_selected
cmp ebx, JACKBOX_TRACK_COUNT
jae jbox_play_stop_selected
mov eax, jboxOpenCommands[ebx*4]
test eax, eax
jz jbox_play_stop_selected
invoke mciSendStringA, eax, 0, 0, 0
invoke mciSendStringA, OFFSET jboxPlayCmd, 0, 0, 0
mov jboxCurrentTrack, ebx
mov jboxIsPlaying, 1
mov jboxEffectiveVolume, -1
call UpdateJukeboxAudio3D
ret
jbox_play_stop_selected:
mov jboxCurrentTrack, -1
call UpdateJukeboxAudio3D
ret
PlayJukeboxTrack3D ENDP
StopJukeboxTrack3D PROC
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0
mov jboxIsPlaying, 0
mov jboxCurrentTrack, -1
call UpdateJukeboxAudio3D
ret
StopJukeboxTrack3D ENDP
BeginJackbox3D PROC
mov engineMode, MODE_JACKBOX
call ClearKeys3D
mov mouseClick3D, 0
ret
BeginJackbox3D ENDP
HandleJackbox3D PROC uses ebx esi
; 1. Mouse interactions
cmp mouseClick3D, 0
je jbox_keys
; Exit via Top-Right LCD slot: x in [232..284], y in [12..26]
cmp mouseY3D, 12
jb @F
cmp mouseY3D, 26
ja @F
cmp mouseX3D, 232
jb @F
cmp mouseX3D, 284
jbe jbox_exit
@@:
; Exit via Bottom Red CANCEL button: x in [224..256], y in [195..225]
cmp mouseY3D, 195
jb @F
cmp mouseY3D, 225
ja @F
cmp mouseX3D, 224
jb @F
cmp mouseX3D, 256
jbe jbox_exit
@@:
; Bottom Green START button: x in [154..188], y in [195..225] -> Play selected
cmp mouseY3D, 195
jb @F
cmp mouseY3D, 225
ja @F
cmp mouseX3D, 154
jb @F
cmp mouseX3D, 188
jbe jbox_play_selected
@@:
; Bottom Orange SELECT button: x in [190..222], y in [195..225] -> Next track & play
cmp mouseY3D, 195
jb @F
cmp mouseY3D, 225
ja @F
cmp mouseX3D, 190
jb @F
cmp mouseX3D, 222
ja @F
inc jboxSelectedTrack
cmp jboxSelectedTrack, JACKBOX_TRACK_COUNT
jb jbox_play_selected
mov jboxSelectedTrack, 0
jmp jbox_play_selected
@@:
; Bottom Left Arrow: x in [256..274], y in [204..224] -> Prev track
cmp mouseY3D, 204
jb @F
cmp mouseY3D, 224
ja @F
cmp mouseX3D, 256
jb @F
cmp mouseX3D, 274
ja @F
dec jboxSelectedTrack
jns jbox_play_selected
mov jboxSelectedTrack, JACKBOX_TRACK_COUNT - 1
jmp jbox_play_selected
@@:
; Bottom Right Arrow: x in [276..294], y in [204..224] -> Next track
cmp mouseY3D, 204
jb @F
cmp mouseY3D, 224
ja @F
cmp mouseX3D, 276
jb @F
cmp mouseX3D, 294
ja @F
inc jboxSelectedTrack
cmp jboxSelectedTrack, JACKBOX_TRACK_COUNT
jb jbox_play_selected
mov jboxSelectedTrack, 0
jmp jbox_play_selected
@@:
; Track list click: x in [46..274], y in [66..108], step = 14
cmp mouseY3D, 66
jb jbox_keys
cmp mouseY3D, 108
ja jbox_keys
cmp mouseX3D, 46
jb jbox_keys
cmp mouseX3D, 274
ja jbox_keys
mov eax, mouseY3D
sub eax, 66
xor edx, edx
mov ebx, 14
div ebx
cmp eax, JACKBOX_TRACK_COUNT
jae jbox_keys
mov jboxSelectedTrack, eax
invoke PlayJukeboxTrack3D, eax
jmp jbox_handled
jbox_keys:
; Exit keys: ESC (1Bh) or 'E' (45h)
invoke Pressed3D, 1Bh
test eax, eax
jnz jbox_exit
invoke Pressed3D, 45h
test eax, eax
jnz jbox_exit
; Navigation: UP (26h) / DOWN (28h)
invoke Pressed3D, 26h
test eax, eax
jz @F
dec jboxSelectedTrack
jns jbox_handled
mov jboxSelectedTrack, JACKBOX_TRACK_COUNT - 1
jmp jbox_handled
@@:
invoke Pressed3D, 28h
test eax, eax
jz @F
inc jboxSelectedTrack
cmp jboxSelectedTrack, JACKBOX_TRACK_COUNT
jb jbox_handled
mov jboxSelectedTrack, 0
jmp jbox_handled
@@:
; Play selection: ENTER (0Dh) or SPACE (20h)
invoke Pressed3D, 0Dh
test eax, eax
jnz jbox_play_selected
invoke Pressed3D, 20h
test eax, eax
jnz jbox_play_selected
; Stop key: 'S' (53h)
invoke Pressed3D, 53h
test eax, eax
jz @F
call StopJukeboxTrack3D
jmp jbox_handled
@@:
; Random key: 'R' (52h)
invoke Pressed3D, 52h
test eax, eax
jz @F
invoke RandomRange3D, 2
mov jboxSelectedTrack, eax
invoke PlayJukeboxTrack3D, eax
jmp jbox_handled
@@:
; Direct digit keys '1'..'2'
mov ebx, 31h ; '1'
jbox_digit_loop:
invoke Pressed3D, ebx
test eax, eax
jz jbox_digit_next
mov eax, ebx
sub eax, 31h
mov jboxSelectedTrack, eax
invoke PlayJukeboxTrack3D, eax
jmp jbox_handled
jbox_digit_next:
inc ebx
cmp ebx, 32h ; '2'
jbe jbox_digit_loop
; '0' key for mute/stop
invoke Pressed3D, 30h
test eax, eax
jz jbox_handled
mov jboxSelectedTrack, 2
invoke PlayJukeboxTrack3D, 2
jmp jbox_handled
jbox_play_selected:
invoke PlayJukeboxTrack3D, jboxSelectedTrack
jmp jbox_handled
jbox_exit:
mov engineMode, MODE_WALK
call ClearKeys3D
jbox_handled:
mov mouseClick3D, 0
ret
HandleJackbox3D ENDP
RenderJackbox3D PROC uses ebx esi edi
local rowY:DWORD, color:DWORD
; 1. Draw base background
invoke DrawBox, 0, 0, 320, 240, 000B1815h
; 2. Overlay screen artwork (640x480 physical buffer)
cmp jackboxScreenArt3D.pixels, 0
je @F
invoke DrawAnimSprite3D, OFFSET jackboxScreenArt3D, 0, 0, 0, 640, 480
@@:
; 3. Top Digital LCD Slots
; Left slot: status text
cmp jboxIsPlaying, 1
jne jbox_top_lcd_stopped
cmp jboxCurrentTrack, -1
je jbox_top_lcd_stopped
cmp jboxCurrentTrack, 2
jae jbox_top_lcd_stopped
invoke DrawString8x8, 42, 17, OFFSET jboxLcdPlay, 0050FF90h
jmp jbox_top_lcd_exit
jbox_top_lcd_stopped:
invoke DrawString8x8, 45, 17, OFFSET jboxLcdStop, 008CB8A5h
jbox_top_lcd_exit:
; Right slot: exit button text
invoke DrawString8x8, 241, 17, OFFSET jboxLcdExit, 00F2A648h
; 4. CRT Monitor Header: Now Playing Status
cmp jboxIsPlaying, 1
jne jbox_render_stopped
cmp jboxCurrentTrack, -1
je jbox_render_stopped
cmp jboxCurrentTrack, 2
jae jbox_render_stopped
invoke DrawString8x8, 48, 52, OFFSET jboxHdrNow, 0050FF90h
mov eax, jboxCurrentTrack
mov edx, jboxTrackNames[eax*4]
invoke DrawString8x8, 108, 52, edx, 00F0E6C3h
jmp jbox_render_divider
jbox_render_stopped:
invoke DrawString8x8, 48, 52, OFFSET jboxHdrStop, 008CB8A5h
jbox_render_divider:
invoke DrawBox, 46, 62, 228, 1, 001C3828h
; 5. Track Selection List (3 rows, step Y = 14, from Y = 70)
mov rowY, 70
xor esi, esi
jbox_render_row:
mov color, 008CB8A5h ; Default CRT phosphor pale cyan
cmp esi, jboxSelectedTrack
jne @F
; Selected row: highlight box, golden cursor, bright white text
invoke DrawBox, 46, rowY, 228, 10, 00143526h
invoke DrawString8x8, 48, rowY, OFFSET jboxCursor, 00FFD700h
mov color, 00FFFFFFh
@@:
mov edx, jboxTrackNames[esi*4]
invoke DrawString8x8, 58, rowY, edx, color
; Mark playing track with [>] tag
cmp esi, jboxCurrentTrack
jne jbox_row_next
cmp jboxIsPlaying, 1
jne jbox_row_next
invoke DrawString8x8, 238, rowY, OFFSET jboxTagPlay, 0050FF90h
jbox_row_next:
add rowY, 14
inc esi
cmp esi, JACKBOX_TRACK_COUNT
jb jbox_render_row
; 6. Footer Controls Help (strictly inside CRT)
invoke DrawBox, 46, 166, 228, 1, 001C3828h
invoke DrawString8x8, 56, 170, OFFSET jboxFooterHelp, 0070AA90h
ret
RenderJackbox3D ENDP
; ==============================================================================
; 3D SOUND PROPAGATION & MULTI-FLOOR ATTENUATION
; ==============================================================================
CalculateJukeboxFalloff PROC uses ebx esi edi
local dxDist:DWORD, dyDist:DWORD, distSq:DWORD, distVal:DWORD
local factor:DWORD
; If music is not playing, factor = 0
cmp jboxIsPlaying, 1
jne falloff_silent
cmp jboxCurrentTrack, -1
je falloff_silent
cmp jboxCurrentTrack, 2
jae falloff_silent
; Check if on stairs (MODE_STAIRS)
cmp engineMode, MODE_STAIRS
je falloff_stairs
; Check floor level
mov eax, hallLevel3D
cmp eax, 0
je falloff_ground
cmp eax, 1
je falloff_bunker
cmp eax, 2
je falloff_upper
cmp eax, 3
je falloff_fortune
jmp falloff_silent
falloff_ground:
; Level 0 (Ground Hall) - standard quadratic 2D distance falloff from Jackbox
; Jackbox: JACKBOX_X = 30.6m, JACKBOX_Y = 23.5m
mov eax, playerX
sub eax, JACKBOX_X
test eax, eax
jns @F
neg eax
@@:
shr eax, 16 ; integer meters
mov dxDist, eax
mov eax, playerY
sub eax, JACKBOX_Y
test eax, eax
jns @F
neg eax
@@:
shr eax, 16 ; integer meters
mov dyDist, eax
; distSq = dx*dx + dy*dy
mov eax, dxDist
imul eax, eax
mov ebx, dyDist
imul ebx, ebx
add eax, ebx
mov distSq, eax
; distVal = sqrt(distSq)
fild distSq
fsqrt
fistp distVal
; Acoustic curve:
; dist <= 2m -> factor = 1000
; dist >= 35m -> factor = 0
; in between: rem = 35 - dist; factor = 1000 * rem * rem / 1089
mov eax, distVal
cmp eax, 2
jbe falloff_full
cmp eax, 35
jae falloff_silent
mov ecx, 35
sub ecx, eax ; ecx = 35 - distVal (in 1..32)
imul eax, ecx, 1000
imul eax, ecx ; eax = 1000 * rem * rem
xor edx, edx
mov ebx, 1089 ; 33 * 33
div ebx
mov factor, eax
jmp falloff_done
falloff_full:
mov factor, 1000
jmp falloff_done
falloff_upper:
; Level 2 (Upper Lounge) - sound travels up the open stairs (stairwell at X=16, Y=21 on upper map)
; Base attenuation through floor + distance from stairwell opening
mov eax, playerX
sub eax, 16*FP_1
test eax, eax
jns @F
neg eax
@@:
shr eax, 16
mov dxDist, eax
mov eax, playerY
sub eax, 21*FP_1
test eax, eax
jns @F
neg eax
@@:
shr eax, 16
mov dyDist, eax
mov eax, dxDist
imul eax, eax
mov ebx, dyDist
imul ebx, ebx
add eax, ebx
mov distSq, eax
fild distSq
fsqrt
fistp distVal
; At stair opening: factor = 420 (muffled from below)
; Drops by 18 per meter, min 90
mov eax, distVal
imul eax, 18
mov ecx, 420
sub ecx, eax
cmp ecx, 90
jge @F
mov ecx, 90
@@:
mov factor, ecx
jmp falloff_done
falloff_bunker:
; Level 1 (Bunker) - underground, heavily muffled concrete absorption (~160)
mov factor, 160
jmp falloff_done
falloff_fortune:
; Level 3 (Fortune Room) - secret room on opposite side of casino (~90)
mov factor, 90
jmp falloff_done
falloff_stairs:
; On stairs between floors
mov eax, stairRoute3D
cmp eax, 2
je falloff_stairs_upper
cmp eax, 1
je falloff_stairs_bunker
mov factor, 150
jmp falloff_done
falloff_stairs_upper:
; Route 2 (Upper stairs)
; playerX goes from ~3 (bottom, beside jukebox) to ~16 (top, upper floor)
; At bottom: 850. At top: 420.
mov eax, playerX
shr eax, 16 ; integer X (3..16)
cmp eax, 3
jge @F
mov eax, 3
@@:
cmp eax, 16
jle @F
mov eax, 16
@@:
; Interpolate: factor = 850 - (eax - 3) * (850 - 420) / 13
sub eax, 3
imul eax, 430
xor edx, edx
mov ebx, 13
div ebx
mov ecx, 850
sub ecx, eax
mov factor, ecx
jmp falloff_done
falloff_stairs_bunker:
; Route 1 (Basement stairs)
; Smooth blend between 350 (top) and 160 (bottom)
mov factor, 250
jmp falloff_done
falloff_silent:
mov factor, 0
falloff_done:
mov eax, factor
ret
CalculateJukeboxFalloff ENDP
UpdateJukeboxAudio3D PROC uses ebx esi edi
local falloff:DWORD, targetVol:DWORD
; Check if music is playing
cmp jboxIsPlaying, 1
jne jbox_audio_mute
; If user volume is 0 -> mute
mov eax, jboxUserVolume
test eax, eax
jz jbox_audio_mute
; Calculate 3D attenuation factor (0..1000)
call CalculateJukeboxFalloff
mov falloff, eax
test eax, eax
jz jbox_audio_mute
; targetVol = (jboxUserVolume * 100) * falloff / 1000
; jboxUserVolume is 0..10 (0..1000 base)
mov eax, jboxUserVolume
imul eax, 100
imul eax, falloff
xor edx, edx
mov ebx, 1000
div ebx
mov targetVol, eax
jmp jbox_apply_vol
jbox_audio_mute:
mov targetVol, 0
jbox_apply_vol:
; If targetVol matches last applied volume, skip redundant MCI call
mov eax, targetVol
cmp eax, jboxEffectiveVolume
je jbox_audio_done
mov jboxEffectiveVolume, eax
; Format: "setaudio jboxmusic volume to <targetVol>"
; Copy prefix to jboxVolCmdBuf
lea esi, jboxVolCmdPrefix
lea edi, jboxVolCmdBuf
jbox_vol_copy:
lodsb
stosb
test al, al
jnz jbox_vol_copy
dec edi ; point at null terminator
; Append decimal number targetVol
invoke FormatNumber, targetVol, edi
; Send to MCI
invoke mciSendStringA, OFFSET jboxVolCmdBuf, 0, 0, 0
jbox_audio_done:
ret
UpdateJukeboxAudio3D ENDP
; ==============================================================================
; VOLUME CONTROL KEYS & POP-UP SLIDING HUD BAR
; ==============================================================================
HandleJukeboxKeys3D PROC
; '+' key: VK_OEM_PLUS (0BBh) or VK_ADD (6Bh - numpad +)
invoke Pressed3D, 0BBh
test eax, eax
jnz jbox_do_plus
invoke Pressed3D, 6Bh
test eax, eax
jz jbox_check_minus
jbox_do_plus:
cmp jboxUserVolume, 10
jae @F
inc jboxUserVolume
@@:
mov jboxHudTimer, 90 ; Show HUD for ~1.5s
call UpdateJukeboxAudio3D
ret
jbox_check_minus:
; '-' key: VK_OEM_MINUS (0BDh) or VK_SUBTRACT (6Dh - numpad -)
invoke Pressed3D, 0BDh
test eax, eax
jnz jbox_do_minus
invoke Pressed3D, 6Dh
test eax, eax
jz jbox_keys_done
jbox_do_minus:
cmp jboxUserVolume, 0
jbe @F
dec jboxUserVolume
@@:
mov jboxHudTimer, 90 ; Show HUD for ~1.5s
call UpdateJukeboxAudio3D
jbox_keys_done:
ret
HandleJukeboxKeys3D ENDP
RenderJukeboxVolumeHUD PROC uses ebx esi edi
local winX:DWORD, winY:DWORD, barX:DWORD, barY:DWORD, barW:DWORD, barH:DWORD
local numBuf[16]:BYTE
; 1. Update Timer and Animation Position
cmp jboxHudTimer, 0
je hud_sliding_down
; Active: decrement timer
dec jboxHudTimer
; Slide UP towards target Y = 206
cmp jboxHudY, 206
jbe hud_at_target
sub jboxHudY, 4
cmp jboxHudY, 206
jae hud_at_target
mov jboxHudY, 206
hud_at_target:
jmp hud_draw
hud_sliding_down:
; Timer expired: slide DOWN towards 240 (offscreen)
cmp jboxHudY, 240
jae hud_hidden
add jboxHudY, 3
cmp jboxHudY, 240
jbe hud_draw
mov jboxHudY, 240
hud_hidden:
ret
hud_draw:
mov eax, jboxHudY
mov winY, eax
mov winX, 72 ; Centered in 320: width 176
; 2. Background and Frame
; Outer dark shadow (178 x 30)
mov eax, winX
dec eax
mov ecx, winY
dec ecx
invoke DrawBox, eax, ecx, 178, 30, 0008100Ch
; Outer bezel (176 x 28)
invoke DrawBox, winX, winY, 176, 28, 001C2E24h
; Inner panel body (174 x 26)
mov eax, winX
inc eax
mov ecx, winY
inc ecx
invoke DrawBox, eax, ecx, 174, 26, 00101A14h
; Brass top trim accent (174 x 1)
mov eax, winX
inc eax
mov ecx, winY
inc ecx
invoke DrawBox, eax, ecx, 174, 1, 008A6B34h
; 3. Title text: "MUZ. AVTOMAT: " + percentage
mov eax, winX
add eax, 10
mov ecx, winY
add ecx, 4
invoke DrawString8x8, eax, ecx, OFFSET jboxHudTitle, 0050FF90h
; Draw percentage or "VYKL"
mov eax, winX
add eax, 126
mov ecx, winY
add ecx, 4
cmp jboxUserVolume, 0
jne hud_draw_pct_num
invoke DrawString8x8, eax, ecx, OFFSET jboxHudMute, 008CB8A5h
jmp hud_draw_trough
hud_draw_pct_num:
push eax
push ecx
mov edx, jboxUserVolume
imul edx, 10
lea edi, numBuf
invoke FormatNumber, edx, edi
hud_find_term:
cmp byte ptr [edi], 0
je hud_append_pct
inc edi
jmp hud_find_term
hud_append_pct:
mov byte ptr [edi], '%'
mov byte ptr [edi+1], 0
pop ecx
pop eax
lea edx, numBuf
invoke DrawString8x8, eax, ecx, edx, 00F0E6C3h
hud_draw_trough:
; Progress trough at barX = winX + 24, barY = winY + 15, barW = 128, barH = 7
mov eax, winX
add eax, 24
mov barX, eax
mov ecx, winY
add ecx, 15
mov barY, ecx
mov barW, 128
mov barH, 7
; Trough border
mov eax, barX
dec eax
mov ecx, barY
dec ecx
invoke DrawBox, eax, ecx, 130, 9, 00223828h
invoke DrawBox, barX, barY, 128, 7, 000C1610h
; Draw 10 blocks: i = 0..9
xor esi, esi
hud_blocks_loop:
mov eax, barX
add eax, 4
mov edx, esi
imul edx, 12
add eax, edx ; blockX = barX + 4 + i*12
mov ecx, barY
inc ecx ; blockY = barY + 1
cmp esi, jboxUserVolume
jae hud_block_empty
; Filled block color gradient
cmp esi, 7
jb hud_col_green
cmp esi, 9
jb hud_col_gold
mov edx, 00465AFFh ; Red/orange accent for 100%
jmp hud_draw_block_rect
hud_col_green:
mov edx, 0050FF90h ; Phosphor green
jmp hud_draw_block_rect
hud_col_gold:
mov edx, 00FFD700h ; Golden amber
jmp hud_draw_block_rect
hud_block_empty:
mov edx, 0018261Eh ; Dim background
hud_draw_block_rect:
invoke DrawBox, eax, ecx, 10, 5, edx
inc esi
cmp esi, 10
jb hud_blocks_loop
; Draw '-' and '+' labels
mov eax, winX
add eax, 8
mov ecx, winY
add ecx, 15
invoke DrawString8x8, eax, ecx, OFFSET jboxHudMinus, 008CB8A5h
mov eax, winX
add eax, 158
mov ecx, winY
add ecx, 15
invoke DrawString8x8, eax, ecx, OFFSET jboxHudPlus, 008CB8A5h
ret
RenderJukeboxVolumeHUD ENDP

View file

@ -0,0 +1,87 @@
; ==============================================================================
; JACKBOX / JUKEBOX DATA - MASM32 / CP1251
; ==============================================================================
MODE_JACKBOX equ 28
INTERACT_JACKBOX equ 48
JACKBOX_X equ FP_1*306/10
JACKBOX_Y equ FP_1*235/10
JACKBOX_TRACK_COUNT equ 3
MATERIAL_JACKBOX_FRONT equ 161
MATERIAL_JACKBOX_SIDE equ 121
MATERIAL_JACKBOX_BACK equ 163
LoadJackboxArt3D PROTO
FreeJackboxArt3D PROTO
ResolveJackboxSurface3D PROTO :DWORD
RenderJackboxCabinet3D PROTO
JackboxCanWalk3D PROTO :DWORD,:DWORD
PlayJukeboxTrack3D PROTO :DWORD
StopJukeboxTrack3D PROTO
BeginJackbox3D PROTO
HandleJackbox3D PROTO
RenderJackbox3D PROTO
CalculateJukeboxFalloff PROTO
UpdateJukeboxAudio3D PROTO
HandleJukeboxKeys3D PROTO
RenderJukeboxVolumeHUD PROTO
.data
jackboxWorldArt3D AnimPack3D <>
jackboxScreenArt3D AnimPack3D <>
jackboxWorldPath3D db "assets\animations\hall\jackbox-v1.bin",0
jackboxScreenPath3D db "assets\animations\hall\jackbox-ui-v1.bin",0
; Prompt: "E: Vkljuchit avtomat"
jackboxPrompt3D db 69,58,32,194,234,235,254,247,232,242,252,32,224,226,242,238,236,224,242,0
jboxCurrentTrack dd -1
jboxSelectedTrack dd 0
jboxIsPlaying dd 0
; Volume control & 3D sound state
jboxUserVolume dd 8
jboxEffectiveVolume dd -1
jboxHudTimer dd 0
jboxHudY dd 240
jboxCloseCmd db "close jboxmusic",0
jboxStopCmd db "stop jboxmusic",0
jboxPlayCmd db "play jboxmusic repeat",0
jboxVolCmdPrefix db "setaudio jboxmusic volume to ",0
jboxVolCmdBuf db 64 dup(0)
; Popup HUD labels in CP1251:
; "MUZ. AVTOMAT: "
jboxHudTitle db 204,211,199,46,32,192,194,210,206,204,192,210,58,32,0
; "VYKL"
jboxHudMute db 194,251,234,235,0
jboxHudMinus db "-",0
jboxHudPlus db "+",0
jboxOpen0 db 'open "assets\music\jackbox\BYBOLDEN. - Art Deco.mp3" type mpegvideo alias jboxmusic',0
jboxOpen1 db 'open "assets\music\jackbox\BYBOLDEN. - Soft Hours.mp3" type mpegvideo alias jboxmusic',0
jboxOpenCommands dd OFFSET jboxOpen0, OFFSET jboxOpen1, 0
jboxName0 db "1. Bolden - Art Deco",0
jboxName1 db "2. Bolden - Soft Hours",0
; "0. [Vykljuchit zvuk]"
jboxName2 db 48,46,32,91,194,251,234,235,254,247,232,242,252,32,231,226,243,234,93,0
jboxTrackNames dd OFFSET jboxName0, OFFSET jboxName1, OFFSET jboxName2
; Top Digital LCD Display Slots
jboxLcdPlay db 200,195,208,192,197,210,0 ; "IGRAET"
jboxLcdStop db 209,210,206,207,0 ; "STOP"
jboxLcdExit db 194,219,213,206,196,0 ; "VYHOD"
; CRT Monitor Screen Text
jboxHdrNow db 209,197,201,215,192,209,58,0 ; "SEJCHAS:"
jboxHdrStop db 209,210,192,210,211,209,58,32,206,209,210,192,205,206,194,203,197,205,0 ; "STATUS: OSTANOVLEN"
jboxCursor db ">",0
jboxTagPlay db "[>]",0
; "ENTER:Pusk S:Stop R:Miks"
jboxFooterHelp db 69,78,84,69,82,58,207,243,241,234,32,32,83,58,209,242,238,239,32,32,82,58,204,232,234,241,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

@ -1,5 +1,6 @@
LoadWorldDressing3D PROC uses ebx esi
call LoadTerminalArt3D
call LoadJackboxArt3D
invoke LoadAnimPack3D,OFFSET worldDressingArt3D,OFFSET worldDressingPath3D
invoke ValidateCityPack3D,OFFSET worldDressingArt3D,128,128,3
cmp worldStreetSized3D,0
@ -89,6 +90,7 @@ UpdateStreetGround3D ENDP
FreeWorldDressing3D PROC
call FreeTerminalArt3D
call FreeJackboxArt3D
invoke FreeAnimPack3D,OFFSET worldDressingArt3D
mov hallLightReady3D,0
ret
@ -115,6 +117,7 @@ RenderHallDressing3D PROC uses ebx esi edi
cmp hallLevel3D,0
jne hall_dressing_done
call RenderTerminalCabinets3D
call RenderJackboxCabinet3D
call DrawHallDarts3D
lea esi,hallWallArt3D
mov edi,WORLD_WALL_ART_COUNT

View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
import hashlib
import json
import os
import sys
import time
import urllib.request
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
MANIFEST_PATH = os.path.join(ROOT, 'ai/model.json')
with open(MANIFEST_PATH, 'r', encoding='utf-8') as f:
manifest = json.load(f)
dest_path = os.path.join(ROOT, 'ai', manifest['file'])
part_path = dest_path + '.part'
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
expected_bytes = manifest['bytes']
expected_sha256 = manifest['sha256'].lower()
if os.path.exists(dest_path):
actual_bytes = os.path.getsize(dest_path)
if actual_bytes == expected_bytes:
print(f"[INFO] Destination file already exists ({actual_bytes} bytes). Verifying SHA256...")
h = hashlib.sha256()
with open(dest_path, 'rb') as f:
while chunk := f.read(1024 * 1024):
h.update(chunk)
if h.hexdigest().lower() == expected_sha256:
print("[SUCCESS] Model already downloaded and verified!")
sys.exit(0)
else:
print("[WARN] Existing file has checksum mismatch, re-downloading...")
os.remove(dest_path)
url = f"https://huggingface.co/{manifest['source']}/resolve/{manifest['revision']}/{manifest['source_file']}?download=true"
print(f"Downloading model from:\n{url}")
print(f"Destination: {dest_path}")
print(f"Expected size: {expected_bytes} bytes (~{expected_bytes / (1024**3):.2f} GB)")
headers = {'User-Agent': 'Mozilla/5.0'}
existing_bytes = 0
if os.path.exists(part_path):
existing_bytes = os.path.getsize(part_path)
if existing_bytes < expected_bytes:
print(f"Resuming download from byte {existing_bytes}...")
headers['Range'] = f"bytes={existing_bytes}-"
else:
existing_bytes = 0
os.remove(part_path)
req = urllib.request.Request(url, headers=headers)
try:
response = urllib.request.urlopen(req, timeout=30)
except Exception as e:
print(f"[ERROR] Failed to open URL: {e}")
sys.exit(1)
mode = 'ab' if existing_bytes > 0 else 'wb'
downloaded = existing_bytes
start_time = time.time()
last_report_time = start_time
last_report_bytes = downloaded
with open(part_path, mode) as out:
while True:
chunk = response.read(1024 * 512) # 512 KB
if not chunk:
break
out.write(chunk)
downloaded += len(chunk)
now = time.time()
if now - last_report_time >= 5.0 or downloaded == expected_bytes:
speed_mb = (downloaded - last_report_bytes) / (now - last_report_time) / (1024 * 1024)
pct = (downloaded / expected_bytes) * 100.0
print(f"Progress: {downloaded / (1024**3):.2f} / {expected_bytes / (1024**3):.2f} GB ({pct:.1f}%) | {speed_mb:.2f} MB/s")
last_report_time = now
last_report_bytes = downloaded
print("\nDownload finished! Verifying SHA256 checksum...")
h = hashlib.sha256()
with open(part_path, 'rb') as f:
while chunk := f.read(1024 * 1024):
h.update(chunk)
actual_sha256 = h.hexdigest().lower()
if actual_sha256 != expected_sha256:
print(f"[ERROR] SHA256 mismatch!\nExpected: {expected_sha256}\nGot: {actual_sha256}")
sys.exit(1)
if os.path.exists(dest_path):
os.remove(dest_path)
os.rename(part_path, dest_path)
print("[SUCCESS] Model successfully downloaded and verified!")

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,168 @@
"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.
Processes high-resolution source images into a solid rectangular cabinet ('прямоугольный шкаф'):
- Front: Arched jukebox art framed in rich dark mahogany wood casing (solid 256x512 BGRA).
- Side: Cropped rectangular wooden cabinet side panel with chrome accents (solid 256x512 BGRA).
- Back: Matching dark walnut cabinet rear panel with ventilation slots (solid 256x512 BGRA).
- UI: 640x480 BGRA track selector screen.
Eliminates all grey studio backgrounds, transparent gaps, and seam holes.
"""
import math
from pathlib import Path
import struct
import cv2
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/jackbox"
OUT = ROOT / "assets/animations/hall"
# Deep rich dark walnut / mahogany wood color (BGR)
WOOD_COLOR = np.array([16, 26, 48], dtype=np.uint8)
def make_solid_front(img_bgr: np.ndarray) -> np.ndarray:
"""Frame the arched jukebox front into a solid rectangular wooden cabinet face."""
h, w = img_bgr.shape[:2]
# Detect studio grey background
diff = np.maximum(
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 1].astype(int)),
np.maximum(np.abs(img_bgr[:, :, 1].astype(int) - img_bgr[:, :, 2].astype(int)),
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 2].astype(int)))
)
bright = np.mean(img_bgr, axis=2)
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
# Seed from borders
seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
# Replace studio grey background with dark mahogany wood casing
front_solid = img_bgr.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
front_solid[:, :, c] = np.clip(
front_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Resize to 256x512 BGRA, 100% opaque (no transparency gaps!)
resized = cv2.resize(front_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
bgra[:, :, 3] = 255
return bgra
def make_solid_side(img_bgr: np.ndarray) -> np.ndarray:
"""Crop side image to a solid rectangular cabinet side panel."""
# Crop to actual rectangular cabinet profile:
# y: 90..2910 (removes studio top margin and floor shadow)
# x: 120..1345 (from front edge to back vertical edge)
side_crop = img_bgr[90:2910, 120:1345]
h, w = side_crop.shape[:2]
# Detect studio grey in the top corner outside the curve
diff = np.maximum(
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 1].astype(int)),
np.maximum(np.abs(side_crop[:, :, 1].astype(int) - side_crop[:, :, 2].astype(int)),
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 2].astype(int)))
)
bright = np.mean(side_crop, axis=2)
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
# Fill corner with dark mahogany cabinet wood
side_solid = side_crop.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
side_solid[:, :, c] = np.clip(
side_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Left of texture (x=0) is FRONT edge; right of texture (x=max) is BACK edge.
# Matches along_x from x0 (front) to x1 (back against wall).
resized = cv2.resize(side_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
bgra[:, :, 3] = 255
return bgra
def make_solid_back() -> np.ndarray:
"""Generate a solid dark walnut cabinet rear panel with ventilation slots."""
rear = np.zeros((512, 256, 4), dtype=np.uint8)
for y in range(512):
grain = int(math.sin(y * 0.2) * 4 + math.sin(y * 0.05) * 6)
rear[y, :, 0] = np.clip(int(WOOD_COLOR[0]) + grain, 0, 255) # B
rear[y, :, 1] = np.clip(int(WOOD_COLOR[1]) + grain + 1, 0, 255) # G
rear[y, :, 2] = np.clip(int(WOOD_COLOR[2]) + grain + 3, 0, 255) # R
rear[y, :, 3] = 255 # Fully opaque
for y in range(120, 240, 12):
rear[y:y+3, 32:224, :3] = 10 # Dark ventilation slots
return rear
def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int):
"""Pack an array of (H, W, 4) BGRA numpy uint8 images into an ANM1 binary pack."""
raw = b"".join(f.tobytes() for f in frames)
path.parent.mkdir(parents=True, exist_ok=True)
header = struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw))
path.write_bytes(header + raw)
print(f"Packed {len(frames)} frame(s) ({w}x{h}) into {path.name}: {len(raw)} bytes")
def main():
front_path = SOURCE / "Jackbox_front.jpg"
side_path = SOURCE / "Jackbox_side.jpg"
ui_path = SOURCE / "Jackbox_UI.jpg"
assert front_path.exists(), f"Missing {front_path}"
assert side_path.exists(), f"Missing {side_path}"
assert ui_path.exists(), f"Missing {ui_path}"
print("Building solid rectangular Jukebox cabinet textures...")
# 1. Front Texture: solid rectangular face
front_bgr = cv2.imread(str(front_path))
front_tex = make_solid_front(front_bgr)
# 2. Side Texture: solid rectangular side panel
side_bgr = cv2.imread(str(side_path))
side_tex = make_solid_side(side_bgr)
# 3. Rear Texture: solid dark walnut panel
rear_tex = make_solid_back()
# Pack 3D world frames: Frame 0 = Front, Frame 1 = Side, Frame 2 = Rear
pack_bgra(OUT / "jackbox-v1.bin", [front_tex, side_tex, rear_tex], 256, 512)
# 4. Screen UI Pack: 640x480 BGRA
ui_bgr = cv2.imread(str(ui_path))
ui_bgra = cv2.cvtColor(ui_bgr, cv2.COLOR_BGR2BGRA)
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480)
print("Solid Jukebox asset packaging complete.")
if __name__ == "__main__":
main()

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,125 @@
"""Verification test suite for the Jukebox / Jackbox music system."""
import ctypes
from pathlib import Path
import re
import struct
ROOT = Path(__file__).resolve().parents[2]
def test_packed_assets():
world_bin = ROOT / "assets/animations/hall/jackbox-v1.bin"
ui_bin = ROOT / "assets/animations/hall/jackbox-ui-v1.bin"
assert world_bin.is_file(), f"Missing {world_bin}"
assert ui_bin.is_file(), f"Missing {ui_bin}"
# Check world bin header: ANM1, 256, 512, 3 frames
wdata = world_bin.read_bytes()
magic, w, h, frames, raw_len = struct.unpack("<4s4I", wdata[:20])
assert magic == b"ANM1", f"Bad magic: {magic}"
assert (w, h, frames) == (256, 512, 3), f"Bad dimensions: {w}x{h}, {frames} frames"
assert len(wdata) == 20 + raw_len == 20 + 256 * 512 * 4 * 3
print(f"[OK] jackbox-v1.bin verified: {frames} frames ({w}x{h}), {raw_len} bytes")
# Check UI bin header: ANM1, 640, 480, 1 frame
uidata = ui_bin.read_bytes()
magic, w, h, frames, raw_len = struct.unpack("<4s4I", uidata[:20])
assert magic == b"ANM1", f"Bad magic: {magic}"
assert (w, h, frames) == (640, 480, 1), f"Bad dimensions: {w}x{h}, {frames} frames"
assert len(uidata) == 20 + raw_len == 20 + 640 * 480 * 4
print(f"[OK] jackbox-ui-v1.bin verified: {frames} frame ({w}x{h}), {raw_len} bytes")
def test_audio_tracks():
data_inc = (ROOT / "src_graphics/jackbox_data.inc").read_bytes().decode("cp1251")
open_cmds = re.findall(r'open\s+"([^"]+)"\s+type\s+(\w+)\s+alias\s+jboxmusic', data_inc)
assert len(open_cmds) == 2, f"Expected 2 audio tracks, got {len(open_cmds)}"
for rel_path, media_type in open_cmds:
assert "assets\\music\\jackbox" in rel_path or "assets/music/jackbox" in rel_path, f"Track must be from jackbox directory: {rel_path}"
for rel_path, media_type in open_cmds:
full_path = ROOT / rel_path
assert full_path.is_file(), f"Audio file missing: {rel_path}"
# Test MCI open & close
cmd_open = f'open "{full_path}" type {media_type} alias testjbox'
err = ctypes.windll.winmm.mciSendStringW(cmd_open, None, 0, None)
assert err == 0, f"MCI failed to open {rel_path}: error {err}"
try:
length_buf = ctypes.create_unicode_buffer(64)
err = ctypes.windll.winmm.mciSendStringW("status testjbox length", length_buf, 64, None)
assert err == 0, f"MCI failed to query length of {rel_path}"
finally:
ctypes.windll.winmm.mciSendStringW("close testjbox", None, 0, None)
print(f"[OK] MCI verified track: {rel_path} ({media_type})")
def test_code_structure():
data_inc = (ROOT / "src_graphics/jackbox_data.inc").read_bytes().decode("cp1251")
code_inc = (ROOT / "src_graphics/jackbox.inc").read_bytes().decode("cp1251")
casino_inc = (ROOT / "casino3d.inc").read_bytes().decode("cp1251")
hall_inc = (ROOT / "src_graphics/hall_scene.inc").read_bytes().decode("cp1251")
world_inc = (ROOT / "src_graphics/world_dressing.inc").read_bytes().decode("cp1251")
city_inc = (ROOT / "src_graphics/city_bins.inc").read_bytes().decode("cp1251")
# Check constants
assert "MODE_JACKBOX equ 28" in data_inc
assert "INTERACT_JACKBOX equ 48" in data_inc
assert "JACKBOX_X equ FP_1*306/10" in data_inc
assert "JACKBOX_Y equ FP_1*235/10" in data_inc
assert "MATERIAL_JACKBOX_SIDE equ 121" in data_inc
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
# Check procs
procs = [
"LoadJackboxArt3D", "FreeJackboxArt3D", "ResolveJackboxSurface3D",
"DrawJackboxCabinet3D", "RenderJackboxCabinet3D", "JackboxCanWalk3D",
"PlayJukeboxTrack3D", "StopJukeboxTrack3D", "BeginJackbox3D",
"HandleJackbox3D", "RenderJackbox3D",
"CalculateJukeboxFalloff", "UpdateJukeboxAudio3D",
"HandleJukeboxKeys3D", "RenderJukeboxVolumeHUD"
]
for p in procs:
assert f"{p} PROC" in code_inc, f"Missing proc: {p}"
# Check integrations
assert "include src_graphics\\jackbox_data.inc" in casino_inc
assert "include src_graphics\\jackbox.inc" in casino_inc
assert "MODE_JACKBOX" in casino_inc
assert "HandleJackbox3D" in casino_inc
assert "RenderJackbox3D" in casino_inc
assert "call HandleJukeboxKeys3D" in casino_inc
assert "call UpdateJukeboxAudio3D" in casino_inc
assert "call RenderJukeboxVolumeHUD" in casino_inc
assert "jboxUserVolume dd" in data_inc
assert "setaudio jboxmusic volume to" in data_inc
assert "call LoadJackboxArt3D" in world_inc
assert "call FreeJackboxArt3D" in world_inc
assert "call RenderJackboxCabinet3D" in world_inc
assert "INTERACT_JACKBOX" in hall_inc
assert "BeginJackbox3D" in hall_inc
assert "JackboxCanWalk3D" in hall_inc
assert "ResolveJackboxSurface3D" in city_inc
print("[OK] Code structure and all integrations verified.")
def test_binary_executable():
exe_path = ROOT / "poker.exe"
assert exe_path.is_file()
exe_bytes = exe_path.read_bytes()
assert b"jackbox-v1.bin" in exe_bytes
assert b"jackbox-ui-v1.bin" in exe_bytes
assert b"jboxmusic" in exe_bytes
print(f"[OK] poker.exe verified with embedded Jackbox strings ({len(exe_bytes)} bytes).")
if __name__ == "__main__":
test_packed_assets()
test_audio_tracks()
test_code_structure()
test_binary_executable()
print("\nALL JACKBOX / JUKEBOX VERIFICATION CHECKS PASSED!")

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

View file

@ -0,0 +1,367 @@
# -*- coding: utf-8 -*-
"""
Visual simulation of the solid rectangular 3D Jukebox cabinet beside billiards.
Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back).
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D and DrawCityHorizontal3D.
"""
import math
import struct
import re
from pathlib import Path
import numpy as np
from PIL import Image
REPO_ROOT = Path(__file__).resolve().parents[2]
DOCS_DIR = REPO_ROOT / 'docs' / 'previews_minigames'
DOCS_DIR.mkdir(parents=True, exist_ok=True)
FP_1 = 65536
SCREEN_W = 640
SCREEN_H = 480
HALF_W = 320
HALF_H = 240
HALL_STANDING_EYE = FP_1 * 21 // 20
# 1. Load map
with open(REPO_ROOT / 'casino3d_data.inc', 'r', encoding='latin1') as f:
text = f.read()
lines = text.splitlines()
map_lines = []
capturing = False
for l in lines:
if 'casinoMap db' in l:
capturing = True
if capturing:
map_lines.append(l)
if len(map_lines) == 32:
break
casinoMap = []
for l in map_lines:
vals = [int(x.strip()) for x in re.findall(r'\d+', l)]
casinoMap.extend(vals)
# 2. Load HD textures
tex_raw = open(REPO_ROOT / 'assets/textures/casino3d-hd.bin', 'rb').read()
slot_size = 128 * 128 * 4
def get_tex_pixel(slot, tx, ty):
idx = slot * slot_size + (ty * 128 + tx) * 4
if idx + 4 > len(tex_raw):
return (50, 50, 50, 255)
b, g, r, a = tex_raw[idx:idx+4]
return (r, g, b, a)
# 3. Load Jackbox Art pack (ANM1)
jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read()
magic, jw, jh, jframes, jlen = struct.unpack('<4s4I', jackbox_raw[:20])
assert magic == b'ANM1' and (jw, jh, jframes) == (256, 512, 3)
jpixels = jackbox_raw[20:]
# 4. Load Terminals Art pack (ANM1) for authentic wooden side panels (Frame 3)
term_raw = open(REPO_ROOT / 'assets/animations/hall/terminals-v2.bin', 'rb').read()
tmagic, tw, th, tframes, tlen = struct.unpack('<4s4I', term_raw[:20])
tpixels = term_raw[20:]
def get_terminal_pixel(frame, tx, ty):
idx = (frame * tw * th + ty * tw + tx) * 4
b, g, r, a = tpixels[idx:idx+4]
return (r, g, b, a)
def get_jackbox_pixel(frame, tx, ty):
idx = frame * (jw * jh * 4) + (ty * jw + tx) * 4
b, g, r, a = jpixels[idx:idx+4]
return (r, g, b, a)
tableCos = [round(math.cos(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
tableSin = [round(math.sin(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
def render_scene(cam_x, cam_y, cam_angle, out_path):
playerX = int(cam_x * FP_1)
playerY = int(cam_y * FP_1)
playerAngle = cam_angle % 512
playerDirX = tableCos[playerAngle]
playerDirY = tableSin[playerAngle]
playerPlaneX = -66 * playerDirY // 100
playerPlaneY = 66 * playerDirX // 100
screen_pixels = [(20, 20, 25)] * (SCREEN_W * SCREEN_H)
zBuffer = [0x7FFFFFFF] * SCREEN_W
depthBuffer = [0x7FFFFFFF] * (SCREEN_W * SCREEN_H)
# Floor & Ceiling
for y in range(HALF_H, SCREEN_H):
pDist = (HALF_H * FP_1) // (y - HALF_H + 1)
shade = max(35, min(255, 255 - (pDist >> 13)))
cShade = max(20, shade - 25)
fStepX = (playerPlaneX * 2 * pDist // FP_1) // SCREEN_W
fStepY = (playerPlaneY * 2 * pDist // FP_1) // SCREEN_W
fX = playerX + ((playerDirX - playerPlaneX) * pDist // FP_1)
fY = playerY + ((playerDirY - playerPlaneY) * pDist // FP_1)
for x in range(SCREEN_W):
tx = (fX >> 9) & 127
ty = (fY >> 9) & 127
r, g, b, _ = get_tex_pixel(0, tx, ty)
screen_pixels[y * SCREEN_W + x] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
cx = (fX >> 10) & 127
cy = (fY >> 10) & 127
cr, cg, cb, _ = get_tex_pixel(5, cx, cy)
screen_pixels[(SCREEN_H - 1 - y) * SCREEN_W + x] = ((cr * cShade) >> 8, (cg * cShade) >> 8, (cb * cShade) >> 8)
fX += fStepX
fY += fStepY
cityRayX = [0] * SCREEN_W
cityRayY = [0] * SCREEN_W
for col in range(SCREEN_W):
camX = (2 * col - SCREEN_W) * FP_1 // SCREEN_W
cityRayX[col] = playerDirX + (playerPlaneX * camX // FP_1)
cityRayY[col] = playerDirY + (playerPlaneY * camX // FP_1)
# Walls
for col in range(SCREEN_W):
rayDirX = cityRayX[col]
rayDirY = cityRayY[col]
mapX = playerX >> 16
mapY = playerY >> 16
deltaDistX = (1 << 32) // abs(rayDirX) if rayDirX != 0 else 0x7FFFFFFF
deltaDistY = (1 << 32) // abs(rayDirY) if rayDirY != 0 else 0x7FFFFFFF
if rayDirX < 0:
stepX = -1
sideDistX = ((playerX - (mapX << 16)) * deltaDistX) >> 16
else:
stepX = 1
sideDistX = (((mapX + 1 << 16) - playerX) * deltaDistX) >> 16
if rayDirY < 0:
stepY = -1
sideDistY = ((playerY - (mapY << 16)) * deltaDistY) >> 16
else:
stepY = 1
sideDistY = (((mapY + 1 << 16) - playerY) * deltaDistY) >> 16
side = 0
hit = 0
steps = 0
while hit == 0 and steps < 64:
steps += 1
if sideDistX < sideDistY:
sideDistX += deltaDistX
mapX += stepX
side = 0
else:
sideDistY += deltaDistY
mapY += stepY
side = 1
if 0 <= mapX < 32 and 0 <= mapY < 32:
tile = casinoMap[mapY * 32 + mapX]
if tile != 0:
hit = tile
else:
hit = 1
perpWallDist = sideDistX - deltaDistX if side == 0 else sideDistY - deltaDistY
perpWallDist = max(2048, perpWallDist)
zBuffer[col] = perpWallDist
lineH = (SCREEN_H * FP_1) // perpWallDist
drawStart = max(0, HALF_H - lineH // 2)
drawEnd = min(SCREEN_H - 1, HALF_H + lineH // 2)
wallHit = playerY + ((perpWallDist * rayDirY) >> 16) if side == 0 else playerX + ((perpWallDist * rayDirX) >> 16)
texX = ((wallHit & 0xFFFF) * 128) >> 16
if (side == 0 and rayDirX > 0) or (side == 1 and rayDirY < 0):
texX = 127 - texX
shade = max(40, min(255, 255 - (perpWallDist >> 12)))
if side == 1:
shade = (shade * 180) >> 8
texId = hit if hit != 0 else 1
for y in range(drawStart, drawEnd):
texY = (((y * 2 - SCREEN_H + lineH) * 128) // (lineH * 2)) & 127
r, g, b, _ = get_tex_pixel(texId, texX, texY)
screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
depthBuffer[y * SCREEN_W + col] = perpWallDist
def draw_vert(axis, coord, loBound, hiBound, zLow, zHigh, frame_idx):
for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256:
continue
orig = playerX if axis == 0 else playerY
num = (coord - orig)
depth = (num << 16) // ray_dir
if depth <= 4096 or depth >= zBuffer[col]:
continue
along = playerY + ((depth * cityRayY[col]) >> 16) if axis == 0 else playerX + ((depth * cityRayX[col]) >> 16)
if along < loBound or along >= hiBound:
continue
srcX = (along - loBound) * jw // (hiBound - loBound)
if srcX < 0 or srcX >= jw:
continue
top = (HALL_STANDING_EYE - zHigh) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - zLow) * SCREEN_H // depth + HALF_H
span = bottom - top
if span <= 0:
continue
stepY = (jh << 16) // span
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
curY = (r_start - top) * stepY
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
srcY = (curY >> 16)
curY += stepY
if srcY < 0 or srcY >= jh:
continue
r, g, b, a = get_jackbox_pixel(frame_idx, srcX, srcY)
if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
# 1. Pool table geometry: X in [26.0, 28.0], Y in [24.0, 26.0], Z in [0, 0.85m]
def draw_pool_side(axis, coord, lo, hi, z0, z1):
for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256:
continue
orig = playerX if axis == 0 else playerY
num = (coord - orig)
depth = (num << 16) // ray_dir
if depth <= 4096 or depth >= zBuffer[col]:
continue
along = playerY + ((depth * cityRayY[col]) >> 16) if axis == 0 else playerX + ((depth * cityRayX[col]) >> 16)
if along < lo or along >= hi:
continue
top = (HALL_STANDING_EYE - z1) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - z0) * SCREEN_H // depth + HALF_H
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((50 * shade) >> 8, (25 * shade) >> 8, (15 * shade) >> 8)
draw_pool_side(1, int(24.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
draw_pool_side(1, int(26.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
draw_pool_side(0, int(26.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
draw_pool_side(0, int(28.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
# 2. Wall Art / Painting 116 on East Wall: X = 30.98m, Y in [25.0m .. 26.5m], Z in [1.0m .. 2.0m]
def draw_painting(axis, coord, lo, hi, z0, z1, slot):
for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256:
continue
orig = playerX if axis == 0 else playerY
num = (coord - orig)
depth = (num << 16) // ray_dir
if depth <= 4096 or depth >= zBuffer[col]:
continue
along = playerY + ((depth * cityRayY[col]) >> 16) if axis == 0 else playerX + ((depth * cityRayX[col]) >> 16)
if along < lo or along >= hi:
continue
top = (HALL_STANDING_EYE - z1) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - z0) * SCREEN_H // depth + HALF_H
span = bottom - top
if span <= 0:
continue
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
span_along = hi - lo
tx = int((along - lo) * 128 / span_along) % 128
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
ty = int((row - top) * 128 / span) % 128
r, g, b, a = get_tex_pixel(slot, tx, ty)
if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
def draw_term_vert(axis, coord, lo, hi, zLow, zHigh, frame_idx):
for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256:
continue
orig = playerX if axis == 0 else playerY
num = (coord - orig)
depth = (num << 16) // ray_dir
if depth <= 4096 or depth >= zBuffer[col]:
continue
along = playerY + ((depth * cityRayY[col]) >> 16) if axis == 0 else playerX + ((depth * cityRayX[col]) >> 16)
if along < lo or along >= hi:
continue
span_along = hi - lo
srcX = int((along - lo) * tw / span_along)
if srcX < 0 or srcX >= tw:
continue
top = (HALL_STANDING_EYE - zHigh) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - zLow) * SCREEN_H // depth + HALF_H
span = bottom - top
if span <= 0:
continue
stepY = (th << 16) // span
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
curY = (r_start - top) * stepY
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
srcY = (curY >> 16)
curY += stepY
if srcY < 0 or srcY >= th:
continue
r, g, b, a = get_terminal_pixel(frame_idx, srcX, srcY)
if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
draw_painting(0, int(30.98 * FP_1), int(25.0 * FP_1), int(26.5 * FP_1), FP_1, 2 * FP_1, 116)
# 3. Solid Rectangular Jukebox Cabinet (at East wall, Y = 23.5m, clearing Painting 116!)
# Depth: X in [30.40m .. 30.95m] (back against East wall at 31.0m)
# Width: Y in [23.075m .. 23.925m] (centered at Y = 23.5m)
# Height: Z in [0 .. 1.85m]
x0 = int(30.40 * FP_1)
x1 = int(30.95 * FP_1)
y0 = int(23.075 * FP_1)
y1 = int(23.925 * FP_1)
zHigh = int(1.85 * FP_1)
# Back Face (against wall: X = x1) - Frame 2
draw_vert(0, x1, y0, y1, 0, zHigh, 2)
# North Side (Y = y0) - Authentic terminal wooden panel (Frame 3)
draw_term_vert(1, y0, x0, x1, 0, zHigh, 3)
# South Side (Y = y1) - Authentic terminal wooden panel (Frame 3)
draw_term_vert(1, y1, x0, x1, 0, zHigh, 3)
# Front Face (facing West towards billiards table: X = x0) - Frame 0
draw_vert(0, x0, y0, y1, 0, zHigh, 0)
img = Image.new('RGB', (SCREEN_W, SCREEN_H))
img.putdata(screen_pixels)
img.save(str(out_path))
print(f'Rendered: {out_path.name}')
if __name__ == '__main__':
# 1. Front View: Standing near billiards at (26.0, 25.0) looking East (angle=0) past table at Jukebox
render_scene(26.0, 24.5, 0, DOCS_DIR / 'jukebox_3d_front.png')
# 2. Angle View: Standing at (27.5, 22.5) looking South-East (angle=480) at pool table and Jukebox
render_scene(28.8, 25.0, 960, DOCS_DIR / 'jukebox_3d_angle.png')
# 3. Close-up View: Standing at (29.2, 25.0) looking East (angle=0) right at Jukebox
render_scene(29.2, 23.5, 0, DOCS_DIR / 'jukebox_3d_close.png')
# 4. Side View: Standing at (30.0, 27.5) looking North (angle=384) at South side of Jukebox
render_scene(30.6, 25.5, 768, DOCS_DIR / 'jukebox_3d_side.png')
print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')