feat(audio): implement interactive 3D jukebox (jackbox) and background music playback

This commit is contained in:
Игорь Федосеев 2026-10-25 15:15:02 +03:00
parent 5aec3ded9f
commit 82f3aa9437
22 changed files with 637 additions and 3 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

View file

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

Binary file not shown.

View file

@ -172,7 +172,8 @@ include src_graphics\scratch_data.inc
include src_graphics\hell_tower_data.inc include src_graphics\hell_tower_data.inc
include src_graphics\charms_data.inc include src_graphics\charms_data.inc
include src_graphics\observer_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 .data
ALIGN 4 ALIGN 4
@ -5197,7 +5198,8 @@ include src_graphics\fortune_dialog.inc
include src_graphics\hell_tower.inc include src_graphics\hell_tower.inc
include src_graphics\charms.inc include src_graphics\charms.inc
include src_graphics\observer.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 --- ; --- Main Loop of the 3D Casino Engine ---
StartHellTower3D PROC uses ebx esi edi StartHellTower3D PROC uses ebx esi edi
@ -5356,6 +5358,14 @@ check_walk_mode:
call RenderMinimap call RenderMinimap
jmp present_frame jmp present_frame
check_slots_mode: 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 cmp engineMode,MODE_DARTS
jne check_darts_done jne check_darts_done
call HandleDarts3D call HandleDarts3D

Binary file not shown.

47
docs/JACKBOX.md Normal file
View file

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

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

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

View file

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

View file

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

323
src_graphics/jackbox.inc Normal file
View file

@ -0,0 +1,323 @@
; ==============================================================================
; JACKBOX / JUKEBOX IMPLEMENTATION - MASM32 / CP1251
; ==============================================================================
.code
LoadJackboxArt3D PROC
invoke LoadAnimPack3D, OFFSET jackboxWorldArt3D, OFFSET jackboxWorldPath3D
invoke ValidateCityPack3D, OFFSET jackboxWorldArt3D, 256, 512, 2
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 @F
cmp eax, MATERIAL_JACKBOX_SIDE
jne jackbox_surface_missing
; Frame 1: side (offset 256*512*4 = 524288)
mov eax, jackboxWorldArt3D.pixels
add eax, 524288
mov ecx, 256
mov edx, 512
ret
@@:
; Frame 0: front
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
; Footprint: width 0.7m, depth 0.45m, height 1.85m
mov eax, wx
sub eax, FP_1*7/20
mov x0, eax
add eax, FP_1*7/10
mov x1, eax
mov eax, wy
sub eax, FP_1*9/40
mov y0, eax
add eax, FP_1*9/20
mov y1, eax
invoke DrawCityBoxSides3D, x0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
invoke DrawCityHorizontal3D, x0, x1, y0, y1, FP_1*37/20, 110
mov eax, y1
add eax, FP_1/500
invoke DrawCityVertical3D, 1, eax, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
ret
DrawJackboxCabinet3D ENDP
RenderJackboxCabinet3D PROC
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
ret
RenderJackboxCabinet3D ENDP
JackboxCanWalk3D PROC px:DWORD, py:DWORD
mov eax, py
sub eax, JACKBOX_Y
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_1*7/20
ja jackbox_walk_clear
mov eax, px
sub eax, JACKBOX_X
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_HALF
jbe jackbox_walk_blocked
jackbox_walk_clear:
mov eax, 1
ret
jackbox_walk_blocked:
xor eax, eax
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, 9
jae jbox_play_done
cmp ebx, JACKBOX_TRACK_COUNT
jae jbox_play_done
mov eax, jboxOpenCommands[ebx*4]
test eax, eax
jz jbox_play_done
invoke mciSendStringA, eax, 0, 0, 0
invoke mciSendStringA, OFFSET jboxPlayCmd, 0, 0, 0
mov jboxCurrentTrack, ebx
mov jboxIsPlaying, 1
jbox_play_done:
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
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
; Check Exit button in top right: x in [228..312], y in [8..26]
cmp mouseY3D, 8
jb @F
cmp mouseY3D, 26
ja @F
cmp mouseX3D, 228
jae jbox_exit
@@:
; Check track row click: y in [54..186]
cmp mouseY3D, 54
jb jbox_keys
cmp mouseY3D, 186
ja jbox_keys
cmp mouseX3D, 16
jb jbox_keys
cmp mouseX3D, 304
ja jbox_keys
mov eax, mouseY3D
sub eax, 54
xor edx, edx
mov ebx, 13
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, 9
mov jboxSelectedTrack, eax
invoke PlayJukeboxTrack3D, eax
jmp jbox_handled
@@:
; Direct digit keys '1'..'9', '0'
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, 39h ; '9'
jbe jbox_digit_loop
; '0' key for mute/stop
invoke Pressed3D, 30h
test eax, eax
jz jbox_handled
mov jboxSelectedTrack, 9
invoke PlayJukeboxTrack3D, 9
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 if available
cmp jackboxScreenArt3D.pixels, 0
je @F
invoke DrawAnimSprite3D, OFFSET jackboxScreenArt3D, 0, 0, 0, 640, 480
@@:
; 3. Title & Exit Button
invoke DrawString8x8, 52, 12, OFFSET jboxTitle, 00E7C479h
invoke DrawBoxBorder, 228, 8, 84, 16, 001A2522h, 00C8AD75h, 1
invoke DrawString8x8, 234, 12, OFFSET jboxClose, 00F2E6C6h
; 4. Now Playing Status Banner
cmp jboxIsPlaying, 1
jne jbox_render_stopped
cmp jboxCurrentTrack, -1
je jbox_render_stopped
cmp jboxCurrentTrack, 9
jae jbox_render_stopped
invoke DrawString8x8, 20, 34, OFFSET jboxStatusPlaying, 008CD8AFh
mov eax, jboxCurrentTrack
mov edx, jboxTrackNames[eax*4]
invoke DrawString8x8, 140, 34, edx, 00F0E6C3h
jmp jbox_render_divider
jbox_render_stopped:
invoke DrawString8x8, 20, 34, OFFSET jboxStatusStopped, 0090B19Fh
jbox_render_divider:
invoke DrawBox, 20, 47, 280, 1, 00495B4Ah
; 5. Track Selection List (10 rows, step Y = 13)
mov rowY, 55
xor esi, esi
jbox_render_row:
mov color, 0090B19Fh ; Default muted
cmp esi, jboxSelectedTrack
jne @F
mov color, 00F2E6C6h ; Bright cream for selection
invoke DrawString8x8, 14, rowY, OFFSET jboxCursor, 00E7C479h
@@:
mov edx, jboxTrackNames[esi*4]
invoke DrawString8x8, 28, rowY, edx, color
; Mark playing track
cmp esi, jboxCurrentTrack
jne jbox_row_next
cmp jboxIsPlaying, 1
jne jbox_row_next
invoke DrawString8x8, 252, rowY, OFFSET jboxPlayingTag, 008CD8AFh
jbox_row_next:
add rowY, 13
inc esi
cmp esi, JACKBOX_TRACK_COUNT
jb jbox_render_row
; 6. Footer Controls Help
invoke DrawBox, 20, 192, 280, 1, 00314538h
invoke DrawString8x8, 20, 200, OFFSET jboxHelp1, 00C1A982h
invoke DrawString8x8, 20, 214, OFFSET jboxHelp2, 0090B19Fh
ret
RenderJackbox3D ENDP

