Projectiles And Item Examples¶
Projectiles are standalone items. A projectile script usually runs with game.this_item, not game.this_character. If Lua owns the projectile's movement, return false so the fallback item controller does not move or destroy it a second time.
Spawn A Projectile From A Character¶
Attach this to the attack animation that should emit the projectile.
return function()
local character = game.this_character
if character == nil then
return true
end
if character.current_animation_frame == 12 then
character.try_spawn_item(
"My Pack", -- pack containing the projectile entity
"Fireball", -- projectile entity name
0, -- projectile animation id
-1, -- object mode; -1 lets the spawned item keep defaults
-2750, -- spawn X offset; helper applies source-facing rules
-2750, -- spawn Y/depth offset
-250, -- X velocity/impulse argument used by spawn helper
0, -- Y/depth velocity argument
90, -- lifetime in frames
8, -- projectile damage
4, -- hit pause when it connects
"fireball-hit", -- optional hit sound
"My Pack", -- hit sound pack
0 -- hit sound channel
)
end
-- Spawning is a one-time action. Let the attack controller continue.
return true
end
Basic Projectile Physics¶
Attach this to the projectile item's active animation.
return function()
local item = game.this_item
if item == nil then
return true
end
-- check_projectile_hits returns true when the projectile hit something.
-- It also applies projectile damage/hit-pause/sound data configured at spawn.
local should_destroy = item.check_projectile_hits()
-- X is horizontal screen/world movement.
item.velocity_x = item.velocity_x + item.impulse_x
-- velocity_y is depth movement along the belt-scroller plane.
item.velocity_y = item.velocity_y + item.impulse_z
-- inner_timer is the usual projectile lifetime.
item.inner_timer = item.inner_timer - 1
if item.inner_timer <= 0 then
should_destroy = true
end
-- position_offset_y is height. Negative means above the ground.
if item.position_offset_y < 0 or item.impulse_y < 0 then
item.position_offset_y = item.position_offset_y + item.impulse_y
if item.position_offset_y >= 0 then
item.position_offset_y = 0
should_destroy = true
end
end
-- Acceleration is applied after movement, matching the default item pattern.
item.impulse_x = item.impulse_x + item.acceleration_x
item.impulse_y = item.impulse_y + item.acceleration_y
item.impulse_z = item.impulse_z + item.acceleration_z
if item.update_position_and_check_world_collisions() then
should_destroy = true
item.play_sound("Classic", "SE10", 0)
end
if should_destroy then
-- Switch to a hit/explosion animation and deactivate the projectile body.
item.set_animation(48)
item.is_active = false
end
return false
end
Projectile That Dies Without A Hit Animation¶
Use this for effects that simply vanish when lifetime, landing, or wall collision ends them.
return function()
local item = game.this_item
if item == nil then
return true
end
local should_destroy = item.check_projectile_hits()
item.velocity_x = item.velocity_x + item.impulse_x
item.velocity_y = item.velocity_y + item.impulse_z
item.inner_timer = item.inner_timer - 1
if item.inner_timer <= 0 then
should_destroy = true
end
if item.position_offset_y < 0 or item.impulse_y < 0 then
item.position_offset_y = item.position_offset_y + item.impulse_y
if item.position_offset_y >= 0 then
item.position_offset_y = 0
should_destroy = true
end
end
item.impulse_x = item.impulse_x + item.acceleration_x
item.impulse_y = item.impulse_y + item.acceleration_y
item.impulse_z = item.impulse_z + item.acceleration_z
if item.update_position_and_check_world_collisions() then
should_destroy = true
end
if should_destroy then
item.is_active = false
item.object_mode = 0
end
return false
end
Stationary Hitbox Or Freeze Field¶
Use this for an item that should keep its current world position and ignore gravity. It still runs world collision so the object remains synchronized with the engine.
return function()
local item = game.this_item
if item == nil then
return true
end
-- This field is intentionally not moved by impulses.
-- The entity can still have hitboxes in logic.json.
item.impulse_x = 0
item.impulse_y = 0
item.impulse_z = 0
item.acceleration_x = 0
item.acceleration_y = 0
item.acceleration_z = 0
item.position_offset_y = 0
item.inner_timer = item.inner_timer - 1
if item.inner_timer <= 0 then
item.is_active = false
item.object_mode = 0
return false
end
item.update_position_and_check_world_collisions()
return false
end
Projectile Burst¶
Spawn many projectile angles from one animation frame. This is useful for fan shots, radial magic, or multi-shuriken bursts.
-- Each row is animation id, velocity X, velocity Y/depth. Built once.
local shots = {
{0, 524, 0},
{1, 511, -116},
{2, 472, -227},
{3, 409, -326},
{4, 326, -409},
{5, 227, -472},
{6, 116, -511},
{7, 0, -524}
}
return function()
local character = game.this_character
if character == nil then
return true
end
if character.current_animation_frame == 20 then
character.play_sound("My Pack", "burst", 2)
for i = 1, #shots do
local shot = shots[i]
character.try_spawn_item(
"My Pack",
"Burst Projectile",
shot[1],
-1,
-850,
2000,
shot[2],
shot[3],
60,
4,
2,
"projectile-hit",
"My Pack",
0
)
end
end
return true
end
Persistent Area Hitbox¶
For a repeated-damage area, use an item for the visual/effect and a global or owner script for the repeated hit rule. This avoids relying on undocumented projectile fields.
return function()
local item = game.this_item
if item == nil then
return true
end
-- Keep the field alive for custom global logic. The global hook can scan
-- active enemies and apply damage when their positions overlap this item.
item.custom_value_1 = 1
item.inner_timer = item.inner_timer - 1
if item.inner_timer <= 0 then
item.is_active = false
item.object_mode = 0
end
return false
end
Pair it with a pack-level post-update script:
return function()
local character = game.this_character
if character == nil or not character.is_enemy then
return true
end
-- The actual area scan would loop over known area object IDs stored in
-- custom values or use a known single spawned item ID. This compact example
-- shows the damage-side rule only.
if character.custom_value_8 > 0 then
character.custom_value_8 = character.custom_value_8 - 1
return true
end
-- A real script should first check distance to the area item.
-- Use custom_value_8 as a per-enemy cooldown so the same field can hit
-- repeatedly without damaging every frame.
character.damage_taken = 2
character.is_being_hit = true
character.check_being_hit()
character.custom_value_8 = 12
return true
end
Spawn An Enemy Helper¶
The item layer also exposes try_spawn_enemy. Use it for minions, traps, or helper attacks.
return function()
local character = game.this_character
if character == nil then
return true
end
if character.current_animation_frame == 30 then
local helper_entity_id = 123456789
character.try_spawn_enemy(
"My Pack",
helper_entity_id, -- replace with the helper enemy's entity type/id
0,
0,
-1000,
0
)
end
return true
end
If your data uses entity names rather than numeric IDs, prefer SpawnEnemyEvent in logic events, which hashes entityName for you. The Lua helper is intentionally lower level and expects the entity type/id value.