Help with tower defense game

sorry for the non descriptive title, cant think of anything else, but i’ve been working on a tower defense game and theres a mechanic i cannot implement

so i have a module script for the towers and here is the function that handles the attacking:

function tower.Attack(newTower,player,name)
	
	local config = newTower:FindFirstChild("Config")
	local target = tower.FindTarget(newTower, config.Range.Value, config.TargetMode.Value)
	
	if target and target.Humanoid and target.Humanoid.Health > 0 then
		
		local targetCFrame = CFrame.lookAt(newTower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
		newTower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame
		
		events:WaitForChild("AnimateTower"):FireAllClients(newTower, "Attack", target)
		
		local module = require(script:FindFirstChild(newTower.Name,true))
		module.Attack(newTower,target)
		
		task.wait(config.Cooldown.Value)
	end
	
	task.wait(0.1)
	
	if newTower and newTower.Parent then
		tower.Attack(newTower,player)
	end
end

so the main part is this here:

local module = require(script:FindFirstChild(newTower.Name,true))
module.Attack(newTower,target)

basically what this is, is that every tower has a seperate module script that does their attacks, and this just finds that module script and calls the attack function.


So now that that’s clear, here is the seperate module script that does the stuff for this specific tower:

local Module = {}

function Module.Attack(tower,target)
	local config = tower.Config
	
	if target and target.Humanoid and target.Humanoid.Health > 0 then
		
		repeat
			task.wait()
			target.Humanoid:TakeDamage(15)
		until target == nil
		
	end
end

return Module

so basically after the if target, there would just be takedamage and that would be it but what i want for this tower, is basically that it keeps dealing continuous damage until the enemy is dead

anyways here is the issue

the target gets shot even if it is out range

so it repeats it until the target is nil, issue is that the target is never actually nil. In the attack function in the main script the attack function gets called only if it has already found a target, which is obviously an issue because it cannot detect if there is no target, therefore damaging the enemy even when it is out of range

the main script would be checking if it is in range, but since we are doing a repeat that means its not getting back to the main script, thats is also why i am trying to do the check in here

it seems like a simple issue but i have tried been trying for 2 days now and there is always something wrong with the solution i come up with, so if anyone has any ideas let me know

2 Likes

You will need to get the position of the target in the loop.
Get the distance between this and the tower position using (targetPos - towerPos).magnitude, then compare this to the tower’s range in an if statement.
If the magnitude to target is greater than the range, set the target to nil to break the loop.

1 Like

tried this, but module gets called in the attack function, and for the tower to attack there needs to be a target in range, so when it is out of range it wont run anymore

1 Like

Hello, I believe that a possible solution is to introduce the range check inside the Module.Attack() function. To do this, you could calculate the distance between the tower and the target at every iteration in the repeat loop. If the target moves out of range, you break the loop, stopping the damage.

function Module.Attack(tower,target)
	local config = tower.Config
	
	if target and target.Humanoid and target.Humanoid.Health > 0 then
		repeat
			task.wait()
			local targetPos = target.HumanoidRootPart.Position
			local towerPos = tower.HumanoidRootPart.Position
			local dist = (targetPos - towerPos).magnitude

			if dist <= config.Range.Value then
				target.Humanoid:TakeDamage(15)
			else
				break
			end
		until target == nil or target.Humanoid.Health <= 0
	end
end

thanks but there is still an issue, in the main script, the Module.Attack function gets called in the main script’s Tower.Attack function, and it only gets called if there is something in range, that’s what the if target is that checks if there is something in range

So much is going on here, it’s pretty hard to follow. You’re using a Tower class, yet the class is abstract and extends upon another class. I would rather see Object Orientated Programming here with metatables than this. I am not even going to talk about how your method names are uppercase, your class names are lowercase, you using ::FindFirstAncestor for requiring your class.

Your issue, simply put, is that Module.Attack does not take into consideration distance. You could do this with a simple Magnitude check and comparing it with some sort of MaxDistance, which it seems you already have a MaxDistance in-place, so this should be relatively simple.

1 Like

yeah sorry for the capitalizations, i also do not know where you are seeing findfirstancestor

but anyways i mentioned above that this function only gets called if there is something in range, so i cannot check if something is outside of the range.

Also i am not familiar with metatables, i am open to any rewriting of the script

Using ::FindFirstChild( _, true ) is the same as ::FindFirstAncestor( _ ).

I do not see any provided code that is reading the magnitude between the target and the tower, or any checks confirming that the distance is less than or equal to the maximum allowed distance the tower may shoot. Could you provide this snippet?

yes here you go:

function tower.FindTarget(newTower, range, mode)
	local bestTarget = nil
	local bestWaypoint = nil
	local bestDistance = nil
	local bestHealth = nil
	local map = workspace.Map:FindFirstChildOfClass("Folder")
	
	for i, mob in ipairs(workspace.Mobs:GetChildren()) do
		local distanceToMob = (mob.HumanoidRootPart.Position - newTower.HumanoidRootPart.Position).Magnitude
		local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude
		
		
		if distanceToMob <= range then
			if mode == "Near" then
				range = distanceToMob
				bestTarget = mob
			elseif mode == "First" then
				if not bestWaypoint or mob.MovingTo.Value >= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value
					
					if not bestDistance or distanceToWaypoint < bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Last" then
				if not bestWaypoint or mob.MovingTo.Value <= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value

					if not bestDistance or distanceToWaypoint > bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Strongest" then
				if not bestHealth or mob.Humanoid.Health > bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			elseif mode == "Weakest" then
				if not bestHealth or mob.Humanoid.Health < bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			end
		end
	end
	
	return bestTarget
end

the one that gets the magnitude between the tower and the enemy is distanceToMob

also the reason i am using findfirstchild(_,true) is because the scripts are in folders and so it just searches inside those

i could take them out of folders and just use findfirstchild normally if you wanted

I wouldn’t recommend nesting if-statements like this. Also, I would maybe recommend Promisifying your return value so you could confirm that it exists and that the Promise resolved. Another reason you may want to consider Promises, you could easily add debug info to rejections to see why something got rejected, and print it in your :catch handler. Also, if the code somehow fails to work instead of erroring the entire thread, a Promise will just immediately throw a rejection in the presence of an unexpected error.

Here’s a re-written example for you.

local Promise = require(pathToPromiseModule)

function tower.FindTarget(newTower, range, mode)
    local bestTarget
    local bestWaypoint
    local bestDistance
    local bestHealth
    local map = workspace.Map:FindFirstChildOfClass("Folder")

    return Promise.new(function(resolve, reject)
        for _, mob in workspace.Mobs:GetChildren() do
            local distanceToMob = (mob.HumanoidRootPart.Position - newTower.HumanoidRootPart.Position).Magnitude
            local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude
            
            if distanceToMob > range then
                continue -- use continue to immediately skip to next index
            end

            -- figure out which is the closest for each type of mode
        end
        
        if bestTarget ~= nil and bestTarget.Humanoid and bestTarget.Humanoid.Health > 0 then
            -- add the humanoid checks here instead of tower.Attack
            return resolve(bestTarget)
        end

        -- reject on failure to find a mob to target
        return reject()
    end)
end

function tower.Attack(newTower, player, name)
    local config = newTower:FindFirstChild("Config")
    
    tower.FindTarget(newTower, config.Range.Value, config.TargetMode.Value):andThen(function(target)
        -- target is now defined, so you can do everything here:
        local targetCFrame = CFrame.lookAt(newTower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
        newTower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame

        events:WaitForChild("AnimateTower"):FireAllClients(newTower, "Attack", target)

        local module = require(script:FindFirstChild(newTower.Name, true))
        module.Attack(newTower, target)

        task.delay(config.Cooldown.Value, function()
            if newTower and newTower.Parent then
                tower.Attack(newTower, player)
            end
        end)
    end):catch(function()
        task.delay(0.1 function()
            if newTower and newTower.Parent then
                tower.Attack(newTower, player)
            end
        end)
    end)
end

ive never heard of this before so im not gonna be good at this, so on this line:

if distance > range then

is it supposed to be bestDistance? its getting underlined with red

bestTarget also has a redline so i assume set it to nil at the top?

1 Like

No, but it shouldn’t be distance, instead it should be distanceToMob.

Why bestTarget is underlined may be because there is no nil check, I will fix that. In honesty I don’t know why I didn’t to begin with, because by default Roblox Instances are truthy statements.

sorry, what do you mean here? paste in the if statements i had?

Yes. I was just too lazy to type it.

yeah this works great i just made a mistake, its actually optimized a lot better

but does this set it to nil? i tried doing this in the seperate module script and it does keep on attacking even out of range:

local Module = {}

function Module.Attack(tower,target)
	local config = tower.Config
	
	repeat
		task.wait()
		target.Humanoid:TakeDamage(1)
	until target == nil
	
end

return Module

here are the functions in case i did something wrong:

function tower.FindTarget(newTower, range, mode)
	local bestTarget = nil
	local bestWaypoint = nil
	local bestDistance = nil
	local bestHealth = nil
	local map = workspace.Map:FindFirstChildOfClass("Folder")

	return Promise.new(function(resolve, reject)
		for _, mob in workspace.Mobs:GetChildren() do
			local distanceToMob = (mob.HumanoidRootPart.Position - newTower.HumanoidRootPart.Position).Magnitude
			local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude

			if distanceToMob >= range then
				continue -- use continue to immediately skip to next index
			end

			-- figure out which is the closest for each type of mode
			if distanceToMob <= range then
				if mode == "Near" then
					range = distanceToMob
					bestTarget = mob
				elseif mode == "First" then
					if not bestWaypoint or mob.MovingTo.Value >= bestWaypoint then
						bestWaypoint = mob.MovingTo.Value

						if not bestDistance or distanceToWaypoint < bestDistance then
							bestDistance = distanceToWaypoint
							bestTarget = mob
						end
					end
				elseif mode == "Last" then
					if not bestWaypoint or mob.MovingTo.Value <= bestWaypoint then
						bestWaypoint = mob.MovingTo.Value

						if not bestDistance or distanceToWaypoint > bestDistance then
							bestDistance = distanceToWaypoint
							bestTarget = mob
						end
					end
				elseif mode == "Strongest" then
					if not bestHealth or mob.Humanoid.Health > bestHealth then
						bestHealth = mob.Humanoid.Health
						bestTarget = mob
					end
				elseif mode == "Weakest" then
					if not bestHealth or mob.Humanoid.Health < bestHealth then
						bestHealth = mob.Humanoid.Health
						bestTarget = mob
					end
				end
			end
			
		end

		if bestTarget ~= nil and bestTarget.Humanoid and bestTarget.Humanoid.Health > 0 then
			-- add the humanoid checks here instead of tower.Attack
			return resolve(bestTarget)
		end

		-- reject on failure to find a mob to target
		return reject()
	end)
end


function tower.Attack(newTower, player, name)
	local config = newTower:FindFirstChild("Config")

	tower.FindTarget(newTower, config.Range.Value, config.TargetMode.Value):andThen(function(target)
		-- target is now defined, so you can do everything here:
		local targetCFrame = CFrame.lookAt(newTower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
		newTower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame

		events:WaitForChild("AnimateTower"):FireAllClients(newTower, "Attack", target)

		local module = require(script:FindFirstChild(newTower.Name, true))
		module.Attack(newTower, target)

		task.delay(config.Cooldown.Value, function()
			if newTower and newTower.Parent then
				tower.Attack(newTower, player)
			end
		end)
	end):catch(function()
		task.delay(0.1, function()
			if newTower and newTower.Parent then
				tower.Attack(newTower, player)
			end
		end)
	end)
end
1 Like

There’s multiple questions to be asked:

  1. What is the value of config.Range.Value?
  2. Do you have it to where Humanoid’s break joints upon reaching zero health? (enabled by default)
  3. Simply test the range “Near”, as that does the simplest search. Does it still target out-of-range mobs?

I cannot seem to figure out why your distance magnitude check is not working, it seems like either something is not being measured correctly, the mobs are somehow not updating their HRP Position, or even the tower has an incorrect HRP Position.

You do not need this part:

if distanceToMob <= range then

This if statement will never be false, as it was checked above with this, which you should also be using > for, not >=.

if distanceToMob > range then
1 Like

changed it but it doesnt work, ill check why but quick question

this is the original script i had:

function tower.FindTarget(newTower, range, mode)
	local bestTarget = nil
	local bestWaypoint = nil
	local bestDistance = nil
	local bestHealth = nil
	local map = workspace.Map:FindFirstChildOfClass("Folder")
	
	for i, mob in ipairs(workspace.Mobs:GetChildren()) do
		local distanceToMob = (mob.HumanoidRootPart.Position - newTower.HumanoidRootPart.Position).Magnitude
		local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude
		
		
		if distanceToMob <= range then
			if mode == "Near" then
				range = distanceToMob
				bestTarget = mob
			elseif mode == "First" then
				if not bestWaypoint or mob.MovingTo.Value >= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value
					
					if not bestDistance or distanceToWaypoint < bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Last" then
				if not bestWaypoint or mob.MovingTo.Value <= bestWaypoint then
					bestWaypoint = mob.MovingTo.Value

					if not bestDistance or distanceToWaypoint > bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
			elseif mode == "Strongest" then
				if not bestHealth or mob.Humanoid.Health > bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			elseif mode == "Weakest" then
				if not bestHealth or mob.Humanoid.Health < bestHealth then
					bestHealth = mob.Humanoid.Health
					bestTarget = mob
				end
			end
		end
	end
	
	return bestTarget
end

am i supposed to leave the return bestTarget at the end or get rid of it?