diff --git a/assets/animations/hall/jackbox-ui-v1.bin b/assets/animations/hall/jackbox-ui-v1.bin index 6298317..c0dd49b 100644 Binary files a/assets/animations/hall/jackbox-ui-v1.bin 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 index 4181fa4..51baaa0 100644 Binary files a/assets/animations/hall/jackbox-v1.bin and b/assets/animations/hall/jackbox-v1.bin differ diff --git a/bin/poker.exe b/bin/poker.exe index aecd96c..8cd30e4 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index 5f59a23..44eea94 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "B1D4D027ACE9BDF6B34C4621191E5DAB855D44E384F45F9B0D0E8EC528DF2B28"; + const string ExpectedPokerSha256 = "7488391B71FCEB1C2A2F68C06445CC1421A650793205498BA19E787C9E184297"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index aecd96c..8cd30e4 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 1e44304..f19b049 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/previews_minigames/jukebox_3d_angle.png b/docs/previews_minigames/jukebox_3d_angle.png new file mode 100644 index 0000000..752bb07 Binary files /dev/null and b/docs/previews_minigames/jukebox_3d_angle.png differ diff --git a/docs/previews_minigames/jukebox_3d_close.png b/docs/previews_minigames/jukebox_3d_close.png new file mode 100644 index 0000000..94e5897 Binary files /dev/null and b/docs/previews_minigames/jukebox_3d_close.png differ diff --git a/docs/previews_minigames/jukebox_3d_front.png b/docs/previews_minigames/jukebox_3d_front.png new file mode 100644 index 0000000..542ebbe Binary files /dev/null and b/docs/previews_minigames/jukebox_3d_front.png differ diff --git a/docs/previews_minigames/jukebox_3d_side.png b/docs/previews_minigames/jukebox_3d_side.png new file mode 100644 index 0000000..6830d9e Binary files /dev/null and b/docs/previews_minigames/jukebox_3d_side.png differ diff --git a/poker.exe b/poker.exe index aecd96c..8cd30e4 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/src_graphics/city_bins.inc b/src_graphics/city_bins.inc index 8b8dd8b..b7861ff 100644 --- a/src_graphics/city_bins.inc +++ b/src_graphics/city_bins.inc @@ -7,7 +7,7 @@ ResolveCitySurface3D PROC material:DWORD mov eax,material cmp eax,161 jb @F - cmp eax,162 + cmp eax,163 ja @F invoke ResolveJackboxSurface3D,material ret diff --git a/src_graphics/jackbox.inc b/src_graphics/jackbox.inc index 91ca83f..70e1731 100644 --- a/src_graphics/jackbox.inc +++ b/src_graphics/jackbox.inc @@ -1,323 +1,335 @@ -; ============================================================================== -; 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 +; ============================================================================== +; 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 + ; Footprint: width 0.75m, depth 0.55m, height 1.85m + mov eax, wx + sub eax, FP_1*15/40 + mov x0, eax + add eax, FP_1*15/20 + mov x1, eax + mov eax, wy + sub eax, FP_1*11/40 + mov y0, eax + add eax, FP_1*11/20 + mov y1, eax + + ; Rear: North face (Y = y0) + invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK + ; Sides: West face (X = x0) and East face (X = x1) + invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE + invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE + ; Front: South face (Y = y1) + invoke DrawCityVertical3D, 1, y1, 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*11/20 + ja jackbox_walk_clear + mov eax, px + sub eax, JACKBOX_X + test eax, eax + jns @F + neg eax +@@: + cmp eax, FP_1*15/20 + 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 index 399dbd6..b9bae27 100644 --- a/src_graphics/jackbox_data.inc +++ b/src_graphics/jackbox_data.inc @@ -10,7 +10,8 @@ JACKBOX_Y equ 20*FP_1 JACKBOX_TRACK_COUNT equ 10 MATERIAL_JACKBOX_FRONT equ 161 -MATERIAL_JACKBOX_SIDE equ 162 +MATERIAL_JACKBOX_SIDE equ 162 +MATERIAL_JACKBOX_BACK equ 163 LoadJackboxArt3D PROTO FreeJackboxArt3D PROTO diff --git a/tools/generators/pack_jackbox.py b/tools/generators/pack_jackbox.py index f32736d..baf1cd8 100644 --- a/tools/generators/pack_jackbox.py +++ b/tools/generators/pack_jackbox.py @@ -1,6 +1,14 @@ -"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.""" +"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer. + +Processes high-resolution source images, removes studio grey background with clean +alpha masking and anti-aliasing, aligns 3D cabinet surfaces (Front, Side, Back), +and outputs binary packs (ANM1). +""" +import math from pathlib import Path import struct +import cv2 +import numpy as np from PIL import Image ROOT = Path(__file__).resolve().parents[2] @@ -8,11 +16,56 @@ 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) +def clean_cutout(img_bgr: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Segment studio grey background using connected components from image borders.""" + h, w = img_bgr.shape[:2] + arr = img_bgr.astype(np.float32) + + # Detect neutral studio grey pixels: low saturation and mid-low brightness + color_diff = np.maximum( + np.abs(arr[:, :, 0] - arr[:, :, 1]), + np.maximum(np.abs(arr[:, :, 1] - arr[:, :, 2]), np.abs(arr[:, :, 0] - arr[:, :, 2])) + ) + brightness = np.mean(arr, axis=2) + is_bg_cand = (color_diff < 10) & (brightness >= 18) & (brightness <= 65) + + # Seed from all 4 borders + seed_mask = np.zeros((h, w), dtype=np.uint8) + seed_mask[0, :] = is_bg_cand[0, :] + seed_mask[-1, :] = is_bg_cand[-1, :] + seed_mask[:, 0] = is_bg_cand[:, 0] + seed_mask[:, -1] = is_bg_cand[:, -1] + + num_labels, labels = cv2.connectedComponents(is_bg_cand.astype(np.uint8)) + border_labels = np.unique(labels[seed_mask == 1]) + border_labels = border_labels[border_labels != 0] + + is_bg = np.isin(labels, border_labels) + clean_fg = (~is_bg).astype(np.uint8) + + # Fill internal holes in foreground if any + num_fg, fg_labels = cv2.connectedComponents(clean_fg) + fg_sizes = np.bincount(fg_labels.flatten()) + fg_sizes[0] = 0 + main_fg_label = np.argmax(fg_sizes) + clean_fg = (fg_labels == main_fg_label).astype(np.uint8) + + # Anti-aliased alpha falloff via distance transform + dist_in = cv2.distanceTransform(clean_fg, cv2.DIST_L2, 3) + dist_out = cv2.distanceTransform(1 - clean_fg, cv2.DIST_L2, 3) + alpha = np.clip((dist_in - dist_out + 0.5) * 255.0, 0, 255).astype(np.uint8) + + bgra = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2BGRA) + bgra[:, :, 3] = alpha + return bgra, clean_fg + + +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) - path.write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw)) + raw) + 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") @@ -25,16 +78,42 @@ def main(): 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]) + print("Extracting and segmenting Jukebox textures...") - # 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]) + # 1. Front Texture + front_bgr = cv2.imread(str(front_path)) + front_bgra, front_fg = clean_cutout(front_bgr) + front_tex = cv2.resize(front_bgra, (256, 512), interpolation=cv2.INTER_LANCZOS4) - print("Jackbox asset packaging complete.") + # 2. Side Texture: clean, crop margins, flip horizontally so back is x=0 and front is x=255 + side_bgr = cv2.imread(str(side_path)) + side_bgra, side_fg = clean_cutout(side_bgr) + # Crop to actual jukebox silhouette (remove studio floor and top empty margins) + side_cropped = side_bgra[80:2912, 0:1350] + side_flipped = cv2.flip(side_cropped, 1) + side_tex = cv2.resize(side_flipped, (256, 512), interpolation=cv2.INTER_LANCZOS4) + + # 3. Rear Texture: authentic dark walnut cabinet panel with ventilation slots and arch top + rear_tex = np.zeros((512, 256, 4), dtype=np.uint8) + for y in range(512): + grain = int(math.sin(y * 0.2) * 5 + math.sin(y * 0.05) * 8) + rear_tex[y, :, 0] = np.clip(20 + grain, 0, 255) # B + rear_tex[y, :, 1] = np.clip(28 + grain + 2, 0, 255) # G + rear_tex[y, :, 2] = np.clip(45 + grain + 5, 0, 255) # R + for y in range(120, 220, 10): + rear_tex[y:y+3, 40:216, :3] = 12 # Dark ventilation slots + rear_tex[:, :, 3] = cv2.resize(front_bgra[:, :, 3], (256, 512), interpolation=cv2.INTER_LANCZOS4) + + # 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("Jukebox asset packaging complete.") if __name__ == "__main__": diff --git a/tools/tests/check_jackbox.py b/tools/tests/check_jackbox.py index d2b7968..14c209d 100644 --- a/tools/tests/check_jackbox.py +++ b/tools/tests/check_jackbox.py @@ -14,12 +14,12 @@ def test_packed_assets(): 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 + # 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, 2), f"Bad dimensions: {w}x{h}, {frames} frames" - assert len(wdata) == 20 + raw_len == 20 + 256 * 512 * 4 * 2 + 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 @@ -65,6 +65,7 @@ def test_code_structure(): 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 + assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc # Check procs procs = [ diff --git a/tools/tests/test_jukebox_render_all_angles.py b/tools/tests/test_jukebox_render_all_angles.py new file mode 100644 index 0000000..4e89d4e --- /dev/null +++ b/tools/tests/test_jukebox_render_all_angles.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +""" +Visual simulation of the 3D Jukebox cabinet in the Casino Hall. +Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back). +Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D with alpha transparency. +""" +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), f'Invalid pack: {magic}, {jw}x{jh}, {jframes}' +jpixels = jackbox_raw[20:] + +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, jb_x, jb_y, 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_vertical(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) + # Alpha testing for materials >= 104 (cmp eax, 80000000h; jb city_vertical_next_pixel) + if a < 128: + continue + 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) + + # Footprint: width 0.75m, depth 0.55m, height 1.85m + wx = int(jb_x * FP_1) + wy = int(jb_y * FP_1) + x0 = wx - FP_1 * 15 // 40 + x1 = wx + FP_1 * 15 // 40 + y0 = wy - FP_1 * 11 // 40 + y1 = wy + FP_1 * 11 // 40 + zHigh = FP_1 * 37 // 20 + + # 1. Rear Face (Y = y0) - Frame 2 + draw_vertical(1, y0, x0, x1, 0, zHigh, 2) + # 2. Left Side (X = x0) - Frame 1 + draw_vertical(0, x0, y0, y1, 0, zHigh, 1) + # 3. Right Side (X = x1) - Frame 1 + draw_vertical(0, x1, y0, y1, 0, zHigh, 1) + # 4. Front Face (Y = y1) - Frame 0 + draw_vertical(1, y1, x0, x1, 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__': + jb_x, jb_y = 6.0, 20.0 + # 1. Front View (from South looking North directly at Front) + render_scene(6.0, 22.2, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_front.png') + # 2. 45-degree angle View (from South-East looking North-West) + render_scene(7.2, 21.8, 416, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_angle.png') + # 3. Side View (from East looking West) + render_scene(8.0, 20.0, 128, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_side.png') + # 4. Close-up Front View (standing right at the jukebox) + render_scene(6.0, 21.1, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_close.png') + print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')