# -*- coding: utf-8 -*- import math import struct from PIL import Image SCREEN_W = 640 SCREEN_H = 480 HALF_W = 320 HALF_H = 240 from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] FP_1 = 65536 FP_HALF = 32768 MAP_WIDTH = 32 MAP_HEIGHT = 32 # Load map with open(REPO_ROOT / 'casino3d_data.inc', 'r', encoding='latin1') as f: text = f.read() import re 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) # Load 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 b, g, r, a = tex_raw[idx:idx+4] return (r, g, b, a) # Angle table (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)] # Player State playerX = 16 * FP_1 + FP_HALF playerY = 16 * FP_1 + FP_HALF playerAngle = 384 # Facing North 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 # 1. Ceiling and Floor for y in range(HALF_H, SCREEN_H): floorRow = y 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): # Floor tx = (fX >> 9) & 127 ty = (fY >> 9) & 127 r, g, b, _ = get_tex_pixel(0, tx, ty) r = (r * shade) >> 8 g = (g * shade) >> 8 b = (b * shade) >> 8 screen_pixels[floorRow * SCREEN_W + x] = (r, g, b) # Ceiling cx = (fX >> 10) & 127 cy = (fY >> 10) & 127 cr, cg, cb, _ = get_tex_pixel(5, cx, cy) cr = (cr * cShade) >> 8 cg = (cg * cShade) >> 8 cb = (cb * cShade) >> 8 screen_pixels[(SCREEN_H - 1 - floorRow) * SCREEN_W + x] = (cr, cg, cb) fX += fStepX fY += fStepY # 2. Walls for col in range(SCREEN_W): camX = (2 * col - SCREEN_W) * FP_1 // SCREEN_W rayDirX = playerDirX + (playerPlaneX * camX // FP_1) rayDirY = playerDirY + (playerPlaneY * camX // FP_1) 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 < MAP_WIDTH and 0 <= mapY < MAP_HEIGHT: tile = casinoMap[mapY * 32 + mapX] if tile != 0: hit = tile else: hit = 1 if side == 0: perpWallDist = sideDistX - deltaDistX else: perpWallDist = 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) if side == 0: wallHit = playerY + ((perpWallDist * rayDirY) >> 16) else: wallHit = 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) r = (r * shade) >> 8 g = (g * shade) >> 8 b = (b * shade) >> 8 screen_pixels[y * SCREEN_W + col] = (r, g, b) # 3. Sprites sprites = [ {'name': 'VIP Dancer 1', 'x': 13 * FP_1 + FP_HALF, 'y': 4 * FP_1, 'tex': 10}, {'name': 'VIP Dancer 2', 'x': 16 * FP_1 + FP_HALF, 'y': 3 * FP_1 + FP_HALF, 'tex': 10}, {'name': 'VIP Dancer 3', 'x': 19 * FP_1 + FP_HALF, 'y': 4 * FP_1, 'tex': 10}, {'name': 'Blackjack', 'x': 12 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 31}, {'name': 'Poker', 'x': 19 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 35}, {'name': 'Casino Roulette', 'x': 16 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 32}, {'name': 'Classic Roulette', 'x': 16 * FP_1 + FP_HALF, 'y': 12 * FP_1, 'tex': 32}, {'name': 'Slots 777', 'x': 5 * FP_1, 'y': 7 * FP_1, 'tex': 24}, {'name': 'Slots CloverPit', 'x': 5 * FP_1, 'y': 10 * FP_1, 'tex': 24}, {'name': 'Classic Slot', 'x': 5 * FP_1, 'y': 13 * FP_1, 'tex': 24}, {'name': 'Umamusume', 'x': 5 * FP_1, 'y': 16 * FP_1, 'tex': 36}, {'name': 'DBD Skillcheck', 'x': 5 * FP_1, 'y': 19 * FP_1, 'tex': 36}, {'name': 'Cashier / ATM', 'x': 27 * FP_1, 'y': 7 * FP_1, 'tex': 33}, {'name': 'VIP Boutique', 'x': 27 * FP_1, 'y': 10 * FP_1, 'tex': 30}, {'name': 'Online Casino', 'x': 27 * FP_1, 'y': 13 * FP_1, 'tex': 36}, {'name': 'Achievements', 'x': 27 * FP_1, 'y': 16 * FP_1, 'tex': 36}, {'name': 'Stats Board', 'x': 27 * FP_1, 'y': 19 * FP_1, 'tex': 36}, {'name': 'Durak', 'x': 12 * FP_1, 'y': 23 * FP_1, 'tex': 37}, {'name': 'Buckshot Roulette', 'x': 16 * FP_1, 'y': 23 * FP_1, 'tex': 34}, {'name': 'Russian Roulette', 'x': 20 * FP_1, 'y': 23 * FP_1, 'tex': 34}, {'name': 'Craps / Dice', 'x': 12 * FP_1, 'y': 26 * FP_1, 'tex': 38}, {'name': 'Skulls & Cups', 'x': 16 * FP_1, 'y': 26 * FP_1, 'tex': 38}, {'name': 'Coin Flip', 'x': 20 * FP_1, 'y': 26 * FP_1, 'tex': 38}, {'name': 'Training', 'x': 10 * FP_1, 'y': 14 * FP_1, 'tex': 37}, {'name': 'Observer AI', 'x': 22 * FP_1, 'y': 14 * FP_1, 'tex': 37}, {'name': 'Daily Tasks', 'x': 27 * FP_1, 'y': 22 * FP_1, 'tex': 36}, {'name': 'Settings', 'x': 27 * FP_1, 'y': 25 * FP_1, 'tex': 36}, {'name': 'Hell Tower Gate', 'x': 16 * FP_1, 'y': 29 * FP_1, 'tex': 39}, ] # Calculate distances for s in sprites: dx = (s['x'] - playerX) >> 8 dy = (s['y'] - playerY) >> 8 s['distSq'] = dx * dx + dy * dy # Sort far to near sprites.sort(key=lambda s: s['distSq'], reverse=True) det = ((playerPlaneX * playerDirY) >> 16) - ((playerPlaneY * playerDirX) >> 16) if det == 0: det = 1 for s in sprites: relX = s['x'] - playerX relY = s['y'] - playerY # Standard Lodev inverse camera transform: # transX = (dirY * relX - dirX * relY) / det # transY = (-planeY * relX + planeX * relY) / det numX = ((playerDirY * relX) >> 16) - ((playerDirX * relY) >> 16) transX = (numX << 16) // det numY = ((-playerPlaneY * relX) >> 16) + ((playerPlaneX * relY) >> 16) transY = (numY << 16) // det # Discard sprites behind camera if transY <= 2048: continue sprScreenX = HALF_W + (transX * HALF_W) // transY sprH = (SCREEN_H * FP_1) // transY sprW = sprH drawStartY = max(0, HALF_H - sprH // 2) drawEndY = min(SCREEN_H - 1, HALF_H + sprH // 2) drawStartX = max(0, sprScreenX - sprW // 2) drawEndX = min(SCREEN_W - 1, sprScreenX + sprW // 2) shade = max(40, min(255, 255 - (transY >> 12))) for stripe in range(drawStartX, drawEndX): if 0 <= stripe < SCREEN_W and transY < zBuffer[stripe]: texX = ((stripe - (sprScreenX - sprW // 2)) * 128 // sprW) & 127 for y in range(drawStartY, drawEndY): texY = ((y - (HALF_H - sprH // 2)) * 128 // sprH) & 127 r, g, b, _ = get_tex_pixel(s['tex'], texX, texY) # Chroma key check: Magenta (0xFF00FF) or pure black (0,0,0) if (r > 240 and g < 20 and b > 240) or (r == 0 and g == 0 and b == 0): continue r = (r * shade) >> 8 g = (g * shade) >> 8 b = (b * shade) >> 8 screen_pixels[y * SCREEN_W + stripe] = (r, g, b) # Save image img = Image.new('RGB', (SCREEN_W, SCREEN_H)) img.putdata(screen_pixels) out_path = REPO_ROOT / 'docs' / 'render_view.png' img.save(out_path) print(f'{out_path} saved successfully!')