View file

@ -0,0 +1,78 @@
; ==============================================================================
; JACKBOX / JUKEBOX DATA - MASM32 / CP1251
; ==============================================================================
MODE_JACKBOX equ 28
INTERACT_JACKBOX equ 48
JACKBOX_X equ 6*FP_1
JACKBOX_Y equ 20*FP_1
JACKBOX_TRACK_COUNT equ 10
MATERIAL_JACKBOX_FRONT equ 161
MATERIAL_JACKBOX_SIDE equ 162
LoadJackboxArt3D PROTO
FreeJackboxArt3D PROTO
ResolveJackboxSurface3D PROTO :DWORD
RenderJackboxCabinet3D PROTO
JackboxCanWalk3D PROTO :DWORD,:DWORD
PlayJukeboxTrack3D PROTO :DWORD
StopJukeboxTrack3D PROTO
BeginJackbox3D PROTO
HandleJackbox3D PROTO
RenderJackbox3D 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
jackboxPrompt3D db "E: ÄÆÓÊÁÎÊÑ",0
jboxCurrentTrack dd -1
jboxSelectedTrack dd 0
jboxIsPlaying dd 0
jboxCloseCmd db "close jboxmusic",0
jboxStopCmd db "stop jboxmusic",0
jboxPlayCmd db "play jboxmusic repeat",0
jboxOpen0 db 'open "assets\music\tower\Keygen_Church_-_Tenebre_Rosso_Sangue_ULTRAKILL_Original_Game_Soundtrack_79610890.mp3" type mpegvideo alias jboxmusic',0
jboxOpen1 db 'open "assets\music\tower\KEYGEN_CHURCH_-_Hareklavit_75543927.mp3" type mpegvideo alias jboxmusic',0
jboxOpen2 db 'open "assets\music\tower\KEYGEN_CHURCH_-_Ladikerfos_75543991.mp3" type mpegvideo alias jboxmusic',0
jboxOpen3 db 'open "assets\music\tower\KEYGEN_CHURCH_-_Tilaskarot_75546826.mp3" type mpegvideo alias jboxmusic',0
jboxOpen4 db 'open "assets\music\tower\Mick_Gordon_-_BFG_Division_48126042.mp3" type mpegvideo alias jboxmusic',0
jboxOpen5 db 'open "assets\music\tower\Mick_Gordon_-_Rip_Tear_48126019.mp3" type mpegvideo alias jboxmusic',0
jboxOpen6 db 'open "assets\music\tower\Mick_Gordon_-_The_Only_Thing_They_Fear_Is_You_OST_DOOM_Eternal_72020713.mp3" type mpegvideo alias jboxmusic',0
jboxOpen7 db 'open "assets\audio\hakari_dance.wav" type waveaudio alias jboxmusic',0
jboxOpen8 db 'open "assets\audio\victory-tf2.wav" type waveaudio alias jboxmusic',0
jboxOpenCommands dd OFFSET jboxOpen0, OFFSET jboxOpen1, OFFSET jboxOpen2, OFFSET jboxOpen3
dd OFFSET jboxOpen4, OFFSET jboxOpen5, OFFSET jboxOpen6, OFFSET jboxOpen7
dd OFFSET jboxOpen8, 0
jboxName0 db "1. Keygen Church - Tenebre Rosso Sangue",0
jboxName1 db "2. Keygen Church - Hareklavit",0
jboxName2 db "3. Keygen Church - Ladikerfos",0
jboxName3 db "4. Keygen Church - Tilaskarot",0
jboxName4 db "5. Mick Gordon - BFG Division (DOOM)",0
jboxName5 db "6. Mick Gordon - Rip & Tear (DOOM)",0
jboxName6 db "7. Mick Gordon - The Only Thing They Fear",0
jboxName7 db "8. Hakari Dance (Jackpot Theme)",0
jboxName8 db "9. Team Fortress 2 - Victory Theme",0
jboxName9 db "0. [ ÂÛÊËÞ×ÈÒÜ ÌÓÇÛÊÓ / ÒÈØÈÍÀ ]",0
jboxTrackNames dd OFFSET jboxName0, OFFSET jboxName1, OFFSET jboxName2, OFFSET jboxName3
dd OFFSET jboxName4, OFFSET jboxName5, OFFSET jboxName6, OFFSET jboxName7
dd OFFSET jboxName8, OFFSET jboxName9
jboxTitle db "ÌÓÇÛÊÀËÜÍÛÉ ÀÂÒÎÌÀÒ",0
jboxClose db "ÂÛÕÎÄ [ESC]",0
jboxStatusPlaying db "ÑÅÉ×ÀÑ ÈÃÐÀÅÒ:",0
jboxStatusStopped db "ÌÓÇÛÊÀ ÂÛÊËÞ×ÅÍÀ",0
jboxCursor db "> ",0
jboxPlayingTag db " [ÈÃÐÀÅÒ]",0
jboxHelp1 db "ÑÒÐÅËÊÈ / 0-9: Âûáîð ENTER: Âêëþ÷èòü",0
jboxHelp2 db "S: Ñòîï R: Ñëó÷àéíûé ESC:  çàë",0

