diff --git a/assets/animations/hall/jackbox-ui-v1.bin b/assets/animations/hall/jackbox-ui-v1.bin new file mode 100644 index 0000000..6298317 Binary files /dev/null and b/assets/animations/hall/jackbox-ui-v1.bin differ diff --git a/assets/animations/hall/jackbox-v1.bin b/assets/animations/hall/jackbox-v1.bin new file mode 100644 index 0000000..4181fa4 Binary files /dev/null and b/assets/animations/hall/jackbox-v1.bin differ diff --git a/assets/music/tower/Keygen_Church_-_Tenebre_Rosso_Sangue_ULTRAKILL_Original_Game_Soundtrack_79610890.mp3 b/assets/music/tower/Keygen_Church_-_Tenebre_Rosso_Sangue_ULTRAKILL_Original_Game_Soundtrack_79610890.mp3 index 97a2e30..44471be 100644 Binary files a/assets/music/tower/Keygen_Church_-_Tenebre_Rosso_Sangue_ULTRAKILL_Original_Game_Soundtrack_79610890.mp3 and b/assets/music/tower/Keygen_Church_-_Tenebre_Rosso_Sangue_ULTRAKILL_Original_Game_Soundtrack_79610890.mp3 differ diff --git a/assets/sprites/jackbox/Jackbox_UI.jpg b/assets/sprites/jackbox/Jackbox_UI.jpg new file mode 100644 index 0000000..27e29eb Binary files /dev/null and b/assets/sprites/jackbox/Jackbox_UI.jpg differ diff --git a/assets/sprites/jackbox/Jackbox_front.jpg b/assets/sprites/jackbox/Jackbox_front.jpg new file mode 100644 index 0000000..de4ce87 Binary files /dev/null and b/assets/sprites/jackbox/Jackbox_front.jpg differ diff --git a/assets/sprites/jackbox/Jackbox_side.jpg b/assets/sprites/jackbox/Jackbox_side.jpg new file mode 100644 index 0000000..ddf4c63 Binary files /dev/null and b/assets/sprites/jackbox/Jackbox_side.jpg differ diff --git a/bin/poker.exe b/bin/poker.exe index 88cc494..aecd96c 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index 23c8d26..5f59a23 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "C3D857FDBF7597C976B16C5021D7F07951628E54F86FF2B7BB53F8A83FDE11CA"; + const string ExpectedPokerSha256 = "B1D4D027ACE9BDF6B34C4621191E5DAB855D44E384F45F9B0D0E8EC528DF2B28"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index 88cc494..aecd96c 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/casino3d.inc b/casino3d.inc index 4e68cff..8d3fe0c 100644 --- a/casino3d.inc +++ b/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 @@ -5356,6 +5358,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 diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 9e3b676..1e44304 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/JACKBOX.md b/docs/JACKBOX.md new file mode 100644 index 0000000..421d2e4 --- /dev/null +++ b/docs/JACKBOX.md @@ -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`: тестовый набор верификации ассетов, аудио-файлов и бинарника. diff --git a/poker.exe b/poker.exe index 88cc494..aecd96c 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index 9da99e2..17fcbb9 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/src_graphics/audio_code.inc b/src_graphics/audio_code.inc index 987205a..ddeb34a 100644 --- a/src_graphics/audio_code.inc +++ b/src_graphics/audio_code.inc @@ -44,5 +44,6 @@ close_legacy_sound: cmp esi, legacySoundCount3D jb close_legacy_sound call StopTowerMusic + call StopJukeboxTrack3D ret CloseLegacySounds3D ENDP diff --git a/src_graphics/city_bins.inc b/src_graphics/city_bins.inc index d7fc9b6..8b8dd8b 100644 --- a/src_graphics/city_bins.inc +++ b/src_graphics/city_bins.inc @@ -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,162 + ja @F + invoke ResolveJackboxSurface3D,material + ret +@@: cmp eax,160 jne @F call ResolveDartsSurface3D diff --git a/src_graphics/hall_scene.inc b/src_graphics/hall_scene.inc index 8056a6c..eaf0560 100644 --- a/src_graphics/hall_scene.inc +++ b/src_graphics/hall_scene.inc @@ -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 diff --git a/src_graphics/jackbox.inc b/src_graphics/jackbox.inc new file mode 100644 index 0000000..91ca83f --- /dev/null +++ b/src_graphics/jackbox.inc @@ -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 diff --git a/src_graphics/jackbox_data.inc b/src_graphics/jackbox_data.inc new file mode 100644 index 0000000..399dbd6 --- /dev/null +++ b/src_graphics/jackbox_data.inc @@ -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 diff --git a/src_graphics/world_dressing.inc b/src_graphics/world_dressing.inc index ac4f275..20280ea 100644 --- a/src_graphics/world_dressing.inc +++ b/src_graphics/world_dressing.inc @@ -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 diff --git a/tools/generators/pack_jackbox.py b/tools/generators/pack_jackbox.py new file mode 100644 index 0000000..f32736d --- /dev/null +++ b/tools/generators/pack_jackbox.py @@ -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() diff --git a/tools/tests/check_jackbox.py b/tools/tests/check_jackbox.py new file mode 100644 index 0000000..d2b7968 --- /dev/null +++ b/tools/tests/check_jackbox.py @@ -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!")