diff --git a/assets/animations/hall/basement-door-v2.bin b/assets/animations/hall/basement-door-v2.bin new file mode 100644 index 0000000..83761d5 Binary files /dev/null and b/assets/animations/hall/basement-door-v2.bin differ diff --git a/assets/animations/hall/fortune-materials-v2.bin b/assets/animations/hall/fortune-materials-v2.bin new file mode 100644 index 0000000..9390638 Binary files /dev/null and b/assets/animations/hall/fortune-materials-v2.bin differ diff --git a/assets/animations/hall/fortune-materials-v3.bin b/assets/animations/hall/fortune-materials-v3.bin new file mode 100644 index 0000000..6ee7532 Binary files /dev/null and b/assets/animations/hall/fortune-materials-v3.bin differ diff --git a/assets/animations/hall/fortune-room-v1.bin b/assets/animations/hall/fortune-room-v1.bin new file mode 100644 index 0000000..27a5879 Binary files /dev/null and b/assets/animations/hall/fortune-room-v1.bin differ diff --git a/assets/animations/hall/fortune-room-v2.bin b/assets/animations/hall/fortune-room-v2.bin new file mode 100644 index 0000000..bd98859 Binary files /dev/null and b/assets/animations/hall/fortune-room-v2.bin differ diff --git a/assets/animations/hall/fortune-room-v3.bin b/assets/animations/hall/fortune-room-v3.bin new file mode 100644 index 0000000..6ec6c71 Binary files /dev/null and b/assets/animations/hall/fortune-room-v3.bin differ diff --git a/assets/animations/hall/guest-walk-left-v2.bin b/assets/animations/hall/guest-walk-left-v2.bin new file mode 100644 index 0000000..cfef0df Binary files /dev/null and b/assets/animations/hall/guest-walk-left-v2.bin differ diff --git a/assets/animations/hall/guest-walk-v2.bin b/assets/animations/hall/guest-walk-v2.bin new file mode 100644 index 0000000..5ccc3f2 Binary files /dev/null and b/assets/animations/hall/guest-walk-v2.bin differ diff --git a/assets/sprites/hall/basement-door-v2.png b/assets/sprites/hall/basement-door-v2.png new file mode 100644 index 0000000..b0a45d6 Binary files /dev/null and b/assets/sprites/hall/basement-door-v2.png differ diff --git a/assets/sprites/hall/fortune-cabinets-v2.png b/assets/sprites/hall/fortune-cabinets-v2.png new file mode 100644 index 0000000..0000993 Binary files /dev/null and b/assets/sprites/hall/fortune-cabinets-v2.png differ diff --git a/assets/sprites/hall/fortune-clutter-v3.png b/assets/sprites/hall/fortune-clutter-v3.png new file mode 100644 index 0000000..f187b55 Binary files /dev/null and b/assets/sprites/hall/fortune-clutter-v3.png differ diff --git a/assets/sprites/hall/fortune-decor-v2.png b/assets/sprites/hall/fortune-decor-v2.png new file mode 100644 index 0000000..5c8bb77 Binary files /dev/null and b/assets/sprites/hall/fortune-decor-v2.png differ diff --git a/assets/sprites/hall/fortune-den-v3.png b/assets/sprites/hall/fortune-den-v3.png new file mode 100644 index 0000000..7f1062f Binary files /dev/null and b/assets/sprites/hall/fortune-den-v3.png differ diff --git a/assets/sprites/hall/fortune-den-v3.prompts.json b/assets/sprites/hall/fortune-den-v3.prompts.json new file mode 100644 index 0000000..c621d89 --- /dev/null +++ b/assets/sprites/hall/fortune-den-v3.prompts.json @@ -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." +} diff --git a/assets/sprites/hall/fortune-materials-v2.png b/assets/sprites/hall/fortune-materials-v2.png new file mode 100644 index 0000000..d80cedd Binary files /dev/null and b/assets/sprites/hall/fortune-materials-v2.png differ diff --git a/assets/sprites/hall/fortune-materials-v3.png b/assets/sprites/hall/fortune-materials-v3.png new file mode 100644 index 0000000..a68f526 Binary files /dev/null and b/assets/sprites/hall/fortune-materials-v3.png differ diff --git a/assets/sprites/hall/fortune-room-v1.png b/assets/sprites/hall/fortune-room-v1.png new file mode 100644 index 0000000..db67e79 Binary files /dev/null and b/assets/sprites/hall/fortune-room-v1.png differ diff --git a/assets/sprites/hall/fortune-room-v1.prompt.txt b/assets/sprites/hall/fortune-room-v1.prompt.txt new file mode 100644 index 0000000..7151414 --- /dev/null +++ b/assets/sprites/hall/fortune-room-v1.prompt.txt @@ -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. diff --git a/assets/sprites/hall/fortune-room-v2-extra.prompts.json b/assets/sprites/hall/fortune-room-v2-extra.prompts.json new file mode 100644 index 0000000..924e880 --- /dev/null +++ b/assets/sprites/hall/fortune-room-v2-extra.prompts.json @@ -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." +} diff --git a/assets/sprites/hall/fortune-room-v2.png b/assets/sprites/hall/fortune-room-v2.png new file mode 100644 index 0000000..9218204 Binary files /dev/null and b/assets/sprites/hall/fortune-room-v2.png differ diff --git a/assets/sprites/hall/fortune-room-v2.prompts.json b/assets/sprites/hall/fortune-room-v2.prompts.json new file mode 100644 index 0000000..acd14fd --- /dev/null +++ b/assets/sprites/hall/fortune-room-v2.prompts.json @@ -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." +} diff --git a/assets/sprites/hall/fortune-wall-v2.png b/assets/sprites/hall/fortune-wall-v2.png new file mode 100644 index 0000000..6484b00 Binary files /dev/null and b/assets/sprites/hall/fortune-wall-v2.png differ diff --git a/assets/sprites/hall/guest-walk-v2.png b/assets/sprites/hall/guest-walk-v2.png new file mode 100644 index 0000000..6d61436 Binary files /dev/null and b/assets/sprites/hall/guest-walk-v2.png differ diff --git a/assets/sprites/hall/prompts-v9.json b/assets/sprites/hall/prompts-v9.json new file mode 100644 index 0000000..c500745 --- /dev/null +++ b/assets/sprites/hall/prompts-v9.json @@ -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." +} diff --git a/bin/poker.exe b/bin/poker.exe index 0320c33..941362f 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index 9dcca74..2f78203 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "071F09A64F0E2F3EC267AC1C963D9849AA3A1DBDA81B3417C90E3499C1D5102F"; + const string ExpectedPokerSha256 = "EADBA9C8C9BD5B7E3CC4CE486ED647C1CE9383D1DCB6EBC94FE755EA1F4BF927"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index 0320c33..941362f 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/casino3d.inc b/casino3d.inc index 50bd3ba..c903393 100644 --- a/casino3d.inc +++ b/casino3d.inc @@ -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 diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index a93e7cc..6587bf7 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/CASINO_NEXT_PASS.md b/docs/CASINO_NEXT_PASS.md index f410279..1bd09bc 100644 --- a/docs/CASINO_NEXT_PASS.md +++ b/docs/CASINO_NEXT_PASS.md @@ -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 diff --git a/docs/HALL_EXPANSION.md b/docs/HALL_EXPANSION.md index a287c68..4e796e0 100644 --- a/docs/HALL_EXPANSION.md +++ b/docs/HALL_EXPANSION.md @@ -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. diff --git a/poker.exe b/poker.exe index 0320c33..941362f 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index a9e4008..1d1fbcd 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/src_graphics/city_bins.inc b/src_graphics/city_bins.inc index b60ffc8..d5c804f 100644 --- a/src_graphics/city_bins.inc +++ b/src_graphics/city_bins.inc @@ -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 diff --git a/src_graphics/fortune_room.inc b/src_graphics/fortune_room.inc new file mode 100644 index 0000000..570d88c --- /dev/null +++ b/src_graphics/fortune_room.inc @@ -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 diff --git a/src_graphics/fortune_room_data.inc b/src_graphics/fortune_room_data.inc new file mode 100644 index 0000000..14cad6f --- /dev/null +++ b/src_graphics/fortune_room_data.inc @@ -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 +fortuneShelf3D CityProp3D +fortuneTapestry3D CityProp3D +fortuneOrb3D CityProp3D +fortuneBench3D CityProp3D +fortuneMirror3D CityProp3D +fortuneCandleA3D CityProp3D +fortuneCandleB3D CityProp3D +fortuneCards3D CityProp3D +fortuneScrolls3D CityProp3D +fortuneHerbs3D CityProp3D +fortuneHerbsB3D CityProp3D +fortuneHerbsC3D CityProp3D +fortuneHanging3D CityProp3D +fortunePotions3D CityProp3D +fortuneBooks3D CityProp3D +fortuneRelics3D CityProp3D +fortuneChest3D CityProp3D +fortuneBookPileA3D CityProp3D +fortuneBookPileB3D CityProp3D +fortuneCauldron3D CityProp3D +fortuneTray3D CityProp3D +fortuneFloorCandlesA3D CityProp3D +fortuneFloorCandlesB3D CityProp3D +fortuneNorthLamp3D CityProp3D +fortuneSouthLamp3D CityProp3D +; 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 diff --git a/src_graphics/hall_data.inc b/src_graphics/hall_data.inc index 95a5ed6..701e117 100644 --- a/src_graphics/hall_data.inc +++ b/src_graphics/hall_data.inc @@ -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 diff --git a/src_graphics/hall_expansion.inc b/src_graphics/hall_expansion.inc index 3847d29..86948b6 100644 --- a/src_graphics/hall_expansion.inc +++ b/src_graphics/hall_expansion.inc @@ -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 diff --git a/src_graphics/hall_expansion_data.inc b/src_graphics/hall_expansion_data.inc index 4e0338b..600fbeb 100644 --- a/src_graphics/hall_expansion_data.inc +++ b/src_graphics/hall_expansion_data.inc @@ -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 <> diff --git a/src_graphics/hall_layout.inc b/src_graphics/hall_layout.inc index 32da369..68f721b 100644 --- a/src_graphics/hall_layout.inc +++ b/src_graphics/hall_layout.inc @@ -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 diff --git a/src_graphics/hall_scene.inc b/src_graphics/hall_scene.inc index 961e33f..80cdaed 100644 --- a/src_graphics/hall_scene.inc +++ b/src_graphics/hall_scene.inc @@ -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 diff --git a/src_graphics/interior_portals.inc b/src_graphics/interior_portals.inc index ce94b9d..d987414 100644 --- a/src_graphics/interior_portals.inc +++ b/src_graphics/interior_portals.inc @@ -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 diff --git a/src_graphics/npc_life.inc b/src_graphics/npc_life.inc index f3b1b6b..32ec70d 100644 --- a/src_graphics/npc_life.inc +++ b/src_graphics/npc_life.inc @@ -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: diff --git a/src_graphics/npc_life_data.inc b/src_graphics/npc_life_data.inc index d31e6e1..4bca78a 100644 --- a/src_graphics/npc_life_data.inc +++ b/src_graphics/npc_life_data.inc @@ -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 diff --git a/src_graphics/steel_door.inc b/src_graphics/steel_door.inc new file mode 100644 index 0000000..f6e6f5e --- /dev/null +++ b/src_graphics/steel_door.inc @@ -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 diff --git a/src_graphics/world_dressing.inc b/src_graphics/world_dressing.inc index 5c54b7a..be7712f 100644 --- a/src_graphics/world_dressing.inc +++ b/src_graphics/world_dressing.inc @@ -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 diff --git a/tools/generators/make_hall_layout.py b/tools/generators/make_hall_layout.py index fd8031e..dcbffbc 100644 --- a/tools/generators/make_hall_layout.py +++ b/tools/generators/make_hall_layout.py @@ -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") diff --git a/tools/generators/pack_door_walk_v2.py b/tools/generators/pack_door_walk_v2.py new file mode 100644 index 0000000..bcad4cb --- /dev/null +++ b/tools/generators/pack_door_walk_v2.py @@ -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.") diff --git a/tools/generators/pack_fortune_room.py b/tools/generators/pack_fortune_room.py new file mode 100644 index 0000000..e85224e --- /dev/null +++ b/tools/generators/pack_fortune_room.py @@ -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() diff --git a/tools/tests/bar_camera_test.inc b/tools/tests/bar_camera_test.inc new file mode 100644 index 0000000..2636bfd --- /dev/null +++ b/tools/tests/bar_camera_test.inc @@ -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 diff --git a/tools/tests/check_card_exit_build.py b/tools/tests/check_card_exit_build.py index 69f5b64..9ffe76f 100644 --- a/tools/tests/check_card_exit_build.py +++ b/tools/tests/check_card_exit_build.py @@ -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 diff --git a/tools/tests/check_hall_expansion.py b/tools/tests/check_hall_expansion.py index 597dcde..76dfa1a 100644 --- a/tools/tests/check_hall_expansion.py +++ b/tools/tests/check_hall_expansion.py @@ -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__": diff --git a/tools/tests/check_terminals.py b/tools/tests/check_terminals.py index 61a4157..9ebceb8 100644 --- a/tools/tests/check_terminals.py +++ b/tools/tests/check_terminals.py @@ -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("