Skip to main content
Projectiles are objects that travel through the level and explode when they hit something. You create them with Level.createProjectile, configure their speed, direction, homing behavior, and explosion rules, then fire them — either automatically at creation or manually in your own code. Every projectile can carry onFire, onExplode, and onUpdate callbacks so you have frame-by-frame control over its behavior.

Creating a Projectile

The simplest signature creates a projectile in the main section using an inline property table:
--- @param projectileProperties { ... }
--- @return Projectile
function Level.createProjectile(projectileProperties)

--- Create in a named section
--- @param sectionId string
--- @param projectileProperties { ... }
--- @return Projectile
function Level.createProjectile(sectionId, projectileProperties)
You can also pass a ProjectileRequest object instead of a table.

Full Property Table

All fields except skin, origin, and rotation are optional.
FieldTypeDescription
skinstringSkin ID to use (see Level.getAvailableProjectileSkins())
originVector3World-space spawn position
rotationQuaternionInitial facing direction
tagstring?Arbitrary tag for identification
powernumber?Damage power of the projectile
lifetimenumber?Seconds before the projectile auto-explodes
speednumber?Initial travel speed
accelerationnumber?Speed change per second
angularSpeednumber?Rotation speed
angularAccelerationnumber?Rotation acceleration
angularLocalAxisRotationVector3Local axis around which the projectile spins
homingStrengthnumber?How strongly the projectile tracks the ball (0 = none)
maxHomingRangenumber?Maximum distance at which homing activates
autoFireboolean?If true, fire immediately at creation
explodeOnCollideWithBlocksboolean?Explode on block contact
explodeOnCollideWithItemsboolean?Explode on item contact
explodeOnCollideWithEnemiesboolean?Explode on enemy contact
explodeOnCollideWithBallsboolean?Explode on ball contact
explodeOnCollideWithDecorationsboolean?Explode on decoration contact
explodeOnCollideWithProjectilesboolean?Explode on projectile contact
onFirefunction?Called when the projectile is fired
onExplodefunction?Called when the projectile explodes
onUpdatefunction?Called every frame with (projectile, deltaTime)

Using ProjectileRequest

ProjectileRequest is a class alternative to the inline table. It is useful when you want to build the configuration incrementally or reuse a base configuration across multiple enemies.
--- Constructors
function ProjectileRequest.new(): ProjectileRequest
function ProjectileRequest.new(skin: string): ProjectileRequest
function ProjectileRequest.new(skin: string, origin: Vector3, rotation: Quaternion): ProjectileRequest
Set properties directly on the object:
local req = ProjectileRequest.new("fireball_skin")
req.origin    = Vector3.new(10, 2, 5)
req.rotation  = Quaternion.identity        -- point forward
req.speed     = 8.0
req.lifetime  = 6.0
req.homingStrength  = 2.5
req.maxHomingRange  = 20.0
req.autoFire  = true
req.explodeOnCollideWithBalls = true

req.onExplode = function(projectile)
    Dialog.createTemporary("Direct hit!", 1.5, Color.red)
end

local proj = Level.createProjectile(req)

Querying Available Skins

Before you create a projectile, make sure the skin you want actually exists in the level:
--- Returns a table of skin ID strings for the main section
function Level.getAvailableProjectileSkins(): table

--- Returns skins for a specific section
function Level.getAvailableProjectileSkins(sectionId: string): table
-- Print all available skins to the log (useful during development)
local skins = Level.getAvailableProjectileSkins()
for _, skinId in ipairs(skins) do
    print("Skin available: " .. skinId)
end

The Projectile Object

Level.createProjectile returns a Projectile. You can read and write its properties after creation, and call methods to fire or detonate it manually.

Key Properties

PropertyTypeR/WDescription
isActivebooleanRtrue while the projectile is in flight
isHomingbooleanRtrue when homingStrength > 0
positionVector3R/WCurrent world position
rotationQuaternionR/WCurrent rotation
forwardVector3RForward direction vector
velocityVector3RCurrent velocity vector
speednumberR/WTravel speed
lifetimenumberR/WRemaining lifetime
homingStrengthnumberR/WHoming tracking strength

