Best Defold/LUA Coding Practices for this code

I have this code currently on 3 types of bullets, so I can change their damage, speed, and blast radius. You can see I have a few custom places in the header where I tell which bullet this is and what bullet factory to use. But the rest of the code is the same. So should I use it like this and make 3 different scripts? Or should I rework it a bit to but all the types of bullets in one script so I only have one script to manage. Keep in mind I may be adding more bullet types in the future and those may or may not change the behavior in the script below. Like one my be a mortar type shell so it’s trajectory may be different. or one my be a straight line laser that doesn’t follow the target.

I have this same issue for the tower placement as well, but there I don’t think they’ll have different behaviors.

local entmgr = require("modules.entity_manager")
local attributes = require("modules.attributes")

local COLLISION_RESPONSE = hash("collision_response")
local TRIGGER_RESPONSE = hash("trigger_response")
local CONTACT_POINT_RESPONSE = hash("contact_point_response")
local ENEMY = hash("enemy")
local OWN_GROUP = hash("bullet")

local BULLET_NAME = "twin_rocket"
local DAMAGE = attributes.get_attribute(BULLET_NAME, "damage")
local SPEED = attributes.get_attribute(BULLET_NAME, "speed")

local function target_enemy(self, enemy_id)
	self.curr_pos = go.get_world_position()
	if go.exists(enemy_id) then
		self.target_pos = go.get_world_position(enemy_id)
		self.in_target_zone = true
		local direction = vmath.vector3(self.target_pos.x - self.curr_pos.x, self.target_pos.y - self.curr_pos.y, 0)
		-- Calculate the angle
		local angle = math.atan2(direction.y, direction.x)
		-- Adjust the angle to point in the direction of the target
		angle = angle + math.pi/2
		angle = angle + math.pi
		-- Set the rotation of the turret
		go.set_rotation(vmath.quat_rotation_z(angle))
	end
end

local function explode_missile(self)
	self.target_id = nil -- Clear the target
	-- Additional logic to handle lost target (e.g., redirect missile)
	sprite.play_flipbook("#sprite", "explosion", function()
		go.cancel_animations(".")
		go.delete() -- deletes the missile game object
	end)
end

function init(self)
	entmgr.subscribe(msg.url("#"), entmgr.enemies)
	self.target_id = nil -- Add this to store the target ID
end

function on_message(self, message_id, message, sender)
	-- Handle the set_target message
	if message_id == hash("set_target") then
		DAMAGE = attributes.get_attribute(BULLET_NAME, "damage")
		SPEED = attributes.get_attribute(BULLET_NAME, "speed")
		self.target_id = message.target_id
		return
	elseif message_id == hash("entity destroyed") and message.group == entmgr.enemies then
		if message.entity == self.target_id then
			explode_missile(self)
		end
	end	
	if message_id == TRIGGER_RESPONSE then
		if message.group == ENEMY  and message.own_group == OWN_GROUP then
			explode_missile(self)
			msg.post(message.other_id, "take_damage", { damage_amount = DAMAGE } )
		end
	end
end

function update(self, dt)
	if go.exists(self.target_id) and self.target_id ~= nil then  --this appears redundant but it is needed to make this work properly
		target_enemy(self, self.target_id)
		local missile_pos = go.get_world_position(self.missile_id)
		local target_pos = go.get_world_position(self.target_id)
		local dir = vmath.normalize(self.target_pos - missile_pos)
		-- move the missile towards the target
		missile_pos = missile_pos + dir * SPEED * dt
		go.set_position(missile_pos, self.missile_id)
		--rotate missile toward target 
		local angle = math.atan2(dir.y, dir.x)
		angle = angle - math.pi/2
		--move missile toward target
		go.set_rotation(vmath.quat_rotation_z(angle), self.missile_id)
	else
		self.target_id = nil  -- need to set self.target_id to nil so the If statement can trap the enemy object being deleted
	end
end

