-- RivetStrider AI (attachment-aware aim + stable yaw/pitch + 180° guard) -- Paste into the RivetStrider root model (replaces prior AI). -- This version: -- - prefers attachments (TurretAttachment, GunBaseAttachment, GunAttachment, Muzzle) -- - picks the sign (forward vs -forward) that produces the smallest yaw change (prevents 180° flip) -- - clamps barrel pitch to sensible range to avoid wild/flip behaviour -- - uses CFrame:Lerp for smooth rotation (dt-based) -- Keeps HeliCoins reward on death (uses _G.HeliCoinsReward if present). local Workspace = game:GetService("Workspace") local Debris = game:GetService("Debris") local RunService = game:GetService("RunService") local Players = game:GetService("Players") local enemy = script.Parent -- WAIT for required parts local hum = enemy:WaitForChild("Humanoid") local hrp = enemy:WaitForChild("HumanoidRootPart") -- Submodels (non-fatal fallbacks) local Hull = enemy:FindFirstChild("Hull") or enemy local TurretModel = Hull and (Hull:FindFirstChild("Turret") or Hull:FindFirstChildWhichIsA("Model")) local BarrelModel = TurretModel and (TurretModel:FindFirstChild("Barrel") or TurretModel:FindFirstChildWhichIsA("Model")) -- helper to find attachments/parts by many possible names local function findAny(root, names) if not root then return nil end for _,n in ipairs(names) do local f = root:FindFirstChild(n, true) if f then return f end end for _,d in ipairs(root:GetDescendants()) do if d:IsA("Attachment") or d:IsA("BasePart") then -- stop at first attachment/part if nothing matched, but prefer attachments above -- we'll still prefer direct finds above end end return nil end -- Common part/attachment lookups (use provided names) local turretPart = nil if TurretModel then turretPart = TurretModel:FindFirstChild("TurretPart", true) or TurretModel:FindFirstChildWhichIsA("BasePart", true) end -- Barrel part primary fallback local barrelPart = nil if BarrelModel then barrelPart = BarrelModel:FindFirstChild("BarrelPart", true) or BarrelModel:FindFirstChildWhichIsA("BasePart", true) end -- Attachments (preferred) local turretAttachment = nil if TurretModel then turretAttachment = TurretModel:FindFirstChild("TurretAttachment", true) or TurretModel:FindFirstChild("GunBaseAttachment", true) end local gunAttachment = nil if BarrelModel then gunAttachment = BarrelModel:FindFirstChild("GunAttachment", true) end local muzzleAttachment = nil if BarrelModel then muzzleAttachment = BarrelModel:FindFirstChild("Muzzle", true) -- fallback to any attachment on barrelPart if (not muzzleAttachment) and barrelPart then for _,c in ipairs(barrelPart:GetChildren()) do if c:IsA("Attachment") then muzzleAttachment = c; break end end end end local function dbg(...) print("[RivetAI]", ...) end dbg("Starting RivetStrider AI for:", enemy.Name) dbg("turretPart:", turretPart and turretPart:GetFullName() or "nil") dbg("barrelPart:", barrelPart and barrelPart:GetFullName() or "nil") dbg("turretAttachment:", turretAttachment and turretAttachment:GetFullName() or "nil") dbg("gunAttachment:", gunAttachment and gunAttachment:GetFullName() or "nil") dbg("muzzleAttachment:", muzzleAttachment and muzzleAttachment:GetFullName() or "nil") -- Animations (StriderIdle / StriderWalk) local function findAnimation(root, fragment) fragment = (fragment or ""):lower() for _, v in ipairs(root:GetDescendants()) do if v:IsA("Animation") and v.Name:lower():find(fragment) then return v end end for _, v in ipairs(root:GetChildren()) do if v:IsA("Animation") and v.Name:lower():find(fragment) then return v end end return nil end local idleAnimObj = findAnimation(enemy, "strideridle") or findAnimation(enemy, "idle") local walkAnimObj = findAnimation(enemy, "striderwalk") or findAnimation(enemy, "walk") local idleTrack, walkTrack pcall(function() if idleAnimObj then idleTrack = hum:LoadAnimation(idleAnimObj); idleTrack.Looped = true end end) pcall(function() if walkAnimObj then walkTrack = hum:LoadAnimation(walkAnimObj); walkTrack.Looped = true end end) local EnemyBullet = game:GetService("ReplicatedStorage"):FindFirstChild("StriderBullet") if not EnemyBullet then dbg("WARNING: StriderBullet missing in ReplicatedStorage") end -- ATTRIBUTES local RANGE = tonumber(enemy:GetAttribute("RangeAttackDistance")) or 90 local DAMAGE = tonumber(enemy:GetAttribute("Damage")) or 12 local RELOAD = tonumber(enemy:GetAttribute("Reload")) or 1.2 local MAIN_DIST = tonumber(enemy:GetAttribute("MainDistance")) or 14 local TURN_SPEED = tonumber(enemy:GetAttribute("TurnSpeedDeg")) or 140 local PITCH_SPEED = tonumber(enemy:GetAttribute("PitchSpeedDeg")) or 90 local BULLET_SPEED = tonumber(enemy:GetAttribute("BulletSpeed")) or 160 local BULLET_LIFE = tonumber(enemy:GetAttribute("BulletLife")) or 4 local PITCH_MIN_DEG = tonumber(enemy:GetAttribute("MinPitchDeg")) or -25 -- clamp down local PITCH_MAX_DEG = tonumber(enemy:GetAttribute("MaxPitchDeg")) or 45 -- clamp up dbg(("ATTRIBUTES: RANGE=%s DAMAGE=%s RELOAD=%s MAIN_DIST=%s"):format(RANGE, DAMAGE, RELOAD, MAIN_DIST)) local AlliancesFolder = Workspace:WaitForChild("Alliances") -- ensure humanoid sensible if hum.PlatformStand then hum.PlatformStand = false end if not hum.WalkSpeed or hum.WalkSpeed <= 0 then hum.WalkSpeed = tonumber(enemy:GetAttribute("DefaultWalkSpeed")) or 16 end local function playIdle() if walkTrack and walkTrack.IsPlaying then pcall(function() walkTrack:Stop() end) end if idleTrack and not idleTrack.IsPlaying then pcall(function() idleTrack:Play() end) end end local function playWalk() if idleTrack and idleTrack.IsPlaying then pcall(function() idleTrack:Stop() end) end if walkTrack and not walkTrack.IsPlaying then pcall(function() walkTrack:Play() end) end end -- target selection (players first then Alliances) local function getClosestTank() local closest, closestDist = nil, math.huge for _, p in ipairs(Players:GetPlayers()) do local ch = p.Character if ch and ch ~= enemy then local r = ch:FindFirstChild("HumanoidRootPart") if r then local d = (hrp.Position - r.Position).Magnitude if d < closestDist then closestDist, closest = d, ch end end end end for _, t in ipairs(AlliancesFolder:GetChildren()) do if t and t ~= enemy then local r = t:FindFirstChild("HumanoidRootPart") if r then local d = (hrp.Position - r.Position).Magnitude if d < closestDist then closestDist, closest = d, t end end end end return closest, closestDist end local function angleDeg(aVec, bVec) local a = aVec.Unit; local b = bVec.Unit local dot = math.clamp(a:Dot(b), -1, 1) return math.deg(math.acos(dot)) end -- Obtain "pivot pos" for turret yaw (prefer attachment world position) local function getTurretPivotPos() if turretAttachment and turretAttachment.WorldPosition then return turretAttachment.WorldPosition elseif turretPart then return turretPart.Position elseif TurretModel and TurretModel.PrimaryPart then return TurretModel.PrimaryPart.Position else return (TurretModel and TurretModel:GetModelCFrame().p) or hrp.Position end end -- Obtain turret "current forward" horizontal vector local function getTurretCurrentForward() if turretPart then local fv = turretPart.CFrame.LookVector local horiz = Vector3.new(fv.X, 0, fv.Z) if horiz.Magnitude < 1e-6 then return Vector3.new(0,0,1) end return horiz.Unit elseif TurretModel and TurretModel.PrimaryPart then local fv = TurretModel.PrimaryPart.CFrame.LookVector local horiz = Vector3.new(fv.X, 0, fv.Z) if horiz.Magnitude < 1e-6 then return Vector3.new(0,0,1) end return horiz.Unit elseif turretAttachment and turretAttachment.WorldCFrame then local fv = turretAttachment.WorldCFrame.LookVector local horiz = Vector3.new(fv.X, 0, fv.Z) if horiz.Magnitude < 1e-6 then return Vector3.new(0,0,1) end return horiz.Unit else return Vector3.new(0,0,1) end end -- Aim function: smooth turret yaw + barrel pitch. Returns yawRemDeg, pitchRemDeg local function aimAt(targetPos, dt) -- require turret/barrel available if not (turretPart or TurretModel) or not (barrelPart or BarrelModel) then return 999, 999 end local turretPos = getTurretPivotPos() local dir = targetPos - turretPos local horiz = Vector3.new(dir.X, 0, dir.Z) if horiz.Magnitude < 1e-6 then return 999, 999 end local desiredLook = horiz.Unit -- stable 180 handling: pick desiredLook or -desiredLook whichever is closer to current forward local currentForward = getTurretCurrentForward() -- compute angles local a1 = angleDeg(currentForward, desiredLook) local a2 = angleDeg(currentForward, -desiredLook) if a2 < a1 then desiredLook = -desiredLook end -- build desired turret CFrame (flat) local desiredTurretCf = CFrame.new(turretPos, turretPos + Vector3.new(desiredLook.X, 0, desiredLook.Z)) -- compute frac from TURN_SPEED and angle local currentFlat = Vector3.new((turretPart and turretPart.CFrame.LookVector or (TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame.LookVector)).X, 0, (turretPart and turretPart.CFrame.LookVector or (TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame.LookVector)).Z) if currentFlat.Magnitude < 1e-6 then currentFlat = Vector3.new(0,0,1) end local angleBetween = angleDeg(currentFlat, Vector3.new(desiredLook.X,0,desiredLook.Z)) local maxYawStep = math.rad(TURN_SPEED) * dt local frac = (angleBetween <= 0.5) and 1 or math.clamp((maxYawStep * 180 / math.pi) / math.max(angleBetween, 0.0001), 0, 1) -- lerp turret orientation local newTurretCf = (turretPart and turretPart.CFrame or (TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame)) :Lerp(desiredTurretCf, frac) pcall(function() if TurretModel and TurretModel.PrimaryPart then local pos = TurretModel.PrimaryPart.Position local look = Vector3.new(newTurretCf.LookVector.X, 0, newTurretCf.LookVector.Z) if look.Magnitude < 1e-6 then look = TurretModel.PrimaryPart.CFrame.LookVector end TurretModel:SetPrimaryPartCFrame(CFrame.new(pos, pos + look)) elseif turretPart then local pos = turretPart.Position local look = Vector3.new(newTurretCf.LookVector.X, 0, newTurretCf.LookVector.Z) if look.Magnitude < 1e-6 then look = turretPart.CFrame.LookVector end turretPart.CFrame = CFrame.new(pos, pos + look) end end) -- barrel pitch: aim from muzzle (preferred) else from barrelPart center local muzzlePos = nil if muzzleAttachment and muzzleAttachment.WorldPosition then muzzlePos = muzzleAttachment.WorldPosition elseif gunAttachment and gunAttachment.WorldPosition then muzzlePos = gunAttachment.WorldPosition elseif barrelPart then muzzlePos = barrelPart.Position elseif BarrelModel and BarrelModel.PrimaryPart then muzzlePos = BarrelModel.PrimaryPart.Position else muzzlePos = turretPos end local aimDir = targetPos - muzzlePos if aimDir.Magnitude < 1e-6 then aimDir = (barrelPart and barrelPart.CFrame.LookVector) or Vector3.new(0,0,1) end -- desired pitch angle (radians) local horizDist = Vector3.new(aimDir.X, 0, aimDir.Z).Magnitude local desiredPitch = 0 if horizDist > 1e-6 or math.abs(aimDir.Y) > 1e-6 then desiredPitch = math.atan2(aimDir.Y, math.max(0.0001, horizDist)) end -- clamp pitch to configured deg range local minP = math.rad(PITCH_MIN_DEG) local maxP = math.rad(PITCH_MAX_DEG) desiredPitch = math.clamp(desiredPitch, minP, maxP) -- derive turret horizontal forward after yaw step local turretForward = (TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame.LookVector) or (turretPart and turretPart.CFrame.LookVector) or Vector3.new(0,0,1) local forward2D = Vector3.new(turretForward.X, 0, turretForward.Z) if forward2D.Magnitude < 1e-6 then forward2D = Vector3.new(0,0,1) end forward2D = forward2D.Unit -- build new look vector using desiredPitch (relative to turret forward) local cosP = math.cos(desiredPitch) local lookVec = Vector3.new(forward2D.X * cosP, math.sin(desiredPitch), forward2D.Z * cosP) if lookVec.Magnitude < 1e-6 then lookVec = turretForward end -- create desired barrel CFrame and LERP based on pitch speed local desiredBarrelCf = CFrame.new((barrelPart and barrelPart.Position) or (BarrelModel and BarrelModel.PrimaryPart and BarrelModel.PrimaryPart.Position) or muzzlePos, ((barrelPart and barrelPart.Position) or muzzlePos) + lookVec) local pitchRemainAngle = angleDeg((barrelPart and barrelPart.CFrame.LookVector) or (BarrelModel and BarrelModel.PrimaryPart and BarrelModel.PrimaryPart.CFrame.LookVector) or Vector3.new(0,0,1), lookVec) local maxPitchStep = math.rad(PITCH_SPEED) * dt local fracP = (pitchRemainAngle <= 0.5) and 1 or math.clamp((maxPitchStep * 180 / math.pi) / math.max(pitchRemainAngle, 0.0001), 0, 1) local currentBarrelCf = (barrelPart and barrelPart.CFrame) or (BarrelModel and BarrelModel.PrimaryPart and BarrelModel.PrimaryPart.CFrame) or CFrame.new(muzzlePos) local newBarrelCf = currentBarrelCf:Lerp(desiredBarrelCf, fracP) pcall(function() if BarrelModel and BarrelModel.PrimaryPart then BarrelModel:SetPrimaryPartCFrame(newBarrelCf) elseif barrelPart then barrelPart.CFrame = newBarrelCf end end) -- compute remaining angles local curYawRem = angleDeg(Vector3.new((TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame.LookVector or turretPart.CFrame.LookVector).X,0, (TurretModel and TurretModel.PrimaryPart and TurretModel.PrimaryPart.CFrame.LookVector or turretPart.CFrame.LookVector).Z), Vector3.new(desiredLook.X,0,desiredLook.Z)) local curPitchRem = angleDeg((BarrelModel and BarrelModel.PrimaryPart and BarrelModel.PrimaryPart.CFrame.LookVector or barrelPart.CFrame.LookVector), lookVec) return curYawRem, curPitchRem end -- spawn bullet from muzzle toward targetHRP local function spawnBulletAt(targetHRP) if not muzzleAttachment or not EnemyBullet then return end local muzzleCf = muzzleAttachment.WorldCFrame local targetPos = (targetHRP and targetHRP.Position) or (muzzleCf.Position + muzzleCf.LookVector * 100) local dir = targetPos - muzzleCf.Position if dir.Magnitude <= 1e-6 then dir = muzzleCf.LookVector end dir = dir.Unit local bullet = EnemyBullet:Clone() local bp = bullet:IsA("BasePart") and bullet or (bullet.PrimaryPart or bullet:FindFirstChildWhichIsA("BasePart", true)) if not bp then bullet.Parent = Workspace Debris:AddItem(bullet, 2) return end bullet.Parent = Workspace if bullet:IsA("BasePart") then bullet.CFrame = muzzleCf else bullet:SetPrimaryPartCFrame(muzzleCf) end -- use BodyVelocity for consistent motion local topPart = bp local bv = topPart:FindFirstChildOfClass("BodyVelocity") if not bv then bv = Instance.new("BodyVelocity") bv.MaxForce = Vector3.new(1e5,1e5,1e5) bv.P = 3000 bv.Parent = topPart end bv.Velocity = dir * BULLET_SPEED -- touch handler local conn conn = topPart.Touched:Connect(function(hit) if not hit or not hit.Parent then return end local targetModel = hit:FindFirstAncestorWhichIsA("Model") if targetModel and targetModel.Parent == AlliancesFolder and targetModel ~= enemy then local h = targetModel:FindFirstChildOfClass("Humanoid") if h then pcall(function() h:TakeDamage(DAMAGE) end) end if conn then conn:Disconnect() end if topPart and topPart.Parent then topPart:Destroy() end return end local pl = Players:GetPlayerFromCharacter(hit.Parent) if pl and pl.Character then local h = pl.Character:FindFirstChildOfClass("Humanoid") if h then pcall(function() h:TakeDamage(DAMAGE) end) end if conn then conn:Disconnect() end if topPart and topPart.Parent then topPart:Destroy() end return end end) Debris:AddItem(bullet, BULLET_LIFE) end -- MAIN LOOP local running = true local lastFire = 0 spawn(function() playIdle() while running and hum.Health > 0 do hum.AutoRotate = true local target, dist = getClosestTank() if not target then playIdle() task.wait(0.25) continue end local targetHRP = target:FindFirstChild("HumanoidRootPart") if not targetHRP then task.wait(0.12); continue end -- approach until MAIN_DIST if dist > MAIN_DIST then playWalk() local ok, err = pcall(function() hum:MoveTo(targetHRP.Position) end) if not ok then dbg("MoveTo failed:", err) end else -- stop and idle pcall(function() hum:MoveTo(hrp.Position) end) playIdle() end -- aim & fire if within RANGE if dist <= RANGE and (turretPart or TurretModel) and (barrelPart or BarrelModel) and muzzleAttachment and EnemyBullet then local yawRem, pitchRem = aimAt(targetHRP.Position, 0.06) if yawRem <= 8 and pitchRem <= 12 and (tick() - lastFire) >= RELOAD then lastFire = tick() spawn(function() spawnBulletAt(targetHRP) end) end end task.wait(0.06) end end) -- Cleanup on death hum.Died:Connect(function() running = false dbg("💀 Enemy "..enemy.Name.." destroyed.") for _, p in ipairs(enemy:GetDescendants()) do if p:IsA("BasePart") then p.Anchored = false p.CanCollide = true end end local explosion = Instance.new("Explosion") explosion.Position = enemy:GetPivot().Position explosion.BlastRadius = 0 explosion.BlastPressure = 0 explosion.Parent = workspace Debris:AddItem(enemy, 2) end) -- HeliCoins reward hook local humanoid = enemy:WaitForChild("Humanoid") humanoid.Died:Connect(function() local reward = tonumber(enemy:GetAttribute("HeliCoins")) or 75 if _G and type(_G.HeliCoinsReward) == "function" then pcall(function() _G.HeliCoinsReward(enemy, reward) end) end end)