Skip to content

Lua scripting

Packs can ship per-girl Lua scripts to drive custom encounters, dialogue, and branching events. This page covers:

  • How scripts are wired into a pack
  • The script lifecycle (init and run)
  • Where to find the API reference

For editor setup (VS Code, JetBrains, Neovim, etc.) see editor-setup.md. The short version: install the sumneko Lua extension, open the game folder as a workspace, and LuaLS picks up Resources/Scripts/definitions/wm.lua automatically.

Scripts are per-girl. Inside your pack:

MyPack/
package.xml
Girls.girlsx
Characters/
Jane/
Profile/
portrait.jpg
triggers.xml <-- maps events to script files
MeetGirl.lua <-- script referenced by triggers.xml

triggers.xml tells the engine which .lua file to run for which event. A minimal version:

<Triggers>
<Trigger Type="Meet" Where="Town" File="MeetGirl.lua" />
</Triggers>

Confirmed trigger types:

TypeFires when
MeetPlayer encounters the girl in the game world
TalkPlayer clicks “Talk” in girl details

Confirmed Where values (scoping the trigger to a location):

WhereContext
TownEncountered walking the streets
BrothelInside a brothel
DungeonIn the player’s dungeon
CatacombsExploring the catacombs

In shipped content, only Meet + Where="Town" is used. Other combinations are recognised by the engine but not exercised in the bundled samples.

The engine calls init() once when the script starts, then run() repeatedly until run() returns false.

function init()
math.randomseed(wm.time())
stage = "start"
return true
end
function run()
if stage == "start" then
wm.message("You see " .. wm.girl.name .. " at the market.", 0)
-- ... branching logic
stage = "next"
return true
end
return false -- end the script
end

Returning true from run() means “keep running, call me again”. Returning false (or omitting the return) ends the script.

Before init() runs, the engine sets these globals:

GlobalWhat it is
wm.girlThe girl involved (table with stats, skills, traits, name)
wm.playerThe player (table with stats, skills, gold)
wm.areaTown encounter area: "market", "slums", "docks", "redlight"
wm.is_dungeontrue if the encounter is in the dungeon

Not every global is set in every context. wm.area is only set for town encounters; wm.is_dungeon is only set for dungeon events.

The API covers messages, menus, stat/skill modification, trait add/remove, image display, inventory operations, gold transfers, moving girls between buildings, and pregnancy/child creation. Full list with types and descriptions is in Resources/Scripts/definitions/wm.lua.

Four functions expose the live trait modifier cache (the same data behind <effect type="…"/> in .traitsx):

  • wm.get_base_stat(name): raw base value, no trait contribution.
  • wm.stat_effect(name): only the trait contribution. effective = base + stat_effect.
  • wm.get_modifier(key): sum of <effect type="modifier"> contributions for key. Use this to read custom flags traits set for your scripts (e.g. if wm.get_modifier("stage_fright") > 0 then ...).
  • wm.add_temp_trait(name, weeks): grants a temporary trait that expires after weeks turns. Returns false if the girl already has it or the name is unknown. Cache rebuilds immediately, so stat_effect / get_modifier reflect the change in the same run() tick.
  • wm.create_named_girl{name=..., package=..., global=...}: deep-copies a named unique-girl template into a fresh Girl and returns her as a Lua table (same shape as wm.create_random_girl). name is the template identity (matches Name= on the <Girl> element in a .girlsx, NOT her runtime m_Realname). package is optional; pass the pack folder name (e.g. "Alpha") to disambiguate when two packs ship a template with the same name. Omit package for first-match across all packs. global=true adds her to the global girl pool so the rest of the engine (market filters, etc.) treats her as live. Returns nil if no template matches.
-- Example: a genie-lamp script that spawns a specific bottled spirit
local g = wm.create_named_girl{ name = "Jeanie", package = "ArabianNights", global = true }
if g then
wm.add_girl_to_brothel(g)
wm.message("With a puff of smoke, Jeanie appears at your door.", 0)
else
wm.message("The lamp is silent. The bottled spirit could not be found.", 1)
end
  • wm.give_player_random_special_item(): picks a random item with rarity Shop05 or rarer (i.e. Shop05, Catacomb15, ScriptOnly, ScriptOrReward, Catacomb05, Catacomb01) from the loaded item pool and adds it to the brothel-wide inventory. Returns the granted item’s name on success, or nil if no candidate items exist or the brothel inventory is full — inspect the return value if you want a custom failure narrative.
-- Example: a genie-lamp consumable that grants one rare item
local name = wm.give_player_random_special_item()
if name then
wm.message("The smoke clears -- you find a " .. name .. " in your hand.", 0)
else
wm.message("The lamp sputters. Nothing happens.", 1)
end

Three functions let scripts read and change the player’s disease state. All are pure state operations: no message is emitted automatically, so the script is responsible for any text shown to the player.

Valid disease names: "AIDS", "Chlamydia", "Syphilis", "Herpes".

  • wm.player_has_disease(name): returns true if the player currently carries the named disease, false otherwise.
  • wm.player_add_disease(name): infects the player with the named disease. Returns nothing.
  • wm.player_cure_disease(name): clears the named disease from the player. Returns nothing.
-- Example: react to the player having AIDS, then cure via a magical item
if wm.player_has_disease("AIDS") then
wm.message("She senses something wrong. She hands you a glowing vial.", 0)
wm.player_cure_disease("AIDS")
end

If the API doesn’t expose something you need, open an issue. New exposures are usually cheap to add if the engine already supports the underlying behaviour.

  1. Validate the pack first: tools/pack-validator/pack-validator.exe. It catches XML errors in triggers.xml and flags missing script files.
  2. Load the pack in-game. Scripts compile lazily on first trigger, so errors show up when the trigger fires, not at game start.
  3. Watch logs/game.log; Lua syntax errors and runtime errors are printed there with line numbers. wm.log("...") calls also go to this file.

See snippets/MeetGirl.lua and snippets/triggers.xml in this kit for a pack-flavoured starter. For a fuller example, copy Resources/Scripts/templates/MeetGirl_template.lua and adapt it.

Real in-game scripts live under Resources/Scripts/ and in the Character folders of the shipped legacy content. MeetTownDefault.lua is the default fallback when a girl has no MeetGirl.lua of her own, and is a good reference for how the engine drives a multi-stage encounter.

  • wm.log("checkpoint A") prints to logs/game.log without interrupting gameplay.
  • wm.message("...", 0) prints to the message panel in-game. Good for visible trace points.
  • print(...) also works and routes through wm.log.
  • Long-running scripts that never return false will block the UI. If your encounter freezes the game, check that every code path eventually returns false.