diff --git a/assets/animations/bowling/player-underarm-v2.bin b/assets/animations/bowling/player-underarm-v2.bin new file mode 100644 index 0000000..9cec8b6 Binary files /dev/null and b/assets/animations/bowling/player-underarm-v2.bin differ diff --git a/assets/audio/bowling/applause.wav b/assets/audio/bowling/applause.wav new file mode 100644 index 0000000..e851628 Binary files /dev/null and b/assets/audio/bowling/applause.wav differ diff --git a/assets/audio/bowling/pins-heavy.wav b/assets/audio/bowling/pins-heavy.wav new file mode 100644 index 0000000..e74e1b2 Binary files /dev/null and b/assets/audio/bowling/pins-heavy.wav differ diff --git a/assets/audio/bowling/pins-light.wav b/assets/audio/bowling/pins-light.wav new file mode 100644 index 0000000..c16f02f Binary files /dev/null and b/assets/audio/bowling/pins-light.wav differ diff --git a/assets/audio/bowling/pinsetter.wav b/assets/audio/bowling/pinsetter.wav new file mode 100644 index 0000000..5d0e0fa Binary files /dev/null and b/assets/audio/bowling/pinsetter.wav differ diff --git a/assets/audio/bowling/release.wav b/assets/audio/bowling/release.wav new file mode 100644 index 0000000..96b9ea1 Binary files /dev/null and b/assets/audio/bowling/release.wav differ diff --git a/assets/audio/bowling/return.wav b/assets/audio/bowling/return.wav new file mode 100644 index 0000000..4cfaceb Binary files /dev/null and b/assets/audio/bowling/return.wav differ diff --git a/assets/audio/bowling/rolling.wav b/assets/audio/bowling/rolling.wav new file mode 100644 index 0000000..ee39c6b Binary files /dev/null and b/assets/audio/bowling/rolling.wav differ diff --git a/assets/sprites/bowling/player-underarm-v2.gif b/assets/sprites/bowling/player-underarm-v2.gif new file mode 100644 index 0000000..5ad2d75 Binary files /dev/null and b/assets/sprites/bowling/player-underarm-v2.gif differ diff --git a/assets/sprites/bowling/player-underarm-v2.json b/assets/sprites/bowling/player-underarm-v2.json new file mode 100644 index 0000000..58c7ec1 --- /dev/null +++ b/assets/sprites/bowling/player-underarm-v2.json @@ -0,0 +1,25 @@ +{ + "generator": "built-in image_gen", + "source": "player-underarm-v2.png", + "brief": "First-person right-handed underarm bowling: low pendulum swing beside thigh, backswing leaves view, hand behind/below blue ball, ankle-height release, palm-up follow-through; dark rolled sleeve; transparent 4 by 3 atlas. Replaces rejected forward-pushing draft.", + "selected_cells": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 11 + ], + "frames": 33, + "frame_size": [ + 256, + 256 + ], + "release_frame": 21, + "audio": "Original seeded PCM synthesis, no sampled third-party recordings." +} diff --git a/assets/sprites/bowling/player-underarm-v2.png b/assets/sprites/bowling/player-underarm-v2.png new file mode 100644 index 0000000..79d3876 Binary files /dev/null and b/assets/sprites/bowling/player-underarm-v2.png differ diff --git a/bin/poker.exe b/bin/poker.exe index 9c926c0..31c6a01 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index d9641d9..a279d80 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "3D0237E09CAED350548457BFAB24C4D67D6FAFC3D689C2D87C8BE360B64EC0AF"; + const string ExpectedPokerSha256 = "536E23BED4055562BC63871826FF44FFED037923A9BE8E0F591131DB4FBD104B"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index 9c926c0..31c6a01 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 71d05d5..1ed54ed 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/BOWLING.md b/docs/BOWLING.md index 4d28057..9f8a73c 100644 --- a/docs/BOWLING.md +++ b/docs/BOWLING.md @@ -35,19 +35,115 @@ the committed parameters without additional random error. Player and bot alternate complete frames. A second ball uses the remaining rack; strikes, spares and tenth-frame bonuses use the shared native bowling scorer. The lane monitor shows the two scores and current phase. Tab toggles a cumulative -ten-frame score sheet; unresolved bonuses stay blank rather than inventing a total. +ten-frame score sheet with every roll, X for a strike, / for a spare and - for a +miss; unresolved cumulative bonuses stay blank rather than inventing a total. The sheet pauses the current throw. Escape or the Exit command returns to the room. After a completed match, click or Space starts a fresh match. -The player game uses an arcade, not regulation-grade, collision approximation: -16-ms fixed steps, a rolling ball disc, ten pin discs, transferred pin impulses -and pin friction. Gutters latch once entered. Pins knocked by the ball can knock -other pins; removed pins cannot be scored again. The opponent uses the identical -solver, with private lane RNG choosing only its launch aim and power. The camera -follows the rolling ball; the bot uses a full-body shared-cast throwing animation. -The blue ball is a native shaded sphere with a procedural marble surface and +The player game uses an arcade, not regulation-grade, collision approximation. +Each 16-ms fixed tick has four 4-ms collision substeps. Normal impulses include +relative velocity, restitution and the ball/pin mass ratio; overlap correction +does not apply a second impulse to opening contacts. Billiards-style contact +resolution is reused without changing the billiards module. Contact normals and +pin axes retain fractional precision and are renormalized after yaw: even tiny +negative impulses cannot turn an ellipsoid into a clipped rectangular slab. Fallen pins still +collide and transfer momentum. Kickbacks and the back cushion return pins to the +deck; friction damps sliding and spin, increasing after the initial collision +cascade. Bounds include the tilted pin's full silhouette. Gutters latch once entered. + +The match uses a shopping-centre-style string pinsetter with cords intentionally +not drawn. Invisible slack tethers allow about 0.5 world units of travel from +each spot, damping outward motion only at the limit rather than pulling pins +back during the roll. Constraints run after every 4-ms contact substep. +A separate 5.2-second reset cycle closes a smooth metal pin shield, raises and +straightens the pins behind it, holds them aloft, lowers only the remaining pins +after the first ball (or all ten for a fresh rack), then opens the shield. A +first-ball miss leaves the standing rack alone, without running the motor. +Knocked-down pins remain physically represented aloft behind the hood, rather +than disappearing halfway up. The close camera eases back to frame the machine. +Rendering this cycle never changes scoring or advances the turn twice. +The sequence is based on Brunswick StringPin's documented standing-pin, strike, +second-ball and gutter cycles, with a front shield as on contemporary string machines: +https://brunswickbowling.com/uploads/document-library/Service-Manuals/Pinsetters/StringPin/BB_Service_Operations_Manual_StringPin_55-900004-000_En_Rev-06-16.pdf +Video reference: https://www.youtube.com/watch?v=EUwpfHz1Fp4 + +Impact excites a separate spring/gravity rocking angle: a small nudge can recover, +while a pin crossing the fall threshold is scored. Pins keep falling and hitting +one another after the ball passes; scoring waits for settlement with a bounded +timeout. Oriented ray/ellipsoid surfaces follow each pin's fall direction and +rolling yaw. This remains a floor-plane impulse solver with angular toppling, +not an unconstrained 3D rigid-body simulation. Other exhibition lanes retain +their existing automatic simulation; player and match opponent share the new +solver. Removed pins cannot be scored twice. + +Private lane RNG chooses only the opponent's launch aim and power. The player +has a 33-frame first-person underarm sequence: the backswing leaves the view, +the camera bends toward the low release, and the actual ball begins moving at +the release event. The held-ball sprite is never drawn over the rolling sphere. +The rejected forward-push draft is not shipped. Direction/power controls remain +unchanged, with the power meter on the left, away from the player's arm. +The camera follows the ball and returns to the approach as the reset finishes. +The bot uses a full-body shared-cast throwing animation. +Each ball is a native shaded sphere with a procedural marble surface and rotating finger-hole details, not a new neural or image runtime. +After every roll, including a first-ball miss and the last ball of the match, +the lane enters a 2.8-second ball-return phase. The ball disappears into the rear +pit, travels through the hidden return, then visibly emerges from the paired +return hood, rolls along the metal guides and stops in the tray. The camera +turns toward the correct left/right return unit and eases back to the approach; +the player's held-ball pose rises into view only after the tray ball is hidden. +The returning ball retains its committed colour; its storage slot stays empty. +Input during return cannot launch or lock the next throw. The existing scorer +advances exactly once before presentation; the final result overlay waits until +the ball has returned. Tab pauses this phase and its motor sound; Escape still +restores the saved room position and camera. The room's four return machines +have native hood/rail geometry and collision around their extended trays. +The return cycle does not change launch parameters or scores. + +### Ball Mass + +Before a match, the camera faces the actual paired return's ball storage rack. +Four balls (4 kg green, 5 kg red, 6 kg blue, 7 kg amber) rest lengthwise on two +metal rails beside the separate automatic return track. Click a sphere or its +projected weight plate to take it; Space takes the default/current ball. Keys +1-4 also select a ball. The weight command during aiming reopens this same rack. +Sphere picking and plates use the same world projection on either lane side. +The chosen ball's slot is empty while it is held, rolled or returned. A cached +single-frame palette transform colours only blue ball pixels in the hand atlas, +preserving skin, sleeve and alpha without duplicating the full animation pack. +Selecting a mass consumes its own event; it never also locks direction. +The default and opponent use 6 kg. The committed mass is frozen +for the whole throw; changing the next selection cannot alter an in-flight ball. + +Each pin has mass 1600 grams, within the USBC standard pin range: +https://bowl.com/welcome/how-bowling-pins-are-made +The normal impulse solver uses Q16 inverse masses proportional to 1600/mass_g, +with signed 64-bit products to avoid overflow. It retains restitution 0.5 and +mass-weighted separation. A heavier ball loses a smaller fraction of its incoming +velocity at an otherwise identical contact. No random accuracy penalty, strike +bonus or mass-dependent floor-friction multiplier is added. + +The gauge fixes the launch work W, calibrated to the original 6 kg release: +v_reference = 3 + 6 * power/100 m/s, W = 0.5 * 6 * v_reference^2 joules. +Actual speed is v_reference * sqrt(6 / mass_kg). The launch direction is normalized +so aiming away from the centre does not add extra speed. This is translational +launch work, not a complete model of arm biomechanics or rolling rotational energy. +Greater mass is therefore not an unconditional advantage: lighter balls launch +faster at equal work, while heavier balls resist collision deflection better. + +The opponent and two nearby spectators use their existing persistent identities +and react to the result. Their restaurant duplicates are suppressed during the +match. Strikes, spares and strong throws trigger shared-cast cheering; misses +leave conversational poses rather than forcing a celebration. + +Seven dedicated MCI waveaudio channels provide release, rolling, light/heavy pin +impacts, applause, the pinsetter motor and ball return. Impacts are driven by contact energy and rate-limited, +not triggered by rendering. The current sound-volume setting, including mute, +is applied before playback. Opening the score sheet stops the rolling channel; +leaving the match closes every alias. Effects are original seeded offline PCM +synthesis, not recordings taken from another game. + ## Visitor Simulation Each occupied lane owns a timer, rack mask, two players, frame counters and roll @@ -64,6 +160,16 @@ the other hall assets. No city mesh or rooftop OpenGL pass is used here. ## Sources And Rebuild - `src_graphics/bowling*.inc`: native room and lane simulation. +- `bowling_physics.inc`, `bowling_deck.inc`, `bowling_pinsetter.inc`, `bowling_return.inc`, + `bowling_weight.inc`, + `bowling_pin_shape.inc`, `bowling_scorecard.inc`, + `bowling_performance.inc`, `bowling_audio.inc`: match physics and presentation. +- `tools/generators/pack_bowling_feel.py`: first-person atlas normalization, + offline in-betweens and original PCM Foley. +- `assets/sprites/bowling/player-underarm-v2.png` and `.json`: retained built-in + imagegen source and generation brief. The packed 33-frame ANM1 asset lives in + `assets/animations/bowling/player-underarm-v2.bin`. +- `assets/audio/bowling/`: seven distributable WAV effects. - `tools/generators/pack_bowling.py`: offline atlas packing, frame normalization, in-between frames and furniture/collision tables. - `assets/sprites/bowling/`: retained image-generation sources. @@ -88,6 +194,26 @@ The additional `bowling_play_test.inc` covers LMB/Space input through native win events, stage separation, deterministic impacts at two frame deltas, gutters, empty/remaining racks, alternating turns, spare feedback, perfect games and the tenth-frame bonuses, unresolved score cells, scene captures and exit restoration. +`bowling_feel_test.inc` additionally checks recoverable rocking, strong toppling, +opening and repeated fallen-pin contacts, late settlement, tenth-frame glyphs, +63 aim/power launches, invisible travel limits, complete tilted-body bounds, +automatic guard/hoist/respots, first-miss idle, duplicate-score prevention, +tiny-impact axis/silhouette regressions, per-substep travel limits, +eight pinsetter captures, +33 native hand captures and real muted MCI playback/cleanup. +`bowling_return_test.inc` covers the paired return positions and camera turns, +visible rise/coast, held-ball handoff, blocked throw input, pause/resume, final-ball +return before results, unchanged scores/turns, tray collision and exit restoration. +Eight native return captures also produce `build/hall-expansion/bowling-native-return.gif`. +`bowling_weight_test.inc` checks world-projected sphere/plate selection on both +lane pairs, non-overlapping kg labels, storage-rail collision, colour and alpha +preservation in source/scratch hand pixels, native mouse/key input, immutable +shot mass, bot independence, equal launch work, momentum balance, restitution and +non-increasing translational contact energy. An additional 84 complete high-power +throws across all four masses verify settlement and deck bounds without replacing +the simulated outcomes. +The handoff and follow-through convention follows the USBC release guidance: +https://bowl.com/welcome/developing-your-release-f7b6dd1aff6c612e4aea7fce9dbb9194 The complete hall suite includes the same bowling checks alongside existing stairs, doors, rooftop, fortune-room, bar and cabaret regressions. `--parity` compares the tested procedures with the shipping assembly object. diff --git a/docs/SHARED_CHARACTERS.md b/docs/SHARED_CHARACTERS.md index ffdefe5..7300fbb 100644 --- a/docs/SHARED_CHARACTERS.md +++ b/docs/SHARED_CHARACTERS.md @@ -42,6 +42,58 @@ pick it up, throw, return, sit and alternate turns while keeping their identity. The historical twelve-slot NPC journal is preserved for save compatibility; its appearance mapping resolves to the ten shared visual identities. +## Main Hall Routines + +The main hall now has ten simultaneous visitors with fixed identities, rather +than six slots changing people at the entrance. The first six legacy poker-mind +records retain their size and personality indices; four extra world actors use +the same bounded entity accessor. The twelve-entry contract journal is retained +without changing existing profile saves. Ten of its identities are currently +represented in the hall; old entries are not deleted or reassigned. + +Each visitor has a preferred game, walking speed, session length, private +ambient wallet and private RNG. Weighted destinations replace the single shared +route. Short abstract game sessions produce a win/loss reaction, after which +guests continue playing or take a break. These are ambient outcomes, not full +hands played by the actual card-game engines. Neither the player's bankroll, +casino RNG, achievements nor contract progress is touched. + +There are twelve reservable places: five game approaches, two bar positions, +two existing lounge chairs, two audience positions and the entrance. Guests +order, drink, rest, converse with a nearby stationary guest and watch the stage. +Only short nearby remarks are displayed. Stale remarks clear on arrival. +Rest/drinks ease negative mood. Out-of-funds guests leave through the entrance, +wait offstage and return as the same person with a new ambient budget. + +BFS and body clearance still use the real room geometry. A visitor reserves a +place before walking and releases it after a successful change of destination. +Blocked guests pause with staggered retries; persistent obstruction releases +the reservation and permits replanning. Gait only advances on actual movement. +Seated poses align with the two existing chairs and pause navigation during the +stand-up transition. Visitor canvas dimensions remain unchanged. + +Pathfinding checks quarter points along each tile edge, not just free tile +centres, so approaches cannot cut through the expanded chair collision. Seating +has its own bounded 800 ms clock: starting a conversation resets the activity +clock but cannot replay the sit-down animation. The visual seat anchor blends +smoothly in both directions; walking frames require actual movement velocity. + +Routines currently belong to the main hall. Other floors keep their existing +ambient controllers and shared appearance assets; this is not yet a persistent +cross-floor population simulation. Hall routines pause outside walking mode or +outside the main hall, including while a modal conversation is open. + +`check_hall_expansion.py --npc-only` compiles production MASM and checks eight +simulated minutes: fixed identities, exclusive reservations, reachable positions, +activity transitions, walking progress, player-bank/RNG isolation, modal/floor +pause and bounded recovery from a player blocking the path. It also captures +native order, drinking, seated, win and loss poses. Full hall checks include the +same cases; no neural model or new runtime dependency is needed. +The chair regression also walks a visitor to a real chair at 16 ms ticks, checks +the complete sit/rise frame sequence and monotonic anchor motion, and starts a +seated conversation without restarting the transition. Native captures and GIFs +are written as `build/hall-expansion/npc-chair-*`. + ## Assets - `assets/sprites/characters/person-00-source-v1.png` through `person-09-source-v1.png`: diff --git a/poker.exe b/poker.exe index 9c926c0..31c6a01 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index cd86569..8236295 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/src_graphics/bowling.inc b/src_graphics/bowling.inc index 2fee45e..9ec7375 100644 --- a/src_graphics/bowling.inc +++ b/src_graphics/bowling.inc @@ -1,7 +1,10 @@ ; Native room, automatic lane simulation and full-body visitors; no AI service. InitBowling3D PROC uses esi edi mov bpActive3D,0 + mov bpWeightKg3D,6 + mov bpBotWeightKg3D,6 call InitBowlingBallSurface3D + call InitBowlingSetterSurface3D mov bwClock3D,-1 mov bwMonitorClock3D,-1 mov bwSeat3D,0 @@ -48,12 +51,18 @@ LoadBowling3D PROC invoke LoadAnimPack3D,OFFSET bwArt3D,OFFSET bwArtPath3D invoke ValidateCityPack3D,OFFSET bwArt3D,256,256,14 call LoadCharacterCast3D + invoke LoadAnimPack3D,OFFSET bpHandArt3D,OFFSET bpHandPath3D + invoke ValidateCityPack3D,OFFSET bpHandArt3D,256,256,33 + mov bpRackHandFrame3D,-1 + mov bpRackHandKg3D,-1 call RefreshBowlingMonitors3D bw_load_done: ret LoadBowling3D ENDP FreeBowling3D PROC + call BowlingAudioClose3D + invoke FreeAnimPack3D,OFFSET bpHandArt3D invoke FreeAnimPack3D,OFFSET bwArt3D invoke FreeAnimPack3D,OFFSET bwMotionArt3D invoke FreeAnimPack3D,OFFSET bwSeatedArt3D @@ -62,6 +71,24 @@ FreeBowling3D ENDP ResolveBowlingSurface3D PROC material:DWORD mov eax,material + cmp eax,255 + jb bw_material_base + cmp eax,257 + ja bw_material_missing + sub eax,254 + shl eax,14 + add eax,OFFSET bpBallSurface3D + mov ecx,64 + mov edx,64 + ret +bw_material_base: + cmp eax,254 + jne @F + mov eax,OFFSET bpSetterSurface3D + mov ecx,64 + mov edx,64 + ret +@@: cmp eax,253 jne @F mov eax,OFFSET bpBallSurface3D @@ -173,6 +200,36 @@ BowlingCanWalk3D PROC uses esi edi px:DWORD,py:DWORD cmp py,FP_1*223/10 jb bw_walk_block bw_walk_furniture: + cmp py,FP_1*252/10 + jb bw_walk_existing_props + cmp py,FP_1*266/10 + ja bw_walk_existing_props + xor esi,esi +bw_walk_return_tray: + imul eax,esi,FP_1*38/10 + add eax,FP_1*395/100 + sub eax,px + mov edi,eax + cdq + xor eax,edx + sub eax,edx + cmp py,26*FP_1 + jb @F + cmp eax,FP_1*4/10 + jbe bw_walk_block +@@: + ; The adjacent storage rails keep the automatic return path clear. + mov eax,edi + neg eax + cmp eax,FP_1*25/100 + jl @F + cmp eax,FP_1*85/100 + jle bw_walk_block +@@: + inc esi + cmp esi,4 + jb bw_walk_return_tray +bw_walk_existing_props: lea esi,bwCollision3D mov edi,BOWL_COLLISION_COUNT bw_walk_prop: diff --git a/src_graphics/bowling_audio.inc b/src_graphics/bowling_audio.inc new file mode 100644 index 0000000..b2c2293 --- /dev/null +++ b/src_graphics/bowling_audio.inc @@ -0,0 +1,119 @@ +BowlingAudioOpen3D PROC uses esi + call BowlingAudioClose3D + ; Offscreen mechanics probes have no playback device or foreground window. + cmp h3DWnd,0 + je bp_audio_open_done + xor esi,esi +bp_audio_open: + invoke mciSendStringA,bpAudioOpens[esi*4],0,0,0 + inc esi + cmp esi,7 + jb bp_audio_open + mov bpAudioReady3D,1 + mov bpAudioVolume3D,-1 +bp_audio_open_done: + ret +BowlingAudioOpen3D ENDP + +BowlingAudioClose3D PROC uses esi + cmp bpAudioReady3D,0 + je bp_audio_close_done + xor esi,esi +bp_audio_close: + invoke mciSendStringA,bpAudioCloses[esi*4],0,0,0 + inc esi + cmp esi,7 + jb bp_audio_close +bp_audio_close_done: + mov bpAudioReady3D,0 + mov bpAudioRolling3D,0 + mov bpAudioReturning3D,0 + ret +BowlingAudioClose3D ENDP + +BowlingAudioUpdate3D PROC uses ebx esi + cmp bpAudioReady3D,0 + je bp_audio_done + mov eax,settingsVolume + cmp eax,bpAudioVolume3D + je bp_audio_state + mov bpAudioVolume3D,eax + xor esi,esi +bp_audio_gain: + mov eax,settingsVolume + imul eax,70 + test esi,esi + jnz @F + imul eax,settingsVolume,35 +@@: + invoke FormatNumber,eax,OFFSET bpText3D + invoke CopyCString,OFFSET bpAudioCommand,bpAudioPrefixes[esi*4] + invoke AppendCString,OFFSET bpAudioCommand,OFFSET bpText3D + invoke mciSendStringA,OFFSET bpAudioCommand,0,0,0 + inc esi + cmp esi,7 + jb bp_audio_gain +bp_audio_state: + xor ebx,ebx + cmp bpState3D,BP_ROLL + jne @F + cmp bpScoreOpen3D,0 + jne @F + cmp bpEndTicks3D,0 + jne @F + mov ebx,1 +@@: + cmp ebx,bpAudioRolling3D + je bp_audio_return + mov bpAudioRolling3D,ebx + test ebx,ebx + jz bp_audio_stop_roll + cmp bpAudioReleased3D,0 + jne @F + mov bpAudioReleased3D,1 + invoke mciSendStringA,OFFSET bpReleasePlay,0,0,0 +@@: + mov eax,OFFSET bpRollPlay + jmp bp_audio_send_roll +bp_audio_stop_roll: + mov eax,OFFSET bpRollStop +bp_audio_send_roll: + invoke mciSendStringA,eax,0,0,0 +bp_audio_return: + xor ebx,ebx + cmp bpState3D,BP_RETURN + jne @F + cmp bpScoreOpen3D,0 + jne @F + cmp bpTime3D,2000 + jae @F + mov ebx,1 +@@: + cmp ebx,bpAudioReturning3D + je bp_audio_impact + mov bpAudioReturning3D,ebx + mov eax,OFFSET bpReturnStop + test ebx,ebx + jz @F + invoke FormatNumber,bpTime3D,OFFSET bpText3D + invoke CopyCString,OFFSET bpAudioCommand,OFFSET bpReturnPlay + invoke AppendCString,OFFSET bpAudioCommand,OFFSET bpText3D + mov eax,OFFSET bpAudioCommand +@@: + invoke mciSendStringA,eax,0,0,0 +bp_audio_impact: + cmp bpImpactCooldown3D,0 + jne bp_audio_done + cmp bpImpactEnergy3D,768 + jb bp_audio_done + mov eax,OFFSET bpLightPlay + cmp bpImpactEnergy3D,4000 + jb @F + mov eax,OFFSET bpImpactPlay +@@: + invoke mciSendStringA,eax,0,0,0 + mov bpImpactCooldown3D,140 + mov bpImpactEnergy3D,0 +bp_audio_done: + ret +BowlingAudioUpdate3D ENDP diff --git a/src_graphics/bowling_audio_data.inc b/src_graphics/bowling_audio_data.inc new file mode 100644 index 0000000..c380f18 --- /dev/null +++ b/src_graphics/bowling_audio_data.inc @@ -0,0 +1,35 @@ +.data +bpRollOpen db 'open "assets/audio/bowling/rolling.wav" type waveaudio alias bowlroll',0 +bpImpactOpen db 'open "assets/audio/bowling/pins-heavy.wav" type waveaudio alias bowlpins',0 +bpReleaseOpen db 'open "assets/audio/bowling/release.wav" type waveaudio alias bowlrelease',0 +bpClapOpen db 'open "assets/audio/bowling/applause.wav" type waveaudio alias bowlclap',0 +bpLightOpen db 'open "assets/audio/bowling/pins-light.wav" type waveaudio alias bowllight',0 +bpResetOpen db 'open "assets/audio/bowling/pinsetter.wav" type waveaudio alias bowlreset',0 +bpReturnOpen db 'open "assets/audio/bowling/return.wav" type waveaudio alias bowlreturn',0 +bpReturnPlay db 'play bowlreturn from ',0 +bpReturnStop db 'stop bowlreturn',0 +bpRollPlay db 'play bowlroll from 0',0 +bpRollStop db 'stop bowlroll',0 +bpImpactPlay db 'play bowlpins from 0',0 +bpReleasePlay db 'play bowlrelease from 0',0 +bpClapPlay db 'play bowlclap from 0',0 +bpLightPlay db 'play bowllight from 0',0 +bpResetPlay db 'play bowlreset from 0',0 +bpRollClose db 'close bowlroll',0 +bpImpactClose db 'close bowlpins',0 +bpReleaseClose db 'close bowlrelease',0 +bpClapClose db 'close bowlclap',0 +bpLightClose db 'close bowllight',0 +bpResetClose db 'close bowlreset',0 +bpReturnClose db 'close bowlreturn',0 +bpAudioOpens dd OFFSET bpRollOpen,OFFSET bpImpactOpen,OFFSET bpReleaseOpen,OFFSET bpClapOpen,OFFSET bpLightOpen,OFFSET bpResetOpen,OFFSET bpReturnOpen +bpAudioCloses dd OFFSET bpRollClose,OFFSET bpImpactClose,OFFSET bpReleaseClose,OFFSET bpClapClose,OFFSET bpLightClose,OFFSET bpResetClose,OFFSET bpReturnClose +bpVolumeRoll db 'setaudio bowlroll volume to ',0 +bpVolumePins db 'setaudio bowlpins volume to ',0 +bpVolumeRelease db 'setaudio bowlrelease volume to ',0 +bpVolumeClap db 'setaudio bowlclap volume to ',0 +bpVolumeLight db 'setaudio bowllight volume to ',0 +bpVolumeReset db 'setaudio bowlreset volume to ',0 +bpVolumeReturn db 'setaudio bowlreturn volume to ',0 +bpAudioPrefixes dd OFFSET bpVolumeRoll,OFFSET bpVolumePins,OFFSET bpVolumeRelease,OFFSET bpVolumeClap,OFFSET bpVolumeLight,OFFSET bpVolumeReset,OFFSET bpVolumeReturn +bpAudioCommand db 80 dup(0) diff --git a/src_graphics/bowling_data.inc b/src_graphics/bowling_data.inc index d039eeb..fca4e36 100644 --- a/src_graphics/bowling_data.inc +++ b/src_graphics/bowling_data.inc @@ -2,7 +2,7 @@ HALL_BOWLING equ 5 INTERACT_BOWL_SEAT equ 53 INTERACT_BOWL_WATCH equ 54 BW_MATERIAL equ 230 -BW_LAST_MATERIAL equ 253 +BW_LAST_MATERIAL equ 257 BW_LANES equ 8 BW_LANE_STEP equ FP_1*19/10 InitBowling3D PROTO @@ -22,6 +22,7 @@ RenderBowlingLanes3D PROTO RenderBowlingVisitors3D PROTO DrawBowlingPin3D PROTO :DWORD,:DWORD,:DWORD DrawBowlingBall3D PROTO :DWORD,:DWORD,:DWORD +DrawBowlingBallAt3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD BowlingImpact3D PROTO :DWORD BowlingRegisterRoll3D PROTO :DWORD,:DWORD BowlingScore3D PROTO :DWORD diff --git a/src_graphics/bowling_deck.inc b/src_graphics/bowling_deck.inc new file mode 100644 index 0000000..acd56fa --- /dev/null +++ b/src_graphics/bowling_deck.inc @@ -0,0 +1,149 @@ +; Invisible slack constraints engage only at the end of a pin's free travel. +BowlingPlayTether3D PROC uses ebx esi pin:DWORD + local deltaX:DWORD,deltaY:DWORD,length8:DWORD,nx:DWORD,ny:DWORD,outward:DWORD + mov esi,pin + mov eax,bpPinX3D[esi*4] + sub eax,bwPinX3D[esi*4] + mov deltaX,eax + sar eax,8 + imul eax,eax + mov ebx,eax + mov eax,bpPinY3D[esi*4] + sub eax,bwPinY3D[esi*4] + mov deltaY,eax + sar eax,8 + imul eax,eax + add eax,ebx + cmp eax,128*128 + jbe bp_tether_done + invoke PoolSqrt3D,eax + mov length8,eax + mov eax,deltaX + imul eax,256 + cdq + idiv length8 + mov nx,eax + mov eax,deltaY + imul eax,256 + cdq + idiv length8 + mov ny,eax + mov eax,nx + imul eax,128 + sar eax,8 + add eax,bwPinX3D[esi*4] + mov bpPinX3D[esi*4],eax + mov eax,ny + imul eax,128 + sar eax,8 + add eax,bwPinY3D[esi*4] + mov bpPinY3D[esi*4],eax + mov eax,bpPinVX3D[esi*4] + imul nx + shrd eax,edx,16 + mov ebx,eax + mov eax,bpPinVY3D[esi*4] + imul ny + shrd eax,edx,16 + add eax,ebx + test eax,eax + jle bp_tether_done + imul eax,4 + cdq + mov ecx,5 + idiv ecx + mov outward,eax + imul nx + shrd eax,edx,16 + sub bpPinVX3D[esi*4],eax + mov eax,outward + imul ny + shrd eax,edx,16 + sub bpPinVY3D[esi*4],eax +bp_tether_done: + ret +BowlingPlayTether3D ENDP + +; Bound the complete pin silhouette, including its horizontal neck/head. +BowlingPlayDeckBounds3D PROC uses ebx esi pin:DWORD + local extension:DWORD,lo:DWORD,hi:DWORD + mov esi,pin + mov ecx,bpPinTilt3D[esi*4] + shr ecx,8 + mov eax,tableSin[ecx*4] + imul eax,35 + cdq + mov ecx,100 + idiv ecx + mov extension,eax + imul bpPinAxisX3D[esi*4] + shrd eax,edx,16 + mov lo,-FP_1*82/100 + mov hi,FP_1*82/100 + test eax,eax + js bp_deck_left_extent + sub hi,eax + jmp bp_deck_x +bp_deck_left_extent: + sub lo,eax +bp_deck_x: + mov eax,bpPinX3D[esi*4] + cmp eax,lo + jge bp_deck_right + mov eax,lo + mov bpPinX3D[esi*4],eax + cmp bpPinVX3D[esi*4],0 + jge bp_deck_y_extent + jmp bp_deck_bounce_x +bp_deck_right: + cmp eax,hi + jle bp_deck_y_extent + mov eax,hi + mov bpPinX3D[esi*4],eax + cmp bpPinVX3D[esi*4],0 + jle bp_deck_y_extent +bp_deck_bounce_x: + mov eax,bpPinVX3D[esi*4] + neg eax + cdq + mov ecx,5 + idiv ecx + mov bpPinVX3D[esi*4],eax +bp_deck_y_extent: + mov eax,extension + imul bpPinAxisY3D[esi*4] + shrd eax,edx,16 + mov lo,FP_1*308/100 + mov hi,FP_1*547/100 + test eax,eax + js bp_deck_back_extent + sub hi,eax + jmp bp_deck_y +bp_deck_back_extent: + sub lo,eax +bp_deck_y: + mov eax,bpPinY3D[esi*4] + cmp eax,lo + jge bp_deck_front + mov eax,lo + mov bpPinY3D[esi*4],eax + cmp bpPinVY3D[esi*4],0 + jge bp_deck_done + jmp bp_deck_bounce_y +bp_deck_front: + cmp eax,hi + jle bp_deck_done + mov eax,hi + mov bpPinY3D[esi*4],eax + cmp bpPinVY3D[esi*4],0 + jle bp_deck_done +bp_deck_bounce_y: + mov eax,bpPinVY3D[esi*4] + neg eax + cdq + mov ecx,5 + idiv ecx + mov bpPinVY3D[esi*4],eax +bp_deck_done: + ret +BowlingPlayDeckBounds3D ENDP diff --git a/src_graphics/bowling_performance.inc b/src_graphics/bowling_performance.inc new file mode 100644 index 0000000..5fd4087 --- /dev/null +++ b/src_graphics/bowling_performance.inc @@ -0,0 +1,120 @@ +DrawBowlingPlayerHand3D PROC uses ebx esi + local offsetY:DWORD + mov offsetY,120 + cmp bpHandArt3D.pixels,0 + je bp_hand_done + cmp bpScoreOpen3D,0 + jne bp_hand_done + mov esi,bpLane3D + cmp bwTurn3D[esi*4],0 + jne bp_hand_done + xor ebx,ebx + cmp bpState3D,BP_RETURN + jne @F + cmp bpAfterReturn3D,BP_FINISHED + je bp_hand_done + mov eax,bpTime3D + sub eax,2100 + jle bp_hand_done + invoke BowlingPinsetterEase3D,eax,700 + neg eax + add eax,FP_1 + imul eax,300 + sar eax,16 + add eax,120 + mov offsetY,eax + jmp bp_hand_draw +@@: + cmp bpState3D,BP_DIRECTION + je bp_hand_draw + cmp bpState3D,BP_POWER + je bp_hand_draw + cmp bpState3D,BP_RELEASE + jne bp_hand_follow + mov eax,bpTime3D + xor edx,edx + mov ecx,33 + div ecx + cmp eax,20 + jbe @F + mov eax,20 +@@: + mov ebx,eax + jmp bp_hand_draw +bp_hand_follow: + cmp bpState3D,BP_ROLL + jne bp_hand_done + cmp bpTime3D,396 + jae bp_hand_done + mov eax,bpTime3D + xor edx,edx + mov ecx,33 + div ecx + add eax,21 + cmp eax,32 + jbe @F + mov eax,32 +@@: + mov ebx,eax +bp_hand_draw: + mov eax,bpShotWeightKg3D + cmp bpState3D,BP_DIRECTION + je bp_hand_selected + cmp bpState3D,BP_POWER + jne @F +bp_hand_selected: + mov eax,bpWeightKg3D +@@: + invoke BowlingTintHand3D,ebx,eax + invoke DrawAnimSprite3D,OFFSET bpTintedHand3D,0,320,offsetY,320,320 +bp_hand_done: + ret +DrawBowlingPlayerHand3D ENDP + +RenderBowlingReactions3D PROC uses ebx esi edi + local phase:DWORD + mov phase,CAST_TALK + cmp bpReactionTime3D,0 + je @F + mov eax,bpReaction3D + mov phase,eax +@@: + xor esi,esi +bp_crowd_person: + lea ebx,[esi+6] + test esi,esi + jnz bp_crowd_draw + mov edi,bpLane3D + cmp bwTurn3D[edi*4],1 + jne bp_crowd_draw + cmp bpState3D,BP_SETTLE + je bp_crowd_draw + cmp bpState3D,BP_FINISHED + jne bp_crowd_next +bp_crowd_draw: + invoke ConfigureCharacter3D,OFFSET bwSmallActor3D,ebx,phase,animationClock3D,0,0 + mov eax,bpCentre3D + mov ecx,FP_1*16/10 + test esi,esi + jnz @F + mov ecx,FP_1*9/10 +@@: + cmp esi,1 + jne @F + neg ecx +@@: + add eax,ecx + mov bwSmallActor3D.posX,eax + mov eax,FP_1*218/10 + test esi,esi + jnz @F + mov eax,23*FP_1 +@@: + mov bwSmallActor3D.posY,eax + invoke DrawCityObject3D,OFFSET bwSmallActor3D,0 +bp_crowd_next: + inc esi + cmp esi,3 + jb bp_crowd_person + ret +RenderBowlingReactions3D ENDP diff --git a/src_graphics/bowling_physics.inc b/src_graphics/bowling_physics.inc new file mode 100644 index 0000000..b738c60 --- /dev/null +++ b/src_graphics/bowling_physics.inc @@ -0,0 +1,482 @@ +; Symmetric normal impulses and overlap correction follow the existing billiards +; solver. Four 4-ms substeps prevent tunnelling; angular rocking is separate. +include src_graphics\bowling_deck.inc + +NormalizeBowlingPinAxis3D PROC uses esi pin:DWORD + local axisLength:REAL8 + mov esi,pin + mov eax,bpPinAxisX3D[esi*4] + or eax,bpPinAxisY3D[esi*4] + jnz bp_axis_length + mov bpPinAxisY3D[esi*4],-FP_1 + ret +bp_axis_length: + fild bpPinAxisX3D[esi*4] + fmul st,st + fild bpPinAxisY3D[esi*4] + fmul st,st + faddp st(1),st + fsqrt + fstp axisLength + fild bpPinAxisX3D[esi*4] + fdiv axisLength + fmul roofFloatFP3D + fistp bpPinAxisX3D[esi*4] + fild bpPinAxisY3D[esi*4] + fdiv axisLength + fmul roofFloatFP3D + fistp bpPinAxisY3D[esi*4] + ret +NormalizeBowlingPinAxis3D ENDP + +BowlingPlayHit3D PROC pin:DWORD,vx:DWORD,vy:DWORD + invoke BowlingPlayKick3D,pin,vx,vy + ret +BowlingPlayHit3D ENDP + +BowlingPlayKick3D PROC uses ebx esi edi pin:DWORD,vx:DWORD,vy:DWORD + local energy:DWORD + mov esi,pin + cmp esi,10 + jae bp_kick_done + ; Preserve fractional impulses: signed pre-rounding can collapse the basis. + fild vx + fmul st,st + fild vy + fmul st,st + faddp st(1),st + fsqrt + fistp energy + mov eax,energy + cmp eax,128 + jb bp_kick_done + cmp eax,bpImpactEnergy3D + jbe @F + mov bpImpactEnergy3D,eax +@@: + cmp eax,128 + jb bp_kick_done + mov edx,bpPhysicsTicks3D + mov bpPinContactTick3D[esi*4],edx + mov ecx,2 + cdq + idiv ecx + cmp eax,2000 + jbe @F + mov eax,2000 +@@: + add bpPinTiltV3D[esi*4],eax + mov eax,bpPinTiltV3D[esi*4] + cmp eax,2400 + jle @F + mov bpPinTiltV3D[esi*4],2400 +@@: + cmp bpPinTilt3D[esi*4],64*256 + jae bp_kick_done + mov eax,vx + mov ecx,FP_1 + imul ecx + idiv energy + mov bpPinAxisX3D[esi*4],eax + mov eax,vy + imul ecx + idiv energy + mov bpPinAxisY3D[esi*4],eax + invoke NormalizeBowlingPinAxis3D,esi + mov eax,vx + sub eax,vy + sar eax,10 + mov bpPinSpin3D[esi*4],eax +bp_kick_done: + ret +BowlingPlayKick3D ENDP + +BowlingPlayContact3D PROC uses ebx esi edi a:DWORD,b:DWORD + local deltaX:DWORD,deltaY:DWORD,radius:DWORD,distance:DWORD,nx:DWORD,ny:DWORD + local impulse:DWORD,changeA:DWORD,changeB:DWORD,ix:DWORD,iy:DWORD,massSum:DWORD + local contactLength:REAL8 + mov esi,a + mov edi,b + mov eax,bpRadius3D[esi*4] + add eax,bpRadius3D[edi*4] + mov radius,eax + shl eax,8 + mov ebx,eax + mov eax,bpPinX3D[edi*4] + sub eax,bpPinX3D[esi*4] + cmp eax,ebx + jg bp_contact_done + neg ebx + cmp eax,ebx + jl bp_contact_done + mov deltaX,eax + mov eax,bpPinY3D[edi*4] + sub eax,bpPinY3D[esi*4] + cmp eax,ebx + jl bp_contact_done + neg ebx + cmp eax,ebx + jg bp_contact_done + mov deltaY,eax + sar eax,8 + imul eax,eax + mov ebx,deltaX + sar ebx,8 + imul ebx,ebx + add eax,ebx + mov ebx,radius + imul ebx,ebx + cmp eax,ebx + jae bp_contact_done + fild deltaX + fmul st,st + fild deltaY + fmul st,st + faddp st(1),st + fsqrt + fst contactLength + fistp distance + cmp distance,0 + jne @F + mov deltaX,1 + mov deltaY,0 + mov distance,1 + fild distance + fstp contactLength +@@: + mov eax,radius + shl eax,8 + cmp distance,eax + jae bp_contact_done + fild deltaX + fdiv contactLength + fmul roofFloatFP3D + fistp nx + fild deltaY + fdiv contactLength + fmul roofFloatFP3D + fistp ny + mov eax,bpInvMass3D[esi*4] + add eax,bpInvMass3D[edi*4] + mov massSum,eax + mov eax,bpPinVX3D[edi*4] + sub eax,bpPinVX3D[esi*4] + imul nx + shrd eax,edx,16 + mov ebx,eax + mov eax,bpPinVY3D[edi*4] + sub eax,bpPinVY3D[esi*4] + imul ny + shrd eax,edx,16 + add eax,ebx + test eax,eax + jns bp_contact_separate + neg eax + mov ecx,FP_1*3/2 + imul ecx + idiv massSum + mov impulse,eax + imul bpInvMass3D[esi*4] + shrd eax,edx,16 + mov changeA,eax + mov eax,impulse + imul bpInvMass3D[edi*4] + shrd eax,edx,16 + mov changeB,eax + mov eax,changeA + imul nx + shrd eax,edx,16 + sub bpPinVX3D[esi*4],eax + neg eax + mov ix,eax + mov eax,changeA + imul ny + shrd eax,edx,16 + sub bpPinVY3D[esi*4],eax + neg eax + invoke BowlingPlayKick3D,esi,ix,eax + mov eax,changeB + imul nx + shrd eax,edx,16 + add bpPinVX3D[edi*4],eax + mov ix,eax + mov eax,changeB + imul ny + shrd eax,edx,16 + add bpPinVY3D[edi*4],eax + invoke BowlingPlayKick3D,edi,ix,eax +bp_contact_separate: + mov eax,radius + shl eax,8 + sub eax,distance + jle bp_contact_done + imul eax,3 + sar eax,2 + mov impulse,eax + imul bpInvMass3D[esi*4] + idiv massSum + mov changeA,eax + mov eax,impulse + imul bpInvMass3D[edi*4] + idiv massSum + mov changeB,eax + mov eax,changeA + imul nx + shrd eax,edx,16 + sub bpPinX3D[esi*4],eax + mov eax,changeA + imul ny + shrd eax,edx,16 + sub bpPinY3D[esi*4],eax + mov eax,changeB + imul nx + shrd eax,edx,16 + add bpPinX3D[edi*4],eax + mov eax,changeB + imul ny + shrd eax,edx,16 + add bpPinY3D[edi*4],eax +bp_contact_done: + ret +BowlingPlayContact3D ENDP + +BowlingPlayStep3D PROC uses ebx esi edi + local substep:DWORD,rack:DWORD + mov esi,bpLane3D + mov eax,bwRack3D[esi*4] + or eax,1024 + mov rack,eax + mov eax,bpBallX3D + mov bpPinX3D[40],eax + mov eax,bpBallY3D + mov bpPinY3D[40],eax + mov eax,bpBallVX3D + mov bpPinVX3D[40],eax + mov eax,bpBallVY3D + mov bpPinVY3D[40],eax + mov substep,0 +bp_physics_substep: + xor esi,esi +bp_physics_integrate: + bt rack,esi + jnc bp_physics_integrate_next + mov eax,bpPinVX3D[esi*4] + sar eax,2 + add bpPinX3D[esi*4],eax + mov eax,bpPinVY3D[esi*4] + sar eax,2 + add bpPinY3D[esi*4],eax + cmp esi,10 + jne bp_physics_integrate_next + cmp bpPinY3D[40],FP_1*28/10 + jge @F + mov bpPinVX3D[40],0 + mov bpPinVY3D[40],0 +@@: + mov eax,bpPinX3D[40] + cdq + xor eax,edx + sub eax,edx + cmp eax,FP_1*74/100 + jbe bp_physics_integrate_next + mov bpGutter3D,1 + mov eax,FP_1*87/100 + cmp bpPinX3D[40],0 + jge @F + neg eax +@@: + mov bpPinX3D[40],eax + mov bpPinVX3D[40],0 +bp_physics_integrate_next: + inc esi + cmp esi,BP_BODY_COUNT + jb bp_physics_integrate + xor esi,esi +bp_physics_pair_a: + bt rack,esi + jnc bp_physics_pair_next_a + lea edi,[esi+1] +bp_physics_pair_b: + bt rack,edi + jnc bp_physics_pair_next_b + cmp edi,10 + jne @F + cmp bpGutter3D,0 + jne bp_physics_pair_next_b +@@: + invoke BowlingPlayContact3D,esi,edi +bp_physics_pair_next_b: + inc edi + cmp edi,BP_BODY_COUNT + jb bp_physics_pair_b +bp_physics_pair_next_a: + inc esi + cmp esi,10 + jb bp_physics_pair_a + xor esi,esi +bp_physics_substep_limits: + bt rack,esi + jnc @F + invoke BowlingPlayTether3D,esi + invoke BowlingPlayDeckBounds3D,esi +@@: + inc esi + cmp esi,10 + jb bp_physics_substep_limits + inc substep + cmp substep,4 + jb bp_physics_substep + xor esi,esi +bp_physics_pin: + bt rack,esi + jnc bp_physics_pin_next + mov ecx,246 + mov eax,bpPhysicsTicks3D + sub eax,bpPinContactTick3D[esi*4] + cmp eax,10 + jb @F + bt bpHitMask3D,esi + jnc @F + mov ecx,224 +@@: + mov eax,bpPinVX3D[esi*4] + imul eax,ecx + sar eax,8 + mov bpPinVX3D[esi*4],eax + mov eax,bpPinVY3D[esi*4] + imul eax,ecx + sar eax,8 + mov bpPinVY3D[esi*4],eax + mov eax,bpPinTilt3D[esi*4] + cmp eax,24*256 + jae bp_physics_topple + mov edx,eax + sar edx,6 + sub bpPinTiltV3D[esi*4],edx + jmp bp_physics_tilt +bp_physics_topple: + add bpPinTiltV3D[esi*4],160 +bp_physics_tilt: + mov edx,bpPinTiltV3D[esi*4] + imul edx,245 + sar edx,8 + mov bpPinTiltV3D[esi*4],edx + add eax,edx + test eax,eax + jns @F + xor eax,eax + mov bpPinTiltV3D[esi*4],0 +@@: + cmp eax,128*256 + jbe @F + mov eax,128*256 + mov edx,bpPinTiltV3D[esi*4] + neg edx + sar edx,2 + cmp edx,-80 + jl bp_physics_bounce + xor edx,edx +bp_physics_bounce: + mov bpPinTiltV3D[esi*4],edx +@@: + mov bpPinTilt3D[esi*4],eax + cmp eax,64*256 + jb bp_physics_pin_next + bts bpHitMask3D,esi + mov bpRadius3D[esi*4],48 + ; Slow rolling yaw, independent of the fall's vertical angle. + mov ecx,bpPinSpin3D[esi*4] + and ecx,511 + mov eax,bpPinAxisX3D[esi*4] + imul tableCos[ecx*4] + shrd eax,edx,16 + mov ebx,eax + mov eax,bpPinAxisY3D[esi*4] + imul tableSin[ecx*4] + shrd eax,edx,16 + sub ebx,eax + mov eax,bpPinAxisX3D[esi*4] + imul tableSin[ecx*4] + shrd eax,edx,16 + mov edi,eax + mov eax,bpPinAxisY3D[esi*4] + imul tableCos[ecx*4] + shrd eax,edx,16 + add eax,edi + mov bpPinAxisY3D[esi*4],eax + mov bpPinAxisX3D[esi*4],ebx + invoke NormalizeBowlingPinAxis3D,esi + mov eax,bpPinSpin3D[esi*4] + imul eax,230 + test eax,eax + jns @F + add eax,255 +@@: + sar eax,8 + mov bpPinSpin3D[esi*4],eax +bp_physics_pin_next: + bt rack,esi + jnc @F + invoke BowlingPlayTether3D,esi + invoke BowlingPlayDeckBounds3D,esi +@@: + inc esi + cmp esi,10 + jb bp_physics_pin + mov eax,bpPinX3D[40] + mov bpBallX3D,eax + mov eax,bpPinY3D[40] + mov bpBallY3D,eax + mov eax,bpPinVX3D[40] + mov bpBallVX3D,eax + mov eax,bpPinVY3D[40] + mov bpBallVY3D,eax + inc bpPhysicsTicks3D + cmp bpBallY3D,3*FP_1 + jge @F + inc bpEndTicks3D +@@: + ret +BowlingPlayStep3D ENDP + +BowlingPlayReady3D PROC uses esi + cmp bpPhysicsTicks3D,900 + jae bp_physics_ready + cmp bpEndTicks3D,100 + jb bp_physics_not_ready + xor esi,esi +bp_physics_quiet: + mov eax,bpPinVX3D[esi*4] + cdq + xor eax,edx + sub eax,edx + cmp eax,96 + ja bp_physics_not_ready + mov eax,bpPinTilt3D[esi*4] + cmp eax,128*256 + jae @F + cmp eax,24*256 + jae bp_physics_not_ready + mov eax,bpPinTiltV3D[esi*4] + cdq + xor eax,edx + sub eax,edx + cmp eax,32 + ja bp_physics_not_ready +@@: + mov eax,bpPinVY3D[esi*4] + cdq + xor eax,edx + sub eax,edx + cmp eax,96 + ja bp_physics_not_ready + inc esi + cmp esi,10 + jb bp_physics_quiet +bp_physics_ready: + mov eax,1 + ret +bp_physics_not_ready: + xor eax,eax + ret +BowlingPlayReady3D ENDP diff --git a/src_graphics/bowling_pin_shape.inc b/src_graphics/bowling_pin_shape.inc new file mode 100644 index 0000000..583e070 --- /dev/null +++ b/src_graphics/bowling_pin_shape.inc @@ -0,0 +1,296 @@ +; The existing ray/ellipsoid renderer, evaluated in a pin-local orthonormal basis. +DrawBowlingEllipsoid3D PROC uses ebx esi edi centreX:DWORD,centreY:DWORD,centreZ:DWORD,radiusX:DWORD,radiusY:DWORD,radiusZ:DWORD,material:DWORD + local leftPx:DWORD,rightPx:DWORD,topPx:DWORD,bottomPx:DWORD,column:DWORD,row:DWORD + local centrePx:DWORD,centreDepth:DWORD,pixelDepth:DWORD,temp:DWORD,texU:DWORD,texV:DWORD + local texture:DWORD,texWidth:DWORD,texHeight:DWORD,shade:DWORD,color:DWORD + local worldOX:DWORD,worldOY:DWORD,worldOZ:DWORD,worldRZ:REAL4,boundRadius:DWORD + local ox:REAL4,oy:REAL4,oz:REAL4,rayX:REAL4,rayY:REAL4,rayZ:REAL4 + local quadraticC:REAL4,quadraticA:REAL4,quadraticB:REAL4,discriminant:REAL4,hitTime:REAL4 + invoke ResolveCitySurface3D,material + test eax,eax + jz bp_shape_done + mov texture,eax + mov texWidth,ecx + mov texHeight,edx + invoke ProjectCityPoint3D,centreX,centreY + cmp edx,FP_1/8 + jle bp_shape_done + mov centrePx,eax + mov centreDepth,edx + mov eax,radiusX + cmp eax,radiusZ + jge @F + mov eax,radiusZ +@@: + mov boundRadius,eax + shl eax,1 + imul eax,SCREEN_H + cdq + idiv centreDepth + mov ecx,centrePx + sub ecx,eax + mov leftPx,ecx + add eax,centrePx + mov rightPx,eax + mov eax,viewEye3D + sub eax,centreZ + sub eax,boundRadius + imul eax,SCREEN_H + cdq + idiv centreDepth + add eax,viewHorizon3D + sub eax,4 + mov topPx,eax + mov eax,viewEye3D + sub eax,centreZ + add eax,boundRadius + imul eax,SCREEN_H + cdq + idiv centreDepth + add eax,viewHorizon3D + add eax,4 + mov bottomPx,eax + cmp leftPx,0 + jge @F + mov leftPx,0 +@@: + cmp rightPx,SCREEN_W-1 + jle @F + mov rightPx,SCREEN_W-1 +@@: + cmp topPx,0 + jge @F + mov topPx,0 +@@: + cmp bottomPx,SCREEN_H-1 + jle @F + mov bottomPx,SCREEN_H-1 +@@: + mov eax,leftPx + cmp eax,rightPx + jge bp_shape_done + mov eax,topPx + cmp eax,bottomPx + jge bp_shape_done + mov eax,playerX + sub eax,centreX + mov worldOX,eax + mov eax,playerY + sub eax,centreY + mov worldOY,eax + mov eax,viewEye3D + sub eax,centreZ + mov worldOZ,eax + fild worldOX + fimul bpRenderUX3D + fild worldOY + fimul bpRenderUY3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusX + fstp ox + fild worldOX + fimul bpRenderVX3D + fild worldOY + fimul bpRenderVY3D + faddp st(1),st + fild worldOZ + fimul bpRenderVZ3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusY + fstp oy + fild worldOX + fimul bpRenderNX3D + fild worldOY + fimul bpRenderNY3D + faddp st(1),st + fild worldOZ + fimul bpRenderNZ3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusZ + fstp oz + fld ox + fmul st,st + fld oy + fmul st,st + faddp st(1),st + fld oz + fmul st,st + faddp st(1),st + fld1 + fsubp st(1),st + fstp quadraticC + mov eax,topPx + mov row,eax +bp_shape_row: + mov eax,viewHorizon3D + sub eax,row + mov temp,eax + fild temp + fmul roofFloatFP3D + fdiv roofFloat4803D + fstp worldRZ + mov eax,row + imul eax,SCREEN_W*4 + mov ebx,leftPx + lea edi,[eax+ebx*4] + mov column,ebx +bp_shape_pixel: + mov ebx,column + fild cityRayX3D[ebx*4] + fimul bpRenderUX3D + fild cityRayY3D[ebx*4] + fimul bpRenderUY3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusX + fstp rayX + fild cityRayX3D[ebx*4] + fimul bpRenderVX3D + fild cityRayY3D[ebx*4] + fimul bpRenderVY3D + faddp st(1),st + fld worldRZ + fimul bpRenderVZ3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusY + fstp rayY + fild cityRayX3D[ebx*4] + fimul bpRenderNX3D + fild cityRayY3D[ebx*4] + fimul bpRenderNY3D + faddp st(1),st + fld worldRZ + fimul bpRenderNZ3D + faddp st(1),st + fdiv roofFloatFP3D + fidiv radiusZ + fstp rayZ + fld rayX + fmul st,st + fld rayY + fmul st,st + faddp st(1),st + fld rayZ + fmul st,st + faddp st(1),st + fstp quadraticA + fld rayX + fmul ox + fld rayY + fmul oy + faddp st(1),st + fld rayZ + fmul oz + faddp st(1),st + fstp quadraticB + fld quadraticB + fmul st,st + fld quadraticA + fmul quadraticC + fsubp st(1),st + fst discriminant + ftst + fnstsw ax + sahf + fstp st(0) + jb bp_shape_next + fld discriminant + fsqrt + fadd quadraticB + fchs + fdiv quadraticA + fst hitTime + fmul roofFloatFP3D + fistp pixelDepth + mov eax,pixelDepth + cmp eax,4096 + jle bp_shape_next + cmp eax,cityDepthBuffer3D[edi] + jae bp_shape_next + fld hitTime + fmul rayX + fadd ox + fld1 + faddp st(1),st + fmul roofFloatHalfTex3D + fistp texU + fld hitTime + fmul rayZ + fadd oz + fchs + fld1 + faddp st(1),st + fmul roofFloatHalfTex3D + fistp texV + fld hitTime + fmul rayZ + fadd oz + fmul roofFloatShade3D + fadd roofFloatAmbient3D + fistp shade + mov eax,texV + and eax,255 + mov ecx,texHeight + imul eax,ecx + shr eax,8 + imul eax,texWidth + mov edx,texU + and edx,255 + imul edx,texWidth + shr edx,8 + add eax,edx + mov esi,texture + mov eax,[esi+eax*4] + mov color,eax + movzx eax,al + imul eax,shade + shr eax,8 + mov ebx,eax + mov eax,color + shr eax,8 + and eax,255 + imul eax,shade + and eax,0FF00h + or ebx,eax + mov eax,color + shr eax,16 + and eax,255 + imul eax,shade + and eax,0FF00h + shl eax,8 + or eax,ebx + cmp roofCityPass3D,0 + je bp_shape_color_ready + invoke ShadeRoofCityPixel3D,eax,pixelDepth,256,material + mov ecx,edi + shr ecx,2 + mov edx,roofPixelGlow3D + mov roofGlowMask3D[ecx],dl +bp_shape_color_ready: + mov screenBuffer[edi],eax + cmp hallLevel3D,HALL_ROOF + jne @F + mov ecx,edi + shr ecx,2 + mov edx,roofCityPass3D + mov roofCityMask3D[ecx],dl +@@: + mov eax,pixelDepth + mov cityDepthBuffer3D[edi],eax +bp_shape_next: + add edi,4 + inc column + mov eax,column + cmp eax,rightPx + jbe bp_shape_pixel + inc row + mov eax,row + cmp eax,bottomPx + jbe bp_shape_row +bp_shape_done: + ret +DrawBowlingEllipsoid3D ENDP diff --git a/src_graphics/bowling_pinsetter.inc b/src_graphics/bowling_pinsetter.inc new file mode 100644 index 0000000..187f3c2 --- /dev/null +++ b/src_graphics/bowling_pinsetter.inc @@ -0,0 +1,204 @@ +InitBowlingSetterSurface3D PROC uses ebx esi edi + lea edi,bpSetterSurface3D + xor esi,esi +bp_setter_surface_row: + xor ebx,ebx +bp_setter_surface_pixel: + mov eax,esi + and eax,15 + mov edx,002C363Dh + test eax,eax + jnz @F + mov edx,00141B20h +@@: + cmp eax,1 + jne @F + mov edx,00414C54h +@@: + mov eax,ebx + and eax,3 + imul eax,00010101h + add eax,edx + or eax,0FF000000h + stosd + inc ebx + cmp ebx,64 + jb bp_setter_surface_pixel + inc esi + cmp esi,64 + jb bp_setter_surface_row + ret +InitBowlingSetterSurface3D ENDP + +; Smooth motor travel, clamped at both mechanical stops. +BowlingPinsetterEase3D PROC ticks:DWORD,duration:DWORD + local phase:DWORD + mov eax,ticks + test eax,eax + jle bp_ease_zero + cmp eax,duration + jae bp_ease_full + imul eax,FP_1 + cdq + idiv duration + mov phase,eax + imul eax + shrd eax,edx,16 + mov ecx,3*FP_1 + sub ecx,phase + sub ecx,phase + imul ecx + shrd eax,edx,16 + ret +bp_ease_zero: + xor eax,eax + ret +bp_ease_full: + mov eax,FP_1 + ret +BowlingPinsetterEase3D ENDP + +; Strings stay invisible; the guard closes before the hoist starts. +BowlingPinsetterGate3D PROC + cmp bpState3D,BP_RESET + jne bp_gate_open + mov eax,bpTime3D + cmp eax,4400 + jae bp_gate_open + cmp eax,400 + jb bp_gate_closing + sub eax,4000 + invoke BowlingPinsetterEase3D,eax,400 + mov ecx,FP_1*87/100 + imul ecx + shrd eax,edx,16 + add eax,FP_1*13/100 + ret +bp_gate_closing: + invoke BowlingPinsetterEase3D,eax,400 + mov ecx,FP_1*87/100 + imul ecx + shrd eax,edx,16 + neg eax + add eax,FP_1 + ret +bp_gate_open: + mov eax,FP_1 + ret +BowlingPinsetterGate3D ENDP + +; All physical pins remain present aloft, including the knocked-down ones. +BowlingPinsetterPose3D PROC uses ebx esi edi pin:DWORD + local progress:DWORD + mov esi,pin + mov edi,bpLane3D + mov ebx,bwRack3D[edi*4] + mov eax,bpPinX3D[esi*4] + mov bpVisualPinX3D,eax + mov eax,bpPinY3D[esi*4] + mov bpVisualPinY3D,eax + mov eax,bpPinTilt3D[esi*4] + mov bpVisualPinTilt3D,eax + mov bpVisualPinLift3D,0 + cmp bpState3D,BP_RESET + jne bp_setter_mask + cmp bwNewRack3D[edi*4],0 + je @F + mov ebx,1023 +@@: + mov eax,bpTime3D + cmp eax,2200 + jae bp_setter_lower + sub eax,400 + invoke BowlingPinsetterEase3D,eax,1300 + mov progress,eax + mov ecx,FP_1*11/10 + imul ecx + shrd eax,edx,16 + mov bpVisualPinLift3D,eax + mov eax,bwPinX3D[esi*4] + sub eax,bpVisualPinX3D + imul progress + shrd eax,edx,16 + add bpVisualPinX3D,eax + mov eax,bwPinY3D[esi*4] + sub eax,bpVisualPinY3D + imul progress + shrd eax,edx,16 + add bpVisualPinY3D,eax + mov eax,FP_1 + sub eax,progress + imul bpVisualPinTilt3D + shrd eax,edx,16 + mov bpVisualPinTilt3D,eax + jmp bp_setter_mask +bp_setter_lower: + cmp bwNewRack3D[edi*4],0 + jne bp_setter_new_pose + bt bpHitMask3D,esi + jnc bp_setter_new_pose + mov bpVisualPinLift3D,FP_1*11/10 + mov bpVisualPinTilt3D,0 + mov eax,bwPinX3D[esi*4] + mov bpVisualPinX3D,eax + mov eax,bwPinY3D[esi*4] + mov bpVisualPinY3D,eax + jmp bp_setter_mask +bp_setter_new_pose: + mov eax,bwPinX3D[esi*4] + mov bpVisualPinX3D,eax + mov eax,bwPinY3D[esi*4] + mov bpVisualPinY3D,eax + mov bpVisualPinTilt3D,0 + mov eax,bpTime3D + sub eax,2200 + invoke BowlingPinsetterEase3D,eax,1500 + neg eax + add eax,FP_1 + mov ecx,FP_1*11/10 + imul ecx + shrd eax,edx,16 + mov bpVisualPinLift3D,eax +bp_setter_mask: + bt ebx,esi + setc al + movzx eax,al + ret +BowlingPinsetterPose3D ENDP + +RenderBowlingPinsetter3D PROC uses ebx esi + local lo:DWORD,hi:DWORD,light:DWORD,gate:DWORD,top:DWORD + mov eax,bpCentre3D + sub eax,FP_1*82/100 + mov lo,eax + add eax,FP_1*164/100 + mov hi,eax + invoke DrawRoofSolid3D,lo,hi,FP_1*28/10,FP_1*52/10,FP_1,FP_1*16/10,254 + mov eax,lo + add eax,FP_1*6/100 + invoke DrawRoofSolid3D,lo,eax,FP_1*52/10,FP_1*53/10,0,FP_1*16/10,238 + mov eax,hi + sub eax,FP_1*6/100 + invoke DrawRoofSolid3D,eax,hi,FP_1*52/10,FP_1*53/10,0,FP_1*16/10,238 + call BowlingPinsetterGate3D + mov gate,eax + add eax,FP_1*9/10 + mov top,eax + invoke DrawRoofSolid3D,lo,hi,FP_1*53/10,FP_1*534/100,gate,top,254 + mov eax,gate + add eax,FP_1*4/100 + invoke DrawCityVertical3D,1,FP_1*535/100,lo,hi,gate,eax,238 + mov light,242 + cmp bpState3D,BP_RESET + jne @F + test bpTime3D,100h + jz @F + mov light,243 +@@: + mov eax,bpCentre3D + sub eax,FP_1/10 + mov ebx,bpCentre3D + add ebx,FP_1/10 + invoke DrawCityVertical3D,1,FP_1*536/100,eax,ebx,FP_1*12/10,FP_1*124/100,light + ret +RenderBowlingPinsetter3D ENDP diff --git a/src_graphics/bowling_play.inc b/src_graphics/bowling_play.inc index a1e6ab7..ba958f0 100644 --- a/src_graphics/bowling_play.inc +++ b/src_graphics/bowling_play.inc @@ -23,6 +23,8 @@ BeginBowlingPlay3D PROC uses esi lane:DWORD mov paused3D,0 mov bwSeat3D,0 call ResetBowlingPlay3D + mov bpState3D,BP_SELECT_BALL + call BowlingAudioOpen3D mov eax,1 ret bp_begin_no: @@ -56,6 +58,8 @@ ResetBowlingPlay3D PROC uses ebx esi edi mov bpScoreOpen3D,0 mov bpLast3D,0 mov bpLastKind3D,0 + mov bpReactionTime3D,0 + mov bpAfterReturn3D,BP_DIRECTION call BowlingPlayRack3D call ClearKeys3D mov mouseClick3D,0 @@ -77,6 +81,7 @@ BowlingPlayStartAim3D ENDP LeaveBowlingPlay3D PROC uses esi cmp bpActive3D,0 je bp_leave_done + call BowlingAudioClose3D mov esi,bpLane3D mov bwBusy3D[esi*4],0 mov bwRack3D[esi*4],1023 @@ -109,180 +114,78 @@ bp_rack_pin: mov bpPinY3D[esi*4],eax mov bpPinVX3D[esi*4],0 mov bpPinVY3D[esi*4],0 + mov bpPinTilt3D[esi*4],0 + mov bpPinTiltV3D[esi*4],0 + mov bpPinAxisX3D[esi*4],0 + mov bpPinAxisY3D[esi*4],-FP_1 + mov bpPinSpin3D[esi*4],0 + mov bpPinContactTick3D[esi*4],0 + mov bpRadius3D[esi*4],24 inc esi cmp esi,10 jb bp_rack_pin mov bpHitMask3D,0 mov bpGutter3D,0 mov bpAccumulator3D,0 - mov bpBallX3D,0 - mov bpBallY3D,FP_1*225/10 + mov bpPhysicsTicks3D,0 + mov bpEndTicks3D,0 + mov bpImpactEnergy3D,0 + mov bpImpactCooldown3D,0 + mov bpAudioReleased3D,0 + mov bpBallX3D,FP_1/2 + mov bpBallY3D,24*FP_1 ret BowlingPlayRack3D ENDP BowlingPlayLaunch3D PROC + local speed:DWORD,aimX:DWORD,aimLength:REAL8 mov bpState3D,BP_RELEASE mov bpTime3D,0 mov bpAccumulator3D,0 + call BowlingApplyBallMass3D mov eax,bpLockedPower3D imul eax,6*FP_1 cdq mov ecx,100 idiv ecx add eax,3*FP_1 - imul eax,16 - cdq - mov ecx,1000 - idiv ecx - mov ecx,eax - neg eax - mov bpBallVY3D,eax + mov speed,eax + ; The gauge fixes launch work, calibrated to the existing six-kilogram ball. + ; v = v_reference * sqrt(m_reference / m); no mass-dependent aim noise. + fild bpReferenceKg3D + fidiv bpShotWeightKg3D + fsqrt + fimul speed + fist bpLaunchSpeed3D + fimul bpTickMs3D + fidiv bpSecondMs3D + fistp speed mov eax,bpLockedAim3D - imul ecx - mov ecx,18*FP_1 - idiv ecx - mov bpBallVX3D,eax + sub eax,bpBallX3D + mov aimX,eax + fild aimX + fmul st,st + fild bpLaneDistance3D + fmul st,st + faddp st(1),st + fsqrt + fstp aimLength + fild aimX + fdiv aimLength + fimul speed + fistp bpBallVX3D + fild bpLaneDistance3D + fdiv aimLength + fimul speed + fchs + fistp bpBallVY3D ret BowlingPlayLaunch3D ENDP -BowlingPlayHit3D PROC uses esi pin:DWORD,vx:DWORD,vy:DWORD - mov esi,pin - mov ecx,esi - mov eax,1 - shl eax,cl - test bpHitMask3D,eax - jnz bp_hit_done - or bpHitMask3D,eax - mov eax,vx - mov bpPinVX3D[esi*4],eax - mov eax,vy - mov bpPinVY3D[esi*4],eax -bp_hit_done: - ret -BowlingPlayHit3D ENDP - -; 16-ms fixed steps, lane-relative 16.16 discs. No random hit selection. -BowlingPlayStep3D PROC uses ebx esi edi - local deltaX:DWORD,deltaY:DWORD,impulseX:DWORD,rackMask:DWORD - mov esi,bpLane3D - mov eax,bwRack3D[esi*4] - mov rackMask,eax - mov eax,bpBallVX3D - add bpBallX3D,eax - mov eax,bpBallVY3D - add bpBallY3D,eax - mov eax,bpBallX3D - cdq - xor eax,edx - sub eax,edx - cmp eax,FP_1*74/100 - jbe bp_step_pins - mov bpGutter3D,1 - mov eax,FP_1*87/100 - cmp bpBallX3D,0 - jge @F - neg eax -@@: - mov bpBallX3D,eax - mov bpBallVX3D,0 -bp_step_pins: - xor esi,esi -bp_step_pin: - mov ecx,esi - mov ebx,1 - shl ebx,cl - test rackMask,ebx - jz bp_step_next_pin - test bpHitMask3D,ebx - jnz bp_step_move_pin - cmp bpGutter3D,0 - jne bp_step_next_pin - mov eax,bpPinX3D[esi*4] - sub eax,bpBallX3D - mov deltaX,eax - sar eax,8 - imul eax,eax - mov edi,eax - mov eax,bpPinY3D[esi*4] - sub eax,bpBallY3D - sar eax,8 - imul eax,eax - add eax,edi - cmp eax,56*56 - ja bp_step_next_pin - mov eax,deltaX - sar eax,3 - add eax,bpBallVX3D - mov impulseX,eax - mov eax,bpBallVY3D - imul eax,3 - sar eax,2 - invoke BowlingPlayHit3D,esi,impulseX,eax -bp_step_move_pin: - mov eax,bpPinVX3D[esi*4] - add bpPinX3D[esi*4],eax - imul eax,245 - sar eax,8 - mov bpPinVX3D[esi*4],eax - mov eax,bpPinVY3D[esi*4] - add bpPinY3D[esi*4],eax - imul eax,245 - sar eax,8 - mov bpPinVY3D[esi*4],eax - xor edi,edi -bp_step_chain: - mov ecx,edi - mov ebx,1 - shl ebx,cl - test rackMask,ebx - jz bp_step_chain_next - test bpHitMask3D,ebx - jnz bp_step_chain_next - mov eax,bpPinX3D[edi*4] - sub eax,bpPinX3D[esi*4] - mov deltaX,eax - sar eax,8 - imul eax,eax - mov ebx,eax - mov eax,bpPinY3D[edi*4] - sub eax,bpPinY3D[esi*4] - mov deltaY,eax - sar eax,8 - imul eax,eax - add eax,ebx - cmp eax,49*49 - ja bp_step_chain_next - mov eax,deltaX - sar eax,8 - imul bpPinVX3D[esi*4] - mov ebx,eax - mov eax,deltaY - sar eax,8 - imul bpPinVY3D[esi*4] - add eax,ebx - jle bp_step_chain_next - mov eax,bpPinVX3D[esi*4] - sar eax,1 - mov ebx,deltaX - sar ebx,3 - add eax,ebx - mov impulseX,eax - mov eax,bpPinVY3D[esi*4] - sar eax,1 - mov ebx,deltaY - sar ebx,3 - add eax,ebx - invoke BowlingPlayHit3D,edi,impulseX,eax -bp_step_chain_next: - inc edi - cmp edi,10 - jb bp_step_chain -bp_step_next_pin: - inc esi - cmp esi,10 - jb bp_step_pin - ret -BowlingPlayStep3D ENDP +include src_graphics\bowling_physics.inc +include src_graphics\bowling_audio.inc +include src_graphics\bowling_return.inc +include src_graphics\bowling_weight.inc BowlingPlayFinishRoll3D PROC uses esi mov eax,bpHitMask3D @@ -296,6 +199,12 @@ bp_finish_count: jmp bp_finish_count bp_finish_register: mov bpLast3D,ecx + mov bpReaction3D,CAST_TALK + cmp ecx,7 + jb @F + mov bpReaction3D,CAST_CHEER +@@: + mov bpReactionTime3D,6500 mov bpLastKind3D,0 mov esi,bpLane3D cmp bwBall3D[esi*4],1 @@ -313,7 +222,17 @@ bp_finish_strike: jne bp_finish_register_score mov bpLastKind3D,1 bp_finish_register_score: - invoke BowlingRegisterRoll3D,bpLane3D,ecx + cmp bpLastKind3D,0 + je @F + mov bpReaction3D,CAST_CHEER +@@: + cmp bpReaction3D,CAST_CHEER + jne @F + cmp bpAudioReady3D,0 + je @F + invoke mciSendStringA,OFFSET bpClapPlay,0,0,0 +@@: + invoke BowlingRegisterRoll3D,bpLane3D,bpLast3D mov bpState3D,BP_SETTLE mov bpTime3D,0 mov bwMonitorClock3D,-1 @@ -356,7 +275,16 @@ bp_next_ready: BowlingPlayNext3D ENDP HandleBowlingPlay3D PROC uses ebx esi - local click:DWORD,elapsed:DWORD + local click:DWORD,elapsed:DWORD,mousePress:DWORD + mov eax,deltaTime3D + sub bpImpactCooldown3D,eax + jns @F + mov bpImpactCooldown3D,0 +@@: + sub bpReactionTime3D,eax + jns @F + mov bpReactionTime3D,0 +@@: invoke Pressed3D,9 test eax,eax jz @F @@ -364,6 +292,7 @@ HandleBowlingPlay3D PROC uses ebx esi @@: mov eax,mouseClick3D mov click,eax + mov mousePress,eax mov mouseClick3D,0 cmp click,0 je @F @@ -383,6 +312,7 @@ HandleBowlingPlay3D PROC uses ebx esi cmp click,0 je bp_handle_done call ResetBowlingPlay3D + mov bpState3D,BP_SELECT_BALL ret bp_handle_clock: mov eax,deltaTime3D @@ -393,8 +323,23 @@ bp_handle_clock: mov elapsed,eax add bpClock3D,eax add bpTime3D,eax + cmp bpState3D,BP_SELECT_BALL + jne @F + cmp mousePress,0 + jne bp_handle_rack_mouse + cmp click,0 + je bp_handle_rack_mouse + mov bpState3D,BP_DIRECTION + ret +bp_handle_rack_mouse: + invoke HandleBowlingWeight3D,mousePress + jmp bp_handle_done +@@: cmp bpState3D,BP_DIRECTION jne bp_handle_power + invoke HandleBowlingWeight3D,mousePress + test eax,eax + jnz bp_handle_done mov eax,bpClock3D xor edx,edx mov ecx,4 @@ -437,7 +382,7 @@ bp_handle_release: cmp bpState3D,BP_RELEASE jne bp_handle_roll mov esi,bpLane3D - mov ecx,350 + mov ecx,BP_RELEASE_MS cmp bwTurn3D[esi*4],0 je @F mov ecx,2400 @@ -457,25 +402,52 @@ bp_handle_fixed_step: jb bp_handle_roll_end sub bpAccumulator3D,16 call BowlingPlayStep3D - cmp bpBallY3D,3*FP_1 - jl bp_handle_impact_done + call BowlingPlayReady3D + test eax,eax + jnz bp_handle_impact_done jmp bp_handle_fixed_step bp_handle_roll_end: - cmp bpBallY3D,3*FP_1 - jl bp_handle_impact_done - cmp bpTime3D,12000 - jb bp_handle_done + jmp bp_handle_done bp_handle_impact_done: call BowlingPlayFinishRoll3D ret bp_handle_settle: cmp bpState3D,BP_SETTLE - jne bp_handle_bot - cmp bpTime3D,1800 + jne bp_handle_reset + cmp bpTime3D,1600 jb bp_handle_done - call BowlingPlayNext3D + cmp bpHitMask3D,0 + jne @F + mov esi,bpLane3D + cmp bwNewRack3D[esi*4],0 + jne @F + call CompleteBowlingRoll3D + ret +@@: + mov bpState3D,BP_RESET + mov bpTime3D,0 + cmp bpAudioReady3D,0 + je bp_handle_done + invoke mciSendStringA,OFFSET bpResetPlay,0,0,0 + jmp bp_handle_done +bp_handle_reset: + cmp bpState3D,BP_RESET + jne bp_handle_bot + cmp bpTime3D,BP_RESET_MS + jb bp_handle_done + call CompleteBowlingRoll3D ret bp_handle_bot: + cmp bpState3D,BP_RETURN + jne bp_handle_bot_ready + cmp bpTime3D,BP_RETURN_MS + jb bp_handle_done + mov eax,bpAfterReturn3D + mov bpState3D,eax + mov bpTime3D,0 + call ClearKeys3D + jmp bp_handle_done +bp_handle_bot_ready: cmp bpState3D,BP_BOT jne bp_handle_done cmp bpTime3D,1100 @@ -501,6 +473,7 @@ bp_handle_bot: mov bpLockedPower3D,eax call BowlingPlayLaunch3D bp_handle_done: + call BowlingAudioUpdate3D ret HandleBowlingPlay3D ENDP diff --git a/src_graphics/bowling_play_data.inc b/src_graphics/bowling_play_data.inc index b691499..3fcd185 100644 --- a/src_graphics/bowling_play_data.inc +++ b/src_graphics/bowling_play_data.inc @@ -7,6 +7,37 @@ BP_ROLL equ 3 BP_SETTLE equ 4 BP_BOT equ 5 BP_FINISHED equ 6 +BP_RESET equ 7 +BP_RETURN equ 8 +BP_SELECT_BALL equ 9 +BP_RELEASE_MS equ 700 +BP_RESET_MS equ 5200 +BP_RETURN_MS equ 2800 +BP_BODY_COUNT equ 11 +BP_BALL_BODY equ 10 +BP_PIN_GRAMS equ 1600 +PoolSqrt3D PROTO :DWORD +BowlingPlayContact3D PROTO :DWORD,:DWORD +BowlingPlayKick3D PROTO :DWORD,:DWORD,:DWORD +NormalizeBowlingPinAxis3D PROTO :DWORD +BowlingPlayReady3D PROTO +BowlingPlayTether3D PROTO :DWORD +BowlingPlayDeckBounds3D PROTO :DWORD +BowlingRollGlyph3D PROTO :DWORD,:DWORD,:DWORD +BowlingAudioOpen3D PROTO +BowlingAudioClose3D PROTO +BowlingAudioUpdate3D PROTO +DrawBowlingPlayerHand3D PROTO +RenderBowlingReactions3D PROTO +RenderBowlingPinsetter3D PROTO +BowlingPinsetterPose3D PROTO :DWORD +BowlingPinsetterEase3D PROTO :DWORD,:DWORD +BowlingPinsetterGate3D PROTO +CompleteBowlingRoll3D PROTO +BowlingReturnCamera3D PROTO :DWORD +BowlingReturnBallPose3D PROTO +RenderBowlingReturnMachine3D PROTO :DWORD +RenderBowlingReturnedBall3D PROTO BeginBowlingPlay3D PROTO :DWORD LeaveBowlingPlay3D PROTO ResetBowlingPlay3D PROTO @@ -23,7 +54,17 @@ DrawBowlingPlayPin3D PROTO :DWORD,:DWORD BowlingFrameTotal3D PROTO :DWORD,:DWORD DrawBowlingPlayScores3D PROTO InitBowlingBallSurface3D PROTO +InitBowlingSetterSurface3D PROTO DrawBowlingGaugeLine3D PROTO :DWORD,:DWORD,:DWORD,:DWORD +BowlingApplyBallMass3D PROTO +HandleBowlingWeight3D PROTO :DWORD +DrawBowlingWeight3D PROTO +BowlingBallMaterial3D PROTO :DWORD +BowlingRackCamera3D PROTO +RenderBowlingBallRack3D PROTO :DWORD,:DWORD +BowlingRackScreen3D PROTO :DWORD +BowlingTintBallPixel3D PROTO :DWORD,:DWORD +BowlingTintHand3D PROTO :DWORD,:DWORD .data bpActive3D dd 0 bpLane3D dd 1 @@ -35,6 +76,23 @@ bpAim3D dd 0 bpPower3D dd 50 bpLockedAim3D dd 0 bpLockedPower3D dd 50 +bpWeightKg3D dd 6 +bpBotWeightKg3D dd 6 +bpShotWeightKg3D dd 6 +bpBallGrams3D dd 6000 +bpLaunchSpeed3D dd 0 +bpReferenceKg3D dd 6 +bpLaneDistance3D dd FP_1*193/10 +bpTickMs3D dd 16 +bpSecondMs3D dd 1000 +bpRackHitX3D dd 4 dup(0) +bpRackHitY3D dd 4 dup(0) +bpRackHitRadius3D dd 4 dup(0) +bpRackHandFrame3D dd -1 +bpRackHandKg3D dd -1 +bpTintedHand3D AnimPack3D <> +bpBallMaterials3D dd 255,256,253,257 +bpBallColors3D dd 0057C78Ch,00D26B65h,005B8DE8h,00D8AB55h bpLast3D dd 0 bpLastKind3D dd 0 bpGutter3D dd 0 @@ -45,19 +103,66 @@ bpBallY3D dd 0 bpBallVX3D dd 0 bpBallVY3D dd 0 bpHitMask3D dd 0 -bpPinX3D dd 10 dup(0) -bpPinY3D dd 10 dup(0) -bpPinVX3D dd 10 dup(0) -bpPinVY3D dd 10 dup(0) +; Inverse masses are Q16, normalized to a 1600-gram pin. Last body is the ball. +bpPinX3D dd BP_BODY_COUNT dup(0) +bpPinY3D dd BP_BODY_COUNT dup(0) +bpPinVX3D dd BP_BODY_COUNT dup(0) +bpPinVY3D dd BP_BODY_COUNT dup(0) +bpInvMass3D dd 10 dup(FP_1),BP_PIN_GRAMS*FP_1/6000 +bpRadius3D dd 10 dup(24),31 +bpPinTilt3D dd 10 dup(0) +bpPinTiltV3D dd 10 dup(0) +bpPinAxisX3D dd 10 dup(0) +bpPinAxisY3D dd 10 dup(-FP_1) +bpPinSpin3D dd 10 dup(0) +bpPinContactTick3D dd 10 dup(0) +bpPhysicsTicks3D dd 0 +bpEndTicks3D dd 0 +bpImpactEnergy3D dd 0 +bpImpactCooldown3D dd 0 +bpReaction3D dd 0 +bpReactionTime3D dd 0 +bpAudioReady3D dd 0 +bpAudioRolling3D dd 0 +bpAudioReturning3D dd 0 +bpAudioReleased3D dd 0 +bpAudioVolume3D dd -1 +bpHandArt3D AnimPack3D <> +bpHandPath3D db "assets\animations\bowling\player-underarm-v2.bin",0 +bpGlyph3D db 0,0 +bpPinSectionHeight3D dd FP_1*14/100,FP_1*27/100,FP_1*285/1000,FP_1*35/100 +bpPinSectionRadius3D dd FP_1*90/1000,FP_1*37/1000,FP_1*39/1000,FP_1*55/1000 +bpPinSectionLength3D dd FP_1*145/1000,FP_1*65/1000,FP_1*16/1000,FP_1*65/1000 +bpPinSectionMaterial3D dd 239,239,240,239 +bpRenderNX3D dd 0 +bpRenderNY3D dd 0 +bpRenderNZ3D dd FP_1 +bpRenderUX3D dd FP_1 +bpRenderUY3D dd 0 +bpRenderVX3D dd 0 +bpRenderVY3D dd FP_1 +bpRenderVZ3D dd 0 +bpVisualPinX3D dd 0 +bpVisualPinY3D dd 0 +bpVisualPinTilt3D dd 0 +bpVisualPinLift3D dd 0 +bpAfterReturn3D dd BP_DIRECTION +bpReturnedX3D dd 0 +bpReturnedY3D dd 0 +bpReturnedZ3D dd 0 +bpReturnedPhase3D dd 0 +include src_graphics\bowling_audio_data.inc bpCentre3D dd 0 bpHomeX3D dd 0 bpHomeY3D dd 0 bpHomeAngle3D dd 0 bpText3D db 24 dup(0) bpMonitorTexts3D dd OFFSET bpDirectionText,OFFSET bpPowerText,OFFSET bpRollText,OFFSET bpRollText - dd OFFSET bpResultText,OFFSET bpBot,OFFSET bpScoreText + dd OFFSET bpResultText,OFFSET bpBot,OFFSET bpScoreText,OFFSET bpResetText,OFFSET bpReturnText,OFFSET bpSelectBallText .data? -bpBallSurface3D dd 64*64 dup(?) +bpBallSurface3D dd 4*64*64 dup(?) +bpTintedHandPixels3D dd 256*256 dup(?) +bpSetterSurface3D dd 64*64 dup(?) .data bpPlayPrompt db 200,195,208,192,210,220,32,194,32,193,206,211,203,200,205,195,0 bpTitle db 193,206,211,203,200,205,195,0 @@ -77,4 +182,15 @@ bpScoreText db 209,215,168,210,0 bpGutterText db 198,197,203,206,193,0 bpStrikeText db 209,210,208,192,201,202,0 bpSpareText db 209,207,221,208,0 +bpResetText db 211,209,210,192,205,206,194,202,192,0 +bpReturnText db 194,206,199,194,208,192,210,32,216,192,208,192,0 +bpKilogramsText db ' ',202,195,0 +bpKg4Text db '4 ',202,195,0 +bpKg5Text db '5 ',202,195,0 +bpKg6Text db '6 ',202,195,0 +bpKg7Text db '7 ',202,195,0 +bpWeightLabels3D dd OFFSET bpKg4Text,OFFSET bpKg5Text,OFFSET bpKg6Text,OFFSET bpKg7Text +bpSelectBallText db 194,219,193,206,208,32,216,192,208,192,0 +bpTakeBallText db 194,199,223,210,220,0 +bpChangeBallText db 216,192,208,0 .code diff --git a/src_graphics/bowling_play_render.inc b/src_graphics/bowling_play_render.inc index 812d85e..775d733 100644 --- a/src_graphics/bowling_play_render.inc +++ b/src_graphics/bowling_play_render.inc @@ -34,44 +34,97 @@ bp_ball_tex_pixel: inc esi cmp esi,64 jb bp_ball_tex_row + xor esi,esi +bp_ball_tex_variants: + mov eax,bpBallSurface3D[esi*4] + invoke BowlingTintBallPixel3D,eax,4 + mov bpBallSurface3D[esi*4+64*64*4],eax + mov eax,bpBallSurface3D[esi*4] + invoke BowlingTintBallPixel3D,eax,5 + mov bpBallSurface3D[esi*4+2*64*64*4],eax + mov eax,bpBallSurface3D[esi*4] + invoke BowlingTintBallPixel3D,eax,7 + mov bpBallSurface3D[esi*4+3*64*64*4],eax + inc esi + cmp esi,64*64 + jb bp_ball_tex_variants ret InitBowlingBallSurface3D ENDP +include src_graphics\bowling_pin_shape.inc +include src_graphics\bowling_pinsetter.inc + DrawBowlingPlayPin3D PROC uses ebx esi edi pin:DWORD,centre:DWORD - local x:DWORD,y:DWORD,z:DWORD,headY:DWORD,ry:DWORD,rz:DWORD + local baseX:DWORD,baseY:DWORD,axisX:DWORD,axisY:DWORD,axisZ:DWORD + local x:DWORD,y:DWORD,z:DWORD,radius:DWORD mov esi,pin - mov ecx,esi - mov ebx,1 - shl ebx,cl - mov edi,bpLane3D - test bwRack3D[edi*4],ebx + invoke BowlingPinsetterPose3D,esi + test eax,eax jz bp_pin_done - mov eax,bpPinX3D[esi*4] + mov eax,bpVisualPinX3D add eax,centre - mov x,eax - mov eax,bpPinY3D[esi*4] - mov y,eax - mov headY,eax - mov z,FP_1*15/100 - mov ry,FP_1/10 - mov rz,FP_1*13/100 - test bpHitMask3D,ebx - jz bp_pin_draw - mov z,FP_1*7/100 - mov ry,FP_1*23/100 - mov rz,FP_1*6/100 - sub headY,FP_1/4 -bp_pin_draw: - invoke DrawRoofEllipsoid3D,x,y,z,FP_1/10,ry,rz,239 - mov eax,FP_1*34/100 - test bpHitMask3D,ebx - jz @F - mov eax,FP_1*7/100 + mov baseX,eax + mov eax,bpVisualPinY3D + mov baseY,eax + mov eax,bpVisualPinTilt3D + shr eax,8 + cmp eax,128 + jbe @F + mov eax,128 @@: - invoke DrawRoofEllipsoid3D,x,headY,eax,FP_1*65/1000,FP_1*65/1000,FP_1*6/100,239 - mov eax,z - add eax,rz - invoke DrawRoofEllipsoid3D,x,headY,eax,FP_1*45/1000,FP_1*45/1000,FP_1*25/1000,240 + mov edi,tableSin[eax*4] + mov ecx,edi + neg ecx + mov bpRenderVZ3D,ecx + mov eax,tableCos[eax*4] + mov axisZ,eax + mov bpRenderNZ3D,eax + imul bpPinAxisX3D[esi*4] + shrd eax,edx,16 + mov bpRenderVX3D,eax + mov eax,axisZ + imul bpPinAxisY3D[esi*4] + shrd eax,edx,16 + mov bpRenderVY3D,eax + mov eax,bpPinAxisY3D[esi*4] + mov bpRenderUX3D,eax + mov eax,bpPinAxisX3D[esi*4] + neg eax + mov bpRenderUY3D,eax + mov eax,bpPinAxisX3D[esi*4] + imul edi + shrd eax,edx,16 + mov axisX,eax + mov bpRenderNX3D,eax + mov eax,bpPinAxisY3D[esi*4] + imul edi + shrd eax,edx,16 + mov axisY,eax + mov bpRenderNY3D,eax + xor ebx,ebx +bp_pin_section: + mov eax,bpPinSectionHeight3D[ebx*4] + imul axisX + shrd eax,edx,16 + add eax,baseX + mov x,eax + mov eax,bpPinSectionHeight3D[ebx*4] + imul axisY + shrd eax,edx,16 + add eax,baseY + mov y,eax + mov eax,bpPinSectionHeight3D[ebx*4] + imul axisZ + shrd eax,edx,16 + add eax,FP_1*8/100 + add eax,bpVisualPinLift3D + mov z,eax + mov eax,bpPinSectionRadius3D[ebx*4] + mov radius,eax + invoke DrawBowlingEllipsoid3D,x,y,z,radius,radius,bpPinSectionLength3D[ebx*4],bpPinSectionMaterial3D[ebx*4] + inc ebx + cmp ebx,4 + jb bp_pin_section bp_pin_done: ret DrawBowlingPlayPin3D ENDP @@ -126,59 +179,7 @@ bp_frame_pending: ret BowlingFrameTotal3D ENDP -DrawBowlingPlayScores3D PROC uses ebx esi edi - local x:DWORD,y:DWORD,owner:DWORD - invoke DrawBoxBorder,4,47,312,139,00151F26h,005BC9BEh,1 - invoke DrawString8x8,12,55,OFFSET bpScoreText,00E8D7ADh - xor esi,esi -bp_score_heading: - imul eax,esi,26 - add eax,44 - mov x,eax - lea eax,[esi+1] - invoke FormatNumber,eax,OFFSET bpText3D - invoke DrawString8x8,x,73,OFFSET bpText3D,00AABDB6h - inc esi - cmp esi,10 - jb bp_score_heading - xor edi,edi -bp_score_owner: - lea eax,[edi*2] - imul eax,18 - add eax,96 - mov y,eax - mov eax,OFFSET bpYou - test edi,edi - jz @F - mov eax,OFFSET bpBot -@@: - invoke DrawString8x8,9,y,eax,005BC9BEh - mov eax,bpLane3D - shl eax,1 - add eax,edi - mov owner,eax - xor esi,esi -bp_score_cell: - imul eax,esi,26 - add eax,44 - mov x,eax - invoke BowlingFrameTotal3D,owner,esi - cmp eax,-1 - je bp_score_empty - invoke FormatNumber,eax,OFFSET bpText3D - invoke DrawString8x8,x,y,OFFSET bpText3D,00FFFFFFh - jmp bp_score_next -bp_score_empty: - invoke DrawBox,x,y,8,1,005C727Bh -bp_score_next: - inc esi - cmp esi,10 - jb bp_score_cell - inc edi - cmp edi,2 - jb bp_score_owner - ret -DrawBowlingPlayScores3D ENDP +include src_graphics\bowling_scorecard.inc DrawBowlingGaugeLine3D PROC uses ebx esi edi x0:DWORD,y0:DWORD,x1:DWORD,y1:DWORD local px:DWORD,py:DWORD,stepX:DWORD,stepY:DWORD @@ -235,6 +236,8 @@ bp_gauge_line: ret DrawBowlingGaugeLine3D ENDP +include src_graphics\bowling_performance.inc + RenderBowlingPlay3D PROC uses ebx esi edi local x:DWORD,y:DWORD,color:DWORD,owner:DWORD,startY:DWORD mov eax,bpCentre3D @@ -252,6 +255,8 @@ RenderBowlingPlay3D PROC uses ebx esi edi cmp bpState3D,BP_ROLL je bp_camera_follow cmp bpState3D,BP_SETTLE + je bp_camera_follow + cmp bpState3D,BP_RESET jne bp_camera_ready bp_camera_follow: mov eax,bpBallY3D @@ -262,6 +267,40 @@ bp_camera_follow: @@: mov playerY,eax mov viewEye3D,FP_1*7/10 + cmp bpState3D,BP_RESET + jne bp_camera_roll_blend + mov ebx,bpTime3D + cmp ebx,400 + jbe @F + mov ebx,400 +@@: + imul eax,ebx,2*FP_1 + cdq + mov ecx,400 + idiv ecx + add playerY,eax + mov eax,playerY + mov ebx,bpTime3D + sub ebx,BP_RESET_MS-800 + jle bp_camera_ready + cmp ebx,800 + jbe @F + mov ebx,800 +@@: + mov edx,startY + sub edx,eax + mov eax,edx + imul eax,ebx + cdq + mov ecx,800 + idiv ecx + add playerY,eax + imul eax,ebx,HALL_STANDING_EYE-FP_1*7/10 + cdq + idiv ecx + add viewEye3D,eax + jmp bp_camera_ready +bp_camera_roll_blend: cmp bpState3D,BP_ROLL jne bp_camera_ready mov ebx,bpTime3D @@ -274,15 +313,48 @@ bp_camera_follow: idiv ecx add eax,startY mov playerY,eax - imul eax,ebx,FP_1*7/20 + mov edx,HALL_STANDING_EYE + cmp bwTurn3D[esi*4],0 + jne @F + mov edx,FP_1*55/100 +@@: + mov x,edx + mov eax,FP_1*7/10 + sub eax,edx + imul eax,ebx cdq idiv ecx + add eax,x + mov viewEye3D,eax +bp_camera_ready: + cmp bpState3D,BP_SELECT_BALL + jne @F + call BowlingRackCamera3D +@@: + cmp bpState3D,BP_RETURN + jne @F + invoke BowlingReturnCamera3D,startY +@@: + cmp bpState3D,BP_RELEASE + jne @F + cmp bwTurn3D[esi*4],0 + jne @F + mov eax,bpTime3D + cmp eax,BP_RELEASE_MS + jbe bp_camera_release_time + mov eax,BP_RELEASE_MS +bp_camera_release_time: + imul eax,FP_1/2 + cdq + mov ecx,BP_RELEASE_MS + idiv ecx mov edx,HALL_STANDING_EYE sub edx,eax mov viewEye3D,edx -bp_camera_ready: +@@: call UpdateCamera3D call RenderBowling3D + call RenderBowlingPinsetter3D mov esi,bpLane3D cmp bwTurn3D[esi*4],1 jne bp_render_ball @@ -306,12 +378,31 @@ bp_render_bot_draw: mov bwActor3D.posY,FP_1*235/10 invoke DrawCityObject3D,OFFSET bwActor3D,0 bp_render_ball: + call RenderBowlingReactions3D + cmp bpState3D,BP_SELECT_BALL + je bp_render_hud cmp bpState3D,BP_FINISHED je bp_render_hud + cmp bpState3D,BP_ROLL + je bp_render_real_ball + cmp bpState3D,BP_SETTLE + je bp_render_real_ball + cmp bpState3D,BP_RESET + je bp_render_hud + cmp bpState3D,BP_RETURN + je bp_render_hud + cmp bpHandArt3D.pixels,0 + jne bp_render_hud +bp_render_real_ball: + cmp bpEndTicks3D,25 + jae bp_render_hud mov eax,bpBallX3D add eax,bpCentre3D - invoke DrawBowlingBall3D,eax,bpBallY3D,253 + mov x,eax + invoke BowlingBallMaterial3D,bpShotWeightKg3D + invoke DrawBowlingBall3D,x,bpBallY3D,eax bp_render_hud: + call DrawBowlingPlayerHand3D invoke DrawBox,0,0,320,17,00121B20h invoke DrawString8x8,8,4,OFFSET bpTitle,00E8D7ADh mov eax,bpLane3D @@ -338,7 +429,10 @@ bp_render_hud: mov ebx,owner mov eax,bwFrames3D[ebx*4] cmp bpState3D,BP_SETTLE + je bp_frame_result + cmp bpState3D,BP_RESET jne @F +bp_frame_result: cmp bwFrameDone3D[esi*4],0 je @F dec eax @@ -353,12 +447,24 @@ bp_render_hud: invoke DrawString8x8,116,227,OFFSET bpText3D,00FFFFFFh cmp bpState3D,BP_DIRECTION je bp_render_direction + cmp bpState3D,BP_SELECT_BALL + je bp_render_done cmp bpState3D,BP_POWER je bp_render_power cmp bpState3D,BP_SETTLE je bp_render_result cmp bpState3D,BP_FINISHED je bp_render_finished + cmp bpState3D,BP_RESET + jne @F + invoke DrawString8x8,104,197,OFFSET bpResetText,00E8D7ADh + jmp bp_render_done +@@: + cmp bpState3D,BP_RETURN + jne @F + invoke DrawString8x8,104,197,OFFSET bpReturnText,00E8D7ADh + jmp bp_render_done +@@: invoke DrawString8x8,8,204,OFFSET bpRollText,00E8D7ADh jmp bp_render_done bp_render_direction: @@ -403,11 +509,11 @@ bp_render_arc: invoke DrawBowlingGaugeLine3D,eax,162,ebx,168 jmp bp_render_done bp_render_power: - invoke DrawString8x8,246,62,OFFSET bpPowerText,00E8D7ADh - invoke DrawBoxBorder,276,79,13,124,00121B20h,00AABDB6h,1 - invoke DrawBox,278,82,9,118,00D67057h - invoke DrawBox,278,128,9,38,00E7C455h - invoke DrawBox,278,82,9,46,005BC98Ch + invoke DrawString8x8,16,62,OFFSET bpPowerText,00E8D7ADh + invoke DrawBoxBorder,30,79,13,124,00121B20h,00AABDB6h,1 + invoke DrawBox,32,82,9,118,00D67057h + invoke DrawBox,32,128,9,38,00E7C455h + invoke DrawBox,32,82,9,46,005BC98Ch mov eax,bpPower3D imul eax,116 xor edx,edx @@ -416,9 +522,9 @@ bp_render_power: mov edx,198 sub edx,eax mov y,edx - invoke DrawBox,270,y,25,2,00FFFFFFh + invoke DrawBox,24,y,25,2,00FFFFFFh invoke FormatNumber,bpPower3D,OFFSET bpText3D - invoke DrawString8x8,246,180,OFFSET bpText3D,00FFFFFFh + invoke DrawString8x8,50,180,OFFSET bpText3D,00FFFFFFh jmp bp_render_done bp_render_result: mov eax,OFFSET bpResultText @@ -451,6 +557,7 @@ bp_render_finished: invoke DrawString8x8,88,161,ebx,00E8D7ADh invoke DrawString8x8,136,187,OFFSET bpAgainText,00FFFFFFh bp_render_done: + call DrawBowlingWeight3D cmp bpScoreOpen3D,0 je @F call DrawBowlingPlayScores3D diff --git a/src_graphics/bowling_render.inc b/src_graphics/bowling_render.inc index 0d5f196..efbb364 100644 --- a/src_graphics/bowling_render.inc +++ b/src_graphics/bowling_render.inc @@ -158,14 +158,25 @@ bw_planes_done: RenderBowlingPlanes3D ENDP DrawBowlingBall3D PROC uses ebx esi x:DWORD,y:DWORD,mat:DWORD - local holeY:DWORD,holeZ:DWORD,phase:DWORD - invoke DrawRoofEllipsoid3D,x,y,FP_1*135/1000,FP_1*12/100,FP_1*12/100,FP_1*12/100,mat - cmp mat,253 - jne bw_ball_done mov eax,y sar eax,8 and eax,511 - mov phase,eax + invoke DrawBowlingBallAt3D,x,y,FP_1*135/1000,mat,eax + ret +DrawBowlingBall3D ENDP + +DrawBowlingBallAt3D PROC uses ebx esi x:DWORD,y:DWORD,z:DWORD,mat:DWORD,phase:DWORD + local holeY:DWORD,holeZ:DWORD + invoke DrawRoofEllipsoid3D,x,y,z,FP_1*12/100,FP_1*12/100,FP_1*12/100,mat + cmp mat,253 + je bw_ball_holes + cmp mat,255 + jb bw_ball_done + cmp mat,257 + ja bw_ball_done +bw_ball_holes: + mov eax,phase + and eax,511 mov eax,tableCos[eax*4] test eax,eax jle bw_ball_done @@ -180,7 +191,7 @@ DrawBowlingBall3D PROC uses ebx esi x:DWORD,y:DWORD,mat:DWORD imul eax,11 cdq idiv ecx - add eax,FP_1*135/1000 + add eax,z mov holeZ,eax mov eax,x sub eax,FP_1*35/1000 @@ -193,7 +204,7 @@ DrawBowlingBall3D PROC uses ebx esi x:DWORD,y:DWORD,mat:DWORD invoke DrawRoofEllipsoid3D,x,holeY,eax,FP_1*18/1000,FP_1*18/1000,FP_1*18/1000,233 bw_ball_done: ret -DrawBowlingBall3D ENDP +DrawBowlingBallAt3D ENDP DrawBowlingPin3D PROC uses ebx esi edi lane:DWORD,pin:DWORD,centre:DWORD local x:DWORD,y:DWORD,t:DWORD,z:DWORD,ry:DWORD,rz:DWORD,headY:DWORD,headZ:DWORD @@ -528,8 +539,8 @@ bw_cast_dining: ; The match opponent leaves this restaurant seat instead of appearing twice. cmp bpActive3D,0 je @F - test esi,esi - jz bw_cast_dining_next + cmp esi,3 + jb bw_cast_dining_next @@: mov eax,bwVisitorX3D[esi*4] mov bwSmallActor3D.posX,eax @@ -602,32 +613,8 @@ bw_detail_return: add eax,FP_1*64/100 mov x1,eax invoke DrawCityVertical3D,1,26*FP_1+512,x0,x1,FP_1/20,FP_1*62/100,241 - xor edi,edi -bw_detail_rack_ball: - mov eax,centerX - imul edx,edi,FP_1*23/100 - add eax,edx - sub eax,FP_1*23/100 - mov ebx,242 - test edi,1 - jz @F - mov ebx,243 -@@: - invoke DrawRoofEllipsoid3D,eax,FP_1*254/10,FP_1*84/100,FP_1*11/100,FP_1*11/100,FP_1*11/100,ebx - mov eax,centerX - imul edx,edi,FP_1*23/100 - add eax,edx - sub eax,FP_1*23/100 - sub eax,FP_1*35/1000 - invoke DrawRoofEllipsoid3D,eax,FP_1*25494/1000,FP_1*905/1000,FP_1*14/1000,FP_1*14/1000,FP_1*14/1000,233 - mov eax,centerX - imul edx,edi,FP_1*23/100 - add eax,edx - sub eax,FP_1*195/1000 - invoke DrawRoofEllipsoid3D,eax,FP_1*25494/1000,FP_1*905/1000,FP_1*14/1000,FP_1*14/1000,FP_1*14/1000,233 - inc edi - cmp edi,3 - jb bw_detail_rack_ball + invoke RenderBowlingReturnMachine3D,centerX + invoke RenderBowlingBallRack3D,centerX,esi mov eax,centerX sub eax,FP_1*27/100 invoke DrawRoofEllipsoid3D,eax,27*FP_1,FP_1*49/100,FP_1*16/100,FP_1*16/100,FP_1*2/100,239 @@ -637,6 +624,7 @@ bw_detail_rack_ball: inc esi cmp esi,4 jb bw_detail_return + call RenderBowlingReturnedBall3D xor esi,esi bw_detail_dining: mov eax,esi diff --git a/src_graphics/bowling_return.inc b/src_graphics/bowling_return.inc new file mode 100644 index 0000000..5a64936 --- /dev/null +++ b/src_graphics/bowling_return.inc @@ -0,0 +1,145 @@ +; Score/turn advancement stays independent of the visible ball-return cycle. +CompleteBowlingRoll3D PROC + call BowlingPlayNext3D + mov eax,bpState3D + mov bpAfterReturn3D,eax + mov bpState3D,BP_RETURN + mov bpTime3D,0 + ret +CompleteBowlingRoll3D ENDP + +BowlingReturnCamera3D PROC uses ebx esi startY:DWORD + local progress:DWORD + mov eax,bpTime3D + cmp eax,2000 + jb bp_return_camera_turn + mov eax,BP_RETURN_MS + sub eax,bpTime3D + invoke BowlingPinsetterEase3D,eax,800 + jmp bp_return_camera_pose +bp_return_camera_turn: + invoke BowlingPinsetterEase3D,eax,400 +bp_return_camera_pose: + mov progress,eax + mov ebx,FP_1*268/10 + sub ebx,startY + imul ebx + shrd eax,edx,16 + add eax,startY + mov playerY,eax + mov eax,progress + imul eax,64 + sar eax,16 + mov esi,bpLane3D + test esi,1 + jz @F + neg eax +@@: + add eax,384 + mov playerAngle,eax + mov viewEye3D,HALL_STANDING_EYE + ret +BowlingReturnCamera3D ENDP + +; Position belongs to the return tray, not to the pin collision solver. +BowlingReturnBallPose3D PROC uses esi + cmp bpState3D,BP_RETURN + jne bp_return_ball_hidden + mov eax,bpTime3D + cmp eax,400 + jb bp_return_ball_hidden + cmp eax,2000 + jae bp_return_ball_hidden + mov esi,bpLane3D + shr esi,1 + imul eax,esi,FP_1*38/10 + add eax,FP_1*395/100 + mov bpReturnedX3D,eax + mov eax,bpTime3D + sub eax,400 + cmp eax,900 + jae bp_return_ball_coast + invoke BowlingPinsetterEase3D,eax,900 + mov esi,eax + mov ecx,FP_1/2 + imul ecx + shrd eax,edx,16 + add eax,FP_1*251/10 + mov bpReturnedY3D,eax + mov eax,esi + mov ecx,FP_1*22/100 + imul ecx + shrd eax,edx,16 + add eax,FP_1*62/100 + mov bpReturnedZ3D,eax + jmp bp_return_ball_phase +bp_return_ball_coast: + sub eax,900 + invoke BowlingPinsetterEase3D,eax,400 + mov ecx,FP_1*55/100 + imul ecx + shrd eax,edx,16 + add eax,FP_1*256/10 + mov bpReturnedY3D,eax + mov bpReturnedZ3D,FP_1*84/100 +bp_return_ball_phase: + mov eax,bpReturnedY3D + sub eax,FP_1*251/10 + imul eax,680 + sar eax,16 + and eax,511 + mov bpReturnedPhase3D,eax + mov eax,1 + ret +bp_return_ball_hidden: + xor eax,eax + ret +BowlingReturnBallPose3D ENDP + +RenderBowlingReturnMachine3D PROC uses ebx esi centre:DWORD + local lo:DWORD,hi:DWORD + mov eax,centre + sub eax,FP_1*33/100 + mov lo,eax + add eax,FP_1*66/100 + mov hi,eax + invoke DrawRoofSolid3D,lo,hi,FP_1*248/10,FP_1*253/10,FP_1*68/100,FP_1,254 + invoke DrawRoofEllipsoid3D,centre,FP_1*2495/100,FP_1*94/100,FP_1*33/100,FP_1*32/100,FP_1*18/100,254 + mov eax,centre + sub eax,FP_1*14/100 + mov ebx,centre + add ebx,FP_1*14/100 + invoke DrawCityVertical3D,1,FP_1*2531/100,eax,ebx,FP_1*70/100,FP_1*95/100,233 + mov esi,-1 +bp_return_rail: + imul eax,esi,FP_1/10 + add eax,centre + mov ebx,eax + sub eax,FP_1*2/100 + add ebx,FP_1*2/100 + invoke DrawRoofSolid3D,eax,ebx,FP_1*253/10,FP_1*2635/100,FP_1*70/100,FP_1*75/100,238 + add esi,2 + cmp esi,1 + jle bp_return_rail + mov eax,centre + sub eax,FP_1*21/100 + mov ebx,centre + add ebx,FP_1*21/100 + mov lo,eax + mov hi,ebx + invoke DrawRoofSolid3D,lo,hi,FP_1*2635/100,FP_1*264/10,FP_1*70/100,FP_1*79/100,254 + invoke DrawRoofSolid3D,lo,hi,FP_1*262/10,FP_1*2628/100,FP_1*6/100,FP_1*70/100,254 + ret +RenderBowlingReturnMachine3D ENDP + +RenderBowlingReturnedBall3D PROC + cmp bpActive3D,0 + je bp_return_draw_done + call BowlingReturnBallPose3D + test eax,eax + jz bp_return_draw_done + invoke BowlingBallMaterial3D,bpShotWeightKg3D + invoke DrawBowlingBallAt3D,bpReturnedX3D,bpReturnedY3D,bpReturnedZ3D,eax,bpReturnedPhase3D +bp_return_draw_done: + ret +RenderBowlingReturnedBall3D ENDP diff --git a/src_graphics/bowling_scorecard.inc b/src_graphics/bowling_scorecard.inc new file mode 100644 index 0000000..902bee8 --- /dev/null +++ b/src_graphics/bowling_scorecard.inc @@ -0,0 +1,164 @@ +; Return the actual roll offset of a frame, including incomplete frames. +BowlingFrameIndex3D PROC uses ebx esi edi owner:DWORD,wanted:DWORD + mov ebx,owner + imul eax,ebx,96 + lea esi,bwRolls3D[eax] + xor edi,edi + xor ecx,ecx +bp_index_frame: + cmp ecx,wanted + je bp_index_ready + cmp edi,bwRollCount3D[ebx*4] + jae bp_index_missing + cmp DWORD PTR [esi+edi*4],10 + je @F + inc edi +@@: + inc edi + inc ecx + jmp bp_index_frame +bp_index_ready: + mov eax,edi + ret +bp_index_missing: + mov eax,-1 + ret +BowlingFrameIndex3D ENDP + +BowlingRollGlyph3D PROC uses ebx esi edi owner:DWORD,frame:DWORD,slot:DWORD + invoke BowlingFrameIndex3D,owner,frame + cmp eax,-1 + je bp_glyph_blank + mov edi,eax + mov ebx,owner + imul eax,ebx,96 + lea esi,bwRolls3D[eax] + mov ecx,slot + cmp frame,9 + je bp_glyph_tenth + cmp ecx,2 + jae bp_glyph_blank + cmp DWORD PTR [esi+edi*4],10 + jne bp_glyph_available + test ecx,ecx + jnz bp_glyph_blank + jmp bp_glyph_available +bp_glyph_tenth: + cmp ecx,3 + jae bp_glyph_blank + cmp ecx,2 + jne bp_glyph_available + mov eax,[esi+edi*4] + cmp eax,10 + je bp_glyph_available + add eax,[esi+edi*4+4] + cmp eax,10 + jne bp_glyph_blank +bp_glyph_available: + add edi,ecx + cmp edi,bwRollCount3D[ebx*4] + jae bp_glyph_blank + mov eax,[esi+edi*4] + test ecx,ecx + jz bp_glyph_digit + cmp ecx,1 + jne bp_glyph_bonus_spare + cmp DWORD PTR [esi+edi*4-4],10 + je bp_glyph_digit + jmp bp_glyph_spare +bp_glyph_bonus_spare: + cmp DWORD PTR [esi+edi*4-8],10 + jne bp_glyph_digit + cmp DWORD PTR [esi+edi*4-4],10 + je bp_glyph_digit +bp_glyph_spare: + mov edx,eax + add edx,[esi+edi*4-4] + cmp edx,10 + jne bp_glyph_digit + mov eax,'/' + ret +bp_glyph_digit: + cmp eax,10 + je bp_glyph_strike + test eax,eax + jz bp_glyph_miss + add eax,'0' + ret +bp_glyph_miss: + mov eax,'-' + ret +bp_glyph_strike: + mov eax,'X' + ret +bp_glyph_blank: + mov eax,' ' + ret +BowlingRollGlyph3D ENDP + +DrawBowlingPlayScores3D PROC uses ebx esi edi + local x:DWORD,y:DWORD,owner:DWORD,slot:DWORD + invoke DrawBoxBorder,4,47,312,160,00151F26h,005BC9BEh,1 + invoke DrawString8x8,12,55,OFFSET bpScoreText,00E8D7ADh + xor esi,esi +bp_card_header: + imul eax,esi,26 + add eax,44 + mov x,eax + invoke DrawBox,x,88,1,108,00425D64h + lea eax,[esi+1] + invoke FormatNumber,eax,OFFSET bpText3D + invoke DrawString8x8,x,73,OFFSET bpText3D,00AABDB6h + inc esi + cmp esi,10 + jb bp_card_header + invoke DrawBox,6,137,308,1,005BC9BEh + xor edi,edi +bp_card_player: + imul eax,edi,54 + add eax,95 + mov y,eax + mov eax,OFFSET bpYou + test edi,edi + jz @F + mov eax,OFFSET bpBot +@@: + invoke DrawString8x8,8,y,eax,005BC9BEh + mov eax,bpLane3D + shl eax,1 + add eax,edi + mov owner,eax + xor esi,esi +bp_card_frame: + mov slot,0 +bp_card_roll: + invoke BowlingRollGlyph3D,owner,esi,slot + mov bpGlyph3D,al + imul eax,esi,26 + add eax,45 + mov ecx,slot + lea eax,[eax+ecx*8] + mov x,eax + invoke DrawString8x8,x,y,OFFSET bpGlyph3D,00E8D7ADh + inc slot + cmp slot,3 + jb bp_card_roll + imul eax,esi,26 + add eax,45 + mov x,eax + invoke BowlingFrameTotal3D,owner,esi + cmp eax,-1 + je bp_card_next + invoke FormatNumber,eax,OFFSET bpText3D + mov eax,y + add eax,22 + invoke DrawString8x8,x,eax,OFFSET bpText3D,00FFFFFFh +bp_card_next: + inc esi + cmp esi,10 + jb bp_card_frame + inc edi + cmp edi,2 + jb bp_card_player + ret +DrawBowlingPlayScores3D ENDP diff --git a/src_graphics/bowling_weight.inc b/src_graphics/bowling_weight.inc new file mode 100644 index 0000000..3c5c59a --- /dev/null +++ b/src_graphics/bowling_weight.inc @@ -0,0 +1,388 @@ +BowlingApplyBallMass3D PROC uses esi + mov esi,bpLane3D + mov eax,bpWeightKg3D + cmp bwTurn3D[esi*4],0 + je @F + mov eax,bpBotWeightKg3D +@@: + cmp eax,4 + jae @F + mov eax,4 +@@: + cmp eax,7 + jbe @F + mov eax,7 +@@: + mov bpShotWeightKg3D,eax + imul eax,1000 + mov bpBallGrams3D,eax + mov ecx,eax + mov eax,BP_PIN_GRAMS*FP_1 + xor edx,edx + div ecx + mov bpInvMass3D[BP_BALL_BODY*4],eax + ret +BowlingApplyBallMass3D ENDP + +; Selection consumes its own event, never the direction-lock/throw event. +HandleBowlingWeight3D PROC uses ebx esi mousePress:DWORD + xor esi,esi +bp_weight_key: + lea eax,[esi+31h] + invoke Pressed3D,eax + test eax,eax + jnz bp_weight_choose + inc esi + cmp esi,4 + jb bp_weight_key + cmp mousePress,0 + je bp_weight_no + cmp bpState3D,BP_SELECT_BALL + je bp_weight_rack_hit + cmp mouseY3D,26 + jb bp_weight_no + cmp mouseY3D,44 + jae bp_weight_no + cmp mouseX3D,8 + jb bp_weight_no + cmp mouseX3D,96 + jae bp_weight_no + mov bpState3D,BP_SELECT_BALL + mov eax,1 + ret +bp_weight_rack_hit: + call BowlingRackCamera3D + call UpdateCamera3D + xor esi,esi +bp_weight_hit: + invoke BowlingRackScreen3D,esi + mov eax,mouseX3D + sub eax,bpRackHitX3D[esi*4] + imul eax,eax + mov ebx,eax + mov eax,mouseY3D + sub eax,bpRackHitY3D[esi*4] + imul eax,eax + add ebx,eax + mov eax,bpRackHitRadius3D[esi*4] + add eax,2 + imul eax,eax + cmp ebx,eax + jbe bp_weight_choose + mov eax,bpRackHitX3D[esi*4] + sub eax,18 + cmp mouseX3D,eax + jb bp_weight_hit_next + add eax,36 + cmp mouseX3D,eax + jae bp_weight_hit_next + mov eax,bpRackHitY3D[esi*4] + add eax,bpRackHitRadius3D[esi*4] + add eax,2 + cmp mouseY3D,eax + jb bp_weight_hit_next + add eax,14 + cmp mouseY3D,eax + jb bp_weight_choose +bp_weight_hit_next: + inc esi + cmp esi,4 + jb bp_weight_hit + ; Ignore clicks on the floor instead of accidentally locking the direction. + cmp mouseY3D,193 + jb bp_weight_rack_consumed + cmp mouseY3D,213 + jae bp_weight_rack_consumed + cmp mouseX3D,128 + jb bp_weight_rack_consumed + cmp mouseX3D,192 + jae bp_weight_rack_consumed + mov bpState3D,BP_DIRECTION +bp_weight_rack_consumed: + mov eax,1 + ret +bp_weight_no: + xor eax,eax + ret +bp_weight_choose: + lea eax,[esi+4] + mov bpWeightKg3D,eax + mov bpState3D,BP_DIRECTION + mov eax,1 + ret +HandleBowlingWeight3D ENDP + +DrawBowlingWeight3D PROC uses ebx esi edi + local x:DWORD,y:DWORD,edge:DWORD + cmp bpScoreOpen3D,0 + jne bp_weight_draw_done + cmp bpState3D,BP_SELECT_BALL + jne bp_weight_locked_label + invoke DrawString8x8,120,32,OFFSET bpSelectBallText,00E8D7ADh + xor esi,esi +bp_weight_rack_label: + invoke BowlingRackScreen3D,esi + mov eax,bpRackHitX3D[esi*4] + sub eax,18 + mov x,eax + mov eax,bpRackHitY3D[esi*4] + add eax,bpRackHitRadius3D[esi*4] + add eax,2 + mov y,eax + mov edge,003C4D53h + lea eax,[esi+4] + cmp eax,bpWeightKg3D + jne @F + mov eax,bpBallColors3D[esi*4] + mov edge,eax +@@: + invoke DrawBoxBorder,x,y,36,14,00121B20h,edge,1 + mov eax,x + add eax,2 + mov ebx,y + add ebx,3 + invoke DrawString8x8,eax,ebx,bpWeightLabels3D[esi*4],00FFFFFFh + inc esi + cmp esi,4 + jb bp_weight_rack_label + invoke DrawBoxBorder,128,193,64,20,00121B20h,005BC9BEh,1 + invoke DrawString8x8,136,199,OFFSET bpTakeBallText,00FFFFFFh + ret +bp_weight_locked_label: + mov eax,bpShotWeightKg3D + cmp bpState3D,BP_DIRECTION + je bp_weight_selected_label + cmp bpState3D,BP_POWER + jne @F +bp_weight_selected_label: + mov eax,bpWeightKg3D +@@: + cmp bpState3D,BP_BOT + jne @F + mov eax,bpBotWeightKg3D +@@: + mov esi,eax + invoke FormatNumber,eax,OFFSET bpText3D + invoke AppendCString,OFFSET bpText3D,OFFSET bpKilogramsText + sub esi,4 + cmp esi,3 + ja bp_weight_draw_done + mov eax,bpBallColors3D[esi*4] + mov edge,eax + cmp bpState3D,BP_DIRECTION + jne bp_weight_static_label + invoke DrawBoxBorder,8,26,88,18,00121B20h,edge,1 + invoke DrawString8x8,14,31,OFFSET bpChangeBallText,00E8D7ADh + invoke DrawString8x8,50,31,OFFSET bpText3D,00FFFFFFh + ret +bp_weight_static_label: + invoke DrawBox,8,27,5,7,edge + invoke DrawString8x8,18,26,OFFSET bpText3D,00E8D7ADh +bp_weight_draw_done: + ret +DrawBowlingWeight3D ENDP + +BowlingBallMaterial3D PROC kg:DWORD + mov eax,kg + sub eax,4 + cmp eax,3 + jbe @F + mov eax,2 +@@: + mov eax,bpBallMaterials3D[eax*4] + ret +BowlingBallMaterial3D ENDP + +BowlingRackCamera3D PROC + mov eax,bpLane3D + shr eax,1 + imul eax,FP_1*38/10 + add eax,FP_1*585/100 + mov playerX,eax + mov playerY,FP_1*2583/100 + mov playerAngle,256 + mov viewEye3D,FP_1*102/100 + ret +BowlingRackCamera3D ENDP + +; Labels and picking share the actual world projection, not guessed UI slots. +BowlingRackScreen3D PROC uses ebx esi index:DWORD + mov esi,index + imul ebx,esi,-FP_1*245/1000 + add ebx,FP_1*262/10 + mov eax,playerX + sub eax,FP_1*135/100 + invoke ProjectCityPoint3D,eax,ebx + sar eax,1 + mov bpRackHitX3D[esi*4],eax + mov ebx,edx + mov eax,viewEye3D + sub eax,FP_1*84/100 + imul eax,SCREEN_H + cdq + idiv ebx + add eax,viewHorizon3D + sar eax,1 + mov bpRackHitY3D[esi*4],eax + mov eax,FP_1*12/100*SCREEN_H + cdq + idiv ebx + shr eax,1 + mov bpRackHitRadius3D[esi*4],eax + ret +BowlingRackScreen3D ENDP + +RenderBowlingBallRack3D PROC uses ebx esi edi centre:DWORD,pairIndex:DWORD + local lo:DWORD,hi:DWORD,x:DWORD,y:DWORD + ; Storage runs lengthwise, alongside (not across) the clear return track. + mov eax,centre + add eax,FP_1*43/100 + mov lo,eax + add eax,FP_1*4/100 + mov hi,eax + invoke DrawRoofSolid3D,lo,hi,FP_1*253/10,FP_1*264/10,FP_1*70/100,FP_1*75/100,238 + add lo,FP_1*20/100 + add hi,FP_1*20/100 + invoke DrawRoofSolid3D,lo,hi,FP_1*253/10,FP_1*264/10,FP_1*70/100,FP_1*75/100,238 + mov eax,centre + add eax,FP_1*35/100 + mov lo,eax + add eax,FP_1*40/100 + mov hi,eax + invoke DrawRoofSolid3D,lo,hi,FP_1*253/10,FP_1*2538/100,FP_1*6/100,FP_1*73/100,254 + invoke DrawRoofSolid3D,lo,hi,FP_1*2632/100,FP_1*264/10,FP_1*6/100,FP_1*73/100,254 + xor esi,esi +bp_rack_stock: + cmp bpActive3D,0 + je bp_rack_stock_draw + mov eax,bpLane3D + shr eax,1 + cmp eax,pairIndex + jne bp_rack_stock_draw + cmp bpState3D,BP_SELECT_BALL + je bp_rack_stock_draw + mov eax,bpWeightKg3D + cmp bpState3D,BP_DIRECTION + je @F + cmp bpState3D,BP_POWER + je @F + mov eax,bpShotWeightKg3D +@@: + sub eax,4 + cmp eax,esi + je bp_rack_stock_next +bp_rack_stock_draw: + mov eax,centre + add eax,FP_1*55/100 + mov x,eax + imul eax,esi,-FP_1*245/1000 + add eax,FP_1*262/10 + mov y,eax + invoke DrawBowlingBallAt3D,x,y,FP_1*84/100,bpBallMaterials3D[esi*4],45 +bp_rack_stock_next: + inc esi + cmp esi,4 + jb bp_rack_stock + ret +RenderBowlingBallRack3D ENDP + +; Reuse the blue marble's luminance; only its hue changes. Preserve alpha. +BowlingTintBallPixel3D PROC uses ebx ecx pixel:DWORD,kg:DWORD + mov eax,pixel + cmp kg,6 + je bp_tint_done + mov ebx,eax + and ebx,255 + mov ecx,eax + shr ecx,16 + and ecx,255 + and eax,0FF00FF00h + cmp kg,4 + jne @F + mov edx,pixel + shr edx,8 + and edx,255 + shl ebx,8 + and eax,0FFFF00FFh + or eax,ebx + or eax,edx + shl ecx,16 + or eax,ecx + ret +@@: + or eax,ecx + cmp kg,5 + jne bp_tint_gold + mov edx,eax + and edx,0000FF00h + shr edx,1 + and edx,0000FF00h + and eax,0FFFF00FFh + or eax,edx +bp_tint_gold: + cmp kg,7 + jne @F + mov edx,pixel + shr edx,8 + and edx,255 + add edx,ebx + shr edx,1 + shl edx,8 + and eax,0FFFF00FFh + or eax,edx +@@: + shl ebx,16 + or eax,ebx +bp_tint_done: + ret +BowlingTintBallPixel3D ENDP + +; A cached single-frame scratch pack keeps skin, sleeve and source assets intact. +BowlingTintHand3D PROC uses ebx esi edi frame:DWORD,kg:DWORD + mov eax,frame + cmp eax,bpRackHandFrame3D + jne bp_tint_hand_build + mov eax,kg + cmp eax,bpRackHandKg3D + je bp_tint_hand_done +bp_tint_hand_build: + mov eax,frame + mov bpRackHandFrame3D,eax + shl eax,18 + add eax,bpHandArt3D.pixels + mov esi,eax + mov eax,kg + mov bpRackHandKg3D,eax + lea edi,bpTintedHandPixels3D + mov ebx,256*256 +bp_tint_hand_pixel: + lodsd + cmp kg,6 + je bp_tint_hand_store + mov edx,eax + and edx,255 + mov ecx,eax + shr ecx,8 + and ecx,255 + add ecx,24 + cmp edx,ecx + jbe bp_tint_hand_store + mov ecx,eax + shr ecx,16 + and ecx,255 + add ecx,24 + cmp edx,ecx + jbe bp_tint_hand_store + invoke BowlingTintBallPixel3D,eax,kg +bp_tint_hand_store: + stosd + dec ebx + jnz bp_tint_hand_pixel + mov bpTintedHand3D.imageWidth,256 + mov bpTintedHand3D.height,256 + mov bpTintedHand3D.frames,1 + mov bpTintedHand3D.bytes,256*256*4 + mov bpTintedHand3D.pixels,OFFSET bpTintedHandPixels3D +bp_tint_hand_done: + ret +BowlingTintHand3D ENDP diff --git a/src_graphics/npc_life.inc b/src_graphics/npc_life.inc index 7e6fca4..c1ef1d9 100644 --- a/src_graphics/npc_life.inc +++ b/src_graphics/npc_life.inc @@ -5,13 +5,14 @@ InitNpcLife3D PROC uses ebx esi edi mov npcSelected3D,-1 npc_life_init: imul ebx,esi,SIZEOF NpcLife3D - mov npcLife3D[ebx].identity,esi + mov eax,npcInitialIdentity3D[esi*4] + mov npcLife3D[ebx].identity,eax mov npcLife3D[ebx].state,NPC_STAND mov eax,esi imul eax,900 add eax,1800 mov npcLife3D[ebx].remaining,eax - mov npcLife3D[ebx].routeStep,esi + mov npcLife3D[ebx].routeStep,0 mov npcLife3D[ebx].pathCount,0 mov npcLife3D[ebx].remainder,0 mov npcLife3D[ebx].poseClock,0 @@ -19,30 +20,29 @@ npc_life_init: mov npcWalkVelocityX3D[esi*4],0 mov npcWalkVelocityY3D[esi*4],0 mov npcRise3D[esi*4],0 + mov npcSitTime3D[esi*4],0 mov eax,npcStartDest3D[esi*4] mov npcLife3D[ebx].destination,eax - imul edi,esi,SIZEOF NpcMindEntity + push eax + invoke NpcEntity3D,esi + mov edi,eax + pop eax mov ecx,npcDestX3D[eax*4] - mov casinoNpcs3D[edi].posX,ecx + mov [edi].NpcMindEntity.posX,ecx mov ecx,npcDestY3D[eax*4] - mov casinoNpcs3D[edi].posY,ecx - cmp eax,7 - je @F - cmp eax,8 - jne npc_life_init_next -@@: - mov npcLife3D[ebx].state,NPC_SIT - mov npcLife3D[ebx].poseClock,0 + mov [edi].NpcMindEntity.posY,ecx npc_life_init_next: inc esi cmp esi,NPC_LIFE_COUNT jb npc_life_init + call InitNpcVisits3D mov npcLifeReady3D,1 ret InitNpcLife3D ENDP ; BFS visits each tile at most once. Body clearance includes furniture and the stage. -NpcTryCell3D PROC uses ebx esi cell:DWORD,parent:DWORD +NpcTryCell3D PROC uses ebx esi edi cell:DWORD,parent:DWORD + local startX:DWORD,startY:DWORD,endX:DWORD,endY:DWORD,probeX:DWORD mov esi,cell cmp esi,NPC_PATH_CAP jae npc_cell_done @@ -56,9 +56,41 @@ NpcTryCell3D PROC uses ebx esi cell:DWORD,parent:DWORD shr edx,5 shl edx,16 add edx,FP_HALF + mov endX,eax + mov endY,edx invoke CanWalk3D,eax,edx test eax,eax jz npc_cell_done + ; Free tile centres do not imply a free edge beside a chair or thin divider. + mov eax,parent + mov edx,eax + and eax,31 + shl eax,16 + add eax,FP_HALF + mov startX,eax + shr edx,5 + shl edx,16 + add edx,FP_HALF + mov startY,edx + mov edi,1 +npc_cell_edge: + mov eax,endX + sub eax,startX + imul eax,edi + sar eax,2 + add eax,startX + mov probeX,eax + mov eax,endY + sub eax,startY + imul eax,edi + sar eax,2 + add eax,startY + invoke CanWalk3D,probeX,eax + test eax,eax + jz npc_cell_done + inc edi + cmp edi,4 + jb npc_cell_edge mov eax,parent mov npcParents3D[esi*4],eax mov ebx,npcSearchTail3D @@ -78,11 +110,12 @@ NpcPlanPath3D PROC uses ebx esi edi slot:DWORD,destination:DWORD jae npc_plan_fail imul ebx,slot,SIZEOF NpcLife3D mov npcLife3D[ebx].pathCount,0 - imul esi,slot,SIZEOF NpcMindEntity - mov eax,casinoNpcs3D[esi].posY + invoke NpcEntity3D,slot + mov esi,eax + mov eax,[esi].NpcMindEntity.posY sar eax,16 shl eax,5 - mov ecx,casinoNpcs3D[esi].posX + mov ecx,[esi].NpcMindEntity.posX sar ecx,16 add eax,ecx cmp eax,NPC_PATH_CAP @@ -190,16 +223,17 @@ npc_space_person: imul ebx,esi,SIZEOF NpcLife3D cmp npcLife3D[ebx].state,NPC_AWAY je npc_space_next - imul ebx,esi,SIZEOF NpcMindEntity + invoke NpcEntity3D,esi + mov ebx,eax mov eax,px - sub eax,casinoNpcs3D[ebx].posX + sub eax,[ebx].NpcMindEntity.posX jns @F neg eax @@: cmp eax,FP_1*2/5 jae npc_space_next mov eax,py - sub eax,casinoNpcs3D[ebx].posY + sub eax,[ebx].NpcMindEntity.posY jns @F neg eax @@: @@ -222,8 +256,8 @@ NpcMove3D PROC uses ebx esi edi slot:DWORD,milliseconds:DWORD local budget:DWORD,wx:DWORD,wy:DWORD,mind:DWORD,pathBase:DWORD local oldX:DWORD,oldY:DWORD imul ebx,slot,SIZEOF NpcLife3D - imul eax,slot,SIZEOF NpcMindEntity - lea esi,casinoNpcs3D[eax] + invoke NpcEntity3D,slot + mov esi,eax mov mind,esi mov eax,[esi].NpcMindEntity.posX mov oldX,eax @@ -234,7 +268,8 @@ NpcMove3D PROC uses ebx esi edi slot:DWORD,milliseconds:DWORD add eax,OFFSET npcPaths3D mov pathBase,eax mov eax,milliseconds - mov ecx,55000 + mov ecx,slot + mov ecx,npcVisitSpeed3D[ecx*4] mul ecx add eax,npcLife3D[ebx].remainder adc edx,0 @@ -338,28 +373,26 @@ npc_move_y: cmp npcLife3D[ebx].pathCount,0 jne npc_move_done npc_move_arrived: - mov eax,npcLife3D[ebx].destination - mov npcLife3D[ebx].state,NPC_STAND - mov npcLife3D[ebx].remaining,3500 - cmp eax,0 - jne @F - mov npcLife3D[ebx].state,NPC_AWAY - mov npcLife3D[ebx].remaining,4500 - jmp npc_move_done -@@: - cmp eax,7 - je @F - cmp eax,8 - jne npc_move_done -@@: - mov npcLife3D[ebx].state,NPC_SIT - mov npcLife3D[ebx].remaining,12000 - mov npcLife3D[ebx].poseClock,0 + invoke NpcArrive3D,slot jmp npc_move_done npc_move_blocked: + mov eax,slot + add npcVisitBlocked3D[eax*4],250 + cmp npcVisitBlocked3D[eax*4],2000 + jae npc_move_abandon + mov npcLife3D[ebx].state,NPC_STAND + mov npcVisitActivity3D[eax*4],NV_YIELD + imul eax,23 + add eax,200 + mov npcLife3D[ebx].remaining,eax + jmp npc_move_done +npc_move_abandon: + invoke NpcReleaseStation3D,slot mov npcLife3D[ebx].state,NPC_STAND mov npcLife3D[ebx].remaining,1000 mov npcLife3D[ebx].pathCount,0 + mov eax,slot + mov npcVisitActivity3D[eax*4],NV_IDLE npc_move_done: ; Advance gait by successful displacement only, including partial blocked steps. mov esi,mind @@ -372,6 +405,7 @@ npc_move_done: mov edi,eax or eax,ecx jz npc_move_no_stride + mov npcVisitBlocked3D[ebx*4],0 mov npcWalkVelocityX3D[ebx*4],ecx mov npcWalkVelocityY3D[ebx*4],edi mov eax,ecx @@ -389,16 +423,25 @@ npc_move_done: mov ecx,FP_1*3/4 div ecx mov npcWalkDistance3D[ebx*4],edx + imul eax,ebx,SIZEOF NpcLife3D + cmp npcLife3D[eax].state,NPC_WALK + jne npc_move_no_stride + ret npc_move_no_stride: + mov eax,slot + mov npcWalkVelocityX3D[eax*4],0 + mov npcWalkVelocityY3D[eax*4],0 ret NpcMove3D ENDP UpdateNpcLife3D PROC uses ebx esi edi - local milliseconds:DWORD,destination:DWORD + local milliseconds:DWORD cmp npcLifeReady3D,1 jne npc_update_done cmp hallLevel3D,0 jne npc_update_done + cmp engineMode,MODE_WALK + jne npc_update_done mov eax,deltaTime3D cmp eax,250 jbe @F @@ -411,6 +454,20 @@ npc_update_person: imul ebx,esi,SIZEOF NpcLife3D mov eax,milliseconds add npcLife3D[ebx].poseClock,eax + sub npcRemarkTime3D[esi*4],eax + jns @F + mov npcRemarkTime3D[esi*4],0 +@@: + cmp npcLife3D[ebx].state,NPC_SIT + jne npc_update_seat_ready + mov ecx,npcSitTime3D[esi*4] + add ecx,eax + cmp ecx,800 + jbe @F + mov ecx,800 +@@: + mov npcSitTime3D[esi*4],ecx +npc_update_seat_ready: cmp npcLife3D[ebx].state,NPC_WALK jne npc_update_wait cmp npcRise3D[esi*4],0 @@ -425,51 +482,23 @@ npc_update_move: npc_update_wait: sub npcLife3D[ebx].remaining,eax ja npc_update_sync - cmp npcLife3D[ebx].state,NPC_AWAY - jne npc_update_choose - mov eax,npcLife3D[ebx].identity - add eax,6 - cmp eax,NPC_IDENTITY_COUNT - jb @F - sub eax,NPC_IDENTITY_COUNT -@@: - mov npcLife3D[ebx].identity,eax - mov npcLife3D[ebx].state,NPC_STAND -npc_update_choose: - mov eax,npcLife3D[ebx].routeStep - xor edx,edx - mov ecx,NPC_DEST_COUNT - div ecx - mov eax,npcVisitRoute3D[edx*4] - mov destination,eax - inc npcLife3D[ebx].routeStep - mov npcRise3D[esi*4],0 - cmp npcLife3D[ebx].state,NPC_SIT - jne @F - mov npcRise3D[esi*4],800 -@@: - invoke NpcPlanPath3D,esi,destination - test eax,eax - jz npc_update_retry - mov npcLife3D[ebx].state,NPC_WALK - mov npcLife3D[ebx].poseClock,0 - jmp npc_update_sync -npc_update_retry: - mov npcRise3D[esi*4],0 - mov npcLife3D[ebx].state,NPC_STAND - mov npcLife3D[ebx].remaining,1000 + invoke NpcAdvanceVisit3D,esi npc_update_sync: - imul edi,esi,SIZEOF NpcMindEntity + invoke NpcEntity3D,esi + mov edi,eax mov eax,npcLife3D[ebx].state - mov casinoNpcs3D[edi].state,eax + mov [edi].NpcMindEntity.state,eax mov eax,npcLife3D[ebx].remaining - mov casinoNpcs3D[edi].stateTimer,eax + mov [edi].NpcMindEntity.stateTimer,eax ; Keep the old public position array coherent for existing world checks. + cmp esi,6 + jae npc_update_sync_next imul ecx,esi,SIZEOF CityProp3D - mov eax,casinoNpcs3D[edi].posX + mov eax,[edi].NpcMindEntity.posX mov hallGuests3D[ecx].posX,eax - mov eax,casinoNpcs3D[edi].posY + mov eax,[edi].NpcMindEntity.posY mov hallGuests3D[ecx].posY,eax +npc_update_sync_next: inc esi cmp esi,NPC_LIFE_COUNT jb npc_update_person @@ -483,8 +512,9 @@ RenderNpcLife3D PROC uses ebx esi edi jne npc_draw_done xor esi,esi npc_draw_depth: - imul ebx,esi,SIZEOF NpcMindEntity - invoke ProjectCityPoint3D,casinoNpcs3D[ebx].posX,casinoNpcs3D[ebx].posY + invoke NpcEntity3D,esi + mov ebx,eax + invoke ProjectCityPoint3D,[ebx].NpcMindEntity.posX,[ebx].NpcMindEntity.posY mov npcDrawDepth3D[esi*4],edx mov npcDrawOrder3D[esi*4],esi inc esi @@ -514,24 +544,29 @@ npc_draw_person: imul ebx,eax,SIZEOF NpcLife3D cmp npcLife3D[ebx].state,NPC_AWAY je npc_draw_next - imul edi,eax,SIZEOF NpcMindEntity - mov eax,casinoNpcs3D[edi].posX + invoke NpcEntity3D,eax + mov edi,eax + mov eax,[edi].NpcMindEntity.posX mov npcSprite3D.posX,eax - mov eax,casinoNpcs3D[edi].posY + mov eax,[edi].NpcMindEntity.posY mov npcSprite3D.posY,eax mov eax,npcLife3D[ebx].identity mov eax,npcGuestAppearance3D[eax*4] mov castId,eax - mov castAction,CAST_TALK + mov castAction,CAST_IDLE mov eax,npcLife3D[ebx].poseClock mov castClock,eax mov eax,npcDrawOrder3D[esi*4] mov slot,eax + invoke NpcResolveVisitPose3D,eax + mov castAction,eax cmp npcLife3D[ebx].state,NPC_SIT jne npc_draw_walk - mov castAction,CAST_SEAT_TALK - cmp castClock,800 + mov eax,slot + mov eax,npcSitTime3D[eax*4] + cmp eax,800 jae npc_draw_sprite + mov castClock,eax mov castAction,CAST_SIT_DOWN jmp npc_draw_sprite npc_draw_walk: @@ -546,6 +581,9 @@ npc_draw_walk: mov castClock,eax jmp npc_draw_sprite npc_draw_moving: + mov eax,npcWalkVelocityX3D[ebx*4] + or eax,npcWalkVelocityY3D[ebx*4] + jz npc_draw_sprite mov castAction,CAST_WALK mov eax,npcWalkDistance3D[ebx*4] mov castClock,eax @@ -553,6 +591,7 @@ npc_draw_sprite: mov ebx,slot invoke ConfigureCharacter3D,OFFSET npcSprite3D,castId,castAction,castClock,npcWalkVelocityX3D[ebx*4],npcWalkVelocityY3D[ebx*4] invoke DrawCityObject3D,OFFSET npcSprite3D,0 + invoke NpcDrawRemark3D,slot npc_draw_next: inc esi cmp esi,NPC_LIFE_COUNT @@ -576,14 +615,15 @@ npc_interact_person: imul ebx,esi,SIZEOF NpcLife3D cmp npcLife3D[ebx].state,NPC_AWAY je npc_interact_next - imul edi,esi,SIZEOF NpcMindEntity - mov eax,casinoNpcs3D[edi].posX + invoke NpcEntity3D,esi + mov edi,eax + mov eax,[edi].NpcMindEntity.posX sub eax,playerX sar eax,8 mov deltaX,eax imul eax,eax mov distance,eax - mov eax,casinoNpcs3D[edi].posY + mov eax,[edi].NpcMindEntity.posY sub eax,playerY sar eax,8 mov deltaY,eax @@ -640,3 +680,5 @@ npc_interact_next: npc_interact_done: ret CheckNpcInteractions3D ENDP + +include src_graphics\npc_visits.inc diff --git a/src_graphics/npc_life_data.inc b/src_graphics/npc_life_data.inc index 19beb5d..788651f 100644 --- a/src_graphics/npc_life_data.inc +++ b/src_graphics/npc_life_data.inc @@ -3,9 +3,9 @@ IFDEF CASINO_ACTION_TEST DeleteFileA PROTO :DWORD ENDIF INTERACT_NPC equ 40 -NPC_LIFE_COUNT equ 6 +NPC_LIFE_COUNT equ 10 NPC_IDENTITY_COUNT equ 12 -NPC_DEST_COUNT equ 10 +NPC_DEST_COUNT equ 12 NPC_STAND equ 0 NPC_WALK equ 1 NPC_SIT equ 2 @@ -23,6 +23,16 @@ NpcPlanPath3D PROTO :DWORD,:DWORD NpcTryCell3D PROTO :DWORD,:DWORD NpcMove3D PROTO :DWORD,:DWORD NpcCanStep3D PROTO :DWORD,:DWORD,:DWORD +NpcEntity3D PROTO :DWORD +InitNpcVisits3D PROTO +NpcArrive3D PROTO :DWORD +NpcChooseVisit3D PROTO :DWORD +NpcAdvanceVisit3D PROTO :DWORD +NpcResolveVisitPose3D PROTO :DWORD +NpcSeatOffset3D PROTO :DWORD +NpcVisitRandom3D PROTO :DWORD,:DWORD +NpcReleaseStation3D PROTO :DWORD +NpcDrawRemark3D PROTO :DWORD NpcRecordRound3D PROTO :DWORD,:DWORD NpcEnsureJournal3D PROTO NpcSaveJournal3D PROTO @@ -43,6 +53,9 @@ NpcLife3D STRUCT NpcLife3D ENDS .data npcLife3D NpcLife3D NPC_LIFE_COUNT dup(<>) +; The original six poker-mind records retain their layout and profile range. +npcExtraActors3D NpcMindEntity 4 dup(<>) +npcInitialIdentity3D dd 0,1,2,3,4,5,6,7,9,10 npcLifeReady3D dd 0 npcGuestArt3D AnimPack3D <> npcGuestPath3D db "assets\animations\hall\guest-cast-v1.bin",0 @@ -55,6 +68,7 @@ npcWalkVelocityX3D dd NPC_LIFE_COUNT dup(0) npcWalkVelocityY3D dd NPC_LIFE_COUNT dup(0) ; Keep the twelve-slot historical journal, but resolve every appearance to the shared ten. npcRise3D dd NPC_LIFE_COUNT dup(0) +npcSitTime3D dd NPC_LIFE_COUNT dup(0) npcGuestAppearance3D dd 0,2,4,1,3,5,6,8,0,7,9,2 npcGuestNames3D dd OFFSET npcName0,OFFSET npcName1,OFFSET npcName2,OFFSET npcGuestAnna3D dd OFFSET npcGuestMarina3D,OFFSET npcGuestVera3D,OFFSET npcName6,OFFSET npcName7 @@ -71,11 +85,12 @@ npcDialogError3D dd 0 npcNameBuffer3D db 64 dup(0) npcJournalIoError3D dd 0 npcDestX3D dd 16*FP_1+FP_HALF,16*FP_1+FP_HALF,10*FP_1+FP_HALF,6*FP_1+FP_HALF,13*FP_1+FP_HALF - dd 21*FP_1+FP_HALF,7*FP_1+FP_HALF,6*FP_1+FP_HALF,16*FP_1+FP_HALF,16*FP_1+FP_HALF + dd 21*FP_1+FP_HALF,7*FP_1+FP_HALF,13*FP_1+FP_HALF,16*FP_1+FP_HALF,19*FP_1+FP_HALF + dd 6*FP_1+FP_HALF,15*FP_1+FP_HALF npcDestY3D dd 28*FP_1+FP_HALF,17*FP_1+FP_HALF,13*FP_1+FP_HALF,14*FP_1+FP_HALF,9*FP_1+FP_HALF - dd 10*FP_1+FP_HALF,25*FP_1+FP_HALF,27*FP_1+FP_HALF,10*FP_1+FP_HALF,7*FP_1+FP_HALF -npcVisitRoute3D dd 1,5,8,2,3,6,7,9,1,0 -npcStartDest3D dd 1,2,3,6,8,7 + dd 10*FP_1+FP_HALF,25*FP_1+FP_HALF,19*FP_1+FP_HALF,10*FP_1+FP_HALF,10*FP_1+FP_HALF + dd 25*FP_1+FP_HALF,19*FP_1+FP_HALF +npcStartDest3D dd 1,2,3,6,8,7,4,5,9,11 npcSprite3D CityProp3D <0,0,FP_1*3/5,FP_1*6/5,0,OFFSET hallPatrons3D> npcDrawOrder3D dd NPC_LIFE_COUNT dup(0) npcDrawDepth3D dd NPC_LIFE_COUNT dup(0) @@ -91,7 +106,7 @@ npcJournalBasePtr3D dd 0 ENDIF npcDateOverride3D dd 0 npcSystemTime3D dw 8 dup(0) -; One bounded journal per profile; visitors 0..5 and 6..11 alternate at the entrance. +; Retain the twelve historical journal entries for existing saves and contracts. npcJournal3D LABEL DWORD npcJournalMagic3D dd 3151504Eh npcJournalDay3D dd 0 @@ -157,4 +172,5 @@ npcQueue3D dd NPC_PATH_CAP dup(?) npcSearchHead3D dd 0 npcSearchTail3D dd 0 npcSearchGoal3D dd 0 +include src_graphics\npc_visits_data.inc .code diff --git a/src_graphics/npc_visits.inc b/src_graphics/npc_visits.inc new file mode 100644 index 0000000..b35a1c6 --- /dev/null +++ b/src_graphics/npc_visits.inc @@ -0,0 +1,511 @@ +; Four additional visitors do not expand the six legacy poker personality records. +NpcEntity3D PROC slot:DWORD + mov eax,slot + cmp eax,6 + jae nv_entity_extra + imul eax,SIZEOF NpcMindEntity + add eax,OFFSET casinoNpcs3D + ret +nv_entity_extra: + sub eax,6 + imul eax,SIZEOF NpcMindEntity + add eax,OFFSET npcExtraActors3D + ret +NpcEntity3D ENDP + +NpcVisitRandom3D PROC slot:DWORD,limit:DWORD + mov ecx,slot + mov eax,npcVisitRng3D[ecx*4] + imul eax,1664525 + add eax,1013904223 + mov npcVisitRng3D[ecx*4],eax + shr eax,8 + xor edx,edx + div limit + mov eax,edx + ret +NpcVisitRandom3D ENDP + +InitNpcVisits3D PROC uses ebx esi edi + lea edi,npcStationOwner3D + mov ecx,NPC_DEST_COUNT + mov eax,-1 + rep stosd + xor esi,esi +nv_init_person: + imul ebx,esi,SIZEOF NpcLife3D + imul eax,esi,7919 + add eax,071C4A931h + mov npcVisitRng3D[esi*4],eax + mov eax,npcVisitInitialChips3D[esi*4] + mov npcVisitChips3D[esi*4],eax + mov npcVisitRounds3D[esi*4],0 + mov npcVisitThirst3D[esi*4],0 + mov npcVisitMood3D[esi*4],0 + mov npcVisitBlocked3D[esi*4],0 + mov npcRemark3D[esi*4],0 + mov npcRemarkTime3D[esi*4],0 + mov eax,npcLife3D[ebx].destination + mov npcStationOwner3D[eax*4],esi + invoke NpcArrive3D,esi + ; Different initial waits prevent a synchronized crowd surge. + imul eax,esi,1100 + add npcLife3D[ebx].remaining,eax + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_init_person + ret +InitNpcVisits3D ENDP + +NpcReleaseStation3D PROC uses esi slot:DWORD + xor esi,esi +nv_release_station: + mov eax,slot + cmp npcStationOwner3D[esi*4],eax + jne @F + mov npcStationOwner3D[esi*4],-1 +@@: + inc esi + cmp esi,NPC_DEST_COUNT + jb nv_release_station + ret +NpcReleaseStation3D ENDP + +NpcArrive3D PROC uses ebx esi slot:DWORD + mov esi,slot + imul ebx,esi,SIZEOF NpcLife3D + mov npcLife3D[ebx].state,NPC_STAND + mov npcLife3D[ebx].poseClock,0 + mov npcSitTime3D[esi*4],0 + mov npcRemark3D[esi*4],0 + mov npcRemarkTime3D[esi*4],0 + mov npcVisitBlocked3D[esi*4],0 + mov npcVisitRounds3D[esi*4],0 + mov eax,npcLife3D[ebx].destination + mov ecx,npcStationKind3D[eax*4] + mov npcVisitActivity3D[esi*4],ecx + cmp ecx,NV_LEAVE + jne @F + invoke NpcReleaseStation3D,esi + mov npcLife3D[ebx].state,NPC_AWAY + mov npcLife3D[ebx].remaining,8000 + ret +@@: + cmp ecx,NV_ORDER + jne @F + mov npcLife3D[ebx].remaining,2400 + mov npcRemark3D[esi*4],OFFSET npcRemarkOrder3D + mov npcRemarkTime3D[esi*4],2200 + ret +@@: + cmp ecx,NV_REST + jne @F + mov npcLife3D[ebx].state,NPC_SIT + invoke NpcVisitRandom3D,esi,9000 + add eax,14000 + mov npcLife3D[ebx].remaining,eax + ret +@@: + cmp ecx,NV_WATCH + jne @F + invoke NpcVisitRandom3D,esi,10000 + add eax,12000 + mov npcLife3D[ebx].remaining,eax + ret +@@: + invoke NpcVisitRandom3D,esi,1800 + add eax,2200 + mov npcLife3D[ebx].remaining,eax + ret +NpcArrive3D ENDP + +; Reserve a free destination before walking; neither a failed path nor a blocked +; walker keeps a permanent claim on furniture. Casino RNG and bankroll are unused. +NpcChooseVisit3D PROC uses ebx esi edi slot:DWORD + local candidate:DWORD,wasSeated:DWORD,attempt:DWORD + mov esi,slot + imul ebx,esi,SIZEOF NpcLife3D + mov eax,npcLife3D[ebx].state + mov wasSeated,eax + mov attempt,0 +nv_choose_candidate: + cmp npcVisitChips3D[esi*4],20 + jae nv_choose_thirst + mov candidate,0 + jmp nv_choose_free +nv_choose_thirst: + cmp npcVisitMood3D[esi*4],-2 + jle nv_choose_break + cmp npcVisitThirst3D[esi*4],3 + jb nv_choose_random +nv_choose_break: + mov eax,6 + test attempt,1 + jz @F + mov eax,10 +@@: + mov candidate,eax + cmp attempt,2 + jb nv_choose_free +nv_choose_random: + invoke NpcVisitRandom3D,esi,100 + cmp eax,45 + jae nv_choose_bar + mov eax,npcVisitFavourite3D[esi*4] + mov candidate,eax + cmp attempt,0 + je nv_choose_free + invoke NpcVisitRandom3D,esi,5 + inc eax + mov candidate,eax + jmp nv_choose_free +nv_choose_bar: + cmp eax,65 + jae nv_choose_rest + invoke NpcVisitRandom3D,esi,2 + imul eax,4 + add eax,6 + mov candidate,eax + jmp nv_choose_free +nv_choose_rest: + cmp eax,83 + jae nv_choose_watch + invoke NpcVisitRandom3D,esi,2 + imul eax,4 + add eax,7 + mov candidate,eax + jmp nv_choose_free +nv_choose_watch: + invoke NpcVisitRandom3D,esi,2 + add eax,8 + mov candidate,eax +nv_choose_free: + mov edi,candidate + cmp edi,npcLife3D[ebx].destination + je nv_choose_next + cmp npcStationOwner3D[edi*4],-1 + jne nv_choose_next + invoke NpcPlanPath3D,esi,edi + test eax,eax + jz nv_choose_next + invoke NpcReleaseStation3D,esi + mov npcStationOwner3D[edi*4],esi + mov npcLife3D[ebx].state,NPC_WALK + mov npcLife3D[ebx].poseClock,0 + mov npcVisitActivity3D[esi*4],NV_IDLE + mov npcRise3D[esi*4],0 + cmp wasSeated,NPC_SIT + jne @F + mov npcRise3D[esi*4],800 +@@: + inc npcLife3D[ebx].routeStep + mov eax,1 + ret +nv_choose_next: + inc attempt + cmp attempt,16 + jb nv_choose_candidate + ; Keep a seated visitor seated when all useful destinations are busy. + mov eax,wasSeated + cmp eax,NPC_AWAY + jne @F + mov eax,NPC_STAND +@@: + mov npcLife3D[ebx].state,eax + mov npcLife3D[ebx].remaining,1800 + xor eax,eax + ret +NpcChooseVisit3D ENDP + +NpcAdvanceVisit3D PROC uses ebx esi edi slot:DWORD + mov esi,slot + imul ebx,esi,SIZEOF NpcLife3D + mov eax,npcVisitActivity3D[esi*4] + cmp eax,NV_YIELD + jne @F + mov npcLife3D[ebx].state,NPC_WALK + mov npcLife3D[ebx].poseClock,0 + ret +@@: + cmp eax,NV_GAME + je nv_play_result + cmp eax,NV_WIN + je nv_after_result + cmp eax,NV_LOSS + je nv_after_result + cmp eax,NV_ORDER + je nv_order_ready + cmp eax,NV_DRINK + je nv_social + cmp eax,NV_REST + je nv_social + cmp eax,NV_LEAVE + jne @F + mov eax,npcVisitInitialChips3D[esi*4] + mov npcVisitChips3D[esi*4],eax + mov npcVisitMood3D[esi*4],0 + mov npcVisitThirst3D[esi*4],0 +@@: + invoke NpcChooseVisit3D,esi + ret +nv_play_result: + mov edi,npcVisitStake3D[esi*4] + invoke NpcVisitRandom3D,esi,100 + cmp eax,42 + jae nv_play_loss + add npcVisitChips3D[esi*4],edi + cmp npcVisitMood3D[esi*4],3 + jge @F + inc npcVisitMood3D[esi*4] +@@: + mov npcVisitActivity3D[esi*4],NV_WIN + mov npcRemark3D[esi*4],OFFSET npcRemarkWin3D + jmp nv_result_done +nv_play_loss: + sub npcVisitChips3D[esi*4],edi + jns @F + mov npcVisitChips3D[esi*4],0 +@@: + cmp npcVisitMood3D[esi*4],-3 + jle @F + dec npcVisitMood3D[esi*4] +@@: + mov npcVisitActivity3D[esi*4],NV_LOSS + mov npcRemark3D[esi*4],OFFSET npcRemarkLoss3D +nv_result_done: + inc npcVisitRounds3D[esi*4] + inc npcVisitThirst3D[esi*4] + mov npcLife3D[ebx].poseClock,0 + mov npcLife3D[ebx].remaining,1800 + mov npcRemarkTime3D[esi*4],1800 + ret +nv_after_result: + cmp npcVisitMood3D[esi*4],-2 + jle nv_take_break + mov eax,npcVisitRounds3D[esi*4] + cmp eax,npcVisitSession3D[esi*4] + jae nv_take_break + mov eax,npcVisitStake3D[esi*4] + cmp eax,npcVisitChips3D[esi*4] + ja nv_take_break + mov npcVisitActivity3D[esi*4],NV_GAME + mov npcLife3D[ebx].poseClock,0 + invoke NpcVisitRandom3D,esi,1800 + add eax,2200 + mov npcLife3D[ebx].remaining,eax + ret +nv_take_break: + mov npcRemark3D[esi*4],OFFSET npcRemarkBreak3D + mov npcRemarkTime3D[esi*4],1600 + invoke NpcChooseVisit3D,esi + ret +nv_order_ready: + sub npcVisitChips3D[esi*4],20 + jns @F + mov npcVisitChips3D[esi*4],0 +@@: + mov npcVisitThirst3D[esi*4],0 + mov npcVisitMood3D[esi*4],0 + mov npcVisitActivity3D[esi*4],NV_DRINK + mov npcLife3D[ebx].poseClock,0 + mov npcLife3D[ebx].remaining,6500 + ret +nv_social: + cmp npcVisitMood3D[esi*4],0 + jge @F + inc npcVisitMood3D[esi*4] +@@: + ; Only chat when another stationary visitor is actually nearby. + invoke NpcEntity3D,esi + mov edi,eax + xor ecx,ecx +nv_social_find: + cmp ecx,esi + je nv_social_next + push ecx + invoke NpcEntity3D,ecx + mov edx,[edi].NpcMindEntity.posX + sub edx,[eax].NpcMindEntity.posX + jns @F + neg edx +@@: + cmp edx,FP_1*5/2 + ja nv_social_far + mov edx,[edi].NpcMindEntity.posY + sub edx,[eax].NpcMindEntity.posY + jns @F + neg edx +@@: + cmp edx,FP_1*5/2 + ja nv_social_far + pop ecx + imul eax,ecx,SIZEOF NpcLife3D + cmp npcLife3D[eax].state,NPC_WALK + je nv_social_next + cmp npcLife3D[eax].state,NPC_AWAY + je nv_social_next + mov npcVisitActivity3D[esi*4],NV_CHAT + mov npcLife3D[ebx].poseClock,0 + mov npcLife3D[ebx].remaining,6000 + mov npcRemark3D[esi*4],OFFSET npcRemarkChat3D + mov npcRemarkTime3D[esi*4],1800 + ret +nv_social_far: + pop ecx +nv_social_next: + inc ecx + cmp ecx,NPC_LIFE_COUNT + jb nv_social_find + invoke NpcChooseVisit3D,esi + ret +NpcAdvanceVisit3D ENDP + +NpcResolveVisitPose3D PROC uses ebx esi slot:DWORD + mov esi,slot + imul ebx,esi,SIZEOF NpcLife3D + mov eax,npcLife3D[ebx].state + cmp eax,NPC_SIT + je nv_pose_seat + cmp npcRise3D[esi*4],0 + jne nv_pose_rising + mov eax,npcVisitActivity3D[esi*4] + cmp eax,NV_WIN + je nv_pose_cheer + cmp eax,NV_DRINK + je nv_pose_drink + cmp eax,NV_CHAT + je nv_pose_talk + cmp eax,NV_ORDER + je nv_pose_talk + mov eax,CAST_IDLE + ret +nv_pose_cheer: + mov eax,CAST_CHEER + ret +nv_pose_drink: + mov eax,CAST_DRINK + ret +nv_pose_talk: + mov eax,CAST_TALK + ret +nv_pose_seat: + invoke NpcSeatOffset3D,npcSitTime3D[esi*4] + mov eax,CAST_SEAT + cmp npcVisitActivity3D[esi*4],NV_CHAT + jne @F + mov eax,CAST_SEAT_TALK +@@: + ret +nv_pose_rising: + invoke NpcSeatOffset3D,npcRise3D[esi*4] + mov eax,CAST_IDLE + ret +NpcResolveVisitPose3D ENDP + +; Smoothstep into/out of the existing seat anchor. Activity clocks cannot replay it. +NpcSeatOffset3D PROC uses ebx milliseconds:DWORD + mov eax,milliseconds + cmp eax,800 + jb nv_seat_fraction + mov eax,FP_1 + jmp nv_seat_offset +nv_seat_fraction: + shl eax,16 + xor edx,edx + mov ecx,800 + div ecx + mov ebx,eax + imul eax + shrd eax,edx,16 + mov ecx,3*FP_1 + sub ecx,ebx + sub ecx,ebx + imul ecx + shrd eax,edx,16 +nv_seat_offset: + mov ebx,eax + mov ecx,FP_HALF + imul ecx + shrd eax,edx,16 + sub npcSprite3D.posX,eax + mov eax,ebx + mov ecx,FP_1*35/100 + imul ecx + shrd eax,edx,16 + sub npcSprite3D.posY,eax + ret +NpcSeatOffset3D ENDP + +NpcDrawRemark3D PROC uses ebx esi edi slot:DWORD + local px:DWORD,py:DWORD,depth:DWORD,boxW:DWORD,textPtr:DWORD + mov esi,slot + cmp npcRemarkTime3D[esi*4],0 + je nv_remark_done + imul ebx,esi,SIZEOF NpcLife3D + cmp npcLife3D[ebx].state,NPC_WALK + je nv_remark_done + invoke ProjectCityPoint3D,npcSprite3D.posX,npcSprite3D.posY + cmp edx,FP_1/2 + jle nv_remark_done + cmp edx,5*FP_1 + jg nv_remark_done + cmp eax,24 + jl nv_remark_done + cmp eax,SCREEN_W-24 + jg nv_remark_done + cmp edx,zBuffer[eax*4] + jg nv_remark_done + mov depth,edx + mov px,eax + mov eax,viewEye3D + sub eax,CAST_HEIGHT + imul eax,SCREEN_H + cdq + idiv depth + add eax,viewHorizon3D + sar eax,1 + sub eax,20 + cmp eax,22 + jl nv_remark_done + cmp eax,194 + jg nv_remark_done + mov py,eax + mov edi,npcRemark3D[esi*4] + test edi,edi + jz nv_remark_done + mov textPtr,edi + xor ecx,ecx +nv_remark_length: + cmp BYTE PTR [edi+ecx],0 + je nv_remark_place + inc ecx + cmp ecx,24 + jb nv_remark_length + ret +nv_remark_place: + lea ebx,[ecx*8+12] + mov boxW,ebx + shr ebx,1 + mov eax,px + sar eax,1 + sub eax,ebx + cmp eax,4 + jge @F + mov eax,4 +@@: + mov ecx,316 + sub ecx,boxW + cmp eax,ecx + jle @F + mov eax,ecx +@@: + mov px,eax + invoke DrawBoxBorder,px,py,boxW,18,00151D22h,00899599h,1 + mov eax,px + add eax,6 + mov ecx,py + add ecx,5 + invoke DrawString8x8,eax,ecx,textPtr,00E8D7ADh +nv_remark_done: + ret +NpcDrawRemark3D ENDP diff --git a/src_graphics/npc_visits_data.inc b/src_graphics/npc_visits_data.inc new file mode 100644 index 0000000..6407c41 --- /dev/null +++ b/src_graphics/npc_visits_data.inc @@ -0,0 +1,35 @@ +NV_IDLE equ 0 +NV_GAME equ 1 +NV_WIN equ 2 +NV_LOSS equ 3 +NV_ORDER equ 4 +NV_DRINK equ 5 +NV_REST equ 6 +NV_WATCH equ 7 +NV_CHAT equ 8 +NV_LEAVE equ 9 +NV_YIELD equ 10 +.data +npcVisitActivity3D dd NPC_LIFE_COUNT dup(0) +npcVisitRng3D dd NPC_LIFE_COUNT dup(0) +npcVisitChips3D dd NPC_LIFE_COUNT dup(0) +npcVisitRounds3D dd NPC_LIFE_COUNT dup(0) +npcVisitThirst3D dd NPC_LIFE_COUNT dup(0) +npcVisitMood3D dd NPC_LIFE_COUNT dup(0) +npcVisitBlocked3D dd NPC_LIFE_COUNT dup(0) +npcStationOwner3D dd NPC_DEST_COUNT dup(-1) +npcRemark3D dd NPC_LIFE_COUNT dup(0) +npcRemarkTime3D dd NPC_LIFE_COUNT dup(0) +npcVisitSpeed3D dd 55000,61000,47000,52000,58000,44000,59000,50000,46000,54000 +npcVisitFavourite3D dd 5,3,2,1,4,2,5,1,3,4 +npcVisitSession3D dd 3,4,2,2,4,1,3,2,1,3 +npcVisitStake3D dd 40,60,20,30,80,20,50,30,20,60 +npcVisitInitialChips3D dd 1200,900,800,1000,1800,500,1300,1000,650,1400 +npcStationKind3D dd NV_LEAVE,NV_GAME,NV_GAME,NV_GAME,NV_GAME,NV_GAME + dd NV_ORDER,NV_REST,NV_WATCH,NV_WATCH,NV_ORDER,NV_REST +npcRemarkWin3D db 197,209,210,220,33,0 +npcRemarkLoss3D db 206,207,223,210,220,32,204,200,204,206,46,46,46,0 +npcRemarkOrder3D db 194,206,196,219,44,32,207,206,198,192,203,211,201,209,210,192,46,0 +npcRemarkChat3D db 202,192,202,32,194,197,215,197,208,63,0 +npcRemarkBreak3D db 207,206,208,192,32,207,197,208,197,196,206,213,205,211,210,220,46,0 + diff --git a/tools/generators/pack_bowling_feel.py b/tools/generators/pack_bowling_feel.py new file mode 100644 index 0000000..a4b33a5 --- /dev/null +++ b/tools/generators/pack_bowling_feel.py @@ -0,0 +1,88 @@ +"""Offline first-person atlas packing and original deterministic bowling Foley.""" +from pathlib import Path +import json +import math +import random +import struct +import wave +from PIL import Image +from pack_bowling import tween, pack + +ROOT = Path(__file__).resolve().parents[2] +ART = ROOT/'assets/sprites/bowling' +SOUND = ROOT/'assets/audio/bowling' + + +def wav(name, seconds, pulses, rolling=False): + rate = 22050 + rng = random.Random(41309) + samples = [] + low = 0.0 + for i in range(round(rate*seconds)): + t = i/rate + noise = rng.uniform(-1,1) + low += .12*(noise-low) + value = 0.0 + if rolling: + envelope = min(1,t/.03,(seconds-t)/.03) + value = .22*low*envelope*(.7+.3*math.sin(t*43)) + for start, gain, frequency in pulses: + age = t-start + if 0 <= age < .45: + value += gain*(.42*math.sin(age*frequency*math.tau)*math.exp(-age*27) + + .58*noise*math.exp(-age*45)) + samples.append(max(-32767,min(32767,round(value*24000)))) + with wave.open(str(SOUND/name),'wb') as output: + output.setparams((1,2,rate,0,'NONE','not compressed')) + output.writeframes(struct.pack(f'<{len(samples)}h',*samples)) + + +def pinsetter_audio(): + wav('pinsetter.wav',5.2,[(0,.24,120),(.4,.22,160),(1.7,.3,105), + (2.2,.24,130),(3.7,.2,105),(4,.22,160),(4.4,.25,140)],True) + + +def return_audio(): + wav('return.wav',2.0,[(.35,.18,95),(.75,.12,130),(1.3,.16,160), + (1.65,.32,110),(1.78,.18,85)],True) + + +def main(): + SOUND.mkdir(parents=True,exist_ok=True) + source = Image.open(ART/'player-underarm-v2.png').convert('RGBA') + keys = [] + # Cell 7 contains a detached ball: release uses the actual simulated sphere. + for cell in (*range(7),8,9,10,11): + x,y = cell%4,cell//4 + keys.append(source.crop((round(x*source.width/4),round(y*source.height/3), + round((x+1)*source.width/4),round((y+1)*source.height/3))) + .resize((256,256),Image.Resampling.LANCZOS)) + frames=[] + for i,a in enumerate(keys): + b=keys[min(i+1,len(keys)-1)] + # Never morph the held ball into an empty hand across the release event. + frames += [a,a,a] if i==6 else [a,tween(a,b,1/3),tween(a,b,2/3)] + pack('player-underarm-v2.bin',frames) + frames[0].save(ART/'player-underarm-v2.gif',save_all=True, + append_images=frames[1:],duration=33,loop=0,disposal=2) + wav('release.wav',.35,[(0,.8,94),(.025,.3,130)]) + wav('rolling.wav',14.5,[],True) + wav('pins-light.wav',.7,[(0,.65,430),(.08,.38,670),(.19,.24,510)]) + wav('pins-heavy.wav',1.3,[(0,.9,390),(.025,.75,710),(.08,.65,510), + (.14,.6,830),(.24,.4,620),(.39,.32,440),(.57,.2,740)]) + wav('applause.wav',1.4,[(i*.09,.25+(i%3)*.04,1700+i*23) for i in range(13)]) + pinsetter_audio() + return_audio() + (ART/'player-underarm-v2.json').write_text(json.dumps({ + 'generator':'built-in image_gen','source':'player-underarm-v2.png', + 'brief':'First-person right-handed underarm bowling: low pendulum swing beside thigh, ' + 'backswing leaves view, hand behind/below blue ball, ankle-height release, palm-up follow-through; ' + 'dark rolled sleeve; transparent 4 by 3 atlas. Replaces rejected forward-pushing draft.', + 'selected_cells':[0,1,2,3,4,5,6,8,9,10,11], + 'frames':len(frames),'frame_size':[256,256],'release_frame':21, + 'audio':'Original seeded PCM synthesis, no sampled third-party recordings.'},indent=2)+'\n') + print('Packed',len(frames),'underarm frames and seven original PCM effects') + + +if __name__=='__main__': + main() diff --git a/tools/tests/bowling_feel_test.inc b/tools/tests/bowling_feel_test.inc new file mode 100644 index 0000000..da42358 --- /dev/null +++ b/tools/tests/bowling_feel_test.inc @@ -0,0 +1,455 @@ +.data +bpFeelScoreRolls dd 10,7,3,0,8,5,4,9,0,6,2,8,1,7,2,9,0,10,7,3 +bpFeelScorePath db 'build\hall-expansion\bowling-scorecard-full.bmp',0 +bpFeelPinPath0 db 'build\hall-expansion\bowling-impact-0.bmp',0 +bpFeelPinPath1 db 'build\hall-expansion\bowling-impact-1.bmp',0 +bpFeelPinPath2 db 'build\hall-expansion\bowling-impact-2.bmp',0 +bpFeelPinPath3 db 'build\hall-expansion\bowling-impact-3.bmp',0 +bpFeelPinPaths dd OFFSET bpFeelPinPath0,OFFSET bpFeelPinPath1,OFFSET bpFeelPinPath2,OFFSET bpFeelPinPath3 +bpFeelStatus db 'status bowlroll mode',0 +bpFeelStatusLight db 'status bowllight mode',0 +bpFeelStatusReset db 'status bowlreset mode',0 +bpFeelStatusReturn db 'status bowlreturn mode',0 +bpFeelReply db 64 dup(0) +bpFeelSampleAim dd -FP_1/2,-FP_1*45/100,-FP_1*4/10,-FP_1*35/100,-FP_1*3/10,-FP_1/4,-FP_1/5,-FP_1*15/100,-FP_1/10,-FP_1/20 + dd 0,FP_1/20,FP_1/10,FP_1*15/100,FP_1/5,FP_1/4,FP_1*3/10,FP_1*35/100,FP_1*4/10,FP_1*45/100,FP_1/2 +bpFeelSamplePower dd 0,50,100 +bpFeelSamples dd 63*4 dup(0) +bpFeelSamplesPath db 'build\hall-expansion\bowling-physics-samples.bin',0 +bpFeelSetterTimes dd 0,150,300,900,2000,3750,4150,4400 +bpFeelEmptyPinPath db 'build\hall-expansion\bowling-shape-empty.bmp',0 +bpFeelTinyPinPath db 'build\hall-expansion\bowling-shape-tiny.bmp',0 +include tools/tests/bowling_hand_paths.inc +.code +BowlingFeelNativeTests PROC uses ebx esi edi + local window:DWORD,volume:DWORD,tilt:DWORD,aim:DWORD,power:DWORD,handle:DWORD,count:DWORD + mov expansionStage,170 + call InitBowling3D + mov hallLevel3D,HALL_BOWLING + invoke BeginBowlingPlay3D,1 + mov expansionStage,179 + call ResetBowlingPlay3D + ; Subpixel negative impulses must not collapse the pin's local basis. + invoke BowlingPlayKick3D,0,1,-1 + cmp bpPinAxisX3D,0 + jne bp_feel_fail + cmp bpPinAxisY3D,-FP_1 + jne bp_feel_fail + invoke BowlingPlayKick3D,0,257,257 + mov eax,bpPinAxisX3D + imul eax + shrd eax,edx,16 + mov ebx,eax + mov eax,bpPinAxisY3D + imul eax + shrd eax,edx,16 + add eax,ebx + cmp eax,FP_1*99/100 + jb bp_feel_fail + cmp eax,FP_1*101/100 + ja bp_feel_fail + mov bpState3D,BP_SETTLE + mov bpBallY3D,FP_1*28/10 + mov bpPinTilt3D,128*256 + mov bwRack3D[4],0 + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpFeelEmptyPinPath + mov bwRack3D[4],1 + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpFeelTinyPinPath + mov expansionStage,170 + call ResetBowlingPlay3D + mov bwRack3D[4],1 + mov bpGutter3D,1 + mov bpPinVX3D,512 + invoke BowlingPlayKick3D,0,512,0 + cmp bpHitMask3D,0 + jne bp_feel_fail + mov esi,200 +bp_feel_weak: + call BowlingPlayStep3D + dec esi + jnz bp_feel_weak + cmp bpHitMask3D,0 + jne bp_feel_fail + cmp bpPinTilt3D,24*256 + jae bp_feel_fail + mov expansionStage,171 + call ResetBowlingPlay3D + mov bwRack3D[4],1 + mov bpGutter3D,1 + mov bpPinVX3D,10000 + invoke BowlingPlayKick3D,0,10000,0 + cmp bpHitMask3D,0 + jne bp_feel_fail + mov esi,120 +bp_feel_topple: + call BowlingPlayStep3D + dec esi + jnz bp_feel_topple + test bpHitMask3D,1 + jz bp_feel_fail + cmp bpPinTilt3D,100*256 + jb bp_feel_fail + cmp bpPinX3D,FP_1*93/100 + jg bp_feel_fail + mov expansionStage,172 + ; Opening contacts are separated but must not gain another impulse. + call ResetBowlingPlay3D + mov bpPinX3D,0 + mov bpPinX3D[4],FP_1*15/100 + mov bpPinY3D,4*FP_1 + mov bpPinY3D[4],4*FP_1 + mov bpPinVX3D,-2000 + mov bpPinVX3D[4],2000 + invoke BowlingPlayContact3D,0,1 + cmp bpPinVX3D,-2000 + jne bp_feel_fail + cmp bpPinVX3D[4],2000 + jne bp_feel_fail + ; A fallen pin can receive a second impact and transfer momentum again. + mov bpPinX3D,0 + mov bpPinX3D[4],FP_1*15/100 + mov bpPinVX3D,2000 + mov bpPinVX3D[4],0 + mov bpPinTilt3D,128*256 + mov bpHitMask3D,1 + invoke BowlingPlayContact3D,0,1 + cmp bpPinVX3D[4],500 + jb bp_feel_fail + cmp bpPinVX3D,2000 + jae bp_feel_fail + mov expansionStage,173 + call ResetBowlingPlay3D + mov bpEndTicks3D,100 + mov bpPinTilt3D,40*256 + call BowlingPlayReady3D + test eax,eax + jnz bp_feel_fail + mov bpPinTilt3D,128*256 + call BowlingPlayReady3D + cmp eax,1 + jne bp_feel_fail + mov expansionStage,174 + call ResetBowlingPlay3D + lea esi,bpFeelScoreRolls + lea edi,bwRolls3D[192] + mov ecx,20 + rep movsd + mov bwRollCount3D[8],20 + invoke BowlingScore3D,2 + invoke BowlingRollGlyph3D,2,0,0 + cmp eax,'X' + jne bp_feel_fail + invoke BowlingRollGlyph3D,2,1,1 + cmp eax,'/' + jne bp_feel_fail + invoke BowlingRollGlyph3D,2,2,0 + cmp eax,'-' + jne bp_feel_fail + invoke BowlingRollGlyph3D,2,9,0 + cmp eax,'X' + jne bp_feel_fail + invoke BowlingRollGlyph3D,2,9,1 + cmp eax,'7' + jne bp_feel_fail + invoke BowlingRollGlyph3D,2,9,2 + cmp eax,'/' + jne bp_feel_fail + mov bpScoreOpen3D,1 + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpFeelScorePath + mov bwRolls3D[192],0 + mov bwRolls3D[196],10 + mov bwRollCount3D[8],2 + invoke BowlingRollGlyph3D,2,0,1 + cmp eax,'/' + jne bp_feel_fail + lea esi,bpFeelScoreRolls + lea edi,bwRolls3D[192] + mov ecx,20 + rep movsd + mov bwRollCount3D[8],20 + mov bwRolls3D[192+18*4],0 + mov bwRolls3D[192+19*4],10 + invoke BowlingRollGlyph3D,2,9,2 + cmp eax,'/' + jne bp_feel_fail + mov expansionStage,175 + call ResetBowlingPlay3D + mov bpLockedAim3D,0 + mov bpLockedPower3D,85 + call BowlingPlayLaunch3D + mov bpState3D,BP_ROLL + mov esi,0 + mov edi,0 +bp_feel_impact: + call BowlingPlayStep3D + inc esi + cmp esi,160 + jb bp_feel_impact + mov eax,esi + and eax,15 + jnz bp_feel_impact + imul eax,esi,16 + mov bpTime3D,eax + call RenderBowlingPlay3D + invoke ExpansionSnapshot,bpFeelPinPaths[edi*4] + inc edi + cmp edi,4 + jb bp_feel_impact + mov power,0 + mov aim,0 + mov edi,0 +bp_feel_sample: + call ResetBowlingPlay3D + mov eax,aim + mov eax,bpFeelSampleAim[eax*4] + mov bpLockedAim3D,eax + mov bpFeelSamples[edi],eax + mov eax,power + mov eax,bpFeelSamplePower[eax*4] + mov bpLockedPower3D,eax + mov bpFeelSamples[edi+4],eax + call BowlingPlayLaunch3D + mov esi,900 +bp_feel_sample_steps: + call BowlingPlayStep3D + ; Check the constraints throughout the roll, not just at settlement. + mov ebx,0 +bp_feel_sample_bounds: + mov eax,bpPinX3D[ebx*4] + sub eax,bwPinX3D[ebx*4] + sar eax,8 + imul eax,eax + mov ecx,eax + mov eax,bpPinY3D[ebx*4] + sub eax,bwPinY3D[ebx*4] + sar eax,8 + imul eax,eax + add eax,ecx + cmp eax,132*132 + ja bp_feel_fail + mov eax,bpPinAxisX3D[ebx*4] + imul eax + shrd eax,edx,16 + mov ecx,eax + mov eax,bpPinAxisY3D[ebx*4] + imul eax + shrd eax,edx,16 + add eax,ecx + cmp eax,FP_1*99/100 + jb bp_feel_fail + cmp eax,FP_1*101/100 + ja bp_feel_fail + inc ebx + cmp ebx,10 + jb bp_feel_sample_bounds + dec esi + jnz bp_feel_sample_steps + mov eax,bpHitMask3D + mov bpFeelSamples[edi+8],eax + mov eax,bpPinTilt3D + mov bpFeelSamples[edi+12],eax + add edi,16 + inc aim + cmp aim,21 + jb bp_feel_sample + mov aim,0 + inc power + cmp power,3 + jb bp_feel_sample + invoke CreateFileA,OFFSET bpFeelSamplesPath,40000000h,0,0,2,80h,0 + mov handle,eax + invoke WriteFile,handle,OFFSET bpFeelSamples,SIZEOF bpFeelSamples,ADDR count,0 + invoke CloseHandle,handle + mov expansionStage,176 + call ResetBowlingPlay3D + xor esi,esi +bp_feel_hand: + mov bpState3D,BP_RELEASE + imul eax,esi,33 + mov bpTime3D,eax + cmp esi,21 + jb @F + mov bpState3D,BP_ROLL + sub eax,21*33 + mov bpTime3D,eax +@@: + call RenderBowlingPlay3D + invoke ExpansionSnapshot,bpFeelHandPaths[esi*4] + inc esi + cmp esi,33 + jb bp_feel_hand + mov expansionStage,178 + call ResetBowlingPlay3D + mov bpPinX3D,2*FP_1 + mov bpPinVX3D,10000 + invoke BowlingPlayTether3D,0 + cmp bpPinX3D,FP_1*67/100 + ja bp_feel_fail + cmp bpPinVX3D,2500 + ja bp_feel_fail + mov bpPinTilt3D,128*256 + mov bpPinAxisX3D,FP_1 + mov bpPinAxisY3D,0 + invoke BowlingPlayDeckBounds3D,0 + cmp bpPinX3D,FP_1*48/100 + ja bp_feel_fail + call ResetBowlingPlay3D + mov bpHitMask3D,1 + mov bpPinTilt3D,128*256 + call BowlingPlayFinishRoll3D + mov deltaTime3D,250 + mov esi,7 +bp_feel_start_setter: + call HandleBowlingPlay3D + dec esi + jnz bp_feel_start_setter + cmp bpState3D,BP_RESET + jne bp_feel_fail + cmp bwRollCount3D[8],1 + jne bp_feel_fail + mov bpTime3D,1100 + invoke BowlingPinsetterPose3D,0 + test eax,eax + jz bp_feel_fail + cmp bpVisualPinLift3D,FP_1/2 + jb bp_feel_fail + cmp bpPinTilt3D,128*256 + jne bp_feel_fail + mov bpTime3D,1800 + invoke BowlingPinsetterPose3D,0 + cmp eax,1 + jne bp_feel_fail + cmp bpVisualPinLift3D,FP_1 + jb bp_feel_fail + mov bpTime3D,2450 + invoke BowlingPinsetterPose3D,0 + cmp eax,1 + jne bp_feel_fail + cmp bpVisualPinLift3D,FP_1 + jb bp_feel_fail + invoke BowlingPinsetterPose3D,1 + cmp eax,1 + jne bp_feel_fail + mov bwNewRack3D[4],1 + invoke BowlingPinsetterPose3D,0 + cmp eax,1 + jne bp_feel_fail + mov bwNewRack3D[4],0 + mov bpTime3D,3750 + invoke BowlingPinsetterPose3D,1 + cmp bpVisualPinLift3D,0 + jne bp_feel_fail + mov esi,0 +bp_feel_setter_capture: + mov eax,bpFeelSetterTimes[esi*4] + mov bpTime3D,eax + mov bpBallY3D,FP_1*28/10 + call RenderBowlingPlay3D + invoke ExpansionSnapshot,bpFeelSetterPaths[esi*4] + inc esi + cmp esi,8 + jb bp_feel_setter_capture + mov bpTime3D,BP_RESET_MS + mov deltaTime3D,0 + call HandleBowlingPlay3D + cmp bpState3D,BP_RETURN + jne bp_feel_fail + mov bpTime3D,BP_RETURN_MS + call HandleBowlingPlay3D + cmp bpState3D,BP_DIRECTION + jne bp_feel_fail + cmp bwRack3D[4],1022 + jne bp_feel_fail + cmp bwRollCount3D[8],1 + jne bp_feel_fail + ; First-ball miss leaves the standing rack alone and never runs the hoist. + call ResetBowlingPlay3D + call BowlingPlayFinishRoll3D + mov bpTime3D,1600 + call HandleBowlingPlay3D + cmp bpState3D,BP_RETURN + jne bp_feel_fail + mov bpTime3D,BP_RETURN_MS + call HandleBowlingPlay3D + cmp bpState3D,BP_DIRECTION + jne bp_feel_fail + cmp bwRack3D[4],1023 + jne bp_feel_fail + mov bpState3D,BP_RESET + mov bpTime3D,0 + call BowlingPinsetterGate3D + cmp eax,FP_1 + jne bp_feel_fail + mov bpTime3D,400 + call BowlingPinsetterGate3D + cmp eax,FP_1*14/100 + ja bp_feel_fail + mov bpTime3D,4400 + call BowlingPinsetterGate3D + cmp eax,FP_1 + jne bp_feel_fail + mov expansionStage,177 + ; Real MCI devices, muted: opens, volume, release/roll/impact and close. + mov eax,h3DWnd + mov window,eax + mov eax,settingsVolume + mov volume,eax + mov h3DWnd,1 + mov settingsVolume,0 + call BowlingAudioOpen3D + mov bpScoreOpen3D,0 + mov bpState3D,BP_ROLL + mov bpEndTicks3D,0 + mov bpImpactEnergy3D,6000 + mov bpImpactCooldown3D,0 + call BowlingAudioUpdate3D + invoke mciSendStringA,OFFSET bpFeelStatus,OFFSET bpFeelReply,64,0 + test eax,eax + jnz bp_feel_audio_fail + cmp BYTE PTR bpFeelReply,'p' + jne bp_feel_audio_fail + invoke mciSendStringA,OFFSET bpFeelStatusLight,OFFSET bpFeelReply,64,0 + test eax,eax + jnz bp_feel_audio_fail + invoke mciSendStringA,OFFSET bpFeelStatusReset,OFFSET bpFeelReply,64,0 + test eax,eax + jnz bp_feel_audio_fail + mov bpScoreOpen3D,1 + call BowlingAudioUpdate3D + cmp bpAudioRolling3D,0 + jne bp_feel_audio_fail + mov bpScoreOpen3D,0 + mov bpState3D,BP_RETURN + mov bpTime3D,500 + call BowlingAudioUpdate3D + invoke mciSendStringA,OFFSET bpFeelStatusReturn,OFFSET bpFeelReply,64,0 + test eax,eax + jnz bp_feel_audio_fail + cmp BYTE PTR bpFeelReply,'p' + jne bp_feel_audio_fail + mov bpScoreOpen3D,1 + call BowlingAudioUpdate3D + cmp bpAudioReturning3D,0 + jne bp_feel_audio_fail + call BowlingAudioClose3D + invoke mciSendStringA,OFFSET bpFeelStatus,OFFSET bpFeelReply,64,0 + test eax,eax + jz bp_feel_audio_fail + invoke mciSendStringA,OFFSET bpFeelStatusReturn,OFFSET bpFeelReply,64,0 + test eax,eax + jz bp_feel_audio_fail + mov eax,window + mov h3DWnd,eax + mov eax,volume + mov settingsVolume,eax + call LeaveBowlingPlay3D + ret +bp_feel_audio_fail: + call BowlingAudioClose3D +bp_feel_fail: + invoke ExitProcess,expansionStage + ret +BowlingFeelNativeTests ENDP diff --git a/tools/tests/bowling_hand_paths.inc b/tools/tests/bowling_hand_paths.inc new file mode 100644 index 0000000..f6975ff --- /dev/null +++ b/tools/tests/bowling_hand_paths.inc @@ -0,0 +1,52 @@ +.data +bpFeelHand0 db 'build\hall-expansion\bowling-hand-00.bmp',0 +bpFeelHand1 db 'build\hall-expansion\bowling-hand-01.bmp',0 +bpFeelHand2 db 'build\hall-expansion\bowling-hand-02.bmp',0 +bpFeelHand3 db 'build\hall-expansion\bowling-hand-03.bmp',0 +bpFeelHand4 db 'build\hall-expansion\bowling-hand-04.bmp',0 +bpFeelHand5 db 'build\hall-expansion\bowling-hand-05.bmp',0 +bpFeelHand6 db 'build\hall-expansion\bowling-hand-06.bmp',0 +bpFeelHand7 db 'build\hall-expansion\bowling-hand-07.bmp',0 +bpFeelHand8 db 'build\hall-expansion\bowling-hand-08.bmp',0 +bpFeelHand9 db 'build\hall-expansion\bowling-hand-09.bmp',0 +bpFeelHand10 db 'build\hall-expansion\bowling-hand-10.bmp',0 +bpFeelHand11 db 'build\hall-expansion\bowling-hand-11.bmp',0 +bpFeelHand12 db 'build\hall-expansion\bowling-hand-12.bmp',0 +bpFeelHand13 db 'build\hall-expansion\bowling-hand-13.bmp',0 +bpFeelHand14 db 'build\hall-expansion\bowling-hand-14.bmp',0 +bpFeelHand15 db 'build\hall-expansion\bowling-hand-15.bmp',0 +bpFeelHand16 db 'build\hall-expansion\bowling-hand-16.bmp',0 +bpFeelHand17 db 'build\hall-expansion\bowling-hand-17.bmp',0 +bpFeelHand18 db 'build\hall-expansion\bowling-hand-18.bmp',0 +bpFeelHand19 db 'build\hall-expansion\bowling-hand-19.bmp',0 +bpFeelHand20 db 'build\hall-expansion\bowling-hand-20.bmp',0 +bpFeelHand21 db 'build\hall-expansion\bowling-hand-21.bmp',0 +bpFeelHand22 db 'build\hall-expansion\bowling-hand-22.bmp',0 +bpFeelHand23 db 'build\hall-expansion\bowling-hand-23.bmp',0 +bpFeelHand24 db 'build\hall-expansion\bowling-hand-24.bmp',0 +bpFeelHand25 db 'build\hall-expansion\bowling-hand-25.bmp',0 +bpFeelHand26 db 'build\hall-expansion\bowling-hand-26.bmp',0 +bpFeelHand27 db 'build\hall-expansion\bowling-hand-27.bmp',0 +bpFeelHand28 db 'build\hall-expansion\bowling-hand-28.bmp',0 +bpFeelHand29 db 'build\hall-expansion\bowling-hand-29.bmp',0 +bpFeelHand30 db 'build\hall-expansion\bowling-hand-30.bmp',0 +bpFeelHand31 db 'build\hall-expansion\bowling-hand-31.bmp',0 +bpFeelHand32 db 'build\hall-expansion\bowling-hand-32.bmp',0 +bpFeelHandPaths LABEL DWORD + dd OFFSET bpFeelHand0,OFFSET bpFeelHand1,OFFSET bpFeelHand2,OFFSET bpFeelHand3,OFFSET bpFeelHand4 + dd OFFSET bpFeelHand5,OFFSET bpFeelHand6,OFFSET bpFeelHand7,OFFSET bpFeelHand8,OFFSET bpFeelHand9 + dd OFFSET bpFeelHand10,OFFSET bpFeelHand11,OFFSET bpFeelHand12,OFFSET bpFeelHand13,OFFSET bpFeelHand14 + dd OFFSET bpFeelHand15,OFFSET bpFeelHand16,OFFSET bpFeelHand17,OFFSET bpFeelHand18,OFFSET bpFeelHand19 + dd OFFSET bpFeelHand20,OFFSET bpFeelHand21,OFFSET bpFeelHand22,OFFSET bpFeelHand23,OFFSET bpFeelHand24 + dd OFFSET bpFeelHand25,OFFSET bpFeelHand26,OFFSET bpFeelHand27,OFFSET bpFeelHand28,OFFSET bpFeelHand29 + dd OFFSET bpFeelHand30,OFFSET bpFeelHand31,OFFSET bpFeelHand32 +bpFeelSetter0 db 'build\hall-expansion\bowling-pinsetter-0.bmp',0 +bpFeelSetter1 db 'build\hall-expansion\bowling-pinsetter-1.bmp',0 +bpFeelSetter2 db 'build\hall-expansion\bowling-pinsetter-2.bmp',0 +bpFeelSetter3 db 'build\hall-expansion\bowling-pinsetter-3.bmp',0 +bpFeelSetter4 db 'build\hall-expansion\bowling-pinsetter-4.bmp',0 +bpFeelSetter5 db 'build\hall-expansion\bowling-pinsetter-5.bmp',0 +bpFeelSetter6 db 'build\hall-expansion\bowling-pinsetter-6.bmp',0 +bpFeelSetter7 db 'build\hall-expansion\bowling-pinsetter-7.bmp',0 +bpFeelSetterPaths dd OFFSET bpFeelSetter0,OFFSET bpFeelSetter1,OFFSET bpFeelSetter2,OFFSET bpFeelSetter3,OFFSET bpFeelSetter4,OFFSET bpFeelSetter5,OFFSET bpFeelSetter6,OFFSET bpFeelSetter7 +.code diff --git a/tools/tests/bowling_play_test.inc b/tools/tests/bowling_play_test.inc index f389026..88131cd 100644 --- a/tools/tests/bowling_play_test.inc +++ b/tools/tests/bowling_play_test.inc @@ -28,6 +28,13 @@ BowlingPlayNativeTests PROC uses ebx esi edi jne bp_test_fail cmp bwBusy3D[4],2 jne bp_test_fail + cmp bpState3D,BP_SELECT_BALL + jne bp_test_fail + mov deltaTime3D,0 + mov pressedKeys3D[20h],1 + call HandleBowlingPlay3D + cmp bpState3D,BP_DIRECTION + jne bp_test_fail mov expansionStage,151 mov deltaTime3D,0 mov bpClock3D,0 @@ -259,8 +266,15 @@ bp_test_flight: mov eax,bankroll cmp eax,bank jne bp_test_fail + call BowlingFeelNativeTests + call BowlingReturnNativeTests + call BowlingWeightNativeTests ret bp_test_fail: invoke ExitProcess,expansionStage ret BowlingPlayNativeTests ENDP + +include tools/tests/bowling_feel_test.inc +include tools/tests/bowling_return_test.inc +include tools/tests/bowling_weight_test.inc diff --git a/tools/tests/bowling_play_ui.inc b/tools/tests/bowling_play_ui.inc index 52d6f3f..c8d845e 100644 --- a/tools/tests/bowling_play_ui.inc +++ b/tools/tests/bowling_play_ui.inc @@ -5,6 +5,7 @@ BowlingUiStart PROC call Create3DWindow test eax,eax jz bp_ui_fail + call ToggleFullscreen3D invoke SetWindowLongA,h3DWnd,-4,OFFSET WindowEvents3D call InitBowling3D call LoadBowling3D diff --git a/tools/tests/bowling_return_test.inc b/tools/tests/bowling_return_test.inc new file mode 100644 index 0000000..c21c882 --- /dev/null +++ b/tools/tests/bowling_return_test.inc @@ -0,0 +1,167 @@ +.data +bpReturnTest0 db 'build\hall-expansion\bowling-return-0.bmp',0 +bpReturnTest1 db 'build\hall-expansion\bowling-return-1.bmp',0 +bpReturnTest2 db 'build\hall-expansion\bowling-return-2.bmp',0 +bpReturnTest3 db 'build\hall-expansion\bowling-return-3.bmp',0 +bpReturnTest4 db 'build\hall-expansion\bowling-return-4.bmp',0 +bpReturnTest5 db 'build\hall-expansion\bowling-return-5.bmp',0 +bpReturnTest6 db 'build\hall-expansion\bowling-return-6.bmp',0 +bpReturnTest7 db 'build\hall-expansion\bowling-return-7.bmp',0 +bpReturnTestPaths dd OFFSET bpReturnTest0,OFFSET bpReturnTest1,OFFSET bpReturnTest2,OFFSET bpReturnTest3 + dd OFFSET bpReturnTest4,OFFSET bpReturnTest5,OFFSET bpReturnTest6,OFFSET bpReturnTest7 +; Capture after the emerging ball clears the solid hood, not while inside it. +bpReturnTestTimes dd 0,400,900,1000,1300,1700,2200,2800 +.code +BowlingReturnNativeTests PROC uses ebx esi edi + local position:DWORD + mov expansionStage,180 + call InitBowling3D + mov hallLevel3D,HALL_BOWLING + mov engineMode,MODE_WALK + mov playerX,5*FP_1 + mov playerY,25*FP_1 + mov playerAngle,384 + invoke BeginBowlingPlay3D,1 + mov bpHitMask3D,1 + call BowlingPlayFinishRoll3D + mov bpState3D,BP_RESET + mov bpTime3D,BP_RESET_MS + mov deltaTime3D,0 + call HandleBowlingPlay3D + cmp bpState3D,BP_RETURN + jne bp_return_test_fail + cmp bpAfterReturn3D,BP_DIRECTION + jne bp_return_test_fail + cmp bwRollCount3D[8],1 + jne bp_return_test_fail + cmp bwRack3D[4],1022 + jne bp_return_test_fail + call BowlingReturnBallPose3D + test eax,eax + jnz bp_return_test_fail + ; A queued throw cannot skip the return or choose the next direction. + mov mouseX3D,160 + mov mouseY3D,120 + mov mouseClick3D,1 + mov pressedKeys3D[20h],1 + call HandleBowlingPlay3D + cmp bpState3D,BP_RETURN + jne bp_return_test_fail + mov bpTime3D,1000 + call BowlingReturnBallPose3D + cmp eax,1 + jne bp_return_test_fail + cmp bpReturnedX3D,FP_1*395/100 + jne bp_return_test_fail + mov eax,bpReturnedY3D + mov position,eax + mov bpTime3D,1700 + call BowlingReturnBallPose3D + cmp eax,1 + jne bp_return_test_fail + mov eax,bpReturnedY3D + cmp eax,position + jle bp_return_test_fail + cmp eax,FP_1*2616/100 + ja bp_return_test_fail + cmp bpReturnedZ3D,FP_1*84/100 + jne bp_return_test_fail + mov bpTime3D,400 + invoke BowlingReturnCamera3D,FP_1*252/10 + cmp playerAngle,320 + jne bp_return_test_fail + cmp playerY,FP_1*268/10 + jne bp_return_test_fail + mov bpTime3D,1000 + mov bpScoreOpen3D,1 + mov deltaTime3D,250 + call HandleBowlingPlay3D + cmp bpTime3D,1000 + jne bp_return_test_fail + mov bpScoreOpen3D,0 + mov deltaTime3D,0 + xor esi,esi +bp_return_test_capture: + mov eax,bpReturnTestTimes[esi*4] + mov bpTime3D,eax + call RenderBowlingPlay3D + invoke ExpansionSnapshot,bpReturnTestPaths[esi*4] + inc esi + cmp esi,8 + jb bp_return_test_capture + call HandleBowlingPlay3D + cmp bpState3D,BP_DIRECTION + jne bp_return_test_fail + cmp bpTime3D,0 + jne bp_return_test_fail + cmp bwRollCount3D[8],1 + jne bp_return_test_fail + mov expansionStage,181 + mov bpHitMask3D,1022 + call BowlingPlayFinishRoll3D + call CompleteBowlingRoll3D + cmp bpAfterReturn3D,BP_BOT + jne bp_return_test_fail + cmp bwTurn3D[4],1 + jne bp_return_test_fail + mov bpTime3D,BP_RETURN_MS + call HandleBowlingPlay3D + cmp bpState3D,BP_BOT + jne bp_return_test_fail + cmp bwRollCount3D[8],2 + jne bp_return_test_fail + ; Even the final ball returns before the result overlay appears. + call ResetBowlingPlay3D + mov esi,23 +bp_return_test_perfect: + mov bpHitMask3D,1023 + call BowlingPlayFinishRoll3D + call BowlingPlayNext3D + dec esi + jnz bp_return_test_perfect + mov bpHitMask3D,1023 + call BowlingPlayFinishRoll3D + call CompleteBowlingRoll3D + cmp bpAfterReturn3D,BP_FINISHED + jne bp_return_test_fail + cmp bpState3D,BP_RETURN + jne bp_return_test_fail + mov bpTime3D,BP_RETURN_MS + call HandleBowlingPlay3D + cmp bpState3D,BP_FINISHED + jne bp_return_test_fail + cmp bwScores3D[8],300 + jne bp_return_test_fail + cmp bwScores3D[12],300 + jne bp_return_test_fail + call LeaveBowlingPlay3D + mov expansionStage,182 + invoke BeginBowlingPlay3D,2 + mov bpState3D,BP_RETURN + mov bpTime3D,1700 + call BowlingReturnBallPose3D + cmp bpReturnedX3D,FP_1*38/10+FP_1*395/100 + jne bp_return_test_fail + invoke BowlingReturnCamera3D,FP_1*252/10 + cmp playerAngle,448 + jne bp_return_test_fail + call LeaveBowlingPlay3D + cmp playerX,5*FP_1 + jne bp_return_test_fail + cmp playerY,25*FP_1 + jne bp_return_test_fail + cmp playerAngle,384 + jne bp_return_test_fail + cmp engineMode,MODE_WALK + jne bp_return_test_fail + invoke BowlingCanWalk3D,FP_1*395/100,FP_1*264/10 + test eax,eax + jnz bp_return_test_fail + invoke BowlingCanWalk3D,FP_1*49/10,FP_1*264/10 + cmp eax,1 + jne bp_return_test_fail + ret +bp_return_test_fail: + invoke ExitProcess,expansionStage + ret +BowlingReturnNativeTests ENDP diff --git a/tools/tests/bowling_weight_test.inc b/tools/tests/bowling_weight_test.inc new file mode 100644 index 0000000..0753973 --- /dev/null +++ b/tools/tests/bowling_weight_test.inc @@ -0,0 +1,299 @@ +.data +bpMassTestPath db 'build\hall-expansion\bowling-mass-samples.bin',0 +bpMassTest4Path db 'build\hall-expansion\bowling-weight-4.bmp',0 +bpMassTest7Path db 'build\hall-expansion\bowling-weight-7.bmp',0 +bpMassTestLockedPath db 'build\hall-expansion\bowling-weight-locked.bmp',0 +bpMassTestRackPath db 'build\hall-expansion\bowling-weight-rack.bmp',0 +bpMassTestRedPath db 'build\hall-expansion\bowling-weight-5.bmp',0 +bpMassTestRackPairPath db 'build\hall-expansion\bowling-weight-rack-pair.bmp',0 +bpMassTestPowers dd 0,50,100 +bpMassShotsPath db 'build\hall-expansion\bowling-mass-shots.bin',0 +bpMassShots dd 84*3 dup(0) +; kg, power, launch m/s Q16, inverse mass Q16, incoming vy, outgoing ball/pin vy, locked kg. +bpMassTestSamples dd 12*8 dup(0) +.code +BowlingWeightNativeTests PROC uses ebx esi edi + local bank:DWORD,rng:DWORD,kg:DWORD,power:DWORD,handle:DWORD,count:DWORD,inverse:DWORD + local aim:DWORD + mov expansionStage,183 + call InitBowling3D + mov hallLevel3D,HALL_BOWLING + invoke BeginBowlingPlay3D,1 + mov eax,bankroll + mov bank,eax + mov eax,bwRng3D + mov rng,eax + mov deltaTime3D,0 + mov viewWidth3D,640 + mov viewHeight3D,480 + mov viewLeft3D,0 + mov viewTop3D,0 + cmp bpState3D,BP_SELECT_BALL + jne bp_mass_test_fail + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTestRackPath + invoke BowlingTintBallPixel3D,55203CC0h,4 + cmp eax,5520C03Ch + jne bp_mass_test_fail + invoke BowlingTintBallPixel3D,55203CC0h,5 + cmp eax,55C01E20h + jne bp_mass_test_fail + invoke BowlingTintBallPixel3D,55203CC0h,7 + cmp eax,55C07E20h + jne bp_mass_test_fail + xor esi,esi +bp_mass_test_material: + lea eax,[esi+4] + invoke BowlingBallMaterial3D,eax + invoke ResolveCitySurface3D,eax + test eax,eax + jz bp_mass_test_fail + cmp ecx,64 + jne bp_mass_test_fail + cmp edx,64 + jne bp_mass_test_fail + cmp esi,3 + jae @F + mov eax,bpRackHitX3D[esi*4+4] + sub eax,bpRackHitX3D[esi*4] + cmp eax,36 + jl bp_mass_test_fail +@@: + inc esi + cmp esi,4 + jb bp_mass_test_material + ; A click away from the actual rack never selects a ball or starts a throw. + invoke WindowEvents3D,0,201h,1,00780014h + call HandleBowlingPlay3D + cmp bpState3D,BP_SELECT_BALL + jne bp_mass_test_fail + mov eax,bpRackHitY3D + shl eax,17 + mov edx,bpRackHitX3D + shl edx,1 + or eax,edx + invoke WindowEvents3D,0,201h,1,eax + call HandleBowlingPlay3D + cmp bpWeightKg3D,4 + jne bp_mass_test_fail + cmp bpState3D,BP_DIRECTION + jne bp_mass_test_fail + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTest4Path + invoke WindowEvents3D,0,100h,34h,0 + call HandleBowlingPlay3D + cmp bpWeightKg3D,7 + jne bp_mass_test_fail + cmp bpState3D,BP_DIRECTION + jne bp_mass_test_fail + call HandleBowlingPlay3D + cmp bpState3D,BP_DIRECTION + jne bp_mass_test_fail + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTest7Path + ; The weight label opens the in-world rack; labels and spheres share picking. + invoke WindowEvents3D,0,201h,1,003C0018h + call HandleBowlingPlay3D + cmp bpState3D,BP_SELECT_BALL + jne bp_mass_test_fail + call RenderBowlingPlay3D + mov eax,bpRackHitY3D[4] + add eax,bpRackHitRadius3D[4] + add eax,9 + shl eax,17 + mov edx,bpRackHitX3D[4] + shl edx,1 + or eax,edx + invoke WindowEvents3D,0,201h,1,eax + call HandleBowlingPlay3D + cmp bpWeightKg3D,5 + jne bp_mass_test_fail + cmp bpState3D,BP_DIRECTION + jne bp_mass_test_fail + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTestRedPath + ; Test the source/scratch pixels directly: transparent hand borders can reveal + ; different rack balls behind them and are not a valid skin screenshot mask. + mov esi,bpHandArt3D.pixels + lea edi,bpTintedHandPixels3D + mov ebx,256*256 + xor ecx,ecx +bp_mass_test_hand_pixel: + mov eax,[esi] + mov edx,[edi] + xor edx,eax + test edx,0FF000000h + jnz bp_mass_test_fail + test edx,edx + jz bp_mass_test_hand_next + inc ecx + mov edx,eax + shr edx,16 + and edx,255 + and eax,255 + cmp edx,eax + jae bp_mass_test_fail +bp_mass_test_hand_next: + add esi,4 + add edi,4 + dec ebx + jnz bp_mass_test_hand_pixel + cmp ecx,500 + jb bp_mass_test_fail + mov eax,bankroll + cmp eax,bank + jne bp_mass_test_fail + mov eax,bwRng3D + cmp eax,rng + jne bp_mass_test_fail + mov expansionStage,184 + mov kg,4 + mov power,0 + xor edi,edi +bp_mass_sample: + call ResetBowlingPlay3D + mov eax,kg + mov bpWeightKg3D,eax + mov bpMassTestSamples[edi],eax + mov eax,power + mov eax,bpMassTestPowers[eax*4] + mov bpLockedPower3D,eax + mov bpMassTestSamples[edi+4],eax + mov bpLockedAim3D,FP_1/2 + call BowlingPlayLaunch3D + cmp bpBallVX3D,0 + jne bp_mass_test_fail + mov eax,bpLaunchSpeed3D + mov bpMassTestSamples[edi+8],eax + mov eax,bpInvMass3D[40] + mov bpMassTestSamples[edi+12],eax + mov eax,bpBallVY3D + mov bpMassTestSamples[edi+16],eax + mov bpPinVY3D[40],eax + mov bpPinVX3D[40],0 + mov bpPinX3D[40],0 + mov bpPinY3D[40],FP_1*42/10 + mov bpPinX3D,0 + mov bpPinY3D,4*FP_1 + invoke BowlingPlayContact3D,0,10 + mov eax,bpPinVY3D[40] + mov bpMassTestSamples[edi+20],eax + mov eax,bpPinVY3D + mov bpMassTestSamples[edi+24],eax + mov eax,bpShotWeightKg3D + mov bpMassTestSamples[edi+28],eax + add edi,32 + inc power + cmp power,3 + jb bp_mass_sample + mov power,0 + inc kg + cmp kg,7 + jbe bp_mass_sample + invoke CreateFileA,OFFSET bpMassTestPath,40000000h,0,0,2,80h,0 + mov handle,eax + invoke WriteFile,handle,OFFSET bpMassTestSamples,SIZEOF bpMassTestSamples,ADDR count,0 + invoke CloseHandle,handle + ; Exercise complete high-power rolls across all selectable masses. + mov kg,4 + mov aim,0 + xor edi,edi +bp_mass_shot: + call ResetBowlingPlay3D + mov eax,kg + mov bpWeightKg3D,eax + mov bpMassShots[edi],eax + mov eax,aim + mov eax,bpFeelSampleAim[eax*4] + mov bpLockedAim3D,eax + mov bpMassShots[edi+4],eax + mov bpLockedPower3D,100 + call BowlingPlayLaunch3D + mov esi,900 +bp_mass_shot_tick: + call BowlingPlayStep3D + xor ebx,ebx +bp_mass_shot_bounds: + mov eax,bpPinX3D[ebx*4] + cmp eax,-FP_1 + jl bp_mass_test_fail + cmp eax,FP_1 + jg bp_mass_test_fail + mov eax,bpPinY3D[ebx*4] + cmp eax,3*FP_1 + jl bp_mass_test_fail + cmp eax,FP_1*55/10 + jg bp_mass_test_fail + inc ebx + cmp ebx,10 + jb bp_mass_shot_bounds + dec esi + jnz bp_mass_shot_tick + call BowlingPlayReady3D + cmp eax,1 + jne bp_mass_test_fail + mov eax,bpHitMask3D + mov bpMassShots[edi+8],eax + add edi,12 + inc aim + cmp aim,21 + jb bp_mass_shot + mov aim,0 + inc kg + cmp kg,7 + jbe bp_mass_shot + invoke CreateFileA,OFFSET bpMassShotsPath,40000000h,0,0,2,80h,0 + mov handle,eax + invoke WriteFile,handle,OFFSET bpMassShots,SIZEOF bpMassShots,ADDR count,0 + invoke CloseHandle,handle + mov expansionStage,185 + call ResetBowlingPlay3D + mov bpWeightKg3D,7 + mov bpLockedAim3D,0 + mov bpLockedPower3D,70 + call BowlingPlayLaunch3D + mov eax,bpInvMass3D[40] + mov inverse,eax + mov bpWeightKg3D,4 + mov pressedKeys3D[31h],1 + call HandleBowlingPlay3D + cmp bpShotWeightKg3D,7 + jne bp_mass_test_fail + mov eax,bpInvMass3D[40] + cmp eax,inverse + jne bp_mass_test_fail + cmp bpState3D,BP_RELEASE + jne bp_mass_test_fail + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTestLockedPath + mov bwTurn3D[4],1 + mov bpWeightKg3D,7 + call BowlingPlayLaunch3D + cmp bpShotWeightKg3D,6 + jne bp_mass_test_fail + cmp bpWeightKg3D,7 + jne bp_mass_test_fail + call LeaveBowlingPlay3D + invoke BeginBowlingPlay3D,2 + call RenderBowlingPlay3D + invoke ExpansionSnapshot,OFFSET bpMassTestRackPairPath + mov eax,bpRackHitY3D[12] + shl eax,17 + mov edx,bpRackHitX3D[12] + shl edx,1 + or eax,edx + invoke WindowEvents3D,0,201h,1,eax + call HandleBowlingPlay3D + cmp bpWeightKg3D,7 + jne bp_mass_test_fail + cmp bpState3D,BP_DIRECTION + jne bp_mass_test_fail + call LeaveBowlingPlay3D + invoke BowlingCanWalk3D,FP_1*45/10,FP_1*257/10 + test eax,eax + jnz bp_mass_test_fail + ret +bp_mass_test_fail: + invoke ExitProcess,expansionStage + ret +BowlingWeightNativeTests ENDP diff --git a/tools/tests/check_hall_expansion.py b/tools/tests/check_hall_expansion.py index 9728d75..3ff0b03 100644 --- a/tools/tests/check_hall_expansion.py +++ b/tools/tests/check_hall_expansion.py @@ -42,6 +42,7 @@ def prepare(): code += "\n" + expand("src_graphics/rooftop.inc") code += "\n" + expand("src_graphics/bowling.inc") code += "\n" + read("src_graphics/character_cast.inc") + code += "\n" + read("src_graphics/npc_visits.inc") code += "\n" + proc(read("src_graphics/scene_animation.inc"),"LoadAnimPack3D") code += "\n" + proc(read("src_graphics/unavailable.inc"),"BeginUnavailable3D") code += "\n" + read("src_graphics/observer.inc") @@ -75,6 +76,7 @@ def prepare(): darts = read("src_graphics/darts_data.inc").replace( "include src_graphics\\darts_art.inc", read("src_graphics/darts_art.inc")) data += "\n" + re.sub(r"^\w+ PROTO[^\n]*\n", "", darts, flags=re.M) + data += "\n" + read("src_graphics/npc_visits_data.inc") own_data = set(re.findall(r"^(\w+) (?:dd|db|dw|REAL4|REAL8|AnimPack3D|CityProp3D)\b",data,re.M)) own_data.update(re.findall(r"^(\w+) LABEL\b",data,re.M)) coff = Coff(ROOT / "build/graphics/poker.obj") @@ -90,7 +92,8 @@ def prepare(): "casinoNpcs3D":"NpcMindEntity","hallSpriteTemp3D":"CityProp3D", "hallObjects3D":"CityProp3D","hallLights3D":"CityProp3D", "hallLoungeDecor3D":"CityProp3D","hallGuests3D":"CityProp3D", - "npcSprite3D":"CityProp3D","scrStation3D":"CityProp3D"} + "npcSprite3D":"CityProp3D","scrStation3D":"CityProp3D", + "npcExtraActors3D":"NpcMindEntity"} all_source = "\n".join(read(str(path.relative_to(ROOT))) for path in [ROOT/"casino3d.inc",ROOT/"casino3d_data.inc",*sorted(path for path in (ROOT/"src_graphics").glob("*.inc") if path.name != "renderer.inc")]) @@ -137,13 +140,13 @@ def prepare(): for dep in re.findall(r"\b[A-Za-z_]\w*",constants[name]): constant(dep) ordered.append(f"{name} equ {constants[name]}") - for name in re.findall(r"\b[A-Za-z_]\w*",code+data+read("tools/tests/hall_expansion_test.inc")): + for name in re.findall(r"\b[A-Za-z_]\w*",code+data+expand("tools/tests/hall_expansion_test.inc")): constant(name) # Win32 prototypes are not declarations in the helper object. for name,args in re.findall(r"^(\w+)\s+PROTO([^\n]*)",all_source,re.M): protos.setdefault(name,args.count(":DWORD")) protos.update({"ExitProcess":1,"WriteFile":5,"GetTickCount":0,"SetUnhandledExceptionFilter":1, - "CreateDirectoryA":2}) + "CreateDirectoryA":2,"mciSendStringA":4}) header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures+ordered+externs) header += "\noption language:stdcall\n" + "\n".join( name+" PROTO"+(" "+",".join([":DWORD"]*size) if size else "") @@ -223,6 +226,10 @@ def check_bowling_room(): return False if 2 <= x <= 17.3 and y < 22.3: return False + if 26 <= y <= 26.6 and any(abs(x-(3.95+3.8*i)) <= .4 for i in range(4)): + return False + if 25.2 <= y <= 26.6 and any(.25 <= x-(3.95+3.8*i) <= .85 for i in range(4)): + return False return not any(a <= x <= b and c <= y <= d for a,b,c,d in layout["collisions"]) start = (68,120) reached,pending = {start},[start] @@ -248,18 +255,118 @@ def check_bowling_room(): assert len(captures) >= 10 for a,b,region in ( ("direction","direction-left",(180,314,462,414)), - ("power","power-low",(536,154,596,408)), + ("power","power-low",(42,154,102,408)), ): images = [Image.open(OUT/f"bowling-play-{name}.bmp").convert("RGB").crop(region) for name in (a,b)] assert ImageChops.difference(*images).getbbox(), (a,b,"timing indicator must move") print("PASS: playable bowling gates, collision solver, frame-rate invariance, remaining racks, bonus balls, opponent and score sheet") + sample_data = (OUT/'bowling-physics-samples.bin').read_bytes() + shots = list(struct.iter_unpack('= 4, 'Aim/power have no physical effect' + assert any(power==100 and mask==1023 for _,power,mask,_ in shots), 'No achievable physical strike' + assert max(m.bit_count() for _,p,m,_ in shots if p==0) < max(m.bit_count() for _,p,m,_ in shots if p==100) + for _,power,mask,_ in shots: + assert 0 <= mask.bit_count() <= 10, (power,mask) + print('Native pin-count ranges by power:', {p:(min(m.bit_count() for _,power,m,_ in shots if power==p),max(m.bit_count() for _,power,m,_ in shots if power==p)) for p in (0,50,100)}) + hand = [Image.open(OUT/f'bowling-hand-{i:02}.bmp').convert('RGB') for i in range(33)] + assert len({im.crop((320,80,640,440)).tobytes() for im in hand}) >= 25 + hand[0].save(OUT/'bowling-native-release.gif',save_all=True,append_images=hand[1:],duration=33,loop=0) + impacts = [Image.open(OUT/f'bowling-impact-{i}.bmp').convert('RGB') for i in range(4)] + assert len({im.tobytes() for im in impacts}) == 4 + empty = Image.open(OUT/'bowling-shape-empty.bmp').convert('RGB') + tiny = Image.open(OUT/'bowling-shape-tiny.bmp').convert('RGB') + bounds = ImageChops.difference(empty,tiny).getbbox() + assert bounds and 8 <= bounds[2]-bounds[0] <= 120 and 8 <= bounds[3]-bounds[1] <= 120, bounds + setter = [Image.open(OUT/f'bowling-pinsetter-{i}.bmp').convert('RGB') for i in range(8)] + # A closed guard correctly hides intermediate hoist motion. + assert len({im.tobytes() for im in setter}) >= 6 + setter[0].save(OUT/'bowling-native-pinsetter.gif',save_all=True,append_images=setter[1:],duration=320,loop=0) + print('PASS: weak rocking/strong toppling, separating contacts, fallen-pin impulses, late settlement, ' + 'stable unit axes and compact silhouettes after tiny impacts, per-step travel limits, ' + 'guard/hoist/respots, first-ball miss without cycling, no duplicate scoring, ' + 'full X/spare/miss scorecard, 33 native release frames and seven muted MCI channels/cleanup') + returned = [Image.open(OUT/f'bowling-return-{i}.bmp').convert('RGB') for i in range(8)] + assert len({im.tobytes() for im in returned}) == 8 + returned[0].save(OUT/'bowling-native-return.gif',save_all=True,append_images=returned[1:], + duration=[400,500,100,300,400,500,600,250],loop=0) + print('PASS: visible return lift/rails, camera turn on either lane side, input/score invariants, ' + 'pause/resume, final-ball return, return-tray collision and exit restoration') + mass = list(struct.iter_unpack('<8i',(OUT/'bowling-mass-samples.bin').read_bytes())) + assert len(mass) == 12 + for kg,power,speed,inv,incoming,ball,pin,locked in mass: + assert kg == locked and 4 <= kg <= 7 + assert abs(inv/65536 - 1.6/kg) < .00002 + work = kg*(speed/65536)**2/2 + expected_work = 3*(3+6*power/100)**2 + assert abs(work/expected_work-1) < .0001,(kg,power,work) + assert abs((-incoming*1000/16)/speed-1) < .0003 + before = kg*incoming + after = kg*ball+1.6*pin + assert abs((after-before)/before) < .001,(kg,before,after) + assert abs((ball-pin)/-incoming-.5) < .002,(kg,ball,pin) + assert kg*ball**2+1.6*pin**2 <= kg*incoming**2 + assert abs(pin/incoming - 1.5*kg/(kg+1.6)) < .002 + for power in (0,50,100): + group = [s for s in mass if s[1] == power] + assert all(a[2] > b[2] for a,b in zip(group,group[1:])) + assert all(abs(a[5]/a[4]) < abs(b[5]/b[4]) for a,b in zip(group,group[1:])) + print('PASS: 4/5/6/7 kg selection, equal-work launch energy, momentum/restitution checks, ' + 'no impact energy gain, immutable shot mass and independent bot weight') + held = {kg:Image.open(OUT/f'bowling-weight-{kg}.bmp').convert('RGB') for kg in (4,5,7)} + assert len({im.crop((470,310,575,414)).tobytes() for im in held.values()}) == 3 + rack = Image.open(OUT/'bowling-weight-rack.bmp').convert('RGB') + pair = Image.open(OUT/'bowling-weight-rack-pair.bmp').convert('RGB') + assert rack.tobytes() != pair.tobytes(), 'Rack selection must use each pair in the actual room' + print('PASS: in-world lengthwise ball rack, projected sphere/plate picking on both pairs, ' + 'non-overlapping labels, native colour/alpha checks, unchanged skin/sleeve and storage-rail collision') + mass_shots = list(struct.iter_unpack('= 4 + print('Native high-power pin-count ranges by kg:', + {kg:(min(mask.bit_count() for k,_,mask in mass_shots if k==kg), + max(mask.bit_count() for k,_,mask in mass_shots if k==kg)) for kg in (4,5,6,7)}) print(f"PASS: eight bowling lanes, {len(reached)} connected walk points, reachable bar/dining/seats, native throw and room captures") +def check_npc_visits(): + records = list(struct.iter_unpack('<8I', (OUT/'npc-visits.bin').read_bytes())) + assert len(records) == 10 and len({r[2] for r in records}) == 10 + seen = 0 + for slot,identity,cast,activities,places,walking,journeys,chips in records: + assert slot < 10 and identity < 12 and cast < 10 + assert walking >= 50 and journeys >= 3 and places.bit_count() >= 2, records + assert chips < 100000 + seen |= activities + assert seen & 0x1FE == 0x1FE, hex(seen) + captures = [] + for kind in ('order','drink','seat','win','loss'): + picture = Image.open(OUT/f'npc-visit-{kind}.bmp').convert('RGB') + assert picture.size == (640,480) and max(ImageStat.Stat(picture).stddev) > 20 + picture.save(OUT/f'npc-visit-{kind}.png') + captures.append(picture) + assert captures[0].tobytes() != captures[1].tobytes() + assert captures[3].tobytes() != captures[4].tobytes() + for action,count in (('sit',10),('rise',9)): + frames = [Image.open(OUT/f'npc-chair-{action}-{index}.bmp').convert('RGB') + for index in range(count)] + assert all(frame.size == (640,480) for frame in frames) + assert len({frame.tobytes() for frame in frames}) >= 8, action + frames[0].save(OUT/f'npc-chair-{action}.gif',save_all=True, + append_images=frames[1:],duration=100,loop=0) + print('PASS: ten stable visitor identities, 480 seconds of private schedules, exclusive ' + 'stations, game/bar/rest/chat transitions, grounded chair poses, pause, ' + 'unchanged player bankroll/RNG, real chair approach and sit/rise sequences') + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--parity",action="store_true") parser.add_argument("--bowling-only",action="store_true") + parser.add_argument("--npc-only",action="store_true") parser.add_argument("--prove-alpha-regression",action="store_true") parser.add_argument("--prove-seam-regression",action="store_true") parser.add_argument("--prove-distance-regression",action="store_true") @@ -291,6 +398,14 @@ def main(): source = source.replace(" call LoadHallAssets3D\n cmp characterCast3D.pixels,0", " call LoadHallAssets3D\n call BowlingNativeTests\n invoke ExitProcess,0\n cmp characterCast3D.pixels,0") (OUT/(stem+".asm")).write_text(source,encoding="cp1251") + if args.npc_only: + stem = "module-npc" + source = (OUT/"module.asm").read_text(encoding="cp1251") + source = source.replace("include tools/tests/hall_expansion_test.inc", + expand("tools/tests/hall_expansion_test.inc")) + source = source.replace(" call LoadHallAssets3D\n cmp characterCast3D.pixels,0", + " call LoadHallAssets3D\n call NpcVisitsNativeTests\n invoke ExitProcess,0\n cmp characterCast3D.pixels,0") + (OUT/(stem+".asm")).write_text(source,encoding="cp1251") if args.prove_fate_anchor_regression: stem = "module-fate-anchor-regression" source = (OUT/"module.asm").read_text(encoding="cp1251") @@ -347,6 +462,10 @@ def main(): f"build/hall-expansion/{stem}.obj","build/hall-expansion/helpers.obj", "kernel32.lib","user32.lib","gdi32.lib","winmm.lib","ws2_32.lib"],cwd=ROOT,check=True) result = subprocess.run([str(OUT/(stem+".exe"))],cwd=ROOT,timeout=360) + if args.npc_only: + assert result.returncode == 0,result.returncode + check_npc_visits() + return if args.bowling_only: assert result.returncode == 0,result.returncode check_bowling_room() @@ -373,6 +492,7 @@ def main(): if not args.prove_seam_regression: check_rooftop_navigation() check_bowling_room() + check_npc_visits() check_character_cast() closed = Image.open(OUT/"fortune-closed.bmp").tobytes() opened = Image.open(OUT/"fortune-open.bmp").tobytes() diff --git a/tools/tests/hall_expansion_test.inc b/tools/tests/hall_expansion_test.inc index f88bc4d..dfb8d5e 100644 --- a/tools/tests/hall_expansion_test.inc +++ b/tools/tests/hall_expansion_test.inc @@ -1000,8 +1000,9 @@ expansion_long_gait: expansion_gait_bounds: cmp npcWalkDistance3D[edi*4],FP_1*3/4 jae expansion_fail - imul ebx,edi,SIZEOF NpcMindEntity - invoke CanWalk3D,casinoNpcs3D[ebx].posX,casinoNpcs3D[ebx].posY + invoke NpcEntity3D,edi + mov ebx,eax + invoke CanWalk3D,[ebx].NpcMindEntity.posX,[ebx].NpcMindEntity.posY cmp eax,1 jne expansion_fail inc edi @@ -1175,6 +1176,7 @@ expansion_leaf_shut: call ExpansionDartsTests call RoofNativeTests call BowlingNativeTests + call NpcVisitsNativeTests invoke ExitProcess,0 expansion_fail: invoke ExitProcess,expansionStage @@ -1187,3 +1189,4 @@ include tools/tests/darts_hall_test.inc include tools/tests/rooftop_test.inc include tools/tests/bowling_test.inc include tools/tests/character_cast_test.inc +include tools/tests/npc_visits_test.inc diff --git a/tools/tests/npc_chair_motion_test.inc b/tools/tests/npc_chair_motion_test.inc new file mode 100644 index 0000000..fd25405 --- /dev/null +++ b/tools/tests/npc_chair_motion_test.inc @@ -0,0 +1,240 @@ +.data +ncTestSit0 db 'build\hall-expansion\npc-chair-sit-0.bmp',0 +ncTestSit1 db 'build\hall-expansion\npc-chair-sit-1.bmp',0 +ncTestSit2 db 'build\hall-expansion\npc-chair-sit-2.bmp',0 +ncTestSit3 db 'build\hall-expansion\npc-chair-sit-3.bmp',0 +ncTestSit4 db 'build\hall-expansion\npc-chair-sit-4.bmp',0 +ncTestSit5 db 'build\hall-expansion\npc-chair-sit-5.bmp',0 +ncTestSit6 db 'build\hall-expansion\npc-chair-sit-6.bmp',0 +ncTestSit7 db 'build\hall-expansion\npc-chair-sit-7.bmp',0 +ncTestSit8 db 'build\hall-expansion\npc-chair-sit-8.bmp',0 +ncTestSit9 db 'build\hall-expansion\npc-chair-sit-9.bmp',0 +ncTestChat db 'build\hall-expansion\npc-chair-chat.bmp',0 +ncTestRise0 db 'build\hall-expansion\npc-chair-rise-0.bmp',0 +ncTestRise1 db 'build\hall-expansion\npc-chair-rise-1.bmp',0 +ncTestRise2 db 'build\hall-expansion\npc-chair-rise-2.bmp',0 +ncTestRise3 db 'build\hall-expansion\npc-chair-rise-3.bmp',0 +ncTestRise4 db 'build\hall-expansion\npc-chair-rise-4.bmp',0 +ncTestRise5 db 'build\hall-expansion\npc-chair-rise-5.bmp',0 +ncTestRise6 db 'build\hall-expansion\npc-chair-rise-6.bmp',0 +ncTestRise7 db 'build\hall-expansion\npc-chair-rise-7.bmp',0 +ncTestRise8 db 'build\hall-expansion\npc-chair-rise-8.bmp',0 +ncTestSitPaths dd OFFSET ncTestSit0,OFFSET ncTestSit1,OFFSET ncTestSit2,OFFSET ncTestSit3,OFFSET ncTestSit4 + dd OFFSET ncTestSit5,OFFSET ncTestSit6,OFFSET ncTestSit7,OFFSET ncTestSit8,OFFSET ncTestSit9 +ncTestRisePaths dd OFFSET ncTestRise0,OFFSET ncTestRise1,OFFSET ncTestRise2,OFFSET ncTestRise3,OFFSET ncTestRise4 + dd OFFSET ncTestRise5,OFFSET ncTestRise6,OFFSET ncTestRise7,OFFSET ncTestRise8 +.code +NpcChairMotionNativeTests PROC uses ebx esi edi + local startX:DWORD,startY:DWORD,endX:DWORD,endY:DWORD,px:DWORD,py:DWORD + local ticks:DWORD,lastX:DWORD,lastY:DWORD + mov expansionStage,210 + call InitNpcLife3D + mov playerX,FP_1*305/10 + mov playerY,FP_1*305/10 + invoke NpcPlanPath3D,0,7 + cmp eax,1 + jne nc_test_fail + mov eax,casinoNpcs3D.posX + mov startX,eax + mov eax,casinoNpcs3D.posY + mov startY,eax + mov esi,npcLife3D.pathCount +nc_test_edge: + dec esi + mov eax,npcPaths3D[esi*4] + mov edx,eax + and eax,31 + shl eax,16 + add eax,FP_HALF + mov endX,eax + shr edx,5 + shl edx,16 + add edx,FP_HALF + mov endY,edx + mov edi,1 +nc_test_edge_sample: + mov eax,endX + sub eax,startX + imul eax,edi + sar eax,2 + add eax,startX + mov px,eax + mov eax,endY + sub eax,startY + imul eax,edi + sar eax,2 + add eax,startY + mov py,eax + invoke CanWalk3D,px,py + cmp eax,1 + jne nc_test_fail + inc edi + cmp edi,4 + jbe nc_test_edge_sample + mov eax,endX + mov startX,eax + mov eax,endY + mov startY,eax + test esi,esi + jnz nc_test_edge + mov expansionStage,211 + mov npcLife3D.destination,7 + mov casinoNpcs3D.posX,FP_1*135/10 + mov casinoNpcs3D.posY,FP_1*195/10 + invoke NpcArrive3D,0 + mov npcSprite3D.posX,FP_1*135/10 + mov npcSprite3D.posY,FP_1*195/10 + invoke NpcResolveVisitPose3D,0 + cmp npcSprite3D.posX,FP_1*135/10 + jne nc_test_fail + cmp npcSprite3D.posY,FP_1*195/10 + jne nc_test_fail + mov expansionStage,212 + call InitNpcLife3D + mov esi,1 +nc_test_hide: + imul ebx,esi,SIZEOF NpcLife3D + mov npcLife3D[ebx].state,NPC_AWAY + mov npcLife3D[ebx].remaining,1000000 + invoke NpcReleaseStation3D,esi + inc esi + cmp esi,NPC_LIFE_COUNT + jb nc_test_hide + ; The guest from the reported screenshot approaches the chair through gameplay. + mov npcLife3D.identity,3 + invoke NpcReleaseStation3D,0 + invoke NpcPlanPath3D,0,7 + cmp eax,1 + jne nc_test_fail + mov npcStationOwner3D[7*4],0 + mov npcLife3D.state,NPC_WALK + mov npcVisitActivity3D,NV_IDLE + mov deltaTime3D,16 + mov ticks,1500 +nc_test_real_approach: + call UpdateNpcLife3D + cmp npcLife3D.state,NPC_SIT + je nc_test_sit_begin + cmp npcLife3D.state,NPC_WALK + jne nc_test_fail + dec ticks + jnz nc_test_real_approach + jmp nc_test_fail +nc_test_sit_begin: + cmp npcSitTime3D,0 + jne nc_test_fail + mov playerX,14*FP_1 + mov playerY,FP_1*222/10 + mov playerAngle,384 + call UpdateInteriorEye3D + xor esi,esi + mov deltaTime3D,0 +nc_test_sit_frames: + call UpdateNpcLife3D + call ExpansionFrame + invoke ExpansionSnapshot,ncTestSitPaths[esi*4] + mov eax,npcSprite3D.frame + xor edx,edx + mov ecx,CAST_STRIDE + div ecx + mov ebx,edx + cmp esi,8 + jae nc_test_sit_hold + lea eax,[esi+56] + cmp ebx,eax + jne nc_test_fail + jmp nc_test_sit_anchor +nc_test_sit_hold: + cmp ebx,12 + jb nc_test_fail + cmp ebx,15 + ja nc_test_fail +nc_test_sit_anchor: + mov eax,npcSprite3D.posX + test esi,esi + jz nc_test_sit_save + mov edx,lastX + sub edx,eax + cmp edx,0 + jl nc_test_fail + cmp edx,FP_1/10 + jg nc_test_fail +nc_test_sit_save: + mov lastX,eax + mov eax,npcSprite3D.posY + mov lastY,eax + mov deltaTime3D,100 + inc esi + cmp esi,10 + jb nc_test_sit_frames + mov expansionStage,213 + ; Changing from resting to conversation must not restart the sit-down strip. + invoke NpcEntity3D,1 + mov [eax].NpcMindEntity.posX,FP_1*155/10 + mov [eax].NpcMindEntity.posY,FP_1*195/10 + mov npcLife3D[SIZEOF NpcLife3D].state,NPC_SIT + invoke NpcAdvanceVisit3D,0 + cmp npcVisitActivity3D,NV_CHAT + jne nc_test_fail + cmp npcLife3D.poseClock,0 + jne nc_test_fail + mov npcLife3D[SIZEOF NpcLife3D].state,NPC_AWAY + call ExpansionFrame + mov eax,npcSprite3D.frame + xor edx,edx + mov ecx,CAST_STRIDE + div ecx + cmp edx,16 + jne nc_test_fail + mov eax,npcSprite3D.posX + cmp eax,lastX + jne nc_test_fail + mov eax,npcSprite3D.posY + cmp eax,lastY + jne nc_test_fail + invoke ExpansionSnapshot,OFFSET ncTestChat + invoke NpcChooseVisit3D,0 + cmp eax,1 + jne nc_test_fail + cmp npcRise3D,800 + jne nc_test_fail + mov deltaTime3D,0 + xor esi,esi +nc_test_rise_frames: + call UpdateNpcLife3D + call ExpansionFrame + invoke ExpansionSnapshot,ncTestRisePaths[esi*4] + mov eax,npcSprite3D.frame + xor edx,edx + mov ecx,CAST_STRIDE + div ecx + mov ebx,edx + test esi,esi + jnz nc_test_rise_anchor + mov eax,npcSprite3D.posX + cmp eax,lastX + jne nc_test_fail + mov eax,npcSprite3D.posY + cmp eax,lastY + jne nc_test_fail +nc_test_rise_anchor: + cmp esi,8 + jae nc_test_rise_end + mov eax,63 + sub eax,esi + cmp ebx,eax + jne nc_test_fail + jmp nc_test_rise_next +nc_test_rise_end: + cmp ebx,3 + ja nc_test_fail +nc_test_rise_next: + mov deltaTime3D,100 + inc esi + cmp esi,9 + jb nc_test_rise_frames + ret +nc_test_fail: + invoke ExitProcess,expansionStage + ret +NpcChairMotionNativeTests ENDP diff --git a/tools/tests/npc_life_test.inc b/tools/tests/npc_life_test.inc index 4793086..f844af1 100644 --- a/tools/tests/npc_life_test.inc +++ b/tools/tests/npc_life_test.inc @@ -70,8 +70,9 @@ npc_test_simulate: call UpdateNpcLife3D xor esi,esi npc_test_person_bounds: - imul ebx,esi,SIZEOF NpcMindEntity - invoke CanWalk3D,casinoNpcs3D[ebx].posX,casinoNpcs3D[ebx].posY + invoke NpcEntity3D,esi + mov ebx,eax + invoke CanWalk3D,[ebx].NpcMindEntity.posX,[ebx].NpcMindEntity.posY cmp eax,1 jne npc_test_fail imul edi,esi,SIZEOF NpcLife3D diff --git a/tools/tests/npc_visits_test.inc b/tools/tests/npc_visits_test.inc new file mode 100644 index 0000000..b795f68 --- /dev/null +++ b/tools/tests/npc_visits_test.inc @@ -0,0 +1,268 @@ +.data +nvTestPath db 'build\hall-expansion\npc-visits.bin',0 +nvTestOrder db 'build\hall-expansion\npc-visit-order.bmp',0 +nvTestDrink db 'build\hall-expansion\npc-visit-drink.bmp',0 +nvTestSeat db 'build\hall-expansion\npc-visit-seat.bmp',0 +nvTestWin db 'build\hall-expansion\npc-visit-win.bmp',0 +nvTestLoss db 'build\hall-expansion\npc-visit-loss.bmp',0 +; slot, identity, cast, activities, destinations, walking ticks, journeys, chips. +nvTestRecords dd NPC_LIFE_COUNT*8 dup(0) +.code +NpcVisitsNativeTests PROC uses ebx esi edi + local ticks:DWORD,bank:DWORD,rng:DWORD,handle:DWORD,count:DWORD,bits:DWORD + local savedClock:DWORD,seenResults:DWORD + mov expansionStage,200 + mov hallLevel3D,0 + mov engineMode,MODE_WALK + mov worldMap3D,OFFSET casinoMap + mov playerX,FP_1*305/10 + mov playerY,FP_1*305/10 + mov eax,bankroll + mov bank,eax + mov eax,seed + mov rng,eax + call NpcChairMotionNativeTests + call InitNpcLife3D + mov bits,0 + xor esi,esi +nv_test_identity: + imul ebx,esi,SIZEOF NpcLife3D + imul edi,esi,32 + mov nvTestRecords[edi],esi + mov eax,npcLife3D[ebx].identity + mov nvTestRecords[edi+4],eax + mov eax,npcGuestAppearance3D[eax*4] + mov nvTestRecords[edi+8],eax + bts bits,eax + jc nv_test_fail + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_test_identity + cmp bits,1023 + jne nv_test_fail + xor esi,esi +nv_test_paths: + invoke NpcPlanPath3D,0,esi + cmp eax,1 + jne nv_test_fail + inc esi + cmp esi,NPC_DEST_COUNT + jb nv_test_paths + call InitNpcLife3D + mov expansionStage,201 + mov deltaTime3D,50 + mov ticks,9600 +nv_test_tick: + call UpdateNpcLife3D + xor esi,esi +nv_test_person: + imul ebx,esi,SIZEOF NpcLife3D + imul edi,esi,32 + mov eax,npcLife3D[ebx].identity + cmp eax,nvTestRecords[edi+4] + jne nv_test_fail + cmp npcVisitChips3D[esi*4],0 + jl nv_test_fail + cmp npcVisitChips3D[esi*4],100000 + ja nv_test_fail + mov eax,npcVisitActivity3D[esi*4] + cmp eax,NV_YIELD + ja nv_test_fail + bts nvTestRecords[edi+12],eax + mov eax,npcLife3D[ebx].destination + cmp eax,NPC_DEST_COUNT + jae nv_test_fail + cmp npcLife3D[ebx].state,NPC_WALK + jne @F + inc nvTestRecords[edi+20] + jmp nv_test_bounds +@@: + cmp npcVisitActivity3D[esi*4],NV_IDLE + je nv_test_bounds + cmp npcVisitActivity3D[esi*4],NV_YIELD + je nv_test_bounds + bts nvTestRecords[edi+16],eax +nv_test_bounds: + invoke NpcEntity3D,esi + mov ebx,eax + invoke CanWalk3D,[ebx].NpcMindEntity.posX,[ebx].NpcMindEntity.posY + cmp eax,1 + jne nv_test_fail + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_test_person + xor esi,esi + mov bits,0 +nv_test_station: + mov eax,npcStationOwner3D[esi*4] + cmp eax,-1 + je nv_test_station_next + cmp eax,NPC_LIFE_COUNT + jae nv_test_fail + bts bits,eax + jc nv_test_fail + imul ebx,eax,SIZEOF NpcLife3D + cmp npcLife3D[ebx].destination,esi + jne nv_test_fail + cmp npcLife3D[ebx].state,NPC_AWAY + je nv_test_fail +nv_test_station_next: + inc esi + cmp esi,NPC_DEST_COUNT + jb nv_test_station + dec ticks + jnz nv_test_tick + mov expansionStage,202 + xor esi,esi +nv_test_record: + imul edi,esi,32 + imul ebx,esi,SIZEOF NpcLife3D + mov eax,npcLife3D[ebx].routeStep + mov nvTestRecords[edi+24],eax + mov eax,npcVisitChips3D[esi*4] + mov nvTestRecords[edi+28],eax + cmp nvTestRecords[edi+20],50 + jb nv_test_fail + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_test_record + invoke CreateFileA,OFFSET nvTestPath,40000000h,0,0,2,80h,0 + mov handle,eax + invoke WriteFile,handle,OFFSET nvTestRecords,SIZEOF nvTestRecords,ADDR count,0 + invoke CloseHandle,handle + mov eax,bankroll + cmp eax,bank + jne nv_test_fail + mov eax,seed + cmp eax,rng + jne nv_test_fail + mov eax,npcLifeClock3D + mov savedClock,eax + mov engineMode,MODE_NPC_DIALOG + call UpdateNpcLife3D + mov eax,savedClock + cmp npcLifeClock3D,eax + jne nv_test_fail + mov engineMode,MODE_WALK + mov hallLevel3D,HALL_ROOF + call UpdateNpcLife3D + mov eax,savedClock + cmp npcLifeClock3D,eax + jne nv_test_fail + mov hallLevel3D,0 + mov expansionStage,204 + call InitNpcLife3D + mov esi,1 +nv_test_yield_clear: + imul ebx,esi,SIZEOF NpcLife3D + mov npcLife3D[ebx].state,NPC_AWAY + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_test_yield_clear + invoke NpcReleaseStation3D,0 + invoke NpcPlanPath3D,0,5 + cmp eax,1 + jne nv_test_fail + mov npcStationOwner3D[5*4],0 + mov npcLife3D.state,NPC_WALK + mov eax,casinoNpcs3D.posX + mov playerX,eax + mov eax,casinoNpcs3D.posY + mov playerY,eax + invoke NpcMove3D,0,16 + cmp npcVisitActivity3D,NV_YIELD + jne nv_test_fail + cmp npcLife3D.pathCount,0 + je nv_test_fail + mov ticks,7 +nv_test_yield_retry: + invoke NpcAdvanceVisit3D,0 + invoke NpcMove3D,0,16 + dec ticks + jnz nv_test_yield_retry + cmp npcLife3D.pathCount,0 + jne nv_test_fail + cmp npcStationOwner3D[5*4],-1 + jne nv_test_fail + cmp npcWalkDistance3D,0 + jne nv_test_fail + mov expansionStage,203 + call InitNpcLife3D + xor esi,esi +nv_test_hide: + imul ebx,esi,SIZEOF NpcLife3D + mov npcLife3D[ebx].state,NPC_AWAY + inc esi + cmp esi,NPC_LIFE_COUNT + jb nv_test_hide + mov npcLife3D.destination,10 + mov casinoNpcs3D.posX,FP_1*65/10 + mov casinoNpcs3D.posY,FP_1*255/10 + invoke NpcArrive3D,0 + mov playerX,FP_1*65/10 + mov playerY,FP_1*282/10 + mov playerAngle,384 + call UpdateInteriorEye3D + call ExpansionFrame + invoke ExpansionSnapshot,OFFSET nvTestOrder + invoke NpcAdvanceVisit3D,0 + cmp npcVisitActivity3D,NV_DRINK + jne nv_test_fail + mov npcLife3D.poseClock,360 + call ExpansionFrame + invoke ExpansionSnapshot,OFFSET nvTestDrink + mov npcLife3D.destination,7 + mov casinoNpcs3D.posX,FP_1*135/10 + mov casinoNpcs3D.posY,FP_1*195/10 + invoke NpcArrive3D,0 + mov npcLife3D.poseClock,1200 + mov npcSitTime3D,800 + mov playerX,13*FP_1 + mov playerY,FP_1*215/10 + call ExpansionFrame + invoke ExpansionSnapshot,OFFSET nvTestSeat + mov npcSprite3D.posX,FP_1*135/10 + mov npcSprite3D.posY,FP_1*195/10 + invoke NpcResolveVisitPose3D,0 + cmp eax,CAST_SEAT + jne nv_test_fail + cmp npcSprite3D.posX,13*FP_1 + jne nv_test_fail + cmp npcSprite3D.posY,FP_1*195/10-FP_1*35/100 + jne nv_test_fail + mov seenResults,0 + mov ticks,32 + mov npcLife3D.destination,2 + mov casinoNpcs3D.posX,FP_1*105/10 + mov casinoNpcs3D.posY,FP_1*135/10 + mov playerX,FP_1*105/10 + mov playerY,16*FP_1 +nv_test_result: + invoke NpcArrive3D,0 + invoke NpcAdvanceVisit3D,0 + mov npcLife3D.poseClock,360 + call ExpansionFrame + cmp npcVisitActivity3D,NV_WIN + jne nv_test_loss + invoke ExpansionSnapshot,OFFSET nvTestWin + or seenResults,1 + jmp nv_test_result_next +nv_test_loss: + cmp npcVisitActivity3D,NV_LOSS + jne nv_test_fail + invoke ExpansionSnapshot,OFFSET nvTestLoss + or seenResults,2 +nv_test_result_next: + cmp seenResults,3 + je nv_test_done + dec ticks + jnz nv_test_result + jmp nv_test_fail +nv_test_done: + call InitNpcLife3D + ret +nv_test_fail: + invoke ExitProcess,expansionStage + ret +NpcVisitsNativeTests ENDP +include tools/tests/npc_chair_motion_test.inc