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. 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>Trigger types the loader accepts:
Type |
Fires when |
|---|---|
Meet |
Player encounters the character in the game world |
Talk |
Player clicks “Talk” on the character’s details screen |
Stat |
A named stat reaches a threshold (see below) |
Skill |
A named skill reaches a threshold |
Status |
The character has a particular status (slave, pregnant and so on) |
Random |
May fire on any week |
WeeksPast |
A number of weeks have passed while the character is employed |
Shopping |
The player goes shopping |
Money |
The character’s money reaches a value |
PlayerMoney |
The player’s gold reaches a value |
GlobalFlag |
A global flag is set |
ScriptRun |
A named script has been run |
Kidnapped |
The character is kidnapped |
Every name above is spelled exactly as the loader expects it, and an
unrecognised Type is logged and the trigger skipped. Only Meet is exercised
in the bundled samples, so treat the rest as working but lightly travelled: test
your trigger in game and check gamelog.txt if it never fires.
Common attributes
Section titled “Common attributes”File is required on every trigger. Chance is a percentage and defaults to
100 when omitted. OnceOnly="true" makes the trigger fire at most once.
Threshold triggers: Stat and Skill
Section titled “Threshold triggers: Stat and Skill”A Stat trigger names the stat and the value it has to reach:
<Trigger Type="Stat" Stat="Age" Threshold="30" File="TurnsThirty.lua" OnceOnly="true" />Stat takes a stat name, not a number, and an unknown name is logged and the
trigger dropped. The valid names are the same ones
effects-reference lists,
including Age. Threshold is required.
Skill works the same way with a skill name.
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.
Trait-cache helpers
Section titled “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.
Granting and removing traits
Section titled “Granting and removing traits”wm.add_trait(name): grants a permanent trait. Returnstruewhen it was added, andfalsewhen the character already carries it or the script has no character in context.wm.remove_trait(name): removes a trait. Returnstruewhen one was actually removed.wm.girl_has_trait(name): returnstruewhen the character carries the trait.
These act on the character the script is running against, so a trigger you ship
in your pack changes that one character. There is no call that grants a trait to
everyone at once. If you want a rule that follows a trait around to every
character carrying it, that is what the <OnGenerate> and <Periodic> rules in
traits-reference are for,
though those inflict state flags rather than traits.
Recipe: grant a trait when a character reaches an age
Section titled “Recipe: grant a trait when a character reaches an age”triggers.xml:
<Triggers> <Trigger Type="Stat" Stat="Age" Threshold="30" File="TurnsThirty.lua" OnceOnly="true" /></Triggers>TurnsThirty.lua:
function init()end
function run() if not wm.girl_has_trait("Mature") then wm.add_trait("Mature") wm.message("She notices the first grey hair and says nothing about it.", 0) end return falseendOnceOnly="true" matters here. Without it the trigger stays armed and re-fires
every time the condition is checked, and while wm.add_trait returns false
rather than duplicating the trait, the message would repeat.
The same shape works for any stat, so Fame, Obedience or Beauty crossing a
line can hand out a trait just as well as Age can.
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
gamelog.txtnext to the game executable; 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 (a town encounter), snippets/TalkGirl.lua (a custom Talk conversation for one character; the game’s default conversation stays in place for everyone else), and snippets/triggers.xml in this kit for pack-flavoured starters. For a fuller example, copy resources/scripts/templates/MeetGirl_template.lua and adapt it, or read resources/scripts/DefaultInteract.lua, the table-driven conversation the Talk button runs by default.
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 togamelog.txtwithout 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.