feat: add native hell expedition

Add a painting entrance, Freedoom combat and Doom-style HUD.

Persist the one-time clear reward in signed saves without changing file sizes. Include native regression checks and synchronized game/launcher builds.
This commit is contained in:
Игорь Федосеев 2026-11-01 22:29:09 +03:00
parent 7e4f7d7be1
commit ce5121debd
54 changed files with 3229 additions and 4 deletions

View file

@ -23,6 +23,14 @@ Running setup again verifies existing weights without downloading them again.
Complete offline release ZIPs already contain the model and need no download.
See [Fortune AI](docs/FORTUNE_AI.md) for details and hardware notes.
## Hell Expedition
The large hell painting opposite the first-floor bar opens an assembly-native FPS expedition:
Freedoom enemies and weapons, a blue-key gate, two secrets, lava and a final
boss, accompanied by the existing Mick Gordon soundtrack. First clear awards
$6,660; death/retry preserves the casino bank. See
[controls, assets and verification](docs/HELL_EXPEDITION.md).
## Native Entrance And Character Scenes
The graphical startup now fades from Eblan Games into a first-person opening:

30
assets/hell/COPYING.txt Normal file
View file

@ -0,0 +1,30 @@
Copyright © 2001-2024
Contributors to the Freedoom project. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Freedoom project nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS
IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
For a list of contributors to the Freedoom project, see the file
CREDITS.

205
assets/hell/CREDITS.txt Normal file
View file

@ -0,0 +1,205 @@
--Phase 1--
Title: "Fight for Freedom" by Korp, based on "Fountain Dance" by Tyler "Picklehammer" Pantella
Intermission: "YOU LIVE!!!!" by Stratos
Victory: "Distance" by Korp
Bunny: "Impactean Welcome Party" by Goji
Episode 1 - Outpost Outbreak
E1M1: "Stanky Leg Specialist" by Lola "BlueWorrior" Harvey
E1M2: "Look At Me I'm Underhalls Too" by Mark "TheMisterCat" McGill-Smith
E1M3: "Alone with Colors" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins
E1M9: "Hidden Between Spades" by Korp, remix of dave3d42.mid by jute
E1M4: "Flood the City" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins
E1M5: "The Raging Sun" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins, modified by Goji
E1M6: "Oxidine" by Korp, remix of dave3d35.mid by jute
E1M7: "Totally Surrounded" by Josephus "DH4050" Astartes
E1M8: "On the Next Day" by Kevin "Velvetic" Martins
Episode 2 - Military Labs
E2M1: "Moonstone" by Korp
E2M2: "Voidgaze" by Korp
E2M3: "Workout" by Korp
E2M4: "Diodine" by Korp
E2M5: "Reminiscence" by Korp
E2M9: "Nightstorm" by Korp
E2M6: "Concept of Technology" by Korp
E2M7: "Ribcage" by Korp
E2M8: "Treble Dissension" by Korp
Episode 3 - Event Horizon
E3M1: "Intrare In Infernum" by Lola "BlueWorrior" Harvey
E3M2: "The Long Road" by Lola "BlueWorrior" Harvey
E3M3: "Wrought Havoc" by Lola "BlueWorrior" Harvey
E3M4: "Cautious Progress" by Lola "BlueWorrior" Harvey
E3M5: "Scorned by Flesh" by Lola "BlueWorrior" Harvey
E3M6: "Soul Banisher" by Lola "BlueWorrior" Harvey
E3M9: "No-Clip to the End" by Lola "BlueWorrior" Harvey
E3M7: "Ultamatum Infurnus" by Lola "BlueWorrior" Harvey
E3M8: "The Zenith" by Lola "BlueWorrior" Harvey
Episode 4 - Double Impact
E4M1: "Liberate the Stars" by continuum.mid
E4M2: "Fight Harder... To Obtain a Better Victory!" by Proxy-MIDI
E4M9: "The Deepest Secrets" by continuum.mid
E4M3: "Space Spectre" by Matzu
E4M4: "Deliberate Concealment" by continuum.mid
E4M5: "Main Code" by Korp
E4M6: "Andromeda" by ZeMystic
E4M7: "Ambiguity" by Korp
E4M8: "Vs. The Assault Quindecapod" by continuum.mid
--Phase 2--
Title: "Horizon's Call" by Korp
Intermission: "FTW You 100" by Lola "BlueWorrior" Harvey
Story: dave3d07.mid by jute
Cluster 1
MAP01: "Not My First Rodeo..." by Lola "BlueWorrior" Harvey
MAP02: "Dark Underworld" by Lola "BlueWorrior" Harvey
MAP03: "Demon Dance" by Discoholic
MAP04: "Indigo" by Korp
MAP05: "Under Maintenance" by Lola "BlueWorrior" Harvey
MAP06: "Refrigeration" by Korp, based on "Dark Trail" by Tyler "Picklehammer" Pantella
MAP07: "Fun is Infinite at AGM" by continuum.mid
MAP08: "Syntax Error" by Lola "BlueWorrior" Harvey
MAP09: "Terrible Secrets" by Lola "BlueWorrior" Harvey
MAP10: "Soviet Porno" by Jeremy "Vandalorian" Emerson
MAP11: "Undergrowth" by Korp, remix of dave3d01.mid by jute
Cluster 2
MAP12: "Sewers" by Discoholic, remix of "Wasteland Scavengers" by Samuel "Blastfrog" Oliver
MAP13: "Fear" by Discoholic
MAP14: "About the Summer" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins
MAP15: "Backpack of Shells" by Lola "BlueWorrior" Harvey
MAP31: "The Machine" by Lola "BlueWorrior" Harvey
MAP32: "Black Mountain" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins
MAP16: "Reflection" by Scragadelic
MAP17: "Mow Them Down!" by Lola "BlueWorrior" Harvey
MAP18: "Realistic Approach" by Kevin "Velvetic" Martins
MAP19: "Deceit" by Julian "Julian Hope" Aubourg, modified by m
MAP20: "Beasts of Horizon" by Josephus "DH4050" Astartes
Cluster 3
MAP21: "Rough Landing" by Korp, remix of dave3d08.mid by jute
MAP22: "Alien Waltz" by Korp
MAP23: "Within the Trilo's Den" by MoonDeLaAxel and Korp
MAP24: "Space's Despair" by Korp, remix of "Balancing Act" by Andrew Bassett
MAP25: "I Sought Wisdom in the Chalice But There Was None" by Jeremy "Vandalorian" Emerson
MAP26: "Stormer" by Korp, originally by Samuel "Blastfrog" Oliver
MAP27: "Dreamcatcher (Black and Blue)" by Kevin "Velvetic" Martins & Jonathan "julnen" Martins
MAP28: "Skull Underfoot" by Lola "BlueWorrior" Harvey
MAP29: "Mists Over the Mire" by Tyler "Picklehammer" Pantella
MAP30: "Devoured by the Jaws of Defeat" by Korp
--FreeDM--
Title: "My Home, The Colosseum" by Goji
Intermission: "Burn the Bodies" by Tyler "Picklehammer" Pantella
Story: "Geoff's Vinyl Collection" by MoonDeLaAxel, remix of "Why?" by Joshua "wastedjamacan" Masek
DM01: "Zero Fort" by Ralph "Ralphis" Vickers
DM02: "Descent from Grace" by Lola "BlueWorrior" Harvey
DM03: "Dynamite!" by Korp
DM04: "Pit" by Anthony "Ajanddino" Pierce
DM05: "Music for Freedoom 9" by Jeremy "Vandalorian" Emerson
DM06: "Tunnel" by Anthony "Ajanddino" Pierce
DM07: "I Fell Off The Stairs" by Korp
DM08: "Jump Is Not Allowed!" by Korp
DM09: "Method to My Madness" by Matias "jupiter_ex" Roldan
DM10: "Paranoia" by Tyler "Picklehammer" Pantella
DM11: "Death's Embrace" by Tyler "Picklehammer" Pantella
DM12: "Parallax" by Korp
DM13: "Shut It" by Korp
DM14: "The Heroic Battle" by Tyler "Picklehammer" Pantella
DM15: "Neat" by Ralph "Ralphis" Vickers
DM16: "Deathmatch Construct" by Korp
DM17: "Panic" by Anthony "Ajanddino" Pierce
DM18: "Metadata" by Korp
DM19: "Tap Water" by Korp, remix of dave3d27.mid by jute
DM20: "Quirkspitter" by Korp, remix of dave3d16.mid by jute
DM21: "Friendly Fire" by Korp
DM22: "Red Foot" by Kevin "Velvetic" Martins and Simone Alauk
DM23: "Shells" by Ospaggi
DM24: "Constructive Criticism" by Matias "jupiter_ex" Roldan
DM25: "Shoot 'em All" by rostuhan
DM26: "World of Pain" by Josephus "DH4050" Astartes
DM27: "White Lights" by Kevin "Velvetic" Martins and Jonathan "julnen" Martins
DM28: "Bobby Sent Me" by Ralph "Ralphis" Vickers
DM29: "SevenFour" by Ralph "Ralphis" Vickers
DM30: "Freedom for Your Shooting Skills" by rostuhan
DM31: "The Syntax of Time" by Kevin "Velvetic" Martins
DM32: "Bullet Hell" by rostuhan
--Previously Featured in 0.12.1--
Phase 1 Title: "Fountain Dance" by Tyler "Picklehammer" Pantella
Phase 1 OPL Title: "I Hate Fredrik Johansson" by Toby "Tobester" Collins Jr. (moved to FreeDM Title, not removed)
Phase 1 Intermission: "Music for Freedoom 10" by Jeremy "Vandalorian" Emerson
Victory: "Ominous Theme" by Jared "BlackJar72" Blackburn
Bunny: "Look At The Bunnies" by RestlessRodent
E1M1: e1m1.mid by Ralph "Ralphis" Vickers
E1M3: "Mortal" by Jeremy "Vandalorian" Emerson
E1M5: "sit on a bench 'n release that tensh'n" by Jazz "jmickle" Mickle
E1M6: dave3d35.mid by jute
E1M7: "Sewers" by Discoholic, remix of "Wasteland Scavengers" by Samuel "Blastfrog" Oliver (moved to MAP12, not removed)
E1M9: "The Raging Sun" by Kevin "Velvetic" Martins and Jonathan "julnen" Martins (moved to E1M5, not removed)
E2M1: dave3d38.mid by jute
E2M2: "Animal Skin" by Kevin "Velvetic" Martins and Jonathan "julnen" Martins
E2M3: "Alone with Colors" by Kevin "Velvetic" Martins and Jonathan "julnen" Martins (moved to E1M3, not removed)
E2M4: "Let 'em Burn" by Kevin "Velvetic" Martins
E2M5: "Music for Freedoom 1" by Jeremy "Vandalorian" Emerson
E2M6: "sit on a bench 'n release that tensh'n" by Jazz "jmickle" Mickle
E2M7: "Music for Freedoom 3" by Jeremy "Vandalorian" Emerson
E2M8: "Pit" by Anthony "Ajanddino" Pierce (moved to DM04, not removed)
E2M9: dave3d34.mid by jute
E3M9: "Lipstick" by Jeremy "Vandalorian" Emerson
Phase 2 + FreeDM Title: "Warpath" by Tyler "Picklehammer" Pantella
Phase 2 Intermission: "Burn the Bodies" by Tyler "Picklehammer" Pantella
MAP04: "Soviet Porno" by Jeremy "Vandalorian" Emerson (moved to MAP10, not removed)
MAP06: "Dark Trail" by Tyler "Picklehammer" Pantella
MAP07: "Riders of Hell" by Tyler "Picklehammer" Pantella
MAP10: "Death's Embrace" by Tyler "Picklehammer" Pantella (moved to DM11, not removed)
MAP11: dave3d02.mid by jute
MAP12: Doom2Alt.mid by Bradley "BobFromReboot" Lavigne
MAP20: dave3d05.mid by jute
MAP21: "Violence Culture" by Jeremy "Vandalorian" Emerson
MAP22: "Mists Over the Mire" by Tyler "Picklehammer" Pantella (moved to MAP29, not removed)
MAP23: dave3d09.mid by jute
MAP24: "Balancing Act" by Andrew Bassett
MAP26: "Crawl" by Jeremy "Vandalorian" Emerson
MAP29: freedoom.mid by Varis Alpha
MAP30: "Paranoia" by Tyler "Picklehammer" Pantella (moved to DM10, not removed)
DM03: "Music for Freedoom 2" by Jeremy "Vandalorian" Emerson
DM04: "Rush" by Kevin "Velvetic" Martins and Simone Alauk
DM05: map01.mid by Ralph "Ralphis" Vickers
DM06: "The Heroic Battle" by Tyler "Picklehammer" Pantella (moved to DM14, not removed)
DM07: dave3d30.mid by jute
DM08: Dave3d13.mid by jute
DM10: Doom1.mid by Bradley "BobFromReboot" Lavigne
DM11: "Spamhambled In Space" by Mark "TheMisterCat" McGill-Smith
DM12: dave3d24.mid by jute
DM13: e1m1.mid by Ralph "Ralphis" Vickers
DM14: "Munt Battle" by Mark "TheMisterCat" McGill-Smith
DM16: dave3d02.mid by jute
DM17: d_stlks3.mid by Vincent "Vicious" Fong
DM18: dave3d06.mid by jute
DM19: dave3d27.mid by jute
DM20: dave3d16.mid by jute
DM21: dave3d12.mid by jute
DM23: dave3d17.mid by jute
DM25: dave3d20.mid by jute
DM26: dave3d09.mid by jute
DM27: "Music for Freedoom 7" by Jeremy "Vandalorian" Emerson
DM30: dave3d01.mid by jute
DM32: "Munt Battle" by Mark "TheMisterCat" McGill-Smith

BIN
assets/hell/boss.bin Normal file

Binary file not shown.

BIN
assets/hell/demon.bin Normal file

Binary file not shown.

BIN
assets/hell/digits.bin Normal file

Binary file not shown.

BIN
assets/hell/dsdoropn.wav Normal file

Binary file not shown.

BIN
assets/hell/dsfirsht.wav Normal file

Binary file not shown.

BIN
assets/hell/dsitemup.wav Normal file

Binary file not shown.

BIN
assets/hell/dspistol.wav Normal file

Binary file not shown.

BIN
assets/hell/dsplpain.wav Normal file

Binary file not shown.

BIN
assets/hell/dspodth1.wav Normal file

Binary file not shown.

BIN
assets/hell/dspopain.wav Normal file

Binary file not shown.

BIN
assets/hell/dssgcock.wav Normal file

Binary file not shown.

BIN
assets/hell/dsshotgn.wav Normal file

Binary file not shown.

BIN
assets/hell/dstelept.wav Normal file

Binary file not shown.

BIN
assets/hell/faces.bin Normal file

Binary file not shown.

BIN
assets/hell/fire.bin Normal file

Binary file not shown.

BIN
assets/hell/hud.bin Normal file

Binary file not shown.

BIN
assets/hell/imp.bin Normal file

Binary file not shown.

BIN
assets/hell/impacts.bin Normal file

Binary file not shown.

BIN
assets/hell/items.bin Normal file

Binary file not shown.

169
assets/hell/manifest.json Normal file
View file

