fix(jukebox): make cabinet solid rectangular with zero gaps, relocate to East wall beside billiards

This commit is contained in:
Игорь Федосеев 2026-10-25 16:43:43 +03:00
parent 0a85e271e9
commit 6bf2a2885c
15 changed files with 217 additions and 147 deletions

Binary file not shown.

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 187 KiB

BIN
poker.exe

Binary file not shown.

View file

@ -52,60 +52,56 @@ jackbox_surface_missing:
ret ret
ResolveJackboxSurface3D ENDP ResolveJackboxSurface3D ENDP
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
; Footprint: width 0.75m, depth 0.55m, height 1.85m ; Solid rectangular cabinet against East wall beside billiards:
mov eax, wx ; Depth along X: 0.55m (x0 = 30.40m, x1 = 30.95m against wall at 31.0m)
sub eax, FP_1*15/40 ; Width along Y: 0.85m (y0 = 24.575m, y1 = 25.425m, centered at Y = 25.0m)
mov x0, eax ; Height Z: 1.85m
add eax, FP_1*15/20 mov x0, FP_1*304/10
mov x1, eax mov x1, FP_1*3095/100
mov eax, wy mov y0, 25*FP_1 - FP_1*425/1000
sub eax, FP_1*11/40 mov y1, 25*FP_1 + FP_1*425/1000
mov y0, eax
add eax, FP_1*11/20 ; 1. Back Face (against East wall: X = x1)
mov y1, eax invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
; 2. North Side (Y = y0)
; Rear: North face (Y = y0) invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK ; 3. South Side (Y = y1)
; Sides: West face (X = x0) and East face (X = x1) invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE ; 4. Front Face (facing West towards billiards: X = x0)
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
; Front: South face (Y = y1) ; 5. Top Lid (horizontal cap at z = 1.85m)
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT invoke DrawCityHorizontal3D, x0, x1, y0, y1, FP_1*37/20, 110
ret ret
DrawJackboxCabinet3D ENDP DrawJackboxCabinet3D ENDP
RenderJackboxCabinet3D PROC RenderJackboxCabinet3D PROC
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
ret ret
RenderJackboxCabinet3D ENDP RenderJackboxCabinet3D ENDP
JackboxCanWalk3D PROC px:DWORD, py:DWORD JackboxCanWalk3D PROC px:DWORD, py:DWORD
mov eax, py mov eax, px
sub eax, JACKBOX_Y cmp eax, FP_1*303/10
test eax, eax jb jackbox_walk_clear
jns @F cmp eax, 31*FP_1+9830
neg eax ja jackbox_walk_clear
@@: mov eax, py
cmp eax, FP_1*11/20 sub eax, 25*FP_1
ja jackbox_walk_clear test eax, eax
mov eax, px jns @F
sub eax, JACKBOX_X neg eax
test eax, eax @@:
jns @F cmp eax, FP_1*55/100
neg eax ja jackbox_walk_clear
@@: xor eax, eax
cmp eax, FP_1*15/20 ret
jbe jackbox_walk_blocked jackbox_walk_clear:
jackbox_walk_clear: mov eax, 1
mov eax, 1 ret
ret JackboxCanWalk3D ENDP
jackbox_walk_blocked:
xor eax, eax
ret
JackboxCanWalk3D ENDP
PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0 invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0 invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0

View file

@ -5,8 +5,8 @@
MODE_JACKBOX equ 28 MODE_JACKBOX equ 28
INTERACT_JACKBOX equ 48 INTERACT_JACKBOX equ 48
JACKBOX_X equ 6*FP_1 JACKBOX_X equ FP_1*306/10
JACKBOX_Y equ 20*FP_1 JACKBOX_Y equ 25*FP_1
JACKBOX_TRACK_COUNT equ 10 JACKBOX_TRACK_COUNT equ 10
MATERIAL_JACKBOX_FRONT equ 161 MATERIAL_JACKBOX_FRONT equ 161

View file

