- Bowling feel: impulse contact solver, string pinsetter cycle, in-world lengthwise return rack and visible ball return lift - Ball weights: 4/5/6/7 kg selection with equal launch work physics, momentum balance, and projected picking - Audio: dedicated MCI channels with 7 synthesized waveaudio cues (release, rolling, impacts, applause, pinsetter, return) - First-person animations: 33-frame player underarm throw/follow-through sequence (player-underarm-v2) - Visitors: 10 persistent shared cast identities with exclusive reservations, activity transitions, and chair sit/rise animations - Tests & verification: comprehensive native regression test suites and updated documentation - Synchronize binaries and update launcher hash to 536E23BED4055562BC63871826FF44FFED037923A9BE8E0F591131DB4FBD104B
511 lines
12 KiB
PHP
511 lines
12 KiB
PHP
; 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
|