feat(audio): implement interactive 3D jukebox (jackbox) and background music playback
This commit is contained in:
parent
5aec3ded9f
commit
82f3aa9437
22 changed files with 637 additions and 3 deletions
BIN
assets/animations/hall/jackbox-ui-v1.bin
Normal file
BIN
assets/animations/hall/jackbox-ui-v1.bin
Normal file
Binary file not shown.
BIN
assets/animations/hall/jackbox-v1.bin
Normal file
BIN
assets/animations/hall/jackbox-v1.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/sprites/jackbox/Jackbox_UI.jpg
Normal file
BIN
assets/sprites/jackbox/Jackbox_UI.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/sprites/jackbox/Jackbox_front.jpg
Normal file
BIN
assets/sprites/jackbox/Jackbox_front.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
assets/sprites/jackbox/Jackbox_side.jpg
Normal file
BIN
assets/sprites/jackbox/Jackbox_side.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
BIN
bin/poker.exe
BIN
bin/poker.exe
Binary file not shown.
|
|
@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
|
|||
{
|
||||
static class Program
|
||||
{
|
||||
const string ExpectedPokerSha256 = "C3D857FDBF7597C976B16C5021D7F07951628E54F86FF2B7BB53F8A83FDE11CA";
|
||||
const string ExpectedPokerSha256 = "B1D4D027ACE9BDF6B34C4621191E5DAB855D44E384F45F9B0D0E8EC528DF2B28";
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
|
|
|
|||
Binary file not shown.
14
casino3d.inc
14
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
|
||||
|
|
|
|||
BIN
dist/Assembler Casino.exe
vendored
BIN
dist/Assembler Casino.exe
vendored
Binary file not shown.
47
docs/JACKBOX.md
Normal file
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
poker.exe
BIN
poker.exe
Binary file not shown.
BIN
poker.obj
BIN
poker.obj
Binary file not shown.
|
|
@ -44,5 +44,6 @@ close_legacy_sound:
|
|||
cmp esi, legacySoundCount3D
|
||||
jb close_legacy_sound
|
||||
call StopTowerMusic
|
||||
call StopJukeboxTrack3D
|
||||
ret
|
||||
CloseLegacySounds3D ENDP
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
323
src_graphics/jackbox.inc
Normal file
323
src_graphics/jackbox.inc
Normal 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
|
||||
78
src_graphics/jackbox_data.inc
Normal file
78
src_graphics/jackbox_data.inc
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
41
tools/generators/pack_jackbox.py
Normal file
41
tools/generators/pack_jackbox.py
Normal 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()
|
||||
114
tools/tests/check_jackbox.py
Normal file
114
tools/tests/check_jackbox.py
Normal 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!")
|
||||
Loading…
Add table
Reference in a new issue