@ -0,0 +1,169 @@
{
"version": "0.13.0",
"url": "https://github.com/freedoom/freedoom/releases/download/v0.13.0/freedoom-0.13.0.zip",
"archive_sha256": "3f9b264f3e3ce503b4fb7f6bdcb1f419d93c7b546f4df3e874dd878db9688f59",
"packs": {
"soldier": [
"POSSA1",
"POSSB1",
"POSSC1",
"POSSD1",
"POSSE1",
"POSSF1",
"POSSG1",
"POSSH0",
"POSSI0",
"POSSJ0",
"POSSK0",
"POSSL0"
],
"demon": [
"SARGA1",
"SARGB1",
"SARGC1",
"SARGD1",
"SARGE1",
"SARGF1",
"SARGH1",
"SARGI0",
"SARGJ0",
"SARGK0",
"SARGL0",
"SARGN0"
],
"imp": [
"TROOA1",
"TROOB1",
"TROOC1",
"TROOD1",
"TROOE1",
"TROOF1",
"TROOH1",
"TROOI0",
"TROOJ0",
"TROOK0",
"TROOL0",
"TROOM0"
],
"boss": [
"BOSSA1",
"BOSSB1",
"BOSSC1",
"BOSSD1",
"BOSSE1",
"BOSSF1",
"BOSSH1",
"BOSSI0",
"BOSSJ0",
"BOSSK0",
"BOSSL0",
"BOSSO0"
],
"pistol": [
"PISGA0",
"PISGB0",
"PISGC0",
"PISGD0",
"PISGE0"
],
"shotgun": [
"SHTGA0",
"SHTGB0",
"SHTGC0",
"SHTGD0"
],
"fire": [
"BAL1A0",
"BAL1B0",
"BAL1C0",
"BAL1D0",
"BAL1E0"
],
"portal": [
"TFOGA0",
"TFOGB0",
"TFOGC0",
"TFOGD0",
"TFOGE0",
"TFOGF0",
"TFOGG0",
"TFOGH0"
],
"items": [
"MEDIA0",
"ARM1A0",
"CLIPA0",
"SHELA0",
"SHOTA0",
"BKEYA0",
"BON2A0"
],
"materials": [
"MARBLE1",
"GSTONE1",
"SKIN2",
"BIGDOOR2",
"SW1GARG",
"SP_HOT1",
"FLOOR7_2",
"CEIL5_2",
"LAVA1",
"LAVA2",
"LAVA3",
"LAVA4"
],
"hud": [
"STBAR"
],
"digits": [
"STTNUM0",
"STTNUM1",
"STTNUM2",
"STTNUM3",
"STTNUM4",
"STTNUM5",
"STTNUM6",
"STTNUM7",
"STTNUM8",
"STTNUM9",
"STTPRCNT"
],
"faces": [
"STFST00",
"STFST01",
"STFST02",
"STFOUCH0",
"STFST10",
"STFST11",
"STFST12",
"STFOUCH1",
"STFST20",
"STFST21",
"STFST22",
"STFOUCH2",
"STFST30",
"STFST31",
"STFST32",
"STFOUCH3",
"STFST40",
"STFST41",
"STFST42",
"STFOUCH4",
"STFDEAD0"
],
"impacts": [
"PUFFA0",
"PUFFB0",
"PUFFC0",
"PUFFD0"
],
"painting": [
"SKY3",
"LAVA1",
"GSTONE1",
"BOSSA1",
"TROOA1",
"SARGA1"
]
}
}

BIN
assets/hell/materials.bin Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/hell/painting.bin Normal file

Binary file not shown.

BIN
assets/hell/pistol.bin Normal file

Binary file not shown.

BIN
assets/hell/portal.bin Normal file

Binary file not shown.

BIN
assets/hell/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/hell/shotgun.bin Normal file

Binary file not shown.

BIN
assets/hell/soldier.bin Normal file

Binary file not shown.

Binary file not shown.

View file

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

Binary file not shown.

View file

@ -175,6 +175,7 @@ include src_graphics\backrooms_data.inc
include src_graphics\observer_data.inc
include src_graphics\darts_data.inc
include src_graphics\jackbox_data.inc
include src_graphics\hell_fps_data.inc
.data
ALIGN 4
@ -472,7 +473,7 @@ screenBuffer dd SCREEN_SIZE dup(?)
ALIGN 4
zBuffer dd SCREEN_W dup(?)
ALIGN 4
texturesBuffer dd (185 * 128 * 128) dup(?)
texturesBuffer dd (197 * 128 * 128) dup(?)
hudNumBuffer db 32 dup(?)
@ -4496,6 +4497,8 @@ unlock_done:
UnlockMouse3D ENDP
UpdateMouseLockState PROC
cmp engineMode,MODE_HELL_FPS
je lock_interior_check
cmp engineMode,MODE_STAIRS
je lock_interior_check
cmp engineMode, MODE_WALK
@ -4567,6 +4570,10 @@ skip_mouse_look:
mov eax,50
@@:
imul eax, 200
cmp engineMode,MODE_HELL_FPS
jne @F
imul eax,2
@@:
mov speed, eax
cmp keys3D[10h], 0
je @F
@ -4667,19 +4674,33 @@ check_total_movement:
add eax, deltaMoveY
mov nextY, eax
cmp engineMode,MODE_HELL_FPS
jne hf_move_regular_x
invoke HFPlayerCanWalk,nextX,playerY
jmp hf_move_check_x
hf_move_regular_x:
invoke CanWalk3D, nextX, playerY
hf_move_check_x:
test eax, eax
jz @F
mov eax, nextX
mov playerX, eax
@@:
cmp engineMode,MODE_HELL_FPS
jne hf_move_regular_y
invoke HFPlayerCanWalk,playerX,nextY
jmp hf_move_check_y
hf_move_regular_y:
invoke CanWalk3D, playerX, nextY
hf_move_check_y:
test eax, eax
jz input_action
mov eax, nextY
mov playerY, eax
input_action:
cmp engineMode,MODE_HELL_FPS
je player_input_complete
cmp brActive3D,0
jne player_input_complete
cmp engineMode,MODE_STAIRS
@ -5254,6 +5275,9 @@ include src_graphics\observer.inc
include src_graphics\darts.inc
include src_graphics\jackbox.inc
include src_graphics\backrooms.inc
include src_graphics\hell_fps.inc
include src_graphics\hell_fps_combat.inc
include src_graphics\hell_fps_render.inc
; --- Main Loop of the 3D Casino Engine ---
StartHellTower3D PROC uses ebx esi edi
@ -5317,6 +5341,11 @@ engine_observer_skip:
invoke Pressed3D, 1Bh
test eax, eax
jz engine_tick
cmp engineMode,MODE_HELL_FPS
jne @F
call LeaveHellFPS3D
jmp engine_tick
@@:
cmp engineMode, MODE_INTRO
jne @F
call FinishStartup3D
@ -5397,6 +5426,14 @@ check_arrival_mode:
call RenderArrival3D
jmp present_frame
check_walk_mode:
cmp engineMode,MODE_HELL_FPS
jne @F
call HandleHellFPS3D
cmp engineMode,MODE_WALK
je check_walk_mode
call RenderHellFPS3D
jmp present_frame
@@:
cmp engineMode, MODE_WALK
jne check_slots_mode
cmp brActive3D,0
@ -5405,6 +5442,8 @@ check_walk_mode:
call UpdateCamera3D
call CheckInteractions
call HandlePlayerInput
cmp engineMode,MODE_HELL_FPS
je check_walk_mode
call BackroomsEntry3D
cmp brActive3D,0
jne engine_backrooms
@ -5741,6 +5780,7 @@ engine_idle:
invoke Sleep, 50
jmp engine_loop
engine_shutdown:
call FreeHellFPS3D
call StopFortuneWorker3D
call SettleScratchOnQuit3D
call UnlockMouse3D

Binary file not shown.

View file

@ -0,0 +1,25 @@
# Hell presentation asset contract
Imported offline from the pinned Freedoom 0.13.0 archive using
`tools/generators/import_freedoom.py`. Runtime remains MASM32.
All packs: 20-byte `ANM1` header (five little-endian DWORDs: magic, width, height,
frame count, payload bytes), then row-major BGRA frames. Nearest-neighbour only.
| Pack | Frame size | Layout / alpha / origin |
|---|---|---|
| `hud.bin` | 320×32 | One `STBAR` frame, rendered 2× at (0,416) |
| `digits.bin` | 16×16 | Ten digits and percent, right-aligned; transparent |
| `faces.bin` | 24×32 | Five health bands × four expressions, then dead; bottom-centred |
| `impacts.bin` | 32×32 | Four `PUFF` frames, shared sprite origin; transparent |
| `painting.bin` | 512×256 | One opaque, power-of-two wall texture; full-frame UVs |
Painting composes Freedoom sky, lava, masonry and monsters with an authored gilt
border. Its preview uses intended landscape aspect. Engine maps the texture onto
a 6×2-world-unit wall panel; it is not a camera-facing sprite. Power-of-two
dimensions are required by `DrawCityVertical3D`'s masked texture coordinates.
No generated AI image or proprietary Doom artwork is used.
HUD glyphs and portrait retain original pixel-art palette. Frame padding avoids
per-expression scaling jitter. See `assets/hell/manifest.json` for source lumps
and `assets/hell/COPYING.txt` / `CREDITS.txt` for licence and attribution.

94
docs/HELL_EXPEDITION.md Normal file
View file

@ -0,0 +1,94 @@
# Hell Expedition
## Play
Launch `poker.exe` or `dist/Assembler Casino.exe`. On the casino's first floor,
approach the large gilt-framed hell painting on the wall opposite the bar
(map position 6, 30.9), face it and press **E**. The upstairs Hell Tower remains
a separate game. The old freestanding portal and its collision frame are gone.
| Input | Action |
|---|---|
| WASD | Move / strafe |
| Mouse | Turn |
| Left mouse / Space | Fire; hold for repeated fire |
| 1 / 2 | Pistol / shotgun, after collecting it |
| E | Open nearby gates/secrets; activate final exit |
| Enter after death | Restart expedition |
| Esc | Return to casino |
Collect the shotgun near the starting point. Cross the barracks and lava court,
find the blue key in the northern crypt, open the eastern gate and defeat the
warden in the southern sanctum. Use the exit portal to finish. Two concealed
side rooms contain extra supplies. Lava causes damage; armour absorbs half of
incoming damage while it lasts. Health and ammunition are expedition-local.
Death and retry do not deduct casino money. The first successful clear credits
**$6,660**, subject to the existing bank cap. Later clears show statistics without
paying again. Leaving or retrying starts a fresh expedition; mid-level progress
is not saved. Completion belongs to the current casino profile.
## Implementation
- MASM32 runtime: `src_graphics/hell_fps*.inc`; integrated with the existing
raycaster, collision, window/input and signed profile code.
- Authored 32×32 map: five encounter regions, 28 enemies across four types,
24 pickups, a blue-key gate, two secrets and a boss-controlled exit.
- Combat simulation: fixed 35 Hz; existing frame-time-scaled player movement.
- Hitscan pistol/shotgun, wall occlusion, enemy attack windups, BFS navigation,
projectile pool, health/armour, weapon and monster animation, HUD/statistics.
- Shotgun: seven independent pellets per shell, approximately 12.7-degree
horizontal cone, individual damage/occlusion and short-lived impact puffs.
A single shot can hit several enemies; the pistol remains a single ray.
- Doom-layout Freedoom status bar: large red ammunition/health/armour numbers,
damage-dependent animated face, weapon slots, key and ammunition reserves.
- Completion flag: bit 18 in native and preview signed saves; file sizes stay
unchanged. Native profiles remain the normal game save path.
- Music: existing `assets/music/tower/Mick_Gordon_-_The_Only_Thing_They_Fear_Is_You_OST_DOOM_Eternal_72020713.mp3`.
Dedicated MCI music alias at 35% of the previous music level (still follows
the volume setting, including mute), applied before playback starts.
Six independent waveaudio effect channels retain their existing loudness.
## Assets and regeneration
Graphics and effects originate from **Freedoom 0.13.0**, imported offline into
ANM1 BGRA packs and WAV files. Licence and credits ship in
`assets/hell/COPYING.txt` and `assets/hell/CREDITS.txt`. Exact source lumps,
download URL and archive SHA-256 are in `assets/hell/manifest.json`.
The casino level is original; no Doom WAD is loaded at runtime.
```powershell
python tools/generators/import_freedoom.py path/to/freedoom-0.13.0.zip
python tools/generators/make_hell_level.py
cmd /c build.bat
powershell -ExecutionPolicy Bypass -File tools/tests/publish_graphics.ps1
```
The final command updates local root/bin game copies and recompiles the existing
C# launcher with the new integrity hash. It does not upload a release.
Python/Pillow are offline development dependencies only. Include all of
`assets/hell/` and the existing soundtrack when copying the game distribution.
Freedoom's licence applies to its assets, not to the separate soundtrack.
## Verification
```powershell
python tools/tests/check_hell_fps.py
python tools/tests/check_hell_fps.py --window
python tools/tests/check_artifacts.py
```
Native checks execute shipping ASM procedures: portal targeting, wall/body
collisions, single-ray pistol versus multi-target pellet spread, wall occlusion,
one-shell consumption/empty weapon, impact pool, pickups, armour/death/retry, key gate,
secrets, 1,050 AI ticks, a finite-ammo boss fight with strafing and normal health,
boss-locked exit, one-time reward, both signed save
formats and return to the hall. Framebuffers are saved in `build/scratch/`.
The window check presents 120 frames through Win32, sends mouse press/release
events, checks shots, all six SFX opens and music's MCI playing status, then
returns and closes. It also exercises asset-path resolution from a non-root
working directory. Tests compare all `runtime/` file hashes before/after.
These are automated mechanics/render/audio checks, not a complete human
playthrough. The requested 10–15-minute first-clear duration and final difficulty
still need playtesting; no measured duration is claimed.

View file

@ -827,6 +827,10 @@ save_stats_played_loop:
shl ecx,17
or eax,ecx
or eax,2
mov ecx,hfCompleted
and ecx,1
shl ecx,18
or eax,ecx
mov DWORD PTR [saveBuffer+304],eax
invoke ArtifactSaveSignature3D,OFFSET saveBuffer
mov DWORD PTR [saveBuffer+308],eax
@ -902,6 +906,10 @@ load_file_opened:
shr eax,17
and eax,1
mov brEnabled3D,eax
mov eax,DWORD PTR [saveBuffer+304]
shr eax,18
and eax,1
mov hfCompleted,eax
load_signature_ok:
mov eax, DWORD PTR [saveBuffer+4]
mov bankroll, eax
@ -2061,7 +2069,18 @@ ENDIF
IFDEF BACKROOMS_TEST
include tools/tests/backrooms_test.inc
ENDIF
IFDEF HELL_FPS_TEST
include tools/tests/hell_fps_test.inc
ENDIF
main PROC
IFDEF HELL_FPS_TEST
IFDEF HELL_FPS_WINDOW_TEST
call RunHellFPSWindowTest
ELSE
call RunHellFPSTest
ENDIF
invoke ExitProcess,eax
ENDIF
IFDEF BACKROOMS_TEST
call RunBackroomsTest3D
invoke ExitProcess,eax

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -33,7 +33,7 @@ ValidateArtifactSave3D PROC uses esi edi source:DWORD
cmp DWORD PTR [esi+280],32465241h
jne artifact_save_invalid
mov eax,[esi+304]
and eax,0FFFCFFFFh
and eax,0FFF8FFFFh
cmp eax,2
jne artifact_save_invalid
test DWORD PTR [esi+304],20000h
@ -72,6 +72,7 @@ ArtifactCredit3D PROC amount:DWORD
ArtifactCredit3D ENDP
ResetArtifacts3D PROC
mov hfCompleted,0
mov brOwned3D,0
mov brEnabled3D,1
mov charmCloverUses,0

View file

@ -5,6 +5,23 @@ 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,HF_PAINTING_MATERIAL
jne hf_not_painting
mov eax,hfPaintingArt.pixels
mov ecx,512
mov edx,256
ret
hf_not_painting:
cmp eax,HF_WALL
jb hf_surface_other
cmp eax,HF_LAVA+3
ja hf_surface_other
shl eax,16
add eax,OFFSET texturesBuffer
mov ecx,128
mov edx,128
ret
hf_surface_other:
cmp hallLevel3D,HALL_BACKROOMS
jne br_surface_other_world
cmp eax,BR_WALL

View file

@ -20,6 +20,10 @@ hall_plane_row:
mov eax, viewCeiling3D
sub eax, viewEye3D
mov material, 101
cmp engineMode,MODE_HELL_FPS
jne @F
mov material,HF_CEILING
@@:
cmp brActive3D,0
je @F
cmp hallLevel3D,HALL_BACKROOMS
@ -756,6 +760,7 @@ hall_render_lounge:
invoke DrawCityObject3D,OFFSET scrStation3D,0
call RenderHallDressing3D
call RenderHallPortals3D
call RenderHellPortal3D
call RenderFortuneEntrance3D
call RenderLift3D
call ApplyHallLighting3D
@ -865,6 +870,7 @@ hall_check_other:
call CheckNpcInteractions3D
call CheckFortuneInteractions3D
call CheckRoofInteractions3D
invoke HallTarget3D,HF_PORTAL_X,HF_PORTAL_Y,INTERACT_HELL_FPS,OFFSET hfPortalPrompt
ret
hall_check_bunker:
mov interactTargetId, 0
@ -874,6 +880,12 @@ hall_check_bunker:
CheckHallInteractions3D ENDP
TriggerHallInteraction3D PROC
cmp interactTargetId,INTERACT_HELL_FPS
jne @F
call BeginHellFPS3D
mov eax,1
ret
@@:
call TryBowlingInteraction3D
test eax,eax
jnz hall_trigger_handled

