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 (
initandrun) - 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.
Where scripts live in a pack
Section titled “Where scripts live in a pack”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.xmltriggers.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:
Type | Fires when |
|---|---|
Meet | Player encounters the girl in the game world |
Talk | Player clicks “Talk” in girl details |
Confirmed Where values (scoping the trigger to a location):
Where | Context |
|---|---|
Town | Encountered walking the streets |
Brothel | Inside a brothel |
Dungeon | In the player’s dungeon |
Catacombs | Exploring 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.
Script lifecycle
Section titled “Script lifecycle”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 trueend
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 scriptendReturning true from run() means “keep running, call me again”. Returning false (or omitting the return) ends the script.
Context the engine provides
Section titled “Context the engine provides”Before init() runs, the engine sets these globals:
| Global | What it is |
|---|---|
wm.girl | The girl involved (table with stats, skills, traits, name) |
wm.player | The player (table with stats, skills, gold) |
wm.area | Town encounter area: "market", "slums", "docks", "redlight" |
wm.is_dungeon | true 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.
What wm.* can do
Section titled “What wm.* can do”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.
1.12 trait-cache helpers
Section titled “1.12 trait-cache helpers”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 forkey. 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 afterweeksturns. Returnsfalseif the girl already has it or the name is unknown. Cache rebuilds immediately, sostat_effect/get_modifierreflect the change in the samerun()tick.
Spawning a specific named girl (1.15.6+)
Section titled “Spawning a specific named girl (1.15.6+)”wm.create_named_girl{name=..., package=..., global=...}: deep-copies a named unique-girl template into a freshGirland returns her as a Lua table (same shape aswm.create_random_girl).nameis the template identity (matchesName=on the<Girl>element in a.girlsx, NOT her runtimem_Realname).packageis optional; pass the pack folder name (e.g."Alpha") to disambiguate when two packs ship a template with the same name. Omitpackagefor first-match across all packs.global=trueadds her to the global girl pool so the rest of the engine (market filters, etc.) treats her as live. Returnsnilif no template matches.
-- Example: a genie-lamp script that spawns a specific bottled spiritlocal 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)endInventory grants (1.15.6+)
Section titled “Inventory grants (1.15.6+)”wm.give_player_random_special_item(): picks a random item with rarityShop05or 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, ornilif 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 itemlocal 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)endPlayer disease helpers
Section titled “Player disease helpers”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): returnstrueif the player currently carries the named disease,falseotherwise.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 itemif wm.player_has_disease("AIDS") then wm.message("She senses something wrong. She hands you a glowing vial.", 0) wm.player_cure_disease("AIDS")endIf 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.
Testing your script
Section titled “Testing your script”- Validate the pack first:
tools/pack-validator/pack-validator.exe. It catches XML errors intriggers.xmland flags missing script files. - Load the pack in-game. Scripts compile lazily on first trigger, so errors show up when the trigger fires, not at game start.
- Watch
logs/game.log; Lua syntax errors and runtime errors are printed there with line numbers.wm.log("...")calls also go to this file.
Worked example
Section titled “Worked example”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.
Debugging tips
Section titled “Debugging tips”wm.log("checkpoint A")prints tologs/game.logwithout interrupting gameplay.wm.message("...", 0)prints to the message panel in-game. Good for visible trace points.print(...)also works and routes throughwm.log.- Long-running scripts that never return
falsewill block the UI. If your encounter freezes the game, check that every code path eventually returnsfalse.