feat: implement secret fortune teller room, lived-in den v3, and bar camera stabilization

- Secret Fortune Teller Room: arcade lantern toggle, sliding timber panel, dedicated staircase and compact cellar chamber

- Lived-in den pass v3: cloth hangings, draped workbench, props/clutter, centered entrance, and custom light sources

- Bar camera stabilization: remove alcohol horizon sway while retaining drink animations

- Synchronize binaries and update launcher hash to EADBA9C8C9BD5B7E3CC4CE486ED647C1CE9383D1DCB6EBC94FE755EA1F4BF927
This commit is contained in:
Artvasil09 2026-10-20 17:29:52 +03:00
parent d0cee618cf
commit 51c6512675
57 changed files with 2982 additions and 125 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -0,0 +1,8 @@
{
"tool": "Built-in image_gen",
"references": "User-supplied blue draped room and cluttered tarot den, used as mood references only.",
"den": "Use case: stylized-concept. Asset type: four independent sprites in a strict 2x2 atlas, genuine transparent background. The two input images are mood/style references only for a lived-in cluttered fortune teller den, NOT edit targets and NOT a request for a full room background. Top left: a complete wide doorway-like frame of heavy draped indigo/teal fabric, upper valance and two gathered floor-length side curtains with ropes, uneven hems and faded stitched stars; the large opening between curtains is fully transparent. Top right: one weathered dark-wood occult bookcase, full height including feet, stuffed chaotically with leaning mismatched old books, odd glass bottles, small skull, rolled parchment, a blue cloth hanging over one shelf, spilled dried herbs; no neat repeated rows. Bottom left: a low cluttered workbench, uneven blue cloth draped over it, open grimoire, loose tarot cards, used candles, jars, piled scrolls and pouches, complete legs. Bottom right: an untidy FLOOR pile of crooked books, loose parchment sheets, a worn leather bag, rolled scrolls, little fabric bundles; three-quarter slightly elevated view, cohesive single clutter cluster. All four fully contained in their own quadrant with transparent gutters, no objects crossing cell edges. Painterly realistic retro adventure game sprite art, tactile worn fabrics, wood and dusty glass; sapphire and faded blue textiles, silver/copper/old wood accents, not uniformly blue lacquer or shiny palace furniture. No lettering, no watermarks, no scenery or ground plane.",
"floor_clutter": "Use case: stylized-concept. Asset type: genuine transparent 2x2 sprite atlas for a lived-in, slightly untidy fortune teller den, in painterly realistic retro 2.5D adventure game art. Four isolated independent FLOOR clutter clusters, each completely within its quadrant, generous transparent gutters. Top left: squat blackened copper cauldron on three short feet with faint pale blue vapor, stained rim, a wooden spoon, herbs and a few potion jars lying beside it, cold cyan reflected light, no orange fire. Top right: an old open travel chest with wrinkled blue fabric, loosely spilling scrolls, book, strings of beads, little worn pouches and loose cards, full base. Bottom left: an untidy low wooden tray and little carpet scrap with mismatched glass bottles, spilled blue crystals, loose paper sheets, feather pen and partly unrolled parchment, floor height. Bottom right: three short half-melted thick blue-white candles sitting on uneven wax-coated floor stones with loose cards and tied scroll beside them, pale blue flames and no large external halo. Worn, handmade, dusty, slightly disordered rather than polished or symmetrical. Blue/teal textiles, natural dark wood/copper accents, muted parchment. View slightly elevated from standing eye height for floor objects, coherent scale, no full room background, no floor plane outside each object's silhouette, no text, no watermark.",
"transparent_cleanup": "Use case: background-extraction. Edit target: the referenced four-object atlas. Remove the entire dark grey/brown gradient studio backdrop, every floor shadow patch and every broad glow halo. Make all space between and around the four isolated object clusters truly transparent with zero alpha, including all four corners and gutters. Keep the cauldron with its small pale blue vapor, chest with spilling contents, tray with parchment and bottles, and blue-white candles with cards exactly intact, same placement, same details and color. Do not repaint objects. Preserve fine herbs, parchment edges, wax, loose cards and little blue candle flames. Transparent background, no checkerboard painted into image, no new room or floor plane.",
"materials": "Use case: stylized-concept. Asset type: opaque square 2x2 material texture atlas, exactly four equal square quadrants without gutters or border, orthographic flat texture scans for a worn, lived-in blue tarot den. Each quadrant fills its entire area and is independently tileable. Top left: heavy faded midnight-blue/teal fabric wall hanging with irregular natural vertical folds, patching and subtle worn embroidered threads, slightly dusty uneven cloth, no rigid panels, no stone blocks, no large symbols. Top right: overlapping slack indigo fabric canopy panels with diagonal soft sagging folds, subtle faded floral weave, dusty cloth, no wood coffer grid or stone ceiling. Bottom left: old irregular dark blue-grey rough stone flags with dusty joints and scattered tiny wax stains, no shiny patterned tiles. Bottom right: threadbare blue/teal woven rug with faded muted rust and silver geometric accents, small all-over repeated Persian-like weave, irregular wear but no outer border, no big medallion. Blue remains dominant, secondary natural muted accents, no uniform polished royal-blue luxury design. Detailed tactile painterly game textures suitable for 256px per quadrant, evenly lit with no directional shadows baked across edges, no text, no objects, no gradients."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -0,0 +1,3 @@
Generated with the built-in image generation tool, transparent background.
Production sprite atlas for an existing 640x480 retro 1990s raycaster casino game, detailed hand-painted pixel art realistic proportions, crisp readable silhouettes, no text, no UI. Transparent background. Exact regular 2x2 grid in a square 1024x1024 image, each quadrant a separate isolated asset with transparent gutters and nothing crossing quadrant boundaries. TOP LEFT: a mysterious elderly female tarot fortune teller, silver grey wavy hair, deep plum shawl with small gold embroidery, seated facing viewer behind a round dark carved walnut table covered with dark aubergine cloth, both hands resting next to a small fan of illustrated tarot cards, two short burning wax candles, tiny smoky glass crystal ball, complete table pedestal and feet visible, chair and entire woman fully visible, warm candlelight and cool muted jade accents, maximum occupied height 450px and width 420px centered in quadrant with floor baseline y=480 within quadrant. TOP RIGHT: exactly same woman and table same scale same anchor same props same face same clothes, very subtle alternate idle pose looking down toward cards with fingers slightly shifted and candle flames flickering, baseline same. BOTTOM LEFT: a tall antique curio bookcase, dark carved wood, aged books, glass bottles, skull ornament, brass moon globe, warm candles on shelf, isolated object front-facing complete feet and top, no room or backdrop, baseline y=480 within quadrant. BOTTOM RIGHT: hanging rectangular ornate dark burgundy velvet tapestry with brass suspension rod, central large golden moon and scattered embroidered stars, rich woven textile, tassels, flat front view isolated on transparency, no text. No cast floor plane, no background wall. Consistent viewpoint slightly looking downward onto tabletop as a standing person; no dramatic perspective or fisheye. These are world sprites not game scene screenshots.

View file

@ -0,0 +1,5 @@
{
"tool": "Built-in image_gen",
"wall": "Use case: stylized-concept. Asset type: production 2x2 transparent atlas of four distinct WALL decorations for a compact blue/silver tarot room in a retro first-person 2.5D game. Strict four equal quadrant grid, each silhouette entirely contained with wide transparent gutter. Top left: ornate horizontal silver framed display of three illustrated tarot cards behind glass, deep blue velvet backing, hanging moon and star charms; no readable text. Top right: small wall-mounted dark blue carved scroll rack with three rolled parchment scrolls, a hanging silver key-ring and cyan sealing wax, front elevation, no floor support. Bottom left: hanging bundle of silvery blue eucalyptus, teal dried herbs and blue ribbon, with small crystal pendants, front elevation. Bottom right: suspended silver crescent lantern with pale icy-blue glass, hanging from a complete short silver chain, front elevation, small enclosed blue-white flame, complete upper hook. Genuine transparent background, no room, no floor plane, no text or watermark. Rich painterly realistic retro adventure sprite finish, details recognizable after 256px reduction, crisp silhouettes. Primarily light blue, sapphire, cyan, silver, no dominant brown, gold, orange or purple. Each object independent, no touching adjacent cells.",
"cabinets": "Use case: stylized-concept. Asset type: four separate blue/silver occult room furniture sprites on genuinely transparent background, strict 2 by 2 equal quadrant grid with generous gutters and no overlap. Top left a tall narrow blackened sapphire wood apothecary cabinet with silver carved frame, glass doors and four shelves filled with distinctly shaped cyan potion bottles, tiny blue crystals, closed blue drawers below, full feet. Top right a low wide blue carved book cupboard, two shelves of different-sized dark blue books, rolled pale scrolls, silver clasps, full feet and flat top surface suitable for placing a crystal ball. Bottom left a small wall mounted silver framed glass relic case containing a single pale blue crystal, antique silver key and a lunar charm on blue velvet. Bottom right a small closed ornate dark blue occult chest with silver corners and crescent emblem, three-quarter view, complete base. Consistent richly detailed painterly pixel-adventure game art, furniture front elevation except chest, legible after 256px scaling, no perspective room or floor plane. Cool palette sapphire, cyan, light blue, silver, no dominant brown, gold, purple or orange. No people, text or watermarks, no large glow outside sprite silhouettes."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -0,0 +1,6 @@
{
"tool": "Built-in image_gen; first atlas edited from fortune-room-v1.png, other two generated.",
"character": "Use case: precise-object-edit. Edit the referenced game sprite atlas: preserve the 2 by 2 quadrant arrangement, object sizes, isolated silhouettes, genuinely transparent background and full furniture legs. Replace the two fortune teller poses in the top row with a mysterious seated figure wearing a deep midnight blue hooded mantle, silver celestial embroidery and light blue cloth highlights. The entire face inside the hood must be completely invisible, pitch-black shadow, no eyes, nose, mouth or hair. Left pose hands resting beside tarot cards; right pose one hand slowly reaching over a card and head slightly tilted, face still totally hidden. Same round tarot table and perspective, now blue velvet cloth, silver tassels, cool cyan crystal ball and blue-white candles, no brown/gold/burgundy dominant colors. Bottom left ornate bookcase becomes blackened blue carved wood with silver trim, many cyan glass bottles, rolled scrolls, navy books, lunar instrument and blue crystals. Bottom right tapestry becomes deep rich blue with embroidered silver moon, constellations and pale cyan stars, silver rod and blue tassels. Painterly detailed retro adventure game sprite art legible at 256 pixels each, crisp edges. Keep all four separate, no overlap across quadrant borders, generous transparent gutters; no backdrop, no floor plane, no lettering.",
"decor": "Use case: stylized-concept. Asset type: production sprite atlas for a retro 2.5D first-person tarot chamber. Exactly four separate fully visible upright furniture objects in a strict 2 by 2 grid on genuine transparent background, generous gutters, no touching quadrant boundaries. Top left: tall silver branched candelabrum on a carved dark blue pedestal with five blue-white candle flames and pooled wax, complete base. Top right: ornate silver armillary sphere with a cyan crystal moon on a narrow deep blue stone pedestal, celestial etchings, complete base. Bottom left: two-seat elegant navy-blue velvet waiting bench with silver feet, matching small side table bearing an open grimoire and cyan glass bottle, coherent single prop. Bottom right: tall oval silver framed scrying mirror on stand with smoky pale blue reflections, navy draped cloth and little hanging moon charms, complete feet. No people. Front-facing slightly elevated perspective, richly detailed painterly realistic pixel-adventure sprite style, crisp silhouettes recognizable after scaling to 256px, all feet visible, no floor plane, no shadows outside silhouettes, no lettering. Cool light-blue, sapphire, silver palette, black accents, no dominant brown or gold, no purple.",
"materials": "Use case: stylized-concept. Asset type: a strictly square four-quadrant material texture atlas, 2 by 2 equal square tiles, for a first-person retro tarot chamber. Fully opaque, fills entire image with no external border, no gutters, no labels or objects, orthographic flat texture scan no perspective or lighting falloff. Top left tile: elegant sapphire blue dressed stone wall blocks with subtle silver celestial engraved vertical pilasters, blue-grey joints, clean dry interior no decay, not bathroom tiles. Top right tile: deep midnight blue coffered ceiling panels with small silver constellations and moon carvings in each panel, restrained light cyan details, no luminous gradient. Bottom left tile: cool blue-grey polished geometric stone floor with thin silver geometric inlay, restrained wear, not wooden planks. Bottom right tile: woven sapphire blue velvet carpet with silver lunar geometric medallions and smaller cyan stars, all-over repeat, no big outer border. All four tiles individually seamless on opposing edges with matching scale and lighting, fine detailed tactile painterly game texture finish suitable at 256px per tile. Cohesive blue, pale cyan and silver palette, no brown, tan, gold, purple, orange. No vignette, no text, no lamps."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -0,0 +1,8 @@
{
"tool": "built-in image_gen",
"sources": ["F:/покер/assets/sprites/hall/guest-walk-v2.png", "F:/покер/assets/sprites/hall/basement-door-v2.png"],
"packs": ["assets/animations/hall/guest-walk-v2.bin", "assets/animations/hall/guest-walk-left-v2.bin", "assets/animations/hall/basement-door-v2.bin"],
"walking": "Use case: identity-preserve. Production transparent game sprite sheet, retro detailed painted 1990s casino raycaster sprites, matching the six people in reference image exactly. Create ONE coherent sheet, EIGHT columns and SIX rows, equal slots, full body with empty gutters and all shoes visible. Each ROW is one person: row1 stocky gray-haired man gray suit red tie; row2 brunette bob woman emerald pantsuit; row3 balding mustached cream dinner-jacket man; row4 redhead blue knee-length dress woman; row5 dark-skinned man green brocade waistcoat white shirt; row6 older silver-haired woman plum knee-length dress. All face three-quarter RIGHT throughout. Each row shows EIGHT sequential genuinely different phases of a natural walking cycle: left heel strike, left loading, right foot passing with bent lifted knee, left toe-off/right reaching; right heel strike, right loading, left foot passing lifted knee, right toe-off/left reaching. Alternate which foot is forward clearly, flex knees and ankles, counter-swing arms, dresses sway with knees; no duplicates, no sliding rigid legs. Preserve faces, clothing, anatomy and head/body proportions across all eight slots. Camera fixed, same height and baseline. Transparent background with actual alpha. No scene, no shadows on floor, no labels, no text or borders. Output wide square large sprite atlas, not poster.",
"door": "Use case: precise-object-edit. Reference is existing casino prop sheet, use ONLY its middle-right heavy iron vault door as identity reference. Produce a production game texture extraction sheet with THREE equal COLUMNS on transparent canvas. LEFT column: full-height heavy rectangular dark worn steel door LEAF alone perfectly FRONT ORTHOGRAPHIC, brass rivets, inset rounded steel panel, hinge plates at left, small brass locking mechanism right, rich detailed retro painted pixel sprite. IMPORTANT remove the large central handwheel entirely: plain uninterrupted steel at its old location. No frame, no pipes, no floor, leaf only. MIDDLE column: the stationary door FRAME alone, straight-on front orthographic, heavy industrial riveted steel jambs and lintel, brass plumbing around top and right and small amber lamp on upper right. Centre of frame is an EMPTY transparent opening, NO door leaf or wheel. RIGHT column: separate large round BRASS FIVE-SPOKE HANDWHEEL alone facing straight at camera, exactly circular, central hub, clean transparency between spokes, no steel background; centered at middle of its slot with generous blank margin. Match the reference door and warm casino lighting, not a futuristic door. All three extracted pieces wholly visible, isolated, crisp texture, real alpha transparency. No scene, no floor shadows, no people, no text, no labels, no border. Large landscape sheet three columns, components must not touch.",
"normalization": "Offline ANM1 packing; one fixed scale per appearance; eight phases plus mirrors; supporting foot anchored at bottom row. Door handwheel rotates on a fixed steel leaf before native hinge motion."
}

Binary file not shown.

View file

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

Binary file not shown.

View file

@ -1190,6 +1190,24 @@ wall_sample_material:
mov eax, [ebx+eax*4]
jmp wall_shade_pixel
wall_sample_indoor_hd:
cmp edx,132
jb wall_sample_other_hd
cmp edx,135
ja wall_sample_other_hd
mov ebx,fortuneMaterialArt3D.pixels
test ebx,ebx
jz wall_fortune_material_missing
sub edx,132
shl edx,18
add ebx,edx
jmp wall_room_hd_coordinates
wall_fortune_material_missing:
mov edx,92
mov eax,edi
shr eax,16
and eax,127
jmp wall_sample_standard
wall_sample_other_hd:
cmp edx,122
jb wall_sample_existing_hd
cmp edx,125

Binary file not shown.

View file

@ -7,7 +7,11 @@ Runtime remains MASM32 assembly. Existing player saves and unrelated edits stay
- [x] Six visually distinct roaming guests, including women; matching seated poses.
- [x] Cabaret with changing acts, audience seats, a programme and meaningful interaction.
- [x] Player-controlled stairs and an opening steel basement door, without timed camera travel.
- [x] Distant doorway continuation, automatic wheel-then-hinge opening and distance-driven visitor gait.
- [x] Upper lounge with its own bar and access to Hell Tower, Buckshot and Russian roulette.
- [x] Secret pull-down lantern, manually walked cellar stairs and furnished fortune teller room with an honest unavailable-dialogue placeholder.
- [x] Independent blue/silver tarot chamber and stairs, concealed hooded face, extra celestial furniture and native palette/collision checks.
- [x] Centred tarot-room entrance with matched stair continuation; worn drapery, cloth ceiling and irregular floor/workbench clutter based on the supplied references.
## Following Integration
@ -17,6 +21,7 @@ Runtime remains MASM32 assembly. Existing player saves and unrelated edits stay
- [ ] More ergonomic billiards power control without changing the verified physics/rules.
- [ ] Scene audit: dimensions, fixtures, intersections and dedicated materials.
- [ ] Video poker; then consider baccarat and Sic Bo as separate complete games.
- [ ] Real offline fortune teller dialogue and tarot interpretations; teacher form and grade lottery remain deferred with it.
An unchecked item is not implemented or not yet verified. Asset generation alone
does not count as a finished feature. Published build details belong in the

View file

@ -23,20 +23,36 @@ Russian roulette. Their collision and interaction centres remain shared.
The bar uses a full-body bartender in the walking view and a separate close
serving animation when seated. Bar stools do not obstruct that seated view.
The original iron door remains in the east-facing alcove at (28.5,10.5), using
the same door artwork as before the stair expansion. Its control opens it sideways
without moving the player
or change modes. The closed leaf blocks the doorway. Walking through its open
The iron door remains in the original east-facing alcove. Its frame is at
(28.38,10.5), while the leaf and hinge are recessed to x=28.9. Approaching from
the hall or the upper end of the basement stairs automatically requests opening;
the old E interaction remains an optional request, not a required teleport.
The dogging handwheel turns through seventeen texture phases over 700ms, then
the leaf swings through 90 degrees over 1000ms around a fixed hinge. A native
ray/rotated-plane intersection renders perspective and occlusion. It does not
slide sideways or turn to face the camera. Leaving the wider safety zone starts
a 1200ms hold, followed by the reverse sequence: shut the leaf, then lock the
handwheel. Both sides of the doorway and the whole swept area hold it open.
Returning during either phase reverses from the current pose without snapping;
the wheel must unlock fully before the leaf can reopen. The wider hold radius
prevents boundary jitter. The closed leaf blocks the doorway.
The same leaf/frame is rendered and collision-checked in the overlapping stair
landing, including the return journey; a closed door cannot be bypassed from below.
Walking through its open
threshold enters the concrete stairwell, which also uses normal movement. The
old timed descent has been removed. The descent goes east, with a level landing
where the player turns north into the existing bunker. The ticket counter is
restored to its original (29,7.5) position.
Both sides of all stair entrances show the adjacent native scene through a
depth-tested aperture. Remote wall rays begin just beyond that aperture, not
depth-tested aperture. The former eight-unit view cutoff has been replaced with
a bound beyond the diagonal of the whole map: moving away no longer hides a
doorway's continuation. Remote wall rays begin just beyond that aperture, not
inside a closed map cell behind the camera. Foreground geometry still occludes
the view. This repairs the former visible-wall/walk-through-wall mismatch;
room-local translations at thresholds are retained, with no timed player travel.
Destination pixel depth is retained so the swinging door can occlude the view
through the opening. Descending risers meet the near edge of their treads.
## Infernal Portal
@ -52,12 +68,22 @@ locked across those phases; the brazier fires have four separate phases.
Walking into the open aperture, or interacting with it, starts the existing
Hell Tower mode. Returning cannot immediately retrigger it until the player
backs away from the entrance.
Both levers remain usable after activation. Interacting again toggles the selected
lever back, disables walking/E entry immediately and shows the sealed portal.
The handle returns through the same six poses over 600ms; toggling mid-motion
reverses from its current pose. Both handles must finish in their enabled position
to reopen the portal. Interaction prompts distinguish pulling from returning.
## Visitors
Six new appearances include three men and three women, with separate standing,
walking and seated frames. Both visitor rotations have a mixed cast. Dialogue
names follow the selected appearance. Existing routes and challenge identities
names follow the selected appearance. Walking now uses eight distinct generated
phases per appearance, plus a mirrored pack selected by camera-relative travel.
The phase follows successful displacement (one cycle per 0.75 world units), not
elapsed time: waiting or being blocked does not march in place. Supporting feet
are normalized to the floor baseline, with the old standing/seated canvas padding
compensated. Existing routes and challenge identities
are retained. These are front/three-quarter sprites, not complete eight-direction
animation sets. Upper-lounge guests are currently seated, not another roaming
population; this limitation is deliberate and not presented as finished NPC AI.
@ -83,6 +109,8 @@ performer sprites and audience applause recordings are not included in this pass
Generated using the built-in image generator, with no API key:
- `assets/sprites/hall/guest-cast-v1.png`
- `assets/sprites/hall/guest-walk-v2.png`
- `assets/sprites/hall/basement-door-v2.png`
- `assets/sprites/hall/cabaret-ensemble-v1.png`
- `assets/sprites/hall/cabaret-musicians-v1.png`
- `assets/sprites/hall/upper-materials-v1.png`
@ -92,10 +120,11 @@ Generated using the built-in image generator, with no API key:
- `assets/sprites/hall/upper-braziers-v3.png`
Exact prompts and packing outputs: `assets/sprites/hall/prompts-v7.json` and
`assets/sprites/hall/prompts-v8.json`.
`assets/sprites/hall/prompts-v8.json`. The new door/walking prompts are in
`assets/sprites/hall/prompts-v9.json` (built-in image_gen, no API key).
Packers: `pack_guest_cast.py`, `pack_cabaret_ensemble.py`,
`pack_cabaret_musicians.py`, `pack_upper_materials.py`, `pack_upper_v2.py`,
`pack_upper_motion.py`
`pack_upper_motion.py`, `pack_door_walk_v2.py`
under `tools/generators`. Original source images are preserved.
## Verification
@ -114,8 +143,19 @@ door/ticket positions, the two-lever lock, intermediate/final lever poses, all
twelve visible vortex phases, tower entry/return and single/dual-thread pixel
parity at the stair entrance. `--prove-seam-regression` disables only aperture
near clipping and confirms that the old solid rear-wall block reappears.
The new tests include far doorway views, automatic opening before any E press,
wheel-before-hinge sequencing, eight native gait captures, reverse-facing art,
blocked movement and equal gait phase after the same travel at 16/32ms updates.
An additional native return-path test starts below a closed door without E,
checks that the body is stopped while the wheel turns, then walks back into the
hall after automatic opening.
All six visitors are also updated for two simulated minutes by the native movement
procedures, checking walkable positions and bounded distance-driven gait phases.
`--prove-distance-regression` restores just the old eight-unit cutoff and must
fail the actual native distant-doorway assertion. The shipping loader probe
also checks both 48-frame gait packs and the 18-texture door pack from root/TEMP.
Published on 2026-10-18 to `poker.exe` and `bin/poker.exe`, with the existing
Previous release, published on 2026-10-18 to `poker.exe` and `bin/poker.exe`, with the existing
launcher integrity hash updated. Both copies and `build/graphics/poker.exe` have
SHA-256 `C0B5A1DD9A678E68599724138AF009D29C935EEFDD6D70D28789AABF071395F8`.
Native scene/input tests, asset loading from root/TEMP, card-rank parity and the
@ -124,3 +164,171 @@ suite and an interactive end-to-end playthrough were not rerun for this pass.
The remaining poker, roulette, achievements, billiards controls and new-game work
is tracked separately in `CASINO_NEXT_PASS.md`; it is not claimed complete here.
## 2026-10-19 Release
Published the distance/door/gait repair to root `poker.exe`, `bin/poker.exe` and
`build/graphics/poker.exe`; updated the existing C# launcher's integrity hash.
All three game copies have SHA-256
`70D16BF2DB85AC23E098B9B31EB4CD9A6664D6E51FEDE470CC6116C3E9F94339`.
Fresh native navigation/rendering/gait tests and shipping procedure parity pass,
including reverse-side closed-door collision and automatic return traversal.
Terminal/lantern parity, billiards rules/ownership parity, 208 card faces and
shipping asset loading from root/TEMP also pass. Negative controls reproduce
the old distant-wall cutoff and missing aperture near clipping. The game runtime
is still assembly-only; C# is used only by the existing launcher packaging.
No full interactive desktop playthrough or full legacy action suite is claimed.
### Portal Switching Update
The same day's follow-up adds reversible lever interaction and immediate portal
shutdown. Root/bin/build game copies and launcher integrity metadata were updated
to SHA-256 `36C1CF0734F3C1E0F30F52E76C236FE0261D76E9089BF5AB5B7FBEDEE4E58FF4`.
Native tests cover both enabled levers remaining reachable, pull/return prompts,
walking and E entry blocked immediately after shutdown, the visible sealed arch,
mid-stroke reversal without a position jump, full release and reactivation.
Tested/shipping procedure parity, billiards and terminal regressions pass.
### Automatic Door Closing Update
The latest build closes the basement door after the player leaves its safety
zone and a 1200ms hold expires. The leaf shuts before the wheel locks; approaching
again reverses either phase without a jump. The upper stair landing and the full
leaf swing remain protected while occupied. A manual E request also expires.
Native assertions cover both sides, the hold delay, one update per frame,
closing/locking order, mid-motion reversal, full closed collision and reopening.
Three native captures show closing, locking and the fully closed door.
Published root/bin/build game copies and launcher metadata use SHA-256
`ADF4CE0A7ECAED4D0293A3791FA8B1CA204C931E5F78FE70C1DAD8166BF7AD24`.
### Secret Fortune Teller Room
The west arcade now has a pull-down lantern beside the slots. E lowers the
lantern, then slides a concealed timber panel aside. E again returns it; the
panel pauses closing while the player occupies its threshold or remains anywhere
inside the secret staircase/chamber, so a pending close cannot trap a visitor.
The original eastern steel basement door and lottery stand have not moved.
Walking through the opening enters a separate, manually traversed west-facing
stairwell (route 3), descending to a compact 5x4-metre cellar (hall level 3),
reduced from the initial 8x9-metre layout. All camera
height changes follow the player's position. Doorway apertures show both ends
at near and far distances; returning uses the same physical staircase.
The chamber and its staircase use dedicated sapphire stone walls, a silver-star
coffered ceiling, blue-grey floor tiles and a blue lunar rug, not the ordinary
basement's worn concrete/green walls. The seated figure wears a blue mantle;
both idle poses keep the entire face hidden inside the hood. The table has tarot
cards, blue-white candles and a crystal ball. A matching curio bookcase, lunar
tapestry, two floor candelabra, armillary sphere, scrying mirror and velvet bench
with a grimoire side table furnish the room without blocking the walking route.
A second potion cabinet, low book cupboard, relic display, chest, card vitrine,
scroll rack and hanging herbs fill the wall/corner space. The fronts use fixed
world-aligned planes rather than camera-facing billboards; the two largest
cabinets and bench also have depth geometry. The room's central walking route
and stair aperture remain clear, with furniture collision checked natively.
The ambient light is dim. Eight world-space point sources at the blue-white
candles, crystal and lantern positions supply local cool illumination. Native
lighting reconstructs each surface position from scene depth and its camera ray,
including height, before applying distance falloff. Flame cores stay bright
only near an actual light. This is not a camera-centred glow or a flat blue tint.
It runs only in the tarot chamber, before the HUD and doorway composition.
Artwork was generated/edited with the built-in image tool. The five version-2
source PNGs and exact prompts are in `assets/sprites/hall/fortune-*-v2.*`.
`tools/generators/pack_fortune_room.py` packs sixteen transparent 256px sprites into
`assets/animations/hall/fortune-room-v2.bin` and four independent surfaces into
`fortune-materials-v2.bin`. Materials 132-135 are resolved separately, without
overwriting any shared casino or basement texture. Fixed prop surfaces use
virtual IDs 136-151 against the same sprite pack. Wall and plane renderers
handle them at every distance, with bounded fallback when an art pack is absent.
All game rendering, navigation and interaction remain native assembly.
E at the fortune teller opens the existing native "Not implemented yet" modal.
Closing it preserves the room, camera and bankroll. There is deliberately no
fake AI dialogue, tarot result, student form, networking or model dependency.
Those features remain deferred until a real dialogue approach is selected.
`fortune_room_test.inc` runs within the fresh MASM hall harness: lantern targeting
and toggle prompts, pull/open poses, closed collision, per-frame animation guard,
near/far doorway views, full walking descent/return, continuous stair eye height,
room furniture collision, two idle poses, modal open/close without spending,
occupied-threshold safety and closing after return. Native captures are written
to `build/hall-expansion/fortune-*.bmp` and `.png`. The shipping asset-path probe
also checks both new art packs. Palette assertions cover the chamber, both side
views, the close-up and the staircase; native assertions cover the extra furniture
collisions, material offsets, point-light falloff and darker-versus-unlit output.
Additional native captures cover oblique furniture and the eastern cabinets.
A full interactive desktop playthrough is not
claimed by these automated native tests.
### Blue Chamber Release (2026-10-20)
Published root/bin/build game copies and the launcher integrity metadata use
SHA-256 `3C6E6B85A3B695B5DD52EB4F1E8BE0345E9DE4D4028D269B925B1F5AAF2C3BFA`.
Fresh native navigation, lighting, furnished-room and dialogue checks pass.
Shipping procedure parity, billiards rules/ownership, all 208 card faces,
terminal regressions and asset loading from root/TEMP also pass.
The parity reader now optionally normalizes branches to named procedure entries,
as it already did calls, so the same tail branch remains comparable across
different object layouts. It still compares the destination procedure name and
all instruction opcodes; ordinary card/terminal comparisons keep their defaults.
### Lived-In Den And Centred Entrance
The room-side entrance is now centred on the east wall: Y=10..12 in a room
spanning Y=9..13. Its centreline Y=11 also meets the fortune teller directly.
The unchanged secret staircase remains at Y=9..11; the lower landing translates
Y by +1 metre on entry and -1 on return. Both doorway views use the same inverse
translation, so the visible continuation agrees with physical walking. The
arcade lantern and the separate steel basement door/lottery stand stay put.
The blue palace-like panelling and ceiling are replaced by worn fabric wall
hangings, a slack cloth canopy, dusty irregular flagstones and a faded rug.
Gathered curtains frame the fortune teller. A stuffed, mismatched curio shelf,
draped workbench, leaning book piles, loose papers, travel bag, open chest,
cauldron, bottles/tray, melted floor candles and hanging herbs make the room
less tidy without closing the central walking aisle. Furniture remains fixed
in world space. The concealed face, local blue light and unavailable dialogue
are retained. Additional low light sources correspond to the floor candles.
Version-3 sources: `assets/sprites/hall/fortune-den-v3.png`,
`fortune-clutter-v3.png`, `fortune-materials-v3.png` and the exact built-in image
generation/edit prompts in `fortune-den-v3.prompts.json`. The offline packer
retains the original sixteen frames and adds eight new ones, yielding
`assets/animations/hall/fortune-room-v3.bin` (256x256x24) and
`fortune-materials-v3.bin` (256x256x4). Surface IDs 152-159 select the new frames.
No external runtime, model, service or library is added.
Fresh native tests cover centred entry alignment, actual descent and return,
the centred doorway mask, clutter collision, art bounds/headers, lighting and
the dialogue placeholder. `fortune-doorway-centered.png` records the room-side
opening; the usual room/side/oblique captures show the new furnishing pass.
Published root/bin/build executables and launcher metadata now use SHA-256
`C65E1B1EE5CE8DDB79EADFED982619378780EE4E866BC3D2F3A17DA83E26446F`.
Fresh native tests and shipping procedure parity pass, including off-centre
crossings and the inverse landing translation. Billiards ownership/rules, card
ranks, terminals and root/TEMP asset loading pass as well. This does not claim
a full interactive desktop playthrough.
### Bar Camera Without Alcohol Sway
Removed alcohol strength, intoxication accumulation and its per-frame horizon
offset. Drink prices, purchases, serving/drinking animations and both bar views
are unchanged. Walking now restores the exact standing horizon and eye height,
including recovery from a previously shifted camera. Staircase height remains
route-controlled.
The native bar fixture exercises 24 visits across both bars, all three drinks,
single-debit protection, animation boundaries, six actual bar renders and 6144
walking updates with exact camera-state assertions. Profile saving is disabled
through the existing arrival-confirmation guard. Removing horizon recovery in a
test-only build is rejected by case 70. The fixture models the return-to-walking
state; it does not automate the Win32 Escape key event loop.
Published game copies and launcher integrity metadata use SHA-256
`EADBA9C8C9BD5B7E3CC4CE486ED647C1CE9383D1DCB6EBC94FE755EA1F4BF927`.
Fresh native checks, shipping procedure parity, billiards, card ranks, terminals,
Russian labels, audio bindings and root/TEMP asset loading pass.

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -5,9 +5,16 @@ 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,132
jb @F
cmp eax,159
ja @F
invoke ResolveFortuneMaterial3D,material
ret
@@:
cmp eax,122
jb @F
cmp eax,130
cmp eax,131
ja @F
invoke ResolveUpperMaterial3D,material
ret

View file

@ -0,0 +1,539 @@
LoadFortuneArt3D PROC
invoke LoadAnimPack3D,OFFSET fortuneArt3D,OFFSET fortuneArtPath3D
invoke ValidateCityPack3D,OFFSET fortuneArt3D,256,256,24
invoke LoadAnimPack3D,OFFSET fortuneMaterialArt3D,OFFSET fortuneMaterialPath3D
invoke ValidateCityPack3D,OFFSET fortuneMaterialArt3D,256,256,4
ret
LoadFortuneArt3D ENDP
FreeFortuneArt3D PROC
invoke FreeAnimPack3D,OFFSET fortuneArt3D
invoke FreeAnimPack3D,OFFSET fortuneMaterialArt3D
ret
FreeFortuneArt3D ENDP
ResolveFortuneMaterial3D PROC material:DWORD
mov eax,material
sub eax,132
cmp eax,3
ja fortune_material_sprite
cmp fortuneMaterialArt3D.pixels,0
je fortune_material_missing
shl eax,18
add eax,fortuneMaterialArt3D.pixels
mov ecx,256
mov edx,256
ret
fortune_material_sprite:
sub eax,4
cmp eax,23
ja fortune_material_missing
cmp fortuneArt3D.pixels,0
je fortune_material_missing
shl eax,18
add eax,fortuneArt3D.pixels
mov ecx,256
mov edx,256
ret
fortune_material_missing:
xor eax,eax
ret
ResolveFortuneMaterial3D ENDP
UpdateFortunePassage3D PROC
mov eax,animationClock3D
cmp eax,fortunePassageClock3D
je fortune_update_done
mov fortunePassageClock3D,eax
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
cmp fortunePassageTarget3D,0
je fortune_update_close
add eax,fortunePassageTime3D
cmp eax,1400
jbe fortune_update_store
mov eax,1400
jmp fortune_update_store
fortune_update_close:
; A pending close waits for the visitor to leave the entire secret branch.
cmp hallLevel3D,3
je fortune_update_done
cmp engineMode,MODE_STAIRS
jne fortune_update_hall_guard
cmp stairRoute3D,3
jne fortune_update_subtract
jmp fortune_update_done
fortune_update_hall_guard:
cmp hallLevel3D,0
jne fortune_update_subtract
cmp playerX,2*FP_1
ja fortune_update_subtract
cmp playerY,FP_1*85/10
jb fortune_update_subtract
cmp playerY,FP_1*115/10
jbe fortune_update_done
fortune_update_subtract:
mov edx,fortunePassageTime3D
sub edx,eax
jae @F
xor edx,edx
@@:
mov eax,edx
fortune_update_store:
mov fortunePassageTime3D,eax
fortune_update_done:
ret
UpdateFortunePassage3D ENDP
FortuneEntranceCanWalk3D PROC px:DWORD,py:DWORD
cmp px,FP_1*17/10
ja fortune_entrance_clear
cmp py,9*FP_1
jb fortune_entrance_clear
cmp py,11*FP_1
ja fortune_entrance_clear
cmp fortunePassageTime3D,1400
jb fortune_entrance_block
cmp py,FP_1*92/10
jb fortune_entrance_block
cmp py,FP_1*108/10
ja fortune_entrance_block
fortune_entrance_clear:
mov eax,1
ret
fortune_entrance_block:
xor eax,eax
ret
FortuneEntranceCanWalk3D ENDP
TryFortuneInteraction3D PROC
cmp interactTargetId,INTERACT_SECRET_LAMP
jne fortune_try_seer
cmp hallLevel3D,0
jne fortune_interaction_none
xor fortunePassageTarget3D,1
call ClearKeys3D
mov eax,1
ret
fortune_try_seer:
cmp interactTargetId,INTERACT_FORTUNE
jne fortune_interaction_none
cmp hallLevel3D,3
jne fortune_interaction_none
call BeginUnavailable3D
mov eax,1
ret
fortune_interaction_none:
xor eax,eax
ret
TryFortuneInteraction3D ENDP
CheckFortuneInteractions3D PROC
cmp hallLevel3D,3
je fortune_check_room
cmp fortunePassageTime3D,1400
jne @F
invoke HallTarget3D,FP_1*13/10,10*FP_1,0,OFFSET fortuneStairHint3D
@@:
cmp fortunePassageTarget3D,0
jne fortune_check_release
invoke HallTarget3D,FP_1*14/10,FP_1*87/10,INTERACT_SECRET_LAMP,OFFSET fortuneLampPull3D
ret
fortune_check_release:
invoke HallTarget3D,FP_1*14/10,FP_1*87/10,INTERACT_SECRET_LAMP,OFFSET fortuneLampReturn3D
ret
fortune_check_room:
mov interactTargetId,0
mov interactPromptPtr,0
invoke HallTarget3D,FP_1*173/10,11*FP_1,0,OFFSET fortuneExitHint3D
invoke HallTarget3D,FP_1*141/10,11*FP_1,INTERACT_FORTUNE,OFFSET fortuneTalk3D
fortune_check_done:
ret
CheckFortuneInteractions3D ENDP
RenderFortuneEntrance3D PROC uses ebx esi
local panelLo:DWORD,panelHi:DWORD,bodyLow:DWORD,bodyHigh:DWORD
; The real wall remains behind this aperture; the portal compositor clips it.
invoke DrawCityBoxSides3D,FP_1*128/100,FP_1*138/100,FP_1*88/10,9*FP_1,0,FP_1*5/2,102
invoke DrawCityBoxSides3D,FP_1*128/100,FP_1*138/100,11*FP_1,FP_1*112/10,0,FP_1*5/2,102
invoke DrawCityBoxSides3D,FP_1*128/100,FP_1*138/100,9*FP_1,11*FP_1,FP_1*23/10,FP_1*5/2,102
mov eax,fortunePassageTime3D
sub eax,280
jns @F
xor eax,eax
@@:
imul eax,2*FP_1
xor edx,edx
mov ecx,1120
div ecx
add eax,9*FP_1
mov panelLo,eax
add eax,2*FP_1
mov panelHi,eax
invoke DrawCityVertical3D,0,FP_1*135/100,panelLo,panelHi,0,FP_1*23/10,1
; A rigid brass arm stays fixed while the lantern itself is pulled down.
mov eax,fortunePassageTime3D
cmp eax,280
jbe @F
mov eax,280
@@:
imul eax,FP_1/5
xor edx,edx
mov ecx,280
div ecx
mov ebx,FP_1*14/10
sub ebx,eax
mov bodyLow,ebx
add ebx,FP_1/2
mov bodyHigh,ebx
invoke DrawCityBoxSides3D,FP_1*131/100,FP_1*155/100,FP_1*857/100,FP_1*883/100,bodyLow,bodyHigh,120
invoke DrawCityHorizontal3D,FP_1*131/100,FP_1*155/100,FP_1*857/100,FP_1*883/100,bodyHigh,110
invoke DrawCityHorizontal3D,FP_1*131/100,FP_1*155/100,FP_1*857/100,FP_1*883/100,bodyLow,110
invoke DrawCityBoxSides3D,FP_1*102/100,FP_1*143/100,FP_1*866/100,FP_1*874/100,FP_1*194/100,2*FP_1,110
invoke DrawCityBoxSides3D,FP_1*140/100,FP_1*146/100,FP_1*866/100,FP_1*874/100,bodyHigh,2*FP_1,110
ret
RenderFortuneEntrance3D ENDP
DrawFortuneFixedProp3D PROC uses esi prop:DWORD,axis:DWORD
local coordinate:DWORD,lo:DWORD,hi:DWORD,zTop:DWORD,material:DWORD
mov esi,prop
mov eax,[esi].CityProp3D.posX
mov ecx,[esi].CityProp3D.posY
cmp axis,0
je @F
xchg eax,ecx
@@:
mov coordinate,eax
mov eax,[esi].CityProp3D.worldW
sar eax,1
mov edx,ecx
sub edx,eax
mov lo,edx
add ecx,eax
mov hi,ecx
mov eax,[esi].CityProp3D.groundZ
add eax,[esi].CityProp3D.worldH
mov zTop,eax
mov eax,[esi].CityProp3D.frame
add eax,136
mov material,eax
invoke DrawCityVertical3D,axis,coordinate,lo,hi,[esi].CityProp3D.groundZ,zTop,material
ret
DrawFortuneFixedProp3D ENDP
RenderFortuneFurnishings3D PROC uses ebx
; Layered cloth, a worn rug and irregular floor clutter make a working den.
invoke DrawCityHorizontal3D,FP_1*135/10,FP_1*176/10,FP_1*97/10,FP_1*124/10,FP_1/100,135
; Furniture fronts have fixed wall axes and solid depth, never billboards.
invoke DrawCityBoxSides3D,FP_1*134/10,FP_1*148/10,FP_1*902/100,FP_1*931/100,0,FP_1*21/10,132
invoke DrawCityHorizontal3D,FP_1*134/10,FP_1*148/10,FP_1*902/100,FP_1*931/100,FP_1*21/10,134
invoke DrawCityBoxSides3D,FP_1*154/10,FP_1*176/10,FP_1*1266/100,FP_1*129/10,FP_1*2/10,FP_1*6/10,135
invoke DrawCityBoxSides3D,FP_1*1771/100,FP_1*1798/100,FP_1*1225/100,FP_1*1285/100,0,FP_1*19/10,132
invoke DrawCityBoxSides3D,FP_1*14,FP_1*152/10,FP_1*1271/100,FP_1*129/10,0,FP_1*6/10,132
; Both poses keep the face hidden; the hands and mantle change instead.
mov eax,animationClock3D
xor edx,edx
mov ecx,6000
div ecx
xor eax,eax
cmp edx,4800
jb @F
inc eax
@@:
mov fortuneSeer3D.frame,eax
invoke DrawFortuneFixedProp3D,OFFSET fortuneTapestry3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneShelf3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneBench3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneMirror3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneCandleA3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneCandleB3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneScrolls3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneHerbs3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneHerbsB3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneHerbsC3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneHanging3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneHanging3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortunePotions3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneBooks3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneChest3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneBookPileA3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneBookPileB3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneCauldron3D,0
invoke DrawFortuneFixedProp3D,OFFSET fortuneTray3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneFloorCandlesA3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneFloorCandlesB3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneNorthLamp3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneSouthLamp3D,1
invoke DrawFortuneFixedProp3D,OFFSET fortuneSeer3D,0
ret
RenderFortuneFurnishings3D ENDP
FortuneLightAt3D PROC uses ebx esi edi wx:DWORD,wy:DWORD,wz:DWORD
local best:DWORD
mov best,0
xor esi,esi
fortune_light_source:
mov eax,wx
sub eax,fortuneLights3D[esi]
imul eax,eax
mov ebx,eax
mov eax,wy
sub eax,fortuneLights3D[esi+4]
imul eax,eax
add ebx,eax
mov eax,wz
sub eax,fortuneLights3D[esi+8]
imul eax,eax
add eax,ebx
cmp eax,900
jae fortune_light_source_next
neg eax
add eax,900
imul eax,fortuneLights3D[esi+12]
xor edx,edx
mov ecx,900
div ecx
cmp eax,best
jbe fortune_light_source_next
mov best,eax
fortune_light_source_next:
add esi,16
cmp esi,FORTUNE_LIGHT_COUNT*16
jb fortune_light_source
mov eax,best
ret
FortuneLightAt3D ENDP
ApplyFortuneLighting3D PROC uses ebx esi edi
local row:DWORD,column:DWORD,planeDepth:DWORD,depth:DWORD
local wx:DWORD,wy:DWORD,wz:DWORD,light:DWORD
local gainR:DWORD,gainG:DWORD,gainB:DWORD,subRow:DWORD,subColumn:DWORD
mov row,0
fortune_light_row:
mov eax,row
cmp eax,viewHorizon3D
jge fortune_light_floor
mov ecx,viewHorizon3D
sub ecx,eax
mov eax,viewCeiling3D
sub eax,viewEye3D
jmp fortune_light_plane
fortune_light_floor:
sub eax,viewHorizon3D
inc eax
mov ecx,eax
mov eax,viewEye3D
fortune_light_plane:
imul eax,SCREEN_H
cdq
idiv ecx
mov planeDepth,eax
mov column,0
fortune_light_column:
imul edi,row,SCREEN_W
add edi,column
mov eax,cityDepthBuffer3D[edi*4]
cmp eax,planeDepth
jle @F
mov eax,planeDepth
@@:
; Limit the reconstructed sample at the horizon before squaring distances.
cmp eax,16*FP_1
jle @F
mov eax,16*FP_1
@@:
mov depth,eax
mov ebx,column
imul cityRayX3D[ebx*4]
shrd eax,edx,16
add eax,playerX
sar eax,12
mov wx,eax
mov eax,depth
imul cityRayY3D[ebx*4]
shrd eax,edx,16
add eax,playerY
sar eax,12
mov wy,eax
mov eax,viewHorizon3D
sub eax,row
imul eax,depth
cdq
mov ecx,SCREEN_H
idiv ecx
add eax,viewEye3D
sar eax,12
mov wz,eax
invoke FortuneLightAt3D,wx,wy,wz
mov light,eax
mov ebx,eax
xor edx,edx
mov ecx,3
div ecx
add eax,48
mov gainR,eax
imul eax,ebx,3
shr eax,2
add eax,64
mov gainG,eax
add ebx,82
mov gainB,ebx
mov subRow,0
fortune_light_block_row:
mov subColumn,0
fortune_light_pixel:
imul esi,subRow,SCREEN_W
add esi,subColumn
add esi,edi
mov ebx,screenBuffer[esi*4]
; Preserve the small blue-white flame cores only beside an actual light.
cmp light,100
jb fortune_light_shade
cmp bl,210
jb fortune_light_shade
cmp bh,180
jae fortune_light_pixel_next
fortune_light_shade:
movzx eax,bl
imul eax,gainB
shr eax,8
mov ecx,eax
movzx eax,bh
imul eax,gainG
shr eax,8
shl eax,8
or ecx,eax
mov eax,ebx
shr eax,16
and eax,255
imul eax,gainR
shr eax,8
shl eax,16
or eax,ecx
mov screenBuffer[esi*4],eax
fortune_light_pixel_next:
inc subColumn
cmp subColumn,2
jb fortune_light_pixel
inc subRow
cmp subRow,2
jb fortune_light_block_row
add column,2
cmp column,SCREEN_W
jb fortune_light_column
add row,2
cmp row,SCREEN_H
jb fortune_light_row
ret
ApplyFortuneLighting3D ENDP
RenderFortuneRoom3D PROC
call RenderFortuneFurnishings3D
call ApplyFortuneLighting3D
ret
RenderFortuneRoom3D ENDP
FortuneCanWalk3D PROC px:DWORD,py:DWORD
cmp px,FP_1*1485/100
ja fortune_walk_north
cmp py,FP_1*98/10
jb fortune_walk_north
cmp py,FP_1*118/10
jbe fortune_walk_block
fortune_walk_north:
cmp py,FP_1*1005/100
ja @F
cmp px,FP_1*146/10
jb @F
cmp px,FP_1*1565/100
jbe fortune_walk_block
@@:
cmp py,FP_1*965/100
ja fortune_walk_south
cmp px,15*FP_1
jb fortune_walk_block
cmp px,FP_1*155/10
jb fortune_walk_south
cmp px,FP_1*171/10
jbe fortune_walk_block
fortune_walk_south:
cmp px,FP_1*172/10
jb @F
cmp py,FP_1*122/10
jae fortune_walk_block
@@:
cmp py,FP_1*1175/100
jb fortune_walk_clear
cmp px,FP_1*146/10
jb fortune_walk_block
cmp px,15*FP_1
jb @F
cmp px,FP_1*158/10
jbe fortune_walk_block
@@:
cmp py,FP_1*121/10
jae fortune_walk_block
fortune_walk_clear:
mov eax,1
ret
fortune_walk_block:
xor eax,eax
ret
FortuneCanWalk3D ENDP
RenderFortuneStairs3D PROC uses ebx esi
local x0:DWORD,x1:DWORD,z0:DWORD,z1:DWORD
; A west-facing descent: the player controls every step, with level landings.
xor esi,esi
fortune_stair_step:
imul eax,esi,FP_1/3
add eax,19*FP_1
mov x0,eax
add eax,FP_1/3
mov x1,eax
imul eax,esi,FP_1/16
mov z0,eax
add eax,FP_1/16
mov z1,eax
invoke DrawCityHorizontal3D,x0,x1,9*FP_1,11*FP_1,z0,134
invoke DrawCityVertical3D,0,x1,9*FP_1,11*FP_1,z0,z1,132
inc esi
cmp esi,24
jb fortune_stair_step
invoke DrawCityHorizontal3D,27*FP_1,31*FP_1,9*FP_1,11*FP_1,FP_1*3/2,134
invoke DrawCityHorizontal3D,16*FP_1,19*FP_1,9*FP_1,11*FP_1,0,134
ret
RenderFortuneStairs3D ENDP
RenderFortuneViews3D PROC
cmp engineMode,MODE_STAIRS
je fortune_views_stairs
cmp hallLevel3D,3
je fortune_views_room
cmp hallLevel3D,0
jne fortune_views_done
cmp fortunePassageTime3D,280
jbe fortune_views_done
cmp playerX,FP_1*14/10
jbe fortune_views_done
invoke DrawInteriorView3D,0,FP_1*138/100,9*FP_1,11*FP_1,0,FP_1*23/10,28*FP_1,0,FP_1*3/2,OFFSET fortuneStairMap3D,MODE_STAIRS,0,134,3
ret
fortune_views_room:
cmp playerX,FP_1*175/10
jae fortune_views_done
invoke DrawInteriorView3D,0,FP_1*174/10,10*FP_1,12*FP_1,0,FP_1*5/2,0,-FP_1,0,OFFSET fortuneStairMap3D,MODE_STAIRS,3,134,3
ret
fortune_views_stairs:
cmp playerX,FP_1*2955/100
jae @F
invoke DrawInteriorView3D,0,FP_1*295/10,9*FP_1,11*FP_1,FP_1*3/2,4*FP_1,-28*FP_1,0,-FP_1*3/2,OFFSET casinoMap,MODE_WALK,0,100,0
@@:
cmp playerX,FP_1*172/10
jbe fortune_views_done
invoke DrawInteriorView3D,0,FP_1*173/10,9*FP_1,11*FP_1,0,FP_1*5/2,0,FP_1,0,OFFSET fortuneRoomMap3D,MODE_WALK,3,134,0
fortune_views_done:
ret
RenderFortuneViews3D ENDP

View file

@ -0,0 +1,67 @@
; Secret tarot chamber. Dialogue deliberately uses the existing unavailable modal.
UpdateFortunePassage3D PROTO
RenderFortuneEntrance3D PROTO
RenderFortuneRoom3D PROTO
RenderFortuneStairs3D PROTO
RenderFortuneViews3D PROTO
CheckFortuneInteractions3D PROTO
TryFortuneInteraction3D PROTO
FortuneCanWalk3D PROTO :DWORD,:DWORD
FortuneEntranceCanWalk3D PROTO :DWORD,:DWORD
LoadFortuneArt3D PROTO
FreeFortuneArt3D PROTO
ResolveFortuneMaterial3D PROTO :DWORD
INTERACT_SECRET_LAMP equ 45
INTERACT_FORTUNE equ 46
.data
fortunePassageTarget3D dd 0
fortunePassageTime3D dd 0
fortunePassageClock3D dd -1
fortuneArt3D AnimPack3D <>
fortuneMaterialArt3D AnimPack3D <>
fortuneArtPath3D db "assets\animations\hall\fortune-room-v3.bin",0
fortuneMaterialPath3D db "assets\animations\hall\fortune-materials-v3.bin",0
fortuneLampPull3D db 69,58,32,207,206,210,223,205,211,210,220,32,212,206,205,192,208,220,0
fortuneLampReturn3D db 69,58,32,194,197,208,205,211,210,220,32,212,206,205,192,208,220,0
fortuneTalk3D db 69,58,32,207,206,195,206,194,206,208,200,210,220,32,209,32,195,192,196,192,203,202,206,201,0
fortuneStairHint3D db 210,192,201,205,192,223,32,203,197,209,210,205,200,214,192,0
fortuneExitHint3D db 203,197,209,210,205,200,214,192,32,194,32,202,192,199,200,205,206,0
fortuneSeer3D CityProp3D <FP_1*141/10,11*FP_1,2*FP_1,FP_1*19/10,0,OFFSET fortuneArt3D>
fortuneShelf3D CityProp3D <FP_1*141/10,FP_1*932/100,FP_1*15/10,FP_1*23/10,17,OFFSET fortuneArt3D>
fortuneTapestry3D CityProp3D <FP_1*1308/100,11*FP_1,4*FP_1,FP_1*28/10,16,OFFSET fortuneArt3D>
fortuneOrb3D CityProp3D <FP_1*146/10,FP_1*126/10,FP_1*6/10,FP_1,5,OFFSET fortuneArt3D,FP_1*6/10>
fortuneBench3D CityProp3D <FP_1*165/10,FP_1*1265/100,FP_1*22/10,FP_1*12/10,6,OFFSET fortuneArt3D>
fortuneMirror3D CityProp3D <FP_1*163/10,FP_1*925/100,FP_1*11/10,2*FP_1,7,OFFSET fortuneArt3D>
fortuneCandleA3D CityProp3D <FP_1*135/10,FP_1*951/100,FP_1*6/10,FP_1*17/10,4,OFFSET fortuneArt3D>
fortuneCandleB3D CityProp3D <FP_1*135/10,FP_1*122/10,FP_1*6/10,FP_1*17/10,4,OFFSET fortuneArt3D>
fortuneCards3D CityProp3D <FP_1*173/10,FP_1*904/100,FP_1*8/10,FP_1*8/10,8,OFFSET fortuneArt3D,FP_1*13/10>
fortuneScrolls3D CityProp3D <FP_1*1768/100,FP_1*1255/100,FP_1*7/10,FP_1*4/10,9,OFFSET fortuneArt3D,FP_1*205/100>
fortuneHerbs3D CityProp3D <FP_1*138/10,FP_1*1296/100,FP_1*5/10,FP_1,10,OFFSET fortuneArt3D,FP_1*12/10>
fortuneHerbsB3D CityProp3D <FP_1*165/10,FP_1*1296/100,FP_1*5/10,FP_1,10,OFFSET fortuneArt3D,FP_1*14/10>
fortuneHerbsC3D CityProp3D <FP_1*156/10,FP_1*1296/100,FP_1*4/10,FP_1*7/10,10,OFFSET fortuneArt3D,FP_1*17/10>
fortuneHanging3D CityProp3D <FP_1*154/10,FP_1*117/10,FP_1*4/10,FP_1*6/10,11,OFFSET fortuneArt3D,FP_1*19/10>
fortunePotions3D CityProp3D <FP_1*177/10,FP_1*1255/100,FP_1*11/10,FP_1*19/10,12,OFFSET fortuneArt3D>
fortuneBooks3D CityProp3D <FP_1*147/10,FP_1*127/10,FP_1*18/10,FP_1*13/10,18,OFFSET fortuneArt3D>
fortuneRelics3D CityProp3D <FP_1*1304/100,FP_1*935/100,FP_1*4/10,FP_1*7/10,14,OFFSET fortuneArt3D,FP_1*16/10>
fortuneChest3D CityProp3D <FP_1*166/10,FP_1*124/10,FP_1*11/10,FP_1,21,OFFSET fortuneArt3D>
fortuneBookPileA3D CityProp3D <FP_1*151/10,FP_1*98/10,FP_1,FP_1*7/10,19,OFFSET fortuneArt3D>
fortuneBookPileB3D CityProp3D <FP_1*158/10,FP_1*122/10,FP_1*9/10,FP_1*6/10,19,OFFSET fortuneArt3D>
fortuneCauldron3D CityProp3D <FP_1*138/10,FP_1*121/10,FP_1*12/10,FP_1*11/10,20,OFFSET fortuneArt3D>
fortuneTray3D CityProp3D <FP_1*171/10,FP_1*123/10,FP_1,FP_1*5/10,22,OFFSET fortuneArt3D>
fortuneFloorCandlesA3D CityProp3D <FP_1*154/10,FP_1*12,FP_1*7/10,FP_1*7/10,23,OFFSET fortuneArt3D>
fortuneFloorCandlesB3D CityProp3D <FP_1*166/10,FP_1*96/10,FP_1*6/10,FP_1*6/10,23,OFFSET fortuneArt3D>
fortuneNorthLamp3D CityProp3D <FP_1*153/10,FP_1*914/100,FP_1*4/10,FP_1*8/10,11,OFFSET fortuneArt3D,FP_1*15/10>
fortuneSouthLamp3D CityProp3D <FP_1*146/10,FP_1*1286/100,FP_1*4/10,FP_1*8/10,11,OFFSET fortuneArt3D,FP_1*15/10>
; Point lights use sixteenth-metre coordinates: X,Y,Z,power. No screen-space halos.
fortuneLights3D dd 216,152,26,174
dd 216,195,26,174
dd 226,187,20,174
dd 226,167,18,145
dd 246,187,34,174
dd 283,201,23,125
dd 245,148,29,160
dd 234,203,29,155
dd 246,192,6,155
dd 266,154,5,155
FORTUNE_LIGHT_COUNT equ ($-fortuneLights3D)/16
.code

View file

@ -159,8 +159,6 @@ barTime3D dd 0
barPhase3D dd 0
barSelection3D dd 0
barPrice3D dd 100,200,0
barStrength3D dd 20000,35000,0
barIntoxication3D dd 0
stairsTime3D dd 0
stairsReturnX3D dd 0
stairsReturnY3D dd 0
@ -168,5 +166,6 @@ stairsReturnAngle3D dd 0
include src_graphics\world_dressing_data.inc
include src_graphics\terminal_data.inc
include src_graphics\hall_expansion_data.inc
include src_graphics\fortune_room_data.inc
include src_graphics\cabaret_data.inc
.code

View file

@ -1,6 +1,16 @@
; Connected stairwells use the normal player movement and body collision routines.
; Only room-local coordinates are translated at a landing, never by a timer.
ResolveUpperMaterial3D PROC material:DWORD
cmp material,131
jne @F
mov eax,basementDoorArt3D.pixels
test eax,eax
jz upper_material_missing
add eax,17*256*512*4
mov ecx,256
mov edx,512
ret
@@:
cmp material,129
jne @F
mov eax,upperPortal3D.frame
@ -20,15 +30,23 @@ ResolveUpperMaterial3D PROC material:DWORD
ret
@@:
cmp material,128
jne @F
mov eax,hallProps3D.pixels
test eax,eax
jz upper_material_missing
add eax,5*256*256*4
mov ecx,256
mov edx,256
ret
jne upper_material_not_door
cmp basementDoorArt3D.pixels,0
je upper_material_missing
imul eax,basementWheelTime3D,16
xor edx,edx
mov ecx,700
div ecx
cmp eax,16
jbe @F
mov eax,16
@@:
imul eax,256*512*4
add eax,basementDoorArt3D.pixels
mov ecx,256
mov edx,512
ret
upper_material_not_door:
cmp material,126
jne @F
mov eax,OFFSET upperFeltPixels3D
@ -177,22 +195,8 @@ BeginWalkableStairs3D ENDP
UpdateInteriorTravel3D PROC
call UpdateUpperEffects3D
mov eax,animationClock3D
cmp eax,interiorDoorClock3D
je interior_travel_check
mov interiorDoorClock3D,eax
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
imul eax,80
add eax,basementDoorOpen3D
cmp eax,basementDoorTarget3D
jbe @F
mov eax,basementDoorTarget3D
@@:
mov basementDoorOpen3D,eax
call UpdateBasementDoor3D
call UpdateFortunePassage3D
interior_travel_check:
cmp engineMode,MODE_STAIRS
je interior_travel_stairs
@ -202,6 +206,24 @@ interior_travel_check:
je interior_travel_bunker
cmp hallLevel3D,2
je interior_travel_upper
cmp hallLevel3D,3
je interior_travel_fortune_room
cmp fortunePassageTime3D,1400
jne interior_travel_east
cmp playerX,FP_1*14/10
ja interior_travel_east
cmp playerY,FP_1*92/10
jb interior_travel_east
cmp playerY,FP_1*108/10
ja interior_travel_east
add playerX,28*FP_1
mov stairRoute3D,3
mov engineMode,MODE_STAIRS
mov worldMap3D,OFFSET fortuneStairMap3D
mov floorTexture3D,134
call UpdateStairHeight3D
ret
interior_travel_east:
cmp playerY,20*FP_1
jb interior_travel_door
cmp playerY,22*FP_1
@ -213,7 +235,7 @@ interior_travel_check:
interior_travel_door:
cmp basementDoorOpen3D,FP_1*9/10
jb interior_travel_done
cmp playerX,FP_1*286/10
cmp playerX,29*FP_1
jb interior_travel_done
cmp playerY,FP_1*99/10
jb interior_travel_done
@ -250,7 +272,23 @@ interior_travel_upper:
mov floorTexture3D,85
call UpdateStairHeight3D
ret
interior_travel_fortune_room:
cmp playerX,FP_1*175/10
jb interior_travel_done
cmp playerY,10*FP_1
jb interior_travel_done
cmp playerY,12*FP_1
jae interior_travel_done
mov stairRoute3D,3
sub playerY,FP_1
mov engineMode,MODE_STAIRS
mov worldMap3D,OFFSET fortuneStairMap3D
mov floorTexture3D,134
call UpdateStairHeight3D
ret
interior_travel_stairs:
cmp stairRoute3D,3
je interior_fortune_thresholds
cmp stairRoute3D,1
jne interior_upper_thresholds
cmp playerX,FP_1*31/10
@ -284,6 +322,22 @@ interior_exit_upper_bottom:
mov worldMap3D,OFFSET casinoMap
add playerX,27*FP_1
mov floorTexture3D,100
jmp interior_finish
interior_fortune_thresholds:
cmp playerX,FP_1*2955/100
jae interior_fortune_top
cmp playerX,FP_1*172/10
ja interior_travel_done
mov hallLevel3D,3
add playerY,FP_1
mov worldMap3D,OFFSET fortuneRoomMap3D
mov floorTexture3D,134
jmp interior_finish
interior_fortune_top:
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
sub playerX,28*FP_1
mov floorTexture3D,100
interior_finish:
mov engineMode,MODE_WALK
mov interactTargetId,0
@ -298,6 +352,22 @@ UpdateStairHeight3D PROC
jne interior_height_done
mov viewHorizon3D,HALF_H
mov viewCeiling3D,FP_1*11/2
cmp stairRoute3D,3
jne @F
mov viewCeiling3D,4*FP_1
mov eax,playerX
sub eax,19*FP_1
jle interior_height_zero
cmp eax,8*FP_1
jle interior_height_fortune
mov eax,8*FP_1
interior_height_fortune:
imul eax,3
cdq
mov ecx,16
idiv ecx
jmp interior_height_store
@@:
cmp stairRoute3D,1
jne interior_height_upper
mov viewCeiling3D,4*FP_1
@ -340,6 +410,7 @@ UpdateInteriorEye3D PROC
je UpdateStairHeight3D
cmp engineMode,MODE_WALK
jne interior_eye_done
mov viewHorizon3D,HALF_H
mov viewEye3D,HALL_STANDING_EYE
mov viewCeiling3D,FP_1*5/2
interior_eye_done:
@ -362,7 +433,7 @@ interior_check_steel:
jb interior_door_clear
cmp py,12*FP_1
jae interior_door_clear
cmp px,FP_1*2835/100
cmp px,FP_1*2875/100
jb interior_door_clear
cmp py,FP_1*96/10
jb interior_portal_blocked
@ -379,15 +450,11 @@ interior_door_clear:
InteriorDoorCanWalk3D ENDP
RenderHallPortals3D PROC uses ebx
; Retain the original iron door artwork in the original east-facing alcove.
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,9*FP_1,FP_1*975/100,0,FP_1*11/5,102
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,FP_1*1125/100,12*FP_1,0,FP_1*11/5,102
mov eax,basementDoorOpen3D
imul eax,2
add eax,FP_1*975/100
lea ebx,[eax+FP_1*3/2]
invoke DrawCityVertical3D,0,FP_1*285/10,eax,ebx,0,FP_1*19/10,128
invoke DrawCityBoxSides3D,FP_1*284/10,FP_1*287/10,FP_1*975/100,FP_1*1125/100,FP_1*19/10,FP_1*11/5,102
; Same east alcove, with a deeper reveal and a stationary iron frame.
invoke DrawCityBoxSides3D,FP_1*284/10,29*FP_1,9*FP_1,FP_1*975/100,0,FP_1*11/5,102
invoke DrawCityBoxSides3D,FP_1*284/10,29*FP_1,FP_1*1125/100,12*FP_1,0,FP_1*11/5,102
invoke DrawCityBoxSides3D,FP_1*284/10,29*FP_1,FP_1*975/100,FP_1*1125/100,FP_1*19/10,FP_1*11/5,102
invoke DrawCityVertical3D,0,FP_1*2838/100,9*FP_1,12*FP_1,-FP_1/10,FP_1*21/10,131
; East stair entrance: projecting jambs, a low first tread and overhead lintel.
invoke DrawCityBoxSides3D,29*FP_1,31*FP_1,FP_1*197/10,20*FP_1,0,FP_1*5/2,102
invoke DrawCityBoxSides3D,29*FP_1,31*FP_1,22*FP_1,FP_1*223/10,0,FP_1*5/2,102
@ -398,6 +465,11 @@ RenderHallPortals3D ENDP
RenderWalkableStairs3D PROC uses ebx esi edi
local treadStart:DWORD,treadEnd:DWORD,z0:DWORD,z1:DWORD
cmp stairRoute3D,3
jne @F
call RenderFortuneStairs3D
ret
@@:
cmp stairRoute3D,1
jne interior_render_upper_steps
; East-facing descent, then a level northward turn into the old bunker.
@ -416,7 +488,7 @@ interior_concrete_step:
sub eax,FP_1/16
mov z1,eax
invoke DrawCityHorizontal3D,treadStart,treadEnd,20*FP_1,23*FP_1,z1,94
invoke DrawCityVertical3D,0,treadEnd,20*FP_1,23*FP_1,z1,z0,92
invoke DrawCityVertical3D,0,treadStart,20*FP_1,23*FP_1,z1,z0,92
inc esi
cmp esi,24
jb interior_concrete_step
@ -539,17 +611,22 @@ CheckUpperInteractions3D PROC
invoke HallTarget3D,3*FP_1,21*FP_1,0,OFFSET upperExitPrompt3D
invoke HallTarget3D,upperStationX3D[0],upperStationY3D[0],INTERACT_BUCKSHOT,OFFSET promptBuckshot
invoke HallTarget3D,upperStationX3D[4],upperStationY3D[4],INTERACT_RUSSIAN_ROULETTE,OFFSET promptRussianRoulette
cmp upperPortalReady3D,1
je upper_check_portal
test upperLeverMask3D,1
jnz @F
jnz upper_check_left_release
invoke HallTarget3D,FP_1*91/10,FP_1*226/10,INTERACT_UPPER_LEFT,OFFSET upperLeftPrompt3D
@@:
jmp upper_check_right
upper_check_left_release:
invoke HallTarget3D,FP_1*91/10,FP_1*226/10,INTERACT_UPPER_LEFT,OFFSET upperLeftRelease3D
upper_check_right:
test upperLeverMask3D,2
jnz upper_check_bar
jnz upper_check_right_release
invoke HallTarget3D,FP_1*119/10,FP_1*226/10,INTERACT_UPPER_RIGHT,OFFSET upperRightPrompt3D
jmp upper_check_bar
jmp upper_check_portal
upper_check_right_release:
invoke HallTarget3D,FP_1*119/10,FP_1*226/10,INTERACT_UPPER_RIGHT,OFFSET upperRightRelease3D
upper_check_portal:
cmp upperPortalReady3D,1
jne upper_check_bar
invoke HallTarget3D,FP_1*105/10,FP_1*237/10,INTERACT_HELL_TOWER,OFFSET upperPortalPrompt3D
upper_check_bar:
cmp playerX,10*FP_1
@ -718,13 +795,13 @@ TryUpperInteraction3D PROC
jne upper_interaction_none
cmp interactTargetId,INTERACT_UPPER_LEFT
jne @F
or upperLeverMask3D,1
jmp upper_interaction_handled
xor upperLeverMask3D,1
jmp upper_interaction_lever
@@:
cmp interactTargetId,INTERACT_UPPER_RIGHT
jne @F
or upperLeverMask3D,2
jmp upper_interaction_handled
xor upperLeverMask3D,2
jmp upper_interaction_lever
@@:
cmp interactTargetId,INTERACT_HELL_TOWER
jne upper_interaction_none
@ -733,6 +810,10 @@ TryUpperInteraction3D PROC
mov upperPortalEntered3D,1
call InitHellTower3D
jmp upper_interaction_handled
upper_interaction_lever:
; Do not wait for the next animation tick to disable both entry paths.
mov upperPortalReady3D,0
jmp upper_interaction_handled
upper_interaction_locked:
invoke CopyString3D,OFFSET upperPortalLocked3D,OFFSET flashMessage
mov flashTimer,120
@ -775,24 +856,36 @@ UpdateUpperEffects3D PROC uses ebx esi
xor esi,esi
mov ebx,1
upper_effect_lever:
test upperLeverMask3D,ebx
jz upper_effect_next
mov eax,deltaTime3D
cmp eax,100
jbe @F
mov eax,100
@@:
test upperLeverMask3D,ebx
jz upper_effect_release
add eax,upperLeverTime3D[esi*4]
cmp eax,600
jbe @F
mov eax,600
@@:
jmp upper_effect_store
upper_effect_release:
mov ecx,upperLeverTime3D[esi*4]
sub ecx,eax
jae @F
xor ecx,ecx
@@:
mov eax,ecx
upper_effect_store:
mov upperLeverTime3D[esi*4],eax
upper_effect_next:
shl ebx,1
inc esi
cmp esi,2
jb upper_effect_lever
mov upperPortalReady3D,0
cmp upperLeverMask3D,3
jne upper_effects_done
cmp upperLeverTime3D,600
jne upper_effects_done
cmp upperLeverTime3D[4],600

View file

@ -4,6 +4,10 @@ UpdateStairHeight3D PROTO
RenderWalkableStairs3D PROTO
RenderUpperLounge3D PROTO
RenderHallPortals3D PROTO
UpdateBasementDoor3D PROTO
DrawBasementLeaf3D PROTO
RenderStairDoor3D PROTO
StairDoorCanWalk3D PROTO :DWORD,:DWORD
CheckUpperInteractions3D PROTO
InteriorDoorCanWalk3D PROTO :DWORD,:DWORD
UpperCanWalk3D PROTO :DWORD,:DWORD
@ -24,6 +28,10 @@ INTERACT_UPPER_RIGHT equ 44
stairRoute3D dd 0
basementDoorTarget3D dd 0
basementDoorOpen3D dd 0
basementWheelTime3D dd 0
basementDoorHold3D dd 0
basementDoorArt3D AnimPack3D <>
basementDoorPath3D db "assets\animations\hall\basement-door-v2.bin",0
interiorDoorClock3D dd -1
portalRendering3D dd 0
portalPixelsWritten3D dd 0
@ -32,6 +40,8 @@ upperPortalReady3D dd 0
upperPortalEntered3D dd 0
upperEffectClock3D dd -1
upperLeverTime3D dd 0,0
upperLeftRelease3D db 69,58,32,194,197,208,205,211,210,220,32,203,197,194,219,201,32,208,219,215,192,195,0
upperRightRelease3D db 69,58,32,194,197,208,205,211,210,220,32,207,208,192,194,219,201,32,208,219,215,192,195,0
upperGamesArt3D AnimPack3D <>
upperPortalArt3D AnimPack3D <>
upperLeverArt3D AnimPack3D <>

View file

@ -160,4 +160,68 @@ basementStairMap3D db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,9
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
db 92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92,92
fortuneRoomMap3D db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
fortuneStairMap3D db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
db 132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132,132
.code

View file

@ -21,7 +21,15 @@ hall_plane_row:
cmp hallLevel3D, 1
jne @F
mov material, 93
@@:
@@:
cmp hallLevel3D,3
je hall_plane_fortune_ceiling
cmp engineMode,MODE_STAIRS
jne hall_plane_distance
cmp stairRoute3D,3
jne hall_plane_distance
hall_plane_fortune_ceiling:
mov material,133
jmp hall_plane_distance
hall_plane_floor:
sub eax, viewHorizon3D
@ -122,6 +130,21 @@ hall_plane_sample:
hall_plane_wood:
mov ebx, 85
hall_plane_choose_hd:
cmp ebx,132
jb hall_plane_other_hd
cmp ebx,135
ja hall_plane_other_hd
mov ecx,fortuneMaterialArt3D.pixels
test ecx,ecx
jz hall_plane_fortune_fallback
sub ebx,132
shl ebx,18
add ecx,ebx
jmp hall_plane_hd_coordinates
hall_plane_fortune_fallback:
mov ebx,94
mov material,ebx
hall_plane_other_hd:
cmp ebx,122
jb hall_plane_existing_hd
cmp ebx,125
@ -313,6 +336,9 @@ InitHall3D PROC uses esi edi
mov ecx, 256
rep movsd
mov hallLevel3D, 0
mov fortunePassageTarget3D,0
mov fortunePassageTime3D,0
mov fortunePassageClock3D,-1
mov worldMap3D, OFFSET casinoMap
mov viewEye3D, HALL_STANDING_EYE
mov viewCeiling3D, FP_1*5/2
@ -346,6 +372,7 @@ InitHall3D ENDP
LoadHallAssets3D PROC
call InitUpperSurfaces3D
call LoadFortuneArt3D
invoke LoadAnimPack3D, OFFSET upperMaterialArt3D, OFFSET upperMaterialPath3D
invoke ValidateCityPack3D, OFFSET upperMaterialArt3D,256,256,4
invoke LoadAnimPack3D,OFFSET upperGamesArt3D,OFFSET upperGamesPath3D
@ -366,6 +393,12 @@ LoadHallAssets3D PROC
invoke ValidateCityPack3D, OFFSET cabaretGuitarArt3D,160,256,4
invoke LoadAnimPack3D, OFFSET npcGuestArt3D, OFFSET npcGuestPath3D
invoke ValidateCityPack3D, OFFSET npcGuestArt3D,128,224,36
invoke LoadAnimPack3D, OFFSET npcWalkArt3D, OFFSET npcWalkPath3D
invoke ValidateCityPack3D, OFFSET npcWalkArt3D,128,224,48
invoke LoadAnimPack3D, OFFSET npcWalkLeftArt3D, OFFSET npcWalkLeftPath3D
invoke ValidateCityPack3D, OFFSET npcWalkLeftArt3D,128,224,48
invoke LoadAnimPack3D, OFFSET basementDoorArt3D, OFFSET basementDoorPath3D
invoke ValidateCityPack3D, OFFSET basementDoorArt3D,256,512,18
call LoadWorldDressing3D
call LoadScratchArt3D
invoke LoadAnimPack3D, OFFSET hallCardFurniture3D, OFFSET hallCardFurniturePath3D
@ -437,6 +470,7 @@ hall_load_sign:
LoadHallAssets3D ENDP
FreeHallAssets3D PROC
call FreeFortuneArt3D
invoke FreeAnimPack3D, OFFSET upperMaterialArt3D
invoke FreeAnimPack3D,OFFSET upperGamesArt3D
invoke FreeAnimPack3D,OFFSET upperPortalArt3D
@ -447,6 +481,9 @@ FreeHallAssets3D PROC
invoke FreeAnimPack3D, OFFSET cabaretPianoArt3D
invoke FreeAnimPack3D, OFFSET cabaretGuitarArt3D
invoke FreeAnimPack3D, OFFSET npcGuestArt3D
invoke FreeAnimPack3D, OFFSET npcWalkArt3D
invoke FreeAnimPack3D, OFFSET npcWalkLeftArt3D
invoke FreeAnimPack3D, OFFSET basementDoorArt3D
call FreeWorldDressing3D
call FreeNativeWheelArt3D
call FreeNativeCoinArt3D
@ -536,11 +573,21 @@ surface_missing:
ret
ResolveWorldSurface3D ENDP
RenderHall3D PROC uses ebx esi
RenderHall3D PROC uses ebx esi
cmp engineMode,MODE_STAIRS
je fortune_hall_stairs
cmp hallLevel3D,3
jne @F
call RenderFortuneRoom3D
call RenderInteriorViews3D
ret
@@:
fortune_hall_stairs:
cmp engineMode,MODE_STAIRS
jne @F
call RenderWalkableStairs3D
call RenderInteriorViews3D
call RenderStairDoor3D
ret
@@:
cmp hallLevel3D,2
@ -646,8 +693,10 @@ hall_render_lounge:
invoke DrawCityObject3D,OFFSET scrStation3D,0
call RenderHallDressing3D
call RenderHallPortals3D
call RenderFortuneEntrance3D
call ApplyHallLighting3D
call RenderInteriorViews3D
call DrawBasementLeaf3D
ret
hall_render_bunker:
invoke DrawCityBoxSides3D, 14*FP_1,20*FP_1,14*FP_1,15*FP_1,0,FP_1/2,96
@ -711,6 +760,11 @@ hall_target_done:
HallTarget3D ENDP
CheckHallInteractions3D PROC
cmp hallLevel3D,3
jne @F
call CheckFortuneInteractions3D
ret
@@:
cmp hallLevel3D,2
jne @F
call CheckUpperInteractions3D
@ -733,6 +787,7 @@ hall_check_other:
invoke HallTarget3D, 27*FP_1,25*FP_1,INTERACT_BILLIARDS,OFFSET hallPoolPrompt3D
invoke HallTarget3D,FP_1*305/10,21*FP_1,0,OFFSET upperStairPrompt3D
call CheckNpcInteractions3D
call CheckFortuneInteractions3D
ret
hall_check_bunker:
mov interactTargetId, 0
@ -742,6 +797,9 @@ hall_check_bunker:
CheckHallInteractions3D ENDP
TriggerHallInteraction3D PROC
call TryFortuneInteraction3D
test eax,eax
jnz hall_trigger_handled
call TryUpperInteraction3D
test eax,eax
jnz hall_trigger_handled
@ -878,17 +936,6 @@ bar_animate:
bar_after_drink:
cmp barTime3D, 2400
jb bar_handle_done
mov ecx, barSelection3D
mov eax, barStrength3D[ecx*4]
test eax, eax
jnz @F
mov barIntoxication3D, 0
@@:
add barIntoxication3D, eax
cmp barIntoxication3D, 60000
jbe @F
mov barIntoxication3D, 60000
@@:
mov barPhase3D, 0
mov barTime3D, 0
bar_handle_done:
@ -1034,9 +1081,16 @@ city_world_guard_right:
RenderCityGuards3D ENDP
CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD
cmp engineMode,MODE_STAIRS
je fortune_collision_stairs
cmp hallLevel3D,3
jne fortune_collision_stairs
invoke FortuneCanWalk3D,px,py
ret
fortune_collision_stairs:
cmp engineMode,MODE_STAIRS
jne @F
mov eax,1
invoke StairDoorCanWalk3D,px,py
ret
@@:
cmp hallLevel3D,2
@ -1046,6 +1100,9 @@ CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD
@@:
cmp hallLevel3D, 0
jne hall_collision_bunker
invoke FortuneEntranceCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
invoke ScratchCanWalk3D,px,py
test eax,eax
jz hall_collision_blocked
@ -1232,23 +1289,6 @@ UpdateHall3D PROC
call UpdateInteriorTravel3D
call UpdatePatrons3D
call UpdateInteriorEye3D
cmp barIntoxication3D, 0
je hall_update_done
mov eax, deltaTime3D
imul eax, 2
sub barIntoxication3D, eax
jnc @F
mov barIntoxication3D, 0
@@:
mov eax, animationClock3D
shr eax, 3
and eax, 511
mov eax, tableSin[eax*4]
imul barIntoxication3D
shrd eax, edx, 16
sar eax, 14
add viewHorizon3D, eax
hall_update_done:
ret
UpdateHall3D ENDP
@ -1347,6 +1387,8 @@ RenderPatrons3D ENDP
include src_graphics\world_dressing.inc
include src_graphics\terminal.inc
include src_graphics\hall_expansion.inc
include src_graphics\steel_door.inc
include src_graphics\interior_portals.inc
include src_graphics\fortune_room.inc
include src_graphics\cabaret.inc
include src_graphics\hall_lighting.inc

View file

@ -17,7 +17,8 @@ portal_mask_y:
portal_mask_depth:
cmp eax,2048
jle portal_mask_next
cmp eax,8*FP_1
; The aperture is visible throughout the room, not just within eight metres.
cmp eax,64*FP_1
ja portal_mask_next
cmp eax,zBuffer[ebx*4]
ja portal_mask_next
@ -101,6 +102,9 @@ portal_mask_next:
cmp targetMode,MODE_STAIRS
jne @F
mov viewCeiling3D,FP_1*11/2
cmp targetRoute,3
jne @F
mov viewCeiling3D,4*FP_1
@@:
mov eax,worldMap3D
mov oldMap,eax
@ -164,7 +168,7 @@ portal_compose_pixel:
cmp eax,portalSourceDepth3D[edi]
ja portal_restore_pixel
; The rendered destination is already in screenBuffer at this exact ray.
mov cityDepthBuffer3D[edi],eax
; Keep destination depth, so the hinged leaf can occlude the continuation.
inc portalPixelsWritten3D
jmp portal_compose_next
portal_restore_pixel:
@ -194,21 +198,29 @@ RenderInteriorViews3D PROC
cmp portalRendering3D,0
jne interior_views_done
mov portalPixelsWritten3D,0
cmp hallLevel3D,3
je RenderFortuneViews3D
cmp engineMode,MODE_STAIRS
jne @F
cmp stairRoute3D,3
je RenderFortuneViews3D
@@:
cmp engineMode,MODE_STAIRS
je interior_views_stairs
cmp hallLevel3D,2
je interior_views_lounge
cmp hallLevel3D,1
je interior_views_bunker
call RenderFortuneViews3D
cmp playerX,FP_1*303/10
jae @F
invoke DrawInteriorView3D,0,FP_1*3025/100,20*FP_1,22*FP_1,0,FP_1*5/2,-27*FP_1,0,0,OFFSET upperStairMap3D,MODE_STAIRS,0,85,2
@@:
cmp basementDoorOpen3D,FP_1*9/10
jb interior_views_done
cmp playerX,FP_1*286/10
cmp basementDoorOpen3D,0
je interior_views_done
cmp playerX,29*FP_1
jae interior_views_done
invoke DrawInteriorView3D,0,FP_1*285/10,FP_1*975/100,FP_1*1125/100,0,FP_1*19/10,-25*FP_1,FP_1*21/2,FP_1*3/2,OFFSET basementStairMap3D,MODE_STAIRS,0,94,1
invoke DrawInteriorView3D,0,FP_1*2895/100,FP_1*975/100,FP_1*1125/100,0,FP_1*19/10,-25*FP_1,FP_1*21/2,FP_1*3/2,OFFSET basementStairMap3D,MODE_STAIRS,0,94,1
ret
interior_views_lounge:
cmp playerX,FP_1*32/10

View file

@ -15,6 +15,9 @@ npc_life_init:
mov npcLife3D[ebx].pathCount,0
mov npcLife3D[ebx].remainder,0
mov npcLife3D[ebx].poseClock,0
mov npcWalkDistance3D[esi*4],0
mov npcWalkVelocityX3D[esi*4],0
mov npcWalkVelocityY3D[esi*4],0
mov eax,npcStartDest3D[esi*4]
mov npcLife3D[ebx].destination,eax
imul edi,esi,SIZEOF NpcMindEntity
@ -215,10 +218,15 @@ NpcCanStep3D ENDP
; Millisecond movement, carrying the remainder and consuming whole route cells.
NpcMove3D PROC uses ebx esi edi slot:DWORD,milliseconds:DWORD
local budget:DWORD,wx:DWORD,wy:DWORD,mind:DWORD,pathBase:DWORD
local oldX:DWORD,oldY:DWORD
imul ebx,slot,SIZEOF NpcLife3D
imul eax,slot,SIZEOF NpcMindEntity
lea esi,casinoNpcs3D[eax]
mov mind,esi
mov eax,[esi].NpcMindEntity.posX
mov oldX,eax
mov eax,[esi].NpcMindEntity.posY
mov oldY,eax
mov eax,slot
imul eax,NPC_PATH_CAP*4
add eax,OFFSET npcPaths3D
@ -350,6 +358,35 @@ npc_move_blocked:
mov npcLife3D[ebx].remaining,1000
mov npcLife3D[ebx].pathCount,0
npc_move_done:
; Advance gait by successful displacement only, including partial blocked steps.
mov esi,mind
mov ebx,slot
mov eax,[esi].NpcMindEntity.posX
sub eax,oldX
mov ecx,eax
mov eax,[esi].NpcMindEntity.posY
sub eax,oldY
mov edi,eax
or eax,ecx
jz npc_move_no_stride
mov npcWalkVelocityX3D[ebx*4],ecx
mov npcWalkVelocityY3D[ebx*4],edi
mov eax,ecx
cdq
xor eax,edx
sub eax,edx
mov ecx,eax
mov eax,edi
cdq
xor eax,edx
sub eax,edx
add eax,ecx
add eax,npcWalkDistance3D[ebx*4]
xor edx,edx
mov ecx,FP_1*3/4
div ecx
mov npcWalkDistance3D[ebx*4],edx
npc_move_no_stride:
ret
NpcMove3D ENDP
@ -466,6 +503,7 @@ npc_draw_person:
mov npcSprite3D.posY,eax
mov npcSprite3D.worldW,FP_1*3/5
mov npcSprite3D.worldH,FP_1*6/5
mov npcSprite3D.groundZ,-FP_1*6/5*4/224
mov npcSprite3D.asset,OFFSET npcGuestArt3D
cmp npcGuestArt3D.pixels,0
je npc_draw_next
@ -483,13 +521,34 @@ npc_draw_person:
npc_draw_walk:
cmp npcLife3D[ebx].state,NPC_WALK
jne npc_draw_sprite
mov eax,npcLife3D[ebx].poseClock
cmp npcWalkArt3D.pixels,0
je npc_draw_sprite
mov npcSprite3D.asset,OFFSET npcWalkArt3D
mov npcSprite3D.groundZ,0
mov eax,npcLife3D[ebx].identity
mov eax,npcGuestAppearance3D[eax*4]
shl eax,3
mov npcSprite3D.frame,eax
mov ebx,npcDrawOrder3D[esi*4]
mov eax,npcWalkDistance3D[ebx*4]
xor edx,edx
mov ecx,180
mov ecx,FP_1*3/32
div ecx
and eax,3
inc eax
and eax,7
add npcSprite3D.frame,eax
; Match the drawn facing to the actual camera-relative direction of travel.
mov eax,npcWalkVelocityY3D[ebx*4]
imul playerDirX
shrd eax,edx,16
mov edi,eax
mov eax,npcWalkVelocityX3D[ebx*4]
imul playerDirY
shrd eax,edx,16
sub edi,eax
jns npc_draw_sprite
cmp npcWalkLeftArt3D.pixels,0
je npc_draw_sprite
mov npcSprite3D.asset,OFFSET npcWalkLeftArt3D
npc_draw_sprite:
invoke DrawCityObject3D,OFFSET npcSprite3D,0
npc_draw_next:

View file

@ -46,6 +46,13 @@ npcLife3D NpcLife3D NPC_LIFE_COUNT dup(<>)
npcLifeReady3D dd 0
npcGuestArt3D AnimPack3D <>
npcGuestPath3D db "assets\animations\hall\guest-cast-v1.bin",0
npcWalkArt3D AnimPack3D <>
npcWalkPath3D db "assets\animations\hall\guest-walk-v2.bin",0
npcWalkLeftArt3D AnimPack3D <>
npcWalkLeftPath3D db "assets\animations\hall\guest-walk-left-v2.bin",0
npcWalkDistance3D dd NPC_LIFE_COUNT dup(0)
npcWalkVelocityX3D dd NPC_LIFE_COUNT dup(0)
npcWalkVelocityY3D dd NPC_LIFE_COUNT dup(0)
; The two rotations share six appearances, with a mixed cast on every visit.
npcGuestAppearance3D dd 0,2,4,1,3,5,2,0,4,1,3,5
npcGuestNames3D dd OFFSET npcName0,OFFSET npcName1,OFFSET npcName2,OFFSET npcGuestAnna3D

359
src_graphics/steel_door.inc Normal file
View file

@ -0,0 +1,359 @@
; Hysteresis and a hold timer keep the shared doorway safe on both sides.
UpdateBasementDoor3D PROC uses ebx
local nearDoor:DWORD,rangeX:DWORD,rangeY:DWORD
mov nearDoor,0
mov rangeX,FP_1*12/5
mov rangeY,FP_1*7/5
mov eax,basementDoorTarget3D
or eax,basementDoorOpen3D
or eax,basementWheelTime3D
jz @F
mov rangeX,3*FP_1
mov rangeY,FP_1*9/5
@@:
cmp engineMode,MODE_WALK
jne steel_approach_stairs
cmp hallLevel3D,0
jne steel_door_clock
mov eax,playerX
sub eax,FP_1*289/10
cdq
xor eax,edx
sub eax,edx
cmp eax,rangeX
ja steel_door_clock
mov eax,playerY
sub eax,FP_1*105/10
cdq
xor eax,edx
sub eax,edx
cmp eax,rangeY
ja steel_door_clock
jmp steel_request_open
steel_approach_stairs:
cmp engineMode,MODE_STAIRS
jne steel_door_clock
cmp stairRoute3D,1
jne steel_door_clock
mov eax,5*FP_1
cmp rangeX,3*FP_1
jne @F
mov eax,FP_1*13/2
@@:
cmp playerX,eax
ja steel_door_clock
cmp playerY,FP_1*195/10
jb steel_door_clock
cmp playerY,FP_1*235/10
ja steel_door_clock
steel_request_open:
mov nearDoor,1
mov basementDoorTarget3D,FP_1
mov basementDoorHold3D,1200
steel_door_clock:
mov eax,animationClock3D
cmp eax,interiorDoorClock3D
je steel_update_done
mov interiorDoorClock3D,eax
mov ebx,deltaTime3D
cmp ebx,100
jbe @F
mov ebx,100
@@:
cmp nearDoor,1
je steel_animate
; A manual E request outside the automatic opening radius still gets time.
cmp basementDoorTarget3D,FP_1
jne steel_far_hold
cmp basementDoorHold3D,0
jne steel_far_hold
mov eax,basementDoorOpen3D
or eax,basementWheelTime3D
jnz steel_far_hold
mov basementDoorHold3D,2000
steel_far_hold:
cmp basementDoorHold3D,0
je steel_request_close
mov eax,basementDoorHold3D
sub eax,ebx
jae @F
xor eax,eax
@@:
mov basementDoorHold3D,eax
jmp steel_animate
steel_request_close:
mov basementDoorTarget3D,0
steel_animate:
cmp basementDoorTarget3D,0
je steel_close_leaf
cmp basementWheelTime3D,700
jae steel_swing
mov eax,700
sub eax,basementWheelTime3D
cmp ebx,eax
jb steel_wheel_partial
sub ebx,eax
mov basementWheelTime3D,700
jmp steel_swing
steel_wheel_partial:
add basementWheelTime3D,ebx
jmp steel_update_done
steel_swing:
; Opening: unlock first, then swing around the fixed hinge over 1000ms.
imul eax,ebx,FP_1
xor edx,edx
mov ecx,1000
div ecx
add eax,basementDoorOpen3D
cmp eax,FP_1
jbe @F
mov eax,FP_1
@@:
mov basementDoorOpen3D,eax
jmp steel_update_done
steel_close_leaf:
cmp basementDoorOpen3D,0
je steel_lock_wheel
; Carry unused milliseconds into locking only after the leaf is fully shut.
imul eax,basementDoorOpen3D,1000
add eax,FP_1-1
shr eax,16
cmp ebx,eax
jb steel_close_partial
sub ebx,eax
mov basementDoorOpen3D,0
jmp steel_lock_wheel
steel_close_partial:
imul eax,ebx,FP_1
xor edx,edx
mov ecx,1000
div ecx
sub basementDoorOpen3D,eax
jmp steel_update_done
steel_lock_wheel:
mov eax,basementWheelTime3D
sub eax,ebx
jae @F
xor eax,eax
@@:
mov basementWheelTime3D,eax
steel_update_done:
ret
UpdateBasementDoor3D ENDP
; The shared doorway occupies the overlapping landing in both local maps.
StairDoorCanWalk3D PROC px:DWORD,py:DWORD
cmp stairRoute3D,1
jne stair_steel_clear
cmp px,FP_1*35/10
jb stair_steel_clear
cmp px,FP_1*42/10
ja stair_steel_clear
cmp py,FP_1*2025/100
jb stair_steel_blocked
cmp py,FP_1*2175/100
jae stair_steel_blocked
mov eax,px
add eax,25*FP_1
mov edx,py
sub edx,FP_1*21/2
invoke InteriorDoorCanWalk3D,eax,edx
ret
stair_steel_blocked:
xor eax,eax
ret
stair_steel_clear:
mov eax,1
ret
StairDoorCanWalk3D ENDP
RenderStairDoor3D PROC
local oldX:DWORD,oldY:DWORD,oldEye:DWORD
cmp stairRoute3D,1
jne stair_steel_done
; Fill the wider landing around the actual narrow doorway on the stair side.
invoke DrawCityBoxSides3D,FP_1*375/100,4*FP_1,20*FP_1,FP_1*2025/100,FP_1*3/2,FP_1*37/10,92
invoke DrawCityBoxSides3D,FP_1*375/100,4*FP_1,FP_1*2175/100,23*FP_1,FP_1*3/2,FP_1*37/10,92
mov eax,playerX
mov oldX,eax
add playerX,25*FP_1
mov eax,playerY
mov oldY,eax
sub playerY,FP_1*21/2
mov eax,viewEye3D
mov oldEye,eax
sub viewEye3D,FP_1*3/2
invoke DrawCityVertical3D,0,FP_1*2838/100,9*FP_1,12*FP_1,-FP_1/10,FP_1*21/10,131
call DrawBasementLeaf3D
mov eax,oldX
mov playerX,eax
mov eax,oldY
mov playerY,eax
mov eax,oldEye
mov viewEye3D,eax
stair_steel_done:
ret
RenderStairDoor3D ENDP
; Perspective-correct vertical plane with a rotating horizontal basis. The wheel
; is baked into offline texture phases, not rotated in screen space at runtime.
DrawBasementLeaf3D PROC uses ebx esi edi
local ux:DWORD,uy:DWORD,nx:DWORD,ny:DWORD,numerator:DWORD
local depth:DWORD,texture:DWORD,sourceX:DWORD,top:DWORD,bottom:DWORD
local stepY:DWORD,sourceY:DWORD,row:DWORD,column:DWORD,destination:DWORD
invoke ResolveUpperMaterial3D,128
test eax,eax
jz steel_leaf_done
mov texture,eax
mov eax,basementDoorOpen3D
imul eax,128
shr eax,16
and eax,511
mov ecx,tableSin[eax*4]
mov ux,ecx
neg ecx
mov ny,ecx
mov ecx,tableCos[eax*4]
mov uy,ecx
mov nx,ecx
mov eax,FP_1*289/10
sub eax,playerX
imul nx
shrd eax,edx,16
mov ebx,eax
mov eax,FP_1*975/100
sub eax,playerY
imul ny
shrd eax,edx,16
add eax,ebx
mov numerator,eax
mov column,0
steel_leaf_column:
mov ebx,column
mov eax,cityRayX3D[ebx*4]
imul nx
shrd eax,edx,16
mov esi,eax
mov eax,cityRayY3D[ebx*4]
imul ny
shrd eax,edx,16
add eax,esi
mov ecx,eax
cdq
xor eax,edx
sub eax,edx
cmp eax,256
jb steel_leaf_next_column
mov eax,numerator
cdq
shld edx,eax,16
shl eax,16
idiv ecx
cmp eax,4096
jle steel_leaf_next_column
cmp eax,zBuffer[ebx*4]
jae steel_leaf_next_column
cmp portalRendering3D,0
je @F
cmp eax,portalClipDepth3D[ebx*4]
jb steel_leaf_next_column
@@:
mov depth,eax
imul cityRayX3D[ebx*4]
shrd eax,edx,16
add eax,playerX
sub eax,FP_1*289/10
imul ux
shrd eax,edx,16
mov esi,eax
mov eax,depth
imul cityRayY3D[ebx*4]
shrd eax,edx,16
add eax,playerY
sub eax,FP_1*975/100
imul uy
shrd eax,edx,16
add eax,esi
test eax,eax
js steel_leaf_next_column
cmp eax,FP_1*3/2
jae steel_leaf_next_column
imul eax,256
xor edx,edx
mov ecx,FP_1*3/2
div ecx
mov sourceX,eax
mov eax,viewEye3D
sub eax,FP_1*19/10
imul eax,SCREEN_H
cdq
idiv depth
add eax,viewHorizon3D
mov top,eax
mov eax,viewEye3D
imul eax,SCREEN_H
cdq
idiv depth
add eax,viewHorizon3D
mov bottom,eax
sub eax,top
jle steel_leaf_next_column
mov ecx,eax
mov eax,512*FP_1
xor edx,edx
div ecx
mov stepY,eax
mov eax,top
test eax,eax
jns @F
xor eax,eax
@@:
mov row,eax
sub eax,top
imul eax,stepY
mov sourceY,eax
cmp bottom,SCREEN_H
jle @F
mov bottom,SCREEN_H
@@:
mov eax,row
cmp eax,bottom
jge steel_leaf_next_column
imul eax,SCREEN_W
add eax,column
shl eax,2
mov destination,eax
steel_leaf_pixel:
mov edi,destination
mov eax,depth
cmp eax,cityDepthBuffer3D[edi]
ja steel_leaf_next_pixel
mov eax,sourceY
shr eax,16
cmp eax,512
jae steel_leaf_next_pixel
shl eax,8
add eax,sourceX
mov esi,texture
mov eax,[esi+eax*4]
cmp eax,80000000h
jb steel_leaf_next_pixel
and eax,00FFFFFFh
mov screenBuffer[edi],eax
mov eax,depth
mov cityDepthBuffer3D[edi],eax
steel_leaf_next_pixel:
mov eax,stepY
add sourceY,eax
add destination,SCREEN_W*4
inc row
mov eax,row
cmp eax,bottom
jb steel_leaf_pixel
steel_leaf_next_column:
inc column
cmp column,SCREEN_W
jb steel_leaf_column
steel_leaf_done:
ret
DrawBasementLeaf3D ENDP

View file

@ -124,7 +124,10 @@ hall_wall_painting:
jnz hall_wall_painting
xor esi,esi
hall_wall_sconce:
cmp esi,1
je hall_wall_secret_lamp
invoke DrawHallLantern3D,FP_1*102/100,hallSconceY3D[esi*4],1
hall_wall_secret_lamp:
invoke DrawHallLantern3D,FP_1*3098/100,hallSconceY3D[esi*4],-1
inc esi
cmp esi,4

View file

@ -62,14 +62,25 @@ def main():
for y in range(13,23):
for x in range(15,18):
down_stairs[y][x] = 0
fortune = [[132]*32 for _ in range(32)]
for y in range(9,13):
for x in range(13,18):
fortune[y][x] = 0
fortune_stairs = [[132]*32 for _ in range(32)]
for y in range(9,11):
for x in range(16,31):
fortune_stairs[y][x] = 0
for grid, start, targets in ((upper,(4,21),[(7,18),(12,20),(9,23),(13,16)]),
(up_stairs,(3,21),[(16,21)]),
(down_stairs,(3,21),[(16,21),(16,14)])):
(down_stairs,(3,21),[(16,21),(16,14)]),
(fortune,(17,10),[(14,10),(16,12),(17,10)]),
(fortune_stairs,(29,10),[(17,10)])):
assert all(point in reachable(grid,start) for point in targets)
output = ["; Generated by make_hall_layout.py. Doorways and interactable approaches are checked.", ".data"]
for name, grid in (("hallLayout3D", hall), ("bunkerMap3D", bunker),
("upperLoungeMap3D",upper), ("upperStairMap3D",up_stairs),
("basementStairMap3D",down_stairs)):
("basementStairMap3D",down_stairs), ("fortuneRoomMap3D",fortune),
("fortuneStairMap3D",fortune_stairs)):
for index, row in enumerate(grid):
output.append((name if index == 0 else " " * len(name)) + " db " + ",".join(map(str, row)))
output.append(".code")

View file

@ -0,0 +1,74 @@
"""Offline normalization of generated door components and distance-driven gait art."""
from pathlib import Path
import struct
from PIL import Image
from pack_guest_cast import isolate
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT / "assets/sprites/hall"
OUT = ROOT / "assets/animations/hall"
PREVIEWS = ROOT / "build/hall-expansion"
def pack(name, frames):
w, h = frames[0].size
assert all(frame.size == (w, h) for frame in frames)
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
(OUT / name).write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(payload)) + payload)
def guests():
sheet = Image.open(ART / "guest-walk-v2.png").convert("RGBA")
# The generated strip has variable row heights; these gutters separate feet/heads.
rows = (0, 190, 376, 560, 746, 916, sheet.height)
frames = []
for appearance in range(6):
cells = [isolate(sheet.crop((col*sheet.width//8, rows[appearance],
(col+1)*sheet.width//8, rows[appearance+1]))) for col in range(8)]
scale = min(126/max(cell.width for cell in cells), 204/max(cell.height for cell in cells))
for cell in cells:
cell = cell.resize((round(cell.width*scale), round(cell.height*scale)), Image.Resampling.LANCZOS)
frame = Image.new("RGBA", (128,224))
frame.alpha_composite(cell, ((128-cell.width)//2, 224-cell.height))
frames.append(frame)
pack("guest-walk-v2.bin", frames)
pack("guest-walk-left-v2.bin", [frame.transpose(Image.Transpose.FLIP_LEFT_RIGHT) for frame in frames])
preview = Image.new("RGBA", (8*128,6*224), (27,30,29,255))
for i, frame in enumerate(frames[:48]):
preview.alpha_composite(frame, ((i%8)*128, (i//8)*224))
preview.save(PREVIEWS / "guest-walk-v2-preview.png")
def door():
sheet = Image.open(ART / "basement-door-v2.png").convert("RGBA")
leaf = isolate(sheet.crop((0,0,640,sheet.height))).resize((256,512), Image.Resampling.LANCZOS)
source_frame = isolate(sheet.crop((640,0,1420,sheet.height)))
wheel = isolate(sheet.crop((1420,0,sheet.width,sheet.height))).resize((92,92), Image.Resampling.LANCZOS)
frames = []
for phase in range(17):
texture = leaf.copy()
rotated = wheel.rotate(-270*phase/16, Image.Resampling.BICUBIC, expand=False)
# UV texels are not square in world space on the 1.5 by 1.9 metre leaf.
rotated = rotated.resize((58,92), Image.Resampling.LANCZOS)
texture.alpha_composite(rotated, (99,244))
frames.append(texture)
# Map the rigid jambs around a known rectangular aperture, without a baked leaf.
w,h = source_frame.size
left, right, lintel = round(w*.24), round(w*.72), round(h*.14)
frame = Image.new("RGBA", (256,512))
frame.alpha_composite(source_frame.crop((0,0,left,h)).resize((64,512)), (0,0))
frame.alpha_composite(source_frame.crop((right,0,w,h)).resize((64,512)), (192,0))
frame.alpha_composite(source_frame.crop((left,0,right,lintel)).resize((128,32)), (64,0))
frames.append(frame)
pack("basement-door-v2.bin", frames)
preview = Image.new("RGBA", (3*256,512), (27,30,29,255))
for i, texture in enumerate((frames[0],frames[8],frame)):
preview.alpha_composite(texture,(i*256,0))
preview.save(PREVIEWS / "basement-door-v2-preview.png")
if __name__ == "__main__":
PREVIEWS.mkdir(parents=True, exist_ok=True)
guests()
door()
print("Packed 96 gait frames and 18 door textures; runtime remains native assembly.")

View file

@ -0,0 +1,63 @@
"""Pack generated tarot-room sprites. Offline tooling, never a game runtime."""
from pathlib import Path
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
def sprites(name, row_split, bottom_split=.5, top_split=.5):
source = ROOT / "assets/sprites/hall" / name
sheet = Image.open(source).convert("RGBA")
w, h = sheet.size
frames = []
# The source has tightly spaced silhouettes. Bounds are checked on alpha.
# Each cell is independently fitted, but the two idle poses share one scale.
# The generated first row ends above the bookcase's crescent crest.
split = round(h * row_split)
top = round(w * top_split)
cells = [sheet.crop((0,0,top,split)), sheet.crop((top,0,w,split)),
sheet.crop((0,split,round(w*bottom_split),h)),
sheet.crop((round(w*bottom_split),split,w,h))]
bounds = [cell.getchannel("A").point(lambda a: 255 if a > 32 else 0).getbbox() for cell in cells]
assert all(bounds), "missing transparent sprite"
cropped = [cell.crop(box) for cell, box in zip(cells, bounds)]
scale = min(242/max(c.width for c in cropped[:2]), 252/max(c.height for c in cropped[:2]))
for i, cell in enumerate(cropped):
factor = scale if i < 2 else min(242/cell.width, 252/cell.height)
cell = cell.resize((round(cell.width*factor), round(cell.height*factor)), Image.Resampling.LANCZOS)
frame = Image.new("RGBA", (256,256))
frame.alpha_composite(cell, ((256-cell.width)//2, 256-cell.height))
frames.append(frame)
return frames
def pack(name, frames):
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
target = ROOT / "assets/animations/hall" / name
target.write_bytes(struct.pack("<4s4I",b"ANM1",256,256,len(frames),len(payload))+payload)
def main():
frames = sprites("fortune-room-v2.png", .47)
frames += sprites("fortune-decor-v2.png", .52, .60)
frames += sprites("fortune-wall-v2.png", .46, top_split=.55)
frames += sprites("fortune-cabinets-v2.png", .58, top_split=.47)
frames += sprites("fortune-den-v3.png", .575, .61, top_split=.64)
frames += sprites("fortune-clutter-v3.png", .53, .55, top_split=.515)
pack("fortune-room-v3.bin", frames)
sheet = Image.open(ROOT / "assets/sprites/hall/fortune-materials-v3.png").convert("RGBA")
w, h = sheet.size
materials = [sheet.crop(box).resize((256,256), Image.Resampling.LANCZOS)
for box in ((0,0,w//2,h//2), (w//2,0,w,h//2),
(0,h//2,w//2,h), (w//2,h//2,w,h))]
pack("fortune-materials-v3.bin", materials)
preview = Image.new("RGBA", (1024,1536), (22,30,48,255))
for i, frame in enumerate(frames):
preview.alpha_composite(frame,((i%4)*256,(i//4)*256))
preview.save(ROOT / "build/hall-expansion/fortune-art-preview.png")
print("Packed twenty-four den sprites and four worn cloth/stone materials.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,139 @@
.data
barTestMain db "build\hall-expansion\bar-camera-main.bmp",0
barTestUpper db "build\hall-expansion\bar-camera-upper.bmp",0
.code
BarCameraAssert PROC
cmp viewHorizon3D,HALF_H
jne bar_camera_assert_fail
cmp viewEye3D,HALL_STANDING_EYE
jne bar_camera_assert_fail
cmp playerX,12*FP_1
jne bar_camera_assert_fail
cmp playerY,14*FP_1
jne bar_camera_assert_fail
cmp playerAngle,384
jne bar_camera_assert_fail
ret
bar_camera_assert_fail:
invoke ExitProcess,expansionStage
ret
BarCameraAssert ENDP
BarCameraNativeTests PROC uses ebx esi edi
local expectedBank:DWORD, oldArrival:DWORD
; Exercise real purchases without writing the player's profile.
mov eax,arrivalConfirmed3D
mov oldArrival,eax
mov arrivalConfirmed3D,0
mov expansionStage,70
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov playerX,12*FP_1
mov playerY,14*FP_1
mov playerAngle,384
mov viewHorizon3D,-300
mov viewEye3D,-FP_1
mov deltaTime3D,16
call UpdateHall3D
call BarCameraAssert
xor ebx,ebx
bar_camera_level:
mov hallLevel3D,ebx
mov worldMap3D,OFFSET casinoMap
test ebx,ebx
jz @F
mov worldMap3D,OFFSET upperLoungeMap3D
@@:
mov bankroll,10000
mov expectedBank,10000
mov esi,12
bar_camera_visit:
mov expansionStage,71
mov interactTargetId,INTERACT_BAR
call TriggerHallInteraction3D
cmp engineMode,MODE_BAR
jne bar_camera_fail
xor edi,edi
bar_camera_drink:
mov eax,barPrice3D[edi*4]
sub expectedBank,eax
invoke BuyBarDrink3D,edi
mov eax,expectedBank
cmp bankroll,eax
jne bar_camera_fail
cmp barPhase3D,1
jne bar_camera_fail
cmp barTime3D,0
jne bar_camera_fail
; A second click while serving must not buy another drink.
invoke BuyBarDrink3D,edi
mov eax,expectedBank
cmp bankroll,eax
jne bar_camera_fail
mov deltaTime3D,2999
call HandleBar3D
cmp barPhase3D,1
jne bar_camera_fail
mov deltaTime3D,1
call HandleBar3D
cmp barPhase3D,2
jne bar_camera_fail
cmp barTime3D,0
jne bar_camera_fail
mov deltaTime3D,2399
call HandleBar3D
cmp barPhase3D,2
jne bar_camera_fail
; Render every drink in each bar once, checking camera restoration.
cmp esi,12
jne @F
call RenderBar3D
call BarCameraAssert
cmp engineMode,MODE_BAR
jne bar_camera_fail
@@:
mov deltaTime3D,1
call HandleBar3D
cmp barPhase3D,0
jne bar_camera_fail
cmp barTime3D,0
jne bar_camera_fail
call BarCameraAssert
inc edi
cmp edi,3
jb bar_camera_drink
; Same return-to-walking state as the menu exit, then real hall updates.
mov engineMode,MODE_WALK
mov deltaTime3D,16
mov edi,256
bar_camera_walk:
add animationClock3D,16
call UpdateHall3D
call UpdateInteriorEye3D
call BarCameraAssert
cmp engineMode,MODE_WALK
jne bar_camera_fail
dec edi
jnz bar_camera_walk
dec esi
jnz bar_camera_visit
mov expansionStage,72
mov eax,expectedBank
cmp bankroll,eax
jne bar_camera_fail
call ExpansionFrame
test ebx,ebx
jnz bar_camera_upper_snapshot
invoke ExpansionSnapshot,OFFSET barTestMain
mov ebx,2
jmp bar_camera_level
bar_camera_upper_snapshot:
invoke ExpansionSnapshot,OFFSET barTestUpper
mov eax,oldArrival
mov arrivalConfirmed3D,eax
ret
bar_camera_fail:
invoke ExitProcess,expansionStage
ret
BarCameraNativeTests ENDP

View file

@ -2,7 +2,7 @@
import struct
from pathlib import Path
from capstone import Cs, CS_ARCH_X86, CS_MODE_32
from capstone import Cs, CS_ARCH_X86, CS_MODE_32, CS_GRP_JUMP
from capstone.x86_const import X86_OP_IMM
@ -35,7 +35,7 @@ class Coff:
for i in range(reloc_count)]
self.sections[section] = (raw, size, entries)
def signature(self, name):
def signature(self, name, tail_branches=False):
section, start = self.functions[name]
raw, size, entries = self.sections[section]
end = min([value for sec, value in self.functions.values()
@ -59,6 +59,12 @@ class Coff:
if ins.mnemonic == "call" and not refs and ins.operands[0].type == X86_OP_IMM:
target = targets[ins.operands[0].imm]
code[ins.imm_offset:ins.imm_offset + ins.imm_size] = b"\0" * ins.imm_size
elif (tail_branches and ins.group(CS_GRP_JUMP) and not refs
and ins.operands[0].type == X86_OP_IMM
and ins.operands[0].imm in targets):
# A tail branch relocates with its callee, just like a direct call.
target = targets[ins.operands[0].imm]
code[ins.imm_offset:ins.imm_offset + ins.imm_size] = b"\0" * ins.imm_size
result.append((bytes(code), tuple(refs), target))
assert sum(len(code) for code, _, _ in result) == end - start, name
return result

View file

@ -4,7 +4,7 @@ import argparse
import re
import struct
import subprocess
from PIL import Image
from PIL import Image, ImageStat
from check_card_exit_build import Coff
from check_terminals import signature
@ -28,16 +28,22 @@ def prepare():
OUT.mkdir(exist_ok=True)
code = read("src_graphics/hall_expansion.inc") + "\n" + read("src_graphics/cabaret.inc")
code += "\n" + read("src_graphics/interior_portals.inc")
code += "\n" + read("src_graphics/steel_door.inc")
code += "\n" + read("src_graphics/fortune_room.inc")
code += "\n" + proc(read("src_graphics/unavailable.inc"),"BeginUnavailable3D")
code += "\n" + proc(read("src_graphics/world_dressing.inc"),"RenderHallDressing3D")
for path, names in (
("src_graphics/hall_scene.inc",["RenderCeilingAndFloor","RenderSprites","InitHall3D",
"LoadHallAssets3D","FreeHallAssets3D","RenderHall3D","HallTarget3D",
"CheckHallInteractions3D","TriggerHallInteraction3D","HandleStairs3D","RenderStairs3D",
"CanWalkHall3D","UpdateHall3D","UpdatePatrons3D","RenderPatrons3D","RenderBar3D"]),
"CanWalkHall3D","UpdateHall3D","UpdatePatrons3D","RenderPatrons3D","RenderBar3D",
"BuyBarDrink3D","HandleBar3D"]),
("src_graphics/platform_code.inc",["CanWalk3D"]),
("casino3d.inc",["HandlePlayerInput","CheckInteractions","RenderWallsRange","RenderWalls","RaycastWorkerProc","Trigger3DInteraction"]),
("src_graphics/city_bins.inc",["ResolveCitySurface3D","PrepareCityDepth3D","PrepareCityRays3D",
"CityRayPlane3D","DrawCityVertical3D","DrawCityHorizontal3D","DrawCityBoxSides3D"]),
("src_graphics/npc_life.inc",["RenderNpcLife3D"]),
("src_graphics/npc_life.inc",["InitNpcLife3D","NpcTryCell3D","NpcPlanPath3D",
"NpcCanStep3D","NpcMove3D","UpdateNpcLife3D","RenderNpcLife3D"]),
("src_graphics/city_objects.inc",["DrawCityObject3D"]),
("src_graphics/npc_contracts.inc",["NpcNameText3D"])):
code += "\n" + "\n".join(proc(read(path),name) for name in names)
@ -45,7 +51,8 @@ def prepare():
npc = read("src_graphics/npc_life_data.inc")
npc = npc[npc.index("npcGuestArt3D "):npc.index("npcLifeClock3D ")]
data = "\n".join(read(path) for path in ("src_graphics/hall_expansion_data.inc",
"src_graphics/cabaret_data.inc","src_graphics/hall_layout.inc")) + "\n.data\n" + npc
"src_graphics/cabaret_data.inc","src_graphics/hall_layout.inc",
"src_graphics/fortune_room_data.inc")) + "\n.data\n" + npc
own_data = set(re.findall(r"^(\w+) (?:dd|db|AnimPack3D|CityProp3D)\b",data,re.M))
coff = Coff(ROOT / "build/graphics/poker.obj")
helper = bytearray(coff.data)
@ -125,6 +132,8 @@ def main():
parser.add_argument("--parity",action="store_true")
parser.add_argument("--prove-alpha-regression",action="store_true")
parser.add_argument("--prove-seam-regression",action="store_true")
parser.add_argument("--prove-distance-regression",action="store_true")
parser.add_argument("--prove-bar-camera-regression",action="store_true")
args = parser.parse_args()
if args.parity:
native,shipping = Coff(OUT/"module.obj"),Coff(ROOT/"build/graphics/poker.obj")
@ -132,11 +141,27 @@ def main():
"include tools/tests/hall_expansion_test.inc")[0]
for name in re.findall(r"^(\w+) PROC",tested_source,re.M):
symbol = next(key for key in native.functions if key.startswith("_"+name+"@"))
assert signature(native,symbol) == signature(shipping,symbol),name
assert signature(native,symbol,tail_branches=True) == signature(shipping,symbol,tail_branches=True),name
print("PASS: all tested navigation, collision, hall/upper rendering, guests and cabaret procedures match the shipping assembly")
return
prepare()
room = proc(read("src_graphics/fortune_room.inc"), "RenderFortuneFurnishings3D")
assert "DrawCityObject3D" not in room, "tarot furniture must not face the camera"
assert room.count("DrawFortuneFixedProp3D") == 24
stem = "module"
if args.prove_bar_camera_regression:
stem = "module-camera-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
before = proc(source,"UpdateInteriorEye3D")
after = before.replace(" mov viewHorizon3D,HALF_H\n", "")
assert before != after
(OUT/(stem+".asm")).write_text(source.replace(before,after),encoding="cp1251")
if args.prove_distance_regression:
stem = "module-old-distance"
source = (OUT/"module.asm").read_text(encoding="cp1251")
assert source.count("cmp eax,64*FP_1") == 1
source = source.replace("cmp eax,64*FP_1", "cmp eax,8*FP_1")
(OUT/(stem+".asm")).write_text(source,encoding="cp1251")
if args.prove_alpha_regression:
stem = "module-alpha-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
@ -165,11 +190,34 @@ def main():
f"build/hall-expansion/{stem}.obj","build/hall-expansion/helpers.obj",
"kernel32.lib","user32.lib","gdi32.lib","winmm.lib","ws2_32.lib"],cwd=ROOT,check=True)
result = subprocess.run([str(OUT/(stem+".exe"))],cwd=ROOT,timeout=180)
if args.prove_bar_camera_regression:
assert result.returncode == 70,result.returncode
print("PASS: removing camera recovery fails the native bar regression")
return
if args.prove_alpha_regression:
assert result.returncode == 1,result.returncode
print("PASS: transparent felt/keyboard regression rejected by actual native assertions")
return
if args.prove_distance_regression:
assert result.returncode == 28,result.returncode
print("PASS: restoring eight-metre cutoff fails the native far-doorway assertion")
return
assert result.returncode == 0,f"native case {result.returncode}"
closed = Image.open(OUT/"fortune-closed.bmp").tobytes()
opened = Image.open(OUT/"fortune-open.bmp").tobytes()
assert closed != opened, "secret panel must visibly reveal the stairwell"
seer = Image.open(OUT/"fortune-seer.bmp").tobytes()
idle = Image.open(OUT/"fortune-seer-idle.bmp").tobytes()
assert seer != idle, "fortune teller must have two distinct idle poses"
dark = ImageStat.Stat(Image.open(OUT/"fortune-seer.bmp").convert("RGB")).mean
full = ImageStat.Stat(Image.open(OUT/"fortune-full-light.bmp").convert("RGB")).mean
assert .25 < sum(dark)/sum(full) < .8, "point lighting must darken the room without blacking it out"
for name in ("fortune-room", "fortune-seer", "fortune-stairs", "fortune-north", "fortune-south"):
pixels = Image.open(OUT/(name+".bmp")).convert("RGB").crop((0,40,640,440))
blue = sum(b > r+12 and b > g for r,g,b in pixels.get_flattened_data())
assert blue > 45000, (name, "chamber must use its own blue palette", blue)
modal = Image.open(OUT/"fortune-unavailable.bmp").convert("RGB").crop((104,176,536,336))
assert len(modal.getcolors(432*160)) > 3, "native unavailable message must be drawn"
entrance = Image.open(OUT / (("old-seam-" if args.prove_seam_regression else "") +
"upper-entrance-open.bmp")).convert("RGB").crop((200,150,300,350))
dominant = max(count for count,_ in entrance.getcolors(20000))/20000
@ -182,6 +230,15 @@ def main():
image = Image.open(path).convert("RGB")
assert image.size == (640,480) and len(image.getcolors(640*480)) > 100
image.save(path.with_suffix(".png"))
gait = [Image.open(OUT/f"gait-{i}.bmp").convert("RGB") for i in range(8)]
assert len({frame.crop((245,360,410,480)).tobytes() for frame in gait}) == 8, "legs must visibly change in native renders"
gait[0].save(OUT/"gait-native.gif",save_all=True,append_images=gait[1:],duration=110,loop=0)
door = [Image.open(OUT/name).convert("RGB") for name in
("old-basement-closed.bmp","door-wheel-turn.bmp","door-swing-half.bmp","basement-door.bmp")]
assert len({frame.tobytes() for frame in door}) == 4, "unlocking, hinge motion and open passage must look different"
closing = [Image.open(OUT/name).convert("RGB") for name in
("door-closing.bmp","door-locking.bmp","door-closed-again.bmp")]
assert len({frame.tobytes() for frame in closing}) == 3, "closing, wheel locking and fully shut poses must differ"
lounge = Image.open(OUT / "upper-lounge.bmp").convert("RGB")
jazz = [Image.open(OUT / ("cabaret-jazz" + (f"-{i}" if i else "") + ".bmp")).convert("RGB")
for i in range(4)]
@ -191,7 +248,15 @@ def main():
vortex = [Image.open(OUT/f"portal-{i:02}.bmp").crop((245,145,395,320)).tobytes()
for i in range(12)]
assert len(set(vortex)) == 12,"all twelve vortex phases must be visible in native frames"
off = Image.open(OUT/"portal-disabled.bmp").crop((245,145,395,320)).tobytes()
assert off not in vortex, "disabled portal must visibly show the sealed arch"
pull = Image.open(OUT/"lever-mid-pull.bmp").crop((200,330,250,480)).tobytes()
release = Image.open(OUT/"lever-mid-release.bmp").crop((200,330,250,480)).tobytes()
assert pull != release, "left handle must visibly return while right remains enabled"
for name,expected in (("guest-cast-v1.bin",(128,224,36)),
("guest-walk-v2.bin",(128,224,48)),
("guest-walk-left-v2.bin",(128,224,48)),
("basement-door-v2.bin",(256,512,18)),
("cabaret-ensemble-v1.bin",(160,256,12)),
("cabaret-piano-v1.bin",(384,256,4)),
("cabaret-guitar-v1.bin",(160,256,4)),
@ -200,14 +265,28 @@ def main():
("upper-bar-v2.bin",(160,256,2)),
("upper-portal-v3.bin",(256,512,13)),
("upper-levers-v3.bin",(128,224,6)),
("upper-braziers-v3.bin",(128,256,4))):
("upper-braziers-v3.bin",(128,256,4)),
("fortune-room-v3.bin",(256,256,24)),
("fortune-materials-v3.bin",(256,256,4))):
raw = (ROOT / "assets/animations/hall" / name).read_bytes()
magic,w,h,count,size = struct.unpack_from("<4s4I",raw)
assert magic == b"ANM1" and (w,h,count) == expected
assert size == w*h*count*4 == len(raw)-20
frames = [raw[20+i*w*h*4:20+(i+1)*w*h*4] for i in range(count)]
if name.startswith("guest"):
if name == "guest-cast-v1.bin":
assert len(set(frames[::6])) == 6, "all six guest appearances must differ"
if name == "guest-walk-v2.bin":
for appearance in range(6):
poses = [Image.frombytes("RGBA",(w,h),frame,"raw","BGRA")
for frame in frames[appearance*8:appearance*8+8]]
assert len({pose.crop((0,120,128,224)).tobytes() for pose in poses}) == 8
assert all(pose.getbbox()[3] == 224 for pose in poses), "supporting foot must touch baseline"
mirrored = (ROOT / "assets/animations/hall/guest-walk-left-v2.bin").read_bytes()[20:]
assert all(Image.frombytes("RGBA",(w,h),frames[i],"raw","BGRA").transpose(
Image.Transpose.FLIP_LEFT_RIGHT).tobytes("raw","BGRA") == mirrored[i*w*h*4:(i+1)*w*h*4] for i in range(48))
if name == "basement-door-v2.bin":
assert len(set(frames[:17])) == 17, "separate rotating handwheel phases"
assert all(frame[:200*256*4] == frames[0][:200*256*4] for frame in frames[:17]), "leaf stays rigid while unlocking"
if name.startswith(("upper-portal", "upper-levers", "upper-braziers", "upper-games")):
assert len(set(frames)) == count,"new animation phases and game tables must differ"
if name.startswith("upper-portal"):
@ -222,7 +301,7 @@ def main():
sprite = Image.frombytes("RGBA", (w,h), frame, "raw", "BGRA")
x0,y0,x1,y1 = sprite.getbbox()
assert 0 < x0 < x1 < w and 0 < y0 < y1 < h, "uncropped musician cutout"
print("PASS: native reversible stairs, all eight doorway views, foreground occlusion, original door/ticket positions, two-lever lock and twelve visible portal phases")
print("PASS: native stairs and doorway views, automatic door, original ticket position, distance-driven gait, reversible portal, secret lantern passage, manual cellar stairs and fortune teller unavailable dialogue")
if __name__ == "__main__":

View file

@ -19,9 +19,9 @@ def proc(text, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", text, re.M | re.S).group()
def signature(coff, name):
def signature(coff, name, tail_branches=False):
result = []
for code, refs, target in coff.signature(name):
for code, refs, target in coff.signature(name, tail_branches=tail_branches):
if code[:1] == b"\xe8" and len(refs) == 1 and refs[0][1][1] == 20:
target = refs[0][1][0]
refs = ()
@ -38,6 +38,7 @@ def prepare():
code += "\n" + proc(read("src_graphics/world_dressing.inc"), "RenderHallDressing3D")
# The shared surface dispatcher also knows the separately packed upper lounge.
code += "\n" + proc(read("src_graphics/hall_expansion.inc"), "ResolveUpperMaterial3D")
code += "\n" + proc(read("src_graphics/fortune_room.inc"), "ResolveFortuneMaterial3D")
trigger = proc(read("casino3d.inc"), "Trigger3DInteraction")
code += "\n" + trigger
if "_InitHellTower3D@0" not in Coff(ROOT / "build/actions/actions-test.obj").functions:
@ -47,9 +48,11 @@ def prepare():
own_functions = set(re.findall(r"^(\w+) PROC", code, re.M))
own_data = set(re.findall(r"^(\w+) (?:dd|db|AnimPack3D)", read("src_graphics/terminal_data.inc"), re.M))
own_data.update(("cityRayX3D", "cityRayY3D"))
own_data.update(("fortuneMaterialArt3D", "fortuneArt3D"))
own_data.update(("upperMaterialArt3D","upperFeltPixels3D","upperKeyPixels3D",
"upperGlowPixels3D","upperPortalArt3D","upperPortal3D",
"portalRendering3D","portalClipDepth3D"))
"portalRendering3D","portalClipDepth3D","basementDoorArt3D",
"basementWheelTime3D"))
coff = Coff(ROOT / "build/actions/actions-test.obj")
data = bytearray(coff.data)
at = struct.unpack_from("<I", data, 8)[0]
@ -142,7 +145,8 @@ NpcWaypoint ENDS
constants.update(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)", path.read_text(encoding="cp1251"), re.M))
for name in sorted(set(re.findall(r"\b(?:MODE_|INTERACT_)\w+", trigger)) - {"MODE_WALK", "MODE_ATM"}):
text += f"\n{name} equ {constants[name]}"
text += "\nResolveUpperMaterial3D PROTO :DWORD\ninclude src_graphics/terminal_data.inc\n.data\nupperMaterialArt3D AnimPack3D <>\nupperPortalArt3D AnimPack3D <>\nupperPortal3D CityProp3D <>\nportalRendering3D dd 0\n.data?\nupperFeltPixels3D dd 1024 dup(?)\nupperKeyPixels3D dd 1024 dup(?)\nupperGlowPixels3D dd 1024 dup(?)\nportalClipDepth3D dd 640 dup(?)\ncityRayX3D dd 640 dup(?)\ncityRayY3D dd 640 dup(?)\n.code\n"
text += "\nResolveUpperMaterial3D PROTO :DWORD\ninclude src_graphics/terminal_data.inc\n.data\nupperMaterialArt3D AnimPack3D <>\nupperPortalArt3D AnimPack3D <>\nupperPortal3D CityProp3D <>\nbasementDoorArt3D AnimPack3D <>\nbasementWheelTime3D dd 0\nportalRendering3D dd 0\n.data?\nupperFeltPixels3D dd 1024 dup(?)\nupperKeyPixels3D dd 1024 dup(?)\nupperGlowPixels3D dd 1024 dup(?)\nportalClipDepth3D dd 640 dup(?)\ncityRayX3D dd 640 dup(?)\ncityRayY3D dd 640 dup(?)\n.code\n"
text += "\nResolveFortuneMaterial3D PROTO :DWORD\n.data\nfortuneMaterialArt3D AnimPack3D <>\nfortuneArt3D AnimPack3D <>\n.code\n"
text += code + "\ninclude tools/tests/terminal_test.inc\nend TerminalTestStart\n"
(OUT / "module.asm").write_text(text, encoding="ascii")

View file

@ -0,0 +1,375 @@
.data
fortuneTestClosed db "build\hall-expansion\fortune-closed.bmp",0
fortuneTestPull db "build\hall-expansion\fortune-lamp-pulled.bmp",0
fortuneTestOpen db "build\hall-expansion\fortune-open.bmp",0
fortuneTestFar db "build\hall-expansion\fortune-open-far.bmp",0
fortuneTestStairs db "build\hall-expansion\fortune-stairs.bmp",0
fortuneTestTopReturn db "build\hall-expansion\fortune-stair-return.bmp",0
fortuneTestRoom db "build\hall-expansion\fortune-room.bmp",0
fortuneTestNear db "build\hall-expansion\fortune-seer.bmp",0
fortuneTestIdle db "build\hall-expansion\fortune-seer-idle.bmp",0
fortuneTestDialog db "build\hall-expansion\fortune-unavailable.bmp",0
fortuneTestReturn db "build\hall-expansion\fortune-room-return.bmp",0
fortuneTestNorth db "build\hall-expansion\fortune-north.bmp",0
fortuneTestSouth db "build\hall-expansion\fortune-south.bmp",0
fortuneTestMissing db "build\hall-expansion\fortune-missing-art.bmp",0
fortuneTestOblique db "build\hall-expansion\fortune-oblique.bmp",0
fortuneTestFullLight db "build\hall-expansion\fortune-full-light.bmp",0
fortuneTestEast db "build\hall-expansion\fortune-east.bmp",0
fortuneTestCentered db "build\hall-expansion\fortune-doorway-centered.bmp",0
fortuneTestBank dd 0
fortuneTestView dd 0
.code
FortuneNativeTests PROC uses esi edi
mov expansionStage,60
cmp fortuneArt3D.frames,24
jne fortune_test_fail
cmp fortuneArt3D.imageWidth,256
jne fortune_test_fail
cmp fortuneMaterialArt3D.frames,4
jne fortune_test_fail
invoke ResolveCitySurface3D,132
cmp eax,fortuneMaterialArt3D.pixels
jne fortune_test_fail
invoke ResolveCitySurface3D,135
mov edx,fortuneMaterialArt3D.pixels
add edx,3*256*256*4
cmp eax,edx
jne fortune_test_fail
invoke ResolveCitySurface3D,159
mov edx,fortuneArt3D.pixels
add edx,23*256*256*4
cmp eax,edx
jne fortune_test_fail
invoke FortuneLightAt3D,216,152,26
cmp eax,174
jb fortune_test_fail
invoke FortuneLightAt3D,0,0,0
test eax,eax
jnz fortune_test_fail
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov fortunePassageTime3D,0
mov fortunePassageTarget3D,0
mov deltaTime3D,100
mov playerX,3*FP_1
mov playerY,10*FP_1
mov playerAngle,256
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestClosed
invoke CanWalk3D,FP_1*15/10,10*FP_1
test eax,eax
jnz fortune_test_fail
mov playerX,FP_1*26/10
mov playerY,FP_1*87/10
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_SECRET_LAMP
jne fortune_test_fail
call Trigger3DInteraction
cmp fortunePassageTarget3D,1
jne fortune_test_fail
add animationClock3D,100
call UpdateInteriorTravel3D
cmp fortunePassageTime3D,100
jne fortune_test_fail
call UpdateFortunePassage3D
cmp fortunePassageTime3D,100
jne fortune_test_fail
add animationClock3D,100
call UpdateInteriorTravel3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestPull
mov esi,12
fortune_test_opening:
add animationClock3D,100
call UpdateInteriorTravel3D
dec esi
jnz fortune_test_opening
cmp fortunePassageTime3D,1400
jne fortune_test_fail
invoke CanWalk3D,FP_1*15/10,10*FP_1
cmp eax,1
jne fortune_test_fail
call CheckInteractions
cmp interactTargetId,INTERACT_SECRET_LAMP
jne fortune_test_fail
cmp interactPromptPtr,OFFSET fortuneLampReturn3D
jne fortune_test_fail
mov playerX,3*FP_1
mov playerY,10*FP_1
call ExpansionFrame
cmp portalPixelsWritten3D,1000
jb fortune_test_fail
invoke ExpansionSnapshot,OFFSET fortuneTestOpen
mov playerX,FP_1*85/10
call ExpansionFrame
cmp portalPixelsWritten3D,1000
jb fortune_test_fail
invoke ExpansionSnapshot,OFFSET fortuneTestFar
mov expansionStage,61
mov deltaTime3D,16
mov playerX,3*FP_1
mov playerY,10*FP_1
mov keys3D[57h],1
xor esi,esi
fortune_test_walk_down:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,3
je fortune_test_arrived
@@:
inc esi
cmp esi,1000
jb fortune_test_walk_down
jmp fortune_test_fail
fortune_test_arrived:
mov keys3D[57h],0
cmp worldMap3D,OFFSET fortuneRoomMap3D
jne fortune_test_fail
cmp floorTexture3D,134
jne fortune_test_fail
cmp playerY,11*FP_1
jne fortune_test_fail
mov eax,fortuneSeer3D.posY
cmp eax,playerY
jne fortune_test_fail
invoke CanWalk3D,FP_1*173/10,FP_1*103/10
cmp eax,1
jne fortune_test_fail
invoke CanWalk3D,FP_1*173/10,FP_1*117/10
cmp eax,1
jne fortune_test_fail
; Off-centre crossing preserves lateral position in both local maps.
mov playerX,FP_1*176/10
mov playerY,FP_1*115/10
call UpdateInteriorTravel3D
cmp engineMode,MODE_STAIRS
jne fortune_test_fail
cmp playerY,FP_1*105/10
jne fortune_test_fail
mov playerX,17*FP_1
call UpdateInteriorTravel3D
cmp hallLevel3D,3
jne fortune_test_fail
cmp playerY,FP_1*115/10
jne fortune_test_fail
mov playerY,11*FP_1
cmp playerAngle,256
jne fortune_test_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestRoom
mov playerX,16*FP_1
mov playerY,FP_1*116/10
mov playerAngle,384
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestNorth
mov playerX,16*FP_1
mov playerY,FP_1*107/10
mov playerAngle,128
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestSouth
mov playerX,FP_1*155/10
mov playerY,FP_1*116/10
mov playerAngle,0
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestEast
invoke CanWalk3D,FP_1*146/10,FP_1*126/10
test eax,eax
jnz fortune_test_fail
invoke CanWalk3D,FP_1*163/10,FP_1*925/100
test eax,eax
jnz fortune_test_fail
mov expansionStage,62
; A mid-flight camera shows physical treads and continuous eye height.
mov engineMode,MODE_STAIRS
mov stairRoute3D,3
mov hallLevel3D,0
mov worldMap3D,OFFSET fortuneStairMap3D
mov playerX,24*FP_1
mov playerY,10*FP_1
mov playerAngle,256
call UpdateStairHeight3D
cmp viewEye3D,HALL_STANDING_EYE+FP_1*15/16
jne fortune_test_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestStairs
mov playerX,28*FP_1
mov playerAngle,0
call UpdateStairHeight3D
call ExpansionFrame
cmp portalPixelsWritten3D,1000
jb fortune_test_fail
invoke ExpansionSnapshot,OFFSET fortuneTestTopReturn
; The panel cannot close over somebody still at its upper landing.
mov fortunePassageTarget3D,0
mov playerX,29*FP_1
add animationClock3D,16
call UpdateFortunePassage3D
cmp fortunePassageTime3D,1400
jne fortune_test_fail
mov playerX,24*FP_1
add animationClock3D,16
call UpdateFortunePassage3D
cmp fortunePassageTime3D,1400
jne fortune_test_fail
mov engineMode,MODE_WALK
mov hallLevel3D,3
add animationClock3D,16
call UpdateFortunePassage3D
cmp fortunePassageTime3D,1400
jne fortune_test_fail
mov fortunePassageTarget3D,1
mov engineMode,MODE_WALK
mov hallLevel3D,3
mov worldMap3D,OFFSET fortuneRoomMap3D
mov playerX,FP_1*158/10
mov playerY,11*FP_1
mov playerAngle,256
call UpdateInteriorEye3D
mov animationClock3D,6000
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestNear
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderFortuneFurnishings3D
invoke ExpansionSnapshot,OFFSET fortuneTestFullLight
call ApplyFortuneLighting3D
mov playerX,FP_1*164/10
mov playerY,FP_1*116/10
mov playerAngle,288
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestOblique
mov playerX,FP_1*158/10
mov playerY,11*FP_1
mov playerAngle,256
mov esi,fortuneMaterialArt3D.pixels
mov edi,hallArchitectureHD3D.pixels
mov fortuneMaterialArt3D.pixels,0
mov hallArchitectureHD3D.pixels,0
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestMissing
mov fortuneMaterialArt3D.pixels,esi
mov hallArchitectureHD3D.pixels,edi
call ExpansionFrame
call CheckInteractions
cmp interactTargetId,INTERACT_FORTUNE
jne fortune_test_fail
invoke CanWalk3D,FP_1*141/10,11*FP_1
test eax,eax
jnz fortune_test_fail
mov animationClock3D,5000
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestIdle
mov eax,bankroll
mov fortuneTestBank,eax
mov eax,playerX
mov fortuneTestView,eax
call Trigger3DInteraction
cmp engineMode,MODE_UNAVAILABLE
jne fortune_test_fail
call RenderUnavailable3D
invoke ExpansionSnapshot,OFFSET fortuneTestDialog
mov pressedKeys3D[0Dh],1
call HandleUnavailable3D
cmp engineMode,MODE_WALK
jne fortune_test_fail
cmp hallLevel3D,3
jne fortune_test_fail
mov eax,bankroll
cmp eax,fortuneTestBank
jne fortune_test_fail
mov eax,playerX
cmp eax,fortuneTestView
jne fortune_test_fail
mov playerX,FP_1*1495/100
mov playerY,11*FP_1
mov playerAngle,0
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET fortuneTestCentered
xor esi,esi
fortune_test_first_column:
cmp portalClipDepth3D[esi*4],0
jne fortune_test_find_last
inc esi
cmp esi,SCREEN_W
jb fortune_test_first_column
jmp fortune_test_fail
fortune_test_find_last:
mov edi,SCREEN_W-1
fortune_test_last_column:
cmp portalClipDepth3D[edi*4],0
jne fortune_test_center_mask
dec edi
jns fortune_test_last_column
jmp fortune_test_fail
fortune_test_center_mask:
add esi,edi
cmp esi,SCREEN_W-2
jb fortune_test_fail
cmp esi,SCREEN_W+2
ja fortune_test_fail
mov playerX,16*FP_1
call ExpansionFrame
cmp portalPixelsWritten3D,1000
jb fortune_test_fail
invoke ExpansionSnapshot,OFFSET fortuneTestReturn
mov expansionStage,63
mov keys3D[57h],1
xor esi,esi
fortune_test_walk_up:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
jne @F
cmp hallLevel3D,0
je fortune_test_back_hall
@@:
inc esi
cmp esi,1000
jb fortune_test_walk_up
jmp fortune_test_fail
fortune_test_back_hall:
mov keys3D[57h],0
cmp worldMap3D,OFFSET casinoMap
jne fortune_test_fail
cmp playerAngle,0
jne fortune_test_fail
cmp playerX,2*FP_1
ja fortune_test_fail
mov playerX,FP_1*26/10
mov playerY,FP_1*87/10
mov playerAngle,256
call UpdateCamera3D
call CheckInteractions
cmp interactTargetId,INTERACT_SECRET_LAMP
jne fortune_test_fail
call Trigger3DInteraction
cmp fortunePassageTarget3D,0
jne fortune_test_fail
mov deltaTime3D,100
mov esi,14
fortune_test_closing:
add animationClock3D,100
call UpdateFortunePassage3D
dec esi
jnz fortune_test_closing
cmp fortunePassageTime3D,0
jne fortune_test_fail
invoke CanWalk3D,FP_1*15/10,10*FP_1
test eax,eax
jnz fortune_test_fail
cmp scrStation3D.posX,29*FP_1
jne fortune_test_fail
cmp scrStation3D.posY,FP_1*15/2
jne fortune_test_fail
ret
fortune_test_fail:
invoke ExitProcess,expansionStage
ret
FortuneNativeTests ENDP

View file

@ -1,6 +1,10 @@
.data
expansionStage dd 1
expansionOcclusionPixel dd 0
expansionGaitX dd 0
expansionGaitY dd 0
expansionGaitPhase dd 0
expansionDoorState dd 0
expansionBmpHeader db 42h,4Dh
dd 1228854
dw 0,0
@ -8,6 +12,25 @@ expansionBmpHeader db 42h,4Dh
dw 1,32
dd 0,1228800,0,0,0,0
expansionDoor db "build\hall-expansion\basement-door.bmp",0
expansionDoorWheel db "build\hall-expansion\door-wheel-turn.bmp",0
expansionDoorSwing db "build\hall-expansion\door-swing-half.bmp",0
expansionDoorClosing db "build\hall-expansion\door-closing.bmp",0
expansionDoorLocking db "build\hall-expansion\door-locking.bmp",0
expansionDoorShut db "build\hall-expansion\door-closed-again.bmp",0
expansionDoorBack db "build\hall-expansion\door-from-stairs.bmp",0
expansionFarUpper db "build\hall-expansion\upper-return-far.bmp",0
expansionFarEntry db "build\hall-expansion\upper-entry-far.bmp",0
expansionFarBasement db "build\hall-expansion\basement-return-far.bmp",0
expansionGait0 db "build\hall-expansion\gait-0.bmp",0
expansionGait1 db "build\hall-expansion\gait-1.bmp",0
expansionGait2 db "build\hall-expansion\gait-2.bmp",0
expansionGait3 db "build\hall-expansion\gait-3.bmp",0
expansionGait4 db "build\hall-expansion\gait-4.bmp",0
expansionGait5 db "build\hall-expansion\gait-5.bmp",0
expansionGait6 db "build\hall-expansion\gait-6.bmp",0
expansionGait7 db "build\hall-expansion\gait-7.bmp",0
expansionGaitPaths dd OFFSET expansionGait0,OFFSET expansionGait1,OFFSET expansionGait2,OFFSET expansionGait3
dd OFFSET expansionGait4,OFFSET expansionGait5,OFFSET expansionGait6,OFFSET expansionGait7
expansionStairs db "build\hall-expansion\upper-stairs.bmp",0
expansionStairsMid db "build\hall-expansion\upper-stairs-mid.bmp",0
expansionUpper db "build\hall-expansion\upper-lounge.bmp",0
@ -32,6 +55,8 @@ expansionRussianTable db "build\hall-expansion\upper-russian-table.bmp",0
expansionBarWorld db "build\hall-expansion\upper-bar-world.bmp",0
expansionSeating db "build\hall-expansion\upper-seating.bmp",0
expansionLeverMid db "build\hall-expansion\lever-mid-pull.bmp",0
expansionPortalOff db "build\hall-expansion\portal-disabled.bmp",0
expansionLeverRelease db "build\hall-expansion\lever-mid-release.bmp",0
expansionPortal0 db "build\hall-expansion\portal-00.bmp",0
expansionPortal1 db "build\hall-expansion\portal-01.bmp",0
expansionPortal2 db "build\hall-expansion\portal-02.bmp",0
@ -94,6 +119,16 @@ ExpansionPortalSnapshot PROC path:DWORD
ret
ExpansionPortalSnapshot ENDP
ExpansionDoorTicks PROC uses esi count:DWORD
mov esi,count
expansion_door_tick:
add animationClock3D,16
call UpdateBasementDoor3D
dec esi
jnz expansion_door_tick
ret
ExpansionDoorTicks ENDP
ExpansionTestStart PROC
call InitPaths3D
call LoadTextures3D
@ -102,6 +137,12 @@ ExpansionTestStart PROC
call LoadHallAssets3D
cmp npcGuestArt3D.pixels,0
je expansion_fail
cmp npcWalkArt3D.frames,48
jne expansion_fail
cmp npcWalkLeftArt3D.frames,48
jne expansion_fail
cmp basementDoorArt3D.frames,18
jne expansion_fail
cmp cabaretArt3D.pixels,0
je expansion_fail
cmp cabaretPianoArt3D.pixels,0
@ -126,25 +167,57 @@ ExpansionTestStart PROC
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov expansionStage,2
invoke CanWalk3D,FP_1*285/10,FP_1*105/10
invoke CanWalk3D,FP_1*289/10,FP_1*105/10
test eax,eax
jnz expansion_fail
mov interactTargetId,INTERACT_BASEMENT
call TriggerHallInteraction3D
mov playerX,27*FP_1
mov playerY,FP_1*105/10
mov playerAngle,0
add animationClock3D,16
call UpdateInteriorTravel3D
cmp engineMode,MODE_WALK
jne expansion_fail
cmp basementDoorTarget3D,FP_1
jne expansion_fail
cmp basementDoorOpen3D,0
jne expansion_fail
xor esi,esi
expansion_open_door:
add animationClock3D,16
call UpdateInteriorTravel3D
inc esi
cmp esi,60
cmp esi,20
jb expansion_open_door
cmp basementWheelTime3D,0
je expansion_fail
cmp basementDoorOpen3D,0
jne expansion_fail
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoorWheel
mov esi,50
expansion_door_swing:
add animationClock3D,16
call UpdateInteriorTravel3D
dec esi
jnz expansion_door_swing
cmp basementWheelTime3D,700
jne expansion_fail
cmp basementDoorOpen3D,0
je expansion_fail
cmp basementDoorOpen3D,FP_1
jae expansion_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoorSwing
mov esi,50
expansion_finish_door:
add animationClock3D,16
call UpdateInteriorTravel3D
dec esi
jnz expansion_finish_door
cmp basementDoorOpen3D,FP_1
jne expansion_fail
invoke CanWalk3D,FP_1*285/10,FP_1*105/10
invoke CanWalk3D,FP_1*289/10,FP_1*105/10
cmp eax,1
jne expansion_fail
mov playerX,27*FP_1
@ -477,11 +550,48 @@ expansion_cast_positions:
mov playerAngle,128
invoke ExpansionPortalSnapshot,OFFSET expansionBunkerReturn
mov expansionStage,28
; These viewpoints used to lose their continuation at the eight-metre cutoff.
mov engineMode,MODE_STAIRS
mov stairRoute3D,2
mov worldMap3D,OFFSET upperStairMap3D
mov floorTexture3D,85
mov playerX,12*FP_1
mov playerY,21*FP_1
mov playerAngle,256
call UpdateInteriorEye3D
call ExpansionFrame
cmp portalPixelsWritten3D,100
jb expansion_fail
invoke ExpansionSnapshot,OFFSET expansionFarUpper
mov engineMode,MODE_WALK
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
mov floorTexture3D,100
mov playerX,21*FP_1
mov playerAngle,0
call UpdateInteriorEye3D
call ExpansionFrame
cmp portalPixelsWritten3D,100
jb expansion_fail
invoke ExpansionSnapshot,OFFSET expansionFarEntry
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
mov floorTexture3D,94
mov playerX,12*FP_1
mov playerAngle,256
call UpdateInteriorEye3D
call ExpansionFrame
cmp portalPixelsWritten3D,100
jb expansion_fail
invoke ExpansionSnapshot,OFFSET expansionFarBasement
mov engineMode,MODE_WALK
mov hallLevel3D,2
mov worldMap3D,OFFSET upperLoungeMap3D
mov floorTexture3D,124
mov playerX,FP_1*105/10
mov playerY,FP_1*193/10
mov playerAngle,128
mov upperLeverMask3D,0
mov upperPortalReady3D,0
mov upperLeverTime3D,0
@ -543,6 +653,119 @@ expansion_portal_motion:
inc esi
cmp esi,12
jb expansion_portal_motion
mov expansionStage,29
mov playerX,FP_1*91/10
mov playerY,FP_1*214/10
mov playerAngle,128
call UpdateCamera3D
call CheckUpperInteractions3D
cmp interactTargetId,INTERACT_UPPER_LEFT
jne expansion_fail
cmp interactPromptPtr,OFFSET upperLeftRelease3D
jne expansion_fail
call TriggerHallInteraction3D
cmp upperLeverMask3D,2
jne expansion_fail
cmp upperPortalReady3D,0
jne expansion_fail
mov playerX,FP_1*105/10
mov playerY,FP_1*244/10
call TryWalkUpperPortal3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov interactTargetId,INTERACT_HELL_TOWER
call TriggerHallInteraction3D
cmp engineMode,MODE_WALK
jne expansion_fail
mov playerY,FP_1*193/10
call ExpansionFrame
cmp upperPortal3D.frame,0
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionPortalOff
mov esi,20
expansion_release_half:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_release_half
cmp upperLeverTime3D,280
jne expansion_fail
cmp upperLeverTime3D[4],600
jne expansion_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionLeverRelease
; Reverse the handle halfway through release, without snapping to an endpoint.
mov interactTargetId,INTERACT_UPPER_LEFT
call TriggerHallInteraction3D
cmp upperLeverTime3D,280
jne expansion_fail
cmp upperPortalReady3D,0
jne expansion_fail
mov esi,20
expansion_reenable_half:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_reenable_half
cmp upperPortalReady3D,1
jne expansion_fail
; The right handle is also available after activation, and disables immediately.
mov playerX,FP_1*119/10
mov playerY,FP_1*214/10
call CheckUpperInteractions3D
cmp interactTargetId,INTERACT_UPPER_RIGHT
jne expansion_fail
cmp interactPromptPtr,OFFSET upperRightRelease3D
jne expansion_fail
call TriggerHallInteraction3D
cmp upperLeverMask3D,1
jne expansion_fail
cmp upperPortalReady3D,0
jne expansion_fail
mov esi,40
expansion_release_right:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_release_right
cmp upperLeverTime3D[4],0
jne expansion_fail
cmp upperLeverTime3D,600
jne expansion_fail
mov interactTargetId,INTERACT_UPPER_LEFT
call TriggerHallInteraction3D
mov esi,40
expansion_release_both:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_release_both
cmp upperLeverMask3D,0
jne expansion_fail
cmp upperLeverTime3D,0
jne expansion_fail
cmp upperLeverTime3D[4],0
jne expansion_fail
cmp upperPortalReady3D,0
jne expansion_fail
call CheckUpperInteractions3D
cmp interactTargetId,INTERACT_UPPER_RIGHT
jne expansion_fail
cmp interactPromptPtr,OFFSET upperRightPrompt3D
jne expansion_fail
mov interactTargetId,INTERACT_UPPER_LEFT
call TriggerHallInteraction3D
mov interactTargetId,INTERACT_UPPER_RIGHT
call TriggerHallInteraction3D
mov esi,40
expansion_reopen_both:
add animationClock3D,16
call UpdateUpperEffects3D
dec esi
jnz expansion_reopen_both
cmp upperPortalReady3D,1
jne expansion_fail
mov playerX,FP_1*105/10
mov playerY,FP_1*244/10
call TryWalkUpperPortal3D
cmp engineMode,MODE_HELL_TOWER
@ -591,8 +814,358 @@ expansion_portal_motion:
mov hRaycastThread,0
mov hRaycastStartEvent,0
mov hRaycastDoneEvent,0
mov expansionStage,31
mov hallLevel3D,0
mov worldMap3D,OFFSET casinoMap
; Equal walked distance at two update rates must select the same gait phase.
call InitNpcLife3D
invoke NpcPlanPath3D,0,5
test eax,eax
jz expansion_fail
mov npcLife3D.state,NPC_WALK
mov esi,10
expansion_gait_rate16:
invoke NpcMove3D,0,16
dec esi
jnz expansion_gait_rate16
mov eax,casinoNpcs3D.posX
mov expansionGaitX,eax
mov eax,casinoNpcs3D.posY
mov expansionGaitY,eax
mov eax,npcWalkDistance3D
mov expansionGaitPhase,eax
call InitNpcLife3D
invoke NpcPlanPath3D,0,5
test eax,eax
jz expansion_fail
mov npcLife3D.state,NPC_WALK
mov esi,5
expansion_gait_rate32:
invoke NpcMove3D,0,32
dec esi
jnz expansion_gait_rate32
mov eax,casinoNpcs3D.posX
cmp eax,expansionGaitX
jne expansion_fail
mov playerX,eax
mov eax,casinoNpcs3D.posY
cmp eax,expansionGaitY
jne expansion_fail
mov playerY,eax
mov eax,npcWalkDistance3D
cmp eax,expansionGaitPhase
jne expansion_fail
invoke NpcMove3D,0,16
mov eax,npcWalkDistance3D
cmp eax,expansionGaitPhase
jne expansion_fail
cmp npcLife3D.state,NPC_STAND
jne expansion_fail
mov playerX,FP_1*285/10
mov playerY,21*FP_1
call InitNpcLife3D
; A blocked move must not tick the gait. A successful one consumes distance.
mov npcLife3D.state,NPC_WALK
mov npcLife3D.pathCount,0
invoke NpcMove3D,0,16
cmp npcWalkDistance3D,0
jne expansion_fail
invoke NpcPlanPath3D,0,5
test eax,eax
jz expansion_fail
mov npcLife3D.state,NPC_WALK
invoke NpcMove3D,0,16
cmp npcWalkDistance3D,0
je expansion_fail
mov eax,npcWalkDistance3D
mov expansionOcclusionPixel,eax
invoke NpcMove3D,0,0
mov eax,npcWalkDistance3D
cmp eax,expansionOcclusionPixel
jne expansion_fail
; Native renders of each gait phase, not just hashes of offline source art.
mov esi,1
expansion_gait_hide:
imul ebx,esi,SIZEOF NpcLife3D
mov npcLife3D[ebx].state,NPC_AWAY
inc esi
cmp esi,NPC_LIFE_COUNT
jb expansion_gait_hide
mov npcLife3D.state,NPC_WALK
mov npcLife3D.identity,0
mov casinoNpcs3D.posX,FP_1*165/10
mov casinoNpcs3D.posY,FP_1*175/10
mov npcWalkVelocityX3D,FP_1
mov npcWalkVelocityY3D,0
mov playerX,FP_1*165/10
mov playerY,FP_1*195/10
mov playerAngle,384
call UpdateInteriorEye3D
xor esi,esi
expansion_gait_frame:
imul eax,esi,FP_1*3/32
mov npcWalkDistance3D,eax
call UpdateCamera3D
call RenderCeilingAndFloor
call RenderWalls
call PrepareCityDepth3D
call RenderNpcLife3D
mov eax,npcSprite3D.frame
cmp eax,esi
jne expansion_fail
invoke ExpansionSnapshot,expansionGaitPaths[esi*4]
inc esi
cmp esi,8
jb expansion_gait_frame
mov npcWalkVelocityX3D,-FP_1
call PrepareCityDepth3D
call RenderNpcLife3D
cmp npcSprite3D.asset,OFFSET npcWalkLeftArt3D
jne expansion_fail
mov expansionStage,32
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
mov floorTexture3D,94
mov playerX,FP_1*46/10
mov playerY,21*FP_1
mov playerAngle,256
mov basementDoorTarget3D,0
mov basementDoorOpen3D,0
mov basementWheelTime3D,0
invoke CanWalk3D,FP_1*39/10,FP_1*227/10
test eax,eax
jnz expansion_fail
mov keys3D[57h],1
mov esi,10
expansion_return_closed:
call ExpansionWalkFrame
dec esi
jnz expansion_return_closed
cmp engineMode,MODE_STAIRS
jne expansion_fail
cmp playerX,FP_1*405/100
jb expansion_fail
cmp basementDoorTarget3D,FP_1
jne expansion_fail
cmp basementDoorOpen3D,0
jne expansion_fail
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoorBack
mov esi,150
expansion_return_auto_open:
call ExpansionWalkFrame
cmp engineMode,MODE_WALK
je expansion_return_opened
dec esi
jnz expansion_return_auto_open
jmp expansion_fail
expansion_return_opened:
mov keys3D[57h],0
cmp hallLevel3D,0
jne expansion_fail
cmp basementWheelTime3D,700
jne expansion_fail
cmp basementDoorOpen3D,FP_1*9/10
jb expansion_fail
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
invoke CanWalk3D,FP_1*39/10,FP_1*227/10
test eax,eax
jnz expansion_fail
invoke CanWalk3D,FP_1*39/10,21*FP_1
cmp eax,1
jne expansion_fail
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET casinoMap
mov expansionStage,33
mov playerX,FP_1*305/10
mov playerY,FP_1*305/10
call InitNpcLife3D
mov deltaTime3D,50
mov esi,2400
expansion_long_gait:
call UpdateNpcLife3D
xor edi,edi
expansion_gait_bounds:
cmp npcWalkDistance3D[edi*4],FP_1*3/4
jae expansion_fail
imul ebx,edi,SIZEOF NpcMindEntity
invoke CanWalk3D,casinoNpcs3D[ebx].posX,casinoNpcs3D[ebx].posY
cmp eax,1
jne expansion_fail
inc edi
cmp edi,NPC_LIFE_COUNT
jb expansion_gait_bounds
dec esi
jnz expansion_long_gait
cmp npcLifeClock3D,120000
jne expansion_fail
mov expansionStage,34
mov deltaTime3D,16
mov floorTexture3D,100
mov playerX,FP_1*288/10
mov playerY,FP_1*105/10
mov playerAngle,0
mov basementDoorTarget3D,FP_1
mov basementDoorOpen3D,FP_1
mov basementWheelTime3D,700
mov basementDoorHold3D,0
invoke ExpansionDoorTicks,200
cmp basementDoorOpen3D,FP_1
jne expansion_fail
cmp basementDoorHold3D,1200
jne expansion_fail
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
mov playerX,FP_1*43/10
mov playerY,21*FP_1
invoke ExpansionDoorTicks,200
cmp basementDoorOpen3D,FP_1
jne expansion_fail
cmp basementWheelTime3D,700
jne expansion_fail
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET casinoMap
mov playerX,25*FP_1
mov playerY,FP_1*105/10
add animationClock3D,16
call UpdateBasementDoor3D
cmp basementDoorHold3D,1184
jne expansion_fail
call UpdateBasementDoor3D
cmp basementDoorHold3D,1184
jne expansion_fail
invoke ExpansionDoorTicks,74
cmp basementDoorHold3D,0
jne expansion_fail
cmp basementDoorOpen3D,FP_1
jne expansion_fail
invoke ExpansionDoorTicks,20
cmp basementDoorOpen3D,0
je expansion_fail
cmp basementDoorOpen3D,FP_1
jae expansion_fail
cmp basementWheelTime3D,700
jne expansion_fail
mov eax,basementDoorOpen3D
mov expansionDoorState,eax
mov playerX,27*FP_1
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoorClosing
; Returning during the swing reverses smoothly and keeps the wheel unlocked.
invoke ExpansionDoorTicks,1
mov eax,basementDoorOpen3D
cmp eax,expansionDoorState
jbe expansion_fail
cmp eax,FP_1
jae expansion_fail
cmp basementWheelTime3D,700
jne expansion_fail
invoke ExpansionDoorTicks,75
cmp basementDoorOpen3D,FP_1
jne expansion_fail
mov playerX,25*FP_1
invoke ExpansionDoorTicks,75
mov esi,70
expansion_wait_closed_leaf:
invoke ExpansionDoorTicks,1
cmp basementDoorOpen3D,0
je expansion_leaf_shut
cmp basementWheelTime3D,700
jne expansion_fail
dec esi
jnz expansion_wait_closed_leaf
jmp expansion_fail
expansion_leaf_shut:
cmp basementWheelTime3D,600
jbe expansion_fail
invoke ExpansionDoorTicks,20
cmp basementDoorOpen3D,0
jne expansion_fail
cmp basementWheelTime3D,0
je expansion_fail
cmp basementWheelTime3D,700
jae expansion_fail
mov eax,basementWheelTime3D
mov expansionDoorState,eax
mov playerX,27*FP_1
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET expansionDoorLocking
; Returning during locking unlocks from that pose before moving the leaf.
invoke ExpansionDoorTicks,1
cmp basementDoorOpen3D,0
jne expansion_fail
mov eax,basementWheelTime3D
cmp eax,expansionDoorState
jbe expansion_fail
invoke ExpansionDoorTicks,100
cmp basementDoorOpen3D,FP_1
jne expansion_fail
mov playerX,25*FP_1
invoke ExpansionDoorTicks,200
cmp basementDoorTarget3D,0
jne expansion_fail
cmp basementDoorOpen3D,0
jne expansion_fail
cmp basementWheelTime3D,0
jne expansion_fail
invoke CanWalk3D,FP_1*289/10,FP_1*105/10
test eax,eax
jnz expansion_fail
mov playerX,27*FP_1
call ExpansionFrame
cmp portalPixelsWritten3D,0
jne expansion_fail
invoke ExpansionSnapshot,OFFSET expansionDoorShut
mov engineMode,MODE_STAIRS
mov stairRoute3D,1
mov worldMap3D,OFFSET basementStairMap3D
mov playerX,FP_1*43/10
mov playerY,21*FP_1
invoke ExpansionDoorTicks,120
cmp basementDoorOpen3D,FP_1
jne expansion_fail
mov playerX,8*FP_1
invoke ExpansionDoorTicks,200
cmp basementDoorOpen3D,0
jne expansion_fail
cmp basementWheelTime3D,0
jne expansion_fail
invoke CanWalk3D,FP_1*39/10,21*FP_1
test eax,eax
jnz expansion_fail
mov playerX,FP_1*43/10
invoke ExpansionDoorTicks,120
cmp basementDoorOpen3D,FP_1
jne expansion_fail
; Optional E also works outside the automatic opening radius, then expires.
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET casinoMap
mov playerX,25*FP_1
mov playerY,FP_1*105/10
mov basementDoorOpen3D,0
mov basementWheelTime3D,0
mov basementDoorHold3D,0
mov interactTargetId,INTERACT_BASEMENT
call TriggerHallInteraction3D
invoke ExpansionDoorTicks,110
cmp basementDoorOpen3D,FP_1
jne expansion_fail
invoke ExpansionDoorTicks,200
cmp basementDoorOpen3D,0
jne expansion_fail
cmp basementWheelTime3D,0
jne expansion_fail
call FortuneNativeTests
call BarCameraNativeTests
invoke ExitProcess,0
expansion_fail:
invoke ExitProcess,expansionStage
ret
ExpansionTestStart ENDP
include tools/tests/fortune_room_test.inc
include tools/tests/bar_camera_test.inc

View file

@ -295,11 +295,12 @@ hall_test_back_in_hall:
call RenderBar3D
invoke WriteActionSnapshot3D,OFFSET hallTestDrink3D
mov barSelection3D, 2
mov barIntoxication3D, 60000
mov barTime3D, 2390
mov deltaTime3D, 16
call HandleBar3D
cmp barIntoxication3D, 0
cmp barPhase3D, 0
jne hall_test_fail
cmp barTime3D, 0
jne hall_test_fail
mov hallTestStage3D, 73
call InitNativeWheel3D

View file

@ -47,6 +47,9 @@ worldDressingProbePath db "assets\animations\hall\world-dressing-v6.bin",0
terminalWorldProbePath db "assets\animations\hall\terminals-v2.bin",0
terminalScreenProbePath db "assets\animations\hall\terminal-screen-v1.bin",0
guestCastProbePath db "assets\animations\hall\guest-cast-v1.bin",0
guestWalkProbePath db "assets\animations\hall\guest-walk-v2.bin",0
guestWalkLeftProbePath db "assets\animations\hall\guest-walk-left-v2.bin",0
basementDoorProbePath db "assets\animations\hall\basement-door-v2.bin",0
cabaretProbePath db "assets\animations\hall\cabaret-ensemble-v1.bin",0
cabaretPianoProbePath db "assets\animations\hall\cabaret-piano-v1.bin",0
cabaretGuitarProbePath db "assets\animations\hall\cabaret-guitar-v1.bin",0
@ -56,6 +59,8 @@ upperBarProbePath db "assets\animations\hall\upper-bar-v2.bin",0
upperPortalProbePath db "assets\animations\hall\upper-portal-v3.bin",0
upperLeversProbePath db "assets\animations\hall\upper-levers-v3.bin",0
upperBraziersProbePath db "assets\animations\hall\upper-braziers-v3.bin",0
fortuneRoomProbePath db "assets\animations\hall\fortune-room-v3.bin",0
fortuneMaterialsProbePath db "assets\animations\hall\fortune-materials-v3.bin",0
furniturePaths dd OFFSET cardFurniturePath,320,208,1
dd OFFSET wheelFurniturePath,288,268,1
dd OFFSET backFurniturePath,304,236,1
@ -76,6 +81,9 @@ furniturePaths dd OFFSET cardFurniturePath,320,208,1
dd OFFSET terminalWorldProbePath,256,512,4
dd OFFSET terminalScreenProbePath,640,480,1
dd OFFSET guestCastProbePath,128,224,36
dd OFFSET guestWalkProbePath,128,224,48
dd OFFSET guestWalkLeftProbePath,128,224,48
dd OFFSET basementDoorProbePath,256,512,18
dd OFFSET cabaretProbePath,160,256,12
dd OFFSET cabaretPianoProbePath,384,256,4
dd OFFSET cabaretGuitarProbePath,160,256,4
@ -85,6 +93,8 @@ furniturePaths dd OFFSET cardFurniturePath,320,208,1
dd OFFSET upperPortalProbePath,256,512,13
dd OFFSET upperLeversProbePath,128,224,6
dd OFFSET upperBraziersProbePath,128,256,4
dd OFFSET fortuneRoomProbePath,256,256,24
dd OFFSET fortuneMaterialsProbePath,256,256,4
furniturePathCount equ ($-furniturePaths)/16
.code
RunStreetAssetProbe PROC