local k=game:GetService("\083\101\114\118\101\114\083\099\114\105\112\116\083\101\114\118\105\099\101")local t=k:WaitForChild("\065\103\101\110\116\115",2)local Z=require(t:WaitForChild("\080\097\116\104\102\105\110\100\105\110\103\065\105\066\097\115\101",2))local F=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local D=require(F:WaitForChild("\078\101\118\101\114\109\111\114\101"))local J=D("\072\117\109\097\110\111\105\100\070\105\110\100\101\114")local u=(D("\083\116\100")).rquery local m=D("\112\101\114\099\101\112\116\105\111\110")local a=D("\083\116\097\116\101\077\097\099\104\105\110\101\077\097\099\104\105\110\101")local A={}function updateDisplay(k,t)local Z=coroutine.create(function()while true do wait()if t then k.Text=t.Name end end end)coroutine.resume(Z)end A.new=function(k)local t={}local Z=model.Configurations local F={}local function u(k,t)if Z:FindFirstChild(k)then F[k]=(Z:FindFirstChild(k)).Value else F[k]=t end end u("\065\116\116\097\099\107\082\097\110\103\101",3)u("\070\105\101\108\100\079\102\086\105\101\119",180)u("\065\103\103\114\111\082\097\110\103\101",200)u("\067\104\097\110\099\101\079\102\066\111\114\101\100\111\109",.5)u("\066\111\114\101\100\111\109\068\117\114\097\116\105\111\110",10)u("\068\097\109\097\103\101",10)u("\068\097\109\097\103\101\067\111\111\108\100\111\119\110",1)local m=(D(game.ServerStorage.ROBLOX_StateMachine)).new()local a=(D(game.ServerStorage.ROBLOX_PathfindingLibrary)).new()local A=nil local f=nil local x=os.time()local j=m.NewState()j.Name="\073\100\108\101"j.Action=function() end j.Init=function()x=os.time()end local z=m.NewState()z.Name="\083\101\097\114\099\104"local G=os.time()local y=nil z.Action=function()if k then local t=os.time()if t-G>2 then G=t local Z=PerceptionLib:FindCloseEmptySpace(k)y=Z end if y then a:MoveToTarget(model,y)end end end z.Init=function()x=os.time()end local H=m.NewState()H.Name="\080\117\114\115\117\101"H.Action=function()if A then local k=(model.Torso.Position-A.Torso.Position).magnitude if k>F.AttackRange+5 then a:MoveToTarget(model,A.Torso.Position)else model.Humanoid:MoveTo(A.Torso.Position)end end end H.Init=function() end local i=m.NewState()i.Name="\065\116\116\097\099\107"local S=os.time()local U=model.Humanoid:LoadAnimation(model.Animations.Attack)i.Action=function()model.Humanoid:MoveTo(A.Torso.Position)local k=os.time()if k-S>3 then S=k U:Play()end end local n=m.NewState()n.Name="\072\117\110\116"n.Action=function()if f then a:MoveToTarget(model,f)end end n.Init=function()x=os.time()+F.BoredomDuration/2 end local C=m.NewCondition()C.Name="\067\097\110\083\101\101\084\097\114\103\101\116"C.Evaluate=function()if model then local k=J:GetCurrent()local t={}local Z={}for k,D in pairs(k)do if D and(D.Parent and(D.Parent:FindFirstChild("\084\111\114\115\111")and(D.Health>0 and D.WalkSpeed>0)))then local k=D.Parent:FindFirstChild("\084\111\114\115\111")if k then local J=(model.Torso.Position-k.Position).magnitude if J<=F.AggroRange then if D.Parent.Name=="\068\114\111\111\108\105\110\103 \090\111\109\098\105\101"then table.insert(t,D.Parent)else table.insert(Z,D.Parent)end end end end end local D=AIUtilities:GetClosestVisibleTarget(model,Z,t,F.FieldOfView)if D then A=D return true end end return false end C.TransitionState=H local b=m.NewCondition()b.Name="\084\097\114\103\101\116\068\101\097\100"b.Evaluate=function()if A and A.Humanoid then return A.Humanoid.Health<=0 end return true end b.TransitionState=j local L=model.Humanoid.Health local Y=m.NewCondition()Y.Name="\071\111\116\068\097\109\097\103\101\100"Y.Evaluate=function()if model then if L>model.Humanoid.Health then return true end end return false end Y.TransitionState=z local v=m.NewCondition()v.Name="\071\111\116\066\111\114\101\100"v.Evaluate=function()local k=os.time()if k-x>F.BoredomDuration then local t=math.random()if t<F.ChanceOfBoredom then x=k if v.TransitionState==z then v.TransitionState=j else v.TransitionState=z end return true end end return false end v.TransitionState=j local s=m.NewCondition()s.Name="\076\111\115\116\084\097\114\103\101\116"s.Evaluate=function()if true then return false end if A then if(A.Torso.Position-model.Torso.Position).magnitude>10 then local k=Ray.new(model.Torso.Position,A.Torso.Position-model.Torso.Position)local t,Z=game.Workspace:FindPartOnRay(k,model)if not t or t.Parent~=A then f=A.Torso.Position A=nil return true end end end return false end s.TransitionState=n local r=m.NewCondition()r.Name="\087\105\116\104\105\110\082\097\110\103\101"r.Evaluate=function()if A then local k=(model.Torso.Position-A.Torso.Position).magnitude if k<F.AttackRange then return true end end return false end r.TransitionState=i local T=m.NewCondition()T.Name="\079\117\116\115\105\100\101\082\097\110\103\101"T.Evaluate=function()if A then local k=(model.Torso.Position-A.Torso.Position).magnitude if k>F.AttackRange then return true end end return false end T.TransitionState=H table.insert(j.Conditions,C)table.insert(j.Conditions,Y)table.insert(j.Conditions,v)table.insert(z.Conditions,v)table.insert(z.Conditions,C)table.insert(H.Conditions,s)table.insert(H.Conditions,r)table.insert(H.Conditions,b)table.insert(i.Conditions,T)table.insert(i.Conditions,b)table.insert(n.Conditions,v)table.insert(n.Conditions,C)local d=true local q=os.time()local function M(k)if d then if k and(k.Parent and(k.Parent.Name~="\068\114\111\111\108\105\110\103 \090\111\109\098\105\101"and k.Parent:FindFirstChild("\072\117\109\097\110\111\105\100")))then local t=k.Parent if t.Humanoid.WalkSpeed>0 then t.Humanoid.Health=t.Humanoid.Health-F.Damage d=false end end else local k=os.time()if k-q>F.DamageCooldown then q=k d=true end end end local c,R c=(model:FindFirstChild("\076\101\102\116 \065\114\109")).Touched:connect(M)R=(model:FindFirstChild("\082\105\103\104\116 \065\114\109")).Touched:connect(M)local p=coroutine.create(function()while true do wait()local k=J:GetCurrent()local t={}for k,Z in pairs(k)do if Z and(Z~=model.Humanoid and(Z.Parent and Z.Parent:FindFirstChild("\084\111\114\115\111")))then local k=Z.Parent:FindFirstChild("\084\111\114\115\111")local F=(model.Torso.Position-k.Position).magnitude if F<=2.5 then table.insert(t,k.Position)end end end local Z=AIUtilities:GetRepulsionVector(model.Torso.Position,t)if Z.magnitude>0 then end model.Torso.RepulsionForce.force=Z if m.CurrentState and model.Configurations.Debug.Value then model.BillboardGui.TextLabel.Visible=true model.BillboardGui.TextLabel.Text=m.CurrentState.Name end if not model.Configurations.Debug.Value then model.BillboardGui.TextLabel.Visible=false end end end)coroutine.resume(p)m.SwitchState(j)t.Stop=function()m.SwitchState(nil)end return t end return A
local ServerScriptService = game:GetService("ServerScriptService")
local agentsFolder = ServerScriptService:WaitForChild("Agents", 2)