277
src_graphics/hell_fps.inc Normal file
View file

@ -0,0 +1,277 @@
; Expedition lifecycle and fixed 35 Hz simulation. No external game runtime.
HFPlaySound PROC channel:DWORD
cmp hfSoundReady,0
je @F
mov eax,channel
cmp eax,6
jae @F
invoke mciSendStringA,hfSndPlays[eax*4],0,0,0
@@:
ret
HFPlaySound ENDP
HFLoad PROC uses esi edi
cmp hfLoaded,1
je hf_load_ok
xor esi,esi
hf_load_next:
invoke LoadAnimPack3D,hfPacks[esi*4],hfPaths[esi*4]
test eax,eax
jz hf_load_failed
inc esi
cmp esi,HF_PACK_COUNT
jb hf_load_next
; Dedicated texture slots, never overwrite casino materials.
mov esi,hfMaterials.pixels
mov edi,OFFSET texturesBuffer+HF_WALL*65536
mov ecx,12*128*128
cld
rep movsd
mov hfLoaded,1
hf_load_ok:
mov eax,1
ret
hf_load_failed:
call FreeHellFPS3D
xor eax,eax
ret
HFLoad ENDP
FreeHellFPS3D PROC uses esi
xor esi,esi
@@:
invoke FreeAnimPack3D,hfPacks[esi*4]
inc esi
cmp esi,HF_PACK_COUNT
jb @B
mov hfLoaded,0
invoke mciSendStringA,OFFSET hfMusicClose,0,0,0
xor esi,esi
@@:
invoke mciSendStringA,hfSndCloses[esi*4],0,0,0
inc esi
cmp esi,6
jb @B
mov hfSoundReady,0
ret
FreeHellFPS3D ENDP
HFReset PROC uses esi edi ebx
mov esi,OFFSET hfMapTemplate
mov edi,OFFSET hfMap
mov ecx,256
cld
rep movsd
xor eax,eax
mov edi,OFFSET hfBolts
mov ecx,SIZEOF hfBolts/4
rep stosd
mov edi,OFFSET hfImpacts
mov ecx,SIZEOF hfImpacts/4
rep stosd
mov edi,OFFSET hfItemTaken
mov ecx,HF_ITEMS
rep stosd
mov esi,OFFSET hfEnemySeeds
mov edi,OFFSET hfEnemies
mov ebx,HF_ENEMIES
hf_reset_enemy:
lodsd
mov [edi].HFEnemy.x,eax
lodsd
mov [edi].HFEnemy.y,eax
lodsd
mov [edi].HFEnemy.kind,eax
mov eax,hfMonsterHP[eax*4]
mov [edi].HFEnemy.hp,eax
mov [edi].HFEnemy.cooldown,35
mov [edi].HFEnemy.pose,0
mov [edi].HFEnemy.timer,0
mov [edi].HFEnemy.awake,0
add edi,SIZEOF HFEnemy
dec ebx
jnz hf_reset_enemy
mov hfState,0
mov hfHealth,100
mov hfArmor,0
mov hfBullets,80
mov hfShells,0
mov hfWeapon,0
mov hfHasShotgun,0
mov hfKey,0
mov hfSecrets,0
mov hfKills,0
mov hfTicks,0
mov hfAccumulator,0
mov hfCooldown,0
mov hfFlash,0
mov hfHurt,0
mov hfDoorMask,0
mov hfAwarded,0
mov hfFireHeld,0
mov hfFirePending,0
mov hfUsePending,0
mov playerX,3*FP_1+FP_HALF
mov playerY,26*FP_1+FP_HALF
mov playerAngle,0
mov worldMap3D,OFFSET hfMap
mov hallLevel3D,99
mov floorTexture3D,HF_FLOOR
mov viewEye3D,FP_1
mov viewCeiling3D,FP_1*5/2
mov outdoorRender3D,0
mov viewHorizon3D,HALF_H
mov engineMode,MODE_HELL_FPS
mov hfMessage,OFFSET hfWelcomeText
mov hfMessageTime,210
call ClearKeys3D
call UpdateCamera3D
call HFBuildFlow
ret
HFReset ENDP
BeginHellFPS3D PROC uses esi
call HFLoad
test eax,eax
jnz @F
invoke CopyString3D,OFFSET hfMissingText,OFFSET flashMessage
mov flashTimer,180
ret
@@:
mov eax,playerX
mov hfOldX,eax
mov eax,playerY
mov hfOldY,eax
mov eax,playerAngle
mov hfOldAngle,eax
mov eax,worldMap3D
mov hfOldMap,eax
mov eax,floorTexture3D
mov hfOldFloor,eax
mov eax,hallLevel3D
mov hfOldLevel,eax
mov eax,viewEye3D
mov hfOldEye,eax
mov eax,viewCeiling3D
mov hfOldCeiling,eax
call StopTowerMusic
call StopJukeboxTrack3D
invoke PlayCasinoSound3D,0,0,0
invoke mciSendStringA,OFFSET hfMusicOpen,0,0,0
mov hfMusicError,eax
mov hfLastVolume,-1
call HFUpdateMusicVolume
invoke mciSendStringA,OFFSET hfMusicPlay,0,0,0
or hfMusicError,eax
mov hfSoundFailures,0
xor esi,esi
hf_open_sound:
invoke mciSendStringA,hfSndOpens[esi*4],0,0,0
test eax,eax
jz @F
inc hfSoundFailures
@@:
inc esi
cmp esi,6
jb hf_open_sound
mov hfSoundReady,1
call HFReset
ret
BeginHellFPS3D ENDP
LeaveHellFPS3D PROC uses esi
mov eax,hfOldX
mov playerX,eax
mov eax,hfOldY
mov playerY,eax
mov eax,hfOldAngle
mov playerAngle,eax
mov eax,hfOldMap
mov worldMap3D,eax
mov eax,hfOldFloor
mov floorTexture3D,eax
mov eax,hfOldLevel
mov hallLevel3D,eax
mov eax,hfOldEye
mov viewEye3D,eax
mov eax,hfOldCeiling
mov viewCeiling3D,eax
mov hfFireHeld,0
mov hfFirePending,0
mov engineMode,MODE_WALK
mov paused3D,0
invoke mciSendStringA,OFFSET hfMusicClose,0,0,0
xor esi,esi
@@:
invoke mciSendStringA,hfSndCloses[esi*4],0,0,0
inc esi
cmp esi,6
jb @B
mov hfSoundReady,0
call ClearKeys3D
call UpdateCamera3D
ret
LeaveHellFPS3D ENDP
HFUpdateMusicVolume PROC
mov eax,settingsVolume
cmp eax,hfLastVolume
je @F
mov hfLastVolume,eax
imul eax,HF_MUSIC_SCALE
invoke FormatNumber,eax,OFFSET hfNumber
invoke CopyCString,OFFSET hfVolumeCommand,OFFSET hfVolumePrefix
invoke AppendCString,OFFSET hfVolumeCommand,OFFSET hfNumber
invoke mciSendStringA,OFFSET hfVolumeCommand,0,0,0
mov hfVolumeError,eax
@@:
ret
HFUpdateMusicVolume ENDP
HandleHellFPS3D PROC uses ebx
call HFUpdateMusicVolume
cmp hfState,0
je hf_handle_play
invoke Pressed3D,0Dh
test eax,eax
jz hf_handle_done
cmp hfState,1
jne hf_handle_exit
call HFReset
ret
hf_handle_exit:
call LeaveHellFPS3D
ret
hf_handle_play:
call HandlePlayerInput
invoke Pressed3D,31h
test eax,eax
jz @F
mov hfWeapon,0
@@:
invoke Pressed3D,32h
test eax,eax
jz @F
cmp hfHasShotgun,0
je @F
mov hfWeapon,1
@@:
invoke Pressed3D,45h
or hfUsePending,eax
mov eax,mouseClick3D
or hfFirePending,eax
mov mouseClick3D,0
mov eax,deltaTime3D
imul eax,35
add hfAccumulator,eax
hf_handle_tick:
cmp hfAccumulator,1000
jb hf_handle_done
sub hfAccumulator,1000
call HFTick
cmp hfState,0
je hf_handle_tick
mov hfAccumulator,0
hf_handle_done:
ret
HandleHellFPS3D ENDP

View file

