diff --git a/README.md b/README.md index 86cdb39..c66749f 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/packs/armour/LOG b/packs/armour/LOG index f203490..54ff43b 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -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 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old index a5fa76f..1a95b9d 100644 --- a/packs/armour/LOG.old +++ b/packs/armour/LOG.old @@ -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 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 10fe401..63b7750 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -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 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old index 545259a..0572678 100644 --- a/packs/expedition/LOG.old +++ b/packs/expedition/LOG.old @@ -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 diff --git a/packs/gear/LOG b/packs/gear/LOG index 1b2dbec..f0f3e4c 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -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 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old index b74605f..2f8e46c 100644 --- a/packs/gear/LOG.old +++ b/packs/gear/LOG.old @@ -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 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index af7da0a..3662260 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -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 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old index c281e28..87539c8 100644 --- a/packs/npcs/LOG.old +++ b/packs/npcs/LOG.old @@ -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 diff --git a/packs/powers/LOG b/packs/powers/LOG index 53ebaa9..cc88f37 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -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 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old index ad0408a..59854ca 100644 --- a/packs/powers/LOG.old +++ b/packs/powers/LOG.old @@ -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 diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb index 1d86f82..b9effac 100644 Binary files a/packs/pregens/000005.ldb and b/packs/pregens/000005.ldb differ diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log deleted file mode 100644 index 89ec2d2..0000000 Binary files a/packs/pregens/000006.log and /dev/null differ diff --git a/packs/pregens/000008.log b/packs/pregens/000008.log new file mode 100644 index 0000000..ec620dc Binary files /dev/null and b/packs/pregens/000008.log differ diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT index cacca75..875cf23 100644 --- a/packs/pregens/CURRENT +++ b/packs/pregens/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000007 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index f053163..275a7a4 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -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 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old index 958cf4f..161f626 100644 --- a/packs/pregens/LOG.old +++ b/packs/pregens/LOG.old @@ -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 diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004 deleted file mode 100644 index f4df0f9..0000000 Binary files a/packs/pregens/MANIFEST-000004 and /dev/null differ diff --git a/packs/pregens/MANIFEST-000007 b/packs/pregens/MANIFEST-000007 new file mode 100644 index 0000000..e908fb6 Binary files /dev/null and b/packs/pregens/MANIFEST-000007 differ diff --git a/packs/primer/LOG b/packs/primer/LOG index ded65ec..3e213e9 100644 --- a/packs/primer/LOG +++ b/packs/primer/LOG @@ -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 diff --git a/packs/primer/LOG.old b/packs/primer/LOG.old index 0f57b52..226bdf0 100644 --- a/packs/primer/LOG.old +++ b/packs/primer/LOG.old @@ -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 diff --git a/packs/rules/LOG b/packs/rules/LOG index 00fb933..9f4b0aa 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -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 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old index 2353f09..de65d5f 100644 --- a/packs/rules/LOG.old +++ b/packs/rules/LOG.old @@ -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 diff --git a/packs/skills/LOG b/packs/skills/LOG index d74332d..f6e2592 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -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 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old index 93fde0f..da94ce6 100644 --- a/packs/skills/LOG.old +++ b/packs/skills/LOG.old @@ -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 diff --git a/packs/slice/LOG b/packs/slice/LOG index 4c5a81c..7a0c089 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -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 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old index 28a7871..c9414a1 100644 --- a/packs/slice/LOG.old +++ b/packs/slice/LOG.old @@ -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 diff --git a/packs/starter/LOG b/packs/starter/LOG index 45b61ed..312649f 100644 --- a/packs/starter/LOG +++ b/packs/starter/LOG @@ -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 diff --git a/packs/starter/LOG.old b/packs/starter/LOG.old index dc38bf9..eec793b 100644 --- a/packs/starter/LOG.old +++ b/packs/starter/LOG.old @@ -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 diff --git a/packs/tables/LOG b/packs/tables/LOG index 6298faa..055b858 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -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 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old index 6a9f6e8..be34eb0 100644 --- a/packs/tables/LOG.old +++ b/packs/tables/LOG.old @@ -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 diff --git a/packs/talents/LOG b/packs/talents/LOG index 0706396..b049e16 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -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 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old index 5d26396..d0a86ea 100644 --- a/packs/talents/LOG.old +++ b/packs/talents/LOG.old @@ -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 diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG index f63009c..bb5fbcf 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -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 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old index 91746f6..1653867 100644 --- a/packs/vehicles/LOG.old +++ b/packs/vehicles/LOG.old @@ -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 diff --git a/packs/weapons/LOG b/packs/weapons/LOG index 613b092..f0883c6 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -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 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old index bfe591f..1bbe80a 100644 --- a/packs/weapons/LOG.old +++ b/packs/weapons/LOG.old @@ -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 diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 8b26b7f..05f3019 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -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: `

${e.label}, ${spec.rank}. ${e.blurb}

${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: `

${b.job}. ${civ.blurb}

${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]); diff --git a/tools/check-behaviour.mjs b/tools/check-behaviour.mjs index 7c85c69..53ed288 100644 --- a/tools/check-behaviour.mjs +++ b/tools/check-behaviour.mjs @@ -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(); diff --git a/tools/roster.mjs b/tools/roster.mjs index 9e41038..dfc647e 100644 --- a/tools/roster.mjs +++ b/tools/roster.mjs @@ -111,3 +111,96 @@ export const ROSTER = [ + "finished. Was consulted once and never quite sent home.

" + "

Anchor: a photograph of a lecture theatre, taken from the back.

" } ]; + +/** + * 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: "

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.

" + + "

How they found her: she interpreted a statement nobody had asked " + + "her to hear, and then wrote down what the room had done while it was said.

" }, + + pc_okonkwo: { trade: "builder", job: "Steel Erector", age: 28, + bio: "

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.

" + + "

How they found him: 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.

" }, + + pc_sandoval: { trade: "it", job: "Network Field Engineer", age: 31, + bio: "

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.

" + + "

How they found her: a relay logged forty minutes of traffic from a " + + "site decommissioned in 1994, and she kept the file instead of clearing it.

" }, + + pc_bhattacharya: { trade: "scientist", job: "Research Chemist", age: 26, + bio: "

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.

" + + "

How they found him: he noticed a batch record amended by somebody " + + "who had never worked there, and he raised it in writing. Twice.

" }, + + pc_rahimi: { trade: "entertainer", job: "Local Radio Presenter", age: 33, + bio: "

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.

" + + "

How they found her: 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.

" }, + + pc_nkemdirim: { trade: "doctor", job: "Registrar, Emergency Medicine", age: 31, + bio: "

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.

" + + "

How they found them: 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.

" }, + + pc_kowalczyk: { trade: "engineer", job: "Structural Engineer", age: 29, + bio: "

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.

" + + "

How they found him: he condemned a footbridge on load figures that " + + "were correct, for a span he had measured three times and got three " + + "answers.

" }, + + pc_finch: { trade: "criminal", job: "Between Things", age: 25, + bio: "

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.

" + + "

How they found her: 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.

" }, + + pc_agyeman: { trade: "police", job: "Detective Sergeant", age: 50, + bio: "

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.

" + + "

How they found him: 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.

" }, + + pc_lindqvist: { trade: "professor", job: "Professor of Comparative Folklore", age: 59, + bio: "

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.

" + + "

How they found him: he was consulted once, as an academic " + + "courtesy, and gave an answer so specific that he was never quite sent " + + "home.

" } +};