function final(self)
	-- Unsubscribe when the script is deleted
	entmgr.unsubscribe(msg.url("#"), entmgr.enemies)
end

This is the tower script. It has a few more variables that need to be changed.

local entmgr = require("modules.entity_manager")
local attributes = require("modules.attributes")

local COLLISION_RESPONSE = hash("collision_response")
local TRIGGER_RESPONSE = hash("trigger_response")
local CONTACT_POINT_RESPONSE = hash("contact_point_response")
local ENEMY = hash("enemy")
local TOWER = hash("tower")
local FACTORY = "/bullet_factories#twin_rocket"
local TOWER_NAME = "twin_rocket_tower"
local RELOAD_DELAY = nil
local AMMO_MAGAZINE = nil

local tower = {}
tower.targets = {} -- Table to store targets

local shots_fired = 0 --number of bullets that can fire at once 
local tower_ready = true

-- Call this function when a target enters the zone
local function onTargetEnter(target)
	tower.addTarget(target)
end

-- Call this function when a target leaves the zone or is destroyed
local function onTargetExit(target)
	tower.removeTarget(target)
end

local function safe_get_world_position(id)
	return go.get_world_position(id)
end

local function target_enemy(self, enemy_id)
	self.curr_pos = go.get_world_position() 
	if go.exists(enemy_id) then 
		self.target_id_pos = go.get_world_position(enemy_id)
		self.in_target_zone = true
		local direction = vmath.vector3(self.target_id_pos.x - self.curr_pos.x, self.target_id_pos.y - self.curr_pos.y, 0)
		-- Calculate the angle
		local angle = math.atan2(direction.y, direction.x)
		-- Adjust the angle to point in the direction of the target
		angle = angle + math.pi/2
		angle = angle + math.pi
		-- Set the rotation of the turret
		go.set_rotation(vmath.quat_rotation_z(angle))
	end
end

local function late_init()
	RELOAD_DELAY = attributes.get_attribute(TOWER_NAME, "reload_delay")
	AMMO_MAGAZINE = attributes.get_attribute(TOWER_NAME, "magazine_size")	
end

function init(self)
	entmgr.subscribe(msg.url("#"), entmgr.enemies)
	self.timer = 0  -- Add this line to initialize the timer
	self.reload_delay = RELOAD_DELAY 
	self.in_target_zone = false
	self.target_id = nil

	-- Set up a timer to call late_init after 2 seconds
	timer.delay(1, false, function()
		late_init()
	end)
end

function update(self, dt)
	if tower_ready and #tower.targets > 0 then
		tower.shootAtTarget(self)
	end
end

function on_message(self, message_id, message, sender)
	if message_id == TRIGGER_RESPONSE then
		if message.enter and message.group == ENEMY then
			tower.addTarget(message.other_id)
		elseif message.enter == false and message.group == ENEMY then
			tower.removeTarget(message.other_id)
		end
	elseif message_id == hash("entity destroyed") and message.group == entmgr.enemies then		
		tower.removeTarget(message.entity)
	end	
end

function tower.addTarget(target)
	table.insert(tower.targets, target)
	-- Additional logic if needed when a target is added
end

function tower.removeTarget(target)
	for i, t in ipairs(tower.targets) do
		if t == target then
			table.remove(tower.targets, i)
			break
		end
	end
	-- Additional logic if needed when a target is removed
end

function tower.chooseTarget()
	-- Example: Choose the first target in the list
	if #tower.targets > 0 then
		return tower.targets[1]
	else
		return nil
	end
end

function tower.shootAtTarget(self)
	if not tower_ready or #tower.targets == 0 then return end

	local target = tower.chooseTarget()
	if target then
		self.target_id = target  -- Set the current target
		target_enemy(self, target)  -- Aim at the target

		self.bullet_id = factory.create(FACTORY, self.curr_pos) -- create the bullet
		msg.post(self.bullet_id, "set_target", { target_id = target }) 
		shots_fired = shots_fired + 1	
		if shots_fired >= AMMO_MAGAZINE then 
			tower_ready = false
			timer.delay(RELOAD_DELAY, false, function()
				tower_ready = true
				shots_fired = 0
			end)
		end
	end
