Help with tower defense game

do you mean that it should all be done in the towers module script and not the main general one?

basically they are both module scripts, there is the main one and it has the attack function, this function finds the target, awards money for killing and does the animations, but since towers have different ablities the takeDamage is not done in the main script, but it goes to a seperate module script that is made for that tower and does it there

i have some placeholder towers and this is how it is:
image

The way you’re handling damage is just generally flawed. Module.Attack should simply be handing out the damage, and special types like poison damage should be handled by the individual towers (or wrappers). An example of this may be something along the line of this.

In reality you would want to define the cooldown between damage ticks, how many extra damage ticks there should be, how much damage initial hit should deal, how much damage ticks should deal, and that module.Attack should have an argument for dealing damage.

Here I am just assuming the following:

  • TickDamage is equal to 5. Meaning each tick will deal 5 damage.
  • Ticks is equal to 3. Meaning it will repeat tick damage 3 times.
  • TimeBetweenTicks is equal to 1. Meaning it will repeat TickDamage after this time.
  • InitialDamage is equal to 10. Meaning when it first hits the mob, it will deal 10 damage before ticks start occurring.
function tower.Attack(newTower, player, name)
    local config = newTower:FindFirstChild("Config")

    tower.FindTarget(newTower, config.Range.Value, config.TargetMode.Value):andThen(function(target)
        -- play tower animation

        -- deal initial damage for hitting the target
        local module = require(script:FindFirstChild(newTower.Name, true))
        module.Attack(newTower, target, config.InitialDamage.Value)

        -- if the target is already poisoned, return here, this will avoid stacking damage
        if target:GetAttribute("Poisoned") then
            return
        end

        -- mark the target as poisoned
        target:SetAttribute("Poisoned", true)
        for i = 1, config.Ticks.Value do -- deal three extra ticks of damage
            task.wait(config.TimeBetweenTicks.Value)
            module.Attack(newTower, target, config.TickDamage.Value)
        end

        target:SetAttribute("Poisoned", false)
    end)

    task.wait(config.Cooldown.Value)
    if newTower and newTower.Parent then
        tower.Attack(newTower, player)
    end
end

this looks good however this is the main script and the reason i have different module scripts is that all the towers dont have to get crammed into one script, so would it be possible to move it over there?

also poison was just something to understand the concept, all it needs to do is if the enemy enters the range, the tower should constantly damage the target until its left the range. by that i mean just like a repeat loop, it shoud deal like 15 damage every heartbeat or so

target enters range, tower plays throwing animation and target takes 15 damage like every 0.1 or 0.01 secs

that’s basically it

I am no advocate of Object Orientated Programming, but since you’re pretty much already using classes, you may as well have classes for tower types.

-- Tower.lua
local Tower = {}
Tower.__index = Tower

export type TowerData = {
    player: Player,
    name: string,
    cooldown: number,
    range: number,
    mode: "Near" | "First" | "Last" | "Strongest" | "Weakest",
    damage: number,
}

function Tower.new(tower, towerData: TowerData)
    (towerData :: {[string]: any}).tower = tower
    return setmetatable(towerData, Tower)
end

function Tower:attack(damage: number?)
    -- while i'm here, i might as well just refactor your code
    self:findTarget()
        :andThen(function(target)
            local targetCFrame = CFrame.lookAt(self.tower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
            self.tower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame

            events:WaitForChild("AnimateTower"):FireAllClients(self.tower, "Attack", target)

            local module = require(script:FindFirstChild(self.tower.Name, true))
            module.Attack(self.tower, target, damage or self.damage)
        end)

    task.wait(self.cooldown)
    if not self:alive() then return end
    self:attack(damage)
end

function Tower:alive()
    return self.tower and self.tower.Parent
end

function Tower:findTarget()
    local bestTarget = nil
    local bestWaypoint = nil
    local bestDistance = self.range
    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 - self.tower.HumanoidRootPart.Position).Magnitude
            local distanceToWaypoint = (mob.HumanoidRootPart.Position - map.Waypoints[mob.MovingTo.Value].Position).Magnitude

            if bestDistance > distanceToMob then
                continue
            end

            if self.mode == "Near" then
                bestDistance = distanceToMob
                bestTarget = mob
            elseif
                ...
            end
        end

        if bestTarget ~= nil and bestTarget.Humanoid and bestTarget.Humanoid.Health > 0 then
            return resolve(bestTarget)
        end

        return reject()
    end)
end

return Tower
-- PoisonTower.lua
local Tower = require(script.Parent.Tower)
local PoisonTower = {}
PoisonTower.__index = PoisonTower

export type PoisonTowerData = Tower.TowerData & {
    tickDamage: number,
    ticks: number,
    timeBetweenTicks: number,
}

function PoisonTower.new(tower, towerData: PoisonTowerData)
    local super = Tower.new(tower, towerData :: Tower.TowerData)
    (towerData :: {[string]: any}).tower = tower
    (towerData :: {[string]: any}).super = super
    return setmetatable(towerData, PoisonTower)
end

