fix(jukebox): remove studio grey background, fix 3D texture orientation and cabinet rendering

This commit is contained in:
Игорь Федосеев 2026-10-25 16:30:40 +03:00
parent 82f3aa9437
commit 0a85e271e9
17 changed files with 696 additions and 342 deletions

Binary file not shown.

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
poker.exe

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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__":

View file

@ -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 = [

View file

@ -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/!')