Yeah, I literally just ChatGPT on how I should do it. I already have modules in my game but I think it would be more modular if I made the setup tower system a OOP system because right now it looks like this
(Server sends a placed tower)
-- COMPLETE FIXED TOWERS MODULE --
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local Animations = require(ReplicatedStorage.Modules.Animations)
local Towers = {}
local targetCache = {}
local mobPositions = {}
-- Pre-calculate mob positions
RunService.Heartbeat:Connect(function()
mobPositions = {}
for _, mob in ipairs(workspace.Hitboxes:GetChildren()) do
if mob:FindFirstChild("Health") then
mobPositions[mob] = mob.Position
end
end
end)
-- Improved targeting with validation
local function GetNextTarget(tower)
-- Cache invalidation
--local cached = targetCache[tower]
--if cached then
-- if not cached.Parent or not cached:FindFirstChild("Health") or cached.Health.Value <= 0 then
-- targetCache[tower] = nil
-- cached = nil
-- else
-- return cached
-- end
--end
local Config = tower.Config
local HRP = tower.HumanoidRootPart
if not (Config and HRP) then return nil end
local range = tower:GetAttribute("Range")
local mode = Config:FindFirstChild("TargetingMode") and Config.TargetingMode.Value or "First"
local candidates = {}
for mob, position in pairs(mobPositions) do
if mob.Parent and mob:FindFirstChild("Health") and mob:FindFirstChild("Progress") and mob.Health.Value > 0 then
if not CollectionService:HasTag(mob, "Hidden") or (CollectionService:HasTag(mob, "Hidden") and tower:GetAttribute("HiddenDetection") == true) then
local dist = (position - HRP.Position).Magnitude
if dist <= range then
table.insert(candidates, {
mob = mob,
dist = dist,
progress = mob.Progress.Value,
health = mob.Health.Value
})
end
end
end
end
if #candidates == 0 then
return nil
end
-- Sorting logic
table.sort(candidates, function(a, b)
if mode == "Closest" then return a.dist < b.dist
elseif mode == "Farthest" then return a.dist > b.dist
elseif mode == "First" then return a.progress > b.progress
elseif mode == "Last" then return a.progress < b.progress
elseif mode == "Strongest" then return a.health > b.health
elseif mode == "Weakest" then return a.health < b.health
else return a.dist < b.dist end
end)
if mode == "Random" then
return candidates[math.random(1, #candidates)].mob
end
return candidates[1].mob
end
-- Robust tower setup
function Towers.SetupTower(tower)
-- Validate tower
if not tower.PrimaryPart or not tower:FindFirstChild("Config") then
tower:Destroy()
return
end
if tower.PrimaryPart then
tower.PrimaryPart.Anchored = true
else
tower:WaitForChild("HumanoidRootPart").Anchored = true
end
local connections = {}
local module = require(tower.Config.Module.Value)
local Config = tower.Config
-- Cleanup function
local function cleanUp()
for _, c in ipairs(connections) do
c:Disconnect()
end
targetCache[tower] = nil
end
-- Track destruction
local destroyConn = tower.AncestryChanged:Connect(function(_, parent)
if not parent then
cleanUp()
end
end)
table.insert(connections, destroyConn)
-- Animation setup
local idleTrack
if Config.IdleAnimation then
idleTrack = Animations.AnimateHumanoid(Config.IdleAnimation, tower, true, 1, Enum.AnimationPriority.Idle)
end
-- CACHE MOTOR6Ds once, before the while‑loop
local leftShoulder = tower:FindFirstChild("Left Shoulder", true)
local rightShoulder = tower:FindFirstChild("Right Shoulder", true)
local function aimArms(targetPos)
if not (leftShoulder and rightShoulder) then return end
local hrp = tower:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local dir = (targetPos - hrp.Position).Unit
local yaw = math.atan2(dir.X, dir.Z) -- horizontal angle
leftShoulder.C0 = leftShoulder.C0 * CFrame.Angles(0, yaw, 0)
rightShoulder.C0 = rightShoulder.C0 * CFrame.Angles(0, yaw, 0)
end
-- Main tower loop
while tower:IsDescendantOf(workspace) do
if CollectionService:HasTag(tower, "Stunned") then task.wait(0.1) continue end
local target = GetNextTarget(tower)
if not target or not target.Parent or not target:FindFirstChild("Health") or target.Health.Value <= 0 then
task.wait(0.1)
continue
end
local targetPos = target.Position
task.wait(tower:GetAttribute("Cooldown"))
if CollectionService:HasTag(tower, "Stunned") then task.wait(0.1) continue end
-- Separate body and aiming rotations
if tower:FindFirstChild("HumanoidRootPart") and target then
local HRP = tower:FindFirstChild("HumanoidRootPart")
local leftShoulder = tower:FindFirstChild("Left Shoulder", true)
local rightShoulder = tower:FindFirstChild("Right Shoulder", true)
local originalLeftC0 = leftShoulder and leftShoulder.C0
local originalRightC0 = rightShoulder and rightShoulder.C0
-- 1) Yaw-only body rotation
local hrp = tower:FindFirstChild("HumanoidRootPart")
if hrp and target then
local fromPos = hrp.Position
local toPos = Vector3.new(target.Position.X, fromPos.Y, target.Position.Z)
local dir = (toPos - fromPos)
if dir.Magnitude > 0 then
hrp.CFrame = CFrame.new(fromPos, fromPos + dir.Unit)
end
end
aimArms(targetPos)
local shootPosition
if Config:FindFirstChild("ShootPosition").Value ~= nil then
shootPosition = Config:FindFirstChild("ShootPosition").Value.Position
else
shootPosition = tower:FindFirstChild("HumanoidRootPart") and tower:FindFirstChild("HumanoidRootPart").Position or tower.PrimaryPart.Position
end
if tower:FindFirstChild("HumanoidRootPart"):FindFirstChild("ShootSound") then
tower.HumanoidRootPart.ShootSound:Play()
end
if Config.AttackAnimation then
Animations.AnimateHumanoid(Config.AttackAnimation, tower, false, 1, Enum.AnimationPriority.Action, tower:GetAttribute("ProjectileType"), targetPos)
end
if target and target:FindFirstChild("Health") and target.Health.Value > 0 then
module.Attack(tower, shootPosition, target, tower:GetAttribute("Damage"))
end
end
end
task.wait(0.1)
end
return Towers
Note: this is not finished at all parts and may contain code that doesn’t do anything.