function PoisonTower:attack(damage: number?)
    self.super:findTarget()
        :andThen(function(target)
            local targetCFrame = CFrame.lookAt(self.tower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
            self.tower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame

            events:WaitForChild("AnimateTower"):FireAllClients(self.tower, "Attack", target)

            local module = require(script:FindFirstChild(self.tower.Name, true))
            module.Attack(self.tower, target, damage or self.damage)

            if target:GetAttribute("Poisoned") then return end
            target:SetAttribute("Poisoned", true)
            for i = 1, self.ticks do
                task.wait(self.timeBetweenTicks)
                module.Attack(self.tower, target, self.tickDamage)
            end
            target:SetAttribute("Poisoned", false)
        end)

    task.wait(self.cooldown)
    if not self.super:alive() then return end
    self:attack(damage)
end

return PoisonTower

woah i never even knew there was object orianted programming in lua, also in poisontower.new the brackets before towerData are getting underlined red

There barely is OOP in Lua without some metatable shenanigans, it’s also discouraged to really use OOP in pretty much any fashion in any programming language. However I just provided this as an example since you already seemed to be implementing pure functions that resembled classes.

how do i fix the brackets getting underlined here?

also there is this error:

ServerScriptService.Main.Tower:180: findTarget is not a valid member of Model “Workspace.Towers.GunnerLvl0”

also if you need the function the spawns the tower let me know that has the coroutine for the attack

Oh sorry I made those scripts on my phone, no testing. You could most likely fix the errors with the parenthesis by simply putting ; at the end of those few lines. The other error it seems I typed something wrong and referenced the model instance instead of the table.

hi, so the errors were that you were dong self:findTarget and not Tower:FindTarget and that was in some other places too so i fixed those

here is the modified function:

function Tower:attack(damage: number?)
	Tower:findTarget()
	:andThen(function(target)
		local targetCFrame = CFrame.lookAt(self.tower.HumanoidRootPart.Position, target.HumanoidRootPart.Position)
		self.tower.HumanoidRootPart.BodyGyro.CFrame = targetCFrame
		
		events:WaitForChild("AnimateTower"):FireAllClients(self.tower, "Attack", target)

		local module = require(script:FindFirstChild(self.tower.Name, true))
		module.Attack(self.tower, target, damage or self.damage)
	end)

	task.wait(self.Config.Cooldown.Value)
	if not Tower:alive() then return end
	self:attack(damage)
end

issue is that the promise module script is now giving me this:
image

this is the line that this error is coming from:

:andThen(function(target)

one important thing is i dont understand why you put damage: number? in the parameters, im not sure if thats intentional

here is the spawn function just in case you wanna fix something:

function tower.Spawn(player, name, cframe, previous)
	local allowedToSpawn = true --tower.CheckSpawn(player,name)
	
	if allowedToSpawn then
		
		local newTower
		local oldTargetMode = nil
		if previous then
			oldTargetMode = previous.Config.TargetMode.Value
			previous:Destroy()
			newTower = game.ReplicatedStorage.Towers.Upgrades[name]:Clone()
		else
			newTower = game.ReplicatedStorage.Towers[name]:Clone()
		end
		
		local ownerValue = Instance.new("StringValue")
		ownerValue.Name = "Owner"
		ownerValue.Value = player.Name
		ownerValue.Parent = newTower.Config
		
		local targetMode = Instance.new("StringValue")
		targetMode.Name = "TargetMode"
		targetMode.Value = oldTargetMode or "First"
		targetMode.Parent = newTower.Config
		
		newTower.HumanoidRootPart.CFrame = cframe
		newTower.Parent = workspace.Towers
		newTower.HumanoidRootPart:SetNetworkOwner(nil)
		
		local bodyGyro = Instance.new("BodyGyro")
		bodyGyro.MaxTorque = Vector3.new(math.huge,math.huge,math.huge)
		bodyGyro.D = 0
		bodyGyro.CFrame = newTower.HumanoidRootPart.CFrame
		bodyGyro.Parent = newTower.HumanoidRootPart
	
		for i, object in ipairs(newTower:GetDescendants()) do
			if object:IsA("BasePart") then
				object.CollisionGroup = "Tower"
			end	
		end
		
		player.Money.Value -= newTower.Config.Price.Value
		
		coroutine.wrap(tower.Attack)(newTower, player, name)
		return newTower
		
	else
		warn("no tower ["..name.."]")
		return false
	end
end

i dont really understand what the script is but more importantly i dont know what youre trying to do with the ticks, because the ticks seems to be a set value but whatever just let me know if you can help

OOP is one of many design paradigms with a specific purpose and usage case. No one frowns upon the usage of OOP. It exists to allow for easy deployment of large-scale business applications and to develop solutions to complex problems using simple logic. Yes, there are better and worse development patterns out there, but OOP is most definitely not frowned upon if used. It is extremely useful and versatile and is primarily what I develop at the company I work for.

1 Like

Pure functions are simply more useful, people like OOP because of its relative simplicity, especially when it comes to languages that actually support it (not Lua). Lua uses OOP in a bad manner, with metatables. OOP is sometimes good, but in Lua it’s inefficient because it’s just not supported, in most cases actually more difficult than just using pure functions.

OOP is widely disliked because it’s design has a lot of cut offs, it’s known to make simple projects more complicated even though it’s purpose is to be the opposite of that. It’s a good reason why most programmers don’t even like touching OOP-strict languages, rather finding an OOP-friendly but not required language, Lua is just neither of those.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.