@ -0,0 +1,764 @@
; All positions/velocities are signed 16.16; bounded pools and fixed ticks.
HFDistance PROC x0:DWORD,y0:DWORD,x1:DWORD,y1:DWORD
mov eax,x0
sub eax,x1
cdq
xor eax,edx
sub eax,edx
mov ecx,eax
mov eax,y0
sub eax,y1
cdq
xor eax,edx
sub eax,edx
cmp eax,ecx
jae @F
mov eax,ecx
@@:
ret
HFDistance ENDP
HFPlayerCanWalk PROC uses esi edi px:DWORD,py:DWORD
invoke CanWalk3D,px,py
test eax,eax
jz hf_player_walk_done
mov esi,OFFSET hfEnemies
mov edi,HF_ENEMIES
hf_player_body:
cmp [esi].HFEnemy.hp,0
jle hf_player_body_next
invoke HFDistance,px,py,[esi].HFEnemy.x,[esi].HFEnemy.y
cmp eax,FP_1*9/20
jb hf_player_body_block
hf_player_body_next:
add esi,SIZEOF HFEnemy
dec edi
jnz hf_player_body
mov eax,1
ret
hf_player_body_block:
xor eax,eax
hf_player_walk_done:
ret
HFPlayerCanWalk ENDP
HFLineClear PROC uses ebx esi edi x0:DWORD,y0:DWORD,x1:DWORD,y1:DWORD
local stepX:DWORD,stepY:DWORD,rx:DWORD,ry:DWORD
invoke HFDistance,x0,y0,x1,y1
shr eax,12 ; quarter of a collision radius per sample, <= 1/16 tile
inc eax
mov ebx,eax
mov eax,x1
sub eax,x0
cdq
idiv ebx
mov stepX,eax
mov eax,y1
sub eax,y0
cdq
idiv ebx
mov stepY,eax
mov esi,x0
mov edi,y0
hf_los_step:
add esi,stepX
add edi,stepY
mov eax,esi
sar eax,16
cmp eax,32
jae hf_los_blocked
mov ecx,edi
sar ecx,16
cmp ecx,32
jae hf_los_blocked
shl ecx,5
add eax,ecx
cmp hfMap[eax],0
jne hf_los_blocked
dec ebx
jnz hf_los_step
mov eax,1
ret
hf_los_blocked:
xor eax,eax
ret
HFLineClear ENDP
HFDamagePlayer PROC uses ebx amount:DWORD
cmp hfState,0
jne hf_damage_done
mov ebx,amount
mov eax,ebx
shr eax,1
cmp eax,hfArmor
jbe @F
mov eax,hfArmor
@@:
sub hfArmor,eax
sub ebx,eax
sub hfHealth,ebx
mov hfHurt,10
invoke HFPlaySound,2
cmp hfHealth,0
jg hf_damage_done
mov hfHealth,0
mov hfState,1
mov hfFireHeld,0
hf_damage_done:
ret
HFDamagePlayer ENDP
HFDamageEnemy PROC uses esi enemy:DWORD,amount:DWORD
mov esi,enemy
cmp [esi].HFEnemy.hp,0
jle hf_enemy_damage_done
mov eax,amount
sub [esi].HFEnemy.hp,eax
mov [esi].HFEnemy.awake,1
mov [esi].HFEnemy.pose,6
mov [esi].HFEnemy.timer,4
cmp [esi].HFEnemy.hp,0
jg hf_enemy_damage_done
mov [esi].HFEnemy.hp,0
mov [esi].HFEnemy.pose,7
mov [esi].HFEnemy.timer,0
inc hfKills
hf_enemy_damage_done:
ret
HFDamageEnemy ENDP
HFFire PROC uses esi edi ebx
local pellet:DWORD,angle:DWORD,rx:DWORD,ry:DWORD,stepX:DWORD,stepY:DWORD,steps:DWORD
cmp hfCooldown,0
jne hf_fire_done
cmp hfWeapon,1
je hf_fire_shotgun
cmp hfBullets,0
je hf_fire_done
dec hfBullets
mov hfCooldown,9
mov pellet,1
invoke HFPlaySound,4
jmp hf_fire_begin
hf_fire_shotgun:
cmp hfShells,0
je hf_fire_done
dec hfShells
mov hfCooldown,29
mov pellet,HF_SHOT_PELLETS
invoke HFPlaySound,0
hf_fire_begin:
mov hfFlash,4
hf_fire_pellet:
mov eax,playerAngle
cmp hfWeapon,0
je @F
mov ecx,pellet
add eax,hfPelletSpread[ecx*4-4]
@@:
and eax,511
mov angle,eax
mov ecx,tableCos[eax*4]
sar ecx,3
mov stepX,ecx
mov ecx,tableSin[eax*4]
sar ecx,3
mov stepY,ecx
mov eax,playerX
mov rx,eax
mov eax,playerY
mov ry,eax
mov steps,192
hf_ray_step:
mov eax,stepX
add rx,eax
mov eax,stepY
add ry,eax
mov eax,rx
sar eax,16
cmp eax,32
jae hf_ray_end
mov ecx,ry
sar ecx,16
cmp ecx,32
jae hf_ray_end
shl ecx,5
add eax,ecx
cmp hfMap[eax],0
jne hf_ray_end
mov esi,OFFSET hfEnemies
mov ebx,HF_ENEMIES
hf_ray_enemy:
cmp [esi].HFEnemy.hp,0
jle hf_ray_next_enemy
invoke HFDistance,rx,ry,[esi].HFEnemy.x,[esi].HFEnemy.y
cmp eax,FP_1*7/20
ja hf_ray_next_enemy
invoke RandomRange3D,9
add eax,8
invoke HFDamageEnemy,esi,eax
jmp hf_ray_end
hf_ray_next_enemy:
add esi,SIZEOF HFEnemy
dec ebx
jnz hf_ray_enemy
dec steps
jnz hf_ray_step
hf_ray_end:
; Each pellet owns its own impact; stay just in front of a wall's depth.
mov ecx,pellet
dec ecx
imul ecx,SIZEOF HFImpact
mov eax,rx
sub eax,stepX
sub eax,stepX
mov hfImpacts[ecx].x,eax
mov eax,ry
sub eax,stepY
sub eax,stepY
mov hfImpacts[ecx].y,eax
mov hfImpacts[ecx].life,8
dec pellet
jnz hf_fire_pellet
hf_fire_done:
ret
HFFire ENDP
HFSpawnBolt PROC uses esi ebx x:DWORD,y:DWORD,damage:DWORD
local distance:DWORD
mov esi,OFFSET hfBolts
mov ebx,32
@@:
cmp [esi].HFBolt.life,0
je hf_bolt_slot
add esi,SIZEOF HFBolt
dec ebx
jnz @B
ret
hf_bolt_slot:
invoke HFDistance,x,y,playerX,playerY
cmp eax,FP_1/2
jae @F
mov eax,FP_1/2
@@:
mov distance,eax
mov eax,x
mov [esi].HFBolt.x,eax
mov eax,y
mov [esi].HFBolt.y,eax
mov eax,damage
mov [esi].HFBolt.damage,eax
mov [esi].HFBolt.life,210
mov eax,playerX
sub eax,x
mov ecx,FP_1/7
imul ecx
idiv distance
mov [esi].HFBolt.vx,eax
mov eax,playerY
sub eax,y
mov ecx,FP_1/7
imul ecx
idiv distance
mov [esi].HFBolt.vy,eax
invoke HFPlaySound,1
ret
HFSpawnBolt ENDP
; Breadth-first distance field permits monsters to navigate around room pillars.
HFBuildFlow PROC uses esi edi ebx
local head:DWORD,tail:DWORD,current:DWORD,neighbor:DWORD
cld
mov edi,OFFSET hfFlow
mov eax,0FFFFFFFFh
mov ecx,512
rep stosd
mov eax,playerY
sar eax,16
shl eax,5
mov ecx,playerX
sar ecx,16
add eax,ecx
mov hfFlow[eax*2],0
mov hfQueue,eax
mov head,0
mov tail,1
hf_flow_pop:
mov eax,head
cmp eax,tail
jae hf_flow_done
mov eax,hfQueue[eax*4]
mov current,eax
movzx ebx,hfFlow[eax*2]
inc ebx
xor esi,esi
hf_flow_neighbor:
mov eax,current
cmp esi,0
jne @F
dec eax
@@:
cmp esi,1
jne @F
inc eax
@@:
cmp esi,2
jne @F
sub eax,32
@@:
cmp esi,3
jne @F
add eax,32
@@:
cmp eax,1024
jae hf_flow_next
cmp hfMap[eax],0
jne hf_flow_next
cmp hfFlow[eax*2],0FFFFh
jne hf_flow_next
mov hfFlow[eax*2],bx
mov ecx,tail
mov hfQueue[ecx*4],eax
inc tail
hf_flow_next:
inc esi
cmp esi,4
jb hf_flow_neighbor
inc head
jmp hf_flow_pop
hf_flow_done:
ret
HFBuildFlow ENDP
HFMoveEnemy PROC uses esi edi ebx enemy:DWORD
local tx:DWORD,ty:DWORD,distance:DWORD,speed:DWORD,best:DWORD,cell:DWORD,nx:DWORD,ny:DWORD
mov esi,enemy
mov eax,playerX
mov tx,eax
mov eax,playerY
mov ty,eax
invoke HFLineClear,[esi].HFEnemy.x,[esi].HFEnemy.y,playerX,playerY
test eax,eax
jnz hf_move_target
mov eax,[esi].HFEnemy.y
sar eax,16
shl eax,5
mov ecx,[esi].HFEnemy.x
sar ecx,16
add eax,ecx
mov cell,eax
movzx eax,hfFlow[eax*2]
mov best,eax
mov edi,-1
xor ebx,ebx
hf_move_flow:
mov eax,cell
cmp ebx,0
jne @F
dec eax
@@:
cmp ebx,1
jne @F
inc eax
@@:
cmp ebx,2
jne @F
sub eax,32
@@:
cmp ebx,3
jne @F
add eax,32
@@:
cmp eax,1024
jae hf_move_flow_next
movzx ecx,hfFlow[eax*2]
cmp ecx,best
jae hf_move_flow_next
mov best,ecx
mov edi,eax
hf_move_flow_next:
inc ebx
cmp ebx,4
jb hf_move_flow
cmp edi,-1
je hf_move_done
mov eax,edi
and eax,31
shl eax,16
add eax,FP_HALF
mov tx,eax
shr edi,5
shl edi,16
add edi,FP_HALF
mov ty,edi
hf_move_target:
invoke HFDistance,[esi].HFEnemy.x,[esi].HFEnemy.y,tx,ty
cmp eax,FP_1/8
jb hf_move_done
mov distance,eax
mov eax,[esi].HFEnemy.kind
mov eax,hfMonsterSpeed[eax*4]
mov speed,eax
mov eax,tx
sub eax,[esi].HFEnemy.x
imul speed
idiv distance
add eax,[esi].HFEnemy.x
mov nx,eax
invoke CanWalk3D,eax,[esi].HFEnemy.y
test eax,eax
jz @F
mov eax,nx
mov [esi].HFEnemy.x,eax
@@:
mov eax,ty
sub eax,[esi].HFEnemy.y
imul speed
idiv distance
add eax,[esi].HFEnemy.y
mov ny,eax
invoke CanWalk3D,[esi].HFEnemy.x,eax
test eax,eax
jz hf_move_done
mov eax,ny
mov [esi].HFEnemy.y,eax
hf_move_done:
ret
HFMoveEnemy ENDP
HFEnemiesTick PROC uses esi edi ebx
local distance:DWORD,visible:DWORD
mov esi,OFFSET hfEnemies
mov edi,HF_ENEMIES
hf_ai_enemy:
cmp [esi].HFEnemy.hp,0
jg hf_ai_alive
cmp [esi].HFEnemy.pose,11
jae hf_ai_next
inc [esi].HFEnemy.timer
cmp [esi].HFEnemy.timer,6
jb hf_ai_next
mov [esi].HFEnemy.timer,0
inc [esi].HFEnemy.pose
jmp hf_ai_next
hf_ai_alive:
cmp [esi].HFEnemy.cooldown,0
je @F
dec [esi].HFEnemy.cooldown
@@:
cmp [esi].HFEnemy.timer,0
je hf_ai_perceive
dec [esi].HFEnemy.timer
jnz hf_ai_next
cmp [esi].HFEnemy.pose,4
jne hf_ai_next
invoke HFLineClear,[esi].HFEnemy.x,[esi].HFEnemy.y,playerX,playerY
test eax,eax
jz hf_ai_next
invoke HFDistance,[esi].HFEnemy.x,[esi].HFEnemy.y,playerX,playerY
mov distance,eax
mov ebx,[esi].HFEnemy.kind
cmp eax,FP_1*7/10
jbe hf_ai_melee
cmp ebx,1
je hf_ai_next
cmp ebx,0
jne hf_ai_projectile
invoke RandomRange3D,4
test eax,eax
jz hf_ai_attack_pose
invoke HFDamagePlayer,6
jmp hf_ai_attack_pose
hf_ai_perceive:
invoke HFDistance,[esi].HFEnemy.x,[esi].HFEnemy.y,playerX,playerY
mov distance,eax
mov visible,0
cmp eax,16*FP_1
ja hf_ai_awake
invoke HFLineClear,[esi].HFEnemy.x,[esi].HFEnemy.y,playerX,playerY
mov visible,eax
test eax,eax
jz hf_ai_awake
mov [esi].HFEnemy.awake,1
hf_ai_awake:
cmp [esi].HFEnemy.awake,0
je hf_ai_next
mov eax,hfTicks
shr eax,3
and eax,3
mov [esi].HFEnemy.pose,eax
cmp visible,0
je hf_ai_move
mov ebx,[esi].HFEnemy.kind
cmp [esi].HFEnemy.cooldown,0
jne hf_ai_spacing
cmp distance,FP_1*7/10
jbe hf_ai_windup
cmp ebx,1
je hf_ai_move
hf_ai_windup:
mov [esi].HFEnemy.pose,4
mov [esi].HFEnemy.timer,12
jmp hf_ai_next
hf_ai_projectile:
invoke HFSpawnBolt,[esi].HFEnemy.x,[esi].HFEnemy.y,hfMonsterDamage[ebx*4]
jmp hf_ai_attack_pose
hf_ai_melee:
invoke HFDamagePlayer,hfMonsterDamage[ebx*4]
hf_ai_attack_pose:
mov [esi].HFEnemy.pose,5
mov [esi].HFEnemy.timer,10
mov [esi].HFEnemy.cooldown,55
cmp ebx,3
jne hf_ai_next
mov [esi].HFEnemy.cooldown,18
jmp hf_ai_next
hf_ai_spacing:
cmp distance,FP_1*7/10
jb hf_ai_next
cmp ebx,1
je hf_ai_move
cmp distance,4*FP_1
jb hf_ai_next
hf_ai_move:
invoke HFMoveEnemy,esi
hf_ai_next:
add esi,SIZEOF HFEnemy
dec edi
jnz hf_ai_enemy
ret
HFEnemiesTick ENDP
HFBoltsTick PROC uses esi edi
mov esi,OFFSET hfBolts
mov edi,32
hf_projectile:
cmp [esi].HFBolt.life,0
je hf_projectile_next
dec [esi].HFBolt.life
mov eax,[esi].HFBolt.vx
add [esi].HFBolt.x,eax
mov eax,[esi].HFBolt.vy
add [esi].HFBolt.y,eax
invoke CanWalk3D,[esi].HFBolt.x,[esi].HFBolt.y
test eax,eax
jz hf_projectile_destroy
invoke HFDistance,[esi].HFBolt.x,[esi].HFBolt.y,playerX,playerY
cmp eax,FP_1*3/10
ja hf_projectile_next
invoke HFDamagePlayer,[esi].HFBolt.damage
hf_projectile_destroy:
mov [esi].HFBolt.life,0
hf_projectile_next:
add esi,SIZEOF HFBolt
dec edi
jnz hf_projectile
ret
HFBoltsTick ENDP
HFPickups PROC uses esi edi ebx
xor ebx,ebx
mov esi,OFFSET hfItemSeeds
hf_pickup:
cmp hfItemTaken[ebx*4],0
jne hf_pickup_next
invoke HFDistance,playerX,playerY,[esi],[esi+4]
cmp eax,FP_1*3/5
ja hf_pickup_next
mov edi,[esi+8]
cmp edi,0
jne hf_pick_armor
cmp hfHealth,100
jae hf_pickup_next
add hfHealth,25
cmp hfHealth,100
jbe hf_pick_commit
mov hfHealth,100
jmp hf_pick_commit
hf_pick_armor:
cmp edi,1
jne hf_pick_bullets
cmp hfArmor,100
jae hf_pickup_next
mov hfArmor,100
jmp hf_pick_commit
hf_pick_bullets:
cmp edi,2
jne hf_pick_shells
add hfBullets,40
jmp hf_pick_commit
hf_pick_shells:
cmp edi,3
jne hf_pick_shotgun
add hfShells,12
jmp hf_pick_commit
hf_pick_shotgun:
cmp edi,4
jne hf_pick_key
mov hfHasShotgun,1
mov hfWeapon,1
add hfShells,16
jmp hf_pick_commit
hf_pick_key:
mov hfKey,1
hf_pick_commit:
mov hfItemTaken[ebx*4],1
mov hfMessage,OFFSET hfPickupText
cmp edi,5
jne @F
mov hfMessage,OFFSET hfKeyFoundText
@@:
mov hfMessageTime,105
invoke HFPlaySound,3
hf_pickup_next:
add esi,12
inc ebx
cmp ebx,HF_ITEMS
jb hf_pickup
ret
HFPickups ENDP
HFUse PROC uses ebx esi
invoke HFDistance,playerX,playerY,FP_1*11/2,FP_1*25/2
cmp eax,FP_1*8/5
ja hf_use_secret_two
test hfDoorMask,1
jnz hf_use_secret_two
or hfDoorMask,1
mov hfMap[12*32+5],0
mov hfMap[12*32+6],0
jmp hf_use_secret
hf_use_secret_two:
invoke HFDistance,playerX,playerY,FP_1*45/2,27*FP_1
cmp eax,FP_1*8/5
ja hf_use_gate
test hfDoorMask,2
jnz hf_use_gate
or hfDoorMask,2
mov hfMap[26*32+22],0
mov hfMap[27*32+22],0
hf_use_secret:
inc hfSecrets
mov hfMessage,OFFSET hfSecretText
mov hfMessageTime,105
invoke HFPlaySound,5
ret
hf_use_gate:
invoke HFDistance,playerX,playerY,FP_1*45/2,7*FP_1
cmp eax,2*FP_1
ja hf_use_exit
cmp hfKey,0
jne hf_use_unlock
mov hfMessage,OFFSET hfNeedKeyText
mov hfMessageTime,105
ret
hf_use_unlock:
mov hfMap[6*32+22],0
mov hfMap[7*32+22],0
or hfDoorMask,4
mov hfMessage,OFFSET hfDoorText
mov hfMessageTime,105
invoke HFPlaySound,5
ret
hf_use_exit:
invoke HFDistance,playerX,playerY,28*FP_1,29*FP_1
cmp eax,2*FP_1
ja hf_use_done
mov esi,OFFSET hfEnemies
mov ebx,HF_ENEMIES
@@:
cmp [esi].HFEnemy.kind,3
jne hf_exit_next
cmp [esi].HFEnemy.hp,0
jg hf_exit_locked
hf_exit_next:
add esi,SIZEOF HFEnemy
dec ebx
jnz @B
mov hfState,2
mov hfFireHeld,0
mov hfMessageTime,0
cmp hfCompleted,0
jne hf_use_done
mov hfCompleted,1
mov hfAwarded,1
invoke ArtifactCredit3D,6660
call SaveProgress3D
ret
hf_exit_locked:
mov hfMessage,OFFSET hfExitText
mov hfMessageTime,105
hf_use_done:
ret
HFUse ENDP
HFTick PROC uses esi ecx
mov esi,OFFSET hfImpacts
mov ecx,HF_SHOT_PELLETS
hf_impact_tick:
cmp [esi].HFImpact.life,0
je @F
dec [esi].HFImpact.life
@@:
add esi,SIZEOF HFImpact
loop hf_impact_tick
inc hfTicks
cmp hfCooldown,0
je @F
dec hfCooldown
@@:
cmp hfFlash,0
je @F
dec hfFlash
@@:
cmp hfHurt,0
je @F
dec hfHurt
@@:
cmp hfMessageTime,0
je @F
dec hfMessageTime
@@:
mov eax,hfFireHeld
or eax,hfFirePending
or al,keys3D[20h]
jz @F
call HFFire
@@:
mov hfFirePending,0
cmp hfUsePending,0
je @F
mov hfUsePending,0
call HFUse
@@:
cmp hfState,0
jne hf_tick_done
mov eax,hfTicks
and eax,7
jnz @F
call HFBuildFlow
@@:
call HFEnemiesTick
call HFBoltsTick
cmp hfState,0
jne hf_tick_done
call HFPickups
; Central lava channel: visible floor patch exactly matches damage bounds.
mov eax,hfTicks
xor edx,edx
mov ecx,18
div ecx
test edx,edx
jnz hf_tick_done
cmp playerX,16*FP_1
jb hf_tick_done
cmp playerX,18*FP_1
jae hf_tick_done
cmp playerY,17*FP_1
jb hf_tick_done
cmp playerY,20*FP_1
jae hf_tick_done
invoke HFDamagePlayer,8
hf_tick_done:
ret
HFTick ENDP

View file

