-- DESCRIPTION: Do not use this script directly, instead call it from one of the sub AI scripts. local module_character = {} -- helps with level sound control (soundinzine script) g_soundinzone_currentlyplayinge = 0 -- move modes g_module_character_movemode_stood = 0 g_module_character_movemode_homing = 1 g_module_character_movemode_waypoint = 2 -- main properties g_module_character_name = {} g_module_character_mode = {} g_module_character_speech = {} g_module_character_lastspeech = {} g_module_character_movex = {} g_module_character_movey = {} g_module_character_movez = {} g_module_character_movestopdist = {} -- waypoint specific properties g_module_character_pathmode = {} g_module_character_pathindex = {} g_module_character_pathpointindex = {} g_module_character_pathpointdirection = {} function module_character.init(e,name) -- initialize character CharacterControlManual(e) local AIObjNo = g_Entity[e]['obj'] AISetEntityControl(AIObjNo,AI_MANUAL) -- properties reset g_module_character_name[e] = name g_module_character_mode[e] = -2 g_module_character_speech[e] = 0 g_module_character_lastspeech[e] = 0 g_module_character_movex[e] = -1 g_module_character_movey[e] = -1 g_module_character_movez[e] = -1 g_module_character_movestopdist[e] = -1 -- waypoint reset g_module_character_pathmode[e] = -1 g_module_character_pathindex[e] = 0 g_module_character_pathpointindex[e] = 0 g_module_character_pathpointdirection[e] = 0 end g_leelee = 0 function module_character.main(e,tMoveMode,tSubMode,tRange,tParam2,tDontLookAtPlayer,fSpeakDistance,fSpeechText,fSpeechDuration) -- handle character logic local AIObjNo = g_Entity[e]['obj'] local PlayerDist = GetPlayerDistance(e) local fGoDistance = AIGetEntityViewRange(AIObjNo) if fSpeechText == nil then fSpeechText = "" end if fGoDistance < 200 then fGoDistance = 200 end local tTriggerMove = tTriggerMove or 0 if g_PlayerHealth > 0 then if PlayerDist < fGoDistance*1.25 then if PlayerDist < fSpeakDistance+50 and fSpeakDistance > 0 then -- trigger speech if g_module_character_speech[e] == 0 then g_module_character_speech[e] = 1 -- works with soundinzine global so only one sound plays at once! if g_soundinzone_currentlyplayinge ~= 0 then StopSound(g_soundinzone_currentlyplayinge,0) StopSound(g_soundinzone_currentlyplayinge,1) StopSound(g_soundinzone_currentlyplayinge,2) StopSound(g_soundinzone_currentlyplayinge,3) g_soundinzone_currentlyplayinge = 0 end g_soundinzone_currentlyplayinge = e PlaySound(e,1) PlaySpeech(e,1) --PromptDuration(fSpeechText, fspeechduration) if tMoveMode == g_module_character_movemode_waypoint then g_module_character_movestopdist[e] = fSpeakDistance+50 end end else if g_module_character_speech[e] ~= 99 then -- different movement triggers if g_module_character_mode[e] < 12345 then -- reserved for the future if tMoveMode == g_module_character_movemode_homing and g_module_character_mode[e] == 0 and PlayerDist < fGoDistance then -- homing movemode if tSubMode == 0 then -- standard sub-mode g_module_character_movex[e] = g_PlayerPosX g_module_character_movey[e] = g_Entity[e]['y'] g_module_character_movez[e] = g_PlayerPosZ g_module_character_movestopdist[e] = fSpeakDistance tTriggerMove = 1 elseif tSubMode == 1 then -- reverse homing if PlayerDist < tRange then local pDX = g_Entity[e]['x'] - g_PlayerPosX local pDZ = g_Entity[e]['z'] - g_PlayerPosZ local pDA = math.atan2(pDX,pDZ) g_module_character_movex[e] = g_Entity[e]['x'] + (math.sin(pDA)*tParam2) g_module_character_movey[e] = g_Entity[e]['y'] g_module_character_movez[e] = g_Entity[e]['z'] + (math.cos(pDA)*tParam2) g_module_character_movestopdist[e] = 100 tTriggerMove = 1 end elseif tSubMode == 2 then -- random wondering if PlayerDist < tRange then local pDA = math.random(0,360) local pDD = math.random(100,500) local pTryX = g_Entity[e]['x'] + (math.sin(pDA)*pDD) local pTryY = g_Entity[e]['y'] + 20; local pTryZ = g_Entity[e]['z'] + (math.cos(pDA)*pDD) if IntersectAll(g_Entity[e]['x'],g_Entity[e]['y']+20,g_Entity[e]['z'],pTryX,pTryY,pTryZ,g_Entity[e]['obj']) == 0 then g_module_character_movex[e] = pTryX g_module_character_movey[e] = g_Entity[e]['y'] g_module_character_movez[e] = pTryZ g_module_character_movestopdist[e] = 100 tTriggerMove = 1 end end end end end end end end end --------------------- if tMoveMode == g_module_character_movemode_waypoint and PlayerDist < fGoDistance then -- waypoint movemode if g_module_character_pathmode[e] == -1 and PlayerDist < tRange then -- find closest waypoint local PathIndex = -1 local PointIndex = 2 local pClosest = 99999 for pa = 1, AIGetTotalPaths(), 1 do for po = 1, AIGetPathCountPoints(pa), 1 do local pDX = g_Entity[e]['x'] - AIPathGetPointX(pa,po) local pDY = g_Entity[e]['y'] - AIPathGetPointY(pa,po) local pDZ = g_Entity[e]['z'] - AIPathGetPointZ(pa,po) local pDist = math.sqrt(math.abs(pDX*pDX)+math.abs(pDY*pDY)+math.abs(pDZ*pDZ)); if pDist < pClosest and pDist < 200 then pClosest = pDist PathIndex = pa PointIndex = po end end end if PathIndex > -1 then -- start moving to point within waypoint g_module_character_pathindex[e] = PathIndex g_module_character_pathpointindex[e] = PointIndex g_module_character_pathpointdirection[e] = 0 g_module_character_pathmode[e] = 2 end end if g_module_character_pathmode[e] == 2 then local patrolx = AIPathGetPointX(g_module_character_pathindex[e],g_module_character_pathpointindex[e]) local patroly = AIPathGetPointY(g_module_character_pathindex[e],g_module_character_pathpointindex[e]) local patrolz = AIPathGetPointZ(g_module_character_pathindex[e],g_module_character_pathpointindex[e]) g_module_character_movex[e] = patrolx g_module_character_movey[e] = patroly g_module_character_movez[e] = patrolz g_module_character_movestopdist[e] = -1 g_module_character_pathmode[e] = 1 tTriggerMove = 1 end if g_module_character_pathmode[e] == 1 then local pDX = g_Entity[e]['x'] - AIPathGetPointX(g_module_character_pathindex[e],g_module_character_pathpointindex[e]) local pDZ = g_Entity[e]['z'] - AIPathGetPointZ(g_module_character_pathindex[e],g_module_character_pathpointindex[e]) local pDD = math.abs(pDX)+math.abs(pDZ) tTriggerMove = 1 local temprange = 100 if ai_bot_state[e] == "avoid" then temprange = temprange * 2 end if pDD < temprange then -- move to next point g_module_character_pathmode[e] = 2 if g_module_character_pathpointdirection[e] == 0 then if g_module_character_pathpointindex[e] >= AIGetPathCountPoints(g_module_character_pathindex[e]) then g_module_character_pathpointdirection[e] = 1 if tSubMode == 1 then g_module_character_pathmode[e] = 98 end else g_module_character_pathpointindex[e] = g_module_character_pathpointindex[e] + 1 end else if g_module_character_pathpointindex[e] <= 1 then g_module_character_pathpointdirection[e] = 0 if tSubMode == 1 then g_module_character_pathmode[e] = 98 end else g_module_character_pathpointindex[e] = g_module_character_pathpointindex[e] - 1 end end end end end ------------------- if g_module_character_pathmode[e] == 98 then -- allow waypoint following to come to an end g_module_character_movestopdist[e] = 100 g_module_character_pathmode[e] = 99 end if tTriggerMove == 1 then -- trigger walking when not in conversation anim or idle if g_module_character_speech[e] == 0 or g_module_character_speech[e] == 11 then if g_module_character_mode[e] < 50 then g_module_character_mode[e] = 50 end --check for dynamic obstacles - code added by smallg ai_bot_state = ai_bot_state or {} ai_bot_state[e] = ai_bot_state[e] or "none" timer = timer or {} timer[e] = timer[e] or {} --timer[e][1] = timer[e][1] or GetTimer(e) + 1250 local raydist = 75 --how far in front of the NPC the raycast looks local checkangle = 10 --how wide the ray cast is (to account for NPC width) local checkdelay = 100 --gives time for the NPC to try move around current obstacle --*if checkdelay is too high we may end up hitting other obstacles while avoiding the previous one so may want to add code to adjust by obstacle distance* local checkheight = 16 --moves ray off the ground so we can still step on lower obstacles local tdist = tdist or {} local blocked = blocked or {} destx = destx or {} desty = desty or {} destz = destz or {} local oldstate = oldstate or {} local movespeed = movespeed or {} local rotatespeed = rotatespeed or {} --rotatespeed[e] = rotatespeed[e] or 25 --movespeed[e] = movespeed[e] or AIGetEntitySpeed(AIObjNo) * 0.8 blocked[e] = blocked[e] or {} local cx,cy,cz,cax,cay,caz = GetEntityPosAng(e) local x1,y1,z1,x2,y2,z2 = {},{},{},{},{},{} local ya1,ya2,ya3 = math.rad(cay-checkangle), math.rad(cay), math.rad(cay+checkangle) --local brl = {} --the 3 main rays check ahead in a cone defined by the settings above for a = 1, 3 do x1[a],y1[a],z1[a] = cx,cy+checkheight,cz if a == 1 then --left x2[a] = cx + (math.sin(ya1) * raydist) y2[a] = cy + checkheight z2[a] = cz + (math.cos(ya1) * raydist) --brl[a] = 8 --CollisionOff(brl[a]) --SetPosition(brl[a],x2[a],y2[a],z2[a]) elseif a == 2 then --straight x2[a] = cx + (math.sin(ya2) * raydist) y2[a] = cy + checkheight z2[a] = cz + (math.cos(ya2) * raydist) --brl[a] = 9 --CollisionOff(brl[a]) --SetPosition(brl[a],x2[a],y2[a],z2[a]) elseif a == 3 then --right x2[a] = cx + (math.sin(ya3) * raydist) y2[a] = cy + checkheight z2[a] = cz + (math.cos(ya3) * raydist) --brl[a] = 10 --CollisionOff(brl[a]) --SetPosition(brl[a],x2[a],y2[a],z2[a]) end --check if there's an obstacle blocking the ray blocked[e][a] = IntersectAll(x1[a], y1[a], z1[a], x2[a], y2[a], z2[a], obj) if blocked[e][a] == nil then blocked[e][a] = 0 end local dynamic = false if blocked[e][a] > 0 then local function GetDistance(x1, z1, x2, z2) local dx, dz = x1 - x2, z1 - z2 return (dx*dx)+(dz*dz) end P = P or require "scriptbank\\physlib" local be = P.ObjectToEntity(blocked[e][a]) if be ~= nil then --check the obstacle is dynamic (not static) if g_Entity[be]['obj'] ~= nil then dynamic = true local x3, z3 = {},{} tdist[a] = raydist --get the point of collision and how far away it is x3[a], z3[a] = GetIntersectCollisionX(), GetIntersectCollisionZ() tdist[a] = GetDistance(x1[a], z1[a], x3[a], z3[a]) tdist[a] = math.sqrt(tdist[a]) end end if dynamic == false then --we can ignore static entitys as the normal pathing will handle those blocked[e][a] = 0 end end end local avoiding = 0 for a = 1, 3 do if blocked[e][a] > 0 then avoiding = a --SetRotation(brl[a],90,0,0) else --SetRotation(brl[a],0,0,0) end end if avoiding > 0 then if ai_bot_state[e] ~= "avoid" then --only update the destination every now and then local temp = 35 --the starting angle to check for free space (should be wide for closer obstacles) local pathsblocked = 0 local left = 0 --checks right then left repeat local tay = 0 if left == 0 then left = 1 tay = cay+temp else left = 0 tay = cay-temp temp = temp + 5 end pathsblocked = 0 x1[e] = cx y1[e] = cy+checkheight z1[e] = cz --checks a bit around the current angle offset to try reduce chance of getting stuck on corners (i.e. trying to account for NPC width again) for a = -1, 1 do ay = math.rad(tay+a+checkangle) x2[e] = cx + (math.sin(ay) * raydist) y2[e] = cy + checkheight z2[e] = cz + (math.cos(ay) * raydist) blocked[e][a] = IntersectAll(x1[e],y1[e],z1[e],x2[e],y2[e],z2[e],AIObjNo) if blocked[e][a] == nil then blocked[e][a] = 0 end if blocked[e][a] > 0 then pathsblocked = pathsblocked + 1 end end until pathsblocked == 0 or temp > 70 --if path out is found or angle is too sharp we should just attempt to move destx[e] = x2[e] desty[e] = cy destz[e] = z2[e] rotatespeed[e] = temp --update the NPC visual movespeed and rotation speed for later if rotatespeed[e] < 15 then rotatespeed[e] = 15 end timer[e][1] = GetTimer(e) + checkdelay movespeed[e] = AIGetEntitySpeed(AIObjNo) * 0.3 oldstate[e] = ai_bot_state[e] ai_bot_state[e] = "avoid" end end --handle moving around obstacle and exit avoid state if enough time has passed if ai_bot_state[e] == "avoid" then if GetTimer(e) > timer[e][1] then ai_bot_state[e] = "none" end AIEntityGoToPosition(AIObjNo, destx[e], desty[e], destz[e]) SetRotationYSlowly(e, AIGetEntityAngleY(AIObjNo), rotatespeed[e]) --SetRotation(e,0,AIGetEntityAngleY(AIObjNo),0) MoveForward(e, movespeed[e]) AISetEntityPosition(AIObjNo,GetEntityPositionX(e),GetEntityPositionY(e),GetEntityPositionZ(e)) else AIEntityGoToPosition(AIObjNo,g_module_character_movex[e],g_module_character_movey[e],g_module_character_movez[e]) AISetEntityPosition(AIObjNo,GetEntityPositionX(e),GetEntityPositionY(e),GetEntityPositionZ(e)) end --if destx[e] ~= nil then --Prompt(destx[e].." , "..destz[e]) --end end if ai_bot_state == nil then ai_bot_state = {} ai_bot_state[e] = "none" end --PromptLocal(e,ai_bot_state[e]) else -- resets and freezes character when out of range g_module_character_mode[e] = -1 if g_module_character_speech[e] ~= 0 then g_module_character_speech[e] = 0 StopSound(e,1) StopSpeech(e) end -- handle turning and head tracking local tIgnorePlayer = 0 if tMoveMode == g_module_character_movemode_stood and tSubMode == 1 then tIgnorePlayer = 1 end if tDontLookAtPlayer == 1 then tIgnorePlayer = 1 end if PlayerDist < fGoDistance*1.25 and tIgnorePlayer == 0 then local tDistX = g_PlayerPosX - g_Entity[e]['x'] local tDistZ = g_PlayerPosZ - g_Entity[e]['z'] local tDA2 = math.deg(math.atan2(tDistX,tDistZ)) local tDA = tDA2 - g_Entity[e]['angley'] if tDA <= -180 then tDA=tDA+360 end if tDA >= 180 then tDA=tDA-360 end if tDA <= -180 then tDA=tDA+360 end if tDA >= 180 then tDA=tDA-360 end local tNeckLimit = 70 local tFull90DegreeTurn = 0 if g_module_character_mode[e] == 0 then if tDA < -tNeckLimit then tFull90DegreeTurn = 3 g_module_character_mode[e] = tFull90DegreeTurn end if tDA > tNeckLimit then tFull90DegreeTurn = 2 g_module_character_mode[e] = tFull90DegreeTurn end if tFull90DegreeTurn == 0 then LookAtPlayer(e,10) else SetAnimFromAnimSet(e,g_module_character_mode[e]) PlayAnimation(e) end else if g_module_character_mode[e] == 2 or g_module_character_mode[e] == 3 then LookForward(e,20) local tFrame = GetAnimationFrame(e) local tStart = GetEntityAnimationStart(e,g_module_character_mode[e]) local tFinish = GetEntityAnimationFinish(e,g_module_character_mode[e]) if tFrame >= tFinish then if g_module_character_mode[e] == 2 then g_module_character_mode[e] = -1 StopAnimation(e) SetAnimationFrame(e,tStart) ResetRotation(e,0,g_Entity[e]['angley']+90,0) end if g_module_character_mode[e] == 3 then g_module_character_mode[e] = -1 StopAnimation(e) SetAnimationFrame(e,tStart) ResetRotation(e,0,g_Entity[e]['angley']-90,0) end end end end else -- slowly restore head direction if out of close range LookForward(e,1.0) end end -- walking logic if g_module_character_mode[e] >= 50 and g_module_character_mode[e] <= 53 then if AIGetEntityIsMoving(AIObjNo) == 1 then --recalc path in bursts if GetTimer(e) > 200 and PlayerDist < fGoDistance and ai_bot_state[e] ~= "avoid" then AIEntityGoToPosition(AIObjNo,g_module_character_movex[e],g_module_character_movey[e],g_module_character_movez[e]) StartTimer(e) end -- smooth out rotation a touch local tAngleY = AIGetEntityAngleY(AIObjNo) if tAngleY >= 180 then tAngleY=tAngleY-360 end if tAngleY < -180 then tAngleY=tAngleY+360 end local tCurrentY = GetEntityAngleY(e) if tCurrentY >= 180 then tCurrentY=tCurrentY-360 end if tCurrentY < -180 then tCurrentY=tCurrentY+360 end local tDiffY = tAngleY - tCurrentY if tDiffY < -180 then tDiffY=tDiffY+360 end if tDiffY >= 180 then tDiffY=tDiffY-360 end tDiffY = tDiffY / 10.0 tDiffY = tCurrentY + tDiffY SetRotation(e,0,tDiffY,0) -- always attempt to glance at player while walking if tDontLookAtPlayer == 0 then LookAtPlayer(e,20) end -- keep moving closer if g_module_character_mode[e] == 50 then MoveForward(e,0) end if g_module_character_mode[e] == 51 then MoveForward(e,AIGetEntitySpeed(AIObjNo)*0.2) end if g_module_character_mode[e] == 52 then MoveForward(e,AIGetEntitySpeed(AIObjNo)*1.0) end if g_module_character_mode[e] == 53 then MoveForward(e,0) end AISetEntityPosition(AIObjNo,GetEntityPositionX(e),GetEntityPositionY(e),GetEntityPositionZ(e)) end -- walk anims local tFrame = GetAnimationFrame(e) if g_module_character_mode[e] == 50 then SetAnimationFrame(e,tStart) SetAnimFromAnimSet(e,11) PlayAnimation(e) g_module_character_mode[e] = 51 else if g_module_character_mode[e] == 51 then local tStart = GetEntityAnimationStart(e,11) local tFinish = GetEntityAnimationFinish(e,11) if tFrame >= tStart and tFrame >= tFinish then g_module_character_mode[e] = 52 SetAnimFromAnimSet(e,12) LoopAnimation(e) end else if g_module_character_mode[e] == 52 then -- anticipate end of path, so can trigger 'walk to idle' animation local tStopWalking = 0 local tx,ty,tz = g_module_character_movex[e], g_module_character_movey[e], g_module_character_movez[e] local DistToDest = GetDistanceTo(e,tx,ty,tz) if g_module_character_movestopdist[e] > -1 then if DistToDest < g_module_character_movestopdist[e] then tStopWalking = 1 end end local tFinish = GetEntityAnimationFinish(e,12) if tFrame >= tFinish-2 then if tStopWalking == 1 then g_module_character_mode[e] = 53 SetAnimFromAnimSet(e,13) PlayAnimation(e) tStart = GetEntityAnimationStart(e,13) SetAnimationFrame(e,tStart) end end else if g_module_character_mode[e] == 53 then tFinish = GetEntityAnimationFinish(e,13) if tFrame >= tFinish then g_module_character_mode[e] = -1 end end end end end end -- startup animation to calm spawning into game if g_module_character_mode[e] == -1 then g_module_character_mode[e] = 0 SetAnimFromAnimSet(e,0) LoopAnimation(e) else if g_module_character_mode[e] == -2 then g_module_character_mode[e] = -3 SetAnimFromAnimSet(e,0) PlayAnimation(e) else if g_module_character_mode[e] == -3 then if tDontLookAtPlayer == 0 then LookAtPlayer(e,10) end local tFrame = GetAnimationFrame(e) local tFinish = GetEntityAnimationFinish(e,0) if tFrame >= tFinish then g_module_character_mode[e] = -1 end end end end -- conversation gestures if g_module_character_mode[e] == 0 then if g_module_character_speech[e] ~= 0 then if g_module_character_speech[e] == 1 then local tSpeech = math.random(7,10) while g_module_character_lastspeech[e] == tSpeech do tSpeech = math.random(7,10) end g_module_character_lastspeech[e] = tSpeech g_module_character_speech[e] = tSpeech SetAnimFromAnimSet(e,g_module_character_speech[e]) PlayAnimation(e) SetAnimationFrame(e,g_module_character_speech[e]) else if g_module_character_speech[e] >= 7 and g_module_character_speech[e] <= 10 then local tFrame = GetAnimationFrame(e) local tStart = GetEntityAnimationStart(e,g_module_character_speech[e]) local tFinish = GetEntityAnimationFinish(e,g_module_character_speech[e]) if tFrame < tStart or tFrame >= tFinish then g_module_character_speech[e] = 11 SetAnimFromAnimSet(e,0) LoopAnimation(e) StartTimer(e) end else if g_module_character_speech[e] == 11 then local tTimeCheck = GetTimer(e) if tTimeCheck > 2500 then local tFrame = GetAnimationFrame(e) local tFinish = GetEntityAnimationFinish(e,0) if tFrame >= tFinish-1 then g_module_character_speech[e] = 1 end end end end end end end -- deactivate speech once completed if g_module_character_speech[e] == 11 and GetSpeech(e) == 0 then g_module_character_speech[e] = 99 end end function module_character.countaiaroundplayer() local tcountai = 0 for ee = 1 , g_EntityElementMax, 1 do if g_Entity[ee] ~= nil then if ai_bot_state[ee] ~= nil then local pDX = g_Entity[ee]['x'] - g_PlayerPosX local pDY = g_Entity[ee]['y'] - g_PlayerPosY local pDZ = g_Entity[ee]['z'] - g_PlayerPosZ local pDist = math.sqrt(math.abs(pDX*pDX)+math.abs(pDY*pDY)+math.abs(pDZ*pDZ)); if pDist < 100 then tcountai = tcountai + 1 end end end end return tcountai end return module_character