diff --git a/postings.mjs b/postings.mjs index 4e2c182..03c833c 100644 --- a/postings.mjs +++ b/postings.mjs @@ -62,7 +62,7 @@ export const ROLES = { blurb: "Keeps the record that a crossing is made against. Knows what the file says and, more usefully, what it used to say.", stat: "edu", core: ["research","occult","knowledge","anomaly_lore","language:own"], support: ["computer_use","insight","spot","first_aid","tradecraft","firearm:pistol"], - talents: ["chain_custody","wrong_memory","debriefer","quorum"], + talents: ["chain_custody","wrong_memory","debriefer","requisition"], kit: ["spectacles","notebook","handset","jacket","pistol_light", "carbon_book", "date_stamp", "seals"] }, survey: { label: "Ground Survey Officer", @@ -76,28 +76,28 @@ export const ROLES = { blurb: "Holds the door open. Transposition, and the discipline to stay filed correctly while doing it.", stat: "pow", core: ["transposition","anomaly_lore","insight","listen","dodge"], support: ["knowledge","spot","first_aid","tradecraft","research"], - talents: ["out_of_step","borrowed_hour","not_quite_here","distributed"], + talents: ["out_of_step","borrowed_hour","not_quite_here","echo_sense"], kit: ["anchor","threshold_key","handset","power_cell","soft_armour", "locator_tags", "cold_light", "blanket"] }, interview: { label: "Interview Officer", blurb: "Sits with the witness afterwards. Patient, unhurried, and very hard to lie to twice.", stat: "cha", core: ["insight","persuade","listen","language:own","anthropology"], support: ["psychology","spot","tradecraft","first_aid","firearm:pistol"], - talents: ["debriefer","read_room","wrong_memory","unblinking"], + talents: ["debriefer","read_room","wrong_memory","cover_work"], kit: ["dictaphone","cover_id","notebook","jacket","pistol_light", "carbon_book", "vacuum_flask", "clipboard"] }, warden: { label: "Night Warden", blurb: "Sits with a site overnight so that nobody else has to. Listens, watches, and writes down the times.", stat: "con", core: ["listen","spot","stealth","dodge","brawl"], support: ["first_aid","melee_weapon:shield","athletics","melee_weapon:club","anomaly_lore"], - talents: ["unblinking","out_of_step","first_through","clean_scene"], + talents: ["dead_reckon","out_of_step","first_through","clean_scene"], kit: ["oil_lantern","ward","cudgel","riot_shield","stab_vest","torch", "vacuum_flask", "stopwatch", "blanket"] }, stores: { label: "Stores Officer", blurb: "Requisition, custody and the paperwork that moves materiel. Can get you a thing the department says it does not have.", stat: "edu", core: ["bargain","status","knowledge","repair:mechanical","computer_use"], support: ["insight","research","first_aid","firearm:pistol","tradecraft"], - talents: ["requisition","chain_custody","quorum","debriefer"], + talents: ["requisition","chain_custody","read_room","debriefer"], kit: ["sealed_cannister","spare_cells","rations","water","jacket", "carbon_book", "seals", "master_keys"] }, transport: { label: "Transport Officer", @@ -111,14 +111,14 @@ export const ROLES = { blurb: "Examines what is left. Establishes what a thing was, which is usually the question the case turns on.", stat: "int", core: ["medicine","science:biology","xenology:baseline","insight","research"], support: ["first_aid","spot","religion","dodge","melee_weapon:knife"], - talents: ["triage","chain_custody","unblinking","clean_scene"], + talents: ["triage","chain_custody","wrong_memory","clean_scene"], kit: ["chirurgeon_kit","gas_mask","envsuit","sample_case","dagger", "specimen_jars", "swab_kit", "barrier_gloves"] }, materials: { label: "Materials Officer", blurb: "Substances, residues and the objects a site leaves behind. Handles what nobody else is cleared to touch.", stat: "int", core: ["science:physics","science:botany","repair:electronics","research","anomaly_lore"], support: ["engineering:field_systems","spot","first_aid","computer_use","firearm:pistol"], - talents: ["chain_custody","echo_sense","requisition","quorum"], + talents: ["chain_custody","echo_sense","requisition","hard_landing"], kit: ["field_kit","geiger","breather","stab_vest","pistol_light", "specimen_jars", "barrier_gloves", "seal_foam"] }, recording: { label: "Recording Officer", @@ -139,63 +139,63 @@ export const ROLES = { blurb: "Renders a thing safe, or renders it gone. Demolition, containment and a very steady hand.", stat: "int", core: ["demolition","repair:electronics","engineering:field_systems","spot","dodge"], support: ["first_aid","athletics","anomaly_lore","firearm:rifle","melee_weapon:axe"], - talents: ["first_through","hard_landing","chain_custody","unblinking"], + talents: ["first_through","hard_landing","chain_custody","clean_scene"], kit: ["field_net","entrenching_tool","hardsuit","toolkit","torch", "seal_foam", "cordon_kit", "ear_defenders"] }, overwatch: { label: "Overwatch Officer", blurb: "Holds the long angle and tells the team what is coming. Shoots when nothing else will do.", stat: "dex", core: ["firearm:rifle","spot","stealth","dodge","athletics"], support: ["navigate","first_aid","listen","track","firearm:pistol"], - talents: ["dead_reckon","unblinking","out_of_step","hard_landing"], + talents: ["dead_reckon","read_room","out_of_step","hard_landing"], kit: ["rifle_heavy","binocs","soft_armour","rations","torch", "spyglass", "groundsheet", "cold_light"] }, rigging: { label: "Rigging Officer", blurb: "Height, depth and confined access. Gets people and equipment up and down shafts that were not built for it.", stat: "str", core: ["athletics","repair:mechanical","engineering:field_systems","dodge","melee_weapon:axe"], support: ["first_aid","spot","navigate","brawl","stealth"], - talents: ["hard_landing","first_through","requisition","distributed"], + talents: ["hard_landing","first_through","requisition","ambidextrous"], kit: ["boarding_axe","climbing_kit","rope","impact_vest","torch", "bolt_croppers", "probe", "door_wedges"] }, translation: { label: "Translation Officer", blurb: "Language, sign and the things said in neither. Reads a document nobody else in the building can.", stat: "int", core: ["language:trade_cant","language:sign","language:own","anthropology","insight"], support: ["research","knowledge","first_aid","xenology:baseline","firearm:pistol"], - talents: ["debriefer","read_room","quorum","wrong_memory"], + talents: ["debriefer","read_room","borrowed_hour","wrong_memory"], kit: ["wax_tablet","notebook","handset","jacket","pistol_light", "quill_ink", "carbon_book", "spectacles"] }, naturalist: { label: "Field Naturalist", blurb: "Living things, and things that are pretending to be. Tracks, samples and identifies what came through.", stat: "int", core: ["science:botany","science:biology","track","scent","spot"], support: ["navigate","stealth","first_aid","athletics","melee_weapon:sword"], - talents: ["dead_reckon","echo_sense","unblinking","triage"], + talents: ["dead_reckon","echo_sense","hard_landing","triage"], kit: ["machete","sample_case","binocs","oilcloth_coat","rations", "specimen_jars", "swab_kit", "groundsheet"] }, antiquities: { label: "Antiquities Officer", blurb: "An archaic posting the department never abolished. Old objects, older records, and the manners to ask about both.", stat: "edu", core: ["religion","art","anomaly_lore","research","status"], support: ["knowledge","insight","spot","first_aid","melee_weapon:sword","firearm:pistol"], - talents: ["chain_custody","wrong_memory","borrowed_hour","quorum"], + talents: ["chain_custody","wrong_memory","borrowed_hour","cover_work"], kit: ["pocket_watch","tallow_candle","duelling_sword","brigandine","flintlock", "quill_ink", "sealing_wax", "sand_glass"] }, pursuit: { label: "Pursuit Officer", blurb: "Follows what has left the site. Ground, scent, and the stamina to keep going after it stops being sensible.", stat: "con", core: ["track","athletics","spot","listen","stealth"], support: ["navigate","dodge","first_aid","scent","firearm:rifle"], - talents: ["dead_reckon","first_through","out_of_step","unblinking"], + talents: ["dead_reckon","first_through","out_of_step","hard_landing"], kit: ["carbine","restraints","soft_armour","rations","torch", "locator_tags", "cold_light", "wake_tablets"] }, settlements: { label: "Settlements Officer", blurb: "Makes the arrangement, files it, and never speaks of the terms again. Most cases close here rather than in a fight.", stat: "cha", core: ["bargain","status","insight","persuade","knowledge"], support: ["research","anthropology","first_aid","tradecraft","firearm:pistol"], - talents: ["read_room","debriefer","requisition","quorum"], + talents: ["read_room","debriefer","requisition","wrong_memory"], kit: ["quiet_ledger","cover_id","notebook","jacket","pistol_light", "carbon_book", "sealing_wax", "vacuum_flask"] }, signals: { label: "Signals Officer", blurb: "Communications, interception and the electronics of a site that is broadcasting when it should not be.", stat: "int", core: ["repair:electronics","computer_use","listen","engineering:field_systems","spot"], support: ["research","first_aid","dodge","firearm:pistol","anomaly_lore"], - talents: ["echo_sense","distributed","quorum","requisition"], + talents: ["echo_sense","chain_custody","dead_reckon","requisition"], kit: ["survey_drone","handset","power_cell","stab_vest","pistol_light", "field_slate", "locator_tags", "uv_lamp"] } }; @@ -225,14 +225,14 @@ export const TRADES = { blurb: "Twenty years of sites, scaffold and other people's mistakes. Knows what a building is doing before it does it.", stat: "str", core: ["athletics","repair:mechanical","melee_weapon:prybar","spot","brawl"], support: ["demolition","engineering:field_systems","first_aid","listen","dodge"], - talents: ["hard_landing","first_through","clean_scene","unblinking"], + talents: ["hard_landing","first_through","clean_scene","ambidextrous"], kit: ["prybar","toolkit","ear_defenders","site_overalls","groundsheet"] }, doctor: { label: "Doctor", blurb: "A&E, mostly. Used to people at their worst and to being the calmest thing in the room.", stat: "edu", core: ["medicine","first_aid","science:biology","insight","knowledge"], support: ["research","psychology","persuade","listen","spot"], - talents: ["triage","debriefer","unblinking","clean_scene"], + talents: ["triage","debriefer","read_room","clean_scene"], kit: ["medkit","trauma_unit","analgesics","specimen_jars","swab_kit"] }, professor: { label: "Professor", diff --git a/tools/check-kits.mjs b/tools/check-kits.mjs index 010fbaa..fc14636 100644 --- a/tools/check-kits.mjs +++ b/tools/check-kits.mjs @@ -10,6 +10,8 @@ */ import { SKILL_CATALOGUE, TALENTS, GEAR, WEAPONS, ARMOURS, DEVICES } from "./content.mjs"; import { SHIELD, SCOPE_TIERS } from "../rules.mjs"; +// One definition of which talents are species-locked, shared with check-creatures. +import { TALENT_SPECIES } from "./creature-schema.mjs"; // Imported, not regexed out of the engine source. See postings.mjs. import { ROLES, TRADES, INDUCTION, CHARACTERISTIC_DICE } from "../postings.mjs"; @@ -34,6 +36,18 @@ for (const [id, r] of Object.entries({ ...ROLES, ...TRADES })) { if (!stats.has(r.stat)) problems.push(`${where}: stat "${r.stat}" is not a characteristic`); for (const s of [...r.core, ...r.support]) if (!skills.has(s)) problems.push(`${where}: no skill "${s}"`); for (const t of r.talents) if (!talents.has(t)) problems.push(`${where}: no talent "${t}"`); + // A posting is a JOB. It cannot offer a talent that is a SPECIES, because a species is + // not something the department trains you in. Eighteen postings offered vesh or cadence + // biology to any agent who took them — nine of them Unblinking, whose own rule text + // reads "A Vesh has no face to read" — and five roster agents and a starter pregen duly + // ended up carrying it. check-creatures caught the characters; nothing was looking at + // the pools they were picked from, so the bug regenerated for every new agent. + // The species map comes from creature-schema.mjs so there is one definition of which + // talents are locked, not two. + for (const t of r.talents) { + const locked = TALENT_SPECIES.get(t); + if (locked) problems.push(`${where}: offers "${t}", which is ${locked}-only — a posting cannot grant a species`); + } for (const g of r.kit) { if (!kit.has(g)) problems.push(`${where}: no catalogue item "${g}"`); issued.set(g, (issued.get(g) ?? 0) + 1);