diff --git a/.gitignore b/.gitignore index 8568fc9..c43ac65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,12 +13,17 @@ __pycache__/ # Generated build/test output; keep packaging source and shipping exe files. /build/actions/ /build/arrival/ +/build/billiards/ /build/buckshot/ /build/coin/ +/build/dice/ /build/hall/ +/build/icon_work/ +/build/scratch/ /build/startup/ /build/tables/ /build/wheel/ +/build/world/ /build/*.res /build/payload_*.zip /build/PokerGameSetup_base.exe diff --git a/assets/animations/arrival/alley-materials-v1.bin b/assets/animations/arrival/alley-materials-v1.bin new file mode 100644 index 0000000..8aea7cc Binary files /dev/null and b/assets/animations/arrival/alley-materials-v1.bin differ diff --git a/assets/animations/arrival/wake-knees-v1.bin b/assets/animations/arrival/wake-knees-v1.bin new file mode 100644 index 0000000..73e39a1 Binary files /dev/null and b/assets/animations/arrival/wake-knees-v1.bin differ diff --git a/assets/animations/arrival/wake-lighter-v1.bin b/assets/animations/arrival/wake-lighter-v1.bin new file mode 100644 index 0000000..f491383 Binary files /dev/null and b/assets/animations/arrival/wake-lighter-v1.bin differ diff --git a/assets/animations/arrival/wake-rocker-v1.bin b/assets/animations/arrival/wake-rocker-v1.bin new file mode 100644 index 0000000..40ad9aa Binary files /dev/null and b/assets/animations/arrival/wake-rocker-v1.bin differ diff --git a/assets/animations/arrival/wake-rocker-v2.bin b/assets/animations/arrival/wake-rocker-v2.bin new file mode 100644 index 0000000..b7a0157 Binary files /dev/null and b/assets/animations/arrival/wake-rocker-v2.bin differ diff --git a/assets/animations/arrival/wake-sleeve-v1.bin b/assets/animations/arrival/wake-sleeve-v1.bin new file mode 100644 index 0000000..bd81658 Binary files /dev/null and b/assets/animations/arrival/wake-sleeve-v1.bin differ diff --git a/assets/animations/dice/hands-v1.bin b/assets/animations/dice/hands-v1.bin new file mode 100644 index 0000000..87d2898 Binary files /dev/null and b/assets/animations/dice/hands-v1.bin differ diff --git a/assets/animations/dice/hands-v2.bin b/assets/animations/dice/hands-v2.bin new file mode 100644 index 0000000..89940c4 Binary files /dev/null and b/assets/animations/dice/hands-v2.bin differ diff --git a/assets/animations/hall/world-dressing-v6.bin b/assets/animations/hall/world-dressing-v6.bin new file mode 100644 index 0000000..f7982d5 Binary files /dev/null and b/assets/animations/hall/world-dressing-v6.bin differ diff --git a/assets/animations/pyramid/pool-balls-v1.bin b/assets/animations/pyramid/pool-balls-v1.bin new file mode 100644 index 0000000..cfbc359 Binary files /dev/null and b/assets/animations/pyramid/pool-balls-v1.bin differ diff --git a/assets/animations/scratch/coin-edge-v1.bin b/assets/animations/scratch/coin-edge-v1.bin new file mode 100644 index 0000000..ed531e2 Binary files /dev/null and b/assets/animations/scratch/coin-edge-v1.bin differ diff --git a/assets/animations/scratch/counter-v1.bin b/assets/animations/scratch/counter-v1.bin new file mode 100644 index 0000000..5eebfbd Binary files /dev/null and b/assets/animations/scratch/counter-v1.bin differ diff --git a/assets/animations/scratch/foil-v1.bin b/assets/animations/scratch/foil-v1.bin new file mode 100644 index 0000000..db58bd5 Binary files /dev/null and b/assets/animations/scratch/foil-v1.bin differ diff --git a/assets/animations/scratch/payout-chips-v1.bin b/assets/animations/scratch/payout-chips-v1.bin new file mode 100644 index 0000000..00d13d5 Binary files /dev/null and b/assets/animations/scratch/payout-chips-v1.bin differ diff --git a/assets/animations/scratch/portrait-fields-v3.bin b/assets/animations/scratch/portrait-fields-v3.bin new file mode 100644 index 0000000..cc53390 Binary files /dev/null and b/assets/animations/scratch/portrait-fields-v3.bin differ diff --git a/assets/animations/scratch/portrait-layouts-v3.json b/assets/animations/scratch/portrait-layouts-v3.json new file mode 100644 index 0000000..d0e0b3d --- /dev/null +++ b/assets/animations/scratch/portrait-layouts-v3.json @@ -0,0 +1,443 @@ +[ + { + "file": "neon-ru-v3.png", + "winning": 5, + "players": 15, + "fields": [ + { + "x": 160, + "y": 339, + "w": 51, + "h": 51, + "area": 1999 + }, + { + "x": 228, + "y": 339, + "w": 51, + "h": 51, + "area": 2015 + }, + { + "x": 297, + "y": 339, + "w": 51, + "h": 51, + "area": 2012 + }, + { + "x": 366, + "y": 339, + "w": 51, + "h": 51, + "area": 2010 + }, + { + "x": 435, + "y": 339, + "w": 51, + "h": 51, + "area": 2032 + }, + { + "x": 133, + "y": 433, + "w": 65, + "h": 47, + "area": 2973 + }, + { + "x": 211, + "y": 433, + "w": 65, + "h": 47, + "area": 2976 + }, + { + "x": 290, + "y": 433, + "w": 65, + "h": 47, + "area": 2972 + }, + { + "x": 369, + "y": 433, + "w": 64, + "h": 47, + "area": 2962 + }, + { + "x": 447, + "y": 433, + "w": 64, + "h": 47, + "area": 2940 + }, + { + "x": 133, + "y": 496, + "w": 65, + "h": 47, + "area": 2977 + }, + { + "x": 211, + "y": 496, + "w": 65, + "h": 47, + "area": 2978 + }, + { + "x": 290, + "y": 496, + "w": 65, + "h": 46, + "area": 2965 + }, + { + "x": 369, + "y": 496, + "w": 64, + "h": 47, + "area": 2932 + }, + { + "x": 447, + "y": 496, + "w": 64, + "h": 47, + "area": 2926 + }, + { + "x": 133, + "y": 558, + "w": 65, + "h": 47, + "area": 2962 + }, + { + "x": 211, + "y": 558, + "w": 65, + "h": 47, + "area": 2975 + }, + { + "x": 290, + "y": 558, + "w": 65, + "h": 47, + "area": 2981 + }, + { + "x": 369, + "y": 558, + "w": 64, + "h": 47, + "area": 2951 + }, + { + "x": 447, + "y": 558, + "w": 64, + "h": 47, + "area": 2942 + } + ] + }, + { + "file": "ruby-ru-v3.png", + "winning": 6, + "players": 18, + "fields": [ + { + "x": 136, + "y": 329, + "w": 50, + "h": 54, + "area": 2138 + }, + { + "x": 202, + "y": 329, + "w": 50, + "h": 53, + "area": 2130 + }, + { + "x": 267, + "y": 329, + "w": 51, + "h": 54, + "area": 2138 + }, + { + "x": 333, + "y": 329, + "w": 50, + "h": 54, + "area": 2143 + }, + { + "x": 398, + "y": 329, + "w": 50, + "h": 54, + "area": 2122 + }, + { + "x": 461, + "y": 329, + "w": 50, + "h": 53, + "area": 2125 + }, + { + "x": 135, + "y": 424, + "w": 54, + "h": 46, + "area": 2424 + }, + { + "x": 201, + "y": 424, + "w": 52, + "h": 46, + "area": 2336 + }, + { + "x": 265, + "y": 424, + "w": 52, + "h": 46, + "area": 2359 + }, + { + "x": 329, + "y": 424, + "w": 52, + "h": 46, + "area": 2351 + }, + { + "x": 393, + "y": 424, + "w": 52, + "h": 46, + "area": 2335 + }, + { + "x": 457, + "y": 424, + "w": 53, + "h": 46, + "area": 2380 + }, + { + "x": 135, + "y": 487, + "w": 54, + "h": 46, + "area": 2432 + }, + { + "x": 201, + "y": 487, + "w": 52, + "h": 46, + "area": 2338 + }, + { + "x": 265, + "y": 487, + "w": 52, + "h": 46, + "area": 2362 + }, + { + "x": 329, + "y": 487, + "w": 52, + "h": 46, + "area": 2353 + }, + { + "x": 393, + "y": 487, + "w": 52, + "h": 46, + "area": 2349 + }, + { + "x": 457, + "y": 487, + "w": 53, + "h": 46, + "area": 2381 + }, + { + "x": 135, + "y": 550, + "w": 54, + "h": 46, + "area": 2423 + }, + { + "x": 201, + "y": 550, + "w": 52, + "h": 46, + "area": 2335 + }, + { + "x": 265, + "y": 550, + "w": 52, + "h": 46, + "area": 2361 + }, + { + "x": 329, + "y": 550, + "w": 52, + "h": 46, + "area": 2340 + }, + { + "x": 393, + "y": 550, + "w": 52, + "h": 46, + "area": 2350 + }, + { + "x": 457, + "y": 550, + "w": 53, + "h": 46, + "area": 2381 + } + ] + }, + { + "file": "sea-ru-v3.png", + "winning": 4, + "players": 12, + "fields": [ + { + "x": 200, + "y": 335, + "w": 53, + "h": 53, + "area": 2213 + }, + { + "x": 270, + "y": 335, + "w": 53, + "h": 53, + "area": 2213 + }, + { + "x": 339, + "y": 335, + "w": 53, + "h": 53, + "area": 2209 + }, + { + "x": 409, + "y": 335, + "w": 53, + "h": 53, + "area": 2212 + }, + { + "x": 147, + "y": 427, + "w": 79, + "h": 48, + "area": 3758 + }, + { + "x": 242, + "y": 427, + "w": 81, + "h": 48, + "area": 3836 + }, + { + "x": 338, + "y": 427, + "w": 80, + "h": 48, + "area": 3765 + }, + { + "x": 434, + "y": 427, + "w": 79, + "h": 48, + "area": 3758 + }, + { + "x": 147, + "y": 490, + "w": 79, + "h": 48, + "area": 3714 + }, + { + "x": 242, + "y": 490, + "w": 81, + "h": 48, + "area": 3797 + }, + { + "x": 338, + "y": 490, + "w": 80, + "h": 48, + "area": 3719 + }, + { + "x": 434, + "y": 490, + "w": 79, + "h": 48, + "area": 3718 + }, + { + "x": 147, + "y": 554, + "w": 79, + "h": 48, + "area": 3756 + }, + { + "x": 242, + "y": 554, + "w": 81, + "h": 48, + "area": 3843 + }, + { + "x": 338, + "y": 553, + "w": 80, + "h": 49, + "area": 3762 + }, + { + "x": 434, + "y": 554, + "w": 79, + "h": 48, + "area": 3766 + } + ] + } +] \ No newline at end of file diff --git a/assets/animations/scratch/portraits-v3.bin b/assets/animations/scratch/portraits-v3.bin new file mode 100644 index 0000000..58fcd72 Binary files /dev/null and b/assets/animations/scratch/portraits-v3.bin differ diff --git a/assets/animations/scratch/table-v1.bin b/assets/animations/scratch/table-v1.bin new file mode 100644 index 0000000..b8da036 Binary files /dev/null and b/assets/animations/scratch/table-v1.bin differ diff --git a/assets/animations/scratch/ticket-v1.bin b/assets/animations/scratch/ticket-v1.bin new file mode 100644 index 0000000..0606b1e Binary files /dev/null and b/assets/animations/scratch/ticket-v1.bin differ diff --git a/assets/audio/generated/scratch-paper.wav b/assets/audio/generated/scratch-paper.wav new file mode 100644 index 0000000..d5afe56 Binary files /dev/null and b/assets/audio/generated/scratch-paper.wav differ diff --git a/assets/audio/generated/scratch-payout.wav b/assets/audio/generated/scratch-payout.wav new file mode 100644 index 0000000..f60a8ae Binary files /dev/null and b/assets/audio/generated/scratch-payout.wav differ diff --git a/assets/audio/generated/scratch-scrape.wav b/assets/audio/generated/scratch-scrape.wav new file mode 100644 index 0000000..3575e4d Binary files /dev/null and b/assets/audio/generated/scratch-scrape.wav differ diff --git a/assets/data/ui-ru.json b/assets/data/ui-ru.json index 536d4e6..fb72ed1 100644 --- a/assets/data/ui-ru.json +++ b/assets/data/ui-ru.json @@ -1,4 +1,11 @@ { + "cityWakeLine1": "Вставай, самурай,", + "cityWakeLine2": "пора дэпнуть все в казино", + "poolSelectTitle3D": "БИЛЬЯРД", + "poolSelectPyramid3D": "1 ПИРАМИДА", + "poolSelectEight3D": "2 ПУЛ ВОСЬМЕРКА", + "poolCallText3D": "ЗАКАЗ", + "poolPredictionText3D": "ПРОГНОЗ", "coinTitle3D": "МОНЕТКА", "coinHeadsText3D": "ОРЁЛ", "coinTailsText3D": "РЕШКА", @@ -355,5 +362,45 @@ "pyrBadEndText3D": "НЕВЕРНЫЙ УДАР", "pyrPlayerName3D": "ВЫ", "pyrAIName3D": "ИИ", - "pyrSkipText3D": "ПРОПУСК" + "pyrSkipText3D": "ПРОПУСК", + "scratchTitle3D": "ЛОТЕРЕЯ", + "scratchGame3D": "ЗОЛОТАЯ ТРОЙКА", + "scratchBuy3D": "КУПИТЬ", + "scratchNext3D": "НОВЫЙ", + "scratchPrice3D": "ЦЕНА", + "scratchWin3D": "ПРИЗ", + "scratchLose3D": "БЕЗ ВЫИГРЫША", + "scratchWait3D": "ОТКРЫТО", + "scratchPoor3D": "МАЛО ФИШЕК", + "scratchMissing3D": "НЕТ СПРАЙТОВ", + "scratchHall3D": "E: ЛОТЕРЕЙНЫЕ БИЛЕТЫ", + "scratchRule3D": "3 ОДИНАКОВЫХ = ПРИЗ", + "scratchActive3D": "БИЛЕТ", + "scratchNumbers3D": "СЧАСТЛИВЫЕ ЧИСЛА", + "scratchRows3D": "ТРИ В РЯД", + "scratchInstant3D": "МИГ УДАЧИ", + "scratchNumberRule3D": "СОВПАДЕНИЕ = ПРИЗ ПОД ЧИСЛОМ", + "scratchRowRule3D": "3 ОДИНАКОВЫХ В ОДНОМ РЯДУ", + "scratchInstantRule3D": "ПРИЗ УКАЗАН ПОД ЗАЩИТОЙ", + "scratchLucky3D": "ВЫИГРЫШНЫЕ ЧИСЛА", + "scratchYours3D": "ВАШИ ЧИСЛА", + "scratchAmount3D": "ВАШ ПРИЗ", + "scratchNeon3D": "НЕОНОВАЯ УДАЧА", + "scratchRuby3D": "РУБИНОВЫЙ ДЖЕКПОТ", + "scratchSea3D": "ВОЛНА СОКРОВИЩ", + "scratchBonusRule3D": "ЧИСЛО ИЛИ БОНУС = ПРИЗ", + "scratchMatch3D": "СОВПАДЕНИЕ: ПРИЗ", + "scratchNeonDouble3D": "x2: ДВОЙНОЙ ПРИЗ", + "scratchNeonTen3D": "x10: ПРИЗ x10", + "scratchRubyDouble3D": "x2: ДВОЙНОЙ ПРИЗ", + "scratchRubyAll3D": "ВСЕ: ВСЕ ПРИЗЫ", + "scratchSeaDouble3D": "x2: ДВОЙНОЙ ПРИЗ", + "scratchSeaAll3D": "ВСЕ: ВСЕ ПРИЗЫ", + "scratchAll3D": "ВСЕ", + "scratchField3D": "ПОЛЕ", + "scratchKind3D": "ВИД БИЛЕТА", + "scratchCost3D": "ЦЕНА БИЛЕТА", + "scratchPayout3D": "ВЫПЛАТА", + "scratchNet3D": "ЧИСТЫЙ ИТОГ", + "scratchPending3D": "БИЛЕТ НЕ ОТКРЫТ" } diff --git a/assets/sprites/arrival/alley-materials-v1.png b/assets/sprites/arrival/alley-materials-v1.png new file mode 100644 index 0000000..b3a788f Binary files /dev/null and b/assets/sprites/arrival/alley-materials-v1.png differ diff --git a/assets/sprites/arrival/wake-knees-v1.png b/assets/sprites/arrival/wake-knees-v1.png new file mode 100644 index 0000000..5629fd0 Binary files /dev/null and b/assets/sprites/arrival/wake-knees-v1.png differ diff --git a/assets/sprites/arrival/wake-lighter-v1.png b/assets/sprites/arrival/wake-lighter-v1.png new file mode 100644 index 0000000..1c69475 Binary files /dev/null and b/assets/sprites/arrival/wake-lighter-v1.png differ diff --git a/assets/sprites/arrival/wake-prompts.json b/assets/sprites/arrival/wake-prompts.json new file mode 100644 index 0000000..9548bb7 --- /dev/null +++ b/assets/sprites/arrival/wake-prompts.json @@ -0,0 +1,6 @@ +{ + "actor": "Use case: game-asset. Primary request: transparent 4 by 4 sprite animation atlas, exactly 16 equally spaced full-body poses of the SAME fictional homeless cyberpunk rocker, loosely reminiscent of Johnny Silverhand: long unwashed dark hair, scruffy beard, dark aviator sunglasses, weathered black leather vest under patched dark coat, faded grey shirt, worn jeans, battered boots, LEFT forearm a crude silver mechanical prosthesis. Friendly sardonic expression, no weapons. Consistent detailed hand-painted late-1990s Doom/Build-engine pixel art, not photographic, not smooth 3D render; muted cold skin and desaturated clothes with small warm highlights matching brick rainy night alley. Clean transparent gutters, no background or ground shadows. All poses face the viewer or turn slightly left with consistent proportions, camera and illumination, full boots visible. Exactly four columns and four rows read left to right. Frames 1-4 crouching on bent knees, leaning toward someone seated on the ground, raising RIGHT natural hand to wake them; frames 5-8 still crouched, subtle mouth speaking and beckoning hand gesture; frames 9-12 smoothly standing from crouch; frames 13-16 turning to his left and walking past the seated viewer. Shared cell coordinates, identical SCALE between poses: crouched poses must be shorter than standing, do NOT enlarge crouched bodies to fill the cell. All feet on the same cell baseline, maximum standing figure occupies 84 percent of cell height, crouched 55 percent, enough headroom. No text, no labels, no logo, no duplicates, no cropped limbs. True transparent background.", + "lighter": "Use case: game-asset. Primary request: first-person LEFT HAND opening and igniting a battered silver flip-top lighter, transparent 4 by 4 sprite animation sheet with exactly 16 equally spaced frames. Crisp late-1990s Doom/Build-engine shaded PIXEL ART, not photographic, not flat cartoon. Adult light skin left hand, black worn leather jacket sleeve entering from lower LEFT of every cell. Natural anatomically correct fingers and LEFT thumb on spark wheel. NO RIGHT HAND, NO CIGARETTE. The ignition flame is meant to light a separate cigarette supplied by the game. Camera looking down at the lighter. Same hand identity, scale, position anchors and warm/cool lighting across all cells, clean transparent margins. Sequence read left to right: frames 1-3 closed lighter raised, frames 4-6 lid opens and thumb rolls spark wheel with tiny sparks, frames 7-12 bright small warm yellow flame fluttering ABOVE nozzle near the upper centre of the cell, frames 13-14 thumb stops flame and lid closes, frames 15-16 closed lighter lowers a little. Lighter nozzle always near x=50 percent, y=20 percent of cell, fingers and wrist below it; no flipping hand orientation. Solid opaque skin/metal/leather, transparency ONLY outside silhouettes and tiny flame wisps. No text, lettering, border, shadow matte or background. One square sprite sheet, exactly 4 rows by 4 columns.", + "knees": "Use case: game-asset. Asset: transparent first-person foreground sprite, wide landscape composition. View of one's OWN bent knees and shins in faded charcoal-blue jeans while seated on the ground of a rainy alley, knees occupying bottom left and bottom right edges, a clear transparent wide gap through the centre and entire upper half, trouser legs descend out of the bottom edge naturally. NO HANDS, NO arms, NO other person, NO background, NO shoes in top area, NO cigarette, NO text. Detailed hand-painted late-1990s Doom / Build engine pixel art with crisp pixels, muted cool denim and subtle warm edge highlights; not photoreal or smooth render. Large creased bent kneecaps at left and right lower corners, believable seated first-person perspective, consistent player black jacket/jeans character. True transparent background outside legs.", + "mode": "built-in" +} diff --git a/assets/sprites/arrival/wake-rocker-v1.png b/assets/sprites/arrival/wake-rocker-v1.png new file mode 100644 index 0000000..2ed0654 Binary files /dev/null and b/assets/sprites/arrival/wake-rocker-v1.png differ diff --git a/assets/sprites/arrival/wake-rocker-v2.png b/assets/sprites/arrival/wake-rocker-v2.png new file mode 100644 index 0000000..49bb77c Binary files /dev/null and b/assets/sprites/arrival/wake-rocker-v2.png differ diff --git a/assets/sprites/arrival/wake-sleeve-v1.png b/assets/sprites/arrival/wake-sleeve-v1.png new file mode 100644 index 0000000..7571480 Binary files /dev/null and b/assets/sprites/arrival/wake-sleeve-v1.png differ diff --git a/assets/sprites/arrival/wake-v2-prompts.json b/assets/sprites/arrival/wake-v2-prompts.json new file mode 100644 index 0000000..295976d --- /dev/null +++ b/assets/sprites/arrival/wake-v2-prompts.json @@ -0,0 +1,7 @@ +{ + "mode": "built-in", + "actor": "Use case: identity-preserve. Production retro 2.5D game sprite animation atlas. Reference image is the approved scruffy homeless Johnny Silverhand-like rocker: preserve EXACT face, shoulder-length dark hair, dark sunglasses, beard, chrome LEFT arm, dirty black leather long coat, worn dark jeans, brown boots, pixel-art shading and proportions. Create ONE transparent atlas of EXACTLY 32 complete isolated character poses, 4 columns by 8 rows, read left-to-right top-to-bottom, clean generous transparent gutters, consistent ground anchor and common scale (do NOT enlarge crouching poses). First 16 frames form a smooth crouching speaking gesture cycle: tiny gradual head nod, mouth movement, shoulders breathing, extended right hand gradually gestures and returns, left chrome hand on knee. Next 8 frames are one smooth rise from crouch to standing then gradually turn away: frame16 crouched lean forward,17 lift hips,18 half rise,19 nearly straight,20 standing front,21 quarter turn,22 three-quarter back,23 full back. Last 8 frames show a smooth walking-away full-body cycle seen from back with gently swinging coat and chrome left arm. On every turned/back-facing coat clearly show Cyberpunk 2077 SAMURAI's large red horned skull/flame emblem with small SAMURAI lettering across upper back, NOT a generic samurai warrior. No new props, no scenery, no cell outlines, no captions, no watermarks. Same gritty detailed pixel-art style as input, no photorealism. Entire boots visible every frame. Transparent background.", + "sleeve": "Use case: precise-object-edit. Input image is STYLE reference of the approved first-person male lighter-hand sprites, not a request for another hand. Generate ONE isolated LEFT FOREARM IN BLACK WORN LEATHER COAT SLEEVE ONLY, no hand, fingers, lighter or flame. This sleeve extends the cropped left wrist in a retro 2.5D first-person game. Tall 2:3 transparent canvas: top cuff opening at horizontal center (50% width) on the very TOP edge, broad enough to disappear underneath the existing wrist; forearm runs smoothly diagonally down and toward lower-left and extends out of the BOTTOM edge, not a floating amputated sleeve. Three-dimensional tapered cylinder with believable elbow perspective, detailed dark leather folds, stitching, subdued brown-grey highlights. Keep same gritty crisp pixel-art shaded texture and scale as reference, no photorealistic skin, no blur. Solid continuous silhouette from TOP to BOTTOM, transparent around sleeve. No black rectangular matte, no labels, no scenery, no detached objects.", + "alley": "Use case: stylized-concept. Asset type: material atlas for retro 2.5D rain-soaked alley geometry. ONE square image exactly TWO columns and TWO rows of four square opaque game textures, no gaps or borders. Top-left: old dark galvanized ventilation grille with closely spaced horizontal louvers, rusty narrow frame, grimy bolts. Top-right: flat weathered pasted-over concert posters on dirty grey paper, small torn edges layered INSIDE the tile, faded crimson and ivory punk-print graphics, no readable modern advertisements. Bottom-left: battered wooden shipping crate planks with seams, dark wet grain, worn nails and diagonal wooden brace. Bottom-right: rusty dark electrical junction cabinet face, recessed door, hinges, small latch, peeling green-grey paint, NO luminous screen. Detailed gritty Doom/Build-engine pixel-art materials, crisp textured clusters, not photorealism, not vector illustration. All four textures orthographic front-on, no background scenery or perspective scene, no shadows outside tiles, entire canvas opaque, consistent subdued rainy-night palette. Each material must fill its own entire square.", + "logo_reference": "https://www.cyberpunk.net/en/news/30631/pre-order-your-samurai-bomber-jacket-now" +} diff --git a/assets/sprites/arrival/wake-walk-v2-prompt.json b/assets/sprites/arrival/wake-walk-v2-prompt.json new file mode 100644 index 0000000..b35041a --- /dev/null +++ b/assets/sprites/arrival/wake-walk-v2-prompt.json @@ -0,0 +1,4 @@ +{ + "mode": "built-in", + "walk": "Use case: identity-preserve. Use reference ONLY for approved character identity and pixel-art style. Production game sprite sheet: exactly EIGHT full-body frames in a FOUR column TWO row atlas, generous transparent gutters, no extra row. SAME homeless Johnny Silverhand-like rocker, dark shoulder-length hair, scruffy beard, sunglasses, chrome LEFT arm, worn black leather LONG COAT, dark jeans, brown boots. Camera sees him FROM THE BACK in EVERY frame. Large red Cyberpunk 2077 SAMURAI horned skull/flame emblem centered on coat back, small SAMURAI name above. Eight successive frames of WALK-IN-PLACE animation: hips, knees, arms and coat hem move gradually; heel lifts, passing legs, alternate foot contacts. He DOES NOT move away from camera and DOES NOT SHRINK. Exactly SAME camera distance and SAME body scale in all eight cells, full standing head-to-foot height constant, boots on common ground baseline, same lighting. All frames occupy about 85% slot height. Crisp gritty detailed pixel art identical to reference, not photo. No scenery, no labels, no borders, no shadows outside silhouette, true transparent background. Keep full boots visible." +} diff --git a/assets/sprites/arrival/wake-walk-v2.png b/assets/sprites/arrival/wake-walk-v2.png new file mode 100644 index 0000000..6ad40f4 Binary files /dev/null and b/assets/sprites/arrival/wake-walk-v2.png differ diff --git a/assets/sprites/dice/hands-v1.png b/assets/sprites/dice/hands-v1.png new file mode 100644 index 0000000..5cd6d10 Binary files /dev/null and b/assets/sprites/dice/hands-v1.png differ diff --git a/assets/sprites/dice/hands-v2.png b/assets/sprites/dice/hands-v2.png new file mode 100644 index 0000000..4b3febe Binary files /dev/null and b/assets/sprites/dice/hands-v2.png differ diff --git a/assets/sprites/dice/prompts-v1.json b/assets/sprites/dice/prompts-v1.json new file mode 100644 index 0000000..eb11d8a --- /dev/null +++ b/assets/sprites/dice/prompts-v1.json @@ -0,0 +1,6 @@ +{ + "mode": "built-in", + "hands": "Use case: identity-preserve. Input image is approved STYLE and hand identity reference, NOT the coin to copy. Make a production first-person RIGHT HAND AND COMPLETE BLACK LEATHER FOREARM sprite atlas for throwing TWO dice supplied separately by the game. NO DICE AND NO COINS in this image. Exact 16 poses, 4 columns by 4 rows, transparent gutters. Same adult light skin, correct five fingers, gritty detailed retro pixel-art shading, black worn leather sleeve as reference. Forearm continues to bottom-right cell edge naturally, never cut at wrist. Palm opens upward, cupped near upper-left of each cell, fingers angled away from viewer. Frames0-3: subtly shifting cupped palm, enough room in palm for two dice. Frames4-7: palm smoothly pushes forward and fingers open to release dice. Frames8-11: open forward hand follows through. Frames12-15: gradually relax fingers and retract. Same camera, scale, light and palm anchor at approximately x=40%,y=28% in every cell; hand and long sleeve fill lower-right. No objects, no scenery, no labels, no mattes, no borders, no extra rows. Crisp pixel clusters matching reference, not photorealistic. Transparent background.", + "table": "Reuses assets/animations/coin/table-v1.bin unchanged.", + "dice": "Reuses the original cubeCorners3D/cubeFaces3D/cubeColors3D/DrawTumblingDie3D; no generated replacement dice." +} diff --git a/assets/sprites/dice/prompts-v2.json b/assets/sprites/dice/prompts-v2.json new file mode 100644 index 0000000..e4ac9e2 --- /dev/null +++ b/assets/sprites/dice/prompts-v2.json @@ -0,0 +1,8 @@ +{ + "mode": "built-in image_gen", + "asset": "hands-v2.png", + "generation": "exec-4dd19e3a-0d6e-445e-bf78-4c4446b3105d", + "prompt_specification": "Transparent 4 by 4 atlas, exactly sixteen consistent first-person right-hand poses in the same black leather cuff, crisp retro pixel-art. Preserve the reference dice identity: small ivory cubes with dark red pips. First four poses: upward cupped palm, two thumb-width dice physically resting together in the centre of the palm, tiny contact shadows and finger overlap, never on the thumb, wrist or fingertips. Remaining twelve poses: empty palm opening, throwing follow-through and retracting. Fixed character, camera, scale, lighting and materials; full forearm to bottom right. No scenery, labels or extra rows.", + "references": ["previous native dice-ready screenshot", "hands-v1.png"], + "note": "Specification retained from the preceding generation; this is not a verbatim tool-prompt transcript." +} diff --git a/assets/sprites/hall/prompts-v6.json b/assets/sprites/hall/prompts-v6.json new file mode 100644 index 0000000..f95462d --- /dev/null +++ b/assets/sprites/hall/prompts-v6.json @@ -0,0 +1,6 @@ +{ + "mode": "built-in image_gen", + "intent": "generate", + "output": "wall-paintings-v6.png", + "prompt": "Use case: stylized-concept. Asset type: two wall decoration textures for a retro 2.5D casino interior. Create a single sheet with EXACTLY TWO separate square framed paintings in a clean 2-column 1-row layout with generous transparent gutters. Both are strictly front-facing orthographic, identical compact aged brass/gold carved frames, no perspective tilt, no floor, no drop shadow outside the frames, no glow, no words, no letters, no price tags. Left painting: an old American brick downtown street on a rainy evening with warm shop windows and a small red awning; muted teal sky, no modern skyscrapers. Right painting: an atmospheric painted still life of playing cards with indistinct unlettered card faces, red/ivory/green casino chips, a short glass on deep green felt, not a diagram or game UI. Hand-painted, visibly pixel-art shading and crisp silhouettes appropriate to a classic software-rendered casino game, richly detailed but readable when each complete framed painting is reduced to 128x128 pixels. Both complete frames fully visible, each image square and aligned, solid opaque paint/canvas/frame, genuinely transparent background outside and between the two paintings. These will mount flush to actual wooden walls, not float as camera-facing billboards." +} diff --git a/assets/sprites/hall/wall-paintings-v6.png b/assets/sprites/hall/wall-paintings-v6.png new file mode 100644 index 0000000..8a1808a Binary files /dev/null and b/assets/sprites/hall/wall-paintings-v6.png differ diff --git a/assets/sprites/pyramid/pool-balls-v1.png b/assets/sprites/pyramid/pool-balls-v1.png new file mode 100644 index 0000000..e613dd8 Binary files /dev/null and b/assets/sprites/pyramid/pool-balls-v1.png differ diff --git a/assets/sprites/pyramid/pool-prompts.json b/assets/sprites/pyramid/pool-prompts.json new file mode 100644 index 0000000..9e93d1b --- /dev/null +++ b/assets/sprites/pyramid/pool-prompts.json @@ -0,0 +1,4 @@ +{ + "mode": "built-in", + "prompt": "Use case: game-asset. Asset type: transparent sprite atlas for a retro 2.5D casino billiards minigame. Create exactly sixteen separate billiard ball sprites in a clean 4 by 4 equally spaced grid, orthographic straight overhead view, identical radius and center anchor, no perspective. Row 1: white cue ball, solid yellow, solid blue, solid red. Row 2: solid purple, solid orange, solid green, solid burgundy. Row 3: solid black eight ball, yellow stripe on white ball, blue stripe on white ball, red stripe on white ball. Row 4: purple stripe on white ball, orange stripe on white ball, green stripe on white ball, burgundy stripe on white ball. Every colored object ball has a small blank ivory circular number badge centered on top; NO numerals or lettering (engine adds correct digits). Each stripe goes horizontally through center with white caps above and below. Crisp polished shaded pixel art, recognizable high contrast colors, highlight upper left, subtle shading lower right, sharp round contours. Ball-only, no cast shadow outside the ball, no debris, no labels, no cloth, no decoration. Transparent background. Consistent sprite size occupying 65 percent of each grid cell with wide transparent gutters. Square atlas." +} diff --git a/assets/sprites/scratch/coin-edge-v1.png b/assets/sprites/scratch/coin-edge-v1.png new file mode 100644 index 0000000..32096c8 Binary files /dev/null and b/assets/sprites/scratch/coin-edge-v1.png differ diff --git a/assets/sprites/scratch/counter-v1.png b/assets/sprites/scratch/counter-v1.png new file mode 100644 index 0000000..402f013 Binary files /dev/null and b/assets/sprites/scratch/counter-v1.png differ diff --git a/assets/sprites/scratch/foil-v1.png b/assets/sprites/scratch/foil-v1.png new file mode 100644 index 0000000..46f806d Binary files /dev/null and b/assets/sprites/scratch/foil-v1.png differ diff --git a/assets/sprites/scratch/neon-ru-v3.png b/assets/sprites/scratch/neon-ru-v3.png new file mode 100644 index 0000000..8a2787f Binary files /dev/null and b/assets/sprites/scratch/neon-ru-v3.png differ diff --git a/assets/sprites/scratch/payout-chips-v1.png b/assets/sprites/scratch/payout-chips-v1.png new file mode 100644 index 0000000..e98dc44 Binary files /dev/null and b/assets/sprites/scratch/payout-chips-v1.png differ diff --git a/assets/sprites/scratch/prompts-v1.json b/assets/sprites/scratch/prompts-v1.json new file mode 100644 index 0000000..1f077e8 --- /dev/null +++ b/assets/sprites/scratch/prompts-v1.json @@ -0,0 +1,17 @@ +{ + "mode": "built-in image_gen", + "ticket": { + "file": "ticket-v1.png", + "prompt": "Use case: stylized-concept. Asset type: production pixel-art lottery scratch-ticket paper frame for a native retro casino game. Generate a single landscape rectangular ticket seen straight from above, aspect ratio 3:2, on a genuinely transparent background, isolated and unrotated. Same warm lighting and crisp detailed pixel clusters as a richly decorated 1990s casino game. Dark emerald and burgundy printed ornamental borders, fine gold-foil engraved trim, tiny clover and diamond motifs ONLY in the corners. A large clean warm white matte paper central area, entirely blank for the game to draw nine scratching zones. Leave a broad blank emerald header strip within the ticket and a small blank white footer strip. Slightly worn paper edges, tactile stock, subtly embossed decoration. No lettering, no numerals, no scratch windows, no symbols in the central area, no coin, no hands, no scenery, no perspective distortion, no drop shadow extending outside the paper. It must read as an actual printed lottery ticket, not a UI card or a painting." + }, + "coin": { + "file": "coin-edge-v1.png", + "prompt": "Use case: stylized-concept. Production game sprite animation atlas: exactly FOUR identical small brass coins in one horizontal row, FOUR evenly spaced cells, same fixed scale and same centre anchor. Genuinely transparent background, no scenery or hands. A worn brass casino token tilted steeply up on its thin ridged edge, seen from a first-person overhead view of scratching a flat lottery ticket. Coin face is a narrow visible ellipse, NOT a flat front-facing circle. Its BOTTOM-CENTRE rim is the contact point touching paper; keep that exact bottom-centre point constant in all four poses. Four subtle scraping rock positions (-6 degrees, neutral, +6 degrees, neutral), not spinning/flipping. Detailed engraved diamond emblem with a rim of tiny beads, no text or numerals. Crisp retro pixel-art clusters matching 1990s detailed casino sprites, warm metal highlights, sharp readable reeded edge. A thin coin, not a chunky poker chip. Entire coin contained in each cell, no background shadow, no sparkles, no labels. Single atlas, exactly four poses, no extra rows." + }, + "counter": { + "file": "counter-v1.png", + "prompt": "Use case: stylized-concept. Asset type: isolated native retro 2.5D casino furniture sprite, front elevation, full object, genuinely transparent background. A waist-height narrow wooden lottery ticket sales counter with a green leather writing top, brass trim, small display of neatly fanned emerald and burgundy paper scratch tickets, and a slim upright sign holder with a blank emerald face. No printed text anywhere. A small tray of brass coins next to the tickets. Warm casino lighting, detailed crisp 1990s pixel-art consistent with Doom-like billboard props, physically plausible furniture proportions. Entire counter legs and base visible, grounded feet, no backdrop, no people, no multiple objects separated as an atlas, no shadow beyond feet. This is one furniture object with its ticket display attached, not a whole room or decorative image." + }, + "foil": {"file": "foil-v1.png", "prompt": "Use case: stylized-concept. Asset type: flat seamless material texture for a retro pixel-art scratch lottery coating. A full-frame opaque landscape swatch of matte SILVER scratch-off latex foil. Fine silvery grey grain, tiny graphite speckles, subtle diagonal security hatching, flat even illumination with restrained metallic highlights. No perspective, no objects, no paper edge, no frame, no letters, no symbols, no numerals, no gradients, no glare spots. Crisp detailed pixel-art texture designed to be readable when reduced to about 100 by 40 game pixels. The entire image is just the silver protective coating material."}, + "preserved": "Original Clover symbols are reused; the coin hotspot is the bottom-centre rim, not its face." +} diff --git a/assets/sprites/scratch/prompts-v2.json b/assets/sprites/scratch/prompts-v2.json new file mode 100644 index 0000000..8728447 --- /dev/null +++ b/assets/sprites/scratch/prompts-v2.json @@ -0,0 +1,9 @@ +{ + "mode": "built-in image_gen", + "prompts": { + "scratch_table_prompt": "Use case: stylized-concept. Production background bitmap for a native retro casino scratch-ticket game, landscape FOUR-TO-THREE composition. A first-person view looking almost directly DOWN at a real lottery counter tabletop, not looking at a person across a gambling table. The entire central area (about 75 percent of image width and 70 percent of image height) is a clean empty dark emerald-green felt writing mat, perfectly flat and nearly orthographic, so a landscape lottery paper ticket can be composited there without floating. Walnut wood edging and restrained brass corner details around the outside. Along the extreme left edge: a small stack of emerald printed paper tickets and a brass coin tray, partly cropped, outside the clean centre. Along the extreme right edge: a warm desk lamp base and small paper slips, partly cropped, also outside the centre. Warm light from upper left, crisp detailed 1990s casino pixel-art clusters, no blur, same rich emerald/burgundy/gold material family as our lottery frame. No dealer, no people, no faces, no hands, no loose coin in the central area, no UI, no text, no numbers, no lottery ticket already placed in the central mat. Centre must be uninterrupted empty green surface extending behind the future ticket. No front-facing upright counter, no horizon, no room view, no gradients. This is a playable close-up tabletop background, not an illustration poster.", + "scratch_numbers_prompt": "Use case: stylized-concept. Asset type: retro pixel casino scratch-off paper ticket, one landscape 14:9 flat rectangle viewed exactly from above, full frame. Primary request: a new 'lucky numbers' lottery ticket background in powder blue, deep petrol and metallic silver security-print guilloche, with small embossed number-ball motifs only in four corners. Detailed 1990s adventure-game bitmap art, crisp pixels, paper grain, consistent with an old luxurious casino. Layout: narrow ornate border, dark blank title band at top 8%-19%, absolutely empty light ivory writing area from x8%-92% y28%-80%, a narrow empty ivory footer strip y87%-95%. No letters, no numerals, no scratch panels, no shadows cast outside paper, no perspective, no UI, no watermark. Fill image with the ticket itself; all decorations confined to border/corners so program can overlay live text and scratch surfaces.", + "scratch_rows_prompt": "Use case: stylized-concept. Asset type: retro pixel casino scratch-off paper ticket, single landscape 14:9 flat rectangle viewed exactly from above filling frame. Primary request: 'three in a row' themed scratch ticket background, vermilion/red, charcoal and antique gold, decorative embossed cherries and bells ONLY in four corners, subtle sunburst security-print border. Detailed 1990s adventure-game bitmap art, crisp pixels, fine paper grain, old luxurious casino. Layout: narrow ornate border, dark blank title band at top 8%-19%, absolutely empty light ivory writing area from x8%-92% y28%-80%, narrow empty ivory footer strip y87%-95%. No letters, no numbers, no scratch panels, no outside shadows, no perspective, no UI, no watermark. Decorations confined to border and corners so program can overlay live symbols and round scratch spots.", + "scratch_instant_prompt": "Use case: stylized-concept. Asset type: retro pixel casino scratch-off paper ticket, one landscape 14:9 flat rectangle viewed exactly from above, full frame. Primary request: instant-prize lottery ticket background, turquoise teal with sunshine yellow and silver, embossed radiant lucky stars and coins confined to four corner motifs and guilloche border. Detailed 1990s adventure-game bitmap art, crisp pixels, fine paper grain, old luxurious casino. Layout: narrow ornate border, deep teal blank title band at top 8%-19%, absolutely empty light ivory writing area from x8%-92% y28%-80%, narrow empty ivory footer strip y87%-95%. No letters, numerals, scratch panels, outside shadows, perspective, UI or watermark. Image is the ticket itself, with empty central area large enough for a single big scratch panel and live prize." + } +} diff --git a/assets/sprites/scratch/prompts-v3.json b/assets/sprites/scratch/prompts-v3.json new file mode 100644 index 0000000..0de9595 --- /dev/null +++ b/assets/sprites/scratch/prompts-v3.json @@ -0,0 +1,27 @@ +{ + "mode": "built-in image_gen", + "intent": "text-localization", + "prompt_record": "Recovered edit briefs, not verbatim tool prompts; the exact pre-compaction tool arguments were unavailable.", + "shared_edit_brief": "Localize the supplied portrait scratch ticket into Russian. Preserve its illustrated theme, composition, borders and exact number and placement of silver scratch fields. Keep each field empty silver for runtime replacement. Remove the fixed dollar price and numerical maximum-prize advertising so it does not contradict the game's selectable prices and actual awards. Preserve a portrait 3:4 layout.", + "assets": [ + { + "input": "codex-clipboard-a52ed500-1ee3-4ee3-bff5-dd0970bfd3d8.png", + "output": "neon-ru-v3.png", + "generated_file": "exec-3ca37508-f784-4007-af7e-b9c49fcabb28.png", + "edit_brief": "Retain the neon cyan/magenta city, chips and diamonds. Russian title: НЕОНОВАЯ УДАЧА. Five winning circles and fifteen player rectangles. Retain number-match and double/ten-times bonus semantics. Replace the numeric top prize with a generic large-prize heading." + }, + { + "input": "codex-clipboard-b32c02a1-cd6a-4b39-995d-eecafadf9a67.png", + "output": "ruby-ru-v3.png", + "generated_file": "exec-255460ed-b60a-43ac-8144-5d8fd5731880.png", + "edit_brief": "Retain red rubies, gold ornament and original layout. Russian title: РУБИНОВЫЙ ДЖЕКПОТ. Six winning circles and eighteen player rectangles. Preserve double-prize and all-prizes bonus semantics without advertising a fixed prize ceiling." + }, + { + "input": "codex-clipboard-ea66185b-a5a8-4081-b022-e7e04556bded.png", + "output": "sea-ru-v3.png", + "generated_file": "exec-3bec4802-64d4-4f4b-9be1-4c779ab4b49b.png", + "edit_brief": "Retain the blue tropical sea, treasure, ship and gold details. Russian title: ВОЛНА СОКРОВИЩ. Four winning circles and twelve player rectangles. Preserve double-prize and all-prizes bonus semantics without fixed numerical price or maximum-prize claims." + } + ], + "packing": "Offline pack_scratch_portraits.py resizes artwork to 540x720 BGRA and extracts 20/24/16 exact silver-region ID masks. All interactive rendering and gameplay remain MASM." +} diff --git a/assets/sprites/scratch/prompts-v4.json b/assets/sprites/scratch/prompts-v4.json new file mode 100644 index 0000000..01bbb34 --- /dev/null +++ b/assets/sprites/scratch/prompts-v4.json @@ -0,0 +1,7 @@ +{ + "mode": "built-in image_gen", + "intent": "generate", + "output": "payout-chips-v1.png", + "transparent_background": true, + "prompt": "Use case: stylized-concept. Asset type: isolated game sprite, casino payout chips for a retro 2.5D pixel-art casino. Create ONE compact cluster of three short stacks of casino chips, one deep emerald with ivory edge marks, one red with ivory edge marks, one warm ivory with dark emerald edge marks, and two small brass tokens lying beside their bases. View is almost overhead, slightly tilted toward the player, matching an overhead green felt writing desk lit warmly from upper left. Entire cluster fully in frame, clearly separated object silhouette, transparent background including all empty space. No desk, no tray, no hands, no cards, no words, no denomination numerals, no borders, no glow, no floating parts. All stacks rest on the same contact plane and cast a short subtle contact shadow. Detailed hand-painted pixel-art edges, crisp distinguishable chip layers, restrained highlights, not glossy photorealistic 3D. Wide compact silhouette suitable for reduction to a 96 by 72 pixel sprite. This is a new asset, not a sprite sheet." +} diff --git a/assets/sprites/scratch/ruby-ru-v3.png b/assets/sprites/scratch/ruby-ru-v3.png new file mode 100644 index 0000000..b93f1cc Binary files /dev/null and b/assets/sprites/scratch/ruby-ru-v3.png differ diff --git a/assets/sprites/scratch/sea-ru-v3.png b/assets/sprites/scratch/sea-ru-v3.png new file mode 100644 index 0000000..8327102 Binary files /dev/null and b/assets/sprites/scratch/sea-ru-v3.png differ diff --git a/assets/sprites/scratch/table-v1.png b/assets/sprites/scratch/table-v1.png new file mode 100644 index 0000000..aefc9f6 Binary files /dev/null and b/assets/sprites/scratch/table-v1.png differ diff --git a/assets/sprites/scratch/ticket-instant-v1.png b/assets/sprites/scratch/ticket-instant-v1.png new file mode 100644 index 0000000..e69f5bd Binary files /dev/null and b/assets/sprites/scratch/ticket-instant-v1.png differ diff --git a/assets/sprites/scratch/ticket-numbers-v1.png b/assets/sprites/scratch/ticket-numbers-v1.png new file mode 100644 index 0000000..6f2db6c Binary files /dev/null and b/assets/sprites/scratch/ticket-numbers-v1.png differ diff --git a/assets/sprites/scratch/ticket-rows-v1.png b/assets/sprites/scratch/ticket-rows-v1.png new file mode 100644 index 0000000..2ba0f20 Binary files /dev/null and b/assets/sprites/scratch/ticket-rows-v1.png differ diff --git a/assets/sprites/scratch/ticket-v1.png b/assets/sprites/scratch/ticket-v1.png new file mode 100644 index 0000000..83ba6eb Binary files /dev/null and b/assets/sprites/scratch/ticket-v1.png differ diff --git a/bin/poker.exe b/bin/poker.exe index 74e1c3c..0adb1b7 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build.bat b/build.bat index 5f77b9a..b10d062 100644 --- a/build.bat +++ b/build.bat @@ -1,7 +1,21 @@ @echo off setlocal -rem Check standard MASM32 paths first +rem Check Visual Studio / MSVC BuildTools first (modern Windows compatible) +set "MSVC_BIN=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\bin\Hostx64\x86" +set "SDK_LIB=C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\um\x86" +set "UCRT_LIB=C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\ucrt\x86" +set "VC_LIB=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\lib\x86" + +if exist "%MSVC_BIN%\ml.exe" ( + set "ML_EXE=%MSVC_BIN%\ml.exe" + set "LINK_EXE=%MSVC_BIN%\link.exe" + set LIB_PATHS=/LIBPATH:"%SDK_LIB%" /LIBPATH:"%UCRT_LIB%" /LIBPATH:"%VC_LIB%" + set "EXTRA_FLAGS=/NOLOGO /SUBSYSTEM:WINDOWS" + goto do_build +) + +rem Check standard MASM32 paths if exist "D:\MASM32\masm32\bin\ml.exe" ( set "ML_EXE=D:\MASM32\masm32\bin\ml.exe" set "LINK_EXE=D:\MASM32\masm32\bin\link.exe" @@ -16,7 +30,6 @@ if exist "C:\masm32\bin\ml.exe" ( set "EXTRA_FLAGS=/SUBSYSTEM:WINDOWS /RELEASE /VERSION:4.0" goto do_build ) - if exist "F:\MASM32\masm32\bin\ml.exe" ( set "ML_EXE=F:\MASM32\masm32\bin\ml.exe" set "LINK_EXE=F:\MASM32\masm32\bin\link.exe" @@ -25,20 +38,6 @@ if exist "F:\MASM32\masm32\bin\ml.exe" ( goto do_build ) -rem Fallback to Visual Studio / MSVC BuildTools -set "MSVC_BIN=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\bin\Hostx64\x86" -set "SDK_LIB=C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\um\x86" -set "UCRT_LIB=C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\ucrt\x86" -set "VC_LIB=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.50.35717\lib\x86" - -if exist "%MSVC_BIN%\ml.exe" ( - set "ML_EXE=%MSVC_BIN%\ml.exe" - set "LINK_EXE=%MSVC_BIN%\link.exe" - set LIB_PATHS=/LIBPATH:"%SDK_LIB%" /LIBPATH:"%UCRT_LIB%" /LIBPATH:"%VC_LIB%" - set "EXTRA_FLAGS=/NOLOGO /SUBSYSTEM:WINDOWS" - goto do_build -) - echo [ERROR] No compatible MASM/MSVC assembler found! goto err diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index 580db24..e2dd3ef 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "7E7165F5B9287FCF0F7A7E8F526DD23C0DA5F3A6CF72F08D1A40EB548FFCD428"; + const string ExpectedPokerSha256 = "3CE946233ABE1D9139501EB56A75B2BE0D9A99714048C5F0A56A150D35F2295C"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index b5a8caf..874292b 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/casino3d.inc b/casino3d.inc index 2903f96..566b49a 100644 --- a/casino3d.inc +++ b/casino3d.inc @@ -167,6 +167,8 @@ include src_graphics\unavailable_data.inc include src_graphics\native_slots_data.inc include src_graphics\native_clover_data.inc include src_graphics\native_coin_data.inc +include src_graphics\native_dice_data.inc +include src_graphics\scratch_data.inc .data ALIGN 4 @@ -3081,13 +3083,15 @@ die_pips_done: DrawCrapsDie ENDP InitDiceGame PROC + call LoadNativeDiceArt3D + mov diceExitQueued3D,0 mov dice3d_state, 0 mov dice3d_timer, 0 mov dice3d_win, 0 ret InitDiceGame ENDP -HandleDiceInput PROC +HandleDiceRules3D PROC cmp dice3d_state, 0 je dice_handle_idle cmp dice3d_state, 1 @@ -3233,121 +3237,10 @@ dice_handle_result: invoke PlayCasinoSound3D, OFFSET soundClick, 0, 1 dice_done: ret -HandleDiceInput ENDP +HandleDiceRules3D ENDP -RenderDiceGame PROC uses ebx esi edi - local kx:DWORD - - ; Mahogany frame & Deep Felt Green table - invoke DrawBox, 0, 0, 320, 240, 00140906h - invoke DrawBoxBorder, 6, 6, 308, 228, 000A3816h, 00FFD700h, 2 - - ; Header - invoke DrawBoxBorder, 20, 12, 280, 26, 00072E13h, 00FFD700h, 2 - invoke DrawString8x8, 116, 21, OFFSET textDiceTitle, 00FFFF00h - - ; Status - invoke DrawBoxBorder, 20, 44, 110, 18, 00072E13h, 001B4B22h, 1 - invoke DrawString8x8, 24, 49, OFFSET textBankPrompt, 003CFF3Ch - invoke FormatNumber, bankroll, OFFSET hudNumBuffer - invoke DrawString8x8, 72, 49, OFFSET hudNumBuffer, 003CFF3Ch - - invoke DrawBoxBorder, 190, 44, 110, 18, 00072E13h, 001B4B22h, 1 - invoke DrawString8x8, 194, 49, OFFSET textBetPrompt, 00FFD700h - invoke FormatNumber, dice3d_bet, OFFSET hudNumBuffer - invoke DrawString8x8, 258, 49, OFFSET hudNumBuffer, 00FFD700h - - ; [1] רÒ - cmp dice3d_choice, 1 - jne dice_even_inactive - invoke DrawBoxBorder, 16, 70, 88, 48, 00072E13h, 00FFFF00h, 2 - invoke DrawString8x8, 30, 76, OFFSET textDiceEven, 00FFFF00h - invoke DrawString8x8, 22, 90, OFFSET textDiceEvenNums, 00FFD700h - invoke DrawString8x8, 26, 104, OFFSET textCoinSelected, 003CFF3Ch - jmp dice_odd_side -dice_even_inactive: - invoke DrawBoxBorder, 16, 70, 88, 48, 00072E13h, 001B4B22h, 1 - invoke DrawString8x8, 30, 76, OFFSET textDiceEven, 00D0D8E0h - invoke DrawString8x8, 22, 90, OFFSET textDiceEvenNums, 0080B090h - -dice_odd_side: - ; [2] ÍÅ×ÅÒ - cmp dice3d_choice, 2 - jne dice_odd_inactive - invoke DrawBoxBorder, 216, 70, 88, 48, 00072E13h, 00FFFF00h, 2 - invoke DrawString8x8, 226, 76, OFFSET textDiceOdd, 00FFFF00h - invoke DrawString8x8, 224, 90, OFFSET textDiceOddNums, 00FFD700h - invoke DrawString8x8, 226, 104, OFFSET textCoinSelected, 003CFF3Ch - jmp dice_center_cubes -dice_odd_inactive: - invoke DrawBoxBorder, 216, 70, 88, 48, 00072E13h, 001B4B22h, 1 - invoke DrawString8x8, 226, 76, OFFSET textDiceOdd, 00D0D8E0h - invoke DrawString8x8, 224, 90, OFFSET textDiceOddNums, 0080B090h - -dice_center_cubes: - call RenderDiceThrow3D - - cmp dice3d_state, 1 - je dice_render_rolling - cmp dice3d_state, 2 - je dice_render_result - jmp dice_render_keys - -dice_render_rolling: - invoke DrawString8x8, 76, 138, OFFSET textDiceRolling, 00FFFF00h - jmp dice_render_keys - -dice_render_result: - invoke DrawBoxBorder, 36, 130, 248, 36, 00072E13h, 00FFD700h, 1 - invoke DrawString8x8, 44, 135, OFFSET textDiceSumHdr, 00FFD700h - invoke FormatNumber, dice3d_val1, OFFSET hudNumBuffer - invoke DrawString8x8, 92, 135, OFFSET hudNumBuffer, 00FFFF00h - invoke DrawString8x8, 110, 135, OFFSET textDicePlus, 00FFD700h - invoke FormatNumber, dice3d_val2, OFFSET hudNumBuffer - invoke DrawString8x8, 126, 135, OFFSET hudNumBuffer, 00FFFF00h - invoke DrawString8x8, 144, 135, OFFSET textDiceEqual, 00FFD700h - invoke FormatNumber, dice3d_sum, OFFSET hudNumBuffer - invoke DrawString8x8, 168, 135, OFFSET hudNumBuffer, 00FFFF00h - - mov eax, dice3d_sum - and eax, 1 - jnz dice_render_odd_res - invoke DrawString8x8, 186, 135, OFFSET textDiceEvenResult, 003CFF3Ch - jmp dice_render_outcome -dice_render_odd_res: - invoke DrawString8x8, 186, 135, OFFSET textDiceOddResult, 003CFF3Ch - -dice_render_outcome: - mov eax, dice3d_win - test eax, eax - jz dice_render_loss_outcome - invoke DrawString8x8, 68, 150, OFFSET textDiceWin, 003CFF3Ch - invoke FormatNumber, dice3d_win, OFFSET hudNumBuffer - invoke DrawString8x8, 156, 150, OFFSET hudNumBuffer, 003CFF3Ch - invoke DrawString8x8, 188, 150, OFFSET textDiceWinMult, 003CFF3Ch - jmp dice_render_keys - -dice_render_loss_outcome: - invoke DrawString8x8, 116, 150, OFFSET textDiceLose, 00FF6464h - -dice_render_keys: - mov kx, 16 - xor eax, eax - cmp dice3d_choice, 1 - sete al - invoke DrawKeycap3D, kx, 192, OFFSET textDiceKey1, eax - add kx, eax - - xor eax, eax - cmp dice3d_choice, 2 - sete al - invoke DrawKeycap3D, kx, 192, OFFSET textDiceKey2, eax - add kx, eax - - invoke DrawKeycap3D, kx, 192, OFFSET textDiceKeySpace, 1 - add kx, eax - - invoke DrawKeycap3D, kx, 192, OFFSET textKeyEsc, 0 +RenderDiceGame PROC + call RenderNativeDice3D ret RenderDiceGame ENDP @@ -5140,6 +5033,8 @@ include src_graphics\scene_animation.inc include src_graphics\native_slots.inc include src_graphics\native_clover.inc include src_graphics\native_coin.inc +include src_graphics\native_dice.inc +include src_graphics\scratch.inc include src_graphics\graphic_profiles.inc include src_graphics\city_arrival_scene.inc include src_graphics\arrival_scene.inc @@ -5212,6 +5107,16 @@ dispatch_frame: xor paused3D, 1 jmp engine_tick leave_table: + cmp engineMode,MODE_SCRATCH + jne @F + call LeaveScratch3D + jmp engine_tick +@@: + cmp engineMode,MODE_DICE + jne @F + call DiceEscape3D + jmp engine_tick +@@: call CardTableEscape3D test eax, eax jnz engine_tick @@ -5264,6 +5169,14 @@ check_walk_mode: call RenderMinimap jmp present_frame check_slots_mode: + cmp engineMode,MODE_SCRATCH + jne @F + call HandleScratch3D + cmp engineMode,MODE_WALK + je check_walk_mode + call RenderScratch3D + jmp present_frame +@@: cmp engineMode,MODE_CLOVER jne @F call HandleNativeClover3D @@ -5494,6 +5407,7 @@ engine_idle: invoke Sleep, 50 jmp engine_loop engine_shutdown: + call SettleScratchOnQuit3D call UnlockMouse3D cmp hRaycastThread, 0 je @F diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 880d27e..9ca986d 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/BILLIARDS_CURRENT.md b/docs/BILLIARDS_CURRENT.md new file mode 100644 index 0000000..4b2abda --- /dev/null +++ b/docs/BILLIARDS_CURRENT.md @@ -0,0 +1,103 @@ +# Native Billiards + +The casino table opens a selection of free Russian pyramid and American +eight-ball pool. All game simulation, rules, input, rendering and prediction +run in x86 MASM. Python only normalizes generated art and verifies captured +frames; no Python or external physics engine ships as the game runtime. + +## Eight-Ball Turn Regression + +The previous implementation retained a default/previous called ball and pocket +for human shots, while the AI supplied a new call before striking. A normal +human pot could therefore pass the turn despite correct contact and no foul. +Native regression case 12 reproduced that failure through aim, Space, rendering +and actual fixed-step motion before the fix. Scoring also credited every pot +to the shooter instead of the owner of the solid/striped group. + +Automatic pre-shot calls now come from the existing physics prediction; manual +ball and pocket overrides remain binding. Group credit is reconstructed from +the actual pocketed balls and assigned groups. No collision, rail, pocket, +friction, force or pyramid-rule formulas were changed for this correction. + +## Controls + +- Select a discipline with the mouse or 1/2. +- Left-click/drag on the cloth to aim; arrows provide fine aim. Shift makes + arrow aim slower. Up/down or the force slider changes shot power. +- Space or the shot button strikes. P or the guide button toggles prediction. +- Pyramid: right-click a ball, B or the cue button selects the cue ball. + The break uses the colored ball; Shift-click places it in the home area. +- Eight-ball: right-click a legal object ball or use the order button/B to + call it. Right-click a pocket or click the pocket indicator to call a pocket. + Without an explicit override, the shared physics preview supplies the obvious + intended ball and pocket before striking, even with the guide hidden. Ball and + pocket overrides are independent and reset after settlement, not before it. + An unknown automatic call is shown as a dash rather than a stale previous call. + After a foul, click a clear place on the cloth to position the cue ball. +- Use the exit button or the casino's Escape flow to leave the table. + +## Rules + +Free pyramid uses fifteen white numbered balls and one colored ball, with +any active ball eligible as cue after the break. The first player to eight +credited balls wins. Legal cue-ball pots count after contact. Incorrectly +potted balls are respotted; the incoming player may take one penalty ball +and choose who plays next. Invalid breaks offer acceptance or a partial +re-rack. Ordered cushion/crossing histories determine legal non-pot shots. + +Eight-ball uses the white cue, seven solids, seven stripes and black eight. +Groups are assigned by a legal called pot after the break. A wrong first +contact, scratch or no post-contact cushion/pot gives ball in hand. +The eight must be called and pocketed in the nominated pocket after clearing +the player's group; an early eight loses. A break eight is respotted, not an +automatic win. Invalid non-scratch breaks offer acceptance or a new break. +Pocketed solids/stripes are credited to their group's player, independently of +who struck or whether a foul occurred. Once groups are assigned, this includes +balls pocketed earlier on the open table. The white and black balls never add +to the group count. A legal called pot keeps the shooter at the table; a miss +passes the turn and a foul gives the opponent ball in hand. + +These are playable casino adaptations, not a complete tournament referee. +Shot-clock/lag/protests, airborne balls, jump shots, deliberate push shots, +and all special tournament options for scratch/eight on the break are not +implemented. Backspin, topspin, side spin and curved trajectories are also +not implemented. Do not describe this simulation as ideal real-world physics. + +## Physics And Prediction + +- Q16.16 positions/velocities; fixed 2 ms steps independent of frame timing. +- Equal-mass normal impulses, separation, restitution and rolling deceleration. +- Distinct narrow pyramid / wider pool pockets and rounded jaw colliders. +- Pocket AABB rejection precedes squared-distance math, preventing long-vector + overflow from deleting a ball at the opposite end of the table. +- Fine aim uses 4096 interpolated phases and 64-bit projection numerators. +- Preview calls the same `PoolStep3D` as actual play. It restores all ball + arrays, collision histories and physics event fields before returning. +- Cached preview depends on the layout, discipline, cue, direction and force. + It samples up to 6000 steps (12 seconds), showing cue/object paths including + rebounds. This is a bounded guide; a moving ball beyond that horizon is not + claimed to have reached its final position. + +## Verification + +`tools/tests/test_casino_actions.ps1 -SkipPreview` runs native MASM assertions, +including disappearance regression, all pockets, 100 breaks, pyramid scoring +and penalties, eight-ball groups/fouls/wins, choice UI, and preview/live parity. +`eightball_regression.inc` adds actual aim/Space/render/physics sequences for +consecutive human pots, both actors, hidden/visible guides, manual calls and +group-based credit. `test_billiards_module.ps1` runs these plus the pre-existing +pyramid/choice tests against fresh MASM code and the native graphics helpers. +`tools/tests/check_billiards.py --parity` checks actual framebuffer captures +and equality of tested versus shipping machine-code procedures. + +Generated art and exact built-in prompts are under `assets/sprites/pyramid`. +Packed game assets are under `assets/animations/pyramid`. + +## References + +- [FBSR pyramid rules, September 2025](https://www.fbsrf.ru/sites/default/files/04-novaya_redakciya_pravil_piramidy_2025-09.pdf) +- [WPA Rules of Play, January 2026](https://www.wpapool.com/wp-content/uploads/2026/01/2026.01.02-WPA-Rules.pdf) +- [Pooltool physics model organization](https://pooltool.readthedocs.io/en/latest/resources/custom_physics.html) + +Pooltool is a reference for separating motion and collision models, not a +runtime dependency. No Pooltool source was copied into the MASM game. diff --git a/docs/BILLIARDS_PLAN.md b/docs/BILLIARDS_PLAN.md index d433aeb..0c52f7d 100644 --- a/docs/BILLIARDS_PLAN.md +++ b/docs/BILLIARDS_PLAN.md @@ -1,5 +1,10 @@ # Native Billiards +This is the historical first-implementation plan. Current disciplines, +controls, physics coefficients and verification are documented in +[BILLIARDS_CURRENT.md](BILLIARDS_CURRENT.md); use that document for the game +now shipping rather than the older controls and coefficients below. + The first playable implementation is in `src_graphics/billiards.inc`, entirely MASM32. The simulation uses positions and velocities, not predetermined shots. diff --git a/docs/CITY_WAKE_SCENE.md b/docs/CITY_WAKE_SCENE.md new file mode 100644 index 0000000..e4f50d8 --- /dev/null +++ b/docs/CITY_WAKE_SCENE.md @@ -0,0 +1,52 @@ +# Alley Wake-Up + +The Eblan Games intro leads into the exterior cinematic on every launch. +Runtime camera, weather, sprite playback, compositing and input remain MASM. +PNG generation and normalization are offline tools only. + +The protagonist opens their eyes while seated on the alley ground. Bent knees +remain in the foreground while a scruffy cyberpunk rocker crouches nearby and +speaks. The agreed dialogue is shown as Russian subtitles, without synthesized +voice: "Вставай, самурай, пора дэпнуть все в казино". + +The protagonist raises a cigarette, opens and ignites a flip-top lighter, +looks into the cloudy sky and exhales, then rises smoothly, turns right and discards the butt +into the existing projected dumpster mouth and follows the city route to the +guards. Profile selection still happens at the casino entrance. Enter can +skip to the guards; existing Escape handling is preserved. + +The seated view faces east, toward the rocker. Dumpsters are south of the +protagonist, off-screen to their right. The separate look-right phase rotates +the camera only after standing; the throw then uses the real dumpster mouth. +Traffic timing follows the pre-alley duration, preserving the safe red-light +crossing after the longer wake-up sequence. + +There are thirty-two grounded rocker poses and sixteen lighter poses. The +speaking cycle has sixteen distinct crouched poses, then eight rise/turn poses +and eight walking-away poses. The coat's red SAMURAI back emblem follows the +[official jacket reference](https://www.cyberpunk.net/en/news/30631/pre-order-your-samurai-bomber-jacket-now). +Character +normalization uses whole connected silhouettes rather than nominal atlas cells, +so neighbouring fragments cannot move the feet anchor. Generated assets and +exact prompts are under `assets/sprites/arrival/wake-*`; packed runtime assets +are under `assets/animations/arrival/wake-*`. + +The lighter forearm is a separate continuous leather sleeve placed underneath +the animated cuff using measured per-frame anchors. It extends out of the +viewport rather than ending at a floating wrist. New vent/poster/crate/cabinet +materials are in `alley-materials-v1.bin`; wall-mounted fittings, drainage +pipes, stacked crates and a floor drain use actual ray/depth-tested surfaces. +The centre walking strip and the right-hand dumpster reveal remain clear. +`wake-v2-prompts.json` records all three built-in image-generation prompts. + +Rain uses three visual-depth layers with different speeds, length and opacity. +Streaks blend into the rendered scene rather than replacing it with blue blocks. +Ground splashes are rejected behind nearer geometry. Weather reads cinematic +time, not the gameplay random generator. Looking vertically into the sky blends +the raycast view into the sky rather than abruptly switching projections. + +Verification uses the actual native action-test renderer, camera continuity +samples, route/vehicle checks, profile transitions, packed-asset checks and +framebuffer sequences. `check_arrival_frames.py` writes wake/ignition preview +GIFs from those native captures. `check_city_wake.py --parity` checks dialogue, +anchors and equality of shipping/tested cinematic procedures. diff --git a/docs/DICE_FIRST_PERSON.md b/docs/DICE_FIRST_PERSON.md new file mode 100644 index 0000000..85b88cd --- /dev/null +++ b/docs/DICE_FIRST_PERSON.md @@ -0,0 +1,32 @@ +# First-Person Dice + +The ivory/red identity and MASM triangle renderer are retained. Held dice are +now integrated into the first four hand poses, with skin contact and shading, +rather than painted as unrelated cubes over the palm. Once released, procedural +MASM cubes take over at anchors measured from the last held sprite. + +The scene reuses the coin table/dealer art and adds sixteen generated hand +poses with a full black-leather cuff. Preparation lasts 240 ms, followed by the +original three-bounce curve remapped into the remaining 1360 ms. The unchanged +parity wager settles once at 1600 ms. The stationary top faces and explicit +Russian sum/payout strip reveal the result. Ready and result states do not +consume random numbers during rendering. + +Rotation completes full revolutions with eased angular speed. The resting +orientation is a yaw on the cloth plane, viewed from above, without sideways +roll. A projected bottom-face contact shadow and soft ambient shadow attach +each settled cube to the felt. Opposite faces sum to seven. + +Mouse buttons map into the original keyboard rules. Escape or the Exit button +queues departure during a paid throw; settlement occurs before returning to +the hall. W/S, arrows, 1/2 and Space remain supported. + +All rendering, input, animation, loading and gameplay are MASM. The Python +script only normalizes a transparent sprite atlas offline. Built-in generation +specification and provenance: `assets/sprites/dice/prompts-v2.json`. + +Verification: the native action suite covers 300 paid throws at four update +rates, queued exits, mouse controls, missing-art fallback, six checkpoints and +forty production-renderer animation frames, release-boundary snapshots and all +six top-face results. `check_dice_scene.py --parity` +compares shipped and tested procedures and checks native Russian result glyphs. diff --git a/docs/SCRATCH_TICKETS.md b/docs/SCRATCH_TICKETS.md new file mode 100644 index 0000000..30a97d0 --- /dev/null +++ b/docs/SCRATCH_TICKETS.md @@ -0,0 +1,185 @@ +# Scratch Tickets + +The runtime is MASM-only. Python only normalizes generated artwork and checks +native framebuffer captures; there is no browser or console implementation. + +## Scene + +A dedicated ticket counter sits at hall coordinates (29,7.5), against the +right-wing wall near Durak. Approach it from the north. It has a grounded sprite and a solid collision footprint. The +interaction opens an overhead writing mat without launching another +window. The tabletop, paper, silver foil, counter and four tilted-coin poses are original +built-in ImageGen assets. Symbols reuse the existing Clover art. + +The cursor hotspot is the bottom-centre scraping rim, (32,60) in a 64x64 sprite. +Pointer coordinates use the presented viewport, including letterboxing. Left +button capture ends on mouse-up, lost capture, lost focus or scene exit. Mouse +events are processed individually, including gestures completed between ticks. +Strokes interpolate a narrow elliptical rim every three native pixels. + +## Presentation + +Purchasing slides the entire ticket onto the mat, including its live coating +and printed contents, with a paper-shaped contact shadow. Presentation lasts +900 ms; pointer scratching and view changes are blocked until it settles. +Portrait tickets arrive at full-ticket scale and smoothly zoom into the playing +fields over the final 350 ms. Paper/zoom use an elapsed-time smoothstep, not a +fixed animation-frame list. Exiting the scene cancels the presentation, not the +already-paid ticket. + +Fresh scratches emit three silver fragments into a 64-slot cosmetic pool. +Positions/velocities use 8.8 fixed point, gravity advances in fixed 8 ms steps, +and fragments settle on the mat before expiring. A private visual seed means +particles never consume the ticket RNG. Hovering and already-erased coating +produce neither fragments nor scraping audio; scraping audio is throttled to +one short sound per 150 ms. + +Positive gross payouts display a grounded chip stack beside the ticket and +five small tokens travelling toward the bank display. The cosmetic payout +clock lasts 1800 ms. Settlement itself remains immediate and single-shot; +the animation cannot issue money, delay saving or block exit/new purchases. +Portrait winning fields also receive a pale green substrate inside their exact +source-space masks. Covered numbers are never tinted through the coating. + +The payout-chip sprite was generated with built-in ImageGen, then normalized +offline to 96x72 binary-alpha BGRA. Its exact prompt is stored in +`assets/sprites/scratch/prompts-v4.json`. Three original short PCM effects +(paper slide, scrape and payout tokens) are generated offline by +`tools/generators/pack_scratch_polish.py`. Rendering, animation clocks, input +gates, debris physics and audio triggers are all assembly. + +Each coating has its own byte mask (up to 240x120, nine slots). Repeated strokes do not +increment coverage twice, gutters and paper cannot be scratched, and a panel +cleans its remaining fragments after 72 percent coverage. Particles use no +gameplay randomness. Rendering is deterministic and cannot reroll a ticket. + +## Rules + +Prices: 50, 200 or 500 fictional chips. These are price tiers, not ticket types. +The upper-right "VID BILETA" control opens a seven-row named catalogue with +thumbnails. A short click keeps its original press coordinates even if the +pointer moves before the next game tick. Escape closes the catalogue first, +then returns to the hall on a subsequent press. + +The original four tickets retain their distinct artwork and rules: + +- Golden Triple: nine rectangular areas; three identical symbols anywhere pay + the printed multiplier, including the stake. Exactly one winning triple or + no triples at all. +- Lucky Numbers: three winning numbers above six player numbers. A match pays + the amount printed below the matching player number. All numbers are 1..30; + winning numbers are distinct, with exactly one matching player number on a + winning ticket and none on a losing ticket. The 104x34 panels leave separate + printed labels for the winning-number row and player-number rows. +- Three in a Row: nine circular areas. Only three matching symbols in the same + horizontal row pay. A scattered or diagonal triple is not a win. +- Instant Luck: one large 240x120 area reveals the direct gross prize, including + zero for a losing ticket. + +Three additional portrait tickets preserve the layouts of the user's supplied +images, localized into Russian using built-in ImageGen: + +- Neon Fortune: five winning-number circles and fifteen player fields. + Matching numbers pay the amount below them; x2 pays twice the printed prize, + x10 pays ten times the printed prize. +- Ruby Jackpot: six winning-number circles and eighteen player fields. + Matching numbers and x2 work as above; the ALL marker pays every player prize. +- Treasure Wave: four winning-number circles and twelve player fields, with + matching numbers, x2 and ALL as on Ruby Jackpot. + +The native bonus markers are x2, x10 and the Russian ALL abbreviation; the +adjacent native rules use the same markers. All-prizes overrides individual +awards, never paying the same prize twice. Winning numbers are distinct 1..60. +The original artwork's fixed prices and advertised maximum jackpots were +removed: the actual selected price and payout are shown by the native HUD. + +Full-ticket preview preserves the 3:4 aspect ratio. Purchase automatically +zooms into the play area; the +/- control, Z and mouse wheel switch views. +Amounts are printed under player numbers in the enlarged view. Hovering a +fully uncovered field shows its actual award, never a covered field's prize. +Each supplied silver region has an exact source-space ID mask. Rounded circles +and decorated frames are not rectangular scratch surfaces. Scaling covers the +whole source footprint of each screen pixel, so both views can be fully scratched +and switching views cannot restore removed coating. + +New tickets clear old coating particles, so the previous ticket cannot leave +scraps over the next one's opaque fields. Ticket types can change only before +purchase or after settlement. +Prices and type are locked while a paid ticket is active. The rules, paper +contents, outcome and amount are fixed at purchase, never while scratching. + +| Outcome | Ideal 0..999 rolls | Gross Multiplier | +| --- | --- | --- | +| No prize | 600 | 0 | +| Clover | 100 | 1 | +| Lemon | 100 | 1 | +| Cherries | 110 | 2 | +| Bell | 50 | 3 | +| Diamond | 25 | 5 | +| Chest | 12 | 10 | +| Seven | 3 | 25 | + +The original four tickets' ideal table has 89% gross return; the existing engine's range generator has +modulo bias, so this is not an exact measured return guarantee. The ticket is +charged once. Revealing all panels settles it once, with the existing billion- +chip balance cap (one panel for Instant Luck, nine for the other original types, +20/24/16 for the portrait types). Buttons +cannot buy a second ticket while one is active. + +Portrait generation uses 0..999 outcome rolls: 600 lose, 300 have one number +match, 80 have x2. Neon uses the remaining 20 for x10. Ruby and Sea use 15 for +two matching numbers and 5 for all-prizes. One-match amounts are weighted +1/2/5/25 times price (Neon cutoffs 700/900/990; Ruby/Sea 650/850/980). +Two-match prizes each equal price; all-prizes fields each equal price/5. +This is fictional-chip gameplay, not a representation of real ticket odds. + +Completed tickets show gross PAYOUT and signed NET RESULT (gross minus price) +separately. A 50-chip ticket paying 50 has net zero, not +50. Golden Triple +highlights its three matching symbols and prints price x multiplier = payout. + +An unfinished ticket survives trips into the hall during the current launch. +On a normal application close its already-generated outcome is automatically +settled and the balance saved. It is not resumed as a partially scratched +ticket after relaunch. Forced termination can bypass normal shutdown handling, +as with other in-progress games; no new per-profile save format is introduced. + +## Verification + +`scratch_syntax.asm` checks the isolated module before the full build. +`test_scratch_module.ps1` is a fast native iteration harness: it links fresh +scratch MASM against a test-only copy of completed native helper code. Only +COFF symbol visibility changes in the copy; instructions and relocations are +not modified. It is not a substitute for the full action/shipping builds. +The native action suite checks capture/release/focus/negative input, viewport +mapping, hover versus held input, fast connected strokes, disconnected strokes, +coverage, hall-return preservation, all price tiers, 500 immutable ticket +layouts, another 200 layouts for each of the four kinds, rule-derived prizes, +type locks, circular clipping, full physical uncovering of every layout, +single settlement and the balance cap. An additional 500 generated boards per +portrait kind are checked against independently derived number/bonus payouts; +fixed bonus fixtures cover x2/x10/all-prizes. Tests also cover all 60 physical +fields, both scales, catalogue clicks after pointer movement, price selection, +paid-type locks, modal Escape and positive/zero/negative net results. +Presentation tests additionally compare 186 native captures at equal timestamps +with 10/15/30 ms updates, exercise blocked scratching during delivery, compare +25 payout frames, assert unchanged bankroll/RNG under cosmetic updates, check +single-shot sound triggers/cooldowns and expire all 64 particle slots. +The suite writes ready, covered, +partly scratched, completed, missing-art and per-kind frames. + +Generated asset prompts: `assets/sprites/scratch/prompts-v1.json` and +`assets/sprites/scratch/prompts-v2.json`. Portrait provenance and recovered edit +briefs are in `assets/sprites/scratch/prompts-v3.json` (briefs are not verbatim +tool prompts). Native stroke sequences and the hall +counter can be captured with `tools/tests/test_scratch_probe.ps1`; its 114 native +frames include 26 scratch frames per portrait kind and view-switch coverage +preservation assertions. The probe +links the tested game's rendering procedures, not a Python recreation. + +Mechanic references, not source-code or art dependencies: +[Scratch the Ticket](https://store.steampowered.com/app/4626940/Scratch_the_Ticket/) +and [Scritchy Scratchy](https://store.steampowered.com/app/3948120/Scritchy_Scratchy/?l=russian). +Number-match inspiration: the official National Lottery +[Double Match procedures](https://www.national-lottery.co.uk/c/files/scratchcards/doublematch.pdf~3). +Our outcomes and prices are independent; the three portrait designs originate +from images supplied by the user, not scraped from lottery websites. diff --git a/docs/WORLD_DRESSING.md b/docs/WORLD_DRESSING.md new file mode 100644 index 0000000..7aca742 --- /dev/null +++ b/docs/WORLD_DRESSING.md @@ -0,0 +1,56 @@ +# Hall and Street Dressing + +The game still runs entirely in MASM32 assembly. The Python packer and capture +checker are offline developer tools, not another game implementation. + +## Hall + +- Standing eye height is 1.05 world units. Basement eye height remains 0.85, + including its stair elevation. Gameplay, table sizes and NPC routes are unchanged. +- Six framed paintings and eight sconces are actual depth-tested wall planes. + They stay attached to the architecture as the camera turns and do not occupy + the walking area. They do not use camera-facing furniture billboards. +- A static 256x256 light map is rebuilt when the hall map is initialized. It + contains the existing chandeliers, lounge lamps, bar lights and new wall lights. + Radial falloff is quadratic, and solid map tiles interrupt the light rays. +- A separate contact map darkens the carpet underneath lounge furniture. This + is not a full dynamic shadow renderer for every NPC and table. +- The lighting pass runs after the world but before the HUD. It computes a gain + per 2x2 area and shades each original pixel separately, retaining texture detail. + It is skipped for the basement, outside scenes and when explicitly disabled. + +## Street + +- Walking eye height is 1.15 plus the six-centimetre pavement elevation. Seated + eye height and the ignition/waking animation remain unchanged; the rising + phase interpolates to the new standing height. +- Building storeys are 1.8 units instead of 1.2. Their bases stay on the ground. +- Hydrants, mailboxes, newspaper boxes, bollards and parking meters have individual + dimensions, smaller than a standing person. Fence height is reduced as well. + Bushes and the discarded billboard benches remain disabled. +- Low curbs separate roadway and pavement. Paving is drawn in individual metre + sections, not one stretched street-length texture. Raised sidewalk props share + the curb's floor anchor. The anchor is recalculated after each pedestrian/car + update, so newly positioned walkers do not keep their old ground height. + Props beyond the raised section and road vehicles stay on the lower ground. +- Three iron drains sit beside the curb, outside the walking strip. Existing + window/lamppost light maps also shade the new paving. + +## Artwork and Verification + +`assets/sprites/hall/wall-paintings-v6.png` was produced with the built-in image +generator. Its prompt is recorded in `prompts-v6.json`. The offline packer adds +a small procedural iron material and writes `world-dressing-v6.bin` (three +128x128 BGRA frames). Loading validates dimensions; missing art is safely skipped. + +Run `tools/tests/test_casino_actions.ps1 -SkipPreview` for native checks of +surface loading, light occlusion, repeatable baking, floor anchors, prop scale, +unchanged gameplay RNG/bank/map, existing interaction paths and framebuffer captures. +Then `tools/tests/check_world_dressing.py --parity` compares the tested and shipping +procedures and validates the native captures under `build/actions/world-*.bmp`. + +`test_world_dressing_probe.ps1` is a quick development preview against a saved +prior-release object. It only changes COFF symbol visibility in a test copy and +links current dressing/surface routines without patching instructions. It is +not evidence of shipping integration; the complete native action suite and +shipping parity check are required before publication. diff --git a/poker.exe b/poker.exe index b5a8caf..874292b 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index 37d3a90..6cfb587 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/poker.wap b/poker.wap index 0ba2817..581916e 100644 --- a/poker.wap +++ b/poker.wap @@ -18,4 +18,4 @@ DebOut= [FILES] 1=poker.asm [SHOWFILE] -1=0000000000000000140600007403000000000000030000004836000012 +1=000000000000000014060000740300000000000003000000F201000087 diff --git a/runtime/casino3d-progress.dat b/runtime/casino3d-progress.dat index 8be1bee..4945fae 100644 Binary files a/runtime/casino3d-progress.dat and b/runtime/casino3d-progress.dat differ diff --git a/src_graphics/billiards.inc b/src_graphics/billiards.inc index 36e1c5f..889602c 100644 --- a/src_graphics/billiards.inc +++ b/src_graphics/billiards.inc @@ -8,7 +8,42 @@ PoolSqrt3D PROC squared:DWORD ret PoolSqrt3D ENDP -InitPool3D PROC uses ebx esi edi +InitPool3D PROC + call ResetPool3D + mov poolState3D,POOL_SELECT + ret +InitPool3D ENDP + +StartBilliards3D PROC variant:DWORD + cmp variant,1 + ja @F + mov eax,variant + mov poolVariant3D,eax + call ResetPool3D + call ClearKeys3D +@@: + ret +StartBilliards3D ENDP + +ResetPool3D PROC uses ebx esi edi + mov poolTraceValid3D,0 + mov poolCapture2Q8,480*480 + mov poolMiddleLeft3D,157*FP_1 + mov poolMiddleRight3D,163*FP_1 + lea esi,poolPyramidJawX3D + lea edi,poolJawX3D + mov ecx,24 + rep movsd + cmp poolVariant3D,0 + je @F + mov poolCapture2Q8,1024*1024 + mov poolMiddleLeft3D,153*FP_1 + mov poolMiddleRight3D,167*FP_1 + lea esi,poolEightJawX3D + lea edi,poolJawX3D + mov ecx,24 + rep movsd +@@: lea edi, poolVX3D xor eax, eax mov ecx, 32 @@ -59,9 +94,10 @@ pool_rack_ball: mov poolStill3D, 1 mov poolCalled3D, 1 mov poolCalledPocket3D, 0 + mov poolCallManual3D,0 call InitPyramidState3D ret -InitPool3D ENDP +ResetPool3D ENDP ; Symmetric equal-mass impulse plus separation; opening contacts never gain energy. PoolCollide3D PROC uses ebx esi edi a:DWORD,b:DWORD @@ -182,24 +218,24 @@ PoolPocketBall3D PROC uses ebx esi ball:DWORD pool_pocket_test: mov eax, poolX3D[esi*4] sub eax, poolPocketX3D[ebx*4] - cmp eax,2*FP_1 + cmp eax,5*FP_1 jg pool_pocket_next - cmp eax,-2*FP_1 + cmp eax,-5*FP_1 jl pool_pocket_next sar eax, 8 imul eax, eax mov ecx, eax mov eax, poolY3D[esi*4] sub eax, poolPocketY3D[ebx*4] - cmp eax,2*FP_1 + cmp eax,5*FP_1 jg pool_pocket_next - cmp eax,-2*FP_1 + cmp eax,-5*FP_1 jl pool_pocket_next sar eax, 8 imul eax, eax add eax, ecx ; Capture is deep inside the narrow mouth; jaws are separate colliders. - cmp eax, 480*480 + cmp eax, poolCapture2Q8 ja pool_pocket_next pool_pocket_capture: mov poolActive3D[esi*4], 0 @@ -398,9 +434,11 @@ pool_rail_y: jle pool_rail_throat_y cmp poolX3D[esi*4], 283*FP_1 jge pool_rail_throat_y - cmp poolX3D[esi*4], 157*FP_1 + mov eax,poolMiddleLeft3D + cmp poolX3D[esi*4], eax jl pool_rail_test_y - cmp poolX3D[esi*4], 163*FP_1 + mov eax,poolMiddleRight3D + cmp poolX3D[esi*4], eax jg pool_rail_test_y pool_rail_throat_y: mov ebx, 51*FP_1 @@ -461,6 +499,12 @@ pool_friction_ball: test eax,eax jns @F neg eax +@@: + add pyrRoll3D[esi*4],eax + mov eax,poolVY3D[esi*4] + test eax,eax + jns @F + neg eax @@: add pyrRoll3D[esi*4],eax mov eax, poolVX3D[esi*4] @@ -504,5 +548,7 @@ pool_friction_next: PoolStep3D ENDP include src_graphics\pyramid_rules.inc +include src_graphics\eightball.inc include src_graphics\pyramid_play.inc +include src_graphics\billiards_prediction.inc include src_graphics\pyramid_render.inc diff --git a/src_graphics/billiards_data.inc b/src_graphics/billiards_data.inc index fc8a04d..619f56e 100644 --- a/src_graphics/billiards_data.inc +++ b/src_graphics/billiards_data.inc @@ -8,7 +8,18 @@ POOL_RADIUS equ FP_1*5/2 POOL_DIAMETER equ 5*FP_1 POOL_PENALTY equ 5 POOL_CHOICE equ 6 +POOL_SELECT equ 7 InitPool3D PROTO +ResetPool3D PROTO +StartBilliards3D PROTO :DWORD +EightLegal3D PROTO :DWORD +EightCount3D PROTO +EightFinish3D PROTO +EightHand3D PROTO +EightAutoCall3D PROTO +EightSyncGroups3D PROTO +PoolPredict3D PROTO +PoolDrawPrediction3D PROTO HandlePool3D PROTO RenderPool3D PROTO PoolStep3D PROTO @@ -44,6 +55,9 @@ poolPotted3D dd 0 poolBreak3D dd 1 poolCalled3D dd 1 poolCalledPocket3D dd 0 +poolCallManual3D dd 0 +poolUnsetText3D db "-",0 +ALIGN 4 poolAtStart3D dd 0 poolFoul3D dd 0 poolAngle3D dd 0 @@ -52,6 +66,38 @@ poolCharge3D dd 0 poolAccumulator3D dd 0 poolTime3D dd 0 poolStill3D dd 0 +poolVariant3D dd 0 +poolCapture2Q8 dd 480*480 +poolMiddleLeft3D dd 157*FP_1 +poolMiddleRight3D dd 163*FP_1 +poolEightJawX3D dd 37*FP_1,30*FP_1,153*FP_1,167*FP_1,290*FP_1,283*FP_1 + dd 37*FP_1,30*FP_1,153*FP_1,167*FP_1,290*FP_1,283*FP_1 +poolEightJawY3D dd 50*FP_1,57*FP_1,50*FP_1,50*FP_1,57*FP_1,50*FP_1 + dd 180*FP_1,173*FP_1,180*FP_1,180*FP_1,173*FP_1,180*FP_1 +poolPyramidJawX3D dd FP_1*137/4,30*FP_1,157*FP_1,163*FP_1,290*FP_1,FP_1*1143/4 + dd FP_1*137/4,30*FP_1,157*FP_1,163*FP_1,290*FP_1,FP_1*1143/4 +poolPyramidJawY3D dd 50*FP_1,FP_1*217/4,50*FP_1,50*FP_1,FP_1*217/4,50*FP_1 + dd 180*FP_1,FP_1*703/4,180*FP_1,180*FP_1,FP_1*703/4,180*FP_1 +poolEightPack3D AnimPack3D <> +poolEightPath3D db "assets\animations\pyramid\pool-balls-v1.bin",0 +poolSelectTitle3D db 193,200,203,220,223,208,196,0 +poolSelectPyramid3D db 49,32,207,200,208,192,204,200,196,192,0 +poolSelectEight3D db 50,32,207,211,203,32,194,206,209,220,204,197,208,202,192,0 +poolCallText3D db 199,192,202,192,199,0 +poolPredictionText3D db 207,208,206,195,205,206,199,0 +poolTraceValid3D dd 0 +poolTraceKey3D dd 0 +poolTraceCount3D dd 0 +poolTraceSteps3D dd 0 +poolTracePotted3D dd 0 +poolTraceFirst3D dd -1 +poolTraceX3D dd 8192 dup(0) +poolTraceY3D dd 8192 dup(0) +poolTraceActive3D dd 512 dup(0) +poolTraceFinal3D dd 112 dup(0) +poolPhysicsBackup3D dd 112 dup(0) +poolHistoryBackup3D dd 80 dup(0) +poolScalarBackup3D dd 7 dup(0) poolTitle3D db 207,211,203,32,47,32,56,0 poolPlayerText3D db 200,195,208,206,202,0 poolDealerText3D db 196,200,203,197,208,0 diff --git a/src_graphics/billiards_prediction.inc b/src_graphics/billiards_prediction.inc new file mode 100644 index 0000000..ac9051d --- /dev/null +++ b/src_graphics/billiards_prediction.inc @@ -0,0 +1,239 @@ +; Prediction runs the shipping fixed-step integrator, without settlement or IO. +; Every field touched by PoolStep3D is saved/restored before returning. +PoolPredict3D PROC uses ebx esi edi + local steps:DWORD,hash:DWORD + cmp poolState3D,POOL_AIM + jne predict_done + call PyramidDirection3D + mov eax,2166136261 + lea esi,poolX3D + mov ecx,112 +predict_hash: + xor eax,[esi] + imul eax,16777619 + add esi,4 + loop predict_hash + xor eax,pyrCue3D + imul eax,16777619 + xor eax,poolPower3D + imul eax,16777619 + xor eax,pyrDirX3D + imul eax,16777619 + xor eax,pyrDirY3D + xor eax,poolVariant3D + mov hash,eax + cmp poolTraceValid3D,0 + je predict_refresh + cmp eax,poolTraceKey3D + je predict_done +predict_refresh: + lea esi,poolX3D + lea edi,poolPhysicsBackup3D + mov ecx,112 + rep movsd + lea esi,pyrRoll3D + lea edi,poolHistoryBackup3D + mov ecx,80 + rep movsd + mov eax,poolFirst3D + mov poolScalarBackup3D,eax + mov eax,poolRail3D + mov poolScalarBackup3D[4],eax + mov eax,poolBreakRails3D + mov poolScalarBackup3D[8],eax + mov eax,poolPotted3D + mov poolScalarBackup3D[12],eax + mov eax,poolStill3D + mov poolScalarBackup3D[16],eax + mov eax,pyrValidEnd3D + mov poolScalarBackup3D[20],eax + mov eax,pyrRailSide3D + mov poolScalarBackup3D[24],eax + lea edi,poolVX3D + xor eax,eax + mov ecx,32 + rep stosd + lea edi,pyrHistoryRails3D + mov ecx,48 + rep stosd + mov poolFirst3D,-1 + mov poolRail3D,0 + mov poolBreakRails3D,0 + mov poolPotted3D,0 + mov pyrValidEnd3D,0 + mov ebx,pyrCue3D + mov eax,pyrDirX3D + imul poolPower3D + shrd eax,edx,16 + mov poolVX3D[ebx*4],eax + mov eax,pyrDirY3D + imul poolPower3D + shrd eax,edx,16 + mov poolVY3D[ebx*4],eax + mov poolTraceCount3D,0 + mov steps,0 +predict_sample: + mov edi,poolTraceCount3D + shl edi,6 + xor esi,esi + xor ebx,ebx +predict_sample_ball: + mov eax,poolX3D[esi*4] + mov poolTraceX3D[edi],eax + mov eax,poolY3D[esi*4] + mov poolTraceY3D[edi],eax + cmp poolActive3D[esi*4],0 + je @F + bts ebx,esi +@@: + add edi,4 + inc esi + cmp esi,16 + jb predict_sample_ball + mov ecx,poolTraceCount3D + mov poolTraceActive3D[ecx*4],ebx + inc poolTraceCount3D + cmp steps,0 + je predict_step + cmp poolStill3D,1 + je predict_restore + cmp steps,6000 + jae predict_restore +predict_step: + call PoolStep3D + inc steps + cmp poolStill3D,1 + je predict_sample + test steps,15 + jz predict_sample + jmp predict_step +predict_restore: + mov eax,steps + mov poolTraceSteps3D,eax + mov eax,poolFirst3D + mov poolTraceFirst3D,eax + mov pyrTarget3D,eax + mov eax,poolPotted3D + mov poolTracePotted3D,eax + lea esi,poolX3D + lea edi,poolTraceFinal3D + mov ecx,112 + rep movsd + lea esi,poolPhysicsBackup3D + lea edi,poolX3D + mov ecx,112 + rep movsd + lea esi,poolHistoryBackup3D + lea edi,pyrRoll3D + mov ecx,80 + rep movsd + mov eax,poolScalarBackup3D + mov poolFirst3D,eax + mov eax,poolScalarBackup3D[4] + mov poolRail3D,eax + mov eax,poolScalarBackup3D[8] + mov poolBreakRails3D,eax + mov eax,poolScalarBackup3D[12] + mov poolPotted3D,eax + mov eax,poolScalarBackup3D[16] + mov poolStill3D,eax + mov eax,poolScalarBackup3D[20] + mov pyrValidEnd3D,eax + mov eax,poolScalarBackup3D[24] + mov pyrRailSide3D,eax + mov eax,hash + mov poolTraceKey3D,eax + mov poolTraceValid3D,1 +predict_done: + ret +PoolPredict3D ENDP + +PoolTraceLine3D PROC uses ebx esi edi x1:DWORD,y1:DWORD,x2:DWORD,y2:DWORD,tint:DWORD + local deltaX:DWORD,deltaY:DWORD,extent:DWORD + mov eax,x2 + sub eax,x1 + mov deltaX,eax + test eax,eax + jns @F + neg eax +@@: + mov extent,eax + mov eax,y2 + sub eax,y1 + mov deltaY,eax + test eax,eax + jns @F + neg eax +@@: + cmp eax,extent + jbe @F + mov extent,eax +@@: + cmp extent,0 + je trace_line_done + xor esi,esi +trace_line_pixel: + mov eax,deltaX + imul esi + idiv extent + add eax,x1 + mov ebx,eax + mov eax,deltaY + imul esi + idiv extent + add eax,y1 + invoke DrawPixel,ebx,eax,tint + add esi,2 + cmp esi,extent + jbe trace_line_pixel +trace_line_done: + ret +PoolTraceLine3D ENDP + +PoolDrawPrediction3D PROC uses ebx esi edi + local sx:DWORD,sy:DWORD,ex:DWORD,ey:DWORD,tint:DWORD + call PoolPredict3D + xor ebx,ebx +trace_draw_ball: + cmp ebx,pyrCue3D + je trace_draw_selected + cmp ebx,poolTraceFirst3D + jne trace_draw_next +trace_draw_selected: + mov tint,008DB5A9h + cmp ebx,pyrCue3D + jne @F + mov tint,00E8EBD5h +@@: + mov esi,1 +trace_draw_segment: + cmp esi,poolTraceCount3D + jae trace_draw_next + mov edi,esi + dec edi + mov eax,poolTraceActive3D[edi*4] + bt eax,ebx + jnc trace_draw_next + shl edi,4 + add edi,ebx + mov eax,poolTraceX3D[edi*4] + sar eax,16 + mov sx,eax + mov eax,poolTraceY3D[edi*4] + sar eax,16 + mov sy,eax + mov eax,poolTraceX3D[edi*4+64] + sar eax,16 + mov ex,eax + mov eax,poolTraceY3D[edi*4+64] + sar eax,16 + mov ey,eax + invoke PoolTraceLine3D,sx,sy,ex,ey,tint + inc esi + jmp trace_draw_segment +trace_draw_next: + inc ebx + cmp ebx,16 + jb trace_draw_ball + ret +PoolDrawPrediction3D ENDP diff --git a/src_graphics/city_alley.inc b/src_graphics/city_alley.inc new file mode 100644 index 0000000..6c16b9b --- /dev/null +++ b/src_graphics/city_alley.inc @@ -0,0 +1,26 @@ +; Keep the cinematic's central walking strip clear; all fittings touch a wall +; or the floor and use the same per-pixel depth buffer as the dumpster geometry. +RenderCityAlley3D PROC + cmp cityDecorPack3D.pixels,0 + je city_alley_done + ; East wall: louvres, layered posters and a shallow junction cabinet. + invoke DrawCityVertical3D,0,FP_1*997/100,FP_1*111/5,FP_1*233/10,FP_1*5/4,FP_1*17/10,111 + invoke DrawCityVertical3D,0,FP_1*997/100,FP_1*119/5,FP_1*124/5,FP_1*11/20,FP_1*27/20,112 + invoke DrawCityBoxSides3D,FP_1*49/5,FP_1*199/20,FP_1*251/10,FP_1*128/5,FP_1*3/5,FP_1*7/5,114 + invoke DrawCityHorizontal3D,FP_1*49/5,FP_1*199/20,FP_1*251/10,FP_1*128/5,FP_1*7/5,110 + ; West wall: service hatch and poster patch, mounted rather than billboards. + invoke DrawCityVertical3D,0,FP_1*803/100,FP_1*183/10,FP_1*381/20,0,FP_1*17/10,114 + invoke DrawCityVertical3D,0,FP_1*803/100,FP_1*117/5,FP_1*121/5,FP_1*4/5,FP_1*17/10,112 + ; Drain pipes rise along masonry, with a short overhead connection. + invoke DrawCityBoxSides3D,FP_1*803/100,FP_1*811/100,FP_1*207/10,FP_1*104/5,0,FP_1*9/2,110 + invoke DrawCityBoxSides3D,FP_1*989/100,FP_1*997/100,FP_1*259/10,26*FP_1,0,FP_1*11/2,110 + invoke DrawCityBoxSides3D,8*FP_1,10*FP_1,FP_1*221/10,FP_1*222/10,FP_1*19/5,FP_1*39/10,110 + ; Stacked crates and a low pallet stay against the walls, never in our path. + invoke DrawCityBenchBlock3D,FP_1*803/100,FP_1*207/25,FP_1*201/10,FP_1*104/5,0,FP_HALF,113 + invoke DrawCityBenchBlock3D,FP_1*804/100,FP_1*827/100,FP_1*203/10,FP_1*207/10,FP_HALF,FP_1*39/50,113 + invoke DrawCityBenchBlock3D,FP_1*39/4,FP_1*497/50,FP_1*93/5,FP_1*191/10,0,FP_1*9/20,113 + invoke DrawCityBenchBlock3D,FP_1*803/100,FP_1*413/50,FP_1*223/10,FP_1*231/10,0,FP_1*3/25,113 + invoke DrawCityHorizontal3D,FP_1*167/20,FP_1*91/10,FP_1*213/10,FP_1*431/20,FP_1/200,111 +city_alley_done: + ret +RenderCityAlley3D ENDP diff --git a/src_graphics/city_arrival_data.inc b/src_graphics/city_arrival_data.inc index c0c2a59..1f4b822 100644 --- a/src_graphics/city_arrival_data.inc +++ b/src_graphics/city_arrival_data.inc @@ -1,19 +1,40 @@ CITY_BLACK equ 0 CITY_WAKE equ 1 -CITY_SMOKE equ 2 -CITY_TOSS equ 3 -CITY_TURN_ALLEY equ 4 -CITY_ALLEY equ 5 -CITY_TURN_STREET equ 6 -CITY_STREET equ 7 -CITY_TURN_DOOR equ 8 -CITY_DOOR equ 9 +CITY_LIGHT equ 2 +CITY_SMOKE equ 3 +CITY_STAND equ 4 +CITY_LOOK_BIN equ 5 +CITY_TOSS equ 6 +CITY_TURN_ALLEY equ 7 +CITY_ALLEY equ 8 +CITY_TURN_STREET equ 9 +CITY_STREET equ 10 +CITY_TURN_DOOR equ 11 +CITY_DOOR equ 12 +CITY_BLACK_MS equ 700 +CITY_WAKE_MS equ 4500 +CITY_LIGHT_MS equ 2300 +CITY_SMOKE_MS equ 3600 +CITY_STAND_MS equ 1800 +CITY_LOOK_BIN_MS equ 1100 +CITY_TOSS_MS equ 1500 +CITY_TURN_ALLEY_MS equ 1200 +CITY_PRE_ALLEY_MS equ CITY_BLACK_MS+CITY_WAKE_MS+CITY_LIGHT_MS+CITY_SMOKE_MS+CITY_STAND_MS+CITY_LOOK_BIN_MS+CITY_TOSS_MS+CITY_TURN_ALLEY_MS +CITY_TRAFFIC_OFFSET_MS equ CITY_PRE_ALLEY_MS-8900 CITY_BUILDING_STOREYS equ 7 -CITY_STOREY_HEIGHT equ FP_1*6/5 +CITY_STOREY_HEIGHT equ FP_1*9/5 +HALL_STANDING_EYE equ FP_1*21/20 +CITY_STANDING_EYE equ FP_1*23/20 LoadCityArrival3D PROTO RenderCityArrival3D PROTO HandleCityApproach3D PROTO PositionCityCamera3D PROTO +DrawCityWakeActor3D PROTO +DrawCitySeatedLegs3D PROTO +DrawCityLighter3D PROTO +DrawCityWakeSubtitle3D PROTO +BlendCityWeatherPixel3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +BlendCitySky3D PROTO :DWORD FadeCityFrame3D PROTO :DWORD CITY_LIGHT_W equ 512 CITY_LIGHT_H equ 256 @@ -30,12 +51,12 @@ floorTexture3D dd 3 outdoorRender3D dd 0 cityLighting3D dd 1 cityLightReady3D dd 0 -viewEye3D dd FP_1*17/20 +viewEye3D dd HALL_STANDING_EYE viewCeiling3D dd FP_1*5/2 cityPhase3D dd CITY_BLACK cityTime3D dd 0 cityClock3D dd 0 -cityDurations3D dd 700,3100,2400,1500,1200,5400,900,7500,900,2800 +cityDurations3D dd CITY_BLACK_MS,CITY_WAKE_MS,CITY_LIGHT_MS,CITY_SMOKE_MS,CITY_STAND_MS,CITY_LOOK_BIN_MS,CITY_TOSS_MS,CITY_TURN_ALLEY_MS,5400,900,7500,900,2800 cityTilesPack3D AnimPack3D <> cityHandsPack3D AnimPack3D <> cityPropsPack3D AnimPack3D <> @@ -43,6 +64,19 @@ citySmokePack3D AnimPack3D <> cityCarsPack3D AnimPack3D <> cityBinPack3D AnimPack3D <> cityCloudPack3D AnimPack3D <> +cityWakePack3D AnimPack3D <> +cityLighterPack3D AnimPack3D <> +cityKneesPack3D AnimPack3D <> +citySleevePack3D AnimPack3D <> +cityDecorPack3D AnimPack3D <> +cityWakePath3D db "assets\animations\arrival\wake-rocker-v2.bin",0 +cityLighterPath3D db "assets\animations\arrival\wake-lighter-v1.bin",0 +cityKneesPath3D db "assets\animations\arrival\wake-knees-v1.bin",0 +citySleevePath3D db "assets\animations\arrival\wake-sleeve-v1.bin",0 +cityDecorPath3D db "assets\animations\arrival\alley-materials-v1.bin",0 +include src_graphics\city_lighter_anchors.inc +cityWakeLine1 db 194,241,242,224,226,224,233,44,32,241,224,236,243,240,224,233,44,0 +cityWakeLine2 db 239,238,240,224,32,228,253,239,237,243,242,252,32,226,241,229,32,226,32,234,224,231,232,237,238,0 cityTilesPath3D db "assets\animations\arrival\city-tiles-v4.bin",0 cityHandsPath3D db "assets\animations\arrival\city-hands.bin",0 cityPropsPath3D db "assets\animations\arrival\city-props.bin",0 @@ -69,6 +103,7 @@ CityProp3D STRUCT asset dd ? groundZ dd 0 CityProp3D ENDS +cityWakeActor3D CityProp3D CITY_PROP_COUNT equ 47 CITY_FIRST_CAR equ 9 CITY_TRAFFIC_EAST equ 14 @@ -165,6 +200,7 @@ cityObjectDepth3D dd CITY_PROP_COUNT dup(0) .data? cityLightMap3D dd CITY_LIGHT_W*CITY_LIGHT_H dup(?) cityDepthBuffer3D dd SCREEN_SIZE dup(?) +citySkyBuffer3D dd SCREEN_SIZE dup(?) IFDEF CASINO_ACTION_TEST cityPixelOwner3D dd SCREEN_SIZE dup(?) ENDIF diff --git a/src_graphics/city_arrival_scene.inc b/src_graphics/city_arrival_scene.inc index 94a4a0f..9345e2a 100644 --- a/src_graphics/city_arrival_scene.inc +++ b/src_graphics/city_arrival_scene.inc @@ -3,6 +3,7 @@ include src_graphics\city_traffic.inc include src_graphics\city_objects.inc include src_graphics\city_bins.inc include src_graphics\city_furniture.inc +include src_graphics\city_alley.inc include src_graphics\city_skyline.inc include src_graphics\city_lighting.inc ValidateCityPack3D PROC uses esi pack:DWORD,widthPx:DWORD,heightPx:DWORD,count:DWORD @@ -30,6 +31,16 @@ LoadCityArrival3D PROC uses esi edi invoke LoadAnimPack3D, OFFSET cityCarsPack3D, OFFSET cityCarsPath3D invoke LoadAnimPack3D, OFFSET cityBinPack3D, OFFSET cityBinPath3D invoke LoadAnimPack3D, OFFSET cityCloudPack3D, OFFSET cityCloudPath3D + invoke LoadAnimPack3D, OFFSET cityWakePack3D, OFFSET cityWakePath3D + invoke LoadAnimPack3D, OFFSET cityLighterPack3D, OFFSET cityLighterPath3D + invoke LoadAnimPack3D, OFFSET cityKneesPack3D, OFFSET cityKneesPath3D + invoke LoadAnimPack3D, OFFSET citySleevePack3D, OFFSET citySleevePath3D + invoke LoadAnimPack3D, OFFSET cityDecorPack3D, OFFSET cityDecorPath3D + invoke ValidateCityPack3D, OFFSET cityWakePack3D, 192,320,32 + invoke ValidateCityPack3D, OFFSET cityLighterPack3D, 256,256,16 + invoke ValidateCityPack3D, OFFSET cityKneesPack3D, 640,240,1 + invoke ValidateCityPack3D, OFFSET citySleevePack3D, 256,384,1 + invoke ValidateCityPack3D, OFFSET cityDecorPack3D, 128,128,4 invoke ValidateCityPack3D, OFFSET cityHandsPack3D, 256, 192, 16 invoke ValidateCityPack3D, OFFSET cityPropsPack3D, 256, 256, 6 invoke ValidateCityPack3D, OFFSET citySmokePack3D, 96, 128, 4 @@ -112,23 +123,62 @@ CityEase3D ENDP PositionCityCamera3D PROC uses ebx mov playerX, (8*FP_1+FP_HALF) mov playerY, (24*FP_1+FP_HALF) - mov playerAngle, 128 + mov playerAngle, 0 mov viewHorizon3D, 240 + mov viewEye3D,CITY_STANDING_EYE cmp arrivalState3D, ARRIVAL_WALK jne city_camera_at_door mov ebx, cityTime3D + cmp cityPhase3D,CITY_STAND + ja city_camera_turn_alley + mov viewEye3D,FP_1*2/5 cmp cityPhase3D, CITY_WAKE - jne city_camera_turn_alley - mov viewHorizon3D, 1600 - cmp ebx, 1800 + jne city_camera_light + cmp ebx,2800 jb city_camera_done - sub ebx, 1800 - invoke CityEase3D, ebx, 1300 - imul eax, 1360 + sub ebx,2800 + invoke CityEase3D,ebx,1700 + imul eax,80 shr eax, 16 - sub viewHorizon3D, eax + add viewHorizon3D,eax + jmp city_camera_done +city_camera_light: + cmp cityPhase3D,CITY_LIGHT + jne city_camera_smoke + mov viewHorizon3D,320 + jmp city_camera_done +city_camera_smoke: + cmp cityPhase3D,CITY_SMOKE + jne city_camera_stand + mov viewHorizon3D,320 + invoke CityEase3D,ebx,1200 + imul eax,1280 + shr eax,16 + add viewHorizon3D,eax + jmp city_camera_done +city_camera_stand: + cmp cityPhase3D,CITY_STAND + jne city_camera_done + invoke CityEase3D,ebx,1800 + mov ebx,eax + imul eax,CITY_STANDING_EYE-FP_1*2/5 + shr eax,16 + add viewEye3D,eax + mov eax,ebx + imul eax,1360 + shr eax,16 + mov viewHorizon3D,1600 + sub viewHorizon3D,eax jmp city_camera_done city_camera_turn_alley: + cmp cityPhase3D,CITY_LOOK_BIN + jne city_camera_bin_ready + invoke CityEase3D,ebx,CITY_LOOK_BIN_MS + shr eax,9 + mov playerAngle,eax + jmp city_camera_done +city_camera_bin_ready: + mov playerAngle,128 cmp cityPhase3D, CITY_TURN_ALLEY jb city_camera_done jne city_camera_alley @@ -215,6 +265,12 @@ city_camera_bob: idiv ecx add viewHorizon3D, eax city_camera_done: + cmp playerY,12*FP_1 + jb @F + cmp playerY,13*FP_1 + jae @F + add viewEye3D,FP_1*3/50 +@@: ret PositionCityCamera3D ENDP @@ -262,32 +318,7 @@ city_sky_pixel: ret DrawCitySky3D ENDP -DrawCityRain3D PROC uses ebx esi - xor esi, esi -city_rain_drop: - mov eax, esi - imul eax, 137 - mov ebx, cityClock3D - shr ebx, 4 - sub eax, ebx - and eax, 511 - imul eax, 5 - shr eax, 3 - mov ebx, eax - mov eax, cityClock3D - shr eax, 2 - mov ecx, esi - imul ecx, 83 - add eax, ecx - xor edx, edx - mov ecx, 240 - div ecx - invoke DrawBox, ebx, edx, 1, 4, 00395468h - inc esi - cmp esi, 70 - jb city_rain_drop - ret -DrawCityRain3D ENDP +include src_graphics\city_weather.inc FadeCityFrame3D PROC uses ebx esi amount:DWORD @@ -397,6 +428,11 @@ city_cigarette_ash: cmp u, eax jb city_cigarette_shade mov color, 00EC6325h + cmp cityPhase3D,CITY_LIGHT + jne city_cigarette_draw + cmp cityTime3D,900 + jae city_cigarette_draw + mov color,00DDD9C9h jmp city_cigarette_draw city_cigarette_shade: mov eax, v @@ -474,6 +510,33 @@ DrawCitySmokingHand3D PROC uses ebx esi edi mov pinchX, 500 mov pinchY, 310 mov offsetY, 0 + cmp cityPhase3D,CITY_LIGHT + jne city_hand_sky_lower + invoke CityEase3D,cityTime3D,600 + neg eax + add eax,FP_1 + imul eax,110 + shr eax,16 + mov offsetY,eax + jmp city_hand_toss +city_hand_sky_lower: + cmp cityPhase3D,CITY_SMOKE + jne city_hand_rising + invoke CityEase3D,cityTime3D,900 + imul eax,240 + shr eax,16 + mov offsetY,eax + jmp city_hand_toss +city_hand_rising: + cmp cityPhase3D,CITY_STAND + jne city_hand_toss + invoke CityEase3D,cityTime3D,1800 + neg eax + add eax,FP_1 + imul eax,240 + shr eax,16 + mov offsetY,eax + jmp city_hand_toss cmp cityPhase3D, CITY_WAKE jne city_hand_toss mov eax, 3100 @@ -601,7 +664,13 @@ city_hand_draw_held: imul eax, 55 sar eax, 16 add eax, cityCigaretteY3D + cmp cityPhase3D,CITY_LIGHT + jne city_hand_tip_smoke + cmp cityTime3D,1000 + jb city_hand_unlit +city_hand_tip_smoke: invoke DrawCityCigaretteSmoke3D, ebx, eax +city_hand_unlit: invoke DrawCityCigarette3D, cityCigaretteX3D, cityCigaretteY3D, 320, 4 city_hand_draw_sprite: ; The opaque fingers cover the filter, with the paper extending out of the grip. @@ -682,6 +751,8 @@ DrawCitySmokingHand3D ENDP DrawCityExhaledSmoke3D PROC uses ebx esi edi mov esi, cityTime3D + sub esi,600 + js city_exhale_done cmp esi, 2300 jae city_exhale_done mov eax, esi @@ -732,9 +803,12 @@ city_exhale_done: ret DrawCityExhaledSmoke3D ENDP +include src_graphics\city_wake_scene.inc + RenderCityArrival3D PROC uses ebx esi edi local savedX:DWORD,savedY:DWORD,savedAngle:DWORD,savedMap:DWORD local savedHorizon:DWORD,savedFloor:DWORD,savedOutdoor:DWORD,savedEye:DWORD,bar:DWORD + local skyBlend:DWORD cmp arrivalState3D, ARRIVAL_WALK jne @F cmp cityPhase3D, CITY_BLACK @@ -762,41 +836,79 @@ RenderCityArrival3D PROC uses ebx esi edi mov worldMap3D, OFFSET cityMap3D mov floorTexture3D, 60 mov outdoorRender3D, 1 - mov viewEye3D, FP_1*17/20 call UpdateCamera3D call DrawCitySky3D + mov skyBlend,0 cmp arrivalState3D, ARRIVAL_WALK jne city_render_world - cmp cityPhase3D, CITY_WAKE + cmp cityPhase3D,CITY_SMOKE + jne city_render_stand_sky + mov eax,cityTime3D + sub eax,550 + js city_render_world + invoke CityEase3D,eax,650 + shr eax,8 + mov skyBlend,eax + jmp city_render_capture_sky +city_render_stand_sky: + cmp cityPhase3D,CITY_STAND jne city_render_world - cmp cityTime3D, 1800 - jb city_render_sky_smoke + invoke CityEase3D,cityTime3D,900 + shr eax,8 + neg eax + add eax,256 + mov skyBlend,eax +city_render_capture_sky: + cmp skyBlend,256 + je city_render_sky_smoke + lea esi,screenBuffer + lea edi,citySkyBuffer3D + mov ecx,SCREEN_SIZE + rep movsd city_render_world: call RenderCeilingAndFloor call DrawCitySkyline3D call RenderWalls call PrepareCityDepth3D + call RenderStreetDressing3D call RenderCityObjects3D call RenderCityFurniture3D call RenderCityBins3D + call RenderCityAlley3D call RenderCityFacade3D call RenderCityGuards3D + cmp arrivalState3D,ARRIVAL_WALK + jne @F + call DrawCityWakeActor3D +@@: + cmp skyBlend,0 + je @F + invoke BlendCitySky3D,skyBlend +@@: call DrawCityRain3D cmp arrivalState3D, ARRIVAL_WALK jne city_render_restore cmp cityPhase3D, CITY_TOSS ja city_render_restore + call DrawCitySeatedLegs3D cmp cityPhase3D, CITY_WAKE jne @F - call DrawCityExhaledSmoke3D -@@: - call DrawCitySmokingHand3D - cmp cityPhase3D, CITY_WAKE - jne city_render_restore + call DrawCityWakeSubtitle3D jmp city_wake_eye_mask +@@: + call DrawCityLighter3D + call DrawCitySmokingHand3D + cmp cityPhase3D,CITY_SMOKE + jne city_render_restore + call DrawCityExhaledSmoke3D + jmp city_render_restore city_render_sky_smoke: call DrawCityRain3D + cmp cityPhase3D,CITY_SMOKE + jne city_render_restore + call DrawCitySmokingHand3D call DrawCityExhaledSmoke3D + jmp city_render_restore city_wake_eye_mask: mov eax, cityTime3D cmp eax, 900 diff --git a/src_graphics/city_bins.inc b/src_graphics/city_bins.inc index 85796be..94dde1b 100644 --- a/src_graphics/city_bins.inc +++ b/src_graphics/city_bins.inc @@ -2,6 +2,36 @@ cityRayX3D dd SCREEN_W dup(?) cityRayY3D dd SCREEN_W dup(?) .code +; Exterior-only materials do not consume or overwrite the hall's texture slots. +ResolveCitySurface3D PROC material:DWORD + mov eax,material + cmp eax,115 + jb @F + cmp eax,118 + jae @F + invoke ResolveDressingSurface3D,material + ret +@@: + cmp eax,111 + jb city_surface_shared + cmp eax,115 + jae city_surface_shared + cmp cityDecorPack3D.pixels,0 + je city_surface_missing + sub eax,111 + shl eax,16 + add eax,cityDecorPack3D.pixels + mov ecx,128 + mov edx,128 + ret +city_surface_missing: + xor eax,eax + ret +city_surface_shared: + invoke ResolveWorldSurface3D,material + ret +ResolveCitySurface3D ENDP + PrepareCityDepth3D PROC uses ebx esi edi lea edi, cityDepthBuffer3D mov ebx, SCREEN_H @@ -78,7 +108,7 @@ DrawCityVertical3D PROC uses ebx esi edi axis:DWORD,coordinate:DWORD,loBound:DWO local depth:DWORD,top:DWORD,bottom:DWORD,span:DWORD,column:DWORD local sourceX:DWORD,stepY:DWORD,sourceY:DWORD,destination:DWORD,row:DWORD,texture:DWORD local textureW:DWORD,textureH:DWORD - invoke ResolveWorldSurface3D,material + invoke ResolveCitySurface3D,material test eax, eax jz city_vertical_done mov texture, eax @@ -240,7 +270,7 @@ DrawCityHorizontal3D PROC uses ebx esi edi xMin:DWORD,xMax:DWORD,yMin:DWORD,yMax local left:DWORD,right:DWORD,top:DWORD,bottom:DWORD,corner:DWORD,pointX:DWORD,pointY:DWORD local depth:DWORD,row:DWORD,column:DWORD,stepX:DWORD,stepY:DWORD,scaleX:DWORD,scaleY:DWORD,texture:DWORD local textureW:DWORD,textureH:DWORD,nearClipped:DWORD - invoke ResolveWorldSurface3D,material + invoke ResolveCitySurface3D,material test eax, eax jz city_horizontal_done mov texture, eax @@ -411,6 +441,20 @@ city_horizontal_pixel: add esi, edx mov ebx, texture mov eax, [ebx+esi*4] + cmp outdoorRender3D,0 + je city_horizontal_alpha + cmp material,72 + jne city_horizontal_alpha + mov ecx,depth + shr ecx,14 + neg ecx + add ecx,256 + cmp ecx,55 + jge @F + mov ecx,55 +@@: + invoke ShadeCityGround3D,eax,pointX,pointY,ecx +city_horizontal_alpha: cmp material, 104 jb @F cmp eax, 80000000h diff --git a/src_graphics/city_lighter_anchors.inc b/src_graphics/city_lighter_anchors.inc new file mode 100644 index 0000000..b095da4 --- /dev/null +++ b/src_graphics/city_lighter_anchors.inc @@ -0,0 +1,2 @@ +cityLighterCuffX3D dd 66,64,65,129,66,66,65,65,65,65,65,65,63,63,64,63 +cityLighterCuffY3D dd 232,232,232,224,220,220,222,222,222,222,222,222,219,215,215,215 diff --git a/src_graphics/city_objects.inc b/src_graphics/city_objects.inc index 5bd728d..5c42195 100644 --- a/src_graphics/city_objects.inc +++ b/src_graphics/city_objects.inc @@ -353,6 +353,7 @@ IFDEF CASINO_ACTION_TEST ENDIF call AnimateCityCars3D call AnimateCityPedestrians3D + call UpdateStreetGround3D xor esi, esi city_objects_depth: imul ebx, esi, SIZEOF CityProp3D diff --git a/src_graphics/city_traffic.inc b/src_graphics/city_traffic.inc index f0122f2..5697ed2 100644 --- a/src_graphics/city_traffic.inc +++ b/src_graphics/city_traffic.inc @@ -2,6 +2,11 @@ AnimateCityCars3D PROC uses ebx esi edi local trafficClock:DWORD mov eax, cityClock3D + ; Keep the crossing's safe stopped positions when the wake-up gets longer. + sub eax,CITY_TRAFFIC_OFFSET_MS + jns @F + xor eax,eax +@@: ; A red-light interval lets the protagonist cross from the alley. ; Resume from the stopped position instead of jumping forward in world space. cmp eax,7000 diff --git a/src_graphics/city_wake_scene.inc b/src_graphics/city_wake_scene.inc new file mode 100644 index 0000000..63bcf94 --- /dev/null +++ b/src_graphics/city_wake_scene.inc @@ -0,0 +1,193 @@ +DrawCityWakeActor3D PROC uses ebx + mov cityWakeActor3D.posX,FP_1*97/10 + mov cityWakeActor3D.posY,FP_1*49/2 + cmp cityPhase3D,CITY_WAKE + jne wake_actor_depart + mov eax,cityTime3D + cmp eax,2800 + jae wake_actor_straighten + xor edx,edx + mov ecx,175 + div ecx + and eax,15 + jmp wake_actor_draw +wake_actor_straighten: + sub eax,2800 + imul eax,8 + xor edx,edx + mov ecx,1700 + div ecx + cmp eax,7 + jbe @F + mov eax,7 +@@: + add eax,16 + jmp wake_actor_draw +wake_actor_depart: + cmp cityPhase3D,CITY_LIGHT + jne wake_actor_done + mov eax,cityTime3D + sub eax,350 + jns @F + xor eax,eax +@@: + invoke CityEase3D,eax,1950 + imul eax,3 + sub cityWakeActor3D.posY,eax + mov eax,cityTime3D + xor edx,edx + mov ecx,90 + div ecx + and eax,7 + add eax,24 +wake_actor_draw: + mov cityWakeActor3D.frame,eax + invoke DrawCityObject3D,OFFSET cityWakeActor3D,0 +wake_actor_done: + ret +DrawCityWakeActor3D ENDP + +DrawCitySeatedLegs3D PROC uses ebx + cmp cityPhase3D,CITY_LIGHT + jbe wake_legs_seated + cmp cityPhase3D,CITY_SMOKE + jne wake_legs_done + invoke CityEase3D,cityTime3D,900 + imul eax,300 + shr eax,16 + add eax,240 + mov ebx,eax + jmp wake_legs_draw +wake_legs_seated: + mov ebx,240 +wake_legs_draw: + invoke DrawAnimSprite3D,OFFSET cityKneesPack3D,0,0,ebx,640,240 +wake_legs_done: + ret +DrawCitySeatedLegs3D ENDP + +DrawCityLighter3D PROC uses ebx esi edi + local px:DWORD,py:DWORD,sleeveX:DWORD,sleeveY:DWORD + cmp cityPhase3D,CITY_LIGHT + jne wake_lighter_done + mov eax,cityTime3D + imul eax,16 + xor edx,edx + mov ecx,2300 + div ecx + cmp eax,15 + jbe @F + mov eax,15 +@@: + mov esi,eax + mov cityCigaretteX3D,500 + mov eax,cityClock3D + shr eax,3 + and eax,511 + mov eax,tableCos[eax*4] + sar eax,14 + add eax,310 + mov edi,eax + invoke CityEase3D,cityTime3D,600 + neg eax + add eax,FP_1 + imul eax,110 + shr eax,16 + add eax,edi + mov cityCigaretteY3D,eax + mov eax,cityCigaretteX3D + sub eax,40 + sub eax,110 + mov px,eax + mov eax,cityCigaretteY3D + sub eax,40 + add eax,8 + sub eax,55 + mov py,eax + cmp cityTime3D,1800 + jb @F + mov eax,cityTime3D + sub eax,1800 + invoke CityEase3D,eax,500 + imul eax,220 + shr eax,16 + add py,eax +@@: + ; Measured cuff anchors join the real forearm underneath each animated wrist. + mov eax,cityLighterCuffX3D[esi*4] + imul eax,220 + shr eax,8 + add eax,px + sub eax,110 + mov sleeveX,eax + mov eax,cityLighterCuffY3D[esi*4] + imul eax,220 + shr eax,8 + add eax,py + sub eax,20 + mov sleeveY,eax + invoke DrawAnimSprite3D,OFFSET citySleevePack3D,0,sleeveX,sleeveY,220,330 + invoke DrawAnimSprite3D,OFFSET cityLighterPack3D,esi,px,py,220,220 +wake_lighter_done: + ret +DrawCityLighter3D ENDP + +DrawCityWakeSubtitle3D PROC + cmp cityTime3D,1100 + jb wake_subtitle_done + cmp cityTime3D,4300 + jae wake_subtitle_done + invoke DrawBox,0,205,320,35,00151B1Dh + invoke DrawString8x8,92,211,OFFSET cityWakeLine1,00E6DED2h + invoke DrawString8x8,60,225,OFFSET cityWakeLine2,00E6DED2h +wake_subtitle_done: + ret +DrawCityWakeSubtitle3D ENDP + +; Sky-only raycasting cannot represent a vertical view. Blend the pitched world +; into the already rendered sky instead of popping between the two projections. +BlendCitySky3D PROC uses ebx esi edi amount:DWORD + local inverse:DWORD,red:DWORD,green:DWORD + mov ebx,amount + mov eax,256 + sub eax,ebx + mov inverse,eax + xor esi,esi +wake_sky_blend: + mov eax,screenBuffer[esi*4] + mov edi,citySkyBuffer3D[esi*4] + movzx ecx,al + imul ecx,inverse + mov edx,edi + and edx,255 + imul edx,ebx + add ecx,edx + shr ecx,8 + mov red,ecx + movzx ecx,ah + imul ecx,inverse + mov edx,edi + shr edx,8 + and edx,255 + imul edx,ebx + add ecx,edx + shr ecx,8 + shl ecx,8 + mov green,ecx + shr eax,16 + and eax,255 + imul eax,inverse + shr edi,16 + and edi,255 + imul edi,ebx + add eax,edi + shr eax,8 + shl eax,16 + or eax,green + or eax,red + mov screenBuffer[esi*4],eax + inc esi + cmp esi,SCREEN_SIZE + jb wake_sky_blend + ret +BlendCitySky3D ENDP diff --git a/src_graphics/city_weather.inc b/src_graphics/city_weather.inc new file mode 100644 index 0000000..e6607f0 --- /dev/null +++ b/src_graphics/city_weather.inc @@ -0,0 +1,184 @@ +; All weather uses visual time and stable seeds, never gameplay RNG. +BlendCityWeatherPixel3D PROC uses ebx esi edi px:DWORD,py:DWORD,tint:DWORD,opacity:DWORD + cmp px,640 + jae weather_pixel_done + cmp py,480 + jae weather_pixel_done + mov esi,py + imul esi,640 + add esi,px + mov ebx,screenBuffer[esi*4] + mov edi,256 + sub edi,opacity + movzx eax,bl + imul eax,edi + mov ecx,tint + and ecx,255 + imul ecx,opacity + add eax,ecx + shr eax,8 + mov edx,eax + movzx eax,bh + imul eax,edi + mov ecx,tint + shr ecx,8 + and ecx,255 + imul ecx,opacity + add eax,ecx + shr eax,8 + shl eax,8 + or edx,eax + shr ebx,16 + and ebx,255 + imul ebx,edi + mov eax,tint + shr eax,16 + and eax,255 + imul eax,opacity + add eax,ebx + shr eax,8 + shl eax,16 + or eax,edx + mov screenBuffer[esi*4],eax +weather_pixel_done: + ret +BlendCityWeatherPixel3D ENDP + +DrawCityRain3D PROC uses ebx esi edi + local layer:DWORD,px:DWORD,py:DWORD,lengthPx:DWORD,opacity:DWORD + local radius:DWORD,alpha:DWORD,rowStart:DWORD,age:DWORD,groundDepth:DWORD + mov layer,0 +rain_layer: + mov eax,layer + imul eax,3 + add eax,4 + mov lengthPx,eax + mov eax,layer + imul eax,28 + add eax,50 + mov opacity,eax + xor esi,esi +rain_drop: + mov eax,layer + imul eax,3 + add eax,3 + imul eax,cityClock3D + imul ecx,esi,829 + add eax,ecx + xor edx,edx + mov ecx,7680 + div ecx + shr edx,4 + mov py,edx + imul eax,esi,137 + imul ebx,layer,211 + add eax,ebx + mov ecx,cityClock3D + shr ecx,6 + sub eax,ecx + and eax,1023 + imul eax,640 + shr eax,10 + mov px,eax + xor edi,edi +rain_streak: + mov eax,edi + shr eax,2 + mov ebx,px + sub ebx,eax + mov ecx,py + add ecx,edi + invoke BlendCityWeatherPixel3D,ebx,ecx,00B2C5CDh,opacity + inc edi + cmp edi,lengthPx + jb rain_streak + inc esi + cmp esi,36 + jb rain_drop + inc layer + cmp layer,3 + jb rain_layer + mov eax,viewHorizon3D + add eax,50 + cmp eax,310 + jge @F + mov eax,310 +@@: + cmp eax,450 + jge rain_done + mov rowStart,eax + xor esi,esi +rain_splash: + imul eax,esi,97 + add eax,cityClock3D + xor edx,edx + mov ecx,800 + div ecx + cmp edx,250 + jae rain_splash_next + mov age,edx + imul eax,esi,71 + add eax,43 + xor edx,edx + mov ecx,600 + div ecx + add edx,20 + mov px,edx + imul eax,esi,37 + xor edx,edx + mov ecx,468 + sub ecx,rowStart + div ecx + add edx,rowStart + mov py,edx + sub edx,viewHorizon3D + mov eax,viewEye3D + imul eax,480 + cdq + mov ecx,py + sub ecx,viewHorizon3D + idiv ecx + sub eax,1024 + mov groundDepth,eax + mov ecx,py + imul ecx,640 + add ecx,px + mov eax,groundDepth + cmp cityDepthBuffer3D[ecx*4],eax + jl rain_splash_next + mov eax,age + xor edx,edx + mov ecx,40 + div ecx + inc eax + mov radius,eax + mov eax,250 + sub eax,age + imul eax,100 + xor edx,edx + mov ecx,250 + div ecx + mov alpha,eax + mov ebx,px + sub ebx,radius + invoke BlendCityWeatherPixel3D,ebx,py,00C5C6AEh,alpha + mov ebx,px + add ebx,radius + invoke BlendCityWeatherPixel3D,ebx,py,00C5C6AEh,alpha + mov eax,radius + shr eax,1 + mov edi,py + sub edi,eax + invoke BlendCityWeatherPixel3D,px,edi,00C5C6AEh,alpha + mov eax,radius + shr eax,1 + mov edi,py + add edi,eax + invoke BlendCityWeatherPixel3D,px,edi,00C5C6AEh,alpha +rain_splash_next: + inc esi + cmp esi,24 + jb rain_splash +rain_done: + ret +DrawCityRain3D ENDP diff --git a/src_graphics/dice_anchors.inc b/src_graphics/dice_anchors.inc new file mode 100644 index 0000000..eaff885 --- /dev/null +++ b/src_graphics/dice_anchors.inc @@ -0,0 +1,5 @@ +; Generated by pack_dice_assets.py from the last held pose. +DICE_RELEASE_X1 EQU 213 +DICE_RELEASE_Y1 EQU 146 +DICE_RELEASE_X2 EQU 232 +DICE_RELEASE_Y2 EQU 141 diff --git a/src_graphics/dice_animation.inc b/src_graphics/dice_animation.inc index dc22532..f44e567 100644 --- a/src_graphics/dice_animation.inc +++ b/src_graphics/dice_animation.inc @@ -1,4 +1,5 @@ ; Moving cube geometry shares the existing 320x240 logical game canvas. +include src_graphics/dice_anchors.inc .data cubeCorners3D dd -32768,-32768,-32768, 32768,-32768,-32768 dd 32768,32768,-32768, -32768,32768,-32768 @@ -24,6 +25,7 @@ cubeCosZ3D dd 65536 cubeVertices3D dd 16 dup(0) cubeFaceValues3D dd 6 dup(0) cubeQuad3D dd 8 dup(0) +cubeContactShadow3D dd 0 .code ; Fill clockwise triangles. Edges are incremented per pixel, avoiding divisions. @@ -302,10 +304,10 @@ DrawTumblingDie3D PROC uses ebx esi edi centerX:DWORD,centerY:DWORD,pitch:DWORD, mov edx, tableCos[eax*4] mov cubeCosZ3D, edx mov eax, value - mov cubeFaceValues3D, eax + mov cubeFaceValues3D[16], eax mov edx, 7 sub edx, eax - mov cubeFaceValues3D[4], edx + mov cubeFaceValues3D[20], edx dec eax mov edx, cubeRight3D[eax*4] mov cubeFaceValues3D[12], edx @@ -313,10 +315,10 @@ DrawTumblingDie3D PROC uses ebx esi edi centerX:DWORD,centerY:DWORD,pitch:DWORD, sub ecx, edx mov cubeFaceValues3D[8], ecx mov edx, cubeTop3D[eax*4] - mov cubeFaceValues3D[16], edx + mov cubeFaceValues3D, edx mov ecx, 7 sub ecx, edx - mov cubeFaceValues3D[20], ecx + mov cubeFaceValues3D[4], ecx xor esi, esi cube_project_loop: imul edi, esi, 12 @@ -326,6 +328,25 @@ cube_project_loop: inc esi cmp esi, 8 jb cube_project_loop + ; A resting cube touches the cloth along its projected bottom face. + ; Reverse the underside winding; the normal face pass culls it. + cmp cubeContactShadow3D,0 + je cube_no_contact_shadow + mov eax,cubeVertices3D[28] + inc eax + mov ebx,cubeVertices3D[60] + inc ebx + mov edi,cubeVertices3D[52] + inc edi + invoke DrawTriangle3D,cubeVertices3D[24],eax,cubeVertices3D[56],ebx,cubeVertices3D[48],edi,00153122h + mov eax,cubeVertices3D[28] + inc eax + mov ebx,cubeVertices3D[52] + inc ebx + mov edi,cubeVertices3D[20] + inc edi + invoke DrawTriangle3D,cubeVertices3D[24],eax,cubeVertices3D[48],ebx,cubeVertices3D[16],edi,00153122h +cube_no_contact_shadow: mov face, 0 cube_face_loop: mov esi, face @@ -389,19 +410,48 @@ DrawTumblingDie3D ENDP RenderDiceThrow3D PROC uses ebx esi edi local t:DWORD,ground:DWORD,lift:DWORD,pitch:DWORD,yaw:DWORD,roll:DWORD - local cx1:DWORD,cx2:DWORD,cy:DWORD - mov t, 1600 + local cx1:DWORD,cx2:DWORD,cy:DWORD,cy2:DWORD,oldScale:DWORD,shadowAlpha:DWORD + ; Held dice are painted into the cupped palm: no second pair over skin. + cmp diceArtReady3D,1 + jne throw_begin + cmp dice3d_state,0 + je throw_done + cmp dice3d_state,1 + jne throw_begin + cmp dice3d_timer,240 + jb throw_done +throw_begin: + mov eax,cubeScale3D + mov oldScale,eax + mov t,0 + cmp dice3d_state,2 + jne @F + mov t,1600 +@@: cmp dice3d_state, 1 jne throw_pose mov eax, dice3d_timer + sub eax,240 + jns @F + xor eax,eax +@@: + cmp eax,1360 + jbe @F + mov eax,1360 +@@: + imul eax,1600 + xor edx,edx + mov ecx,1360 + div ecx mov t, eax throw_pose: - mov eax, t - imul eax, 59 + mov cubeScale3D,12 + mov eax,t + imul eax, DICE_RELEASE_Y1-146 xor edx, edx mov ecx, 1600 div ecx - mov ebx, 155 + mov ebx, DICE_RELEASE_Y1 sub ebx, eax mov ground, ebx mov lift, 0 @@ -447,54 +497,88 @@ throw_position: sub eax, lift mov cy, eax mov eax, t - imul eax, 12 + imul eax, DICE_RELEASE_X1-134 xor edx, edx mov ecx, 1600 div ecx - mov ebx, 146 + mov ebx, DICE_RELEASE_X1 sub ebx, eax mov cx1, ebx - add eax, 174 - mov cx2, eax + mov eax,t + imul eax,DICE_RELEASE_X2-186 + xor edx,edx + mov ecx,1600 + div ecx + mov ebx,DICE_RELEASE_X2 + sub ebx,eax + mov cx2,ebx mov eax, 1600 sub eax, t - mov esi, eax - imul eax, 3 - shr eax, 2 - add eax, 40 - mov pitch, eax - mov eax, esi - imul eax, 3 + imul eax,eax + imul eax,1024 xor edx, edx - mov ecx, 5 + mov ecx,2560000 div ecx - add eax, 44 - mov yaw, eax - mov eax, esi - imul eax, esi - shl eax, 1 - xor edx, edx - mov ecx, 1600 - div ecx - mov roll, eax - mov eax, cx1 - sub eax, 16 - mov edx, ground - add edx, 18 - invoke DrawBox, eax, edx, 33, 3, 00082011h - mov eax, cx2 - sub eax, 16 - mov edx, ground - add edx, 18 - invoke DrawBox, eax, edx, 33, 3, 00082011h - invoke DrawTumblingDie3D, cx1, cy, pitch, yaw, roll, dice3d_val1 - mov eax, roll - neg eax - mov edx, cy - cmp dice3d_state, 1 + mov esi,1024 + sub esi,eax + mov roll,esi + mov eax,esi + add eax,70 + mov pitch,eax + mov eax,esi + shr eax,1 + add eax,44 + mov yaw,eax + mov cubeContactShadow3D,0 + cmp t,1600 jne @F - sub edx, 3 + mov cubeContactShadow3D,1 @@: - invoke DrawTumblingDie3D, cx2, edx, pitch, yaw, eax, dice3d_val2 + mov eax,t + imul eax,DICE_RELEASE_Y1-DICE_RELEASE_Y2 + xor edx,edx + mov ecx,1600 + div ecx + add eax,cy + sub eax,DICE_RELEASE_Y1-DICE_RELEASE_Y2 + mov cy2,eax + cmp dice3d_state,0 + je throw_cubes + cmp dice3d_state,1 + jne throw_shadows + cmp dice3d_timer,240 + jb throw_cubes +throw_shadows: + cmp diceShadowPass3D,0 + je throw_cubes + mov eax,85 + sub eax,lift + mov shadowAlpha,eax + mov eax,cx1 + shl eax,1 + mov edx,cubeScale3D + shr edx,2 + add edx,ground + shl edx,1 + invoke DrawCoinShadow3D,eax,edx,cubeScale3D,6,shadowAlpha + mov eax,cx2 + shl eax,1 + mov edx,cubeScale3D + shr edx,2 + add edx,ground + shl edx,1 + invoke DrawCoinShadow3D,eax,edx,cubeScale3D,6,shadowAlpha +throw_cubes: + cmp diceShadowPass3D,0 + jne throw_restore + invoke DrawTumblingDie3D, cx1, cy, pitch, yaw, roll, dice3d_val1 + mov eax,roll + neg eax + invoke DrawTumblingDie3D, cx2, cy2, pitch, yaw, eax, dice3d_val2 +throw_restore: + mov eax,oldScale + mov cubeScale3D,eax + mov cubeContactShadow3D,0 +throw_done: ret RenderDiceThrow3D ENDP diff --git a/src_graphics/eightball.inc b/src_graphics/eightball.inc new file mode 100644 index 0000000..718c538 --- /dev/null +++ b/src_graphics/eightball.inc @@ -0,0 +1,338 @@ +EightCount3D PROC uses ebx esi + mov poolAtStart3D,0 + mov ecx,poolPlayer3D + mov ebx,poolGroups3D[ecx*4] + test ebx,ebx + jz eight_count_done + mov esi,1 +eight_count_loop: + cmp esi,8 + je eight_count_next + mov eax,1 + cmp esi,8 + jb @F + mov eax,2 +@@: + cmp eax,ebx + jne eight_count_next + mov eax,poolActive3D[esi*4] + add poolAtStart3D,eax +eight_count_next: + inc esi + cmp esi,16 + jb eight_count_loop +eight_count_done: + ret +EightCount3D ENDP + +EightLegal3D PROC uses ebx ball:DWORD + xor eax,eax + mov ecx,ball + test ecx,ecx + jle eight_legal_done + cmp ecx,15 + ja eight_legal_done + mov ebx,poolPlayer3D + mov ebx,poolGroups3D[ebx*4] + test ebx,ebx + jz eight_legal_open + cmp poolAtStart3D,0 + jne eight_legal_group + cmp ecx,8 + sete al + ret +eight_legal_group: + cmp ecx,8 + je eight_legal_done + mov eax,1 + cmp ecx,8 + jb @F + mov eax,2 +@@: + cmp eax,ebx + sete al + movzx eax,al + ret +eight_legal_open: + cmp ecx,8 + setne al +eight_legal_done: + ret +EightLegal3D ENDP + +; An obvious shot uses the same predictor as the visible guide. Explicit calls +; lock either ball or pocket independently; cached prediction never settles play. +EightAutoCall3D PROC uses ebx esi edi + cmp poolVariant3D,1 + jne eight_auto_done + cmp poolState3D,POOL_AIM + jne eight_auto_done + call EightCount3D + call PoolPredict3D + test poolCallManual3D,1 + jnz eight_auto_pocket + mov poolCalled3D,0 + mov ebx,poolTraceFirst3D + invoke EightLegal3D,ebx + test eax,eax + jz eight_auto_scan + mov poolCalled3D,ebx + bt poolTracePotted3D,ebx + jnc eight_auto_scan + test poolCallManual3D,2 + jz eight_auto_select + mov eax,poolTraceFinal3D[80*4+ebx*4] + cmp eax,poolCalledPocket3D + je eight_auto_select +eight_auto_scan: + mov ebx,1 +eight_auto_next: + bt poolTracePotted3D,ebx + jnc eight_auto_advance + invoke EightLegal3D,ebx + test eax,eax + jz eight_auto_advance +eight_auto_filter: + test poolCallManual3D,2 + jz eight_auto_select + mov eax,poolTraceFinal3D[80*4+ebx*4] + cmp eax,poolCalledPocket3D + je eight_auto_select +eight_auto_advance: + inc ebx + cmp ebx,16 + jb eight_auto_next + jmp eight_auto_pocket +eight_auto_select: + mov poolCalled3D,ebx +eight_auto_pocket: + test poolCallManual3D,2 + jnz eight_auto_done + mov poolCalledPocket3D,-1 + mov ebx,poolCalled3D + cmp ebx,1 + jb eight_auto_done + cmp ebx,15 + ja eight_auto_done + bt poolTracePotted3D,ebx + jnc eight_auto_done + mov eax,poolTraceFinal3D[80*4+ebx*4] + cmp eax,5 + ja eight_auto_done + mov poolCalledPocket3D,eax +eight_auto_done: + ret +EightAutoCall3D ENDP + +; In pool the group owns the ball, not whoever happened to pocket it. +; Recounting also assigns balls pocketed while the table was still open. +EightSyncGroups3D PROC uses ebx esi edi + mov pyrScore3D,0 + mov pyrScore3D[4],0 + mov pyrOwner3D,-1 + mov esi,1 +eight_sync_ball: + mov pyrOwner3D[esi*4],-1 + cmp esi,8 + je eight_sync_next + cmp poolActive3D[esi*4],0 + jne eight_sync_next + mov eax,1 + cmp esi,8 + jb @F + mov eax,2 +@@: + xor ebx,ebx + cmp eax,poolGroups3D + je eight_sync_credit + inc ebx + cmp eax,poolGroups3D[4] + jne eight_sync_next +eight_sync_credit: + mov pyrOwner3D[esi*4],ebx + inc pyrScore3D[ebx*4] +eight_sync_next: + inc esi + cmp esi,16 + jb eight_sync_ball + ret +EightSyncGroups3D ENDP + +; Called-ball eight-ball rules share only simulation and aiming with pyramid. +EightFinish3D PROC uses ebx esi edi + local originalPots:DWORD + cmp pyrSettled3D,0 + jne eight_finish_done + mov pyrSettled3D,1 + mov poolCallManual3D,0 + mov poolTraceValid3D,0 + mov eax,poolPotted3D + mov originalPots,eax + mov poolFoul3D,0 + cmp poolActive3D,0 + je eight_finish_foul + cmp poolFirst3D,-1 + je eight_finish_foul + cmp poolBreak3D,0 + jne eight_finish_break + invoke EightLegal3D,poolFirst3D + test eax,eax + jz eight_finish_foul + cmp poolRail3D,0 + jne eight_finish_eight + cmp poolPotted3D,0 + jne eight_finish_eight + jmp eight_finish_foul +eight_finish_break: + mov eax,poolPotted3D + and eax,0FFFEh + jnz eight_finish_eight + mov eax,poolBreakRails3D + and eax,0FFFEh + xor ecx,ecx +eight_break_rails: + test eax,eax + jz @F + lea edx,[eax-1] + and eax,edx + inc ecx + jmp eight_break_rails +@@: + cmp ecx,4 + jae eight_finish_eight + mov pyrIllegalBreak3D,1 + mov pyrFoulReason3D,2 + mov eax,poolPlayer3D + xor eax,1 + mov poolPlayer3D,eax + mov pyrChoiceActor3D,eax + mov poolState3D,POOL_CHOICE + mov poolTime3D,0 + call EightSyncGroups3D + ret +eight_finish_foul: + mov poolFoul3D,1 +eight_finish_eight: + bt poolPotted3D,8 + jnc eight_finish_continue + cmp poolBreak3D,0 + je eight_finish_result + invoke PyramidRespot3D,8 + btr poolPotted3D,8 + jmp eight_finish_continue +eight_finish_result: + mov eax,poolPlayer3D + cmp poolFoul3D,0 + jne eight_finish_lost + cmp poolAtStart3D,0 + jne eight_finish_lost + mov ecx,poolGroups3D[eax*4] + test ecx,ecx + jz eight_finish_lost + cmp poolCalled3D,8 + jne eight_finish_lost + mov ecx,poolCalledPocket3D + cmp poolPocket3D[32],ecx + je eight_finish_winner +eight_finish_lost: + xor eax,1 +eight_finish_winner: + mov poolWinner3D,eax + mov poolState3D,POOL_RESULT + call EightSyncGroups3D + ret +eight_finish_continue: + cmp poolFoul3D,0 + jne eight_finish_hand + cmp poolBreak3D,0 + je eight_finish_called + mov eax,originalPots + and eax,0FFFEh + jnz eight_finish_aim + jmp eight_finish_switch +eight_finish_called: + mov ebx,poolCalled3D + cmp ebx,1 + jb eight_finish_switch + cmp ebx,15 + ja eight_finish_switch + bt poolPotted3D,ebx + jnc eight_finish_switch + mov eax,poolCalledPocket3D + cmp poolPocket3D[ebx*4],eax + jne eight_finish_switch + invoke EightLegal3D,ebx + test eax,eax + jz eight_finish_switch + mov ecx,poolPlayer3D + cmp poolGroups3D[ecx*4],0 + jne eight_finish_aim + mov eax,1 + cmp ebx,8 + jb @F + mov eax,2 +@@: + mov poolGroups3D[ecx*4],eax + xor ecx,1 + xor eax,3 + mov poolGroups3D[ecx*4],eax + jmp eight_finish_aim +eight_finish_hand: + xor poolPlayer3D,1 + mov poolState3D,POOL_HAND + mov poolBreak3D,0 + mov poolTime3D,0 + mov pyrCue3D,0 + mov poolActive3D,0 + call EightSyncGroups3D + ret +eight_finish_switch: + xor poolPlayer3D,1 +eight_finish_aim: + mov poolState3D,POOL_AIM + mov poolBreak3D,0 + mov poolTime3D,0 + mov pyrCue3D,0 + call EightCount3D + call EightSyncGroups3D +eight_finish_done: + ret +EightFinish3D ENDP + +EightHand3D PROC uses ebx esi + cmp poolPlayer3D,1 + jne eight_hand_human + cmp poolAI3D,0 + je eight_hand_human + mov esi,55*FP_1 +eight_hand_row: + mov ebx,40*FP_1 +eight_hand_cell: + invoke PoolPlaceCue3D,ebx,esi + test eax,eax + jnz eight_hand_placed + add ebx,6*FP_1 + cmp ebx,280*FP_1 + jle eight_hand_cell + add esi,6*FP_1 + cmp esi,175*FP_1 + jle eight_hand_row + ret +eight_hand_human: + cmp mouseClick3D,0 + je eight_hand_done + mov mouseClick3D,0 + mov eax,mouseX3D + shl eax,16 + mov ebx,mouseY3D + shl ebx,16 + invoke PoolPlaceCue3D,eax,ebx + test eax,eax + jz eight_hand_done +eight_hand_placed: + mov poolState3D,POOL_AIM + mov poolTime3D,0 + mov poolTraceValid3D,0 +eight_hand_done: + ret +EightHand3D ENDP diff --git a/src_graphics/hall_data.inc b/src_graphics/hall_data.inc index 6021a9d..f25afcc 100644 --- a/src_graphics/hall_data.inc +++ b/src_graphics/hall_data.inc @@ -165,4 +165,5 @@ stairsTime3D dd 0 stairsReturnX3D dd 0 stairsReturnY3D dd 0 stairsReturnAngle3D dd 0 +include src_graphics\world_dressing_data.inc .code diff --git a/src_graphics/hall_lighting.inc b/src_graphics/hall_lighting.inc new file mode 100644 index 0000000..372cc22 --- /dev/null +++ b/src_graphics/hall_lighting.inc @@ -0,0 +1,383 @@ +; Static world-space light and contact maps, shaded only before the HUD. +HallLightVisible3D PROC uses ebx esi lx:DWORD,ly:DWORD,tx:DWORD,ty:DWORD + mov esi,1 +hall_light_ray: + mov eax,tx + sub eax,lx + imul eax,esi + sar eax,3 + add eax,lx + sar eax,3 + mov ebx,eax + mov eax,ty + sub eax,ly + imul eax,esi + sar eax,3 + add eax,ly + sar eax,3 + shl eax,5 + add eax,ebx + cmp casinoMap[eax],0 + jne hall_light_blocked + inc esi + cmp esi,8 + jb hall_light_ray + mov eax,1 + ret +hall_light_blocked: + xor eax,eax + ret +HallLightVisible3D ENDP + +AddHallLight3D PROC uses ebx esi edi wx:DWORD,wy:DWORD,radius:DWORD,power:DWORD + local lightX:DWORD,lightY:DWORD,gx:DWORD,gy:DWORD,minX:DWORD,maxX:DWORD,maxY:DWORD + local radiusSq:DWORD,weight:DWORD + mov eax,wx + sar eax,13 + mov lightX,eax + mov eax,wy + sar eax,13 + mov lightY,eax + mov eax,radius + imul eax,eax + mov radiusSq,eax + mov eax,lightX + sub eax,radius + jns @F + xor eax,eax +@@: + mov minX,eax + mov eax,lightX + add eax,radius + cmp eax,255 + jle @F + mov eax,255 +@@: + mov maxX,eax + mov eax,lightY + sub eax,radius + jns @F + xor eax,eax +@@: + mov gy,eax + mov eax,lightY + add eax,radius + cmp eax,255 + jle @F + mov eax,255 +@@: + mov maxY,eax +hall_light_bake_row: + mov eax,minX + mov gx,eax +hall_light_bake_cell: + mov eax,gx + sub eax,lightX + imul eax,eax + mov ebx,gy + sub ebx,lightY + imul ebx,ebx + add eax,ebx + cmp eax,radiusSq + jae hall_light_bake_next + neg eax + add eax,radiusSq + shl eax,8 + xor edx,edx + div radiusSq + imul eax,eax + imul eax,power + shr eax,16 + test eax,eax + jz hall_light_bake_next + mov weight,eax + invoke HallLightVisible3D,lightX,lightY,gx,gy + test eax,eax + jz hall_light_bake_next + mov edi,gy + shl edi,8 + add edi,gx + movzx eax,hallLightMap3D[edi] + add eax,weight + cmp eax,160 + jbe @F + mov eax,160 +@@: + mov hallLightMap3D[edi],al +hall_light_bake_next: + inc gx + mov eax,gx + cmp eax,maxX + jle hall_light_bake_cell + inc gy + mov eax,gy + cmp eax,maxY + jle hall_light_bake_row + ret +AddHallLight3D ENDP + +AddHallContact3D PROC uses ebx esi edi wx:DWORD,wy:DWORD,worldWidth:DWORD + local centerX:DWORD,centerY:DWORD,radius:DWORD,gx:DWORD,gy:DWORD,minX:DWORD,maxX:DWORD,maxY:DWORD + mov eax,wx + sar eax,13 + mov centerX,eax + mov eax,wy + sar eax,13 + mov centerY,eax + mov eax,worldWidth + shr eax,14 + add eax,2 + mov radius,eax + mov eax,centerX + sub eax,radius + jns @F + xor eax,eax +@@: + mov minX,eax + mov eax,centerX + add eax,radius + cmp eax,255 + jle @F + mov eax,255 +@@: + mov maxX,eax + mov eax,centerY + sub eax,4 + jns @F + xor eax,eax +@@: + mov gy,eax + mov eax,centerY + add eax,4 + cmp eax,255 + jle @F + mov eax,255 +@@: + mov maxY,eax +hall_contact_row: + mov eax,minX + mov gx,eax +hall_contact_cell: + mov eax,gx + sub eax,centerX + imul eax,256 + cdq + idiv radius + imul eax,eax + mov ebx,eax + mov eax,gy + sub eax,centerY + imul eax,64 + imul eax,eax + add eax,ebx + cmp eax,65536 + jae hall_contact_next + neg eax + add eax,65536 + imul eax,40 + shr eax,16 + mov edi,gy + shl edi,8 + add edi,gx + cmp al,hallContactMap3D[edi] + jbe hall_contact_next + mov hallContactMap3D[edi],al +hall_contact_next: + inc gx + mov eax,gx + cmp eax,maxX + jle hall_contact_cell + inc gy + mov eax,gy + cmp eax,maxY + jle hall_contact_row + ret +AddHallContact3D ENDP + +BuildHallLighting3D PROC uses ebx esi edi + lea edi,hallLightMap3D + xor eax,eax + mov ecx,WORLD_LIGHT_W*WORLD_LIGHT_H + rep stosb + lea edi,hallContactMap3D + mov ecx,WORLD_LIGHT_W*WORLD_LIGHT_H + rep stosb + lea esi,hallLights3D + mov edi,HALL_LIGHT_COUNT +hall_bake_chandelier: + invoke AddHallLight3D,[esi].CityProp3D.posX,[esi].CityProp3D.posY,32,86 + add esi,SIZEOF CityProp3D + dec edi + jnz hall_bake_chandelier + lea esi,hallLoungeDecor3D + mov edi,HALL_DECOR_COUNT +hall_bake_furniture: + cmp [esi].CityProp3D.asset,OFFSET hallRopeFurniture3D + je hall_bake_furniture_next + invoke AddHallContact3D,[esi].CityProp3D.posX,[esi].CityProp3D.posY,[esi].CityProp3D.worldW + cmp [esi].CityProp3D.asset,OFFSET hallLampFurniture3D + jne hall_bake_furniture_next + invoke AddHallLight3D,[esi].CityProp3D.posX,[esi].CityProp3D.posY,20,110 +hall_bake_furniture_next: + add esi,SIZEOF CityProp3D + dec edi + jnz hall_bake_furniture + xor esi,esi +hall_bake_sconce: + invoke AddHallLight3D,FP_1*5/4,hallSconceY3D[esi*4],22,110 + invoke AddHallLight3D,FP_1*123/4,hallSconceY3D[esi*4],22,110 + inc esi + cmp esi,4 + jb hall_bake_sconce + invoke AddHallLight3D,4*FP_1,23*FP_1,22,90 + invoke AddHallLight3D,6*FP_1,23*FP_1,22,90 + invoke AddHallLight3D,8*FP_1,23*FP_1,22,90 + mov hallLightReady3D,1 + ret +BuildHallLighting3D ENDP + +ApplyHallLighting3D PROC uses ebx esi edi + local rowY:DWORD,columnX:DWORD,planeDepth:DWORD,planeFloor:DWORD + local gainR:DWORD,gainG:DWORD,gainB:DWORD,cell:DWORD,floorHit:DWORD + local light:DWORD,contact:DWORD,pixelRow:DWORD,pixelColumn:DWORD + cmp hallLighting3D,0 + je hall_light_draw_done + cmp hallLightReady3D,0 + je hall_light_draw_done + cmp outdoorRender3D,0 + jne hall_light_draw_done + cmp hallLevel3D,0 + jne hall_light_draw_done + mov rowY,0 +hall_light_draw_row: + mov eax,rowY + mov planeFloor,0 + cmp eax,viewHorizon3D + jge hall_light_floor_depth + mov ecx,viewHorizon3D + sub ecx,eax + mov eax,viewCeiling3D + sub eax,viewEye3D + jmp hall_light_plane_depth +hall_light_floor_depth: + sub eax,viewHorizon3D + inc eax + mov ecx,eax + mov eax,viewEye3D + mov planeFloor,1 +hall_light_plane_depth: + imul eax,480 + cdq + idiv ecx + mov planeDepth,eax + mov columnX,0 +hall_light_draw_column: + imul edi,rowY,640 + add edi,columnX + mov eax,cityDepthBuffer3D[edi*4] + mov floorHit,0 + cmp eax,planeDepth + jl @F + mov eax,planeDepth + mov edx,planeFloor + mov floorHit,edx +@@: + mov esi,eax + mov ebx,columnX + imul cityRayX3D[ebx*4] + shrd eax,edx,16 + add eax,playerX + sar eax,13 + cmp eax,256 + jae hall_light_ambient + mov cell,eax + mov eax,esi + imul cityRayY3D[ebx*4] + shrd eax,edx,16 + add eax,playerY + sar eax,13 + cmp eax,256 + jae hall_light_ambient + shl eax,8 + add eax,cell + movzx ecx,hallLightMap3D[eax] + mov light,ecx + mov contact,0 + cmp floorHit,0 + je hall_light_gains + movzx eax,hallContactMap3D[eax] + mov contact,eax + jmp hall_light_gains +hall_light_ambient: + mov light,0 + mov contact,0 +hall_light_gains: + mov eax,light + add eax,184 + sub eax,contact + mov gainR,eax + mov eax,light + imul eax,7 + shr eax,3 + add eax,188 + sub eax,contact + mov gainG,eax + mov eax,light + shr eax,1 + add eax,200 + sub eax,contact + mov gainB,eax + mov pixelRow,0 +hall_light_pixel_row: + mov pixelColumn,0 +hall_light_pixel: + mov eax,pixelRow + imul eax,640 + add eax,pixelColumn + add eax,edi + mov esi,eax + mov ebx,screenBuffer[esi*4] + movzx eax,bl + imul eax,gainB + shr eax,8 + cmp eax,255 + jbe @F + mov eax,255 +@@: + mov ecx,eax + movzx eax,bh + imul eax,gainG + shr eax,8 + cmp eax,255 + jbe @F + mov eax,255 +@@: + shl eax,8 + or ecx,eax + mov eax,ebx + shr eax,16 + and eax,255 + imul eax,gainR + shr eax,8 + cmp eax,255 + jbe @F + mov eax,255 +@@: + shl eax,16 + or eax,ecx + mov screenBuffer[esi*4],eax + inc pixelColumn + cmp pixelColumn,2 + jb hall_light_pixel + inc pixelRow + cmp pixelRow,2 + jb hall_light_pixel_row + add columnX,2 + cmp columnX,640 + jb hall_light_draw_column + add rowY,2 + cmp rowY,480 + jb hall_light_draw_row +hall_light_draw_done: + ret +ApplyHallLighting3D ENDP diff --git a/src_graphics/hall_scene.inc b/src_graphics/hall_scene.inc index 85f4a5e..94ca684 100644 --- a/src_graphics/hall_scene.inc +++ b/src_graphics/hall_scene.inc @@ -280,7 +280,7 @@ InitHall3D PROC uses esi edi rep movsd mov hallLevel3D, 0 mov worldMap3D, OFFSET casinoMap - mov viewEye3D, FP_1*17/20 + mov viewEye3D, HALL_STANDING_EYE mov viewCeiling3D, FP_1*5/2 mov floorTexture3D, 100 mov casinoSprites[14*SIZEOF SpriteObject].interactId, 0 @@ -303,10 +303,13 @@ init_plane_lut: init_plane_done: call InitCasinoNpcMind call InitNpcLife3D + call BuildHallLighting3D ret InitHall3D ENDP LoadHallAssets3D PROC + call LoadWorldDressing3D + call LoadScratchArt3D invoke LoadAnimPack3D, OFFSET hallCardFurniture3D, OFFSET hallCardFurniturePath3D invoke LoadAnimPack3D, OFFSET hallWheelFurniture3D, OFFSET hallWheelFurniturePath3D invoke LoadAnimPack3D, OFFSET hallBackFurniture3D, OFFSET hallBackFurniturePath3D @@ -376,6 +379,7 @@ hall_load_sign: LoadHallAssets3D ENDP FreeHallAssets3D PROC + call FreeWorldDressing3D call FreeNativeWheelArt3D call FreeNativeCoinArt3D invoke FreeAnimPack3D, OFFSET hallCardFurniture3D @@ -556,6 +560,9 @@ hall_render_lounge: cmp esi, HALL_DECOR_COUNT jb hall_render_lounge call RenderPatrons3D + invoke DrawCityObject3D,OFFSET scrStation3D,0 + call RenderHallDressing3D + call ApplyHallLighting3D ret hall_render_bunker: mov esi, 0 @@ -649,6 +656,7 @@ CheckHallInteractions3D PROC ; A point on the serving edge remains in front even when standing close. invoke HallTarget3D, playerX,23*FP_1+FP_HALF,INTERACT_BAR,OFFSET hallBarPrompt3D hall_check_other: + invoke HallTarget3D,scrStation3D.posX,scrStation3D.posY,INTERACT_SCRATCH,OFFSET scratchHall3D invoke HallTarget3D, 28*FP_1+FP_HALF,10*FP_1+FP_HALF,INTERACT_BASEMENT,OFFSET hallBunkerPrompt3D invoke HallTarget3D, 27*FP_1,25*FP_1,INTERACT_BILLIARDS,OFFSET hallPoolPrompt3D call CheckNpcInteractions3D @@ -662,6 +670,12 @@ hall_check_bunker: CheckHallInteractions3D ENDP TriggerHallInteraction3D PROC + cmp interactTargetId,INTERACT_SCRATCH + jne @F + call BeginScratch3D + mov eax,1 + ret +@@: cmp interactTargetId,INTERACT_NPC jne @F call OpenNpcDialog3D @@ -737,6 +751,10 @@ stairs_back_to_hall: mov playerAngle, eax stairs_complete: mov viewEye3D, FP_1*17/20 + cmp hallLevel3D,0 + jne @F + mov viewEye3D,HALL_STANDING_EYE +@@: mov engineMode, MODE_WALK call ClearKeys3D stairs_handle_done: @@ -915,7 +933,7 @@ RenderBar3D PROC uses ebx mov playerY, 25*FP_1+FP_1/3 mov playerAngle, 384 mov viewHorizon3D, HALF_H - mov viewEye3D, FP_1*17/20 + mov viewEye3D, HALL_STANDING_EYE call UpdateCamera3D call RenderCeilingAndFloor call RenderWalls @@ -1027,6 +1045,9 @@ RenderCityGuards3D ENDP CanWalkHall3D PROC uses esi ebx px:DWORD,py:DWORD cmp hallLevel3D, 0 jne hall_collision_bunker + invoke ScratchCanWalk3D,px,py + test eax,eax + jz hall_collision_blocked xor esi, esi hall_collision_decor: imul ebx, esi, SIZEOF CityProp3D @@ -1204,7 +1225,10 @@ UpdateHall3D PROC mov viewHorizon3D, HALF_H mov viewEye3D, FP_1*17/20 cmp hallLevel3D, 0 - je @F + jne hall_update_bunker_eye + mov viewEye3D,HALL_STANDING_EYE + jmp @F +hall_update_bunker_eye: mov eax, playerY sub eax, 24*FP_1 jle @F @@ -1327,3 +1351,5 @@ patron_render_loop: jb patron_render_loop ret RenderPatrons3D ENDP +include src_graphics\world_dressing.inc +include src_graphics\hall_lighting.inc diff --git a/src_graphics/native_dice.inc b/src_graphics/native_dice.inc new file mode 100644 index 0000000..f1ab4bf --- /dev/null +++ b/src_graphics/native_dice.inc @@ -0,0 +1,249 @@ +LoadNativeDiceArt3D PROC + cmp diceArtReady3D,1 + je dice_art_loaded + invoke LoadAnimPack3D,OFFSET diceTableArt3D,OFFSET diceTablePath3D + invoke LoadAnimPack3D,OFFSET diceHandsArt3D,OFFSET diceHandsPath3D + invoke ValidateCityPack3D,OFFSET diceTableArt3D,640,480,1 + invoke ValidateCityPack3D,OFFSET diceHandsArt3D,256,320,16 + cmp diceTableArt3D.pixels,0 + je dice_art_failed + cmp diceHandsArt3D.pixels,0 + je dice_art_failed + mov diceArtReady3D,1 +dice_art_loaded: + mov eax,1 + ret +dice_art_failed: + call FreeNativeDiceArt3D + xor eax,eax + ret +LoadNativeDiceArt3D ENDP + +FreeNativeDiceArt3D PROC + invoke FreeAnimPack3D,OFFSET diceTableArt3D + invoke FreeAnimPack3D,OFFSET diceHandsArt3D + mov diceArtReady3D,0 + ret +FreeNativeDiceArt3D ENDP + +DiceEscape3D PROC + mov paused3D,0 + cmp dice3d_state,1 + jne dice_escape_now + mov diceExitQueued3D,1 + call ClearKeys3D + ret +dice_escape_now: + mov diceExitQueued3D,0 + mov engineMode,MODE_WALK + call ClearKeys3D + invoke PlayCasinoSound3D,0,0,0 + ret +DiceEscape3D ENDP + +HandleDiceMouse3D PROC + cmp mouseClick3D,0 + je dice_mouse_done + mov mouseClick3D,0 + cmp mouseY3D,3 + jb dice_mouse_done + cmp mouseY3D,17 + jae dice_mouse_footer + cmp mouseX3D,252 + jb dice_mouse_done + cmp mouseX3D,312 + jae dice_mouse_done + call DiceEscape3D + ret +dice_mouse_footer: + cmp dice3d_state,1 + je dice_mouse_done + cmp mouseY3D,218 + jb dice_mouse_stake + cmp mouseY3D,236 + jae dice_mouse_done + cmp mouseX3D,198 + jb dice_mouse_choice + cmp mouseX3D,312 + jae dice_mouse_done + mov pressedKeys3D[20h],1 + ret +dice_mouse_choice: + cmp dice3d_state,0 + jne dice_mouse_done + cmp mouseX3D,8 + jb dice_mouse_done + cmp mouseX3D,96 + jae @F + mov pressedKeys3D[31h],1 + ret +@@: + cmp mouseX3D,100 + jb dice_mouse_done + cmp mouseX3D,188 + jae dice_mouse_done + mov pressedKeys3D[32h],1 + ret +dice_mouse_stake: + cmp dice3d_state,0 + jne dice_mouse_done + cmp mouseY3D,199 + jb dice_mouse_done + cmp mouseY3D,215 + jae dice_mouse_done + cmp mouseX3D,118 + jb dice_mouse_done + cmp mouseX3D,134 + jae @F + mov pressedKeys3D[28h],1 + ret +@@: + cmp mouseX3D,140 + jb dice_mouse_done + cmp mouseX3D,156 + jae dice_mouse_done + mov pressedKeys3D[26h],1 +dice_mouse_done: + ret +HandleDiceMouse3D ENDP + +; Original payment/parity rules remain a single command path for both inputs. +HandleDiceInput PROC + call HandleDiceMouse3D + call HandleDiceRules3D + cmp diceExitQueued3D,1 + jne dice_input_done + cmp dice3d_state,1 + je dice_input_done + call DiceEscape3D +dice_input_done: + ret +HandleDiceInput ENDP + +DrawDiceHand3D PROC uses ebx esi + local frame:DWORD,py:DWORD + mov frame,0 + mov py,90 + cmp dice3d_state,0 + je dice_hand_draw + cmp dice3d_state,1 + jne dice_hand_done + mov eax,dice3d_timer + cmp eax,240 + jae dice_hand_release + imul eax,4 + xor edx,edx + mov ecx,240 + div ecx + mov frame,eax + jmp dice_hand_draw +dice_hand_release: + cmp eax,1000 + jae dice_hand_lower + sub eax,240 + imul eax,12 + xor edx,edx + mov ecx,760 + div ecx + add eax,4 + mov frame,eax + jmp dice_hand_draw +dice_hand_lower: + cmp eax,1250 + jae dice_hand_done + sub eax,1000 + imul eax,260 + xor edx,edx + mov ecx,250 + div ecx + add py,eax + mov frame,15 +dice_hand_draw: + invoke DrawAnimSprite3D,OFFSET diceHandsArt3D,frame,320,py,256,320 +dice_hand_done: + ret +DrawDiceHand3D ENDP + +RenderNativeDice3D PROC uses ebx esi edi + local evenTint:DWORD,oddTint:DWORD + cmp diceArtReady3D,1 + jne dice_scene_fallback + mov esi,diceTableArt3D.pixels + lea edi,screenBuffer + mov ecx,SCREEN_SIZE + rep movsd + jmp dice_scene_objects +dice_scene_fallback: + invoke DrawBox,0,0,320,240,000B342Bh + invoke DrawString8x8,96,64,OFFSET coinMissingText3D,00E5D29Eh +dice_scene_objects: + ; Cloth shadows belong behind the foreground hand, never on its skin. + mov diceShadowPass3D,1 + call RenderDiceThrow3D + mov diceShadowPass3D,0 + call DrawDiceHand3D + call RenderDiceThrow3D + invoke DrawBox,0,0,320,20,0014191Dh + invoke DrawString8x8,12,6,OFFSET textDiceTitle,00E5D29Eh + invoke DrawBoxBorder,252,3,60,14,00252D2Ah,0086754Eh,1 + invoke DrawString8x8,266,6,OFFSET coinExitText3D,00E5D29Eh + invoke DrawBox,4,23,132,15,0014191Dh + invoke DrawString8x8,8,27,OFFSET textBankPrompt,00E5D29Eh + invoke FormatNumber,bankroll,OFFSET hudNumBuffer + invoke DrawString8x8,56,27,OFFSET hudNumBuffer,00FFFFFFh + invoke DrawBox,0,194,320,46,0014191Dh + invoke DrawString8x8,8,201,OFFSET coinStakeText3D,00E5D29Eh + invoke FormatNumber,dice3d_bet,OFFSET hudNumBuffer + invoke DrawString8x8,72,201,OFFSET hudNumBuffer,00FFFFFFh + invoke DrawBoxBorder,118,199,16,16,00252D2Ah,0086754Eh,1 + invoke DrawString8x8,124,203,OFFSET coinMinusText3D,00E5D29Eh + invoke DrawBoxBorder,140,199,16,16,00252D2Ah,0086754Eh,1 + invoke DrawString8x8,146,203,OFFSET coinPlusText3D,00E5D29Eh + mov evenTint,0086754Eh + mov oddTint,0086754Eh + cmp dice3d_choice,1 + jne @F + mov evenTint,00E5D29Eh + jmp dice_scene_choices +@@: + mov oddTint,00E5D29Eh +dice_scene_choices: + invoke DrawBoxBorder,8,218,88,18,00252D2Ah,evenTint,1 + invoke DrawString8x8,26,223,OFFSET textDiceEven,evenTint + invoke DrawBoxBorder,100,218,88,18,00252D2Ah,oddTint,1 + invoke DrawString8x8,112,223,OFFSET textDiceOdd,oddTint + invoke DrawBoxBorder,198,218,114,18,00252D2Ah,0086754Eh,1 + cmp dice3d_state,2 + jne @F + invoke DrawString8x8,224,223,OFFSET diceAgainText3D,00E5D29Eh + jmp dice_scene_status +@@: + invoke DrawString8x8,226,223,OFFSET diceThrowText3D,00E5D29Eh +dice_scene_status: + cmp diceExitQueued3D,0 + je @F + invoke DrawString8x8,164,201,OFFSET coinLeavingText3D,00E5D29Eh +@@: + cmp dice3d_state,2 + jne dice_scene_done + invoke DrawBox,8,176,304,16,0014191Dh + invoke DrawString8x8,12,180,OFFSET textDiceSumHdr,00E5D29Eh + invoke FormatNumber,dice3d_val1,OFFSET hudNumBuffer + invoke DrawString8x8,72,180,OFFSET hudNumBuffer,00FFFFFFh + invoke DrawString8x8,86,180,OFFSET coinPlusText3D,00E5D29Eh + invoke FormatNumber,dice3d_val2,OFFSET hudNumBuffer + invoke DrawString8x8,98,180,OFFSET hudNumBuffer,00FFFFFFh + invoke DrawString8x8,112,180,OFFSET textDiceEqual+1,00E5D29Eh + invoke FormatNumber,dice3d_sum,OFFSET hudNumBuffer + invoke DrawString8x8,126,180,OFFSET hudNumBuffer,00FFFFFFh + cmp dice3d_win,0 + je dice_scene_loss + invoke DrawString8x8,158,180,OFFSET textDiceWin,0090D5ADh + invoke FormatNumber,dice3d_win,OFFSET hudNumBuffer + invoke DrawString8x8,248,180,OFFSET hudNumBuffer,0090D5ADh + jmp dice_scene_done +dice_scene_loss: + invoke DrawString8x8,192,180,OFFSET textDiceLose,00FF6464h +dice_scene_done: + ret +RenderNativeDice3D ENDP diff --git a/src_graphics/native_dice_data.inc b/src_graphics/native_dice_data.inc new file mode 100644 index 0000000..0b4c4da --- /dev/null +++ b/src_graphics/native_dice_data.inc @@ -0,0 +1,17 @@ +LoadNativeDiceArt3D PROTO +FreeNativeDiceArt3D PROTO +RenderNativeDice3D PROTO +HandleDiceInput PROTO +HandleDiceRules3D PROTO +DiceEscape3D PROTO +.data +diceTableArt3D AnimPack3D <> +diceHandsArt3D AnimPack3D <> +diceTablePath3D db "assets\animations\coin\table-v1.bin",0 +diceHandsPath3D db "assets\animations\dice\hands-v2.bin",0 +diceArtReady3D dd 0 +diceExitQueued3D dd 0 +diceShadowPass3D dd 0 +diceThrowText3D db 193,208,206,209,206,202,0 +diceAgainText3D db 197,217,168,32,208,192,199,0 +.code diff --git a/src_graphics/platform_code.inc b/src_graphics/platform_code.inc index a4505e8..4ae72a4 100644 --- a/src_graphics/platform_code.inc +++ b/src_graphics/platform_code.inc @@ -1,5 +1,12 @@ ; Main and owned coin-detail windows are processed on the same thread; no CMD fallback. WindowEvents3D PROC uses ebx esi edi hwnd:DWORD, uMsg:DWORD, wp:DWORD, lp:DWORD + cmp engineMode,MODE_SCRATCH + jne @F + invoke ScratchMouseEvent3D,hwnd,uMsg,wp,lp + test eax,eax + jz @F + ret +@@: cmp uMsg, 102h jne @F invoke ArrivalChar3D, wp diff --git a/src_graphics/pyramid_play.inc b/src_graphics/pyramid_play.inc index f25d3d7..187c1d5 100644 --- a/src_graphics/pyramid_play.inc +++ b/src_graphics/pyramid_play.inc @@ -1,4 +1,9 @@ PyramidSelect3D PROC uses ebx ball:DWORD + cmp poolVariant3D,1 + jne @F + mov pyrCue3D,0 + ret +@@: cmp poolBreak3D,0 je @F mov pyrCue3D,0 @@ -135,6 +140,8 @@ PoolShoot3D PROC uses ebx esi cmp poolX3D,95*FP_1 jge pyr_shoot_done @@: + call PoolCountGroup3D + call EightAutoCall3D mov poolFirst3D,-1 mov poolRail3D,0 mov poolBreakRails3D,0 @@ -234,6 +241,8 @@ PoolChooseAI3D PROC uses ebx esi edi local cue:DWORD,target:DWORD,pocket:DWORD,deltaX:DWORD,deltaY:DWORD,distance:DWORD local gx:DWORD,gy:DWORD,best:DWORD,bestCue:DWORD,bestX:DWORD,bestY:DWORD,bestPower:DWORD local shotLength:DWORD + local bestTarget:DWORD,bestPocket:DWORD + call PoolCountGroup3D cmp poolBreak3D,0 je pyr_ai_search mov pyrCue3D,0 @@ -247,6 +256,11 @@ pyr_ai_search: mov cue,0 pyr_ai_cue: mov ecx,cue + cmp poolVariant3D,1 + jne @F + test ecx,ecx + jnz pyr_ai_search_done +@@: cmp poolActive3D[ecx*4],1 jne pyr_ai_next_cue mov target,0 @@ -256,6 +270,12 @@ pyr_ai_target: je pyr_ai_next_target cmp poolActive3D[esi*4],1 jne pyr_ai_next_target + cmp poolVariant3D,1 + jne @F + invoke EightLegal3D,esi + test eax,eax + jz pyr_ai_next_target +@@: mov pocket,0 pyr_ai_pocket: mov edi,pocket @@ -332,6 +352,10 @@ pyr_ai_pocket: mov bestX,ecx mov ecx,gy mov bestY,ecx + mov ecx,target + mov bestTarget,ecx + mov ecx,pocket + mov bestPocket,ecx ; v^2/(2*rolling loss) estimates the travel budget, with margin for a cut. sar eax,16 imul eax,220 @@ -353,6 +377,7 @@ pyr_ai_next_cue: inc cue cmp cue,16 jb pyr_ai_cue +pyr_ai_search_done: cmp bestCue,-1 je pyr_ai_safety mov eax,bestCue @@ -360,8 +385,17 @@ pyr_ai_next_cue: invoke PoolAimPoint3D,bestX,bestY mov eax,bestPower mov poolPower3D,eax + cmp poolVariant3D,1 + jne @F + mov eax,bestTarget + mov poolCalled3D,eax + mov eax,bestPocket + mov poolCalledPocket3D,eax + mov poolCallManual3D,3 +@@: ret pyr_ai_safety: + mov poolCallManual3D,0 invoke PyramidSelect3D,-1 mov esi,pyrCue3D xor edi,edi @@ -369,7 +403,10 @@ pyr_ai_safety_target: cmp edi,esi je @F cmp poolActive3D[edi*4],1 - je pyr_ai_safety_found + jne @F + invoke PoolBallLegal3D,edi + test eax,eax + jnz pyr_ai_safety_found @@: inc edi cmp edi,16 @@ -382,6 +419,45 @@ pyr_ai_safety_found: PoolChooseAI3D ENDP HandlePool3D PROC uses ebx esi edi + cmp poolState3D,POOL_SELECT + jne pyr_normal_input + invoke Pressed3D,31h + test eax,eax + jnz pyr_start_pyramid + invoke Pressed3D,32h + test eax,eax + jnz pyr_start_eight + cmp mouseClick3D,0 + je pyr_handle_done + mov mouseClick3D,0 + cmp mouseX3D,252 + jb @F + cmp mouseY3D,17 + jb pyr_selector_exit +@@: + cmp mouseX3D,40 + jb pyr_handle_done + cmp mouseX3D,280 + jae pyr_handle_done + cmp mouseY3D,80 + jb pyr_handle_done + cmp mouseY3D,128 + jb pyr_start_pyramid + cmp mouseY3D,144 + jb pyr_handle_done + cmp mouseY3D,192 + jae pyr_handle_done +pyr_start_eight: + invoke StartBilliards3D,1 + ret +pyr_start_pyramid: + invoke StartBilliards3D,0 + ret +pyr_selector_exit: + mov engineMode,MODE_WALK + call ClearKeys3D + ret +pyr_normal_input: xor esi,esi pyr_sink_tick: mov eax,deltaTime3D @@ -409,6 +485,7 @@ pyr_sink_tick: call SaveProgress3D ret pyr_update: + mov eax,deltaTime3D cmp poolState3D,POOL_MOVING jne pyr_strike add poolAccumulator3D,eax @@ -451,9 +528,14 @@ pyr_idle: cmp mouseX3D,202 jb pyr_handle_done pyr_restart: - call InitPool3D + call ResetPool3D ret pyr_actor: + cmp poolState3D,POOL_HAND + jne @F + call EightHand3D + ret +@@: cmp poolPlayer3D,1 jne pyr_human cmp poolAI3D,0 @@ -560,11 +642,45 @@ pyr_human_aim: cmp pyrPickClick3D,0 je @F mov pyrPickClick3D,0 + cmp poolVariant3D,1 + jne pyr_pick_object + xor esi,esi +eight_pick_pocket: + mov ecx,eax + sub ecx,poolPocketX3D[esi*4] + sar ecx,12 + imul ecx,ecx + mov edx,ebx + sub edx,poolPocketY3D[esi*4] + sar edx,12 + imul edx,edx + add ecx,edx + cmp ecx,81*16*16 + jbe eight_pick_pocket_found + inc esi + cmp esi,6 + jb eight_pick_pocket +pyr_pick_object: invoke PyramidPick3D,eax,ebx cmp eax,-1 je pyr_handle_done + cmp poolVariant3D,1 + jne pyr_pick_pyramid + mov ebx,eax + call EightCount3D + invoke EightLegal3D,ebx + test eax,eax + jz pyr_handle_done + mov poolCalled3D,ebx + or poolCallManual3D,1 + ret +pyr_pick_pyramid: invoke PyramidSelect3D,eax ret +eight_pick_pocket_found: + mov poolCalledPocket3D,esi + or poolCallManual3D,2 + ret @@: cmp poolBreak3D,0 je pyr_mouse_aim @@ -578,6 +694,23 @@ pyr_mouse_aim: invoke PoolAimPoint3D,eax,ebx ret pyr_mouse_footer: + cmp poolVariant3D,1 + jne @F + cmp ebx,187 + jb @F + cmp ebx,209 + ja @F + cmp eax,128 + jb @F + cmp eax,194 + ja @F + inc poolCalledPocket3D + or poolCallManual3D,2 + cmp poolCalledPocket3D,6 + jb pyr_handle_done + mov poolCalledPocket3D,0 + ret +@@: cmp ebx,193 jb pyr_keys cmp ebx,209 @@ -669,6 +802,8 @@ pyr_toggle_guide: xor pyrGuideOn3D,1 ret pyr_cycle_cue: + cmp poolVariant3D,1 + je eight_cycle_call cmp poolBreak3D,0 jne pyr_handle_done mov esi,pyrCue3D @@ -683,6 +818,26 @@ pyr_cycle_scan: ret pyr_cycle_found: invoke PyramidSelect3D,esi + ret +eight_cycle_call: + call EightCount3D + mov esi,poolCalled3D + mov edi,16 +eight_cycle_scan: + inc esi + and esi,15 + cmp poolActive3D[esi*4],0 + je eight_cycle_next + invoke EightLegal3D,esi + test eax,eax + jnz eight_cycle_found +eight_cycle_next: + dec edi + jnz eight_cycle_scan + ret +eight_cycle_found: + mov poolCalled3D,esi + or poolCallManual3D,1 pyr_handle_done: ret HandlePool3D ENDP diff --git a/src_graphics/pyramid_render.inc b/src_graphics/pyramid_render.inc index 6811600..972e304 100644 --- a/src_graphics/pyramid_render.inc +++ b/src_graphics/pyramid_render.inc @@ -1,5 +1,16 @@ PyramidBall3D PROC uses ebx esi ball:DWORD,x:DWORD,y:DWORD,sizePx:DWORD mov ebx,ball + cmp poolVariant3D,1 + jne pyr_ball_palette + mov eax,sizePx + shr eax,1 + mov edx,x + sub edx,eax + mov ecx,y + sub ecx,eax + invoke DrawAnimSprite3D,OFFSET poolEightPack3D,ebx,edx,ecx,sizePx,sizePx + jmp pyr_ball_number +pyr_ball_palette: mov eax,pyrRoll3D[ebx*4] shr eax,17 and eax,3 @@ -15,6 +26,7 @@ PyramidBall3D PROC uses ebx esi ball:DWORD,x:DWORD,y:DWORD,sizePx:DWORD mov ecx,y sub ecx,eax invoke DrawAnimSprite3D,OFFSET pyrBallsPack3D,esi,edx,ecx,sizePx,sizePx +pyr_ball_number: cmp sizePx,12 jne pyr_ball_done test ebx,ebx @@ -126,13 +138,40 @@ RenderPool3D PROC uses ebx esi edi lea edi,screenBuffer mov ecx,SCREEN_SIZE rep movsd + cmp poolState3D,POOL_SELECT + jne pyr_render_game + invoke DrawBox,0,0,320,35,00151D20h + invoke DrawString8x8,16,12,OFFSET poolSelectTitle3D,00E8D7ADh + invoke DrawBoxBorder,252,3,60,14,002C3637h,00687672h,1 + invoke DrawString8x8,258,6,OFFSET cardExitButton3D,00E9D8B0h + invoke DrawBoxBorder,40,80,240,48,00192720h,00D6B971h,2 + invoke DrawString8x8,68,99,OFFSET poolSelectPyramid3D,00FFFFFFh + invoke DrawBoxBorder,40,144,240,48,00192720h,00D6B971h,2 + invoke DrawString8x8,58,163,OFFSET poolSelectEight3D,00FFFFFFh + ret +pyr_render_game: + call EightAutoCall3D xor esi,esi pyr_render_mouths: mov eax,poolPocketX3D[esi*4] sar eax,16 mov ebx,poolPocketY3D[esi*4] sar ebx,16 + mov px,eax + mov py,ebx invoke DrawCircle,eax,ebx,3,000B1010h + cmp poolVariant3D,1 + jne @F + mov eax,poolPocketX3D[esi*4] + sar eax,16 + mov ebx,poolPocketY3D[esi*4] + sar ebx,16 + invoke DrawCircle,px,py,5,000B1010h + cmp esi,poolCalledPocket3D + jne @F + invoke DrawCircle,px,py,6,00D6B971h + invoke DrawCircle,px,py,4,000B1010h +@@: inc esi cmp esi,6 jb pyr_render_mouths @@ -148,7 +187,7 @@ pyr_render_mouths: call PyramidDirection3D cmp pyrGuideOn3D,0 je pyr_render_objects - call PyramidGuide3D + call PoolDrawPrediction3D pyr_render_objects: xor esi,esi pyr_render_ball: @@ -160,6 +199,16 @@ pyr_render_ball: mov eax,poolY3D[esi*4] sar eax,15 mov py,eax + cmp poolVariant3D,1 + jne @F + cmp esi,poolCalled3D + jne @F + mov eax,px + sar eax,1 + mov ebx,py + sar ebx,1 + invoke DrawCircle,eax,ebx,4,00D6B971h +@@: cmp esi,pyrCue3D jne @F cmp poolState3D,POOL_AIM @@ -245,10 +294,20 @@ pyr_cue_draw: jnz pyr_cue_draw pyr_render_hud: invoke DrawBox,0,0,320,20,00151D20h - invoke DrawString8x8,8,6,OFFSET pyrTitle3D,00E8D7ADh + mov ebx,OFFSET pyrTitle3D + cmp poolVariant3D,0 + je @F + mov ebx,OFFSET poolTitle3D +@@: + invoke DrawString8x8,8,6,ebx,00E8D7ADh invoke DrawNumber,112,6,pyrScore3D,00F4F3E8h invoke DrawString8x8,128,6,OFFSET pyrScoreSep3D,00E8D7ADh invoke DrawNumber,144,6,pyrScore3D[4],00F4F3E8h + cmp poolVariant3D,1 + jne @F + invoke DrawString8x8,88,6,OFFSET pyrPlayerName3D,00E8D7ADh + invoke DrawString8x8,160,6,OFFSET pyrAIName3D,00E8D7ADh +@@: mov ebx,OFFSET pyrPlayerName3D cmp poolPlayer3D,0 je @F @@ -277,9 +336,20 @@ pyr_render_hud: cmp poolState3D,POOL_STRIKE jne @F mov ebx,OFFSET poolShotText3D +@@: + cmp poolState3D,POOL_HAND + jne @F + mov ebx,OFFSET poolPlaceText3D @@: invoke DrawBox,0,22,320,15,00151D20h invoke DrawString8x8,8,26,ebx,00E8D7ADh + cmp poolVariant3D,1 + jne @F + mov ecx,poolPlayer3D + mov ecx,poolGroups3D[ecx*4] + mov eax,poolGroupTexts3D[ecx*4] + invoke DrawString8x8,176,26,eax,00E8D7ADh +@@: cmp poolState3D,POOL_PENALTY je @F cmp poolState3D,POOL_CHOICE @@ -330,9 +400,36 @@ pyr_shelf_next: add eax,201 invoke DrawBox,eax,199,2,9,00F4F0DDh invoke DrawBoxBorder,12,215,74,22,0039412Fh,00AC9862h,1 + cmp poolVariant3D,1 + je eight_render_call invoke DrawString8x8,18,222,OFFSET pyrCueText3D,00FFFFFFh invoke DrawNumber,62,222,pyrCue3D,00EAD19Bh - invoke DrawBoxBorder,92,215,104,22,0039412Fh,00AC9862h,1 + jmp eight_render_call_done +eight_render_call: + invoke DrawString8x8,18,222,OFFSET poolCallText3D,00FFFFFFh + cmp poolCalled3D,0 + je eight_render_no_call + invoke DrawNumber,64,222,poolCalled3D,00EAD19Bh + jmp eight_render_pocket +eight_render_no_call: + invoke DrawString8x8,64,222,OFFSET poolUnsetText3D,00EAD19Bh +eight_render_pocket: + mov eax,poolCalledPocket3D + inc eax + jz eight_render_no_pocket + invoke DrawNumber,173,187,eax,00EAD19Bh + jmp eight_render_pocket_label +eight_render_no_pocket: + invoke DrawString8x8,173,187,OFFSET poolUnsetText3D,00EAD19Bh +eight_render_pocket_label: + invoke DrawString8x8,129,187,OFFSET poolPocketText3D,00FFFFFFh +eight_render_call_done: + mov ebx,0039412Fh + cmp pyrGuideOn3D,0 + jne @F + mov ebx,00242A2Ch +@@: + invoke DrawBoxBorder,92,215,104,22,ebx,00AC9862h,1 invoke DrawString8x8,106,222,OFFSET pyrGuideText3D,00FFFFFFh invoke DrawBoxBorder,202,215,106,22,00425232h,00BBA76Fh,1 mov ebx,OFFSET poolShotText3D diff --git a/src_graphics/pyramid_rules.inc b/src_graphics/pyramid_rules.inc index 00697db..3264015 100644 --- a/src_graphics/pyramid_rules.inc +++ b/src_graphics/pyramid_rules.inc @@ -23,12 +23,14 @@ pyr_init_records: jb pyr_init_records invoke LoadAnimPack3D,OFFSET pyrTablePack3D,OFFSET pyrTablePath3D invoke LoadAnimPack3D,OFFSET pyrBallsPack3D,OFFSET pyrBallsPath3D + invoke LoadAnimPack3D,OFFSET poolEightPack3D,OFFSET poolEightPath3D ret InitPyramidState3D ENDP FreePyramidAssets3D PROC invoke FreeAnimPack3D,OFFSET pyrTablePack3D invoke FreeAnimPack3D,OFFSET pyrBallsPack3D + invoke FreeAnimPack3D,OFFSET poolEightPack3D ret FreePyramidAssets3D ENDP @@ -275,6 +277,11 @@ PoolPlaceCue3D PROC px:DWORD,py:DWORD PoolPlaceCue3D ENDP PoolBallLegal3D PROC ball:DWORD + cmp poolVariant3D,1 + jne @F + invoke EightLegal3D,ball + ret +@@: mov ecx,ball xor eax,eax cmp ecx,15 @@ -288,11 +295,21 @@ PoolBallLegal3D PROC ball:DWORD PoolBallLegal3D ENDP PoolCountGroup3D PROC + cmp poolVariant3D,1 + jne @F + call EightCount3D + ret +@@: mov poolAtStart3D,0 ret PoolCountGroup3D ENDP PoolFinishShot3D PROC uses ebx esi edi + cmp poolVariant3D,1 + jne @F + call EightFinish3D + ret +@@: cmp pyrSettled3D,0 jne pyr_finish_done mov pyrSettled3D,1 @@ -426,6 +443,25 @@ PyramidChoice3D PROC uses ebx esi edi choice:DWORD jne pyr_choice_done cmp choice,3 ja pyr_choice_done + cmp poolVariant3D,1 + jne pyr_choice_pyramid + mov ebx,pyrChoiceActor3D + test choice,1 + jz @F + xor ebx,1 +@@: + cmp choice,2 + jb eight_choice_accept + call ResetPool3D + mov poolPlayer3D,ebx + ret +eight_choice_accept: + mov poolPlayer3D,ebx + mov poolBreak3D,0 + mov poolState3D,POOL_AIM + mov poolTime3D,0 + ret +pyr_choice_pyramid: cmp choice,2 jb pyr_choice_actor cmp pyrIllegalBreak3D,0 diff --git a/src_graphics/scene_animation.inc b/src_graphics/scene_animation.inc index f262b01..d70c3ff 100644 --- a/src_graphics/scene_animation.inc +++ b/src_graphics/scene_animation.inc @@ -22,7 +22,7 @@ LoadAnimPack3D PROC uses ebx esi edi pack:DWORD,path:DWORD mov ecx, [esi].AnimPack3D.height test ecx, ecx jz anim_close_failure - cmp ecx, 480 + cmp ecx, 1024 ja anim_close_failure imul eax, ecx shl eax, 2 @@ -293,6 +293,8 @@ props_copy_store: InstallHallProps3D ENDP FreeArrivalAssets3D PROC + call FreeScratchArt3D + call FreeNativeDiceArt3D call FreeHallAssets3D invoke FreeAnimPack3D, OFFSET streetPack3D invoke FreeAnimPack3D, OFFSET guardPack3D @@ -309,6 +311,11 @@ FreeArrivalAssets3D PROC invoke FreeAnimPack3D, OFFSET cityCarsPack3D invoke FreeAnimPack3D, OFFSET cityBinPack3D invoke FreeAnimPack3D, OFFSET cityCloudPack3D + invoke FreeAnimPack3D, OFFSET cityWakePack3D + invoke FreeAnimPack3D, OFFSET citySleevePack3D + invoke FreeAnimPack3D, OFFSET cityDecorPack3D + invoke FreeAnimPack3D, OFFSET cityLighterPack3D + invoke FreeAnimPack3D, OFFSET cityKneesPack3D ret FreeArrivalAssets3D ENDP diff --git a/src_graphics/scratch.inc b/src_graphics/scratch.inc new file mode 100644 index 0000000..8fbba2b --- /dev/null +++ b/src_graphics/scratch.inc @@ -0,0 +1,1211 @@ +; Scratch-off state, coating masks, pointer capture and rendering are all MASM. +LoadScratchArt3D PROC + cmp scrReady3D,1 + je scratch_art_ok + invoke LoadAnimPack3D,OFFSET scrTicketArt3D,OFFSET scrTicketPath3D + invoke LoadAnimPack3D,OFFSET scrTableArt3D,OFFSET scrTablePath3D + invoke LoadAnimPack3D,OFFSET scrCoinArt3D,OFFSET scrCoinPath3D + invoke LoadAnimPack3D,OFFSET scrFoilArt3D,OFFSET scrFoilPath3D + invoke LoadAnimPack3D,OFFSET scrSymbolsArt3D,OFFSET clpSymbolsPath3D + invoke LoadAnimPack3D,OFFSET scrCounterArt3D,OFFSET scrCounterPath3D + invoke LoadAnimPack3D,OFFSET scrPortraitArt3D,OFFSET scrPortraitPath3D + invoke LoadAnimPack3D,OFFSET scrPortraitMap3D,OFFSET scrPortraitMapPath3D + invoke LoadAnimPack3D,OFFSET scrPayoutArt3D,OFFSET scrPayoutPath3D + invoke ValidateCityPack3D,OFFSET scrPayoutArt3D,96,72,1 + invoke ValidateCityPack3D,OFFSET scrTicketArt3D,448,288,4 + invoke ValidateCityPack3D,OFFSET scrTableArt3D,640,480,1 + invoke ValidateCityPack3D,OFFSET scrCoinArt3D,64,64,4 + invoke ValidateCityPack3D,OFFSET scrFoilArt3D,SCR_W,SCR_H,1 + invoke ValidateCityPack3D,OFFSET scrSymbolsArt3D,56,56,7 + invoke ValidateCityPack3D,OFFSET scrCounterArt3D,224,240,1 + invoke ValidateCityPack3D,OFFSET scrPortraitArt3D,540,720,3 + invoke ValidateCityPack3D,OFFSET scrPortraitMap3D,540,720,3 + cmp scrPortraitArt3D.pixels,0 + je scratch_art_fail + cmp scrPortraitMap3D.pixels,0 + je scratch_art_fail + cmp scrTicketArt3D.pixels,0 + je scratch_art_fail + cmp scrCoinArt3D.pixels,0 + je scratch_art_fail + cmp scrSymbolsArt3D.pixels,0 + je scratch_art_fail + cmp scrFoilArt3D.pixels,0 + je scratch_art_fail + mov scrReady3D,1 +scratch_art_ok: + mov eax,1 + ret +scratch_art_fail: + call FreeScratchArt3D + xor eax,eax + ret +LoadScratchArt3D ENDP + +FreeScratchArt3D PROC + invoke FreeAnimPack3D,OFFSET scrTicketArt3D + invoke FreeAnimPack3D,OFFSET scrTableArt3D + invoke FreeAnimPack3D,OFFSET scrCoinArt3D + invoke FreeAnimPack3D,OFFSET scrFoilArt3D + invoke FreeAnimPack3D,OFFSET scrSymbolsArt3D + invoke FreeAnimPack3D,OFFSET scrCounterArt3D + invoke FreeAnimPack3D,OFFSET scrPortraitArt3D + invoke FreeAnimPack3D,OFFSET scrPortraitMap3D + invoke FreeAnimPack3D,OFFSET scrPayoutArt3D + mov scrReady3D,0 + ret +FreeScratchArt3D ENDP + +ScratchCancelStroke3D PROC + mov scrLastValid3D,0 + cmp scrHeld3D,0 + je scratch_cancel_done + mov scrHeld3D,0 + invoke ReleaseCapture +scratch_cancel_done: + ret +ScratchCancelStroke3D ENDP + +BeginScratch3D PROC + mov scrCatalog3D,0 + call LoadScratchArt3D + cmp scrState3D,0 + jne @F + call ScratchConfigure3D +@@: + mov engineMode,MODE_SCRATCH + mov paused3D,0 + mov scrError3D,0 + mov scrNewPixels3D,0 + mov scrPointerValid3D,0 + call ClearKeys3D + ret +BeginScratch3D ENDP + +LeaveScratch3D PROC + cmp scrCatalog3D,0 + je @F + mov scrCatalog3D,0 + call ClearKeys3D + ret +@@: + call ScratchCancelStroke3D + mov scrDealMs3D,0 + mov scrPayoutMs3D,0 + mov engineMode,MODE_WALK + mov paused3D,0 + call ClearKeys3D + call SaveProgress3D + ret +LeaveScratch3D ENDP + +; One mask format serves rectangular, circular and single-area tickets. +ScratchConfigure3D PROC uses ebx esi edi + local rowY:DWORD,pointX:DWORD + mov scrDealMs3D,0 + mov scrPayoutMs3D,0 + mov scrDustStep3D,0 + mov scrScrapeCooldown3D,0 + mov scrNewPixels3D,0 + mov scrDustIndex3D,0 + lea edi,scrDustLife3D + xor eax,eax + mov ecx,SCR_DUST_COUNT + rep stosd + lea edi,scrCounts3D + mov ecx,48 + rep stosd + cmp scrType3D,4 + jb @F + call ScratchConfigurePortrait3D + ret +@@: + mov scrCellCount3D,9 + mov scrCellW3D,SCR_W + mov scrCellH3D,SCR_H + cmp scrType3D,1 + jne @F + mov scrCellH3D,34 +@@: + cmp scrType3D,2 + jne @F + mov scrCellW3D,44 + mov scrCellH3D,44 +@@: + cmp scrType3D,3 + jne @F + mov scrCellCount3D,1 + mov scrCellW3D,240 + mov scrCellH3D,120 +@@: + lea edi,scrMask3D + xor eax,eax + mov ecx,9*SCR_MASK_AREA + rep stosb + lea edi,scrCounts3D + mov ecx,48 + rep stosd + xor ebx,ebx + xor esi,esi +scratch_config_cell: + imul edi,esi,SCR_MASK_AREA + add edi,OFFSET scrMask3D + mov rowY,0 +scratch_config_row: + mov pointX,0 +scratch_config_pixel: + cmp scrType3D,2 + jne scratch_config_opaque + mov eax,pointX + sub eax,21 + imul eax,eax + mov edx,rowY + sub edx,21 + imul edx,edx + add eax,edx + cmp eax,21*21 + ja scratch_config_next +scratch_config_opaque: + mov BYTE PTR [edi],255 + cmp esi,0 + jne scratch_config_next + inc ebx +scratch_config_next: + inc edi + inc pointX + mov eax,pointX + cmp eax,scrCellW3D + jb scratch_config_pixel + inc rowY + mov eax,rowY + cmp eax,scrCellH3D + jb scratch_config_row + inc esi + cmp esi,scrCellCount3D + jb scratch_config_cell + mov scrCellArea3D,ebx + imul eax,ebx,72 + xor edx,edx + mov ecx,100 + div ecx + mov scrRevealAt3D,eax + ret +ScratchConfigure3D ENDP + +ScratchSelectType3D PROC kind:DWORD + cmp scrState3D,1 + je scratch_select_done + mov eax,kind + cmp eax,-1 + jne @F + mov eax,6 +@@: + cmp eax,7 + jb @F + xor eax,eax +@@: + cmp eax,scrType3D + je scratch_select_done + mov scrType3D,eax + mov scrState3D,0 + mov scrOpened3D,0 + mov scrGross3D,0 + mov scrError3D,0 + mov scrZoom3D,0 + call ScratchCancelStroke3D + call ScratchConfigure3D +scratch_select_done: + ret +ScratchSelectType3D ENDP + +; A paid ticket is immutable. Losing tickets never accidentally contain a +; triple; winning tickets contain exactly one triple. Shuffle only at purchase. +BuyScratch3D PROC uses ebx esi edi + cmp scrState3D,1 + je scratch_buy_done + cmp scrReady3D,1 + jne scratch_buy_done + mov ecx,scrPriceIndex3D + cmp ecx,2 + ja scratch_buy_done + mov eax,scrPrices3D[ecx*4] + cmp bankroll,eax + jb scratch_buy_poor + mov scrStake3D,eax + sub bankroll,eax + mov scrState3D,1 + mov scrGross3D,0 + mov scrOpened3D,0 + mov scrError3D,0 + mov scrPrizeSymbol3D,-1 + call ScratchConfigure3D + cmp scrType3D,4 + jb @F + call ScratchBuildPortrait3D + mov scrZoom3D,1 + jmp scratch_buy_save +@@: + invoke RandomRange3D,1000 + cmp eax,600 + jb scratch_losing_bag + xor esi,esi +scratch_pick_prize: + cmp eax,scrCutoffs3D[esi*4] + jb scratch_prize_picked + inc esi + jmp scratch_pick_prize +scratch_prize_picked: + mov scrPrizeSymbol3D,esi + mov eax,scrMultipliers3D[esi*4] + imul eax,scrStake3D + mov scrGross3D,eax + mov scrBag3D,esi + mov scrBag3D[4],esi + mov scrBag3D[8],esi + mov edi,3 + xor ebx,ebx +scratch_win_fill: + cmp ebx,esi + je @F + mov scrBag3D[edi*4],ebx + inc edi +@@: + inc ebx + cmp ebx,7 + jb scratch_win_fill + mov esi,8 + jmp scratch_shuffle +scratch_losing_bag: + xor esi,esi +scratch_loss_fill: + mov eax,esi + shr eax,1 + mov scrBag3D[esi*4],eax + inc esi + cmp esi,14 + jb scratch_loss_fill + mov esi,13 +scratch_shuffle: + lea eax,[esi+1] + invoke RandomRange3D,eax + mov ebx,scrBag3D[esi*4] + mov edx,scrBag3D[eax*4] + mov scrBag3D[esi*4],edx + mov scrBag3D[eax*4],ebx + dec esi + jnz scratch_shuffle + lea esi,scrBag3D + lea edi,scrGrid3D + mov ecx,9 + rep movsd + call ScratchBuildVariant3D +scratch_buy_save: + call ScratchCancelStroke3D + call ScratchStartDeal3D + call SaveProgress3D +scratch_buy_done: + ret +scratch_buy_poor: + mov scrError3D,1 + ret +BuyScratch3D ENDP + +ScratchBuildVariant3D PROC uses ebx esi edi + local rowStart:DWORD + cmp scrType3D,1 + je scratch_numbers_build + cmp scrType3D,2 + jne scratch_variant_done + cmp scrPrizeSymbol3D,-1 + je scratch_variant_done + ; Preserve the shuffled six distinct nonwinning symbols, put the triple + ; in exactly one horizontal row. No diagonal or scattered triple pays. + xor esi,esi + xor edi,edi +scratch_row_collect: + mov eax,scrGrid3D[esi*4] + cmp eax,scrPrizeSymbol3D + je @F + mov scrBag3D[edi*4],eax + inc edi +@@: + inc esi + cmp esi,9 + jb scratch_row_collect + invoke RandomRange3D,3 + imul eax,3 + mov rowStart,eax + xor esi,esi + xor edi,edi +scratch_row_place: + mov eax,esi + sub eax,rowStart + cmp eax,3 + jb scratch_row_winner + mov eax,scrBag3D[edi*4] + inc edi + jmp scratch_row_store +scratch_row_winner: + mov eax,scrPrizeSymbol3D +scratch_row_store: + mov scrGrid3D[esi*4],eax + inc esi + cmp esi,9 + jb scratch_row_place + ret +scratch_numbers_build: + xor esi,esi +scratch_number_pool: + lea eax,[esi+1] + mov scrBag3D[esi*4],eax + inc esi + cmp esi,30 + jb scratch_number_pool + mov esi,29 +scratch_number_shuffle: + lea eax,[esi+1] + invoke RandomRange3D,eax + mov ebx,scrBag3D[esi*4] + mov edx,scrBag3D[eax*4] + mov scrBag3D[esi*4],edx + mov scrBag3D[eax*4],ebx + dec esi + jnz scratch_number_shuffle + xor esi,esi +scratch_number_copy: + mov eax,scrBag3D[esi*4] + mov scrGrid3D[esi*4],eax + invoke RandomRange3D,7 + mov eax,scrMultipliers3D[eax*4] + imul eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + inc esi + cmp esi,9 + jb scratch_number_copy + cmp scrGross3D,0 + je scratch_variant_done + invoke RandomRange3D,6 + lea esi,[eax+3] + invoke RandomRange3D,3 + mov eax,scrGrid3D[eax*4] + mov scrGrid3D[esi*4],eax + mov eax,scrGross3D + mov scrCellPrizes3D[esi*4],eax +scratch_variant_done: + ret +ScratchBuildVariant3D ENDP + +FinishScratch3D PROC + cmp scrState3D,1 + jne scratch_finish_done + mov eax,scrCellCount3D + cmp scrOpened3D,eax + jne scratch_finish_done + ; Mark paid before invoking persistence; repeated clicks cannot pay twice. + mov scrState3D,2 + mov eax,bankroll + add eax,scrGross3D + cmp eax,1000000000 + jbe @F + mov eax,1000000000 +@@: + mov bankroll,eax + call ScratchStartPayout3D + call SaveProgress3D +scratch_finish_done: + ret +FinishScratch3D ENDP + +; A normal application close settles an already-paid ticket, even if the +; player has since returned to another table. No pending prize is discarded. +SettleScratchOnQuit3D PROC + cmp scrState3D,1 + jne scratch_quit_done + mov eax,scrCellCount3D + mov scrOpened3D,eax + call FinishScratch3D +scratch_quit_done: + ret +SettleScratchOnQuit3D ENDP + +ScratchCellOrigin3D PROC cell:DWORD + cmp scrType3D,4 + jb @F + invoke ScratchPortraitBounds3D,cell + ret +@@: + cmp scrType3D,3 + jne @F + mov eax,200 + mov edx,176 + ret +@@: + mov eax,cell + xor edx,edx + mov ecx,3 + div ecx + imul eax,SCR_DY + add eax,SCR_Y + imul edx,SCR_DX + add edx,SCR_X + xchg eax,edx + cmp scrType3D,1 + jne @F + add edx,8 +@@: + cmp scrType3D,2 + jne @F + add eax,30 +@@: + ret +ScratchCellOrigin3D ENDP + +ScratchErasePixel3D PROC uses ebx esi edi px:DWORD,py:DWORD + cmp scrDealMs3D,0 + jne scratch_pixel_done + cmp scrState3D,1 + jne scratch_pixel_done + cmp scrType3D,4 + jb @F + invoke ScratchErasePortrait3D,px,py + ret +@@: + xor esi,esi +scratch_pixel_find: + invoke ScratchCellOrigin3D,esi + mov ebx,px + sub ebx,eax + cmp ebx,scrCellW3D + jae scratch_pixel_next_cell + mov eax,py + sub eax,edx + cmp eax,scrCellH3D + jb scratch_pixel_found +scratch_pixel_next_cell: + inc esi + cmp esi,scrCellCount3D + jb scratch_pixel_find + ret +scratch_pixel_found: + cmp scrRevealed3D[esi*4],0 + jne scratch_pixel_done + imul eax,scrCellW3D + mov edx,eax + add edx,ebx + imul edi,esi,SCR_MASK_AREA + add edx,edi + cmp scrMask3D[edx],0 + je scratch_pixel_done + mov scrMask3D[edx],0 + inc scrNewPixels3D + inc scrCounts3D[esi*4] + mov eax,scrRevealAt3D + cmp scrCounts3D[esi*4],eax + jb scratch_pixel_done + mov scrRevealed3D[esi*4],1 + mov eax,scrCellArea3D + mov scrCounts3D[esi*4],eax + add edi,OFFSET scrMask3D + mov ecx,SCR_MASK_AREA + xor eax,eax + rep stosb + inc scrOpened3D + call FinishScratch3D +scratch_pixel_done: + ret +ScratchErasePixel3D ENDP + +; A narrow ellipse matches the scraping rim, rather than a circular eraser. +ScratchStamp3D PROC uses ebx esi brushX:DWORD,brushY:DWORD + mov esi,-4 +scratch_stamp_y: + mov ebx,-10 +scratch_stamp_x: + mov eax,ebx + imul eax,eax + imul eax,16 + mov edx,esi + imul edx,edx + imul edx,100 + add eax,edx + cmp eax,1600 + ja scratch_stamp_next + mov eax,brushX + add eax,ebx + mov edx,brushY + add edx,esi + invoke ScratchErasePixel3D,eax,edx +scratch_stamp_next: + inc ebx + cmp ebx,10 + jle scratch_stamp_x + inc esi + cmp esi,4 + jle scratch_stamp_y + ret +ScratchStamp3D ENDP + +ScratchStroke3D PROC uses ebx esi edi x0:DWORD,y0:DWORD,x1:DWORD,y1:DWORD + local deltaX:DWORD,deltaY:DWORD,steps:DWORD,px:DWORD + mov eax,x1 + sub eax,x0 + mov deltaX,eax + cdq + xor eax,edx + sub eax,edx + mov ebx,eax + mov eax,y1 + sub eax,y0 + mov deltaY,eax + cdq + xor eax,edx + sub eax,edx + cmp eax,ebx + jbe @F + mov ebx,eax +@@: + mov eax,ebx + xor edx,edx + mov ecx,3 + div ecx + inc eax + mov steps,eax + xor esi,esi +scratch_stroke_step: + mov eax,deltaX + imul eax,esi + cdq + idiv steps + add eax,x0 + mov px,eax + mov eax,deltaY + imul eax,esi + cdq + idiv steps + add eax,y0 + invoke ScratchStamp3D,px,eax + inc esi + cmp esi,steps + jbe scratch_stroke_step + ret +ScratchStroke3D ENDP + +ScratchApplyPointer3D PROC + cmp scrDealMs3D,0 + jne scratch_apply_reset + cmp scrCatalog3D,0 + jne scratch_apply_reset + cmp scrHeld3D,1 + jne scratch_apply_reset + cmp scrPointerValid3D,1 + jne scratch_apply_reset + cmp scrState3D,1 + jne scratch_apply_reset + cmp scrLastValid3D,1 + je @F + mov eax,scrPointerX3D + mov scrLastX3D,eax + mov eax,scrPointerY3D + mov scrLastY3D,eax +@@: + invoke ScratchStroke3D,scrLastX3D,scrLastY3D,scrPointerX3D,scrPointerY3D + mov eax,scrPointerX3D + mov scrLastX3D,eax + mov eax,scrPointerY3D + mov scrLastY3D,eax + mov scrLastValid3D,1 + ret +scratch_apply_reset: + mov scrLastValid3D,0 + ret +ScratchApplyPointer3D ENDP + +; Return 1 only for messages owned by this scene. Other games retain their +; existing logical pointer coordinates and click behaviour unchanged. +ScratchMouseEvent3D PROC uses ebx hwnd:DWORD,msg:DWORD,wp:DWORD,lp:DWORD + cmp msg,20Ah + jne @F + cmp scrDealMs3D,0 + jne scratch_event_yes + cmp scrType3D,4 + jb scratch_event_yes + movsx eax,WORD PTR wp[2] + test eax,eax + jz scratch_event_yes + setg al + movzx eax,al + cmp eax,scrZoom3D + je scratch_event_yes + mov scrZoom3D,eax + call ScratchCancelStroke3D + jmp scratch_event_yes +@@: + cmp msg,20h + jne @F + cmp scrPointerValid3D,1 + jne scratch_event_no + invoke SetCursor,0 + jmp scratch_event_yes +@@: + cmp msg,215h + je scratch_event_lost + cmp msg,8 + jne @F + call ScratchCancelStroke3D + mov scrPointerValid3D,0 + jmp scratch_event_no +@@: + cmp msg,202h + jne @F + call ScratchCancelStroke3D + jmp scratch_event_yes +@@: + cmp msg,200h + je scratch_event_position + cmp msg,201h + je scratch_event_position + cmp msg,203h + jne scratch_event_no +scratch_event_position: + mov scrPointerValid3D,0 + cmp viewWidth3D,0 + je scratch_event_invalid + cmp viewHeight3D,0 + je scratch_event_invalid + movsx eax,WORD PTR lp + sub eax,viewLeft3D + cmp eax,viewWidth3D + jae scratch_event_invalid + imul eax,640 + xor edx,edx + div viewWidth3D + mov scrPointerX3D,eax + movsx eax,WORD PTR lp[2] + sub eax,viewTop3D + cmp eax,viewHeight3D + jae scratch_event_invalid + imul eax,480 + xor edx,edx + div viewHeight3D + mov scrPointerY3D,eax + mov scrPointerValid3D,1 + cmp msg,200h + je scratch_event_move + mov scrClick3D,1 + mov eax,scrPointerX3D + mov scrClickX3D,eax + mov eax,scrPointerY3D + mov scrClickY3D,eax + mov scrHeld3D,1 + mov scrLastValid3D,0 + invoke SetCapture,hwnd + call ScratchApplyPointer3D + jmp scratch_event_yes +scratch_event_move: + test wp,1 + jz @F + ; Process every event, including a complete quick gesture between ticks. + call ScratchApplyPointer3D + jmp scratch_event_yes +@@: + call ScratchCancelStroke3D + jmp scratch_event_yes +scratch_event_invalid: + mov scrLastValid3D,0 + jmp scratch_event_yes +scratch_event_lost: + mov scrHeld3D,0 + mov scrLastValid3D,0 +scratch_event_yes: + mov eax,1 + ret +scratch_event_no: + xor eax,eax + ret +ScratchMouseEvent3D ENDP + +HandleScratch3D PROC uses ebx esi + call ScratchTickVisuals3D + cmp scrCatalog3D,0 + je @F + call HandleScratchCatalog3D + ret +@@: + cmp scrClick3D,0 + je scratch_keys + mov scrClick3D,0 + cmp scrClickY3D,6 + jb scratch_keys + cmp scrClickY3D,34 + jae scratch_footer_hit + cmp scrClickX3D,504 + jb scratch_keys + cmp scrClickX3D,624 + jae scratch_keys + call LeaveScratch3D + ret +scratch_footer_hit: + cmp scrClickY3D,46 + jb scratch_keys + cmp scrClickY3D,76 + jae scratch_footer_buttons + cmp scrClickX3D,348 + jb scratch_keys + cmp scrClickX3D,624 + jae scratch_keys + call ScratchCancelStroke3D + mov scrCatalog3D,1 + mov scrCatalogLocked3D,0 + ret +scratch_footer_buttons: + cmp scrClickY3D,436 + jb scratch_keys + cmp scrClickY3D,472 + jae scratch_keys + cmp scrClickX3D,384 + jb scratch_price_hit + cmp scrClickX3D,624 + jae scratch_keys + call BuyScratch3D + jmp scratch_keys +scratch_price_hit: + cmp scrType3D,4 + jb @F + cmp scrClickX3D,344 + jb @F + cmp scrClickX3D,376 + jae @F + call ScratchToggleZoom3D + jmp scratch_keys +@@: + cmp scrState3D,1 + je scratch_keys + mov eax,scrClickX3D + sub eax,16 + js scratch_keys + xor edx,edx + mov ecx,108 + div ecx + cmp eax,2 + ja scratch_keys + cmp edx,100 + jae scratch_keys + mov scrPriceIndex3D,eax + mov scrError3D,0 +scratch_keys: + invoke Pressed3D,5Ah + test eax,eax + jz @F + call ScratchToggleZoom3D +@@: + invoke Pressed3D,25h + test eax,eax + jz @F + mov eax,scrType3D + dec eax + invoke ScratchSelectType3D,eax +@@: + invoke Pressed3D,27h + test eax,eax + jz @F + mov eax,scrType3D + inc eax + invoke ScratchSelectType3D,eax +@@: + mov ebx,31h +scratch_price_key: + invoke Pressed3D,ebx + test eax,eax + jz @F + cmp scrState3D,1 + je @F + mov eax,ebx + sub eax,31h + mov scrPriceIndex3D,eax + mov scrError3D,0 +@@: + inc ebx + cmp ebx,33h + jbe scratch_price_key + invoke Pressed3D,20h + test eax,eax + jz scratch_draw_stroke + call BuyScratch3D +scratch_draw_stroke: + call ScratchApplyPointer3D + cmp scrNewPixels3D,0 + je scratch_handle_done + call ScratchSpawnDust3D + mov scrNewPixels3D,0 + ret +scratch_handle_done: + ret +HandleScratch3D ENDP + +ScratchDrawContent3D PROC uses ebx esi edi cell:DWORD,originX:DWORD,originY:DWORD + local textX:DWORD,textY:DWORD,ink:DWORD + mov esi,cell + cmp scrType3D,1 + je scratch_content_number + cmp scrType3D,3 + je scratch_content_instant + cmp scrState3D,2 + jne @F + mov eax,scrGrid3D[esi*4] + cmp eax,scrPrizeSymbol3D + jne @F + mov eax,originX + shr eax,1 + mov textX,eax + mov eax,originY + shr eax,1 + mov textY,eax + mov eax,scrCellW3D + shr eax,1 + mov edx,scrCellH3D + shr edx,1 + invoke DrawBoxBorder,textX,textY,eax,edx,00F2EAD0h,00056837h,1 +@@: + mov ebx,38 + cmp scrType3D,2 + jne @F + mov ebx,30 +@@: + mov eax,scrCellW3D + sub eax,ebx + shr eax,1 + add eax,originX + mov edx,scrCellH3D + sub edx,ebx + shr edx,1 + add edx,originY + invoke DrawAnimSprite3D,OFFSET scrSymbolsArt3D,scrGrid3D[esi*4],eax,edx,ebx,ebx + ret +scratch_content_number: + mov ink,00303B32h + cmp esi,3 + jb scratch_content_num_draw + xor edi,edi +scratch_content_match: + mov eax,scrGrid3D[edi*4] + cmp eax,scrGrid3D[esi*4] + jne @F + mov ink,00056837h +@@: + inc edi + cmp edi,3 + jb scratch_content_match +scratch_content_num_draw: + mov eax,originX + add eax,36 + shr eax,1 + mov textX,eax + mov eax,originY + add eax,4 + shr eax,1 + mov textY,eax + invoke FormatNumber,scrGrid3D[esi*4],OFFSET hudNumBuffer + invoke DrawString8x8,textX,textY,OFFSET hudNumBuffer,ink + cmp esi,3 + jb scratch_content_done + invoke FormatNumber,scrCellPrizes3D[esi*4],OFFSET hudNumBuffer + mov eax,originX + add eax,32 + mov textX,eax + mov eax,originY + add eax,22 + invoke DrawBuckshotText3D,textX,eax,OFFSET hudNumBuffer,ink + ret +scratch_content_instant: + invoke DrawString8x8,128,100,OFFSET scratchAmount3D,00303B32h + invoke FormatNumber,scrGross3D,OFFSET hudNumBuffer + lea edi,hudNumBuffer + xor ecx,ecx +scratch_amount_length: + cmp BYTE PTR [edi+ecx],0 + je @F + inc ecx + jmp scratch_amount_length +@@: + imul eax,ecx,4 + mov ebx,160 + sub ebx,eax + invoke DrawString8x8,ebx,124,OFFSET hudNumBuffer,00056837h +scratch_content_done: + ret +ScratchDrawContent3D ENDP + +RenderScratch3D PROC uses ebx esi edi + local cell:DWORD,centerX:DWORD,centerY:DWORD,py:DWORD,px:DWORD,maskAt:DWORD,ink:DWORD + cmp scrReady3D,1 + jne scratch_render_missing + ; The ticket lies on an overhead writing mat, not in front of a dealer. + cmp scrTableArt3D.pixels,0 + je scratch_render_plain + mov esi,scrTableArt3D.pixels + lea edi,screenBuffer + mov ecx,SCREEN_SIZE + rep movsd + jmp scratch_render_paper +scratch_render_plain: + invoke DrawBox,0,0,320,240,001B4839h +scratch_render_paper: + call ScratchSaveBackdrop3D + cmp scrType3D,4 + jb @F + call RenderScratchPortrait3D + jmp scratch_render_hud +@@: + invoke DrawAnimSprite3D,OFFSET scrTicketArt3D,scrType3D,96,92,448,288 + mov eax,scrType3D + mov esi,scrNames3D[eax*4] + xor ecx,ecx +scratch_title_length: + cmp BYTE PTR [esi+ecx],0 + je @F + inc ecx + jmp scratch_title_length +@@: + imul eax,ecx,4 + mov ebx,160 + sub ebx,eax + invoke DrawString8x8,ebx,65,esi,00E5D29Eh + cmp scrType3D,1 + jne @F + invoke DrawBuckshotText3D,144,170,OFFSET scratchLucky3D,00303B32h + invoke DrawBuckshotText3D,144,220,OFFSET scratchYours3D,00303B32h +@@: + mov cell,0 +scratch_render_cell: + invoke ScratchCellOrigin3D,cell + mov centerX,eax + mov centerY,edx + cmp scrState3D,0 + je scratch_render_coating + invoke ScratchDrawContent3D,cell,centerX,centerY +scratch_render_coating: + mov eax,cell + imul eax,SCR_MASK_AREA + mov maskAt,eax + mov py,0 +scratch_coating_row: + mov eax,py + imul eax,scrCellW3D + mov esi,eax + add esi,maskAt + mov eax,centerY + add eax,py + imul eax,640 + add eax,centerX + lea edi,screenBuffer[eax*4] + mov px,0 +scratch_coating_pixel: + cmp scrMask3D[esi],0 + je scratch_coating_next +scratch_coating_opaque: + mov eax,py + xor edx,edx + mov ecx,SCR_H + div ecx + imul ebx,edx,SCR_W + mov eax,px + xor edx,edx + mov ecx,SCR_W + div ecx + lea eax,[ebx+edx] + mov edx,scrFoilArt3D.pixels + mov eax,[edx+eax*4] + mov [edi],eax +scratch_coating_next: + add edi,4 + inc esi + inc px + mov eax,scrCellW3D + cmp px,eax + jb scratch_coating_pixel + inc py + mov eax,scrCellH3D + cmp py,eax + jb scratch_coating_row + inc cell + mov eax,scrCellCount3D + cmp cell,eax + jb scratch_render_cell + ; Printed odds legend belongs on the paper, not over the scratch surface. + cmp scrType3D,1 + je scratch_print_rule + cmp scrType3D,3 + je scratch_print_rule + xor esi,esi +scratch_legend: + imul ebx,esi,43 + add ebx,176 + invoke DrawAnimSprite3D,OFFSET scrSymbolsArt3D,esi,ebx,338,16,16 + add ebx,18 + invoke FormatNumber,scrMultipliers3D[esi*4],OFFSET hudNumBuffer + invoke DrawBuckshotText3D,ebx,346,OFFSET hudNumBuffer,00303B32h + add ebx,16 + invoke DrawBuckshotText3D,ebx,346,OFFSET scrTimes3D,00303B32h + inc esi + cmp esi,7 + jb scratch_legend + jmp scratch_render_hud +scratch_print_rule: + mov eax,scrType3D + mov ebx,340 + cmp eax,1 + jne @F + mov ebx,328 +@@: + invoke DrawBuckshotText3D,176,ebx,scrRules3D[eax*4],00303B32h + jmp scratch_render_hud +scratch_render_missing: + invoke DrawBox,0,0,320,240,001B4839h + invoke DrawString8x8,104,112,OFFSET scratchMissing3D,00E99383h +scratch_render_hud: + cmp scrReady3D,1 + jne @F + call ScratchDrawDelivery3D + call ScratchDrawPayout3D + cmp scrType3D,4 + jb @F + call ScratchPortraitInfo3D +@@: + invoke DrawBox,0,0,320,20,0014191Dh + invoke DrawString8x8,12,6,OFFSET scratchTitle3D,00E5D29Eh + invoke DrawBoxBorder,252,3,60,14,00252D2Ah,0086754Eh,1 + invoke DrawString8x8,266,6,OFFSET coinExitText3D,00E5D29Eh + invoke DrawBox,4,23,148,15,0014191Dh + invoke DrawString8x8,8,27,OFFSET coinBankText3D,00E5D29Eh + invoke FormatNumber,bankroll,OFFSET hudNumBuffer + invoke DrawString8x8,56,27,OFFSET hudNumBuffer,00FFFFFFh + invoke DrawBoxBorder,174,23,138,15,00252D2Ah,0086754Eh,1 + invoke DrawBuckshotText3D,356,50,OFFSET scratchKind3D,00ADB9BCh + invoke DrawBuckshotText3D,608,57,OFFSET scrNext3D,00E5D29Eh + mov eax,scrType3D + invoke DrawBuckshotText3D,356,62,scrNames3D[eax*4],00E5D29Eh + invoke DrawBox,0,194,320,46,0014191Dh + invoke DrawBuckshotText3D,16,405,OFFSET scratchCost3D,00E5D29Eh + mov eax,scrPriceIndex3D + mov eax,scrPrices3D[eax*4] + cmp scrState3D,0 + je @F + mov eax,scrStake3D +@@: + invoke FormatNumber,eax,OFFSET hudNumBuffer + invoke DrawString8x8,56,201,OFFSET hudNumBuffer,00FFFFFFh + cmp scrError3D,0 + je scratch_status + invoke DrawString8x8,168,201,OFFSET scratchPoor3D,00E99383h + jmp scratch_buttons +scratch_status: + cmp scrState3D,2 + je scratch_result + cmp scrState3D,1 + jne scratch_rule + invoke DrawString8x8,168,201,OFFSET scratchWait3D,00E5D29Eh + invoke FormatNumber,scrOpened3D,OFFSET hudNumBuffer + invoke DrawString8x8,232,201,OFFSET hudNumBuffer,00FFFFFFh + jmp scratch_buttons +scratch_rule: + mov eax,scrType3D + invoke DrawBuckshotText3D,328,405,scrRules3D[eax*4],00ADB9BCh + jmp scratch_buttons +scratch_result: + invoke DrawBuckshotText3D,336,405,OFFSET scratchPayout3D,00ADB9BCh + invoke FormatNumber,scrGross3D,OFFSET hudNumBuffer + invoke DrawString8x8,212,201,OFFSET hudNumBuffer,0090D5ADh + call RenderScratchNet3D +scratch_buttons: + cmp scrType3D,4 + jb @F + invoke DrawBoxBorder,172,218,16,18,00252D2Ah,0086754Eh,1 + mov eax,OFFSET scrZoomIn3D + cmp scrZoom3D,0 + je scratch_zoom_label + mov eax,OFFSET scrZoomOut3D +scratch_zoom_label: + invoke DrawString8x8,176,223,eax,00E5D29Eh +@@: + xor esi,esi +scratch_price_button: + mov ink,0086754Eh + cmp scrState3D,1 + je @F + cmp esi,scrPriceIndex3D + jne @F + mov ink,00E5D29Eh +@@: + imul ebx,esi,54 + add ebx,8 + invoke DrawBoxBorder,ebx,218,50,18,00252D2Ah,ink,1 + add ebx,9 + invoke FormatNumber,scrPrices3D[esi*4],OFFSET hudNumBuffer + invoke DrawString8x8,ebx,223,OFFSET hudNumBuffer,ink + inc esi + cmp esi,3 + jb scratch_price_button + invoke DrawBoxBorder,192,218,120,18,00252D2Ah,0086754Eh,1 + mov eax,OFFSET scratchBuy3D + cmp scrState3D,2 + jne @F + mov eax,OFFSET scratchNext3D +@@: + cmp scrState3D,1 + jne @F + mov eax,OFFSET scratchActive3D +@@: + invoke DrawString8x8,224,223,eax,00E5D29Eh + cmp scrReady3D,1 + jne scratch_render_done + cmp scrCatalog3D,0 + je @F + call RenderScratchCatalog3D + jmp scratch_coin_draw +@@: + call ScratchDrawDust3D +scratch_coin_draw: + cmp scrPointerValid3D,1 + jne scratch_render_done + mov eax,scrPointerX3D + sub eax,32 + mov centerX,eax + mov eax,scrPointerY3D + sub eax,60 + mov centerY,eax + xor eax,eax + cmp scrHeld3D,1 + jne @F + mov eax,animationClock3D + shr eax,7 + and eax,3 +@@: + invoke DrawAnimSprite3D,OFFSET scrCoinArt3D,eax,centerX,centerY,64,64 +scratch_render_done: + ret +RenderScratch3D ENDP + +ScratchCanWalk3D PROC px:DWORD,py:DWORD + mov eax,px + sub eax,scrStation3D.posX + cdq + xor eax,edx + sub eax,edx + cmp eax,FP_1*19/20 + ja scratch_walk_clear + mov eax,py + sub eax,scrStation3D.posY + cdq + xor eax,edx + sub eax,edx + cmp eax,FP_1*2/5 + ja scratch_walk_clear + xor eax,eax + ret +scratch_walk_clear: + mov eax,1 + ret +ScratchCanWalk3D ENDP +include src_graphics/scratch_portrait.inc +include src_graphics/scratch_ui.inc +include src_graphics/scratch_effects.inc diff --git a/src_graphics/scratch_data.inc b/src_graphics/scratch_data.inc new file mode 100644 index 0000000..0d00527 --- /dev/null +++ b/src_graphics/scratch_data.inc @@ -0,0 +1,205 @@ +MODE_SCRATCH equ 22 +INTERACT_SCRATCH equ 25 +SCR_X equ 144 +SCR_Y equ 174 +SCR_W equ 104 +SCR_H equ 42 +SCR_DX equ 124 +SCR_DY equ 50 +SCR_AREA equ SCR_W*SCR_H +SCR_REVEAL equ SCR_AREA*72/100 +SCR_MASK_AREA equ 240*120 +SCR_PORTRAIT_STRIDE equ 128*96 +SCR_DUST_COUNT equ 64 +ScratchStartDeal3D PROTO +ScratchStartPayout3D PROTO +ScratchTickVisuals3D PROTO +ScratchSpawnDust3D PROTO +ScratchDrawDust3D PROTO +ScratchSaveBackdrop3D PROTO +ScratchDrawDelivery3D PROTO +ScratchDrawPayout3D PROTO +ScratchEase3D PROTO :DWORD,:DWORD +PlayCasinoSound3D PROTO :DWORD,:DWORD,:DWORD +ScratchConfigurePortrait3D PROTO +ScratchBuildPortrait3D PROTO +ScratchPortraitPayout3D PROTO +ScratchPortraitView3D PROTO +ScratchPortraitField3D PROTO :DWORD +ScratchPortraitBounds3D PROTO :DWORD +ScratchPortraitHit3D PROTO :DWORD,:DWORD +ScratchErasePortrait3D PROTO :DWORD,:DWORD +ScratchPortraitLayers3D PROTO :DWORD +RenderScratchPortrait3D PROTO +ScratchPortraitInfo3D PROTO +ScratchToggleZoom3D PROTO +ScratchPortraitMapBase3D PROTO +ScratchPortraitText3D PROTO :DWORD +HandleScratchCatalog3D PROTO +RenderScratchCatalog3D PROTO +RenderScratchNet3D PROTO +ScratchConfigure3D PROTO +ScratchSelectType3D PROTO :DWORD +ScratchBuildVariant3D PROTO +ScratchDrawContent3D PROTO :DWORD,:DWORD,:DWORD +LoadScratchArt3D PROTO +FreeScratchArt3D PROTO +BeginScratch3D PROTO +LeaveScratch3D PROTO +BuyScratch3D PROTO +FinishScratch3D PROTO +SettleScratchOnQuit3D PROTO +HandleScratch3D PROTO +RenderScratch3D PROTO +ScratchCancelStroke3D PROTO +ScratchMouseEvent3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +ScratchStamp3D PROTO :DWORD,:DWORD +ScratchErasePixel3D PROTO :DWORD,:DWORD +ScratchStroke3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +ScratchApplyPointer3D PROTO +ScratchCellOrigin3D PROTO :DWORD +ScratchCanWalk3D PROTO :DWORD,:DWORD +SetCapture PROTO :DWORD +ReleaseCapture PROTO +SetCursor PROTO :DWORD +.data +scrTicketArt3D AnimPack3D <> +scrTableArt3D AnimPack3D <> +scrCoinArt3D AnimPack3D <> +scrFoilArt3D AnimPack3D <> +scrSymbolsArt3D AnimPack3D <> +scrCounterArt3D AnimPack3D <> +scrPortraitArt3D AnimPack3D <> +scrPortraitMap3D AnimPack3D <> +scrPayoutArt3D AnimPack3D <> +scrPayoutPath3D db "assets\animations\scratch\payout-chips-v1.bin",0 +scrScrapeSound3D db "assets\audio\generated\scratch-scrape.wav",0 +scrPaperSound3D db "assets\audio\generated\scratch-paper.wav",0 +scrPayoutSound3D db "assets\audio\generated\scratch-payout.wav",0 +scrDealMs3D dd 0 +scrPayoutMs3D dd 0 +scrScrapeCooldown3D dd 0 +scrDustStep3D dd 0 +scrVisualSeed3D dd 713 +scrTicketPath3D db "assets\animations\scratch\ticket-v1.bin",0 +scrTablePath3D db "assets\animations\scratch\table-v1.bin",0 +scrCoinPath3D db "assets\animations\scratch\coin-edge-v1.bin",0 +scrFoilPath3D db "assets\animations\scratch\foil-v1.bin",0 +scrCounterPath3D db "assets\animations\scratch\counter-v1.bin",0 +scrPortraitPath3D db "assets\animations\scratch\portraits-v3.bin",0 +scrPortraitMapPath3D db "assets\animations\scratch\portrait-fields-v3.bin",0 +scrStation3D CityProp3D <29*FP_1,7*FP_1+FP_HALF,FP_1*8/5,FP_1*7/5,0,OFFSET scrCounterArt3D> +scrReady3D dd 0 +scrState3D dd 0 +scrType3D dd 0 +scrCellCount3D dd 9 +scrCellW3D dd SCR_W +scrCellH3D dd SCR_H +scrCellArea3D dd SCR_AREA +scrRevealAt3D dd SCR_REVEAL +scrPriceIndex3D dd 0 +scrStake3D dd 0 +scrGross3D dd 0 +scrPrizeSymbol3D dd -1 +scrOpened3D dd 0 +scrError3D dd 0 +scrPrices3D dd 50,200,500 +scrMultipliers3D dd 1,1,2,3,5,10,25 +scrCutoffs3D dd 700,800,910,960,985,997,1000 +scrGrid3D dd 24 dup(0) +scrCellPrizes3D dd 24 dup(0) +scrBag3D dd 60 dup(0) +scrCounts3D dd 24 dup(0) +scrRevealed3D dd 24 dup(0) +scrMask3D db 9*SCR_MASK_AREA dup(0) +scrPortraitMask3D db 24*SCR_PORTRAIT_STRIDE dup(0) +scrPortraitAreas3D dd 24 dup(0) +scrAwards3D dd 24 dup(0) +scrZoom3D dd 0 +scrCatalog3D dd 0 +scrCatalogLocked3D dd 0 +scrClickX3D dd 0 +scrClickY3D dd 0 +scrHitX3D dd 0 +scrHitY3D dd 0 +scrPortraitWins3D dd 5 +scrPortraitOutcome3D dd 0 +scrViewX3D dd 194 +scrViewY3D dd 46 +scrViewW3D dd 252 +scrViewH3D dd 336 +scrZoomIn3D db "+",0 +scrZoomOut3D db "-",0 +scrDouble3D db "x2",0 +scrTen3D db "x10",0 +scrHeld3D dd 0 +scrClick3D dd 0 +scrPointerValid3D dd 0 +scrPointerX3D dd 320 +scrPointerY3D dd 240 +scrLastValid3D dd 0 +scrLastX3D dd 0 +scrLastY3D dd 0 +scrNewPixels3D dd 0 +scrDustIndex3D dd 0 +scrDustX3D dd SCR_DUST_COUNT dup(0) +scrDustY3D dd SCR_DUST_COUNT dup(0) +scrDustLife3D dd SCR_DUST_COUNT dup(0) +scrDustVX3D dd SCR_DUST_COUNT dup(0) +scrDustVY3D dd SCR_DUST_COUNT dup(0) +scrDustFloor3D dd SCR_DUST_COUNT dup(0) +scrDustInk3D dd 00D8DCD8h,00868D89h,00B7BDB8h,00EEECE3h +scratchTitle3D db 203,206,210,197,208,197,223,0 +scratchGame3D db 199,206,203,206,210,192,223,32,210,208,206,201,202,192,0 +scratchBuy3D db 202,211,207,200,210,220,0 +scratchNext3D db 205,206,194,219,201,0 +scratchPrice3D db 214,197,205,192,0 +scratchWin3D db 207,208,200,199,0 +scratchLose3D db 193,197,199,32,194,219,200,195,208,219,216,192,0 +scratchWait3D db 206,210,202,208,219,210,206,0 +scratchPoor3D db 204,192,203,206,32,212,200,216,197,202,0 +scratchMissing3D db 205,197,210,32,209,207,208,192,201,210,206,194,0 +scratchHall3D db 69,58,32,203,206,210,197,208,197,201,205,219,197,32,193,200,203,197,210,219,0 +scratchRule3D db 51,32,206,196,200,205,192,202,206,194,219,213,32,61,32,207,208,200,199,0 +scratchActive3D db 193,200,203,197,210,0 +scrTimes3D db "x",0 +scrPrev3D db "<",0 +scrNext3D db ">",0 +scratchNumbers3D db 209,215,192,209,210,203,200,194,219,197,32,215,200,209,203,192,0 +scratchRows3D db 210,208,200,32,194,32,208,223,196,0 +scratchInstant3D db 204,200,195,32,211,196,192,215,200,0 +scratchNumberRule3D db 209,206,194,207,192,196,197,205,200,197,32,61,32,207,208,200,199,32,207,206,196,32,215,200,209,203,206,204,0 +scratchRowRule3D db 51,32,206,196,200,205,192,202,206,194,219,213,32,194,32,206,196,205,206,204,32,208,223,196,211,0 +scratchInstantRule3D db 207,208,200,199,32,211,202,192,199,192,205,32,207,206,196,32,199,192,217,200,210,206,201,0 +scratchLucky3D db 194,219,200,195,208,219,216,205,219,197,32,215,200,209,203,192,0 +scratchYours3D db 194,192,216,200,32,215,200,209,203,192,0 +scratchAmount3D db 194,192,216,32,207,208,200,199,0 +scratchNeon3D db 205,197,206,205,206,194,192,223,32,211,196,192,215,192,0 +scratchRuby3D db 208,211,193,200,205,206,194,219,201,32,196,198,197,202,207,206,210,0 +scratchSea3D db 194,206,203,205,192,32,209,206,202,208,206,194,200,217,0 +scratchBonusRule3D db 215,200,209,203,206,32,200,203,200,32,193,206,205,211,209,32,61,32,207,208,200,199,0 +scratchMatch3D db 209,206,194,207,192,196,197,205,200,197,58,32,207,208,200,199,0 +scratchNeonDouble3D db 120,50,58,32,196,194,206,201,205,206,201,32,207,208,200,199,0 +scratchNeonTen3D db 120,49,48,58,32,207,208,200,199,32,120,49,48,0 +scratchRubyDouble3D db 120,50,58,32,196,194,206,201,205,206,201,32,207,208,200,199,0 +scratchRubyAll3D db 194,209,197,58,32,194,209,197,32,207,208,200,199,219,0 +scratchSeaDouble3D db 120,50,58,32,196,194,206,201,205,206,201,32,207,208,200,199,0 +scratchSeaAll3D db 194,209,197,58,32,194,209,197,32,207,208,200,199,219,0 +scratchAll3D db 194,209,197,0 +scratchField3D db 207,206,203,197,0 +scratchKind3D db 194,200,196,32,193,200,203,197,210,192,0 +scratchCost3D db 214,197,205,192,32,193,200,203,197,210,192,0 +scratchPayout3D db 194,219,207,203,192,210,192,0 +scratchNet3D db 215,200,209,210,219,201,32,200,210,206,195,0 +scratchPending3D db 193,200,203,197,210,32,205,197,32,206,210,202,208,219,210,0 +scrEquals3D db "=",0 +scrNames3D dd OFFSET scratchGame3D,OFFSET scratchNumbers3D,OFFSET scratchRows3D,OFFSET scratchInstant3D + dd OFFSET scratchNeon3D,OFFSET scratchRuby3D,OFFSET scratchSea3D +scrRules3D dd OFFSET scratchRule3D,OFFSET scratchNumberRule3D,OFFSET scratchRowRule3D,OFFSET scratchInstantRule3D + dd 3 dup(OFFSET scratchBonusRule3D) +scrPaperLayerPack3D AnimPack3D <314D4E41h,640,480,1,640*480*4,OFFSET scrPaperLayer3D> +.data? +scrBackdrop3D dd 640*480 dup(?) +scrPaperLayer3D dd 640*480 dup(?) +.code +include src_graphics/scratch_portrait_fields.inc diff --git a/src_graphics/scratch_effects.inc b/src_graphics/scratch_effects.inc new file mode 100644 index 0000000..1123d06 --- /dev/null +++ b/src_graphics/scratch_effects.inc @@ -0,0 +1,377 @@ +; Presentation state has its own clock and RNG; it never changes ticket odds. +ScratchEase3D PROC elapsed:DWORD,duration:DWORD + mov eax,elapsed + cmp eax,duration + jb @F + mov eax,256 + ret +@@: + shl eax,8 + xor edx,edx + div duration + mov ecx,eax + imul eax,eax + shl ecx,1 + neg ecx + add ecx,768 + imul eax,ecx + shr eax,16 + ret +ScratchEase3D ENDP + +ScratchStartDeal3D PROC + mov scrDealMs3D,900 + mov scrPayoutMs3D,0 + invoke PlayCasinoSound3D,OFFSET scrPaperSound3D,0,1 + ret +ScratchStartDeal3D ENDP + +ScratchStartPayout3D PROC + mov scrDealMs3D,0 + mov scrPayoutMs3D,0 + cmp engineMode,MODE_SCRATCH + jne @F + cmp scrGross3D,0 + je @F + mov scrPayoutMs3D,1800 + invoke PlayCasinoSound3D,OFFSET scrPayoutSound3D,0,1 +@@: + ret +ScratchStartPayout3D ENDP + +ScratchTickVisuals3D PROC uses ebx esi + mov eax,deltaTime3D + cmp eax,250 + jbe @F + mov eax,250 +@@: + sub scrDealMs3D,eax + jns @F + mov scrDealMs3D,0 +@@: + sub scrPayoutMs3D,eax + jns @F + mov scrPayoutMs3D,0 +@@: + sub scrScrapeCooldown3D,eax + jns @F + mov scrScrapeCooldown3D,0 +@@: + cmp eax,100 + jbe @F + mov eax,100 +@@: + add scrDustStep3D,eax +scratch_visual_step: + cmp scrDustStep3D,8 + jb scratch_visual_done + sub scrDustStep3D,8 + xor esi,esi +scratch_visual_particle: + cmp scrDustLife3D[esi*4],0 + je scratch_visual_next + sub scrDustLife3D[esi*4],8 + jns @F + mov scrDustLife3D[esi*4],0 +@@: + mov eax,scrDustY3D[esi*4] + cmp eax,scrDustFloor3D[esi*4] + jge scratch_visual_next + mov eax,scrDustVX3D[esi*4] + add scrDustX3D[esi*4],eax + add scrDustVY3D[esi*4],6 + mov eax,scrDustVY3D[esi*4] + add scrDustY3D[esi*4],eax + mov eax,scrDustFloor3D[esi*4] + cmp scrDustY3D[esi*4],eax + jl scratch_visual_next + mov scrDustY3D[esi*4],eax +scratch_visual_next: + inc esi + cmp esi,SCR_DUST_COUNT + jb scratch_visual_particle + jmp scratch_visual_step +scratch_visual_done: + ret +ScratchTickVisuals3D ENDP + +ScratchSpawnDust3D PROC uses ebx esi edi + cmp scrState3D,1 + jne scratch_spawn_begin + cmp scrScrapeCooldown3D,0 + jne @F + invoke PlayCasinoSound3D,OFFSET scrScrapeSound3D,0,1 + mov scrScrapeCooldown3D,150 +@@: +scratch_spawn_begin: + mov edi,3 +scratch_spawn_fragment: + mov esi,scrDustIndex3D + mov eax,scrVisualSeed3D + imul eax,1664525 + add eax,1013904223 + mov scrVisualSeed3D,eax + mov ebx,eax + and eax,511 + add eax,1200 + mov scrDustLife3D[esi*4],eax + mov eax,scrPointerX3D + shl eax,8 + mov scrDustX3D[esi*4],eax + mov eax,scrPointerY3D + shl eax,8 + mov scrDustY3D[esi*4],eax + mov eax,ebx + shr eax,12 + and eax,255 + sub eax,128 + mov scrDustVX3D[esi*4],eax + mov eax,ebx + shr eax,20 + and eax,127 + add eax,64 + neg eax + mov scrDustVY3D[esi*4],eax + mov eax,ebx + shr eax,8 + and eax,31 + add eax,scrPointerY3D + add eax,20 + cmp eax,385 + jbe @F + mov eax,385 +@@: + shl eax,8 + mov scrDustFloor3D[esi*4],eax + inc esi + and esi,SCR_DUST_COUNT-1 + mov scrDustIndex3D,esi + dec edi + jnz scratch_spawn_fragment + ret +ScratchSpawnDust3D ENDP + +ScratchDrawDust3D PROC uses ebx esi edi + xor esi,esi +scratch_draw_fragment: + cmp scrDustLife3D[esi*4],0 + je scratch_draw_fragment_next + mov eax,scrDustX3D[esi*4] + sar eax,8 + cmp eax,638 + jae scratch_draw_fragment_next + mov edx,scrDustY3D[esi*4] + sar edx,8 + cmp edx,40 + jb scratch_draw_fragment_next + cmp edx,386 + jae scratch_draw_fragment_next + imul edx,640 + add edx,eax + mov ebx,esi + and ebx,3 + mov eax,scrDustInk3D[ebx*4] + mov screenBuffer[edx*4],eax + mov screenBuffer[edx*4+4],eax + test esi,1 + jz scratch_draw_fragment_next + mov screenBuffer[edx*4+640*4],00868D89h +scratch_draw_fragment_next: + inc esi + cmp esi,SCR_DUST_COUNT + jb scratch_draw_fragment + ret +ScratchDrawDust3D ENDP + +ScratchSaveBackdrop3D PROC uses esi edi + cmp scrDealMs3D,350 + jbe @F + lea esi,screenBuffer[640*40*4] + lea edi,scrBackdrop3D[640*40*4] + mov ecx,640*348 + rep movsd +@@: + ret +ScratchSaveBackdrop3D ENDP + +ScratchDrawDelivery3D PROC uses ebx esi edi + local left:DWORD,top:DWORD,right:DWORD,bottom:DWORD,rowY:DWORD,pointX:DWORD + local offsetX:DWORD,offsetY:DWORD,sourceBase:DWORD + cmp scrDealMs3D,350 + jbe scratch_delivery_done + mov eax,900 + sub eax,scrDealMs3D + invoke ScratchEase3D,eax,550 + neg eax + add eax,256 + imul edx,eax,360 + sar edx,8 + mov offsetY,edx + imul eax,-56 + sar eax,8 + mov offsetX,eax + lea edi,scrPaperLayer3D + xor eax,eax + mov ecx,640*480 + rep stosd + mov left,96 + mov top,92 + mov right,544 + mov bottom,380 + cmp scrType3D,4 + jae scratch_delivery_portrait + mov eax,scrType3D + imul eax,448*288*4 + add eax,scrTicketArt3D.pixels + mov sourceBase,eax + jmp scratch_delivery_bounds +scratch_delivery_portrait: + mov eax,scrViewX3D + mov left,eax + add eax,scrViewW3D + mov right,eax + mov eax,scrViewY3D + mov top,eax + add eax,scrViewH3D + mov bottom,eax +scratch_delivery_bounds: + cmp top,40 + jge @F + mov top,40 +@@: + cmp bottom,388 + jle @F + mov bottom,388 +@@: + mov eax,top + mov rowY,eax +scratch_delivery_row: + mov eax,left + mov pointX,eax +scratch_delivery_pixel: + cmp scrType3D,4 + jae scratch_delivery_copy + mov eax,rowY + sub eax,92 + imul eax,448 + add eax,pointX + sub eax,96 + mov edi,sourceBase + cmp BYTE PTR [edi+eax*4+3],0 + je scratch_delivery_next +scratch_delivery_copy: + imul ebx,rowY,640 + add ebx,pointX + mov eax,screenBuffer[ebx*4] + or eax,0FF000000h + mov scrPaperLayer3D[ebx*4],eax +scratch_delivery_next: + inc pointX + mov eax,pointX + cmp eax,right + jb scratch_delivery_pixel + inc rowY + mov eax,rowY + cmp eax,bottom + jb scratch_delivery_row + lea esi,scrBackdrop3D[640*40*4] + lea edi,screenBuffer[640*40*4] + mov ecx,640*348 + rep movsd + ; A clipped, paper-shaped contact shadow, never a rectangular overlay. + mov eax,top + mov rowY,eax +scratch_delivery_shadow_row: + mov eax,left + mov pointX,eax +scratch_delivery_shadow_pixel: + imul ebx,rowY,640 + add ebx,pointX + cmp BYTE PTR scrPaperLayer3D[ebx*4+3],0 + je scratch_delivery_shadow_next + mov eax,pointX + add eax,offsetX + add eax,4 + cmp eax,640 + jae scratch_delivery_shadow_next + mov edx,rowY + add edx,offsetY + add edx,6 + cmp edx,40 + jb scratch_delivery_shadow_next + cmp edx,388 + jae scratch_delivery_shadow_next + imul edx,640 + add edx,eax + mov eax,screenBuffer[edx*4] + mov ecx,eax + and ecx,00FCFCFCh + shr ecx,2 + sub eax,ecx + mov screenBuffer[edx*4],eax +scratch_delivery_shadow_next: + inc pointX + mov eax,pointX + cmp eax,right + jb scratch_delivery_shadow_pixel + inc rowY + mov eax,rowY + cmp eax,bottom + jb scratch_delivery_shadow_row + invoke DrawAnimSprite3D,OFFSET scrPaperLayerPack3D,0,offsetX,offsetY,640,480 +scratch_delivery_done: + ret +ScratchDrawDelivery3D ENDP + +ScratchDrawPayout3D PROC uses ebx esi edi + local elapsed:DWORD,progress:DWORD,coinX:DWORD,coinY:DWORD + cmp scrState3D,2 + jne scratch_payout_done + cmp scrGross3D,0 + je scratch_payout_done + mov eax,1800 + sub eax,scrPayoutMs3D + mov elapsed,eax + invoke ScratchEase3D,eax,400 + imul eax,96 + sar eax,8 + sub eax,88 + invoke DrawAnimSprite3D,OFFSET scrPayoutArt3D,0,eax,300,88,66 + xor esi,esi +scratch_payout_coin: + imul eax,esi,110 + add eax,350 + mov edx,elapsed + sub edx,eax + js scratch_payout_coin_next + cmp edx,850 + jae scratch_payout_coin_next + invoke ScratchEase3D,edx,850 + mov progress,eax + imul ebx,eax,60 + sar ebx,8 + add ebx,40 + mov edx,256 + sub edx,eax + imul edx,eax + imul edx,80 + sar edx,16 + add ebx,edx + mov coinX,ebx + imul eax,260 + sar eax,8 + neg eax + add eax,330 + mov coinY,eax + mov eax,elapsed + shr eax,6 + add eax,esi + and eax,3 + invoke DrawAnimSprite3D,OFFSET scrCoinArt3D,eax,coinX,coinY,20,20 +scratch_payout_coin_next: + inc esi + cmp esi,5 + jb scratch_payout_coin +scratch_payout_done: + ret +ScratchDrawPayout3D ENDP diff --git a/src_graphics/scratch_portrait.inc b/src_graphics/scratch_portrait.inc new file mode 100644 index 0000000..5c2bf5c --- /dev/null +++ b/src_graphics/scratch_portrait.inc @@ -0,0 +1,720 @@ +; Source-space masks keep the supplied ticket art intact across both views. +ScratchPortraitField3D PROC cell:DWORD + mov eax,scrType3D + sub eax,4 + imul eax,24 + add eax,cell + shl eax,4 + add eax,OFFSET scrPortraitFields3D + ret +ScratchPortraitField3D ENDP + +ScratchPortraitMapBase3D PROC + mov eax,scrType3D + sub eax,4 + imul eax,540*720*4 + add eax,scrPortraitMap3D.pixels + ret +ScratchPortraitMapBase3D ENDP + +ScratchPortraitView3D PROC + mov scrViewX3D,194 + mov scrViewY3D,46 + mov scrViewW3D,252 + mov scrViewH3D,336 + cmp scrZoom3D,0 + je @F + mov scrViewX3D,68 + mov scrViewY3D,-230 + mov scrViewW3D,504 + mov scrViewH3D,672 + cmp scrDealMs3D,0 + je @F + mov eax,350 + sub eax,scrDealMs3D + jns portrait_view_ease + xor eax,eax +portrait_view_ease: + invoke ScratchEase3D,eax,350 + imul ecx,eax,252 + sar ecx,8 + add ecx,252 + mov scrViewW3D,ecx + imul ecx,eax,336 + sar ecx,8 + add ecx,336 + mov scrViewH3D,ecx + imul ecx,eax,126 + sar ecx,8 + neg ecx + add ecx,194 + mov scrViewX3D,ecx + imul ecx,eax,276 + sar ecx,8 + neg ecx + add ecx,46 + mov scrViewY3D,ecx +@@: + ret +ScratchPortraitView3D ENDP + +ScratchToggleZoom3D PROC + cmp scrDealMs3D,0 + jne @F + cmp scrType3D,4 + jb @F + xor scrZoom3D,1 + call ScratchCancelStroke3D +@@: + ret +ScratchToggleZoom3D ENDP + +ScratchConfigurePortrait3D PROC uses ebx esi edi + local rowY:DWORD,pointX:DWORD,fieldPtr:DWORD,mapBase:DWORD,maskBase:DWORD + mov eax,scrType3D + sub eax,4 + mov ecx,scrPortraitWinning3D[eax*4] + mov scrPortraitWins3D,ecx + add ecx,scrPortraitPlayers3D[eax*4] + mov scrCellCount3D,ecx + lea edi,scrPortraitMask3D + xor eax,eax + mov ecx,24*SCR_PORTRAIT_STRIDE + rep stosb + lea edi,scrPortraitAreas3D + mov ecx,24 + rep stosd + cmp scrPortraitMap3D.pixels,0 + je portrait_config_done + call ScratchPortraitMapBase3D + mov mapBase,eax + xor esi,esi +portrait_config_field: + invoke ScratchPortraitField3D,esi + mov fieldPtr,eax + imul eax,esi,SCR_PORTRAIT_STRIDE + add eax,OFFSET scrPortraitMask3D + mov maskBase,eax + mov rowY,0 +portrait_config_row: + mov pointX,0 +portrait_config_pixel: + mov ebx,fieldPtr + mov eax,rowY + add eax,[ebx+4] + imul eax,540 + add eax,[ebx] + add eax,pointX + mov edx,mapBase + movzx eax,BYTE PTR [edx+eax*4] + dec eax + cmp eax,esi + jne portrait_config_next + mov eax,rowY + imul eax,[ebx+8] + add eax,pointX + mov edi,maskBase + mov BYTE PTR [edi+eax],255 + inc scrPortraitAreas3D[esi*4] +portrait_config_next: + inc pointX + mov eax,pointX + cmp eax,[ebx+8] + jb portrait_config_pixel + inc rowY + mov eax,rowY + cmp eax,[ebx+12] + jb portrait_config_row + inc esi + cmp esi,scrCellCount3D + jb portrait_config_field +portrait_config_done: + ret +ScratchConfigurePortrait3D ENDP + +ScratchPortraitBounds3D PROC uses ebx esi cell:DWORD + local left:DWORD,top:DWORD + call ScratchPortraitView3D + invoke ScratchPortraitField3D,cell + mov esi,eax + mov eax,[esi] + imul eax,scrViewW3D + xor edx,edx + mov ebx,540 + div ebx + add eax,scrViewX3D + mov left,eax + mov eax,[esi] + add eax,[esi+8] + imul eax,scrViewW3D + xor edx,edx + div ebx + add eax,scrViewX3D + sub eax,left + inc eax + mov scrCellW3D,eax + mov eax,[esi+4] + imul eax,scrViewH3D + xor edx,edx + mov ebx,720 + div ebx + add eax,scrViewY3D + mov top,eax + mov eax,[esi+4] + add eax,[esi+12] + imul eax,scrViewH3D + xor edx,edx + div ebx + add eax,scrViewY3D + sub eax,top + inc eax + mov scrCellH3D,eax + mov eax,left + mov edx,top + ret +ScratchPortraitBounds3D ENDP + +ScratchPortraitHit3D PROC uses ebx px:DWORD,py:DWORD + cmp scrPortraitMap3D.pixels,0 + je portrait_hit_no + cmp py,40 + jb portrait_hit_no + cmp py,388 + jae portrait_hit_no + call ScratchPortraitView3D + mov eax,px + sub eax,scrViewX3D + cmp eax,scrViewW3D + jae portrait_hit_no + imul eax,540 + xor edx,edx + div scrViewW3D + mov scrHitX3D,eax + mov eax,py + sub eax,scrViewY3D + cmp eax,scrViewH3D + jae portrait_hit_no + imul eax,720 + xor edx,edx + div scrViewH3D + mov scrHitY3D,eax + imul ebx,eax,540 + add ebx,scrHitX3D + call ScratchPortraitMapBase3D + movzx eax,BYTE PTR [eax+ebx*4] + dec eax + cmp eax,scrCellCount3D + jb portrait_hit_done +portrait_hit_no: + mov eax,-1 +portrait_hit_done: + ret +ScratchPortraitHit3D ENDP + +ScratchErasePortrait3D PROC uses ebx esi edi px:DWORD,py:DWORD + local rowY:DWORD,startX:DWORD,endX:DWORD,endY:DWORD + invoke ScratchPortraitHit3D,px,py + cmp eax,-1 + je portrait_erase_done + mov esi,eax + cmp scrRevealed3D[esi*4],0 + jne portrait_erase_done + invoke ScratchPortraitField3D,esi + mov ebx,eax + imul edi,esi,SCR_PORTRAIT_STRIDE + add edi,OFFSET scrPortraitMask3D + ; A screen pixel covers multiple source pixels in the full-ticket view. + mov eax,scrHitX3D + sub eax,[ebx] + mov startX,eax + mov eax,540 + add eax,scrViewW3D + dec eax + xor edx,edx + div scrViewW3D + add eax,startX + cmp eax,[ebx+8] + jbe @F + mov eax,[ebx+8] +@@: + mov endX,eax + mov eax,scrHitY3D + sub eax,[ebx+4] + mov rowY,eax + mov eax,720 + add eax,scrViewH3D + dec eax + xor edx,edx + div scrViewH3D + add eax,rowY + cmp eax,[ebx+12] + jbe @F + mov eax,[ebx+12] +@@: + mov endY,eax +portrait_erase_row: + mov eax,rowY + imul eax,[ebx+8] + add eax,startX + mov ecx,startX +portrait_erase_point: + cmp BYTE PTR [edi+eax],0 + je portrait_erase_next + mov BYTE PTR [edi+eax],0 + inc scrCounts3D[esi*4] + inc scrNewPixels3D +portrait_erase_next: + inc eax + inc ecx + cmp ecx,endX + jb portrait_erase_point + inc rowY + mov eax,rowY + cmp eax,endY + jb portrait_erase_row + mov eax,scrCounts3D[esi*4] + imul eax,100 + imul edx,scrPortraitAreas3D[esi*4],72 + cmp eax,edx + jb portrait_erase_done + mov scrRevealed3D[esi*4],1 + mov eax,scrPortraitAreas3D[esi*4] + mov scrCounts3D[esi*4],eax + xor eax,eax + mov ecx,SCR_PORTRAIT_STRIDE + rep stosb + inc scrOpened3D + call FinishScratch3D +portrait_erase_done: + ret +ScratchErasePortrait3D ENDP + +ScratchBuildPortrait3D PROC uses ebx esi edi + local first:DWORD,chosen:DWORD + xor esi,esi +portrait_pool: + lea eax,[esi+1] + mov scrBag3D[esi*4],eax + inc esi + cmp esi,60 + jb portrait_pool + mov esi,59 +portrait_shuffle: + lea eax,[esi+1] + invoke RandomRange3D,eax + mov ebx,scrBag3D[esi*4] + mov edx,scrBag3D[eax*4] + mov scrBag3D[esi*4],edx + mov scrBag3D[eax*4],ebx + dec esi + jnz portrait_shuffle + xor esi,esi +portrait_fill: + mov eax,scrBag3D[esi*4] + mov scrGrid3D[esi*4],eax + invoke RandomRange3D,7 + mov eax,scrMultipliers3D[eax*4] + imul eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + inc esi + cmp esi,scrCellCount3D + jb portrait_fill + mov scrPortraitOutcome3D,0 + invoke RandomRange3D,1000 + cmp eax,600 + jb portrait_build_payout + mov ebx,eax + mov eax,scrCellCount3D + sub eax,scrPortraitWins3D + invoke RandomRange3D,eax + add eax,scrPortraitWins3D + mov esi,eax + cmp ebx,900 + jb portrait_number_win + cmp ebx,980 + jb portrait_double_win + cmp scrType3D,4 + je portrait_ten_win + cmp ebx,995 + jae portrait_all_win + mov scrPortraitOutcome3D,4 + mov scrCellPrizes3D[esi*4],0 + mov eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + mov eax,scrGrid3D + mov scrGrid3D[esi*4],eax + inc esi + cmp esi,scrCellCount3D + jb @F + mov esi,scrPortraitWins3D +@@: + mov eax,scrGrid3D[4] + mov scrGrid3D[esi*4],eax + mov eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + jmp portrait_build_payout +portrait_number_win: + mov scrPortraitOutcome3D,1 + invoke RandomRange3D,scrPortraitWins3D + mov eax,scrGrid3D[eax*4] + mov scrGrid3D[esi*4],eax + invoke RandomRange3D,1000 + mov ebx,700 + mov edi,990 + cmp scrType3D,4 + je @F + mov ebx,650 + mov edi,980 +@@: + mov ecx,1 + cmp eax,ebx + jb portrait_number_amount + mov ecx,2 + add ebx,200 + cmp eax,ebx + jb portrait_number_amount + mov ecx,5 + cmp eax,edi + jb portrait_number_amount + mov ecx,25 +portrait_number_amount: + imul ecx,scrStake3D + mov scrCellPrizes3D[esi*4],ecx + jmp portrait_build_payout +portrait_double_win: + mov scrPortraitOutcome3D,2 + mov scrGrid3D[esi*4],61 + mov eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + jmp portrait_build_payout +portrait_ten_win: + mov scrPortraitOutcome3D,3 + mov scrGrid3D[esi*4],62 + mov eax,scrStake3D + mov scrCellPrizes3D[esi*4],eax + jmp portrait_build_payout +portrait_all_win: + mov scrPortraitOutcome3D,5 + mov scrGrid3D[esi*4],62 + mov esi,scrPortraitWins3D + mov eax,scrStake3D + xor edx,edx + mov ecx,5 + div ecx +portrait_all_amount: + mov scrCellPrizes3D[esi*4],eax + inc esi + cmp esi,scrCellCount3D + jb portrait_all_amount +portrait_build_payout: + call ScratchPortraitPayout3D + mov scrGross3D,eax + ret +ScratchBuildPortrait3D ENDP + +; All-prizes overrides the individual awards, rather than paying them twice. +ScratchPortraitPayout3D PROC uses ebx esi edi + local total:DWORD,allPrizes:DWORD + mov total,0 + mov allPrizes,0 + mov esi,scrPortraitWins3D +portrait_award: + mov scrAwards3D[esi*4],0 + mov eax,scrGrid3D[esi*4] + cmp eax,61 + je portrait_award_double + cmp eax,62 + je portrait_award_special + xor edi,edi +portrait_award_match: + cmp eax,scrGrid3D[edi*4] + je portrait_award_normal + inc edi + cmp edi,scrPortraitWins3D + jb portrait_award_match + jmp portrait_award_next +portrait_award_normal: + mov eax,scrCellPrizes3D[esi*4] + jmp portrait_award_store +portrait_award_double: + imul eax,scrCellPrizes3D[esi*4],2 + jmp portrait_award_store +portrait_award_special: + cmp scrType3D,4 + je portrait_award_ten + mov allPrizes,1 + jmp portrait_award_next +portrait_award_ten: + imul eax,scrCellPrizes3D[esi*4],10 +portrait_award_store: + mov scrAwards3D[esi*4],eax + add total,eax +portrait_award_next: + inc esi + cmp esi,scrCellCount3D + jb portrait_award + cmp allPrizes,0 + je portrait_award_done + mov total,0 + mov esi,scrPortraitWins3D +portrait_award_all: + mov eax,scrCellPrizes3D[esi*4] + mov scrAwards3D[esi*4],eax + add total,eax + inc esi + cmp esi,scrCellCount3D + jb portrait_award_all +portrait_award_done: + mov eax,total + ret +ScratchPortraitPayout3D ENDP + +ScratchPortraitLayers3D PROC uses ebx esi edi coating:DWORD + local cell:DWORD,fieldPtr:DWORD,mapBase:DWORD,left:DWORD,top:DWORD,right:DWORD,bottom:DWORD + local screenX:DWORD,screenY:DWORD,sourceRow:DWORD,sourceY:DWORD,sourceX:DWORD,maskRow:DWORD + call ScratchPortraitMapBase3D + mov mapBase,eax + mov cell,0 +portrait_layer_cell: + invoke ScratchPortraitBounds3D,cell + mov left,eax + add eax,scrCellW3D + mov right,eax + mov top,edx + add edx,scrCellH3D + mov bottom,edx + invoke ScratchPortraitField3D,cell + mov fieldPtr,eax + mov eax,top + mov screenY,eax +portrait_layer_row: + cmp screenY,40 + jb portrait_layer_next_row + cmp screenY,388 + jae portrait_layer_next_row + mov eax,screenY + sub eax,scrViewY3D + imul eax,720 + xor edx,edx + div scrViewH3D + mov sourceY,eax + imul eax,540 + mov sourceRow,eax + mov ebx,fieldPtr + mov eax,sourceY + sub eax,[ebx+4] + cmp eax,[ebx+12] + jae portrait_layer_next_row + imul eax,[ebx+8] + imul edx,cell,SCR_PORTRAIT_STRIDE + add eax,edx + mov maskRow,eax + mov eax,left + mov screenX,eax +portrait_layer_pixel: + cmp screenX,640 + jae portrait_layer_next_pixel + mov eax,screenX + sub eax,scrViewX3D + imul eax,540 + xor edx,edx + div scrViewW3D + mov sourceX,eax + add eax,sourceRow + mov edi,mapBase + movzx eax,BYTE PTR [edi+eax*4] + dec eax + cmp eax,cell + jne portrait_layer_next_pixel + mov ebx,fieldPtr + mov eax,sourceX + sub eax,[ebx] + add eax,maskRow + cmp coating,0 + je portrait_layer_paper + ; Coating is indexed in source pixels so zoom cannot restore old scratches. + cmp scrPortraitMask3D[eax],0 + je portrait_layer_next_pixel + mov eax,sourceY + xor edx,edx + mov ecx,42 + div ecx + imul esi,edx,104 + mov eax,sourceX + xor edx,edx + mov ecx,104 + div ecx + add esi,edx + mov edi,scrFoilArt3D.pixels + mov eax,[edi+esi*4] + jmp portrait_layer_write +portrait_layer_paper: + mov eax,00F3EBD7h + cmp scrState3D,2 + jne portrait_layer_write + mov edx,cell + cmp edx,scrPortraitWins3D + jb portrait_layer_write + cmp scrAwards3D[edx*4],0 + je portrait_layer_write + mov eax,00D5EFDCh +portrait_layer_write: + imul edx,screenY,640 + add edx,screenX + mov screenBuffer[edx*4],eax +portrait_layer_next_pixel: + inc screenX + mov eax,screenX + cmp eax,right + jb portrait_layer_pixel +portrait_layer_next_row: + inc screenY + mov eax,screenY + cmp eax,bottom + jb portrait_layer_row + inc cell + mov eax,cell + cmp eax,scrCellCount3D + jb portrait_layer_cell + ret +ScratchPortraitLayers3D ENDP + +ScratchPortraitText3D PROC uses ebx esi edi cell:DWORD + local left:DWORD,top:DWORD,ink:DWORD,textPtr:DWORD + invoke ScratchPortraitBounds3D,cell + mov left,eax + mov top,edx + mov esi,cell + mov ink,00303B32h + cmp esi,scrPortraitWins3D + jb @F + cmp scrAwards3D[esi*4],0 + je @F + mov ink,00056837h +@@: + mov eax,scrGrid3D[esi*4] + cmp eax,61 + je portrait_text_double + cmp eax,62 + je portrait_text_special + invoke FormatNumber,eax,OFFSET hudNumBuffer + mov textPtr,OFFSET hudNumBuffer + jmp portrait_text_number +portrait_text_double: + mov textPtr,OFFSET scrDouble3D + jmp portrait_text_number +portrait_text_special: + mov textPtr,OFFSET scrTen3D + cmp scrType3D,4 + je portrait_text_number + mov textPtr,OFFSET scratchAll3D +portrait_text_number: + mov edi,textPtr + xor ecx,ecx +portrait_text_length: + cmp BYTE PTR [edi+ecx],0 + je @F + inc ecx + jmp portrait_text_length +@@: + shl ecx,3 + mov eax,scrCellW3D + sub eax,ecx + sar eax,1 + add eax,left + mov ebx,eax + mov eax,scrCellH3D + shr eax,1 + add eax,top + sub eax,4 + cmp esi,scrPortraitWins3D + jb @F + cmp scrZoom3D,0 + je @F + sub eax,7 +@@: + invoke DrawBuckshotText3D,ebx,eax,textPtr,ink + cmp esi,scrPortraitWins3D + jb portrait_text_done + cmp scrZoom3D,0 + je portrait_text_done + invoke FormatNumber,scrCellPrizes3D[esi*4],OFFSET hudNumBuffer + lea edi,hudNumBuffer + xor ecx,ecx +portrait_prize_length: + cmp BYTE PTR [edi+ecx],0 + je @F + inc ecx + jmp portrait_prize_length +@@: + shl ecx,3 + mov eax,scrCellW3D + sub eax,ecx + sar eax,1 + add eax,left + mov ebx,eax + mov eax,scrCellH3D + shr eax,1 + add eax,top + add eax,4 + invoke DrawBuckshotText3D,ebx,eax,OFFSET hudNumBuffer,ink +portrait_text_done: + ret +ScratchPortraitText3D ENDP + +ScratchPortraitInfo3D PROC uses ebx esi edi + invoke DrawBox,4,44,88,35,0014191Dh + invoke DrawBuckshotText3D,16,100,OFFSET scratchMatch3D,00E5D29Eh + mov eax,OFFSET scratchNeonDouble3D + mov ebx,OFFSET scratchNeonTen3D + cmp scrType3D,4 + je portrait_info_rules + mov eax,OFFSET scratchRubyDouble3D + mov ebx,OFFSET scratchRubyAll3D + cmp scrType3D,5 + je portrait_info_rules + mov eax,OFFSET scratchSeaDouble3D + mov ebx,OFFSET scratchSeaAll3D +portrait_info_rules: + invoke DrawBuckshotText3D,16,120,eax,00E5D29Eh + invoke DrawBuckshotText3D,16,140,ebx,00E5D29Eh + cmp scrPointerValid3D,1 + jne portrait_info_done + invoke ScratchPortraitHit3D,scrPointerX3D,scrPointerY3D + cmp eax,-1 + je portrait_info_done + mov esi,eax + cmp scrRevealed3D[esi*4],1 + jne portrait_info_done + invoke DrawBox,4,88,88,50,0014191Dh + invoke DrawBuckshotText3D,16,190,OFFSET scratchField3D,00ADB9BCh + lea eax,[esi+1] + invoke FormatNumber,eax,OFFSET hudNumBuffer + invoke DrawString8x8,8,102,OFFSET hudNumBuffer,00FFFFFFh + cmp esi,scrPortraitWins3D + jb portrait_info_done + invoke DrawBuckshotText3D,16,240,OFFSET scratchPayout3D,00ADB9BCh + invoke FormatNumber,scrAwards3D[esi*4],OFFSET hudNumBuffer + invoke DrawString8x8,8,128,OFFSET hudNumBuffer,0090D5ADh +portrait_info_done: + ret +ScratchPortraitInfo3D ENDP + +RenderScratchPortrait3D PROC uses esi + call ScratchPortraitView3D + mov eax,scrType3D + sub eax,4 + invoke DrawAnimSprite3D,OFFSET scrPortraitArt3D,eax,scrViewX3D,scrViewY3D,scrViewW3D,scrViewH3D + invoke ScratchPortraitLayers3D,0 + cmp scrState3D,0 + je portrait_render_foil + xor esi,esi +portrait_render_text: + invoke ScratchPortraitText3D,esi + inc esi + cmp esi,scrCellCount3D + jb portrait_render_text +portrait_render_foil: + invoke ScratchPortraitLayers3D,1 + ret +RenderScratchPortrait3D ENDP diff --git a/src_graphics/scratch_portrait_fields.inc b/src_graphics/scratch_portrait_fields.inc new file mode 100644 index 0000000..afd9b55 --- /dev/null +++ b/src_graphics/scratch_portrait_fields.inc @@ -0,0 +1,78 @@ +; Generated by pack_scratch_portraits.py; source coordinates, not screen coordinates. +.data +scrPortraitWinning3D dd 5,6,4 +scrPortraitPlayers3D dd 15,18,12 +scrPortraitFields3D label DWORD + dd 160,339,51,51 + dd 228,339,51,51 + dd 297,339,51,51 + dd 366,339,51,51 + dd 435,339,51,51 + dd 133,433,65,47 + dd 211,433,65,47 + dd 290,433,65,47 + dd 369,433,64,47 + dd 447,433,64,47 + dd 133,496,65,47 + dd 211,496,65,47 + dd 290,496,65,46 + dd 369,496,64,47 + dd 447,496,64,47 + dd 133,558,65,47 + dd 211,558,65,47 + dd 290,558,65,47 + dd 369,558,64,47 + dd 447,558,64,47 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 136,329,50,54 + dd 202,329,50,53 + dd 267,329,51,54 + dd 333,329,50,54 + dd 398,329,50,54 + dd 461,329,50,53 + dd 135,424,54,46 + dd 201,424,52,46 + dd 265,424,52,46 + dd 329,424,52,46 + dd 393,424,52,46 + dd 457,424,53,46 + dd 135,487,54,46 + dd 201,487,52,46 + dd 265,487,52,46 + dd 329,487,52,46 + dd 393,487,52,46 + dd 457,487,53,46 + dd 135,550,54,46 + dd 201,550,52,46 + dd 265,550,52,46 + dd 329,550,52,46 + dd 393,550,52,46 + dd 457,550,53,46 + dd 200,335,53,53 + dd 270,335,53,53 + dd 339,335,53,53 + dd 409,335,53,53 + dd 147,427,79,48 + dd 242,427,81,48 + dd 338,427,80,48 + dd 434,427,79,48 + dd 147,490,79,48 + dd 242,490,81,48 + dd 338,490,80,48 + dd 434,490,79,48 + dd 147,554,79,48 + dd 242,554,81,48 + dd 338,553,80,49 + dd 434,554,79,48 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 + dd 0,0,0,0 +.code diff --git a/src_graphics/scratch_ui.inc b/src_graphics/scratch_ui.inc new file mode 100644 index 0000000..2777640 --- /dev/null +++ b/src_graphics/scratch_ui.inc @@ -0,0 +1,135 @@ +HandleScratchCatalog3D PROC uses ebx esi + cmp scrClick3D,0 + je scratch_catalog_keys + mov scrClick3D,0 + mov eax,scrClickX3D + cmp eax,112 + jb scratch_catalog_close + cmp eax,528 + jae scratch_catalog_close + mov eax,scrClickY3D + sub eax,112 + js scratch_catalog_close + xor edx,edx + mov ecx,38 + div ecx + cmp eax,7 + jae scratch_catalog_close + cmp edx,36 + jae scratch_catalog_done + mov esi,eax + jmp scratch_catalog_choose +scratch_catalog_keys: + mov ebx,31h +scratch_catalog_key: + invoke Pressed3D,ebx + test eax,eax + jnz scratch_catalog_key_pick + inc ebx + cmp ebx,37h + jbe scratch_catalog_key + ret +scratch_catalog_key_pick: + mov esi,ebx + sub esi,31h +scratch_catalog_choose: + cmp scrState3D,1 + jne @F + cmp esi,scrType3D + je scratch_catalog_close + mov scrCatalogLocked3D,1 + ret +@@: + invoke ScratchSelectType3D,esi +scratch_catalog_close: + mov scrCatalog3D,0 + call ScratchCancelStroke3D +scratch_catalog_done: + ret +HandleScratchCatalog3D ENDP + +RenderScratchCatalog3D PROC uses ebx esi edi + local rowY:DWORD,ink:DWORD + invoke DrawBoxBorder,52,40,216,164,0014191Dh,0086754Eh,1 + invoke DrawString8x8,60,44,OFFSET scratchKind3D,00E5D29Eh + xor esi,esi +scratch_catalog_row: + mov ink,0086754Eh + cmp esi,scrType3D + jne @F + mov ink,00E5D29Eh +@@: + imul eax,esi,19 + add eax,56 + mov rowY,eax + invoke DrawBoxBorder,56,eax,208,18,00252D2Ah,ink,1 + mov eax,rowY + shl eax,1 + add eax,2 + mov ebx,eax + cmp esi,4 + jae scratch_catalog_portrait + invoke DrawAnimSprite3D,OFFSET scrTicketArt3D,esi,120,ebx,56,32 + jmp scratch_catalog_name +scratch_catalog_portrait: + mov eax,esi + sub eax,4 + invoke DrawAnimSprite3D,OFFSET scrPortraitArt3D,eax,136,ebx,24,32 +scratch_catalog_name: + mov eax,rowY + add eax,5 + invoke DrawString8x8,94,eax,scrNames3D[esi*4],00E5D29Eh + inc esi + cmp esi,7 + jb scratch_catalog_row + cmp scrCatalogLocked3D,0 + je @F + invoke DrawBuckshotText3D,120,390,OFFSET scratchPending3D,00E99383h +@@: + ret +RenderScratchCatalog3D ENDP + +RenderScratchNet3D PROC uses ebx esi + local ink:DWORD + mov eax,scrGross3D + sub eax,scrStake3D + mov ink,00ADB9BCh + mov ebx,0 + test eax,eax + jz scratch_net_number + js scratch_net_loss + mov ink,0090D5ADh + mov ebx,OFFSET scrZoomIn3D + jmp scratch_net_number +scratch_net_loss: + neg eax + mov ink,00E99383h + mov ebx,OFFSET scrZoomOut3D +scratch_net_number: + invoke FormatNumber,eax,OFFSET hudNumBuffer + invoke DrawBuckshotText3D,336,422,OFFSET scratchNet3D,00ADB9BCh + cmp ebx,0 + je @F + invoke DrawBuckshotText3D,440,422,ebx,ink +@@: + invoke DrawBuckshotText3D,448,422,OFFSET hudNumBuffer,ink + cmp scrType3D,0 + je scratch_net_formula + cmp scrType3D,2 + jne scratch_net_done +scratch_net_formula: + mov esi,scrPrizeSymbol3D + cmp esi,6 + ja scratch_net_done + invoke DrawAnimSprite3D,OFFSET scrSymbolsArt3D,esi,16,418,14,14 + invoke FormatNumber,scrStake3D,OFFSET hudNumBuffer + invoke DrawBuckshotText3D,36,422,OFFSET hudNumBuffer,00E5D29Eh + invoke DrawBuckshotText3D,72,422,OFFSET scrTimes3D,00ADB9BCh + invoke FormatNumber,scrMultipliers3D[esi*4],OFFSET hudNumBuffer + invoke DrawBuckshotText3D,88,422,OFFSET hudNumBuffer,00E5D29Eh + invoke DrawBuckshotText3D,120,422,OFFSET scrEquals3D,00ADB9BCh + invoke FormatNumber,scrGross3D,OFFSET hudNumBuffer + invoke DrawBuckshotText3D,136,422,OFFSET hudNumBuffer,00E5D29Eh +scratch_net_done: + ret +RenderScratchNet3D ENDP diff --git a/src_graphics/startup_code.inc b/src_graphics/startup_code.inc index 218f514..1734451 100644 --- a/src_graphics/startup_code.inc +++ b/src_graphics/startup_code.inc @@ -1,5 +1,7 @@ ; A single 640x480 bitmap; scan reveal and illumination run entirely in MASM. ClearKeys3D PROC uses edi + call ScratchCancelStroke3D + mov scrClick3D,0 mov mouseClick3D, 0 lea edi, keys3D xor eax, eax diff --git a/src_graphics/world_dressing.inc b/src_graphics/world_dressing.inc new file mode 100644 index 0000000..fb60eb4 --- /dev/null +++ b/src_graphics/world_dressing.inc @@ -0,0 +1,170 @@ +LoadWorldDressing3D PROC uses ebx esi + invoke LoadAnimPack3D,OFFSET worldDressingArt3D,OFFSET worldDressingPath3D + invoke ValidateCityPack3D,OFFSET worldDressingArt3D,128,128,3 + cmp worldStreetSized3D,0 + jne world_dressing_loaded + lea esi,cityObjects3D + mov ebx,CITY_PROP_COUNT +world_size_street: + cmp [esi].CityProp3D.asset,OFFSET hallStreetLife3D + jne world_size_ground + mov eax,[esi].CityProp3D.frame + cmp eax,33 + jne @F + mov [esi].CityProp3D.worldW,FP_1*8/25 + mov [esi].CityProp3D.worldH,FP_1*9/20 +@@: + cmp eax,34 + jne @F + mov [esi].CityProp3D.worldW,FP_1*9/20 + mov [esi].CityProp3D.worldH,FP_1*4/5 +@@: + cmp eax,35 + jne @F + mov [esi].CityProp3D.worldW,FP_1*9/20 + mov [esi].CityProp3D.worldH,FP_1*13/20 +@@: + cmp eax,36 + jne @F + mov [esi].CityProp3D.worldW,FP_1/5 + mov [esi].CityProp3D.worldH,FP_1*2/5 +@@: + cmp eax,38 + jne @F + mov [esi].CityProp3D.worldW,FP_1*3/10 + mov [esi].CityProp3D.worldH,FP_1*4/5 +@@: + cmp eax,39 + jne @F + mov [esi].CityProp3D.worldH,FP_1*4/5 +@@: +world_size_ground: + add esi,SIZEOF CityProp3D + dec ebx + jnz world_size_street + mov worldStreetSized3D,1 +world_dressing_loaded: + call UpdateStreetGround3D + ret +LoadWorldDressing3D ENDP + +StreetGroundHeight3D PROC wx:DWORD,wy:DWORD + mov eax,wx + cmp eax,28*FP_1+FP_HALF + jae street_ground_road + cmp wy,12*FP_1 + jb street_ground_road + cmp wy,13*FP_1 + jae street_ground_far + cmp eax,10*FP_1 + jb street_ground_road + jmp street_ground_kerb +street_ground_far: + cmp eax,3*FP_1 + jb street_ground_road + cmp wy,16*FP_1+FP_1/10 + jb street_ground_road + cmp wy,17*FP_1 + jae street_ground_road +street_ground_kerb: + mov eax,FP_1*3/50 + ret +street_ground_road: + xor eax,eax + ret +StreetGroundHeight3D ENDP + +UpdateStreetGround3D PROC uses esi edi + lea esi,cityObjects3D + mov edi,CITY_PROP_COUNT +street_ground_object: + invoke StreetGroundHeight3D,[esi].CityProp3D.posX,[esi].CityProp3D.posY + mov [esi].CityProp3D.groundZ,eax + add esi,SIZEOF CityProp3D + dec edi + jnz street_ground_object + ret +UpdateStreetGround3D ENDP + +FreeWorldDressing3D PROC + invoke FreeAnimPack3D,OFFSET worldDressingArt3D + mov hallLightReady3D,0 + ret +FreeWorldDressing3D ENDP + +ResolveDressingSurface3D PROC material:DWORD + mov eax,material + sub eax,115 + cmp eax,3 + jae world_dressing_surface_missing + cmp worldDressingArt3D.pixels,0 + je world_dressing_surface_missing + shl eax,16 + add eax,worldDressingArt3D.pixels + mov ecx,128 + mov edx,128 + ret +world_dressing_surface_missing: + xor eax,eax + ret +ResolveDressingSurface3D ENDP + +RenderHallDressing3D PROC uses ebx esi edi + cmp hallLevel3D,0 + jne hall_dressing_done + lea esi,hallWallArt3D + mov edi,WORLD_WALL_ART_COUNT +hall_wall_painting: + invoke DrawCityVertical3D,[esi],[esi+4],[esi+8],[esi+12],[esi+16],[esi+20],[esi+24] + add esi,28 + dec edi + jnz hall_wall_painting + xor esi,esi +hall_wall_sconce: + mov eax,hallSconceY3D[esi*4] + lea ebx,[eax-FP_1*9/50] + add eax,FP_1*9/50 + invoke DrawCityVertical3D,0,FP_1*1025/1000,ebx,eax,FP_1*27/20,FP_1*39/20,105 + mov eax,hallSconceY3D[esi*4] + lea ebx,[eax-FP_1*9/50] + add eax,FP_1*9/50 + invoke DrawCityVertical3D,0,FP_1*30975/1000,ebx,eax,FP_1*27/20,FP_1*39/20,105 + inc esi + cmp esi,4 + jb hall_wall_sconce +hall_dressing_done: + ret +RenderHallDressing3D ENDP + +RenderStreetDressing3D PROC uses ebx esi edi + ; Low kerbs follow the street instead of filling the pavement with billboards. + invoke DrawCityBoxSides3D,10*FP_1,28*FP_1+FP_HALF,FP_1*1296/100,13*FP_1,0,FP_1*3/50,72 + invoke DrawCityBoxSides3D,3*FP_1,28*FP_1+FP_HALF,16*FP_1,FP_1*1604/100,0,FP_1*3/50,72 + ; Each metre gets a separate tile; never stretch paving over the whole street. + mov esi,3*FP_1 +street_paving: + lea edi,[esi+FP_1] + cmp edi,28*FP_1+FP_HALF + jbe @F + mov edi,28*FP_1+FP_HALF +@@: + invoke DrawCityHorizontal3D,esi,edi,16*FP_1,17*FP_1,FP_1*3/50,72 + cmp esi,10*FP_1 + jb @F + invoke DrawCityHorizontal3D,esi,edi,12*FP_1,13*FP_1,FP_1*3/50,72 +@@: + add esi,FP_1 + cmp esi,28*FP_1+FP_HALF + jb street_paving + ; Drainage grates sit on the road side of the kerb, not in the walking lane. + mov esi,0 +street_drain: + imul eax,esi,7*FP_1 + add eax,12*FP_1 + lea ebx,[eax+FP_1*9/20] + invoke DrawCityHorizontal3D,eax,ebx,13*FP_1+FP_1/20,13*FP_1+FP_1*7/20,FP_1/200,117 + inc esi + cmp esi,3 + jb street_drain + ret +RenderStreetDressing3D ENDP diff --git a/src_graphics/world_dressing_data.inc b/src_graphics/world_dressing_data.inc new file mode 100644 index 0000000..2fa9d86 --- /dev/null +++ b/src_graphics/world_dressing_data.inc @@ -0,0 +1,33 @@ +WORLD_LIGHT_W equ 256 +WORLD_LIGHT_H equ 256 +LoadWorldDressing3D PROTO +FreeWorldDressing3D PROTO +RenderHallDressing3D PROTO +RenderStreetDressing3D PROTO +StreetGroundHeight3D PROTO :DWORD,:DWORD +UpdateStreetGround3D PROTO +ResolveDressingSurface3D PROTO :DWORD +BuildHallLighting3D PROTO +AddHallLight3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +HallLightVisible3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +ApplyHallLighting3D PROTO +AddHallContact3D PROTO :DWORD,:DWORD,:DWORD +.data +worldDressingArt3D AnimPack3D <> +worldDressingPath3D db "assets\animations\hall\world-dressing-v6.bin",0 +worldStreetSized3D dd 0 +hallLightReady3D dd 0 +hallLighting3D dd 1 +hallSconceY3D dd 6*FP_1,10*FP_1,16*FP_1,20*FP_1 +; axis, wall plane, horizontal bounds, vertical bounds, material. +hallWallArt3D dd 0,FP_1*102/100,7*FP_1,8*FP_1+FP_HALF,FP_1,2*FP_1,115 + dd 0,FP_1*102/100,16*FP_1+FP_HALF,18*FP_1,FP_1,2*FP_1,116 + dd 0,FP_1*3098/100,16*FP_1+FP_HALF,18*FP_1,FP_1,2*FP_1,115 + dd 0,FP_1*3098/100,25*FP_1,26*FP_1+FP_HALF,FP_1,2*FP_1,116 + dd 1,FP_1*3098/100,3*FP_1,4*FP_1+FP_HALF,FP_1,2*FP_1,115 + dd 1,FP_1*3098/100,8*FP_1+FP_HALF,10*FP_1,FP_1,2*FP_1,116 +WORLD_WALL_ART_COUNT equ 6 +.data? +hallLightMap3D db WORLD_LIGHT_W*WORLD_LIGHT_H dup(?) +hallContactMap3D db WORLD_LIGHT_W*WORLD_LIGHT_H dup(?) +.code diff --git a/tools/generators/pack_city_wake.py b/tools/generators/pack_city_wake.py new file mode 100644 index 0000000..2ee5848 --- /dev/null +++ b/tools/generators/pack_city_wake.py @@ -0,0 +1,140 @@ +"""Offline normalization of approved waking-scene sprites; runtime is MASM.""" +from pathlib import Path +import struct +import sys +from PIL import Image +from collections import deque + +ROOT = Path(__file__).resolve().parents[2] +SRC = ROOT / "assets/sprites/arrival" +OUT = ROOT / "assets/animations/arrival" + + +def pack(name, frames): + w, h = frames[0].size + data = b"".join(f.tobytes("raw", "BGRA") for f in frames) + (OUT / name).write_bytes(struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(data)) + data) + + +def cells(name): + sheet = Image.open(SRC / name).convert("RGBA") + result = [] + for i in range(16): + c, r = i % 4, i // 4 + cell = sheet.crop((c * sheet.width // 4, r * sheet.height // 4, + (c + 1) * sheet.width // 4, (r + 1) * sheet.height // 4)) + cell.putalpha(cell.getchannel("A").point(lambda a: 255 if a >= 160 else 0)) + result.append(cell) + return result + + +def figures(name, expected=16): + # Generator gutters are not necessarily equal grid cells. Recover whole + # connected silhouettes before anchoring; otherwise a neighbouring head + # fragment can become the foot anchor of a crouching pose. + source = Image.open(SRC / name).convert("RGBA") + source.putalpha(source.getchannel("A").point(lambda a: 255 if a >= 160 else 0)) + w, h = source.size + mask = bytearray(source.getchannel("A").tobytes()) + blobs = [] + for start in range(len(mask)): + if not mask[start]: + continue + queue = deque([start]) + mask[start] = 0 + pixels = [] + while queue: + at = queue.popleft() + pixels.append(at) + x, y = at % w, at // w + for yy in range(max(0, y - 1), min(h, y + 2)): + for xx in range(max(0, x - 1), min(w, x + 2)): + neighbour = yy * w + xx + if mask[neighbour]: + mask[neighbour] = 0 + queue.append(neighbour) + if len(pixels) < 800: + continue + left = min(p % w for p in pixels) + right = max(p % w for p in pixels) + 1 + top = min(p // w for p in pixels) + bottom = max(p // w for p in pixels) + 1 + figure = source.crop((left, top, right, bottom)) + own_mask = bytearray(figure.width * figure.height) + for p in pixels: + own_mask[(p // w - top) * figure.width + p % w - left] = 255 + figure.putalpha(Image.frombytes("L", figure.size, bytes(own_mask))) + blobs.append(((top + bottom) / 2, (left + right) / 2, figure)) + allowed = expected if isinstance(expected, tuple) else (expected,) + assert len(blobs) in allowed, f"Unexpected figure count: {len(blobs)}" + blobs.sort(key=lambda b: b[0]) + return [b[2] for r in range(len(blobs)//4) for b in sorted(blobs[r * 4:(r + 1) * 4], key=lambda b: b[1])] + + +def pack_actor(): + # Reject the atlas's shrinking departure rows. Calibrate the separate + # in-place strip's source resolution once, then use one shared final scale. + bodies = figures("wake-rocker-v2.png", (32,36))[:24] + walks = figures("wake-walk-v2.png",8) + units = max(c.height for c in bodies[19:24])/max(c.height for c in walks) + bodies += [c.resize((round(c.width*units),round(c.height*units)),Image.Resampling.NEAREST) for c in walks] + scale = min(176 / max(c.width for c in bodies), 304 / max(c.height for c in bodies)) + frames = [] + for body in bodies: + body = body.resize((round(body.width * scale), round(body.height * scale)), Image.Resampling.NEAREST) + frame = Image.new("RGBA", (192, 320)) + frame.paste(body, ((192 - body.width) // 2, 320 - body.height)) + frames.append(frame) + pack("wake-rocker-v2.bin", frames) + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + pack_actor() + frames = [] + # Measured nozzle anchors in source-cell fractions, not a per-frame bbox fit. + anchors = [(0.56, .25)] * 3 + [(.60, .30)] * 3 + [(.61, .29)] * 7 + [(.57, .31)] * 3 + for cell, (ax, ay) in zip(cells("wake-lighter-v1.png"), anchors): + frame = Image.new("RGBA", (256, 256)) + sprite = cell.resize((230, 230), Image.Resampling.NEAREST) + frame.paste(sprite, (128 - round(ax * 230), 64 - round(ay * 230))) + frames.append(frame) + pack("wake-lighter-v1.bin", frames) + joints = [] + for frame in frames: + alpha = frame.getchannel("A") + bottom = alpha.getbbox()[3]-1 + xs = [x for x in range(256) if alpha.getpixel((x,bottom)) >= 160] + joints.append(((min(xs)+max(xs))//2,bottom)) + joint_source = "cityLighterCuffX3D dd " + ",".join(str(x) for x,y in joints) + "\n" + joint_source += "cityLighterCuffY3D dd " + ",".join(str(y) for x,y in joints) + "\n" + (ROOT / "src_graphics/city_lighter_anchors.inc").write_text(joint_source,encoding="ascii") + sleeve = Image.open(SRC / "wake-sleeve-v1.png").convert("RGBA") + sleeve.putalpha(sleeve.getchannel("A").point(lambda a: 255 if a >= 160 else 0)) + sleeve = sleeve.crop(sleeve.getbbox()).resize((256,384),Image.Resampling.NEAREST) + xs = [x for x in range(256) if sleeve.getchannel("A").getpixel((x,12))] + anchor = (min(xs)+max(xs))//2 + aligned = Image.new("RGBA",(256,384)) + aligned.paste(sleeve,(128-anchor,0)) + pack("wake-sleeve-v1.bin",[aligned]) + materials = Image.open(SRC / "alley-materials-v1.png").convert("RGBA") + textures = [] + for row in range(2): + for col in range(2): + tile = materials.crop((col*materials.width//2,row*materials.height//2, + (col+1)*materials.width//2,(row+1)*materials.height//2)) + tile = tile.resize((128,128),Image.Resampling.NEAREST) + tile.putalpha(255) + textures.append(tile) + pack("alley-materials-v1.bin",textures) + legs = Image.open(SRC / "wake-knees-v1.png").convert("RGBA").resize((640, 240), Image.Resampling.NEAREST) + legs.putalpha(legs.getchannel("A").point(lambda a: 255 if a >= 160 else 0)) + pack("wake-knees-v1.bin", [legs]) + print("Packed 32 grounded rocker poses, 16 cuff anchors, continuous sleeve, four alley materials and knees") + + +if __name__ == "__main__": + if "--actor-only" in sys.argv: + pack_actor() + else: + main() diff --git a/tools/generators/pack_dice_assets.py b/tools/generators/pack_dice_assets.py new file mode 100644 index 0000000..c681227 --- /dev/null +++ b/tools/generators/pack_dice_assets.py @@ -0,0 +1,66 @@ +"""Offline hand normalization; dice geometry and all animation remain MASM.""" +from pathlib import Path +import struct +import json +from collections import deque +from PIL import Image +from pack_city_wake import figures + +ROOT = Path(__file__).resolve().parents[2] + + +def dice_bounds(frame): + points = {(x,y) for y in range(frame.height) for x in range(frame.width) + if (lambda p: p[3] == 255 and p[0] > 210 and p[1] > 200 and p[2] > 180)(frame.getpixel((x,y)))} + cubes = [] + while points: + start = points.pop() + queue = deque([start]) + body = [start] + while queue: + x,y = queue.popleft() + for at in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)): + if at in points: + points.remove(at) + queue.append(at) + body.append(at) + if len(body) >= 100: + xs,ys = zip(*body) + cubes.append((min(xs),min(ys),max(xs)+1,max(ys)+1)) + assert len(cubes) == 2, cubes + return sorted(cubes) + + +def main(): + # Reuse the connected-silhouette recovery without nominal grid clipping. + import pack_city_wake + pack_city_wake.SRC = ROOT / "assets/sprites/dice" + bodies = figures("hands-v2.png") + scale = min(240/max(b.width for b in bodies),300/max(b.height for b in bodies)) + frames = [] + for body in bodies: + body = body.resize((round(body.width*scale),round(body.height*scale)),Image.Resampling.NEAREST) + frame = Image.new("RGBA",(256,320)) + frame.paste(body,(256-body.width,320-body.height)) + frames.append(frame) + data = b"".join(f.tobytes("raw","BGRA") for f in frames) + (ROOT / "assets/animations/dice/hands-v2.bin").write_bytes(struct.pack("<4s4I",b"ANM1",256,320,16,len(data))+data) + frames[0].save(ROOT / "build/dice/hand-ready.png") + bounds = [dice_bounds(f) for f in frames[:4]] + # Release geometry starts at the centres of the last held pose, in the + # same logical coordinate system as the MASM cube renderer. + anchors = [((320+(b[0]+b[2])//2)//2,(90+(b[1]+b[3])//2)//2) for b in bounds[-1]] + source = "; Generated by pack_dice_assets.py from the last held pose.\n" + for i,(x,y) in enumerate(anchors,1): + source += f"DICE_RELEASE_X{i} EQU {x}\nDICE_RELEASE_Y{i} EQU {y}\n" + (ROOT / "src_graphics/dice_anchors.inc").write_text(source,encoding="ascii") + (ROOT / "build/dice/held-bounds.json").write_text(json.dumps(bounds,indent=2)) + sheet = Image.new("RGBA",(1024,1280)) + for i,frame in enumerate(frames): + sheet.paste(frame,((i%4)*256,(i//4)*320)) + sheet.save(ROOT / "build/dice/hands-normalized.png") + print("Packed sixteen full-cuff poses, two palm-contact dice and measured release anchors",anchors) + + +if __name__ == "__main__": + main() diff --git a/tools/generators/pack_pyramid_assets.py b/tools/generators/pack_pyramid_assets.py index 5a4c92d..fe92c0b 100644 --- a/tools/generators/pack_pyramid_assets.py +++ b/tools/generators/pack_pyramid_assets.py @@ -49,7 +49,20 @@ def main(): frame.paste(cell, ((12 - cell.width) // 2, (12 - cell.height) // 2)) frames.append(frame) pack("balls-v1.bin", frames) - print("Packed anchored 640x480 table and eight 12x12 Russian ball poses") + sheet = Image.open(SOURCE / "pool-balls-v1.png").convert("RGBA") + frames = [] + for i in range(16): + col, row = i % 4, i // 4 + cell = sheet.crop((round(col * sheet.width / 4), round(row * sheet.height / 4), + round((col + 1) * sheet.width / 4), round((row + 1) * sheet.height / 4))) + cell.putalpha(cell.getchannel("A").point(lambda a: 255 if a >= 192 else 0)) + cell = cell.crop(cell.getchannel("A").getbbox()) + cell.thumbnail((10, 10), Image.Resampling.LANCZOS) + frame = Image.new("RGBA", (12, 12)) + frame.paste(cell, ((12 - cell.width) // 2, (12 - cell.height) // 2)) + frames.append(frame) + pack("pool-balls-v1.bin", frames) + print("Packed anchored 640x480 table, eight pyramid poses and sixteen pool balls") if __name__ == "__main__": diff --git a/tools/generators/pack_scratch_assets.py b/tools/generators/pack_scratch_assets.py new file mode 100644 index 0000000..96ad3e2 --- /dev/null +++ b/tools/generators/pack_scratch_assets.py @@ -0,0 +1,68 @@ +"""Offline normalization only; scratch masks, input and prizes run in MASM.""" +from pathlib import Path +import json +import struct +from PIL import Image +import pack_city_wake + +ROOT = Path(__file__).resolve().parents[2] +SRC = ROOT / "assets/sprites/scratch" +OUT = ROOT / "assets/animations/scratch" + + +def opaque(image): + image = image.convert("RGBA") + image.putalpha(image.getchannel("A").point(lambda a: 255 if a >= 160 else 0)) + return image + + +def pack(name, frames): + w,h = frames[0].size + raw = b"".join(f.tobytes("raw","BGRA") for f in frames) + (OUT / name).write_bytes(struct.pack("<4s4I",b"ANM1",w,h,len(frames),len(raw))+raw) + + +def main(): + OUT.mkdir(parents=True,exist_ok=True) + table = Image.open(SRC / "table-v1.png").convert("RGBA").resize((640,480),Image.Resampling.NEAREST) + table.putalpha(255) + pack("table-v1.bin",[table]) + tickets = [] + for name in ("ticket-v1.png", "ticket-numbers-v1.png", "ticket-rows-v1.png", "ticket-instant-v1.png"): + ticket = opaque(Image.open(SRC / name)) + tickets.append(ticket.crop(ticket.getbbox()).resize((448,288),Image.Resampling.NEAREST)) + pack("ticket-v1.bin",tickets) + foil = Image.open(SRC / "foil-v1.png").convert("RGBA").resize((104,42),Image.Resampling.BOX) + foil.putalpha(255) + pack("foil-v1.bin",[foil]) + pack_city_wake.SRC = SRC + bodies = pack_city_wake.figures("coin-edge-v1.png",4) + scale = min(48/max(b.width for b in bodies),56/max(b.height for b in bodies)) + frames = [] + for body in bodies: + body = body.resize((round(body.width*scale),round(body.height*scale)),Image.Resampling.NEAREST) + bottom = body.height-1 + xs = [x for x in range(body.width) if body.getpixel((x,bottom))[3]] + anchor = (min(xs)+max(xs))//2 + frame = Image.new("RGBA",(64,64)) + frame.paste(body,(32-anchor,60-bottom)) + frames.append(frame) + pack("coin-edge-v1.bin",frames) + counter = opaque(Image.open(SRC / "counter-v1.png")) + counter = counter.crop(counter.getbbox()) + scale = min(224/counter.width,240/counter.height) + counter = counter.resize((round(counter.width*scale),round(counter.height*scale)),Image.Resampling.NEAREST) + frame = Image.new("RGBA",(224,240)) + frame.paste(counter,((224-counter.width)//2,240-counter.height)) + pack("counter-v1.bin",[frame]) + for index,ticket in enumerate(tickets): + ticket.save(ROOT / f"build/scratch/ticket-{index}-normalized.png") + sheet = Image.new("RGBA",(256,64)) + for i,frame in enumerate(frames): + sheet.paste(frame,(64*i,0)) + sheet.save(ROOT / "build/scratch/coin-normalized.png") + print("Packed lottery paper, four edge-contact coins (hotspot 32,60), grounded ticket counter") + + +if __name__ == "__main__": + main() diff --git a/tools/generators/pack_scratch_polish.py b/tools/generators/pack_scratch_polish.py new file mode 100644 index 0000000..c6673ce --- /dev/null +++ b/tools/generators/pack_scratch_polish.py @@ -0,0 +1,62 @@ +"""Offline normalization and sound design; none of this runs inside the game.""" +from pathlib import Path +import math +import random +import struct +import wave +from PIL import Image + +ROOT = Path(__file__).resolve().parents[2] + + +def sound(name,seconds,style): + rate = 22050 + rng = random.Random(713+style) + samples = [] + previous = 0.0 + low = 0.0 + for i in range(int(rate*seconds)): + t = i/rate + noise = rng.uniform(-1,1) + low = low*.72+noise*.28 + high = noise-previous + previous = noise + envelope = min(1,t/.012)*min(1,(seconds-t)/.04) + if style==0: + value = (high*.16+low*.22)*(0.65+0.35*math.sin(t*79)**2) + elif style==1: + value = low*.22+high*.08 + envelope *= math.sin(math.pi*t/seconds)**.7 + else: + value = 0.0 + for offset in (0,.065,.135,.23): + age = t-offset + if age>=0: + value += (math.sin(age*math.tau*1760)+math.sin(age*math.tau*2410)*.4)*math.exp(-age*34)*.15 + value += high*.04*math.exp(-t*12) + samples.append(max(-32767,min(32767,round(value*envelope*20000)))) + path = ROOT/"assets/audio/generated"/name + with wave.open(str(path),"wb") as out: + out.setparams((1,2,rate,0,"NONE","not compressed")) + out.writeframes(struct.pack(f"<{len(samples)}h",*samples)) + + +def main(): + image = Image.open(ROOT/"assets/sprites/scratch/payout-chips-v1.png").convert("RGBA") + alpha = image.getchannel("A").point(lambda a:255 if a>=32 else 0) + image = image.crop(alpha.getbbox()) + image.thumbnail((96,72),Image.Resampling.LANCZOS) + canvas = Image.new("RGBA",(96,72)) + canvas.paste(image,((96-image.width)//2,72-image.height)) + canvas.putalpha(canvas.getchannel("A").point(lambda a:255 if a>=128 else 0)) + raw = canvas.tobytes("raw","BGRA") + (ROOT/"assets/animations/scratch/payout-chips-v1.bin").write_bytes(struct.pack("<4s4I",b"ANM1",96,72,1,len(raw))+raw) + canvas.save(ROOT/"build/scratch/payout-chips-packed.png") + sound("scratch-scrape.wav",.155,0) + sound("scratch-paper.wav",.42,1) + sound("scratch-payout.wav",.48,2) + print("Packed grounded 96x72 payout sprite and three short original scratch/paper/payout sounds") + + +if __name__=="__main__": + main() diff --git a/tools/generators/pack_scratch_portraits.py b/tools/generators/pack_scratch_portraits.py new file mode 100644 index 0000000..04bb0a8 --- /dev/null +++ b/tools/generators/pack_scratch_portraits.py @@ -0,0 +1,94 @@ +"""Offline import of supplied artwork and exact interactive silver-area maps.""" +from pathlib import Path +from collections import deque +import json +import struct +from PIL import Image, ImageDraw + +ROOT = Path(__file__).resolve().parents[2] +SRC = ROOT / "assets/sprites/scratch" +OUT = ROOT / "assets/animations/scratch" +SIZE = (540,720) +KINDS = (("neon-ru-v3.png",5,15),("ruby-ru-v3.png",6,18),("sea-ru-v3.png",4,12)) + + +def silver_components(image): + w,h = image.size + pixels = image.load() + candidates = set() + for y in range(310,610): + for x in range(125,525): + r,g,b = pixels[x,y][:3] + if max(r,g,b)-min(r,g,b) < 22 and 105 < min(r,g,b) and max(r,g,b) < 225: + candidates.add((x,y)) + components = [] + while candidates: + start = candidates.pop() + queue = deque([start]) + body = [start] + while queue: + x,y = queue.popleft() + for point in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)): + if point in candidates: + candidates.remove(point) + queue.append(point) + body.append(point) + if len(body)>1000: + components.append(body) + return sorted(components,key=lambda body:(min(y for x,y in body)//25,min(x for x,y in body))) + + +def pack(name,frames): + w,h = frames[0].size + raw = b"".join(image.tobytes("raw","BGRA") for image in frames) + (OUT/name).write_bytes(struct.pack("<4s4I",b"ANM1",w,h,len(frames),len(raw))+raw) + + +def main(): + arts,maps,layouts = [],[],[] + for filename,winning,players in KINDS: + art = Image.open(SRC/filename).convert("RGBA").resize(SIZE,Image.Resampling.LANCZOS) + art.putalpha(255) + components = silver_components(art) + assert len(components)==winning+players,(filename,len(components)) + field_map = Image.new("RGBA",SIZE) + fields = [] + preview = art.copy() + draw = ImageDraw.Draw(preview) + for i,body in enumerate(components): + left,top = min(x for x,y in body),min(y for x,y in body) + right,bottom = max(x for x,y in body)+1,max(y for x,y in body)+1 + assert right-left<=128 and bottom-top<=96 + # Close tiny texture holes without expanding into the decorated frame. + rows = {} + for x,y in body: + rows.setdefault(y,[]).append(x) + area = 0 + for y,xs in rows.items(): + for x in range(min(xs),max(xs)+1): + field_map.putpixel((x,y),(0,0,i+1,255)) + area += 1 + fields.append(dict(x=left,y=top,w=right-left,h=bottom-top,area=area)) + draw.rectangle((left,top,right-1,bottom-1),outline="red") + draw.text((left+4,top+4),str(i),fill="black") + preview.save(ROOT/f"build/scratch/{filename[:-4]}-fields.png") + arts.append(art) + maps.append(field_map) + layouts.append(dict(file=filename,winning=winning,players=players,fields=fields)) + pack("portraits-v3.bin",arts) + pack("portrait-fields-v3.bin",maps) + (OUT/"portrait-layouts-v3.json").write_text(json.dumps(layouts,indent=2),encoding="utf-8") + lines = ["; Generated by pack_scratch_portraits.py; source coordinates, not screen coordinates.",".data"] + lines += ["scrPortraitWinning3D dd 5,6,4","scrPortraitPlayers3D dd 15,18,12","scrPortraitFields3D label DWORD"] + for layout in layouts: + fields = layout["fields"] + for field in fields: + lines.append(" dd "+",".join(str(field[k]) for k in ("x","y","w","h"))) + lines.extend([" dd 0,0,0,0"]*(24-len(fields))) + lines += [".code",""] + (ROOT/"src_graphics/scratch_portrait_fields.inc").write_text("\n".join(lines),encoding="ascii") + print("Packed 3 supplied portrait designs, 20/24/16 exact silver masks and source-space geometry") + + +if __name__=="__main__": + main() diff --git a/tools/generators/pack_world_dressing.py b/tools/generators/pack_world_dressing.py new file mode 100644 index 0000000..b66146d --- /dev/null +++ b/tools/generators/pack_world_dressing.py @@ -0,0 +1,33 @@ +"""Offline packing only: wall paintings and a small iron drainage texture.""" +from pathlib import Path +import struct +from PIL import Image + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + image = Image.open(ROOT/"assets/sprites/hall/wall-paintings-v6.png").convert("RGBA") + frames = [] + for side in range(2): + part = image.crop((side*image.width//2,0,(side+1)*image.width//2,image.height)) + mask = part.getchannel("A").point(lambda a:255 if a>=96 else 0) + part = part.crop(mask.getbbox()).resize((128,128),Image.Resampling.LANCZOS) + part.putalpha(part.getchannel("A").point(lambda a:255 if a>=128 else 0)) + frames.append(part) + drain = Image.new("RGBA",(128,128),(51,55,57,255)) + for y in range(128): + for x in range(128): + if 7= min(count, 5), f"Frozen animation: {prefix}" - palette = frames[0].quantize(colors=256) + # Closed eyes at the start must not give the entire preview a black palette. + palette_frame = max(frames, key=lambda frame: sum(ImageStat.Stat(frame).mean)) + palette = palette_frame.quantize(colors=256) encoded = [frame.quantize(palette=palette) for frame in frames] encoded[0].save(OUTPUT / f"{prefix}.gif", save_all=True, append_images=encoded[1:], duration=duration, loop=0, disposal=2) @@ -66,7 +81,7 @@ def check_sequence(prefix, count, crop, duration): def main(): packs = check_packs() - names = ["city-eyes", "city-upward", "city-smoke", "city-toss", "city-alley", "city-street", "city-skyline", + names = ["city-eyes", "city-wake-rocker", "city-rocker-back", "city-light-cigarette", "city-upward", "city-standing", "city-look-right", "city-smoke", "city-toss", "city-alley", "city-street", "city-skyline", "arrival-street", "arrival-stop", "arrival-profile", "arrival-name", "arrival-admit", "merchant-idle", "merchant-gesture", "arrival-hall"] black = Image.open(OUTPUT / "city-black.bmp").convert("RGB") @@ -84,6 +99,8 @@ def main(): assert len(hashes) == len(names), "Repeated scene snapshots" sheet.save(OUTPUT / "arrival-contact-sheet.png") sequences = { + "wake": check_sequence("city-wake",36,(0,0,640,480),125), + "ignition": check_sequence("city-light",20,(0,0,640,480),115), "guard": check_sequence("guard-admit", 12, (0, 0, 640, 400), 100), "merchant": check_sequence("merchant-talk", 12, (0, 56, 330, 390), 100), "dealer_hand": check_sequence("poker-hand", 24, (0, 88, 640, 444), 60), diff --git a/tools/tests/check_audio_bindings.py b/tools/tests/check_audio_bindings.py index 438aadc..21a21d4 100644 --- a/tools/tests/check_audio_bindings.py +++ b/tools/tests/check_audio_bindings.py @@ -7,7 +7,8 @@ ROOT = Path(__file__).resolve().parents[2] def asm_paths(): - for file in [ROOT / "src_graphics/audio_data.inc", ROOT / "src_graphics/casino_actions_data.inc"]: + for file in [ROOT / "src_graphics/audio_data.inc", ROOT / "src_graphics/casino_actions_data.inc", + ROOT / "src_graphics/scratch_data.inc"]: definitions = {} current = None for line in file.read_text(encoding="ascii").splitlines(): @@ -37,9 +38,18 @@ def main(): assert native["rrFireSound3D"].endswith("b05e1ac8be34248.wav") assert native["soundClick"] != native["rrDrySound3D"] assert native["soundDiceRoll"] != native["soundCards"] + assert len({native[name] for name in ("scrScrapeSound3D","scrPaperSound3D","scrPayoutSound3D")})==3 engine = (ROOT / "casino3d.inc").read_text(encoding="cp1251") assert "invoke PlaySoundA, OFFSET sound" not in engine - assert "call InitSounds" in engine.split("run_game_body:")[1].split("LaunchClassicGame ENDP")[0] + if "LaunchClassicGame PROC" in engine: + assert "call InitSounds" in engine.split("run_game_body:")[1].split("LaunchClassicGame ENDP")[0] + else: + # Native-only builds intentionally removed the terminal launch path. + assert "run_game_body:" not in engine + assert "invoke PlayCasinoSound3D, OFFSET soundDiceRoll" in engine + audio = (ROOT / "src_graphics/audio_code.inc").read_text(encoding="ascii") + assert "invoke PlaySoundA, ebx, 0, eax" in audio + assert "20003h" in audio # SND_FILENAME | SND_NODEFAULT | SND_ASYNC legacy = (ROOT / "poker.asm").read_text(encoding="cp1251") recordings = set(native.values()) recordings.update(re.findall(r'open "(assets\\audio\\[^"\r\n]+\.wav)" type waveaudio', legacy)) diff --git a/tools/tests/check_billiards.py b/tools/tests/check_billiards.py new file mode 100644 index 0000000..e6e7adc --- /dev/null +++ b/tools/tests/check_billiards.py @@ -0,0 +1,77 @@ +"""Validate real MASM billiards captures, packed art and tested/shipping parity.""" +import argparse +import hashlib +import struct +from pathlib import Path +from PIL import Image +from check_card_exit_build import Coff +from check_coin_detail import font, text_matches + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "build/actions" +NAMES = ("billiards-select", "billiards-eight", "billiards-trace", "pyramid-rack", + "pyramid-aim", "pyramid-strike", "pyramid-penalty", "pyramid-choice", "pyramid-win") +FUNCTIONS = {"InitPool3D": 0, "ResetPool3D": 0, "StartBilliards3D": 4, + "PoolStep3D": 0, "PoolCollide3D": 8, "PoolPocketBall3D": 4, + "PoolJawContact3D": 4, "PoolFinishShot3D": 0, "PoolShoot3D": 0, + "PoolPredict3D": 0, "PoolDrawPrediction3D": 0, + "PoolTraceLine3D": 20, "PoolAimPoint3D": 8, + "PoolChooseAI3D": 0, "EightLegal3D": 4, "EightCount3D": 0, + "EightAutoCall3D": 0, "EightSyncGroups3D": 0, + "EightFinish3D": 0, "EightHand3D": 0, "PyramidSelect3D": 4, + "PyramidRail3D": 8, "PyramidCross3D": 4, "PyramidContact3D": 8, + "PyramidPenalty3D": 4, "PyramidChoice3D": 4, "PyramidRespot3D": 4, + "HandlePool3D": 0, "RenderPool3D": 0, "PyramidBall3D": 16} + + +def main(parity=False): + for name, count in (("balls-v1.bin", 8), ("pool-balls-v1.bin", 16)): + raw = (ROOT / "assets/animations/pyramid" / name).read_bytes() + stride = 12 * 12 * 4 + assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 12, 12, count, stride * count) + assert len(raw) == 20 + stride * count + for index in range(count): + frame = Image.frombytes("RGBA", (12, 12), raw[20 + index * stride:20 + (index + 1) * stride], "raw", "BGRA") + assert frame.getbbox() is not None + assert frame.getbbox()[0] > 0 and frame.getbbox()[1] > 0 + frames = {} + for name in NAMES: + frame = Image.open(OUT / (name + ".bmp")).convert("RGB") + assert frame.size == (640, 480) + assert len(frame.getcolors(640 * 480)) > 300, name + frames[name] = frame + frame.save(OUT / (name + ".png")) + assert len({hashlib.sha256(im.tobytes()).digest() for im in frames.values()}) == len(NAMES) + glyphs = font() + text_matches(frames["billiards-select"], "БИЛЬЯРД", 32, 24, 2, (232, 215, 173), glyphs) + text_matches(frames["billiards-eight"], "ПУЛ / 8", 16, 12, 2, (232, 215, 173), glyphs) + text_matches(frames["pyramid-rack"], "ПИРАМИДА", 16, 12, 2, (232, 215, 173), glyphs) + for name, total in (("eight-human-shot", "6"), ("eight-group-credit", "1")): + frame = Image.open(OUT / (name + ".bmp")).convert("RGB") + text_matches(frame, "ВЫ", 176, 12, 2, (232, 215, 173), glyphs) + text_matches(frame, "ИИ", 320, 12, 2, (232, 215, 173), glyphs) + text_matches(frame, total, 224, 12, 2, (244, 243, 232), glyphs) + text_matches(frame, total, 288, 12, 2, (244, 243, 232), glyphs) + frame.save(OUT / (name + ".png")) + trace = frames["billiards-trace"] + # The actual two-ball preview must show an object path toward and back from + # the right cushion, not merely the cue's first-contact ray. + assert sum(trace.getpixel((x, 200)) == (141, 181, 169) for x in range(350, 570)) > 15 + contact = Image.new("RGB", (1920, 1440)) + for i, name in enumerate(NAMES): + contact.paste(frames[name], ((i % 3) * 640, (i // 3) * 480)) + contact.save(OUT / "billiards-contact.png") + if parity: + tested = Coff(OUT / "actions-test.obj") + shipped = Coff(ROOT / "build/graphics/poker.obj") + for name, size in FUNCTIONS.items(): + symbol = f"_{name}@{size}" + assert tested.signature(symbol) == shipped.signature(symbol), name + print("PASS: Russian selection, both ball sets, native captures, rebound prediction" + + (" and shipping/tested billiards parity" if parity else "")) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--parity", action="store_true") + main(parser.parse_args().parity) diff --git a/tools/tests/check_city_wake.py b/tools/tests/check_city_wake.py new file mode 100644 index 0000000..230e78c --- /dev/null +++ b/tools/tests/check_city_wake.py @@ -0,0 +1,100 @@ +"""Check grounded waking assets, native dialogue captures and shipping parity.""" +import argparse +import struct +from collections import deque +from pathlib import Path +from PIL import Image +from check_card_exit_build import Coff +from check_coin_detail import font, text_matches + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "build/actions" +FUNCTIONS = {"LoadCityArrival3D": 0, "HandleCityApproach3D": 0, + "PositionCityCamera3D": 0, "RenderCityArrival3D": 0, + "DrawCityWakeActor3D": 0, "DrawCitySeatedLegs3D": 0, + "DrawCityLighter3D": 0, "DrawCityWakeSubtitle3D": 0, + "BlendCitySky3D": 4, "BlendCityWeatherPixel3D": 16, + "DrawCityRain3D": 0, "DrawCitySmokingHand3D": 0, + "DrawCityExhaledSmoke3D": 0, "DrawCityCigarette3D": 16, + "BeginArrival3D": 0, "HandleArrival3D": 0, "ArrivalEscape3D": 0, + "AnimateCityCars3D": 0, "FreeArrivalAssets3D": 0, + "RenderCityAlley3D": 0, "ResolveCitySurface3D": 4, + "DrawCityVertical3D": 28, "DrawCityHorizontal3D": 24} + + +def check_sleeve(): + root = ROOT / "assets/animations/arrival" + raw = (root / "wake-sleeve-v1.bin").read_bytes() + assert struct.unpack_from("<4I",raw,4) == (256,384,1,256*384*4) + sleeve = Image.frombytes("RGBA",(256,384),raw[20:],"raw","BGRA").getchannel("A") + assert set(sleeve.tobytes()) == {0,255} + assert all(sleeve.crop((0,y,256,y+1)).getbbox() for y in range(384)) + anchors = (ROOT / "src_graphics/city_lighter_anchors.inc").read_text() + xs, ys = [[int(v) for v in line.split("dd ")[1].split(",")] for line in anchors.splitlines()] + lighter = (root / "wake-lighter-v1.bin").read_bytes() + for i in range(16): + hand = Image.frombytes("RGBA",(256,256),lighter[20+i*262144:20+(i+1)*262144],"raw","BGRA").getchannel("A") + mask = Image.new("L",(640,480)) + arm = sleeve.resize((220,330),Image.Resampling.NEAREST) + mask.paste(arm,(350+xs[i]*220//256-110,223+ys[i]*220//256-20)) + hand = hand.resize((220,220),Image.Resampling.NEAREST) + mask.paste(255,(350,223),hand) + # Start inside the cuff: its connected silhouette must leave the viewport. + x,y = 350+xs[i]*220//256,223+ys[i]*220//256-4 + queue = deque([(x,y)]) + seen = set() + reached = False + while queue: + x,y = queue.popleft() + if (x,y) in seen or not (0 <= x < 640 and 0 <= y < 480) or mask.getpixel((x,y)) < 128: + continue + seen.add((x,y)) + if y == 479: + reached = True + break + queue.extend(((x-1,y),(x+1,y),(x,y-1),(x,y+1))) + assert reached, f"Detached lighter cuff in frame {i}" + + +def main(parity): + check_sleeve() + raw = (ROOT / "assets/animations/arrival/wake-rocker-v2.bin").read_bytes() + w, h, count, length = struct.unpack_from("<4I", raw, 4) + assert (w, h, count, length) == (192,320,32,192*320*4*32) + boxes = [] + for i in range(count): + frame = Image.frombytes("RGBA", (w,h), raw[20+i*w*h*4:20+(i+1)*w*h*4], "raw", "BGRA") + box = frame.getbbox() + assert box and box[3] == 320, (i, box) + assert set(frame.getchannel("A").tobytes()) == {0,255} + boxes.append(box) + assert max(b[3]-b[1] for b in boxes[:16]) < min(b[3]-b[1] for b in boxes[19:24]) + walking = [b[3]-b[1] for b in boxes[24:]] + assert max(walking)-min(walking) <= 8, "Walking sprite shrinks between frames" + assert abs(max(walking)-(boxes[19][3]-boxes[19][1])) <= 8 + wake = Image.open(OUT / "city-wake-rocker.bmp").convert("RGB") + text_matches(wake, "Вставай, самурай,",184,422,2,(230,222,210),font()) + text_matches(wake, "пора дэпнуть все в казино",120,450,2,(230,222,210),font()) + for name in ("city-wake-rocker", "city-rocker-back", "city-light-cigarette", "city-standing", "city-look-right", "city-upward"): + frame = Image.open(OUT / (name + ".bmp")).convert("RGB") + assert frame.size == (640,480) + assert len(frame.getcolors(640*480)) > 500 + frame.save(OUT / (name + ".png")) + if parity: + tested = Coff(OUT / "actions-test.obj") + shipped = Coff(ROOT / "build/graphics/poker.obj") + for name, size in FUNCTIONS.items(): + symbol = f"_{name}@{size}" + assert tested.signature(symbol) == shipped.signature(symbol), name + back = Image.open(OUT / "city-rocker-back.bmp").convert("RGB") + patch = back.crop((250,30,380,180)) + pixels = (patch.getpixel((x,y)) for y in range(patch.height) for x in range(patch.width)) + assert sum(r > 100 and r > g*1.5 and r > b*1.5 for r,g,b in pixels) > 60, "Back emblem absent" + print("PASS: thirty-two grounded poses, back emblem, crouch/standing scale, Russian subtitle, native scenes" + + (" and shipping/tested cinematic parity" if parity else "")) + + +if __name__ == "__main__": + p = argparse.ArgumentParser() + p.add_argument("--parity",action="store_true") + main(p.parse_args().parity) diff --git a/tools/tests/check_dice_scene.py b/tools/tests/check_dice_scene.py new file mode 100644 index 0000000..fd5d18e --- /dev/null +++ b/tools/tests/check_dice_scene.py @@ -0,0 +1,153 @@ +"""Native palm contact, release continuity, readable top results and MASM parity.""" +import argparse +import hashlib +import struct +import json +import math +from collections import deque +from pathlib import Path +from PIL import Image, ImageDraw, ImageChops +from check_card_exit_build import Coff +from check_coin_detail import font, text_matches + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "build/actions" + + +def pack_frames(): + raw = (ROOT / "assets/animations/dice/hands-v2.bin").read_bytes() + assert struct.unpack_from("<4s4I",raw) == (b"ANM1",256,320,16,256*320*4*16) + assert set(raw[23::4]) == {0,255} + frames = [Image.frombytes("RGBA",(256,320),raw[20+i*327680:20+(i+1)*327680],"raw","BGRA") for i in range(16)] + for i,frame in enumerate(frames): + assert frame.getbbox()[3] == 320, ("Cuff not bottom anchored",i) + return frames + + +def components(points,minimum=1): + result = [] + while points: + start = points.pop() + queue = deque([start]) + body = {start} + while queue: + x,y = queue.popleft() + for at in ((x-1,y),(x+1,y),(x,y-1),(x,y+1)): + if at in points: + points.remove(at) + body.add(at) + queue.append(at) + if len(body) >= minimum: + result.append(body) + return result + + +def check_held_contact(frames): + raw = (ROOT / "assets/animations/coin/table-v1.bin").read_bytes() + table = Image.frombytes("RGBA",(640,480),raw[20:],"raw","BGRA").convert("RGB") + # Exact framebuffer compositing catches an extra procedural die painted + # over skin, not merely a centre point that happens to be inside a palm. + for name,index in (("dice-ready",0),("dice-held-last",3)): + expected = table.copy() + expected.paste(frames[index],(320,90),frames[index].getchannel("A")) + actual = Image.open(OUT / (name+".bmp")).convert("RGB") + assert ImageChops.difference(expected,actual).crop((320,90,576,350)).getbbox() is None, name + bounds = json.loads((ROOT / "build/dice/held-bounds.json").read_text()) + for row in bounds: + assert len(row) == 2 + for left,top,right,bottom in row: + assert right-left <= 42 and bottom-top <= 44, "Die exceeds a thumb-width" + assert 70 <= left < right <= 170 and 170 <= top < bottom <= 240, "Die left central palm" + frame = Image.open(OUT / "dice-release.bmp").convert("RGB") + colors = {(250,241,223),(201,193,176),(209,197,174), + (223,211,189),(255,249,234),(181,170,152)} + points = {(x,y) for y in range(240,335) for x in range(375,510) + if frame.getpixel((x,y)) in colors} + cubes = sorted(components(points,100),key=lambda c:min(x for x,y in c)) + assert len(cubes) == 2, "Released dice merge or disappear" + for cube,b in zip(cubes,bounds[-1]): + xs,ys = zip(*cube) + assert abs((min(xs)+max(xs))/2-(320+(b[0]+b[2])/2)) <= 4, "Release jumps sideways" + assert abs((min(ys)+max(ys))/2-(90+(b[1]+b[3])/2)) <= 4, "Release jumps vertically" + # The floor shadow pass must not paint over the foreground hand. + hand = frames[4] + for y in range(260): + for x in range(256): + rgba = hand.getpixel((x,y)) + actual = frame.getpixel((x+320,y+90)) + if rgba[3] and actual not in colors and actual != (163,29,37): + assert actual == rgba[:3], ("Floor shadow overlays skin",x,y) + + +def check_top_results(): + # The top plane is the y=-half face at a fixed down-looking camera angle. + # Count actual native red pip clusters inside that face, not UI text. + def project(x,z,cx): + s,c = int(math.sin(44*math.tau/512)*65536),int(math.cos(44*math.tau/512)*65536) + rx = ((x*c)>>16)+((z*s)>>16) + rz = ((z*c)>>16)-((x*s)>>16) + sy,cy = int(math.sin(70*math.tau/512)*65536),int(math.cos(70*math.tau/512)*65536) + ry = ((-32768*cy)>>16)-((rz*sy)>>16) + return ((cx+(rx*12>>16))*2,(146+(ry*12>>16))*2) + for value in range(1,7): + frame = Image.open(OUT / f"dice-face-{value}.bmp").convert("RGB") + for cx,expected in ((134,value),(186,7-value)): + mask = Image.new("1",frame.size) + ImageDraw.Draw(mask).polygon([project(x,z,cx) for x,z in + ((-32768,-32768),(-32768,32768),(32768,32768),(32768,-32768))],fill=1) + points = {(x,y) for y in range(270,301) for x in range(cx*2-25,cx*2+25) + if mask.getpixel((x,y)) and frame.getpixel((x,y)) == (163,29,37)} + assert len(components(points,2)) == expected, ("Wrong top pips",value,cx,expected) + assert sum(frame.getpixel((x,y)) == (21,49,34) + for y in range(292,322) for x in range(cx*2-25,cx*2+25)) >= 4, "Missing underside contact shadow" + frame.save(OUT / f"dice-face-{value}.png") + + +def main(parity=False): + source = (ROOT / "src_graphics/dice_animation.inc").read_text(encoding="cp1251") + assert "00FAF1DFh" in source and "00A31D25h" in source, "Ivory/red identity changed" + frames = pack_frames() + names = ["dice-ready","dice-0","dice-200","dice-500","dice-800","dice-1200","dice-1600","dice-missing"] + contact = Image.new("RGB",(1280,2048),(20,25,29)) + draw = ImageDraw.Draw(contact) + for i,name in enumerate(names): + frame = Image.open(OUT / (name+".bmp")).convert("RGB") + assert frame.size == (640,480) + assert len(frame.getcolors(640*480)) > 100, name + frame.save(OUT / (name+".png")) + contact.paste(frame,((i%2)*640,(i//2)*512)) + draw.text(((i%2)*640+8,(i//2)*512+488),name,fill="white") + contact.save(OUT / "dice-contact.png") + check_held_contact(frames) + check_top_results() + result = Image.open(OUT / "dice-1600.bmp").convert("RGB") + glyphs = font() + text_matches(result,"СУММА:",24,360,2,(229,210,158),glyphs) + text_matches(result,"+",172,360,2,(229,210,158),glyphs) + text_matches(result,"=",224,360,2,(229,210,158),glyphs) + for text,x in (("4",144),("6",196),("10",252),("100",496)): + ink = (144,213,173) if text == "100" else (255,255,255) + text_matches(result,text,x,360,2,ink,glyphs) + sequence = [Image.open(OUT / f"dice-scene-{i:02}.bmp").convert("RGB") for i in range(40)] + assert len({hashlib.sha256(f.crop((200,180,590,388)).tobytes()).digest() for f in sequence}) >= 35 + sequence.append(result) + palette = sequence[15].quantize(colors=256) + gif = [f.quantize(palette=palette) for f in sequence] + gif[0].save(OUT / "dice-first-person.gif",save_all=True,append_images=gif[1:],duration=[40]*40+[800],loop=0,disposal=2) + if parity: + tested = Coff(OUT / "actions-test.obj") + shipped = Coff(ROOT / "build/graphics/poker.obj") + for name,size in (("RenderDiceGame",0),("RenderNativeDice3D",0),("RenderDiceThrow3D",0), + ("DrawTumblingDie3D",24),("DrawDiceHand3D",0),("HandleDiceInput",0), + ("HandleDiceRules3D",0),("HandleDiceMouse3D",0),("DiceEscape3D",0), + ("LoadNativeDiceArt3D",0),("FreeNativeDiceArt3D",0)): + symbol = f"_{name}@{size}" + assert tested.signature(symbol) == shipped.signature(symbol), name + print("PASS: palm-contact compositing, measured release, six correct upward results, sixteen hand poses, forty native frames, Russian sum/payout" + + (", shipping/tested MASM parity" if parity else "")) + + +if __name__ == "__main__": + p = argparse.ArgumentParser() + p.add_argument("--parity",action="store_true") + main(p.parse_args().parity) diff --git a/tools/tests/check_scratch.py b/tools/tests/check_scratch.py new file mode 100644 index 0000000..0f2672d --- /dev/null +++ b/tools/tests/check_scratch.py @@ -0,0 +1,317 @@ +"""Check real MASM scratch-off captures and exact coating/pointer invariants.""" +import argparse +import hashlib +import json +import struct +import wave +from pathlib import Path +from PIL import Image, ImageDraw, ImageChops +from check_card_exit_build import Coff +from check_coin_detail import font, text_matches + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "build/actions" + + +def asset(name,size,count): + data = (ROOT / "assets/animations/scratch" / name).read_bytes() + w,h = size + assert struct.unpack_from("<4s4I",data) == (b"ANM1",w,h,count,w*h*4*count) + assert len(data) == 20+w*h*4*count + result = [Image.frombytes("RGBA",size,data[20+i*w*h*4:20+(i+1)*w*h*4],"raw","BGRA") for i in range(count)] + for frame in result: + assert set(frame.getchannel("A").tobytes()) <= {0,255} + return result + + +def check_assets(): + asset("table-v1.bin",(640,480),1) + tickets = asset("ticket-v1.bin",(448,288),4) + assert len({hashlib.sha256(f.tobytes()).digest() for f in tickets}) == 4 + foil = asset("foil-v1.bin",(104,42),1)[0] + assert set(foil.getchannel("A").tobytes()) == {255} + coins = asset("coin-edge-v1.bin",(64,64),4) + assert len({hashlib.sha256(f.tobytes()).digest() for f in coins}) == 4 + for frame in coins: + alpha = frame.getchannel("A") + assert alpha.getbbox()[3] == 61, "Coin edge drifts off its contact point" + assert alpha.getpixel((32,60)) == 255, "Hotspot not on the scraping rim" + left,top,right,bottom = alpha.getbbox() + assert right-left < bottom-top, "Coin is face-on, not tilted on its edge" + counter = asset("counter-v1.bin",(224,240),1)[0] + assert counter.getbbox()[3] == 240 + prompts = json.loads((ROOT / "assets/sprites/scratch/prompts-v1.json").read_text()) + assert prompts["mode"] == "built-in image_gen" + payout = asset("payout-chips-v1.bin",(96,72),1)[0] + assert payout.getbbox()[3] == 72 + assert len(payout.getcolors(96*72))>100 + for name in ("scratch-scrape.wav","scratch-paper.wav","scratch-payout.wav"): + with wave.open(str(ROOT/"assets/audio/generated"/name),"rb") as audio: + assert audio.getnchannels()==1 and audio.getsampwidth()==2 + assert audio.getframerate()==22050 + samples = struct.unpack(f"<{audio.getnframes()}h",audio.readframes(audio.getnframes())) + assert 1000 (3 if name == "missing" else 100), name + frame.save(OUT / f"scratch-{name}.png") + frames[name] = frame + at = ((i%2)*640,(i//2)*512) + sheet.paste(frame,at) + draw.text((at[0]+8,at[1]+488),name,fill="white") + sheet.save(OUT / "scratch-contact.png") + foil = asset("foil-v1.bin",(104,42),1)[0].convert("RGB") + changed = [] + for cell in range(9): + col,row = cell%3,cell//3 + x,y = 144+124*col,174+50*row + crop = (x,y,x+104,y+42) + assert ImageChops.difference(frames["covered"].crop(crop),foil).getbbox() is None, "Symbol leaks through opaque coating" + diff = ImageChops.difference(frames["covered"].crop(crop),frames["partial"].crop(crop)).convert("L") + changed.append(sum(p != 0 for p in diff.tobytes())) + assert ImageChops.difference(frames["result"].crop(crop),foil).getbbox() is not None, "Completed panel still covered" + assert all(400 < changed[i] < 2200 for i in (0,1,2)), changed + assert all(changed[i] == 0 for i in (3,4,5,7,8)), "Disconnected drags joined across paper" + assert 50 < changed[6] < 250, changed + glyphs = font() + text_matches(frames["result"],"ВЫПЛАТА",336,405,1,(173,185,188),glyphs) + text_matches(frames["result"],"250",424,402,2,(144,213,173),glyphs) + text_matches(frames["result"],"ЦЕНА БИЛЕТА",16,405,1,(229,210,158),glyphs) + text_matches(frames["result"],"ЧИСТЫЙ ИТОГ",336,422,1,(173,185,188),glyphs) + text_matches(frames["result"],"+200",440,422,1,(144,213,173),glyphs) + text_matches(frames["missing"],"НЕТ СПРАЙТОВ",208,224,2,(233,147,131),glyphs) + assert len({hashlib.sha256(f.tobytes()).digest() for f in frames.values()}) == 5 + print("PASS: opaque coatings, local continuous removal, separate gestures, Russian prize and safe missing-art frame") + + +def check_variants(): + foil = asset("foil-v1.bin",(104,42),1)[0].convert("RGB") + papers = asset("ticket-v1.bin",(448,288),4) + glyphs = font() + sheet = Image.new("RGB",(1280,960)) + covered_sheet = Image.new("RGB",(1280,960)) + titles = ("ЗОЛОТАЯ ТРОЙКА","СЧАСТЛИВЫЕ ЧИСЛА","ТРИ В РЯД","МИГ УДАЧИ") + for kind,title in enumerate(titles): + covered = Image.open(OUT / f"scratch-kind-{kind}-covered.bmp").convert("RGB") + result = Image.open(OUT / f"scratch-kind-{kind}-result.bmp").convert("RGB") + text_matches(covered,title,320-len(title)*8,130,2,(229,210,158),glyphs) + text_matches(result,"ВЫПЛАТА",336,405,1,(173,185,188),glyphs) + text_matches(result,"250",424,402,2,(144,213,173),glyphs) + if kind == 1: + text_matches(covered,"ВЫИГРЫШНЫЕ ЧИСЛА",144,170,1,(48,59,50),glyphs) + text_matches(covered,"ВАШИ ЧИСЛА",144,220,1,(48,59,50),glyphs) + text_matches(result,"СОВПАДЕНИЕ = ПРИЗ ПОД ЧИСЛОМ",176,328,1,(48,59,50),glyphs) + if kind == 3: + text_matches(result,"ПРИЗ УКАЗАН ПОД ЗАЩИТОЙ",176,340,1,(48,59,50),glyphs) + for state,frame in (("covered",covered),("result",result)): + frame.save(OUT / f"scratch-kind-{kind}-{state}.png") + sheet.paste(result,((kind%2)*640,(kind//2)*480)) + covered_sheet.paste(covered,((kind%2)*640,(kind//2)*480)) + if kind == 3: + expected = Image.new("RGB",(240,120)) + for y in range(120): + for x in range(240): + expected.putpixel((x,y),foil.getpixel((x%104,y%42))) + assert ImageChops.difference(covered.crop((200,176,440,296)),expected).getbbox() is None + else: + for i in range(9): + x,y = 144+124*(i%3),174+50*(i//3) + if kind != 2: + height = 34 if kind == 1 else 42 + if kind == 1: + y += 8 + assert ImageChops.difference(covered.crop((x,y,x+104,y+height)),foil.crop((0,0,104,height))).getbbox() is None + else: + x += 30 + for dy in range(44): + for dx in range(44): + if (dx-21)**2+(dy-21)**2 <= 21**2: + assert covered.getpixel((x+dx,y+dy)) == foil.getpixel((dx%104,dy%42)) + else: + paper = papers[kind] + assert covered.getpixel((x+dx,y+dy)) == paper.getpixel((x+dx-96,y+dy-92))[:3], "Circle leaks onto paper" + assert ImageChops.difference(covered,result).getbbox() is not None + sheet.save(OUT / "scratch-four-types.png") + covered_sheet.save(OUT / "scratch-four-covers.png") + print("PASS: four native layouts, Russian titles, opaque number/prize fields, circle clipping and large coating") + + +def check_portraits(): + glyphs = font() + maps = asset("portrait-fields-v3.bin",(540,720),3) + foil = asset("foil-v1.bin",(104,42),1)[0] + layouts = json.loads((ROOT/"assets/animations/scratch/portrait-layouts-v3.json").read_text()) + sheet = Image.new("RGB",(1920,960)) + for k,(mapping,layout) in enumerate(zip(maps,layouts),4): + frames = {} + for state in ("full","covered","result"): + frame = Image.open(OUT/f"scratch-kind-{k}-{state}.bmp").convert("RGB") + frame.save(OUT/f"scratch-kind-{k}-{state}.png") + frames[state] = frame + for state,vx,vy,vw,vh in (("full",194,46,252,336),("covered",68,-230,504,672)): + frame = frames[state] + samples = [0]*len(layout["fields"]) + for y in range(80,388): + sy = (y-vy)*720//vh + if not 0<=sy<720: + continue + for x in range(vx,vx+vw): + sx = (x-vx)*540//vw + i = mapping.getpixel((sx,sy))[2] + if i: + assert frame.getpixel((x,y)) == foil.getpixel((sx%104,sy%42))[:3], (k,state,x,y) + samples[i-1] += 1 + assert min(samples)>100,(k,state,samples) + result = frames["result"] + text_matches(result,"250",424,402,2,(144,213,173),glyphs) + text_matches(result,"+200",440,422,1,(144,213,173),glyphs) + for i,field in enumerate(layout["fields"]): + x,y,w,h = (field[q] for q in ("x","y","w","h")) + left,top = 68+x*504//540,-230+y*672//720 + width = (x+w)*504//540-x*504//540+1 + height = (y+h)*672//720-y*672//720+1 + value = str(i+1) if i!=layout["winning"] else "1" + ink = (5,104,55) if i==layout["winning"] else (48,59,50) + ty = top+height//2-4-(7 if i>=layout["winning"] else 0) + text_matches(result,value,left+(width-len(value)*8)//2,ty,1,ink,glyphs) + sheet.paste(frames["full"],((k-4)*640,0)) + sheet.paste(result,((k-4)*640,480)) + sheet.save(OUT/"scratch-portrait-types.png") + catalog = Image.open(OUT/"scratch-catalog.bmp").convert("RGB") + titles = ("ЗОЛОТАЯ ТРОЙКА","СЧАСТЛИВЫЕ ЧИСЛА","ТРИ В РЯД","МИГ УДАЧИ", + "НЕОНОВАЯ УДАЧА","РУБИНОВЫЙ ДЖЕКПОТ","ВОЛНА СОКРОВИЩ") + for i,title in enumerate(titles): + text_matches(catalog,title,188,122+i*38,2,(229,210,158),glyphs) + catalog.save(OUT/"scratch-catalog.png") + for i,(text,x,ink) in enumerate((("-50",440,(233,147,131)),("0",448,(173,185,188)),("+50",440,(144,213,173)))): + frame = Image.open(OUT/f"scratch-net-{i}.bmp").convert("RGB") + text_matches(frame,text,x,422,1,ink,glyphs) + text_matches(frame,"50",112,402,2,(255,255,255),glyphs) + print("PASS: 60 exact portrait fields in both scales, readable native numbers, seven named choices and signed net results") + + +def check_motion(): + coins = asset("coin-edge-v1.bin",(64,64),4) + frames = [] + for i in range(36): + frame = Image.open(OUT / f"scratch-stroke-{i:02}.bmp").convert("RGB") + row,step = divmod(i,12) + pointer_x = 164 + (308-28*step if row%2 else 28*step) + pointer_y = 183+15*row + coin = coins[((i*33)>>7)&3] + for y in range(64): + for x in range(64): + pixel = coin.getpixel((x,y)) + if pixel[3]: + assert frame.getpixel((pointer_x-32+x,pointer_y-60+y)) == pixel[:3], (i,x,y) + frames.append(frame) + frames[0].save(OUT / "scratch-edge-stroke.gif",save_all=True,append_images=frames[1:],duration=50,loop=0) + for name in ("pointer","counter"): + Image.open(OUT / f"scratch-{name}.bmp").save(OUT / f"scratch-{name}.png") + for kind in range(4,7): + motion = [] + for i in range(26): + frame = Image.open(OUT/f"scratch-portrait-{kind}-{i:02}.bmp").convert("RGB") + px,py = 220+20*(i%13),188+12*(i//13)+(i&1) + coin = coins[((i*33)>>7)&3] + for y in range(64): + for x in range(64): + pixel = coin.getpixel((x,y)) + if pixel[3]: + assert frame.getpixel((px-32+x,py-60+y)) == pixel[:3], (kind,i,x,y) + motion.append(frame) + assert len({hashlib.sha256(f.tobytes()).digest() for f in motion}) == 26 + motion[0].save(OUT/f"scratch-portrait-{kind}.gif",save_all=True,append_images=motion[1:],duration=60,loop=0) + print("PASS: 114 actual native motion frames, edge-coin contact and three portrait scratch sequences") + + +def check_effects(): + glyphs = font() + for kind in (0,4): + frames = [] + for i in range(31): + versions = [Image.open(OUT/f"scratch-deal-{kind}-rate-{r}-frame-{i:02}.bmp").convert("RGB") for r in range(3)] + assert all(ImageChops.difference(versions[0],frame).getbbox() is None for frame in versions[1:]),(kind,i,"frame-rate dependent paper") + text_matches(versions[0],"9950",112,54,2,(255,255,255),glyphs) + frames.append(versions[0]) + assert len({hashlib.sha256(f.tobytes()).digest() for f in frames})>(10 if kind==0 else 20) + # The normal ticket's title and numbers move together, not over a table screenshot. + assert frames[0].getpixel((320,200)) == asset("table-v1.bin",(640,480),1)[0].getpixel((320,200))[:3] + frames[0].save(OUT/f"scratch-delivery-{kind}.gif",save_all=True,append_images=frames[1:],duration=30,loop=0) + frames = [Image.open(OUT/f"scratch-payout-{i:02}.bmp").convert("RGB") for i in range(25)] + for frame in frames: + text_matches(frame,"10200",112,54,2,(255,255,255),glyphs) + text_matches(frame,"250",424,402,2,(144,213,173),glyphs) + assert len({hashlib.sha256(f.tobytes()).digest() for f in frames})>15 + frames[0].save(OUT/"scratch-payout.gif",save_all=True,append_images=frames[1:],duration=75,loop=0) + frames[-1].save(OUT/"scratch-payout.png") + colors = frames[-1].crop((190,170,255,220)).getcolors(65*50) + assert sum(count for count,pixel in colors if pixel==(213,239,220))>1000 + print("PASS: 186 frame-rate-identical paper/zoom captures, 25 payout frames, unchanged bank/RNG and highlighted winning field") + + +def parity(): + tested = Coff(OUT / "actions-test.obj") + shipped = Coff(ROOT / "build/graphics/poker.obj") + for name,argc in (("LoadScratchArt3D",0),("FreeScratchArt3D",0),("BeginScratch3D",0), + ("LeaveScratch3D",0),("BuyScratch3D",0),("FinishScratch3D",0),("SettleScratchOnQuit3D",0), + ("ScratchCellOrigin3D",4),("ScratchErasePixel3D",8),("ScratchStamp3D",8), + ("ScratchStroke3D",16),("ScratchApplyPointer3D",0),("ScratchMouseEvent3D",16), + ("ScratchConfigure3D",0),("ScratchSelectType3D",4),("ScratchBuildVariant3D",0),("ScratchDrawContent3D",12), + ("HandleScratch3D",0),("RenderScratch3D",0),("ScratchCanWalk3D",8), + ("ScratchBuildPortrait3D",0),("ScratchPortraitPayout3D",0), + ("ScratchErasePortrait3D",8),("ScratchPortraitLayers3D",4), + ("ScratchPortraitHit3D",8),("RenderScratchPortrait3D",0), + ("ScratchConfigurePortrait3D",0),("ScratchPortraitField3D",4), + ("ScratchPortraitMapBase3D",0),("ScratchPortraitView3D",0), + ("ScratchToggleZoom3D",0),("ScratchPortraitBounds3D",4), + ("ScratchPortraitText3D",4),("ScratchPortraitInfo3D",0), + ("HandleScratchCatalog3D",0),("RenderScratchCatalog3D",0),("RenderScratchNet3D",0), + ("LoadAnimPack3D",8), + ("ScratchStartDeal3D",0),("ScratchStartPayout3D",0),("ScratchTickVisuals3D",0), + ("ScratchSpawnDust3D",0),("ScratchDrawDust3D",0),("ScratchSaveBackdrop3D",0), + ("ScratchDrawDelivery3D",0),("ScratchDrawPayout3D",0),("ScratchEase3D",8), + ("WindowEvents3D",16),("TriggerHallInteraction3D",0),("CheckHallInteractions3D",0)): + symbol = f"_{name}@{argc}" + assert tested.signature(symbol) == shipped.signature(symbol), name + print("PASS: shipping input, coating, economy, hall entry and graphics match native-tested MASM") + + +if __name__ == "__main__": + p = argparse.ArgumentParser() + p.add_argument("--parity",action="store_true") + p.add_argument("--assets-only",action="store_true") + p.add_argument("--motion",action="store_true") + args = p.parse_args() + check_assets() + if not args.assets_only: + check_captures() + check_variants() + check_portraits() + check_effects() + if args.parity: + parity() + if args.motion: + check_motion() diff --git a/tools/tests/check_world_dressing.py b/tools/tests/check_world_dressing.py new file mode 100644 index 0000000..5c8b295 --- /dev/null +++ b/tools/tests/check_world_dressing.py @@ -0,0 +1,103 @@ +"""Check real MASM framebuffer captures, packed decor and tested/shipping parity.""" +import argparse +import struct +from pathlib import Path + +from PIL import Image, ImageChops, ImageStat +from check_card_exit_build import Coff + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "build/actions" +FUNCTIONS = { + "LoadWorldDressing3D": 0, "FreeWorldDressing3D": 0, + "ResolveDressingSurface3D": 4, "RenderHallDressing3D": 0, + "RenderStreetDressing3D": 0, "HallLightVisible3D": 16, + "StreetGroundHeight3D": 8, "UpdateStreetGround3D": 0, + "AddHallLight3D": 16, "AddHallContact3D": 12, + "BuildHallLighting3D": 0, "ApplyHallLighting3D": 0, + "InitHall3D": 0, "UpdateHall3D": 0, "RenderHall3D": 0, + "LoadHallAssets3D": 0, "FreeHallAssets3D": 0, + "PositionCityCamera3D": 0, "RenderCityArrival3D": 0, + "DrawCityHorizontal3D": 24, "ResolveCitySurface3D": 4, + "RenderWalls": 0, +} + + +def assets(): + raw = (ROOT / "assets/animations/hall/world-dressing-v6.bin").read_bytes() + assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 128, 128, 3, 196608) + assert len(raw) == 196628 + for index in range(3): + start = 20 + index * 65536 + frame = Image.frombytes("RGBA", (128, 128), raw[start:start+65536], "raw", "BGRA") + assert set(frame.getchannel("A").tobytes()) <= {0, 255} + assert frame.getbbox() == (0, 0, 128, 128) + assert len(frame.convert("RGB").getcolors(16384)) > (100 if index < 2 else 5) + print("PASS: two framed paintings and a drain material; valid dimensions, length and binary alpha") + + +def capture(name): + frame = Image.open(OUT / (name + ".bmp")).convert("RGB") + assert frame.size == (640, 480), name + assert len(frame.getcolors(307200)) > 1000, name + frame.save(OUT / (name + ".png")) + return frame + + +def captures(): + unlit = capture("world-hall-unlit") + lit = capture("world-hall-lit") + difference = ImageChops.difference(unlit, lit) + assert difference.getbbox() == (0, 0, 640, 480), "Light pass did not cover the world" + values = [sum(pixel) for pixel in difference.get_flattened_data()] + assert sum(value > 6 for value in values) > 170000, "No visible lighting change" + # Lighting changes channel gains, not the underlying texture resolution. + for channel in range(3): + gains = [new[channel] / old[channel] for old, new in zip(unlit.get_flattened_data(), lit.get_flattened_data()) + if 32 <= old[channel] <= 180] + assert min(gains) > .4 and max(gains) < 1.5, channel + regions = [(0, 0, 160, 160), (240, 240, 400, 400), (400, 300, 560, 460)] + means = [ImageStat.Stat(difference.crop(region)).mean for region in regions] + assert max(sum(mean) for mean in means) - min(sum(mean) for mean in means) > 3 + front = capture("world-wall-front") + angle = capture("world-wall-angle") + assert ImageChops.difference(front, angle).getbbox(), "World-mounted decor did not reproject" + frames = [capture("world-street-" + phase) for phase in ("early", "middle", "late")] + assert all(ImageChops.difference(a, b).getbbox() for a, b in zip(frames, frames[1:])) + print("PASS: native lit/unlit comparison, preserved texture detail, wall parallax and three street positions") + + +def parity(): + tested = Coff(OUT / "actions-test.obj") + shipped = Coff(ROOT / "build/graphics/poker.obj") + for name, arguments in FUNCTIONS.items(): + symbol = f"_{name}@{arguments}" + assert tested.signature(symbol) == shipped.signature(symbol), name + # Test-only pixel ownership counters wrap this renderer. Compare the complete + # production body after its register prologue, excluding only its epilogue. + symbol = "_RenderCityObjects3D@0" + actual = tested.signature(symbol) + expected = shipped.signature(symbol) + begin = next(i for i, item in enumerate(expected) if item[2] == "_AnimateCityCars3D@0") + end = len(expected) + epilogue = {b"\x5f", b"\x5e", b"\x5b", b"\x5d", b"\xc9", b"\xc3", b"\x8b\xe5"} + while end > begin and expected[end-1][0] in epilogue: + end -= 1 + core = expected[begin:end] + assert len(core) > 35 and len(expected)-end >= 4 + offset = next(i for i, item in enumerate(actual) if item[2] == "_AnimateCityCars3D@0") + assert actual[offset:offset+len(core)] == core, "Street object production body" + assert any(item[2] == "_UpdateStreetGround3D@0" for item in core) + print("PASS: shipped hall/street dressing, light baker, shader and camera match native-tested MASM") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--assets-only", action="store_true") + parser.add_argument("--parity", action="store_true") + options = parser.parse_args() + assets() + if not options.assets_only: + captures() + if options.parity: + parity() diff --git a/tools/tests/dice_scene_test.inc b/tools/tests/dice_scene_test.inc new file mode 100644 index 0000000..bd0acc3 --- /dev/null +++ b/tools/tests/dice_scene_test.inc @@ -0,0 +1,159 @@ +.data +diceSceneFormat3D db "build\actions\dice-scene-%02u.bmp",0 +diceSceneName3D db 128 dup(0) +diceReadySnapshot3D db "build\actions\dice-ready.bmp",0 +diceMissingSnapshot3D db "build\actions\dice-missing.bmp",0 +diceHeldSnapshot3D db "build\actions\dice-held-last.bmp",0 +diceReleaseSnapshot3D db "build\actions\dice-release.bmp",0 +diceFaceFormat3D db "build\actions\dice-face-%u.bmp",0 +.code +RunDiceSceneTest3D PROC uses ebx esi edi + local oldBank:DWORD,oldRng:DWORD,expected:DWORD,renderRng:DWORD + mov eax,bankroll + mov oldBank,eax + mov eax,rngSeed3D + mov oldRng,eax + call InitDiceGame + cmp diceArtReady3D,1 + jne dice_scene_test_fail + mov dice3d_val1,3 + mov dice3d_val2,6 + call RenderDiceGame + invoke WriteActionSnapshot3D,OFFSET diceReadySnapshot3D + mov dice3d_state,1 + xor esi,esi +dice_scene_capture: + imul eax,esi,40 + mov dice3d_timer,eax + call RenderDiceGame + invoke wsprintfA,OFFSET diceSceneName3D,OFFSET diceSceneFormat3D,esi + invoke WriteActionSnapshot3D,OFFSET diceSceneName3D + inc esi + cmp esi,40 + jb dice_scene_capture + mov dice3d_timer,239 + call RenderDiceGame + invoke WriteActionSnapshot3D,OFFSET diceHeldSnapshot3D + mov dice3d_timer,240 + call RenderDiceGame + invoke WriteActionSnapshot3D,OFFSET diceReleaseSnapshot3D + ; Every declared result must be on the upward-facing plane, with no RNG + ; calls in drawing and no leaked shadow state for other cube callers. + mov dice3d_state,2 + mov esi,1 +dice_scene_faces: + mov dice3d_val1,esi + mov eax,7 + sub eax,esi + mov dice3d_val2,eax + mov dice3d_sum,7 + mov eax,rngSeed3D + mov renderRng,eax + call RenderDiceGame + mov eax,dice3d_val2 + cmp cubeFaceValues3D[16],eax + jne dice_scene_test_fail + mov eax,renderRng + cmp rngSeed3D,eax + jne dice_scene_test_fail + cmp cubeContactShadow3D,0 + jne dice_scene_test_fail + cmp diceShadowPass3D,0 + jne dice_scene_test_fail + invoke wsprintfA,OFFSET diceSceneName3D,OFFSET diceFaceFormat3D,esi + invoke WriteActionSnapshot3D,OFFSET diceSceneName3D + inc esi + cmp esi,6 + jbe dice_scene_faces + ; Both parity choices, four tick sizes, queued mouse/keyboard-style exits. + xor esi,esi +dice_scene_round: + call ClearKeys3D + mov mouseClick3D,0 + mov engineMode,MODE_DICE + mov bankroll,10000 + mov dice3d_bet,50 + mov eax,esi + and eax,1 + inc eax + mov dice3d_choice,eax + call InitDiceGame + mov pressedKeys3D[20h],1 + call HandleDiceInput + cmp dice3d_state,1 + jne dice_scene_test_fail + cmp bankroll,9950 + jne dice_scene_test_fail + mov eax,dice3d_val1 + add eax,dice3d_val2 + and eax,1 + inc eax + ; Even is choice 1; odd is choice 2. + mov expected,9950 + cmp eax,dice3d_choice + jne @F + add expected,100 +@@: + call DiceEscape3D + cmp diceExitQueued3D,1 + jne dice_scene_test_fail + cmp engineMode,MODE_DICE + jne dice_scene_test_fail + mov eax,esi + and eax,3 + inc eax + imul eax,17 + mov deltaTime3D,eax + mov edi,100 +dice_scene_tick: + call HandleDiceInput + cmp dice3d_state,2 + je dice_scene_settled + dec edi + jnz dice_scene_tick + jmp dice_scene_test_fail +dice_scene_settled: + mov eax,expected + cmp bankroll,eax + jne dice_scene_test_fail + cmp engineMode,MODE_WALK + jne dice_scene_test_fail + call HandleDiceInput + mov eax,expected + cmp bankroll,eax + jne dice_scene_test_fail + inc esi + cmp esi,300 + jb dice_scene_round + ; Footer clicks use the same original rules as keyboard commands. + call InitDiceGame + call ClearKeys3D + mov dice3d_choice,1 + mov mouseClick3D,1 + mov mouseX3D,130 + mov mouseY3D,225 + call HandleDiceInput + cmp dice3d_choice,2 + jne dice_scene_test_fail + mov mouseClick3D,1 + mov mouseX3D,145 + mov mouseY3D,205 + call HandleDiceInput + cmp dice3d_bet,100 + jne dice_scene_test_fail + mov diceArtReady3D,0 + call RenderDiceGame + invoke WriteActionSnapshot3D,OFFSET diceMissingSnapshot3D + mov diceArtReady3D,1 + mov eax,oldBank + mov bankroll,eax + mov eax,oldRng + mov rngSeed3D,eax + mov mouseClick3D,0 + call ClearKeys3D + mov eax,1 + ret +dice_scene_test_fail: + xor eax,eax + ret +RunDiceSceneTest3D ENDP diff --git a/tools/tests/eightball_regression.inc b/tools/tests/eightball_regression.inc new file mode 100644 index 0000000..84fe4c6 --- /dev/null +++ b/tools/tests/eightball_regression.inc @@ -0,0 +1,348 @@ +.data +eightRegressionStage3D dd 0 +eightRegressionShotBmp3D db "build\actions\eight-human-shot.bmp",0 +eightRegressionGroupBmp3D db "build\actions\eight-group-credit.bmp",0 +.code +EightRegressionPot3D PROC uses ebx + mov pressedKeys3D[20h],1 + mov deltaTime3D,16 + call HandlePool3D + cmp poolState3D,POOL_STRIKE + jne eight_regression_pot_failed + call RenderPool3D + mov deltaTime3D,240 + call HandlePool3D + mov ebx,2000 +eight_regression_motion: + mov deltaTime3D,16 + call HandlePool3D + call RenderPool3D + cmp poolState3D,POOL_MOVING + jne eight_regression_pot_done + dec ebx + jnz eight_regression_motion +eight_regression_pot_failed: + xor eax,eax + ret +eight_regression_pot_done: + mov eax,1 + ret +EightRegressionPot3D ENDP + +RunEightRegression3D PROC uses ebx esi edi + local actor:DWORD,guide:DWORD,oldBank:DWORD,oldSeed:DWORD + mov eax,bankroll + mov oldBank,eax + mov eax,rngSeed3D + mov oldSeed,eax + mov eightRegressionStage3D,10 + invoke StartBilliards3D,1 + mov poolBreak3D,0 + mov poolGroups3D,1 + mov poolGroups3D[4],2 + mov esi,1 +eight_regression_clear: + mov poolActive3D[esi*4],0 + inc esi + cmp esi,16 + jb eight_regression_clear + mov poolActive3D[5*4],1 + mov poolActive3D[6*4],1 + mov poolActive3D[9*4],1 + mov poolX3D,160*FP_1 + mov poolY3D,100*FP_1 + mov poolX3D[5*4],160*FP_1 + mov poolY3D[5*4],70*FP_1 + mov poolX3D[6*4],220*FP_1 + mov poolY3D[6*4],120*FP_1 + mov poolX3D[9*4],240*FP_1 + mov poolY3D[9*4],140*FP_1 + mov poolPower3D,45000 + invoke PoolAimPoint3D,160*FP_1,70*FP_1 + ; Ordinary aim/Space/render path, with the stale default call still present. + call EightRegressionPot3D + test eax,eax + jz eight_regression_failed + mov eightRegressionStage3D,11 + cmp poolActive3D[5*4],0 + jne eight_regression_failed + mov eightRegressionStage3D,12 + cmp poolPlayer3D,0 + jne eight_regression_failed + cmp poolState3D,POOL_AIM + jne eight_regression_failed + cmp pyrOwner3D[5*4],0 + jne eight_regression_failed + cmp pyrScore3D,6 + jne eight_regression_failed + cmp pyrScore3D[4],6 + jne eight_regression_failed + invoke WriteActionSnapshot3D,OFFSET eightRegressionShotBmp3D + mov eightRegressionStage3D,13 + mov deltaTime3D,1200 + call HandlePool3D + cmp poolState3D,POOL_AIM + jne eight_regression_failed + ; The same human must be able to make a second normal shot immediately. + mov poolX3D,160*FP_1 + mov poolY3D,100*FP_1 + mov poolX3D[6*4],160*FP_1 + mov poolY3D[6*4],70*FP_1 + invoke PoolAimPoint3D,160*FP_1,70*FP_1 + call EightRegressionPot3D + cmp eax,1 + jne eight_regression_failed + cmp poolPlayer3D,0 + jne eight_regression_failed + cmp pyrScore3D,7 + jne eight_regression_failed + cmp pyrOwner3D[6*4],0 + jne eight_regression_failed + ; The same ordinary-shot path works for either actor with guides on or off. + mov actor,0 +eight_regression_actor: + mov guide,0 +eight_regression_guide: + mov eightRegressionStage3D,14 + invoke StartBilliards3D,1 + mov eax,actor + mov poolPlayer3D,eax + mov poolAI3D,0 + mov eax,guide + mov pyrGuideOn3D,eax + mov poolBreak3D,0 + mov esi,1 +eight_regression_actor_clear: + mov poolActive3D[esi*4],0 + inc esi + cmp esi,16 + jb eight_regression_actor_clear + mov poolActive3D[5*4],1 + mov poolActive3D[9*4],1 + mov poolX3D,160*FP_1 + mov poolY3D,100*FP_1 + mov poolX3D[5*4],160*FP_1 + mov poolY3D[5*4],70*FP_1 + mov poolX3D[9*4],240*FP_1 + mov poolY3D[9*4],140*FP_1 + mov poolPower3D,45000 + invoke PoolAimPoint3D,160*FP_1,70*FP_1 + call RenderPool3D + cmp poolCalled3D,5 + jne eight_regression_failed + cmp poolCalledPocket3D,1 + jne eight_regression_failed + call EightRegressionPot3D + cmp eax,1 + jne eight_regression_failed + mov ebx,actor + cmp poolPlayer3D,ebx + jne eight_regression_failed + cmp poolGroups3D[ebx*4],1 + jne eight_regression_failed + cmp pyrOwner3D[5*4],ebx + jne eight_regression_failed + inc guide + cmp guide,2 + jb eight_regression_guide + inc actor + cmp actor,2 + jb eight_regression_actor + mov poolAI3D,1 + mov pyrGuideOn3D,1 + ; Each independent manual override remains binding across real input/render. + mov guide,0 +eight_regression_manual: + mov eightRegressionStage3D,15 + invoke StartBilliards3D,1 + mov poolBreak3D,0 + mov poolGroups3D,1 + mov poolGroups3D[4],2 + mov esi,1 +eight_regression_manual_clear: + mov poolActive3D[esi*4],0 + inc esi + cmp esi,16 + jb eight_regression_manual_clear + mov poolActive3D[5*4],1 + mov poolActive3D[2*4],1 + mov poolActive3D[9*4],1 + mov poolX3D,160*FP_1 + mov poolY3D,100*FP_1 + mov poolX3D[5*4],160*FP_1 + mov poolY3D[5*4],70*FP_1 + mov poolX3D[2*4],220*FP_1 + mov poolY3D[2*4],120*FP_1 + mov poolX3D[9*4],240*FP_1 + mov poolY3D[9*4],140*FP_1 + mov poolPower3D,45000 + invoke PoolAimPoint3D,160*FP_1,70*FP_1 + mov eax,guide + mov poolCallManual3D,eax + mov poolCalled3D,2 + mov poolCalledPocket3D,4 + call RenderPool3D + test guide,1 + jz eight_regression_auto_ball + cmp poolCalled3D,2 + jne eight_regression_failed + jmp eight_regression_manual_pocket +eight_regression_auto_ball: + cmp poolCalled3D,5 + jne eight_regression_failed +eight_regression_manual_pocket: + test guide,2 + jz eight_regression_auto_pocket + cmp poolCalledPocket3D,4 + jne eight_regression_failed + jmp eight_regression_manual_shoot +eight_regression_auto_pocket: + mov eax,1 + test guide,1 + jz @F + mov eax,-1 +@@: + cmp poolCalledPocket3D,eax + jne eight_regression_failed +eight_regression_manual_shoot: + call EightRegressionPot3D + cmp eax,1 + jne eight_regression_failed + cmp poolActive3D[5*4],0 + jne eight_regression_failed + xor eax,eax + cmp guide,0 + je @F + inc eax +@@: + cmp poolPlayer3D,eax + jne eight_regression_failed + cmp poolCallManual3D,0 + jne eight_regression_failed + inc guide + cmp guide,4 + jb eight_regression_manual + ; Exhaustive group credit is independent of the shooter and a foul. + mov actor,0 +eight_regression_credit_actor: + mov guide,0 +eight_regression_credit_foul: + mov eightRegressionStage3D,16 + invoke StartBilliards3D,1 + mov eax,actor + mov poolPlayer3D,eax + mov poolGroups3D,1 + mov poolGroups3D[4],2 + mov poolBreak3D,0 + mov poolFirst3D,1 + cmp actor,0 + je @F + mov poolFirst3D,9 +@@: + mov poolRail3D,1 + mov poolCalled3D,1 + mov poolCalledPocket3D,1 + mov poolPotted3D,202h + mov poolActive3D[4],0 + mov poolActive3D[9*4],0 + mov poolPocket3D[4],1 + mov poolPocket3D[9*4],1 + cmp guide,0 + je @F + mov poolActive3D,0 + or poolPotted3D,1 +@@: + call PoolFinishShot3D + cmp pyrScore3D,1 + jne eight_regression_failed + cmp pyrScore3D[4],1 + jne eight_regression_failed + cmp pyrOwner3D[4],0 + jne eight_regression_failed + cmp pyrOwner3D[9*4],1 + jne eight_regression_failed + call PoolFinishShot3D + cmp pyrScore3D,1 + jne eight_regression_failed + cmp pyrScore3D[4],1 + jne eight_regression_failed + inc guide + cmp guide,2 + jb eight_regression_credit_foul + inc actor + cmp actor,2 + jb eight_regression_credit_actor + call RenderPool3D + invoke WriteActionSnapshot3D,OFFSET eightRegressionGroupBmp3D + ; Real AI scheduling retains a legal pot but stops after a miss. + mov eightRegressionStage3D,17 + invoke StartBilliards3D,1 + mov poolPlayer3D,1 + mov poolAI3D,1 + mov poolGroups3D,2 + mov poolGroups3D[4],1 + mov poolBreak3D,0 + mov esi,1 +eight_regression_ai_clear: + mov poolActive3D[esi*4],0 + inc esi + cmp esi,16 + jb eight_regression_ai_clear + mov poolActive3D[5*4],1 + mov poolActive3D[6*4],1 + mov poolActive3D[9*4],1 + mov poolX3D,160*FP_1 + mov poolY3D,100*FP_1 + mov poolX3D[5*4],160*FP_1 + mov poolY3D[5*4],70*FP_1 + mov poolX3D[6*4],220*FP_1 + mov poolY3D[6*4],120*FP_1 + mov poolX3D[9*4],240*FP_1 + mov poolY3D[9*4],140*FP_1 + mov deltaTime3D,1200 + call HandlePool3D + cmp poolState3D,POOL_STRIKE + jne eight_regression_failed + call RenderPool3D + mov deltaTime3D,240 + call HandlePool3D + mov ebx,2000 +eight_regression_ai_motion: + mov deltaTime3D,16 + call HandlePool3D + call RenderPool3D + cmp poolState3D,POOL_MOVING + jne eight_regression_ai_finished + dec ebx + jnz eight_regression_ai_motion + jmp eight_regression_failed +eight_regression_ai_finished: + cmp poolActive3D[5*4],0 + jne eight_regression_failed + cmp poolPlayer3D,1 + jne eight_regression_failed + cmp pyrOwner3D[5*4],1 + jne eight_regression_failed + mov pyrSettled3D,0 + mov poolFirst3D,6 + mov poolRail3D,1 + mov poolPotted3D,0 + call PoolFinishShot3D + cmp poolPlayer3D,0 + jne eight_regression_failed + mov deltaTime3D,1200 + call HandlePool3D + cmp poolState3D,POOL_AIM + jne eight_regression_failed + mov eax,oldBank + cmp bankroll,eax + jne eight_regression_failed + mov eax,oldSeed + cmp rngSeed3D,eax + jne eight_regression_failed + xor eax,eax + ret +eight_regression_failed: + mov eax,eightRegressionStage3D + ret +RunEightRegression3D ENDP diff --git a/tools/tests/export_scratch_probe.py b/tools/tests/export_scratch_probe.py new file mode 100644 index 0000000..b7fdc40 --- /dev/null +++ b/tools/tests/export_scratch_probe.py @@ -0,0 +1,45 @@ +"""Expose selected data labels in a COPY of the tested COFF for a render probe. + +No instructions, relocations or game executable are rewritten. Native scene +procedures stay byte-identical to the action-tested game; only COFF visibility +changes so a small ASM entry point can set deterministic scene inputs. +""" +from pathlib import Path +import struct +from check_card_exit_build import Coff + +ROOT = Path(__file__).resolve().parents[2] +NAMES = { + "bankroll","rngSeed3D","engineMode","scrState3D","scrPriceIndex3D", + "scrHeld3D","scrPointerValid3D","scrPointerX3D","scrPointerY3D", + "animationClock3D","deltaTime3D","playerX","playerY","playerAngle","scrTableArt3D","interactTargetId", + "scrType3D","scrZoom3D","scrCounts3D","scrCatalog3D", + "scrDealMs3D", +} + + +def main(): + source = ROOT / "build/actions/actions-test.obj" + coff = Coff(source) + original = coff.data + data = bytearray(original) + offset = struct.unpack_from(" 0: + target = 3 + if target is not None and target != storage: + data[at+16] = target + changes.add(at+16) + assert found == EXPOSE, EXPOSE-found + assert {i for i, (a, b) in enumerate(zip(original, data)) if a != b} == changes + (folder / "helpers.obj").write_bytes(data) + print("Prepared test-only native helpers; only COFF visibility bytes changed") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/prepare_scratch_module.py b/tools/tests/prepare_scratch_module.py new file mode 100644 index 0000000..5ccbd9a --- /dev/null +++ b/tools/tests/prepare_scratch_module.py @@ -0,0 +1,46 @@ +"""Link fresh scratch MASM against existing native graphics helpers for fast QA. + +Only COFF symbol visibility changes in a test-only COPY. Instructions and +relocations remain untouched; neither shipping binaries nor source are patched. +The full engine is rebuilt/tested separately before publication. +""" +from pathlib import Path +import struct +from check_card_exit_build import Coff + +ROOT = Path(__file__).resolve().parents[2] +EXPOSE = {"engineMode","paused3D","bankroll","deltaTime3D","animationClock3D", + "viewWidth3D","viewHeight3D","viewLeft3D","viewTop3D","clpSymbolsPath3D", + "coinExitText3D","coinBankText3D","hudNumBuffer","screenBuffer","rngSeed3D","h3DWnd","lastSoundPath3D"} + + +def main(): + coff = Coff(ROOT/"build/actions/actions-test.obj") + original = coff.data + data = bytearray(original) + symbols = struct.unpack_from("0 + exposed.add(name) + target = 2 + elif ("Scratch" in name or name=="_LoadAnimPack3D@8") and kind==0x20 and storage==2 and section>0: + target = 3 + if target is not None and storage!=target: + data[at+16] = target + touched.add(at+16) + assert exposed==EXPOSE,EXPOSE-exposed + assert {i for i,(a,b) in enumerate(zip(original,data)) if a!=b}==touched + (ROOT/"build/scratch/module-base.obj").write_bytes(data) + loader = (ROOT/"src_graphics/scene_animation.inc").read_text(encoding="cp1251") + loader = loader.split("LoadAnimPack3D ENDP",1)[0]+"LoadAnimPack3D ENDP\n" + (ROOT/"build/scratch/module-loader.inc").write_text(loader,encoding="ascii") + print("Prepared test-only native helper object; all code and relocations unchanged") + + +if __name__=="__main__": + main() diff --git a/tools/tests/prepare_world_dressing_probe.py b/tools/tests/prepare_world_dressing_probe.py new file mode 100644 index 0000000..0573734 --- /dev/null +++ b/tools/tests/prepare_world_dressing_probe.py @@ -0,0 +1,51 @@ +"""Expose native state in a test-only COFF copy; never patch instructions.""" +from pathlib import Path +import struct +from check_card_exit_build import Coff + +ROOT = Path(__file__).resolve().parents[2] +EXPOSE = {"casinoMap","hallLevel3D","outdoorRender3D","hallLights3D","hallLoungeDecor3D", + "hallRopeFurniture3D","hallLampFurniture3D","cityObjects3D","hallStreetLife3D","cityDecorPack3D", + "playerX","playerY","playerAngle","viewEye3D","viewCeiling3D","viewHorizon3D", + "worldMap3D","floorTexture3D","cityMap3D","cityPhase3D","cityTime3D","cityClock3D","arrivalState3D", + "screenBuffer","cityDepthBuffer3D","cityRayX3D","cityRayY3D","zBuffer","cityBackdrop3D", + "playerPlaneX","playerPlaneY"} + + +def main(): + out = ROOT/"build/world" + out.mkdir(parents=True,exist_ok=True) + baseline = out/"prior-release.obj" + if not baseline.exists(): + baseline.write_bytes((ROOT/"build/actions/actions-test.obj").read_bytes()) + coff = Coff(baseline) + original = coff.data + data = bytearray(original) + start = struct.unpack_from("0: + target = 3 + if target is not None and target!=storage: + data[at+16] = target + changed.add(at+16) + assert found==EXPOSE,EXPOSE-found + assert {i for i,(a,b) in enumerate(zip(data,original)) if a!=b}==changed + (out/"base.obj").write_bytes(data) + source = (ROOT/"src_graphics/city_bins.inc").read_text(encoding="cp1251") + bodies = [] + for name in ("ResolveCitySurface3D","DrawCityVertical3D","DrawCityHorizontal3D"): + body = source.split(name+" PROC",1)[1].split(name+" ENDP",1)[0] + bodies.append(name+" PROC"+body+name+" ENDP\n") + (out/"surfaces.inc").write_text("\n".join(bodies),encoding="ascii") + print("Prepared native renderer probe: storage visibility only, no modified code or relocations") + + +if __name__=="__main__": + main() diff --git a/tools/tests/pyramid_test.inc b/tools/tests/pyramid_test.inc index 17674af..7a2c6b0 100644 --- a/tools/tests/pyramid_test.inc +++ b/tools/tests/pyramid_test.inc @@ -7,6 +7,7 @@ pyrTestPenalty db "build\actions\pyramid-penalty.bmp",0 pyrTestChoice db "build\actions\pyramid-choice.bmp",0 pyrTestWin db "build\actions\pyramid-win.bmp",0 .code +RunBilliardsChoiceTest3D PROTO PyramidTestLedger3D PROC uses ebx esi edi xor esi,esi xor ebx,ebx @@ -56,7 +57,7 @@ RunPyramidTest3D PROC uses ebx esi edi mov poolAI3D,0 call ClearKeys3D mov pyrTestStage3D,1 - call InitPool3D + call ResetPool3D call PyramidTestLedger3D test eax,eax jnz pyr_test_fail @@ -74,7 +75,7 @@ RunPyramidTest3D PROC uses ebx esi edi cmp poolActive3D,1 jne pyr_test_fail mov pyrTestStage3D,2 - call InitPool3D + call ResetPool3D mov poolBreak3D,0 invoke PoolAimPoint3D,250*FP_1,115*FP_1+FP_1/2 test pyrFine3D,7 @@ -104,7 +105,7 @@ RunPyramidTest3D PROC uses ebx esi edi cmp poolVX3D,0 jne pyr_test_fail mov pyrTestStage3D,3 - call InitPool3D + call ResetPool3D mov poolBreak3D,0 mov poolFirst3D,1 mov poolActive3D,0 @@ -126,7 +127,7 @@ RunPyramidTest3D PROC uses ebx esi edi test eax,eax jnz pyr_test_fail mov pyrTestStage3D,4 - call InitPool3D + call ResetPool3D mov poolBreak3D,0 mov poolFirst3D,-1 mov poolActive3D[3*4],0 @@ -158,7 +159,7 @@ RunPyramidTest3D PROC uses ebx esi edi test eax,eax jnz pyr_test_fail ; Invalid break has partial-rack choices; the penalty is not erased. - call InitPool3D + call ResetPool3D mov poolFirst3D,1 mov poolBreakRails3D,6 call PoolFinishShot3D @@ -177,13 +178,13 @@ RunPyramidTest3D PROC uses ebx esi edi jnz pyr_test_fail mov pyrTestStage3D,5 ; Three object cushions, or two plus an object crossing, is a legal break. - call InitPool3D + call ResetPool3D mov poolFirst3D,1 mov poolBreakRails3D,0Eh call PoolFinishShot3D cmp poolFoul3D,0 jne pyr_test_fail - call InitPool3D + call ResetPool3D mov poolFirst3D,1 mov poolBreakRails3D,6 mov pyrHistoryCross3D[4],1 @@ -192,7 +193,7 @@ RunPyramidTest3D PROC uses ebx esi edi jne pyr_test_fail mov pyrTestStage3D,6 ; Legal completion needs ordered events along the same transfer chain. - call InitPool3D + call ResetPool3D mov poolBreak3D,0 mov poolFirst3D,1 invoke PyramidRail3D,1,0 @@ -202,7 +203,7 @@ RunPyramidTest3D PROC uses ebx esi edi invoke PyramidRail3D,1,1 cmp pyrValidEnd3D,1 jne pyr_test_fail - call InitPool3D + call ResetPool3D mov poolFirst3D,1 mov pyrPreviousX3D[4],159*FP_1 mov poolX3D[4],161*FP_1 @@ -213,7 +214,7 @@ RunPyramidTest3D PROC uses ebx esi edi invoke PyramidRail3D,1,0 cmp pyrValidEnd3D,1 jne pyr_test_fail - call InitPool3D + call ResetPool3D mov poolFirst3D,1 invoke PyramidRail3D,1,0 mov pyrPreviousX3D[4],159*FP_1 @@ -221,7 +222,7 @@ RunPyramidTest3D PROC uses ebx esi edi invoke PyramidCross3D,1 cmp pyrValidEnd3D,1 jne pyr_test_fail - call InitPool3D + call ResetPool3D mov poolFirst3D,1 invoke PyramidRail3D,1,0 mov poolX3D[4],80*FP_1 @@ -232,7 +233,7 @@ RunPyramidTest3D PROC uses ebx esi edi jne pyr_test_fail mov pyrTestStage3D,7 ; The real bot must pot through geometry, not inject a pocket result. - call InitPool3D + call ResetPool3D mov poolBreak3D,0 mov poolPlayer3D,1 mov poolAI3D,0 @@ -266,7 +267,7 @@ pyr_test_ai_tick: cmp poolPocket3D[4],1 jne pyr_test_fail mov pyrTestStage3D,8 - call InitPool3D + call ResetPool3D mov poolBreak3D,0 mov poolFirst3D,9 mov poolPotted3D,01FEh @@ -289,6 +290,9 @@ pyr_test_winning_balls: jnz pyr_test_fail call RenderPool3D invoke WriteActionSnapshot3D,OFFSET pyrTestWin + call RunBilliardsChoiceTest3D + test eax,eax + jnz pyr_test_choice_fail mov eax,oldBank cmp bankroll,eax jne pyr_test_fail @@ -302,8 +306,11 @@ pyr_test_winning_balls: call ClearKeys3D xor eax,eax ret +pyr_test_choice_fail: + ret pyr_test_fail: mov eax,pyrTestStage3D add eax,920 ret RunPyramidTest3D ENDP +include tools\tests\billiards_choice_test.inc diff --git a/tools/tests/scratch_effects_test.inc b/tools/tests/scratch_effects_test.inc new file mode 100644 index 0000000..3a01f20 --- /dev/null +++ b/tools/tests/scratch_effects_test.inc @@ -0,0 +1,169 @@ +.data +scratchFXDeal3D db "build\actions\scratch-deal-" +scratchFXKind3D db "0-rate-" +scratchFXRate3D db "0-frame-" +scratchFXFrameTens3D db "0" +scratchFXFrameOnes3D db "0.bmp",0 +scratchFXPay3D db "build\actions\scratch-payout-" +scratchFXPayTens3D db "0" +scratchFXPayOnes3D db "0.bmp",0 +scratchFXSteps3D dd 10,15,30 +.code +RunScratchEffectsTest3D PROC uses ebx esi edi + local kind:DWORD,rate:DWORD,frame:DWORD,elapsedMs:DWORD,targetMs:DWORD,cosmeticRng:DWORD + mov kind,0 +scratch_fx_kind: + mov rate,0 +scratch_fx_rate: + mov scrState3D,0 + mov eax,kind + mov scrType3D,eax + mov scrPriceIndex3D,0 + mov bankroll,10000 + mov rngSeed3D,47 + call BuyScratch3D + cmp scrDealMs3D,900 + jne scratch_fx_fail + cmp lastSoundPath3D,OFFSET scrPaperSound3D + jne scratch_fx_fail + mov eax,rngSeed3D + mov cosmeticRng,eax + invoke ScratchStamp3D,200,195 + cmp scrCounts3D,0 + jne scratch_fx_fail + mov eax,kind + add eax,'0' + mov scratchFXKind3D,al + mov eax,rate + add eax,'0' + mov scratchFXRate3D,al + mov frame,0 + mov elapsedMs,0 + mov scrPointerValid3D,0 +scratch_fx_deal_frame: + imul eax,frame,30 + mov targetMs,eax +scratch_fx_deal_tick: + mov eax,elapsedMs + cmp eax,targetMs + jae scratch_fx_deal_render + mov eax,rate + mov eax,scratchFXSteps3D[eax*4] + mov deltaTime3D,eax + add elapsedMs,eax + call ScratchTickVisuals3D + jmp scratch_fx_deal_tick +scratch_fx_deal_render: + mov eax,frame + xor edx,edx + mov ecx,10 + div ecx + add eax,'0' + add edx,'0' + mov scratchFXFrameTens3D,al + mov scratchFXFrameOnes3D,dl + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchFXDeal3D + mov eax,cosmeticRng + cmp rngSeed3D,eax + jne scratch_fx_fail + cmp bankroll,9950 + jne scratch_fx_fail + inc frame + cmp frame,31 + jb scratch_fx_deal_frame + cmp scrDealMs3D,0 + jne scratch_fx_fail + inc rate + cmp rate,3 + jb scratch_fx_rate + add kind,4 + cmp kind,4 + jbe scratch_fx_kind + ; A fixed winning ticket exercises payout without touching the save file. + xor esi,esi +scratch_fx_winning_fixture: + lea eax,[esi+1] + mov scrGrid3D[esi*4],eax + mov scrCellPrizes3D[esi*4],50 + inc esi + cmp esi,scrCellCount3D + jb scratch_fx_winning_fixture + mov esi,scrPortraitWins3D + mov scrGrid3D[esi*4],1 + mov scrCellPrizes3D[esi*4],250 + call ScratchPortraitPayout3D + mov scrGross3D,eax + call ScratchTestUncover3D + cmp scrPayoutMs3D,1800 + jne scratch_fx_fail + cmp lastSoundPath3D,OFFSET scrPayoutSound3D + jne scratch_fx_fail + cmp bankroll,10200 + jne scratch_fx_fail + mov frame,0 +scratch_fx_payout_frame: + mov eax,frame + xor edx,edx + mov ecx,10 + div ecx + add eax,'0' + add edx,'0' + mov scratchFXPayTens3D,al + mov scratchFXPayOnes3D,dl + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchFXPay3D + call FinishScratch3D + cmp bankroll,10200 + jne scratch_fx_fail + mov deltaTime3D,75 + call ScratchTickVisuals3D + inc frame + cmp frame,25 + jb scratch_fx_payout_frame + mov eax,cosmeticRng + cmp rngSeed3D,eax + jne scratch_fx_fail + ; Dust uses a private RNG and only starts audio when fresh coating is removed. + mov scrState3D,0 + mov scrType3D,0 + call ScratchConfigure3D + mov scrState3D,1 + mov scrHeld3D,0 + mov scrPointerX3D,200 + mov scrPointerY3D,195 + mov scrNewPixels3D,1 + mov deltaTime3D,0 + call HandleScratch3D + cmp lastSoundPath3D,OFFSET scrScrapeSound3D + jne scratch_fx_fail + cmp scrDustIndex3D,3 + jne scratch_fx_fail + mov lastSoundPath3D,0 + mov scrNewPixels3D,1 + call HandleScratch3D + cmp lastSoundPath3D,0 + jne scratch_fx_fail + mov deltaTime3D,16 + mov edi,140 +scratch_fx_dust_tick: + call ScratchTickVisuals3D + dec edi + jnz scratch_fx_dust_tick + xor esi,esi +scratch_fx_dust_expired: + cmp scrDustLife3D[esi*4],0 + jne scratch_fx_fail + inc esi + cmp esi,SCR_DUST_COUNT + jb scratch_fx_dust_expired + mov eax,cosmeticRng + cmp rngSeed3D,eax + jne scratch_fx_fail + mov scrState3D,0 + mov eax,1 + ret +scratch_fx_fail: + mov eax,148 + ret +RunScratchEffectsTest3D ENDP diff --git a/tools/tests/scratch_native_probe.asm b/tools/tests/scratch_native_probe.asm new file mode 100644 index 0000000..eed9a4d --- /dev/null +++ b/tools/tests/scratch_native_probe.asm @@ -0,0 +1,219 @@ +; Native render-only probe. Links the action-tested game object, not a renderer +; reimplementation. It never loads or writes the player's profile. +.386 +.model flat +option casemap:none +ExitProcess PROTO STDCALL :DWORD +wsprintfA PROTO C :DWORD,:DWORD,:VARARG +InitPaths3D PROTO STDCALL +BeginScratch3D PROTO STDCALL +BuyScratch3D PROTO STDCALL +HandleScratch3D PROTO STDCALL +RenderScratch3D PROTO STDCALL +LoadAnimPack3D PROTO STDCALL :DWORD,:DWORD +FreeAnimPack3D PROTO STDCALL :DWORD +WriteActionSnapshot3D PROTO STDCALL :DWORD +LoadTextures3D PROTO STDCALL +LoadArrivalAssets3D PROTO STDCALL +InitializeCasinoSprites3D PROTO STDCALL +InitHall3D PROTO STDCALL +LoadHallAssets3D PROTO STDCALL +UpdateCamera3D PROTO STDCALL +RenderCeilingAndFloor PROTO STDCALL +RenderWalls PROTO STDCALL +PrepareCityDepth3D PROTO STDCALL +RenderSprites PROTO STDCALL +RenderHall3D PROTO STDCALL +CheckHallInteractions3D PROTO STDCALL +TriggerHallInteraction3D PROTO STDCALL +LeaveScratch3D PROTO STDCALL +ScratchCanWalk3D PROTO STDCALL :DWORD,:DWORD +ScratchSelectType3D PROTO STDCALL :DWORD +ScratchToggleZoom3D PROTO STDCALL +SettleScratchOnQuit3D PROTO STDCALL +EXTERN bankroll:DWORD,rngSeed3D:DWORD,engineMode:DWORD +EXTERN scrState3D:DWORD,scrPriceIndex3D:DWORD,scrHeld3D:DWORD +EXTERN scrPointerValid3D:DWORD,scrPointerX3D:DWORD,scrPointerY3D:DWORD +EXTERN animationClock3D:DWORD,deltaTime3D:DWORD +EXTERN scrTableArt3D:DWORD +EXTERN playerX:DWORD,playerY:DWORD,playerAngle:DWORD +EXTERN interactTargetId:DWORD +EXTERN scrType3D:DWORD,scrZoom3D:DWORD,scrCounts3D:DWORD,scrCatalog3D:DWORD +EXTERN scrDealMs3D:DWORD +.data +scratchProbeCounter db "build\actions\scratch-counter.bmp",0 +scratchProbePointer db "build\actions\scratch-pointer.bmp",0 +scratchProbeFormat db "build\actions\scratch-stroke-%02u.bmp",0 +scratchProbePath db 128 dup(0) +scratchProbeTable db "assets\animations\scratch\table-v1.bin",0 +scratchPortraitProbeFormat db "build\actions\scratch-portrait-%u-%02u.bmp",0 +scratchPortraitProbeKind dd 4 +scratchPortraitProbeCount dd 0 +.code +ScratchPortraitProbe PROC +scratch_portrait_probe_kind: + call SettleScratchOnQuit3D + mov scrState3D,0 + invoke ScratchSelectType3D,scratchPortraitProbeKind + mov bankroll,10000 + call BuyScratch3D + mov scrDealMs3D,0 + mov scrPointerValid3D,1 + xor esi,esi +scratch_portrait_probe_frame: + mov eax,esi + xor edx,edx + mov ecx,13 + div ecx + imul eax,12 + add eax,188 + mov scrPointerY3D,eax + test esi,1 + jz @F + ; Slightly offset successive strokes, not a pasted reveal animation. + inc scrPointerY3D +@@: + imul edx,20 + add edx,220 + mov scrPointerX3D,edx + mov scrHeld3D,1 + imul eax,esi,33 + mov animationClock3D,eax + call HandleScratch3D + call RenderScratch3D + invoke wsprintfA,OFFSET scratchProbePath,OFFSET scratchPortraitProbeFormat,scratchPortraitProbeKind,esi + invoke WriteActionSnapshot3D,OFFSET scratchProbePath + cmp eax,1 + jne scratch_portrait_probe_fail + inc esi + cmp esi,26 + jb scratch_portrait_probe_frame + xor esi,esi + xor ebx,ebx +scratch_portrait_probe_sum: + add ebx,scrCounts3D[esi*4] + inc esi + cmp esi,24 + jb scratch_portrait_probe_sum + test ebx,ebx + jz scratch_portrait_probe_fail + mov scratchPortraitProbeCount,ebx + call ScratchToggleZoom3D + call RenderScratch3D + call ScratchToggleZoom3D + xor esi,esi + xor ebx,ebx +scratch_portrait_probe_compare: + add ebx,scrCounts3D[esi*4] + inc esi + cmp esi,24 + jb scratch_portrait_probe_compare + cmp ebx,scratchPortraitProbeCount + jne scratch_portrait_probe_fail + inc scratchPortraitProbeKind + cmp scratchPortraitProbeKind,7 + jb scratch_portrait_probe_kind + call SettleScratchOnQuit3D + mov scrState3D,0 + mov scrCatalog3D,0 + mov bankroll,9950 + mov eax,1 + ret +scratch_portrait_probe_fail: + xor eax,eax + ret +ScratchPortraitProbe ENDP + +PUBLIC _ScratchProbeStart +_ScratchProbeStart: + call InitPaths3D + mov bankroll,10000 + mov rngSeed3D,47 + mov scrState3D,0 + mov scrPriceIndex3D,0 + call BeginScratch3D + invoke FreeAnimPack3D,OFFSET scrTableArt3D + invoke LoadAnimPack3D,OFFSET scrTableArt3D,OFFSET scratchProbeTable + call BuyScratch3D + mov scrDealMs3D,0 + mov scrHeld3D,1 + mov scrPointerValid3D,1 + mov deltaTime3D,33 + xor esi,esi +scratch_probe_frame: + mov eax,esi + xor edx,edx + mov ecx,12 + div ecx + mov ebx,eax + imul eax,15 + add eax,183 + mov scrPointerY3D,eax + imul edx,28 + test ebx,1 + jz @F + mov eax,308 + sub eax,edx + mov edx,eax +@@: + add edx,164 + mov scrPointerX3D,edx + imul eax,esi,33 + mov animationClock3D,eax + call HandleScratch3D + call RenderScratch3D + invoke wsprintfA,OFFSET scratchProbePath,OFFSET scratchProbeFormat,esi + invoke WriteActionSnapshot3D,OFFSET scratchProbePath + cmp eax,1 + jne scratch_probe_fail + inc esi + cmp esi,36 + jb scratch_probe_frame + invoke WriteActionSnapshot3D,OFFSET scratchProbePointer + call ScratchPortraitProbe + cmp eax,1 + jne scratch_probe_fail + call LoadTextures3D + call LoadArrivalAssets3D + call InitializeCasinoSprites3D + call InitHall3D + call LoadHallAssets3D + mov engineMode,0 + mov playerX,29*65536 + mov playerY,5*65536+32768 + mov playerAngle,128 + call UpdateCamera3D + call RenderCeilingAndFloor + call RenderWalls + call PrepareCityDepth3D + call RenderSprites + call RenderHall3D + invoke WriteActionSnapshot3D,OFFSET scratchProbeCounter + cmp eax,1 + jne scratch_probe_fail + invoke ScratchCanWalk3D,29*65536,7*65536+32768 + test eax,eax + jnz scratch_probe_fail + invoke ScratchCanWalk3D,20*65536,27*65536 + cmp eax,1 + jne scratch_probe_fail + mov playerY,6*65536 + call UpdateCamera3D + mov interactTargetId,0 + call CheckHallInteractions3D + cmp interactTargetId,25 + jne scratch_probe_fail + call TriggerHallInteraction3D + cmp eax,1 + jne scratch_probe_fail + cmp engineMode,22 + jne scratch_probe_fail + call LeaveScratch3D + cmp engineMode,0 + jne scratch_probe_fail + cmp bankroll,9950 + jne scratch_probe_fail + invoke ExitProcess,0 +scratch_probe_fail: + invoke ExitProcess,145 +end _ScratchProbeStart diff --git a/tools/tests/scratch_portrait_test.inc b/tools/tests/scratch_portrait_test.inc new file mode 100644 index 0000000..20fe877 --- /dev/null +++ b/tools/tests/scratch_portrait_test.inc @@ -0,0 +1,293 @@ +.data +scratchPortraitCapture3D db "build\actions\scratch-kind-4-covered.bmp",0 +scratchPortraitFull3D db "build\actions\scratch-kind-4-full.bmp",0 +scratchPortraitResult3D db "build\actions\scratch-kind-4-result.bmp",0 +scratchCatalogCapture3D db "build\actions\scratch-catalog.bmp",0 +scratchNetCapture3D db "build\actions\scratch-net-0.bmp",0 +.code +RunScratchPortraitTest3D PROC uses ebx esi edi + local kind:DWORD,round:DWORD,total:DWORD,allPrizes:DWORD,expected:DWORD,savedRng:DWORD + mov kind,4 +portrait_test_kind: + mov round,0 +portrait_test_round: + mov scrState3D,0 + mov eax,kind + mov scrType3D,eax + mov eax,round + xor edx,edx + mov ecx,3 + div ecx + mov scrPriceIndex3D,edx + mov eax,round + add eax,7919 + mov rngSeed3D,eax + mov bankroll,100000 + call BuyScratch3D + cmp scrState3D,1 + jne portrait_test_fail + mov total,0 + mov allPrizes,0 + xor esi,esi +portrait_test_unique: + mov eax,scrGrid3D[esi*4] + dec eax + cmp eax,60 + jae portrait_test_fail + xor edi,edi +portrait_test_previous: + cmp edi,esi + jae portrait_test_unique_next + mov eax,scrGrid3D[esi*4] + cmp eax,scrGrid3D[edi*4] + je portrait_test_fail + inc edi + jmp portrait_test_previous +portrait_test_unique_next: + inc esi + cmp esi,scrPortraitWins3D + jb portrait_test_unique +portrait_test_award: + mov eax,scrGrid3D[esi*4] + cmp eax,61 + je portrait_test_double + cmp eax,62 + je portrait_test_special + dec eax + cmp eax,60 + jae portrait_test_fail + xor edi,edi +portrait_test_match: + mov eax,scrGrid3D[esi*4] + cmp eax,scrGrid3D[edi*4] + je portrait_test_normal + inc edi + cmp edi,scrPortraitWins3D + jb portrait_test_match + jmp portrait_test_award_next +portrait_test_double: + imul eax,scrCellPrizes3D[esi*4],2 + jmp portrait_test_add +portrait_test_special: + cmp kind,4 + jne portrait_test_all + imul eax,scrCellPrizes3D[esi*4],10 + jmp portrait_test_add +portrait_test_all: + mov allPrizes,1 + jmp portrait_test_award_next +portrait_test_normal: + mov eax,scrCellPrizes3D[esi*4] +portrait_test_add: + add total,eax +portrait_test_award_next: + inc esi + cmp esi,scrCellCount3D + jb portrait_test_award + cmp allPrizes,0 + je portrait_test_total + mov total,0 + mov esi,scrPortraitWins3D +portrait_test_sum_all: + mov eax,scrCellPrizes3D[esi*4] + add total,eax + inc esi + cmp esi,scrCellCount3D + jb portrait_test_sum_all +portrait_test_total: + mov eax,total + cmp scrGross3D,eax + jne portrait_test_fail + add eax,bankroll + mov expected,eax + mov eax,rngSeed3D + mov savedRng,eax + call SettleScratchOnQuit3D + call SettleScratchOnQuit3D + mov eax,expected + cmp bankroll,eax + jne portrait_test_fail + mov eax,savedRng + cmp rngSeed3D,eax + jne portrait_test_fail + inc round + cmp round,500 + jb portrait_test_round + ; Stable visual fixture, independent of the generated outcome. + mov scrState3D,0 + mov scrPriceIndex3D,0 + mov bankroll,10000 + call BuyScratch3D + mov scrDealMs3D,0 + xor esi,esi +portrait_test_fixture: + lea eax,[esi+1] + mov scrGrid3D[esi*4],eax + mov scrCellPrizes3D[esi*4],50 + inc esi + cmp esi,scrCellCount3D + jb portrait_test_fixture + mov esi,scrPortraitWins3D + mov eax,scrGrid3D + mov scrGrid3D[esi*4],eax + mov scrCellPrizes3D[esi*4],250 + call ScratchPortraitPayout3D + cmp eax,250 + jne portrait_test_fail + mov scrGross3D,eax + mov eax,kind + add eax,'0' + mov scratchPortraitCapture3D[27],al + mov scratchPortraitFull3D[27],al + mov scratchPortraitResult3D[27],al + mov scrZoom3D,0 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchPortraitFull3D + mov scrZoom3D,1 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchPortraitCapture3D + ; The sea ticket is uncovered at the smaller full-ticket scale, too. + cmp kind,6 + jne @F + mov scrZoom3D,0 +@@: + call ScratchTestUncover3D + mov eax,scrCellCount3D + cmp scrOpened3D,eax + jne portrait_test_fail + cmp scrState3D,2 + jne portrait_test_fail + cmp bankroll,10200 + jne portrait_test_fail + mov scrZoom3D,1 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchPortraitResult3D + call ScratchToggleZoom3D + ; The state and all source-space masks survive view changes. + mov eax,scrCellCount3D + cmp scrOpened3D,eax + jne portrait_test_fail + cmp scrState3D,2 + jne portrait_test_fail + ; Bonus fixtures: x2, neon x10 and all-prizes (without double payment). + xor esi,esi +portrait_test_bonus_grid: + lea eax,[esi+1] + mov scrGrid3D[esi*4],eax + mov scrCellPrizes3D[esi*4],50 + inc esi + cmp esi,scrCellCount3D + jb portrait_test_bonus_grid + mov esi,scrPortraitWins3D + mov scrGrid3D[esi*4],61 + call ScratchPortraitPayout3D + cmp eax,100 + jne portrait_test_fail + mov scrGrid3D[esi*4],62 + call ScratchPortraitPayout3D + mov ecx,500 + cmp kind,4 + je @F + mov ecx,scrCellCount3D + sub ecx,scrPortraitWins3D + imul ecx,50 +@@: + cmp eax,ecx + jne portrait_test_fail + inc kind + cmp kind,7 + jb portrait_test_kind + mov eax,1 + ret +portrait_test_fail: + mov eax,146 + ret +RunScratchPortraitTest3D ENDP + +RunScratchCatalogTest3D PROC uses ebx esi edi + local catalogRng:DWORD + mov viewWidth3D,640 + mov viewHeight3D,480 + mov viewLeft3D,0 + mov viewTop3D,0 + mov scrState3D,0 + mov scrType3D,0 + call ScratchConfigure3D + ; Moving after a fast click cannot redirect the pending button action. + invoke ScratchMouseEvent3D,0,201h,1,003C01F4h + invoke ScratchMouseEvent3D,0,200h,0,00C80014h + invoke ScratchMouseEvent3D,0,202h,0,00C80014h + call HandleScratch3D + cmp scrCatalog3D,1 + jne catalog_test_fail + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchCatalogCapture3D + mov scrClick3D,1 + mov scrClickX3D,220 + mov scrClickY3D,358 + call HandleScratch3D + cmp scrType3D,6 + jne catalog_test_fail + cmp scrCatalog3D,0 + jne catalog_test_fail + mov scrClick3D,1 + mov scrClickX3D,160 + mov scrClickY3D,450 + call HandleScratch3D + cmp scrPriceIndex3D,1 + jne catalog_test_fail + cmp scrType3D,6 + jne catalog_test_fail + mov scrPriceIndex3D,0 + mov bankroll,10000 + call BuyScratch3D + mov eax,rngSeed3D + mov catalogRng,eax + mov scrCatalog3D,1 + mov scrClick3D,1 + mov scrClickX3D,220 + mov scrClickY3D,120 + call HandleScratch3D + cmp scrType3D,6 + jne catalog_test_fail + cmp scrCatalogLocked3D,1 + jne catalog_test_fail + cmp bankroll,9950 + jne catalog_test_fail + mov eax,catalogRng + cmp rngSeed3D,eax + jne catalog_test_fail + call LeaveScratch3D + cmp scrCatalog3D,0 + jne catalog_test_fail + cmp engineMode,MODE_SCRATCH + jne catalog_test_fail + call LeaveScratch3D + cmp engineMode,MODE_WALK + jne catalog_test_fail + call SettleScratchOnQuit3D + call BeginScratch3D + mov scrType3D,0 + mov scrState3D,2 + mov scrStake3D,50 + mov scrPriceIndex3D,1 + mov scrPrizeSymbol3D,-1 + xor esi,esi +catalog_test_net: + mov eax,esi + imul eax,50 + mov scrGross3D,eax + mov eax,esi + add eax,'0' + mov scratchNetCapture3D[26],al + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchNetCapture3D + inc esi + cmp esi,3 + jb catalog_test_net + mov eax,1 + ret +catalog_test_fail: + mov eax,147 + ret +RunScratchCatalogTest3D ENDP diff --git a/tools/tests/scratch_syntax.asm b/tools/tests/scratch_syntax.asm new file mode 100644 index 0000000..647df41 --- /dev/null +++ b/tools/tests/scratch_syntax.asm @@ -0,0 +1,102 @@ +; Fast isolated assembly pass; the full action suite tests the real engine. +.386 +.model flat +IFDEF SCRATCH_MODULE_TEST +EXTERN engineMode:DWORD,paused3D:DWORD,bankroll:DWORD,deltaTime3D:DWORD,animationClock3D:DWORD +EXTERN viewWidth3D:DWORD,viewHeight3D:DWORD,viewLeft3D:DWORD,viewTop3D:DWORD +EXTERN clpSymbolsPath3D:BYTE,coinExitText3D:BYTE,coinBankText3D:BYTE,hudNumBuffer:BYTE +EXTERN screenBuffer:DWORD,rngSeed3D:DWORD,h3DWnd:DWORD +EXTERN lastSoundPath3D:DWORD +ENDIF +option language:stdcall +option casemap:none +FP_1 equ 65536 +FP_HALF equ 32768 +SCREEN_SIZE equ 640*480 +MODE_WALK equ 0 +AnimPack3D STRUCT + magic dd ? + imageWidth dd ? + height dd ? + frames dd ? + bytes dd ? + pixels dd ? +AnimPack3D ENDS +CityProp3D STRUCT + posX dd ? + posY dd ? + worldW dd ? + worldH dd ? + frame dd ? + asset dd ? + groundZ dd 0 +CityProp3D ENDS +LoadAnimPack3D PROTO :DWORD,:DWORD +FreeAnimPack3D PROTO :DWORD +ValidateCityPack3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +DrawAnimSprite3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +DrawBox PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD +DrawBoxBorder PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +DrawString8x8 PROTO :DWORD,:DWORD,:DWORD,:DWORD +DrawBuckshotText3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +FormatNumber PROTO :DWORD,:DWORD +RandomRange3D PROTO :DWORD +Pressed3D PROTO :DWORD +ClearKeys3D PROTO +SaveProgress3D PROTO +IFNDEF SCRATCH_MODULE_TEST +.data +engineMode dd 0 +paused3D dd 0 +bankroll dd 10000 +deltaTime3D dd 16 +animationClock3D dd 0 +viewWidth3D dd 640 +viewHeight3D dd 480 +viewLeft3D dd 0 +viewTop3D dd 0 +coinTablePath3D db 0 +clpSymbolsPath3D db 0 +coinExitText3D db 0 +coinBankText3D db 0 +hudNumBuffer db 64 dup(0) +.data? +screenBuffer dd SCREEN_SIZE dup(?) +ENDIF +.code +include src_graphics/scratch_data.inc +include src_graphics/scratch.inc +IFDEF SCRATCH_TEST_SYNTAX +WriteActionSnapshot3D PROTO :DWORD +WindowEvents3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +IFNDEF SCRATCH_MODULE_TEST +.data +rngSeed3D dd 47 +h3DWnd dd 0 +ENDIF +.code +include tools/tests/scratch_test.inc +ENDIF +IFDEF SCRATCH_MODULE_TEST +ExitProcess PROTO :DWORD +CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +ReadFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD +GetFileSize PROTO :DWORD,:DWORD +GetProcessHeap PROTO +HeapAlloc PROTO :DWORD,:DWORD,:DWORD +HeapFree PROTO :DWORD,:DWORD,:DWORD +CloseHandle PROTO :DWORD +InitPaths3D PROTO +.code +include build/scratch/module-loader.inc +ScratchModuleStart PROC + call InitPaths3D + call RunScratchTest3D + cmp eax,1 + jne @F + xor eax,eax +@@: + invoke ExitProcess,eax +ScratchModuleStart ENDP +ENDIF +end diff --git a/tools/tests/scratch_test.inc b/tools/tests/scratch_test.inc new file mode 100644 index 0000000..ce1a555 --- /dev/null +++ b/tools/tests/scratch_test.inc @@ -0,0 +1,481 @@ +GetCapture PROTO +.data +scratchTestReady3D db "build\actions\scratch-ready.bmp",0 +scratchTestCovered3D db "build\actions\scratch-covered.bmp",0 +scratchTestPartial3D db "build\actions\scratch-partial.bmp",0 +scratchTestResult3D db "build\actions\scratch-result.bmp",0 +scratchTestMissing3D db "build\actions\scratch-missing.bmp",0 +scratchTestStage3D dd 140 +scratchTestCounts3D dd 7 dup(0) +scratchTestGrid3D dd 9 dup(0) +scratchTypeCapture3D db "build\actions\scratch-kind-0-covered.bmp",0 +scratchTypeResult3D db "build\actions\scratch-kind-0-result.bmp",0 +.code + +RunScratchPointerTest3D PROC uses ebx esi edi + local oldMode:DWORD,oldW:DWORD,oldH:DWORD,oldLeft:DWORD,oldTop:DWORD,ok:DWORD + mov eax,engineMode + mov oldMode,eax + mov eax,viewWidth3D + mov oldW,eax + mov eax,viewHeight3D + mov oldH,eax + mov eax,viewLeft3D + mov oldLeft,eax + mov eax,viewTop3D + mov oldTop,eax + mov ok,0 + mov engineMode,MODE_SCRATCH + mov scrState3D,0 + mov viewWidth3D,1280 + mov viewHeight3D,960 + mov viewLeft3D,20 + mov viewTop3D,40 + ; (200,195) native -> (420,430) client, including letterbox offsets. + invoke WindowEvents3D,h3DWnd,201h,1,01AE01A4h + cmp scrPointerX3D,200 + jne scratch_pointer_restore + cmp scrPointerY3D,195 + jne scratch_pointer_restore + cmp scrHeld3D,1 + jne scratch_pointer_restore + invoke GetCapture + cmp eax,h3DWnd + jne scratch_pointer_restore + ; Release outside the viewport still releases capture; quick click survives. + invoke WindowEvents3D,h3DWnd,202h,0,0FFFFFFFFh + cmp scrHeld3D,0 + jne scratch_pointer_restore + cmp scrClick3D,1 + jne scratch_pointer_restore + invoke GetCapture + test eax,eax + jnz scratch_pointer_restore + invoke WindowEvents3D,h3DWnd,201h,1,01AE01A4h + invoke WindowEvents3D,h3DWnd,8,0,0 + cmp scrHeld3D,0 + jne scratch_pointer_restore + cmp scrLastValid3D,0 + jne scratch_pointer_restore + cmp scrPointerValid3D,0 + jne scratch_pointer_restore + invoke GetCapture + test eax,eax + jnz scratch_pointer_restore + invoke WindowEvents3D,h3DWnd,201h,1,0FFFFFFFFh + cmp scrPointerValid3D,0 + jne scratch_pointer_restore + cmp scrHeld3D,0 + jne scratch_pointer_restore + mov ok,1 +scratch_pointer_restore: + call ClearKeys3D + mov eax,oldMode + mov engineMode,eax + mov eax,oldW + mov viewWidth3D,eax + mov eax,oldH + mov viewHeight3D,eax + mov eax,oldLeft + mov viewLeft3D,eax + mov eax,oldTop + mov viewTop3D,eax + mov eax,ok + ret +RunScratchPointerTest3D ENDP + +ScratchTestUncover3D PROC uses ebx esi edi + local rowY:DWORD,left:DWORD,right:DWORD + mov scrDealMs3D,0 + xor esi,esi +scratch_uncover_cell: + invoke ScratchCellOrigin3D,esi + mov left,eax + add eax,scrCellW3D + dec eax + mov right,eax + mov rowY,edx + mov edi,edx + add edi,scrCellH3D +scratch_uncover_row: + invoke ScratchStroke3D,left,rowY,right,rowY + add rowY,6 + cmp rowY,edi + jb scratch_uncover_row + inc esi + cmp esi,scrCellCount3D + jb scratch_uncover_cell + ret +ScratchTestUncover3D ENDP + +RunScratchVariantsTest3D PROC uses ebx esi edi + local kind:DWORD,round:DWORD,expected:DWORD,hits:DWORD,total:DWORD,ticketSeed:DWORD + mov kind,0 +scratch_variant_test_kind: + mov scrState3D,0 + mov eax,kind + mov scrType3D,eax + mov round,0 +scratch_variant_test_round: + mov bankroll,10000 + mov scrState3D,0 + mov scrPriceIndex3D,0 + mov eax,round + add eax,47 + mov rngSeed3D,eax + call BuyScratch3D + mov scrDealMs3D,0 + cmp scrState3D,1 + jne scratch_variants_fail + cmp scrNewPixels3D,0 + jne scratch_variants_fail + xor esi,esi +scratch_test_clean_dust: + cmp scrDustLife3D[esi*4],0 + jne scratch_variants_fail + inc esi + cmp esi,32 + jb scratch_test_clean_dust + mov eax,scrGross3D + add eax,9950 + mov expected,eax + mov eax,rngSeed3D + mov ticketSeed,eax + mov eax,kind + inc eax + invoke ScratchSelectType3D,eax + mov eax,kind + cmp scrType3D,eax + jne scratch_variants_fail + mov hits,0 + mov total,0 + cmp kind,1 + je scratch_test_number_rules + cmp kind,2 + jne scratch_variant_check_payment + xor esi,esi +scratch_test_row_rules: + mov eax,scrGrid3D[esi*4] + cmp eax,scrGrid3D[esi*4+4] + jne scratch_test_row_next + cmp eax,scrGrid3D[esi*4+8] + jne scratch_test_row_next + inc hits + mov eax,scrMultipliers3D[eax*4] + imul eax,scrStake3D + add total,eax +scratch_test_row_next: + add esi,3 + cmp esi,9 + jb scratch_test_row_rules + jmp scratch_test_rule_total +scratch_test_number_rules: + xor esi,esi +scratch_test_number_bounds: + mov eax,scrGrid3D[esi*4] + dec eax + cmp eax,30 + jae scratch_variants_fail + inc esi + cmp esi,9 + jb scratch_test_number_bounds + mov esi,3 +scratch_test_number_cell: + xor edi,edi +scratch_test_number_match: + mov eax,scrGrid3D[edi*4] + cmp eax,scrGrid3D[esi*4] + jne @F + inc hits + mov eax,scrCellPrizes3D[esi*4] + add total,eax +@@: + inc edi + cmp edi,3 + jb scratch_test_number_match + inc esi + cmp esi,9 + jb scratch_test_number_cell +scratch_test_rule_total: + mov eax,total + cmp eax,scrGross3D + jne scratch_variants_fail + cmp hits,1 + ja scratch_variants_fail +scratch_variant_check_payment: + cmp round,0 + jne scratch_variant_quit_test + mov eax,kind + add al,'0' + mov BYTE PTR scratchTypeCapture3D[27],al + mov BYTE PTR scratchTypeResult3D[27],al + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTypeCapture3D + cmp kind,2 + jne @F + ; The corner of a circle is paper, not erasable silver. + invoke ScratchCellOrigin3D,0 + invoke ScratchErasePixel3D,eax,edx + cmp scrCounts3D,0 + jne scratch_variants_fail +@@: + call ScratchTestUncover3D + cmp scrState3D,2 + jne scratch_variants_fail + mov eax,scrCellCount3D + cmp scrOpened3D,eax + jne scratch_variants_fail + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTypeResult3D +scratch_variant_quit_test: + call SettleScratchOnQuit3D + call SettleScratchOnQuit3D + mov eax,expected + cmp bankroll,eax + jne scratch_variants_fail + mov eax,ticketSeed + cmp rngSeed3D,eax + jne scratch_variants_fail + inc round + cmp round,200 + jb scratch_variant_test_round + inc kind + cmp kind,4 + jb scratch_variant_test_kind + mov scrState3D,0 + invoke ScratchSelectType3D,0 + cmp scrType3D,0 + jne scratch_variants_fail + mov eax,1 + ret +scratch_variants_fail: + mov eax,145 + ret +RunScratchVariantsTest3D ENDP + +include tools/tests/scratch_portrait_test.inc +include tools/tests/scratch_effects_test.inc + +RunScratchTest3D PROC uses ebx esi edi + local oldBank:DWORD,oldRng:DWORD,paintSeed:DWORD,cleared:DWORD,expected:DWORD,round:DWORD + mov eax,bankroll + mov oldBank,eax + mov eax,rngSeed3D + mov oldRng,eax + mov scratchTestStage3D,140 + mov scrState3D,0 + mov scrType3D,0 + call BeginScratch3D + cmp scrReady3D,1 + jne scratch_test_fail + cmp scrCounterArt3D.pixels,0 + je scratch_test_fail + mov scrState3D,0 + mov scrPriceIndex3D,0 + mov bankroll,10000 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTestReady3D + mov rngSeed3D,47 + call BuyScratch3D + mov scrDealMs3D,0 + cmp bankroll,9950 + jne scratch_test_fail + cmp scrGross3D,250 + jne scratch_test_fail + cmp scrPrizeSymbol3D,4 + jne scratch_test_fail + mov eax,rngSeed3D + mov paintSeed,eax + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTestCovered3D + mov scratchTestStage3D,141 + call BuyScratch3D + cmp bankroll,9950 + jne scratch_test_fail + mov eax,paintSeed + cmp rngSeed3D,eax + jne scratch_test_fail + ; Negative coordinates and panel gutters must not change any coating. + invoke ScratchStamp3D,-100,-100 + invoke ScratchStamp3D,258,220 + cmp scrCounts3D,0 + jne scratch_test_fail + mov scrHeld3D,0 + mov scrPointerValid3D,1 + mov scrPointerX3D,200 + mov scrPointerY3D,195 + call HandleScratch3D + cmp scrCounts3D,0 + jne scratch_test_fail + ; Fast drag, a release, and a second disconnected drag. + mov scrHeld3D,1 + call HandleScratch3D + mov scrPointerX3D,490 + call HandleScratch3D + mov eax,scrCounts3D + mov cleared,eax + cmp eax,400 + jb scratch_test_fail + cmp scrCounts3D[4],400 + jb scratch_test_fail + cmp scrCounts3D[8],400 + jb scratch_test_fail + call ScratchCancelStroke3D + mov scrPointerX3D,200 + mov scrPointerY3D,295 + mov scrHeld3D,1 + call HandleScratch3D + cmp scrCounts3D[12],0 + jne scratch_test_fail + mov scrPointerValid3D,0 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTestPartial3D + mov scratchTestStage3D,142 + call LeaveScratch3D + cmp engineMode,MODE_WALK + jne scratch_test_fail + call BeginScratch3D + mov eax,cleared + cmp scrCounts3D,eax + jne scratch_test_fail + mov eax,paintSeed + cmp rngSeed3D,eax + jne scratch_test_fail + cmp bankroll,9950 + jne scratch_test_fail + call ScratchTestUncover3D + cmp scrOpened3D,9 + jne scratch_test_fail + cmp scrState3D,2 + jne scratch_test_fail + cmp bankroll,10200 + jne scratch_test_fail + call FinishScratch3D + call SettleScratchOnQuit3D + cmp bankroll,10200 + jne scratch_test_fail + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTestResult3D + mov eax,paintSeed + cmp rngSeed3D,eax + jne scratch_test_fail + mov scratchTestStage3D,143 + mov round,0 +scratch_test_round: + mov scrState3D,0 + mov bankroll,10000 + mov eax,round + xor edx,edx + mov ecx,3 + div ecx + mov scrPriceIndex3D,edx + call BuyScratch3D + cmp scrState3D,1 + jne scratch_test_fail + mov eax,10000 + sub eax,scrStake3D + mov expected,eax + cmp bankroll,eax + jne scratch_test_fail + lea edi,scratchTestCounts3D + xor eax,eax + mov ecx,7 + rep stosd + xor esi,esi +scratch_test_grid: + mov eax,scrGrid3D[esi*4] + cmp eax,6 + ja scratch_test_fail + inc scratchTestCounts3D[eax*4] + inc esi + cmp esi,9 + jb scratch_test_grid + xor esi,esi +scratch_test_triples: + cmp esi,scrPrizeSymbol3D + jne scratch_test_other_count + cmp scratchTestCounts3D[esi*4],3 + jne scratch_test_fail + mov eax,scrMultipliers3D[esi*4] + imul eax,scrStake3D + cmp scrGross3D,eax + jne scratch_test_fail + jmp scratch_test_next_count +scratch_test_other_count: + cmp scratchTestCounts3D[esi*4],2 + ja scratch_test_fail +scratch_test_next_count: + inc esi + cmp esi,7 + jb scratch_test_triples + mov eax,scrGross3D + add expected,eax + mov eax,rngSeed3D + mov paintSeed,eax + call BuyScratch3D + mov eax,paintSeed + cmp rngSeed3D,eax + jne scratch_test_fail + ; Normal exit pays the pending amount once without further RNG. + call SettleScratchOnQuit3D + call SettleScratchOnQuit3D + mov eax,expected + cmp bankroll,eax + jne scratch_test_fail + mov eax,paintSeed + cmp rngSeed3D,eax + jne scratch_test_fail + inc round + cmp round,500 + jb scratch_test_round + mov scratchTestStage3D,144 + mov bankroll,0 + mov scrState3D,0 + call BuyScratch3D + cmp scrState3D,0 + jne scratch_test_fail + cmp scrError3D,1 + jne scratch_test_fail + mov scrState3D,1 + mov scrOpened3D,9 + mov scrGross3D,12500 + mov bankroll,999999990 + call FinishScratch3D + cmp bankroll,1000000000 + jne scratch_test_fail + call RunScratchVariantsTest3D + cmp eax,1 + jne scratch_test_extra_fail + call RunScratchPortraitTest3D + cmp eax,1 + jne scratch_test_extra_fail + call RunScratchCatalogTest3D + cmp eax,1 + jne scratch_test_extra_fail + call RunScratchEffectsTest3D + cmp eax,1 + je @F +scratch_test_extra_fail: + mov scratchTestStage3D,eax + jmp scratch_test_fail +@@: + mov scrReady3D,0 + call RenderScratch3D + invoke WriteActionSnapshot3D,OFFSET scratchTestMissing3D + mov scrReady3D,1 + mov scrState3D,0 + mov scrOpened3D,0 + mov scrError3D,0 + mov scrGross3D,0 + mov scrPriceIndex3D,0 + mov eax,oldBank + mov bankroll,eax + mov eax,oldRng + mov rngSeed3D,eax + call ClearKeys3D + mov eax,1 + ret +scratch_test_fail: + mov eax,scratchTestStage3D + ret +RunScratchTest3D ENDP diff --git a/tools/tests/street_asset_probe.asm b/tools/tests/street_asset_probe.asm index d998e20..d5aec4c 100644 --- a/tools/tests/street_asset_probe.asm +++ b/tools/tests/street_asset_probe.asm @@ -16,6 +16,10 @@ LoadNativeWheelArt3D PROTO FreeNativeWheelArt3D PROTO LoadNativeCoinArt3D PROTO FreeNativeCoinArt3D PROTO +LoadNativeDiceArt3D PROTO +FreeNativeDiceArt3D PROTO +LoadScratchArt3D PROTO +FreeScratchArt3D PROTO .data streetPack dd 6 dup(0) streetPath db "assets\animations\hall\street-life-v4.bin",0 @@ -28,6 +32,15 @@ chairFurniturePath db "assets\animations\hall\lounge-chair-v5.bin",0 lampFurniturePath db "assets\animations\hall\lamp-table-v5.bin",0 ropeFurniturePath db "assets\animations\hall\velvet-rope-v5.bin",0 slotFurniturePath db "assets\animations\hall\slot-cabinets-v5.bin",0 +pyramidTableProbePath db "assets\animations\pyramid\table-v2.bin",0 +pyramidBallsProbePath db "assets\animations\pyramid\balls-v1.bin",0 +poolBallsProbePath db "assets\animations\pyramid\pool-balls-v1.bin",0 +wakeRockerProbePath db "assets\animations\arrival\wake-rocker-v2.bin",0 +wakeLighterProbePath db "assets\animations\arrival\wake-lighter-v1.bin",0 +wakeKneesProbePath db "assets\animations\arrival\wake-knees-v1.bin",0 +wakeSleeveProbePath db "assets\animations\arrival\wake-sleeve-v1.bin",0 +alleyMaterialsProbePath db "assets\animations\arrival\alley-materials-v1.bin",0 +worldDressingProbePath db "assets\animations\hall\world-dressing-v6.bin",0 furniturePaths dd OFFSET cardFurniturePath,320,208,1 dd OFFSET wheelFurniturePath,288,268,1 dd OFFSET backFurniturePath,304,236,1 @@ -36,6 +49,15 @@ furniturePaths dd OFFSET cardFurniturePath,320,208,1 dd OFFSET lampFurniturePath,192,224,1 dd OFFSET ropeFurniturePath,288,192,1 dd OFFSET slotFurniturePath,224,256,3 + dd OFFSET pyramidTableProbePath,640,480,1 + dd OFFSET pyramidBallsProbePath,12,12,8 + dd OFFSET poolBallsProbePath,12,12,16 + dd OFFSET wakeRockerProbePath,192,320,32 + dd OFFSET wakeLighterProbePath,256,256,16 + dd OFFSET wakeKneesProbePath,640,240,1 + dd OFFSET wakeSleeveProbePath,256,384,1 + dd OFFSET alleyMaterialsProbePath,128,128,4 + dd OFFSET worldDressingProbePath,128,128,3 .code RunStreetAssetProbe PROC call InitPaths3D @@ -70,8 +92,16 @@ RunStreetAssetProbe PROC cmp eax,1 jne probe_failed call FreeNativeCoinArt3D + call LoadNativeDiceArt3D + cmp eax,1 + jne probe_failed + call FreeNativeDiceArt3D + call LoadScratchArt3D + cmp eax,1 + jne probe_failed + call FreeScratchArt3D mov esi,OFFSET furniturePaths - mov edi,8 + mov edi,17 probe_furniture: invoke LoadAnimPack3D,OFFSET streetPack,[esi] cmp eax,1 diff --git a/tools/tests/test_billiards_benchmark.ps1 b/tools/tests/test_billiards_benchmark.ps1 new file mode 100644 index 0000000..e17a999 --- /dev/null +++ b/tools/tests/test_billiards_benchmark.ps1 @@ -0,0 +1,20 @@ +param([string]$MasmRoot = 'F:\MASM32\masm32') +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $root +try { + & "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /Fobuild\actions\billiards-benchmark.obj tools\tests\billiards_benchmark.asm + if ($LASTEXITCODE -ne 0) { throw 'Billiards benchmark assembly failed' } + $sdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Lib' + $sdk = Get-ChildItem -LiteralPath $sdkRoot -Directory | + Sort-Object Name -Descending | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\x86\kernel32.lib') } | + Select-Object -First 1 + $libraries = @() + $libraries += '/LIBPATH:' + (Join-Path $sdk.FullName 'um\x86') + $libraries += "/LIBPATH:$MasmRoot\lib" + & "$MasmRoot\bin\link.exe" /nologo /SUBSYSTEM:CONSOLE /ENTRY:RunBilliardsBenchmark@0 @libraries /OUT:build\actions\billiards-benchmark.exe build\actions\billiards-benchmark.obj build\graphics\poker.obj kernel32.lib winmm.lib user32.lib ws2_32.lib gdi32.lib + if ($LASTEXITCODE -ne 0) { throw 'Billiards benchmark link failed' } + & .\build\actions\billiards-benchmark.exe + if ($LASTEXITCODE -ne 0) { throw "Billiards benchmark failed: $LASTEXITCODE" } +} finally { Pop-Location } diff --git a/tools/tests/test_billiards_module.ps1 b/tools/tests/test_billiards_module.ps1 new file mode 100644 index 0000000..503837f --- /dev/null +++ b/tools/tests/test_billiards_module.ps1 @@ -0,0 +1,21 @@ +param([string]$MasmRoot = 'F:\MASM32\masm32') +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $root +try { + & 'C:\Python314\python.exe' tools\tests\prepare_billiards_module.py + if ($LASTEXITCODE -ne 0) { throw 'Billiards helper preparation failed' } + & "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /I. /Fobuild\billiards\module.obj tools\tests\billiards_module.asm + if ($LASTEXITCODE -ne 0) { throw 'Billiards module assembly failed' } + $sdk = Get-ChildItem -LiteralPath "${env:ProgramFiles(x86)}\Windows Kits\10\Lib" -Directory | + Sort-Object Name -Descending | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\x86\kernel32.lib') } | Select-Object -First 1 + $libraries = @() + $libraries += '/LIBPATH:' + (Join-Path $sdk.FullName 'um\x86') + $libraries += "/LIBPATH:$MasmRoot\lib" + & "$MasmRoot\bin\link.exe" /nologo /SUBSYSTEM:CONSOLE /ENTRY:BilliardsModuleStart@0 @libraries /OUT:build\billiards\module.exe build\billiards\module.obj build\billiards\helpers.obj kernel32.lib winmm.lib user32.lib ws2_32.lib gdi32.lib + if ($LASTEXITCODE -ne 0) { throw 'Billiards module link failed' } + & .\build\billiards\module.exe + if ($LASTEXITCODE -ne 0) { throw "Eight-ball native regression failed at case $LASTEXITCODE" } + Write-Output 'PASS: fresh MASM billiards with real native graphics/input helpers' +} finally { Pop-Location } diff --git a/tools/tests/test_scratch_module.ps1 b/tools/tests/test_scratch_module.ps1 new file mode 100644 index 0000000..9f70e8c --- /dev/null +++ b/tools/tests/test_scratch_module.ps1 @@ -0,0 +1,22 @@ +param([string]$MasmRoot = 'F:\MASM32\masm32') +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $root +try { + & 'C:\Python314\python.exe' tools\tests\prepare_scratch_module.py + if ($LASTEXITCODE -ne 0) { throw 'Scratch helper preparation failed' } + & "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /I. /DSCRATCH_TEST_SYNTAX=1 /DSCRATCH_MODULE_TEST=1 /Fobuild\scratch\module.obj tools\tests\scratch_syntax.asm + if ($LASTEXITCODE -ne 0) { throw 'Scratch module assembly failed' } + $sdk = Get-ChildItem -LiteralPath "${env:ProgramFiles(x86)}\Windows Kits\10\Lib" -Directory | + Sort-Object Name -Descending | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\x86\kernel32.lib') } | + Select-Object -First 1 + $libraries = @() + $libraries += '/LIBPATH:' + (Join-Path $sdk.FullName 'um\x86') + $libraries += "/LIBPATH:$MasmRoot\lib" + & "$MasmRoot\bin\link.exe" /nologo /SUBSYSTEM:CONSOLE /ENTRY:ScratchModuleStart@0 @libraries /OUT:build\scratch\module-test.exe build\scratch\module.obj build\scratch\module-base.obj kernel32.lib winmm.lib user32.lib ws2_32.lib gdi32.lib + if ($LASTEXITCODE -ne 0) { throw 'Scratch module link failed' } + & .\build\scratch\module-test.exe + if ($LASTEXITCODE -ne 0) { throw "Scratch module assertion failed: $LASTEXITCODE" } + Write-Output 'PASS: fresh MASM scratch module with real native graphics/input helpers (not a renderer simulation)' +} finally { Pop-Location } diff --git a/tools/tests/test_scratch_probe.ps1 b/tools/tests/test_scratch_probe.ps1 new file mode 100644 index 0000000..8943a94 --- /dev/null +++ b/tools/tests/test_scratch_probe.ps1 @@ -0,0 +1,23 @@ +param([string]$MasmRoot = 'F:\MASM32\masm32') +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $root +try { + & 'C:\Python314\python.exe' tools\tests\export_scratch_probe.py + if ($LASTEXITCODE -ne 0) { throw 'Scratch probe state export failed' } + & "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /I. /Fobuild\scratch\native-probe.obj tools\tests\scratch_native_probe.asm + if ($LASTEXITCODE -ne 0) { throw 'Scratch probe assembly failed' } + $sdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Lib' + $sdk = Get-ChildItem -LiteralPath $sdkRoot -Directory | + Sort-Object Name -Descending | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\x86\kernel32.lib') } | + Select-Object -First 1 + $libraries = @() + if ($sdk) { $libraries += '/LIBPATH:' + (Join-Path $sdk.FullName 'um\x86') } + $libraries += "/LIBPATH:$MasmRoot\lib" + & "$MasmRoot\bin\link.exe" /nologo /SUBSYSTEM:CONSOLE /ENTRY:ScratchProbeStart @libraries /OUT:build\scratch\native-probe.exe build\scratch\native-probe.obj build\scratch\native-probe-base.obj kernel32.lib winmm.lib user32.lib ws2_32.lib gdi32.lib + if ($LASTEXITCODE -ne 0) { throw 'Scratch probe link failed' } + & .\build\scratch\native-probe.exe + if ($LASTEXITCODE -ne 0) { throw "Scratch probe failed: $LASTEXITCODE" } + Write-Output 'PASS: 36 edge-coin frames + 78 portrait scratching frames; coating survives view switches; grounded solid counter and hall entry/exit without console or extra charge' +} finally { Pop-Location } diff --git a/tools/tests/test_street_asset_paths.ps1 b/tools/tests/test_street_asset_paths.ps1 index de2cb69..c026f74 100644 --- a/tools/tests/test_street_asset_paths.ps1 +++ b/tools/tests/test_street_asset_paths.ps1 @@ -26,5 +26,5 @@ try { if ($LASTEXITCODE -ne 0) { throw "Street pack failed to load from $directory" } } finally { Pop-Location } } - Write-Output 'PASS: shipping MASM loader loads street, roulette, coin, native 777/Clover 5x3 and eight hall furniture packs from root/TEMP and clears failed headers' + Write-Output 'PASS: shipping MASM loader loads street, roulette, coin/dice, seven scratch-ticket kinds, 777/Clover, both billiards ball sets/table, eight hall furniture, wall decor and five wake-up/alley packs from root/TEMP and clears failed headers' } finally { Pop-Location } diff --git a/tools/tests/test_world_dressing_probe.ps1 b/tools/tests/test_world_dressing_probe.ps1 new file mode 100644 index 0000000..f3fbcbc --- /dev/null +++ b/tools/tests/test_world_dressing_probe.ps1 @@ -0,0 +1,26 @@ +param([string]$MasmRoot = 'F:\MASM32\masm32', [switch]$Benchmark) +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $root +try { + & 'C:\Python314\python.exe' tools\tests\prepare_world_dressing_probe.py + if ($LASTEXITCODE -ne 0) { throw 'World probe preparation failed' } + & "$MasmRoot\bin\ml.exe" /c /coff /Cp /nologo /I. /Fobuild\world\probe.obj tools\tests\world_dressing_probe.asm + if ($LASTEXITCODE -ne 0) { throw 'World probe assembly failed' } + $sdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Lib' + $sdk = Get-ChildItem -LiteralPath $sdkRoot -Directory | Sort-Object Name -Descending | + Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\x86\kernel32.lib') } | Select-Object -First 1 + $libraries = @() + if ($sdk) { $libraries += '/LIBPATH:' + (Join-Path $sdk.FullName 'um\x86') } + $libraries += "/LIBPATH:$MasmRoot\lib" + $entry = if ($Benchmark) { '/ENTRY:WorldDressingBenchmarkStart@0' } else { '/ENTRY:WorldDressingProbeStart@0' } + & "$MasmRoot\bin\link.exe" /nologo /SUBSYSTEM:CONSOLE $entry @libraries /OUT:build\world\probe.exe build\world\probe.obj build\world\base.obj kernel32.lib winmm.lib user32.lib ws2_32.lib gdi32.lib + if ($LASTEXITCODE -ne 0) { throw 'World probe link failed' } + & .\build\world\probe.exe + if ($Benchmark) { + Write-Output "Native street preview benchmark: $LASTEXITCODE ms/frame (40 frames)" + return + } + if ($LASTEXITCODE -ne 0) { throw "World probe failed: $LASTEXITCODE" } + Write-Output 'PASS: native before/after hall, fixed wall decor and street scale captures' +} finally { Pop-Location } diff --git a/tools/tests/world_dressing_probe.asm b/tools/tests/world_dressing_probe.asm new file mode 100644 index 0000000..9629c1b --- /dev/null +++ b/tools/tests/world_dressing_probe.asm @@ -0,0 +1,219 @@ +.386 +.model flat +option casemap:none +FP_1 equ 65536 +FP_HALF equ 32768 +SCREEN_W equ 640 +SCREEN_H equ 480 +SCREEN_SIZE equ 640*480 +CITY_PROP_COUNT equ 47 +HALL_LIGHT_COUNT equ 6 +HALL_DECOR_COUNT equ 12 +AnimPack3D STRUCT + magic dd ? + imageWidth dd ? + height dd ? + frames dd ? + bytes dd ? + pixels dd ? +AnimPack3D ENDS +CityProp3D STRUCT + posX dd ? + posY dd ? + worldW dd ? + worldH dd ? + frame dd ? + asset dd ? + groundZ dd ? +CityProp3D ENDS +EXTERN casinoMap:BYTE,hallLevel3D:DWORD,outdoorRender3D:DWORD +EXTERN hallLights3D:BYTE,hallLoungeDecor3D:BYTE,hallRopeFurniture3D:BYTE,hallLampFurniture3D:BYTE +EXTERN cityObjects3D:BYTE,hallStreetLife3D:BYTE,cityDecorPack3D:AnimPack3D +EXTERN playerX:DWORD,playerY:DWORD,playerAngle:DWORD,viewEye3D:DWORD,viewCeiling3D:DWORD,viewHorizon3D:DWORD +EXTERN worldMap3D:DWORD,floorTexture3D:DWORD,cityMap3D:BYTE +EXTERN cityPhase3D:DWORD,cityTime3D:DWORD,cityClock3D:DWORD,arrivalState3D:DWORD +EXTERN screenBuffer:DWORD,cityDepthBuffer3D:DWORD,cityRayX3D:DWORD,cityRayY3D:DWORD,zBuffer:DWORD,cityBackdrop3D:DWORD +EXTERN playerPlaneX:DWORD,playerPlaneY:DWORD +option language:stdcall +ExitProcess PROTO :DWORD +GetTickCount PROTO +InitPaths3D PROTO +LoadTextures3D PROTO +LoadArrivalAssets3D PROTO +InitializeCasinoSprites3D PROTO +InitHall3D PROTO +LoadHallAssets3D PROTO +LoadCityArrival3D PROTO +UpdateCamera3D PROTO +RenderCeilingAndFloor PROTO +RenderWalls PROTO +PrepareCityDepth3D PROTO +RenderSprites PROTO +RenderHall3D PROTO +PositionCityCamera3D PROTO +DrawCitySky3D PROTO +DrawCitySkyline3D PROTO +RenderCityObjects3D PROTO +AnimateCityPedestrians3D PROTO +RenderCityFurniture3D PROTO +RenderCityBins3D PROTO +RenderCityAlley3D PROTO +RenderCityFacade3D PROTO +RenderCityGuards3D PROTO +DrawCityRain3D PROTO +WriteActionSnapshot3D PROTO :DWORD +LoadAnimPack3D PROTO :DWORD,:DWORD +FreeAnimPack3D PROTO :DWORD +ValidateCityPack3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +DrawCityVertical3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +DrawCityBoxSides3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +DrawCityHorizontal3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD +ResolveWorldSurface3D PROTO :DWORD +CityRayPlane3D PROTO :DWORD,:DWORD,:DWORD +ProjectCityPoint3D PROTO :DWORD,:DWORD +IsCityEmissive3D PROTO :DWORD,:DWORD +ShadeCityGround3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +.code +include src_graphics/world_dressing_data.inc +include src_graphics/world_dressing.inc +include src_graphics/hall_lighting.inc +include build/world/surfaces.inc +.data +worldHallBefore3D db "build\world\hall-before.bmp",0 +worldHallAfter3D db "build\world\hall-after.bmp",0 +worldWallAfter3D db "build\world\wall-after.bmp",0 +worldStreetBefore3D db "build\world\street-before.bmp",0 +worldStreetAfter3D db "build\world\street-after.bmp",0 +.code +WorldProbeHall PROC + call UpdateCamera3D + call RenderCeilingAndFloor + call RenderWalls + call PrepareCityDepth3D + call RenderSprites + call RenderHall3D + ret +WorldProbeHall ENDP +WorldProbeStreet PROC + call UpdateCamera3D + call DrawCitySky3D + call RenderCeilingAndFloor + call DrawCitySkyline3D + call RenderWalls + call PrepareCityDepth3D + cmp worldStreetSized3D,0 + je @F + call RenderStreetDressing3D +@@: + call RenderCityObjects3D + call RenderCityFurniture3D + call RenderCityBins3D + call RenderCityAlley3D + call RenderCityFacade3D + call RenderCityGuards3D + call DrawCityRain3D + ret +WorldProbeStreet ENDP +WorldDressingProbeStart PROC + call InitPaths3D + call LoadTextures3D + call LoadArrivalAssets3D + call InitializeCasinoSprites3D + call InitHall3D + call LoadHallAssets3D + call LoadCityArrival3D + mov playerX,14*FP_1 + mov playerY,22*FP_1 + mov playerAngle,384 + call WorldProbeHall + invoke WriteActionSnapshot3D,OFFSET worldHallBefore3D + mov arrivalState3D,0 + mov cityPhase3D,10 + mov cityTime3D,3000 + mov cityClock3D,18200 + call PositionCityCamera3D + mov outdoorRender3D,1 + mov worldMap3D,OFFSET cityMap3D + mov floorTexture3D,60 + call WorldProbeStreet + invoke WriteActionSnapshot3D,OFFSET worldStreetBefore3D + call LoadWorldDressing3D + mov viewEye3D,FP_1*121/100 + call WorldProbeStreet + invoke WriteActionSnapshot3D,OFFSET worldStreetAfter3D + call InitHall3D + call BuildHallLighting3D + cmp hallLightMap3D[19*256*8+14*8],80 + jb world_probe_bake_failed + cmp hallContactMap3D[19*256*8+13*8],40 + jne world_probe_bake_failed + invoke HallLightVisible3D,132,148,132,164 + cmp eax,1 + jne world_probe_bake_failed + mov casinoMap[19*32+16],1 + invoke HallLightVisible3D,132,148,132,164 + mov casinoMap[19*32+16],0 + test eax,eax + jnz world_probe_bake_failed + invoke StreetGroundHeight3D,14*FP_1,FP_1*1265/100 + cmp eax,FP_1*3/50 + jne world_probe_bake_failed + invoke StreetGroundHeight3D,35*FP_1,FP_1*1265/100 + test eax,eax + jnz world_probe_bake_failed + mov outdoorRender3D,0 + mov worldMap3D,OFFSET casinoMap + mov floorTexture3D,100 + mov viewEye3D,FP_1*21/20 + mov viewHorizon3D,240 + mov playerX,14*FP_1 + mov playerY,22*FP_1 + mov playerAngle,384 + call WorldProbeHall + call RenderHallDressing3D + call ApplyHallLighting3D + invoke WriteActionSnapshot3D,OFFSET worldHallAfter3D + mov playerX,27*FP_1 + mov playerY,18*FP_1 + mov playerAngle,0 + call WorldProbeHall + call RenderHallDressing3D + call ApplyHallLighting3D + invoke WriteActionSnapshot3D,OFFSET worldWallAfter3D + invoke ExitProcess,0 +world_probe_bake_failed: + invoke ExitProcess,1 +WorldDressingProbeStart ENDP +WorldDressingBenchmarkStart PROC uses ebx esi + call InitPaths3D + call LoadTextures3D + call LoadArrivalAssets3D + call InitializeCasinoSprites3D + call InitHall3D + call LoadHallAssets3D + call LoadCityArrival3D + call LoadWorldDressing3D + mov arrivalState3D,0 + mov cityPhase3D,10 + mov cityTime3D,3000 + mov cityClock3D,18200 + call PositionCityCamera3D + mov outdoorRender3D,1 + mov worldMap3D,OFFSET cityMap3D + mov floorTexture3D,60 + mov viewEye3D,FP_1*121/100 + call GetTickCount + mov esi,eax + mov ebx,40 +world_probe_benchmark: + call WorldProbeStreet + dec ebx + jnz world_probe_benchmark + call GetTickCount + sub eax,esi + xor edx,edx + mov ecx,40 + div ecx + invoke ExitProcess,eax +WorldDressingBenchmarkStart ENDP +end diff --git a/tools/tests/world_dressing_test.inc b/tools/tests/world_dressing_test.inc new file mode 100644 index 0000000..659c78a --- /dev/null +++ b/tools/tests/world_dressing_test.inc @@ -0,0 +1,224 @@ +.data +worldUnlitPath3D db "build\actions\world-hall-unlit.bmp",0 +worldLitPath3D db "build\actions\world-hall-lit.bmp",0 +worldFrontPath3D db "build\actions\world-wall-front.bmp",0 +worldAnglePath3D db "build\actions\world-wall-angle.bmp",0 +worldStreetPaths3D dd OFFSET worldStreetEarlyPath3D,OFFSET worldStreetMiddlePath3D,OFFSET worldStreetLatePath3D +worldStreetEarlyPath3D db "build\actions\world-street-early.bmp",0 +worldStreetMiddlePath3D db "build\actions\world-street-middle.bmp",0 +worldStreetLatePath3D db "build\actions\world-street-late.bmp",0 +.code +WorldDressingTestFrame3D PROC + call UpdateCamera3D + call RenderCeilingAndFloor + call RenderWalls + call PrepareCityDepth3D + call RenderSprites + call RenderHall3D + ret +WorldDressingTestFrame3D ENDP + +TestWorldDressing3D PROC uses ebx esi edi + local oldBank:DWORD,oldSeed:DWORD,mapSum:DWORD,oldPixel:DWORD,oldCell:DWORD,lightSum:DWORD + mov hallTestStage3D,7720 + mov eax,bankroll + mov oldBank,eax + mov eax,rngSeed3D + mov oldSeed,eax + cmp worldDressingArt3D.pixels,0 + je world_test_dressing_fail + cmp worldDressingArt3D.frames,3 + jne world_test_dressing_fail + invoke ResolveCitySurface3D,116 + cmp ecx,128 + jne world_test_dressing_fail + cmp edx,128 + jne world_test_dressing_fail + mov ebx,worldDressingArt3D.pixels + add ebx,128*128*4 + cmp eax,ebx + jne world_test_dressing_fail + mov eax,worldDressingArt3D.pixels + mov oldPixel,eax + mov worldDressingArt3D.pixels,0 + invoke ResolveDressingSurface3D,115 + mov ebx,eax + mov eax,oldPixel + mov worldDressingArt3D.pixels,eax + test ebx,ebx + jnz world_test_dressing_fail + call InitHall3D + cmp viewEye3D,HALL_STANDING_EYE + jne world_test_dressing_fail + mov mapSum,0 + xor esi,esi +world_test_map_sum: + movzx eax,casinoMap[esi] + add mapSum,eax + inc esi + cmp esi,1024 + jb world_test_map_sum + ; A temporary solid tile must block the baked light ray and is always restored. + invoke HallLightVisible3D,132,148,132,164 + cmp eax,1 + jne world_test_dressing_fail + movzx eax,casinoMap[19*32+16] + mov oldCell,eax + mov casinoMap[19*32+16],1 + invoke HallLightVisible3D,132,148,132,164 + mov ebx,eax + mov eax,oldCell + mov casinoMap[19*32+16],al + test ebx,ebx + jnz world_test_dressing_fail + cmp hallLightMap3D[19*256*8+14*8],80 + jb world_test_dressing_fail + cmp hallContactMap3D[19*256*8+13*8],40 + jne world_test_dressing_fail + mov lightSum,0 + xor esi,esi +world_test_light_sum: + movzx eax,hallLightMap3D[esi] + add lightSum,eax + inc esi + cmp esi,65536 + jb world_test_light_sum + call BuildHallLighting3D + xor ebx,ebx + xor esi,esi +world_test_rebake: + movzx eax,hallLightMap3D[esi] + add ebx,eax + inc esi + cmp esi,65536 + jb world_test_rebake + cmp ebx,lightSum + jne world_test_dressing_fail + mov hallTestStage3D,7721 + ; Props retain one-time dimensions and floor anchors when assets are reloaded. + call LoadWorldDressing3D + xor esi,esi +world_test_street_scale: + imul ebx,esi,SIZEOF CityProp3D + cmp cityObjects3D[ebx].asset,OFFSET hallStreetLife3D + jne world_test_scale_next + cmp cityObjects3D[ebx].frame,32 + jb world_test_scale_next + cmp cityObjects3D[ebx].frame,37 + je world_test_dressing_fail + cmp cityObjects3D[ebx].worldH,FP_1 + ja world_test_dressing_fail + cmp cityObjects3D[ebx].posX,10*FP_1 + jb world_test_scale_next + cmp cityObjects3D[ebx].posX,28*FP_1+FP_HALF + jae world_test_scale_next + mov eax,cityObjects3D[ebx].posY + cmp eax,12*FP_1 + jb world_test_scale_next + cmp eax,13*FP_1 + jae world_test_scale_next + cmp cityObjects3D[ebx].groundZ,FP_1*3/50 + jne world_test_dressing_fail +world_test_scale_next: + inc esi + cmp esi,CITY_PROP_COUNT + jb world_test_street_scale + ; Outdoor, basement and disabled lighting must not alter a single pixel. + lea edi,screenBuffer + mov eax,00123456h + mov ecx,SCREEN_SIZE + rep stosd + mov outdoorRender3D,1 + call ApplyHallLighting3D + mov outdoorRender3D,0 + mov hallLevel3D,1 + call ApplyHallLighting3D + mov hallLevel3D,0 + mov hallLighting3D,0 + call ApplyHallLighting3D + mov hallLighting3D,1 + lea esi,screenBuffer + mov ecx,SCREEN_SIZE +world_test_unchanged: + lodsd + cmp eax,00123456h + jne world_test_dressing_fail + loop world_test_unchanged + mov hallTestStage3D,7722 + invoke StreetGroundHeight3D,14*FP_1,FP_1*1265/100 + cmp eax,FP_1*3/50 + jne world_test_dressing_fail + invoke StreetGroundHeight3D,14*FP_1,14*FP_1 + test eax,eax + jnz world_test_dressing_fail + invoke StreetGroundHeight3D,35*FP_1,FP_1*1265/100 + test eax,eax + jnz world_test_dressing_fail + mov engineMode,MODE_WALK + mov playerX,14*FP_1 + mov playerY,22*FP_1 + mov playerAngle,384 + mov viewHorizon3D,240 + mov hallLighting3D,0 + call WorldDressingTestFrame3D + invoke WriteActionSnapshot3D,OFFSET worldUnlitPath3D + mov hallLighting3D,1 + call WorldDressingTestFrame3D + invoke WriteActionSnapshot3D,OFFSET worldLitPath3D + mov playerX,27*FP_1 + mov playerY,18*FP_1 + mov playerAngle,0 + call WorldDressingTestFrame3D + invoke WriteActionSnapshot3D,OFFSET worldFrontPath3D + mov playerY,19*FP_1 + mov playerAngle,496 + call WorldDressingTestFrame3D + invoke WriteActionSnapshot3D,OFFSET worldAnglePath3D + mov hallTestStage3D,7723 + mov arrivalState3D,ARRIVAL_WALK + mov cityPhase3D,CITY_STREET + xor esi,esi +world_test_street_frame: + imul eax,esi,2800 + add eax,800 + mov cityTime3D,eax + add eax,CITY_PRE_ALLEY_MS+6300 + mov cityClock3D,eax + call RenderCityArrival3D + xor edi,edi +world_test_feet: + imul ebx,edi,SIZEOF CityProp3D + invoke StreetGroundHeight3D,cityObjects3D[ebx].posX,cityObjects3D[ebx].posY + cmp cityObjects3D[ebx].groundZ,eax + jne world_test_dressing_fail + inc edi + cmp edi,CITY_PROP_COUNT + jb world_test_feet + invoke WriteActionSnapshot3D,worldStreetPaths3D[esi*4] + inc esi + cmp esi,3 + jb world_test_street_frame + ; Rendering is cosmetic: casino geometry, balance and gameplay RNG are untouched. + xor esi,esi + xor ebx,ebx +world_test_map_compare: + movzx eax,casinoMap[esi] + add ebx,eax + inc esi + cmp esi,1024 + jb world_test_map_compare + cmp ebx,mapSum + jne world_test_dressing_fail + mov eax,oldBank + cmp bankroll,eax + jne world_test_dressing_fail + mov eax,oldSeed + cmp rngSeed3D,eax + jne world_test_dressing_fail + call InitHall3D + mov eax,1 + ret +world_test_dressing_fail: + xor eax,eax + ret +TestWorldDressing3D ENDP diff --git a/tools/tests/world_regression.inc b/tools/tests/world_regression.inc index 9ef8c42..5bcf516 100644 --- a/tools/tests/world_regression.inc +++ b/tools/tests/world_regression.inc @@ -118,7 +118,7 @@ world_route_car_next: cmp sample,eax jb world_route_sample inc phase - cmp phase,10 + cmp phase,CITY_DOOR+1 jb world_route_phase mov cityPhase3D,CITY_STREET mov cityTime3D,3100 @@ -181,7 +181,7 @@ world_route_car_next: mov barPhase3D,0 mov hallTestStage3D,7704 ; A ball on cloth just outside the visible mouth must not vanish. - call InitPool3D + call ResetPool3D mov poolX3D,38*FP_1 mov poolY3D,58*FP_1 invoke PoolPocketBall3D,0 @@ -190,7 +190,7 @@ world_route_car_next: ; All six throats are reached through motion, not direct state manipulation. xor esi,esi world_pocket_motion: - call InitPool3D + call ResetPool3D mov edi,1 world_pocket_clear: mov poolActive3D[edi*4],0 @@ -273,7 +273,7 @@ world_pocket_tick: ; inactive balls must name a real pocket. No silent deletion or runaway. mov rounds,0 world_break_round: - call InitPool3D + call ResetPool3D mov eax,rounds imul eax,37 and eax,511 @@ -317,7 +317,7 @@ world_break_finished: inc rounds cmp rounds,100 jb world_break_round - call InitPool3D + call ResetPool3D mov poolPower3D,49000 call RenderPool3D invoke WriteActionSnapshot3D,OFFSET hallTestPower3D