Lua level script - dkfans/keeperfx GitHub Wiki

Lua Documentation (WIP)

Contents


Introduction

Lua support in KeeperFX is currently in early stages. It is available in the alphas, not yet in the stable. This documentation is also a work in progress and subject to change.

nothing is final at this point, feel free to try stuff, and give feedback, but assume changes might still break maps

KeeperFX uses LuaJIT, which is based on Lua 5.1. For general Lua language reference, see the official Lua 5.1 manual. This document focuses specifically on KeeperFX's Lua implementation.

Example maps can be found here.

Setup IDE

The recommended IDE for writing Lua scripts in KeeperFX is VS Code with the sumneko Lua extension.

Note:

  • This is the second one when searching for "lua" in the VS Code extension marketplace.

Steps to Set Up:

  1. Install the Lua extension in VS Code.
  2. Set the workspace folder and path. (TODO: more detail)
  3. Change the Lua runtime to LuaJIT:
    • Open VS Code Settings (Ctrl + ,).
    • Search for "Lua › Runtime › Version."
    • Set it to LuaJIT.

Triggers

Lua scripts in KeeperFX are event-driven, meaning they respond to in-game events (triggers). All available triggers are defined in fxdata/lua/triggers/Events.lua.

Example: Detecting a Creature Being Slapped

function ThingToDoWhenSlapIsCast(eventData, triggerData)
  ThingToDoAtSlapPosition(eventData.stl_x, eventData.stl_y)
end

function OnGameStart()
  RegisterPowerCastEvent(ThingToDoWhenSlapIsCast, "POWER_SLAP")
end
  • OnGameStart is a fixed function that runs at level start. It is commonly used to set up triggers.
  • RegisterPowerCastEvent binds an event listener for when a spell is cast.

Registering an Event Listener

---@param action function|string Function to call when the event occurs
---@param powerKind? power_kind Optional: Specific spell type that triggers the event
---@return Trigger
function RegisterPowerCastEvent(action, powerKind)
  local trigData = {PowerKind = powerKind}
  local trigger = CreateTrigger("PowerCast", action, trigData)
  if powerKind then
    TriggerAddCondition(trigger, function(eventData, triggerData)
      return eventData.PowerKind == triggerData.PowerKind
    end)
  end
  return trigger
end
--- Called when a spell is cast on a unit
--- @param pwkind power_kind
--- @param caster Player
--- @param target_thing Creature
--- @param stl_x integer
--- @param stl_y integer
--- @param splevel integer
function OnPowerCast(pwkind, caster, target_thing, stl_x, stl_y, splevel)
  local eventData = {
    Thing = target_thing,
    PowerKind = pwkind,
    Player = caster,
    stl_x = stl_x,
    stl_y = stl_y,
    splevel = splevel
  }
  ProcessEvent("PowerCast", eventData)
end

💡 Not all spells target a unit or a position. Some fields may be nil.

Commands

All native bindings to the game can be found in fxdata/lua/bindings folder This folder serves two purposes:

  • It informs the IDE about the native functions provided by DK.
  • It acts as a reference for available Lua functions.

KeeperFX Lua supports most commands from the original DKScript language. While many are duplicated in native.lua, the DKScript documentation is also useful. Be aware:

  • Some commands behave differently or have new syntax.
  • Some are unique to either DKScript or Lua.
  • There are alternative approaches for some DKScript-only features.

Flow Control

  • NEXT_COMMAND_REUSABLE is not needed — all Lua commands are reusable.
  • Use standard Lua flow control (if, while, for, etc.).
  • Use OnGameStart() for initialization logic.

Conditional Checks

  • Player variables:
    PLAYER0.MONEY

  • Action Point trigger:

    RegisterOnActionPointEvent(function()
        AddPartyToLevel(PLAYER_GOOD, "PARTY2", 1, 1)
    end, 2, PLAYER0)
    
  • Checks:

    PLAYER0:available("WORKSHOP")
    PLAYER0:controls("IMP")
    
  • Slab properties:

    local slab = GetSlab(x, y)
    slab.owner
    slab.type
    slab.style
    
  • Allied check:

    PLAYER0:allied_with(PLAYER2)
    

Flags and Variables

  • Regular flags:

    PLAYER0.FLAG0 = 1
    
  • Campaign flags:

    PLAYER0.CAMPAIGN_FLAG2 = 1
    
  • Math operations:

    PLAYER0.CAMPAIGN_FLAG2 = PLAYER0.MONEY + 75 * PLAYER0.IMP
    

