From b0981008b3f5ac185ed89454e140afdec7eefbf8 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:34:37 +0100 Subject: [PATCH] No posting may grant a species MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eighteen postings offered vesh or cadence biology to any agent who took them: unblinking nine times, quorum seven, distributed three. That is the root cause behind the six characters check-creatures caught — they were not rogue, they had taken talents their own postings advertised, and every newly generated agent would have done the same. Unblinking is the clearest case. It is cat: species, species: vesh, and its own rule text reads "A Vesh has no face to read and does not need to read yours". Nine postings offered it, including Interview, Pathology and the doctor trade. Each of the nineteen slots is replaced with a talent drawn from what the posting actually does rather than from what was nearest alphabetically: Registry gets Requisition, the Anchor gets Echo Sense, Disposal gets Clean Scene, Signals gets Chain of Custody and Dead Reckoning. No pool gained a duplicate, and no talent is left unused — every legal talent is now offered by at least two postings. The guard lives in check-kits, which is the file that validates postings, and it imports TALENT_SPECIES from creature-schema.mjs so there is one definition of which talents are locked rather than two. check-creatures was looking at characters; nothing was looking at the pools they were picked from. Verified by reintroducing unblinking on the doctor trade and watching it fail, not merely by watching it pass. Co-Authored-By: Claude Opus 5 --- postings.mjs | 36 ++++++++++++++++++------------------ tools/check-kits.mjs | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 18 deletions(-) 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);