Fire and Explode

-- Fire from the projectile's current origin and rotation
function Projectile:fire(): any

-- Fire with a specific initial speed override
function Projectile:fire(initialSpeed: number): any

-- Fire from a specific world position
function Projectile:fire(position: Vector3): any

-- Fire with a specific rotation
function Projectile:fire(rotation: Quaternion): any

-- Fire from a position and rotation
function Projectile:fire(position: Vector3, rotation: Quaternion): any

-- Fire with full override: position, rotation, and speed
function Projectile:fire(position: Vector3, rotation: Quaternion, initialSpeed: number): any

-- Explode immediately at current position
function Projectile:explode(): any

-- Explode with a specific rotation (for directional explosion effects)
function Projectile:explode(rotation: Quaternion): any

The OnProjectileCollide Hook

Any entity script (Ball, Block, Enemy, Item, Side) can define OnProjectileCollide to react when a projectile hits it:
---@param self Ball
---@param projectile Projectile
function OnProjectileCollide(self, projectile)
    -- 'projectile' is the Projectile that struck this entity
    if projectile.tag == "enemy_shot" then
        Dialog.createTemporary("Ouch! You were hit!", 2.0, Color.red)
    end
end
OnProjectileCollide fires before the projectile explodes. The projectile is still active inside this callback.

Complete Example — Enemy That Fires a Homing Projectile

This script is attached to an Enemy entity. It fires a homing shot at the ball every 4 seconds using a cooldown tracked via elapsed time. When the projectile hits the ball, the player loses a life.
-- Enemy script: homing_shooter

local FIRE_INTERVAL = 4.0   -- seconds between shots
local PROJECTILE_SKIN = "enemy_fireball"

local cooldown = FIRE_INTERVAL  -- start ready to fire on spawn

function OnSpawn(self)
    -- Nothing special needed on spawn; cooldown starts at max
end

function OnUpdate(self, deltaTime)
    cooldown = cooldown - deltaTime

    if cooldown <= 0 then
        cooldown = FIRE_INTERVAL
        fireAtBall(self)
    end
end

function fireAtBall(enemy)
    local ball = Level.currentBall
    if ball == nil or not ball.isAlive then
        return
    end

    -- Calculate direction from enemy to ball
    local enemyPos = enemy.position
    local ballPos  = ball.position

    local dx = ballPos.x - enemyPos.x
    local dy = ballPos.y - enemyPos.y
    local dz = ballPos.z - enemyPos.z

    -- Use Quaternion.lookRotation to face the ball
    local direction = Vector3.new(dx, dy, dz).normalized
    local shotRotation = Quaternion.lookRotation(direction, Vector3.up)

    Level.createProjectile({
        skin     = PROJECTILE_SKIN,
        origin   = Vector3.new(enemyPos.x, enemyPos.y, enemyPos.z),
        rotation = shotRotation,
        tag      = "enemy_shot",
        speed    = 6.0,
        lifetime = 8.0,
        homingStrength = 3.0,
        maxHomingRange = 25.0,
        autoFire = true,
        explodeOnCollideWithBalls   = true,
        explodeOnCollideWithBlocks  = true,

        onFire = function(projectile)
            -- Optional: play a visual cue (not shown — use engine events)
        end,

        onExplode = function(projectile)
            -- If the projectile is near the ball when it explodes, lose a life
            local proj  = projectile
            local bPos  = Level.currentBall.position
            local dist  = Vector3.distance(proj.position, bPos)
            if dist < 1.5 then
                Level.loseLives(1)
                Dialog.createTemporary("You were hit!", 2.0, Color.red)
            end
        end,
    })
end

function OnDeath(self)
    -- Enemy died; nothing to clean up since projectiles have their own lifetime
end
Always check ball.isAlive before reading ball.position inside OnUpdate. The ball reference from Level.currentBall can briefly be inactive between levels or after a death sequence.
Use Vector3 and Quaternion for all position and rotation math. See the Vector3 reference and Quaternion reference pages for a full list of helpers like Vector3.distance, Vector3.normalized, and Quaternion.lookRotation.