Compare commits

...

2 commits

Author SHA1 Message Date
d0cee618cf Merge branch 'origin/main' into main: integrate charms system with hall expansion, cabaret and terminals 2026-10-18 22:02:50 +03:00
797ee366dc feat: implement upper lounge, manual stairs, cabaret, and casino terminals
- Native player-controlled stairs, upper lounge bar, and Hell Tower infernal portal
- Cabaret stage with musical acts, audience seating, and interaction
- Dedicated cash/trophy wall terminals with native balance and achievement screens
- Projecting 3D wall lanterns with ray/plane intersection
- Six roaming guests with seated poses and card rank rendering updates
- Synchronized launcher hash and updated binaries
2026-10-18 21:36:12 +03:00
80 changed files with 4630 additions and 208 deletions

8
.gitignore vendored
View file

@ -15,6 +15,14 @@ __pycache__/
/build/actions/
/build/arrival/
/build/billiards/
/build/card-ranks/
/build/terminals/
/build/hall-expansion/
/build/guest-cast-preview.png
/build/cabaret-ensemble-preview.png
/build/cabaret-musicians-preview.png
/build/upper-motion-preview.png
/build/*.before-expansion
/build/buckshot/
/build/coin/
/build/dice/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,53 @@
{
"tool": "built-in image_gen",
"runtime": "MASM32 only; Python files are offline packers and native-test orchestration",
"assets": [
{
"path": "assets/sprites/hall/cabaret-musicians-v1.png",
"prompt": "Production sprite sheet for a retro realistic detailed hand-painted pixel art casino cabaret, transparent alpha background. EXACTLY TWO columns and FOUR rows, EIGHT clearly separated cells, generous transparent gaps, no grid lines, no captions. LEFT COLUMN in all four rows: the SAME adult male pianist in a black tuxedo seated on a proper piano bench AT THE KEYBOARD of the SAME beautiful glossy black BABY GRAND PIANO, entire instrument, all three legs, pedals, open triangular lid, white and black keys and pianist fully visible. Audience front three-quarter view. Pianist sits at the LEFT end of the piano facing RIGHT toward its keyboard; both forearms naturally extend to the actual keys immediately in front of his body, fingers touch keys. The piano's curved body and raised lid extend to his RIGHT. His head looks down toward his hands on the keys, he is not turned sideways away from the keyboard. This is ONE coherent combined pianist-and-piano sprite, not detached pieces and not a rectangular counter. RIGHT COLUMN in all four rows: SAME adult male jazz guitarist, burgundy waistcoat, rolled-up white shirt sleeves, black trousers, full body standing, holding a golden-brown hollow-body jazz guitar in natural playing position. Left hand on guitar neck, right hand over strings. Rows1,2,3,4 are successive SUBTLE playing poses, coherent placement, identity, piano shape and dimensions; no camera movement and no scale change. Warm amber stage light with deep burgundy shadows, sharp detailed Doom-era bitmap game art, NOT cartoon, no blurry painted scenery. Entire piano, bench, feet, head, guitar and neck remain inside each cell. Truly transparent backdrop, no floor, no background scenery, no particles, no text.",
"transparencyEditPrompt": "Edit this sprite sheet ONLY to remove the brown background completely. Make EVERY background pixel genuinely transparent alpha, including all brown glows, shadows between cells, spaces under piano and bench, gaps around legs and inside the piano silhouette. Preserve the exact eight coherent musician sprites, all their colours, all black piano and guitar details, current pixel detail, identities, positions and sizes unchanged. The four left sprites each include pianist seated AT the actual grand piano keyboard, hands on keys; preserve the whole piano and bench. The four right sprites are the guitarist. Do not add any backdrop, glow, floor, frame, vignette or text. Transparent cutouts only.",
"approvedLayout": {
"size": [
1024,
1536
],
"rows": 4,
"columnBoundaries": [
0,
640,
1024
],
"anchor": "common opaque bounds across all four poses per performer"
},
"outputs": [
"assets/animations/hall/cabaret-piano-v1.bin",
"assets/animations/hall/cabaret-guitar-v1.bin"
]
},
{
"path": "assets/sprites/hall/guest-cast-v1.png",
"approvedLayout": {
"columns": 6,
"rows": 5,
"framesPerGuest": 6,
"walkMapping": [
1,
2,
3,
2
]
},
"output": "assets/animations/hall/guest-cast-v1.bin"
},
{
"path": "assets/sprites/hall/cabaret-ensemble-v1.png",
"prompt": "A production sprite sheet on a genuinely transparent alpha background, for a retro realistic hand-painted detailed 1990s casino PC game. EXACTLY 3 columns and 4 rows, 12 separate full body sprites, precisely evenly spaced cells, no grid lines or text. SAME character in each COLUMN: column1 adult woman cabaret jazz singer, silver floor-length evening gown, short dark wavy hair, holding a handheld vintage microphone; column2 adult male pianist in black tuxedo seated on a small piano stool, arms extended playing invisible keyboard in front of him, NO PIANO included; column3 adult woman saxophonist in black and gold evening trouser outfit, holding brass saxophone. ROW1 base performing pose, ROW2 small natural alternate pose, ROW3 expressive performing pose, ROW4 returning pose. Consistent costume, face, camera angle, scale and foot baseline throughout each column. Entire heads, shoes, sax and microphone in each cell, generous empty margins, do not touch adjacent cells. Warm amber stage highlights and burgundy shadows. Straight-on audience eye view. No scenery, no stage floor, no spotlight beam, no piano, no frame, no decorative particles. Sharp pixel-art edges, not blurry and not cartoon.",
"output": "assets/animations/hall/cabaret-ensemble-v1.bin"
},
{
"path": "assets/sprites/hall/upper-materials-v1.png",
"prompt": "Production environment texture atlas for detailed retro realistic pixel art casino, EXACT 2 columns by 2 rows of square seamless material tiles, no perspective, orthographic flat material scan, crisp hand-painted bitmap texture, no captions, no borders between tiles. Top-left: dark patinated steel service door surface, riveted broad metal panels, subtle scratches and worn paint, NO handle or opening, no text. Top-right: luxurious deep teal and charcoal marble countertop with fine pale natural veins, no objects, no lamps. Bottom-left: dark burgundy geometric Art Deco carpet with muted gold and cool green fine ornament, seamless pattern at small scale, not wood. Bottom-right: midnight teal casino ceiling with understated embossed square coffer pattern and warm brass thin seams, seamless, not a wall or outdoor facade. Entire image opaque, all four tiles equally sized and cleanly separated, no people, no bottles, no furniture, no windows, no logos.",
"output": "assets/animations/hall/upper-materials-v1.bin"
}
]
}

View file

@ -0,0 +1,62 @@
{
"tool": "built-in image_gen",
"runtime": "All rendering, physics, camera, interaction and animation timing are MASM32; these Python packers are offline tools only.",
"assets": [
{
"path": "assets/sprites/hall/guest-cast-v1.png",
"output": "assets/animations/hall/guest-cast-v1.bin",
"normalization": "Mirror only the emerald guest's seated pose so the paired upper-lounge chairs face toward their shared table; idle and walking frames stay unchanged."
},
{
"path": "assets/sprites/hall/upper-portal-v2.png",
"outputs": [
"assets/animations/hall/upper-portal-v2.bin",
"assets/animations/hall/upper-levers-v2.bin"
],
"usage": "Preserved source and offline inactive-portal reference; superseded for runtime animation."
},
{
"path": "assets/sprites/hall/upper-furniture-v2.png",
"outputs": [
"assets/animations/hall/upper-games-v2.bin",
"assets/animations/hall/upper-bar-v2.bin"
]
},
{
"path": "assets/sprites/hall/upper-portal-v3.png",
"prompt": "Use the supplied atlas as a STRICT identity reference for the gothic arched portal in its first four cells. Generate a NEW game animation sprite sheet: EXACTLY THREE columns and FOUR rows, TWELVE separate full arch sprites in row-major playback order, generous transparent margins between cells, no text, no grid. Every frame shows the SAME complete activated black iron and brass arched portal from exactly the same front three-quarter camera, with the same red gem at its apex, same columns, same base, same outline, same size and fixed ground anchor. Only the crimson/amber fire vortex INSIDE the arch animates: a coherent clockwise flowing spiral advances gradually across twelve frames; glow gently pulses, a few tiny sparks are contained inside the opening, loop frame12 naturally into frame1. The solid frame must NEVER change shape, tilt, move or scale. Entire arch, top gem and base visible in every cell, no clipping. Match the supplied detailed realistic 1990s retro bitmap game sprite style. Genuinely transparent alpha background, including all space around the arch, no brown backdrop, no floor, no room, no scenery, no other objects and NO LEVERS on this new sheet. Each portal cutout isolated cleanly.",
"outputs": [
"assets/animations/hall/upper-portal-v3.bin"
],
"layout": {
"columns": 3,
"rows": 4
},
"normalization": "Rigid arch locked; only generated interior vortex varies. One inactive frame plus twelve active frames, 256x512 for native power-of-two wall sampling."
},
{
"path": "assets/sprites/hall/upper-levers-v3.png",
"prompt": "Use ONLY the two floor-mounted lever sprites in the bottom row of this supplied atlas as strict visual reference. Generate a six-frame mechanical lever pull animation sheet, EXACTLY THREE columns TWO rows, six isolated full pedestal sprites in row-major time order. Same ornate black iron and brass pedestal, same bolted broad base, same crimson inset, same red teardrop handle attached to the same round side pivot. Fixed camera, fixed pedestal dimensions, fixed bottom anchor; the pedestal itself must not change between frames. Frame1 handle fully UP; frames2,3,4,5 show SMALL successive angular steps of the handle rotating DOWN clockwise around the pivot; frame6 fully DOWN diagonally toward lower-right, green indicator illuminated. The handle is a rigid lever attached to pivot, never detached. Final green light only comes on in last frame. Fine crisp realistic retro hand-painted 1990s bitmap game sprite detail, match supplied style. Generous empty margins, entire base and all handle positions in every cell, no portal, no room, no floor, no labels, no text, no captions or grid. GENUINELY transparent alpha background without a brown backdrop or glow outside the objects.",
"outputs": [
"assets/animations/hall/upper-levers-v3.bin"
],
"layout": {
"columns": 3,
"rows": 2
},
"normalization": "Common bounds and foot anchor. Six positions across a 600ms native lever pull."
},
{
"path": "assets/sprites/hall/upper-braziers-v3.png",
"prompt": "Game sprite animation atlas, exactly TWO columns TWO rows, FOUR full-body isolated successive frames of the SAME gothic floor brazier. A heavy black wrought iron tripod pedestal with a broad dark brass bowl and pointed iron ribs, old chains hanging tightly from bowl, deep glowing coals and a sinister CRIMSON red fire with amber core. The four frames animate the flames flickering naturally while the identical rigid metal bowl, chains, legs, scale, viewpoint and ground anchor stay fixed. Whole tall pedestal and full flames visible in every cell, generous gaps and margins. Realistic detailed hand-painted 1990s retro pixel game art for a dark casino's infernal portal alcove. No people, no skulls, no creatures, no room, no floor, no writing or grid. Truly transparent alpha background; no brown backdrop, no light halo or pixels outside the sprite silhouette.",
"outputs": [
"assets/animations/hall/upper-braziers-v3.bin"
],
"layout": {
"columns": 2,
"rows": 2
},
"normalization": "Four flame phases; rigid lower pedestal retained."
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,26 @@
{
"tool": "Built-in image_gen",
"purpose": "Retro casino kiosk front surfaces, lantern glass and native fullscreen kiosk background. Game runtime remains MASM x86.",
"assets": [
{
"file": "cash-front-v1.png",
"prompt": "Use case: stylized-concept. Asset type: front-face texture for a human-scale 2.5D retro casino cash terminal, not a scene. Create ONE full-bleed rectangular front elevation, perfectly orthographic, width-to-height ratio 1:2, edges at image borders. A carefully crafted 1930s American casino Art Deco cash/balance kiosk: aged dark walnut cabinet, engraved brass corners and trim, green glass CRT screen occupying upper central third, recessed metal numerical keypad below, slim receipt slot, brass coin ornament on lower door, realistic material construction interpreted as crisp hand-painted 1990s adventure-game pixel art, consistent individual pixels, restrained details, excellent readability, not photorealistic, not flat primitive rectangles. Screen dark emerald with subtle luminous center and NO text or symbols, a blank horizontal brass title plaque above. Light from upper left. No perspective sides, no background, no floor, no shadow beyond boundary, no people, no labels, no letters, no watermark. Whole front is a rectangle filled with cabinet material, not transparent."
},
{
"file": "trophy-front-v1.png",
"prompt": "Use case: stylized-concept. Asset type: front-face texture for a human-scale 2.5D retro casino achievements terminal. ONE full-bleed rectangular front elevation, perfectly orthographic, aspect ratio 1:2, cabinet edges touch image borders. Crafted vintage casino Art Deco walnut-and-brass trophy kiosk, same 1930s furniture family as a brass-trimmed cash machine: blank brass header plaque, broad recessed burgundy-black glass display in upper third, a small golden laurel and trophy relief inside the LOWER door, beautiful carved dark walnut panels and brass corner caps, two inset physical arrow buttons below the glass. Glass screen must be blank and very dark, NO text or icons on the screen. Crisp hand-painted 1990s adventure-game pixel art, solid dimensional material shading, not photorealism, not flat vector. Neutral warm light from upper left. No people, no perspective sides, no separate objects, no backdrop or floor, no protruding shadows, no labels/letters/digits/watermark. Completely opaque rectangular texture."
},
{
"file": "lantern-glass-v1.png",
"prompt": "Use case: stylized-concept. Asset type: seamless front elevation texture for the rectangular glass body of a small wall-mounted casino lantern. ONE completely opaque full-bleed rectangle, width-to-height ratio 1:2. Aged dark bronze cage with slim brass edge rails and two slender crossbars; warm ivory-amber glowing frosted glass panes fill the middle. Rich but readable crisp hand-painted 1990s adventure-game pixel art consistent with dark walnut/brass casino furniture; controlled pixel clusters, luminous glass, dark solid borders. The entire image is the lantern body front surface, no mounting arm, no roof, no perspective side, no background, no transparency, no detached glow or shadows outside object, no people, no text, no symbols, no watermark. It will wrap a genuine three-dimensional small box with separate mounting arm and roof."
},
{
"file": "display-v1.png",
"prompt": "Use case: stylized-concept. Asset type: full-screen interactive kiosk background for a retro 2.5D casino game, landscape 4:3. Straight-on close-up of the upper display of a 1930s Art Deco casino terminal, carved walnut narrow outer frame and thin aged brass trim, large dark nearly uniform green-black glass screen occupying x=6%..94%, y=10%..86% of the composition, perfectly empty to display 8-bit Russian text through the game renderer. Two inset brass triangular paging buttons at the bottom left and bottom right outside the main screen. Narrow blank brass title strip along the top. A few tactile screws and very restrained reflections at the extreme edges only. Crisp hand-painted pixel art, material shading and carved relief, late-1990s adventure game furniture. No perspective, no people, no keyboard, no letters, no digits, no icons inside the screen, no decorative objects obscuring it, no watermark. Screen must remain extremely dark and flat, free of grain, glare or bright gradients in the text area. The display fills the whole frame, not a small monitor floating against background."
},
{
"file": "body-side-rear-v2.png",
"prompt": "Use case: stylized-concept. Asset type: dedicated SIDE and REAR surface texture of a vintage casino COMPUTER terminal; ONE full-bleed perfectly orthographic rectangle, aspect ratio 1:2. Dark walnut veneered equipment housing matching an Art Deco brass-trimmed 1930s casino cash kiosk. Mostly clean continuous vertical woodgrain, narrow brass corner strips at left and right edges, small recessed dark bronze ventilation grille in the upper quarter, four tiny service screws, flush lower rectangular metal service hatch without a handle. It must read as a COMPUTER EQUIPMENT ENCLOSURE, not furniture storage, not a bar or a refrigerator. Crisp hand-painted 1990s adventure-game pixel art with controlled pixel clusters, consistent scale, subtle surface wear, warm upper-left light. NO shelves, NO bottles, NO alcohol, NO glasses, NO dishes, NO contents, NO doors with handles, NO countertop, NO screen, NO keyboard, NO letters, NO numbers, NO text, NO logos, NO background, NO perspective, NO separate objects. Opaque texture fills every pixel to the borders."
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "3CE946233ABE1D9139501EB56A75B2BE0D9A99714048C5F0A56A150D35F2295C";
const string ExpectedPokerSha256 = "071F09A64F0E2F3EC267AC1C963D9849AA3A1DBDA81B3417C90E3499C1D5102F";
[STAThread]
static void Main()

Binary file not shown.

View file

@ -807,6 +807,30 @@ col_loop:
sar eax, 16
mov mapY, eax
cmp portalRendering3D,0
je wall_ray_start_ready
mov ebx,col
mov ecx,portalClipDepth3D[ebx*4]
test ecx,ecx
jnz @F
mov zBuffer[ebx*4],7FFFFFFFh
jmp next_col
@@:
; Start DDA just inside the destination aperture, never in its rear wall.
add ecx,FP_1/100
mov eax,rayDirX
imul ecx
shrd eax,edx,16
add eax,playerX
sar eax,16
mov mapX,eax
mov eax,rayDirY
imul ecx
shrd eax,edx,16
add eax,playerY
sar eax,16
mov mapY,eax
wall_ray_start_ready:
mov eax, rayDirX
test eax, eax
jnz calc_delta_x
@ -1166,6 +1190,20 @@ wall_sample_material:
mov eax, [ebx+eax*4]
jmp wall_shade_pixel
wall_sample_indoor_hd:
cmp edx,122
jb wall_sample_existing_hd
cmp edx,125
ja wall_sample_existing_hd
mov ebx,upperMaterialArt3D.pixels
test ebx,ebx
jz wall_upper_material_missing
sub edx,122
shl edx,18
add ebx,edx
jmp wall_room_hd_coordinates
wall_upper_material_missing:
mov edx,102
wall_sample_existing_hd:
cmp edx, 80
jb wall_sample_standard
cmp edx, 103
@ -1181,6 +1219,7 @@ wall_sample_room_hd:
sub edx, 80
shl edx, 18
add ebx, edx
wall_room_hd_coordinates:
mov eax, edi
shr eax, 15
and eax, 255
@ -1373,6 +1412,12 @@ RenderMinimap PROC uses ebx esi edi
radar_spr_loop:
cmp esi, NUM_SPRITES
jge draw_player_radar
cmp esi,17
je radar_skip_sprite
cmp esi,19
je radar_skip_sprite
cmp esi,21
je radar_skip_sprite
mov eax, esi
imul eax, SIZEOF SpriteObject
mov eax, [casinoSprites + eax].posX
@ -1391,6 +1436,7 @@ radar_spr_loop:
add eax, radarY
invoke DrawPixel, ebx, eax, 00FFAA00h
radar_skip_sprite:
inc esi
jmp radar_spr_loop
@ -2397,18 +2443,12 @@ RenderShopGame ENDP
; IN-ENGINE 2.5D MINI-GAME: ATM TERMINAL
; ==============================================================================
HandleATMInput PROC
call HandleTerminal3D
ret
HandleATMInput ENDP
RenderATMGame PROC
invoke DrawBox, 0, 0, 320, 240, 00172120h
invoke DrawString8x8, 24, 30, OFFSET textATM3D, 00D8BC6Ah
invoke DrawString8x8, 24, 70, OFFSET textBank3D, 00FFFFFFh
invoke FormatNumber, bankroll, OFFSET hudNumBuffer
invoke DrawString8x8, 96, 70, OFFSET hudNumBuffer, 0089DAB2h
invoke DrawString8x8, 24, 115, OFFSET textATMNote3D, 00B7C8C1h
invoke DrawString8x8, 24, 140, OFFSET textATMNote2_3D, 00B7C8C1h
invoke DrawString8x8, 24, 208, OFFSET textBack3D, 00D8BC6Ah
call RenderTerminal3D
ret
RenderATMGame ENDP
@ -4387,8 +4427,11 @@ unlock_done:
UnlockMouse3D ENDP
UpdateMouseLockState PROC
cmp engineMode,MODE_STAIRS
je lock_interior_check
cmp engineMode, MODE_WALK
jne unlock_it
lock_interior_check:
cmp paused3D, 0
jne unlock_it
invoke GetForegroundWindow
@ -4450,6 +4493,10 @@ skip_mouse_look:
; Movement speed with sprint (Shift = 10h)
mov eax, deltaTime3D
cmp eax,50
jbe @F
mov eax,50
@@:
imul eax, 200
mov speed, eax
cmp keys3D[10h], 0
@ -4564,11 +4611,14 @@ check_total_movement:
mov playerY, eax
input_action:
cmp engineMode,MODE_STAIRS
je player_input_complete
invoke Pressed3D, 45h
test eax, eax
jz @F
call Trigger3DInteraction
@@:
player_input_complete:
ret
HandlePlayerInput ENDP
@ -4577,10 +4627,20 @@ CheckInteractions PROC uses ebx esi edi
mov nearest, 262144
mov interactTargetId, 0
mov interactPromptPtr, 0
cmp hallLevel3D,0
jne interaction_done
xor esi, esi
interaction_scan:
cmp esi, NUM_SPRITES
jge interaction_done
cmp esi,3
jb interaction_next
cmp esi,17
je interaction_next
cmp esi,19
je interaction_next
cmp esi,21
je interaction_next
imul edi, esi, SIZEOF SpriteObject
cmp [casinoSprites+edi].interactId, 0
je interaction_next
@ -4766,13 +4826,13 @@ check_target_shop:
check_target_atm:
cmp eax, INTERACT_ATM
jne check_target_terminal
mov engineMode, MODE_ATM
invoke BeginTerminal3D,0
ret
check_target_terminal:
cmp eax, INTERACT_TERMINAL
jne check_target_observer
call BeginUnavailable3D
invoke BeginTerminal3D,1
ret
check_target_observer:
@ -5179,8 +5239,11 @@ dispatch_frame:
call ArrivalEscape3D
jmp engine_tick
@@:
cmp engineMode,MODE_STAIRS
je pause_interior_walk
cmp engineMode, MODE_WALK
jne leave_table
pause_interior_walk:
xor paused3D, 1
jmp engine_tick
leave_table:
@ -5241,6 +5304,8 @@ check_walk_mode:
call UpdateCamera3D
call CheckInteractions
call HandlePlayerInput
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
@ -5251,6 +5316,12 @@ check_walk_mode:
call RenderMinimap
jmp present_frame
check_slots_mode:
cmp engineMode,MODE_CABARET
jne @F
call HandleCabaret3D
call RenderCabaret3D
jmp present_frame
@@:
cmp engineMode,MODE_SCRATCH
jne @F
call HandleScratch3D

Binary file not shown.

23
docs/CASINO_NEXT_PASS.md Normal file
View file

@ -0,0 +1,23 @@
# Casino Completion Pass
Runtime remains MASM32 assembly. Existing player saves and unrelated edits stay intact.
## Integrated World Pass
- [x] Six visually distinct roaming guests, including women; matching seated poses.
- [x] Cabaret with changing acts, audience seats, a programme and meaningful interaction.
- [x] Player-controlled stairs and an opening steel basement door, without timed camera travel.
- [x] Upper lounge with its own bar and access to Hell Tower, Buckshot and Russian roulette.
## Following Integration
- [ ] Native three/four-seat poker: betting, all-ins, side pots and split pots.
- [ ] European roulette: split, street, corner and six-line bets, winning bet highlights.
- [ ] Game-specific achievements with saved progress and unlock notifications.
- [ ] More ergonomic billiards power control without changing the verified physics/rules.
- [ ] Scene audit: dimensions, fixtures, intersections and dedicated materials.
- [ ] Video poker; then consider baccarat and Sic Bo as separate complete games.
An unchecked item is not implemented or not yet verified. Asset generation alone
does not count as a finished feature. Published build details belong in the
corresponding feature document after native verification.

93
docs/CASINO_TERMINALS.md Normal file
View file

@ -0,0 +1,93 @@
# Casino Terminals and Wall Lanterns
The runtime is MASM x86 only. Python is used solely to compile art and run offline
native regression fixtures, never to implement a second game runtime.
## World Objects
- Cash and achievement stations retain interaction IDs 7 and 17. They now stand
against the south face of the east lounge's wall at y=13: centres (28,13.22)
and (29.5,13.22), screens facing south into the hall. Their backs leave a 2 cm
gap to the wall rather than intersecting it. The old floating positions
(26,16) and (26,20) are no longer rendered or collidable.
Interaction/minimap anchors and the visitor ATM approach move with the cabinets.
- Each new cabinet has a 0.7 x 0.4 metre footprint, 1.6 metre height, textured
dedicated walnut equipment sides/rear, a metal lid and a brass-trimmed front.
Solid footprints stop the player walking through the cabinets while preserving
the front approach.
- Eight wall lanterns now have a solid glass body projecting 0.3 metres from
the wall, a horizontal metal arm, a mounting plate and top/bottom surfaces.
They use real ray/plane intersections and per-pixel depth, not a wall decal
or a sprite that rotates towards the player. Existing baked lighting remains.
## Native Screens
Both stations open the same full-screen terminal, initially showing their own
page. The four pages are balance and three achievement pages. Mouse arrows and
Left/Right cycle through them; Exit, E and the engine's Escape handler return to
the hall without opening a console or changing a wager.
Balance shows the actual wallet, owned card backs out of 13 and unlocked
achievements out of eight. No new money transfer or deposit is implied.
Eight achievements have visible conditions and progress:
| Condition | Saved Bit |
| --- | --- |
| Reach a wallet of 50,000 chips | 8 |
| Reach 100,000 chips | 9 |
| Reach 500,000 chips | 10 |
| Reach 1,000,000 chips | 11 |
| Purchase a card back | 4, existing merchant bit |
| Own five purchased backs, excluding the default | 12 |
| Claim a visitor contract reward | 13 |
| Claim six rewards during the current visitor/journal rotation | 14 |
Conditions are checked whenever native progress saves and when opening a kiosk.
Unlocks are permanent even if the wallet falls or visitors change. Existing
achievement bits are not cleared. The existing signed profile field stores the
mask without a save-format change; no reward currency is granted for these
achievements. Old peaks that were never recorded cannot be reconstructed.
## Art
Five original generated PNG sources and the exact built-in image_gen prompts
are in `assets/sprites/terminals/`. `tools/generators/pack_terminals.py` resizes
the art, compiles Russian cabinet plaques using the game's CP1251 font, and
writes `terminals-v2.bin` (four 256x512 BGRA frames) plus
`terminal-screen-v1.bin` (one 640x480 frame). Sources remain unchanged.
The first version incorrectly reused bar-shelf material 86 on the enclosure,
which exposed bottles along the cabinet edges from the rear. V2 uses dedicated
material 121 for every side/rear face and a metal lid. Its source is
`assets/sprites/terminals/body-side-rear-v2.png`, generated with the built-in
image_gen tool. Existing front, lamp and screen pixels are unchanged. The old
v1 pack is retained for pre-update executables, not modified in place.
Missing art leaves a safe, readable native screen; it does not block exit or
change a wallet. World pack dimensions and frame counts are validated on load.
## Verification
Run `C:\Python314\python.exe tools/tests/check_terminals.py` to compile fresh
MASM handlers, geometry and interaction dispatch against the existing native
test helpers. The helper object is copied with COFF visibility changes only;
shipping instructions and binaries are never patched.
The fixture checks threshold/sticky unlocks, preserved unrelated bits, actual
signed profile save/load in `build/terminals/test-profile.sav`, mouse/key paging
and exit, missing art, cabinet collisions and fourteen native framebuffer captures,
including the requested wall/lounging-area view, real front interaction, clear
old footprints and front/side/rear views of both stations. Rear inspection uses
an isolated model frame because the back is now against a solid wall in play.
The former bar material is
temporarily poisoned in test memory; isolated side/rear cabinet frames must
remain pixel-identical. `--prove-regression` compiles a test-only old-material
variant and asserts that this test rejects it. Shipping sources are not mutated.
It cannot write the user's profile. `--parity` compares the tested procedures
with a completed shipping object, normalizing internal/external call encoding.
Do not run parity while the shipping assembler is still writing the object.
The full game is still assembled through `tools/tests/build_graphics.ps1` and
published through `tools/tests/publish_graphics.ps1`. The latter refreshes the
existing C# launcher's integrity hash; that launcher is not assembly code.

126
docs/HALL_EXPANSION.md Normal file
View file

@ -0,0 +1,126 @@
# Upper Lounge, Manual Stairs and Cabaret
## Native Implementation
All movement, collision, camera height, room changes, interaction, rendering and
animation run in MASM32. The Python files only compile bitmap packs or build and
run the actual native test executable. No alternate game runtime was added.
The east entrance, at approximately (30,21) in the main hall, leads to an enclosed
wooden stairwell. WASD, turning and mouse look stay active. Twenty treads rise
2.6 world units; the camera height follows position, not elapsed animation time.
The stairwell may be stopped on or reversed anywhere. Crossing its landing joins
the upper lounge; returning uses the same route. Room-local coordinate translations
at door thresholds preserve facing and never advance the player on a timer.
The upper lounge is a compact 13-by-12-unit room with a dedicated carpet, teal
coffers, marble bar, seating, guests and projecting lanterns. Buckshot, Russian
roulette and Hell Tower are available there; their old ground-floor billboards
and interaction targets are suppressed. Their gameplay code is unchanged.
The two game stations now use distinct detailed table cutouts: a shotgun,
shells and handcuffs for Buckshot; an octagonal revolver/whiskey table for
Russian roulette. Their collision and interaction centres remain shared.
The bar uses a full-body bartender in the walking view and a separate close
serving animation when seated. Bar stools do not obstruct that seated view.
The original iron door remains in the east-facing alcove at (28.5,10.5), using
the same door artwork as before the stair expansion. Its control opens it sideways
without moving the player
or change modes. The closed leaf blocks the doorway. Walking through its open
threshold enters the concrete stairwell, which also uses normal movement. The
old timed descent has been removed. The descent goes east, with a level landing
where the player turns north into the existing bunker. The ticket counter is
restored to its original (29,7.5) position.
Both sides of all stair entrances show the adjacent native scene through a
depth-tested aperture. Remote wall rays begin just beyond that aperture, not
inside a closed map cell behind the camera. Foreground geometry still occludes
the view. This repairs the former visible-wall/walk-through-wall mismatch;
room-local translations at thresholds are retained, with no timed player travel.
## Infernal Portal
Hell Tower is no longer represented by an ordinary table. A 2.5-unit-wide,
almost ceiling-height arch is mounted in the south wall, with a projecting
dark metal surround, chains and two animated crimson braziers. Its floor glow
is applied to the existing carpet, not drawn as an opaque rectangle.
Each of the two floor levers has six generated positions over a 600ms pull.
The portal stays sealed until both complete. Its vortex then loops through
twelve generated phases at 80ms per frame. The rigid arch is registered and
locked across those phases; the brazier fires have four separate phases.
Walking into the open aperture, or interacting with it, starts the existing
Hell Tower mode. Returning cannot immediately retrigger it until the player
backs away from the entrance.
## Visitors
Six new appearances include three men and three women, with separate standing,
walking and seated frames. Both visitor rotations have a mixed cast. Dialogue
names follow the selected appearance. Existing routes and challenge identities
are retained. These are front/three-quarter sprites, not complete eight-direction
animation sets. Upper-lounge guests are currently seated, not another roaming
population; this limitation is deliberate and not presented as finished NPC AI.
Their chairs are grouped around lamp tables rather than placed singly at blank
walls; the emerald guest's seated pose faces inward toward the shared table.
## Cabaret
A nearby seat interaction opens a seated view of the stage. Three thirty-second
acts cycle: the existing twelve-frame dance chorus, a singer/pianist/guitar/sax
quartet, and a duet. The pianist and grand piano are one coherent generated
cutout, with his bench, hands on the actual keyboard, pedals and raised lid.
The guitarist has four separate playing poses, as does the piano/pianist.
Their common packing bounds retain fixed scale and foot anchors. The seated
camera frames all four performers without clipping the piano or saxophonist.
The programme shows the current act and time to the next one. Requesting
the next act or leaving a tip costs 100; insufficient funds never create debt.
Standing up and Escape return to the hall. New act-specific music, eight-direction
performer sprites and audience applause recordings are not included in this pass.
## Assets
Generated using the built-in image generator, with no API key:
- `assets/sprites/hall/guest-cast-v1.png`
- `assets/sprites/hall/cabaret-ensemble-v1.png`
- `assets/sprites/hall/cabaret-musicians-v1.png`
- `assets/sprites/hall/upper-materials-v1.png`
- `assets/sprites/hall/upper-furniture-v2.png`
- `assets/sprites/hall/upper-portal-v3.png`
- `assets/sprites/hall/upper-levers-v3.png`
- `assets/sprites/hall/upper-braziers-v3.png`
Exact prompts and packing outputs: `assets/sprites/hall/prompts-v7.json` and
`assets/sprites/hall/prompts-v8.json`.
Packers: `pack_guest_cast.py`, `pack_cabaret_ensemble.py`,
`pack_cabaret_musicians.py`, `pack_upper_materials.py`, `pack_upper_v2.py`,
`pack_upper_motion.py`
under `tools/generators`. Original source images are preserved.
## Verification
`python tools/tests/check_hall_expansion.py` compiles current MASM procedures and
tests opening without movement, closed-door collision, manual traversal and
reversal of both stairwells, no idle timed movement, upper-floor game/bar targets,
bar camera restoration, cabaret costs and exits. It records native frames
under `build/hall-expansion`. `--parity` checks new runtime procedures against the
shipping object after the main build. Helpers are reused by changing COFF symbol
visibility only; no helper instruction bytes or player save files are modified.
All four jazz poses are rendered and checked for visible changes in both the
piano and guitar regions. The loader probe also covers both new musician packs.
The repair tests cover all eight doorway views, foreground occlusion, original
door/ticket positions, the two-lever lock, intermediate/final lever poses, all
twelve visible vortex phases, tower entry/return and single/dual-thread pixel
parity at the stair entrance. `--prove-seam-regression` disables only aperture
near clipping and confirms that the old solid rear-wall block reappears.
Published on 2026-10-18 to `poker.exe` and `bin/poker.exe`, with the existing
launcher integrity hash updated. Both copies and `build/graphics/poker.exe` have
SHA-256 `C0B5A1DD9A678E68599724138AF009D29C935EEFDD6D70D28789AABF071395F8`.
Native scene/input tests, asset loading from root/TEMP, card-rank parity and the
eight-ball ownership/retained-turn regression passed. The full legacy action
suite and an interactive end-to-end playthrough were not rerun for this pass.
The remaining poker, roulette, achievements, billiards controls and new-game work
is tracked separately in `CASINO_NEXT_PASS.md`; it is not claimed complete here.

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -18,4 +18,4 @@ DebOut=
[FILES]
1=poker.asm
[SHOWFILE]
1=000000000000000014060000740300000000000003000000F201000087
1=000000000000000014060000740300000000000003000000850300001C

188
src_graphics/cabaret.inc Normal file
View file

@ -0,0 +1,188 @@
UpdateCabaret3D PROC
mov eax,deltaTime3D
add eax,cabaretTime3D
xor edx,edx
mov ecx,90000
div ecx
mov cabaretTime3D,edx
mov eax,edx
xor edx,edx
mov ecx,30000
div ecx
mov cabaretAct3D,eax
ret
UpdateCabaret3D ENDP
BeginCabaret3D PROC
mov engineMode,MODE_CABARET
mov mouseClick3D,0
call ClearKeys3D
ret
BeginCabaret3D ENDP
HandleCabaret3D PROC
call UpdateCabaret3D
cmp mouseClick3D,0
je cabaret_handle_keys
cmp mouseY3D,210
jb cabaret_handle_keys
cmp mouseX3D,236
jae cabaret_leave
cmp mouseX3D,148
jae cabaret_tip
cmp bankroll,100
jb cabaret_handled
sub bankroll,100
mov eax,cabaretAct3D
inc eax
cmp eax,3
jb @F
xor eax,eax
@@:
imul eax,30000
mov cabaretTime3D,eax
call SaveProgress3D
jmp cabaret_handled
cabaret_tip:
cmp bankroll,100
jb cabaret_handled
sub bankroll,100
invoke CopyString3D,OFFSET cabaretApplause3D,OFFSET flashMessage
mov flashTimer,120
call SaveProgress3D
jmp cabaret_handled
cabaret_handle_keys:
invoke Pressed3D,45h
test eax,eax
jnz cabaret_leave
jmp cabaret_handled
cabaret_leave:
mov engineMode,MODE_WALK
call ClearKeys3D
cabaret_handled:
mov mouseClick3D,0
ret
HandleCabaret3D ENDP
RenderCabaretActors3D PROC uses ebx esi edi
mov eax,animationClock3D
xor edx,edx
mov ecx,200
div ecx
xor edx,edx
mov ecx,12
div ecx
mov edi,edx
cmp cabaretAct3D,1
je cabaret_jazz
; Existing dancers retain their complete twelve-frame loop.
xor esi,esi
cabaret_dancer:
mov eax,edi
add eax,esi
xor edx,edx
mov ecx,12
div ecx
add edx,10
shl edx,16
add edx,OFFSET texturesBuffer
mov hallLegacyPack3D.pixels,edx
mov cabaretActor3D.asset,OFFSET hallLegacyPack3D
mov cabaretActor3D.frame,0
imul eax,esi,3*FP_1
add eax,13*FP_1
mov cabaretActor3D.posX,eax
mov cabaretActor3D.posY,4*FP_1
invoke DrawCityObject3D,OFFSET cabaretActor3D,0
inc esi
cmp cabaretAct3D,2
je cabaret_singer
cmp esi,3
jb cabaret_dancer
ret
cabaret_jazz:
mov eax,edi
and eax,3
mov cabaretPiano3D.frame,eax
invoke DrawCityObject3D,OFFSET cabaretPiano3D,0
mov eax,edi
and eax,3
mov cabaretGuitar3D.frame,eax
invoke DrawCityObject3D,OFFSET cabaretGuitar3D,0
mov cabaretActor3D.asset,OFFSET cabaretArt3D
mov cabaretActor3D.posX,FP_1*187/10
mov cabaretActor3D.posY,4*FP_1
mov eax,edi
and eax,3
add eax,8
mov cabaretActor3D.frame,eax
invoke DrawCityObject3D,OFFSET cabaretActor3D,0
cabaret_singer:
mov cabaretActor3D.asset,OFFSET cabaretArt3D
mov cabaretActor3D.posX,FP_1*162/10
mov cabaretActor3D.posY,4*FP_1
mov eax,edi
and eax,3
mov cabaretActor3D.frame,eax
invoke DrawCityObject3D,OFFSET cabaretActor3D,0
ret
RenderCabaretActors3D ENDP
RenderCabaret3D PROC uses ebx
local oldEye:DWORD,oldHorizon:DWORD,oldX:DWORD,oldY:DWORD,oldAngle:DWORD
mov eax,playerX
mov oldX,eax
mov eax,playerY
mov oldY,eax
mov eax,playerAngle
mov oldAngle,eax
mov playerX,16*FP_1
mov playerY,FP_1*92/10
mov playerAngle,384
mov eax,viewEye3D
mov oldEye,eax
mov eax,viewHorizon3D
mov oldHorizon,eax
mov viewEye3D,FP_1*3/4
mov viewHorizon3D,HALF_H
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderSprites
call RenderHall3D
invoke DrawBox,0,0,320,25,0014191Dh
invoke DrawString8x8,12,8,OFFSET cabaretTitle3D,00E7CA88h
invoke DrawBox,0,178,320,62,0014191Dh
mov eax,cabaretAct3D
invoke DrawString8x8,8,183,cabaretActNames3D[eax*4],00E7CA88h
invoke DrawString8x8,8,197,OFFSET cabaretNext3D,0098A9AAh
mov eax,cabaretTime3D
xor edx,edx
mov ecx,30000
div ecx
mov eax,30000
sub eax,edx
xor edx,edx
mov ecx,1000
div ecx
invoke DrawNumber,200,197,eax,00E7CA88h
invoke DrawBoxBorder,5,212,139,23,00252B29h,00836D42h,1
invoke DrawBoxBorder,148,212,84,23,00252B29h,00836D42h,1
invoke DrawBoxBorder,236,212,79,23,00252B29h,00836D42h,1
invoke DrawString8x8,12,219,OFFSET cabaretRequest3D,00E7CA88h
invoke DrawString8x8,152,219,OFFSET cabaretTip3D,00E7CA88h
invoke DrawString8x8,244,219,OFFSET cabaretStand3D,00E7CA88h
mov eax,oldEye
mov viewEye3D,eax
mov eax,oldHorizon
mov viewHorizon3D,eax
mov eax,oldX
mov playerX,eax
mov eax,oldY
mov playerY,eax
mov eax,oldAngle
mov playerAngle,eax
call UpdateCamera3D
ret
RenderCabaret3D ENDP

View file

@ -0,0 +1,32 @@
MODE_CABARET equ 24
INTERACT_CABARET_SEAT equ 42
UpdateCabaret3D PROTO
BeginCabaret3D PROTO
HandleCabaret3D PROTO
RenderCabaret3D PROTO
RenderCabaretActors3D PROTO
.data
cabaretArt3D AnimPack3D <>
cabaretPath3D db "assets\animations\hall\cabaret-ensemble-v1.bin",0
cabaretPianoArt3D AnimPack3D <>
cabaretPianoPath3D db "assets\animations\hall\cabaret-piano-v1.bin",0
cabaretGuitarArt3D AnimPack3D <>
cabaretGuitarPath3D db "assets\animations\hall\cabaret-guitar-v1.bin",0
cabaretTime3D dd 0
cabaretAct3D dd 0
cabaretActSeen3D dd 0
cabaretActor3D CityProp3D <16*FP_1,4*FP_1,FP_1*4/5,FP_1*13/10,0,OFFSET cabaretArt3D,FP_1/8>
cabaretPiano3D CityProp3D <FP_1*147/10,FP_1*36/10,FP_1*26/10,FP_1*175/100,0,OFFSET cabaretPianoArt3D,FP_1/8>
cabaretGuitar3D CityProp3D <FP_1*175/10,FP_1*4,FP_1*9/10,FP_1*7/5,0,OFFSET cabaretGuitarArt3D,FP_1/8>
cabaretTitle3D db 202,192,193,192,208,197,32,34,207,206,203,205,206,215,220,34,0
cabaretSeat3D db 69,58,32,209,197,209,210,220,32,200,32,209,204,206,210,208,197,210,220,32,216,206,211,0
cabaretActNames3D dd OFFSET cabaretChorus3D,OFFSET cabaretJazz3D,OFFSET cabaretFinale3D
cabaretChorus3D db 194,197,215,197,208,205,197,197,32,194,192,208,220,197,210,197,0
cabaretJazz3D db 196,198,192,199,206,194,219,201,32,202,194,192,208,210,197,210,0
cabaretFinale3D db 209,197,208,197,193,208,223,205,219,201,32,196,211,221,210,0
cabaretRequest3D db 205,206,204,197,208,32,36,49,48,48,0
cabaretTip3D db 215,192,201,32,36,49,48,48,0
cabaretStand3D db 194,209,210,192,210,220,0
cabaretNext3D db 196,206,32,209,204,197,205,219,32,205,206,204,197,208,192,0
cabaretApplause3D db 192,240,242,232,241,242,251,32,225,235,224,227,238,228,224,240,255,242,32,231,224,32,239,238,228,228,229,240,230,234,243,0
.code

View file

@ -215,10 +215,15 @@ card_fill_ready:
card_draw_face:
mov eax, x
add eax, 3
cmp cardWidth, 20
jae @F
sub eax, 2
@@:
mov edx, y
add edx, 3
mov ecx, rank
lea ecx, cardRankNames3D[ecx*2]
lea ecx, [ecx+ecx*2]
add ecx, OFFSET cardRankNames3D
invoke DrawString8x8, eax, edx, ecx, ink
mov eax, x
add eax, 3

View file

@ -72,7 +72,7 @@ cardTableHeader3D dd 4 dup(0)
cardRanksHeader3D dd 4 dup(0)
cardTablePath3D db "assets\textures\card-table.bin",0
cardRanksPath3D db "assets\data\poker-ranks.bin",0
cardRankNames3D db "2",0,"3",0,"4",0,"5",0,"6",0,"7",0,"8",0,"9",0,"T",0,"J",0,"Q",0,"K",0,"A",0
cardRankNames3D db "2",0,0,"3",0,0,"4",0,0,"5",0,0,"6",0,0,"7",0,0,"8",0,0,"9",0,0,"10",0,"J",0,0,"Q",0,0,"K",0,0,"A",0,0
cardSuitMasks3D db 18h,3Ch,18h,7Eh,0FFh,7Eh,18h,3Ch
db 18h,3Ch,7Eh,0FFh,7Eh,3Ch,18h,0
db 66h,0FFh,0FFh,7Eh,3Ch,18h,0,0

View file

@ -5,6 +5,20 @@ cityRayY3D dd SCREEN_W dup(?)
; Exterior-only materials do not consume or overwrite the hall's texture slots.
ResolveCitySurface3D PROC material:DWORD
mov eax,material
cmp eax,122
jb @F
cmp eax,130
ja @F
invoke ResolveUpperMaterial3D,material
ret
@@:
cmp eax,118
jb @F
cmp eax,122
jae @F
invoke ResolveTerminalSurface3D,material
ret
@@:
cmp eax,115
jb @F
cmp eax,118
@ -217,6 +231,12 @@ city_vertical_along:
city_vertical_pixel:
mov edi, destination
mov eax, depth
cmp portalRendering3D,0
je @F
mov ebx,column
cmp eax,portalClipDepth3D[ebx*4]
jb city_vertical_next_pixel
@@:
cmp eax, cityDepthBuffer3D[edi]
ja city_vertical_next_pixel
mov eax, sourceY
@ -422,6 +442,12 @@ city_horizontal_pixel:
cmp eax, yMax
jge city_horizontal_advance
mov eax, depth
cmp portalRendering3D,0
je @F
mov ebx,column
cmp eax,portalClipDepth3D[ebx*4]
jb city_horizontal_advance
@@:
cmp eax, cityDepthBuffer3D[edi]
ja city_horizontal_advance
mov eax, pointX

View file

@ -272,6 +272,11 @@ city_object_project:
mov ebx, xStart
city_object_column:
mov eax, depth
cmp portalRendering3D,0
je @F
cmp eax,portalClipDepth3D[ebx*4]
jb city_object_next_column
@@:
cmp eax, zBuffer[ebx*4]
jge city_object_next_column
mov eax, ebx

View file

@ -166,4 +166,7 @@ stairsReturnX3D dd 0
stairsReturnY3D dd 0
stairsReturnAngle3D dd 0
include src_graphics\world_dressing_data.inc
include src_graphics\terminal_data.inc
include src_graphics\hall_expansion_data.inc
include src_graphics\cabaret_data.inc
.code

View file

@ -0,0 +1,885 @@
; Connected stairwells use the normal player movement and body collision routines.
; Only room-local coordinates are translated at a landing, never by a timer.
ResolveUpperMaterial3D PROC material:DWORD
cmp material,129
jne @F
mov eax,upperPortal3D.frame
imul eax,256*512*4
cmp upperPortalArt3D.pixels,0
je upper_material_missing
add eax,upperPortalArt3D.pixels
mov ecx,256
mov edx,512
ret
@@:
cmp material,130
jne @F
mov eax,OFFSET upperGlowPixels3D
mov ecx,32
mov edx,32
ret
@@:
cmp material,128
jne @F
mov eax,hallProps3D.pixels
test eax,eax
jz upper_material_missing
add eax,5*256*256*4
mov ecx,256
mov edx,256
ret
@@:
cmp material,126
jne @F
mov eax,OFFSET upperFeltPixels3D
mov ecx,32
mov edx,32
ret
@@:
cmp material,127
jne @F
mov eax,OFFSET upperKeyPixels3D
mov ecx,32
mov edx,32
ret
@@:
mov eax,upperMaterialArt3D.pixels
test eax,eax
jz upper_material_missing
mov ecx,material
sub ecx,122
cmp ecx,3
ja upper_material_missing
shl ecx,18
add eax,ecx
mov ecx,256
mov edx,256
ret
upper_material_missing:
xor eax,eax
ret
ResolveUpperMaterial3D ENDP
InitUpperSurfaces3D PROC uses esi
xor esi,esi
upper_surface_pixel:
; Small native material swatches, not a replacement external renderer.
mov eax,esi
imul eax,1103515245
xor eax,5A63h
and eax,7
add eax,0FF1B5039h
mov upperFeltPixels3D[esi*4],eax
and eax,7
add eax,0FF311114h
mov upperGlowPixels3D[esi*4],eax
mov eax,esi
and eax,31
and eax,3
cmp eax,3
je upper_key_dark
cmp esi,16*32
jae upper_key_white
test eax,eax
jz upper_key_dark
upper_key_white:
mov eax,0FFECE3C8h
jmp upper_key_store
upper_key_dark:
mov eax,0FF16191Bh
upper_key_store:
mov upperKeyPixels3D[esi*4],eax
inc esi
cmp esi,1024
jb upper_surface_pixel
ret
InitUpperSurfaces3D ENDP
DrawUpperGameTable3D PROC uses ebx esi edi centreX:DWORD,centreY:DWORD
local x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD,legX:DWORD,legY:DWORD
mov eax,centreX
sub eax,FP_1
mov x0,eax
add eax,2*FP_1
mov x1,eax
mov eax,centreY
sub eax,FP_1/2
mov y0,eax
add eax,FP_1
mov y1,eax
invoke DrawCityBoxSides3D,x0,x1,y0,y1,FP_1/2,FP_1*13/20,85
invoke DrawCityHorizontal3D,x0,x1,y0,y1,FP_1*13/20,85
mov eax,x0
add eax,FP_1/10
mov ebx,x1
sub ebx,FP_1/10
mov ecx,y0
add ecx,FP_1/10
mov edx,y1
sub edx,FP_1/10
invoke DrawCityHorizontal3D,eax,ebx,ecx,edx,FP_1*33/50,126
xor esi,esi
upper_table_leg:
mov eax,x0
add eax,FP_1/8
test esi,1
jz @F
mov eax,x1
sub eax,FP_1/4
@@:
mov legX,eax
mov eax,y0
add eax,FP_1/8
test esi,2
jz @F
mov eax,y1
sub eax,FP_1/4
@@:
mov legY,eax
mov ebx,legX
add ebx,FP_1/8
add eax,FP_1/8
invoke DrawCityBoxSides3D,legX,ebx,legY,eax,0,FP_1/2,85
inc esi
cmp esi,4
jb upper_table_leg
ret
DrawUpperGameTable3D ENDP
BeginWalkableStairs3D PROC route:DWORD,fromFarEnd:DWORD
mov eax,route
mov stairRoute3D,eax
mov engineMode,MODE_STAIRS
mov interactTargetId,0
mov interactPromptPtr,0
cmp route,1
jne interior_begin_upper
mov worldMap3D,OFFSET basementStairMap3D
mov floorTexture3D,94
sub playerX,25*FP_1
add playerY,FP_1*21/2
cmp fromFarEnd,0
je interior_begin_finish
add playerX,12*FP_1
sub playerY,32*FP_1
jmp interior_begin_finish
interior_begin_upper:
mov worldMap3D,OFFSET upperStairMap3D
mov floorTexture3D,85
sub playerX,27*FP_1
cmp fromFarEnd,0
je interior_begin_finish
add playerX,39*FP_1
interior_begin_finish:
call UpdateStairHeight3D
ret
BeginWalkableStairs3D ENDP
UpdateInteriorTravel3D PROC
call UpdateUpperEffects3D
mov eax,animationClock3D
cmp eax,interiorDoorClock3D
je interior_travel_check
mov interiorDoorClock3D,eax
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
imul eax,80
add eax,basementDoorOpen3D
cmp eax,basementDoorTarget3D
jbe @F
mov eax,basementDoorTarget3D
@@:
mov basementDoorOpen3D,eax
interior_travel_check:
cmp engineMode,MODE_STAIRS
je interior_travel_stairs
cmp engineMode,MODE_WALK
jne interior_travel_done
cmp hallLevel3D,1
je interior_travel_bunker
cmp hallLevel3D,2
je interior_travel_upper
cmp playerY,20*FP_1
jb interior_travel_door
cmp playerY,22*FP_1
ja interior_travel_done
cmp playerX,FP_1*303/10
jb interior_travel_done
invoke BeginWalkableStairs3D,2,0
ret
interior_travel_door:
cmp basementDoorOpen3D,FP_1*9/10
jb interior_travel_done
cmp playerX,FP_1*286/10
jb interior_travel_done
cmp playerY,FP_1*99/10
jb interior_travel_done
cmp playerY,FP_1*111/10
ja interior_travel_done
invoke BeginWalkableStairs3D,1,0
ret
interior_travel_bunker:
cmp playerY,FP_1*277/10
jb interior_travel_done
; Bunker landing (16.5,27.7) continues to the stairwell (16.5,14.7).
sub playerY,13*FP_1
mov stairRoute3D,1
mov engineMode,MODE_STAIRS
mov worldMap3D,OFFSET basementStairMap3D
mov floorTexture3D,94
call UpdateStairHeight3D
ret
interior_travel_upper:
call TryWalkUpperPortal3D
cmp engineMode,MODE_WALK
jne interior_travel_done
cmp playerY,20*FP_1
jb interior_travel_done
cmp playerY,22*FP_1
ja interior_travel_done
cmp playerX,FP_1*32/10
ja interior_travel_done
; Upper west landing and the far east end of its stairwell share an axis.
add playerX,12*FP_1
mov stairRoute3D,2
mov engineMode,MODE_STAIRS
mov worldMap3D,OFFSET upperStairMap3D
mov floorTexture3D,85
call UpdateStairHeight3D
ret
interior_travel_stairs:
cmp stairRoute3D,1
jne interior_upper_thresholds
cmp playerX,FP_1*31/10
jbe interior_exit_basement_top
cmp playerY,FP_1*143/10
ja interior_travel_done
mov hallLevel3D,1
mov worldMap3D,OFFSET bunkerMap3D
add playerY,13*FP_1
mov floorTexture3D,94
jmp interior_finish
interior_exit_basement_top:
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
add playerX,25*FP_1
sub playerY,FP_1*21/2
mov floorTexture3D,100
jmp interior_finish
interior_upper_thresholds:
cmp playerX,FP_1*31/10
jbe interior_exit_upper_bottom
cmp playerX,16*FP_1
jb interior_travel_done
mov hallLevel3D,2
mov worldMap3D,OFFSET upperLoungeMap3D
sub playerX,12*FP_1
mov floorTexture3D,124
jmp interior_finish
interior_exit_upper_bottom:
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
add playerX,27*FP_1
mov floorTexture3D,100
interior_finish:
mov engineMode,MODE_WALK
mov interactTargetId,0
mov interactPromptPtr,0
call UpdateInteriorEye3D
interior_travel_done:
ret
UpdateInteriorTravel3D ENDP
UpdateStairHeight3D PROC
cmp engineMode,MODE_STAIRS
jne interior_height_done
mov viewHorizon3D,HALF_H
mov viewCeiling3D,FP_1*11/2
cmp stairRoute3D,1
jne interior_height_upper
mov viewCeiling3D,4*FP_1
mov eax,13*FP_1
sub eax,playerX
jle interior_height_zero
cmp eax,8*FP_1
jle @F
mov eax,8*FP_1
@@:
imul eax,3
cdq
mov ecx,16
idiv ecx
jmp interior_height_store
interior_height_upper:
mov eax,playerX
sub eax,5*FP_1
jle interior_height_zero
cmp eax,8*FP_1
jle @F
mov eax,8*FP_1
@@:
imul eax,13
cdq
mov ecx,40
idiv ecx
jmp interior_height_store
interior_height_zero:
xor eax,eax
interior_height_store:
add eax,HALL_STANDING_EYE
mov viewEye3D,eax
interior_height_done:
ret
UpdateStairHeight3D ENDP
UpdateInteriorEye3D PROC
cmp engineMode,MODE_STAIRS
je UpdateStairHeight3D
cmp engineMode,MODE_WALK
jne interior_eye_done
mov viewEye3D,HALL_STANDING_EYE
mov viewCeiling3D,FP_1*5/2
interior_eye_done:
ret
UpdateInteriorEye3D ENDP
InteriorDoorCanWalk3D PROC px:DWORD,py:DWORD
cmp px,29*FP_1-FP_1/6
jb interior_check_steel
cmp py,FP_1*1955/100
jb interior_check_steel
cmp py,20*FP_1+FP_1/6
jbe interior_portal_blocked
cmp py,22*FP_1-FP_1/6
jb interior_check_steel
cmp py,FP_1*2245/100
jbe interior_portal_blocked
interior_check_steel:
cmp py,9*FP_1
jb interior_door_clear
cmp py,12*FP_1
jae interior_door_clear
cmp px,FP_1*2835/100
jb interior_door_clear
cmp py,FP_1*96/10
jb interior_portal_blocked
cmp py,FP_1*114/10
ja interior_portal_blocked
cmp basementDoorOpen3D,FP_1*9/10
jae interior_door_clear
interior_portal_blocked:
xor eax,eax
ret
interior_door_clear:
mov eax,1
ret
InteriorDoorCanWalk3D ENDP
RenderHallPortals3D PROC uses ebx
; Retain the original iron door artwork in the original east-facing alcove.
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,9*FP_1,FP_1*975/100,0,FP_1*11/5,102
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,FP_1*1125/100,12*FP_1,0,FP_1*11/5,102
mov eax,basementDoorOpen3D
imul eax,2
add eax,FP_1*975/100
lea ebx,[eax+FP_1*3/2]
invoke DrawCityVertical3D,0,FP_1*285/10,eax,ebx,0,FP_1*19/10,128
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,FP_1*975/100,FP_1*1125/100,FP_1*19/10,FP_1*11/5,102
; East stair entrance: projecting jambs, a low first tread and overhead lintel.
invoke DrawCityBoxSides3D,29*FP_1,31*FP_1,FP_1*197/10,20*FP_1,0,FP_1*5/2,102
invoke DrawCityBoxSides3D,29*FP_1,31*FP_1,22*FP_1,FP_1*223/10,0,FP_1*5/2,102
invoke DrawCityHorizontal3D,30*FP_1,31*FP_1,20*FP_1,22*FP_1,FP_1/8,85
invoke DrawCityVertical3D,0,30*FP_1,20*FP_1,22*FP_1,0,FP_1/8,102
ret
RenderHallPortals3D ENDP
RenderWalkableStairs3D PROC uses ebx esi edi
local treadStart:DWORD,treadEnd:DWORD,z0:DWORD,z1:DWORD
cmp stairRoute3D,1
jne interior_render_upper_steps
; East-facing descent, then a level northward turn into the old bunker.
xor esi,esi
interior_concrete_step:
mov eax,esi
imul eax,FP_1/3
add eax,5*FP_1
mov treadStart,eax
add eax,FP_1/3
mov treadEnd,eax
imul eax,esi,FP_1/16
neg eax
add eax,FP_1*3/2
mov z0,eax
sub eax,FP_1/16
mov z1,eax
invoke DrawCityHorizontal3D,treadStart,treadEnd,20*FP_1,23*FP_1,z1,94
invoke DrawCityVertical3D,0,treadEnd,20*FP_1,23*FP_1,z1,z0,92
inc esi
cmp esi,24
jb interior_concrete_step
invoke DrawCityHorizontal3D,2*FP_1,5*FP_1,20*FP_1,23*FP_1,FP_1*3/2,94
invoke DrawCityHorizontal3D,13*FP_1,18*FP_1,20*FP_1,23*FP_1,0,94
ret
interior_render_upper_steps:
xor esi,esi
interior_wood_step:
imul eax,esi,FP_1*2/5
add eax,5*FP_1
mov treadStart,eax
add eax,FP_1*2/5
mov treadEnd,eax
imul eax,esi,FP_1*13/100
mov z0,eax
add eax,FP_1*13/100
mov z1,eax
invoke DrawCityHorizontal3D,treadStart,treadEnd,20*FP_1,22*FP_1,z1,85
invoke DrawCityVertical3D,0,treadStart,20*FP_1,22*FP_1,z0,z1,102
mov eax,z1
add eax,FP_1*3/4
mov ebx,eax
add eax,FP_1/20
mov edi,eax
invoke DrawCityVertical3D,1,FP_1*201/10,treadStart,treadEnd,ebx,edi,110
invoke DrawCityVertical3D,1,FP_1*219/10,treadStart,treadEnd,ebx,edi,110
; Posts follow the slope instead of a billboard painted on the wall.
test esi,3
jnz @F
mov eax,z1
add eax,FP_1*3/4
mov ebx,eax
invoke DrawCityVertical3D,0,treadStart,FP_1*201/10,FP_1*202/10,z1,ebx,110
invoke DrawCityVertical3D,0,treadStart,FP_1*218/10,FP_1*219/10,z1,ebx,110
@@:
inc esi
cmp esi,20
jb interior_wood_step
invoke DrawCityHorizontal3D,13*FP_1,18*FP_1,20*FP_1,22*FP_1,FP_1*13/5,85
invoke DrawCityVertical3D,1,20*FP_1,2*FP_1,18*FP_1,0,FP_1*11/2,102
invoke DrawCityVertical3D,1,22*FP_1,2*FP_1,18*FP_1,0,FP_1*11/2,102
ret
RenderWalkableStairs3D ENDP
UpperCanWalk3D PROC px:DWORD,py:DWORD
; The same station centres drive art, interaction and collision.
cmp px,10*FP_1-FP_1/5
jb upper_walk_tables
cmp px,15*FP_1+FP_1/5
ja upper_walk_tables
cmp py,13*FP_1-FP_1/5
jb upper_walk_tables
cmp py,15*FP_1+FP_1/5
jbe upper_walk_block
upper_walk_tables:
xor ecx,ecx
upper_walk_station:
mov eax,px
sub eax,upperStationX3D[ecx*4]
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_1*11/10
ja upper_walk_next
mov eax,py
sub eax,upperStationY3D[ecx*4]
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_1*7/10
jbe upper_walk_block
upper_walk_next:
inc ecx
cmp ecx,2
jb upper_walk_station
xor ecx,ecx
upper_walk_guest:
mov eax,px
sub eax,upperGuestX3D[ecx*4]
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_1*2/5
ja upper_walk_guest_next
mov eax,py
sub eax,upperGuestY3D[ecx*4]
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_1*2/5
jbe upper_walk_block
upper_walk_guest_next:
inc ecx
cmp ecx,3
jb upper_walk_guest
cmp upperPortalReady3D,1
je upper_walk_clear
cmp py,24*FP_1
jb upper_walk_clear
cmp px,FP_1*95/10
jb upper_walk_clear
cmp px,FP_1*115/10
jbe upper_walk_block
upper_walk_clear:
mov eax,1
ret
upper_walk_block:
xor eax,eax
ret
UpperCanWalk3D ENDP
CheckUpperInteractions3D PROC
mov interactTargetId,0
mov interactPromptPtr,0
invoke HallTarget3D,3*FP_1,21*FP_1,0,OFFSET upperExitPrompt3D
invoke HallTarget3D,upperStationX3D[0],upperStationY3D[0],INTERACT_BUCKSHOT,OFFSET promptBuckshot
invoke HallTarget3D,upperStationX3D[4],upperStationY3D[4],INTERACT_RUSSIAN_ROULETTE,OFFSET promptRussianRoulette
cmp upperPortalReady3D,1
je upper_check_portal
test upperLeverMask3D,1
jnz @F
invoke HallTarget3D,FP_1*91/10,FP_1*226/10,INTERACT_UPPER_LEFT,OFFSET upperLeftPrompt3D
@@:
test upperLeverMask3D,2
jnz upper_check_bar
invoke HallTarget3D,FP_1*119/10,FP_1*226/10,INTERACT_UPPER_RIGHT,OFFSET upperRightPrompt3D
jmp upper_check_bar
upper_check_portal:
invoke HallTarget3D,FP_1*105/10,FP_1*237/10,INTERACT_HELL_TOWER,OFFSET upperPortalPrompt3D
upper_check_bar:
cmp playerX,10*FP_1
jb @F
cmp playerX,15*FP_1
ja @F
cmp playerY,15*FP_1
jb @F
invoke HallTarget3D,playerX,15*FP_1,INTERACT_BAR,OFFSET hallBarPrompt3D
@@:
ret
CheckUpperInteractions3D ENDP
RenderUpperLounge3D PROC uses ebx esi
; Small midnight lounge: dark panels, brass ribs, a marble-topped straight bar.
invoke DrawCityBoxSides3D,10*FP_1,15*FP_1,14*FP_1,15*FP_1,0,FP_1*13/20,102
invoke DrawCityHorizontal3D,10*FP_1,15*FP_1,14*FP_1,15*FP_1,FP_1*13/20,123
invoke DrawCityVertical3D,1,13*FP_1+FP_1/20,10*FP_1,15*FP_1,FP_1/2,2*FP_1,87
invoke DrawCityBoxSides3D,10*FP_1,15*FP_1,13*FP_1,FP_1*132/10,0,FP_1/2,102
invoke DrawCityBoxSides3D,10*FP_1,15*FP_1,13*FP_1,FP_1*132/10,2*FP_1,FP_1*21/10,102
invoke DrawCityVertical3D,1,FP_1*152/10,10*FP_1,15*FP_1,FP_1/8,FP_1*16/100,110
mov upperBarTender3D.asset,OFFSET upperBarArt3D
mov upperBarTender3D.worldW,FP_1*4/5
mov upperBarTender3D.worldH,FP_1*7/5
mov upperBarTender3D.groundZ,0
mov upperBarTender3D.frame,0
mov upperBarTender3D.posX,FP_1*25/2
mov upperBarTender3D.posY,FP_1*27/2
cmp engineMode,MODE_BAR
jne upper_draw_bartender
mov upperBarTender3D.posX,FP_1*115/10
mov upperBarTender3D.asset,OFFSET hallBartender3D
mov upperBarTender3D.worldW,FP_1
mov upperBarTender3D.worldH,FP_1
mov upperBarTender3D.groundZ,FP_1*9/20
mov eax,barSelection3D
shl eax,3
mov upperBarTender3D.frame,eax
mov eax,barTime3D
xor edx,edx
mov ecx,375
div ecx
cmp eax,7
jbe @F
mov eax,7
@@:
add upperBarTender3D.frame,eax
upper_draw_bartender:
invoke DrawCityObject3D,OFFSET upperBarTender3D,0
xor esi,esi
upper_draw_table:
mov ebx,upperStationX3D[esi*4]
mov upperTable3D.posX,ebx
mov eax,upperStationY3D[esi*4]
mov upperTable3D.posY,eax
mov upperTable3D.worldW,2*FP_1
mov upperTable3D.worldH,FP_1*6/5
mov upperTable3D.asset,OFFSET upperGamesArt3D
mov upperTable3D.frame,esi
invoke DrawCityObject3D,OFFSET upperTable3D,0
mov upperDealer3D.posX,ebx
mov eax,upperStationY3D[esi*4]
sub eax,FP_1*3/4
mov upperDealer3D.posY,eax
mov upperDealer3D.frame,esi
invoke DrawCityObject3D,OFFSET upperDealer3D,0
inc esi
cmp esi,2
jb upper_draw_table
mov upperPortal3D.frame,0
cmp upperPortalReady3D,1
jne upper_draw_portal
mov eax,animationClock3D
xor edx,edx
mov ecx,80
div ecx
xor edx,edx
mov ecx,12
div ecx
inc edx
mov upperPortal3D.frame,edx
upper_draw_portal:
invoke DrawCityVertical3D,1,FP_1*249/10,9*FP_1,12*FP_1,0,FP_1*5/2,122
invoke DrawCityBoxSides3D,FP_1*89/10,FP_1*915/100,FP_1*243/10,25*FP_1,0,FP_1*5/2,122
invoke DrawCityBoxSides3D,FP_1*1185/100,FP_1*121/10,FP_1*243/10,25*FP_1,0,FP_1*5/2,122
invoke DrawCityBoxSides3D,9*FP_1,12*FP_1,FP_1*243/10,25*FP_1,FP_1*24/10,FP_1*5/2,122
invoke DrawCityVertical3D,1,FP_1*247/10,FP_1*925/100,FP_1*1175/100,0,FP_1*245/100,129
mov eax,animationClock3D
shr eax,7
and eax,3
mov upperBrazier3D.frame,eax
mov upperBrazier3D.posX,FP_1*84/10
invoke DrawCityObject3D,OFFSET upperBrazier3D,0
mov upperBrazier3D.posX,FP_1*126/10
invoke DrawCityObject3D,OFFSET upperBrazier3D,0
invoke DrawCityVertical3D,1,FP_1*242/10,FP_1*92/10,FP_1*924/100,FP_1*16/10,FP_1*25/10,110
invoke DrawCityVertical3D,1,FP_1*242/10,FP_1*1176/100,FP_1*118/10,FP_1*16/10,FP_1*25/10,110
mov upperLever3D.posX,FP_1*91/10
mov eax,upperLeverTime3D
xor edx,edx
mov ecx,120
div ecx
mov upperLever3D.frame,eax
invoke DrawCityObject3D,OFFSET upperLever3D,0
mov upperLever3D.posX,FP_1*119/10
mov eax,upperLeverTime3D[4]
xor edx,edx
mov ecx,120
div ecx
mov upperLever3D.frame,eax
invoke DrawCityObject3D,OFFSET upperLever3D,0
mov upperGuest3D.asset,OFFSET npcGuestArt3D
xor esi,esi
upper_draw_guest:
mov eax,upperGuestX3D[esi*4]
mov upperGuest3D.posX,eax
mov eax,upperGuestY3D[esi*4]
mov upperGuest3D.posY,eax
mov eax,upperGuestFrame3D[esi*4]
mov upperGuest3D.frame,eax
invoke DrawCityObject3D,OFFSET upperGuest3D,0
inc esi
cmp esi,3
jb upper_draw_guest
mov upperChair3D.posX,FP_1*56/10
mov upperChair3D.posY,FP_1*225/10
mov upperChair3D.worldW,FP_1*3/5
mov upperChair3D.worldH,FP_1*7/10
mov upperChair3D.asset,OFFSET hallLampFurniture3D
invoke DrawCityObject3D,OFFSET upperChair3D,0
mov upperChair3D.posX,FP_1*143/10
mov upperChair3D.posY,FP_1*221/10
invoke DrawCityObject3D,OFFSET upperChair3D,0
mov upperChair3D.asset,OFFSET hallChairFurniture3D
mov upperChair3D.worldW,FP_1*3/4
mov upperChair3D.worldH,FP_1
mov upperChair3D.posX,15*FP_1
mov upperChair3D.posY,FP_1*227/10
invoke DrawCityObject3D,OFFSET upperChair3D,0
invoke DrawCityObject3D,OFFSET upperSofa3D,0
cmp engineMode,MODE_BAR
je upper_bar_stools_done
xor esi,esi
upper_bar_stools:
imul eax,esi,FP_1*14/10
add eax,FP_1*109/10
mov upperStool3D.posX,eax
invoke DrawCityObject3D,OFFSET upperStool3D,0
inc esi
cmp esi,3
jb upper_bar_stools
upper_bar_stools_done:
invoke DrawHallLantern3D,3*FP_1+FP_1/50,15*FP_1,1
invoke DrawHallLantern3D,3*FP_1+FP_1/50,18*FP_1,1
invoke DrawHallLantern3D,16*FP_1-FP_1/50,17*FP_1,-1
invoke DrawHallLantern3D,16*FP_1-FP_1/50,22*FP_1,-1
invoke DrawCityVertical3D,0,16*FP_1-FP_1/40,14*FP_1,16*FP_1,FP_1,2*FP_1,115
invoke DrawCityVertical3D,0,3*FP_1+FP_1/40,15*FP_1,17*FP_1,FP_1,2*FP_1,115
invoke DrawCityHorizontal3D,3*FP_1,5*FP_1,20*FP_1,22*FP_1,0,85
call ApplyUpperPortalGlow3D
ret
RenderUpperLounge3D ENDP
TryUpperInteraction3D PROC
cmp hallLevel3D,2
jne upper_interaction_none
cmp interactTargetId,INTERACT_UPPER_LEFT
jne @F
or upperLeverMask3D,1
jmp upper_interaction_handled
@@:
cmp interactTargetId,INTERACT_UPPER_RIGHT
jne @F
or upperLeverMask3D,2
jmp upper_interaction_handled
@@:
cmp interactTargetId,INTERACT_HELL_TOWER
jne upper_interaction_none
cmp upperPortalReady3D,1
jne upper_interaction_locked
mov upperPortalEntered3D,1
call InitHellTower3D
jmp upper_interaction_handled
upper_interaction_locked:
invoke CopyString3D,OFFSET upperPortalLocked3D,OFFSET flashMessage
mov flashTimer,120
upper_interaction_handled:
call ClearKeys3D
mov eax,1
ret
upper_interaction_none:
xor eax,eax
ret
TryUpperInteraction3D ENDP
TryWalkUpperPortal3D PROC
cmp playerY,FP_1*235/10
jae @F
mov upperPortalEntered3D,0
@@:
cmp upperPortalReady3D,1
jne upper_portal_walk_done
cmp upperPortalEntered3D,0
jne upper_portal_walk_done
cmp playerX,10*FP_1
jb upper_portal_walk_done
cmp playerX,11*FP_1
ja upper_portal_walk_done
cmp playerY,FP_1*2425/100
jb upper_portal_walk_done
mov upperPortalEntered3D,1
call InitHellTower3D
call ClearKeys3D
upper_portal_walk_done:
ret
TryWalkUpperPortal3D ENDP
UpdateUpperEffects3D PROC uses ebx esi
mov eax,animationClock3D
cmp eax,upperEffectClock3D
je upper_effects_done
mov upperEffectClock3D,eax
xor esi,esi
mov ebx,1
upper_effect_lever:
test upperLeverMask3D,ebx
jz upper_effect_next
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
add eax,upperLeverTime3D[esi*4]
cmp eax,600
jbe @F
mov eax,600
@@:
mov upperLeverTime3D[esi*4],eax
upper_effect_next:
shl ebx,1
inc esi
cmp esi,2
jb upper_effect_lever
cmp upperLeverTime3D,600
jne upper_effects_done
cmp upperLeverTime3D[4],600
jne upper_effects_done
mov upperPortalReady3D,1
upper_effects_done:
ret
UpdateUpperEffects3D ENDP
ApplyUpperPortalGlow3D PROC uses ebx esi edi
local row:DWORD,column:DWORD,depth:DWORD,wx:DWORD,amount:DWORD,pixel:DWORD,pulse:DWORD
mov eax,animationClock3D
shr eax,7
and eax,3
add eax,10
cmp upperPortalReady3D,1
jne @F
add eax,8
@@:
mov pulse,eax
mov eax,viewHorizon3D
add eax,2
mov row,eax
upper_glow_row:
mov ecx,row
sub ecx,viewHorizon3D
mov eax,viewEye3D
imul eax,SCREEN_H
cdq
idiv ecx
mov depth,eax
mov eax,row
imul eax,SCREEN_W*4
mov edi,eax
mov column,0
upper_glow_pixel:
mov ebx,column
mov eax,depth
cmp eax,cityDepthBuffer3D[edi]
ja upper_glow_next
imul cityRayX3D[ebx*4]
shrd eax,edx,16
add eax,playerX
sub eax,FP_1*105/10
test eax,eax
jns @F
neg eax
@@:
mov wx,eax
mov eax,depth
imul cityRayY3D[ebx*4]
shrd eax,edx,16
add eax,playerY
sub eax,FP_1*243/10
test eax,eax
jns @F
neg eax
@@:
add eax,wx
cmp eax,3*FP_1
jae upper_glow_next
neg eax
add eax,3*FP_1
imul pulse
shrd eax,edx,16
mov amount,eax
mov eax,screenBuffer[edi]
mov pixel,eax
shr eax,16
and eax,255
add eax,amount
cmp eax,255
jbe @F
mov eax,255
@@:
shl eax,16
mov ebx,pixel
and ebx,0000FFFFh
or eax,ebx
mov screenBuffer[edi],eax
upper_glow_next:
add edi,4
inc column
cmp column,SCREEN_W
jb upper_glow_pixel
inc row
cmp row,SCREEN_H
jb upper_glow_row
ret
ApplyUpperPortalGlow3D ENDP

View file

@ -0,0 +1,79 @@
UpdateInteriorTravel3D PROTO
UpdateInteriorEye3D PROTO
UpdateStairHeight3D PROTO
RenderWalkableStairs3D PROTO
RenderUpperLounge3D PROTO
RenderHallPortals3D PROTO
CheckUpperInteractions3D PROTO
InteriorDoorCanWalk3D PROTO :DWORD,:DWORD
UpperCanWalk3D PROTO :DWORD,:DWORD
ResolveUpperMaterial3D PROTO :DWORD
InitUpperSurfaces3D PROTO
DrawUpperGameTable3D PROTO :DWORD,:DWORD
BeginWalkableStairs3D PROTO :DWORD,:DWORD
RenderInteriorViews3D PROTO
DrawInteriorView3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
TryUpperInteraction3D PROTO
TryWalkUpperPortal3D PROTO
UpdateUpperEffects3D PROTO
ApplyUpperPortalGlow3D PROTO
INTERACT_UPPER_LEFT equ 43
INTERACT_UPPER_RIGHT equ 44
; stairRoute: 1=basement, 2=upper lounge. Direction is controlled by movement.
.data
stairRoute3D dd 0
basementDoorTarget3D dd 0
basementDoorOpen3D dd 0
interiorDoorClock3D dd -1
portalRendering3D dd 0
portalPixelsWritten3D dd 0
upperLeverMask3D dd 0
upperPortalReady3D dd 0
upperPortalEntered3D dd 0
upperEffectClock3D dd -1
upperLeverTime3D dd 0,0
upperGamesArt3D AnimPack3D <>
upperPortalArt3D AnimPack3D <>
upperLeverArt3D AnimPack3D <>
upperBarArt3D AnimPack3D <>
upperBrazierArt3D AnimPack3D <>
upperGamesPath3D db "assets\animations\hall\upper-games-v2.bin",0
upperPortalPath3D db "assets\animations\hall\upper-portal-v3.bin",0
upperLeverPath3D db "assets\animations\hall\upper-levers-v3.bin",0
upperBarPath3D db "assets\animations\hall\upper-bar-v2.bin",0
upperBrazierPath3D db "assets\animations\hall\upper-braziers-v3.bin",0
upperLeftPrompt3D db 69,58,32,207,206,210,223,205,211,210,220,32,203,197,194,219,201,32,208,219,215,192,195,0
upperRightPrompt3D db 69,58,32,207,206,210,223,205,211,210,220,32,207,208,192,194,219,201,32,208,219,215,192,195,0
upperPortalPrompt3D db 69,58,32,194,206,201,210,200,32,194,32,193,192,216,205,222,32,192,196,192,0
upperPortalLocked3D db 205,211,198,205,206,32,207,206,210,223,205,211,210,220,32,206,193,192,32,208,219,215,192,195,192,0
upperPortal3D CityProp3D <FP_1*105/10,FP_1*247/10,FP_1*25/10,FP_1*245/100,0,OFFSET upperPortalArt3D>
upperBrazier3D CityProp3D <0,FP_1*236/10,FP_1*55/100,FP_1*11/10,0,OFFSET upperBrazierArt3D>
upperLever3D CityProp3D <0,FP_1*226/10,FP_1*45/100,FP_1*85/100,0,OFFSET upperLeverArt3D>
upperStool3D CityProp3D <0,FP_1*157/10,FP_1*55/100,FP_1*9/10,1,OFFSET upperBarArt3D>
upperSofa3D CityProp3D <FP_1*48/10,FP_1*142/10,FP_1*25/10,FP_1*11/10,6,OFFSET hallProps3D>
upperMaterialArt3D AnimPack3D <>
upperMaterialPath3D db "assets\animations\hall\upper-materials-v1.bin",0
upperBarName3D db 205,206,215,205,206,201,32,193,192,208,32,34,199,192,210,204,197,205,200,197,34,0
upperStairPrompt3D db 203,197,209,210,205,200,214,192,32,205,192,32,194,210,206,208,206,201,32,221,210,192,198,0
upperExitPrompt3D db 203,197,209,210,205,200,214,192,32,194,32,195,203,192,194,205,219,201,32,199,192,203,0
upperBarTender3D CityProp3D <26*FP_1,14*FP_1,FP_1,FP_1,0,OFFSET hallBartender3D,FP_1*9/20>
upperTable3D CityProp3D <8*FP_1,7*FP_1,FP_1*8/5,FP_1*17/20,0,OFFSET hallBackFurniture3D>
upperChair3D CityProp3D <8*FP_1,9*FP_1,FP_1*3/4,FP_1,0,OFFSET hallChairFurniture3D>
upperStationX3D dd 7*FP_1,12*FP_1,9*FP_1
upperStationY3D dd 17*FP_1,18*FP_1,FP_1*237/10
upperDealer3D CityProp3D <0,0,FP_1*3/5,FP_1*7/5,0,OFFSET hallProps3D>
upperGuest3D CityProp3D <0,0,FP_1*3/5,FP_1*6/5,5,0>
upperGuestX3D dd FP_1*48/10,FP_1*64/10,FP_1*143/10
upperGuestY3D dd FP_1*231/10,FP_1*231/10,FP_1*231/10
upperGuestFrame3D dd 11,35,5
.data?
upperFeltPixels3D dd 1024 dup(?)
upperKeyPixels3D dd 1024 dup(?)
upperGlowPixels3D dd 1024 dup(?)
portalClipTop3D dd SCREEN_W dup(?)
portalClipBottom3D dd SCREEN_W dup(?)
portalClipDepth3D dd SCREEN_W dup(?)
portalSourcePixels3D dd SCREEN_SIZE dup(?)
portalSourceDepth3D dd SCREEN_SIZE dup(?)
portalSourceWalls3D dd SCREEN_W dup(?)
.code

View file

@ -64,4 +64,100 @@ bunkerMap3D db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
upperLoungeMap3D db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,0,0,0,0,0,0,0,0,0,0,0,0,0,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
db 125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125,125
upperStairMap3D db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
db 102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102
basementStairMap3D db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
.code

View file

@ -14,8 +14,12 @@ hall_plane_row:
mov eax, viewCeiling3D
sub eax, viewEye3D
mov material, 101
cmp hallLevel3D, 0
je @F
cmp hallLevel3D,2
jne @F
mov material,125
@@:
cmp hallLevel3D, 1
jne @F
mov material, 93
@@:
jmp hall_plane_distance
@ -118,6 +122,21 @@ hall_plane_sample:
hall_plane_wood:
mov ebx, 85
hall_plane_choose_hd:
cmp ebx,122
jb hall_plane_existing_hd
cmp ebx,125
ja hall_plane_existing_hd
mov ecx,upperMaterialArt3D.pixels
test ecx,ecx
jz hall_plane_upper_fallback
sub ebx,122
shl ebx,18
add ecx,ebx
jmp hall_plane_hd_coordinates
hall_plane_upper_fallback:
mov ebx,85
mov material,ebx
hall_plane_existing_hd:
cmp distance, 6*FP_1
ja hall_plane_low_sample
mov ecx, hallRoomHD3D.pixels
@ -135,6 +154,7 @@ hall_plane_old_hd:
sub ebx, 80
shl ebx, 18
add ecx, ebx
hall_plane_hd_coordinates:
mov eax, wy
shr eax, 8
and eax, 255
@ -196,13 +216,27 @@ hall_plane_next_row:
ret
RenderCeilingAndFloor ENDP
RenderSprites PROC uses ebx esi edi
RenderSprites PROC uses ebx esi edi
cmp engineMode,MODE_STAIRS
je hall_sprites_done
cmp hallLevel3D, 0
jne hall_sprites_done
xor esi, esi
hall_sprite_loop:
cmp esi,3
jb hall_sprite_next
cmp esi,17
je hall_sprite_next
cmp esi,19
je hall_sprite_next
cmp esi,21
je hall_sprite_next
cmp esi, 14
je hall_sprite_next
je hall_sprite_next
cmp esi, 15
je hall_sprite_next
cmp esi, 16
je hall_sprite_next
imul ebx, esi, SIZEOF SpriteObject
mov eax, casinoSprites[ebx].posX
mov hallSpriteTemp3D.posX, eax
@ -301,13 +335,37 @@ init_plane_lut:
inc ecx
jmp init_plane_lut
init_plane_done:
call InitCasinoNpcMind
call InitCasinoNpcMind
call PositionTerminalStations3D
mov scrStation3D.posX,29*FP_1
mov scrStation3D.posY,FP_1*15/2
call InitNpcLife3D
call BuildHallLighting3D
ret
InitHall3D ENDP
LoadHallAssets3D PROC
call InitUpperSurfaces3D
invoke LoadAnimPack3D, OFFSET upperMaterialArt3D, OFFSET upperMaterialPath3D
invoke ValidateCityPack3D, OFFSET upperMaterialArt3D,256,256,4
invoke LoadAnimPack3D,OFFSET upperGamesArt3D,OFFSET upperGamesPath3D
invoke ValidateCityPack3D,OFFSET upperGamesArt3D,320,224,2
invoke LoadAnimPack3D,OFFSET upperPortalArt3D,OFFSET upperPortalPath3D
invoke ValidateCityPack3D,OFFSET upperPortalArt3D,256,512,13
invoke LoadAnimPack3D,OFFSET upperLeverArt3D,OFFSET upperLeverPath3D
invoke ValidateCityPack3D,OFFSET upperLeverArt3D,128,224,6
invoke LoadAnimPack3D,OFFSET upperBarArt3D,OFFSET upperBarPath3D
invoke ValidateCityPack3D,OFFSET upperBarArt3D,160,256,2
invoke LoadAnimPack3D,OFFSET upperBrazierArt3D,OFFSET upperBrazierPath3D
invoke ValidateCityPack3D,OFFSET upperBrazierArt3D,128,256,4
invoke LoadAnimPack3D, OFFSET cabaretArt3D, OFFSET cabaretPath3D
invoke ValidateCityPack3D, OFFSET cabaretArt3D,160,256,12
invoke LoadAnimPack3D, OFFSET cabaretPianoArt3D, OFFSET cabaretPianoPath3D
invoke ValidateCityPack3D, OFFSET cabaretPianoArt3D,384,256,4
invoke LoadAnimPack3D, OFFSET cabaretGuitarArt3D, OFFSET cabaretGuitarPath3D
invoke ValidateCityPack3D, OFFSET cabaretGuitarArt3D,160,256,4
invoke LoadAnimPack3D, OFFSET npcGuestArt3D, OFFSET npcGuestPath3D
invoke ValidateCityPack3D, OFFSET npcGuestArt3D,128,224,36
call LoadWorldDressing3D
call LoadScratchArt3D
invoke LoadAnimPack3D, OFFSET hallCardFurniture3D, OFFSET hallCardFurniturePath3D
@ -379,6 +437,16 @@ hall_load_sign:
LoadHallAssets3D ENDP
FreeHallAssets3D PROC
invoke FreeAnimPack3D, OFFSET upperMaterialArt3D
invoke FreeAnimPack3D,OFFSET upperGamesArt3D
invoke FreeAnimPack3D,OFFSET upperPortalArt3D
invoke FreeAnimPack3D,OFFSET upperLeverArt3D
invoke FreeAnimPack3D,OFFSET upperBarArt3D
invoke FreeAnimPack3D,OFFSET upperBrazierArt3D
invoke FreeAnimPack3D, OFFSET cabaretArt3D
invoke FreeAnimPack3D, OFFSET cabaretPianoArt3D
invoke FreeAnimPack3D, OFFSET cabaretGuitarArt3D
invoke FreeAnimPack3D, OFFSET npcGuestArt3D
call FreeWorldDressing3D
call FreeNativeWheelArt3D
call FreeNativeCoinArt3D
@ -469,6 +537,18 @@ surface_missing:
ResolveWorldSurface3D ENDP
RenderHall3D PROC uses ebx esi
cmp engineMode,MODE_STAIRS
jne @F
call RenderWalkableStairs3D
call RenderInteriorViews3D
ret
@@:
cmp hallLevel3D,2
jne @F
call RenderUpperLounge3D
call RenderInteriorViews3D
ret
@@:
cmp hallLevel3D, 0
jne hall_render_bunker
; Fixed bar counter/stage are world geometry, with height and occlusion.
@ -538,6 +618,8 @@ hall_render_props:
je hall_render_next_prop
imul eax, esi, SIZEOF CityProp3D
add eax, OFFSET hallObjects3D
cmp esi,6
je hall_render_next_prop
invoke DrawCityObject3D, eax, 0
hall_render_next_prop:
inc esi
@ -560,30 +642,14 @@ hall_render_lounge:
cmp esi, HALL_DECOR_COUNT
jb hall_render_lounge
call RenderPatrons3D
call RenderCabaretActors3D
invoke DrawCityObject3D,OFFSET scrStation3D,0
call RenderHallDressing3D
call RenderHallPortals3D
call ApplyHallLighting3D
call RenderInteriorViews3D
ret
hall_render_bunker:
mov esi, 0
hall_stair_step:
mov eax, esi
imul eax, FP_1/2
add eax, 24*FP_1
lea ebx, [eax+FP_1/2]
lea ecx, [esi+1]
imul ecx, FP_1/8
invoke DrawCityHorizontal3D,15*FP_1,18*FP_1,eax,ebx,ecx,88
mov eax, esi
imul eax, FP_1/2
add eax, 24*FP_1
mov ecx, esi
imul ecx, FP_1/8
lea edx, [ecx+FP_1/8]
invoke DrawCityVertical3D,1,eax,15*FP_1,18*FP_1,ecx,edx,92
inc esi
cmp esi, 8
jb hall_stair_step
invoke DrawCityBoxSides3D, 14*FP_1,20*FP_1,14*FP_1,15*FP_1,0,FP_1/2,96
invoke DrawCityHorizontal3D, 14*FP_1,20*FP_1,14*FP_1,15*FP_1,FP_1/2,97
invoke DrawCityVertical3D,1,11*FP_1+FP_1/10,14*FP_1,16*FP_1,FP_1/4,FP_1*8/5,95
@ -606,8 +672,8 @@ hall_stair_step:
mov bunkerMerchant3D.frame, eax
invoke DrawCityObject3D, OFFSET bunkerMerchant3D, 0
invoke DrawCityObject3D, OFFSET bunkerDisplay3D, 0
invoke DrawCityObject3D, OFFSET bunkerDesk3D, 0
invoke DrawCityObject3D, OFFSET bunkerDoor3D, 0
invoke DrawCityObject3D, OFFSET bunkerDesk3D, 0
call RenderInteriorViews3D
ret
RenderHall3D ENDP
@ -645,6 +711,11 @@ hall_target_done:
HallTarget3D ENDP
CheckHallInteractions3D PROC
cmp hallLevel3D,2
jne @F
call CheckUpperInteractions3D
ret
@@:
cmp hallLevel3D, 0
jne hall_check_bunker
cmp playerX, 3*FP_1
@ -656,20 +727,30 @@ CheckHallInteractions3D PROC
; A point on the serving edge remains in front even when standing close.
invoke HallTarget3D, playerX,23*FP_1+FP_HALF,INTERACT_BAR,OFFSET hallBarPrompt3D
hall_check_other:
invoke HallTarget3D,16*FP_1,FP_1*73/10,INTERACT_CABARET_SEAT,OFFSET cabaretSeat3D
invoke HallTarget3D,scrStation3D.posX,scrStation3D.posY,INTERACT_SCRATCH,OFFSET scratchHall3D
invoke HallTarget3D, 28*FP_1+FP_HALF,10*FP_1+FP_HALF,INTERACT_BASEMENT,OFFSET hallBunkerPrompt3D
invoke HallTarget3D,FP_1*285/10,FP_1*105/10,INTERACT_BASEMENT,OFFSET hallBunkerPrompt3D
invoke HallTarget3D, 27*FP_1,25*FP_1,INTERACT_BILLIARDS,OFFSET hallPoolPrompt3D
invoke HallTarget3D,FP_1*305/10,21*FP_1,0,OFFSET upperStairPrompt3D
call CheckNpcInteractions3D
ret
hall_check_bunker:
mov interactTargetId, 0
mov interactPromptPtr, 0
invoke HallTarget3D, 16*FP_1,14*FP_1+FP_HALF,INTERACT_SHOP,OFFSET promptShop
invoke HallTarget3D, 16*FP_1,27*FP_1,INTERACT_UPSTAIRS,OFFSET hallUpPrompt3D
ret
CheckHallInteractions3D ENDP
TriggerHallInteraction3D PROC
TriggerHallInteraction3D PROC
call TryUpperInteraction3D
test eax,eax
jnz hall_trigger_handled
cmp interactTargetId,INTERACT_CABARET_SEAT
jne @F
call BeginCabaret3D
mov eax,1
ret
@@:
cmp interactTargetId,INTERACT_SCRATCH
jne @F
call BeginScratch3D
@ -693,21 +774,9 @@ TriggerHallInteraction3D PROC
ret
hall_trigger_stairs:
cmp interactTargetId, INTERACT_BASEMENT
je hall_begin_stairs
cmp interactTargetId, INTERACT_UPSTAIRS
jne hall_trigger_none
hall_begin_stairs:
cmp hallLevel3D, 0
jne @F
mov eax, playerX
mov stairsReturnX3D, eax
mov eax, playerY
mov stairsReturnY3D, eax
mov eax, playerAngle
mov stairsReturnAngle3D, eax
@@:
mov engineMode, MODE_STAIRS
mov stairsTime3D, 0
; The button opens the steel leaf. Walking across its threshold is separate.
mov basementDoorTarget3D,FP_1
call ClearKeys3D
mov eax, 1
ret
@ -720,114 +789,26 @@ hall_trigger_none:
mov eax, 1
ret
@@:
xor eax, eax
xor eax, eax
hall_trigger_handled:
ret
TriggerHallInteraction3D ENDP
HandleStairs3D PROC
mov eax, deltaTime3D
add stairsTime3D, eax
cmp stairsTime3D, 1600
jb stairs_handle_done
xor hallLevel3D, 1
cmp hallLevel3D, 0
je stairs_back_to_hall
mov worldMap3D, OFFSET bunkerMap3D
mov floorTexture3D, 94
mov viewCeiling3D, FP_1*5/2
mov playerX, 16*FP_1+FP_HALF
mov playerY, 23*FP_1+FP_HALF
mov playerAngle, 384
jmp stairs_complete
stairs_back_to_hall:
mov worldMap3D, OFFSET casinoMap
mov floorTexture3D, 100
mov viewCeiling3D, FP_1*5/2
mov eax, stairsReturnX3D
mov playerX, eax
mov eax, stairsReturnY3D
mov playerY, eax
mov eax, stairsReturnAngle3D
mov playerAngle, eax
stairs_complete:
mov viewEye3D, FP_1*17/20
cmp hallLevel3D,0
jne @F
mov viewEye3D,HALL_STANDING_EYE
@@:
mov engineMode, MODE_WALK
call ClearKeys3D
stairs_handle_done:
call UpdateStairHeight3D
call HandlePlayerInput
call UpdateInteriorTravel3D
call UpdateStairHeight3D
ret
HandleStairs3D ENDP
RenderStairs3D PROC uses ebx esi
local oldX:DWORD,oldY:DWORD,oldAngle:DWORD,oldMap:DWORD,oldEye:DWORD,oldCeiling:DWORD,oldLevel:DWORD,oldFloor:DWORD
mov eax, playerX
mov oldX, eax
mov eax, playerY
mov oldY, eax
mov eax, playerAngle
mov oldAngle, eax
mov eax, worldMap3D
mov oldMap, eax
mov eax, viewEye3D
mov oldEye, eax
mov eax, viewCeiling3D
mov oldCeiling, eax
mov eax, hallLevel3D
mov oldLevel, eax
mov eax, floorTexture3D
mov oldFloor, eax
mov hallLevel3D, 1
mov worldMap3D, OFFSET bunkerMap3D
mov floorTexture3D, 94
mov viewCeiling3D, FP_1*5/2
mov playerX, 16*FP_1+FP_HALF
mov playerAngle, 384
mov eax, stairsTime3D
cmp eax, 1600
jbe @F
mov eax, 1600
@@:
cmp oldLevel, 0
je @F
neg eax
add eax, 1600
@@:
imul eax, 4*FP_1
xor edx, edx
mov ecx, 1600
div ecx
mov ebx, 27*FP_1+FP_HALF
sub ebx, eax
mov playerY, ebx
shr eax, 2
neg eax
add eax, FP_1*37/20
mov viewEye3D, eax
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderHall3D
mov eax, oldX
mov playerX, eax
mov eax, oldY
mov playerY, eax
mov eax, oldAngle
mov playerAngle, eax
mov eax, oldMap
mov worldMap3D, eax
mov eax, oldEye
mov viewEye3D, eax
mov eax, oldCeiling
mov viewCeiling3D, eax
mov eax, oldLevel
mov hallLevel3D, eax
mov eax, oldFloor
mov floorTexture3D, eax
call UpdateCamera3D
call RenderHUD
ret
RenderStairs3D ENDP
@ -932,6 +913,11 @@ RenderBar3D PROC uses ebx
mov playerX, 6*FP_1
mov playerY, 25*FP_1+FP_1/3
mov playerAngle, 384
cmp hallLevel3D,2
jne @F
mov playerX,FP_1*25/2
mov playerY,16*FP_1+FP_1/3
@@:
mov viewHorizon3D, HALF_H
mov viewEye3D, HALL_STANDING_EYE
call UpdateCamera3D
@ -956,7 +942,12 @@ bar_active_scene:
invoke DrawString8x8,176,91,ebx,00E7CA88h
bar_draw_title:
invoke DrawBox,0,0,320,28,0014191Dh
invoke DrawString8x8, 18,15,OFFSET barName3D,00E7CA88h
mov eax,OFFSET barName3D
cmp hallLevel3D,2
jne @F
mov eax,OFFSET upperBarName3D
@@:
invoke DrawString8x8, 18,15,eax,00E7CA88h
cmp barPhase3D, 2
jne bar_render_done
mov eax, barTime3D
@ -1043,11 +1034,27 @@ city_world_guard_right:
RenderCityGuards3D ENDP
CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD
cmp engineMode,MODE_STAIRS
jne @F
mov eax,1
ret
@@:
cmp hallLevel3D,2
jne @F
invoke UpperCanWalk3D,px,py
ret
@@:
cmp hallLevel3D, 0
jne hall_collision_bunker
invoke ScratchCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
invoke InteriorDoorCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
invoke TerminalCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
xor esi, esi
hall_collision_decor:
imul ebx, esi, SIZEOF CityProp3D
@ -1221,24 +1228,10 @@ hall_city_next_pilaster:
RenderCityFacade3D ENDP
UpdateHall3D PROC
call UpdateCabaret3D
call UpdateInteriorTravel3D
call UpdatePatrons3D
mov viewHorizon3D, HALF_H
mov viewEye3D, FP_1*17/20
cmp hallLevel3D, 0
jne hall_update_bunker_eye
mov viewEye3D,HALL_STANDING_EYE
jmp @F
hall_update_bunker_eye:
mov eax, playerY
sub eax, 24*FP_1
jle @F
shr eax, 2
cmp eax, FP_1
jbe hall_step_eye
mov eax, FP_1
hall_step_eye:
add viewEye3D, eax
@@:
call UpdateInteriorEye3D
cmp barIntoxication3D, 0
je hall_update_done
mov eax, deltaTime3D
@ -1352,4 +1345,8 @@ patron_render_loop:
ret
RenderPatrons3D ENDP
include src_graphics\world_dressing.inc
include src_graphics\terminal.inc
include src_graphics\hall_expansion.inc
include src_graphics\interior_portals.inc
include src_graphics\cabaret.inc
include src_graphics\hall_lighting.inc

View file

@ -0,0 +1,244 @@
; Doorways show the adjacent native scene through a depth-tested screen-space
; aperture. Camera translations are the same as the walking thresholds.
DrawInteriorView3D PROC uses ebx esi edi axis:DWORD,plane:DWORD,lo:DWORD,hi:DWORD,z0:DWORD,z1:DWORD,offsetX:DWORD,offsetY:DWORD,offsetZ:DWORD,targetMap:DWORD,targetMode:DWORD,targetLevel:DWORD,targetFloor:DWORD,targetRoute:DWORD
local oldX:DWORD,oldY:DWORD,oldEye:DWORD,oldCeiling:DWORD,oldMap:DWORD
local oldMode:DWORD,oldLevel:DWORD,oldFloor:DWORD,oldRoute:DWORD
local found:DWORD,depth:DWORD,row:DWORD,pixel:DWORD
mov found,0
xor ebx,ebx
portal_mask_column:
mov portalClipDepth3D[ebx*4],0
cmp axis,0
jne portal_mask_y
invoke CityRayPlane3D,plane,playerX,cityRayX3D[ebx*4]
jmp portal_mask_depth
portal_mask_y:
invoke CityRayPlane3D,plane,playerY,cityRayY3D[ebx*4]
portal_mask_depth:
cmp eax,2048
jle portal_mask_next
cmp eax,8*FP_1
ja portal_mask_next
cmp eax,zBuffer[ebx*4]
ja portal_mask_next
mov depth,eax
cmp axis,0
jne portal_along_x
imul cityRayY3D[ebx*4]
shrd eax,edx,16
add eax,playerY
jmp portal_mask_along
portal_along_x:
imul cityRayX3D[ebx*4]
shrd eax,edx,16
add eax,playerX
portal_mask_along:
cmp eax,lo
jl portal_mask_next
cmp eax,hi
jge portal_mask_next
mov eax,viewEye3D
sub eax,z1
imul eax,SCREEN_H
cdq
idiv depth
add eax,viewHorizon3D
test eax,eax
jns @F
xor eax,eax
@@:
mov portalClipTop3D[ebx*4],eax
mov eax,viewEye3D
sub eax,z0
imul eax,SCREEN_H
cdq
idiv depth
add eax,viewHorizon3D
cmp eax,SCREEN_H
jle @F
mov eax,SCREEN_H
@@:
cmp eax,portalClipTop3D[ebx*4]
jle portal_mask_next
mov portalClipBottom3D[ebx*4],eax
mov eax,depth
mov portalClipDepth3D[ebx*4],eax
inc found
portal_mask_next:
inc ebx
cmp ebx,SCREEN_W
jb portal_mask_column
cmp found,0
je portal_view_done
cld
lea esi,screenBuffer
lea edi,portalSourcePixels3D
mov ecx,SCREEN_SIZE
rep movsd
lea esi,cityDepthBuffer3D
lea edi,portalSourceDepth3D
mov ecx,SCREEN_SIZE
rep movsd
lea esi,zBuffer
lea edi,portalSourceWalls3D
mov ecx,SCREEN_W
rep movsd
mov eax,playerX
mov oldX,eax
add eax,offsetX
mov playerX,eax
mov eax,playerY
mov oldY,eax
add eax,offsetY
mov playerY,eax
mov eax,viewEye3D
mov oldEye,eax
add eax,offsetZ
mov viewEye3D,eax
mov eax,viewCeiling3D
mov oldCeiling,eax
mov viewCeiling3D,FP_1*5/2
cmp targetMode,MODE_STAIRS
jne @F
mov viewCeiling3D,FP_1*11/2
@@:
mov eax,worldMap3D
mov oldMap,eax
mov eax,targetMap
mov worldMap3D,eax
mov eax,engineMode
mov oldMode,eax
mov eax,targetMode
mov engineMode,eax
mov eax,hallLevel3D
mov oldLevel,eax
mov eax,targetLevel
mov hallLevel3D,eax
mov eax,floorTexture3D
mov oldFloor,eax
mov eax,targetFloor
mov floorTexture3D,eax
mov eax,stairRoute3D
mov oldRoute,eax
mov eax,targetRoute
mov stairRoute3D,eax
mov portalRendering3D,1
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderSprites
call RenderHall3D
mov portalRendering3D,0
mov eax,oldX
mov playerX,eax
mov eax,oldY
mov playerY,eax
mov eax,oldEye
mov viewEye3D,eax
mov eax,oldCeiling
mov viewCeiling3D,eax
mov eax,oldMap
mov worldMap3D,eax
mov eax,oldMode
mov engineMode,eax
mov eax,oldLevel
mov hallLevel3D,eax
mov eax,oldFloor
mov floorTexture3D,eax
mov eax,oldRoute
mov stairRoute3D,eax
mov row,0
xor edi,edi
portal_compose_row:
xor ebx,ebx
portal_compose_pixel:
mov eax,portalClipDepth3D[ebx*4]
test eax,eax
jz portal_restore_pixel
mov ecx,row
cmp ecx,portalClipTop3D[ebx*4]
jl portal_restore_pixel
cmp ecx,portalClipBottom3D[ebx*4]
jge portal_restore_pixel
cmp eax,portalSourceDepth3D[edi]
ja portal_restore_pixel
; The rendered destination is already in screenBuffer at this exact ray.
mov cityDepthBuffer3D[edi],eax
inc portalPixelsWritten3D
jmp portal_compose_next
portal_restore_pixel:
mov eax,portalSourcePixels3D[edi]
mov screenBuffer[edi],eax
mov eax,portalSourceDepth3D[edi]
mov cityDepthBuffer3D[edi],eax
portal_compose_next:
add edi,4
inc ebx
cmp ebx,SCREEN_W
jb portal_compose_pixel
inc row
cmp row,SCREEN_H
jb portal_compose_row
lea esi,portalSourceWalls3D
lea edi,zBuffer
mov ecx,SCREEN_W
rep movsd
call UpdateCamera3D
call PrepareCityRays3D
portal_view_done:
ret
DrawInteriorView3D ENDP
RenderInteriorViews3D PROC
cmp portalRendering3D,0
jne interior_views_done
mov portalPixelsWritten3D,0
cmp engineMode,MODE_STAIRS
je interior_views_stairs
cmp hallLevel3D,2
je interior_views_lounge
cmp hallLevel3D,1
je interior_views_bunker
cmp playerX,FP_1*303/10
jae @F
invoke DrawInteriorView3D,0,FP_1*3025/100,20*FP_1,22*FP_1,0,FP_1*5/2,-27*FP_1,0,0,OFFSET upperStairMap3D,MODE_STAIRS,0,85,2
@@:
cmp basementDoorOpen3D,FP_1*9/10
jb interior_views_done
cmp playerX,FP_1*286/10
jae interior_views_done
invoke DrawInteriorView3D,0,FP_1*285/10,FP_1*975/100,FP_1*1125/100,0,FP_1*19/10,-25*FP_1,FP_1*21/2,FP_1*3/2,OFFSET basementStairMap3D,MODE_STAIRS,0,94,1
ret
interior_views_lounge:
cmp playerX,FP_1*32/10
jbe interior_views_done
invoke DrawInteriorView3D,0,FP_1*325/100,20*FP_1,22*FP_1,0,FP_1*5/2,12*FP_1,0,FP_1*13/5,OFFSET upperStairMap3D,MODE_STAIRS,2,85,2
ret
interior_views_bunker:
cmp playerY,FP_1*277/10
jae interior_views_done
invoke DrawInteriorView3D,1,FP_1*2765/100,15*FP_1,18*FP_1,0,FP_1*5/2,0,-13*FP_1,0,OFFSET basementStairMap3D,MODE_STAIRS,1,94,1
ret
interior_views_stairs:
cmp stairRoute3D,1
je interior_views_concrete
cmp playerX,FP_1*31/10
jbe @F
invoke DrawInteriorView3D,0,FP_1*315/100,20*FP_1,22*FP_1,0,FP_1*5/2,27*FP_1,0,0,OFFSET casinoMap,MODE_WALK,0,100,0
@@:
cmp playerX,16*FP_1
jae interior_views_done
invoke DrawInteriorView3D,0,FP_1*159/10,20*FP_1,22*FP_1,FP_1*13/5,FP_1*51/10,-12*FP_1,0,-FP_1*13/5,OFFSET upperLoungeMap3D,MODE_WALK,2,124,0
ret
interior_views_concrete:
cmp playerX,FP_1*31/10
jbe @F
invoke DrawInteriorView3D,0,FP_1*315/100,FP_1*2025/100,FP_1*2175/100,FP_1*3/2,FP_1*34/10,25*FP_1,-FP_1*21/2,-FP_1*3/2,OFFSET casinoMap,MODE_WALK,0,100,0
@@:
cmp playerY,FP_1*143/10
jbe interior_views_done
invoke DrawInteriorView3D,1,FP_1*144/10,15*FP_1,18*FP_1,0,FP_1*5/2,0,13*FP_1,0,OFFSET bunkerMap3D,MODE_WALK,1,94,0
interior_views_done:
ret
RenderInteriorViews3D ENDP

View file

@ -58,9 +58,11 @@ DrawBJMiniCard3D PROC uses ebx esi x:DWORD,y:DWORD,card:DWORD,hidden:DWORD
@@:
mov ink,00BF263Ch
bj_mini_face:
lea edx,cardRankNames3D[edx*2]
mov edx,rank
lea edx,[edx+edx*2]
add edx,OFFSET cardRankNames3D
mov eax,x
add eax,3
add eax,2
mov ecx,y
add ecx,3
invoke DrawString8x8,eax,ecx,edx,ink

View file

@ -360,7 +360,7 @@ NpcNameText3D PROC uses esi edi identity:DWORD
mov eax,identity
cmp eax,NPC_IDENTITY_COUNT
jae npc_name_empty
mov esi,npcNames3D[eax*4]
mov esi,npcGuestNames3D[eax*4]
lea edi,npcNameBuffer3D
mov ecx,60
npc_name_copy:

View file

@ -466,20 +466,19 @@ npc_draw_person:
mov npcSprite3D.posY,eax
mov npcSprite3D.worldW,FP_1*3/5
mov npcSprite3D.worldH,FP_1*6/5
mov npcSprite3D.frame,0
cmp npcLife3D[ebx].identity,11
jne @F
mov npcSprite3D.frame,4
@@:
mov npcSprite3D.asset,OFFSET npcGuestArt3D
cmp npcGuestArt3D.pixels,0
je npc_draw_next
mov eax,npcLife3D[ebx].identity
mov eax,npcGuestAppearance3D[eax*4]
imul eax,6
mov npcSprite3D.frame,eax
cmp npcLife3D[ebx].state,NPC_SIT
jne npc_draw_walk
mov npcSprite3D.frame,8
cmp npcLife3D[ebx].identity,11
jne @F
mov npcSprite3D.frame,10
@@:
mov npcSprite3D.worldW,FP_1*4/5
mov npcSprite3D.worldH,FP_1
add npcSprite3D.frame,5
; Seated pose is already shorter within the same foot-anchored canvas.
mov npcSprite3D.worldW,FP_1*3/5
mov npcSprite3D.worldH,FP_1*6/5
jmp npc_draw_sprite
npc_draw_walk:
cmp npcLife3D[ebx].state,NPC_WALK
@ -489,6 +488,7 @@ npc_draw_walk:
mov ecx,180
div ecx
and eax,3
inc eax
add npcSprite3D.frame,eax
npc_draw_sprite:
invoke DrawCityObject3D,OFFSET npcSprite3D,0

View file

@ -44,6 +44,18 @@ NpcLife3D ENDS
.data
npcLife3D NpcLife3D NPC_LIFE_COUNT dup(<>)
npcLifeReady3D dd 0
npcGuestArt3D AnimPack3D <>
npcGuestPath3D db "assets\animations\hall\guest-cast-v1.bin",0
; The two rotations share six appearances, with a mixed cast on every visit.
npcGuestAppearance3D dd 0,2,4,1,3,5,2,0,4,1,3,5
npcGuestNames3D dd OFFSET npcName0,OFFSET npcName1,OFFSET npcName2,OFFSET npcGuestAnna3D
dd OFFSET npcGuestMarina3D,OFFSET npcGuestVera3D,OFFSET npcName6,OFFSET npcName7
dd OFFSET npcName8,OFFSET npcGuestOlga3D,OFFSET npcGuestDaria3D,OFFSET npcName11
npcGuestAnna3D db 192,237,237,224,32,204,232,240,237,224,255,0
npcGuestMarina3D db 204,224,240,232,237,224,32,203,243,237,237,224,255,0
npcGuestVera3D db 194,229,240,224,32,209,229,240,229,225,240,255,237,224,255,0
npcGuestOlga3D db 206,235,252,227,224,32,204,232,240,237,224,255,0
npcGuestDaria3D db 196,224,240,252,255,32,193,238,227,224,242,224,255,0
npcLifeClock3D dd 0
npcSelected3D dd -1
npcDialogIdentity3D dd 0

View file

@ -576,6 +576,7 @@ SaveProgress3D PROC uses ebx
ENDIF
cmp arrivalConfirmed3D, 0
je progress_saved
call RefreshTerminalAchievements3D
cmp nativeProfileActive3D, 0
je @F
call SaveGraphicProfile3D

378
src_graphics/terminal.inc Normal file
View file

@ -0,0 +1,378 @@
LoadTerminalArt3D PROC
invoke LoadAnimPack3D,OFFSET terminalWorldArt3D,OFFSET terminalWorldPath3D
invoke ValidateCityPack3D,OFFSET terminalWorldArt3D,256,512,4
invoke LoadAnimPack3D,OFFSET terminalScreenArt3D,OFFSET terminalScreenPath3D
invoke ValidateCityPack3D,OFFSET terminalScreenArt3D,640,480,1
ret
LoadTerminalArt3D ENDP
FreeTerminalArt3D PROC
invoke FreeAnimPack3D,OFFSET terminalWorldArt3D
invoke FreeAnimPack3D,OFFSET terminalScreenArt3D
ret
FreeTerminalArt3D ENDP
ResolveTerminalSurface3D PROC material:DWORD
mov eax,material
sub eax,118
cmp eax,4
jae terminal_surface_missing
cmp terminalWorldArt3D.pixels,0
je terminal_surface_missing
shl eax,19
add eax,terminalWorldArt3D.pixels
mov ecx,256
mov edx,512
ret
terminal_surface_missing:
xor eax,eax
ret
ResolveTerminalSurface3D ENDP
PositionTerminalStations3D PROC
mov casinoSprites[15*SIZEOF SpriteObject].SpriteObject.posX,TERMINAL_CASH_X
mov casinoSprites[15*SIZEOF SpriteObject].SpriteObject.posY,TERMINAL_CENTER_Y
mov casinoSprites[16*SIZEOF SpriteObject].SpriteObject.posX,TERMINAL_TROPHY_X
mov casinoSprites[16*SIZEOF SpriteObject].SpriteObject.posY,TERMINAL_CENTER_Y
mov casinoWaypoints3D[14*SIZEOF NpcWaypoint].NpcWaypoint.posX,TERMINAL_CASH_X
mov casinoWaypoints3D[14*SIZEOF NpcWaypoint].NpcWaypoint.posY,FP_1*29/2
ret
PositionTerminalStations3D ENDP
DrawTerminalCabinet3D PROC uses ebx esi edi wx:DWORD,wy:DWORD,material:DWORD
local x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD
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/5
mov y0,eax
add eax,FP_1*2/5
mov y1,eax
invoke DrawCityBoxSides3D,x0,x1,y0,y1,0,FP_1*8/5,121
invoke DrawCityHorizontal3D,x0,x1,y0,y1,FP_1*8/5,110
mov eax,y1
add eax,FP_1/500
invoke DrawCityVertical3D,1,eax,x0,x1,0,FP_1*8/5,material
ret
DrawTerminalCabinet3D ENDP
RenderTerminalCabinets3D PROC
invoke DrawTerminalCabinet3D,TERMINAL_CASH_X,TERMINAL_CENTER_Y,118
invoke DrawTerminalCabinet3D,TERMINAL_TROPHY_X,TERMINAL_CENTER_Y,119
ret
RenderTerminalCabinets3D ENDP
TerminalCanWalk3D PROC uses esi px:DWORD,py:DWORD
mov eax,py
sub eax,TERMINAL_CENTER_Y
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_1*7/20
ja terminal_walk_clear
mov esi,TERMINAL_CASH_X
terminal_walk_station:
mov eax,px
sub eax,esi
test eax,eax
jns @F
neg eax
@@:
cmp eax,FP_HALF
jbe terminal_walk_blocked
add esi,FP_1*3/2
cmp esi,TERMINAL_TROPHY_X
jbe terminal_walk_station
terminal_walk_clear:
mov eax,1
ret
terminal_walk_blocked:
xor eax,eax
ret
TerminalCanWalk3D ENDP
; A projecting solid bracket supports a glass body, not a plane on the wall.
DrawHallLantern3D PROC uses ebx esi edi wallX:DWORD,wy:DWORD,direction:DWORD
local x0:DWORD,x1:DWORD,y0:DWORD,y1:DWORD,bodyX:DWORD
mov eax,wallX
mov ebx,direction
imul ebx,FP_1*3/10
add eax,ebx
mov bodyX,eax
mov eax,wy
sub eax,FP_1*13/100
mov y0,eax
add eax,FP_1*26/100
mov y1,eax
mov eax,bodyX
sub eax,FP_1*3/25
mov x0,eax
add eax,FP_1*6/25
mov x1,eax
invoke DrawCityBoxSides3D,x0,x1,y0,y1,FP_1*7/5,FP_1*19/10,120
invoke DrawCityHorizontal3D,x0,x1,y0,y1,FP_1*19/10,110
invoke DrawCityHorizontal3D,x0,x1,y0,y1,FP_1*7/5,110
; A short horizontal arm and wall plate remain visible at grazing angles.
mov eax,wallX
mov edx,bodyX
cmp eax,edx
jle @F
xchg eax,edx
@@:
mov x0,eax
mov x1,edx
mov eax,wy
sub eax,FP_1/30
mov y0,eax
add eax,FP_1/15
mov y1,eax
invoke DrawCityBoxSides3D,x0,x1,y0,y1,FP_1*31/20,FP_1*8/5,110
invoke DrawCityHorizontal3D,x0,x1,y0,y1,FP_1*8/5,110
mov eax,wallX
sub eax,FP_1/100
mov x0,eax
add eax,FP_1/50
mov x1,eax
mov eax,wy
sub eax,FP_1/12
mov y0,eax
add eax,FP_1/6
mov y1,eax
invoke DrawCityBoxSides3D,x0,x1,y0,y1,FP_1*29/20,FP_1*7/4,110
ret
DrawHallLantern3D ENDP
TerminalCountBits3D PROC value:DWORD
mov ecx,value
xor eax,eax
terminal_count_bit:
test ecx,ecx
jz terminal_count_done
lea edx,[ecx-1]
and ecx,edx
inc eax
jmp terminal_count_bit
terminal_count_done:
ret
TerminalCountBits3D ENDP
TerminalProgress3D PROC index:DWORD
cmp index,4
jae terminal_progress_collection
mov eax,bankroll
ret
terminal_progress_collection:
cmp index,6
jae terminal_progress_quests
mov eax,ownedBacks3D
and eax,1FFEh
invoke TerminalCountBits3D,eax
ret
terminal_progress_quests:
mov eax,npcClaimed3D
and eax,0FFFh
invoke TerminalCountBits3D,eax
ret
TerminalProgress3D ENDP
; Existing saved achievement bits are preserved; the merchant still owns bit 4.
RefreshTerminalAchievements3D PROC uses esi
xor esi,esi
terminal_refresh_next:
invoke TerminalProgress3D,esi
cmp eax,terminalGoals3D[esi*4]
jb @F
mov ecx,terminalBits3D[esi*4]
bts achievements,ecx
@@:
inc esi
cmp esi,8
jb terminal_refresh_next
ret
RefreshTerminalAchievements3D ENDP
BeginTerminal3D PROC kind:DWORD
mov eax,kind
and eax,1
mov terminalTab3D,eax
mov terminalPage3D,0
call NpcEnsureJournal3D
call RefreshTerminalAchievements3D
call SaveProgress3D
mov engineMode,MODE_ATM
call ClearKeys3D
mov mouseClick3D,0
ret
BeginTerminal3D ENDP
HandleTerminal3D PROC
cmp mouseClick3D,0
je terminal_handle_keys
cmp mouseY3D,8
jb terminal_handle_keys
cmp mouseY3D,25
ja terminal_handle_bottom
cmp mouseX3D,248
jae terminal_close
terminal_handle_bottom:
cmp mouseY3D,203
jb terminal_handle_keys
cmp mouseX3D,48
jb terminal_previous
cmp mouseX3D,272
ja terminal_next
terminal_handle_keys:
invoke Pressed3D,45h
test eax,eax
jnz terminal_close
invoke Pressed3D,25h
test eax,eax
jnz terminal_previous
invoke Pressed3D,27h
test eax,eax
jnz terminal_next
jmp terminal_handled
terminal_previous:
cmp terminalTab3D,0
je terminal_previous_from_cash
cmp terminalPage3D,0
je terminal_show_cash
dec terminalPage3D
jmp terminal_handled
terminal_previous_from_cash:
mov terminalTab3D,1
mov terminalPage3D,2
jmp terminal_handled
terminal_next:
cmp terminalTab3D,0
je terminal_next_from_cash
cmp terminalPage3D,2
jae terminal_show_cash
inc terminalPage3D
jmp terminal_handled
terminal_next_from_cash:
mov terminalTab3D,1
mov terminalPage3D,0
jmp terminal_handled
terminal_show_cash:
mov terminalTab3D,0
mov terminalPage3D,0
jmp terminal_handled
terminal_close:
mov engineMode,MODE_WALK
call ClearKeys3D
terminal_handled:
mov mouseClick3D,0
ret
HandleTerminal3D ENDP
RenderTerminal3D PROC uses ebx esi edi
local rowY:DWORD,opened:DWORD,progress:DWORD
invoke DrawBox,0,0,320,240,000B1815h
cmp terminalScreenArt3D.pixels,0
je @F
invoke DrawAnimSprite3D,OFFSET terminalScreenArt3D,0,0,0,640,480
@@:
mov edx,OFFSET termCash
cmp terminalTab3D,0
je @F
mov edx,OFFSET termTrophy
@@:
invoke DrawString8x8,64,12,edx,002D2116h
invoke DrawBoxBorder,248,8,56,17,001A2522h,00C8AD75h,1
invoke DrawString8x8,256,12,OFFSET termClose,00F2E6C6h
mov opened,0
xor esi,esi
terminal_count_unlocked:
mov ecx,terminalBits3D[esi*4]
bt achievements,ecx
jnc @F
inc opened
@@:
inc esi
cmp esi,8
jb terminal_count_unlocked
cmp terminalTab3D,1
je terminal_render_achievements
invoke DrawString8x8,24,45,OFFSET termAccount,00DDC99Dh
invoke DrawString8x8,24,69,OFFSET termBalance,0090B19Fh
invoke DrawNumber,24,87,bankroll,00F0E6C3h
invoke DrawBox,24,110,272,1,00495B4Ah
invoke DrawString8x8,24,128,OFFSET termCollection,0090B19Fh
mov eax,ownedBacks3D
and eax,1FFFh
invoke TerminalCountBits3D,eax
invoke DrawNumber,216,128,eax,00F0E6C3h
invoke DrawString8x8,232,128,OFFSET termSlash,0090B19Fh
invoke DrawNumber,248,128,13,00F0E6C3h
invoke DrawString8x8,24,156,OFFSET termTrophy,0090B19Fh
invoke DrawNumber,216,156,opened,00F0E6C3h
invoke DrawString8x8,232,156,OFFSET termSlash,0090B19Fh
invoke DrawNumber,248,156,8,00F0E6C3h
jmp terminal_render_footer
terminal_render_achievements:
invoke DrawString8x8,24,40,OFFSET termOpened,0090B19Fh
invoke DrawNumber,232,40,opened,00F0E6C3h
invoke DrawString8x8,248,40,OFFSET termSlash,0090B19Fh
invoke DrawNumber,264,40,8,00F0E6C3h
imul esi,terminalPage3D,3
mov rowY,62
mov edi,3
terminal_render_row:
cmp esi,8
jae terminal_render_footer
mov ebx,0090B19Fh
mov ecx,terminalBits3D[esi*4]
bt achievements,ecx
jnc @F
mov ebx,00E7C479h
invoke DrawString8x8,256,rowY,OFFSET termEarned,008CD8AFh
@@:
invoke DrawString8x8,24,rowY,terminalNames3D[esi*4],ebx
mov eax,rowY
add eax,12
invoke DrawString8x8,24,eax,terminalConditions3D[esi*4],0090B19Fh
invoke TerminalProgress3D,esi
mov ecx,terminalGoals3D[esi*4]
cmp eax,ecx
jbe @F
mov eax,ecx
@@:
mov edx,terminalBits3D[esi*4]
bt achievements,edx
jnc @F
mov eax,ecx
@@:
mov progress,eax
mov eax,rowY
add eax,24
invoke DrawNumber,24,eax,progress,00D2DCCEh
mov eax,rowY
add eax,24
invoke DrawString8x8,112,eax,OFFSET termSlash,0090B19Fh
mov eax,rowY
add eax,24
invoke DrawNumber,128,eax,terminalGoals3D[esi*4],00D2DCCEh
mov eax,rowY
add eax,35
invoke DrawBox,24,eax,272,1,00314538h
add rowY,44
inc esi
dec edi
jnz terminal_render_row
terminal_render_footer:
mov eax,terminalPage3D
add eax,2
cmp terminalTab3D,0
jne @F
mov eax,1
@@:
invoke DrawNumber,140,216,eax,00E3CCA0h
invoke DrawString8x8,156,216,OFFSET termSlash,00C1A982h
invoke DrawNumber,172,216,4,00E3CCA0h
ret
RenderTerminal3D ENDP

View file

@ -0,0 +1,60 @@
LoadTerminalArt3D PROTO
PositionTerminalStations3D PROTO
FreeTerminalArt3D PROTO
ResolveTerminalSurface3D PROTO :DWORD
RenderTerminalCabinets3D PROTO
TerminalCanWalk3D PROTO :DWORD,:DWORD
DrawTerminalCabinet3D PROTO :DWORD,:DWORD,:DWORD
DrawHallLantern3D PROTO :DWORD,:DWORD,:DWORD
BeginTerminal3D PROTO :DWORD
HandleTerminal3D PROTO
RenderTerminal3D PROTO
RefreshTerminalAchievements3D PROTO
TerminalProgress3D PROTO :DWORD
TerminalCountBits3D PROTO :DWORD
TERMINAL_CASH_X equ 28*FP_1
TERMINAL_TROPHY_X equ FP_1*59/2
TERMINAL_WALL_Y equ 13*FP_1
TERMINAL_CENTER_Y equ TERMINAL_WALL_Y+FP_1*11/50
.data
terminalWorldArt3D AnimPack3D <>
terminalScreenArt3D AnimPack3D <>
terminalWorldPath3D db "assets\animations\hall\terminals-v2.bin",0
terminalScreenPath3D db "assets\animations\hall\terminal-screen-v1.bin",0
terminalTab3D dd 0
terminalPage3D dd 0
terminalBits3D dd 8,9,10,11,4,12,13,14
terminalGoals3D dd 50000,100000,500000,1000000,1,5,1,6
terminalNames3D dd OFFSET termName0,OFFSET termName1,OFFSET termName2,OFFSET termName3
dd OFFSET termName4,OFFSET termName5,OFFSET termName6,OFFSET termName7
terminalConditions3D dd OFFSET termCond0,OFFSET termCond1,OFFSET termCond2,OFFSET termCond3
dd OFFSET termCond4,OFFSET termCond5,OFFSET termCond6,OFFSET termCond7
; Numeric CP1251 labels keep all runtime text in Russian.
termCash db 202,192,209,209,192,0
termTrophy db 196,206,209,210,200,198,197,205,200,223,0
termClose db 194,219,213,206,196,0
termAccount db 203,200,215,205,219,201,32,209,215,168,210,0
termBalance db 212,200,216,202,200,32,205,192,32,209,215,168,210,197,0
termCollection db 208,211,193,192,216,202,200,0
termOpened db 206,210,202,208,219,210,206,0
termEarned db 197,209,210,220,0
termSlash db "/",0
termLeft db "<",0
termRight db ">",0
termName0 db 205,192,32,202,206,205,197,0
termName1 db 202,208,211,207,205,219,201,32,200,195,208,206,202,0
termName2 db 207,206,203,204,200,203,203,200,206,205,192,0
termName3 db 204,200,203,203,200,206,205,197,208,0
termName4 db 207,197,208,194,192,223,32,207,206,202,211,207,202,192,0
termName5 db 202,206,203,203,197,202,214,200,206,205,197,208,0
termName6 db 209,194,206,201,32,215,197,203,206,194,197,202,0
termName7 db 216,197,209,210,220,32,207,206,208,211,215,197,205,200,201,0
termCond0 db 205,224,234,238,239,232,242,252,32,53,48,32,48,48,48,32,244,232,248,229,234,0
termCond1 db 205,224,234,238,239,232,242,252,32,49,48,48,32,48,48,48,32,244,232,248,229,234,0
termCond2 db 205,224,234,238,239,232,242,252,32,53,48,48,32,48,48,48,32,244,232,248,229,234,0
termCond3 db 205,224,234,238,239,232,242,252,32,49,32,48,48,48,32,48,48,48,32,244,232,248,229,234,0
termCond4 db 202,243,239,232,242,252,32,240,243,225,224,248,234,243,32,228,235,255,32,234,224,240,242,0
termCond5 db 209,238,225,240,224,242,252,32,53,32,239,238,234,243,239,237,251,245,32,240,243,225,224,248,229,234,0
termCond6 db 199,224,225,240,224,242,252,32,237,224,227,240,224,228,243,32,231,224,32,239,238,240,243,247,229,237,232,229,0
termCond7 db 199,224,225,240,224,242,252,32,54,32,237,224,227,240,224,228,32,243,32,227,238,241,242,229,233,0
.code

View file

@ -1,4 +1,5 @@
LoadWorldDressing3D PROC uses ebx esi
call LoadTerminalArt3D
invoke LoadAnimPack3D,OFFSET worldDressingArt3D,OFFSET worldDressingPath3D
invoke ValidateCityPack3D,OFFSET worldDressingArt3D,128,128,3
cmp worldStreetSized3D,0
@ -87,6 +88,7 @@ street_ground_object:
UpdateStreetGround3D ENDP
FreeWorldDressing3D PROC
call FreeTerminalArt3D
invoke FreeAnimPack3D,OFFSET worldDressingArt3D
mov hallLightReady3D,0
ret
@ -112,6 +114,7 @@ ResolveDressingSurface3D ENDP
RenderHallDressing3D PROC uses ebx esi edi
cmp hallLevel3D,0
jne hall_dressing_done
call RenderTerminalCabinets3D
lea esi,hallWallArt3D
mov edi,WORLD_WALL_ART_COUNT
hall_wall_painting:
@ -121,14 +124,8 @@ hall_wall_painting:
jnz hall_wall_painting
xor esi,esi
hall_wall_sconce:
mov eax,hallSconceY3D[esi*4]
lea ebx,[eax-FP_1*9/50]
add eax,FP_1*9/50
invoke DrawCityVertical3D,0,FP_1*1025/1000,ebx,eax,FP_1*27/20,FP_1*39/20,105
mov eax,hallSconceY3D[esi*4]
lea ebx,[eax-FP_1*9/50]
add eax,FP_1*9/50
invoke DrawCityVertical3D,0,FP_1*30975/1000,ebx,eax,FP_1*27/20,FP_1*39/20,105
invoke DrawHallLantern3D,FP_1*102/100,hallSconceY3D[esi*4],1
invoke DrawHallLantern3D,FP_1*3098/100,hallSconceY3D[esi*4],-1
inc esi
cmp esi,4
jb hall_wall_sconce

View file

@ -34,7 +34,7 @@ def main():
hall[8][x] = hall[12][x] = 1
targets = [(12, 8), (16, 10), (20, 8), (24, 8), (6, 8), (6, 11),
(6, 14), (10, 15), (14, 15), (18, 15), (22, 15), (28, 10),
(26, 16), (26, 20), (12, 22), (20, 22), (16, 25), (18, 25),
(28, 14), (29, 14), (12, 22), (20, 22), (16, 25), (18, 25),
(16, 29), (6, 25), (27, 25)]
assert all(point in reachable(hall, (16, 27)) for point in targets)
# Dedicated indoor concrete, never a street facade or refuse-container texture.
@ -46,8 +46,30 @@ def main():
for x in range(15, 18):
bunker[y][x] = 0
assert (16, 12) in reachable(bunker, (16, 26))
upper = [[125]*32 for _ in range(32)]
for y in range(13,25):
for x in range(3,16):
upper[y][x] = 0
# Enclosed, manually traversed stairwells with level landings at either end.
up_stairs = [[102]*32 for _ in range(32)]
for y in range(20,22):
for x in range(2,18):
up_stairs[y][x] = 0
down_stairs = [[92]*32 for _ in range(32)]
for y in range(20,23):
for x in range(2,18):
down_stairs[y][x] = 0
for y in range(13,23):
for x in range(15,18):
down_stairs[y][x] = 0
for grid, start, targets in ((upper,(4,21),[(7,18),(12,20),(9,23),(13,16)]),
(up_stairs,(3,21),[(16,21)]),
(down_stairs,(3,21),[(16,21),(16,14)])):
assert all(point in reachable(grid,start) for point in targets)
output = ["; Generated by make_hall_layout.py. Doorways and interactable approaches are checked.", ".data"]
for name, grid in (("hallLayout3D", hall), ("bunkerMap3D", bunker)):
for name, grid in (("hallLayout3D", hall), ("bunkerMap3D", bunker),
("upperLoungeMap3D",upper), ("upperStairMap3D",up_stairs),
("basementStairMap3D",down_stairs)):
for index, row in enumerate(grid):
output.append((name if index == 0 else " " * len(name)) + " db " + ",".join(map(str, row)))
output.append(".code")

View file

@ -0,0 +1,36 @@
"""Normalize the generated ensemble; native MASM owns timing and staging."""
from pathlib import Path
import struct
from PIL import Image
from pack_guest_cast import isolate
ROOT = Path(__file__).resolve().parents[2]
def main():
sheet = Image.open(ROOT / "assets/sprites/hall/cabaret-ensemble-v1.png").convert("RGBA")
frames = []
for column in range(3):
cells = [isolate(sheet.crop((column*sheet.width//3, row*sheet.height//4,
(column+1)*sheet.width//3,(row+1)*sheet.height//4)))
for row in range(4)]
scale = min(142/max(cell.width for cell in cells),
(180 if column == 1 else 242)/max(cell.height for cell in cells))
for cell in cells:
size = (round(cell.width*scale),round(cell.height*scale))
canvas = Image.new("RGBA",(160,256))
canvas.alpha_composite(cell.resize(size,Image.Resampling.LANCZOS),
((160-size[0])//2,250-size[1]))
frames.append(canvas)
raw = b"".join(frame.tobytes("raw","BGRA") for frame in frames)
(ROOT / "assets/animations/hall/cabaret-ensemble-v1.bin").write_bytes(
struct.pack("<4s4I",b"ANM1",160,256,len(frames),len(raw))+raw)
preview = Image.new("RGBA",(480,1024),(24,28,29,255))
for index,frame in enumerate(frames):
preview.alpha_composite(frame,((index//4)*160,(index%4)*256))
preview.save(ROOT / "build/cabaret-ensemble-preview.png")
print("Packed three performers, four poses each")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,47 @@
"""Pack the approved piano/pianist and guitarist cutouts for the MASM stage."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
def pack(sheet, left, right, width, name):
cells = [sheet.crop((left, row * 384, right, (row + 1) * 384))
for row in range(4)]
bounds = [cell.getchannel("A").point(lambda a: 255 if a >= 128 else 0).getbbox()
for cell in cells]
assert all(bounds), "each playing pose must contain opaque pixels"
box = (min(b[0] for b in bounds), min(b[1] for b in bounds),
max(b[2] for b in bounds), max(b[3] for b in bounds))
scale = min((width - 12) / (box[2] - box[0]), 244 / (box[3] - box[1]))
size = (round((box[2] - box[0]) * scale), round((box[3] - box[1]) * scale))
frames = []
for cell in cells:
sprite = cell.crop(box).resize(size, Image.Resampling.LANCZOS)
# The native sprite rasterizer uses a binary alpha test.
sprite.putalpha(sprite.getchannel("A").point(lambda a: 255 if a >= 128 else 0))
canvas = Image.new("RGBA", (width, 256))
canvas.alpha_composite(sprite, ((width - size[0]) // 2, 250 - size[1]))
frames.append(canvas)
raw = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
(ROOT / f"assets/animations/hall/{name}-v1.bin").write_bytes(
struct.pack("<4s4I", b"ANM1", width, 256, 4, len(raw)) + raw)
return frames
def main():
sheet = Image.open(ROOT / "assets/sprites/hall/cabaret-musicians-v1.png").convert("RGBA")
assert sheet.size == (1024, 1536)
piano = pack(sheet, 0, 640, 384, "cabaret-piano")
guitar = pack(sheet, 640, 1024, 160, "cabaret-guitar")
preview = Image.new("RGBA", (544, 1024), (24, 28, 29, 255))
for row in range(4):
preview.alpha_composite(piano[row], (0, row * 256))
preview.alpha_composite(guitar[row], (384, row * 256))
preview.save(ROOT / "build/cabaret-musicians-preview.png")
print("Packed coherent grand piano/pianist and guitarist: four anchored poses each")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,68 @@
"""Offline conversion of generated guest art into the existing ANM1 format."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/hall/guest-cast-v1.png"
TARGET = ROOT / "assets/animations/hall/guest-cast-v1.bin"
def isolate(cell):
alpha = cell.getchannel("A")
pixels = alpha.load()
remaining = {(x,y) for y in range(cell.height) for x in range(cell.width)
if pixels[x,y] >= 48}
components = []
while remaining:
point = remaining.pop()
component, pending = {point}, [point]
while pending:
x,y = pending.pop()
for other in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)):
if other in remaining:
remaining.remove(other)
component.add(other)
pending.append(other)
components.append(component)
largest = max(components,key=len)
mask = Image.new("L",cell.size)
for x,y in largest:
mask.putpixel((x,y),pixels[x,y])
cell.putalpha(mask)
return cell.crop(cell.getbbox())
def main():
sheet = Image.open(SOURCE).convert("RGBA")
frames = []
# The approved sheet contains idle, three gait poses and a seated row.
# Repeat the passing pose to close the four-phase walk without a jump.
for column in range(6):
boundaries = (0,277,535,795,1055,sheet.height)
cells = [isolate(sheet.crop((column*sheet.width//6, boundaries[row],
(column+1)*sheet.width//6, boundaries[row+1])))
for row in range(5)]
scale = min(120/max(cell.width for cell in cells[:4]),
204/max(cell.height for cell in cells[:4]))
for row in (0, 1, 2, 3, 2, 4):
cell = cells[row]
pose_scale = min(scale,124/cell.width,160/cell.height) if row == 4 else scale
size = (max(1, round(cell.width*pose_scale)), max(1, round(cell.height*pose_scale)))
cell = cell.resize(size, Image.Resampling.LANCZOS)
if column == 1 and row == 4:
cell = cell.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
frame = Image.new("RGBA", (128, 224))
frame.alpha_composite(cell, ((128-size[0])//2, 220-size[1]))
frames.append(frame)
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
TARGET.write_bytes(struct.pack("<4s4I", b"ANM1",128,224,len(frames),len(payload))+payload)
contact = Image.new("RGBA", (6*128,6*224), (24,28,29,255))
for index, frame in enumerate(frames):
contact.alpha_composite(frame, ((index//6)*128,(index%6)*224))
contact.save(ROOT / "build/guest-cast-preview.png")
print("Packed six guests, six frames each:", TARGET)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,41 @@
"""Offline packing of generated kiosk/lantern art for the assembly renderer."""
from pathlib import Path
import re
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/terminals"
OUT = ROOT / "assets/animations/hall"
def pack(path, frames):
w, h = frames[0].size
raw = b"".join(image.tobytes("raw", "BGRA") for image in frames)
path.write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw)) + raw)
def title(image, text, y):
font = bytes(int(x, 16) for x in re.findall(r"\b([0-9A-Fa-f]{3})h\b",
(ROOT / "font8x8.inc").read_text(encoding="cp1251")))
x = (image.width - len(text) * 8) // 2
for n, char in enumerate(text):
for row in range(8):
for col in range(8):
if font[char * 8 + row] & (128 >> col):
image.putpixel((x + n * 8 + col, y + row), (48, 30, 16, 255))
def main():
frames = [Image.open(SOURCE / name).convert("RGBA").resize((256, 512), Image.Resampling.LANCZOS)
for name in ("cash-front-v1.png", "trophy-front-v1.png", "lantern-glass-v1.png", "body-side-rear-v2.png")]
title(frames[0], b"\xca\xc0\xd1\xd1\xc0", 27)
title(frames[1], b"\xc4\xce\xd1\xd2\xc8\xc6\xc5\xcd\xc8\xdf", 25)
pack(OUT / "terminals-v2.bin", frames)
screen = Image.open(SOURCE / "display-v1.png").convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
pack(OUT / "terminal-screen-v1.bin", [screen])
print("Packed four 256x512 world materials, including a dedicated equipment enclosure, and one 640x480 screen")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,14 @@
"""Compile the generated four-tile upper-lounge material atlas."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
sheet = Image.open(ROOT / "assets/sprites/hall/upper-materials-v1.png").convert("RGBA")
frames = [sheet.crop((x*sheet.width//2,y*sheet.height//2,
(x+1)*sheet.width//2,(y+1)*sheet.height//2)).resize((256,256),Image.Resampling.LANCZOS)
for y in range(2) for x in range(2)]
raw = b"".join(frame.tobytes("raw","BGRA") for frame in frames)
(ROOT / "assets/animations/hall/upper-materials-v1.bin").write_bytes(
struct.pack("<4s4I",b"ANM1",256,256,4,len(raw))+raw)
print("Packed steel, marble, carpet and coffer materials")

View file

@ -0,0 +1,74 @@
"""Register generated motion strips to rigid arch/pedestal anchors."""
from pathlib import Path
import struct
from PIL import Image, ImageDraw
from pack_guest_cast import isolate
ROOT = Path(__file__).resolve().parents[2]
def cells(name, columns, rows):
sheet = Image.open(ROOT/f"assets/sprites/hall/{name}.png").convert("RGBA")
return [sheet.crop((c*sheet.width//columns,r*sheet.height//rows,
(c+1)*sheet.width//columns,(r+1)*sheet.height//rows))
for r in range(rows) for c in range(columns)]
def write(name, frames):
w,h = frames[0].size
raw = b"".join(frame.tobytes("raw","BGRA") for frame in frames)
(ROOT/f"assets/animations/hall/{name}.bin").write_bytes(
struct.pack("<4s4I",b"ANM1",w,h,len(frames),len(raw))+raw)
def anchored(source, width, height):
bounds = [cell.getchannel("A").point(lambda a: 255 if a >=128 else 0).getbbox()
for cell in source]
box = min(b[0] for b in bounds),min(b[1] for b in bounds),max(b[2] for b in bounds),max(b[3] for b in bounds)
scale = min((width-12)/(box[2]-box[0]),(height-12)/(box[3]-box[1]))
size = round((box[2]-box[0])*scale),round((box[3]-box[1])*scale)
result = []
for cell in source:
sprite = cell.crop(box).resize(size,Image.Resampling.LANCZOS)
sprite.putalpha(sprite.getchannel("A").point(lambda a: 255 if a >=128 else 0))
frame = Image.new("RGBA",(width,height))
frame.alpha_composite(sprite,((width-size[0])//2,height-6-size[1]))
result.append(frame)
return result
def main():
source = [isolate(cell) for cell in cells("upper-portal-v3",3,4)]
active = []
for cell in source:
sprite = cell.resize((244,308),Image.Resampling.LANCZOS)
sprite.putalpha(sprite.getchannel("A").point(lambda a: 255 if a >=128 else 0))
frame = Image.new("RGBA",(256,320))
frame.alpha_composite(sprite,(6,6))
active.append(frame)
# Keep every rigid arch pixel identical; register only the generated vortex.
aperture = Image.new("L",(256,320))
ImageDraw.Draw(aperture).polygon([(77,295),(77,145),(82,115),(99,94),
(128,73),(157,94),(174,115),(179,145),(179,295)],fill=255)
locked = [Image.composite(frame,active[0],aperture) for frame in active]
old = (ROOT/"assets/animations/hall/upper-portal-v2.bin").read_bytes()
inactive = Image.frombytes("RGBA",(224,320),old[20:20+224*320*4],"raw","BGRA")
inactive = inactive.resize((256,320),Image.Resampling.LANCZOS)
inactive = Image.composite(inactive,active[0],aperture)
write("upper-portal-v3",[frame.resize((256,512),Image.Resampling.LANCZOS)
for frame in [inactive,*locked]])
levers = anchored(cells("upper-levers-v3",3,2),128,224)
write("upper-levers-v3",levers)
braziers = anchored(cells("upper-braziers-v3",2,2),128,256)
for frame in braziers[1:]:
frame.paste(braziers[0].crop((0,100,128,256)),(0,100))
write("upper-braziers-v3",braziers)
preview = Image.new("RGBA",(768,640),(20,22,23,255))
for i,frame in enumerate([inactive,locked[0],locked[5],*levers[:3]]):
preview.alpha_composite(frame,((i%3)*256,(i//3)*320))
preview.save(ROOT/"build/upper-motion-preview.png")
print("Packed 12 vortex phases plus inactive arch, six lever positions, four brazier phases")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,44 @@
"""Offline cutout packing; all scene and lever behaviour lives in MASM."""
from pathlib import Path
import struct
from PIL import Image
from pack_guest_cast import isolate
ROOT = Path(__file__).resolve().parents[2]
def save(name, cells, width, height):
frames = []
for cell in cells:
cell = isolate(cell)
scale = min((width-12)/cell.width, (height-12)/cell.height)
size = round(cell.width*scale),round(cell.height*scale)
sprite = cell.resize(size,Image.Resampling.LANCZOS)
sprite.putalpha(sprite.getchannel("A").point(lambda a: 255 if a >= 128 else 0))
frame = Image.new("RGBA",(width,height))
frame.alpha_composite(sprite,((width-size[0])//2,height-6-size[1]))
frames.append(frame)
raw = b"".join(frame.tobytes("raw","BGRA") for frame in frames)
(ROOT/f"assets/animations/hall/{name}.bin").write_bytes(
struct.pack("<4s4I",b"ANM1",width,height,len(frames),len(raw))+raw)
return frames
def main():
portal = Image.open(ROOT/"assets/sprites/hall/upper-portal-v2.png").convert("RGBA")
cells = [portal.crop((c*512,r*512,(c+1)*512,(r+1)*512))
for r in range(3) for c in range(2)]
save("upper-portal-v2",cells[:4],224,320)
save("upper-levers-v2",cells[4:],128,224)
sheet = Image.open(ROOT/"assets/sprites/hall/upper-furniture-v2.png").convert("RGBA")
tables = [sheet.crop((0,0,768,512)),sheet.crop((768,0,1536,512))]
save("upper-games-v2",tables,320,224)
# The approved sheet places the bartender's head above the lower row line.
bartender = sheet.crop((180,400,650,1024))
stool = sheet.crop((860,540,1160,1024))
save("upper-bar-v2",[bartender,stool],160,256)
print("Packed two distinct game tables, full bartender/stool, portal and two lever states")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,146 @@
"""Compile the actual MASM card renderers and check every visible rank."""
import argparse
from pathlib import Path
import re
import struct
import subprocess
from PIL import Image
from check_card_exit_build import Coff
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/card-ranks"
RANKS = ("2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A")
FUNCTIONS = {
"casino3d.inc": ("DrawPixel", "DrawBox", "DrawBoxBorder", "DrawChar8x8", "DrawString8x8"),
"src_graphics/card_table_assets.inc": ("DrawNativeSuit3D", "DrawNativeCard3D"),
"src_graphics/native_blackjack_party_render.inc": ("DrawBJMiniCard3D",),
}
def read(path):
return (ROOT / path).read_text(encoding="cp1251")
def procedure(source, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", source, re.M | re.S).group()
def prepare():
OUT.mkdir(exist_ok=True)
ranks = re.search(r"^cardRankNames3D db .*", read("src_graphics/card_tables_data.inc"), re.M).group()
suits = re.search(r"^cardSuitMasks3D db .*\n(?: db .*\n){3}", read("src_graphics/card_tables_data.inc"), re.M).group()
backs = re.search(r"^cardBackColors3D dd .*\n dd .*", read("src_graphics/platform_data.inc"), re.M).group()
parts = [""".386
.model flat,stdcall
option casemap:none
SCREEN_W equ 640
SCREEN_H equ 480
ExitProcess PROTO :DWORD
CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
WriteFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
CloseHandle PROTO :DWORD
DrawBox PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
DrawAnimSprite3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
AnimPack3D STRUCT
magic dd ?
imageWidth dd ?
height dd ?
frames dd ?
bytes dd ?
pixels dd ?
AnimPack3D ENDS
.data
include font8x8.inc
selectedBack3D dd 0
backsPack3D AnimPack3D <>
actionBmpHeader db 42h,4Dh
dd 1228854
dw 0,0
dd 54,40,640,-480
dw 1,32
dd 0,1228800,0,0,0,0
""", ranks, suits, backs]
for suit in range(4):
for page in range(2):
parts.append(f'sheet{suit}_{page} db "build/card-ranks/suit-{suit}-{page}.bmp",0')
parts.append(".data?\nscreenBuffer dd 640*480 dup(?)\n.code")
for path, names in FUNCTIONS.items():
source = read(path)
parts.extend(procedure(source, name) for name in names)
parts.append(procedure(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D"))
parts.append("""DrawAnimSprite3D PROC pack:DWORD,frame:DWORD,x:DWORD,y:DWORD,w:DWORD,h:DWORD
invoke ExitProcess,99
ret
DrawAnimSprite3D ENDP
CardRanksStart PROC
""")
for suit in range(4):
for page in range(2):
parts.append("invoke DrawBox,0,0,320,240,00213629h")
for rank in range(page * 7, min(page * 7 + 7, 13)):
x = 6 + (rank % 7) * 44
for mode, width in enumerate((34, 20, 18, 20)):
y = 4 + mode * 60
if mode == 3:
parts.append(f"invoke DrawBJMiniCard3D,{x},{y},{suit*13+rank},0")
else:
parts.append(f"invoke DrawNativeCard3D,{x},{y},{suit*13+rank},0,{width}")
parts.append(f"invoke WriteActionSnapshot3D,OFFSET sheet{suit}_{page}\ntest eax,eax\njz failed")
parts.append("invoke ExitProcess,0\nfailed:\ninvoke ExitProcess,1\nCardRanksStart ENDP\nend CardRanksStart")
(OUT / "ranks.asm").write_text("\n".join(parts), encoding="ascii")
def verify():
font = bytes(int(x, 16) for x in re.findall(r"\b([0-9A-Fa-f]{3})h\b", read("font8x8.inc")))
assert len(font) == 256 * 8
for suit in range(4):
ink = (191, 38, 60) if suit in (1, 2) else (25, 31, 36)
for page in range(2):
image = Image.open(OUT / f"suit-{suit}-{page}.bmp").convert("RGB")
for rank in range(page * 7, min(page * 7 + 7, 13)):
x = 6 + (rank % 7) * 44
for mode, width in enumerate((34, 20, 18, 20)):
y = 4 + mode * 60
offset = 2 if mode == 3 else (1 if width == 18 else 3)
expected = {(x + offset + letter * 8 + col, y + 3 + row)
for letter, char in enumerate(RANKS[rank])
for row in range(8) for col in range(8)
if font[ord(char) * 8 + row] & (128 >> col)}
actual = {(px, py) for px in range(x, x + width)
for py in range(y + 3, y + 11)
if image.getpixel((px * 2, py * 2)) == ink}
assert actual == expected, (suit, rank, mode, actual ^ expected)
assert all(x < px < x + width - 1 for px, _ in expected)
image.save(OUT / f"suit-{suit}-{page}.png")
print("PASS: 208 native card faces; 10/A and all ranks, four suits, narrow/normal/guest cards")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity", action="store_true")
args = parser.parse_args()
if args.parity:
tested, shipped = Coff(OUT / "ranks.obj"), Coff(ROOT / "build/graphics/poker.obj")
for name, count in (("DrawNativeCard3D", 20), ("DrawBJMiniCard3D", 16)):
symbol = f"_{name}@{count}"
assert tested.signature(symbol) == shipped.signature(symbol), name
table = b"".join(rank.encode().ljust(3, b"\0") for rank in RANKS)
assert table in tested.data and table in shipped.data
verify()
print("PASS: shipping card renderers and rank strings match the tested MASM")
return
prepare()
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
"/Fobuild/card-ranks/ranks.obj", "build/card-ranks/ranks.asm"], cwd=ROOT, check=True)
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:CONSOLE",
f"/LIBPATH:{masm / 'lib'}", "/OUT:build/card-ranks/ranks.exe",
"build/card-ranks/ranks.obj", "kernel32.lib"], cwd=ROOT, check=True)
subprocess.run([str(OUT / "ranks.exe")], cwd=ROOT, check=True)
verify()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,229 @@
"""Compile and execute fresh MASM navigation, actors and room renderers."""
from pathlib import Path
import argparse
import re
import struct
import subprocess
from PIL import Image
from check_card_exit_build import Coff
from check_terminals import signature
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/hall-expansion"
def read(path):
data = (ROOT / path).read_bytes()
try:
return data.decode("utf8").replace("\r\r\n","\n").replace("\r\n","\n")
except UnicodeDecodeError:
return data.decode("cp1251").replace("\r\r\n","\n").replace("\r\n","\n")
def proc(source, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP",source,re.M|re.S).group()
def prepare():
OUT.mkdir(exist_ok=True)
code = read("src_graphics/hall_expansion.inc") + "\n" + read("src_graphics/cabaret.inc")
code += "\n" + read("src_graphics/interior_portals.inc")
for path, names in (
("src_graphics/hall_scene.inc",["RenderCeilingAndFloor","RenderSprites","InitHall3D",
"LoadHallAssets3D","FreeHallAssets3D","RenderHall3D","HallTarget3D",
"CheckHallInteractions3D","TriggerHallInteraction3D","HandleStairs3D","RenderStairs3D",
"CanWalkHall3D","UpdateHall3D","UpdatePatrons3D","RenderPatrons3D","RenderBar3D"]),
("src_graphics/platform_code.inc",["CanWalk3D"]),
("casino3d.inc",["HandlePlayerInput","CheckInteractions","RenderWallsRange","RenderWalls","RaycastWorkerProc","Trigger3DInteraction"]),
("src_graphics/city_bins.inc",["ResolveCitySurface3D","PrepareCityDepth3D","PrepareCityRays3D",
"CityRayPlane3D","DrawCityVertical3D","DrawCityHorizontal3D","DrawCityBoxSides3D"]),
("src_graphics/npc_life.inc",["RenderNpcLife3D"]),
("src_graphics/city_objects.inc",["DrawCityObject3D"]),
("src_graphics/npc_contracts.inc",["NpcNameText3D"])):
code += "\n" + "\n".join(proc(read(path),name) for name in names)
own_funcs = set(re.findall(r"^(\w+) PROC",code,re.M))
npc = read("src_graphics/npc_life_data.inc")
npc = npc[npc.index("npcGuestArt3D "):npc.index("npcLifeClock3D ")]
data = "\n".join(read(path) for path in ("src_graphics/hall_expansion_data.inc",
"src_graphics/cabaret_data.inc","src_graphics/hall_layout.inc")) + "\n.data\n" + npc
own_data = set(re.findall(r"^(\w+) (?:dd|db|AnimPack3D|CityProp3D)\b",data,re.M))
coff = Coff(ROOT / "build/graphics/poker.obj")
helper = bytearray(coff.data)
at = struct.unpack_from("<I",helper,8)[0]
sections = set()
for sec in range(1,struct.unpack_from("<H",helper,2)[0]+1):
name = bytes(helper[20+(sec-1)*40:28+(sec-1)*40]).rstrip(b"\0")
if name in (b".data",b".bss",b".const"):
sections.add(sec)
externs, protos = [], {}
types = {"casinoSprites":"SpriteObject","npcLife3D":"NpcLife3D",
"casinoNpcs3D":"NpcMindEntity","hallSpriteTemp3D":"CityProp3D",
"hallObjects3D":"CityProp3D","hallLights3D":"CityProp3D",
"hallLoungeDecor3D":"CityProp3D","hallGuests3D":"CityProp3D",
"npcSprite3D":"CityProp3D","scrStation3D":"CityProp3D"}
all_source = "\n".join(read(str(path.relative_to(ROOT))) for path in
[ROOT/"casino3d.inc",ROOT/"casino3d_data.inc",*sorted(path for path in
(ROOT/"src_graphics").glob("*.inc") if path.name != "renderer.inc")])
for name in re.findall(r"^(\w+) AnimPack3D\b",all_source,re.M):
types[name] = "AnimPack3D"
for name in re.findall(r"^(\w+) CityProp3D\b",all_source,re.M):
types[name] = "CityProp3D"
for name in re.findall(r"^(\w+) CitySpriteCrop3D\b",all_source,re.M):
types[name] = "CitySpriteCrop3D"
for name,kind in re.findall(r"^(\w+)\s+(db|dw|dd)\b",all_source,re.M|re.I):
types.setdefault(name,{"db":"BYTE","dw":"WORD","dd":"DWORD"}[kind.lower()])
for index,name in coff.symbols.items():
pos = at+index*18
_,sec,kind,storage,_ = struct.unpack_from("<IhHBB",helper,pos+8)
if sec in sections and re.fullmatch(r"[A-Za-z_]\w*",name) and storage in (2,3):
helper[pos+16] = 3 if name in own_data else 2
if name not in own_data:
externs.append(f"EXTERN {name}:{types.get(name,'DWORD')}")
match = re.fullmatch(r"_(\w+)@(\d+)",name)
if match and kind == 0x20 and sec > 0:
name,size = match[1],int(match[2])//4
if name in own_funcs:
helper[pos+16] = 3
else:
protos[name] = size
for name in own_funcs:
protos[name] = proc(code,name).splitlines()[0].count(":DWORD")
# Local helper instructions remain byte-for-byte intact.
allowed = {at+index*18+16 for index in coff.symbols}
assert all(i in allowed for i,(a,b) in enumerate(zip(helper,coff.data)) if a != b)
(OUT/"helpers.obj").write_bytes(helper)
structures = []
for name in ("C3D_POINT","SpriteObject","AnimPack3D","CityProp3D","CitySpriteCrop3D","NpcLife3D","NpcMindEntity"):
structures.append(re.search(rf"^{name} STRUCT\b.*?^{name} ENDS",all_source,re.M|re.S).group())
constants = dict(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)",all_source,re.M))
already = set(re.findall(r"^(\w+) equ",data,re.M))
used, ordered = set(), []
def constant(name):
if name in used or name in already or name not in constants:
return
used.add(name)
for dep in re.findall(r"\b[A-Za-z_]\w*",constants[name]):
constant(dep)
ordered.append(f"{name} equ {constants[name]}")
for name in re.findall(r"\b[A-Za-z_]\w*",code+data+read("tools/tests/hall_expansion_test.inc")):
constant(name)
# Win32 prototypes are not declarations in the helper object.
for name,args in re.findall(r"^(\w+)\s+PROTO([^\n]*)",all_source,re.M):
protos.setdefault(name,args.count(":DWORD"))
protos.update({"ExitProcess":1,"WriteFile":5})
header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures+ordered+externs)
header += "\noption language:stdcall\n" + "\n".join(
name+" PROTO"+(" "+",".join([":DWORD"]*size) if size else "")
for name,size in protos.items())
text = header+"\n"+data+"\n.code\n"+code+"\ninclude tools/tests/hall_expansion_test.inc\nend ExpansionTestStart\n"
(OUT/"module.asm").write_text(text,encoding="cp1251")
return own_funcs
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity",action="store_true")
parser.add_argument("--prove-alpha-regression",action="store_true")
parser.add_argument("--prove-seam-regression",action="store_true")
args = parser.parse_args()
if args.parity:
native,shipping = Coff(OUT/"module.obj"),Coff(ROOT/"build/graphics/poker.obj")
tested_source = (OUT/"module.asm").read_text(encoding="cp1251").split(
"include tools/tests/hall_expansion_test.inc")[0]
for name in re.findall(r"^(\w+) PROC",tested_source,re.M):
symbol = next(key for key in native.functions if key.startswith("_"+name+"@"))
assert signature(native,symbol) == signature(shipping,symbol),name
print("PASS: all tested navigation, collision, hall/upper rendering, guests and cabaret procedures match the shipping assembly")
return
prepare()
stem = "module"
if args.prove_alpha_regression:
stem = "module-alpha-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
before = proc(source,"InitUpperSurfaces3D")
after = before.replace("0FF1B5039h","001B5039h").replace("0FFECE3C8h","00ECE3C8h").replace("0FF16191Bh","0016191Bh")
assert before != after
(OUT/(stem+".asm")).write_text(source.replace(before,after),encoding="cp1251")
if args.prove_seam_regression:
stem = "module-old-seam"
source = (OUT/"module.asm").read_text(encoding="cp1251")
source = source.replace("include tools/tests/hall_expansion_test.inc",
read("tools/tests/hall_expansion_test.inc"))
before = "cmp portalRendering3D,0\n je wall_ray_start_ready"
assert source.count(before) == 1
source = source.replace(before,"jmp wall_ray_start_ready")
source = source.replace("build\\hall-expansion\\","build\\hall-expansion\\old-seam-")
source = source.replace("invoke ExpansionPortalSnapshot,OFFSET expansionUpEntry",
"invoke ExpansionPortalSnapshot,OFFSET expansionUpEntry\n invoke ExitProcess,0")
(OUT/(stem+".asm")).write_text(source,encoding="cp1251")
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm/"bin/ml.exe"),"/c","/coff","/Cp","/nologo","/I.",
f"/Fobuild/hall-expansion/{stem}.obj",f"build/hall-expansion/{stem}.asm"],cwd=ROOT,check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
subprocess.run([str(masm/"bin/link.exe"),"/nologo","/SUBSYSTEM:CONSOLE","/ENTRY:ExpansionTestStart@0",
f"/LIBPATH:{sdk}",f"/LIBPATH:{masm/'lib'}",f"/OUT:build/hall-expansion/{stem}.exe",
f"build/hall-expansion/{stem}.obj","build/hall-expansion/helpers.obj",
"kernel32.lib","user32.lib","gdi32.lib","winmm.lib","ws2_32.lib"],cwd=ROOT,check=True)
result = subprocess.run([str(OUT/(stem+".exe"))],cwd=ROOT,timeout=180)
if args.prove_alpha_regression:
assert result.returncode == 1,result.returncode
print("PASS: transparent felt/keyboard regression rejected by actual native assertions")
return
assert result.returncode == 0,f"native case {result.returncode}"
entrance = Image.open(OUT / (("old-seam-" if args.prove_seam_regression else "") +
"upper-entrance-open.bmp")).convert("RGB").crop((200,150,300,350))
dominant = max(count for count,_ in entrance.getcolors(20000))/20000
if args.prove_seam_regression:
assert dominant > 0.9,"negative control must reproduce the solid rear-wall block"
print("PASS: removing portal DDA near clipping reproduces the native solid-wall regression")
return
assert dominant < 0.5,"stair entrance must show textured continuation on both sides, not a solid rear-wall block"
for path in OUT.glob("*.bmp"):
image = Image.open(path).convert("RGB")
assert image.size == (640,480) and len(image.getcolors(640*480)) > 100
image.save(path.with_suffix(".png"))
lounge = Image.open(OUT / "upper-lounge.bmp").convert("RGB")
jazz = [Image.open(OUT / ("cabaret-jazz" + (f"-{i}" if i else "") + ".bmp")).convert("RGB")
for i in range(4)]
for region in ((110,150,300,290),(420,180,500,300)):
poses = [frame.crop(region).tobytes() for frame in jazz]
assert len(set(poses)) == 4, "piano and guitarist must both animate in the native scene"
vortex = [Image.open(OUT/f"portal-{i:02}.bmp").crop((245,145,395,320)).tobytes()
for i in range(12)]
assert len(set(vortex)) == 12,"all twelve vortex phases must be visible in native frames"
for name,expected in (("guest-cast-v1.bin",(128,224,36)),
("cabaret-ensemble-v1.bin",(160,256,12)),
("cabaret-piano-v1.bin",(384,256,4)),
("cabaret-guitar-v1.bin",(160,256,4)),
("upper-materials-v1.bin",(256,256,4)),
("upper-games-v2.bin",(320,224,2)),
("upper-bar-v2.bin",(160,256,2)),
("upper-portal-v3.bin",(256,512,13)),
("upper-levers-v3.bin",(128,224,6)),
("upper-braziers-v3.bin",(128,256,4))):
raw = (ROOT / "assets/animations/hall" / name).read_bytes()
magic,w,h,count,size = struct.unpack_from("<4s4I",raw)
assert magic == b"ANM1" and (w,h,count) == expected
assert size == w*h*count*4 == len(raw)-20
frames = [raw[20+i*w*h*4:20+(i+1)*w*h*4] for i in range(count)]
if name.startswith("guest"):
assert len(set(frames[::6])) == 6, "all six guest appearances must differ"
if name.startswith(("upper-portal", "upper-levers", "upper-braziers", "upper-games")):
assert len(set(frames)) == count,"new animation phases and game tables must differ"
if name.startswith("upper-portal"):
base = Image.frombytes("RGBA",(w,h),frames[1],"raw","BGRA")
for frame in frames[2:]:
sprite = Image.frombytes("RGBA",(w,h),frame,"raw","BGRA")
for box in ((0,0,74,h),(183,0,w,h),(0,0,w,110),(0,482,w,h)):
assert sprite.crop(box).tobytes() == base.crop(box).tobytes(),"rigid arch must not wobble"
if name.startswith(("cabaret-piano", "cabaret-guitar")):
assert len(set(frames)) == 4, "four genuinely different playing poses"
for frame in frames:
sprite = Image.frombytes("RGBA", (w,h), frame, "raw", "BGRA")
x0,y0,x1,y1 = sprite.getbbox()
assert 0 < x0 < x1 < w and 0 < y0 < y1 < h, "uncropped musician cutout"
print("PASS: native reversible stairs, all eight doorway views, foreground occlusion, original door/ticket positions, two-lever lock and twelve visible portal phases")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,217 @@
"""Native MASM kiosk/lantern regression and framebuffer checks; no game simulation."""
import argparse
from pathlib import Path
import re
import struct
import subprocess
from PIL import Image, ImageChops
from check_card_exit_build import Coff
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/terminals"
def read(path):
return (ROOT / path).read_text(encoding="cp1251")
def proc(text, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", text, re.M | re.S).group()
def signature(coff, name):
result = []
for code, refs, target in coff.signature(name):
if code[:1] == b"\xe8" and len(refs) == 1 and refs[0][1][1] == 20:
target = refs[0][1][0]
refs = ()
result.append((code, refs, target))
return result
def prepare():
OUT.mkdir(exist_ok=True)
terminal = read("src_graphics/terminal.inc")
city = read("src_graphics/city_bins.inc")
city_names = re.findall(r"^(\w+) PROC", city, re.M)[:7]
code = terminal + "\n" + "\n".join(proc(city, name) for name in city_names)
code += "\n" + proc(read("src_graphics/world_dressing.inc"), "RenderHallDressing3D")
# The shared surface dispatcher also knows the separately packed upper lounge.
code += "\n" + proc(read("src_graphics/hall_expansion.inc"), "ResolveUpperMaterial3D")
trigger = proc(read("casino3d.inc"), "Trigger3DInteraction")
code += "\n" + trigger
if "_InitHellTower3D@0" not in Coff(ROOT / "build/actions/actions-test.obj").functions:
# This unrelated initializer is newer than the reusable helper object.
# The kiosk fixture must never enter it; fail immediately if it does.
code += "\nInitHellTower3D PROC\ninvoke ExitProcess,98\nret\nInitHellTower3D ENDP\n"
own_functions = set(re.findall(r"^(\w+) PROC", code, re.M))
own_data = set(re.findall(r"^(\w+) (?:dd|db|AnimPack3D)", read("src_graphics/terminal_data.inc"), re.M))
own_data.update(("cityRayX3D", "cityRayY3D"))
own_data.update(("upperMaterialArt3D","upperFeltPixels3D","upperKeyPixels3D",
"upperGlowPixels3D","upperPortalArt3D","upperPortal3D",
"portalRendering3D","portalClipDepth3D"))
coff = Coff(ROOT / "build/actions/actions-test.obj")
data = bytearray(coff.data)
at = struct.unpack_from("<I", data, 8)[0]
count = struct.unpack_from("<H", data, 2)[0]
data_sections = set()
for section in range(1, count + 1):
offset = 20 + (section - 1) * 40
name = bytes(data[offset:offset+8]).rstrip(b"\0")
if name in (b".data", b".bss", b".const"):
data_sections.add(section)
externs = []
protos = []
for index, name in coff.symbols.items():
pos = at + index * 18
_, section, kind, storage, _ = struct.unpack_from("<IhHBB", data, pos+8)
if section in data_sections and re.fullmatch(r"[A-Za-z_]\w*", name) and storage in (2, 3):
if name in own_data:
data[pos+16] = 3
else:
data[pos+16] = 2
typ = "AnimPack3D" if name in ("cityDecorPack3D","hallProps3D") else "DWORD"
externs.append(f"EXTERN {name}:{typ}")
match = re.fullmatch(r"_(\w+)@(\d+)", name)
if match and kind == 0x20 and section > 0:
function, size = match[1], int(match[2])
if function in own_functions:
data[pos+16] = 3
else:
protos.append(function + " PROTO" + (" " + ",".join([":DWORD"] * (size//4)) if size else ""))
# Visibility-only helper copy. Never patch instructions or the shipping object.
allowed = {at + index * 18 + 16 for index in coff.symbols}
assert all(i in allowed for i, (a, b) in enumerate(zip(data, coff.data)) if a != b)
(OUT / "helpers.obj").write_bytes(data)
header = """.386
.model flat
option casemap:none
FP_1 equ 65536
FP_HALF equ 32768
SCREEN_W equ 640
SCREEN_H equ 480
MODE_WALK equ 0
MODE_ATM equ 5
WORLD_WALL_ART_COUNT equ 6
AnimPack3D STRUCT
magic dd ?
imageWidth dd ?
height dd ?
frames dd ?
bytes dd ?
pixels dd ?
AnimPack3D ENDS
CityProp3D STRUCT
posX dd ?
posY dd ?
worldW dd ?
worldH dd ?
frame dd ?
asset dd ?
groundZ dd 0
CityProp3D ENDS
SpriteObject STRUCT
posX dd ?
posY dd ?
baseTexId dd ?
frameCount dd ?
curFrame dd ?
animTimer dd ?
animSpeed dd ?
interactId dd ?
promptPtr dd ?
distSq dd ?
SpriteObject ENDS
NpcWaypoint STRUCT
posX dd ?
posY dd ?
neighbor0 dd ?
neighbor1 dd ?
neighbor2 dd ?
neighbor3 dd ?
zoneType dd ?
NpcWaypoint ENDS
"""
for name in city_names:
line = proc(city, name).splitlines()[0]
size = line.count(":DWORD")
protos.append(name + " PROTO" + (" " + ",".join([":DWORD"] * size) if size else ""))
text = header + "\n".join(externs) + "\noption language:stdcall\nExitProcess PROTO :DWORD\n" + "\n".join(protos)
constants = {}
for path in [ROOT / "casino3d.inc", *sorted((ROOT / "src_graphics").glob("*_data.inc"))]:
constants.update(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)", path.read_text(encoding="cp1251"), re.M))
for name in sorted(set(re.findall(r"\b(?:MODE_|INTERACT_)\w+", trigger)) - {"MODE_WALK", "MODE_ATM"}):
text += f"\n{name} equ {constants[name]}"
text += "\nResolveUpperMaterial3D PROTO :DWORD\ninclude src_graphics/terminal_data.inc\n.data\nupperMaterialArt3D AnimPack3D <>\nupperPortalArt3D AnimPack3D <>\nupperPortal3D CityProp3D <>\nportalRendering3D dd 0\n.data?\nupperFeltPixels3D dd 1024 dup(?)\nupperKeyPixels3D dd 1024 dup(?)\nupperGlowPixels3D dd 1024 dup(?)\nportalClipDepth3D dd 640 dup(?)\ncityRayX3D dd 640 dup(?)\ncityRayY3D dd 640 dup(?)\n.code\n"
text += code + "\ninclude tools/tests/terminal_test.inc\nend TerminalTestStart\n"
(OUT / "module.asm").write_text(text, encoding="ascii")
def verify():
for path, expected in (("terminals-v2.bin", (256, 512, 4)), ("terminal-screen-v1.bin", (640, 480, 1))):
data = (ROOT / "assets/animations/hall" / path).read_bytes()
magic, w, h, frames, size = struct.unpack_from("<4s4I", data)
assert magic == b"ANM1" and (w, h, frames) == expected
assert size == w*h*frames*4 == len(data)-20
assert set(data[23::4]) == {255}
data = (ROOT / "assets/animations/hall/terminals-v2.bin").read_bytes()
old = (ROOT / "assets/animations/hall/terminals-v1.bin").read_bytes()
assert data[20:20+3*256*512*4] == old[20:]
body = Image.open(ROOT / "assets/sprites/terminals/body-side-rear-v2.png").convert("RGBA").resize((256,512),Image.Resampling.LANCZOS)
assert data[20+3*256*512*4:] == body.tobytes("raw","BGRA")
cabinet = proc(read("src_graphics/terminal.inc"),"DrawTerminalCabinet3D")
assert "FP_1*8/5,121" in cabinet and "FP_1*8/5,86" not in cabinet
images = {}
for name in ("cash", "ach-0", "ach-1", "ach-2", "ach-locked", "cabinet-front", "cabinet-side", "cabinet-rear", "trophy-front", "trophy-side", "trophy-rear", "wall-placement", "lantern-front", "lantern-side"):
image = Image.open(OUT / (name + ".bmp")).convert("RGB")
assert image.size == (640, 480)
assert len(image.getcolors(640*480)) > 1000, name
image.save(OUT / (name + ".png"))
images[name] = image
for a, b in (("cash", "ach-0"), ("ach-0", "ach-1"), ("ach-1", "ach-2"),
("cabinet-front", "cabinet-side"), ("lantern-front", "lantern-side")):
assert ImageChops.difference(images[a], images[b]).getbbox(), (a, b)
print("PASS: wall placement, real front interaction, old footprints clear, dedicated enclosure/bar independence, native save/load/input, fourteen captures")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity", action="store_true")
parser.add_argument("--prove-regression", action="store_true")
args = parser.parse_args()
if args.parity:
a, b = Coff(OUT / "module.obj"), Coff(ROOT / "build/graphics/poker.obj")
names = re.findall(r"^(\w+) PROC[^\n]*", read("src_graphics/terminal.inc"), re.M)
for name in names + ["Trigger3DInteraction", "RenderHallDressing3D", "ResolveCitySurface3D", "DrawCityVertical3D", "DrawCityHorizontal3D", "DrawCityBoxSides3D"]:
symbol = next(key for key in a.functions if key.startswith("_" + name + "@"))
assert signature(a,symbol) == signature(b,symbol), name
verify()
print("PASS: shipping terminal/lantern renderer matches fresh native tests")
return
prepare()
stem = "module"
if args.prove_regression:
stem = "old-body"
source = (OUT / "module.asm").read_text(encoding="ascii")
before = "invoke DrawCityBoxSides3D,x0,x1,y0,y1,0,FP_1*8/5,121"
assert source.count(before) == 1
source = source.replace(before, before[:-3] + "86")
(OUT / "old-body.asm").write_text(source, encoding="ascii")
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
f"/Fobuild/terminals/{stem}.obj", f"build/terminals/{stem}.asm"], cwd=ROOT, check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:CONSOLE", "/ENTRY:TerminalTestStart@0", f"/LIBPATH:{sdk}",
f"/LIBPATH:{masm / 'lib'}", f"/OUT:build/terminals/{stem}.exe", f"build/terminals/{stem}.obj",
"build/terminals/helpers.obj", "kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib", "ws2_32.lib"], cwd=ROOT, check=True)
result = subprocess.run([str(OUT / (stem + ".exe"))], cwd=ROOT, check=False)
if args.prove_regression:
assert result.returncode == 6, result.returncode
print("PASS: restoring the old bar material is caught by the native texture-dependency regression")
return
result.check_returncode()
verify()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,598 @@
.data
expansionStage dd 1
expansionOcclusionPixel dd 0
expansionBmpHeader db 42h,4Dh
dd 1228854
dw 0,0
dd 54,40,640,-480
dw 1,32
dd 0,1228800,0,0,0,0
expansionDoor db "build\hall-expansion\basement-door.bmp",0
expansionStairs db "build\hall-expansion\upper-stairs.bmp",0
expansionStairsMid db "build\hall-expansion\upper-stairs-mid.bmp",0
expansionUpper db "build\hall-expansion\upper-lounge.bmp",0
expansionUpperBar db "build\hall-expansion\upper-bar.bmp",0
expansionJazz db "build\hall-expansion\cabaret-jazz.bmp",0
expansionJazz1 db "build\hall-expansion\cabaret-jazz-1.bmp",0
expansionJazz2 db "build\hall-expansion\cabaret-jazz-2.bmp",0
expansionJazz3 db "build\hall-expansion\cabaret-jazz-3.bmp",0
expansionChorus db "build\hall-expansion\cabaret-chorus.bmp",0
expansionCast db "build\hall-expansion\guest-cast.bmp",0
expansionUpEntry db "build\hall-expansion\upper-entrance-open.bmp",0
expansionUpReturn db "build\hall-expansion\upper-stair-return.bmp",0
expansionUpLanding db "build\hall-expansion\upper-landing-open.bmp",0
expansionUpExit db "build\hall-expansion\upper-exit-open.bmp",0
expansionDownClosed db "build\hall-expansion\old-basement-closed.bmp",0
expansionDownReturn db "build\hall-expansion\basement-stair-return.bmp",0
expansionDownLanding db "build\hall-expansion\basement-landing-open.bmp",0
expansionBunkerReturn db "build\hall-expansion\bunker-return-open.bmp",0
expansionPortalClosed db "build\hall-expansion\portal-closed.bmp",0
expansionBuckshotTable db "build\hall-expansion\upper-buckshot-table.bmp",0
expansionRussianTable db "build\hall-expansion\upper-russian-table.bmp",0
expansionBarWorld db "build\hall-expansion\upper-bar-world.bmp",0
expansionSeating db "build\hall-expansion\upper-seating.bmp",0
expansionLeverMid db "build\hall-expansion\lever-mid-pull.bmp",0
expansionPortal0 db "build\hall-expansion\portal-00.bmp",0
expansionPortal1 db "build\hall-expansion\portal-01.bmp",0
expansionPortal2 db "build\hall-expansion\portal-02.bmp",0
expansionPortal3 db "build\hall-expansion\portal-03.bmp",0
expansionPortal4 db "build\hall-expansion\portal-04.bmp",0
expansionPortal5 db "build\hall-expansion\portal-05.bmp",0
expansionPortal6 db "build\hall-expansion\portal-06.bmp",0
expansionPortal7 db "build\hall-expansion\portal-07.bmp",0
expansionPortal8 db "build\hall-expansion\portal-08.bmp",0
expansionPortal9 db "build\hall-expansion\portal-09.bmp",0
expansionPortal10 db "build\hall-expansion\portal-10.bmp",0
expansionPortal11 db "build\hall-expansion\portal-11.bmp",0
expansionPortalPaths dd OFFSET expansionPortal0,OFFSET expansionPortal1,OFFSET expansionPortal2,OFFSET expansionPortal3,OFFSET expansionPortal4,OFFSET expansionPortal5
dd OFFSET expansionPortal6,OFFSET expansionPortal7,OFFSET expansionPortal8,OFFSET expansionPortal9,OFFSET expansionPortal10,OFFSET expansionPortal11
.data?
expansionSinglePixels dd SCREEN_SIZE dup(?)
.code
ExpansionSnapshot PROC uses ebx path:DWORD
local count:DWORD
invoke CreateFileA,path,40000000h,0,0,2,80h,0
cmp eax,-1
je expansion_snapshot_fail
mov ebx,eax
invoke WriteFile,ebx,OFFSET expansionBmpHeader,54,ADDR count,0
invoke WriteFile,ebx,OFFSET screenBuffer,1228800,ADDR count,0
invoke CloseHandle,ebx
ret
expansion_snapshot_fail:
invoke ExitProcess,99
ret
ExpansionSnapshot ENDP
ExpansionFrame PROC
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderSprites
call RenderHall3D
ret
ExpansionFrame ENDP
ExpansionWalkFrame PROC
add animationClock3D,16
call UpdateInteriorTravel3D
call HandlePlayerInput
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
ret
ExpansionWalkFrame ENDP
ExpansionPortalSnapshot PROC path:DWORD
call UpdateInteriorEye3D
call ExpansionFrame
cmp portalPixelsWritten3D,1000
jae @F
invoke ExitProcess,expansionStage
@@:
invoke ExpansionSnapshot,path
ret
ExpansionPortalSnapshot ENDP
ExpansionTestStart PROC
call InitPaths3D
call LoadTextures3D
call InitializeCasinoSprites3D
call InitHall3D
call LoadHallAssets3D
cmp npcGuestArt3D.pixels,0
je expansion_fail
cmp cabaretArt3D.pixels,0
je expansion_fail
cmp cabaretPianoArt3D.pixels,0
je expansion_fail
cmp cabaretGuitarArt3D.pixels,0
je expansion_fail
cmp upperPortalArt3D.frames,13
jne expansion_fail
cmp upperLeverArt3D.frames,6
jne expansion_fail
cmp upperBrazierArt3D.frames,4
jne expansion_fail
cmp scrStation3D.posX,29*FP_1
jne expansion_fail
cmp scrStation3D.posY,FP_1*15/2
jne expansion_fail
cmp upperFeltPixels3D[0],0FF1B5039h
jb expansion_fail
cmp upperKeyPixels3D[0],80000000h
jb expansion_fail
mov deltaTime3D,16
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov expansionStage,2
invoke CanWalk3D,FP_1*285/10,FP_1*105/10
test eax,eax
jnz expansion_fail
mov interactTargetId,INTERACT_BASEMENT
call TriggerHallInteraction3D
cmp engineMode,MODE_WALK
jne expansion_fail
cmp basementDoorTarget3D,FP_1
jne expansion_fail
xor esi,esi
expansion_open_door:
add animationClock3D,16
call UpdateInteriorTravel3D
inc esi
cmp esi,60
jb expansion_open_door
cmp basementDoorOpen3D,FP_1
jne expansion_fail
invoke CanWalk3D,FP_1*285/10,FP_1*105/10
cmp eax,1
jne expansion_fail
mov playerX,27*FP_1
mov playerY,FP_1*105/10
mov playerAngle,0
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoor
mov expansionStage,3
mov keys3D[57h],1
xor esi,esi
expansion_down:
cmp engineMode,MODE_STAIRS
jne @F
cmp playerX,FP_1*165/10
jb @F
mov playerAngle,384
@@:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,1
je expansion_at_bunker
@@:
inc esi
cmp esi,800
jb expansion_down
jmp expansion_fail
expansion_at_bunker:
mov keys3D[57h],0
cmp playerAngle,384
jne expansion_fail
cmp worldMap3D,OFFSET bunkerMap3D
jne expansion_fail
mov expansionStage,4
mov playerAngle,128
mov keys3D[57h],1
xor esi,esi
expansion_up_basement:
cmp engineMode,MODE_STAIRS
jne @F
cmp playerY,21*FP_1
jb @F
mov playerAngle,256
@@:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,0
je expansion_back_hall
@@:
inc esi
cmp esi,800
jb expansion_up_basement
jmp expansion_fail
expansion_back_hall:
mov keys3D[57h],0
cmp playerX,28*FP_1-FP_1/10
jb expansion_fail
cmp playerX,FP_1*282/10
ja expansion_fail
mov expansionStage,5
mov playerX,FP_1*294/10
mov playerY,21*FP_1
mov playerAngle,0
mov keys3D[57h],1
xor esi,esi
expansion_upper_walk:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,2
je expansion_at_upper
@@:
inc esi
cmp esi,1100
jb expansion_upper_walk
jmp expansion_fail
expansion_at_upper:
mov keys3D[57h],0
cmp worldMap3D,OFFSET upperLoungeMap3D
jne expansion_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionUpper
mov expansionStage,6
mov playerX,7*FP_1
mov playerY,FP_1*187/10
mov playerAngle,384
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_BUCKSHOT
jne expansion_fail
mov playerX,12*FP_1
mov playerY,FP_1*197/10
call CheckInteractions
cmp interactTargetId,INTERACT_RUSSIAN_ROULETTE
jne expansion_fail
mov playerX,9*FP_1
mov playerY,FP_1*221/10
mov playerX,FP_1*91/10
mov playerAngle,128
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_UPPER_LEFT
jne expansion_fail
mov playerX,13*FP_1
mov playerY,FP_1*163/10
mov playerAngle,384
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_BAR
jne expansion_fail
call TriggerHallInteraction3D
cmp engineMode,MODE_BAR
jne expansion_fail
call RenderBar3D
cmp playerX,13*FP_1
jne expansion_fail
cmp hallLevel3D,2
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionUpperBar
mov expansionStage,7
mov engineMode,MODE_WALK
mov playerX,4*FP_1
mov playerY,21*FP_1
mov playerAngle,256
mov keys3D[57h],1
xor esi,esi
expansion_descend_upper:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,0
je expansion_upper_returned
@@:
inc esi
cmp esi,1100
jb expansion_descend_upper
jmp expansion_fail
expansion_upper_returned:
mov keys3D[57h],0
; No key held: neither position nor floor changes even after a long wait.
mov engineMode,MODE_STAIRS
mov stairRoute3D,2
mov worldMap3D,OFFSET upperStairMap3D
mov floorTexture3D,85
mov playerX,7*FP_1
mov playerY,21*FP_1
mov playerAngle,0
mov deltaTime3D,5000
call HandleStairs3D
cmp playerX,7*FP_1
jne expansion_fail
cmp engineMode,MODE_STAIRS
jne expansion_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionStairs
mov playerX,11*FP_1
call UpdateStairHeight3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionStairsMid
mov expansionStage,8
mov deltaTime3D,16
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,16*FP_1
mov playerY,FP_1*67/10
mov playerAngle,384
mov cabaretTime3D,30000
call UpdateCabaret3D
call BeginCabaret3D
mov animationClock3D,0
call RenderCabaret3D
invoke ExpansionSnapshot,OFFSET expansionJazz
mov animationClock3D,200
call RenderCabaret3D
cmp cabaretPiano3D.frame,1
jne expansion_fail
cmp cabaretGuitar3D.frame,1
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionJazz1
mov animationClock3D,400
call RenderCabaret3D
invoke ExpansionSnapshot,OFFSET expansionJazz2
mov animationClock3D,600
call RenderCabaret3D
cmp cabaretPiano3D.frame,3
jne expansion_fail
cmp cabaretGuitar3D.frame,3
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionJazz3
mov cabaretTime3D,0
call UpdateCabaret3D
call RenderCabaret3D
invoke ExpansionSnapshot,OFFSET expansionChorus
mov pressedKeys3D[45h],1
call HandleCabaret3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov bankroll,100
call BeginCabaret3D
mov mouseClick3D,1
mov mouseX3D,40
mov mouseY3D,220
call HandleCabaret3D
cmp bankroll,0
jne expansion_fail
mov mouseClick3D,1
call HandleCabaret3D
cmp bankroll,0
jne expansion_fail
mov mouseClick3D,1
mov mouseX3D,280
call HandleCabaret3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov expansionStage,9
; Each visitor maps to their own frame block, not the first man's idle pose.
mov playerX,16*FP_1
mov playerY,20*FP_1
mov playerAngle,384
xor esi,esi
expansion_cast_positions:
imul ebx,esi,SIZEOF NpcMindEntity
imul eax,esi,FP_1
add eax,FP_1*27/2
mov casinoNpcs3D[ebx].posX,eax
mov casinoNpcs3D[ebx].posY,16*FP_1
imul ebx,esi,SIZEOF NpcLife3D
mov npcLife3D[ebx].identity,esi
mov npcLife3D[ebx].state,NPC_STAND
inc esi
cmp esi,6
jb expansion_cast_positions
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionCast
mov hallLevel3D,2
mov worldMap3D,OFFSET upperLoungeMap3D
mov floorTexture3D,124
mov playerX,7*FP_1
mov playerY,FP_1*194/10
mov playerAngle,384
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionBuckshotTable
mov playerX,12*FP_1
mov playerY,FP_1*204/10
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionRussianTable
mov playerX,FP_1*125/10
mov playerY,FP_1*168/10
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionBarWorld
mov playerX,FP_1*56/10
mov playerY,FP_1*205/10
mov playerAngle,128
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionSeating
mov expansionStage,20
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,FP_1*285/10
mov playerY,21*FP_1
mov playerAngle,0
invoke ExpansionPortalSnapshot,OFFSET expansionUpEntry
invoke DrawCityVertical3D,0,29*FP_1,FP_1*208/10,FP_1*212/10,0,FP_1*22/10,122
mov eax,screenBuffer[(240*SCREEN_W+320)*4]
mov expansionOcclusionPixel,eax
call RenderInteriorViews3D
mov eax,screenBuffer[(240*SCREEN_W+320)*4]
cmp eax,expansionOcclusionPixel
jne expansion_fail
mov expansionStage,21
mov engineMode,MODE_STAIRS
mov stairRoute3D,2
mov worldMap3D,OFFSET upperStairMap3D
mov floorTexture3D,85
mov playerX,FP_1*48/10
mov playerAngle,256
invoke ExpansionPortalSnapshot,OFFSET expansionUpReturn
mov expansionStage,22
mov playerX,15*FP_1
mov playerAngle,0
invoke ExpansionPortalSnapshot,OFFSET expansionUpLanding
mov expansionStage,23
mov engineMode,MODE_WALK
mov hallLevel3D,2
mov worldMap3D,OFFSET upperLoungeMap3D
mov floorTexture3D,124
mov playerX,FP_1*45/10
mov playerAngle,256
invoke ExpansionPortalSnapshot,OFFSET expansionUpExit
mov expansionStage,24
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,FP_1*267/10
mov playerY,FP_1*105/10
mov playerAngle,0
mov basementDoorOpen3D,0
call UpdateInteriorEye3D
call ExpansionFrame
cmp portalPixelsWritten3D,0
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionDownClosed
mov basementDoorOpen3D,FP_1
invoke ExpansionPortalSnapshot,OFFSET expansionDoor
mov expansionStage,25
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
mov floorTexture3D,94
mov playerX,FP_1*45/10
mov playerY,21*FP_1
mov playerAngle,256
invoke ExpansionPortalSnapshot,OFFSET expansionDownReturn
mov expansionStage,26
mov playerX,FP_1*165/10
mov playerY,FP_1*155/10
mov playerAngle,384
invoke ExpansionPortalSnapshot,OFFSET expansionDownLanding
mov expansionStage,27
mov engineMode,MODE_WALK
mov hallLevel3D,1
mov worldMap3D,OFFSET bunkerMap3D
mov playerY,FP_1*265/10
mov playerAngle,128
invoke ExpansionPortalSnapshot,OFFSET expansionBunkerReturn
mov expansionStage,28
mov hallLevel3D,2
mov worldMap3D,OFFSET upperLoungeMap3D
mov floorTexture3D,124
mov playerX,FP_1*105/10
mov playerY,FP_1*193/10
mov upperLeverMask3D,0
mov upperPortalReady3D,0
mov upperLeverTime3D,0
mov upperLeverTime3D[4],0
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionPortalClosed
mov interactTargetId,INTERACT_UPPER_LEFT
call TriggerHallInteraction3D
mov esi,40
expansion_first_lever:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_first_lever
cmp upperLeverTime3D,600
jne expansion_fail
cmp upperLeverTime3D[4],0
jne expansion_fail
cmp upperPortalReady3D,0
jne expansion_fail
mov playerY,FP_1*244/10
call TryWalkUpperPortal3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov playerY,FP_1*193/10
mov interactTargetId,INTERACT_UPPER_RIGHT
call TriggerHallInteraction3D
mov esi,20
expansion_second_lever_half:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_second_lever_half
cmp upperPortalReady3D,0
jne expansion_fail
call ExpansionFrame
cmp upperLever3D.frame,2
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionLeverMid
mov esi,20
expansion_second_lever_end:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_second_lever_end
cmp upperPortalReady3D,1
jne expansion_fail
xor esi,esi
expansion_portal_motion:
imul eax,esi,80
mov animationClock3D,eax
call ExpansionFrame
mov eax,esi
inc eax
cmp upperPortal3D.frame,eax
jne expansion_fail
invoke ExpansionSnapshot,expansionPortalPaths[esi*4]
inc esi
cmp esi,12
jb expansion_portal_motion
mov playerY,FP_1*244/10
call TryWalkUpperPortal3D
cmp engineMode,MODE_HELL_TOWER
jne expansion_fail
call TowerExitToHall
call TryWalkUpperPortal3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov expansionStage,30
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,FP_1*285/10
mov playerY,21*FP_1
mov playerAngle,0
call UpdateInteriorEye3D
call ExpansionFrame
cld
lea esi,screenBuffer
lea edi,expansionSinglePixels
mov ecx,SCREEN_SIZE
rep movsd
invoke CreateEventA,0,0,0,0
mov hRaycastStartEvent,eax
invoke CreateEventA,0,0,0,0
mov hRaycastDoneEvent,eax
mov bRaycastQuit,0
invoke CreateThread,0,65536,OFFSET RaycastWorkerProc,0,0,OFFSET raycastThreadId
mov hRaycastThread,eax
test eax,eax
jz expansion_fail
call ExpansionFrame
lea esi,screenBuffer
lea edi,expansionSinglePixels
mov ecx,SCREEN_SIZE
repe cmpsd
jne expansion_fail
mov bRaycastQuit,1
invoke SetEvent,hRaycastStartEvent
invoke WaitForSingleObject,hRaycastThread,2000
test eax,eax
jnz expansion_fail
invoke CloseHandle,hRaycastThread
invoke CloseHandle,hRaycastStartEvent
invoke CloseHandle,hRaycastDoneEvent
mov hRaycastThread,0
mov hRaycastStartEvent,0
mov hRaycastDoneEvent,0
invoke ExitProcess,0
expansion_fail:
invoke ExitProcess,expansionStage
ret
ExpansionTestStart ENDP

View file

@ -93,6 +93,15 @@ include tools\tests\wheel_scene_test.inc
include tools\tests\npc_life_test.inc
HallTestWalkFrame3D PROC
add animationClock3D,16
call UpdateInteriorTravel3D
call HandlePlayerInput
call UpdateInteriorTravel3D
call UpdateInteriorEye3D
ret
HallTestWalkFrame3D ENDP
RunHallTest3D PROC uses ebx esi edi
local oldBank:DWORD,oldSeed:DWORD,endX:DWORD,endV:DWORD
mov eax, bankroll
@ -185,32 +194,83 @@ hall_test_patron_route:
test eax, eax
jnz hall_test_fail
mov interactTargetId, INTERACT_BASEMENT
mov playerX, 27*FP_1
mov playerY, FP_1*105/10
mov playerAngle,0
mov deltaTime3D,16
call ClearKeys3D
call TriggerHallInteraction3D
mov stairsTime3D, 800
cmp engineMode,MODE_WALK
jne hall_test_fail
mov esi,60
hall_test_open_door:
add animationClock3D,16
call UpdateInteriorTravel3D
dec esi
jnz hall_test_open_door
mov keys3D[57h],1
mov esi,800
hall_test_enter_stairs:
call HallTestWalkFrame3D
cmp engineMode,MODE_STAIRS
je hall_test_stair_frame
dec esi
jnz hall_test_enter_stairs
jmp hall_test_fail
hall_test_stair_frame:
call RenderStairs3D
invoke WriteActionSnapshot3D,OFFSET hallTestStair3D
mov deltaTime3D, 1600
call HandleStairs3D
mov esi,800
hall_test_descend_basement:
cmp engineMode,MODE_STAIRS
jne @F
cmp playerX,FP_1*165/10
jb @F
mov playerAngle,384
@@:
call HallTestWalkFrame3D
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D, 1
jne hall_test_fail
je hall_test_basement_arrived
@@:
dec esi
jnz hall_test_descend_basement
jmp hall_test_fail
hall_test_basement_arrived:
mov keys3D[57h],0
cmp worldMap3D, OFFSET bunkerMap3D
jne hall_test_fail
invoke CanWalk3D,16*FP_1+FP_HALF,16*FP_1
cmp eax, 1
jne hall_test_fail
mov playerY, 17*FP_1
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderHall3D
invoke WriteActionSnapshot3D,OFFSET hallTestBunker3D
mov interactTargetId, INTERACT_UPSTAIRS
call TriggerHallInteraction3D
mov deltaTime3D, 1600
call HandleStairs3D
cmp hallLevel3D, 0
jne hall_test_fail
mov playerAngle,128
mov keys3D[57h],1
mov esi,800
hall_test_return_basement:
cmp engineMode,MODE_STAIRS
jne @F
cmp playerY,21*FP_1
jb @F
mov playerAngle,256
@@:
call HallTestWalkFrame3D
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,0
je hall_test_back_in_hall
@@:
dec esi
jnz hall_test_return_basement
jmp hall_test_fail
hall_test_back_in_hall:
mov keys3D[57h],0
cmp worldMap3D, OFFSET casinoMap
jne hall_test_fail
mov hallTestStage3D, 72

View file

@ -20,6 +20,9 @@ LoadNativeDiceArt3D PROTO
FreeNativeDiceArt3D PROTO
LoadScratchArt3D PROTO
FreeScratchArt3D PROTO
LoadTerminalArt3D PROTO
FreeTerminalArt3D PROTO
ResolveTerminalSurface3D PROTO :DWORD
.data
streetPack dd 6 dup(0)
streetPath db "assets\animations\hall\street-life-v4.bin",0
@ -41,6 +44,18 @@ wakeKneesProbePath db "assets\animations\arrival\wake-knees-v1.bin",0
wakeSleeveProbePath db "assets\animations\arrival\wake-sleeve-v1.bin",0
alleyMaterialsProbePath db "assets\animations\arrival\alley-materials-v1.bin",0
worldDressingProbePath db "assets\animations\hall\world-dressing-v6.bin",0
terminalWorldProbePath db "assets\animations\hall\terminals-v2.bin",0
terminalScreenProbePath db "assets\animations\hall\terminal-screen-v1.bin",0
guestCastProbePath db "assets\animations\hall\guest-cast-v1.bin",0
cabaretProbePath db "assets\animations\hall\cabaret-ensemble-v1.bin",0
cabaretPianoProbePath db "assets\animations\hall\cabaret-piano-v1.bin",0
cabaretGuitarProbePath db "assets\animations\hall\cabaret-guitar-v1.bin",0
upperMaterialsProbePath db "assets\animations\hall\upper-materials-v1.bin",0
upperGamesProbePath db "assets\animations\hall\upper-games-v2.bin",0
upperBarProbePath db "assets\animations\hall\upper-bar-v2.bin",0
upperPortalProbePath db "assets\animations\hall\upper-portal-v3.bin",0
upperLeversProbePath db "assets\animations\hall\upper-levers-v3.bin",0
upperBraziersProbePath db "assets\animations\hall\upper-braziers-v3.bin",0
furniturePaths dd OFFSET cardFurniturePath,320,208,1
dd OFFSET wheelFurniturePath,288,268,1
dd OFFSET backFurniturePath,304,236,1
@ -58,6 +73,19 @@ furniturePaths dd OFFSET cardFurniturePath,320,208,1
dd OFFSET wakeSleeveProbePath,256,384,1
dd OFFSET alleyMaterialsProbePath,128,128,4
dd OFFSET worldDressingProbePath,128,128,3
dd OFFSET terminalWorldProbePath,256,512,4
dd OFFSET terminalScreenProbePath,640,480,1
dd OFFSET guestCastProbePath,128,224,36
dd OFFSET cabaretProbePath,160,256,12
dd OFFSET cabaretPianoProbePath,384,256,4
dd OFFSET cabaretGuitarProbePath,160,256,4
dd OFFSET upperMaterialsProbePath,256,256,4
dd OFFSET upperGamesProbePath,320,224,2
dd OFFSET upperBarProbePath,160,256,2
dd OFFSET upperPortalProbePath,256,512,13
dd OFFSET upperLeversProbePath,128,224,6
dd OFFSET upperBraziersProbePath,128,256,4
furniturePathCount equ ($-furniturePaths)/16
.code
RunStreetAssetProbe PROC
call InitPaths3D
@ -100,8 +128,20 @@ RunStreetAssetProbe PROC
cmp eax,1
jne probe_failed
call FreeScratchArt3D
call LoadTerminalArt3D
invoke ResolveTerminalSurface3D,118
test eax,eax
jz probe_failed
cmp ecx,256
jne probe_failed
cmp edx,512
jne probe_failed
call FreeTerminalArt3D
invoke ResolveTerminalSurface3D,118
test eax,eax
jnz probe_failed
mov esi,OFFSET furniturePaths
mov edi,17
mov edi,furniturePathCount
probe_furniture:
invoke LoadAnimPack3D,OFFSET streetPack,[esi]
cmp eax,1

View file

@ -0,0 +1,314 @@
.data
termTestJournal db "build\terminals\isolated-profile",0
termTestDir db "build\terminals",0
termTestSaveName db "test-profile.sav",0
termTestCash db "build\terminals\cash.bmp",0
termTestAch0 db "build\terminals\ach-0.bmp",0
termTestAch1 db "build\terminals\ach-1.bmp",0
termTestAch2 db "build\terminals\ach-2.bmp",0
termTestLocked db "build\terminals\ach-locked.bmp",0
termTestFront db "build\terminals\cabinet-front.bmp",0
termTestSide db "build\terminals\cabinet-side.bmp",0
termTestRear db "build\terminals\cabinet-rear.bmp",0
termTestTrophyFront db "build\terminals\trophy-front.bmp",0
termTestTrophySide db "build\terminals\trophy-side.bmp",0
termTestTrophyRear db "build\terminals\trophy-rear.bmp",0
termTestLamp db "build\terminals\lantern-front.bmp",0
termTestLampSide db "build\terminals\lantern-side.bmp",0
termTestWall db "build\terminals\wall-placement.bmp",0
termTestStage dd 1
.data?
termTestOriginalBar dd 65536 dup(?)
termTestReferenceFrame dd 307200 dup(?)
.code
TerminalTestIsolatedCabinet PROC
invoke DrawBox,0,0,320,240,00121C20h
lea edi,zBuffer
mov ecx,640
mov eax,100*FP_1
rep stosd
call UpdateCamera3D
call PrepareCityDepth3D
call RenderTerminalCabinets3D
ret
TerminalTestIsolatedCabinet ENDP
; Poison the former bar material. A terminal must render identically without it.
TerminalTestNoBarTexture PROC uses ebx esi edi
local texture:DWORD,unchanged:DWORD
mov eax,DWORD PTR hallRoomHD3D[20]
test eax,eax
jz terminal_bar_test_failed
add eax,6*256*256*4
mov texture,eax
call TerminalTestIsolatedCabinet
lea esi,screenBuffer
lea edi,termTestReferenceFrame
mov ecx,307200
rep movsd
mov esi,texture
lea edi,termTestOriginalBar
mov ecx,65536
rep movsd
mov edi,texture
mov eax,00F000FFh
mov ecx,65536
rep stosd
call TerminalTestIsolatedCabinet
mov unchanged,1
lea esi,screenBuffer
lea edi,termTestReferenceFrame
mov ecx,307200
repe cmpsd
je @F
mov unchanged,0
@@:
lea esi,termTestOriginalBar
mov edi,texture
mov ecx,65536
rep movsd
mov eax,unchanged
ret
terminal_bar_test_failed:
xor eax,eax
ret
TerminalTestNoBarTexture ENDP
.code
TerminalTestWorldFrame PROC
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderHallDressing3D
ret
TerminalTestWorldFrame ENDP
TerminalTestStart PROC
call InitPaths3D
mov npcJournalBasePtr3D,OFFSET termTestJournal
call LoadTextures3D
call InitializeCasinoSprites3D
call InitHall3D
call PositionTerminalStations3D
call LoadHallAssets3D
call LoadTerminalArt3D
cmp terminalWorldArt3D.pixels,0
je terminal_test_fail
cmp terminalScreenArt3D.pixels,0
je terminal_test_fail
mov termTestStage,2
mov achievements,80h
mov bankroll,49999
mov ownedBacks3D,1
mov npcClaimed3D,0
call RefreshTerminalAchievements3D
cmp achievements,80h
jne terminal_test_fail
mov bankroll,50000
call RefreshTerminalAchievements3D
cmp achievements,180h
jne terminal_test_fail
mov bankroll,1
call RefreshTerminalAchievements3D
cmp achievements,180h
jne terminal_test_fail
mov termTestStage,3
mov bankroll,1000000
mov ownedBacks3D,3Fh
mov npcClaimed3D,3Fh
call RefreshTerminalAchievements3D
cmp achievements,7F90h
jne terminal_test_fail
; Round-trip through the real profile serializer, isolated from user saves.
mov nativeProfileActive3D,1
mov nativeProfilesDirPtr3D,OFFSET termTestDir
invoke CopyCString,OFFSET saveFileName,OFFSET termTestSaveName
call SaveGraphicProfile3D
mov achievements,0
mov bankroll,1
call LoadSave
cmp achievements,7F90h
jne terminal_test_fail
cmp bankroll,1000000
jne terminal_test_fail
mov nativeProfileActive3D,0
call RefreshTerminalAchievements3D
cmp achievements,7F90h
jne terminal_test_fail
mov termTestStage,4
invoke TerminalCanWalk3D,TERMINAL_CASH_X,TERMINAL_CENTER_Y
test eax,eax
jnz terminal_test_fail
invoke TerminalCanWalk3D,TERMINAL_TROPHY_X,TERMINAL_CENTER_Y
test eax,eax
jnz terminal_test_fail
invoke TerminalCanWalk3D,TERMINAL_CASH_X,FP_1*71/5
cmp eax,1
jne terminal_test_fail
invoke TerminalCanWalk3D,26*FP_1,16*FP_1
cmp eax,1
jne terminal_test_fail
invoke TerminalCanWalk3D,26*FP_1,20*FP_1
cmp eax,1
jne terminal_test_fail
cmp BYTE PTR casinoMap[12*32+28],0
je terminal_test_fail
cmp BYTE PTR casinoMap[13*32+28],0
jne terminal_test_fail
cmp casinoSprites[15*SIZEOF SpriteObject].SpriteObject.posX,TERMINAL_CASH_X
jne terminal_test_fail
cmp casinoSprites[16*SIZEOF SpriteObject].SpriteObject.posY,TERMINAL_CENTER_Y
jne terminal_test_fail
cmp casinoWaypoints3D[14*SIZEOF NpcWaypoint].NpcWaypoint.posX,TERMINAL_CASH_X
jne terminal_test_fail
mov eax,casinoWaypoints3D[14*SIZEOF NpcWaypoint].NpcWaypoint.posX
mov edx,casinoWaypoints3D[14*SIZEOF NpcWaypoint].NpcWaypoint.posY
invoke TerminalCanWalk3D,eax,edx
cmp eax,1
jne terminal_test_fail
invoke TerminalCanWalk3D,26*FP_1,18*FP_1
cmp eax,1
jne terminal_test_fail
mov termTestStage,5
mov engineMode,MODE_WALK
mov playerX,TERMINAL_CASH_X
mov playerY,FP_1*71/5
mov playerAngle,384
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_ATM
jne terminal_test_fail
call Trigger3DInteraction
cmp engineMode,MODE_ATM
jne terminal_test_fail
cmp terminalTab3D,0
jne terminal_test_fail
call RenderTerminal3D
invoke WriteActionSnapshot3D,OFFSET termTestCash
mov engineMode,MODE_WALK
mov playerX,TERMINAL_TROPHY_X
call CheckInteractions
cmp interactTargetId,INTERACT_TERMINAL
jne terminal_test_fail
call Trigger3DInteraction
cmp terminalTab3D,1
jne terminal_test_fail
call RenderTerminal3D
invoke WriteActionSnapshot3D,OFFSET termTestAch0
mov pressedKeys3D[27h],1
call HandleTerminal3D
cmp terminalPage3D,1
jne terminal_test_fail
call RenderTerminal3D
invoke WriteActionSnapshot3D,OFFSET termTestAch1
mov mouseClick3D,1
mov mouseX3D,290
mov mouseY3D,220
call HandleTerminal3D
cmp terminalPage3D,2
jne terminal_test_fail
call RenderTerminal3D
invoke WriteActionSnapshot3D,OFFSET termTestAch2
mov pressedKeys3D[27h],1
call HandleTerminal3D
cmp terminalTab3D,0
jne terminal_test_fail
mov mouseClick3D,1
mov mouseX3D,25
mov mouseY3D,220
call HandleTerminal3D
cmp terminalPage3D,2
jne terminal_test_fail
mov mouseClick3D,1
mov mouseX3D,270
mov mouseY3D,15
call HandleTerminal3D
cmp engineMode,MODE_WALK
jne terminal_test_fail
cmp mouseClick3D,0
jne terminal_test_fail
invoke BeginTerminal3D,1
mov pressedKeys3D[45h],1
call HandleTerminal3D
cmp engineMode,MODE_WALK
jne terminal_test_fail
mov achievements,0
mov bankroll,10000
mov ownedBacks3D,1
mov npcClaimed3D,0
call RenderTerminal3D
invoke WriteActionSnapshot3D,OFFSET termTestLocked
mov termTestStage,6
mov viewHorizon3D,240
mov playerX,TERMINAL_CASH_X
mov playerY,FP_1*31/2
mov playerAngle,384
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestFront
mov playerX,FP_1*53/2
mov playerY,FP_1*29/2
mov playerAngle,448
call TerminalTestNoBarTexture
test eax,eax
jz terminal_test_fail
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestSide
mov playerX,TERMINAL_CASH_X
mov playerY,12*FP_1
mov playerAngle,128
call TerminalTestNoBarTexture
test eax,eax
jz terminal_test_fail
call TerminalTestIsolatedCabinet
invoke WriteActionSnapshot3D,OFFSET termTestRear
mov playerX,TERMINAL_TROPHY_X
mov playerY,FP_1*31/2
mov playerAngle,384
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestTrophyFront
mov playerX,FP_1*61/2
mov playerY,FP_1*29/2
mov playerAngle,320
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestTrophySide
mov playerX,TERMINAL_TROPHY_X
mov playerY,12*FP_1
mov playerAngle,128
call TerminalTestIsolatedCabinet
invoke WriteActionSnapshot3D,OFFSET termTestTrophyRear
mov playerX,28*FP_1
mov playerY,20*FP_1
mov playerAngle,384
call TerminalTestWorldFrame
invoke DrawCityObject3D,OFFSET hallLoungeDecor3D+3*28,0
invoke DrawCityObject3D,OFFSET hallLoungeDecor3D+4*28,0
invoke DrawCityObject3D,OFFSET hallLoungeDecor3D+5*28,0
invoke WriteActionSnapshot3D,OFFSET termTestWall
mov playerX,3*FP_1
mov playerY,16*FP_1
mov playerAngle,256
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestLamp
mov playerX,FP_1*5/2
mov playerY,14*FP_1
mov playerAngle,175
call TerminalTestWorldFrame
invoke WriteActionSnapshot3D,OFFSET termTestLampSide
mov termTestStage,7
mov ebx,terminalWorldArt3D.pixels
mov terminalWorldArt3D.pixels,0
invoke ResolveTerminalSurface3D,118
test eax,eax
jnz terminal_test_fail
mov terminalWorldArt3D.pixels,ebx
mov ebx,terminalScreenArt3D.pixels
mov terminalScreenArt3D.pixels,0
call RenderTerminal3D
mov terminalScreenArt3D.pixels,ebx
call FreeTerminalArt3D
cmp terminalWorldArt3D.pixels,0
jne terminal_test_fail
invoke ExitProcess,0
terminal_test_fail:
invoke ExitProcess,termTestStage
TerminalTestStart ENDP

View file

@ -26,5 +26,5 @@ try {
if ($LASTEXITCODE -ne 0) { throw "Street pack failed to load from $directory" }
} finally { Pop-Location }
}
Write-Output 'PASS: shipping MASM loader loads street, roulette, coin/dice, seven scratch-ticket kinds, 777/Clover, both billiards ball sets/table, eight hall furniture, wall decor and five wake-up/alley packs from root/TEMP and clears failed headers'
Write-Output 'PASS: shipping MASM loader loads game packs, street/alley, hall furniture, terminals/lanterns, six guests, cabaret quartet and upper-floor materials from root/TEMP and clears failed headers'
} finally { Pop-Location }