KAZIK/src_graphics/npc_life.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

684 lines
16 KiB
PHP

; The visible people and the user's personality records share the same positions.
InitNpcLife3D PROC uses ebx esi edi
xor esi,esi
mov npcLifeClock3D,0
mov npcSelected3D,-1
npc_life_init:
imul ebx,esi,SIZEOF NpcLife3D
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,0
mov npcLife3D[ebx].pathCount,0
mov npcLife3D[ebx].remainder,0
mov npcLife3D[ebx].poseClock,0
mov npcWalkDistance3D[esi*4],0
mov npcWalkVelocityX3D[esi*4],0
mov npcWalkVelocityY3D[esi*4],0
mov npcRise3D[esi*4],0
mov npcSitTime3D[esi*4],0
mov eax,npcStartDest3D[esi*4]
mov npcLife3D[ebx].destination,eax
push eax
invoke NpcEntity3D,esi
mov edi,eax
pop eax
mov ecx,npcDestX3D[eax*4]
mov [edi].NpcMindEntity.posX,ecx
mov ecx,npcDestY3D[eax*4]
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 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
cmp npcParents3D[esi*4],-1
jne npc_cell_done
mov eax,esi
and eax,31
shl eax,16
add eax,FP_HALF
mov edx,esi
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
cmp ebx,NPC_PATH_CAP
jae npc_cell_done
mov npcQueue3D[ebx*4],esi
inc npcSearchTail3D
npc_cell_done:
ret
NpcTryCell3D ENDP
NpcPlanPath3D PROC uses ebx esi edi slot:DWORD,destination:DWORD
local firstCell:DWORD,currentCell:DWORD,pathBase:DWORD
cmp slot,NPC_LIFE_COUNT
jae npc_plan_fail
cmp destination,NPC_DEST_COUNT
jae npc_plan_fail
imul ebx,slot,SIZEOF NpcLife3D
mov npcLife3D[ebx].pathCount,0
invoke NpcEntity3D,slot
mov esi,eax
mov eax,[esi].NpcMindEntity.posY
sar eax,16
shl eax,5
mov ecx,[esi].NpcMindEntity.posX
sar ecx,16
add eax,ecx
cmp eax,NPC_PATH_CAP
jae npc_plan_fail
mov firstCell,eax
mov ecx,destination
mov eax,npcDestY3D[ecx*4]
sar eax,16
shl eax,5
mov edx,npcDestX3D[ecx*4]
sar edx,16
add eax,edx
mov npcSearchGoal3D,eax
lea edi,npcParents3D
mov ecx,NPC_PATH_CAP
mov eax,-1
rep stosd
mov eax,firstCell
mov npcParents3D[eax*4],eax
mov npcQueue3D,eax
mov npcSearchHead3D,0
mov npcSearchTail3D,1
npc_plan_search:
mov eax,npcSearchHead3D
cmp eax,npcSearchTail3D
jae npc_plan_fail
mov esi,npcQueue3D[eax*4]
inc npcSearchHead3D
cmp esi,npcSearchGoal3D
je npc_plan_found
mov eax,esi
and eax,31
test eax,eax
jz @F
lea eax,[esi-1]
invoke NpcTryCell3D,eax,esi
@@:
mov eax,esi
and eax,31
cmp eax,31
je @F
lea eax,[esi+1]
invoke NpcTryCell3D,eax,esi
@@:
cmp esi,32
jb @F
lea eax,[esi-32]
invoke NpcTryCell3D,eax,esi
@@:
cmp esi,NPC_PATH_CAP-32
jae npc_plan_search
lea eax,[esi+32]
invoke NpcTryCell3D,eax,esi
jmp npc_plan_search
npc_plan_found:
mov eax,slot
imul eax,NPC_PATH_CAP*4
add eax,OFFSET npcPaths3D
mov pathBase,eax
xor edi,edi
npc_plan_reverse:
cmp esi,firstCell
je npc_plan_success
cmp edi,NPC_PATH_CAP
jae npc_plan_fail
mov edx,pathBase
mov [edx+edi*4],esi
inc edi
mov esi,npcParents3D[esi*4]
jmp npc_plan_reverse
npc_plan_success:
mov npcLife3D[ebx].pathCount,edi
mov eax,destination
mov npcLife3D[ebx].destination,eax
mov eax,1
ret
npc_plan_fail:
xor eax,eax
ret
NpcPlanPath3D ENDP
NpcCanStep3D PROC uses ebx esi slot:DWORD,px:DWORD,py:DWORD
invoke CanWalk3D,px,py
test eax,eax
jz npc_space_done
mov eax,px
sub eax,playerX
jns @F
neg eax
@@:
cmp eax,FP_1*3/10
jae npc_space_people
mov eax,py
sub eax,playerY
jns @F
neg eax
@@:
cmp eax,FP_1*3/10
jb npc_space_blocked
npc_space_people:
xor esi,esi
npc_space_person:
cmp esi,slot
je npc_space_next
imul ebx,esi,SIZEOF NpcLife3D
cmp npcLife3D[ebx].state,NPC_AWAY
je npc_space_next
invoke NpcEntity3D,esi
mov ebx,eax
mov eax,px
sub eax,[ebx].NpcMindEntity.posX
jns @F
neg eax
@@:
cmp eax,FP_1*2/5
jae npc_space_next
mov eax,py
sub eax,[ebx].NpcMindEntity.posY
jns @F
neg eax
@@:
cmp eax,FP_1*2/5
jb npc_space_blocked
npc_space_next:
inc esi
cmp esi,NPC_LIFE_COUNT
jb npc_space_person
mov eax,1
ret
npc_space_blocked:
xor eax,eax
npc_space_done:
ret
NpcCanStep3D ENDP
; Millisecond movement, carrying the remainder and consuming whole route cells.
NpcMove3D PROC uses ebx esi edi slot:DWORD,milliseconds:DWORD
local budget:DWORD,wx:DWORD,wy:DWORD,mind:DWORD,pathBase:DWORD
local oldX:DWORD,oldY:DWORD
imul ebx,slot,SIZEOF NpcLife3D
invoke NpcEntity3D,slot
mov esi,eax
mov mind,esi
mov eax,[esi].NpcMindEntity.posX
mov oldX,eax
mov eax,[esi].NpcMindEntity.posY
mov oldY,eax
mov eax,slot
imul eax,NPC_PATH_CAP*4
add eax,OFFSET npcPaths3D
mov pathBase,eax
mov eax,milliseconds
mov ecx,slot
mov ecx,npcVisitSpeed3D[ecx*4]
mul ecx
add eax,npcLife3D[ebx].remainder
adc edx,0
mov ecx,1000
div ecx
mov npcLife3D[ebx].remainder,edx
mov budget,eax
npc_move_cell:
mov eax,npcLife3D[ebx].pathCount
test eax,eax
jz npc_move_arrived
dec eax
mov edx,pathBase
mov edi,[edx+eax*4]
mov eax,edi
and eax,31
shl eax,16
add eax,FP_HALF
mov wx,eax
mov eax,edi
shr eax,5
shl eax,16
add eax,FP_HALF
mov wy,eax
mov esi,mind
mov eax,wx
sub eax,[esi].NpcMindEntity.posX
mov edi,1
test eax,eax
jns @F
neg eax
mov edi,-1
@@:
test eax,eax
jz npc_move_y
cmp eax,budget
jbe @F
mov eax,budget
@@:
sub budget,eax
imul eax,edi
add eax,[esi].NpcMindEntity.posX
mov wx,eax
invoke NpcCanStep3D,slot,wx,[esi].NpcMindEntity.posY
test eax,eax
jz npc_move_blocked
mov eax,wx
mov [esi].NpcMindEntity.posX,eax
npc_move_y:
mov eax,wy
sub eax,[esi].NpcMindEntity.posY
mov edi,1
test eax,eax
jns @F
neg eax
mov edi,-1
@@:
cmp eax,budget
jbe @F
mov eax,budget
@@:
sub budget,eax
imul eax,edi
add eax,[esi].NpcMindEntity.posY
mov wy,eax
invoke NpcCanStep3D,slot,[esi].NpcMindEntity.posX,wy
test eax,eax
jz npc_move_blocked
mov eax,wy
mov [esi].NpcMindEntity.posY,eax
mov edx,pathBase
mov eax,npcLife3D[ebx].pathCount
dec eax
mov edi,[edx+eax*4]
mov eax,[esi].NpcMindEntity.posX
sub eax,FP_HALF
sar eax,16
mov edx,[esi].NpcMindEntity.posY
sub edx,FP_HALF
sar edx,16
shl edx,5
add eax,edx
cmp eax,edi
jne npc_move_done
; Integer cell membership is insufficient: finish at the exact cell centre.
mov eax,edi
and eax,31
shl eax,16
add eax,FP_HALF
cmp eax,[esi].NpcMindEntity.posX
jne npc_move_done
mov eax,edi
shr eax,5
shl eax,16
add eax,FP_HALF
cmp eax,[esi].NpcMindEntity.posY
jne npc_move_done
dec npcLife3D[ebx].pathCount
cmp budget,0
jne npc_move_cell
cmp npcLife3D[ebx].pathCount,0
jne npc_move_done
npc_move_arrived:
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
mov ebx,slot
mov eax,[esi].NpcMindEntity.posX
sub eax,oldX
mov ecx,eax
mov eax,[esi].NpcMindEntity.posY
sub eax,oldY
mov edi,eax
or eax,ecx
jz npc_move_no_stride
mov npcVisitBlocked3D[ebx*4],0
mov npcWalkVelocityX3D[ebx*4],ecx
mov npcWalkVelocityY3D[ebx*4],edi
mov eax,ecx
cdq
xor eax,edx
sub eax,edx
mov ecx,eax
mov eax,edi
cdq
xor eax,edx
sub eax,edx
add eax,ecx
add eax,npcWalkDistance3D[ebx*4]
xor edx,edx
mov ecx,FP_1*3/4
div ecx
mov npcWalkDistance3D[ebx*4],edx
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
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
mov eax,250
@@:
mov milliseconds,eax
add npcLifeClock3D,eax
xor esi,esi
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
je npc_update_move
sub npcRise3D[esi*4],eax
ja npc_update_sync
mov npcRise3D[esi*4],0
jmp npc_update_sync
npc_update_move:
invoke NpcMove3D,esi,milliseconds
jmp npc_update_sync
npc_update_wait:
sub npcLife3D[ebx].remaining,eax
ja npc_update_sync
invoke NpcAdvanceVisit3D,esi
npc_update_sync:
invoke NpcEntity3D,esi
mov edi,eax
mov eax,npcLife3D[ebx].state
mov [edi].NpcMindEntity.state,eax
mov eax,npcLife3D[ebx].remaining
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,[edi].NpcMindEntity.posX
mov hallGuests3D[ecx].posX,eax
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
npc_update_done:
ret
UpdateNpcLife3D ENDP
RenderNpcLife3D PROC uses ebx esi edi
local castId:DWORD,castAction:DWORD,castClock:DWORD,slot:DWORD
cmp npcLifeReady3D,1
jne npc_draw_done
xor esi,esi
npc_draw_depth:
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
cmp esi,NPC_LIFE_COUNT
jb npc_draw_depth
xor esi,esi
npc_draw_sort:
lea edi,[esi+1]
npc_draw_compare:
mov eax,npcDrawOrder3D[esi*4]
mov ebx,npcDrawDepth3D[eax*4]
mov ecx,npcDrawOrder3D[edi*4]
cmp ebx,npcDrawDepth3D[ecx*4]
jge @F
mov npcDrawOrder3D[esi*4],ecx
mov npcDrawOrder3D[edi*4],eax
@@:
inc edi
cmp edi,NPC_LIFE_COUNT
jb npc_draw_compare
inc esi
cmp esi,NPC_LIFE_COUNT-1
jb npc_draw_sort
xor esi,esi
npc_draw_person:
mov eax,npcDrawOrder3D[esi*4]
imul ebx,eax,SIZEOF NpcLife3D
cmp npcLife3D[ebx].state,NPC_AWAY
je npc_draw_next
invoke NpcEntity3D,eax
mov edi,eax
mov eax,[edi].NpcMindEntity.posX
mov npcSprite3D.posX,eax
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_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 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:
cmp npcLife3D[ebx].state,NPC_WALK
jne npc_draw_sprite
mov ebx,slot
cmp npcRise3D[ebx*4],0
je npc_draw_moving
mov castAction,CAST_STAND_UP
mov eax,800
sub eax,npcRise3D[ebx*4]
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
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
jb npc_draw_person
npc_draw_done:
ret
RenderNpcLife3D ENDP
CheckNpcInteractions3D PROC uses ebx esi edi
local distance:DWORD,nearest:DWORD,deltaX:DWORD,deltaY:DWORD
cmp npcLifeReady3D,1
jne npc_interact_done
cmp hallLevel3D,0
jne npc_interact_done
cmp interactTargetId,0
jne npc_interact_done
mov npcSelected3D,-1
mov nearest,165000
xor esi,esi
npc_interact_person:
imul ebx,esi,SIZEOF NpcLife3D
cmp npcLife3D[ebx].state,NPC_AWAY
je npc_interact_next
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,[edi].NpcMindEntity.posY
sub eax,playerY
sar eax,8
mov deltaY,eax
imul eax,eax
add eax,distance
cmp eax,nearest
jae npc_interact_next
mov distance,eax
mov eax,playerDirX
sar eax,8
imul eax,deltaX
mov edx,playerDirY
sar edx,8
imul edx,deltaY
add eax,edx
cmp eax,6000
jl npc_interact_next
; Short, bounded line-of-sight test prevents talking through a wall.
mov ebx,1
npc_interact_ray:
mov eax,deltaX
imul eax,ebx
shl eax,5
add eax,playerX
sar eax,16
mov edx,deltaY
imul edx,ebx
shl edx,5
add edx,playerY
sar edx,16
cmp eax,32
jae npc_interact_next
cmp edx,32
jae npc_interact_next
shl edx,5
add edx,eax
mov ecx,worldMap3D
cmp BYTE PTR [ecx+edx],0
jne npc_interact_next
inc ebx
cmp ebx,8
jb npc_interact_ray
mov eax,distance
mov nearest,eax
mov npcSelected3D,esi
npc_interact_next:
inc esi
cmp esi,NPC_LIFE_COUNT
jb npc_interact_person
cmp npcSelected3D,-1
je npc_interact_done
mov interactTargetId,INTERACT_NPC
mov interactPromptPtr,OFFSET npcTalkPrompt3D
npc_interact_done:
ret
CheckNpcInteractions3D ENDP
include src_graphics\npc_visits.inc