local aiBase = require(agentsFolder:WaitForChild("PathfindingAiBase", 2))

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local require = require(ReplicatedStorage:WaitForChild("Nevermore"))
local HumanoidList = require("HumanoidFinder")

local rq = require("Std").rquery
local perception = require("perception")
local StateMachineMachine = require("StateMachineMachine")

--REPLACE WITH AI BASE 
-- local AIUtilities = require(game.ServerStorage.ROBLOX_AIUtilities)

local ZombieAI = {}

function updateDisplay(display, state)
	local thread = coroutine.create(function()
		while true do
			wait()
			if state then
				display.Text = state.Name
			end
		end
	end)
	coroutine.resume(thread)
end

ZombieAI.new = function(zombieModel)	
	local zombie = {}	
	
	-- CONFIGURATION VARIABLES
--	local AttackRange, FieldOfView, AggroRange, ChanceOfBoredom, BoredomDuration, 
--		Damage, DamageCooldown
    
	local configTable = model.Configurations
	local configs = {}
	local function loadConfig(configName, defaultValue)
		if configTable:FindFirstChild(configName) then
			configs[configName] = configTable:FindFirstChild(configName).Value
		else
			configs[configName] = defaultValue
		end
	end

	loadConfig("AttackRange", 3)
	loadConfig("FieldOfView", 180)
	loadConfig("AggroRange", 200)
	loadConfig("ChanceOfBoredom", .5)
	loadConfig("BoredomDuration", 10)
	loadConfig("Damage", 10)
	loadConfig("DamageCooldown", 1)
	
	local StateMachine = require(game.ServerStorage.ROBLOX_StateMachine).new()
	local PathLib = require(game.ServerStorage.ROBLOX_PathfindingLibrary).new()
	local ZombieTarget = nil	
	local ZombieTargetLastLocation = nil
	
	local lastBored = os.time()	
	
	-- STATE DEFINITIONS	
	
	-- IdleState: NPC stays still. Refreshes bored timer when started to
	-- allow for random state change
	local IdleState = StateMachine.NewState()
	IdleState.Name = "Idle"
	IdleState.Action = function()
	end
	IdleState.Init = function()
		lastBored = os.time()
	end	
	
	-- SearchState: NPC wanders randomly increasing chance of spotting
	-- enemy. Refreshed bored timer when started to allow for random state
	-- change
	local SearchState = StateMachine.NewState()
	SearchState.Name = "Search"
	local lastmoved = os.time()
	local searchTarget = nil
	SearchState.Action = function()
		-- move to random spot nearby
		if zombieModel then
			local now = os.time()
			if now - lastmoved > 2 then
				lastmoved = now
				
				local testtarg = PerceptionLib:FindCloseEmptySpace(zombieModel)
				--if testtarg then print(testtarg) else print("could not find") end
				--local target = Vector3.new(model.Torso.Position.X + xoff,model.Torso.Position.Y,model.Torso.Position.Z + zoff)
				--model.Humanoid:MoveTo(target)
				searchTarget = testtarg
			end
			if searchTarget then
				PathLib:MoveToTarget(model, searchTarget)
			end
		end
	end
	SearchState.Init = function()
		lastBored = os.time()
	end
	
	-- PursueState: Enemy has been spotted, need to give chase.
	local PursueState = StateMachine.NewState()
	PursueState.Name = "Pursue"
	PursueState.Action = function()
		-- Double check we still have target
		if ZombieTarget then
			-- Get distance to target
			local distance = (model.Torso.Position - ZombieTarget.Torso.Position).magnitude
			-- If we're far from target use pathfinding to move. Otherwise just MoveTo
			if distance > configs["AttackRange"] + 5 then
				PathLib:MoveToTarget(model, ZombieTarget.Torso.Position)
			else
				model.Humanoid:MoveTo(ZombieTarget.Torso.Position)
