Ambitions
What an adult townsperson wants out of life, drawn from who they are and pursued step by step over months and years. Data files any mod can extend.
An ambition is a lifetime aim: become rich, find love, be looked up to, get out of a life that is crushing them. Every adult of the town draws one or two from who they are (their facets and traits) and holds them for months and years; the Life sheet shows what they are after and how far along they are, and the chronicle says when they set out, got there or gave up. The player never draws one: the player stays free.
The ambition says what they want; their traits say how they go about it. "Become rich" is one ambition: a diligent person saves and climbs, a cruel one extorts. Which action they take for it is the work of the means and the planner (the next step of the person-first rework); nothing in an ambition names an action, a scene or a crime.
Files
Ambitions/*.ulambition, JSON, discovered in every mod in load order. ul_Base ships base.ulambition (twelve groups) and dark.ulambition (the dark group). A later file redefines an ambition or a group by id (the whole object is replaced, and the override is logged); a new id adds one. Prefix your ids with your mod's prefix (ul_Fame_be_a_star); the ul_Base ids are the base vocabulary.
json{
"version": 1,
"draw": { "atAge": 18, "count": 1, "secondChance": 0.35, "noise": 0.35, "redrawAfterDays": 30 },
"groups": [ { "id": "fortune", "name": "Fortune", "description": "Money: enough of it, then more." } ],
"ambitions": [
{ "id": "own_business", "text": "Own a business of my own", "group": "fortune",
"score": { "ambition": 0.4, "autonomy": 0.4, "risk_tolerance": 0.3 },
"traits": { "ambitious": 0.4, "resourceful": 0.4, "cautious": -0.3 },
"age": [18, 65], "tone": { "peaceful": 1, "balanced": 1, "dark": 1 },
"state": ["owns_business >= 1", "business_days >= 180"],
"steps": [
{ "text": "Save the money to start", "state": ["wealth >= 15000"], "windowDays": 365, "failBudget": 2 },
{ "text": "Open the doors", "state": ["owns_business >= 1"], "windowDays": 180, "failBudget": 2 } ],
"next": "grow_business",
"onFail": [ { "do": "abandon" } ] }
]
}
Tools/ModLint/ambitions_check.py checks every file against the personality data, lists the readers the engine does not provide yet, and --simulate N estimates what a town of N adults would draw. The full field list is in Content/Modules/ul_Base/Ambitions/README.md.
The draw
The first midnight a person spends as an adult (their eighteenth birthday, or the day they are created when they are created older) draws their ambitions, deterministically from their id. Each candidate weighs
weight × tone[world] × exp(Σ (facet − 0.5) × score[facet] × 4 + Σ traits held) × noise
the same affinity the traits themselves are generated with. A candidate is skipped when it is outside the person's age band, forbidden by one of their traits (forbidTraits), already achieved (state holds), exclusive with or in the same group as one they hold, when a requires condition fails, or when its target finds nobody. draw.count ambitions are always drawn, plus one more with secondChance (raised by the ambition facet). A life whose ambitions have all ended draws again after redrawAfterDays.
Conditions and readers
requires, state, steps[].state and onFail[].if are lists of strings that must all hold: wealth >= 25000, !has_home (zero), has_partner (above zero), @target:friends <= 1 (read on the target). A reader is one of the life readers (wealth, career_rank, owns_business, married, children, friends, feared_by, has_partner, has_job, has_home, homeless_days, rent_debt, social_debt, social_pressure, temperature…), stat:<id> for any stat, opinion:<dimension>:<id>, grudge_against:<id>, memories:<kind>, facet:<id> and trait:<id> for the personality, or the bare name of a defined stat. A reader the engine does not know reads as false, never as zero, and is logged once: a catalogue written ahead of its readers never completes by accident.
The means added a second family of readers, computed on the spot from the town's state and never stored. Standing: fame (the stat), respected_by (people whose respect for them is 30 or more), afraid_of, grudges_held, controls (people who fear them and owe them), controlled_by, crew (people who both fear and trust them), best_trust, lovers, leverage (people whose acts they know of). Family: parents_alive, family_estranged, outdid_parents, children_settled. Work: career_top, job_field:<field>, subordinates, employees, self_employed, qualifications, clubs, best_skill (the best of the stats a .ulmeans lists as skills), owns_home. About one person: turned_against:<id>, harmed:<id>, brought_down:<id>, knows_weakness:<id>, with {target} for the id. The ambition's own clock: held_days; the person's: mood_avg30. And every life counter by its bare name (works_made, nights_out, trips, thrills, performances, bets_placed, helped_others, parent_care_days, legacy, job_days, sober_days…): a counter is known as soon as a .ulmeans declares it or an action's ADVANCES names it, and reads 0 until something moves it. The reference table is in Content/Modules/ul_Base/Ambitions/README.md.
Steps, success, failure
One step is current at a time, checked every seven days of the person's life. Its state holds: the step is done and the next starts with a fresh window; after the last one the ambition's own state is tested and the ambition is achieved (and its next tier starts, when it has one). windowDays pass without the state holding: one failure, the window restarts. Failures over failBudget: the ambition fails, and onFail says what that does. abandon ends it (a new draw comes redrawAfterDays later); mutate turns it into another ambition, with the person who blocked the step as its target. Mutation and the facet marks (marks) are read today and acted on by the next steps of the rework.
Means
An ambition says what; a means says how a step moves. Ambitions/*.ulmeans (JSON, discovered like the ambitions, a later id replacing an earlier one) declares three things: the daily counters, the skills best_skill reads, and the means themselves. ul_Base/Ambitions/means.ulmeans is the reference file.
json{
"version": 1,
"counters": { "job_days": { "daily": ["has_job"], "reset": ["!has_job"] },
"sober_days": { "daily": ["stat:drug_dependence <= 0"], "reset": ["stat:drug_dependence > 0"] } },
"skills": ["music", "martial", "fitness", "intelligence", "cooking", "art", "dancing", "charisma", "driving"],
"means": [
{ "id": "open_business", "advances": { "owns_business": 1, "self_employed": 1 },
"do": { "engine": "business.open", "kind": "shop", "cost": 10000, "income": 3200, "title": "Owner" },
"requires": ["wealth >= 12000"], "odds": 0.1, "behaviors": { "ambition": 0.5, "risk_taking": 0.5 } },
{ "id": "perform", "advances": { "performances": 1, "fame": 1 }, "do": { "action": "*" } }
]
}
A counter gains 1 at midnight while its daily conditions hold and drops to 0 when its reset conditions do (same syntax as an ambition's conditions). A means has an id, what it advances (reader to amount), what it does (do), what it requires first, and how acceptable it is: odds (daily chance, default 0.1), norm (0 to 1, the share of people who would), minTemperature (opens only once the person lacks enough: the bar is a quarter higher in a peaceful world, a quarter lower in a dark one), risk (cuts the odds in a peaceful or balanced world), adultOnly (default true) and behaviors (behaviour id to weight, scaling the odds by the person's bias).
do is one of three forms:
{ "action": "*" }: any action whoseEFFECTSorADVANCESmove the reader. The planner makes the step a goal on that reader and the ordinary decision loop finds the action.{ "action": "<id>" }names one action.{ "lifeChoice": "<id>" }: one of the life choices.{ "engine": "<routine>", ...params }: a routine of the engine, tried once a day with the means' odds for the person whose ambition goal names it:
| Routine | What it does | Params (default) |
|---|---|---|
jobs.apply |
Applies to a catalogue job at its first rank that they qualify for and that pays at least a tenth more than today's (anything when jobless); hired by the town's hiring chance. | — |
jobs.promote |
The ordinary promotion check, now. | — |
residence.move |
Moves into a vacant home they can afford: a dearer one within a third of their income when better is 1 and the rent is paid, a cheaper one when it is 0. Minor children follow. |
better (1) |
residence.buy |
Buys the home they rent at years of rent, the money going to its owner. |
years (10) |
business.open |
Registers a business of kind to them, at cost; their job from now on, paid income a month. |
kind (shop), cost (10000), income (3000), title (Owner) |
business.hire |
A jobless adult of the town joins their business, up to max staff. |
max (5), salary (1500), title (Employee) |
partner.seek |
The free adult, not family, they find most attractive and who likes them back; at the same place unless anywhere. The pair starts dating. |
anywhere (1), minScore (0) |
partner.marry |
Marries the partner after days days together, when the partner's rapport is at least rapport. |
days (180), rapport (0) |
partner.leave |
Leaves the partner or spouse. | — |
advance |
Lands the counters in advances and nothing else. |
— |
The planner. Each midnight, for a person with ambitions: the first condition of the current step that does not hold, the means that advances its reader and whose requirements hold (or, when a means needs something first, that requirement, three levels down: save, then open the business, then hire) becomes the goal at the top of the person's goals, with the step's window as its deadline and the group's drive (a .ulambition group may name one) as what it is scored under. The decision loop pursues it like any goal; an engine means is also tried once a day, more often as the goal escalates.
ADVANCES on actions. Any action can carry a table of counters it moves when it completes, for the NPC or the player who did it (Actions):
luaADVANCES = { performances = 1, nights_out = 1 },
Counter names only: a name here becomes a counter and shadows a stat or life reader of the same name, so a stat change stays in EFFECTS. An action with ADVANCES and no NPC block is still player-only (the generic NPC rules only value effects a drive reads): add NPC = { ENABLED = true, BASE = ... } when an NPC can do it. The base content carries ADVANCES on about eighty actions: gigs, opening nights, streams and karaoke (performances), crafts, photo walks, writing and a thesis (works_made), bars, clubs, bowling, parties and festivals (nights_out), holidays, cruises and camping (trips), climbing, surfing, skating and the marathon (thrills), gambling (bets_placed), volunteering, donations and prison visits (helped_others), visiting an ageing parent (parent_care_days), the letter to a grown child and the will (legacy).
The engine means write their own history: npc_job_application, npc_job_found, npc_changed_job, npc_promoted, npc_moved_home, npc_bought_home, npc_business_opened (with {business}), npc_hired_staff, npc_started_dating, npc_married, npc_broke_up. Tools/ModLint/ambitions_check.py lists the readers nothing provides yet, and modlint.py check validates .ulmeans keys and ADVANCES tables.
Adults only
The loader refuses an ambition whose age[0] is under 18, nobody draws before atAge (itself never under 18), and every target is an adult, whatever the data says. The dark group (revenge_control) is weighed by cruelty, domination and grudge, and its share of the adults is measured by the long simulation: at most 8 % in a peaceful world, 20 % in a dark one.
What the player sees
The Life sheet of a person shows each ambition's text, its tier, the current step and a progress bar: the done steps plus, for the current one, how far its readers went from where they stood when the step began toward their thresholds. The history records npc_ambition_drawn, npc_ambition_step, npc_ambition_achieved and npc_ambition_abandoned, with the ambition's text. In Sandbox everything shows; what the player's own character may know of it is the Story-mode filter of a later step.