Add the same ten before the department found them

Ten civilian counterparts, one per roster agent: same person, same
characteristics, same face, no induction and nothing ever issued to them. A
player can hold the two sheets side by side and read exactly what the job did.

Keyed off ROSTER rather than written out again, so the pair cannot drift apart.
The five who joined straight into a posting get the trade they came out of, and
between them the ten now cover all ten trades exactly once — which makes this
set a demonstration of the whole trade half of character creation as well as a
set of characters.

expandFromRegister grows a `civilian` mode: no induction skills, no standing
issue, no tier issue, no trade cant, and nothing anomalous or Coherence-costing.

Three things the build guard caught, each of which would have shipped:

  - Holloway was "carrying issued kit: notebook". Wrong: an interpreter owned a
    notebook long before anyone issued her one. The rule is now kit that could
    ONLY have come from the department.
  - Same again for skills — the linguist's core list contains trade cant,
    because it is a language and she is a linguist.
  - Wren Finch was carrying the black mirror, which is era "anomalous", from a
    transposed tradition, and costs Coherence to look into twice. That is
    something you acquire after the night the department notices you, not
    before. It is the only anomalous item in any trade kit.

The guard runs over the built objects rather than trusting the flag that made
them, because the flag is three call-frames away from the result.

Verified in Foundry: 24 pregens, the before sheets show the civilian job in
place of rank, no armour, nothing readied but fists, and the right face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-08-12 16:19:50 +01:00
co-authored by Claude Opus 5
parent 4101221eb1
commit 26ad00174f
40 changed files with 279 additions and 101 deletions
+1 -1
View File
@@ -183,6 +183,6 @@ The build runs all three guards before it writes anything:
check-rules: OK — 87 rules, 18 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified
check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct
check-lang: OK — en.json, 927 keys, no leaf/branch collisions
check-behaviour: OK — 41 behavioural tests
check-behaviour: OK — 42 behavioural tests
```
<!-- guards:end -->
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.335240 17275b000 Recovering log #3
2026/08/12-11:03:53.335622 17275b000 Level-0 table #5: started
2026/08/12-11:03:53.335876 17275b000 Level-0 table #5: 4591 bytes OK
2026/08/12-11:03:53.336211 17275b000 Delete type=0 #3
2026/08/12-11:03:53.336262 17275b000 Delete type=3 #2
2026/08/12-16:18:56.886898 171b6b000 Recovering log #3
2026/08/12-16:18:56.887034 171b6b000 Level-0 table #5: started
2026/08/12-16:18:56.887151 171b6b000 Level-0 table #5: 4591 bytes OK
2026/08/12-16:18:56.887345 171b6b000 Delete type=0 #3
2026/08/12-16:18:56.887367 171b6b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.559352 16fe23000 Delete type=3 #1
2026/08/12-16:18:25.436599 1717c7000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.359869 171f4f000 Recovering log #3
2026/08/12-11:03:53.360351 171f4f000 Level-0 table #5: started
2026/08/12-11:03:53.360577 171f4f000 Level-0 table #5: 4129 bytes OK
2026/08/12-11:03:53.360909 171f4f000 Delete type=0 #3
2026/08/12-11:03:53.360948 171f4f000 Delete type=3 #2
2026/08/12-16:18:56.902582 172377000 Recovering log #3
2026/08/12-16:18:56.902909 172377000 Level-0 table #5: started
2026/08/12-16:18:56.903051 172377000 Level-0 table #5: 4129 bytes OK
2026/08/12-16:18:56.903282 172377000 Delete type=0 #3
2026/08/12-16:18:56.903309 172377000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.616347 16f617000 Delete type=3 #1
2026/08/12-16:18:25.488321 1717c7000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.337242 171f4f000 Recovering log #3
2026/08/12-11:03:53.337820 171f4f000 Level-0 table #5: started
2026/08/12-11:03:53.338200 171f4f000 Level-0 table #5: 31614 bytes OK
2026/08/12-11:03:53.338535 171f4f000 Delete type=0 #3
2026/08/12-11:03:53.338582 171f4f000 Delete type=3 #2
2026/08/12-16:18:56.888063 172b83000 Recovering log #3
2026/08/12-16:18:56.888800 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.889116 172b83000 Level-0 table #5: 31614 bytes OK
2026/08/12-16:18:56.889365 172b83000 Delete type=0 #3
2026/08/12-16:18:56.889399 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.561859 16ee0b000 Delete type=3 #1
2026/08/12-16:18:25.438634 1717c7000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.319350 171f4f000 Recovering log #3
2026/08/12-11:03:53.321070 171f4f000 Level-0 table #5: started
2026/08/12-11:03:53.322779 171f4f000 Level-0 table #5: 225945 bytes OK
2026/08/12-11:03:53.323152 171f4f000 Delete type=0 #3
2026/08/12-11:03:53.323422 171f4f000 Delete type=3 #2
2026/08/12-16:18:56.874850 172b83000 Recovering log #3
2026/08/12-16:18:56.876605 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.877996 172b83000 Level-0 table #5: 225945 bytes OK
2026/08/12-16:18:56.878250 172b83000 Delete type=0 #3
2026/08/12-16:18:56.878288 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.588411 16f617000 Delete type=3 #1
2026/08/12-16:18:25.464794 171fd3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.331118 171f4f000 Recovering log #3
2026/08/12-11:03:53.331736 171f4f000 Level-0 table #5: started
2026/08/12-11:03:53.331902 171f4f000 Level-0 table #5: 3295 bytes OK
2026/08/12-11:03:53.332213 171f4f000 Delete type=0 #3
2026/08/12-11:03:53.332254 171f4f000 Delete type=3 #2
2026/08/12-16:18:56.883808 171b6b000 Recovering log #3
2026/08/12-16:18:56.883933 171b6b000 Level-0 table #5: started
2026/08/12-16:18:56.884050 171b6b000 Level-0 table #5: 3295 bytes OK
2026/08/12-16:18:56.884293 171b6b000 Delete type=0 #3
2026/08/12-16:18:56.884326 171b6b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.609842 16ee0b000 Delete type=3 #1
2026/08/12-16:18:25.483235 171fd3000 Delete type=3 #1
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000004
MANIFEST-000007
+3 -5
View File
@@ -1,5 +1,3 @@
2026/08/12-11:03:53.311385 17275b000 Recovering log #3
2026/08/12-11:03:53.313147 17275b000 Level-0 table #5: started
2026/08/12-11:03:53.314996 17275b000 Level-0 table #5: 234094 bytes OK
2026/08/12-11:03:53.315506 17275b000 Delete type=0 #3
2026/08/12-11:03:53.315692 17275b000 Delete type=3 #2
2026/08/12-16:18:56.870778 172b83000 Recovering log #6
2026/08/12-16:18:56.871068 172b83000 Delete type=0 #6
2026/08/12-16:18:56.871093 172b83000 Delete type=3 #4
+5 -1
View File
@@ -1 +1,5 @@
2026/08/12-11:01:46.574790 16ee0b000 Delete type=3 #1
2026/08/12-16:18:35.612370 16f80f000 Recovering log #3
2026/08/12-16:18:35.614035 16f80f000 Level-0 table #5: started
2026/08/12-16:18:35.616454 16f80f000 Level-0 table #5: 362249 bytes OK
2026/08/12-16:18:35.616802 16f80f000 Delete type=0 #3
2026/08/12-16:18:35.616841 16f80f000 Delete type=3 #2
Binary file not shown.
Binary file not shown.
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.341396 171f4f000 Recovering log #3
2026/08/12-11:03:53.341499 171f4f000 Level-0 table #5: started
2026/08/12-11:03:53.341782 171f4f000 Level-0 table #5: 21613 bytes OK
2026/08/12-11:03:53.342135 171f4f000 Delete type=0 #3
2026/08/12-11:03:53.342170 171f4f000 Delete type=3 #2
2026/08/12-16:18:56.891462 172b83000 Recovering log #3
2026/08/12-16:18:56.891878 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.892083 172b83000 Level-0 table #5: 21613 bytes OK
2026/08/12-16:18:56.892260 172b83000 Delete type=0 #3
2026/08/12-16:18:56.892281 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.606226 17062f000 Delete type=3 #1
2026/08/12-16:18:25.480345 171fd3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.343398 17275b000 Recovering log #3
2026/08/12-11:03:53.344128 17275b000 Level-0 table #5: started
2026/08/12-11:03:53.344828 17275b000 Level-0 table #5: 33484 bytes OK
2026/08/12-11:03:53.345178 17275b000 Delete type=0 #3
2026/08/12-11:03:53.345409 17275b000 Delete type=3 #2
2026/08/12-16:18:56.892919 172377000 Recovering log #3
2026/08/12-16:18:56.893384 172377000 Level-0 table #5: started
2026/08/12-16:18:56.893653 172377000 Level-0 table #5: 33484 bytes OK
2026/08/12-16:18:56.893911 172377000 Delete type=0 #3
2026/08/12-16:18:56.893937 172377000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.613896 16ee0b000 Delete type=3 #1
2026/08/12-16:18:25.486733 170fbb000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.326666 172f67000 Recovering log #3
2026/08/12-11:03:53.326865 172f67000 Level-0 table #5: started
2026/08/12-11:03:53.327127 172f67000 Level-0 table #5: 7590 bytes OK
2026/08/12-11:03:53.327438 172f67000 Delete type=0 #3
2026/08/12-11:03:53.327686 172f67000 Delete type=3 #2
2026/08/12-16:18:56.880643 172377000 Recovering log #3
2026/08/12-16:18:56.881084 172377000 Level-0 table #5: started
2026/08/12-16:18:56.881264 172377000 Level-0 table #5: 7590 bytes OK
2026/08/12-16:18:56.881656 172377000 Delete type=0 #3
2026/08/12-16:18:56.881712 172377000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.551765 16ee0b000 Delete type=3 #1
2026/08/12-16:18:25.431162 1707af000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.354941 172f67000 Recovering log #3
2026/08/12-11:03:53.355204 172f67000 Level-0 table #5: started
2026/08/12-11:03:53.355515 172f67000 Level-0 table #5: 16902 bytes OK
2026/08/12-11:03:53.355809 172f67000 Delete type=0 #3
2026/08/12-11:03:53.355952 172f67000 Delete type=3 #2
2026/08/12-16:18:56.899908 171b6b000 Recovering log #3
2026/08/12-16:18:56.900066 171b6b000 Level-0 table #5: started
2026/08/12-16:18:56.900268 171b6b000 Level-0 table #5: 16902 bytes OK
2026/08/12-16:18:56.900450 171b6b000 Delete type=0 #3
2026/08/12-16:18:56.900482 171b6b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.608270 16f617000 Delete type=3 #1
2026/08/12-16:18:25.482003 170fbb000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.349870 172f67000 Recovering log #3
2026/08/12-11:03:53.350807 172f67000 Level-0 table #5: started
2026/08/12-11:03:53.351355 172f67000 Level-0 table #5: 82299 bytes OK
2026/08/12-11:03:53.351750 172f67000 Delete type=0 #3
2026/08/12-11:03:53.352023 172f67000 Delete type=3 #2
2026/08/12-16:18:56.896117 171b6b000 Recovering log #3
2026/08/12-16:18:56.896990 171b6b000 Level-0 table #5: started
2026/08/12-16:18:56.897537 171b6b000 Level-0 table #5: 82299 bytes OK
2026/08/12-16:18:56.897727 171b6b000 Delete type=0 #3
2026/08/12-16:18:56.897756 171b6b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.600665 16f617000 Delete type=3 #1
2026/08/12-16:18:25.475315 170fbb000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.357157 17275b000 Recovering log #3
2026/08/12-11:03:53.357573 17275b000 Level-0 table #5: started
2026/08/12-11:03:53.358045 17275b000 Level-0 table #5: 3739 bytes OK
2026/08/12-11:03:53.358519 17275b000 Delete type=0 #3
2026/08/12-11:03:53.358556 17275b000 Delete type=3 #2
2026/08/12-16:18:56.901141 172b83000 Recovering log #3
2026/08/12-16:18:56.901259 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.901369 172b83000 Level-0 table #5: 3739 bytes OK
2026/08/12-16:18:56.901569 172b83000 Delete type=0 #3
2026/08/12-16:18:56.901596 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.593778 16fe23000 Delete type=3 #1
2026/08/12-16:18:25.469608 171fd3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.329230 173773000 Recovering log #3
2026/08/12-11:03:53.329412 173773000 Level-0 table #5: started
2026/08/12-11:03:53.329602 173773000 Level-0 table #5: 5770 bytes OK
2026/08/12-11:03:53.329896 173773000 Delete type=0 #3
2026/08/12-11:03:53.329940 173773000 Delete type=3 #2
2026/08/12-16:18:56.882581 172b83000 Recovering log #3
2026/08/12-16:18:56.882721 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.882874 172b83000 Level-0 table #5: 5770 bytes OK
2026/08/12-16:18:56.883168 172b83000 Delete type=0 #3
2026/08/12-16:18:56.883196 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.555278 17062f000 Delete type=3 #1
2026/08/12-16:18:25.433399 170fbb000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.339804 17275b000 Recovering log #3
2026/08/12-11:03:53.339950 17275b000 Level-0 table #5: started
2026/08/12-11:03:53.340109 17275b000 Level-0 table #5: 6939 bytes OK
2026/08/12-11:03:53.340345 17275b000 Delete type=0 #3
2026/08/12-11:03:53.340376 17275b000 Delete type=3 #2
2026/08/12-16:18:56.890146 171b6b000 Recovering log #3
2026/08/12-16:18:56.890265 171b6b000 Level-0 table #5: started
2026/08/12-16:18:56.890497 171b6b000 Level-0 table #5: 6939 bytes OK
2026/08/12-16:18:56.890691 171b6b000 Delete type=0 #3
2026/08/12-16:18:56.890715 171b6b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.564184 17062f000 Delete type=3 #1
2026/08/12-16:18:25.440779 1717c7000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-11:03:53.333070 173773000 Recovering log #3
2026/08/12-11:03:53.333335 173773000 Level-0 table #5: started
2026/08/12-11:03:53.333713 173773000 Level-0 table #5: 14767 bytes OK
2026/08/12-11:03:53.333997 173773000 Delete type=0 #3
2026/08/12-11:03:53.334140 173773000 Delete type=3 #2
2026/08/12-16:18:56.885054 172b83000 Recovering log #3
2026/08/12-16:18:56.885653 172b83000 Level-0 table #5: started
2026/08/12-16:18:56.885938 172b83000 Level-0 table #5: 14767 bytes OK
2026/08/12-16:18:56.886208 172b83000 Delete type=0 #3
2026/08/12-16:18:56.886245 172b83000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-11:01:46.557371 16fe23000 Delete type=3 #1
2026/08/12-16:18:25.435075 170fbb000 Delete type=3 #1
+71 -9
View File
@@ -181,11 +181,16 @@ function deviceItem(d) {
* Shared by the starter scenario and the duty roster, because two copies of this is
* two places for the induction rule to drift. Ratings are FIXED at the middle of each
* band rather than rolled: a pregen that varies between installs is not a pregen.
*
* `civilian` builds the person BEFORE the department found them: their trade and the
* tools of it, and nothing the agency ever handed them. No induction skills, no
* standing issue, no tier issue, no trade cant — that last one matters, because trade
* cant is the department's own argot and a civilian has never heard it spoken.
*/
const REG = await import("../postings.mjs");
const midBand = b => Math.round((b[0] + b[1]) / 2);
function expandFromRegister(spec, { where = "roster" } = {}) {
function expandFromRegister(spec, { where = "roster", civilian = false } = {}) {
const { ROLES, TRADES, INDUCTION, TRADE_BANDS, STANDING_ISSUE, TIER_ISSUE, TIERS } = REG;
const isTrade = !!spec.trade;
const R = isTrade ? TRADES[spec.trade] : ROLES[spec.role];
@@ -208,8 +213,8 @@ function expandFromRegister(spec, { where = "roster" } = {}) {
};
for (const k of R.core) addSkill(k, coreV);
for (const k of R.support) addSkill(k, supportV);
if (isTrade) for (const k of INDUCTION.skills) addSkill(k, inducV);
addSkill("language:trade_cant", 55);
if (isTrade && !civilian) for (const k of INDUCTION.skills) addSkill(k, inducV);
if (!civilian) addSkill("language:trade_cant", 55);
// Floors only ever RAISE a rating, so a posting already better stays better.
for (const [id, floor] of Object.entries(spec.floors ?? {})) {
const [fam, sp = ""] = id.split(":");
@@ -218,10 +223,19 @@ function expandFromRegister(spec, { where = "roster" } = {}) {
else { skills.push({ fam, spec: sp, val: floor }); seen.add(id); }
}
const kitKeys = [...STANDING_ISSUE, ...R.kit,
...(isTrade ? INDUCTION.kit : []),
...(spec.extraWeapons ?? []), ...(spec.extraGear ?? []),
...(TIER_ISSUE[tierKey] ?? [])];
// A civilian has never been near a crossing, so nothing anomalous and nothing that
// costs Coherence to use — the criminal's trade kit contains a black mirror, which is
// exactly the sort of thing you acquire AFTER the night the department noticed you.
const isAnomalous = k => {
const it = GEAR_LOOKUP.get(k) ?? WEAPON_LOOKUP.get(k) ?? ARMOUR_LOOKUP.get(k);
return !!it && (it.era === "anomalous" || !!it.coherence);
};
const kitKeys = civilian
? [...R.kit, ...(spec.extraWeapons ?? []), ...(spec.extraGear ?? [])].filter(k => !isAnomalous(k))
: [...STANDING_ISSUE, ...R.kit,
...(isTrade ? INDUCTION.kit : []),
...(spec.extraWeapons ?? []), ...(spec.extraGear ?? []),
...(TIER_ISSUE[tierKey] ?? [])];
const weapons = [], armour = [], gear = [];
for (const k of new Set(kitKeys)) {
if (WEAPON_LOOKUP.has(k)) weapons.push(k);
@@ -513,13 +527,61 @@ await writePack("armour", ARMOURS.map(a => {
// pregens — the six hand-written originals plus the ten-agent duty roster
{
const { ROSTER } = await import("./roster.mjs");
const { ROSTER, BEFORE } = await import("./roster.mjs");
const roster = ROSTER.map(spec => {
const e = expandFromRegister(spec, { where: "roster" });
return { ...e, bio: `<p><strong>${e.label}, ${spec.rank}.</strong> ${e.blurb}</p>${spec.bio}` };
});
// The same ten before the department found them. Same key stem, same face, same
// characteristics — a person's STR does not change when they sign the Act. What
// changes is everything the job gave them, which is the whole point of the pair.
const { TRADES } = REG;
const before = ROSTER.map(spec => {
const b = BEFORE[spec.key];
if (!b) throw new Error(`roster: ${spec.name} has no "before" entry`);
const civ = expandFromRegister(
{ ...spec, key: `${spec.key}_before`, name: `${spec.name} (before)`,
role: undefined, trade: b.trade, rank: b.job,
age: { chronological: b.age, physiological: b.age },
// One talent, taken from the trade's own list rather than invented, so a
// civilian never holds a knack the department taught them.
talents: TRADES[b.trade].talents.slice(0, 1) },
{ where: "roster:before", civilian: true });
return { ...civ, bio: `<p><strong>${b.job}.</strong> ${civ.blurb}</p>${b.bio}` };
});
// The whole value of this set is that it holds NOTHING the department gave them, and
// that is a property of a flag passed three call-frames away. Check it here, where
// the objects actually are, rather than trusting the flag.
const { STANDING_ISSUE, INDUCTION } = REG;
for (const c of before) {
const carried = [...c.weapons, ...c.armour, ...c.gear];
// An item the trade itself owns stays, even when the department also hands it out:
// an interpreter had a notebook long before anyone issued her one. Only kit that
// could ONLY have come from the department is a failure.
const ownKit = TRADES[c.trade].kit;
const issued = carried.filter(k => !ownKit.includes(k)
&& (STANDING_ISSUE.includes(k) || INDUCTION.kit.includes(k)));
if (issued.length) throw new Error(`${c.name} is carrying issued kit: ${issued.join(", ")}`);
const odd = carried.filter(k => {
const it = GEAR_LOOKUP.get(k) ?? WEAPON_LOOKUP.get(k) ?? ARMOUR_LOOKUP.get(k);
return !!it && (it.era === "anomalous" || !!it.coherence);
});
if (odd.length) throw new Error(`${c.name} is carrying something anomalous: ${odd.join(", ")}`);
// Same rule for skills: the linguist's core list contains trade cant, because it is
// a language and she is a linguist. Only a rating she could ONLY have been taught
// at the training centre is a failure.
const ownSkills = new Set([...TRADES[c.trade].core, ...TRADES[c.trade].support]);
const taught = c.skills.filter(s => {
const id = s.spec ? `${s.fam}:${s.spec}` : s.fam;
return !ownSkills.has(id) && (INDUCTION.skills.includes(id) || id === "language:trade_cant");
});
if (taught.length) throw new Error(`${c.name} knows induction skills: ${taught.map(s => s.spec ? `${s.fam}:${s.spec}` : s.fam).join(", ")}`);
}
const entries = [];
for (const p of [...PREGENS, ...roster]) {
for (const p of [...PREGENS, ...roster, ...before]) {
const { actorDoc, itemDocs } = buildActor(p, "character");
entries.push([`!actors!${actorDoc._id}`, actorDoc]);
for (const it of itemDocs) entries.push([`!actors.items!${actorDoc._id}.${it._id}`, it]);
+21
View File
@@ -312,6 +312,27 @@ test("every roster agent names a real posting or trade and has a portrait on dis
assert.equal(ROSTER.filter(a => a.trade).length, 5, "the roster should be half trades");
});
// The "before" set is only worth having if it is the SAME ten people, so it is keyed
// off the roster rather than written out again. The build proves the harder property —
// that a civilian carries nothing the department issued — because that is where the
// expanded objects exist; this proves the table they are built from.
test("every roster agent has a before-the-department entry, and the ten cover ten trades", async () => {
const { ROSTER, BEFORE } = await import("./roster.mjs");
const { TRADES } = await import("../postings.mjs");
const keys = new Set(ROSTER.map(a => a.key));
for (const a of ROSTER) {
const b = BEFORE[a.key];
assert.ok(b, `${a.name} has no "before" entry`);
assert.ok(TRADES[b.trade], `${a.name} was a "${b.trade}", which is not a trade`);
assert.ok(b.job && b.bio, `${a.name}: a before entry needs a job and a biography`);
assert.ok(b.age < a.age.chronological, `${a.name} is not younger before than after`);
}
for (const k of Object.keys(BEFORE)) assert.ok(keys.has(k), `${k} is a before with nobody after it`);
const trades = Object.values(BEFORE).map(b => b.trade);
assert.equal(new Set(trades).size, Object.keys(TRADES).length,
"the ten should cover all ten trades exactly once — that is what makes the set a demonstration");
});
/* ---------------------------------------------------------------- */
await runAll();
+93
View File
@@ -111,3 +111,96 @@ export const ROSTER = [
+ "finished. Was consulted once and never quite sent home.</p>"
+ "<p><em>Anchor:</em> a photograph of a lecture theatre, taken from the back.</p>" }
];
/**
* THE SAME TEN, BEFORE THE DEPARTMENT FOUND THEM.
*
* Same person, same characteristics, same face — a civilian who has not been inducted
* and has never been issued anything. Skills and tools come from the trade alone, so a
* player can hold the two sheets side by side and see exactly what the job did to them.
* (The linguist is the one who already speaks trade cant before she arrives. It is a
* language, and she is a linguist.)
*
* Five of the ten went into the department straight into a posting rather than out of a
* trade, so this is where they came from. Between them the ten cover all ten trades
* exactly once, which means this set also shows a table the whole trade half of
* character creation without anybody having to read the rules for it.
*
* `job` replaces the rank line on the sheet. The second paragraph of each biography is
* the seam: what they did that got them noticed. It is the hook a GM pulls on.
*/
export const BEFORE = {
pc_holloway: { trade: "linguist", job: "Court Interpreter", age: 25,
bio: "<p>Interpreting at magistrates' courts and immigration tribunals. Ten hours a "
+ "day saying other people's worst sentences in the first person, and going home "
+ "able to repeat every one of them.</p>"
+ "<p><em>How they found her:</em> she interpreted a statement nobody had asked "
+ "her to hear, and then wrote down what the room had done while it was said.</p>" },
pc_okonkwo: { trade: "builder", job: "Steel Erector", age: 28,
bio: "<p>Ten storeys up on a frame with no floor in it yet, in weather that sends "
+ "everyone else down the ladder. Never late, never sick, and never the one who "
+ "says the job cannot be done.</p>"
+ "<p><em>How they found him:</em> he carried two men off a site that had gone "
+ "wrong in a way the report could not describe, and he did not ask a single "
+ "question about it.</p>" },
pc_sandoval: { trade: "it", job: "Network Field Engineer", age: 31,
bio: "<p>Alone at the top of a mast in the dark with a laptop and a flask, fixing "
+ "links for towns that only notice the network when it stops. Long climbs, long "
+ "views, long silences.</p>"
+ "<p><em>How they found her:</em> a relay logged forty minutes of traffic from a "
+ "site decommissioned in 1994, and she kept the file instead of clearing it.</p>" },
pc_bhattacharya: { trade: "scientist", job: "Research Chemist", age: 26,
bio: "<p>An industrial laboratory, a bench, and a notebook filled in the same hand "
+ "every day for four years. Slow, exact, and constitutionally unable to leave a "
+ "discrepancy alone.</p>"
+ "<p><em>How they found him:</em> he noticed a batch record amended by somebody "
+ "who had never worked there, and he raised it in writing. Twice.</p>" },
pc_rahimi: { trade: "entertainer", job: "Local Radio Presenter", age: 33,
bio: "<p>Mid-morning phone-in on a station nobody outside the county has heard of. "
+ "Six years of listening to people who rang up angry and hung up calmer.</p>"
+ "<p><em>How they found her:</em> she talked a caller down from something on air "
+ "for eleven minutes, and the recording of those minutes does not play back the "
+ "same way twice.</p>" },
pc_nkemdirim: { trade: "doctor", job: "Registrar, Emergency Medicine", age: 31,
bio: "<p>Six years of A&amp;E and the flat calm that comes with it. The one the "
+ "department turns to when nobody else will say the decision out loud.</p>"
+ "<p><em>How they found them:</em> a patient came in with an injury that had "
+ "healed in the wrong direction, and the notes they wrote were too good to leave "
+ "in the system.</p>" },
pc_kowalczyk: { trade: "engineer", job: "Structural Engineer", age: 29,
bio: "<p>Chartered, practical, and happiest with a survey and a set of drawings that "
+ "disagree. Asks what a thing is made of before he asks what it is for.</p>"
+ "<p><em>How they found him:</em> he condemned a footbridge on load figures that "
+ "were correct, for a span he had measured three times and got three "
+ "answers.</p>" },
pc_finch: { trade: "criminal", job: "Between Things", age: 25,
bio: "<p>Not organised, and not caught either — doors, locks and other people's "
+ "assumptions, worked quietly and left tidy. Rents by the week and pays cash.</p>"
+ "<p><em>How they found her:</em> she was inside a building she had no business "
+ "in on the night it stopped being on the street it was on, and she was the only "
+ "witness who could describe the inside.</p>" },
pc_agyeman: { trade: "police", job: "Detective Sergeant", age: 50,
bio: "<p>Twenty-six years on division. Takes a statement properly, keeps a scene, "
+ "and has watched three generations of the same families come through the same "
+ "door.</p>"
+ "<p><em>How they found him:</em> a file with no crime number landed on his desk "
+ "and he worked it anyway, on his own time, and got further than he was meant "
+ "to.</p>" },
pc_lindqvist: { trade: "professor", job: "Professor of Comparative Folklore", age: 59,
bio: "<p>A chair, a reading list, and forty years of stories about places that take "
+ "people and give them back wrong. Treats them as evidence rather than as "
+ "literature.</p>"
+ "<p><em>How they found him:</em> he was consulted once, as an academic "
+ "courtesy, and gave an answer so specific that he was never quite sent "
+ "home.</p>" }
};