diff --git a/assets/sprites/backrooms/Backrooms_ceiling.jpg b/assets/sprites/backrooms/Backrooms_ceiling.jpg new file mode 100644 index 0000000..f08c02f Binary files /dev/null and b/assets/sprites/backrooms/Backrooms_ceiling.jpg differ diff --git a/assets/sprites/backrooms/Backrooms_ceiling_lamp.jpg b/assets/sprites/backrooms/Backrooms_ceiling_lamp.jpg new file mode 100644 index 0000000..53f320b Binary files /dev/null and b/assets/sprites/backrooms/Backrooms_ceiling_lamp.jpg differ diff --git a/assets/sprites/backrooms/Backrooms_floor.jpg b/assets/sprites/backrooms/Backrooms_floor.jpg new file mode 100644 index 0000000..013c4f9 Binary files /dev/null and b/assets/sprites/backrooms/Backrooms_floor.jpg differ diff --git a/assets/sprites/backrooms/Backrooms_wall.jpg b/assets/sprites/backrooms/Backrooms_wall.jpg new file mode 100644 index 0000000..6de7076 Binary files /dev/null and b/assets/sprites/backrooms/Backrooms_wall.jpg differ diff --git a/assets/sprites/backrooms/materials.bin b/assets/sprites/backrooms/materials.bin new file mode 100644 index 0000000..21b07a3 Binary files /dev/null and b/assets/sprites/backrooms/materials.bin differ diff --git a/bin/poker.exe b/bin/poker.exe index f404bae..acc5ab5 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index a11204f..79658a3 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "25FBDF78EDF3DFD3F6B72657E78C3156F74931A656B4CBDB15260D496C96849A"; + const string ExpectedPokerSha256 = "B0F5CE31D9A2467CFE8AF66CA90CC545F75318F11D149473A4377A2A278C8B23"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index f404bae..acc5ab5 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/casino3d.inc b/casino3d.inc index 18e5180..65d28e4 100644 --- a/casino3d.inc +++ b/casino3d.inc @@ -170,7 +170,8 @@ include src_graphics\native_coin_data.inc include src_graphics\native_dice_data.inc include src_graphics\scratch_data.inc include src_graphics\hell_tower_data.inc -include src_graphics\charms_data.inc +include src_graphics\charms_data.inc +include src_graphics\backrooms_data.inc include src_graphics\observer_data.inc include src_graphics\darts_data.inc include src_graphics\jackbox_data.inc @@ -471,7 +472,7 @@ screenBuffer dd SCREEN_SIZE dup(?) ALIGN 4 zBuffer dd SCREEN_W dup(?) ALIGN 4 -texturesBuffer dd (104 * 128 * 128) dup(?) +texturesBuffer dd (185 * 128 * 128) dup(?) hudNumBuffer db 32 dup(?) @@ -3249,6 +3250,25 @@ dice_start_roll: invoke RandomRange3D, 6 inc eax mov dice3d_val2, eax + call BackroomsLuck3D + cmp eax,-1 + je br_dice_fixed + xor eax,1 + mov ecx,dice3d_choice + dec ecx + xor eax,ecx + mov edx,dice3d_val1 + add edx,dice3d_val2 + and edx,1 + cmp eax,edx + je br_dice_fixed + xor edx,edx + mov eax,dice3d_val2 + mov ecx,6 + div ecx + inc edx + mov dice3d_val2,edx +br_dice_fixed: invoke PlayCasinoSound3D, OFFSET soundDiceRoll, 0, 1 call SaveProgress3D ret @@ -3485,6 +3505,21 @@ InitSkullsGame ENDP FinalizeSkullsGuess PROC uses ebx cmp skulls3d_state,3 jne skulls_guess_done + cmp brSkullsLuck3D,-1 + je br_skulls_fixed + mov eax,skulls3d_choice + cmp brSkullsLuck3D,1 + je @F + inc eax + xor edx,edx + mov ecx,3 + div ecx + mov eax,edx +@@: + mov skulls3d_ballPos,eax + ; Reality distortion explicitly covers relocation before the reveal. + mov skulls3d_timer,0 +br_skulls_fixed: mov eax, skulls3d_choice cmp eax, skulls3d_ballPos jne skulls_guess_wrong @@ -3568,6 +3603,8 @@ skulls_bet_down: ret skulls_start_game: + call BackroomsLuck3D + mov brSkullsLuck3D,eax mov eax, skulls3d_bet cmp eax, bankroll jle @F @@ -3716,6 +3753,16 @@ RenderSkullsGame PROC uses ebx esi edi jne @F invoke DrawString8x8, 28, 62, OFFSET textSkullsPhase3, 00FFFF00h jmp skulls_draw_idle_cups +@@: + ; Short artifact glitch, then reveal the actual ball and matching payout. + cmp brSkullsLuck3D,-1 + je @F + cmp skulls3d_timer,12 + jae @F + inc skulls3d_timer + invoke DrawBox,8,76,304,122,00130F20h + invoke DrawString8x8,108,128,OFFSET brRealityGlitch3D,00FF88CCh + jmp skulls_render_controls @@: ; Phase 4: Reveal outcome mov eax, skulls3d_win @@ -4633,6 +4680,8 @@ check_total_movement: mov playerY, eax input_action: + cmp brActive3D,0 + jne player_input_complete cmp engineMode,MODE_STAIRS je player_input_complete invoke Pressed3D, 45h @@ -4706,9 +4755,12 @@ CheckInteractions ENDP ; (Legacy console arcade bezel bridge removed) Trigger3DInteraction PROC + cmp brActive3D,0 + jne trigger_backrooms_station call TriggerHallInteraction3D test eax, eax jnz trigger_done +trigger_backrooms_station: mov eax, interactTargetId test eax, eax jz trigger_done @@ -5200,7 +5252,8 @@ include src_graphics\hell_tower.inc include src_graphics\charms.inc include src_graphics\observer.inc include src_graphics\darts.inc -include src_graphics\jackbox.inc +include src_graphics\jackbox.inc +include src_graphics\backrooms.inc ; --- Main Loop of the 3D Casino Engine --- StartHellTower3D PROC uses ebx esi edi @@ -5279,6 +5332,9 @@ engine_observer_skip: cmp engineMode, MODE_WALK jne leave_table pause_interior_walk: + cmp brCutscene3D,0 + jne engine_tick + mov brEntryHold3D,0 xor paused3D, 1 jmp engine_tick leave_table: @@ -5343,10 +5399,15 @@ check_arrival_mode: check_walk_mode: cmp engineMode, MODE_WALK jne check_slots_mode + cmp brActive3D,0 + jne engine_backrooms call UpdateHall3D call UpdateCamera3D call CheckInteractions call HandlePlayerInput + call BackroomsEntry3D + cmp brActive3D,0 + jne engine_backrooms call HandleJukeboxKeys3D call UpdateInteriorTravel3D call UpdateInteriorEye3D @@ -5365,6 +5426,14 @@ walk_world_ready: call RenderHUD call RenderMinimap jmp present_frame +engine_backrooms: + call HandleBackrooms3D + cmp brActive3D,0 + je check_walk_mode + cmp engineMode,MODE_WALK + jne check_slots_mode + call RenderBackrooms3D + jmp present_frame check_slots_mode: cmp engineMode,MODE_JACKBOX jne check_jackbox_done @@ -5531,6 +5600,7 @@ check_russian_mode: call RenderRussianGame3D jmp present_frame draw_pause: + mov brEntryHold3D,0 invoke DrawBoxBorder, 28, 36, 264, 168, 00151C21h, 00D8BC6Ah, 2 invoke DrawString8x8, 44, 48, OFFSET textPause3D, 00FFFFFFh cmp displayFill3D, 0 @@ -5559,7 +5629,10 @@ pause_draw_quality_value: invoke DrawString8x8, 44, 130, OFFSET textSoundLevel3D, 00BBBBBBh invoke FormatNumber, settingsVolume, OFFSET hudNumBuffer invoke DrawString8x8, 124, 130, OFFSET hudNumBuffer, 00FFFFFFh - invoke DrawString8x8, 44, 150, OFFSET textFullscreenKey3D, 00A0A0A0h + cmp brOwned3D,0 + jne @F + invoke DrawString8x8, 44, 150, OFFSET textFullscreenKey3D, 00A0A0A0h +@@: invoke DrawString8x8, 44, 170, OFFSET textResume3D, 00D8BC6Ah invoke DrawString8x8, 44, 188, OFFSET textQuit3D, 00BBBBBBh invoke Pressed3D, 31h @@ -5597,12 +5670,37 @@ pause_low_spec_saved: call ApplySoundVolume call SaveProgress3D @@: + cmp brOwned3D,0 + je br_pause_inventory_done + invoke DrawString8x8,44,150,OFFSET brToggle3D,00D8BC6Ah + invoke DrawString8x8,36,214,OFFSET brInventory3D,00D8BC6Ah + mov edx,OFFSET brDisabledText3D + cmp brEnabled3D,0 + je @F + mov edx,OFFSET brEnabledText3D +@@: + invoke DrawString8x8,248,150,edx,00FFFFFFh + invoke Pressed3D,52h + test eax,eax + jz br_pause_inventory_done + xor brEnabled3D,1 + call SaveProgress3D +br_pause_inventory_done: invoke Pressed3D, 51h test eax, eax jz @F call SaveProgress3D jmp engine_shutdown present_frame: + cmp brActive3D,0 + je br_record_clock_done + cmp engineMode,MODE_WALK + je br_record_clock_done + cmp paused3D,0 + jne br_record_clock_done + mov eax,deltaTime3D + add brClock3D,eax +br_record_clock_done: call UpdateJukeboxAudio3D call RenderTowerInGameBadge call RenderJukeboxVolumeHUD @@ -5638,6 +5736,7 @@ present_frame: invoke Sleep, ecx jmp engine_loop engine_idle: + mov brEntryHold3D,0 call UnlockMouse3D invoke Sleep, 50 jmp engine_loop @@ -5659,6 +5758,7 @@ engine_shutdown: @@: call SaveProgress3D invoke PlayCasinoSound3D, 0, 0, 0 + invoke FreeAnimPack3D,OFFSET brMaterials3D call FreeRussianAssets3D call FreeDarts3D call FreeStartupAssets3D diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 0dcfb19..95dfdd2 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/poker.asm b/poker.asm index 8affb54..cf7ce96 100644 --- a/poker.asm +++ b/poker.asm @@ -820,7 +820,14 @@ save_stats_played_loop: mov DWORD PTR [saveBuffer+296],eax mov eax,charmEnabledMask3D mov DWORD PTR [saveBuffer+300],eax - mov DWORD PTR [saveBuffer+304],2 + mov eax,brOwned3D + shl eax,16 + mov ecx,brEnabled3D + and ecx,brOwned3D + shl ecx,17 + or eax,ecx + or eax,2 + mov DWORD PTR [saveBuffer+304],eax invoke ArtifactSaveSignature3D,OFFSET saveBuffer mov DWORD PTR [saveBuffer+308],eax call BuildSaveFullPath @@ -887,6 +894,14 @@ load_file_opened: mov charmDevilUses,eax mov eax,DWORD PTR [saveBuffer+300] mov charmEnabledMask3D,eax + mov eax,DWORD PTR [saveBuffer+304] + shr eax,16 + and eax,1 + mov brOwned3D,eax + mov eax,DWORD PTR [saveBuffer+304] + shr eax,17 + and eax,1 + mov brEnabled3D,eax load_signature_ok: mov eax, DWORD PTR [saveBuffer+4] mov bankroll, eax @@ -2043,7 +2058,14 @@ test_error_exit_engine: RunHellTowerTest3D ENDP ENDIF +IFDEF BACKROOMS_TEST +include tools/tests/backrooms_test.inc +ENDIF main PROC +IFDEF BACKROOMS_TEST + call RunBackroomsTest3D + invoke ExitProcess,eax +ENDIF IFDEF HELL_TOWER_TEST call RunHellTowerTest3D invoke ExitProcess,eax diff --git a/poker.exe b/poker.exe index f404bae..acc5ab5 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index 4971ea3..41ce6b8 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/src_graphics/artifact_shop.inc b/src_graphics/artifact_shop.inc index 6195a91..ba1c2d4 100644 --- a/src_graphics/artifact_shop.inc +++ b/src_graphics/artifact_shop.inc @@ -1,4 +1,7 @@ HandleArtifactShop3D PROC uses ebx + call HandleBackroomsInventory3D + test eax,eax + jnz artifact_shop_done invoke Pressed3D,09h test eax,eax jz @F @@ -95,6 +98,11 @@ HandleArtifactShop3D ENDP RenderArtifactShop3D PROC uses ebx esi edi local y:DWORD,x:DWORD,ink:DWORD + cmp brInventoryOpen3D,0 + je @F + call RenderBackroomsInventory3D + ret +@@: invoke DrawBox,0,0,320,240,0015191Bh invoke DrawBox,0,0,320,24,002B302Ah invoke DrawString8x8,8,7,OFFSET artifactShopTitle3D,00EED5A0h @@ -140,7 +148,13 @@ artifact_shop_card: invoke DrawString8x8,120,77,charmDescs3D[ebx*4],00FFFFFFh ; Detail descriptions use full width below cards to avoid clipped text. invoke DrawString8x8,8,216,artifactRules3D[ebx*4],00DDCDAAh + cmp brOwned3D,0 + je @F + invoke DrawString8x8,8,228,OFFSET brInventoryHelp3D,00D8BC6Ah + jmp br_shop_help_done +@@: invoke DrawString8x8,8,228,OFFSET artifactHelp3D,0099AAA5h +br_shop_help_done: invoke DrawString8x8,120,96,OFFSET artifactPack3D,0099AAA5h invoke FormatNumber,charmUsesPack3D[ebx*4],OFFSET artifactNum3D invoke DrawString8x8,280,96,OFFSET artifactNum3D,00FFFFFFh diff --git a/src_graphics/artifact_system.inc b/src_graphics/artifact_system.inc index a4c753c..34bcd6d 100644 --- a/src_graphics/artifact_system.inc +++ b/src_graphics/artifact_system.inc @@ -32,8 +32,15 @@ ValidateArtifactSave3D PROC uses esi edi source:DWORD mov esi,source cmp DWORD PTR [esi+280],32465241h jne artifact_save_invalid - cmp DWORD PTR [esi+304],2 + mov eax,[esi+304] + and eax,0FFFCFFFFh + cmp eax,2 jne artifact_save_invalid + test DWORD PTR [esi+304],20000h + jz @F + test DWORD PTR [esi+304],10000h + jz artifact_save_invalid +@@: invoke ArtifactSaveSignature3D,esi cmp eax,[esi+308] jne artifact_save_invalid @@ -65,6 +72,8 @@ ArtifactCredit3D PROC amount:DWORD ArtifactCredit3D ENDP ResetArtifacts3D PROC + mov brOwned3D,0 + mov brEnabled3D,1 mov charmCloverUses,0 mov charmHorseshoeUses,0 mov charmSkullUses,0 diff --git a/src_graphics/backrooms.inc b/src_graphics/backrooms.inc new file mode 100644 index 0000000..8aa8caf --- /dev/null +++ b/src_graphics/backrooms.inc @@ -0,0 +1,1068 @@ +; Native Backrooms: connected DFS maze, rooms/loops, BFS farthest exit. +LoadBackroomsAssets3D PROC uses esi edi + invoke LoadAnimPack3D,OFFSET brMaterials3D,OFFSET brMaterialPath3D + test eax,eax + jz br_assets_done + cmp brMaterials3D.imageWidth,128 + jne br_assets_invalid + cmp brMaterials3D.height,128 + jne br_assets_invalid + cmp brMaterials3D.frames,5 + jne br_assets_invalid + mov esi,brMaterials3D.pixels + mov edi,OFFSET texturesBuffer+BR_WALL*65536 + mov ecx,5*128*128 + rep movsd + mov eax,1 + ret +br_assets_invalid: + invoke FreeAnimPack3D,OFFSET brMaterials3D + xor eax,eax +br_assets_done: + ret +LoadBackroomsAssets3D ENDP + +GenerateBackrooms3D PROC uses ebx esi edi + local top:DWORD,count:DWORD,current:DWORD,next:DWORD,tail:DWORD,head:DWORD + lea edi,brMap3D + mov eax,0B4B4B4B4h + mov ecx,256 + rep stosd + mov brMap3D[33],0 + mov brStack3D,33 + mov top,1 +br_dfs: + mov eax,top + test eax,eax + jz br_rooms + dec eax + mov ebx,brStack3D[eax*4] + mov current,ebx + mov count,0 + xor esi,esi +br_neighbors: + mov edx,ebx + and edx,31 + cmp esi,0 + jne @F + cmp ebx,65 + jb br_neighbor_next +@@: + cmp esi,1 + jne @F + cmp edx,29 + jae br_neighbor_next +@@: + cmp esi,2 + jne @F + cmp ebx,29*32 + jae br_neighbor_next +@@: + cmp esi,3 + jne @F + cmp edx,3 + jb br_neighbor_next +@@: + mov eax,ebx + add eax,brSteps3D[esi*4] + cmp brMap3D[eax],0 + je br_neighbor_next + mov ecx,count + mov brCandidates3D[ecx*4],eax + inc count +br_neighbor_next: + inc esi + cmp esi,4 + jb br_neighbors + cmp count,0 + je br_backtrack + invoke RandomRange3D,count + mov eax,brCandidates3D[eax*4] + mov brMap3D[eax],0 + mov edx,eax + add edx,current + shr edx,1 + mov brMap3D[edx],0 + mov ecx,top + mov brStack3D[ecx*4],eax + inc top + jmp br_dfs +br_backtrack: + dec top + jmp br_dfs +br_rooms: + ; Open a small landing room so the recovered camera never faces a wall. + mov brMap3D[33],0 + mov brMap3D[34],0 + mov brMap3D[35],0 + mov brMap3D[65],0 + mov brMap3D[66],0 + mov brMap3D[67],0 + mov brMap3D[97],0 + mov brMap3D[98],0 + mov brMap3D[99],0 + ; Every 3x3 room includes an odd lattice node: cannot be disconnected. + mov esi,12 +br_room: + invoke RandomRange3D,13 + shl eax,1 + inc eax + mov ebx,eax + invoke RandomRange3D,13 + shl eax,1 + inc eax + shl eax,5 + add ebx,eax + mov edi,3 +br_room_row: + mov brMap3D[ebx],0 + mov brMap3D[ebx+1],0 + mov brMap3D[ebx+2],0 + add ebx,32 + dec edi + jnz br_room_row + dec esi + jnz br_room + ; Open connecting walls only; never carve outside the sealed border. + mov esi,55 +br_loop: + invoke RandomRange3D,29 + inc eax + mov ebx,eax + invoke RandomRange3D,29 + inc eax + shl eax,5 + add ebx,eax + cmp brMap3D[ebx-1],0 + jne br_loop_vertical + cmp brMap3D[ebx+1],0 + je br_loop_open +br_loop_vertical: + cmp brMap3D[ebx-32],0 + jne br_loop_next + cmp brMap3D[ebx+32],0 + jne br_loop_next +br_loop_open: + mov brMap3D[ebx],0 +br_loop_next: + dec esi + jnz br_loop + lea edi,brDistance3D + mov eax,-1 + mov ecx,1024 + rep stosd + mov brDistance3D[33*4],0 + mov brStack3D,33 + mov head,0 + mov tail,1 + mov brExitCell3D,33 +br_bfs: + mov eax,head + cmp eax,tail + jae br_place_games + mov ebx,brStack3D[eax*4] + inc head + mov eax,brDistance3D[ebx*4] + mov ecx,brExitCell3D + cmp eax,brDistance3D[ecx*4] + jbe @F + mov brExitCell3D,ebx +@@: + xor esi,esi +br_bfs_neighbor: + mov eax,brSteps3D[esi*4] + sar eax,1 + add eax,ebx + cmp brMap3D[eax],0 + jne br_bfs_next + cmp brDistance3D[eax*4],-1 + jne br_bfs_next + mov edx,brDistance3D[ebx*4] + inc edx + mov brDistance3D[eax*4],edx + mov ecx,tail + mov brStack3D[ecx*4],eax + inc tail +br_bfs_next: + inc esi + cmp esi,4 + jb br_bfs_neighbor + jmp br_bfs +br_place_games: + ; Sample distinct reachable cells at evenly spaced BFS distances. + xor esi,esi +br_place_game: + mov eax,tail + imul eax,esi + xor edx,edx + mov ecx,7 + div ecx + add eax,8 + mov ebx,brStack3D[eax*4] + cmp ebx,brExitCell3D + jne @F + mov ebx,brStack3D[eax*4+4] +@@: + mov brGameCells3D[esi*4],ebx + inc esi + cmp esi,6 + jb br_place_game + ret +GenerateBackrooms3D ENDP + +BackroomsEntry3D PROC + cmp brActive3D,0 + jne br_entry_reset + cmp engineMode,MODE_WALK + jne br_entry_reset + cmp hallLevel3D,0 + jne br_entry_reset + cmp paused3D,0 + jne br_entry_reset + cmp playerX,16*FP_1 + jb br_entry_reset + cmp playerX,17*FP_1 + jae br_entry_reset + cmp playerY,30*FP_1-15000 + jb br_entry_reset + cmp playerY,30*FP_1 + jae br_entry_reset + cmp playerAngle,104 + jb br_entry_reset + cmp playerAngle,152 + ja br_entry_reset + cmp keys3D[53h],0 + jne br_entry_reset + cmp keys3D[28h],0 + jne br_entry_reset + cmp keys3D[41h],0 + jne br_entry_reset + cmp keys3D[44h],0 + jne br_entry_reset + cmp keys3D[57h],0 + jne br_entry_hold + cmp keys3D[26h],0 + je br_entry_reset +br_entry_hold: + mov eax,playerAngle + cmp brEntryHold3D,0 + je @F + cmp eax,brEntryAngle3D + jne br_entry_reset +@@: + mov brEntryAngle3D,eax + mov eax,deltaTime3D + add brEntryHold3D,eax + cmp brEntryHold3D,2000 + jb br_entry_done + call BeginBackrooms3D + ret +br_entry_reset: + mov brEntryHold3D,0 +br_entry_done: + ret +BackroomsEntry3D ENDP + +BeginBackrooms3D PROC + call LoadBackroomsAssets3D + test eax,eax + jz br_begin_done + call GenerateBackrooms3D + mov brActive3D,1 + mov brEntryHold3D,0 + mov brCutscene3D,1 + mov brClock3D,0 + mov brDialogue3D,0 + mov paused3D,0 + ; First 900ms still show the casino while camera clips through the wall. + mov viewEye3D,HALL_STANDING_EYE + mov viewCeiling3D,FP_1*5/2 + mov interactTargetId,0 + call ClearKeys3D +br_begin_done: + ret +BeginBackrooms3D ENDP + +LeaveBackrooms3D PROC reward:DWORD + cmp brActive3D,0 + je br_leave_done + cmp reward,0 + je @F + cmp brOwned3D,1 + je @F + mov brOwned3D,1 + mov brEnabled3D,1 +@@: + invoke CopyString3D,OFFSET brAward3D,OFFSET flashMessage + mov flashTimer,200 +@@: + mov brActive3D,0 + mov brCutscene3D,0 + mov brEntryHold3D,0 + mov worldMap3D,OFFSET casinoMap + mov hallLevel3D,0 + mov playerX,16*FP_1+FP_HALF + mov playerY,27*FP_1+FP_HALF + mov playerAngle,384 + mov viewEye3D,HALL_STANDING_EYE + mov viewCeiling3D,FP_1*5/2 + mov floorTexture3D,100 + mov engineMode,MODE_WALK + mov paused3D,0 + call ClearKeys3D + call SaveProgress3D +br_leave_done: + ret +LeaveBackrooms3D ENDP + +HandleBackrooms3D PROC uses ebx esi edi + mov eax,deltaTime3D + add brClock3D,eax + cmp brCutscene3D,0 + je br_walk + cmp brClock3D,900 + jae br_fall_world + mov eax,deltaTime3D + imul eax,30 + add playerY,eax + ret +br_fall_world: + cmp hallLevel3D,HALL_BACKROOMS + je @F + mov hallLevel3D,HALL_BACKROOMS + mov worldMap3D,OFFSET brMap3D + mov playerX,FP_1+FP_HALF + mov playerY,FP_1+FP_HALF + mov playerAngle,64 + mov floorTexture3D,BR_FLOOR +@@: + cmp brClock3D,6500 + jae br_stand + ; Camera fall, impact, then low-to-standing recovery; no video player. + mov eax,brClock3D + cmp eax,2200 + jae br_impact + imul eax,36 + mov edx,FP_1*3/2 + sub edx,eax + cmp edx,FP_1/8 + jge @F + mov edx,FP_1/8 +@@: + mov viewEye3D,edx + mov eax,brClock3D + shr eax,5 + and eax,511 + mov playerAngle,eax + ret +br_impact: + cmp eax,3100 + jae br_rise + mov viewEye3D,FP_1/8 + mov playerAngle,64 + ret +br_rise: + sub eax,3100 + imul eax,HALL_STANDING_EYE-FP_1/8 + xor edx,edx + mov ecx,3400 + div ecx + add eax,FP_1/8 + mov viewEye3D,eax + ret +br_stand: + mov brCutscene3D,0 + mov viewEye3D,HALL_STANDING_EYE + call ClearKeys3D +br_walk: + mov eax,deltaTime3D + add brDialogue3D,eax + call HandlePlayerInput + mov interactTargetId,0 + mov interactPromptPtr,0 + mov brTarget3D,-1 + xor esi,esi +br_target_scan: + cmp esi,6 + jb @F + mov ebx,brExitCell3D + jmp br_target_cell +@@: + mov ebx,brGameCells3D[esi*4] +br_target_cell: + mov eax,ebx + and eax,31 + shl eax,16 + add eax,FP_HALF + sub eax,playerX + sar eax,8 + imul eax,eax + mov edi,eax + mov eax,ebx + shr eax,5 + shl eax,16 + add eax,FP_HALF + sub eax,playerY + sar eax,8 + imul eax,eax + add eax,edi + cmp eax,FP_1*3/4 + ja br_target_next + mov brTarget3D,esi + cmp esi,6 + je @F + mov eax,brGamePrompts3D[esi*4] + mov interactPromptPtr,eax + jmp br_targets_done +@@: + mov interactPromptPtr,OFFSET brExitPrompt3D + jmp br_targets_done +br_target_next: + inc esi + cmp esi,7 + jb br_target_scan +br_targets_done: + invoke Pressed3D,56h + test eax,eax + jz @F + xor brVhs3D,1 +@@: + invoke Pressed3D,45h + test eax,eax + jz br_handle_done + mov esi,brTarget3D + cmp esi,6 + ja br_handle_done + jne br_launch + invoke LeaveBackrooms3D,1 + ret +br_launch: + ; Reuse normal station dispatch; suppress all hall-specific interactions. + mov eax,brGames3D[esi*4] + mov interactTargetId,eax + call Trigger3DInteraction + call ClearKeys3D +br_handle_done: + ret +HandleBackrooms3D ENDP + +RenderBackroomsDialogue3D PROC uses ebx esi edi + local x:DWORD,y:DWORD,age:DWORD,offsetY:DWORD + cmp brDialogue3D,9500 + jae br_dialogue_done + invoke DrawBoxBorder,4,179,312,57,00191711h,009C9464h,1 + mov x,12 + mov y,189 + xor esi,esi +br_letter: + movzx ebx,brText3D[esi] + test ebx,ebx + jz br_dialogue_done + ; Wrap whole words, not arbitrary characters. + cmp ebx,32 + jne br_letter_position + mov edi,esi + xor ecx,ecx +br_word_length: + inc edi + mov al,brText3D[edi] + test al,al + jz br_word_end + cmp al,32 + je br_word_end + inc ecx + jmp br_word_length +br_word_end: + inc ecx + shl ecx,3 + add ecx,x + cmp ecx,300 + jbe br_letter_position + mov x,12 + add y,14 + jmp br_letter_next +br_letter_position: + mov eax,esi + imul eax,35 + mov ecx,brDialogue3D + sub ecx,eax + js br_letter_advance + cmp ecx,6400 + jae br_letter_advance + mov age,ecx + mov offsetY,0 + cmp ecx,420 + jae br_letter_out + mov eax,420 + sub eax,ecx + imul eax,18 + xor edx,edx + mov ecx,420 + div ecx + mov offsetY,eax + jmp br_letter_draw +br_letter_out: + cmp ecx,5900 + jb br_letter_draw + sub ecx,5900 + imul ecx,18 + mov eax,ecx + xor edx,edx + mov ecx,500 + div ecx + neg eax + mov offsetY,eax +br_letter_draw: + mov eax,age + shr eax,3 + mov ecx,esi + shl ecx,4 + add eax,ecx + and eax,511 + mov eax,tableSin[eax*4] + sar eax,15 + add eax,y + add eax,offsetY + mov brGlyph3D,bl + invoke DrawString8x8,x,eax,OFFSET brGlyph3D,00EBE4B5h +br_letter_advance: + add x,8 +br_letter_next: + inc esi + jmp br_letter +br_dialogue_done: + ret +RenderBackroomsDialogue3D ENDP + +RenderBackrooms3D PROC uses ebx esi edi + local exitX:DWORD,exitY:DWORD + call UpdateCamera3D + call RenderCeilingAndFloor + call RenderWalls + call PrepareCityDepth3D + cmp hallLevel3D,HALL_BACKROOMS + je @F + call RenderSprites + call RenderHall3D + jmp br_camera_hud +@@: + xor esi,esi +br_render_station: + mov ebx,brGameCells3D[esi*4] + mov eax,ebx + and eax,31 + shl eax,16 + add eax,FP_HALF + mov brProp3D.posX,eax + mov eax,ebx + shr eax,5 + shl eax,16 + add eax,FP_HALF + mov brProp3D.posY,eax + mov eax,brGamePacks3D[esi*4] + mov brProp3D.asset,eax + mov eax,brGameFrames3D[esi*4] + mov brProp3D.frame,eax + invoke DrawCityObject3D,OFFSET brProp3D,0 + inc esi + cmp esi,6 + jb br_render_station + ; Upright door on a neighboring solid wall; collision map stays unchanged. + mov ebx,brExitCell3D + mov eax,ebx + and eax,31 + shl eax,16 + mov exitX,eax + mov eax,ebx + shr eax,5 + shl eax,16 + mov exitY,eax + cmp brMap3D[ebx-32],0 + je br_exit_south + add exitY,512 + jmp br_exit_y_door +br_exit_south: + cmp brMap3D[ebx+32],0 + je br_exit_west + add exitY,FP_1-512 +br_exit_y_door: + mov eax,exitX + add eax,FP_1 + invoke DrawCityVertical3D,1,exitY,exitX,eax,0,FP_1*21/10,BR_EXIT + jmp br_exit_drawn +br_exit_west: + cmp brMap3D[ebx-1],0 + je br_exit_east + add exitX,512 + jmp br_exit_x_door +br_exit_east: + add exitX,FP_1-512 +br_exit_x_door: + mov eax,exitY + add eax,FP_1 + invoke DrawCityVertical3D,0,exitX,exitY,eax,0,FP_1*21/10,BR_EXIT +br_exit_drawn: + cmp brVhs3D,0 + je br_camera_hud + ; Deterministic screen noise, independent of gameplay RNG. + lea edi,screenBuffer + mov ecx,SCREEN_SIZE + mov ebx,brClock3D +br_vhs_pixel: + imul ebx,1664525 + add ebx,1013904223 + mov eax,[edi] + ; Reserve two low bits per channel: adding 0..3 cannot carry into RGB. + and eax,00FCFCFCh + mov edx,ebx + shr edx,30 + imul edx,00010101h + add eax,edx + stosd + dec ecx + jnz br_vhs_pixel + mov esi,2 +br_scanline: + imul eax,esi,SCREEN_W*4 + lea edi,screenBuffer[eax] + mov ecx,SCREEN_W +br_scan_pixel: + mov eax,[edi] + and eax,00FCFCFCh + mov edx,eax + shr edx,2 + sub eax,edx + stosd + loop br_scan_pixel + add esi,6 + cmp esi,SCREEN_H + jb br_scanline +br_camera_hud: + mov eax,brClock3D + xor edx,edx + mov ecx,1000 + div ecx + xor esi,esi + mov esi,6 +br_time_pair: + xor edx,edx + mov ecx,60 + div ecx + push eax + mov eax,edx + xor edx,edx + mov ecx,10 + div ecx + add al,'0' + add dl,'0' + mov brTime3D[esi],al + mov brTime3D[esi+1],dl + pop eax + sub esi,3 + jns br_time_pair + invoke DrawString8x8,224,12,OFFSET brTime3D,00EEEEDDh + invoke DrawString8x8,25,12,OFFSET brRec3D,00EEEEDDh + test brClock3D,512 + jnz @F + invoke DrawBox,12,13,7,7,00EE3030h +@@: + invoke DrawString8x8,8,29,OFFSET brHelp3D,00C8C8A0h + cmp brCutscene3D,0 + je br_hud_walk + cmp brClock3D,2200 + jb br_render_done + cmp brClock3D,2800 + jae br_render_done + invoke DrawBox,0,0,SCREEN_W,SCREEN_H,00080704h + ret +br_hud_walk: + cmp interactPromptPtr,0 + je @F + invoke DrawString8x8,112,158,interactPromptPtr,00F1F1B5h +@@: + call RenderBackroomsDialogue3D +br_render_done: + ret +RenderBackrooms3D ENDP + +; -1 = disabled; 0 = unlucky; 1 = lucky. Caller latches once per round. +BackroomsLuck3D PROC + cmp brOwned3D,1 + jne br_luck_off + cmp brEnabled3D,1 + jne br_luck_off + cmp brActive3D,0 + jne br_luck_off + cmp tournamentMode,0 + jne br_luck_off + cmp obsActive3D,0 + jne br_luck_off + invoke RandomRange3D,100 + cmp eax,90 + setb al + movzx eax,al + ret +br_luck_off: + mov eax,-1 + ret +BackroomsLuck3D ENDP + +; Select a physically possible roulette result by actual net return. +BackroomsWheelLuck3D PROC uses ebx esi edi original:DWORD + local desired:DWORD,total:DWORD,count:DWORD + call BackroomsLuck3D + cmp eax,-1 + je br_wheel_original + mov desired,eax + mov count,0 + xor edi,edi +br_wheel_result: + mov total,0 + xor esi,esi +br_wheel_bet: + invoke WheelBetMultiplier3D,esi,edi + imul eax,wheelBets3D[esi*4] + add total,eax + inc esi + cmp esi,49 + jb br_wheel_bet + mov eax,total + cmp eax,wheelStake3D + seta al + movzx eax,al + cmp eax,desired + jne br_wheel_next + inc count + invoke RandomRange3D,count + test eax,eax + jnz br_wheel_next + mov original,edi +br_wheel_next: + inc edi + cmp edi,37 + jb br_wheel_result +br_wheel_original: + mov eax,original + ret +BackroomsWheelLuck3D ENDP + +; Permute a real deck by swapping cards, preserving all 52 unique identities. +BackroomsDeck3D PROC uses ebx esi edi deck:DWORD,targets:DWORD,count:DWORD + mov edi,deck + mov esi,targets + xor ebx,ebx +br_deck_position: + cmp ebx,count + jae br_deck_done + mov eax,[esi+ebx*4] + cmp eax,52 + jae br_deck_next + xor ecx,ecx +br_deck_find: + cmp ecx,52 + jae br_deck_next + cmp [edi+ecx*4],eax + je br_deck_swap + inc ecx + jmp br_deck_find +br_deck_swap: + mov edx,[edi+ebx*4] + mov [edi+ecx*4],edx + mov [edi+ebx*4],eax +br_deck_next: + inc ebx + jmp br_deck_position +br_deck_done: + ret +BackroomsDeck3D ENDP + +BackroomsBlackjackDeck3D PROC uses ebx esi edi + call BackroomsLuck3D + cmp eax,-1 + je br_bj_done + mov ebx,eax + lea edi,brDeckTargets3D + mov eax,-1 + mov ecx,16 + rep stosd + ; Two aces + picture cards remain distinct. Party seats keep their cards. + mov ecx,cbBotCount3D + add ecx,2 + cmp ebx,1 + jne br_bj_unlucky + mov brDeckTargets3D,12 + mov brDeckTargets3D[ecx*4],11 + dec ecx + mov brDeckTargets3D[ecx*4],8 + inc ecx + add ecx,ecx + dec ecx + mov brDeckTargets3D[ecx*4],7 + jmp br_bj_apply +br_bj_unlucky: + mov brDeckTargets3D,8 + mov brDeckTargets3D[ecx*4],7 + dec ecx + mov brDeckTargets3D[ecx*4],12 + inc ecx + add ecx,ecx + dec ecx + mov brDeckTargets3D[ecx*4],11 +br_bj_apply: + invoke BackroomsDeck3D,OFFSET cbDeck3D,OFFSET brDeckTargets3D,10 +br_bj_done: + ret +BackroomsBlackjackDeck3D ENDP + +BackroomsPokerDeck3D PROC uses ebx edi + call BackroomsLuck3D + cmp eax,-1 + je br_pk_done + mov ebx,eax + lea edi,brDeckTargets3D + mov eax,-1 + mov ecx,16 + rep stosd + ; Fixed board creates a real royal flush for the lucky seat, no duplicate cards. + ; Flop deck 5,6,7; turn 9; river 11. Burns remain shuffled. + mov brDeckTargets3D[5*4],8 + mov brDeckTargets3D[6*4],9 + mov brDeckTargets3D[7*4],10 + mov brDeckTargets3D[9*4],13 + mov brDeckTargets3D[11*4],27 + mov ecx,pkButton3D + xor ecx,1 + xor ebx,1 + xor ecx,ebx + mov brDeckTargets3D[ecx*4],11 + mov brDeckTargets3D[ecx*4+8],12 + xor ecx,1 + mov brDeckTargets3D[ecx*4],16 + mov brDeckTargets3D[ecx*4+8],30 + invoke BackroomsDeck3D,OFFSET pkDeck3D,OFFSET brDeckTargets3D,12 +br_pk_done: + ret +BackroomsPokerDeck3D ENDP + +BackroomsDurakDeck3D PROC uses ebx esi edi + call BackroomsLuck3D + cmp eax,-1 + je br_dk_done + mov ebx,eax + ; Favor six initial trump cards, preserving the compacted 36-card deck. + ; A poor move can still lose: no settlement or rule is overridden. + mov ecx,0 +br_dk_pair: + mov eax,ecx + add eax,7 + cmp ebx,1 + jne @F + add eax,26 +@@: + mov edx,ecx + shl edx,1 + xor esi,esi +br_dk_find: + cmp esi,36 + jae br_dk_next + cmp dkDeck3D[esi*4],eax + je br_dk_swap + inc esi + jmp br_dk_find +br_dk_swap: + mov edi,dkDeck3D[edx*4] + mov dkDeck3D[esi*4],edi + mov dkDeck3D[edx*4],eax +br_dk_next: + inc ecx + cmp ecx,6 + jb br_dk_pair + ; Exposed trump must match the favored side, without introducing duplicates. + mov eax,43 + cmp ebx,1 + jne @F + mov eax,30 +@@: + xor esi,esi +br_dk_trump_find: + cmp dkDeck3D[esi*4],eax + je br_dk_trump_swap + inc esi + cmp esi,36 + jb br_dk_trump_find + jmp br_dk_done +br_dk_trump_swap: + mov edx,dkDeck3D[35*4] + mov dkDeck3D[esi*4],edx + mov dkDeck3D[35*4],eax +br_dk_done: + ret +BackroomsDurakDeck3D ENDP + +; Reorder only at loading time, before shell knowledge or previews exist. +; This biases real cartridges, never changes a cartridge after it is revealed. +BackroomsBuckshotLuck3D PROC uses ebx esi edi + call BackroomsLuck3D + cmp eax,-1 + je br_bs_done + mov brBuckshotLuck3D,eax + xor eax,bs3d_turn + mov ebx,eax + xor esi,esi +br_bs_shell: + ; Only permute cartridges already loaded; preserve advertised counts. + xor eax,eax + cmp ebx,1 + jne br_bs_blanks_first + cmp esi,bs3d_liveCount + setb al + jmp br_bs_store +br_bs_blanks_first: + cmp esi,bs3d_blankCount + setae al +br_bs_store: + mov bs3d_shells[esi*4],eax + inc esi + cmp esi,bs3d_shellCount + jb br_bs_shell +br_bs_done: + ret +BackroomsBuckshotLuck3D ENDP + +BackroomsRussianLuck3D PROC + ; Cylinder is arranged before LOAD/SPIN; shot animation consumes it unchanged. + call BackroomsLuck3D + cmp eax,-1 + je br_rr_done + mov brRussianLuck3D,eax + mov rr3d_turn,eax + call BackroomsRussianCylinder3D +br_rr_done: + ret +BackroomsRussianLuck3D ENDP + +; Search legal cylinders before loading animation, not at the trigger frame. +; Keep selected bullet count. Lucky cylinders eliminate guests, never player; +; subsequent reloads retain the single round's luck roll. +BackroomsRussianCylinder3D PROC uses ebx esi edi + local candidate:DWORD,alive:DWORD,actor:DWORD,step:DWORD + cmp brRussianLuck3D,-1 + je br_cylinder_done + cmp brRussianLuck3D,0 + jne br_cylinder_search + mov eax,1 + mov ecx,rr3d_bullets + shl eax,cl + dec eax + mov rr3d_mask,eax + mov rr3d_turn,0 + mov rr3d_chamber,0 + ret +br_cylinder_search: + mov candidate,1 +br_cylinder_candidate: + mov eax,candidate + xor ecx,ecx +br_cylinder_popcount: + shr eax,1 + adc ecx,0 + test eax,eax + jnz br_cylinder_popcount + cmp ecx,rr3d_bullets + jne br_cylinder_next + mov eax,rrPartyAlive3D + mov alive,eax + mov eax,rr3d_turn + mov actor,eax + mov step,0 +br_cylinder_simulate: + mov eax,candidate + mov ecx,step + bt eax,ecx + jnc br_cylinder_advance + mov ecx,actor + test ecx,ecx + jz br_cylinder_next + btr alive,ecx + cmp alive,1 + je br_cylinder_found +br_cylinder_advance: + inc actor + mov eax,actor + cmp eax,rrPartySize3D + jb @F + mov actor,0 + xor eax,eax +@@: + bt alive,eax + jnc br_cylinder_advance + inc step + cmp step,6 + jb br_cylinder_simulate +br_cylinder_found: + mov eax,candidate + mov rr3d_mask,eax + mov rr3d_chamber,0 + ret +br_cylinder_next: + inc candidate + cmp candidate,64 + jb br_cylinder_candidate +br_cylinder_done: + ret +BackroomsRussianCylinder3D ENDP + +HandleBackroomsInventory3D PROC + cmp brOwned3D,0 + jne @F + mov brInventoryOpen3D,0 + xor eax,eax + ret +@@: + invoke Pressed3D,52h + test eax,eax + jz @F + xor brInventoryOpen3D,1 + mov eax,1 + ret +@@: + cmp brInventoryOpen3D,0 + je br_inventory_unhandled + invoke Pressed3D,09h + test eax,eax + jz @F + mov brInventoryOpen3D,0 +@@: + invoke Pressed3D,45h + test eax,eax + jz @F + xor brEnabled3D,1 + call SaveProgress3D +@@: + mov eax,1 + ret +br_inventory_unhandled: + xor eax,eax + ret +HandleBackroomsInventory3D ENDP + +RenderBackroomsInventory3D PROC uses esi + invoke DrawBox,0,0,320,240,0015191Bh + invoke DrawBoxBorder,8,8,304,224,001D2425h,00D8BC6Ah,1 + invoke DrawString8x8,24,24,OFFSET brInventory3D,00EED5A0h + invoke DrawString8x8,24,52,OFFSET brInventoryRules1,00FFFFFFh + invoke DrawString8x8,24,72,OFFSET brInventoryRules2,00FFFFFFh + invoke DrawString8x8,24,100,OFFSET brInventoryRules3,00DDCDAAh + invoke DrawString8x8,24,120,OFFSET brInventoryRules4,00DDCDAAh + invoke DrawString8x8,24,140,OFFSET brInventoryRules5,00DDCDAAh + invoke DrawString8x8,24,160,OFFSET brInventoryRules6,00DDCDAAh + invoke DrawString8x8,24,188,OFFSET brInventoryHelp3D,00D8BC6Ah + mov esi,OFFSET brDisabledText3D + cmp brEnabled3D,0 + je @F + mov esi,OFFSET brEnabledText3D +@@: + invoke DrawString8x8,264,188,esi,00FFFFFFh + invoke DrawString8x8,24,212,OFFSET brInventoryReturn3D,0099AAA5h + ret +RenderBackroomsInventory3D ENDP diff --git a/src_graphics/backrooms_data.inc b/src_graphics/backrooms_data.inc new file mode 100644 index 0000000..43a7954 --- /dev/null +++ b/src_graphics/backrooms_data.inc @@ -0,0 +1,86 @@ +; Dedicated world/session. All coordinates are 16.16, all time is active ms. +HALL_BACKROOMS equ 6 +BR_WALL equ 180 +BR_FLOOR equ 181 +BR_CEILING equ 182 +BR_LAMP equ 183 +BR_EXIT equ 184 +BackroomsEntry3D PROTO +BeginBackrooms3D PROTO +GenerateBackrooms3D PROTO +HandleBackrooms3D PROTO +RenderBackrooms3D PROTO +LeaveBackrooms3D PROTO :DWORD +BackroomsLuck3D PROTO +BackroomsWheelLuck3D PROTO :DWORD +BackroomsDeck3D PROTO :DWORD,:DWORD,:DWORD +BackroomsBlackjackDeck3D PROTO +BackroomsPokerDeck3D PROTO +BackroomsDurakDeck3D PROTO +BackroomsBuckshotLuck3D PROTO +BackroomsRussianLuck3D PROTO +BackroomsRussianCylinder3D PROTO +LoadBackroomsAssets3D PROTO +HandleBackroomsInventory3D PROTO +RenderBackroomsInventory3D PROTO +.data +brActive3D dd 0 +brEntryHold3D dd 0 +brEntryAngle3D dd 0 +brCutscene3D dd 0 +brClock3D dd 0 +brSkullsLuck3D dd -1 +brSlotLuck3D dd -1 +brRussianLuck3D dd -1 +brBuckshotLuck3D dd -1 +brDeckTargets3D dd 16 dup(0) +brDialogue3D dd 0 +brOwned3D dd 0 +brEnabled3D dd 1 +brVhs3D dd 1 +brExitCell3D dd 0 +brTarget3D dd -1 +brInventoryOpen3D dd 0 +brMap3D db 1024 dup(BR_WALL) +brDistance3D dd 1024 dup(-1) +brStack3D dd 1024 dup(0) +brCandidates3D dd 4 dup(0) +brSteps3D dd -64,2,64,-2 +brGames3D dd INTERACT_COIN,INTERACT_DICE,INTERACT_SLOTS_777,INTERACT_BLACKJACK,INTERACT_ROULETTE,INTERACT_SLOTS_CLOVER +brGameCells3D dd 6 dup(0) +brGamePacks3D dd OFFSET hallCardFurniture3D,OFFSET hallCardFurniture3D,OFFSET hallSlotFurniture3D,OFFSET hallCardFurniture3D,OFFSET hallWheelFurniture3D,OFFSET hallSlotFurniture3D +brGameFrames3D dd 0,0,0,0,0,1 +brGamePrompts3D dd OFFSET brCoinPrompt3D,OFFSET brDicePrompt3D,OFFSET brSlotPrompt3D,OFFSET brCardPrompt3D,OFFSET brWheelPrompt3D,OFFSET brCloverPrompt3D +brCoinPrompt3D db 'E: COIN',0 +brDicePrompt3D db 'E: DICE',0 +brSlotPrompt3D db 'E: 777',0 +brCardPrompt3D db 'E: BLACKJACK',0 +brWheelPrompt3D db 'E: ROULETTE',0 +brCloverPrompt3D db 'E: CLOVERPIT',0 +brProp3D CityProp3D <0,0,FP_1*3/5,FP_1*4/5,0,OFFSET hallCardFurniture3D> +brMaterials3D AnimPack3D <0> +brMaterialPath3D db "assets\sprites\backrooms\materials.bin",0 +brRec3D db 'REC',0 +brTime3D db '00:00:00',0 +brExitPrompt3D db 'E: EXIT',0 +brHelp3D db 'V: VHS ESC: PAUSE',0 +brInventory3D db 'REALITY ERROR: 90% / INFINITE',0 +brToggle3D db 'R: TOGGLE REALITY ERROR',0 +brEnabledText3D db 'ON',0 +brDisabledText3D db 'OFF',0 +brRealityGlitch3D db 'REALITY ERROR',0 +brAward3D db 'REALITY ERROR / INFINITE / 90%',0 +brInventoryHelp3D db 'R: REALITY ERROR / E: TOGGLE',0 +brInventoryReturn3D db 'R / TAB: BACK',0 +brInventoryRules1 db '90% FAVORABLE RANDOM OUTCOME',0 +brInventoryRules2 db 'NO CHARGES / NO DUPLICATES',0 +brInventoryRules3 db 'NORMAL CASINO ONLY',0 +brInventoryRules4 db 'OFF IN TOWER / BACKROOMS',0 +brInventoryRules5 db 'SKILL / CARD RULES STILL APPLY',0 +brInventoryRules6 db 'BILLIARDS/BOWLING/DARTS UNCHANGED',0 +brGlyph3D db 0,0 +; Exact CP1251 dialogue, wrapped only at spaces by the renderer. +brText3D db 221,242,238,32,237,229,32,242,243,242,44,32,242,251,32,237,229,32 + db 228,238,235,230,229,237,32,225,251,242,252,32,231,228,229,241,252,44,32 + db 241,250,229,225,232,32,239,238,32,225,240,224,242,241,234,232,32,239,230,0 +.code diff --git a/src_graphics/buckshot.inc b/src_graphics/buckshot.inc index 5f15329..2547b7a 100644 --- a/src_graphics/buckshot.inc +++ b/src_graphics/buckshot.inc @@ -97,6 +97,7 @@ bs_reload_next: inc bs3d_blankCount dec bs3d_liveCount @@: + call BackroomsBuckshotLuck3D ; The loading motion loops once per actual cartridge, then racks the pump. mov eax, bs3d_shellCount imul eax, 330 diff --git a/src_graphics/city_bins.inc b/src_graphics/city_bins.inc index edc74cb..1291832 100644 --- a/src_graphics/city_bins.inc +++ b/src_graphics/city_bins.inc @@ -5,6 +5,19 @@ 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 hallLevel3D,HALL_BACKROOMS + jne br_surface_other_world + cmp eax,BR_WALL + jb @F + cmp eax,BR_EXIT + ja @F + shl eax,16 + add eax,OFFSET texturesBuffer + mov ecx,128 + mov edx,128 + ret +@@: +br_surface_other_world: cmp eax,BW_MATERIAL jb @F cmp eax,BW_LAST_MATERIAL @@ -290,6 +303,27 @@ city_vertical_along: dec edx and eax, edx @@: + ; Door lettering faces the viewer on either wall face. Other worlds keep UVs. + cmp hallLevel3D,HALL_BACKROOMS + jne br_door_uv_done + cmp material,BR_EXIT + jne br_door_uv_done + cmp axis,0 + jne br_door_uv_y + mov edx,playerX + cmp edx,coordinate + jle br_door_uv_done + jmp br_door_uv_flip +br_door_uv_y: + mov edx,playerY + cmp edx,coordinate + jge br_door_uv_done +br_door_uv_flip: + mov edx,textureW + dec edx + sub edx,eax + mov eax,edx +br_door_uv_done: mov sourceX, eax mov eax, viewEye3D sub eax, zHigh diff --git a/src_graphics/hall_scene.inc b/src_graphics/hall_scene.inc index 717eb60..a4e2a16 100644 --- a/src_graphics/hall_scene.inc +++ b/src_graphics/hall_scene.inc @@ -19,7 +19,13 @@ hall_plane_row: sub ecx, eax mov eax, viewCeiling3D sub eax, viewEye3D - mov material, 101 + mov material, 101 + cmp brActive3D,0 + je @F + cmp hallLevel3D,HALL_BACKROOMS + jne @F + mov material,BR_CEILING +@@: cmp hallLevel3D,2 jne @F mov material,125 @@ -130,7 +136,27 @@ hall_plane_crossroad: jl hall_plane_sample hall_plane_sidewalk: mov ebx, 72 -hall_plane_sample: +hall_plane_sample: + cmp brActive3D,0 + je br_plane_sample_done + cmp hallLevel3D,HALL_BACKROOMS + jne br_plane_sample_done + mov eax,row + cmp eax,viewHorizon3D + jae br_plane_sample_done + mov ebx,BR_CEILING + mov eax,wx + shr eax,16 + and eax,3 + cmp eax,1 + jne br_plane_sample_done + mov eax,wy + shr eax,16 + and eax,3 + cmp eax,1 + jne br_plane_sample_done + mov ebx,BR_LAMP +br_plane_sample_done: cmp outdoorRender3D, 0 jne hall_plane_low_sample mov eax, row diff --git a/src_graphics/native_blackjack.inc b/src_graphics/native_blackjack.inc index acdd1e4..ba89a6f 100644 --- a/src_graphics/native_blackjack.inc +++ b/src_graphics/native_blackjack.inc @@ -120,6 +120,7 @@ StartNativeBlackjack3D PROC mov cbDealerCount3D, 2 mov cbCursor3D, 4 invoke ShuffleCards3D, OFFSET cbDeck3D + call BackroomsBlackjackDeck3D mov eax, cbDeck3D[0] mov cbPlayer3D[0], eax mov eax, cbDeck3D[4] diff --git a/src_graphics/native_clover.inc b/src_graphics/native_clover.inc index 596d0f6..45d561a 100644 --- a/src_graphics/native_clover.inc +++ b/src_graphics/native_clover.inc @@ -104,6 +104,41 @@ clp_start_rows: inc esi cmp esi,5 jb clp_start_column + call BackroomsLuck3D + cmp eax,-1 + je br_clover_fixed + mov ebx,eax + xor esi,esi +br_clover_row: + xor edi,edi +br_clover_column: + mov eax,esi + imul eax,2 + add eax,edi + xor edx,edx + mov ecx,7 + div ecx + cmp ebx,1 + jne @F + mov edx,1 +@@: + mov eax,esi + imul eax,5 + add eax,edi + mov clpGrid3D[eax*4],edx + mov eax,clpSteps3D[edi*4] + add eax,esi + mov ecx,edi + shl ecx,8 + lea ecx,clpTape3D[ecx] + mov [ecx+eax*4],edx + inc edi + cmp edi,5 + jb br_clover_column + inc esi + cmp esi,3 + jb br_clover_row +br_clover_fixed: call SaveProgress3D invoke PlayCasinoSound3D,OFFSET soundSlotSpin,0,1 clp_start_done: diff --git a/src_graphics/native_coin.inc b/src_graphics/native_coin.inc index 4c9f32c..b0e2ec4 100644 --- a/src_graphics/native_coin.inc +++ b/src_graphics/native_coin.inc @@ -308,6 +308,16 @@ coin_native_start: invoke RandomRange3D,2 inc eax mov coin3d_result,eax + call BackroomsLuck3D + cmp eax,-1 + je br_coin_fixed + xor eax,1 + mov ecx,coin3d_choice + dec ecx + xor eax,ecx + inc eax + mov coin3d_result,eax +br_coin_fixed: call SaveProgress3D invoke PlayCasinoSound3D,OFFSET soundCoin,0,1 call ClearKeys3D diff --git a/src_graphics/native_durak.inc b/src_graphics/native_durak.inc index 93a1b8a..a647a75 100644 --- a/src_graphics/native_durak.inc +++ b/src_graphics/native_durak.inc @@ -40,6 +40,7 @@ dk_compact: inc esi cmp esi, 52 jb dk_compact + call BackroomsDurakDeck3D mov eax, dkDeck3D[35*4] mov durakTrumpCard, eax invoke DurakCardSuit, eax diff --git a/src_graphics/native_poker.inc b/src_graphics/native_poker.inc index 3219d2d..597a0e7 100644 --- a/src_graphics/native_poker.inc +++ b/src_graphics/native_poker.inc @@ -75,6 +75,7 @@ StartNativePoker3D PROC uses ebx esi mov pkRaise3D, eax mov pkCurrent3D, eax invoke ShuffleCards3D, OFFSET pkDeck3D + call BackroomsPokerDeck3D mov esi, pkButton3D xor esi, 1 shl esi, 3 ; first card goes to the non-button player diff --git a/src_graphics/native_slots.inc b/src_graphics/native_slots.inc index ff3c84e..80b7938 100644 --- a/src_graphics/native_slots.inc +++ b/src_graphics/native_slots.inc @@ -69,11 +69,22 @@ StartNativeSlots3D PROC uses ebx esi mov slotLeaveAfter3D,0 mov slotError3D,0 mov slotEffects3D,0 + call BackroomsLuck3D + mov brSlotLuck3D,eax xor esi,esi slot_start_reel: mov eax,slotStops3D[esi*4] mov slotStartStops3D[esi*4],eax invoke RandomRange3D,20 + cmp brSlotLuck3D,-1 + je br_slot_target + mov eax,0 + cmp brSlotLuck3D,1 + je br_slot_target + ; Distinct symbols, with no refund pair, for the unlucky 10%. + mov eax,esi + inc eax +br_slot_target: mov slotTargets3D[esi*4],eax mov edx,slotStrip3D[eax*4] mov slotReel1[esi*4],edx diff --git a/src_graphics/native_wheel.inc b/src_graphics/native_wheel.inc index f668a99..f659dcc 100644 --- a/src_graphics/native_wheel.inc +++ b/src_graphics/native_wheel.inc @@ -352,6 +352,7 @@ wheel_regular_spin: mov roulState, 1 mov wheelTime3D, 0 invoke RandomRange3D, 37 + invoke BackroomsWheelLuck3D,eax mov wheelResult3D, eax call SaveProgress3D invoke PlayCasinoSound3D, OFFSET soundRoulette,0,1 diff --git a/src_graphics/platform_code.inc b/src_graphics/platform_code.inc index 9c5f2d3..8b5c8e3 100644 --- a/src_graphics/platform_code.inc +++ b/src_graphics/platform_code.inc @@ -137,6 +137,12 @@ event_key_up: cmp ecx, 255 ja event_done mov keys3D[ecx], 0 + cmp ecx,57h + je event_entry_release + cmp ecx,26h + jne event_done +event_entry_release: + mov brEntryHold3D,0 jmp event_done event_fullscreen: test lp, 40000000h @@ -144,6 +150,7 @@ event_fullscreen: call ToggleFullscreen3D jmp event_done event_focus_lost: + mov brEntryHold3D,0 mov dtHolding3D,0 mov dtMouseReleased3D,0 call ClearKeys3D @@ -505,9 +512,12 @@ UpdateCamera3D ENDP ; Four corners of a 0.3-tile-wide body; bounds checked before every map read. CanWalk3D PROC uses ebx esi edi px:DWORD, py:DWORD + cmp brActive3D,0 + jne br_generic_collision invoke CanWalkHall3D, px, py test eax, eax jz walk_blocked +br_generic_collision: mov esi, -9830 walk_corner_x: mov edi, -9830 @@ -550,6 +560,8 @@ LoadProgress3D PROC uses ebx mov charmSkullUses, 0 mov charmDevilUses, 0 mov charmEnabledMask3D,7 + mov brOwned3D,0 + mov brEnabled3D,1 invoke CreateFileA, OFFSET savePath3D, 80000000h, 1, 0, 3, 80h, 0 cmp eax, -1 je progress_loaded @@ -566,7 +578,23 @@ LoadProgress3D PROC uses ebx cmp eax,saveData3D[36] jne progress_loaded mov eax,saveData3D[32] - mov charmEnabledMask3D,eax + test eax,0FFFCFFF0h + jnz progress_loaded + test eax,20000h + jz @F + test eax,10000h + jz progress_loaded +@@: + mov ecx,eax + and ecx,15 + mov charmEnabledMask3D,ecx + shr eax,16 + and eax,1 + mov brOwned3D,eax + mov eax,saveData3D[32] + shr eax,17 + and eax,1 + mov brEnabled3D,eax jmp progress_version_ok progress_legacy_version: cmp saveData3D, 33443343h @@ -653,7 +681,15 @@ SaveProgress3D PROC uses ebx mov saveData3D[24], eax mov eax, charmDevilUses mov saveData3D[28], eax - mov eax,charmEnabledMask3D + mov eax,brOwned3D + and eax,1 + shl eax,16 + mov ecx,brEnabled3D + and ecx,brOwned3D + and ecx,1 + shl ecx,17 + or eax,ecx + or eax,charmEnabledMask3D mov saveData3D[32],eax invoke PreviewArtifactSignature3D,OFFSET saveData3D mov saveData3D[36],eax diff --git a/src_graphics/russian_party.inc b/src_graphics/russian_party.inc index 73ae0b5..aae060c 100644 --- a/src_graphics/russian_party.inc +++ b/src_graphics/russian_party.inc @@ -69,6 +69,10 @@ rr_party_cartridge: jb rr_party_cartridge invoke RandomRange3D,6 mov rr3d_chamber,eax + cmp rrPartyReloads3D,0 + je @F + call BackroomsRussianCylinder3D +@@: inc rrPartyReloads3D ret RRLoadCylinder3D ENDP diff --git a/src_graphics/russian_roulette.inc b/src_graphics/russian_roulette.inc index 0573132..ae21496 100644 --- a/src_graphics/russian_roulette.inc +++ b/src_graphics/russian_roulette.inc @@ -98,9 +98,11 @@ StartRussianRound3D PROC uses ebx esi sub bankroll, eax mov rr3d_payout, 0 mov rr3d_error, 0 + mov brRussianLuck3D,-1 call RRLoadCylinder3D invoke RandomRange3D, rrPartySize3D mov rr3d_turn, eax + call BackroomsRussianLuck3D mov rr3d_state, RR_LOAD mov rr3d_timer, 0 invoke PlayCasinoSound3D, OFFSET rrLoadSound3D, 0, 20003h diff --git a/src_graphics/scratch.inc b/src_graphics/scratch.inc index 9b39834..05cf067 100644 --- a/src_graphics/scratch.inc +++ b/src_graphics/scratch.inc @@ -246,6 +246,16 @@ BuyScratch3D PROC uses ebx esi edi jmp scratch_buy_save @@: invoke RandomRange3D,1000 + push eax + call BackroomsLuck3D + pop edx + cmp eax,-1 + je br_scratch_original + imul eax,650 + jmp br_scratch_fixed +br_scratch_original: + mov eax,edx +br_scratch_fixed: cmp eax,600 jb scratch_losing_bag xor esi,esi diff --git a/src_graphics/scratch_portrait.inc b/src_graphics/scratch_portrait.inc index 5c2bf5c..db07d29 100644 --- a/src_graphics/scratch_portrait.inc +++ b/src_graphics/scratch_portrait.inc @@ -321,6 +321,16 @@ portrait_fill: jb portrait_fill mov scrPortraitOutcome3D,0 invoke RandomRange3D,1000 + push eax + call BackroomsLuck3D + pop edx + cmp eax,-1 + je br_portrait_original + imul eax,920 + jmp br_portrait_fixed +br_portrait_original: + mov eax,edx +br_portrait_fixed: cmp eax,600 jb portrait_build_payout mov ebx,eax diff --git a/tools/generators/make_backrooms_assets.py b/tools/generators/make_backrooms_assets.py new file mode 100644 index 0000000..44c5e58 --- /dev/null +++ b/tools/generators/make_backrooms_assets.py @@ -0,0 +1,32 @@ +"""Offline-only texture packing; runtime generation/rendering stays MASM.""" +from pathlib import Path +import struct +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + source = ROOT / 'assets/sprites/backrooms' + frames = [] + for name in ('wall', 'floor', 'ceiling', 'ceiling_lamp'): + image = Image.open(source / f'Backrooms_{name}.jpg').convert('RGBA') + image = image.resize((128, 128), Image.Resampling.LANCZOS) + frames.append(image.tobytes('raw', 'BGRA')) + # Exit is an offline-generated material, not a second game implementation. + image = Image.new('RGBA', (128, 128), (36, 60, 38, 255)) + draw = ImageDraw.Draw(image) + draw.rectangle((9, 4, 119, 127), fill=(61, 84, 55), outline=(143, 160, 98), width=5) + # Eye-level sign stays visible even when standing in the exit cell. + draw.rectangle((18, 45, 110, 73), fill=(32, 170, 77)) + font = ImageFont.truetype('arialbd.ttf', 23) + draw.text((64, 59), 'EXIT', fill='white', font=font, anchor='mm') + draw.rectangle((94, 76, 99, 82), fill=(240, 211, 101)) + frames.append(image.tobytes('raw', 'BGRA')) + out = source / 'materials.bin' + out.write_bytes(struct.pack('<5I', 0x314D4E41, 128, 128, len(frames), len(frames)*65536) + b''.join(frames)) + print(f'Packed {len(frames)} Backrooms materials: {out.relative_to(ROOT)}') + + +if __name__ == '__main__': + main() diff --git a/tools/tests/backrooms_extended_test.inc b/tools/tests/backrooms_extended_test.inc new file mode 100644 index 0000000..eb5f84f --- /dev/null +++ b/tools/tests/backrooms_extended_test.inc @@ -0,0 +1,391 @@ +; Additional native invariants. No player files or runtime writes. +RunBackroomsExtendedTest3D PROC uses ebx esi edi + local rounds:DWORD,wins:DWORD,kind:DWORD,party:DWORD,bullets:DWORD,steps:DWORD + mov brOwned3D,1 + mov brEnabled3D,0 + mov rngSeed3D,87654 + call BackroomsLuck3D + cmp eax,-1 + jne br_extended_fail + cmp rngSeed3D,87654 + jne br_extended_fail + mov brEnabled3D,1 + mov obsActive3D,1 + call BackroomsLuck3D + cmp eax,-1 + jne br_extended_fail + mov obsActive3D,0 + mov brTestStage,111 + ; Actual blackjack party dealing, all guest counts. + mov party,0 +br_ext_bj_party: + mov wins,0 + mov rounds,1000 +br_ext_bj_round: + mov bankroll,1000000 + mov eax,party + mov cbBotCount3D,eax + mov cbState3D,CB_IDLE + call StartNativeBlackjack3D + cmp cbPlayerScore3D,21 + jne @F + inc wins + cmp cbDealerScore3D,19 + jne br_extended_fail +@@: + dec rounds + jnz br_ext_bj_round + cmp wins,860 + jb br_extended_fail + cmp wins,940 + ja br_extended_fail + inc party + cmp party,4 + jb br_ext_bj_party + mov cbBotCount3D,0 + ; Every scratch design, generated by the actual purchase path. + mov scrReady3D,1 + mov brTestStage,112 + mov kind,0 +br_ext_ticket_type: + mov eax,kind + add eax,120 + mov brTestStage,eax + mov wins,0 + mov rounds,1000 +br_ext_ticket: + mov bankroll,1000000 + mov scrState3D,0 + mov scrPriceIndex3D,0 + mov eax,kind + mov scrType3D,eax + call BuyScratch3D + mov eax,scrGross3D + test eax,eax + jz @F + inc wins +@@: + cmp kind,4 + jb @F + call ScratchPortraitPayout3D + cmp eax,scrGross3D + jne br_extended_fail +@@: + dec rounds + jnz br_ext_ticket + cmp wins,860 + jb br_extended_fail + cmp wins,940 + ja br_extended_fail + inc kind + cmp kind,7 + jb br_ext_ticket_type + ; Roulette: a real straight-up bet, including actual multiplier matching. + lea edi,wheelBets3D + mov brTestStage,113 + xor eax,eax + mov ecx,49 + rep stosd + mov wheelBets3D[7*4],100 + mov wheelStake3D,100 + mov wins,0 + mov rounds,1000 +br_ext_wheel: + invoke BackroomsWheelLuck3D,0 + cmp eax,36 + ja br_extended_fail + invoke WheelBetMultiplier3D,7,eax + test eax,eax + jz @F + inc wins +@@: + dec rounds + jnz br_ext_wheel + cmp wins,860 + jb br_extended_fail + cmp wins,940 + ja br_extended_fail + ; Durak: 36 distinct legal cards, correctly exposed trump on favorable deals. + mov rounds,1000 +br_ext_durak: + mov brTestStage,114 + mov bankroll,1000000 + mov dkState3D,DK_IDLE + call StartNativeDurak3D + xor esi,esi +br_ext_durak_card: + mov eax,dkDeck3D[esi*4] + cmp eax,52 + jae br_extended_fail + invoke DurakCardRank,eax + cmp eax,4 + jb br_extended_fail + lea edi,[esi+1] +br_ext_durak_unique: + cmp edi,36 + jae br_ext_durak_next + mov eax,dkDeck3D[edi*4] + cmp eax,dkDeck3D[esi*4] + je br_extended_fail + inc edi + jmp br_ext_durak_unique +br_ext_durak_next: + inc esi + cmp esi,36 + jb br_ext_durak_card + cmp dkDeck3D,33 + jne @F + cmp durakTrumpSuit,2 + jne br_extended_fail +@@: + dec rounds + jnz br_ext_durak + ; Russian roulette: real party elimination/reload rules, all 1..3 guests/bullets. + mov party,1 + mov brTestStage,115 +br_ext_rr_party: + mov bullets,1 +br_ext_rr_bullets: + mov rounds,300 + mov wins,0 +br_ext_rr_round: + mov brTestStage,151 + mov eax,party + mov rrPartyCount3D,eax + call RRPartyReset3D + mov eax,bullets + mov rr3d_bullets,eax + mov rrPartySettled3D,0 + mov rrPartyStake3D,50 + mov rr3d_bet,50 + mov rr3d_turn,0 + call BackroomsRussianLuck3D + mov rrPartyReloads3D,1 + mov steps,0 +br_ext_rr_shot: + mov eax,rr3d_mask + mov ecx,rr3d_chamber + bt eax,ecx + jnc br_ext_rr_advance + call RRPartyHit3D + cmp rrPartySettled3D,0 + jne br_ext_rr_outcome +br_ext_rr_advance: + call RRPartyNext3D + inc steps + cmp steps,100 + jae br_extended_fail + jmp br_ext_rr_shot +br_ext_rr_outcome: + mov brTestStage,152 + cmp rr3d_state,RR_WIN + jne @F + inc wins + cmp brRussianLuck3D,1 + jne br_extended_fail +@@: + cmp brRussianLuck3D,1 + jne @F + mov eax,party + imul eax,10 + add eax,bullets + add eax,200 + mov brTestStage,eax + cmp rr3d_state,RR_WIN + jne br_extended_fail +@@: + dec rounds + jnz br_ext_rr_round + mov brTestStage,154 + cmp wins,240 + jb br_extended_fail + cmp wins,295 + ja br_extended_fail + inc bullets + cmp bullets,4 + jb br_ext_rr_bullets + inc party + cmp party,4 + jb br_ext_rr_party + ; Buckshot permutations retain cartridge counts and valid binary identities. + mov rounds,1000 +br_ext_bs: + mov brTestStage,116 + call BuckshotReload3D + xor edi,edi + xor esi,esi +br_ext_bs_shell: + mov eax,bs3d_shells[esi*4] + cmp eax,1 + ja br_extended_fail + add edi,eax + inc esi + cmp esi,bs3d_shellCount + jb br_ext_bs_shell + cmp edi,bs3d_liveCount + jne br_extended_fail + add edi,bs3d_blankCount + cmp edi,bs3d_shellCount + jne br_extended_fail + dec rounds + jnz br_ext_bs + xor eax,eax + ret +br_extended_fail: + mov eax,brTestStage + ret +RunBackroomsExtendedTest3D ENDP + +RunBackroomsSaveTest3D PROC uses ebx esi edi + ; Signed but impossible enabled-without-owned flag must be rejected. + mov DWORD PTR saveBuffer[304],20002h + invoke ArtifactSaveSignature3D,OFFSET saveBuffer + mov DWORD PTR saveBuffer[308],eax + invoke ValidateArtifactSave3D,OFFSET saveBuffer + test eax,eax + jnz br_save_fail + ; Native preview keeps its 40-byte, signed backward-compatible layout. + invoke CopyCString,OFFSET savePath3D,OFFSET brTestPreviewPath + mov nativeProfileActive3D,0 + mov arrivalConfirmed3D,1 + mov brOwned3D,1 + mov brEnabled3D,1 + call SaveProgress3D + mov brOwned3D,0 + mov brEnabled3D,0 + call LoadProgress3D + cmp brOwned3D,1 + jne br_save_fail + cmp brEnabled3D,1 + jne br_save_fail + mov brActive3D,1 + mov brEnabled3D,0 + invoke LeaveBackrooms3D,1 + cmp brOwned3D,1 + jne br_save_fail + cmp brEnabled3D,0 + jne br_save_fail + call LoadProgress3D + cmp brEnabled3D,0 + jne br_save_fail + xor eax,eax + ret +br_save_fail: + mov eax,102 + ret +RunBackroomsSaveTest3D ENDP + +.data +brContextModes dd MODE_COIN,MODE_DICE,MODE_SLOTS,MODE_BLACKJACK,MODE_ROULETTE,MODE_CLOVER +brContextMap db 1024 dup(0) +.code +RunBackroomsContextTest3D PROC uses ebx esi edi + local index:DWORD,x:DWORD,y:DWORD + call GenerateBackrooms3D + mov brActive3D,1 + mov brCutscene3D,0 + mov hallLevel3D,HALL_BACKROOMS + mov worldMap3D,OFFSET brMap3D + lea esi,brMap3D + lea edi,brContextMap + mov ecx,256 + rep movsd + mov index,0 +br_context_game: + mov esi,index + mov ebx,brGameCells3D[esi*4] + mov eax,ebx + and eax,31 + shl eax,16 + add eax,FP_HALF + mov x,eax + mov playerX,eax + mov eax,ebx + shr eax,5 + shl eax,16 + add eax,FP_HALF + mov y,eax + mov playerY,eax + mov playerAngle,77 + mov engineMode,MODE_WALK + mov mouseLocked3D,0 + mov deltaTime3D,16 + call ClearKeys3D + mov pressedKeys3D[45h],1 + call HandleBackrooms3D + mov esi,index + mov eax,brContextModes[esi*4] + cmp eax,engineMode + jne br_context_fail + cmp eax,MODE_DICE + jne @F + call DiceEscape3D + jmp br_context_returned +@@: + call CardTableEscape3D +br_context_returned: + cmp engineMode,MODE_WALK + jne br_context_fail + mov eax,x + cmp playerX,eax + jne br_context_fail + mov eax,y + cmp playerY,eax + jne br_context_fail + cmp playerAngle,77 + jne br_context_fail + cmp worldMap3D,OFFSET brMap3D + jne br_context_fail + cmp hallLevel3D,HALL_BACKROOMS + jne br_context_fail + lea esi,brMap3D + lea edi,brContextMap + mov ecx,1024 + repe cmpsb + jne br_context_fail + inc index + cmp index,6 + jb br_context_game + mov brActive3D,0 + mov hallLevel3D,0 + mov worldMap3D,OFFSET casinoMap + xor eax,eax + ret +br_context_fail: + mov eax,103 + ret +RunBackroomsContextTest3D ENDP + +RunBackroomsWallTest3D PROC uses esi + mov brActive3D,0 + mov engineMode,MODE_WALK + mov mouseLocked3D,0 + mov playerX,16*FP_1+FP_HALF + mov playerY,27*FP_1+FP_HALF + mov playerAngle,128 + mov deltaTime3D,16 + mov brEntryHold3D,0 + call ClearKeys3D + mov keys3D[57h],1 + mov esi,500 +br_wall_walk: + call HandlePlayerInput + call BackroomsEntry3D + cmp brActive3D,1 + je br_wall_entered + dec esi + jnz br_wall_walk + mov eax,104 + ret +br_wall_entered: + cmp playerY,30*FP_1 + jae br_wall_fail + cmp playerY,30*FP_1-15000 + jb br_wall_fail + call ClearKeys3D + xor eax,eax + ret +br_wall_fail: + mov eax,105 + ret +RunBackroomsWallTest3D ENDP diff --git a/tools/tests/backrooms_test.inc b/tools/tests/backrooms_test.inc new file mode 100644 index 0000000..30d1d82 --- /dev/null +++ b/tools/tests/backrooms_test.inc @@ -0,0 +1,288 @@ +.data +brTestPath db 'build\scratch\backrooms-native.bin',0 +brTestFrame db 'build\scratch\backrooms-frame.bin',0 +brTestExitFrame db 'build\scratch\backrooms-exit.bin',0 +brTestIo dd 0 +brTestSeeds dd 256 +brTestStage dd 91 +brTestRows dd 0 +brTestLuckPath db 'build\scratch\backrooms-luck.bin',0 +brTestProfileDir db 'build\scratch',0 +brTestProfileName db 'backrooms-profile.dat',0 +brTestProfilePath db 'build\scratch\backrooms-profile.dat',0 +brTestPreviewPath db 'build\scratch\br-preview.dat',0 +brTestFacesPath db 'build\scratch\backrooms-exit-faces.bin',0 +brTestFaceNeighbors dd -32,32,-1,1 +brTestFaceAngles dd 384,128,256,0 +.code +include tools/tests/backrooms_extended_test.inc +RunBackroomsTest3D PROC uses ebx esi edi + invoke CreateFileA,OFFSET brTestPath,40000000h,0,0,2,80h,0 + mov ebx,eax + cmp eax,-1 + je br_test_fail + mov rngSeed3D,12345 +br_test_seed: + call GenerateBackrooms3D + invoke WriteFile,ebx,OFFSET brMap3D,1024,OFFSET brTestIo,0 + invoke WriteFile,ebx,OFFSET brExitCell3D,4,OFFSET brTestIo,0 + invoke WriteFile,ebx,OFFSET brGameCells3D,24,OFFSET brTestIo,0 + dec brTestSeeds + jnz br_test_seed + invoke CloseHandle,ebx + mov brTestStage,92 + mov obsActive3D,0 + mov tournamentMode,0 + mov brActive3D,0 + mov engineMode,MODE_WALK + mov hallLevel3D,0 + mov playerX,16*FP_1+FP_HALF + mov playerY,30*FP_1-11000 + mov playerAngle,128 + mov keys3D[57h],1 + mov deltaTime3D,50 + mov esi,39 +br_test_entry: + call BackroomsEntry3D + cmp brActive3D,0 + jne br_test_fail + dec esi + jnz br_test_entry + cmp brEntryHold3D,1950 + jne br_test_fail + mov brTestStage,93 + mov playerAngle,129 + call BackroomsEntry3D + cmp brEntryHold3D,0 + jne br_test_fail + mov playerAngle,128 + call BackroomsEntry3D + cmp brEntryHold3D,50 + jne br_test_fail + mov keys3D[57h],0 + call BackroomsEntry3D + cmp brEntryHold3D,0 + jne br_test_fail + mov brTestStage,94 + mov keys3D[57h],1 + mov brEntryHold3D,1950 + mov brEntryAngle3D,128 + call BackroomsEntry3D + cmp brActive3D,1 + jne br_test_fail + cmp brCutscene3D,1 + jne br_test_fail + mov brClock3D,900 + call HandleBackrooms3D + mov brTestStage,95 + invoke CanWalk3D,FP_1+FP_HALF,FP_1+FP_HALF + cmp eax,1 + jne br_test_fail + invoke CanWalk3D,FP_HALF,FP_HALF + test eax,eax + jnz br_test_fail + mov brTestStage,96 + ; Set up standard projection LUT through the real hall initializer first. + call InitHall3D + call RunBackroomsWallTest3D + test eax,eax + jnz br_test_return + mov brActive3D,1 + mov hallLevel3D,HALL_BACKROOMS + mov worldMap3D,OFFSET brMap3D + mov floorTexture3D,BR_FLOOR + mov viewHorizon3D,HALF_H + mov brClock3D,8100 + mov brDialogue3D,2500 + mov brCutscene3D,0 + mov playerX,FP_1+FP_HALF + mov playerY,FP_1+FP_HALF + mov playerAngle,64 + call RenderBackrooms3D + invoke CreateFileA,OFFSET brTestFrame,40000000h,0,0,2,80h,0 + mov ebx,eax + invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0 + invoke CloseHandle,ebx + ; Door capture from its reachable exit cell, facing a neighboring wall. + mov ebx,brExitCell3D + mov eax,ebx + and eax,31 + shl eax,16 + add eax,FP_HALF + mov playerX,eax + mov eax,ebx + shr eax,5 + shl eax,16 + add eax,FP_HALF + mov playerY,eax + mov playerAngle,384 + cmp brMap3D[ebx-32],0 + jne br_test_exit_camera + mov playerAngle,128 + cmp brMap3D[ebx+32],0 + jne br_test_exit_camera + mov playerAngle,256 + cmp brMap3D[ebx-1],0 + jne br_test_exit_camera + mov playerAngle,0 +br_test_exit_camera: + mov brDialogue3D,10000 + call RenderBackrooms3D + invoke CreateFileA,OFFSET brTestExitFrame,40000000h,0,0,2,80h,0 + mov ebx,eax + invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0 + invoke CloseHandle,ebx + mov arrivalConfirmed3D,0 + call RunBackroomsDoorFrames3D + invoke LeaveBackrooms3D,1 + cmp brOwned3D,1 + jne br_test_fail + cmp brActive3D,0 + jne br_test_fail + cmp hallLevel3D,0 + jne br_test_fail + mov brTestStage,97 + call RunBackroomsContextTest3D + test eax,eax + jnz br_test_return + mov brActive3D,1 + call BackroomsLuck3D + cmp eax,-1 + jne br_test_fail + mov brActive3D,0 + mov tournamentMode,1 + call BackroomsLuck3D + cmp eax,-1 + jne br_test_fail + mov tournamentMode,0 + xor edi,edi + mov esi,100000 +br_test_luck: + call BackroomsLuck3D + add edi,eax + dec esi + jnz br_test_luck + cmp edi,89000 + jb br_test_fail + cmp edi,91000 + ja br_test_fail + call RunBackroomsExtendedTest3D + test eax,eax + jnz br_test_return + mov brTestStage,98 + call LoadNativeCoinArt3D + call InitNativeSlots3D + call InitNativeClover3D + call InitNativeBlackjack3D + call InitNativePoker3D + invoke CreateFileA,OFFSET brTestLuckPath,40000000h,0,0,2,80h,0 + mov ebx,eax + mov brTestRows,1000 +br_test_rounds: + mov bankroll,1000000 + mov coin3d_choice,1 + mov coin3d_state,COIN_IDLE + mov pressedKeys3D[20h],1 + call HandleNativeCoin3D + invoke WriteFile,ebx,OFFSET coin3d_result,4,OFFSET brTestIo,0 + mov dice3d_state,0 + mov dice3d_choice,1 + mov pressedKeys3D[20h],1 + call HandleDiceRules3D + invoke WriteFile,ebx,OFFSET dice3d_val1,8,OFFSET brTestIo,0 + mov slotSpinState,SLOT_READY + call StartNativeSlots3D + invoke WriteFile,ebx,OFFSET slotReel1,12,OFFSET brTestIo,0 + mov clpState3D,SLOT_READY + call StartNativeClover3D + call CloverEvaluate3D + invoke WriteFile,ebx,OFFSET clpTotal3D,4,OFFSET brTestIo,0 + mov cbBotCount3D,0 + mov cbState3D,CB_IDLE + call StartNativeBlackjack3D + invoke WriteFile,ebx,OFFSET cbPlayerScore3D,4,OFFSET brTestIo,0 + invoke WriteFile,ebx,OFFSET cbDeck3D,208,OFFSET brTestIo,0 + mov pkState3D,PK_IDLE + call StartNativePoker3D + invoke WriteFile,ebx,OFFSET pkDeck3D,208,OFFSET brTestIo,0 + invoke WriteFile,ebx,OFFSET pkButton3D,4,OFFSET brTestIo,0 + dec brTestRows + jnz br_test_rounds + invoke CloseHandle,ebx + mov brTestStage,99 + mov nativeProfilesDirPtr3D,OFFSET brTestProfileDir + invoke CopyCString,OFFSET saveFileName,OFFSET brTestProfileName + mov nativeProfileActive3D,1 + mov brOwned3D,1 + mov brEnabled3D,0 + call SaveGame + mov brOwned3D,0 + mov brEnabled3D,1 + call LoadSave + cmp brOwned3D,1 + jne br_test_fail + cmp brEnabled3D,0 + jne br_test_fail + invoke ValidateGraphicProfile3D,OFFSET brTestProfilePath + cmp eax,1 + jne br_test_fail + call RunBackroomsSaveTest3D + test eax,eax + jnz br_test_return + xor eax,eax + ret +br_test_fail: + mov eax,brTestStage +br_test_return: + ret +RunBackroomsTest3D ENDP + +; Identical door/camera distance on every face, VHS off/on pairs. +RunBackroomsDoorFrames3D PROC uses ebx esi edi + local face:DWORD,effect:DWORD + invoke CreateFileA,OFFSET brTestFacesPath,40000000h,0,0,2,80h,0 + mov ebx,eax + mov face,0 +br_test_door_face: + lea edi,brMap3D + xor eax,eax + mov ecx,256 + rep stosd + ; Keep all rays inside the fixture map, including peripheral FOV rays. + xor esi,esi +br_test_door_border: + mov brMap3D[esi],BR_WALL + mov brMap3D[esi+31*32],BR_WALL + mov eax,esi + shl eax,5 + mov brMap3D[eax],BR_WALL + mov brMap3D[eax+31],BR_WALL + inc esi + cmp esi,32 + jb br_test_door_border + mov brExitCell3D,16*32+16 + mov esi,face + mov eax,brTestFaceNeighbors[esi*4] + mov brMap3D[eax+16*32+16],BR_WALL + mov playerX,16*FP_1+FP_HALF + mov playerY,16*FP_1+FP_HALF + mov eax,brTestFaceAngles[esi*4] + mov playerAngle,eax + mov viewEye3D,HALL_STANDING_EYE + mov viewHorizon3D,HALF_H + mov brDialogue3D,10000 + mov effect,0 +br_test_door_effect: + mov eax,effect + mov brVhs3D,eax + call RenderBackrooms3D + invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET brTestIo,0 + inc effect + cmp effect,2 + jb br_test_door_effect + inc face + cmp face,4 + jb br_test_door_face + invoke CloseHandle,ebx + ret +RunBackroomsDoorFrames3D ENDP diff --git a/tools/tests/check_backrooms.py b/tools/tests/check_backrooms.py new file mode 100644 index 0000000..7ae7d0e --- /dev/null +++ b/tools/tests/check_backrooms.py @@ -0,0 +1,128 @@ +"""Execute shipping MASM maze/entry/collision/render, inspect native output.""" +from collections import deque +from pathlib import Path +import struct +import subprocess +from PIL import Image +from check_artifacts import toolchain + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / 'build/scratch' + + +def check_exit_faces(): + raw = (OUT/'backrooms-exit-faces.bin').read_bytes() + assert len(raw) == 8*640*480*4 + frames = [Image.frombytes('RGBA', (640, 480), raw[i:i+640*480*4], + 'raw', 'BGRA').convert('RGB') + for i in range(0, len(raw), 640*480*4)] + # Source glyph at runtime's exact nearest-neighbor UV scale. Compare both + # orientations rather than merely counting green pixels (mirrors pass that). + pack = (ROOT/'assets/sprites/backrooms/materials.bin').read_bytes() + art = Image.frombytes('RGBA', (128, 128), pack[20+4*65536:], 'raw', 'BGRA') + glyph = [(x, y) for y in range(48, 70) for x in range(27, 101) + if min(art.getpixel((x, y))[:3]) > 240] + for face, name in enumerate(('north', 'south', 'west', 'east')): + clean, vhs = frames[2*face:2*face+2] + clean.save(OUT/f'backrooms-exit-{name}.png') + vhs.save(OUT/f'backrooms-exit-{name}-vhs.png') + white = [(x, y) for y in range(0, 300) for x in range(60, 580) + if min(clean.getpixel((x, y))) > 240] + assert white, f'{name}: EXIT missing' + x0, x1 = min(x for x, y in white), max(x for x, y in white) + y0, y1 = min(y for x, y in white), max(y for x, y in white) + gx0, gx1 = min(x for x, y in glyph), max(x for x, y in glyph) + gy0, gy1 = min(y for x, y in glyph), max(y for x, y in glyph) + def score(mirror): + hits = 0 + for x, y in glyph: + u = (x-gx0)/(gx1-gx0) + if mirror: + u = 1-u + px = round(x0+u*(x1-x0)) + py = round(y0+(y-gy0)/(gy1-gy0)*(y1-y0)) + hits += min(clean.getpixel((px, py))) > 240 + return hits/len(glyph) + normal, mirrored = score(False), score(True) + assert normal > .85 and normal > mirrored+.12, (name, normal, mirrored) + # White glyphs may darken on scanlines, never wrap into black/color noise. + corrupt = sum(min(vhs.getpixel(p)) < 180 or + max(vhs.getpixel(p))-min(vhs.getpixel(p)) > 12 for p in white) + assert corrupt == 0, (name, 'VHS corrupt white pixels', corrupt, len(white)) + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + ml, link = toolchain() + sdk = Path('C:/Program Files (x86)/Windows Kits/10/Lib') + lib = next(p/'um/x86' for p in sorted(sdk.iterdir(), reverse=True) + if (p/'um/x86/kernel32.lib').exists()) + for args in ([ml, '/c', '/coff', '/Cp', '/nologo', '/I.', '/DBACKROOMS_TEST=1', + '/Fobuild/scratch/backrooms.obj', 'poker.asm'], + [link, '/nologo', '/SUBSYSTEM:CONSOLE', '/LIBPATH:'+str(lib), + '/OUT:build/scratch/backrooms-test.exe', 'build/scratch/backrooms.obj', + 'kernel32.lib', 'winmm.lib', 'user32.lib', 'ws2_32.lib', 'gdi32.lib']): + subprocess.run(list(map(str, args)), cwd=ROOT, check=True, timeout=120) + subprocess.run([str(OUT/'backrooms-test.exe')], cwd=ROOT, check=True, timeout=90) + raw = (OUT/'backrooms-native.bin').read_bytes() + assert len(raw) == 256 * 1052 + fingerprints = set() + minimum = 10000 + for offset in range(0, len(raw), 1052): + grid = raw[offset:offset+1024] + exit_cell, *games = struct.unpack_from('<7I', raw, offset+1024) + assert all(grid[x] for x in range(32)) + assert all(grid[31*32+x] for x in range(32)) + assert all(grid[y*32] and grid[y*32+31] for y in range(32)) + distance, queue = {33: 0}, deque([33]) + while queue: + cell = queue.popleft() + for neighbor in (cell-32, cell+1, cell+32, cell-1): + if grid[neighbor] == 0 and neighbor not in distance: + distance[neighbor] = distance[cell]+1 + queue.append(neighbor) + assert len(distance) == grid.count(0), 'Disconnected open component' + assert distance[exit_cell] == max(distance.values()) + assert len(set(games)) == 6 and exit_cell not in games and 33 not in games + assert all(cell in distance for cell in games) + minimum = min(minimum, distance[exit_cell]) + fingerprints.add(grid) + assert len(fingerprints) == 256 + assert minimum >= 40, minimum + luck = (OUT/'backrooms-luck.bin').read_bytes() + assert len(luck) == 1000*452 + wins = dict.fromkeys(('coin', 'dice', '777', 'clover', 'blackjack', 'poker'), 0) + for row in struct.iter_unpack('<113I', luck): + coin, d1, d2, s1, s2, s3, clover, bj = row[:8] + assert coin in (1, 2) and 1 <= d1 <= 6 and 1 <= d2 <= 6 + wins['coin'] += coin == 1 + wins['dice'] += (d1+d2) % 2 == 0 + assert (s1, s2, s3) in ((1, 1, 1), (2, 5, 3)) + wins['777'] += s1 == s2 == s3 + assert clover in (0, 78) + wins['clover'] += clover > 0 + deck, poker, button = row[8:60], row[60:112], row[112] + assert sorted(deck) == sorted(poker) == list(range(52)), 'Duplicate/missing card' + assert bj in (19, 21) + assert bj == (21 if deck[0] == 12 else 19) + wins['blackjack'] += bj == 21 + # Actual non-button-first deal order, plus actual burn/flop/turn/river offsets. + seat = button ^ 1 + hand = [poker[seat], poker[seat+2], *(poker[i] for i in (5, 6, 7, 9, 11))] + wins['poker'] += set(range(8, 13)).issubset(hand) + assert all(860 <= n <= 940 for n in wins.values()), wins + image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-frame.bin').read_bytes(), 'raw', 'BGRA').convert('RGB') + image.save(OUT/'backrooms-frame.png') + assert len(image.getcolors(640*480)) > 100 + image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-exit.bin').read_bytes(), 'raw', 'BGRA').convert('RGB') + image.save(OUT/'backrooms-exit.png') + assert sum(g > 1.4*r and g > 1.2*b for r, g, b in image.get_flattened_data()) > 100, 'Exit not visibly green' + check_exit_faces() + print('PASS: readable non-mirrored EXIT on all four wall faces; VHS preserves white glyphs') + print(f'PASS: 256 native seeds; sealed connected mazes; farthest exit >= {minimum} steps; unique reachable games; entry hold/reset; collision; isolation; profile roundtrip; render') + print(f'PASS: 1000 production rounds per game, valid unique decks, wins: {wins}') + print('PASS: all blackjack party sizes; 7 scratch designs; roulette; Durak conservation/trump; 9 Russian party/bullet setups; Buckshot counts; preview saves; repeat reward') + + +if __name__ == '__main__': + main()