-- unobstructed path has a flaw where people in the air are considered a straight path (on ground check?) -- have an icon that indicates they're a bot? -- give them a better class? ZSBOTS = {} local TEAM_HUMAN = TEAM_HUMAN local RealTime = RealTime local CurTime = CurTime local IN_ATTACK = IN_ATTACK local IN_JUMP = IN_JUMP local IN_DUCK = IN_DUCK local IN_FORWARD = IN_FORWARD local IN_MOVELEFT = IN_MOVELEFT local IN_MOVERIGHT = IN_MOVERIGHT local util_TraceEntity = util.TraceEntity local Path = Path local Bots = {} function ZSBOTS:GetBots() return Bots end local eyepos local function SortObstructions(posa, posb) return posa:DistToSqr(eyepos) < posb:DistToSqr(eyepos) end local target local obstrace = {mask = MASK_PLAYERSOLID, filter = function(ent) return ent ~= target and not ent:IsPlayer() end} function ZSBOTS.StartCommand(pl, cmd) if not pl.IsZSBot or not pl:OldAlive() then return end local buttons = 0 if pl.HoldDuckUntil then if CurTime() > pl.HoldDuckUntil then pl.HoldDuckUntil = nil else buttons = bit.bor(buttons, IN_DUCK) end end if pl.ShouldJump then pl.ShouldJump = false buttons = bit.bor(buttons, IN_JUMP) pl.HoldDuckUntil = CurTime() + 1 end local mypos = pl:GetPos() eyepos = pl:EyePos() target = pl.CurrentEnemy local destination = pl.MovementTarget local targetpos local targetdist local targetunobstructed if target:IsValid() then if not destination then destination = target:GetPos() end targetpos = (target:WorldSpaceCenter() * 2 + target:NearestPoint(eyepos)) / 3 targetdist = targetpos:DistToSqr(eyepos) --if target:VisibleVec(eyepos) then obstrace.start = pl:GetPos() % 1 obstrace.endpos = target:WorldSpaceCenter() if not util_TraceEntity(obstrace, pl).Hit then targetunobstructed = true end --end end local viewang if destination then if targetunobstructed then if targetdist < 5000 then -- Check if we're "inside" the target or they're on top of us, we're on top of them. local eyepos2d = Vector() eyepos2d:Set(eyepos) eyepos2d.z = 0 local targetpos2d = Vector() targetpos2d:Set(targetpos) targetpos2d.z = 0 if eyepos2d:DistToSqr(targetpos2d) < 2000 then buttons = bit.bor(buttons, IN_BACK) cmd:SetForwardMove(-10000) viewang = pl:EyeAngles() if eyepos.z < targetpos.z then -- They're on top of us. viewang.pitch = -89 else -- We're on top of them. viewang.pitch = 89 buttons = bit.bor(buttons, IN_DUCK) end else -- No but we're very close, so start doing anti-juke measures. buttons = bit.bor(buttons, IN_FORWARD) cmd:SetForwardMove(10000) viewang = (targetpos - eyepos):Angle() viewang.roll = 0 local target_vel2d = target:GetVelocity() target_vel2d.z = 0 targetpos = targetpos + --[[FrameTime() * 2 *]] target_vel2d * 1.5 end else viewang = (destination - mypos):Angle() viewang.pitch = 0 viewang.roll = 0 if destination:DistToSqr(mypos) > 256 then buttons = bit.bor(buttons, IN_FORWARD) cmd:SetForwardMove(10000) end end local strafe_randomness = (CurTime() + pl:EntIndex() * 0.2) % 1 + math.Rand(0, 1) if strafe_randomness < 0.5 then buttons = bit.bor(buttons, IN_MOVELEFT) cmd:SetSideMove(-10000) elseif strafe_randomness > 1.5 then buttons = bit.bor(buttons, IN_MOVERIGHT) cmd:SetSideMove(10000) end else viewang = (destination - mypos):Angle() viewang.pitch = 0 viewang.roll = 0 if destination:DistToSqr(mypos) > 256 then buttons = bit.bor(buttons, IN_FORWARD) cmd:SetForwardMove(10000) end end local meleerange = 3600 if targetdist then local wep = pl:GetActiveWeapon() if wep and wep:IsValid() and wep.MeleeRange then meleerange = wep.MeleeRange * wep.MeleeRange --+ 128 end if targetdist <= meleerange then buttons = bit.bor(buttons, IN_ATTACK) end end if CurTime() <= pl.UnStuckTime then if CurTime() % 2 < 0.5 then buttons = bit.bor(buttons, IN_MOVELEFT) cmd:SetSideMove(-10000) else buttons = bit.bor(buttons, IN_MOVERIGHT) cmd:SetSideMove(10000) end buttons = bit.bor(buttons, IN_ATTACK) -- Break obstructions. if not targetunobstructed and (not targetdist or targetdist > meleerange) then local obstruction_positions = {} local obstruction_position for _, ent in pairs(ents.FindInSphere(eyepos, math.sqrt(meleerange))) do if ent:GetClass() == "func_breakable" or ent:IsBarricadeProp() or (ent:GetMoveType() == MOVETYPE_VPHYSICS and ent:GetPhysicsObject():IsValid() and ent:GetPhysicsObject():IsMoveable()) --[[and ent:VisibleVec(eyepos)]] then local nearest = ent:NearestPoint(eyepos) if nearest == vector_origin then nearest = ent:WorldSpaceCenter() end table.insert(obstruction_positions, (nearest * 2 + ent:WorldSpaceCenter()) / 3) end end table.sort(obstruction_positions, SortObstructions) if obstruction_positions[2] then obstruction_position = obstruction_positions[math.random(1, math.min(3, #obstruction_positions))] else obstruction_position = obstruction_positions[1] end if obstruction_position then viewang = (obstruction_position - eyepos):Angle() viewang.roll = 0 --[[buttons = bit.bor(buttons, IN_FORWARD) cmd:SetForwardMove(10000)]] end end end end if viewang then --viewang = viewang or angle_zero cmd:SetViewAngles(viewang) pl:SetEyeAngles(viewang) end cmd:SetButtons(buttons) end function ZSBOTS.PlayerTick(pl, mv) if not pl.IsZSBot then return end pl.NB:SetPos(pl:GetPos() % 8) pl.NB:SetLocalVelocity(vector_origin) if not pl:OldAlive() then return end if pl:Team() == TEAM_HUMAN then -- "suicide" since we only want to be a zombie pl:Kill() else -- We don't update our path every frame because it would be excessive. -- We move directly towards a player if we're very near and visible so that's okay. if CurTime() >= pl.NextPathUpdate then pl.NextPathUpdate = CurTime() + 0.1 pl:UpdateBotPath() end if CurTime() > pl.UnStuckTime and mv:GetVelocity():Length2DSqr() < 4096 then pl.StuckFrames = pl.StuckFrames + 1 if pl.StuckFrames >= 10 then pl.UnStuckTime = CurTime() + 1 pl.StuckFrames = 0 end else pl.StuckFrames = 0 end end end local temp_bot_pos local function SortPathableTargets(enta, entb) local dista = enta:GetPos():DistToSqr(temp_bot_pos) dista = dista + enta._temp_bot_dist_add dista = dista * enta._temp_bot_dist_mul local distb = entb:GetPos():DistToSqr(temp_bot_pos) distb = distb + entb._temp_bot_dist_add distb = distb * entb._temp_bot_dist_mul return dista < distb end local NextBotTick = 0 function ZSBOTS.Think() for _, bot in ipairs(Bots) do if not bot:OldAlive() then gamemode.Call("PlayerDeathThink", bot) end end if CurTime() < NextBotTick then return end NextBotTick = CurTime() + 0.25 -- This is significantly cheaper than pathfinding to all valid targets. for _, bot in pairs(Bots) do bot.PathableTargets = {} for __, pl in ipairs(player.GetAll()) do if pl:Team() == TEAM_HUMAN and pl:OldAlive() and pl:GetObserverMode() == OBS_MODE_NONE then table.insert(bot.PathableTargets, pl) elseif pl:IsBot() and not pl:OldAlive() then gamemode.Call("PlayerDeathThink", pl) end end --[[for _, ent in pairs(ents.FindByClass("prop_*")) do if ent:IsBarricadeProp() and ent:HumanNearby() then table.insert(bot.PathableTargets, ent) end end]] --[[for _, ent in pairs(ents.FindByClass("func_breakable*")) do if ent:HumanNearby() then table.insert(bot.PathableTargets, ent) end end]] --[[for _, ent in pairs(ents.FindByClass("prop_obj_sigil")) do if not ent:GetSigilCorrupted() then table.insert(bot.PathableTargets, ent) end end]] temp_bot_pos = bot:GetPos() obstrace.start = temp_bot_pos % 1 local dist_add, dist_mul for __, target in ipairs(bot.PathableTargets) do obstrace.endpos = target:WorldSpaceCenter() dist_add = 0 dist_mul = 1 -- Favor people with low health if target:IsPlayer() then if target:Health() < 50 then dist_add = dist_add - 75 end -- Greatly favor people that are visible obstrace.endpos = target:WorldSpaceCenter() if --[[target:VisibleVec(eyepos)]] not util_TraceEntity(obstrace, bot).Hit then dist_mul = dist_mul / 2 end -- Favor current enemy if target == bot.CurrentEnemy then dist_add = dist_add - 50 end else -- Unfavor non-players dist_add = dist_add + 128 end --[[if target.TargetPriority then distadd = distadd / target.TargetPriority end if target.TargetExtraPriority then distadd = distadd - target.TargetExtraPriority end]] target._temp_bot_dist_add = dist_add target._temp_bot_dist_mul = dist_mul end table.sort(bot.PathableTargets, SortPathableTargets) end end local autonameindex = 0 function ZSBOTS:CreateBot(teamid, name) if game.SinglePlayer() then return end if not navmesh.IsLoaded() then print("No navmesh - can't create bot") return end if not name then autonameindex = autonameindex + 1 name = "Player "..autonameindex end name = "BOT "..name ZSBOT = true local pl = player.CreateNextBot(name) if pl:IsValid() then pl.IsZSBot = true pl:ChangeTeam(teamid) pl:Spawn() local nb = ents.Create("zsbotnb") nb:SetPos(pl:GetPos()) nb:Spawn() nb:SetOwner(pl) pl:DeleteOnRemove(nb) pl.NB = nb nb.PL = pl pl.CurrentEnemy = NULL pl.TargetAcquireTime = 0 pl.StuckFrames = 0 pl.UnStuckTime = 0 pl.NextPathUpdate = 0 pl.PathableTargets = {} pl.PointsMultiplier = 0.5 pl.VoiceSet = "male" table.insert(Bots, pl) self:AddOrRemoveHooks() end ZSBOT = false end local randomtaunts = { "dang owned :remnic:", "woooooow killed by a bot", ":ez:", "fucking owned lol :ez::gg:", ":dsp drot=-90 rotrate=60::gunl drot=25 rotrate=130::ahhahahaha c=255,0,0::youdied:" } function ZSBOTS.PostZombieKilledHuman(pl, attacker, inflictor, dmginfo, headshot, suicide) if attacker.IsZSBot then attacker:Say(table.Random(randomtaunts)) end end function ZSBOTS.PrePlayerRedeemed(pl) if pl.IsZSBot then return true end -- Disallow redeeming end function ZSBOTS:AddOrRemoveHooks() if #Bots == 0 then hook.Remove("StartCommand", "zsbots") hook.Remove("Think", "zsbots") hook.Remove("PlayerTick", "zsbots") hook.Remove("PostZombieKilledHuman", "zsbots") hook.Remove("PrePlayerRedeemed", "zsbots") else hook.Add("StartCommand", "zsbots", self.StartCommand) hook.Add("Think", "zsbots", self.Think) hook.Add("PlayerTick", "zsbots", self.PlayerTick) hook.Add("PostZombieKilledHuman", "zsbots", self.PostZombieKilledHuman) hook.Add("PrePlayerRedeemed", "zsbots", self.PrePlayerRedeemed) end end hook.Add("PlayerDisconnect", "zsbots", function(pl) if pl:IsBot() then table.RemoveByValue(Bots, pl) ZSBOTS:AddOrRemoveHooks() end end) local meta = FindMetaTable("Player") if not meta then return end function meta:SetCurrentEnemy(enemy) if not enemy or not enemy:IsValid() then enemy = NULL end if self.CurrentEnemy ~= enemy then local old_enemy = self.CurrentEnemy self.CurrentEnemy = enemy self:EnemyChanged(old_enemy) end end function meta:ClearCurrentEnemy() self:SetCurrentEnemy(NULL) end function meta:SetMovementTarget(vec) self.MovementTarget = vec end function meta:ClearMovementTarget() self:SetMovementTarget(nil) end local loco --local compute_pl local compute_step_height local function Compute(area, fromArea, ladder, elevator, length) -- first area in path, no cost if not fromArea or not fromArea:IsValid() then return 0 end -- our locomotor says we can't move here --[[if not loco:IsAreaTraversable(area) then return -1 end]] if area:HasAttributes(NAV_MESH_INVALID) then return -1 end if area:HasAttributes(NAV_MESH_AVOID) then return -1 end --[[if area:HasAttributes(NAV_MESH_NO_MERGE) then return -1 end if area:HasAttributes(NAV_MESH_NAV_BLOCKER) then return -1 end]] if not area:IsVisible(fromArea:GetClosestPointOnArea(area:GetCenter())) then return -1 end -- compute distance traveled along path so far local dist = 0 if ladder and ladder:IsValid() then dist = ladder:GetLength() elseif length > 0 then dist = length -- optimization to avoid recomputing length else dist = (area:GetCenter() - fromArea:GetCenter()):Length() end local cost = dist + fromArea:GetCostSoFar() --[[if not fromArea:IsConnected(area) then -- Use unconnected areas only as a last resort cost = cost + 10 * dist elseif not area:IsVisible(temp_bot_pos) then -- Penalty for not visible areas cost = cost + 2 * dist end]] -- check height change local deltaZ = fromArea:ComputeAdjacentConnectionHeightChange(area) if deltaZ >= compute_step_height then if deltaZ >= 64 then return -1 -- too high to reach end -- jumping is slower than flat ground cost = cost + 2 * dist elseif deltaZ < -2000 then return -1 -- too far to drop end return cost end local pathlength function meta:UpdateBotPath() -- Nothing to kill if #self.PathableTargets == 0 then self:ClearCurrentEnemy() self:ClearMovementTarget() return end local length = 10000 local path, tpath, new_enemy loco = self.NB.loco --compute_pl = self compute_step_height = self:GetStepSize() --temp_bot_pos = self:EyePos() -- Find the target with the shortest route. -- This is presorted without path distance in the tick function so this won't always be accurate but it needs to be cheap. for i, ent in pairs(self.PathableTargets) do tpath = Path("Follow") --tpath:Invalidate() tpath:SetMinLookAheadDistance(300) tpath:SetGoalTolerance(20) tpath:Compute(self.NB, ent:GetPos(), Compute) pathlength = tpath:GetLength() if tpath:IsValid() and pathlength < length --[[and tpath:LastSegment().pos:DistToSqr(ent:GetPos()) <= 4096]] then path = tpath length = pathlength new_enemy = ent end -- This amount of tries is enough. if i >= 4 --[[or length < 128]] then break end end self:SetCurrentEnemy(new_enemy) self:ClearMovementTarget() if not new_enemy then return end path:Draw() -- Find the first segment not immediately near us local goal = path:GetCurrentGoal() if not goal then return end self:SetMovementTarget(self:GetPos() + goal.forward * 32) -- Have to look ahead to the next segment for jumping, ducking, etc. if goal.length < 48 then goal = path:GetAllSegments()[2] if goal and (goal.type == 2 or goal.type == 3) then self.ShouldJump = true end end end function meta:EnemyChanged(old_enemy) self.TargetAcquireTime = CurTime() if not self.CurrentEnemy:IsValid() then self:OnTargetLost() end end function meta:OnTargetLost() end function meta:HasBeenTrackingTargetFor(time) return self.CurrentEnemy:IsValid() and CurTime() >= self.TargetAcquireTime + time end concommand.Add("createnavmesh", function(sender, command, arguments) if sender:IsSuperAdmin() and not game.IsDedicated() then if sender:GetObserverMode() == OBS_MODE_NONE and sender:IsOnGround() and sender:OnGround() then for _, ent in pairs(ents.FindByClass("func_door*")) do ent:Fire("open", "", 0) ent:Fire("kill", "", 1) end for _, ent in pairs(ents.FindByClass("prop_door*")) do ent:Fire("open", "", 0) ent:Fire("kill", "", 1) end for _, ent in pairs(ents.FindByClass("prop_physics*")) do ent:Remove() end for _, ent in pairs(ents.FindByClass("func_breakable")) do ent:Remove() end for _, ent in pairs(ents.FindByClass("func_physbox")) do ent:Remove() end local ent = ents.Create("info_player_start") if ent:IsValid() then ent:SetPos(sender:GetPos()) ent:Spawn() timer.Simple(2, function() navmesh.BeginGeneration() end) end else print("You must be firmly planted on the ground.") end end end) local ENT = {} ENT.Type = "nextbot" ENT.Base = "base_nextbot" ENT.IsZSBot = true function ENT:Initialize() self:AddEFlags(EFL_SERVER_ONLY + EFL_FORCE_CHECK_TRANSMIT) self.BaseClass.Initialize(self) self:DrawShadow(false) self:SetModel("models/player/zombie_classic.mdl") self:SetCollisionBounds(Vector(-16, -16, 0), Vector(16, 16, 72)) self:SetSolidMask(MASK_PLAYERSOLID) self:SetMoveType(MOVETYPE_NONE) self:SetSolid(SOLID_NONE) self:SetCustomCollisionCheck(true) self:CollisionRulesChanged() --self:SetCollisionGroup(COLLISION_GROUP_WORLD) --self.loco:SetStepHeight(18) self.loco:SetDeathDropHeight(2000) self.loco:SetJumpHeight(64) self.loco:SetAcceleration(900) self.loco:SetDeceleration(900) end function ENT:OnKilled(dmginfo) end function ENT:UpdateTransmitState() return TRANSMIT_NONE end function ENT:ShouldNotCollide(ent) return ent.IsZSBot end function ENT:RunBehaviour() -- dummy, does nothing end scripted_ents.Register(ENT, "zsbotnb")