@ -0,0 +1,203 @@
; Native MASM32 hell expedition. Assets: Freedoom 0.13.0 (assets/hell/COPYING.txt).
MODE_HELL_FPS equ 29
INTERACT_HELL_FPS equ 55
HF_WALL equ 185
HF_FLOOR equ 191
HF_CEILING equ 192
HF_LAVA equ 193
; South wall faces the customer side of the bar at x=3..9, y=23..24.
HF_PORTAL_X equ 6*FP_1
HF_PORTAL_Y equ FP_1*309/10
HF_PAINTING_MATERIAL equ 500
HF_PACK_COUNT equ 15
HF_SHOT_PELLETS equ 7
HF_MUSIC_SCALE equ 35 ; settings 0..10 -> MCI 0..350 (35% of previous level)
BeginHellFPS3D PROTO
LeaveHellFPS3D PROTO
HandleHellFPS3D PROTO
RenderHellFPS3D PROTO
RenderHellPortal3D PROTO
FreeHellFPS3D PROTO
HFReset PROTO
HFBuildFlow PROTO
HFTick PROTO
HFFire PROTO
HFUse PROTO
HFLineClear PROTO :DWORD,:DWORD,:DWORD,:DWORD
HFDamagePlayer PROTO :DWORD
HFDamageEnemy PROTO :DWORD,:DWORD
HFSpawnBolt PROTO :DWORD,:DWORD,:DWORD
HFDistance PROTO :DWORD,:DWORD,:DWORD,:DWORD
HFPlayerCanWalk PROTO :DWORD,:DWORD
HFPlaySound PROTO :DWORD
HFUpdateMusicVolume PROTO
HFEnemy STRUCT
x dd ?
y dd ?
kind dd ?
hp dd ?
cooldown dd ?
pose dd ?
timer dd ?
awake dd ?
HFEnemy ENDS
HFBolt STRUCT
x dd ?
y dd ?
vx dd ?
vy dd ?
life dd ?
damage dd ?
HFBolt ENDS
HFImpact STRUCT
x dd ?
y dd ?
life dd ?
HFImpact ENDS
include src_graphics\hell_fps_map.inc
.data
hfCompleted dd 0
hfState dd 0 ; 0 playing, 1 dead, 2 victory
hfHealth dd 100
hfArmor dd 0
hfBullets dd 80
hfShells dd 0
hfWeapon dd 0
hfHasShotgun dd 0
hfKey dd 0
hfSecrets dd 0
hfKills dd 0
hfTicks dd 0
hfAccumulator dd 0
hfCooldown dd 0
hfFlash dd 0
hfHurt dd 0
hfFireHeld dd 0
hfFirePending dd 0
hfUsePending dd 0
hfOldX dd 0
hfOldY dd 0
hfOldAngle dd 0
hfOldMap dd 0
hfOldFloor dd 0
hfOldLevel dd 0
hfOldEye dd 0
hfOldCeiling dd 0
hfDoorMask dd 0
hfMessage dd 0
hfMessageTime dd 0
hfLoaded dd 0
hfSoundReady dd 0
hfMusicError dd 0
hfSoundFailures dd 0
hfVolumeError dd 0
hfNumber db 32 dup(0)
hfTitle db 'HELL: THE HOUSE ALWAYS BLEEDS',0
hfHelp db 'WASD MOVE MOUSE AIM LMB FIRE 1/2 WEAPON E USE ESC RETURN',0
hfHealthText db 'HEALTH',0
hfArmorText db 'ARMOR',0
hfAmmoText db 'AMMO',0
hfKillsText db 'KILLS',0
hfKeyText db 'BLUE KEY',0
hfPistolText db 'PISTOL',0
hfShotgunText db 'SHOTGUN',0
hfDeadText db 'YOU DIED - ENTER: RETRY / ESC: CASINO',0
hfWinText db 'HELL CLEARED - ENTER / ESC: CASINO',0
hfRewardText db 'FIRST CLEAR: +$6,660',0
hfRepeatText db 'EXPEDITION COMPLETE',0
hfAwarded dd 0
hfStatsText db 'TIME (SEC) KILLS / 28 SECRETS / 2',0
hfPortalPrompt db '[E] HELL EXPEDITION',0
hfMissingText db 'HELL ASSETS MISSING - RESTORE assets/hell',0
hfNeedKeyText db 'THE SANCTUM NEEDS THE BLUE KEY',0
hfDoorText db 'THE GATE OPENS',0
hfSecretText db 'SECRET FOUND',0
hfPickupText db 'SUPPLIES COLLECTED',0
hfKeyFoundText db 'BLUE KEY ACQUIRED - FIND THE EASTERN GATE',0
hfExitText db 'DEFEAT THE WARDEN, THEN USE THE EXIT PORTAL',0
hfWelcomeText db 'FIND THE SHOTGUN. RECOVER THE BLUE KEY. KILL THE WARDEN.',0
hfMaterials AnimPack3D <>
hfSoldier AnimPack3D <>
hfDemon AnimPack3D <>
hfImp AnimPack3D <>
hfBoss AnimPack3D <>
hfPistol AnimPack3D <>
hfShotgun AnimPack3D <>
hfItemsArt AnimPack3D <>
hfFireArt AnimPack3D <>
hfPortalArt AnimPack3D <>
hfHUDArt AnimPack3D <>
hfDigitsArt AnimPack3D <>
hfFacesArt AnimPack3D <>
hfPaintingArt AnimPack3D <>
hfImpactsArt AnimPack3D <>
hfPacks dd OFFSET hfMaterials,OFFSET hfSoldier,OFFSET hfDemon,OFFSET hfImp,OFFSET hfBoss,OFFSET hfPistol,OFFSET hfShotgun,OFFSET hfItemsArt,OFFSET hfFireArt,OFFSET hfPortalArt,OFFSET hfHUDArt,OFFSET hfDigitsArt,OFFSET hfFacesArt,OFFSET hfPaintingArt,OFFSET hfImpactsArt
hfMatPath db 'assets\hell\materials.bin',0
hfSoldierPath db 'assets\hell\soldier.bin',0
hfDemonPath db 'assets\hell\demon.bin',0
hfImpPath db 'assets\hell\imp.bin',0
hfBossPath db 'assets\hell\boss.bin',0
hfPistolPath db 'assets\hell\pistol.bin',0
hfShotgunPath db 'assets\hell\shotgun.bin',0
hfItemsPath db 'assets\hell\items.bin',0
hfFirePath db 'assets\hell\fire.bin',0
hfPortalPath db 'assets\hell\portal.bin',0
hfHUDPath db 'assets\hell\hud.bin',0
hfDigitsPath db 'assets\hell\digits.bin',0
hfFacesPath db 'assets\hell\faces.bin',0
hfPaintingPath db 'assets\hell\painting.bin',0
hfImpactsPath db 'assets\hell\impacts.bin',0
hfPaths dd OFFSET hfMatPath,OFFSET hfSoldierPath,OFFSET hfDemonPath,OFFSET hfImpPath,OFFSET hfBossPath,OFFSET hfPistolPath,OFFSET hfShotgunPath,OFFSET hfItemsPath,OFFSET hfFirePath,OFFSET hfPortalPath,OFFSET hfHUDPath,OFFSET hfDigitsPath,OFFSET hfFacesPath,OFFSET hfPaintingPath,OFFSET hfImpactsPath
hfMonsterPacks dd OFFSET hfSoldier,OFFSET hfDemon,OFFSET hfImp,OFFSET hfBoss
hfMonsterHP dd 55,105,85,1100
hfMonsterSpeed dd 1900,3300,1800,1350
hfMonsterDamage dd 6,15,10,18
hfObject CityProp3D <0,0,FP_1,FP_1*3/2,0,OFFSET hfSoldier,0>
hfPelletSpread dd -9,-6,-3,0,3,6,9 ; independent rays, ~12.7-degree full cone
hfPercentText db '%',0
hfArmsText db 'ARMS',0
hfSlot1Text db '1',0
hfSlot2Text db '2',0
hfAmmoTypesText db 'BULL',0
hfShellTypeText db 'SHEL',0
hfEmptyAmmoText db 'RCKT 0 50',0
hfEmptyCellText db 'CELL 0 300',0
hfMaxBulletsText db '/ 280',0
hfMaxShellsText db '/ 160',0
hfMusicOpen db 'open "assets\music\tower\Mick_Gordon_-_The_Only_Thing_They_Fear_Is_You_OST_DOOM_Eternal_72020713.mp3" type mpegvideo alias hellmusic',0
hfMusicPlay db 'play hellmusic repeat',0
hfMusicClose db 'close hellmusic',0
hfVolumePrefix db 'setaudio hellmusic volume to ',0
hfVolumeCommand db 80 dup(0)
hfLastVolume dd -1
; Six independent waveaudio channels let combat, pain and pickups overlap.
hfSndOpen0 db 'open "assets\hell\dsshotgn.wav" type waveaudio alias hfshot',0
hfSndOpen1 db 'open "assets\hell\dsfirsht.wav" type waveaudio alias hfmonster',0
hfSndOpen2 db 'open "assets\hell\dsplpain.wav" type waveaudio alias hfhurt',0
hfSndOpen3 db 'open "assets\hell\dsitemup.wav" type waveaudio alias hfpick',0
hfSndOpen4 db 'open "assets\hell\dspistol.wav" type waveaudio alias hfpistol',0
hfSndOpen5 db 'open "assets\hell\dsdoropn.wav" type waveaudio alias hfdoor',0
hfSndPlay0 db 'play hfshot from 0',0
hfSndPlay1 db 'play hfmonster from 0',0
hfSndPlay2 db 'play hfhurt from 0',0
hfSndPlay3 db 'play hfpick from 0',0
hfSndPlay4 db 'play hfpistol from 0',0
hfSndPlay5 db 'play hfdoor from 0',0
hfSndClose0 db 'close hfshot',0
hfSndClose1 db 'close hfmonster',0
hfSndClose2 db 'close hfhurt',0
hfSndClose3 db 'close hfpick',0
hfSndClose4 db 'close hfpistol',0
hfSndClose5 db 'close hfdoor',0
hfSndOpens dd OFFSET hfSndOpen0,OFFSET hfSndOpen1,OFFSET hfSndOpen2,OFFSET hfSndOpen3,OFFSET hfSndOpen4,OFFSET hfSndOpen5
hfSndPlays dd OFFSET hfSndPlay0,OFFSET hfSndPlay1,OFFSET hfSndPlay2,OFFSET hfSndPlay3,OFFSET hfSndPlay4,OFFSET hfSndPlay5
hfSndCloses dd OFFSET hfSndClose0,OFFSET hfSndClose1,OFFSET hfSndClose2,OFFSET hfSndClose3,OFFSET hfSndClose4,OFFSET hfSndClose5
.data?
hfMap db 1024 dup(?)
hfEnemies HFEnemy HF_ENEMIES dup(<>)
hfItemTaken dd HF_ITEMS dup(?)
hfBolts HFBolt 32 dup(<>)
hfFlow dw 1024 dup(?)
hfQueue dd 1024 dup(?)
hfImpacts HFImpact HF_SHOT_PELLETS dup(<>)
.code

View file

@ -0,0 +1,92 @@
; Generated by tools/generators/make_hell_level.py.
.data
hfMapTemplate label byte
db 185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,186,186,186,186,186,186,186,186,186,186,187,187,187,187,187,187,187,187,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,186,186,186,186,186,186,186,186,186,186,187,187,187,187,187,187,187,187,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,0,0,0,0,187,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,0,0,0,0,187,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,0,0,0,0,187,185
db 185,185,185,185,185,185,185,185,185,185,185,185,186,0,0,0,186,0,0,0,0,0,188,0,0,0,0,0,0,0,187,185
db 185,185,0,0,0,0,0,0,0,185,185,185,186,0,0,0,0,0,0,186,0,0,188,0,0,0,0,0,0,0,187,185
db 185,185,0,0,0,0,0,0,0,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,0,0,0,0,187,185
db 185,185,0,0,0,0,0,0,0,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,0,0,0,0,187,185
db 185,185,0,0,0,0,0,0,0,185,185,185,186,0,0,0,0,0,0,0,0,0,186,187,0,0,186,0,0,0,187,185
db 185,185,0,0,0,0,0,0,0,185,185,185,186,186,186,186,186,0,0,186,186,186,186,187,0,0,0,0,0,0,187,185
db 185,185,185,185,185,188,188,185,185,185,185,185,185,185,185,185,185,0,0,185,185,185,185,187,0,0,0,0,0,0,187,185
db 185,185,185,185,185,0,0,185,185,185,185,185,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,185,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,185,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,0,0,0,0,186,0,0,0,0,0,0,185,187,0,0,0,186,0,0,187,185
db 185,185,185,0,0,186,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,186,0,0,185,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,185,0,0,0,0,0,0,0,186,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,185,0,0,0,0,0,0,0,0,0,0,185,187,0,0,0,0,0,0,187,185
db 185,185,185,0,0,0,0,0,0,0,0,185,0,0,0,0,0,0,0,0,0,0,185,187,187,187,0,0,187,187,187,185
db 185,185,185,185,185,0,0,185,185,185,185,185,0,0,0,0,0,0,0,0,0,0,185,190,190,190,0,0,190,190,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,185,185,185,185,185,0,0,0,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,185,185,185,185,185,0,0,0,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,0,0,0,0,185,0,0,0,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,0,0,0,0,188,0,0,186,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,0,0,0,0,188,0,0,0,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,0,0,0,0,185,0,0,0,0,0,0,0,190,185
db 185,185,0,0,0,0,0,0,0,0,0,185,185,185,185,185,185,185,185,185,185,185,185,0,0,0,0,0,0,0,190,185
db 185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,190,190,190,190,190,190,190,190,185
db 185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185,185
hfEnemySeeds label dword
dd 7*FP_1+FP_HALF,25*FP_1+FP_HALF,0
dd 8*FP_1+FP_HALF,28*FP_1+FP_HALF,0
dd 4*FP_1+FP_HALF,20*FP_1+FP_HALF,0
dd 8*FP_1+FP_HALF,16*FP_1+FP_HALF,1
dd 9*FP_1+FP_HALF,20*FP_1+FP_HALF,2
dd 4*FP_1+FP_HALF,15*FP_1+FP_HALF,0
dd 14*FP_1+FP_HALF,20*FP_1+FP_HALF,2
dd 20*FP_1+FP_HALF,21*FP_1+FP_HALF,0
dd 20*FP_1+FP_HALF,14*FP_1+FP_HALF,2
dd 13*FP_1+FP_HALF,14*FP_1+FP_HALF,1
dd 18*FP_1+FP_HALF,17*FP_1+FP_HALF,1
dd 15*FP_1+FP_HALF,18*FP_1+FP_HALF,0
dd 14*FP_1+FP_HALF,8*FP_1+FP_HALF,2
dd 20*FP_1+FP_HALF,4*FP_1+FP_HALF,2
dd 15*FP_1+FP_HALF,4*FP_1+FP_HALF,0
dd 20*FP_1+FP_HALF,9*FP_1+FP_HALF,1
dd 18*FP_1+FP_HALF,5*FP_1+FP_HALF,0
dd 25*FP_1+FP_HALF,5*FP_1+FP_HALF,0
dd 28*FP_1+FP_HALF,8*FP_1+FP_HALF,2
dd 25*FP_1+FP_HALF,12*FP_1+FP_HALF,1
dd 28*FP_1+FP_HALF,15*FP_1+FP_HALF,0
dd 25*FP_1+FP_HALF,19*FP_1+FP_HALF,2
dd 28*FP_1+FP_HALF,19*FP_1+FP_HALF,1
dd 24*FP_1+FP_HALF,25*FP_1+FP_HALF,2
dd 28*FP_1+FP_HALF,25*FP_1+FP_HALF,2
dd 27*FP_1+FP_HALF,28*FP_1+FP_HALF,3
dd 4*FP_1+FP_HALF,9*FP_1+FP_HALF,1
dd 7*FP_1+FP_HALF,8*FP_1+FP_HALF,0
HF_ENEMIES equ 28
hfItemSeeds label dword
dd 3*FP_1+FP_HALF,27*FP_1+FP_HALF,4
dd 4*FP_1+FP_HALF,28*FP_1+FP_HALF,3
dd 9*FP_1+FP_HALF,24*FP_1+FP_HALF,2
dd 3*FP_1+FP_HALF,19*FP_1+FP_HALF,0
dd 8*FP_1+FP_HALF,14*FP_1+FP_HALF,3
dd 9*FP_1+FP_HALF,15*FP_1+FP_HALF,2
dd 13*FP_1+FP_HALF,21*FP_1+FP_HALF,1
dd 14*FP_1+FP_HALF,14*FP_1+FP_HALF,3
dd 20*FP_1+FP_HALF,20*FP_1+FP_HALF,0
dd 19*FP_1+FP_HALF,14*FP_1+FP_HALF,2
dd 14*FP_1+FP_HALF,4*FP_1+FP_HALF,0
dd 20*FP_1+FP_HALF,3*FP_1+FP_HALF,5
dd 17*FP_1+FP_HALF,9*FP_1+FP_HALF,3
dd 25*FP_1+FP_HALF,4*FP_1+FP_HALF,2
dd 28*FP_1+FP_HALF,11*FP_1+FP_HALF,0
dd 25*FP_1+FP_HALF,17*FP_1+FP_HALF,3
dd 28*FP_1+FP_HALF,20*FP_1+FP_HALF,3
dd 24*FP_1+FP_HALF,24*FP_1+FP_HALF,0
dd 29*FP_1+FP_HALF,28*FP_1+FP_HALF,3
dd 24*FP_1+FP_HALF,28*FP_1+FP_HALF,2
dd 3*FP_1+FP_HALF,8*FP_1+FP_HALF,1
dd 7*FP_1+FP_HALF,10*FP_1+FP_HALF,3
dd 19*FP_1+FP_HALF,26*FP_1+FP_HALF,0
dd 20*FP_1+FP_HALF,27*FP_1+FP_HALF,3
HF_ITEMS equ 24
.code

View file