Assigning to an invalid or read-only variable will throw an error.

Note on naming: Avoid using generic flags like PLAYER0.FLAG0 unless necessary. Prefer descriptive names:

Game.roundCounter = roundCounter

Working with Creatures

  • Manipulating individual creatures:
    --multiple ways to get them, the GetCreatureByCriterion functions similar to how they were fetched in dkscript
    local imp = GetCreatureByCriterion (PLAYER0, "IMP", "MOST_EXPERIENCED")
    -- then interact with them
    imp:level_up(2)
    imp:kill()
    

Examples by Category

🎮 Game Setup

SetGenerateSpeed(300)			  
StartMoney(PLAYER0, 7500)															 
CreatureAvailable(PLAYER1, "TROLL", true, 2)

🤖 Spawning Creatures & Parties

--when spawning a creature you could store it in a variable, this var can then later be used to do things with said unit
local dragon = AddCreatureToLevel(PLAYER0, "DRAGON", 1, 3, 200)

														 
CreateParty("RAIDERS")
AddToParty("RAIDERS", "TUNNELLER", 5, 0, "ATTACK_DUNGEON_HEART", 0)
AddTunnellerPartyToLevel(PLAYER0, "RAIDERS", 2, "DUNGEON", 1, 5, 0)

🧠 Scripting Logic

--when one of PLAYER0's units enters actionpoint 3, show message 42
RegisterOnActionPointEvent(function() DisplayInformation(42, PLAYER0) end, 3, PLAYER0)

Field Reference: Player Variables

Field Name Description
PLAYER0.MONEY Amount of gold the player currently has
PLAYER0.IMP Number of imps controlled
PLAYER0.FLAG0 General-purpose flag (avoid if not needed)
PLAYER0.CAMPAIGN_FLAG2 A campaign-wide persistent flag

these are the same as Dkscript, so find full list in dkscript document

cfg-functions

example custom keeperpower

in the cfg some functions

[power7]
Name = POWER_TELEPORT_ORC
...
UseFunction = magic_use_power_teleport_orc_to_heart

in templates.lua you'll find the possible fields that take functions, these will be structured as cfgname_block_field so in this case cfgname magic block [powerX] field UseFunction so Magic_power_UseFunction_template

-- -first check if the power is castable, then call the Pay_for_power fuction to pay,
--- if the player doesn't havve the gold said function will return false, implement it in a way that makes it return early in that case
---@param player Player player who cast the power
---@param power_kind power_kind what power was cast
---@param power_level integer how much the power was charged up
---@param stl_x integer|nil the x coordinate of the tile where the power was cast, will be nil for powers that are cast on the entire level
---@param stl_y integer|nil the y coordinate of the tile where the power was cast, will be nil for powers that are cast on the entire level
---@param thing Thing|nil the thing that was hit by the power, will be nil for powers that are cast on the entire level or on a tile
---@param is_free boolean if the power should be cast for free (no gold cost)
---@return -1|0|1 if the power was cast successfully 1, if it failed -1, if it was cast but the power didn't do anything 0
function Magic_power_UseFunction_template(player,power_kind,power_level,stl_x,stl_y,thing,is_free) return 0 end

to implement this function make a function that uses the same parameters

---this will teleport orcs to the heart of the caster
function magic_use_power_teleport_orc_to_heart(player,power_kind,power_level,stl_x,stl_y,thing,is_free)

    if (thing.model ~= "ORC") then
        -- 0 means power simply doesn't have a valid target, so so simply does nothing
        return 0
    end

    --pay for the power, the function will return false if the player doesn't have enough gold
    if (not PayForPower(player, power_kind, power_level, is_free)) then

        -- -1 means it failed, and will make a reject sound
        return -1
    end

    --player here is used as a location, so it will teleport the player to the heart
    thing:teleport(player, "EFFECT_EXPLOSION_4")

    -- 1 means it was cast successfully
    return 1
end

IDE Type Hinting

Thanks to annotations in native.lua, the Lua language server supports:

  • Autocompletion
  • Hover documentation
  • Type checking

This improves the experience in VS Code significantly.

Savegames

All persistent data should be stored in the global Game table:

Game.hasTriggeredEvent = true
Game.playerScore = 1234