People
Finding, creating and changing people from a script, their stats, their memory, and their relationships.
Every person a script touches, the player included, is a person handle: Player, Actor, and whatever the lookup functions return. They all have the same methods, listed in the Actor reference.
Finding people
| Call | Returns |
|---|---|
Game:GetPlayer() |
The player (also passed to RUN as Player) |
Game:getCompanion() |
Who the player is with right now |
Game:getTarget() |
The person the player clicked to start an interaction |
Game:getSpecific(role) |
Someone the player has that relationship with (see below) |
Game:getRelative(type) |
A family member: "Parent", "Child", "Sibling", "GrandParent", "GrandChild", "Cousin", "StepParent", "StepChild", "StepSibling", "ParentSibling" (uncle/aunt), "SiblingChild" (nephew/niece), or nil for any. The same names work in isRelativeType and setRelativeType |
Game:getRelativeExcept(type, …) |
A relative who is none of the listed types |
Game:getPerson(tag) |
The person marked with Actor:setActorVar(tag, true) (a storyline's NPC) |
Game:getPerson() |
A random known person |
Game:getPersonHere(permanent) |
Someone at the player's location: nil anyone, true known people only, false strangers only |
Game:getRelatedPeople(type, …) |
A list of people who share that relationship with each other (a couple, siblings) |
Game:getSelectedNPC() |
The contact picked with Game:selectNPC() |
getSpecific roles: Dating, DatingFriend, Affair, Spouse, Colleague, Neighbour, Boss, Landlord, PersonalTrainer, Ex, Companion, plus Lecturer, Student, Coursemate, Fraternity, Employee, Prostitute, Criminal, CrimeBoss, Impregnated, CurrentBabyDaddy, PassedOn, a job title ("Doctor"), or a numeric id from Actor:getID(). If the person is not in the world at the moment, they are brought in with their saved appearance.
Always check isValid()
A lookup that finds nobody does not fail: it returns an invalid person, on which every method is harmless and isValid() is false.
lualocal boss = Game:getSpecific("Boss")
if not boss:isValid() then
return false -- no boss: this scene is not for this player
end
People without a body: People
Every lookup above brings the person into the world when they are not there: a scene needs a body to stage. A library, a Mods page or an Events handler usually does not: listing a diary, or calling a plan missed at 3 a.m. while the player is across town, must not spawn anyone, nor bring back someone who was blocked or deleted. Use a reference instead:
lualocal p = People:byTag("ul_phone_partner_1") -- or People:get(saved_id), People:contacts()
if p and p:isValid() and p:isContact() then
p:modifyStat("rapportwithplayer", -3) -- kept for when they next appear
p:record("ul_phone_missed", People:player(), "major")
end
People:get(id)andPeople:byTag(tag)returnnil(not an invalid person) when nobody known matches: a deleted person is gone.- A reference reads and writes the same things an Actor does (stats,
getVar/setVar, tags, history) and answers the Actor spellings (isValid,GetStat,ModifyStat,isSameAs,isContactExchanged...), so library functions written for an Actor accept one.isValid()means known, not spawned;isLive()says whether they have a body. p:actor()is their Actor only if they already have a body, elsenil. When a scene really needs them on stage, useGame:getPerson(tag)there.- Keep ids, not references: save
p:id()in a variable andPeople:getit back. The id is the same oneActor:id()gives.
Creating people
lualocal stranger = Game:generatePersonTemporary(true, "athletic") -- a man, blended with a preset
local rival = Game:generatePerson(false, "age_groups/twenties, athletic_F")
rival:setName("Chloé", "Martin")
Game:generatePerson(isMale, presets)creates a permanent person: they are added to the people the player knows, saved, and can be found again later.Game:generatePersonTemporary(isMale, presets)creates someone for this scene only.presetsis a comma-separated list of character presets blended onto a random appearance. It may benil.- New people get a random name from the city's name lists;
setName(first, last)replaces it. Game:setRaceGenerated("asian")makes everyone generated afterwards in this scene that ethnicity (arab,asian,black,indian,latin,white).Actor:generatePersonMatchRace()/generatePersonTemporaryMatchRace()create someone ofActor's ethnicity.
Always pass the gender explicitly. The legacy forms generatePersonTemporary() and generatePersonTemporary("preset") still run, but today the first one always creates a woman and the second always creates a man and ignores the preset. This will be fixed; the two-argument form is correct either way.
Temporary and permanent
| Call | Effect |
|---|---|
Actor:isTemporary() |
Not (yet) someone the player knows |
Actor:makePermanent() |
Keep them: they join the known people and are saved |
Actor:delete() |
Remove them from the scene (a known person still exists) |
Actor:saveAndDelete() |
makePermanent() then delete() |
Actor:deletePerson() |
Erase them from the game for good: relationships, memory, everything |
Death
Actor:die(cause, killer) and People:get(id):die(cause, killer) kill a known person for good (never the player): a partner is widowed, the home is vacated, the family stays family, and the history keeps an npc_died line. Actor:die also drops the body where it stands. cause is free text ("stabbed", "overdose"); killer is optional (an Actor, a reference or an id). Unlike deletePerson, the dead stay known:
| Call | Gives |
|---|---|
People:dead() |
Every known person who has died, most recent first, as references |
ref:isDead() |
Dead (also the stat deceased, 1) |
ref:diedAt() |
Game hours of the death, nil while alive |
ref:getVar("death_cause") |
The cause given to die, or the illness |
ref:killer() |
The reference of whoever die named, nil if nobody |
Every death, natural ones included, fires the personDied event.
Graves
The dead lie at one of the city's cemeteries. Real OpenStreetMap cemeteries become cemetery venues, named after them; a city with none still gets one. Each dead person gets a plot there the first time anyone asks, and keeps it.
| Call | Gives |
|---|---|
ref:grave() |
{ building, name, plot }, or nil while alive or with no grave |
ref:setGrave(buildingId) / ref:setGrave(false) |
Moves the grave to that cemetery / no grave at all (ashes scattered) |
ref:visitGrave() |
The player stands at it: counted on the dead person (grave_visits, grave_last_visit), fires graveVisited |
People:buried([buildingId] [, knownOnly]) |
The dead buried at that cemetery (default: where the player is), most recent first; knownOnly keeps the player's own people |
People:visitedGrave() |
Whose grave was visited last |
At a cemetery, clicking a headstone shows whose grave it is (name, years, plot) and offers "Stand at the grave", which counts the visit and rolls the scenes whose WHAT lists visit_grave. Those scenes read the person from People:visitedGrave(). Every dead person buried there gets a stone with their name and years, the player's own dead nearest the middle. To send the player to a grave, pin an objective to it with alias:pinTo(ref:grave().building).
Epitaphs. Under a named stone's name and years, the engine engraves one line from Graves/*.ulgraves in any mod: { "generic": ["..."], "by_cause": { "illness": ["..."] } }. The cause is the one the town recorded for the death; one line is picked per person (always the same for a given stone), | breaks a line, and a later mod's file adds to the pools. ul_Base ships a set.
Watching other people's sex
When two NPCs have sex in the room the player is in, the engine plays it for them: one scene of two or three positions, each a different clip ranked by the pair's likes, lengths from ul_Base's autonomy policy (watchedSexStagesMin/Max, watchedSexStageMinSeconds/MaxSeconds). Minors in the room are never on screen: they are hidden and drawn as the shared adult shadow. People:sexSeen() returns { a, b, forced } (or nil); People:interruptSex() stops it. The player's var npc_sex_here is 1 or 2 (forced) meanwhile, so an action can use it in CONDITIONS. ul_Voyeur is the player's side.
Lasting conditions
A limp, a bad back, a chair: something that stays with a person, saved with them and visible to every mod. Conditions are mod data, in Conditions/*.ulconditions (JSON):
json{
"conditions": [
{ "id": "limp", "name": "A limp", "pain": 35, "speed": 0.75, "walk": "walk_limp", "sex_avoid": ["Standing"] }
]
}
| Field | Does |
|---|---|
pain |
The pain stat never heals below this while the condition is held |
speed |
Walk speed factor; the slowest held condition wins |
walk |
An activity walked instead of walk, when a mod ships one (a limp, a wheelchair); otherwise the walk is only slowed |
sex_avoid |
Sex positions with any of these tags are not offered while the holder takes part |
confines |
Shut in (a cell, a cage, a locked room): an NPC holder does nothing, and the player's actions can test the var confined (STAT = "confined") |
starve_days, starve_cause |
Days without feed() before the holder dies of that cause (an NPC through the town's death, the player gets the playerStarved event) |
A later mod redefines an id in place. ul_Base/Conditions/base.ulconditions also holds walk_speed_by_age ([age, factor] points, for everyone: children a little quicker, the old slower), pain_heal_per_day and pain_return_per_day.
| Call (Actor or reference) | Does |
|---|---|
addCondition(id [, cause]) |
Gives it; false for an id no mod defines or one already held |
removeCondition(id) |
Takes it away |
hasCondition(id) |
Held now |
getConditions() |
{ { id, name, text, since, cause, pain, speed, confines, module } } |
confine(id [, place [, cause]]) |
Gives a condition that has confines, with a place a mod reads back |
confined() |
{ condition, place, since }, or nil |
feed() |
Restarts the starve_days of the confining conditions |
Each change fires the conditionChanged event.
Pain. The pain stat (0–100, ul_Base) is how much the body hurts today. Overnight it heals pain_heal_per_day down to the floor: the highest pain of the held conditions, less the person's pain_relief var (physio raises it, giving up can make it negative). Taken below the floor (pills), it comes back pain_return_per_day a day. A sex scene starts from it and leaves some behind.
Stats
lualocal mood = Player:getStat("mood")
Actor:modifyStat("rapportwithplayer", 10)
Actor:setStat("arousal", 0)
Stats are defined by .ulstat files (Stats); values are kept within the stat's minimum and maximum. modifyStat is scaled by the game difficulty for stats marked HIGHER_BETTER: gains shrink and losses grow on harder settings. Call Game:PreciseModify() once to turn that off for the rest of the scene.
Memory: variables
Each person can hold your mod's own data. It is saved with them.
luaActor:setVar("ynl_times_met", (Actor:getVar("ynl_times_met") or 0) + 1)
Actor:setVar("ynl_nickname", "Red")
Actor:setVar("ynl_nickname", nil) -- delete it
if Actor:getVar("ynl_times_met") == nil then
-- never set
end
- Values can be numbers, text or true/false.
getVarreturnsnilfor a key that was never set. - Keys are shared by every mod: always prefix them with your mod's id.
- On NPCs, variables ending in
_today,_thisweekor_thismonthare reset to 0 at the start of each day, on Sundays, and on the 1st of the month.
Actor:setActorVar(tag, true) is different: it marks a person so Game:getPerson(tag) can find them later from any scene. Use it to pick "the" NPC of a storyline once and come back to them.
For data that belongs to nobody, use story globals.
Relationships
Questions, all relative to the player:
| Call | True when… |
|---|---|
isDating(), isMarried(), isRelative(), isExRelative() |
…they are the player's partner, spouse, family, ex |
isColleague(), isNeighbour(), isBoss(), isEmployee(), isLandlord() |
…they have that role |
hasRelationship("Spouses", "Dating") |
…any of up to three roles |
isRelativeType("Sibling", "Cousin") |
…they are one of these kinds of relative |
isContactExchanged(), livesWithPlayer(), hadSex() |
…self-explanatory |
isRelationshipWith(other, "Siblings") |
…they have that relationship with other (NPC to NPC) |
isInterestedIn(other), isGay(), isStraight() |
orientation checks |
Changes:
| Call | Effect |
|---|---|
Actor:startDate() / Game:endDate() |
Start or end a date (they become / stop being the companion) |
Actor:setDating() |
They become the player's partner |
Game:getMarried() |
Marry the current partner |
Actor:addColleague(), addNeighbour(), addDatingFriend(), setBoss(), setLandlord(), setPT() |
Give them that role |
Game:exchangeContact(Actor), Game:blockContact(Actor) |
Add to or block from the player's contacts |
Actor:setRelativeType("Cousin") |
Make them family |
Game:addNpcRelationship("Siblings", a, b) |
A relationship between two NPCs |
Actor:makeInterested(Player) |
Shift their orientation towards someone |
Clothes
| Call | Effect |
|---|---|
Actor:dress() |
An everyday outfit (sportswear at the gym, swimwear at the pool or beach) |
Actor:dress("Nice-Jeans_1_F", "Cute-Croptop_2_F") |
These garments, over what they wear |
Actor:dress("sports"), ("swim"), ("work") |
An outfit from that list |
Actor:dressUniform("Police"), dressUniform() |
A uniform by name, or the one of the place they are in |
Actor:dressFormal() |
Evening wear or a suit |
Actor:dressBondage("Handcuffs"), dressBondage() |
Restraints over the clothes (no name: collar and handcuffs) |
Actor:dressExcept("Underwear", …) |
Dress but leave these categories as they are |
Actor:strip(), Actor:stripOne("Top") |
Undress completely or one category |
Actor:isNaked(), Actor:isEquipped("Top") |
Checks |
Actor:getExposure(), Actor:isExposed("genitals") |
What shows through the clothes: { breasts, genitals, butt, any } booleans. Breasts show when the top (or coat) and the bra are off, ripped or pushed aside; genitals when the bottom is off, ripped open or hiked up and the underwear off, ripped, aside or down past the thighs (a chastity belt covers); the butt likewise, but a panty pushed aside still covers it. A minor never shows anything. The engine raises exposureChanged and exposureSeen (Game-time events) from the same rules |
Actor:setNoRedress() |
Keep this outfit after the scene instead of restoring the previous one |
Categories: Top, Bottom, Dress, Outerwear, Underwear, Shoes, Socks, Headwear, Accessory.
An id is a .ulcloth file name; on a woman "Jeans_1" also finds "Jeans_1_F". An id that no mod ships is skipped, and when none of them is found they get an everyday outfit.
Outfit lists: Clothes/Outfits/
Any mod can add to these files in its own Clothes/Outfits/ folder:
outfits_F.txt,outfits_M.txt,outfits_F_sports.txt,outfits_M_swim.txt,outfits_F_work.txt... : outfits, one block of item ids per outfit, blocks separated by an empty line. Every mod's blocks are added together. A new list (outfits_F_goth.txt) is used bydress("goth").uniforms.txt: blocks of four lines: the name, the venue tags it is worn at, the women's outfits (separated by;, items by,) and the men's. A later mod's block of the same name replaces it. The engine also uses these tags on its own: a known NPC who appears at their own workplace during their shift wears the first uniform whose tags include that venue's tag (that uniform is not saved as their everyday clothes).
textpolice
police, prison
Uniform-Police_1_F, Uniform-Police-Cap_1_F, Jill-Valentine-Boots_1_F
Uniform-Police-Top_1, Uniform-Police-Pants_1, Uniform-Police-Cap_1, Urban-Ace-Shoes_1
restraints.txt:name = alternative, alternative, one alternative picked at random;+joins the pieces of one alternative (default = Bdg_Collar + Bdg_Handcuffs).
Appearance
| Call | Effect |
|---|---|
Actor:blendPreset("athletic") |
Blend a preset onto their current look |
Actor:setMorph(name, value), modifyMorph(name, delta), getMorph(name) |
Reshape the body or face over time (below) |
Actor:copyAppearanceFrom(other), restoreAppearance() |
Wear someone else's look, and get yours back |
Actor:setMorphValue(name, value), getMorphValue(name) |
One Genesis 8 dial, raw |
Actor:randomizeFace(), randomizeHairs(), randomizeRace() |
Re-roll parts of their look |
Actor:blendAppearanceFrom(a, b) |
A child's face from two parents |
Actor:isEthnic("asian"), isSameEthnic(other) |
Ethnicity (arab, asian, black, indian, latin, white) |
Actor:getRace(), setRace("Elf"), isRace("Orc"), isSameRace(other) |
Fantasy race: Human (default), Elf, Orc, Vampire |
Changing the body
A diet, a gym habit or a surgeon reshapes someone with named shapes, so your mod never needs Genesis 8 dial names:
luaPlayer:modifyMorph("weight", -0.05) -- a month of dieting; -1 thin .. 1 heavy
Player:modifyMorph("muscle", 0.03)
Player:setMorph("nose", 0.2) -- surgery
if Player:getMorph("weight") > 0.6 then ... end
The shapes are listed in ul_Base/CharGen/MorphAliases.txt (weight, muscle, bodybuilder, tone, breasts, implants, pectorals, waist, belly, hips, butt, thighs, calves, shoulders, arms, nose, lips, jaw...); a mod adds its own there (see Character creator). Any other name is taken as a dial. Changes are saved, for the player and for anyone they know.
copyAppearanceFrom(other) gives someone another person's whole look (face, body, hair, skin, clothes) while they stay who they are; restoreAppearance() gives theirs back. Two calls swap two people:
luaPlayer:copyAppearanceFrom(Stranger)
Stranger:copyAppearanceFrom(Player) -- gets the player's real look, not the borrowed one
-- ... later
Player:restoreAppearance()
Stranger:restoreAppearance()
Where they are
| Call | Effect |
|---|---|
Actor:getBuilding("Home") / "Work" / "Current" |
A venue tag |
Actor:isAt("Home") / "Work" |
Whether they are there right now |
Actor:addToPeopleHere() |
Keep them at the player's location after the scene |
Actor:addScheduleEntry(from, to, weekdays, tag) |
A rule in their weekly schedule (weekdays is a bitmask, 127 = every day, bit 0 = Sunday) |
Actor:clearSchedule() |
Drop all their schedule rules |
Day-to-day movement of known people is driven by NPC routines.
Injuries
A fight leaves marks. Actor:addInjury(name, severity, days) paints a bruise or cut over the skin and fades it out over the days; healInjury(name?) takes it away (all of them without a name), hasInjury, getInjuries read them.
luaVictim:addInjury("black_eye_left", 0.9, 6)
Victim:addInjury("split_lip", 1, 3)
if Player:hasInjury() then Game:Narrate("The nurse frowns at your face.") end
ul_Base ships black_eye_left/right, cheek_bruise_left/right, split_lip, forehead_cut (Face), ribs_left/right, stomach, chest (Torso), forearm_left/right, upper_arm_left/right, wrists (Arm), thigh_left/right, shin_left/right (Leg). A mod adds its own as Injuries/<Face|Torso|Arm|Leg>/<name>.png: a 1024 RGBA image in that skin's UV layout (the same as tattoos and makeup), dark colour with soft alpha. Injuries are part of the look, so they are saved.
Makeup and tears
Actor:setMakeup(zone, style, r, g, b, a) puts one layer on the face: Eyeshadow, Blush, Liner, Lips or Tears, a style from Makeup/<zone>/, a colour and a strength. Actor:setTears(strength) runs mascara down the cheeks; setTears(0) dries it. Both are part of the look and saved.
luaVictim:setTears(0.9)
Date:setMakeup("Lips", "LK_Lips_02", 0.5, 0.05, 0.1, 0.85)
A mod adds styles as Makeup/<Eyeshadow|Blush|Liner|Lips|Tears>/<name>.png: white, the shape in the alpha channel, on the Genesis 8 face UV (the same layout as injuries and Makeup/Face/ looks). The game tints it with the zone's colour. Every style of the four creator zones (Eyeshadow, Liner, Blush, Lips) is listed on the creator's Makeup page under its own heading, with a colour palette, from its Thumbs/<name>.png; Tears stay a scene effect. Makeup/makeup.json sets how often generated women wear each zone and in which colours:
json{ "zones": { "Lips": { "chance": 0.5, "strength": [0.45, 0.85], "colors": [[0.55, 0.12, 0.14], [0.8, 0.45, 0.45]] } } }
A later mod's file replaces the zones it names.
The party
Up to four people can walk with the player: a bodyguard, the crew on a job, a rescued relative brought home. They are wherever the player is and every room places them, with no show() needed.
luaif Actor:joinParty() then
Game:Narrate("<Actor.name> falls in beside you.")
end
for _, id in ipairs(Game:getParty()) do ... end
Actor:leaveParty()
joinParty() returns false when the party is full or the person is already in it. Each member has a chip under the player's bars on the city map: the name opens their sheet, the house sends them home (their sheet has "Send home" too). An action with END_PARTY = true (sleep) breaks the party up.
History
People remember what happened to them. See History.
luaActor:record("argued", Player, "major")
if Actor:hasEvent("argued") > 2 then
Game:Say(Actor, "Angry", "Not this again.")
end
Methods added by mods
Mods can add methods to every person (see Libraries). With ul_Health enabled, for example, everyone has infect, injure, treat, isSick and condition:
luaif Game:isModEnabled("ul_Health") and Actor:isSick() then
Game:Say(Actor, "Sad", "Sorry, I feel awful today.")
end