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:
co-authored by
Claude Opus 5
parent
4101221eb1
commit
26ad00174f
@@ -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
@@ -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 @@
|
||||
2026/08/12-11:01:46.559352 16fe23000 Delete type=3 #1
|
||||
2026/08/12-16:18:25.436599 1717c7000 Delete type=3 #1
|
||||
|
||||
@@ -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 @@
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 @@
|
||||
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 @@
|
||||
MANIFEST-000004
|
||||
MANIFEST-000007
|
||||
|
||||
+3
-5
@@ -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
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
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
@@ -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
@@ -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
@@ -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 @@
|
||||
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
@@ -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
@@ -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
@@ -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 @@
|
||||
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
@@ -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 @@
|
||||
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
@@ -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 @@
|
||||
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
@@ -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 @@
|
||||
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
@@ -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 @@
|
||||
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
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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&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>" }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user