@ -0,0 +1,348 @@
; World sprites share casino depth testing. HUD uses native 640x480 pixels.
HFText PROC uses esi edi ebx x:DWORD,y:DWORD,text:DWORD,color:DWORD
local row:DWORD,column:DWORD,cursor:DWORD,bits:DWORD
mov esi,text
mov eax,x
mov cursor,eax
hf_text_char:
movzx eax,BYTE PTR [esi]
test eax,eax
jz hf_text_done
lea edi,font8x8[eax*8]
mov row,0
hf_text_row:
mov eax,row
movzx eax,BYTE PTR [edi+eax]
mov bits,eax
mov column,0
hf_text_pixel:
test bits,80h
jz hf_text_next_pixel
mov eax,y
add eax,row
cmp eax,480
jae hf_text_next_pixel
mov ebx,cursor
add ebx,column
cmp ebx,640
jae hf_text_next_pixel
imul eax,640
add eax,ebx
mov ebx,color
mov screenBuffer[eax*4],ebx
hf_text_next_pixel:
shl bits,1
inc column
cmp column,8
jb hf_text_pixel
inc row
cmp row,8
jb hf_text_row
add cursor,8
inc esi
jmp hf_text_char
hf_text_done:
ret
HFText ENDP
HFNumber PROC x:DWORD,y:DWORD,value:DWORD
invoke FormatNumber,value,OFFSET hfNumber
invoke HFText,x,y,OFFSET hfNumber,00FFE0A0h
ret
HFNumber ENDP
HFLargeNumber PROC uses ebx edi rightEdge:DWORD,y:DWORD,value:DWORD,percent:DWORD
mov ebx,value
mov edi,rightEdge
cmp percent,0
je hf_large_digit
sub edi,28
invoke DrawAnimSprite3D,OFFSET hfDigitsArt,10,edi,y,32,32
hf_large_digit:
mov eax,ebx
xor edx,edx
mov ecx,10
div ecx
mov ebx,eax
sub edi,28
invoke DrawAnimSprite3D,OFFSET hfDigitsArt,edx,edi,y,32,32
test ebx,ebx
jnz hf_large_digit
ret
HFLargeNumber ENDP
HFRenderStatus PROC uses ebx
invoke DrawAnimSprite3D,OFFSET hfHUDArt,0,0,416,640,64
mov eax,hfBullets
cmp hfWeapon,0
je @F
mov eax,hfShells
@@:
invoke HFLargeNumber,86,422,eax,0
invoke HFLargeNumber,208,422,hfHealth,1
invoke HFLargeNumber,466,422,hfArmor,1
; Replace multiplayer FRAGS field with the owned weapon slots.
invoke DrawBox,108,209,35,31,003C3B36h
invoke HFText,228,460,OFFSET hfArmsText,00B0ACA0h
mov eax,00777770h
cmp hfWeapon,0
jne @F
mov eax,00FFE070h
@@:
invoke HFText,230,434,OFFSET hfSlot1Text,eax
cmp hfHasShotgun,0
je hf_status_face
mov eax,00777770h
cmp hfWeapon,1
jne @F
mov eax,00FFE070h
@@:
invoke HFText,258,434,OFFSET hfSlot2Text,eax
hf_status_face:
mov eax,100
sub eax,hfHealth
xor edx,edx
mov ecx,20
div ecx
cmp eax,4
jbe @F
mov eax,4
@@:
shl eax,2
mov ebx,eax
cmp hfHurt,0
je hf_face_idle
add ebx,3
jmp hf_face_draw
hf_face_idle:
mov eax,hfTicks
shr eax,5
xor edx,edx
mov ecx,3
div ecx
add ebx,edx
hf_face_draw:
cmp hfState,1
jne @F
mov ebx,20
@@:
invoke DrawAnimSprite3D,OFFSET hfFacesArt,ebx,296,416,48,64
cmp hfKey,0
je @F
invoke DrawAnimSprite3D,OFFSET hfItemsArt,5,476,420,28,28
@@:
; Clear original ammo glyphs, then draw live reserve counts.
invoke DrawBox,257,209,62,30,002E302Ch
invoke HFText,518,420,OFFSET hfAmmoTypesText,00B0ACA0h
invoke HFNumber,558,420,hfBullets
invoke HFText,590,420,OFFSET hfMaxBulletsText,008E8C80h
invoke HFText,518,434,OFFSET hfShellTypeText,00B0ACA0h
invoke HFNumber,558,434,hfShells
invoke HFText,590,434,OFFSET hfMaxShellsText,008E8C80h
invoke HFText,518,448,OFFSET hfEmptyAmmoText,008E8C80h
invoke HFText,518,462,OFFSET hfEmptyCellText,008E8C80h
ret
HFRenderStatus ENDP
RenderHellPortal3D PROC
cmp hallLevel3D,0
jne hf_portal_done
invoke LoadAnimPack3D,OFFSET hfPaintingArt,OFFSET hfPaintingPath
cmp hfPaintingArt.pixels,0
je hf_portal_done
; Gilt landscape hung flat on the south wall opposite the bar.
invoke DrawCityVertical3D,1,HF_PORTAL_Y,3*FP_1,9*FP_1,FP_1*35/100,FP_1*235/100,HF_PAINTING_MATERIAL
hf_portal_done:
ret
RenderHellPortal3D ENDP
RenderHellFPS3D PROC uses esi edi ebx
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
; Animated lava patch, same bounds as HFTick damage.
mov eax,hfTicks
shr eax,3
and eax,3
add eax,HF_LAVA
invoke DrawCityHorizontal3D,16*FP_1,18*FP_1,17*FP_1,20*FP_1,128,eax
mov esi,OFFSET hfEnemies
mov edi,HF_ENEMIES
hf_render_enemy:
mov eax,[esi].HFEnemy.x
mov hfObject.posX,eax
mov eax,[esi].HFEnemy.y
mov hfObject.posY,eax
mov eax,[esi].HFEnemy.kind
mov eax,hfMonsterPacks[eax*4]
mov hfObject.asset,eax
mov eax,[esi].HFEnemy.pose
mov hfObject.frame,eax
mov hfObject.worldW,FP_1*6/5
mov hfObject.worldH,FP_1*3/2
mov hfObject.groundZ,0
cmp [esi].HFEnemy.kind,3
jne @F
mov hfObject.worldW,FP_1*9/5
mov hfObject.worldH,FP_1*22/10
@@:
invoke DrawCityObject3D,OFFSET hfObject,0
add esi,SIZEOF HFEnemy
dec edi
jnz hf_render_enemy
mov hfObject.asset,OFFSET hfItemsArt
mov hfObject.worldW,FP_1/2
mov hfObject.worldH,FP_1/2
mov hfObject.groundZ,0
xor edi,edi
mov esi,OFFSET hfItemSeeds
hf_render_item:
cmp hfItemTaken[edi*4],0
jne hf_render_item_next
mov eax,[esi]
mov hfObject.posX,eax
mov eax,[esi+4]
mov hfObject.posY,eax
mov eax,[esi+8]
mov hfObject.frame,eax
invoke DrawCityObject3D,OFFSET hfObject,0
hf_render_item_next:
add esi,12
inc edi
cmp edi,HF_ITEMS
jb hf_render_item
mov hfObject.asset,OFFSET hfFireArt
mov hfObject.worldW,FP_1/3
mov hfObject.worldH,FP_1/3
mov hfObject.groundZ,FP_1*3/4
mov eax,hfTicks
shr eax,2
and eax,1
mov hfObject.frame,eax
mov esi,OFFSET hfBolts
mov edi,32
hf_render_bolt:
cmp [esi].HFBolt.life,0
je hf_render_bolt_next
mov eax,[esi].HFBolt.x
mov hfObject.posX,eax
mov eax,[esi].HFBolt.y
mov hfObject.posY,eax
invoke DrawCityObject3D,OFFSET hfObject,0
hf_render_bolt_next:
add esi,SIZEOF HFBolt
dec edi
jnz hf_render_bolt
mov hfObject.asset,OFFSET hfImpactsArt
mov hfObject.worldW,FP_1/3
mov hfObject.worldH,FP_1/3
mov hfObject.groundZ,FP_1*5/6
mov esi,OFFSET hfImpacts
mov edi,HF_SHOT_PELLETS
hf_render_impact:
cmp [esi].HFImpact.life,0
je hf_render_impact_next
mov eax,[esi].HFImpact.x
mov hfObject.posX,eax
mov eax,[esi].HFImpact.y
mov hfObject.posY,eax
mov eax,8
sub eax,[esi].HFImpact.life
shr eax,1
mov hfObject.frame,eax
invoke DrawCityObject3D,OFFSET hfObject,0
hf_render_impact_next:
add esi,SIZEOF HFImpact
dec edi
jnz hf_render_impact
mov hfObject.asset,OFFSET hfPortalArt
mov hfObject.posX,28*FP_1
mov hfObject.posY,29*FP_1
mov hfObject.worldW,FP_1*3/2
mov hfObject.worldH,2*FP_1
mov hfObject.groundZ,0
mov eax,hfTicks
shr eax,2
and eax,7
mov hfObject.frame,eax
invoke DrawCityObject3D,OFFSET hfObject,0
cmp hfState,0
jne hf_render_hud
mov esi,OFFSET hfPistol
mov edi,0
cmp hfWeapon,1
jne hf_weapon_pose
mov esi,OFFSET hfShotgun
hf_weapon_pose:
cmp hfCooldown,0
je hf_weapon_draw
cmp hfWeapon,0
jne hf_shotgun_pose
; Four recovery poses over the nine-tick pistol cycle.
mov eax,9
sub eax,hfCooldown
shr eax,1
inc eax
cmp eax,4
jbe @F
mov eax,4
@@:
mov edi,eax
jmp hf_weapon_draw
hf_shotgun_pose:
mov edi,3
cmp hfCooldown,10
jbe hf_weapon_draw
mov edi,2
cmp hfCooldown,18
jbe hf_weapon_draw
mov edi,1
hf_weapon_draw:
invoke DrawAnimSprite3D,esi,edi,176,176,288,240
cmp hfFlash,0
je @F
invoke DrawAnimSprite3D,OFFSET hfFireArt,0,296,202,48,48
@@:
invoke DrawBox,159,117,2,7,00D0C0A0h
invoke DrawBox,156,120,8,1,00D0C0A0h
hf_render_hud:
cmp hfHurt,0
je @F
invoke DrawBox,0,0,320,3,00902010h
invoke DrawBox,0,0,3,210,00902010h
invoke DrawBox,317,0,3,210,00902010h
@@:
call HFRenderStatus
cmp hfMessageTime,0
je @F
invoke HFText,12,34,hfMessage,00FFD090h
@@:
cmp hfState,0
je hf_render_done
invoke DrawBox,24,80,272,65,00181010h
mov eax,OFFSET hfDeadText
cmp hfState,1
je @F
mov eax,OFFSET hfWinText
@@:
invoke HFText,64,176,eax,00FFB070h
invoke HFText,64,208,OFFSET hfStatsText,00C0A080h
mov eax,hfTicks
xor edx,edx
mov ecx,35
div ecx
invoke HFNumber,80,226,eax
invoke HFNumber,232,226,hfKills
invoke HFNumber,392,226,hfSecrets
cmp hfState,2
jne hf_render_done
mov eax,OFFSET hfRepeatText
cmp hfAwarded,0
je @F
mov eax,OFFSET hfRewardText
@@:
invoke HFText,144,258,eax,00FFE0A0h
hf_render_done:
ret
RenderHellFPS3D ENDP

View file

@ -14,6 +14,19 @@ WindowEvents3D PROC uses ebx esi edi hwnd:DWORD, uMsg:DWORD, wp:DWORD, lp:DWORD
xor eax, eax
ret
@@:
cmp engineMode,MODE_HELL_FPS
jne hf_window_not_mouse
cmp uMsg,201h
jne @F
mov hfFireHeld,1
mov hfFirePending,1
jmp event_done
@@:
cmp uMsg,202h
jne hf_window_not_mouse
mov hfFireHeld,0
jmp event_done
hf_window_not_mouse:
cmp uMsg,202h
jne @F
cmp engineMode,MODE_DARTS
@ -150,6 +163,8 @@ event_fullscreen:
call ToggleFullscreen3D
jmp event_done
event_focus_lost:
mov hfFireHeld,0
mov hfFirePending,0
mov brEntryHold3D,0
mov dtHolding3D,0
mov dtMouseReleased3D,0
@ -512,6 +527,8 @@ 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 engineMode,MODE_HELL_FPS
je br_generic_collision
cmp brActive3D,0
jne br_generic_collision
invoke CanWalkHall3D, px, py
@ -560,6 +577,7 @@ LoadProgress3D PROC uses ebx
mov charmSkullUses, 0
mov charmDevilUses, 0
mov charmEnabledMask3D,7
mov hfCompleted,0
mov brOwned3D,0
mov brEnabled3D,1
invoke CreateFileA, OFFSET savePath3D, 80000000h, 1, 0, 3, 80h, 0
@ -578,7 +596,7 @@ LoadProgress3D PROC uses ebx
cmp eax,saveData3D[36]
jne progress_loaded
mov eax,saveData3D[32]
test eax,0FFFCFFF0h
test eax,0FFF8FFF0h
jnz progress_loaded
test eax,20000h
jz @F
@ -595,6 +613,10 @@ LoadProgress3D PROC uses ebx
shr eax,17
and eax,1
mov brEnabled3D,eax
mov eax,saveData3D[32]
shr eax,18
and eax,1
mov hfCompleted,eax
jmp progress_version_ok
progress_legacy_version:
cmp saveData3D, 33443343h
@ -690,6 +712,10 @@ SaveProgress3D PROC uses ebx
shl ecx,17
or eax,ecx
or eax,charmEnabledMask3D
mov ecx,hfCompleted
and ecx,1
shl ecx,18
or eax,ecx
mov saveData3D[32],eax
invoke PreviewArtifactSignature3D,OFFSET saveData3D
mov saveData3D[36],eax

View file

