Gameplay content

Knowledge

The knowledge tree, a score out of a hundred per subject that gates jobs, ranks, actions and scene branches.

Knowledge is what a person knows, kept as a tree shown on the map HUD's Knowledge screen (the bookmark on the left rail). Every node is a value out of a hundred, like a stat, that you push up by studying, practising and working, with named quarters on the way (Basics at 25, Competent at 50, Skilled at 75, Mastered at 100). Nothing is bought with points (that is the perk tree). It gates access by number: a job in the catalogue asks for "anatomy:60", a rank on its ladder, an action, a branch of your scene. The tree is mod data: every Knowledge/*.ulknowledge of every enabled mod adds to it.

A knowledge file: Knowledge/*.ulknowledge

json{
	"fields": [
		{ "id": "medicine", "name": "Medicine", "icon": "medicine", "color": "#FF4FA3", "order": 10 }
	],
	"nodes": [
		{ "id": "first_aid", "field": "medicine", "name": "First aid", "tier": 1,
		  "text": "Bleeding, breathing, the recovery position, when to call.",
		  "bands": ["Basics", "Trained", "Certified", "Instructor"], "study": 60 },
		{ "id": "anatomy", "field": "medicine", "name": "Anatomy and physiology", "tier": 2,
		  "requires": ["first_aid"], "study": 200, "teaches": { "intelligence": 1 } },
		{ "id": "degree_medicine", "field": "medicine", "name": "Degree in medicine", "tier": 3,
		  "qualification": "ul_Base/degree_medicine", "requires": ["anatomy:60"] },
		{ "id": "cooking", "field": "food", "name": "Cooking", "tier": 1, "stat": "cooking",
		  "bands": ["Home cook", "Confident", "Professional", "Chef"] }
	]
}
Field Meaning
fields[].id, name A branch of the tree. icon is Knowledge/Icons/<icon>.png of any mod (defaults to the id; _group when none is found), color an sRGB hex, order its place round the tree (default 1000). A field named by a node but not declared is created with the id as its name.
nodes[].id Unique across all mods: prefix yours with your mod's name. ul_Base nodes are unprefixed; they are the shared vocabulary (first_aid, anatomy, cooking...). A later mod with the same id replaces the node.
field, name, text Shown on the Knowledge screen. name defaults to the id.
tier Layout only: 1 sits next to the field, higher tiers further out. Access is requires.
bands Optional, up to four names for the quarters reached at 25, 50, 75 and 100. Missing ones read "Basics", "Competent", "Skilled", "Mastered".
requires Node ids that must be reached before progress on this node counts: "id" means a score of 25, "id:60" a score.
study A learned node: hours of study or practice from 0 to 100 at the nominal pace (default 100). The last quarter, 75 to 100, goes at half pace, so it costs as much as the first three together.
stat A stat-backed node: a stat id. The score is the stat's value.
qualification A certified node: 100 while the person holds that qualification, the full "<mod>/<id>", 0 otherwise.
teaches { "stat": amount } added (ModifyStat) each time a learned node crosses into a new band.
grace Learned nodes only: days without practice before the score starts to fade (default 45).
decay Points lost per week once fading (default 1); 0 means the node never fades.
floor The share of the node's best score it never fades below (default 0.5).

A node is exactly one of study, stat or qualification; none of the three means study with the default hours. Fields and nodes from several files and mods are merged in load order.

The three kinds

Learned (study). The score grows with hours; study hours take it from 0 to 75, as many again from 75 to 100. Only this kind is stored.

json{ "id": "ynl_mixology", "field": "food", "name": "Mixology", "tier": 2, "requires": ["ynl_bar_work:50"],
  "bands": ["Long drinks", "Classics", "Signature", "Name on the door"], "study": 120 }

Stat-backed (stat). Read-only: the score is the stat, read when asked, so a stat raised by any scene shows in the tree at once. This is how the skill stats (cooking, music, martial, sneak, dance, interpersonal, intelligence, fitness) are in the tree without anything being migrated. Knowledge:learn on one of these is refused; raise the stat.

json{ "id": "dance", "field": "arts", "name": "Dance", "tier": 1, "stat": "dance" }

Certified (qualification). 100 while Qualifications:has is true for that id, 0 otherwise; degrees, licences, certificates. Nothing to learn: the qualification's own mod awards it.

json{ "id": "ynl_door_licence", "field": "street", "name": "Door supervisor licence", "tier": 2,
  "qualification": "ynl_Nightlife/door_licence", "requires": ["ynl_bar_work"] }

How the score goes up

  • Study and practice from a scene: Knowledge:learn(id, hours). Refused, with why, when a requirement is unmet, the node is stat-backed or certified, or it is already at 100.
  • Actions: an EFFECTS entry with KNOWLEDGE = (below). The Study action of ul_Base is one.
  • Worked shifts: a job of the catalogue lists the nodes it LEARNS; every worked shift counts a quarter of the shift's hours as practice on each.
  • Granting: Knowledge:grant(id, score) raises the score to at least that, ignoring requirements and hours: a course passed, a mentor, a quest reward.

Each band a learned node crosses applies the node's teaches, tells the player ("Knowledge grows: Anatomy: Competent (50)"; "Knowledge mastered" at 100), and fires knowledgeChanged.

Forgetting

A learned node left alone fades. After grace days without practice (45 by default) it loses decay points a week (1 by default), and never drops below floor of the best score it ever reached (half, by default), so a doctor who has not held a scalpel in years is rusty, not ignorant. Practice resets the clock: Knowledge:learn (even at 100, when it adds nothing), a worked shift of a job whose LEARNS names the node, a course that grants it. The player is told once per band lost ("Knowledge fades: Anatomy slips to 49 (Foundations). Practise it, or study."), and the notification opens the node.

"decay": 0 switches fading off for a node. Stat-backed nodes follow their stat, which has its own drift; certified nodes never fade.

From a scene

lualocal M = "ynl_Nightlife"

if Knowledge:has("ynl_mixology", 50) then                -- the score defaults to 25
	Game:Say(Actor, "Happy", "You can take the cocktail list tonight.")
end
Knowledge:score("first_aid")                             -- 0 .. 100 (Knowledge:level is the same number)
Knowledge:score("first_aid", Actor)                      -- anyone: an Actor, a People reference or an id
local band, name = Knowledge:band("first_aid")           -- 0 .. 4, "Trained"

local r = Knowledge:learn("ynl_mixology", 2)             -- two hours behind the bar
-- r = { score, band, bandName, delta, gained, why }; delta is the points added, gained is true
-- when a band boundary was crossed, why is set and nothing changes when it was refused
Knowledge:grant("ynl_mixology", 75)                      -- true when the score rose
Knowledge:forget("ynl_mixology")                         -- learned nodes only

local why = Knowledge:why("anatomy", 60)                 -- nil when reached, else one sentence:
                                                         -- "Anatomy and physiology: 42 of 60, about 36 more hours of study or practice."
local missing = Knowledge:check({ "degree_medicine", "anatomy:60" })  -- nil, or the first reason
Knowledge:describe("anatomy", 60)                        -- "Anatomy and physiology 60 (Skilled)"

for _, node in ipairs(Knowledge:tree()) do
	-- { id, field, name, text, tier, bands, requires, kind, mod,
	--   study | stat | qualification,
	--   score, band, bandName, why }; why is nil when the score can be worked on now
end
Knowledge:get("anatomy")                                 -- the definition alone, or nil
Knowledge:fields()                                       -- { { id, name, icon, color, order } }
Call Does
Knowledge:score(id [, person]) The score, 0 to 100. Knowledge:level is an alias.
Knowledge:band(id [, person]) The band 0 to 4 and its name.
Knowledge:has(id [, score [, person]]) Whether that score (default 25) is reached.
Knowledge:learn(id, hours [, person]) Hours of practice on a learned node; { score, band, bandName, delta, gained, why }.
Knowledge:grant(id, score [, person]) Raises the score to at least score, ignoring requirements and hours; true when it rose.
Knowledge:forget(id [, person]) Drops a learned node to 0; true when something was forgotten.
Knowledge:why(id [, score [, person]]) nil when reached, else what is missing.
Knowledge:check(list [, person]) A list of "id" / "id:score" (or one string); nil when all met, else the first reason.
Knowledge:describe(id [, score]) The name, the score and its band: "Anatomy and physiology 60 (Skilled)"; just the name for a certified node.
Knowledge:get(id), Knowledge:fields(), Knowledge:tree([person]) Definitions from the enabled mods; tree with the person's state.
Knowledge:version() The API version, 2.

A person is an Actor, a People reference or an id; omitted, the player. Every person has their own tree: an NPC whose job is in the catalogue is given what their rank asks and practises it, so Knowledge:score("anatomy", doctor) means something.

In actions

An action teaches through its EFFECTS, and asks through its CONDITIONS (see Actions):

luaEFFECTS = {
	{ KNOWLEDGE = "ynl_mixology", AMOUNT = 1, TRIGGER = "DURATION" },   -- an hour of practice per hour of the action
},
CONDITIONS = {
	{ TYPE = "KNOWLEDGE", KNOWLEDGE = "ynl_bar_work", SCORE = 50, HIDES = true },
},

AMOUNT is hours of practice: per hour of the action with DURATION, once with START or END. A node whose requirements are unmet is skipped silently. SCORE defaults to 25 (LEVEL is accepted as the same number); a failed condition refuses with "Needs Bar work 50 (Competent)."; HIDES as for stats.

The screen

The rail's bookmark opens the Knowledge screen: the tree in the middle (a node lit by its score, locked while its requirements are unmet), the chosen node's details on the right ("42 / 100 · Competent", "Next: Skilled at 75", how it is measured, prerequisites ticked, the jobs and ranks it opens), and under them the player's job with its next rank and what that still needs. A notification with TargetKind = "knowledge" and a node id as its target opens the screen on that node, which is what the engine's own "Knowledge grows" notification does.

Events and saves

knowledgeChanged(id, score, personId) fires when a learned node crosses a band, by learning, granting or forgetting (see Events). Stat-backed and certified nodes raise nothing of their own: listen to the stat, or to qualificationGained.

luaEvents:on(M, "knowledgeChanged", function(id, score, personId)
	if id == "ynl_mixology" and score >= 75 then Player:ModifyStat("ynl_reputation", 5) end
end)

Learned nodes are saved per person, with the score, the best score reached and when it was last practised. Stat-backed and certified nodes are computed from their sources, so nothing of them is saved twice. A node whose mod is turned off keeps its saved score and comes back with the mod.