end

function final(self)
	-- Unsubscribe from notifications when the script is destroyed
	entmgr.unsubscribe(msg.url("#"), entmgr.enemies)
end

I think it’s up to you. :sweat_smile:
If you prefer control all your bullet types in a manager module then you can have the same script for all your bullet object, just pass the neccessary visual data into it (the manager module) and put something like entmgr.update(dt) into the script’s function update(dt).
Or you can have each script per each bullet type. This way, when you get a message for example “shoot”, you can do its own animation in the current script.

FWIW: I ended up using one script for the bullets.

local entmgr = require("modules.entity_manager")
local attributes = require("modules.attributes")

local COLLISION_RESPONSE = hash("collision_response")
local TRIGGER_RESPONSE = hash("trigger_response")
local CONTACT_POINT_RESPONSE = hash("contact_point_response")
local ENEMY = hash("enemy")
local OWN_GROUP = hash("bullet")

local function target_enemy(self, enemy_id)
	self.curr_pos = go.get_world_position()
	if go.exists(enemy_id) then
		self.target_pos = go.get_world_position(enemy_id)
		self.in_target_zone = true
		local direction = vmath.vector3(self.target_pos.x - self.curr_pos.x, self.target_pos.y - self.curr_pos.y, 0)
		-- Calculate the angle
		local angle = math.atan2(direction.y, direction.x)
		-- Adjust the angle to point in the direction of the target
		angle = angle + math.pi/2
		angle = angle + math.pi
		-- Set the rotation of the turret
		go.set_rotation(vmath.quat_rotation_z(angle))
	end
end

local function explode_missile(self)
	self.target_id = nil -- Clear the target
	if BULLET_TYPE == plasma_bullet then
		go.delete()
	else
		-- Additional logic to handle lost target (e.g., redirect missile)
		sprite.play_flipbook("#sprite", "explosion", function()
			go.cancel_animations(".")
			go.delete() -- deletes the missile game object
		end)
	end
end

function init(self)
	entmgr.subscribe(msg.url("#"), entmgr.enemies)
	self.target_id = nil 
end

function on_message(self, message_id, message, sender)
	-- Handle the set_target message
	if message_id == hash("set_target") then
		damage = attributes.get_attribute(message.bullet_type, "damage")
		speed = attributes.get_attribute(message.bullet_type, "speed")
		self.target_id = message.target_id
		return
	elseif message_id == hash("entity destroyed") and message.group == entmgr.enemies then
		if message.entity == self.target_id then
			explode_missile(self)
		end
	end	
	if message_id == TRIGGER_RESPONSE then
		if message.group == ENEMY  and message.own_group == OWN_GROUP then
			explode_missile(self)
			msg.post(message.other_id, "take_damage", { damage_amount = damage } )
		end
	end
end

function update(self, dt)
	if go.exists(self.target_id) and self.target_id ~= nil then  --this appears redundant but it is needed to make this work properly
		target_enemy(self, self.target_id)
		local missile_pos = go.get_world_position(self.missile_id)
		local target_pos = go.get_world_position(self.target_id)
		local dir = vmath.normalize(self.target_pos - missile_pos)
		-- move the missile towards the target
		missile_pos = missile_pos + dir * speed * dt
		go.set_position(missile_pos, self.missile_id)
		--rotate missile toward target 
		local angle = math.atan2(dir.y, dir.x)
		angle = angle - math.pi/2
		--move missile toward target
		go.set_rotation(vmath.quat_rotation_z(angle), self.missile_id)
	else
		self.target_id = nil  -- need to set self.target_id to nil so the If statement can trap the enemy object being deleted
	end
end

function final(self)
	-- Unsubscribe when the script is deleted
	entmgr.unsubscribe(msg.url("#"), entmgr.enemies)
end
1 Like