Jobs and university
Majors and subjects as data, and the calls scenes use for jobs, school, rent and salaries.
University majors: .ulmajors
The majors a student can pick, and the subjects taught under each, are a data file found anywhere in any mod:
lua-- ynl_majors.ulmajors
return {
{ NAME = "Hospitality", SUBJECTS = { "Mixology", "Hotel Management", "Event Planning", "Wine Studies" } },
{ NAME = "History", SUBJECTS = { "Roman History", "Medieval History", "Modern History", "Art History" } },
}
- A major with a
NAMEalready defined by an earlier mod is replaced; a new name is added. - The order of
SUBJECTSmatters: lecturers are assigned subjects in that order (Actor:setSubject()). - Scenes show them through
Actor:GetToken("Major"): the subject a lecturer teaches, or the major a student studies.
The base list lives in ul_Base/Majors.ulmajors.
The player's job and studies
| Call | Effect |
|---|---|
Game:moveJobs() |
The player is hired at the building they are in; set the salary and boss separately |
Game:getSalary(), Game:setSalary(n) |
The player's salary, a monthly amount in dollars. The game pays it on payday by itself, on the job's schedule: see Pay and payday |
Game:loseJob() |
Fired: salary and workplace cleared, boss and colleagues become acquaintances |
Game:startMayor() |
Become mayor (the job change after winning the election) |
Game:moveSchools(), Game:isStudent() |
Enrol at the building they are in |
Game:chooseMajor() then Game:setMajor() |
Let the player pick a major (a built-in choice menu) |
Game:setTuition(n), Game:setFraternityFees(n), Game:moveFraternity() |
Student costs and fraternity |
Game:quitSchool(), quitMedical(), quitLecturing(), quitFraternity(), quitCrime() |
Leave those careers |
Game:getRent(), Game:setRent(n), Game:moveHome(), Game:loseHome() |
Housing |
Game:imprison(), Game:endPrison() |
Prison (the sentence is the player's sentence stat, set by the scene) |
Other people's jobs
| Call | Effect |
|---|---|
Actor:setJob("Doctor"), Actor:getJob(), Actor:isJob("Nurse") |
A free-text job title |
Actor:setWorkHere() |
They now work at the building the player is in |
Actor:getBuilding("Work"), Actor:isAt("Work") |
Where they work, and whether they are there now |
Actor:setBoss(), Actor:addColleague(), Actor:removeColleague() |
Workplace relationships with the player |
Actor:setSameMajor(), Actor:setDifferentMajor(), Actor:setSubject() |
University roles |
Actor:addProstitute(), Actor:removeProstitute(), Actor:isProstitute() |
Sex work |
Actor:addCriminal(), Actor:setCrimeBoss() |
Organised crime |
Job titles are free text: nothing validates them, so a mod can invent new ones and find those people with Game:getSpecific("Bartender").
Reading the job: Work:snapshot()
A mod that keeps progress for "this job" (a portfolio, a rank, a reputation at work) reads the job through Work:snapshot() instead of comparing titles:
lualocal job = Work:snapshot() -- nil when the player has no job
if job then
-- job.title, job.salary (monthly dollars), job.workplace (venue tag)
-- job.building: the employer's own building id ("" when the job was not taken at one)
-- job.place: its name; job.atWork: the player is there now
-- job.roleVersion: changes with any new title or employer, even with the same title
-- job.since: when it last changed; job.boss (person id) and job.bossAtWork (their routine)
end
- Key what the player earns in a role to
job.roleVersion, not to the title: moving to another office as "Analyst" is a new employer, and the version says so. moveJobsmakes the building the player is in the employer, so do it there (an interview at the venue, then the offer).Events:on(mod, "jobChanged", function(what, roleVersion) end)runs when the title, the workplace or the salary changes, or the job is lost ("title","workplace","salary","lost").- The salary is paid by the game: see Pay and payday.
Several jobs, hours and attendance: Jobs
The player can hold several jobs at once. The main job is still the one setJob, moveJobs, setSalary and loseJob manage (it is Jobs:get("main")); a mod adds side jobs with Jobs:add, which never touch it. Use a side job for anything that is not the player's career: a bar job in the evenings, a weekend shift, a gig.
Every job has days and hours. Attendance is measured, not declared: the time the player spends at the job's building during a shift. Below half the shift it is a missed shift:
- missed shifts earn nothing: the main job's monthly salary is paid only for the shifts worked (a full week pays the whole salary), a side job is paid after each worked shift;
- two missed shifts within 30 days bring a warning, the third the sack (the player is notified, and the job is gone);
- overlapping jobs are allowed: the player chooses where to be, and the shift they skip is missed.
Jobs:addreturns the jobs that clash, so a hiring scene can say so.
Titles that are not jobs (Unemployed, Deceased, On remand...) and a job with nowhere to be are not tracked.
Hours are mod data: Jobs/schedules.json
json{
"default": { "days": [1, 2, 3, 4, 5], "hours": [9, 17] },
"no_schedule": ["Unemployed", "Deceased", "On remand"],
"by_title": { "Nurse": { "days": [1, 3, 5, 6], "hours": [7, 19] } },
"by_workplace": { "bar": { "days": [3, 4, 5, 6, 0], "hours": [18, 2] } },
"rules": { "warn_after": 2, "fire_after": 3, "window_days": 30, "low_performance": 20, "performance_after_shifts": 5, "attendance_share": 0.5 }
}
Days are 0 (Sunday) to 6; an end hour at or before the start runs past midnight. A job takes its title's entry, else its workplace tag's, else default. A mod overrides any of these by shipping its own Jobs/schedules.json; a side job can also bring its own days and hours in Jobs:add.
Events:on(mod, "jobChanged", fn(what, id)) also reports "added", "worked", "missed", "warning", "fired", "quit" with the job id ("main" for the main job).
The job catalogue: Jobs/*.uljobs
A catalogue job is one with a ladder: what each rank needs from the knowledge tree, what it pays, and what working it teaches. It is what lets a hospital refuse a doctor who never studied. The catalogue is mod data, merged by ID in load order (a later mod with the same ID replaces the job):
lua-- ynl_Nightlife/Jobs/bar.uljobs
return {
{ ID = "ynl_bartender", NAME = "Bartender", FIELD = "food", WORKPLACES = { "bar", "nightclub" },
LEARNS = { "ynl_bar_work", "ynl_mixology" },
RANKS = {
{ TITLE = "Barback", SALARY = 1400 },
{ TITLE = "Bartender", SALARY = 2100, MONTHS = 3,
REQUIRES = { KNOWLEDGE = { "ynl_bar_work:50" } } },
{ TITLE = "Head bartender", SALARY = 3200, MONTHS = 12,
REQUIRES = { KNOWLEDGE = { "ynl_mixology:50" }, QUALIFICATIONS = { "ynl_Nightlife/door_licence" },
STATS = { interpersonal = 50, jobperformance = 60 } } },
} },
}
| Field | Meaning |
|---|---|
ID |
Unique across mods: prefix yours with your mod's name. |
NAME, FIELD |
Shown on the Knowledge screen; FIELD is a field of the tree. NAME defaults to the ID. |
WORKPLACES |
Venue tags where it is offered (Jobs:catalogue("bar") lists the jobs of a bar). |
LEARNS |
Knowledge nodes every worked shift adds progress to: a quarter of the shift's hours to each. |
ALIASES |
Optional: other titles that mean this job, the ones the NPC simulation writes ("Office worker", "Care worker"). An NPC with one is placed on the ladder at rank 1 or 2. |
RANKS[].TITLE |
What the job is called at that rank. It is what setJob writes, so Jobs/schedules.json, payroll and every isJob("Bartender") keep working. Defaults to NAME. |
SALARY |
Monthly dollars before the city's cost of living; the game scales it. |
MONTHS |
Tenure in the previous rank before this one can be reached. |
REQUIRES |
KNOWLEDGE as "node" (a score of 25) or "node:50" (a score out of 100), QUALIFICATIONS as full "mod/id", STATS as minimum values. Cumulative: a rank's requirements are on top of every rank below it. |
A job with no RANKS is a one-rank ladder: its NAME as the title, with SALARY and REQUIRES given on the job itself.
Legacy titles land on the ladder. The main job remembers its catalogue id. When a scene uses the old Player:setJob("Bartender") or an old save is loaded, the title is matched to a rank's TITLE (case-insensitive) and the job takes that place on the ladder, so it gets a next rank too; a title no catalogue knows is simply a job without a ladder.
Hiring. Jobs:hire(id [, rank]) makes the catalogue job the player's main job: the rank's title and salary, and the building the player is in becomes the employer, as Game:moveJobs does, so do it at the venue. It refuses, with the first missing thing, unless every rank up to the one asked is met. jobChanged reports "hired".
lua-- in a scene at the bar
for _, job in ipairs(Jobs:catalogue(Places:here().tag)) do -- the vacancies of this venue
-- job = { id, name, field, mod, workplaces, learns, canApply, why, held,
-- ranks = { { title, salary, months, met, why, current, knowledge } } }
-- ranks[].knowledge is the REQUIRES.KNOWLEDGE strings ("ynl_bar_work:50")
end
local ok, why = Jobs:canApply("ynl_bartender") -- rank 1 unless told otherwise
if not ok then
Game:Say(Actor, "Neutral", "Not yet. " .. why) -- "Needs Bar work 50 (Competent), you are at 31."
return
end
Game:Say(Actor, "Happy", "Start Thursday. Barback, " .. Jobs:catalogue()[1].ranks[1].salary .. " a month.")
Jobs:hire("ynl_bartender") -- or Jobs:hire("ynl_bartender", 2) for an experienced hire
Promotion. Jobs:nextRank() says what the next rank is and what still stands in the way; Jobs:promote() applies it (title, salary, rank, the tenure clock reset) and reports "promoted" on jobChanged. Both take a job id and default to "main".
lua-- "Ask for a raise" at work
local next = Jobs:nextRank() -- nil at the top, or for a job with no ladder
if not next then
Game:Say(Actor, "Neutral", "There's nowhere up from here.")
elseif next.why then -- "Needs 2 more months in the current role." / "Needs Mixology 50 (Classics), you are at 38."
Game:Say(Actor, "Neutral", next.why) -- next = { title, salary, months, monthsLeft, why }
else
Game:Say(Actor, "Happy", next.title .. ", then. " .. next.salary .. " a month.")
Jobs:promote()
end
local ladder = Jobs:ladder() -- the held job's catalogue entry with rank, months and
-- ranks[].current; nil when it is not from the catalogue
NPCs on the ladder. Everyone else shares the catalogue. Once a day, an NPC whose job title is a rank's TITLE or one of the job's ALIASES is placed on that ladder: the knowledge their rank asks is seeded a little above the bar (prerequisites too), its qualifications awarded, and some tenure assumed. From then on they practise the job's LEARNS most days, and when tenure and the next rank's requirements are met they are promoted, title and salary, on a slow daily roll, so a ward's nurses do not all become senior on the same morning. Jobs:careerOf(person) reads where someone stands; for the player it reads the main job's ladder.
lualocal c = Jobs:careerOf(Actor) -- nil when their job is not on the catalogue
if c and c.rank < c.ranks then -- { id, name, field, title, rank, ranks (how many), months (in this rank) }
Game:Say(Actor, "Sad", "Still " .. c.title .. " after " .. math.floor(c.months) .. " months.")
end
Two notifications, once each, checked daily: Ready for promotion when the held job meets everything its next rank asks ("Ask for it at work": your promotion scene does the rest), and You now qualify when the player first meets the entry rank of a catalogue job they do not hold, naming its workplaces. Jobs whose first rank asks for nothing are never announced.
| Call | Does |
|---|---|
Jobs:catalogue([workplaceTag]) |
The catalogue, or the jobs offered at venues of that tag. |
Jobs:canApply(id [, rank]) |
ok, why for being hired at that rank now. |
Jobs:hire(id [, rank]) |
Hires into the main job here; ok, why. |
Jobs:ladder([jobId]) |
The held job's catalogue entry and rank, or nil. |
Jobs:nextRank([jobId]) |
{ title, salary, months, monthsLeft, why }, or nil. |
Jobs:promote([jobId]) |
Applies the next rank; ok, why. |
Jobs:careerOf(person) |
Anyone's place on a ladder: { id, name, field, title, rank, ranks, months }, or nil. |