Lua scripting

Dialogue and choices

Lines, moods, pronouns, choices, text input and staging people in the room.

Lines

luaGame:Narrate("The rain has not stopped all day.")
Game:Say(Actor, "Happy", "You came! I was starting to think you wouldn't.")
Game:Say(Player, "Flirty", "I wouldn't miss it.")
  • Game:Narrate(text): narration, no speaker. Underlives's narration is written in the first person, from the player's point of view ("I", "my").
  • Game:Say(person, mood, text): a line spoken by person. The dialogue box shows their name and portrait, and their face takes the mood's expression while the line is on screen. mood may be nil.

Each line waits for the player to advance it (or for the auto-advance timer, depending on the player's setting). Lines are kept in the scene's history panel.

Moods

Mood
Neutral Happy Flirty
Angry Furious Sad
Crying Surprised Shocked
Scared Pain Embarrassed
Wink Evil Kiss (same as Flirty)

An unknown mood shows a neutral face.

Moods are data: CharGen/moods.json maps each one to Genesis 8 expression morphs and their weights. Your mod's own CharGen/moods.json adds moods or replaces one by name:

json{
  "moods": { "Smug": { "eCTRLSmileFullFace_HD": 0.4, "eCTRLGlare_HD": 0.3 } },
  "aliases": { "Proud": "Smug" }
}
note

The mood list is fixed for now; mods cannot add moods or change which expression a mood uses yet.

Names and pronouns

Build lines from the people in them, so they work for anyone. Write <Name.token> in the text:

lualocal Partner = Game:getPerson("partner")
Game:Narrate("<Partner.name> looks up from <Partner.his> phone.")
-- "Mia looks up from her phone."  /  "Tom looks up from his phone."
Game:Say(Partner, nil, "Tell <Player.him> I said hi.")

Name is any variable of your scene that holds a person: a local (Partner above), Player, Actor, CurrentCompanion. A tag whose name is not a person (<b>, <Nobody.name>) stays as written. Game:fillText(text) does the same for text that is not a line (a notification, a panel). The same tokens work as a function call:

luaGame:Narrate(Actor:name() .. " looks up from " .. Actor:GetToken("p") .. " phone.")

Game:Say(Actor, nil, "Call me your " .. Actor:GetGenderedWord("boyfriend", "girlfriend") .. " one more time.")

Actor:GetToken(token):

Token He She
S / s (or He / he) He / he She / she
O / o (or Him / him) Him / him Her / her
P / p (or His / his) His / his Her / her
PP / pp His / his Hers / hers
R / r (or Himself / himself) Himself / himself Herself / herself
name, name_last First name, last name
relationship What the player is to this person ("son", "niece"…)
callplayer What this person is to the player ("mom", "cousin"…)
Major The subject they study or teach
word1_or_word2 word1 word2

The last row means any a_or_b token works: Actor:GetToken("mister_or_miss"). Tokens are case-sensitive (S capitalises, s does not). Pronouns follow the person's presented gender.

Actor:GetGenderedWord(male, female) is the same choice as a function call.

A face without a line

A line's mood is put on the speaker's face when the line shows. Between lines, Actor:mood(name) does the same on anyone, and holds it until the next mood or line: a reaction shot in a cutscene, someone listening, a face that changes before the words do.

luap:mood("Shocked")        -- the hand lands; nothing is said
Game:Hold(0.6)
p:mood("Furious", 0.4)   -- reached in a blend of 0.4 s instead of at once
Game:Say(p, "Furious", "Hands. Off.")

Beyond the recipes: p:face({ eCTRLAngry_HD = 0.6, eCTRLSmileFullFace_HD = 0.3 }, 0.3) composes a face from any expression morphs; p:alive(0.5) adds the face's own life on top (breath, eyelids, brows; 0 is still); p:blink() blinks once, p:blink(false) stops blinking (a stare, a corpse).

Eyes are directed too: p:lookAt(other) turns the neck, the head and the eyes to another person and keeps them there; p:lookAt("camera") looks into the lens wherever each shot puts it; p:lookAt({0, 0, 0.9}) at a point of the stage frame (metres from the player's feet); p:lookAt(target, { seconds = 0.5, weight = 1, max = 60, eyes = true }) sets the ease, how far the head follows, its cap in degrees, or moves the eyes alone. p:lookAt() lets go. p:turnTo(target) turns the body; p:freeze(true) holds it on its frame; p:animSpeed(0.5) slows its clips; p:walkTo({ x, y }, "idle_f") walks to a stage point, and p:walkTo("piece:window") (or any piece:<match>) to the spot in front of that piece, half a metre off it.

The names are the moods of CharGen/moods.json (Neutral, Happy, Flirty, Angry, Sad, Surprised, Shocked, Scared, Furious, Crying, Pain, Evil, Embarrassed, Bored, Irritated, Hurt, Unconscious…); an unknown name is Neutral. A mod adds or changes recipes in its own CharGen/moods.json.

Choices

Register the options, then ask:

luaGame:Narrate("What do I say?")
Game:Choice(0, nil, "Tell the truth")
Game:Choice(1, Player:getStat("interpersonal") > 40, "Talk my way out of it")
Game:Choice(2, nil, "Say nothing")

local pick = Game:GetChoice()
if pick == 0 then
	-- …
elseif pick == 1 then
	-- …
end
  • Game:Choice(index, condition, text, options?): index is what GetChoice returns if this option is picked. condition is nil (always available) or a true/false value; an option whose condition is false is not shown.
  • Game:GetChoice() shows the options, waits, and returns the index of the pick.
  • Calling GetChoice() again without registering new options returns the same answer, so you can test it in several places.
  • If every option at a choice point is hidden, the scene closes and the log names your file. Always leave one unconditional option.

Greyed options and odds

A fourth argument tells the player more:

luaGame:Choice(0, nil, "Talk your way in", { odds = 0.6, tooltip = "Charm against the bouncer" })
Game:Choice(1, Player:hasItem("vip_card"), "Flash a VIP card", { reason = "You have no VIP card" })
Game:Choice(2, nil, "Walk away")
  • reason: when the condition is false, the option is shown greyed with the reason beside it (and on hover) instead of hidden. It cannot be picked and takes no number key.
  • odds: a chance of success, 0.6 or 60, printed after the text: "Talk your way in (60%)". Showing it is your promise: roll the same number (Game:Random(1, 100) <= 60).
  • tooltip: hover text for any option.
luarepeat
	Game:Choice(0, nil, "Ask about the job")
	Game:Choice(1, not askedFamily, "Ask about the family")
	Game:Choice(9, nil, "Leave")
	local pick = Game:GetChoice()
	if pick == 1 then
		askedFamily = true
		Game:Say(Actor, "Sad", "We don't really talk anymore.")
	end
until pick == 9

Built-in pickers

These register their own options and wait, like GetChoice:

Call Asks the player to… Read the answer with
Game:selectNPC(tag) pick one of their contacts (or "Nobody") Game:getSelectedNPC(), an invalid person for "Nobody"
Game:chooseContact() same, legacy name Game:getSpecific("chooseContact")
Game:chooseMajor() then Game:setMajor() pick a university major the player's major is set

Text input

lualocal name = Actor:enterFirstName()   -- the player types a first name for Actor
local nick = Actor:enterCustom("nickname")
local shop = Game:enterBusinessName()

Each waits for the player to type and returns the text.

Staging people

People in a scene are real people in the room, not cut-outs.

Call Effect
Actor:show(), Actor:hide() Put the person in the scene or take them out
Actor:moveToPerson(other) Walk up to other and face them
Actor:moveToPersonStand(other, cm) Walk to cm centimetres from other (0 = their spot)
Game:faceEachOther(a, b) Turn two people towards each other
Actor:animate("smoke") Play an activity (see Animations); no argument returns to idle
Game:AnimatePair(a, b, "Kissing") A paired animation (a kiss, a hug) without starting a sex scene
Actor:closeEyes(true) Hold the eyes shut; false opens them
Actor:dress(), Actor:strip(), Actor:stripOne("Top") Change what they wear. See People.

Phone calls and thoughts

Game:secondScreen(person) puts a person "on the phone": their lines still show their portrait, but show() will not place them in the room. Game:secondScreen() with no argument ends it for everyone. Game:secondScreenIfHidden(person) does it only if they are not already visible.

lualocal mom = Game:getRelative("Parent")
Game:secondScreen(mom)
Game:Say(mom, "Happy", "Sweetie! You never call.")
Game:secondScreen()

Changing place

Game:setBackground("park") (or Game:moveTo("park")) moves the scene, and the player, to a place of that venue type. Game:GetCurrentLocation() returns the current venue tag. Game:isAtHome() is true at the player's home.

On-screen messages

Game:ShowMessage("Saved!", 3) shows a short message that does not wait for the player; the second argument is seconds (default 5). Useful for feedback from libraries and UI scripts too.