KAZIK/src_graphics/npc_visits.inc
Artvasil09 7e081beeca feat: enhance bowling physics, audio, ball weight selection, and visitor cast routines
- 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
2026-11-04 18:44:12 +03:00

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