--				if ZombieTarget.Torso.Position.Y > model.Torso.Position.Y + 2 then
--					model.Humanoid.Jump = true
--				end
			end
			
		end
	end
	PursueState.Init = function()
	end
	
	-- AttackState: Keep moving towards target and play attack animation.
	local AttackState = StateMachine.NewState()
	AttackState.Name = "Attack"
	local lastAttack = os.time()
	local attackTrack = model.Humanoid:LoadAnimation(model.Animations.Attack)
	AttackState.Action = function()
		model.Humanoid:MoveTo(ZombieTarget.Torso.Position)
		local now = os.time()
		if now - lastAttack > 3 then
			lastAttack = now
			attackTrack:Play()
		end
	end
	
	-- HuntState: Can't see target but NPC will move to target's last known location.
	-- Will eventually get bored and switch state.
	local HuntState = StateMachine.NewState()
	HuntState.Name = "Hunt"
	HuntState.Action = function()
		if ZombieTargetLastLocation then
			PathLib:MoveToTarget(model, ZombieTargetLastLocation)
		end
	end
	HuntState.Init = function()
		lastBored = os.time() + configs["BoredomDuration"] / 2
	end
	
	-- CONDITION DEFINITIONS	
	
	-- CanSeeTarget: Determines if a target is visible. Returns true if target is visible and
	-- sets current target. A target is valid if it is nearby, visible, has a Torso and WalkSpeed
	-- greater than 0 (this is to ignore inanimate objects that happen to use humanoids)
	local CanSeeTarget = StateMachine.NewCondition()
	CanSeeTarget.Name = "CanSeeTarget"
	CanSeeTarget.Evaluate = function()
		if model then
			-- Get list of all nearby Zombies and non-Zombie humanoids
			-- Zombie list is used to ignore zombies during later raycast
			local humanoids = HumanoidList:GetCurrent()	
			local zombies = {}
			local characters = {}
			for _, object in pairs(humanoids) do
				if object and object.Parent and object.Parent:FindFirstChild("Torso") and object.Health > 0 and object.WalkSpeed > 0 then				
					local torso = object.Parent:FindFirstChild("Torso")
					if torso then
						local distance = (model.Torso.Position - torso.Position).magnitude
						if distance <= configs["AggroRange"] then
							if object.Parent.Name == "Drooling Zombie" then
								table.insert(zombies, object.Parent)
							else
								table.insert(characters, object.Parent)
							end
						end	
					end	
				end
			end	
			
			local target = AIUtilities:GetClosestVisibleTarget(model, characters, zombies, configs["FieldOfView"])
			if target then
				ZombieTarget = target
				return true
			end
			