@ -0,0 +1,237 @@
"""Import pinned Freedoom assets into native ANM1 packs (offline build tool)."""
import argparse
import hashlib
import io
import json
import struct
import urllib.request
import wave
import zipfile
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
SHA256 = "3f9b264f3e3ce503b4fb7f6bdcb1f419d93c7b546f4df3e874dd878db9688f59"
URL = "https://github.com/freedoom/freedoom/releases/download/v0.13.0/freedoom-0.13.0.zip"
class Wad:
def __init__(self, data):
magic, count, offset = struct.unpack_from('<4sII', data)
assert magic == b'IWAD'
self.lumps = {}
for i in range(count):
pos, size, name = struct.unpack_from('<II8s', data, offset + i * 16)
assert pos + size <= len(data)
self.lumps[name.rstrip(b'\0').decode()] = data[pos:pos + size]
self.palette = self.lumps['PLAYPAL'][:768]
def patch(self, name):
data = self.lumps[name]
w, h, left, top = struct.unpack_from('<HHhh', data)
image = Image.new('RGBA', (w, h))
pixels = image.load()
for x in range(w):
pos = struct.unpack_from('<I', data, 8 + x * 4)[0]
previous = -1
while data[pos] != 255:
y, n = data[pos], data[pos + 1]
if y <= previous:
y += previous
previous = y
for dy, index in enumerate(data[pos + 3:pos + 3 + n]):
if y + dy < h:
pixels[x, y + dy] = (*self.palette[index * 3:index * 3 + 3], 255)
pos += n + 4
return image, left, top
def flat(self, name):
image = Image.frombytes('P', (64, 64), self.lumps[name])
image.putpalette(self.palette)
return image.convert('RGBA')
def texture(self, name):
pnames = self.lumps['PNAMES']
patches = [pnames[4+i*8:12+i*8].rstrip(b'\0').decode()
for i in range(struct.unpack_from('<I', pnames)[0])]
for table in ('TEXTURE1', 'TEXTURE2'):
data = self.lumps.get(table)
if not data:
continue
for i in range(struct.unpack_from('<I', data)[0]):
pos = struct.unpack_from('<I', data, 4+i*4)[0]
texname = data[pos:pos+8].rstrip(b'\0').decode()
if texname != name:
continue
w, h = struct.unpack_from('<HH', data, pos+12)
image = Image.new('RGBA', (w, h), (0, 0, 0, 255))
count = struct.unpack_from('<H', data, pos+20)[0]
for j in range(count):
x, y, patch = struct.unpack_from('<hhH', data, pos+22+j*10)
sprite, _, _ = self.patch(patches[patch])
image.paste(sprite, (x, y), sprite)
return image
raise KeyError(name)
def pack(path, images):
w, h = images[0].size
assert all(im.size == (w, h) for im in images)
payload = b''.join(im.tobytes('raw', 'BGRA') for im in images)
path.write_bytes(struct.pack('<5I', 0x314d4e41, w, h, len(images), len(payload)) + payload)
def aligned(wad, names, size=(128, 128)):
frames = [wad.patch(n) for n in names]
# Shared Doom origin, shared scale: no per-frame trim/resize jitter.
x0 = min(-left for _, left, _ in frames)
x1 = max(im.width-left for im, left, _ in frames)
y0 = min(-top for _, _, top in frames)
y1 = max(im.height-top for im, _, top in frames)
scale = min(size[0] / (x1-x0), size[1] / (y1-y0))
result = []
for im, left, top in frames:
canvas = Image.new('RGBA', size)
im = im.resize((max(1, round(im.width*scale)), max(1, round(im.height*scale))), Image.Resampling.NEAREST)
x = round((-left-x0)*scale + (size[0]-(x1-x0)*scale)/2)
y = round((-top-y0)*scale + size[1]-(y1-y0)*scale)
canvas.paste(im, (x, y))
result.append(canvas)
return result
def interface_assets(wad, out, manifest):
"""Doom-layout status bar, native-size glyphs and common-origin face frames."""
pack(out/'hud.bin', [wad.patch('STBAR')[0]])
names = [f'STTNUM{i}' for i in range(10)] + ['STTPRCNT']
digits = []
for name in names:
image = wad.patch(name)[0]
canvas = Image.new('RGBA', (16,16))
canvas.paste(image, (16-image.width,0))
digits.append(canvas)
pack(out/'digits.bin', digits)
names_face = [name for health in range(5)
for name in [f'STFST{health}0', f'STFST{health}1',
f'STFST{health}2', f'STFOUCH{health}']] + ['STFDEAD0']
faces = []
for name in names_face:
image = wad.patch(name)[0]
canvas = Image.new('RGBA', (24,32))
canvas.paste(image, ((24-image.width)//2,32-image.height))
faces.append(canvas)
pack(out/'faces.bin', faces)
impacts = ['PUFFA0','PUFFB0','PUFFC0','PUFFD0']
pack(out/'impacts.bin', aligned(wad, impacts, (32,32)))
manifest['packs'].update(hud=['STBAR'], digits=names, faces=names_face, impacts=impacts)
# Authored landscape, entirely composed from Freedoom textures and sprites.
# Opaque power-of-two 512x256 BGRA surface; wall renderer wraps with bit masks.
image = Image.new('RGBA',(512,192),(20,9,8,255))
sky = wad.texture('SKY3').resize((488,166),Image.Resampling.NEAREST)
image.paste(sky,(12,12))
draw = ImageDraw.Draw(image)
draw.polygon([(12,136),(65,98),(96,126),(139,86),(174,125),(222,104),
(273,142),(321,90),(376,123),(423,94),(500,130),(500,180),(12,180)],
fill=(34,20,22,255))
lava = wad.flat('LAVA1').resize((488,48),Image.Resampling.NEAREST)
image.paste(lava,(12,132))
for x,h in [(42,70),(92,52),(362,62),(428,84)]:
stone = wad.texture('GSTONE1').resize((28,h),Image.Resampling.NEAREST)
image.paste(stone,(x,164-h))
draw.rectangle((x-3,160-h,x+30,165-h),fill=(107,72,45,255))
for name,x,y,w,h in [('BOSSA1',224,48,70,116),('TROOA1',145,104,34,58),
('SARGA1',330,112,42,52)]:
actor = wad.patch(name)[0]
actor.thumbnail((w,h),Image.Resampling.NEAREST)
image.alpha_composite(actor,(x,y))
# Layered gilt moulding and dark inner bevel: a painting, not an open portal.
for inset,color in [(0,(42,25,12)),(2,(199,147,64)),(4,(104,61,22)),
(6,(236,191,99)),(8,(116,74,28)),(10,(26,13,8))]:
draw.rectangle((inset,inset,511-inset,191-inset),outline=(*color,255),width=2)
for x in range(18,495,16):
draw.line((x,4,x+3,7),fill=(255,216,126,255),width=1)
draw.line((x,184,x+3,187),fill=(255,216,126,255),width=1)
pack(out/'painting.bin',[image.resize((512,256),Image.Resampling.NEAREST)])
image.save(out/'painting-preview.png')
manifest['packs']['painting'] = ['SKY3','LAVA1','GSTONE1','BOSSA1','TROOA1','SARGA1']
def main():
parser = argparse.ArgumentParser()
parser.add_argument('archive', type=Path)
args = parser.parse_args()
data = args.archive.read_bytes()
assert hashlib.sha256(data).hexdigest() == SHA256, 'Freedoom archive hash mismatch'
archive = zipfile.ZipFile(io.BytesIO(data))
wad = Wad(archive.read(next(n for n in archive.namelist() if n.endswith('/freedoom2.wad'))))
out = ROOT / 'assets/hell'
out.mkdir(parents=True, exist_ok=True)
manifest = {'version': '0.13.0', 'url': URL, 'archive_sha256': SHA256, 'packs': {}}
monsters = {
'soldier': ['POSSA1','POSSB1','POSSC1','POSSD1','POSSE1','POSSF1','POSSG1','POSSH0','POSSI0','POSSJ0','POSSK0','POSSL0'],
'demon': ['SARGA1','SARGB1','SARGC1','SARGD1','SARGE1','SARGF1','SARGH1','SARGI0','SARGJ0','SARGK0','SARGL0','SARGN0'],
'imp': ['TROOA1','TROOB1','TROOC1','TROOD1','TROOE1','TROOF1','TROOH1','TROOI0','TROOJ0','TROOK0','TROOL0','TROOM0'],
'boss': ['BOSSA1','BOSSB1','BOSSC1','BOSSD1','BOSSE1','BOSSF1','BOSSH1','BOSSI0','BOSSJ0','BOSSK0','BOSSL0','BOSSO0'],
'pistol': ['PISGA0','PISGB0','PISGC0','PISGD0','PISGE0'],
'shotgun': ['SHTGA0','SHTGB0','SHTGC0','SHTGD0'],
'fire': ['BAL1A0','BAL1B0','BAL1C0','BAL1D0','BAL1E0'],
'portal': ['TFOGA0','TFOGB0','TFOGC0','TFOGD0','TFOGE0','TFOGF0','TFOGG0','TFOGH0'],
}
for name, names in monsters.items():
# Some Doom rotations are packed in mirrored pairs (A2A8 etc.).
for i, lump in enumerate(names):
if lump not in wad.lumps:
choices = [n for n in wad.lumps if n.startswith(lump[:5]) and len(n) in (6,8)]
if not choices:
raise KeyError(lump)
names[i] = sorted(choices)[0]
images = aligned(wad, names, (192, 160) if name in ('pistol','shotgun') else (128,128))
if name == 'portal':
# Recolour original Freedoom teleport animation to infernal orange/red.
for image in images:
image.putdata([(max(r,g), min(180,g//2), b//4, a)
for r,g,b,a in image.getdata()])
pack(out / (name+'.bin'), images)
manifest['packs'][name] = names
items = ['MEDIA0','ARM1A0','CLIPA0','SHELA0','SHOTA0','BKEYA0','BON2A0']
pack(out/'items.bin', [aligned(wad, [n])[0] for n in items])
manifest['packs']['items'] = items
textures = ['MARBLE1','GSTONE1','SKIN2','BIGDOOR2','SW1GARG','SP_HOT1']
flats = ['FLOOR7_2','F_SKY1','LAVA1','LAVA2','LAVA3','LAVA4']
images = [wad.texture(n).resize((128,128),Image.Resampling.NEAREST) for n in textures]
# F_SKY1 is a marker, use a real stone flat for ceiling.
flats[1] = 'CEIL5_2'
images += [wad.flat(n).resize((128,128),Image.Resampling.NEAREST) for n in flats]
pack(out/'materials.bin', images)
manifest['packs']['materials'] = textures+flats
interface_assets(wad,out,manifest)
for name in ['DSPISTOL','DSSHOTGN','DSSGCOCK','DSPLPAIN','DSPOPAIN','DSFIRSHT','DSPODTH1','DSITEMUP','DSDOROPN','DSTELEPT']:
raw = wad.lumps[name]
fmt, rate, count = struct.unpack_from('<HHI', raw)
assert fmt == 3 and count <= len(raw)-8
with wave.open(str(out/(name.lower()+'.wav')), 'wb') as wav:
wav.setparams((1,1,rate,0,'NONE','not compressed'))
wav.writeframes(raw[24:8+count-16])
for basename in ('COPYING','CREDITS'):
names = [n for n in archive.namelist() if Path(n).name.startswith(basename)]
if names:
(out/(basename+'.txt')).write_bytes(archive.read(names[0]))
else:
url = f'https://raw.githubusercontent.com/freedoom/freedoom/v0.13.0/{basename}.adoc'
(out/(basename+'.txt')).write_bytes(urllib.request.urlopen(url).read())
(out/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n',encoding='utf-8')
sheet = Image.new('RGBA',(6*128,3*128),(30,25,25,255))
for i, name in enumerate(monsters):
raw = (out/(name+'.bin')).read_bytes()
_,w,h,_,_ = struct.unpack_from('<5I',raw)
im = Image.frombytes('RGBA',(w,h),raw[20:20+w*h*4],'raw','BGRA')
im.thumbnail((128,128),Image.Resampling.NEAREST)
sheet.paste(im,((i%6)*128,(i//6)*128),im)
sheet.save(out/'preview.png')
print(f'Imported {len(manifest["packs"])} packs to {out}')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,96 @@
"""Compile the authored 32x32 hell map and validate progression offline."""
from pathlib import Path
from collections import deque
ROOT = Path(__file__).resolve().parents[2]
grid = [[185 for _ in range(32)] for _ in range(32)]
def room(x0,y0,x1,y1):
for y in range(y0,y1+1):
for x in range(x0,x1+1):
grid[y][x] = 0
# Winding route through five arenas, with locked final sanctum.
room(2,23,10,29) # entry
room(3,14,10,21) # barracks
room(5,21,6,23)
room(12,13,21,22) # lava court
room(10,16,12,17)
room(13,3,21,10) # key crypt
room(17,10,18,13)
room(24,3,29,20) # final approach
room(21,6,24,7)
room(23,23,29,29) # boss
room(26,20,27,23)
# Pillars and cover; enough width for movement and projectile dodging.
for x,y in [(5,17),(8,18),(15,16),(19,19),(16,6),(19,7),(26,10),(27,16),(25,26)]:
grid[y][x] = 186
# Two discoverable secret doors.
room(2,7,8,11)
room(5,11,6,14)
room(24,23,25,23)
room(18,25,21,28)
room(21,26,23,27)
doors = [(5,12,0),(6,12,0),(22,26,1),(22,27,1),(22,6,2),(22,7,2)]
for x,y,_ in doors:
grid[y][x] = 188
for y in range(1,31):
for x in range(1,31):
if grid[y][x] == 185:
if x >= 23 and y >= 22:
grid[y][x] = 190 # infernal sanctum
elif x >= 23:
grid[y][x] = 187 # flesh-lined approach
elif x >= 12 and y <= 11:
grid[y][x] = 186 # crypt stone
# kind: 0 soldier, 1 melee, 2 imp, 3 boss. Coordinates at tile centres.
enemies = [(7,25,0),(8,28,0),(4,20,0),(8,16,1),(9,20,2),(4,15,0),
(14,20,2),(20,21,0),(20,14,2),(13,14,1),(18,17,1),(15,18,0),
(14,8,2),(20,4,2),(15,4,0),(20,9,1),(18,5,0),
(25,5,0),(28,8,2),(25,12,1),(28,15,0),(25,19,2),(28,19,1),
(24,25,2),(28,25,2),(27,28,3),(4,9,1),(7,8,0)]
# item kinds correspond to items.bin: health, armour, bullets, shells, shotgun, key, secret.
items = [(3,27,4),(4,28,3),(9,24,2),(3,19,0),(8,14,3),(9,15,2),
(13,21,1),(14,14,3),(20,20,0),(19,14,2),(14,4,0),(20,3,5),
(17,9,3),(25,4,2),(28,11,0),(25,17,3),(28,20,3),
(24,24,0),(29,28,3),(24,28,2),(3,8,1),(7,10,3),(19,26,0),(20,27,3)]
for x,y,_ in enemies+items:
assert grid[y][x] == 0, (x,y)
def reachable(open_groups=()):
seen = {(3,26)}
todo = deque(seen)
while todo:
x,y = todo.popleft()
for nx,ny in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]:
if (nx,ny) in seen or not (0<=nx<32 and 0<=ny<32):
continue
if grid[ny][nx] and (nx,ny) not in [(x,y) for x,y,group in doors if group in open_groups]:
continue
seen.add((nx,ny))
todo.append((nx,ny))
return seen
assert (20,3) in reachable() and (27,28) not in reachable()
assert (27,28) not in reachable((0,1)), 'Secrets must not bypass the key gate'
assert (27,28) in reachable((2,)), 'Secrets must remain optional'
assert all((x,y) in reachable((0,1,2)) for x,y,_ in enemies+items)
# Conservative ammunition budget: half the pellets land at minimum damage.
bullets = 80 + 40*sum(kind == 2 for _,_,kind in items)
shells = 16 + 12*sum(kind == 3 for _,_,kind in items)
enemy_hp = sum((55,105,85,1100)[kind] for _,_,kind in enemies)
assert (bullets + shells*7)*8//2 > enemy_hp
lines = ['; Generated by tools/generators/make_hell_level.py.','.data','hfMapTemplate label byte']
lines += [' db '+','.join(map(str,row)) for row in grid]
lines += ['hfEnemySeeds label dword']
lines += [f' dd {x}*FP_1+FP_HALF,{y}*FP_1+FP_HALF,{kind}' for x,y,kind in enemies]
lines += [f'HF_ENEMIES equ {len(enemies)}','hfItemSeeds label dword']
lines += [f' dd {x}*FP_1+FP_HALF,{y}*FP_1+FP_HALF,{kind}' for x,y,kind in items]
lines += [f'HF_ITEMS equ {len(items)}','.code']
(ROOT/'src_graphics/hell_fps_map.inc').write_text('\n'.join(lines)+'\n',encoding='ascii')
print(f'Hell map: {len(enemies)} enemies, {len(items)} pickups, key gate and two secrets validated')

View file

@ -0,0 +1,53 @@
"""Build and execute native MASM combat, renderer, progression and save checks."""
from pathlib import Path
import argparse
import hashlib
import subprocess
from PIL import Image
from check_artifacts import toolchain
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'build/scratch'
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--window',action='store_true')
options = parser.parse_args()
OUT.mkdir(parents=True, exist_ok=True)
before = {p: hashlib.sha256(p.read_bytes()).digest() for p in (ROOT/'runtime').rglob('*') if p.is_file()}
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())
commands = [[ml,'/c','/coff','/Cp','/nologo','/I.','/DHELL_FPS_TEST=1',
'/Fobuild/scratch/hell-fps.obj','poker.asm'],
[link,'/nologo','/SUBSYSTEM:CONSOLE','/LIBPATH:'+str(lib),
'/OUT:build/scratch/hell-fps-test.exe','build/scratch/hell-fps.obj',
'kernel32.lib','winmm.lib','user32.lib','ws2_32.lib','gdi32.lib']]
if options.window:
commands[0].insert(1,'/DHELL_FPS_WINDOW_TEST=1')
for command in commands:
subprocess.run(list(map(str,command)),cwd=ROOT,check=True,timeout=120)
try:
result = subprocess.run([str(OUT/'hell-fps-test.exe')],cwd=OUT if options.window else ROOT,timeout=90)
finally:
assert before == {p: hashlib.sha256(p.read_bytes()).digest() for p in (ROOT/'runtime').rglob('*') if p.is_file()}, 'Test changed player saves'
assert result.returncode == 0, f'Native test failed at stage/exception {result.returncode:#x}'
for name in (('window',) if options.window else ('entry','court','portal','victory')):
raw = (OUT/f'hell-{name}.bgra').read_bytes()
assert len(raw) == 640*480*4
image = Image.frombytes('RGBA',(640,480),raw,'raw','BGRA').convert('RGB')
image.save(OUT/f'hell-{name}.png')
assert len(image.getcolors(640*480)) > 100, f'{name}: blank renderer'
if options.window:
print('PASS: real Win32 window, 120 presented frames, mouse-down/up events, firing, Mick Gordon playback, six SFX channels, clean return')
return
print('PASS: painting entrance, collision/LOS, multi-target seven-pellet shotgun, '
'single-ray pistol, armour/death/retry, gates/secrets, '
'1050 AI ticks, finite-ammo boss fight with dodging, first-clear reward, '
'native/preview save roundtrips, return, 4 native render captures')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,509 @@
.data
hfTestStage dd 1
hfTestIo dd 0
hfTestFrame db 'build\scratch\hell-entry.bgra',0
hfTestCourt db 'build\scratch\hell-court.bgra',0
hfTestHall db 'build\scratch\hell-portal.bgra',0
hfTestWin db 'build\scratch\hell-victory.bgra',0
hfTestPreview db 'build\scratch\hf-prev.dat',0
hfTestProfileDir db 'build\scratch',0
hfTestProfileName db 'hell-profile.dat',0
hfTestBoss dd 0
hfTestStrafe dd 5000
.code
HFTestAimBoss PROC uses esi edi ebx
local vx:DWORD,vy:DWORD,best:DWORD
mov esi,hfTestBoss
mov eax,[esi].HFEnemy.x
sub eax,playerX
sar eax,8
mov vx,eax
mov eax,[esi].HFEnemy.y
sub eax,playerY
sar eax,8
mov vy,eax
mov best,80000000h
xor edi,edi
hf_test_aim_angle:
mov eax,tableCos[edi*4]
sar eax,8
imul eax,vx
mov ebx,eax
mov eax,tableSin[edi*4]
sar eax,8
imul eax,vy
add eax,ebx
cmp eax,best
jle @F
mov best,eax
mov playerAngle,edi
@@:
inc edi
cmp edi,512
jb hf_test_aim_angle
ret
HFTestAimBoss ENDP
HFTestCapture PROC uses ebx path:DWORD
invoke CreateFileA,path,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET screenBuffer,SCREEN_SIZE*4,OFFSET hfTestIo,0
invoke CloseHandle,ebx
ret
HFTestCapture ENDP
RunHellFPSTest PROC uses esi edi ebx
call InitHall3D
call LoadTextures3D
call LoadHallAssets3D
call InitializeCasinoSprites3D
mov hfTestStage,2
call HFLoad
test eax,eax
jz hf_test_fail
mov playerX,HF_PORTAL_X
mov playerY,29*FP_1
mov playerAngle,128
mov engineMode,MODE_WALK
call UpdateCamera3D
call CheckHallInteractions3D
; Approach must be within the standard interaction distance.
mov playerY,FP_1*294/10
call CheckHallInteractions3D
cmp interactTargetId,INTERACT_HELL_FPS
jne hf_test_fail
invoke CanWalk3D,HF_PORTAL_X,FP_1*294/10
cmp eax,1
jne hf_test_fail
; Old standing frame no longer blocks the west aisle.
invoke CanWalk3D,FP_1*25/10,11*FP_1
cmp eax,1
jne hf_test_fail
mov playerX,4*FP_1
mov playerY,12*FP_1
mov playerAngle,256
call UpdateCamera3D
mov interactTargetId,0
call CheckHallInteractions3D
cmp interactTargetId,INTERACT_HELL_FPS
je hf_test_fail
mov playerX,HF_PORTAL_X
mov playerY,FP_1*255/10
mov playerAngle,128
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderHall3D
invoke HFTestCapture,OFFSET hfTestHall
mov hfOldX,HF_PORTAL_X
mov hfOldY,FP_1*294/10
mov hfOldAngle,128
mov hfOldMap,OFFSET casinoMap
mov hfOldFloor,100
mov hfOldLevel,0
mov hfOldEye,HALL_STANDING_EYE
mov hfOldCeiling,FP_1*5/2
call HFReset
mov hfTestStage,3
call RenderHellFPS3D
invoke HFTestCapture,OFFSET hfTestFrame
invoke HFLineClear,3*FP_1+FP_HALF,26*FP_1+FP_HALF,7*FP_1+FP_HALF,26*FP_1+FP_HALF
cmp eax,1
jne hf_test_fail
invoke HFLineClear,3*FP_1+FP_HALF,26*FP_1+FP_HALF,13*FP_1,26*FP_1+FP_HALF
test eax,eax
jnz hf_test_fail
invoke CanWalk3D,0,0
test eax,eax
jnz hf_test_fail
mov hfTestStage,4
; Same hitscan must stop at wall, then damage the nearest target in open space.
mov hfEnemies.x,12*FP_1
mov hfEnemies.y,26*FP_1+FP_HALF
mov hfEnemies.hp,100
mov hfEnemies[SIZEOF HFEnemy].hp,0
call HFFire
cmp hfEnemies.hp,100
jne hf_test_fail
mov hfEnemies.x,7*FP_1
mov hfCooldown,0
call HFFire
cmp hfEnemies.hp,100
jae hf_test_fail
cmp hfBullets,78
jne hf_test_fail
mov hfTestStage,5
mov bankroll,12345
invoke HFPlayerCanWalk,hfEnemies.x,hfEnemies.y
test eax,eax
jnz hf_test_fail
mov hfArmor,20
invoke HFDamagePlayer,10
cmp hfHealth,95
jne hf_test_fail
cmp hfArmor,15
jne hf_test_fail
mov hfArmor,0
invoke HFDamagePlayer,1000
cmp hfState,1
jne hf_test_fail
call HFReset
cmp hfState,0
jne hf_test_fail
cmp hfHealth,100
jne hf_test_fail
cmp bankroll,12345
jne hf_test_fail
; Natural shotgun pickup and consumption; no injected weapon ownership.
mov playerX,3*FP_1+FP_HALF
mov playerY,27*FP_1+FP_HALF
call HFPickups
cmp hfHasShotgun,1
jne hf_test_fail
cmp hfShells,16
jne hf_test_fail
call HFFire
cmp hfShells,15
jne hf_test_fail
mov hfTestStage,12
; Three separated targets: pistol hits centre only; one shell hits all three.
call HFReset
mov esi,OFFSET hfEnemies
mov ecx,HF_ENEMIES
@@:
mov [esi].HFEnemy.hp,0
add esi,SIZEOF HFEnemy
loop @B
mov hfEnemies.x,8*FP_1
mov hfEnemies.y,FP_1*103/4
mov hfEnemies.hp,1000
mov hfEnemies[SIZEOF HFEnemy].x,8*FP_1
mov hfEnemies[SIZEOF HFEnemy].y,FP_1*106/4
mov hfEnemies[SIZEOF HFEnemy].hp,1000
mov hfEnemies[2*SIZEOF HFEnemy].x,8*FP_1
mov hfEnemies[2*SIZEOF HFEnemy].y,FP_1*109/4
mov hfEnemies[2*SIZEOF HFEnemy].hp,1000
call HFFire
cmp hfEnemies.hp,1000
jne hf_test_fail
cmp hfEnemies[SIZEOF HFEnemy].hp,1000
jae hf_test_fail
cmp hfEnemies[2*SIZEOF HFEnemy].hp,1000
jne hf_test_fail
mov hfCooldown,0
mov hfWeapon,1
mov hfShells,2
call HFFire
cmp hfShells,1
jne hf_test_fail
cmp hfEnemies.hp,1000
jae hf_test_fail
cmp hfEnemies[2*SIZEOF HFEnemy].hp,1000
jae hf_test_fail
mov esi,OFFSET hfImpacts
mov ecx,HF_SHOT_PELLETS
@@:
cmp [esi].HFImpact.life,8
jne hf_test_fail
add esi,SIZEOF HFImpact
loop @B
; The whole pellet cone must stop at the separating wall.
mov hfEnemies.x,12*FP_1
mov hfEnemies.hp,1000
mov hfEnemies[SIZEOF HFEnemy].x,12*FP_1
mov hfEnemies[SIZEOF HFEnemy].hp,1000
mov hfEnemies[2*SIZEOF HFEnemy].x,12*FP_1
mov hfEnemies[2*SIZEOF HFEnemy].hp,1000
mov hfCooldown,0
call HFFire
cmp hfEnemies.hp,1000
jne hf_test_fail
cmp hfEnemies[SIZEOF HFEnemy].hp,1000
jne hf_test_fail
cmp hfEnemies[2*SIZEOF HFEnemy].hp,1000
jne hf_test_fail
cmp hfShells,0
jne hf_test_fail
mov hfCooldown,0
call HFFire
cmp hfCooldown,0
jne hf_test_fail
call HFReset
mov hfTestStage,6
mov playerX,21*FP_1+FP_HALF
mov playerY,6*FP_1+FP_HALF
call HFUse
cmp hfMap[6*32+22],188
jne hf_test_fail
mov playerX,20*FP_1+FP_HALF
mov playerY,3*FP_1+FP_HALF
call HFPickups
cmp hfKey,1
jne hf_test_fail
mov playerX,21*FP_1+FP_HALF
mov playerY,6*FP_1+FP_HALF
call HFUse
cmp hfMap[6*32+22],0
jne hf_test_fail
mov playerX,5*FP_1+FP_HALF
mov playerY,13*FP_1+FP_HALF
call HFUse
call HFUse
cmp hfSecrets,1
jne hf_test_fail
mov playerX,23*FP_1
mov playerY,27*FP_1
call HFUse
cmp hfSecrets,2
jne hf_test_fail
mov hfTestStage,7
; Real AI/projectile/pickup loop stress; no alternate Python simulation.
call HFReset
mov playerX,14*FP_1
mov playerY,21*FP_1
mov playerAngle,430
mov hfWeapon,1
mov hfHasShotgun,1
mov hfShells,80
mov hfFireHeld,1
mov ebx,1050
hf_test_combat:
mov hfHealth,100
mov hfState,0
call HFTick
dec ebx
jnz hf_test_combat
mov hfFireHeld,0
call RenderHellFPS3D
invoke HFTestCapture,OFFSET hfTestCourt
mov hfTestStage,11
; Isolated boss encounter: real pickups, finite ammo, normal damage and ticks.
call HFReset
mov esi,OFFSET hfEnemies
mov ecx,HF_ENEMIES
hf_test_isolate_boss:
cmp [esi].HFEnemy.kind,3
jne hf_test_disable_enemy
mov hfTestBoss,esi
jmp @F
hf_test_disable_enemy:
mov [esi].HFEnemy.hp,0
@@:
add esi,SIZEOF HFEnemy
loop hf_test_isolate_boss
mov playerX,3*FP_1+FP_HALF
mov playerY,27*FP_1+FP_HALF
call HFPickups
mov playerX,4*FP_1+FP_HALF
mov playerY,28*FP_1+FP_HALF
call HFPickups
mov playerX,8*FP_1+FP_HALF
mov playerY,14*FP_1+FP_HALF
call HFPickups
mov playerX,27*FP_1+FP_HALF
mov playerY,24*FP_1+FP_HALF
mov playerAngle,128
mov hfFireHeld,1
mov ebx,1400
hf_test_boss_fight:
cmp playerX,28*FP_1+FP_HALF
jl @F
mov hfTestStrafe,-5000
@@:
cmp playerX,26*FP_1+FP_HALF
jg @F
mov hfTestStrafe,5000
@@:
mov eax,playerX
add eax,hfTestStrafe
push eax
invoke HFPlayerCanWalk,eax,playerY
pop ecx
test eax,eax
jz @F
mov playerX,ecx
@@:
call HFTestAimBoss
call HFTick
cmp hfState,0
jne hf_test_fail
cmp hfKills,1
je hf_test_boss_killed
dec ebx
jnz hf_test_boss_fight
jmp hf_test_fail
hf_test_boss_killed:
mov hfFireHeld,0
mov hfTestStage,8
; Locked exit, then one-time reward, then repeat use cannot pay twice.
call HFReset
mov bankroll,10000
mov hfCompleted,0
mov playerX,28*FP_1
mov playerY,28*FP_1
call HFUse
cmp hfState,0
jne hf_test_fail
mov esi,OFFSET hfEnemies
mov ecx,HF_ENEMIES
@@:
mov [esi].HFEnemy.hp,0
add esi,SIZEOF HFEnemy
loop @B
call HFUse
cmp hfState,2
jne hf_test_fail
cmp bankroll,16660
jne hf_test_fail
call HFUse
cmp bankroll,16660
jne hf_test_fail
mov playerX,26*FP_1
mov playerY,24*FP_1
mov playerAngle,96
mov hfTicks,18000
mov hfKills,28
mov hfSecrets,2
call RenderHellFPS3D
invoke HFTestCapture,OFFSET hfTestWin
mov hfTestStage,9
; Save extension survives native signed profile round trip.
mov nativeProfilesDirPtr3D,OFFSET hfTestProfileDir
mov nativeProfileActive3D,1
invoke CopyCString,OFFSET saveFileName,OFFSET hfTestProfileName
call BuildNativeProfileDir3D
call SaveGame
mov hfCompleted,0
call LoadSave
cmp hfCompleted,1
jne hf_test_fail
cmp bankroll,16660
jne hf_test_fail
mov hfTestStage,10
; Preview save uses the same new bit without changing its 40-byte format.
mov nativeProfileActive3D,0
mov arrivalConfirmed3D,1
invoke CopyCString,OFFSET savePath3D,OFFSET hfTestPreview
call SaveProgress3D
mov hfCompleted,0
call LoadProgress3D
cmp hfCompleted,1
jne hf_test_fail
cmp bankroll,16660
jne hf_test_fail
mov arrivalConfirmed3D,0
call LeaveHellFPS3D
cmp engineMode,MODE_WALK
jne hf_test_fail
cmp playerX,HF_PORTAL_X
jne hf_test_fail
cmp hallLevel3D,0
jne hf_test_fail
call FreeHellFPS3D
xor eax,eax
ret
hf_test_fail:
mov eax,hfTestStage
ret
RunHellFPSTest ENDP
IFDEF HELL_FPS_WINDOW_TEST
.data
hfWindowShot db 'build\scratch\hell-window.bgra',0
hfMusicStatus db 'status hellmusic mode',0
hfAudioMode db 32 dup(0)
.code
RunHellFPSWindowTest PROC uses esi ebx
call InitPaths3D
call Create3DWindow
test eax,eax
jz hf_window_failed
; Keep test window modest and avoid taking keyboard/mouse focus.
cmp fullscreen3D,0
je @F
call ToggleFullscreen3D
@@:
invoke SetWindowPos,h3DWnd,0,80,80,800,640,14h
call InitHall3D
call LoadTextures3D
call LoadHallAssets3D
call InitializeCasinoSprites3D
mov playerX,HF_PORTAL_X
mov playerY,FP_1*294/10
mov playerAngle,128
mov engineMode,MODE_WALK
call BeginHellFPS3D
cmp engineMode,MODE_HELL_FPS
jne hf_window_failed
cmp hfMusicError,0
jne hf_window_failed
cmp hfSoundFailures,0
jne hf_window_failed
cmp hfVolumeError,0
jne hf_window_failed
mov settingsVolume,10
call HFUpdateMusicVolume
cmp hfVolumeError,0
jne hf_window_failed
mov eax,SIZEOF hfVolumePrefix-1
cmp DWORD PTR hfVolumeCommand[eax],00303533h ; "350", not full volume 1000
jne hf_window_failed
mov settingsVolume,0
call HFUpdateMusicVolume
cmp hfVolumeError,0
jne hf_window_failed
mov eax,SIZEOF hfVolumePrefix-1
cmp WORD PTR hfVolumeCommand[eax],0030h
jne hf_window_failed
mov settingsVolume,5
call HFUpdateMusicVolume
cmp hfVolumeError,0
jne hf_window_failed
invoke ShowWindow,h3DWnd,4
mov esi,120
hf_window_frame:
invoke PeekMessageA,OFFSET msg3D,0,0,0,PM_REMOVE
test eax,eax
jz hf_window_simulate
invoke TranslateMessage,OFFSET msg3D
invoke DispatchMessageA,OFFSET msg3D
jmp hf_window_frame
hf_window_simulate:
mov deltaTime3D,17
add animationClock3D,17
cmp esi,110
jne @F
invoke WindowEvents3D,h3DWnd,201h,1,0
@@:
cmp esi,35
jne @F
invoke WindowEvents3D,h3DWnd,202h,0,0
@@:
call HandleHellFPS3D
call RenderHellFPS3D
call Present3D
invoke Sleep,17
dec esi
jnz hf_window_frame
invoke HFTestCapture,OFFSET hfWindowShot
cmp hfBullets,80
jae hf_window_failed
cmp hfFireHeld,0
jne hf_window_failed
invoke mciSendStringA,OFFSET hfMusicStatus,OFFSET hfAudioMode,32,0
test eax,eax
jnz hf_window_failed
cmp DWORD PTR hfAudioMode,'yalp'
jne hf_window_failed
call LeaveHellFPS3D
call FreeHellFPS3D
invoke DestroyWindow,h3DWnd
xor eax,eax
ret
hf_window_failed:
call FreeHellFPS3D
mov eax,111
ret
RunHellFPSWindowTest ENDP
ENDIF