View file

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

View file

@ -0,0 +1,41 @@
"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/jackbox"
OUT = ROOT / "assets/animations/hall"
def pack(path: Path, frames: list):
w, h = frames[0].size
raw = b"".join(image.tobytes("raw", "BGRA") for image in frames)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw)) + 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}"
# 1. World Cabinet Pack: 2 frames (256x512 BGRA)
front_img = Image.open(front_path).convert("RGBA").resize((256, 512), Image.Resampling.LANCZOS)
side_img = Image.open(side_path).convert("RGBA").resize((256, 512), Image.Resampling.LANCZOS)
pack(OUT / "jackbox-v1.bin", [front_img, side_img])
# 2. Screen UI Pack: 1 frame (640x480 BGRA)
ui_img = Image.open(ui_path).convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
pack(OUT / "jackbox-ui-v1.bin", [ui_img])
print("Jackbox asset packaging complete.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,114 @@
"""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, 2 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, 2), f"Bad dimensions: {w}x{h}, {frames} frames"
assert len(wdata) == 20 + raw_len == 20 + 256 * 512 * 4 * 2
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) == 9, f"Expected 9 audio tracks, got {len(open_cmds)}"
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 6*FP_1" in data_inc
assert "JACKBOX_Y equ 20*FP_1" in data_inc
# Check procs
procs = [
"LoadJackboxArt3D", "FreeJackboxArt3D", "ResolveJackboxSurface3D",
"DrawJackboxCabinet3D", "RenderJackboxCabinet3D", "JackboxCanWalk3D",
"PlayJukeboxTrack3D", "StopJukeboxTrack3D", "BeginJackbox3D",
"HandleJackbox3D", "RenderJackbox3D"
]
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 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!")