Compare commits
11 commits
23dc40ee5c
...
eef2039c7e
| Author | SHA1 | Date | |
|---|---|---|---|
| eef2039c7e | |||
| 75d594ec79 | |||
| d4436674b4 | |||
| 4c2428c367 | |||
| 7c262a15ad | |||
| 6bf2a2885c | |||
| 0a85e271e9 | |||
| 82f3aa9437 | |||
| 5aec3ded9f | |||
| 2ab7c9ed40 | |||
| 4ca7912562 |
2
.gitignore
vendored
|
|
@ -48,3 +48,5 @@ __pycache__/
|
|||
/debug_render.png
|
||||
/render_view.png
|
||||
/docs/*.zip
|
||||
|
||||
/scratch/
|
||||
|
|
|
|||
BIN
assets/animations/buckshot/dealer-actions-v3-hell.bin
Normal file
BIN
assets/animations/buckshot/dealer-v2-hell.bin
Normal file
BIN
assets/animations/buckshot/death-v3-hell.bin
Normal file
BIN
assets/animations/buckshot/room-v2-hell.bin
Normal file
BIN
assets/animations/coin/table-v1-hell.bin
Normal file
BIN
assets/animations/hall/helltower-menu.bin
Normal file
BIN
assets/animations/hall/jackbox-ui-v1.bin
Normal file
BIN
assets/animations/hall/jackbox-v1.bin
Normal file
BIN
assets/animations/wheel/closeup-v1-hell.bin
Normal file
BIN
assets/animations/wheel/table-v1-hell.bin
Normal file
BIN
assets/hell_tower/01_coin/faces-v1.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/hell_tower/01_coin/hands-v1.png
Normal file
|
After Width: | Height: | Size: 847 KiB |
BIN
assets/hell_tower/01_coin/hands-v2.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/hell_tower/01_coin/table-v1-hell.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/hell_tower/02_dice/hands-v1.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/hell_tower/02_dice/hands-v2.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/hell_tower/06_russian_roulette/dealer-source-v2-hell.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
assets/hell_tower/06_russian_roulette/gman-talk-v1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/hell_tower/06_russian_roulette/hands-v5.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/hell_tower/06_russian_roulette/revolver-actions-v1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/hell_tower/06_russian_roulette/room-v2-hell.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/hell_tower/07_casino_roulette/closeup-v1-hell.jpg
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
assets/hell_tower/07_casino_roulette/table-v1-hell.jpg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
assets/hell_tower/10_buckshot/blood-v3.png
Normal file
|
After Width: | Height: | Size: 576 KiB |
BIN
assets/hell_tower/10_buckshot/dealer-actions-v3-hell.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
assets/hell_tower/10_buckshot/dealer-source-v2-hell.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
assets/hell_tower/10_buckshot/death-v3-hell.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
assets/hell_tower/10_buckshot/items-v2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/hell_tower/10_buckshot/loading-v2.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/hell_tower/10_buckshot/medical-dealer-v5.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
assets/hell_tower/10_buckshot/medical-player-v5.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
assets/hell_tower/10_buckshot/medical-v3.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
assets/hell_tower/10_buckshot/props-v2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/hell_tower/10_buckshot/room-v2-hell.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/hell_tower/10_buckshot/self-flash-v4.png
Normal file
|
After Width: | Height: | Size: 1,005 KiB |
BIN
assets/hell_tower/10_buckshot/self-v3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/hell_tower/10_buckshot/shots-v2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
68
assets/hell_tower/README.md
Normal 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`).*
|
||||
BIN
assets/hell_tower/environment/HellTower_ menu.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/hell_tower/environment/tex_wall_hell.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets/music/jackbox/BYBOLDEN. - Art Deco.mp3
Normal file
BIN
assets/music/jackbox/BYBOLDEN. - Art Deco.opus
Normal file
BIN
assets/music/jackbox/BYBOLDEN. - Soft Hours.mp3
Normal file
BIN
assets/music/jackbox/BYBOLDEN. - Soft Hours.opus
Normal file
BIN
assets/sprites/jackbox/Jackbox_UI.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/sprites/jackbox/Jackbox_front.jpg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
assets/sprites/jackbox/Jackbox_side.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/textures/card-table-hell.bin
Normal file
BIN
bin/poker.exe
12
build.bat
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
|
|||
{
|
||||
static class Program
|
||||
{
|
||||
const string ExpectedPokerSha256 = "C3D857FDBF7597C976B16C5021D7F07951628E54F86FF2B7BB53F8A83FDE11CA";
|
||||
const string ExpectedPokerSha256 = "07D474A0223390E7AC4F4F779818D77CC2DAE5123F4BDB0F72DD975473267310";
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
|
|
|
|||
18
casino3d.inc
|
|
@ -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
|
||||
|
|
|
|||
BIN
dist/Assembler Casino.exe
vendored
47
docs/JACKBOX.md
Normal 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`: тестовый набор верификации ассетов, аудио-файлов и бинарника.
|
||||
BIN
docs/previews_minigames/jukebox_3d_angle.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
docs/previews_minigames/jukebox_3d_close.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
docs/previews_minigames/jukebox_3d_front.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/previews_minigames/jukebox_3d_side.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
120
poker.asm
|
|
@ -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
BIN
poker.obj
|
|
@ -44,5 +44,6 @@ close_legacy_sound:
|
|||
cmp esi, legacySoundCount3D
|
||||
jb close_legacy_sound
|
||||
call StopTowerMusic
|
||||
call StopJukeboxTrack3D
|
||||
ret
|
||||
CloseLegacySounds3D ENDP
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
87
src_graphics/jackbox_data.inc
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
95
tools/fortune_ai/download_model.py
Normal 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!")
|
||||
105
tools/generators/copy_hell_tower_assets.ps1
Normal 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!"
|
||||
148
tools/generators/pack_hell_tower_assets.py
Normal 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()
|
||||
168
tools/generators/pack_jackbox.py
Normal 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()
|
||||
78
tools/generators/setup_hell_tower_sprites.ps1
Normal 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!"
|
||||
93
tools/tests/check_hell_tower.py
Normal 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!")
|
||||
125
tools/tests/check_jackbox.py
Normal 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!")
|
||||
115
tools/tests/render_hell_tower_preview.py
Normal 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()
|
||||
45
tools/tests/test_hell_tower_runthrough.py
Normal 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())
|
||||
367
tools/tests/test_jukebox_render_all_angles.py
Normal 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/!')
|
||||