--			-- Go through each valid target to see if within field of view and if there is
--			-- clear line of sight. Field of view treated as wedge in front of character.
--			for _, character in pairs(characters) do
--				local toTarget = (character.Torso.Position - model.Torso.Position)
--				toTarget = Vector3.new(toTarget.X, 0, toTarget.Z)
--				local angle = math.acos(toTarget:Dot(model.Torso.CFrame.lookVector)/toTarget.magnitude)
--				if math.deg(angle) < configs["FieldOfView"]/2 then	
--					ZombieTarget = character
--					-- raycast to see if target is actually visible
--					local toTarget = Ray.new(model.Torso.Position, (ZombieTarget.Torso.Position - model.Torso.Position))
--					local part, position = game.Workspace:FindPartOnRayWithIgnoreList(toTarget, zombies)
--					if part and part.Parent == ZombieTarget then
--						return true
--					end		
--					ZombieTarget = nil						
--				end
--			end
		end
		return false
	end
	CanSeeTarget.TransitionState = PursueState
	
	-- TargetDead: Check if target is dead.
	local TargetDead = StateMachine.NewCondition()
	TargetDead.Name = "TargetDead"
	TargetDead.Evaluate = function()
		if ZombieTarget and ZombieTarget.Humanoid then
			return ZombieTarget.Humanoid.Health <= 0
		end
		return true
	end
	TargetDead.TransitionState = IdleState
	
	-- GotDamaged: Check if NPC has taken damage
	local lastHealth = model.Humanoid.Health
	local GotDamaged = StateMachine.NewCondition()
	GotDamaged.Name = "GotDamaged"
	GotDamaged.Evaluate = function()
		if model then
			if lastHealth > model.Humanoid.Health then
				return true
			end
		end
		return false
	end	
	GotDamaged.TransitionState = SearchState
	
	-- GotBored: Used to provide random state change.
	local GotBored = StateMachine.NewCondition()
	GotBored.Name = "GotBored"
	GotBored.Evaluate = function()
		local now = os.time()
		if now - lastBored > configs["BoredomDuration"] then
			local roll = math.random()
			if roll < configs["ChanceOfBoredom"] then
				lastBored = now
				if GotBored.TransitionState == SearchState then
					GotBored.TransitionState = IdleState
				else
					GotBored.TransitionState = SearchState
				end
				return true
			end
		end
		return false
	end
	GotBored.TransitionState = IdleState
	
	-- LostTarget: Checks clear line of sight
	local LostTarget = StateMachine.NewCondition()
	LostTarget.Name = "LostTarget"
	LostTarget.Evaluate = function()
		if true then return false end
		if ZombieTarget then
			if (ZombieTarget.Torso.Position - model.Torso.Position).magnitude > 10 then
				local toTarget = Ray.new(model.Torso.Position, (ZombieTarget.Torso.Position - model.Torso.Position))
				local part, position = game.Workspace:FindPartOnRay(toTarget, model)
				if not part or part.Parent ~= ZombieTarget  then
					--print("Lost target!")
					ZombieTargetLastLocation = ZombieTarget.Torso.Position
					ZombieTarget = nil
					return true
				end
			end
		end
		return false
	end
	LostTarget.TransitionState = HuntState
	
	local WithinRange = StateMachine.NewCondition()
	WithinRange.Name = "WithinRange"
	WithinRange.Evaluate = function()
		if ZombieTarget then
			local distance = (model.Torso.Position - ZombieTarget.Torso.Position).magnitude
			if distance < configs["AttackRange"] then
				--print("Within attack range!")
				return true
			end
		end
		return false
	end
	WithinRange.TransitionState = AttackState
	
	local OutsideRange = StateMachine.NewCondition()
	OutsideRange.Name = "OutsideRange"
	OutsideRange.Evaluate = function()
		if ZombieTarget then
			local distance = (model.Torso.Position - ZombieTarget.Torso.Position).magnitude
			if distance > configs["AttackRange"] then
				--print("Outside attack range!")
				return true
			end
		end
		return false
	end
	OutsideRange.TransitionState = PursueState
	
	table.insert(IdleState.Conditions, CanSeeTarget)
	table.insert(IdleState.Conditions, GotDamaged)
	table.insert(IdleState.Conditions, GotBored)
	
	table.insert(SearchState.Conditions, GotBored)
	table.insert(SearchState.Conditions, CanSeeTarget)
	
	table.insert(PursueState.Conditions, LostTarget)
	table.insert(PursueState.Conditions, WithinRange)
	table.insert(PursueState.Conditions, TargetDead)
	
	table.insert(AttackState.Conditions, OutsideRange)
	table.insert(AttackState.Conditions, TargetDead)
	
	table.insert(HuntState.Conditions, GotBored)
	table.insert(HuntState.Conditions, CanSeeTarget)
	
	-- Setup arms damage
	local canHit = true
	local lastHit = os.time()
	local function handleHit(other)
		if canHit then
			if other and other.Parent and other.Parent.Name ~= "Drooling Zombie" and other.Parent:FindFirstChild("Humanoid") then
				local enemy = other.Parent						
				if enemy.Humanoid.WalkSpeed > 0 then
					enemy.Humanoid.Health = enemy.Humanoid.Health - configs["Damage"]
					canHit = false
				end
			end
		else
			local now = os.time()
			if now - lastHit > configs["DamageCooldown"] then
				lastHit = now
				canHit = true
			end
		end
	end	
	local leftHitConnect, rightHitConnect
	leftHitConnect = model:FindFirstChild("Left Arm").Touched:connect(handleHit)
	rightHitConnect = model:FindFirstChild("Right Arm").Touched:connect(handleHit)
	
	--ZombieAI.Animate(model)
	--updateDisplay()
	--updateDisplay(model.BillboardGui.TextLabel, StateMachine.CurrentState)
	local thread = coroutine.create(function()
		while true do
			wait()
			-- calculate repulsion force
						
			local humanoids = HumanoidList:GetCurrent()
			local localZombies = {}
			for _, humanoid in pairs(humanoids) do
				if humanoid and humanoid ~= model.Humanoid and humanoid.Parent and humanoid.Parent:FindFirstChild("Torso") then
					local torso = humanoid.Parent:FindFirstChild("Torso")
					local distance = (model.Torso.Position - torso.Position).magnitude
					if distance <= 2.5 then
						table.insert(localZombies, torso.Position)
					end
				end
			end
			local repulsionDirection = AIUtilities:GetRepulsionVector(model.Torso.Position, localZombies)
			if repulsionDirection.magnitude > 0 then
				--print("replusion direction: " .. tostring(repulsionDirection))
			end
			model.Torso.RepulsionForce.force = repulsionDirection
			
			if StateMachine.CurrentState and model.Configurations.Debug.Value then
				model.BillboardGui.TextLabel.Visible = true
				model.BillboardGui.TextLabel.Text = StateMachine.CurrentState.Name
			end
			if not model.Configurations.Debug.Value then
				model.BillboardGui.TextLabel.Visible = false
			end
		end
	end)
	coroutine.resume(thread)	
	
	StateMachine.SwitchState(IdleState)
	
	zombie.Stop = function()
		StateMachine.SwitchState(nil)
	end	
	
	return zombie
end

return ZombieAI