Animation and message system

Trying to figure out best solution for this in defold.

I have a spell factory that fires a spell and it has two animation one when it shots and one when it hits or travels X distance.

When I fire this and it collides with an enemy I have it send out a message “hit” and then play the animation for when it hits. There is a delay to this animation and then it is deleted. But due to this 0.3 delay the “hit” message gets sent and received multiple times by the enemy.

If I call go.delete right away only one hit message is sent and everything works as expected. But then I don’t get my second animation. If I call the function even without a delay it still sends two hit messages before deletion.

What is the best way to handle this when your spells have more than one animation? Or should you simply avoid this and trigger a second factory that spawns the new animation on hit/target?

--cosmic_shot script 
go.property("dir", vmath.vector3(1, 0, 0))
local SPEED = 900
local DISTANCE = 700
local DAMAGE_AMOUNT = 2
local BLAST_DURATION = 0.3 -- Adjust based on actual animation length

function init(self)
	sound.play("#sound", { delay = 0, gain = 0.5, pan = -1.0, speed = 1.25 })

	-- Ensure self.dir is correctly set; default to right if missing
	self.dir = self.dir or vmath.vector3(1, 0, 0)

	self.start_pos = go.get_position()
	self.target_x = self.start_pos.x + (self.dir.x * DISTANCE)

	-- Animate movement
	local duration = DISTANCE / SPEED
	go.animate(".", "position.x", go.PLAYBACK_ONCE_FORWARD, self.target_x, go.EASING_LINEAR, duration, 0, function()
		play_blast_animation(self)
	end)
end

function play_blast_animation(self)
	-- Flip the sprite properly instead of setting negative scale
	sprite.set_vflip("#sprite", self.dir.x < 0)

	-- Play explosion animation
	sprite.play_flipbook("#sprite", "blast")

	-- Ensure projectile gets deleted after animation duration
	timer.delay(BLAST_DURATION, false, function()
		go.delete()
	end)
end

function on_message(self, message_id, message, sender)
	if message_id == hash("collision_response") and message.group == hash("enemy") then
		msg.post(message.other_id, "hit", { damage = DAMAGE_AMOUNT })

		-- Stop movement and play explosion effect
		-- go.cancel_animations(".", "position")
		play_blast_animation(self)
	end
end
-- enemy script
local SPEED = 150         -- Adjust speed as needed
local GRAVITY = -300      -- Gravity force applied to enemy
local is_grounded = false -- Track if enemy is touching the ground
local STARTING_HEALTH = 10

--  Store hash values at the top for easy changes
local HASH_HIT = hash("hit")
local HASH_CONTACT = hash("contact_point_response")
local HASH_GROUND = hash("ground")
local HASH_PLAYER = hash("player")
local HASH_DINO_ATTACK = hash("dino_attack")

function init(self)
	self.player_id = "/player/player"     -- Use the correct path for player
	self.speed = SPEED
	self.velocity = vmath.vector3(-50, 0, 0) -- Store velocity for movement
	self.health = STARTING_HEALTH
end

function update(self, dt)
	if not self.player_id then return end -- Ensure player exists

	local enemy_pos = go.get_position()
	local player_pos = go.get_position(self.player_id)

	-- Calculate direction to player (only move horizontally)
	local direction = player_pos.x - enemy_pos.x

	if math.abs(direction) > 1 then
		direction = direction / math.abs(direction) -- Normalize to -1 or 1
	else
		direction = 0                         -- Stop moving if very close
	end

	-- Update velocity based on direction
	self.velocity.x = direction * SPEED

	-- Apply gravity if not grounded
	if not is_grounded then
		self.velocity.y = self.velocity.y + GRAVITY * dt
	else
		self.velocity.y = 0 -- Prevents gravity from interfering with movement
	end

	-- Move enemy
	local new_position = enemy_pos + self.velocity * dt
	go.set_position(new_position)
end

function take_damage(self, amount)
	self.health = self.health - amount
	print("Enemy took damage! Health:", self.health)

	if self.health <= 0 then
		die(self)
	end
end

function die(self)
	print("Enemy has died!")
	go.delete()
end

function on_message(self, message_id, message, sender)
	print(message.other_id)
	if message_id == HASH_HIT then
		local damage = message.damage or 1 -- Default to 1 if no value is provided
		take_damage(self, damage)
	end
	if message_id == HASH_CONTACT then
		-- Check if the enemy is colliding with the ground
		if message.group == HASH_GROUND then
			is_grounded = true
			self.velocity.y = 0 -- Stop falling
		end

		-- Stop enemy from pushing through objects
		if message.group == HASH_PLAYER then
			self.velocity.x = 0
		end
	end
end

-- cosmic_shot.script

go.property("dir", vmath.vector3(1, 0, 0))

local SPEED = 900

local DISTANCE = 700

local DAMAGE_AMOUNT = 2

local BLAST_DURATION = 0.3 -- Duration of the blast animation

function init(self)

    sound.play("#sound", { delay = 0, gain = 0.5, pan = -1.0, speed = 1.25 })

    self.dir = self.dir or vmath.vector3(1, 0, 0)

    self.start_pos = go.get_position()

    self.target_x = self.start_pos.x + (self.dir.x * DISTANCE)

    self.hit_detected = false -- Prevent multiple triggers

    local duration = DISTANCE / SPEED

    go.animate(".", "position.x", go.PLAYBACK_ONCE_FORWARD, self.target_x, go.EASING_LINEAR, duration, 0, function()

        play_blast_animation(self)

    end)

end

function play_blast_animation(self)

    sprite.set_vflip("#sprite", self.dir.x < 0)

    sprite.play_flipbook("#sprite", "blast")

    timer.delay(BLAST_DURATION, false, function()

        go.delete()

    end)

end

function on_message(self, message_id, message, sender)

    if message_id == hash("collision_response") and message.group == hash("enemy") then

        if self.hit_detected then return end -- Ignore further collisions

        self.hit_detected = true       -- Mark as hit

        msg.post(message.other_id, "hit", { damage = DAMAGE_AMOUNT })

        -- Stop movement and play explosion effect
        go.cancel_animations(".", "position")

        play_blast_animation(self)

    end

end

This is the solution I came up with but it seems like a hack rather than a proper solution and would love to know if there is a better way. Add a hit detected check then return if it is if not then do damage, repeat