@ -1,63 +1,124 @@
"""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 Processes high-resolution source images into a solid rectangular cabinet ('прямоугольный шкаф'):
alpha masking and anti-aliasing, aligns 3D cabinet surfaces (Front, Side, Back), - Front: Arched jukebox art framed in rich dark mahogany wood casing (solid 256x512 BGRA).
and outputs binary packs (ANM1). - Side: Cropped rectangular wooden cabinet side panel with chrome accents (solid 256x512 BGRA).
- Back: Matching dark walnut cabinet rear panel with ventilation slots (solid 256x512 BGRA).
- UI: 640x480 BGRA track selector screen.
Eliminates all grey studio backgrounds, transparent gaps, and seam holes.
""" """
import math import math
from pathlib import Path from pathlib import Path
import struct import struct
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/jackbox" SOURCE = ROOT / "assets/sprites/jackbox"
OUT = ROOT / "assets/animations/hall" OUT = ROOT / "assets/animations/hall"
# Deep rich dark walnut / mahogany wood color (BGR)
WOOD_COLOR = np.array([16, 26, 48], dtype=np.uint8)
def clean_cutout(img_bgr: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Segment studio grey background using connected components from image borders.""" def make_solid_front(img_bgr: np.ndarray) -> np.ndarray:
"""Frame the arched jukebox front into a solid rectangular wooden cabinet face."""
h, w = img_bgr.shape[:2] h, w = img_bgr.shape[:2]
arr = img_bgr.astype(np.float32)
# Detect neutral studio grey pixels: low saturation and mid-low brightness # Detect studio grey background
color_diff = np.maximum( diff = np.maximum(
np.abs(arr[:, :, 0] - arr[:, :, 1]), np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 1].astype(int)),
np.maximum(np.abs(arr[:, :, 1] - arr[:, :, 2]), np.abs(arr[:, :, 0] - arr[:, :, 2])) np.maximum(np.abs(img_bgr[:, :, 1].astype(int) - img_bgr[:, :, 2].astype(int)),
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 2].astype(int)))
) )
brightness = np.mean(arr, axis=2) bright = np.mean(img_bgr, axis=2)
is_bg_cand = (color_diff < 10) & (brightness >= 18) & (brightness <= 65) is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
# Seed from all 4 borders # Seed from borders
seed_mask = np.zeros((h, w), dtype=np.uint8) seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg_cand[0, :] seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg_cand[-1, :] seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg_cand[:, 0] seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg_cand[:, -1] seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg_cand.astype(np.uint8)) num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1]) border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0] border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
is_bg = np.isin(labels, border_labels) # Replace studio grey background with dark mahogany wood casing
clean_fg = (~is_bg).astype(np.uint8) front_solid = img_bgr.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
front_solid[:, :, c] = np.clip(
front_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Fill internal holes in foreground if any # Resize to 256x512 BGRA, 100% opaque (no transparency gaps!)
num_fg, fg_labels = cv2.connectedComponents(clean_fg) resized = cv2.resize(front_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
fg_sizes = np.bincount(fg_labels.flatten()) bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
fg_sizes[0] = 0 bgra[:, :, 3] = 255
main_fg_label = np.argmax(fg_sizes) return bgra
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) def make_solid_side(img_bgr: np.ndarray) -> np.ndarray:
bgra[:, :, 3] = alpha """Crop side image to a solid rectangular cabinet side panel."""
return bgra, clean_fg # Crop to actual rectangular cabinet profile:
# y: 90..2910 (removes studio top margin and floor shadow)
# x: 120..1345 (from front edge to back vertical edge)
side_crop = img_bgr[90:2910, 120:1345]
h, w = side_crop.shape[:2]
# Detect studio grey in the top corner outside the curve
diff = np.maximum(
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 1].astype(int)),
np.maximum(np.abs(side_crop[:, :, 1].astype(int) - side_crop[:, :, 2].astype(int)),
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 2].astype(int)))
)
bright = np.mean(side_crop, axis=2)
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
# Fill corner with dark mahogany cabinet wood
side_solid = side_crop.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
side_solid[:, :, c] = np.clip(
side_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Left of texture (x=0) is FRONT edge; right of texture (x=max) is BACK edge.
# Matches along_x from x0 (front) to x1 (back against wall).
resized = cv2.resize(side_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
bgra[:, :, 3] = 255
return bgra
def make_solid_back() -> np.ndarray:
"""Generate a solid dark walnut cabinet rear panel with ventilation slots."""
rear = np.zeros((512, 256, 4), dtype=np.uint8)
for y in range(512):
grain = int(math.sin(y * 0.2) * 4 + math.sin(y * 0.05) * 6)
rear[y, :, 0] = np.clip(int(WOOD_COLOR[0]) + grain, 0, 255) # B
rear[y, :, 1] = np.clip(int(WOOD_COLOR[1]) + grain + 1, 0, 255) # G
rear[y, :, 2] = np.clip(int(WOOD_COLOR[2]) + grain + 3, 0, 255) # R
rear[y, :, 3] = 255 # Fully opaque
for y in range(120, 240, 12):
rear[y:y+3, 32:224, :3] = 10 # Dark ventilation slots
return rear
def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int): def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int):
@ -78,31 +139,18 @@ def main():
assert side_path.exists(), f"Missing {side_path}" assert side_path.exists(), f"Missing {side_path}"
assert ui_path.exists(), f"Missing {ui_path}" assert ui_path.exists(), f"Missing {ui_path}"
print("Extracting and segmenting Jukebox textures...") print("Building solid rectangular Jukebox cabinet textures...")
# 1. Front Texture # 1. Front Texture: solid rectangular face
front_bgr = cv2.imread(str(front_path)) front_bgr = cv2.imread(str(front_path))
front_bgra, front_fg = clean_cutout(front_bgr) front_tex = make_solid_front(front_bgr)
front_tex = cv2.resize(front_bgra, (256, 512), interpolation=cv2.INTER_LANCZOS4)
# 2. Side Texture: clean, crop margins, flip horizontally so back is x=0 and front is x=255 # 2. Side Texture: solid rectangular side panel
side_bgr = cv2.imread(str(side_path)) side_bgr = cv2.imread(str(side_path))
side_bgra, side_fg = clean_cutout(side_bgr) side_tex = make_solid_side(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 # 3. Rear Texture: solid dark walnut panel
rear_tex = np.zeros((512, 256, 4), dtype=np.uint8) rear_tex = make_solid_back()
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 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) pack_bgra(OUT / "jackbox-v1.bin", [front_tex, side_tex, rear_tex], 256, 512)
@ -113,7 +161,7 @@ def main():
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4) ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480) pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480)
print("Jukebox asset packaging complete.") print("Solid Jukebox asset packaging complete.")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -63,8 +63,8 @@ def test_code_structure():
# Check constants # Check constants
assert "MODE_JACKBOX equ 28" in data_inc assert "MODE_JACKBOX equ 28" in data_inc
assert "INTERACT_JACKBOX equ 48" in data_inc assert "INTERACT_JACKBOX equ 48" in data_inc
assert "JACKBOX_X equ 6*FP_1" in data_inc assert "JACKBOX_X equ FP_1*306/10" in data_inc
assert "JACKBOX_Y equ 20*FP_1" in data_inc assert "JACKBOX_Y equ 25*FP_1" in data_inc
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
# Check procs # Check procs

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Visual simulation of the 3D Jukebox cabinet in the Casino Hall. Visual simulation of the solid rectangular 3D Jukebox cabinet beside billiards.
Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back). Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back).
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D with alpha transparency. Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D and DrawCityHorizontal3D.
""" """
import math import math
import struct import struct
@ -56,7 +56,7 @@ def get_tex_pixel(slot, tx, ty):
# 3. Load Jackbox Art pack (ANM1) # 3. Load Jackbox Art pack (ANM1)
jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read() jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read()
magic, jw, jh, jframes, jlen = struct.unpack('<4s4I', jackbox_raw[:20]) 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}' assert magic == b'ANM1' and (jw, jh, jframes) == (256, 512, 3)
jpixels = jackbox_raw[20:] jpixels = jackbox_raw[20:]
def get_jackbox_pixel(frame, tx, ty): def get_jackbox_pixel(frame, tx, ty):
@ -67,7 +67,7 @@ def get_jackbox_pixel(frame, tx, ty):
tableCos = [round(math.cos(i * 2 * math.pi / 512) * FP_1) for i in range(512)] 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)] 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): def render_scene(cam_x, cam_y, cam_angle, out_path):
playerX = int(cam_x * FP_1) playerX = int(cam_x * FP_1)
playerY = int(cam_y * FP_1) playerY = int(cam_y * FP_1)
playerAngle = cam_angle % 512 playerAngle = cam_angle % 512
@ -181,7 +181,7 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8) screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
depthBuffer[y * SCREEN_W + col] = perpWallDist depthBuffer[y * SCREEN_W + col] = perpWallDist
def draw_vertical(axis, coord, loBound, hiBound, zLow, zHigh, frame_idx): def draw_vert(axis, coord, loBound, hiBound, zLow, zHigh, frame_idx):
for col in range(SCREEN_W): for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col] ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256: if abs(ray_dir) < 256:
@ -218,30 +218,57 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
if srcY < 0 or srcY >= jh: if srcY < 0 or srcY >= jh:
continue continue
r, g, b, a = get_jackbox_pixel(frame_idx, srcX, srcY) 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]: if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8) 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 # 1. Pool table geometry: X in [26.0, 28.0], Y in [24.0, 26.0], Z in [0, 0.85m]
wx = int(jb_x * FP_1) def draw_pool_side(axis, coord, lo, hi, z0, z1):
wy = int(jb_y * FP_1) for col in range(SCREEN_W):
x0 = wx - FP_1 * 15 // 40 ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
x1 = wx + FP_1 * 15 // 40 if abs(ray_dir) < 256:
y0 = wy - FP_1 * 11 // 40 continue
y1 = wy + FP_1 * 11 // 40 orig = playerX if axis == 0 else playerY
zHigh = FP_1 * 37 // 20 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 < lo or along >= hi:
continue
top = (HALL_STANDING_EYE - z1) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - z0) * SCREEN_H // depth + HALF_H
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
if depth < depthBuffer[row * SCREEN_W + col]:
depthBuffer[row * SCREEN_W + col] = depth
screen_pixels[row * SCREEN_W + col] = ((50 * shade) >> 8, (25 * shade) >> 8, (15 * shade) >> 8)
# 1. Rear Face (Y = y0) - Frame 2 draw_pool_side(1, int(24.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
draw_vertical(1, y0, x0, x1, 0, zHigh, 2) draw_pool_side(1, int(26.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
# 2. Left Side (X = x0) - Frame 1 draw_pool_side(0, int(26.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
draw_vertical(0, x0, y0, y1, 0, zHigh, 1) draw_pool_side(0, int(28.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
# 3. Right Side (X = x1) - Frame 1
draw_vertical(0, x1, y0, y1, 0, zHigh, 1) # 2. Solid Rectangular Jukebox Cabinet (at East wall, next to billiards table!)
# 4. Front Face (Y = y1) - Frame 0 # Depth: X in [30.40m .. 30.95m] (back against East wall at 31.0m)
draw_vertical(1, y1, x0, x1, 0, zHigh, 0) # Width: Y in [24.575m .. 25.425m] (centered at Y = 25.0m)
# Height: Z in [0 .. 1.85m]
x0 = int(30.40 * FP_1)
x1 = int(30.95 * FP_1)
y0 = int(24.575 * FP_1)
y1 = int(25.425 * FP_1)
zHigh = int(1.85 * FP_1)
# Back Face (against wall: X = x1) - Frame 2
draw_vert(0, x1, y0, y1, 0, zHigh, 2)
# North Side (Y = y0) - Frame 1
draw_vert(1, y0, x0, x1, 0, zHigh, 1)
# South Side (Y = y1) - Frame 1
draw_vert(1, y1, x0, x1, 0, zHigh, 1)
# Front Face (facing West towards billiards table: X = x0) - Frame 0
draw_vert(0, x0, y0, y1, 0, zHigh, 0)
img = Image.new('RGB', (SCREEN_W, SCREEN_H)) img = Image.new('RGB', (SCREEN_W, SCREEN_H))
img.putdata(screen_pixels) img.putdata(screen_pixels)
@ -249,13 +276,12 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
print(f'Rendered: {out_path.name}') print(f'Rendered: {out_path.name}')
if __name__ == '__main__': if __name__ == '__main__':
jb_x, jb_y = 6.0, 20.0 # 1. Front View: Standing near billiards at (26.0, 25.0) looking East (angle=0) past table at Jukebox
# 1. Front View (from South looking North directly at Front) render_scene(26.0, 25.0, 0, DOCS_DIR / 'jukebox_3d_front.png')
render_scene(6.0, 22.2, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_front.png') # 2. Angle View: Standing at (27.5, 22.5) looking South-East (angle=480) at pool table and Jukebox
# 2. 45-degree angle View (from South-East looking North-West) render_scene(27.5, 22.5, 480, DOCS_DIR / 'jukebox_3d_angle.png')
render_scene(7.2, 21.8, 416, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_angle.png') # 3. Close-up View: Standing at (29.2, 25.0) looking East (angle=0) right at Jukebox
# 3. Side View (from East looking West) render_scene(29.2, 25.0, 0, DOCS_DIR / 'jukebox_3d_close.png')
render_scene(8.0, 20.0, 128, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_side.png') # 4. Side View: Standing at (30.0, 27.5) looking North (angle=384) at South side of Jukebox
# 4. Close-up Front View (standing right at the jukebox) render_scene(30.0, 27.5, 384, DOCS_DIR / 'jukebox_3d_side.png')
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/!') print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')