Files
RingBRP/tools/mission-tables.mjs
T
slaguru666andClaude Opus 5 d0144f744e The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that
deals with crossings, where reality keeps a record of what belongs where and can be
made to get it wrong.

Contents:
- rules.mjs as the single authority for every rule formula, with 40 rules and 132
  spot-checked values. Nothing may restate a formula it defines.
- 25 postings, each with a key characteristic, trained skills, talents and its own
  kit; a 175-item catalogue across four eras; 231 generated icons.
- Reaction Lanes initiative, re-rolled every round including the first.
- Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong.
- Graded defences: only a critical stops a critical.
- Borrowed Authority — access as a record rather than a hacking roll.
- A case generator, and a rules journal generated from rules.mjs so the book cannot
  drift from the code.

Built compendia are committed so a remote Foundry needs only the files.
node_modules is not; `npm install` restores it for rebuilding content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:09:07 +01:00

431 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MISSION TABLES — the raw material.
*
* Separated from the composer because this file is content and that one is
* machinery, and they change for entirely different reasons: a table gains rows
* when the setting gains texture, the composer changes when the SHAPE of a case
* changes. Keeping them apart means adding twenty new anomalies never risks the
* logic that makes a brief contradict its truth.
*
* Everything here is data. No Foundry, no functions with side effects.
*/
/* ============================================================
* WHAT IS WRONG
* ============================================================ */
export const ANOMALIES = [
{ id: "held_open", name: "A crossing held open", threat: "structural",
brief: "A threshold at {place} has been open for {duration} and has not closed.",
truth: "It is being held open deliberately, from the other side, and whoever is doing it is getting better at it.",
signs: ["compasses lag by a degree", "the tide comes in twice", "radio carries a conversation nobody is having",
"the same gull, repeatedly", "batteries drain from full in an hour"],
evidence: ["a regulator plate, cracked, not agency issue", "scorch in a ring, no heat damage at the centre",
"eleven days of identical weather readings"],
containment: "Anyone who walks through it is a second incident.",
closes: "Find what is holding it and stop holding it. It will close on its own in under a minute.",
fits: { resolutions: ["close", "map", "arrange"], gaps: ["worse", "already_contained", "wrong_thing", "agency_knew", "local_stake", "clock_running", "the_brief_is_bait", "already_over"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near", "deep", "hollow", "wrong"],
placeKinds: ["remote", "structure", "settled"] } },
{ id: "duplicate", name: "A duplicate", threat: "personal",
brief: "A person at {place} is reported to be in two places, and both are attending work.",
truth: "There is no original left. Both are copies, and one of them knows.",
signs: ["two sets of the same fingerprints, hours apart", "a wedding ring that fits neither",
"a dog that will not go near one of them", "the same signature, improving"],
evidence: ["a duplicated payslip", "a photograph with one of them slightly out of focus",
"a dental record that matches twice"],
containment: "The town has noticed. It has been explained as twins for eleven days.",
closes: "One of them must be taken into custody. Deciding which is the case.",
fits: { resolutions: ["custody", "arrange", "close"], gaps: ["not_hostile", "wrong_thing", "one_of_them", "local_stake", "clock_running", "already_over", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "duplicate"], classes: ["near"],
placeKinds: ["settled"] } },
{ id: "misfiled_object", name: "Something misfiled in", threat: "material",
brief: "An object recovered near {place} is not manufacturable and is not ours.",
truth: "It is not an object. It is the part of a larger thing that fitted through.",
signs: ["it is warm on the side away from the light", "instruments read it as absent",
"it has a serial number in our numerals", "it is heavier on Tuesdays"],
evidence: ["a second, smaller piece, buried", "tool marks made by nothing in the catalogue",
"a packing crate with a shipping label for a road that does not exist"],
containment: "It has been photographed. The photographs are already circulating.",
closes: "Recover it whole, or account for every piece. Half of it is worse than none.",
fits: { resolutions: ["recover", "close", "map"], gaps: ["worse", "already_contained", "wrong_thing", "agency_knew", "local_stake", "clock_running", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near", "deep", "hollow", "wrong"],
placeKinds: ["remote", "structure", "settled"] } },
{ id: "revenant", name: "Something wearing a shape", threat: "personal",
brief: "A member of the public at {place} has been behaving as somebody who died {duration} ago.",
truth: "It came through without a body and took the nearest one that was not being used.",
signs: ["it uses the dead man's turn of phrase", "it does not eat", "it keeps going back to the same doorway",
"it will not be photographed twice"],
evidence: ["a grave that has been opened neatly from above", "a coat that fits nobody in the family",
"a diary continuing in the same hand, badly"],
containment: "The family have started telling people.",
closes: "It is looking for something. Give it that, or take the body away from it.",
fits: { resolutions: ["custody", "close", "arrange"], gaps: ["not_hostile", "wrong_thing", "one_of_them", "local_stake", "already_over", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "revenant"], classes: ["near"],
placeKinds: ["settled"] } },
{ id: "thin_place", name: "A place wearing thin", threat: "structural",
brief: "Three separate people have walked into {place} and come out somewhere they should not have.",
truth: "The location is filed twice and the two filings are drifting apart.",
signs: ["the same building on two streets", "post that arrives before it is sent",
"a staircase with a landing nobody built", "two sets of church bells, half a beat apart"],
evidence: ["an Ordnance Survey sheet that disagrees with itself", "a key that opens the same door twice differently",
"a bus timetable for a route that runs one way only"],
containment: "It is on a public right of way.",
closes: "Decide which filing is the true one and make the other untenable.",
fits: { resolutions: ["close", "map", "handover"], gaps: ["worse", "already_contained", "wrong_thing", "agency_knew", "clock_running", "local_stake"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near", "deep", "hollow", "wrong"],
placeKinds: ["structure", "settled", "remote"] } },
{ id: "missing_team", name: "A team that did not come back", threat: "agency",
brief: "A Custodian team crossed to {place} on a routine survey {duration} ago and their beacon is still transmitting.",
truth: "Three of the four are still alive. The fourth is what the beacon is transmitting.",
signs: ["the beacon repeats a fourteen-second interval", "their kit is stacked, not scattered",
"somebody has been eating", "one set of boots, walking out and not back"],
evidence: ["a case log with the last four pages torn out", "a regulator with its cell removed and pocketed",
"four sleeping bags, three used"],
containment: "The agency will not say the word 'rescue' in writing.",
closes: "Bring back what is left, and the log. The agency wants the log.",
fits: { resolutions: ["first_team", "close", "map"], gaps: ["worse", "not_hostile", "wrong_thing", "one_of_them", "local_stake", "already_over", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "quiet_neighbours", "duplicate", "revenant"], classes: ["near", "deep", "hollow", "wrong"],
placeKinds: ["remote", "structure", "settled"] } },
{ id: "recurrence", name: "A day that will not file", threat: "structural",
brief: "{place} has recorded the same eleven minutes four times in {duration}.",
truth: "Something is being rehearsed, and each attempt gets further before it stops.",
signs: ["a till roll printing the same sale", "birds landing in identical formation",
"one witness who remembers all four", "a clock that gains exactly eleven minutes"],
evidence: ["four identical CCTV files with different timestamps", "a shopping receipt for a future date",
"a witness statement written in the witness's hand before they gave it"],
containment: "A local paper has run it as a curiosity.",
closes: "Find out what the rehearsal is FOR. Stopping the loop without knowing is the mistake.",
fits: { resolutions: ["close", "map", "arrange"], gaps: ["worse", "already_contained", "wrong_thing", "agency_knew", "clock_running", "already_over", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near", "deep", "wrong"],
placeKinds: ["settled", "structure"] } },
{ id: "quiet_ledger", name: "A record that disagrees", threat: "agency",
brief: "The parish records for {place} list a family that no living person remembers.",
truth: "They were removed from the world and not from the paperwork. Something is using the gap.",
signs: ["a headstone with no plot", "a house number between two others",
"a school photograph with a gap in the row", "an electoral roll nobody will amend"],
evidence: ["a deed of sale signed by a person who does not exist", "a ration book, unused, correctly issued",
"a christening record in fresh ink on old paper"],
containment: "An amateur historian has published.",
closes: "Restore the record or complete the removal. The agency has a preference and will not state it.",
fits: { resolutions: ["restore", "arrange", "map"], gaps: ["worse", "agency_knew", "local_stake", "one_of_them", "already_over", "the_brief_is_bait", "wrong_thing"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near"],
placeKinds: ["records"] } },
{ id: "inheritance", name: "Something left to somebody", threat: "material",
brief: "A property at {place} has passed to an heir along with a clause the solicitors cannot explain.",
truth: "The clause is a containment protocol. The family have been Custodians without the word for four generations.",
signs: ["a room that is cleaned but not used", "a standing order to a bank that closed",
"instructions in three hands, the earliest in Latin", "a key held by the solicitor, not the family"],
evidence: ["a household ledger with one line per decade", "a photograph of a previous agency team, uncaptioned",
"a will that names the agency without naming it"],
containment: "The heir intends to sell.",
closes: "Take up the protocol, or persuade the heir to. There is no third answer that holds.",
fits: { resolutions: ["handover", "arrange", "custody"], gaps: ["not_hostile", "wrong_thing", "one_of_them", "local_stake", "clock_running", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near"],
placeKinds: ["settled", "structure"] } },
{ id: "wrong_weather", name: "Weather that is not from here", threat: "structural",
brief: "{place} has had {duration} of conditions that do not appear in any regional record.",
truth: "It is not weather. It is the other side's weather, arriving through a gap too small to walk through.",
signs: ["snow that does not melt on skin", "rain that falls upward at the edges",
"a wind with a direction but no source", "pollen from nothing that grows here"],
evidence: ["a sample that is not water", "a barometer reading below the survivable",
"livestock refusing one specific field"],
containment: "The Met Office has asked a question in writing.",
closes: "Find the gap. It is smaller than a letterbox and it is somewhere absurd.",
fits: { resolutions: ["close", "map", "recover"], gaps: ["worse", "already_contained", "wrong_thing", "agency_knew", "clock_running", "the_brief_is_bait"],
adversaries: ["cleaners", "rival_desk", "locals", "quiet_neighbours"], classes: ["near", "deep", "hollow", "wrong"],
placeKinds: ["remote", "structure", "settled"] } }
];
/* ============================================================
* WHERE AND WHEN
* ============================================================ */
export const CLASSES = [
{ id: "near", name: "Near", weight: 5, modifier: 0, era: "modern",
note: "Almost this. Recognisable, survivable, dangerous because you relax.",
texture: ["the shops are the wrong chain", "the coins are right and the queen is not",
"everyone is very slightly better dressed", "a war ended a year late"] },
{ id: "deep", name: "Deep", weight: 2, modifier: -10, era: "future",
note: "The error resolved a long way out. The biology went another way, or the physics did.",
texture: ["the trees are wrong at the root", "there are no birds and something else does that job",
"the light arrives a moment after the sun moves", "the buildings are grown"] },
{ id: "hollow", name: "Hollow", weight: 2, modifier: -10, era: "antique",
note: "Nobody home. Something used to be.",
texture: ["the tables are laid", "a fire that has been out for a long time and is warm",
"no bodies anywhere", "every door open, every gate shut"] },
{ id: "wrong", name: "Wrong", weight: 1, modifier: -20, era: "anomalous",
note: "It does not behave, and the technique does not work cleanly there. Not given to probationers.",
texture: ["distances do not agree between two people", "the dead speak but only in the plural",
"your own handwriting is unfamiliar", "there is a sound that instruments do not record"] }
];
/**
* Places, tagged. `settled` means there are people here to talk to; `records` means
* there is an institution with paperwork; `structure` means there is a building to
* be inside. An anomaly that needs a parish register cannot happen in a forestry
* plantation, and one about a man attending work cannot happen on a canal bank.
*/
export const PLACES = [
{ text: "a stretch of coast outside Ilfracombe", kinds: ["remote"] },
{ text: "a flooded quarry above Nenthead", kinds: ["remote"] },
{ text: "the back of a working reservoir in Powys", kinds: ["remote", "structure"] },
{ text: "a decommissioned filter house on the Lea", kinds: ["structure"] },
{ text: "a farm at the end of a private road in Angus", kinds: ["settled", "structure"] },
{ text: "a pier that closed in 1974", kinds: ["structure"] },
{ text: "sixty acres of managed forestry near Kielder", kinds: ["remote"] },
{ text: "a chalk pit outside Ashford", kinds: ["remote"] },
{ text: "the top four floors of a car park in Coventry", kinds: ["structure", "settled"] },
{ text: "a tidal island off the Solway", kinds: ["remote"] },
{ text: "an aerodrome that appears on one map", kinds: ["structure", "records"] },
{ text: "a village with two war memorials", kinds: ["settled", "records", "structure"] },
{ text: "a Victorian pumping station under a retail park", kinds: ["structure"] },
{ text: "a stretch of canal with no towpath", kinds: ["remote"] },
{ text: "a boarding school out of term", kinds: ["structure", "records"] },
{ text: "a caravan site closed for the winter", kinds: ["structure"] },
{ text: "a market town with a working parish office", kinds: ["settled", "records", "structure"] },
{ text: "a colliery village three streets wide", kinds: ["settled", "structure"] },
{ text: "a county records office over a bank", kinds: ["settled", "records", "structure"] },
{ text: "a hospital annexe awaiting demolition", kinds: ["structure", "records"] }
];
/**
* Split, because the brief says "has been open for {duration}" and "since the
* spring" cannot follow "for". Spans are countable; vague dates are not, and only
* spans may be substituted into a slot that expects a length of time.
*/
export const DURATION_SPANS = ["four days", "eleven days", "three weeks", "nineteen hours",
"two months", "six days", "a fortnight"];
export const DURATION_VAGUE = ["the spring", "a date nobody will confirm", "the last hard frost"];
/* ============================================================
* THE GAP — the specific way the brief is wrong
* ============================================================ */
export const GAPS = [
{ id: "worse", name: "It is worse than briefed",
text: "The scale in the brief is out by an order of magnitude, and the brief was written before the last two reports came in.",
reveal: "This phase makes the true scale unmissable." },
{ id: "already_contained", name: "Somebody has already contained it",
text: "Somebody got there first and did the job badly. Their solution is holding, and it is the reason things are getting worse.",
reveal: "The team find the earlier fix before they find the anomaly." },
{ id: "not_hostile", name: "It is not hostile",
text: "The thing the brief calls a threat is frightened, and it is frightened of something the brief does not mention.",
reveal: "It runs from them at the first opportunity, in a direction that means something." },
{ id: "wrong_thing", name: "The brief describes the symptom",
text: "What they were sent for is a side effect. The cause is elsewhere in the same location and has not been noticed.",
reveal: "Dealing with the briefed problem makes the real one worse, visibly, within the hour." },
{ id: "agency_knew", name: "The agency knew",
text: "This is not the first team. The file has been edited, and the edit is visible if anybody reads the page numbers.",
reveal: "A previous team's kit, or their handwriting, somewhere it should not be." },
{ id: "local_stake", name: "Somebody local wants it",
text: "A person or group on the ground is protecting the anomaly, because it is the only thing keeping something else true.",
reveal: "The first helpful local turns out to have been steering them all day." },
{ id: "clock_running", name: "There is a deadline nobody mentioned",
text: "Something happens on a fixed date, and the brief was written before anyone worked out what.",
reveal: "A countdown that is legible once you know what you are looking at." },
{ id: "one_of_them", name: "It concerns one of them",
text: "One agent has a connection to this place or these people that the assigning officer either missed or did not miss.",
reveal: "Somebody uses that agent's name before it is given." },
{ id: "already_over", name: "It has already happened",
text: "The event the brief is trying to prevent took place before the team crossed. They are inside the aftermath and have not realised.",
reveal: "A date, written somewhere ordinary, that is in the past." },
{ id: "the_brief_is_bait", name: "The brief is bait",
text: "Somebody wanted a Custodian team at this location on this date, and the case file is how they arranged it.",
reveal: "Everything has been made slightly too easy up to the point where it is not." }
];
/* ============================================================
* WHO IS IN THE WAY — and the people, with names
* ============================================================ */
export const ADVERSARIES = [
{ id: "cleaners", name: "Contract cleaners", tier: "armed", npc: "Cleaner (contractor)", count: [2, 4],
motive: "Somebody is paying them to make sure nothing is recovered intact.",
tactics: "Professional, unenthusiastic, willing to withdraw. They will not die for it.",
leverage: "They can be bought, or convinced the invoice will not be honoured." },
{ id: "rival_desk", name: "A rival desk", tier: "dangerous", npc: "Cleaner (marksman)", count: [2, 3],
motive: "Another part of the agency wants this closed before your desk writes it up.",
tactics: "Correct paperwork, better equipment, no interest in a fight they can win by arriving first.",
leverage: "Clearance. Show them a higher authorisation and they must comply." },
{ id: "revenant", name: "A revenant", tier: "anomalous", npc: "Revenant", count: [1, 1],
motive: "It is looking for the person whose shape it is wearing.",
tactics: "It will pass anyone it does not recognise to reach someone it does.",
leverage: "Tell it where that person is. It will go, and that becomes somebody's problem." },
{ id: "locals", name: "People who live here", tier: "trained", npc: "The Keepers (waystation)", count: [3, 6],
motive: "They have made an arrangement with the thing and the arrangement is working.",
tactics: "Obstruction, misdirection, and eventually a shotgun. They are not evil and they are not wrong.",
leverage: "Offer them a better arrangement. The agency can, and hates to." },
{ id: "quiet_neighbours", name: "The quiet neighbours", tier: "armed", npc: "The Quiet Neighbours", count: [2, 5],
motive: "They were misfiled here generations ago and this is their last stable ground.",
tactics: "They watch. They negotiate. They do not bluff.",
leverage: "Somewhere else to be. It has to be real and it has to be shown." },
{ id: "duplicate", name: "A duplicate of an agent", tier: "dangerous", npc: "Duplicate (of an agent)", count: [1, 1],
motive: "It has the same brief, the same training, and one more crossing of experience.",
tactics: "It knows what the team will do because it is what it would do.",
leverage: "It wants to be the one who goes home. Offer it that and mean it." }
];
export const NAMES_FIRST = ["Marianne", "Errol", "Bess", "Hadley", "Ivo", "Constance", "Peter",
"Ada", "Roland", "Nell", "Gwilym", "Ottoline", "Sam", "Ruth", "Alec"];
export const NAMES_LAST = ["Trewin", "Kettle", "Ashgrove", "Bardsley", "Vane", "Pyle", "Mottram",
"Sallow", "Quiller", "Nye", "Harker", "Dunmore", "Speed", "Ferriby"];
export const NPC_ROLES = [
{ role: "the one who called it in", wants: "to be told they were right to call",
lies: "about how long they have known" },
{ role: "the person who owns the ground", wants: "the team gone before the weekend",
lies: "about who else has been here" },
{ role: "the local officer", wants: "a file they can close",
lies: "about the first report, which they lost" },
{ role: "the one who has seen it", wants: "somebody to say the word out loud",
lies: "about being alone when it happened" },
{ role: "the one profiting", wants: "the arrangement to continue",
lies: "about everything, fluently and well" },
{ role: "the child who is not frightened", wants: "to show somebody",
lies: "not at all, which is the problem" },
{ role: "the previous team's survivor", wants: "not to be taken back",
lies: "about what the fourth one did" }
];
/* ============================================================
* PHASES — the shape of the approach
* ============================================================ */
export const PHASE_KINDS = [
{ id: "approach", name: "Approach", verb: "getting there unobserved",
terrains: ["silted river flats", "a disused railway cutting", "overgrown allotments",
"a cliff path above the cove", "three miles of forestry track", "a flooded underpass"] },
{ id: "cordon", name: "The cordon", verb: "getting past whoever is already here",
terrains: ["a police outer cordon", "a farm gate with a man on it", "a coastguard station",
"a private road with cameras", "a village that has stopped talking to strangers"] },
{ id: "search", name: "The search", verb: "finding the thing itself",
terrains: ["forty rooms and a cellar", "two miles of foreshore at low water", "a plantation on a grid",
"a records office", "a caravan site in the dark"] },
{ id: "interior", name: "Inside", verb: "the place where the rules are different",
terrains: ["a pumping station below the water table", "the fourth floor and above",
"a chalk gallery", "the far side of the threshold", "a house with the wrong number of rooms"] },
{ id: "containment", name: "Containment", verb: "doing the thing they were sent to do",
terrains: ["the room it is in", "the doorway it keeps returning to", "a kitchen table and four chairs",
"the threshold itself", "a records room with the lights off", "wherever the decision has to be made"] },
{ id: "extraction", name: "Extraction", verb: "getting out with it",
terrains: ["back the way they came, in daylight", "a tidal causeway with ninety minutes",
"an open field and a mile of it", "the only road, now watched"] }
];
/* ============================================================
* WHAT MAKES IT HARD, AND WHAT GETS WORSE
* ============================================================ */
export const COMPLICATIONS = [
"The window is nineteen hours and the agency has not said what happens at twenty.",
"The team's regulator is inert. Nobody on the team knows how it starts.",
"The cover identity will not survive a document check, and there is a police presence.",
"One phase must be crossed twice, and it is worse the second time.",
"The only route in is public, in daylight, in front of people.",
"Local law enforcement has opened a file and is looking for the team.",
"There is a child involved and the containment answer does not account for children.",
"Comms do not work across. Whatever the team decides, they decide alone.",
"One of the team is recognised on arrival, by name, by somebody they have never met.",
"The weather closes the only route out four hours after they arrive."
];
export const CLOCKS = [
{ id: "public", name: "The public find out", costs: "Containment",
steps: ["a photograph is posted", "a local reporter arrives", "it is on the national news"] },
{ id: "spread", name: "It gets bigger", costs: "Containment",
steps: ["a second sighting", "a second location", "it is no longer one incident"] },
{ id: "deniable", name: "The desk backs away", costs: "Clearance",
steps: ["the liaison stops answering", "the authorisation is withdrawn", "the case becomes deniable"] },
{ id: "window", name: "The window closes", costs: "the way home",
steps: ["the margin narrows", "the return is Difficult", "the return is aimed at −30%"] },
{ id: "attention", name: "It notices them", costs: "everything",
steps: ["it knows somebody is here", "it knows how many", "it knows their names"] }
];
/* ============================================================
* WHAT IT TAKES OUT OF THEM, AND WHAT THEY GET
* ============================================================ */
export const COHERENCE_HOOKS = [
"Transposed materiel is the only thing that will open the inner door.",
"There is a paradox in plain sight and somebody will look at it.",
"The local law of reality is different in one specific, tempting way.",
"A duplicate of a team member is present and can be spoken to.",
"The fastest way through is to break something that is true here.",
"The window will be overstayed. It is only a question of by how long.",
"Somebody will have to go back through a second time.",
"There is a body that has to be carried, and it is one of ours."
];
/**
* Objectives. Given ids because an anomaly must be able to say which of these it
* can actually support: ordering a team to "recover the object" when the anomaly is
* a duplicated person is the single worst thing this generator did.
*/
export const RESOLUTIONS = [
{ id: "close", text: "Close it, and leave the public a version of events they can live with." },
{ id: "recover", text: "Recover the object and let the location resolve itself. It will, in about a year." },
{ id: "custody", text: "Take the person into custody. Custody, in the oldest sense the agency uses the word." },
{ id: "first_team",text: "Establish what happened to the first team and bring back whatever is left of them." },
{ id: "arrange", text: "Make an arrangement, file it, and never speak of the terms again." },
{ id: "map", text: "Nothing can be closed. Map it, mark it, and get out with the map." },
{ id: "handover", text: "Persuade somebody local to take on the job permanently, and never check on them again." },
{ id: "restore", text: "Put the record back the way it was, and make sure it stays put." }
];
export const DEBRIEF_QUESTIONS = [
"Who else knows, and can they be relied upon to be wrong about it?",
"Was anything brought back that was not on the manifest?",
"At what point did the team stop following the brief, and why?",
"Is the location safe to re-enter, and would you?",
"Which of you was alone, and for how long?",
"The file will say this was routine. Do you object to that?"
];
export const FAILURE_STATES = [
"The anomaly is still there and the agency has to send somebody else, which is a sentence in a file with names in it.",
"It is public. Containment becomes a press office problem and the desk remembers whose case it was.",
"It followed somebody home. That is next season's case and it has a Custodian's name on it.",
"The arrangement the locals had is broken and nothing has replaced it.",
"The team came back. Not all of them came back as themselves."
];
/* ============================================================
* GROUPS — so a generated case can reach the contact system
*
* The engine has a full first-contact pillar: three stages, standings, taboos
* learned in play, desires, a correct offer form, and rewards that the phase
* resolver already reads and applies. A generated case shipped with no group item
* at all, so none of it could ever fire. These are the people already on the
* ground when the team arrives.
* ============================================================ */
export const GROUPS = [
{ id: "parish", name: "The parish", population: 400,
description: "Four hundred people who have lived with it long enough to have manners about it.",
taboo: { id: "no_naming", text: "It is never named aloud indoors. Say what it is under a roof and the room empties.", penaltyIfViolated: -30 },
desire: { id: "burial", text: "A body returned. There is one they have never been able to bury.", offerBonusStep: "easy" },
offerForm: "A name, a date, and somewhere to put a stone — delivered outdoors." },
{ id: "keepers", name: "The keepers of the ground", population: 12,
description: "A dozen of them, and an arrangement none of them will describe to an outsider.",
taboo: { id: "no_photographs", text: "Nothing is photographed. A raised camera ends the conversation and starts a different one.", penaltyIfViolated: -30 },
desire: { id: "relief", text: "To be relieved. Somebody, anybody, official enough to take the duty on.", offerBonusStep: "easy" },
offerForm: "A written undertaking on agency paper, signed, with a date on it." },
{ id: "displaced", name: "The ones who came through", population: 30,
description: "Misfiled here generations ago. This is their last stable ground and they know exactly how stable it is.",
taboo: { id: "ground_speech", text: "They will not converse with anyone standing on the ground. Address them from a stair, a wall, a vehicle roof.", penaltyIfViolated: -30 },
desire: { id: "medicine", text: "Antibiotics. Theirs ran out two generations ago.", offerBonusStep: "easy" },
offerForm: "A sealed course of antibiotics, handed over from above ground level." },
{ id: "staff", name: "What is left of the staff", population: 6,
description: "Six people still turning up to a job at a site that closed. Nobody has told them to stop.",
taboo: { id: "no_authority", text: "Do not produce a warrant or an order. The last people who did took someone away.", penaltyIfViolated: -30 },
desire: { id: "wages", text: "To be paid. Eleven months, and they have kept the log the whole time.", offerBonusStep: "easy" },
offerForm: "Money, in cash, counted in front of them, and a receipt they can keep." }
];