Lua level script - dkfans/keeperfx GitHub Wiki
Lua Documentation (WIP)
Contents
- Introduction
- Setup IDE
- Triggers
- Commands
- Examples by Category
- Field Reference: Player Variables
- cfg-functions
- IDE Type Hinting
- Savegames
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:
- Install the Lua extension in VS Code.
- Set the workspace folder and path. (TODO: more detail)
- Change the Lua runtime to LuaJIT:
- Open VS Code Settings (
Ctrl + ,
). - Search for "Lua › Runtime › Version."
- Set it to LuaJIT.
- Open VS Code Settings (
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