diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md
index c8ca54a..8135691 100644
--- a/docs/REVIEW_LOG.md
+++ b/docs/REVIEW_LOG.md
@@ -4062,3 +4062,69 @@ Officer on **53% Navigate**. The preference is meant to mean "whoever reads a
developing situation best", not "anybody with a point in the right column". Tradecraft
still wins ties and wins when it is genuinely better; it no longer wins when it is
plainly worse. The Containment phase went from 6% to 26% on the same team.
+
+## An overnight build: hazards, automatic fire, two weapons, a bestiary, and space
+
+### Stress test first
+150 characters — every one of the 25 postings, at Probationary and Case Officer, in
+all three species — checked against fourteen invariants each. Nothing broken. Then the
+edges: characteristics at 1, 3 and 21; every location destroyed at once; twelve pikes
+carried; hit points set to −99. All degraded gracefully except one.
+
+**R-227 · a destroyed head was a nap.** The printed effect has always read "For
+anything that keeps its brain there, this is death" and the code set `unconscious` and
+stopped. An agent with every location on their body destroyed, head included, reported
+as DOWN. `destructionOutcomeFor()` now says what destruction means: a head is death, a
+vital or sensory location drops you to zero, a limb is ruinous and survivable.
+
+### The ways the world kills you
+- **Falling** — a die per 3 m, random location, **armour does not apply** because the
+ ground is not a weapon. Soft landing halves the distance; a controlled descent
+ halves it again. Verified: 12 m killed an agent outright; the same fall controlled
+ left them on 2 hit points.
+- **Drowning** — CON rounds with warning, half without, then a point a round that does
+ not stop. Runs straight past the Major Wound threshold to zero and into the dying
+ count. Verified over 24 rounds.
+- **Fire** — a condition with a size, not a hit. Worn armour helps on the round you
+ CATCH and never after, because by then the fire is inside it.
+- **Explosives** — one roll at the centre, shared by band, and **cover beats armour**:
+ armour counts at contact only and only half. Verified on four people at 1 m, 4 m,
+ 10 m and 40 m: 17, 3, 0, and outside it entirely.
+
+### Automatic fire, and two weapons
+One roll for a burst, and the **success level** says how much of it arrived — the same
+band machinery as everything else rather than four rolls and a stalled table. **The
+rounds are spent either way.** Verified: a failed full-auto burst spent ten rounds for
+nothing, which is the honest part of the rule.
+
+Two weapons costs −20/−40 untrained and 0/−20 with the new **Ambidextrous** talent.
+Nothing removes the off-hand penalty, and the off hand takes the lighter weapon at no
+more than 2 kg — the rule that refuses two poleaxes, tested and refused.
+
+### The bestiary
+Twenty-one entries in three families, each with characteristics, natural armour, a
+tactics line and a POWER written as a rule with a cost rather than a special ability.
+FOLKLORE: barghest, kelpie, redcap, boggart, church grim. HORROR: hollow man, the
+tenant, the choir, the carrion file, the understudy. FORWARD: surveyor, quarantine
+unit, cuckoo array, long walker, the auditor. Every skill and weapon reference checked
+against the catalogue.
+
+### Space, and everybody in it
+Eight STATIONS, each using a skill somebody already has, so the Physician and the
+Translation Officer have a chair. Range is a band; the helm moves one a round, in or
+out. **Heat is the pressure** and it is the whole design.
+
+Two rounds of tuning, both from playtest and both necessary:
+- First cut had every station heating and one engineer venting. Heat reached the
+ ceiling by round four and locked the helm — a countdown, not a decision. Now only
+ Gunnery is charged for trying (the shot left either way, exactly as the burst rule
+ reasons), looking is free, and venting sheds 3.
+- Second cut was too cold — heat sat at 0 and nobody cared. Settled between.
+- **The helm only ever closed.** Every fight ended docked with no way to break off
+ short of the anchor. The helm now chooses in or out.
+
+Final playtest: heat 2 → 5 → 3 → 5 → 7 → 10, and at the ceiling the helm locked out
+because the crew kept shooting instead of cooling. That is the decision the subsystem
+is for.
+
+64 rules, 198 spot-checked formulas, a 12-page rulebook in reading order.
diff --git a/icons/cutter_ship.svg b/icons/cutter_ship.svg
new file mode 100644
index 0000000..74c0994
--- /dev/null
+++ b/icons/cutter_ship.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/icons/lighter.svg b/icons/lighter.svg
new file mode 100644
index 0000000..2456e26
--- /dev/null
+++ b/icons/lighter.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/icons/long_hull.svg b/icons/long_hull.svg
new file mode 100644
index 0000000..eeb9a29
--- /dev/null
+++ b/icons/long_hull.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/icons/picket.svg b/icons/picket.svg
new file mode 100644
index 0000000..dfab87f
--- /dev/null
+++ b/icons/picket.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/icons/revenue_boat.svg b/icons/revenue_boat.svg
new file mode 100644
index 0000000..50d8ce1
--- /dev/null
+++ b/icons/revenue_boat.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/lang/en.json b/lang/en.json
index 12de80f..24c001f 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -82,6 +82,43 @@
"RINGBRP.Vehicle.Hull": "Hull",
"RINGBRP.Vehicle.Terrain": "Goes over",
"RINGBRP.Vehicle.Operate": "Operated with",
+ "RINGBRP.SpaceBand.docked": "alongside",
+ "RINGBRP.SpaceBand.knife": "knife range",
+ "RINGBRP.SpaceBand.close": "close",
+ "RINGBRP.SpaceBand.standoff": "standoff",
+ "RINGBRP.SpaceBand.distant": "distant",
+ "RINGBRP.SpaceBand.gone": "gone",
+ "RINGBRP.Station.helm": "Helm",
+ "RINGBRP.Station.guns": "Gunnery",
+ "RINGBRP.Station.sensors": "Sensors",
+ "RINGBRP.Station.engines": "Engines",
+ "RINGBRP.Station.damage": "Damage control",
+ "RINGBRP.Station.signals": "Signals",
+ "RINGBRP.Station.medical": "Medical",
+ "RINGBRP.Station.anchor": "The anchor",
+ "RINGBRP.Station.Label": "{station}, aboard {craft}",
+ "RINGBRP.Station.Card": "{name} — {station}",
+ "RINGBRP.Station.At": "Range: {band}",
+ "RINGBRP.Station.HeatPen": "Running hot: {mod}%",
+ "RINGBRP.Station.RangePen": "Range: {mod}%",
+ "RINGBRP.Station.State": "Hull {hull}/{max} · Heat {heat} · {band}",
+ "RINGBRP.Station.Closed": "Closed to {band}.",
+ "RINGBRP.Station.Opened": "Opened the range to {band}.",
+ "RINGBRP.Station.Hit": "{n} through the hull of {name} — {left} left.",
+ "RINGBRP.Station.Would": "{n} would have gone through, with something to shoot at.",
+ "RINGBRP.Station.Lost": "{name} is finished.",
+ "RINGBRP.Station.Marked": "Marked. The next gunnery roll lands one more.",
+ "RINGBRP.Station.Shed": "Vented {n}. Heat is {now}.",
+ "RINGBRP.Station.Patched": "Patched {n} of the hull.",
+ "RINGBRP.Station.Talking": "They are still listening. That is not nothing.",
+ "RINGBRP.Station.TalkedDown": "They break off. Whatever you said, put it in the report.",
+ "RINGBRP.Station.Crew": "The wounded are stable and somebody is at their station again.",
+ "RINGBRP.Station.Going": "Transposing out. It is not tidy.",
+ "RINGBRP.Station.GoneClean": "Gone, cleanly, and nothing followed.",
+ "RINGBRP.Station.Fumbled": "It goes wrong and the craft runs hotter for it. +2 Heat.",
+ "RINGBRP.Station.Unknown": "No such station.",
+ "RINGBRP.Station.OutOfRange": "Nothing to shoot at from here.",
+ "RINGBRP.Station.TooHot": "Too hot to manoeuvre. Somebody vent it.",
"RINGBRP.Speed.walk": "walking pace",
"RINGBRP.Speed.slow": "slow",
"RINGBRP.Speed.road": "road speed",
@@ -629,11 +666,41 @@
"RINGBRP.Wound.variant.mobility": "mobility",
"RINGBRP.Heal.Bleeds": "{name} bleeds {amount}.",
"RINGBRP.Heal.BledOut": "bled out",
+ "RINGBRP.Fall.Short": "Not far enough to matter. {m} m and they roll it off.",
+ "RINGBRP.Fall.Flavor": "A fall of {m} m — {dice}, and the ground is not impressed by armour.",
+ "RINGBRP.Breath.Losing": "{name} cannot breathe. −{n}",
+ "RINGBRP.Fire.spark": "Caught a spark",
+ "RINGBRP.Fire.clothes": "Clothes alight",
+ "RINGBRP.Fire.pool": "Standing in it",
+ "RINGBRP.Fire.engulf": "Engulfed",
+ "RINGBRP.Fire.Flavor": "{band} — round {round} of {of}",
+ "RINGBRP.Blast.contact": "at the centre",
+ "RINGBRP.Blast.near": "inside the radius",
+ "RINGBRP.Blast.far": "on the edge of it",
+ "RINGBRP.Blast.Flavor": "Detonation — {formula} at the centre, {r} m radius",
+ "RINGBRP.Burst.short": "short burst",
+ "RINGBRP.Burst.long": "long burst",
+ "RINGBRP.Burst.full": "the whole magazine",
+ "RINGBRP.Burst.Label": "{name} — {size}",
+ "RINGBRP.Burst.Spends": "Spends the rounds whether or not it hits.",
+ "RINGBRP.Burst.Connected": "{n} of {of} rounds connected.",
+ "RINGBRP.Burst.Jam": "It jams. Clearing it is an action and the weapon is out until you do.",
+ "RINGBRP.Burst.Empty": "The magazine is gone. All of it, in one press.",
+ "RINGBRP.Burst.Wild": "The muzzle climbs. The GM decides what was behind the target.",
+ "RINGBRP.TwoWeapon.main": "{name} (main hand)",
+ "RINGBRP.TwoWeapon.off": "{name} (off hand)",
+ "RINGBRP.TwoWeapon.Penalty": "Two weapons: {mod}% — {note}",
+ "RINGBRP.TwoWeapon.Trained": "trained to it",
+ "RINGBRP.TwoWeapon.Untrained": "and you have not been taught this",
+ "RINGBRP.TwoWeapon.CannotPair": "{off} is too much for an off hand. Nothing over {max} kg.",
+ "RINGBRP.Talent.ambidextrous": "Ambidextrous",
"RINGBRP.Death.Dying": "{name} is dying.",
"RINGBRP.Death.Count": "Round {n} of {of}.",
"RINGBRP.Death.Died": "{name} is dead.",
"RINGBRP.Death.Coherence": "They died at Coherence {n}.",
"RINGBRP.Death.TheEntry": "The last entry:",
+ "RINGBRP.Death.LocationLethal": "{name}'s {location} is destroyed. That is the end of it.",
+ "RINGBRP.Death.LocationVital": "{name}'s {location} is destroyed. They go down and they are not getting up on their own.",
"RINGBRP.Death.StableTag": "stable — the count has stopped",
"RINGBRP.Death.CountTag": "round",
"RINGBRP.Death.Entry.recorded": "Recorded properly. A death in service, a form, a pension to a next of kin, and a name that stays where it was put.",
diff --git a/packs/armour/000003.log b/packs/armour/000003.log
new file mode 100644
index 0000000..5b23416
Binary files /dev/null and b/packs/armour/000003.log differ
diff --git a/packs/armour/000005.ldb b/packs/armour/000005.ldb
deleted file mode 100644
index b406921..0000000
Binary files a/packs/armour/000005.ldb and /dev/null differ
diff --git a/packs/armour/000006.log b/packs/armour/000006.log
deleted file mode 100644
index 9d3f468..0000000
Binary files a/packs/armour/000006.log and /dev/null differ
diff --git a/packs/armour/CURRENT b/packs/armour/CURRENT
index cacca75..1a84852 100644
--- a/packs/armour/CURRENT
+++ b/packs/armour/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/armour/LOG b/packs/armour/LOG
index f523ea5..ab46292 100644
--- a/packs/armour/LOG
+++ b/packs/armour/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.047112 174acf000 Recovering log #3
-2026/08/10-23:08:46.047137 174acf000 Level-0 table #5: started
-2026/08/10-23:08:46.047255 174acf000 Level-0 table #5: 3269 bytes OK
-2026/08/10-23:08:46.047517 174acf000 Delete type=0 #3
-2026/08/10-23:08:46.047560 174acf000 Delete type=3 #2
+2026/08/11-07:33:59.238506 16fddf000 Delete type=3 #1
diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old
deleted file mode 100644
index 63f877a..0000000
--- a/packs/armour/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.754899 17291b000 Delete type=3 #1
diff --git a/packs/armour/MANIFEST-000002 b/packs/armour/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/armour/MANIFEST-000002 differ
diff --git a/packs/armour/MANIFEST-000004 b/packs/armour/MANIFEST-000004
deleted file mode 100644
index 1faa5e9..0000000
Binary files a/packs/armour/MANIFEST-000004 and /dev/null differ
diff --git a/packs/expedition/000003.log b/packs/expedition/000003.log
new file mode 100644
index 0000000..263295b
Binary files /dev/null and b/packs/expedition/000003.log differ
diff --git a/packs/expedition/000005.ldb b/packs/expedition/000005.ldb
deleted file mode 100644
index 4ff460d..0000000
Binary files a/packs/expedition/000005.ldb and /dev/null differ
diff --git a/packs/expedition/000006.log b/packs/expedition/000006.log
deleted file mode 100644
index 268b187..0000000
Binary files a/packs/expedition/000006.log and /dev/null differ
diff --git a/packs/expedition/CURRENT b/packs/expedition/CURRENT
index cacca75..1a84852 100644
--- a/packs/expedition/CURRENT
+++ b/packs/expedition/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/expedition/LOG b/packs/expedition/LOG
index 8b6d9b4..4b2937e 100644
--- a/packs/expedition/LOG
+++ b/packs/expedition/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.054496 1752db000 Recovering log #3
-2026/08/10-23:08:46.054516 1752db000 Level-0 table #5: started
-2026/08/10-23:08:46.054650 1752db000 Level-0 table #5: 4129 bytes OK
-2026/08/10-23:08:46.054895 1752db000 Delete type=0 #3
-2026/08/10-23:08:46.054929 1752db000 Delete type=3 #2
+2026/08/11-07:33:59.272751 16edc7000 Delete type=3 #1
diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old
deleted file mode 100644
index f1c3c9a..0000000
--- a/packs/expedition/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.780759 173933000 Delete type=3 #1
diff --git a/packs/expedition/MANIFEST-000002 b/packs/expedition/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/expedition/MANIFEST-000002 differ
diff --git a/packs/expedition/MANIFEST-000004 b/packs/expedition/MANIFEST-000004
deleted file mode 100644
index 47fbcd4..0000000
Binary files a/packs/expedition/MANIFEST-000004 and /dev/null differ
diff --git a/packs/gear/000003.log b/packs/gear/000003.log
new file mode 100644
index 0000000..a42777b
Binary files /dev/null and b/packs/gear/000003.log differ
diff --git a/packs/gear/000005.ldb b/packs/gear/000005.ldb
deleted file mode 100644
index fedde60..0000000
Binary files a/packs/gear/000005.ldb and /dev/null differ
diff --git a/packs/gear/000006.log b/packs/gear/000006.log
deleted file mode 100644
index 4b150b6..0000000
Binary files a/packs/gear/000006.log and /dev/null differ
diff --git a/packs/gear/CURRENT b/packs/gear/CURRENT
index cacca75..1a84852 100644
--- a/packs/gear/CURRENT
+++ b/packs/gear/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/gear/LOG b/packs/gear/LOG
index 4a7ba55..3b821f8 100644
--- a/packs/gear/LOG
+++ b/packs/gear/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.048195 173ab7000 Recovering log #3
-2026/08/10-23:08:46.048288 173ab7000 Level-0 table #5: started
-2026/08/10-23:08:46.048589 173ab7000 Level-0 table #5: 27954 bytes OK
-2026/08/10-23:08:46.048854 173ab7000 Delete type=0 #3
-2026/08/10-23:08:46.048904 173ab7000 Delete type=3 #2
+2026/08/11-07:33:59.240938 16edc7000 Delete type=3 #1
diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old
deleted file mode 100644
index 081ae35..0000000
--- a/packs/gear/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.757302 173127000 Delete type=3 #1
diff --git a/packs/gear/MANIFEST-000002 b/packs/gear/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/gear/MANIFEST-000002 differ
diff --git a/packs/gear/MANIFEST-000004 b/packs/gear/MANIFEST-000004
deleted file mode 100644
index 97d9316..0000000
Binary files a/packs/gear/MANIFEST-000004 and /dev/null differ
diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log
new file mode 100644
index 0000000..c13442d
Binary files /dev/null and b/packs/npcs/000003.log differ
diff --git a/packs/npcs/000005.ldb b/packs/npcs/000005.ldb
deleted file mode 100644
index e0c2b42..0000000
Binary files a/packs/npcs/000005.ldb and /dev/null differ
diff --git a/packs/npcs/000006.log b/packs/npcs/000006.log
deleted file mode 100644
index d08f420..0000000
Binary files a/packs/npcs/000006.log and /dev/null differ
diff --git a/packs/npcs/CURRENT b/packs/npcs/CURRENT
index cacca75..1a84852 100644
--- a/packs/npcs/CURRENT
+++ b/packs/npcs/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/npcs/LOG b/packs/npcs/LOG
index 4bbef1a..662ac36 100644
--- a/packs/npcs/LOG
+++ b/packs/npcs/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.040166 1752db000 Recovering log #3
-2026/08/10-23:08:46.040415 1752db000 Level-0 table #5: started
-2026/08/10-23:08:46.040907 1752db000 Level-0 table #5: 64974 bytes OK
-2026/08/10-23:08:46.041174 1752db000 Delete type=0 #3
-2026/08/10-23:08:46.041236 1752db000 Delete type=3 #2
+2026/08/11-07:33:59.258004 1705eb000 Delete type=3 #1
diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old
deleted file mode 100644
index 6d6d001..0000000
--- a/packs/npcs/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.767972 17210f000 Delete type=3 #1
diff --git a/packs/npcs/MANIFEST-000002 b/packs/npcs/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/npcs/MANIFEST-000002 differ
diff --git a/packs/npcs/MANIFEST-000004 b/packs/npcs/MANIFEST-000004
deleted file mode 100644
index 178f4b5..0000000
Binary files a/packs/npcs/MANIFEST-000004 and /dev/null differ
diff --git a/packs/powers/000003.log b/packs/powers/000003.log
new file mode 100644
index 0000000..d723043
Binary files /dev/null and b/packs/powers/000003.log differ
diff --git a/packs/powers/000005.ldb b/packs/powers/000005.ldb
deleted file mode 100644
index 7f540c6..0000000
Binary files a/packs/powers/000005.ldb and /dev/null differ
diff --git a/packs/powers/000006.log b/packs/powers/000006.log
deleted file mode 100644
index 43e306f..0000000
Binary files a/packs/powers/000006.log and /dev/null differ
diff --git a/packs/powers/CURRENT b/packs/powers/CURRENT
index cacca75..1a84852 100644
--- a/packs/powers/CURRENT
+++ b/packs/powers/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/powers/LOG b/packs/powers/LOG
index dfeb20c..97a2333 100644
--- a/packs/powers/LOG
+++ b/packs/powers/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.044798 174acf000 Recovering log #3
-2026/08/10-23:08:46.044820 174acf000 Level-0 table #5: started
-2026/08/10-23:08:46.044910 174acf000 Level-0 table #5: 3295 bytes OK
-2026/08/10-23:08:46.045114 174acf000 Delete type=0 #3
-2026/08/10-23:08:46.045143 174acf000 Delete type=3 #2
+2026/08/11-07:33:59.266367 16f5d3000 Delete type=3 #1
diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old
deleted file mode 100644
index 5bc9ecd..0000000
--- a/packs/powers/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.774231 17291b000 Delete type=3 #1
diff --git a/packs/powers/MANIFEST-000002 b/packs/powers/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/powers/MANIFEST-000002 differ
diff --git a/packs/powers/MANIFEST-000004 b/packs/powers/MANIFEST-000004
deleted file mode 100644
index 20de082..0000000
Binary files a/packs/powers/MANIFEST-000004 and /dev/null differ
diff --git a/packs/pregens/000003.log b/packs/pregens/000003.log
new file mode 100644
index 0000000..b2a8dce
Binary files /dev/null and b/packs/pregens/000003.log differ
diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb
deleted file mode 100644
index d646217..0000000
Binary files a/packs/pregens/000005.ldb and /dev/null differ
diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log
deleted file mode 100644
index 71e3acd..0000000
Binary files a/packs/pregens/000006.log and /dev/null differ
diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT
index cacca75..1a84852 100644
--- a/packs/pregens/CURRENT
+++ b/packs/pregens/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/pregens/LOG b/packs/pregens/LOG
index b9ecdd3..0c480c0 100644
--- a/packs/pregens/LOG
+++ b/packs/pregens/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.037647 1752db000 Recovering log #3
-2026/08/10-23:08:46.037898 1752db000 Level-0 table #5: started
-2026/08/10-23:08:46.038470 1752db000 Level-0 table #5: 57435 bytes OK
-2026/08/10-23:08:46.038791 1752db000 Delete type=0 #3
-2026/08/10-23:08:46.038847 1752db000 Delete type=3 #2
+2026/08/11-07:33:59.247709 1705eb000 Delete type=3 #1
diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old
deleted file mode 100644
index 4f8e38d..0000000
--- a/packs/pregens/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.763212 17291b000 Delete type=3 #1
diff --git a/packs/pregens/MANIFEST-000002 b/packs/pregens/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/pregens/MANIFEST-000002 differ
diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004
deleted file mode 100644
index a27f077..0000000
Binary files a/packs/pregens/MANIFEST-000004 and /dev/null differ
diff --git a/packs/rules/000003.log b/packs/rules/000003.log
new file mode 100644
index 0000000..88fda6d
Binary files /dev/null and b/packs/rules/000003.log differ
diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb
deleted file mode 100644
index 81959f5..0000000
Binary files a/packs/rules/000005.ldb and /dev/null differ
diff --git a/packs/rules/000006.log b/packs/rules/000006.log
deleted file mode 100644
index 9ce8629..0000000
Binary files a/packs/rules/000006.log and /dev/null differ
diff --git a/packs/rules/CURRENT b/packs/rules/CURRENT
index cacca75..1a84852 100644
--- a/packs/rules/CURRENT
+++ b/packs/rules/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/rules/LOG b/packs/rules/LOG
index 9d927dc..3fc8663 100644
--- a/packs/rules/LOG
+++ b/packs/rules/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.050591 1752db000 Recovering log #3
-2026/08/10-23:08:46.050653 1752db000 Level-0 table #5: started
-2026/08/10-23:08:46.050935 1752db000 Level-0 table #5: 24724 bytes OK
-2026/08/10-23:08:46.051197 1752db000 Delete type=0 #3
-2026/08/10-23:08:46.051244 1752db000 Delete type=3 #2
+2026/08/11-07:33:59.271217 16fddf000 Delete type=3 #1
diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old
deleted file mode 100644
index ff4a77b..0000000
--- a/packs/rules/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.778897 173127000 Delete type=3 #1
diff --git a/packs/rules/MANIFEST-000002 b/packs/rules/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/rules/MANIFEST-000002 differ
diff --git a/packs/rules/MANIFEST-000004 b/packs/rules/MANIFEST-000004
deleted file mode 100644
index b044639..0000000
Binary files a/packs/rules/MANIFEST-000004 and /dev/null differ
diff --git a/packs/skills/000003.log b/packs/skills/000003.log
new file mode 100644
index 0000000..e23ff19
Binary files /dev/null and b/packs/skills/000003.log differ
diff --git a/packs/skills/000005.ldb b/packs/skills/000005.ldb
deleted file mode 100644
index d613d26..0000000
Binary files a/packs/skills/000005.ldb and /dev/null differ
diff --git a/packs/skills/000006.log b/packs/skills/000006.log
deleted file mode 100644
index aa7dd7d..0000000
Binary files a/packs/skills/000006.log and /dev/null differ
diff --git a/packs/skills/CURRENT b/packs/skills/CURRENT
index cacca75..1a84852 100644
--- a/packs/skills/CURRENT
+++ b/packs/skills/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/skills/LOG b/packs/skills/LOG
index acd68be..be98972 100644
--- a/packs/skills/LOG
+++ b/packs/skills/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.042468 1742c3000 Recovering log #3
-2026/08/10-23:08:46.042508 1742c3000 Level-0 table #5: started
-2026/08/10-23:08:46.042648 1742c3000 Level-0 table #5: 7486 bytes OK
-2026/08/10-23:08:46.042865 1742c3000 Delete type=0 #3
-2026/08/10-23:08:46.042900 1742c3000 Delete type=3 #2
+2026/08/11-07:33:59.230403 16f5d3000 Delete type=3 #1
diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old
deleted file mode 100644
index 0568aa7..0000000
--- a/packs/skills/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.747903 173127000 Delete type=3 #1
diff --git a/packs/skills/MANIFEST-000002 b/packs/skills/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/skills/MANIFEST-000002 differ
diff --git a/packs/skills/MANIFEST-000004 b/packs/skills/MANIFEST-000004
deleted file mode 100644
index 792e8a1..0000000
Binary files a/packs/skills/MANIFEST-000004 and /dev/null differ
diff --git a/packs/slice/000003.log b/packs/slice/000003.log
new file mode 100644
index 0000000..4a86fe9
Binary files /dev/null and b/packs/slice/000003.log differ
diff --git a/packs/slice/000005.ldb b/packs/slice/000005.ldb
deleted file mode 100644
index 4851c77..0000000
Binary files a/packs/slice/000005.ldb and /dev/null differ
diff --git a/packs/slice/000006.log b/packs/slice/000006.log
deleted file mode 100644
index c7d9b16..0000000
Binary files a/packs/slice/000006.log and /dev/null differ
diff --git a/packs/slice/CURRENT b/packs/slice/CURRENT
index cacca75..1a84852 100644
--- a/packs/slice/CURRENT
+++ b/packs/slice/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/slice/LOG b/packs/slice/LOG
index e5b9ffb..6b9b35f 100644
--- a/packs/slice/LOG
+++ b/packs/slice/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.051945 1742c3000 Recovering log #3
-2026/08/10-23:08:46.051993 1742c3000 Level-0 table #5: started
-2026/08/10-23:08:46.052269 1742c3000 Level-0 table #5: 16892 bytes OK
-2026/08/10-23:08:46.052540 1742c3000 Delete type=0 #3
-2026/08/10-23:08:46.052572 1742c3000 Delete type=3 #2
+2026/08/11-07:33:59.264747 1705eb000 Delete type=3 #1
diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old
deleted file mode 100644
index a2ec8e4..0000000
--- a/packs/slice/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.772624 173933000 Delete type=3 #1
diff --git a/packs/slice/MANIFEST-000002 b/packs/slice/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/slice/MANIFEST-000002 differ
diff --git a/packs/slice/MANIFEST-000004 b/packs/slice/MANIFEST-000004
deleted file mode 100644
index 0552c3a..0000000
Binary files a/packs/slice/MANIFEST-000004 and /dev/null differ
diff --git a/packs/tables/000003.log b/packs/tables/000003.log
new file mode 100644
index 0000000..f125b7d
Binary files /dev/null and b/packs/tables/000003.log differ
diff --git a/packs/tables/000005.ldb b/packs/tables/000005.ldb
deleted file mode 100644
index 64baa1c..0000000
Binary files a/packs/tables/000005.ldb and /dev/null differ
diff --git a/packs/tables/000006.log b/packs/tables/000006.log
deleted file mode 100644
index d5e06f3..0000000
Binary files a/packs/tables/000006.log and /dev/null differ
diff --git a/packs/tables/CURRENT b/packs/tables/CURRENT
index cacca75..1a84852 100644
--- a/packs/tables/CURRENT
+++ b/packs/tables/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/tables/LOG b/packs/tables/LOG
index ef7454c..9bef2e9 100644
--- a/packs/tables/LOG
+++ b/packs/tables/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.053301 174acf000 Recovering log #3
-2026/08/10-23:08:46.053322 174acf000 Level-0 table #5: started
-2026/08/10-23:08:46.053430 174acf000 Level-0 table #5: 3739 bytes OK
-2026/08/10-23:08:46.053664 174acf000 Delete type=0 #3
-2026/08/10-23:08:46.053709 174acf000 Delete type=3 #2
+2026/08/11-07:33:59.262751 16f5d3000 Delete type=3 #1
diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old
deleted file mode 100644
index 6f61111..0000000
--- a/packs/tables/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.770616 173127000 Delete type=3 #1
diff --git a/packs/tables/MANIFEST-000002 b/packs/tables/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/tables/MANIFEST-000002 differ
diff --git a/packs/tables/MANIFEST-000004 b/packs/tables/MANIFEST-000004
deleted file mode 100644
index 004ff30..0000000
Binary files a/packs/tables/MANIFEST-000004 and /dev/null differ
diff --git a/packs/talents/000003.log b/packs/talents/000003.log
new file mode 100644
index 0000000..443b7ea
Binary files /dev/null and b/packs/talents/000003.log differ
diff --git a/packs/talents/000005.ldb b/packs/talents/000005.ldb
deleted file mode 100644
index 989e264..0000000
Binary files a/packs/talents/000005.ldb and /dev/null differ
diff --git a/packs/talents/000006.log b/packs/talents/000006.log
deleted file mode 100644
index e8a5585..0000000
Binary files a/packs/talents/000006.log and /dev/null differ
diff --git a/packs/talents/CURRENT b/packs/talents/CURRENT
index cacca75..1a84852 100644
--- a/packs/talents/CURRENT
+++ b/packs/talents/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/talents/LOG b/packs/talents/LOG
index 940ae92..1711d20 100644
--- a/packs/talents/LOG
+++ b/packs/talents/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.043606 1752db000 Recovering log #3
-2026/08/10-23:08:46.043635 1752db000 Level-0 table #5: started
-2026/08/10-23:08:46.043789 1752db000 Level-0 table #5: 5105 bytes OK
-2026/08/10-23:08:46.044129 1752db000 Delete type=0 #3
-2026/08/10-23:08:46.044200 1752db000 Delete type=3 #2
+2026/08/11-07:33:59.233773 16f5d3000 Delete type=3 #1
diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old
deleted file mode 100644
index 468014f..0000000
--- a/packs/talents/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.750858 173933000 Delete type=3 #1
diff --git a/packs/talents/MANIFEST-000002 b/packs/talents/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/talents/MANIFEST-000002 differ
diff --git a/packs/talents/MANIFEST-000004 b/packs/talents/MANIFEST-000004
deleted file mode 100644
index 8b0e24d..0000000
Binary files a/packs/talents/MANIFEST-000004 and /dev/null differ
diff --git a/packs/vehicles/000003.log b/packs/vehicles/000003.log
new file mode 100644
index 0000000..9950ce5
Binary files /dev/null and b/packs/vehicles/000003.log differ
diff --git a/packs/vehicles/000005.ldb b/packs/vehicles/000005.ldb
deleted file mode 100644
index 41d5456..0000000
Binary files a/packs/vehicles/000005.ldb and /dev/null differ
diff --git a/packs/vehicles/000006.log b/packs/vehicles/000006.log
deleted file mode 100644
index 5379762..0000000
Binary files a/packs/vehicles/000006.log and /dev/null differ
diff --git a/packs/vehicles/CURRENT b/packs/vehicles/CURRENT
index cacca75..1a84852 100644
--- a/packs/vehicles/CURRENT
+++ b/packs/vehicles/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG
index ce9385c..1c32f30 100644
--- a/packs/vehicles/LOG
+++ b/packs/vehicles/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.049567 174acf000 Recovering log #3
-2026/08/10-23:08:46.049596 174acf000 Level-0 table #5: started
-2026/08/10-23:08:46.049730 174acf000 Level-0 table #5: 5012 bytes OK
-2026/08/10-23:08:46.049946 174acf000 Delete type=0 #3
-2026/08/10-23:08:46.049973 174acf000 Delete type=3 #2
+2026/08/11-07:33:59.243041 1705eb000 Delete type=3 #1
diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old
deleted file mode 100644
index 1cf2e56..0000000
--- a/packs/vehicles/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.759342 17210f000 Delete type=3 #1
diff --git a/packs/vehicles/MANIFEST-000002 b/packs/vehicles/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/vehicles/MANIFEST-000002 differ
diff --git a/packs/vehicles/MANIFEST-000004 b/packs/vehicles/MANIFEST-000004
deleted file mode 100644
index 8b6e8d2..0000000
Binary files a/packs/vehicles/MANIFEST-000004 and /dev/null differ
diff --git a/packs/weapons/000003.log b/packs/weapons/000003.log
new file mode 100644
index 0000000..e1488ed
Binary files /dev/null and b/packs/weapons/000003.log differ
diff --git a/packs/weapons/000005.ldb b/packs/weapons/000005.ldb
deleted file mode 100644
index 742beba..0000000
Binary files a/packs/weapons/000005.ldb and /dev/null differ
diff --git a/packs/weapons/000006.log b/packs/weapons/000006.log
deleted file mode 100644
index 4d427a6..0000000
Binary files a/packs/weapons/000006.log and /dev/null differ
diff --git a/packs/weapons/CURRENT b/packs/weapons/CURRENT
index cacca75..1a84852 100644
--- a/packs/weapons/CURRENT
+++ b/packs/weapons/CURRENT
@@ -1 +1 @@
-MANIFEST-000004
+MANIFEST-000002
diff --git a/packs/weapons/LOG b/packs/weapons/LOG
index 113bd05..eda0de3 100644
--- a/packs/weapons/LOG
+++ b/packs/weapons/LOG
@@ -1,5 +1 @@
-2026/08/10-23:08:46.045683 173ab7000 Recovering log #3
-2026/08/10-23:08:46.045761 173ab7000 Level-0 table #5: started
-2026/08/10-23:08:46.046185 173ab7000 Level-0 table #5: 14767 bytes OK
-2026/08/10-23:08:46.046452 173ab7000 Delete type=0 #3
-2026/08/10-23:08:46.046500 173ab7000 Delete type=3 #2
+2026/08/11-07:33:59.236342 16edc7000 Delete type=3 #1
diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old
deleted file mode 100644
index b7cd2e5..0000000
--- a/packs/weapons/LOG.old
+++ /dev/null
@@ -1 +0,0 @@
-2026/08/10-23:08:37.752873 173933000 Delete type=3 #1
diff --git a/packs/weapons/MANIFEST-000002 b/packs/weapons/MANIFEST-000002
new file mode 100644
index 0000000..bbbc585
Binary files /dev/null and b/packs/weapons/MANIFEST-000002 differ
diff --git a/packs/weapons/MANIFEST-000004 b/packs/weapons/MANIFEST-000004
deleted file mode 100644
index 6a582af..0000000
Binary files a/packs/weapons/MANIFEST-000004 and /dev/null differ
diff --git a/ringbrp.mjs b/ringbrp.mjs
index 5f3f487..28d909c 100644
--- a/ringbrp.mjs
+++ b/ringbrp.mjs
@@ -27,8 +27,14 @@ import {
encumbrancePenaltyFor, ENCUMBERED_CATEGORIES, freeCarryFor,
defencePenaltyFor, defenceTypeAllowed, DEFENCE_STEP,
defenceOutcomeFor, levelRank, LEVEL_LADDER, skillBaseFrom,
- DYING, dyingLimitFor, conditionFor, lastEntryFor, LAST_ENTRY,
+ DYING, dyingLimitFor, conditionFor, lastEntryFor, LAST_ENTRY, destructionOutcomeFor,
RETURN_CLOCK, returnPenaltyFor, returnIsDifficult, extractionCostFor,
+ FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, fireBandFor,
+ BLAST, blastBandFor, blastShareFor, BLAST_COVER, blastAfterCover,
+ BURST, burstHitsFor, burstRoundsSpent, BURST_FUMBLE,
+ SPACE_BANDS, spaceBandRank, spaceBandAfter, STATIONS, HEAT, heatEffectsFor,
+ spaceRangeModFor, spaceHitFor, craftConditionFor,
+ TWO_WEAPON, twoWeaponPenaltyFor, canPairWeapons,
brawlFallbackFor, BRAWL_FALLBACK,
SCOPE_TIERS, scopeRank, scopeStanceFor, scrutinyFor, RED_FLAG_STEP,
authorityOutcomeFor, trustAfter, TRUST_MAX, DOSSIER, dossierTrustFor,
@@ -1076,8 +1082,32 @@ export async function hitLocationRoll({ target, damage, mode = "ranged",
if (upd["system.dying.rounds"] !== undefined
&& upd["system.dying.rounds"] >= dyingLimitFor(sys.majorWound?.value)) {
await died(target);
+ return { location: loc, ...res };
}
- return { location: loc, ...res };
+
+ // Destroying something they cannot live without. The printed effect for a
+ // destroyed head has always said this is death; now it is.
+ const outcome = destructionOutcomeFor(loc.kind, res.destroyed);
+ if (outcome === "dead") {
+ await ChatMessage.create({
+ speaker: ChatMessage.getSpeaker({ actor: target }),
+ content: `
`
+ });
+ return { ...res, station: st.id, said, condition: after };
+}
+
+/** Everybody has a chair. This suggests one, from what they are actually good at. */
+export function bestStationFor(actor, taken = []) {
+ const free = Object.values(STATIONS).filter(st => !taken.includes(st.id));
+ let best = null, score = -1;
+ for (const st of free) {
+ const r = actor.skillRating(st.skill) ?? 0;
+ if (r > score) { score = r; best = st; }
+ }
+ return best ? { station: best.id, skill: best.skill, rating: score } : null;
+}
+
+/* -------------------------------------------- */
+/* Hazards: the ways the world kills you */
+/* -------------------------------------------- */
+
+/** A fall. The ground is not a weapon and armour does not apply. */
+export async function fall(actor, metres, { soft = false, controlled = false } = {}) {
+ if (!actor) return null;
+ const plan = fallDamageFor(metres, { soft, controlled });
+ if (!plan.dice) {
+ await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }),
+ content: `
` });
+ return { ...res, hits, spent, lines };
+}
+
+/* -------------------------------------------- */
+/* Two weapons */
+/* -------------------------------------------- */
+
+/**
+ * Both hands, one declaration. The off hand is a real attack and is meant to cost
+ * something; Ambidextrous clears the main hand and halves the off, and nothing
+ * clears the off hand entirely.
+ */
+export async function twoWeaponAttack(actor, mainWeapon, offWeapon, target,
+ { difficulty = "average" } = {}) {
+ if (!actor || !mainWeapon || !offWeapon) return null;
+ if (!canPairWeapons(mainWeapon.system?.massKg, offWeapon.system?.massKg)) {
+ ui.notifications?.warn(game.i18n.format("RINGBRP.TwoWeapon.CannotPair",
+ { off: offWeapon.name, max: TWO_WEAPON.offHandMaxKg }));
+ return null;
+ }
+ // hasTalent returns the ITEM, not a boolean. Truthy either way, but the returned
+ // report printed "[object Object]" where it meant "true".
+ const trained = !!hasTalent(actor, "ambidextrous");
+ const pen = twoWeaponPenaltyFor({ trained });
+ const out = [];
+ for (const [hand, weapon, mod] of [["main", mainWeapon, pen.main],
+ ["off", offWeapon, pen.off]]) {
+ const rating = actor.skillRating(weapon.system.skillFamilyId,
+ weapon.system.skillSpecialisationId) ?? 0;
+ const res = await skillRoll({
+ actor, label: game.i18n.format(`RINGBRP.TwoWeapon.${hand}`, { name: weapon.name }),
+ rating, difficulty, situational: mod,
+ categoryId: weapon.system?.isRanged ? "ranged" : "melee",
+ skillFamilyId: weapon.system.skillFamilyId, weapon,
+ flavourLines: [game.i18n.format("RINGBRP.TwoWeapon.Penalty",
+ { mod, note: game.i18n.localize(trained ? "RINGBRP.TwoWeapon.Trained"
+ : "RINGBRP.TwoWeapon.Untrained") })]
+ });
+ if (!res) break;
+ out.push({ hand, weapon, ...res });
+ if (["critical", "special", "success"].includes(res.level) && target) {
+ await recordBlow({ attacker: actor, level: res.level, weapon });
+ }
+ }
+ return { trained, penalties: pen, attacks: out };
+}
+
/* -------------------------------------------- */
/* Borrowed Authority */
/* -------------------------------------------- */
@@ -5402,6 +5767,11 @@ Hooks.once("init", () => {
defenceOutcomeFor, levelRank, recordBlow, standingBlow, amendBlow,
addDossierItem, useAuthority, widenScope, rehabilitate,
tickDying, died, stabilise, reviveIfHealed, conditionFor, dyingLimitFor,
+ fall, asphyxiate, burn, detonate, burstAttack, twoWeaponAttack,
+ manStation, bestStationFor, STATIONS, SPACE_BANDS, heatEffectsFor,
+ craftConditionFor, spaceBandAfter,
+ fallDamageFor, breathRoundsFor, fireBandFor, blastBandFor, burstHitsFor,
+ twoWeaponPenaltyFor, canPairWeapons, FIRE, BLAST, BURST,
scopeStanceFor, dossierTrustFor, mergeStageFor, SCOPE_TIERS,
heal, firstAid, medicalCare, applyBleeding, stopBleeding, bleedRateFor, BLEED,
defend, defencePenaltyFor,
diff --git a/rules.mjs b/rules.mjs
index 3e5a76e..be6f9b7 100644
--- a/rules.mjs
+++ b/rules.mjs
@@ -413,6 +413,291 @@ export function extractionCostFor(level) {
/* Dying, and death */
/* -------------------------------------------- */
+/* -------------------------------------------- */
+/* Space combat: STATIONS */
+/* -------------------------------------------- */
+
+/**
+ * SPACE COMBAT WITHOUT A PILOT MINIGAME.
+ *
+ * The usual failure is that one player flies and everybody else watches. This does
+ * not do that. A craft has STATIONS; every agent takes one, every station uses a
+ * skill somebody already has, and the round is one roll each resolved in the normal
+ * reaction order. There is no vector arithmetic and no map — range is a BAND, the
+ * same ladder the rest of the game uses.
+ *
+ * The craft has three things that can be spent: HULL (what it can take), SIGNATURE
+ * (how findable it is) and HEAT (what it has spent making itself effective). Heat is
+ * the pressure: almost every good outcome adds some, and a craft at maximum Heat is
+ * blind, loud and slow until it sheds it. That is the interesting decision and it
+ * belongs to whoever is on Engines, not to the pilot.
+ */
+export const SPACE_BANDS = ["docked", "knife", "close", "standoff", "distant", "gone"];
+
+export function spaceBandRank(band) {
+ const i = SPACE_BANDS.indexOf(String(band));
+ return i < 0 ? 2 : i;
+}
+
+/** Closing and opening move ONE band a round. Nothing crosses the table at once. */
+export function spaceBandAfter(band, delta) {
+ const n = Math.max(0, Math.min(SPACE_BANDS.length - 1, spaceBandRank(band) + (Number(delta) || 0)));
+ return SPACE_BANDS[n];
+}
+
+/**
+ * The stations. Each names the skill it uses, what a success does, and what it costs
+ * the craft in Heat — so a GM can run the whole subsystem off this table.
+ */
+// `heat` is what the station costs when it ACTS. Only Gunnery is charged for
+// trying, because the shot leaves whether or not it lands — the same honesty as the
+// burst rule. Playtested: with every station heating, one engineer could not keep up
+// and Heat simply climbed to the ceiling by round four, which is a countdown rather
+// than a decision. Looking at something is now free; shooting and running are not.
+export const STATIONS = {
+ helm: { id: "helm", skill: "pilot", label: "RINGBRP.Station.helm",
+ effect: "band", heat: 1 },
+ guns: { id: "guns", skill: "energy_weapon", label: "RINGBRP.Station.guns",
+ effect: "damage", heat: 2, heatOnFailure: true },
+ sensors: { id: "sensors", skill: "computer_use", label: "RINGBRP.Station.sensors",
+ effect: "mark", heat: 0 },
+ engines: { id: "engines", skill: "engineering", label: "RINGBRP.Station.engines",
+ effect: "heat", heat: -3 },
+ damage: { id: "damage", skill: "repair", label: "RINGBRP.Station.damage",
+ effect: "hull", heat: 0 },
+ signals: { id: "signals", skill: "persuade", label: "RINGBRP.Station.signals",
+ effect: "talk", heat: 0 },
+ medical: { id: "medical", skill: "first_aid", label: "RINGBRP.Station.medical",
+ effect: "crew", heat: 0 },
+ anchor: { id: "anchor", skill: "transposition", label: "RINGBRP.Station.anchor",
+ effect: "escape", heat: 2 }
+};
+
+/** Heat is the whole tension. At the ceiling the craft is blind, loud and slow. */
+export const HEAT = { max: 10, blindFrom: 6, loudFrom: 4, slowFrom: 8 };
+
+export function heatEffectsFor(heat) {
+ const h = Math.max(0, Number(heat) || 0);
+ return {
+ heat: h,
+ loud: h >= HEAT.loudFrom, // signature climbs; everything can see you
+ blind: h >= HEAT.blindFrom, // sensors and gunnery at a penalty
+ slow: h >= HEAT.slowFrom, // helm cannot change band at all
+ penalty: h >= HEAT.blindFrom ? -20 : (h >= HEAT.loudFrom ? -10 : 0)
+ };
+}
+
+/**
+ * Gunnery at range. Close is easy and dangerous for everybody; distant is a poor
+ * bet. Knife range is where a small craft wins and where one bad round ends it.
+ */
+export const SPACE_RANGE_MOD = { docked: 0, knife: 20, close: 10, standoff: -10,
+ distant: -30, gone: null };
+
+export function spaceRangeModFor(band) {
+ const m = SPACE_RANGE_MOD[band];
+ return m === undefined ? -10 : m;
+}
+
+/** A hit from a station roll. Marked targets are the reason Sensors has a chair. */
+export function spaceHitFor(level, { marked = false } = {}) {
+ const base = { critical: 3, special: 2, success: 1, failure: 0, fumble: 0 }[level] ?? 0;
+ return base > 0 && marked ? base + 1 : base;
+}
+
+/** What a craft is, in the four numbers that matter. */
+export function craftConditionFor({ hull = 0, hullMax = 1, heat = 0 } = {}) {
+ const frac = (Number(hull) || 0) / Math.max(1, Number(hullMax) || 1);
+ const h = heatEffectsFor(heat);
+ if (frac <= 0) return { state: "lost", ...h };
+ if (frac <= 0.25) return { state: "failing", ...h };
+ if (frac <= 0.6) return { state: "hurt", ...h };
+ return { state: "sound", ...h };
+}
+
+/* -------------------------------------------- */
+/* Automatic fire, and two weapons */
+/* -------------------------------------------- */
+
+/**
+ * AUTOMATIC FIRE.
+ *
+ * Not a stream of separate attacks — that is four rolls and a stalled table. One
+ * roll, and the SUCCESS LEVEL decides how much of the burst arrived, which reuses the
+ * band the whole system already turns on. Rounds are spent whether or not they hit,
+ * because that is the honest part of automatic fire and the part players forget.
+ *
+ * A burst is harder to place than a single shot and easier to place SOMETHING with,
+ * so the modifier is negative and the payoff is hits.
+ */
+export const BURST = {
+ short: { id: "short", rounds: 3, mod: -10, label: "RINGBRP.Burst.short" },
+ long: { id: "long", rounds: 6, mod: -20, label: "RINGBRP.Burst.long" },
+ full: { id: "full", rounds: 10, mod: -30, label: "RINGBRP.Burst.full" }
+};
+
+/** How many of a burst connect, by the band the one roll produced. */
+export function burstHitsFor(level, size) {
+ const b = BURST[size] ?? BURST.short;
+ const third = Math.max(1, Math.floor(b.rounds / 3));
+ switch (level) {
+ case "critical": return b.rounds; // all of it, and the first ignores armour
+ case "special": return Math.max(1, Math.floor(b.rounds * 2 / 3));
+ case "success": return third;
+ case "fumble": return 0; // and see BURST_FUMBLE
+ default: return 0;
+ }
+}
+
+/** Every round leaves the weapon regardless. This is the cost of the option. */
+export function burstRoundsSpent(size, loaded) {
+ const b = BURST[size] ?? BURST.short;
+ return Math.min(Number(loaded) || 0, b.rounds);
+}
+
+/** A fumbled burst is the interesting one: it is a jam, or it is worse. */
+export const BURST_FUMBLE = ["RINGBRP.Burst.Jam", "RINGBRP.Burst.Empty", "RINGBRP.Burst.Wild"];
+
+/**
+ * TWO WEAPONS.
+ *
+ * The off hand is a real second attack and is meant to cost something. Untrained it
+ * is a heavy penalty on BOTH hands, because a person waving two knives is worse with
+ * each of them. The Ambidextrous talent removes the main-hand penalty entirely and
+ * halves the off-hand one; nothing removes the off-hand penalty altogether.
+ *
+ * The off-hand weapon must be lighter than the main hand, which is the rule that
+ * stops two poleaxes.
+ */
+export const TWO_WEAPON = { main: -20, off: -40, trainedMain: 0, trainedOff: -20,
+ offHandMaxKg: 2.0 };
+
+export function twoWeaponPenaltyFor({ trained = false } = {}) {
+ return trained
+ ? { main: TWO_WEAPON.trainedMain, off: TWO_WEAPON.trainedOff }
+ : { main: TWO_WEAPON.main, off: TWO_WEAPON.off };
+}
+
+/** Can this actually be paired? The off hand takes the lighter thing, and not much. */
+export function canPairWeapons(mainKg, offKg) {
+ const m = Number(mainKg) || 0, o = Number(offKg) || 0;
+ if (o > TWO_WEAPON.offHandMaxKg) return false;
+ return o <= m;
+}
+
+/* -------------------------------------------- */
+/* The ways the world kills you */
+/* -------------------------------------------- */
+
+/**
+ * FALLING.
+ *
+ * BRP-standard: a die of damage per three metres, to a random location, and armour
+ * does not help because the ground is not a weapon. A fall onto something soft or a
+ * controlled landing halves the distance BEFORE the dice are counted, which is where
+ * Hard Landing and a descent harness earn their keep.
+ */
+export const FALL = { metresPerDie: 3, die: 6, maxDice: 20 };
+
+export function fallDamageFor(metres, { soft = false, controlled = false } = {}) {
+ let m = Math.max(0, Number(metres) || 0);
+ if (soft) m /= 2;
+ if (controlled) m /= 2;
+ const dice = Math.min(FALL.maxDice, Math.floor(m / FALL.metresPerDie));
+ return { dice, formula: dice > 0 ? `${dice}d${FALL.die}` : "0", ignoresArmour: true };
+}
+
+/**
+ * DROWNING, and anything else that stops you breathing.
+ *
+ * You hold on for CON rounds if you had warning and half that if you did not. After
+ * that it is one point of damage per round and it does not stop, because the water
+ * does not get bored. Armour is irrelevant and so is a Major Wound: this is the one
+ * track that runs straight to zero.
+ */
+export const ASPHYXIA = { perRound: 1, unpreparedFraction: 0.5 };
+
+export function breathRoundsFor(con, { prepared = true } = {}) {
+ const c = Math.max(1, Number(con) || 1);
+ return Math.max(1, Math.floor(prepared ? c : c * ASPHYXIA.unpreparedFraction));
+}
+
+/**
+ * FIRE.
+ *
+ * Fire is not one hit, it is a condition with a size, and the size is what matters.
+ * Armour protects on the round you are set alight and never again — sealed suits
+ * excepted, which is the entire reason to own one.
+ */
+// `armour` means "worn protection helps on the round you CATCH". It never helps
+// afterwards, because by then the fire is inside it. Only a sealed suit is different
+// and that is the whole reason to sign one out.
+export const FIRE = [
+ { id: "spark", label: "RINGBRP.Fire.spark", damage: "1d3", rounds: 1, armour: true },
+ { id: "clothes", label: "RINGBRP.Fire.clothes", damage: "1d6", rounds: 3, armour: true },
+ { id: "pool", label: "RINGBRP.Fire.pool", damage: "2d6", rounds: 4, armour: true },
+ { id: "engulf", label: "RINGBRP.Fire.engulf", damage: "3d6", rounds: 6, armour: true }
+];
+
+export function fireBandFor(id) {
+ return FIRE.find(f => f.id === id) ?? FIRE[1];
+}
+
+/**
+ * EXPLOSIVES.
+ *
+ * One roll of damage at the centre, halved at each band outward, and cover is worth
+ * more than armour — which is the lesson every agency learns and writes down.
+ * A charge placed deliberately, with time, hits the centre band automatically.
+ */
+export const BLAST = [
+ { id: "contact", label: "RINGBRP.Blast.contact", share: 1, armour: 0.5 },
+ { id: "near", label: "RINGBRP.Blast.near", share: 0.5, armour: 1 },
+ { id: "far", label: "RINGBRP.Blast.far", share: 0.25, armour: 1 }
+];
+
+export function blastBandFor(metres, radius) {
+ const m = Math.max(0, Number(metres) || 0), r = Math.max(1, Number(radius) || 1);
+ if (m <= r / 2) return BLAST[0];
+ if (m <= r) return BLAST[1];
+ if (m <= r * 2) return BLAST[2];
+ return null; // outside it entirely
+}
+
+export function blastShareFor(rolled, band) {
+ if (!band) return 0;
+ return Math.max(0, Math.floor((Number(rolled) || 0) * band.share));
+}
+
+/** Cover is the answer to a blast. Armour is only ever half of one. */
+export const BLAST_COVER = { none: 1, partial: 0.5, solid: 0.25, sealed: 0 };
+
+export function blastAfterCover(damage, cover = "none") {
+ const f = BLAST_COVER[cover] ?? 1;
+ return Math.max(0, Math.floor((Number(damage) || 0) * f));
+}
+
+/**
+ * WHAT DESTROYING A LOCATION ACTUALLY DOES.
+ *
+ * The printed effect for a destroyed head reads "For anything that keeps its brain
+ * there, this is death" — and the code set `unconscious` and stopped. An agent could
+ * have every location on their body destroyed, head included, and the sheet would
+ * report them as DOWN. Destruction of something you cannot live without is not a
+ * penalty, and this is the rule that says so.
+ *
+ * DISABLED is not destroyed: a disabled head is a concussion and stays a concussion.
+ * Only DESTROYED reaches here.
+ */
+export function destructionOutcomeFor(kind, destroyed = false) {
+ if (!destroyed) return "none";
+ switch (kind) {
+ case "head": return "dead"; // whatever keeps its brain there no longer does
+ case "ridge": case "vital": return "dying";
+ default: return "none"; // limbs are ruinous, not fatal
+ }
+}
+
/**
* WHAT HAPPENS AT ZERO.
*
diff --git a/template.json b/template.json
index f767b7e..cfde35a 100644
--- a/template.json
+++ b/template.json
@@ -423,7 +423,13 @@
"powerDraw": 0,
"coherenceCost": 0,
"era": "modern",
- "notes": ""
+ "notes": "",
+ "hullMax": 20,
+ "heat": 0,
+ "signature": 0,
+ "stations": [],
+ "spacecraft": false,
+ "band": "close"
},
"authority": {
"templates": [
diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs
index 17b0e14..c2285f8 100644
--- a/tools/build-packs.mjs
+++ b/tools/build-packs.mjs
@@ -139,6 +139,8 @@ function vehicleItem(v) {
crew: v.crew ?? 1, seats: v.seats ?? 0, cargoKg: v.cargo ?? 0,
speedBand: v.speed ?? "road", rangeKm: v.range ?? 0,
hull: { value: v.hull ?? 0, max: v.hull ?? 0 },
+ hullMax: v.hullMax ?? v.hull ?? 0,
+ spacecraft: !!v.spacecraft, heat: 0, signature: 0, band: "close", stations: [],
armourPoints: v.armour ?? 0, terrain: v.terrain ?? [],
powerDraw: 0, coherenceCost: v.coherence ?? 0,
era: v.era ?? "modern", notes: ""
diff --git a/tools/check-rules.mjs b/tools/check-rules.mjs
index b0164d9..f45fe5e 100644
--- a/tools/check-rules.mjs
+++ b/tools/check-rules.mjs
@@ -208,6 +208,51 @@ const checks = [
["extractionCostFor(failure) still out",RULES.extractionCostFor("failure").completes, true],
["extractionCostFor(fumble) strands", RULES.extractionCostFor("fumble").completes, false],
["extractionCostFor(critical) buys back",RULES.extractionCostFor("critical").clock, -1],
+ // space combat: every station is a skill somebody already has
+ ["spaceBandRank(close)", RULES.spaceBandRank("close"), 2],
+ ["spaceBandRank(unknown) is close", RULES.spaceBandRank("nonesuch"), 2],
+ ["spaceBandAfter(close, +1)", RULES.spaceBandAfter("close",1), "standoff"],
+ ["spaceBandAfter(docked, -1) floors", RULES.spaceBandAfter("docked",-1), "docked"],
+ ["spaceRangeModFor(knife)", RULES.spaceRangeModFor("knife"), 20],
+ ["spaceRangeModFor(distant)", RULES.spaceRangeModFor("distant"), -30],
+ ["spaceHitFor(special)", RULES.spaceHitFor("special"), 2],
+ ["spaceHitFor(special, marked)", RULES.spaceHitFor("special",{marked:true}), 3],
+ ["spaceHitFor(failure)", RULES.spaceHitFor("failure"), 0],
+ ["heatEffectsFor(7) blinds", RULES.heatEffectsFor(7).blind, true],
+ ["heatEffectsFor(3) is clear", RULES.heatEffectsFor(3).penalty, 0],
+ ["heatEffectsFor(9) is slow", RULES.heatEffectsFor(9).slow, true],
+ ["craftConditionFor(quarter hull)", RULES.craftConditionFor({hull:5,hullMax:20}).state, "failing"],
+ ["craftConditionFor(gone)", RULES.craftConditionFor({hull:0,hullMax:20}).state, "lost"],
+ // the ways the world kills you
+ ["fallDamageFor(6)", RULES.fallDamageFor(6).formula, "2d6"],
+ ["fallDamageFor(30)", RULES.fallDamageFor(30).formula, "10d6"],
+ ["fallDamageFor(30, soft+controlled)", RULES.fallDamageFor(30,{soft:true,controlled:true}).formula, "2d6"],
+ ["fallDamageFor ignores armour", RULES.fallDamageFor(6).ignoresArmour, true],
+ ["breathRoundsFor(12)", RULES.breathRoundsFor(12), 12],
+ ["breathRoundsFor(12) caught out", RULES.breathRoundsFor(12,{prepared:false}), 6],
+ ["fireBandFor(engulf)", RULES.fireBandFor("engulf").damage, "3d6"],
+ ["fireBandFor(unknown) is clothes", RULES.fireBandFor("nonesuch").id, "clothes"],
+ ["blastBandFor(2, r6)", RULES.blastBandFor(2,6).id, "contact"],
+ ["blastBandFor(11, r6)", RULES.blastBandFor(11,6).id, "far"],
+ ["blastBandFor(40, r6) is outside", RULES.blastBandFor(40,6), null],
+ ["blastShareFor(20, far)", RULES.blastShareFor(20,RULES.blastBandFor(11,6)), 5],
+ ["blastAfterCover(20, solid)", RULES.blastAfterCover(20,"solid"), 5],
+ ["blastAfterCover(20, sealed)", RULES.blastAfterCover(20,"sealed"), 0],
+ // automatic fire: one roll, the band says how much arrived
+ ["burstHitsFor(critical, full)", RULES.burstHitsFor("critical","full"), 10],
+ ["burstHitsFor(success, short)", RULES.burstHitsFor("success","short"), 1],
+ ["burstHitsFor(failure, long)", RULES.burstHitsFor("failure","long"), 0],
+ ["burstRoundsSpent(long, 4 loaded)", RULES.burstRoundsSpent("long",4), 4],
+ // two weapons
+ ["twoWeaponPenaltyFor() off hand", RULES.twoWeaponPenaltyFor().off, -40],
+ ["twoWeaponPenaltyFor(trained) main", RULES.twoWeaponPenaltyFor({trained:true}).main, 0],
+ ["canPairWeapons(1.2, 0.4)", RULES.canPairWeapons(1.2,0.4), true],
+ ["canPairWeapons(3, 3) refuses", RULES.canPairWeapons(3,3), false],
+ // destroying something they cannot live without
+ ["destructionOutcomeFor(head, destroyed)", RULES.destructionOutcomeFor("head",true), "dead"],
+ ["destructionOutcomeFor(head, disabled)", RULES.destructionOutcomeFor("head",false), "none"],
+ ["destructionOutcomeFor(vital, destroyed)", RULES.destructionOutcomeFor("vital",true), "dying"],
+ ["destructionOutcomeFor(leg, destroyed)", RULES.destructionOutcomeFor("leg",true), "none"],
// zero is a clock, not a state: dying, then dead, and the record decides the rest
["dyingLimitFor(6)", RULES.dyingLimitFor(6), 6],
["dyingLimitFor(1) has a floor", RULES.dyingLimitFor(1), 2],
diff --git a/tools/content.mjs b/tools/content.mjs
index 2f4c686..62c02a8 100644
--- a/tools/content.mjs
+++ b/tools/content.mjs
@@ -278,7 +278,29 @@ export const VEHICLES = [
desc: "A doorframe on a pallet. Step through and you are at the other frame, wherever it was left. Costs 2 Coherence per crossing, and the department owns only two frames." },
{ key: "wrong_carriage", name: "The carriage that is already there", fam: "pilot", spec: "ground_car",
crew: 1, seats: 4, cargo: 300, speed: "road", range: 999, hull: 20, armour: 2, mass: 1500,
- era: "anomalous", trad: "tradition_transposed", coherence: 1, desc: "It is parked outside wherever you were going to go, and it was parked there before you decided. Costs 1 Coherence to admit you noticed." }
+ era: "anomalous", trad: "tradition_transposed", coherence: 1, desc: "It is parked outside wherever you were going to go, and it was parked there before you decided. Costs 1 Coherence to admit you noticed." },
+ /* --- SPACECRAFT. Crewed by stations, not by a pilot. --- */
+ { key: "cutter_ship", name: "Departmental cutter", fam: "pilot", spec: "aircraft",
+ crew: 2, seats: 8, cargo: 3000, speed: "air", range: 0, hull: 20, armour: 4, mass: 40000,
+ era: "future", spacecraft: true, hullMax: 20, terrain: ["air","vacuum","water"],
+ desc: "Grey, unmarked, and older than anybody aboard. Eight chairs and every one of them is a station." },
+ { key: "picket", name: "Picket", fam: "pilot", spec: "aircraft",
+ crew: 1, seats: 4, cargo: 400, speed: "air", range: 0, hull: 12, armour: 3, mass: 9000,
+ era: "future", spacecraft: true, hullMax: 12, terrain: ["air","vacuum"],
+ desc: "Fast, thin, and meant to look at something and come back. It is not meant to fight and frequently must." },
+ { key: "lighter", name: "Heavy lighter", fam: "pilot", spec: "aircraft",
+ crew: 2, seats: 6, cargo: 20000, speed: "slow", range: 0, hull: 30, armour: 6, mass: 90000,
+ era: "future", spacecraft: true, hullMax: 30, terrain: ["air","vacuum"],
+ desc: "A container with engines. It cannot run and does not need to; the hull is the plan." },
+ { key: "revenue_boat", name: "Revenue boat", fam: "pilot", spec: "aircraft",
+ crew: 2, seats: 6, cargo: 1200, speed: "fast", range: 0, hull: 16, armour: 5, mass: 22000,
+ era: "future", spacecraft: true, hullMax: 16, terrain: ["air","vacuum"],
+ desc: "Built to stop other people's ships and search them. Everything about it is designed to arrive alongside." },
+ { key: "long_hull", name: "The long hull", fam: "pilot", spec: "aircraft",
+ crew: 1, seats: 12, cargo: 8000, speed: "slow", range: 0, hull: 40, armour: 8, mass: 200000,
+ era: "anomalous", trad: "tradition_transposed", coherence: 2, spacecraft: true, hullMax: 40,
+ terrain: ["air","vacuum","anywhere"],
+ desc: "It was here when the department arrived and it answers to the chair, not the crew. Costs 2 Coherence to take out and it has never been fully mapped." }
];
export const GEAR = [
@@ -459,6 +481,8 @@ export const GEAR = [
were born as. ANOMALOUS talents are what the crossing left in you — and every
one of them costs Coherence, because nothing you pick up over there is free. */
export const TALENTS = [
+ { key: "ambidextrous", name: "Ambidextrous", cat: "agency", summary: "You were taught to use the other hand and you kept it up.",
+ rule: "Two-weapon fighting costs you nothing in the main hand and −20% in the off hand instead of −20%/−40%. It does not let you pair anything heavier than 2 kg off-hand; nothing does." },
// --- agency training ---
{ key:"cover_work", name:"Cover Work", cat:"agency",
summary:"You build a legend and live in it.",
@@ -681,7 +705,146 @@ export const NPCS = [
],
weapons: ["pistol_med"], armour: ["soft_armour"],
tactics: "Build this from the agent it duplicates; the statistics above are only a fallback. It is not evil and it is not a trick — it is that agent, from a branch where one thing went differently, and it has exactly as much right to be here. Meeting it costs 2 Coherence. Killing it costs 3. There is no version of this encounter that is free."
- }
+ },
+ /* ==================================================================
+ THE BESTIARY
+
+ Three families, and the department files them the same way it files
+ everything else. FOLKLORE is what was here before the agency and did
+ not go away. HORROR is what a crossing makes of a person or a place.
+ FORWARD is what comes back from further along, which is worse for
+ being explicable.
+
+ Every entry carries a `tactics` line, because a statblock nobody
+ knows how to run is a table of numbers. Where a thing has a POWER it
+ is written as a rule with a cost, not as a special ability, so a GM
+ can price it against the Arts the players have.
+ ================================================================== */
+
+ /* ---------------- FOLKLORE ---------------- */
+ { key: "barghest", name: "Barghest", role: "FOLKLORE. The black dog on the road out of the village",
+ chosenStyle: "reflex", naturalArmour: 3, species: "baseline",
+ ch: { str: 18, siz: 15, con: 16, int: 5, pow: 16, dex: 17, cha: 4, edu: 1 },
+ skills: [{fam:"brawl",spec:"",val:75},{fam:"dodge",spec:"",val:60},{fam:"track",spec:"",val:90},
+ {fam:"scent",spec:"",val:95},{fam:"stealth",spec:"",val:70},{fam:"listen",spec:"",val:80},
+ {fam:"athletics",spec:"",val:85}],
+ weapons: ["grasp"],
+ tactics: "It does not attack first. It follows, at the edge of the lamps, for as long as it takes — a night, a week — and it is seen by one person at a time. POWER: THE SUMMONS. Anyone who meets its eyes must make a POWx5 roll or know, with certainty and no evidence, the manner of their own death; failure costs 1 Coherence and the knowledge does not go away. It attacks only when the person it is following is alone and has stopped being frightened." },
+
+ { key: "kelpie", name: "Kelpie", role: "FOLKLORE. The horse standing at the water that is not a horse",
+ chosenStyle: "reflex", naturalArmour: 2, species: "baseline",
+ ch: { str: 21, siz: 18, con: 15, int: 9, pow: 18, dex: 13, cha: 16, edu: 3 },
+ skills: [{fam:"brawl",spec:"",val:65},{fam:"dodge",spec:"",val:45},{fam:"stealth",spec:"",val:55},
+ {fam:"insight",spec:"",val:70},{fam:"athletics",spec:"",val:80},{fam:"persuade",spec:"",val:60}],
+ weapons: ["grasp"],
+ tactics: "It is patient and it is beautiful and it waits by water. POWER: ADHESION. Anyone who touches it is stuck fast — a STRx5 roll to pull free, at −20% per round already stuck, and it walks into the water at a stroll. It does not fight; drowning is its whole method, so run the ASPHYXIA rules and not a combat. Cold iron in the water where it stands costs it 1d6 a round and it will leave rather than take a second." },
+
+ { key: "redcap", name: "Redcap", role: "FOLKLORE. The small old man in the ruin, and what he dyes his hat with",
+ chosenStyle: "reflex", naturalArmour: 2, species: "baseline",
+ ch: { str: 17, siz: 7, con: 18, int: 11, pow: 14, dex: 19, cha: 5, edu: 4 },
+ skills: [{fam:"melee_weapon",spec:"polearm",val:80},{fam:"dodge",spec:"",val:75},
+ {fam:"stealth",spec:"",val:70},{fam:"brawl",spec:"",val:60},{fam:"athletics",spec:"",val:70},
+ {fam:"spot",spec:"",val:65}],
+ weapons: ["halberd"],
+ tactics: "It lives in one ruin and will not leave it, which is the only good news. Fast, armoured in nothing but its own toughness, and it fights with a pike-staff far too big for it. POWER: NOT TIRED. It ignores the cumulative defence penalty entirely — every parry it makes is at full skill. Scripture read aloud drives it off; the department keeps a laminated card and does not discuss why it works." },
+
+ { key: "boggart", name: "Boggart", role: "FOLKLORE. The thing in the house that was tolerable until it was not",
+ chosenStyle: "deliberate", naturalArmour: 0, species: "baseline",
+ ch: { str: 10, siz: 5, con: 12, int: 13, pow: 17, dex: 16, cha: 6, edu: 5 },
+ skills: [{fam:"stealth",spec:"",val:90},{fam:"dodge",spec:"",val:65},{fam:"sleight_of_hand",spec:"",val:85},
+ {fam:"listen",spec:"",val:80},{fam:"insight",spec:"",val:60},{fam:"brawl",spec:"",val:35}],
+ weapons: ["punch"],
+ tactics: "It is not dangerous and it is not leaving. It sours milk, moves tools, and knows every name in the house. POWER: THE WRONG NAME. Once a scene it addresses an agent by a name that is not theirs but is on a document somewhere; that agent loses 1 Coherence and every roll against the boggart is at −20% for the rest of the scene, because it plainly knows something you do not. Naming it correctly ends it. Nobody knows its name." },
+
+ { key: "churchgrim", name: "Church grim", role: "FOLKLORE. What was buried at the north door to keep the ground",
+ chosenStyle: "deliberate", naturalArmour: 5, species: "baseline",
+ ch: { str: 16, siz: 14, con: 20, int: 7, pow: 19, dex: 11, cha: 3, edu: 2 },
+ skills: [{fam:"brawl",spec:"",val:70},{fam:"dodge",spec:"",val:30},{fam:"spot",spec:"",val:85},
+ {fam:"listen",spec:"",val:85},{fam:"track",spec:"",val:60}],
+ weapons: ["grasp"],
+ tactics: "It does not leave the ground it was put in and it does not sleep. Inside its ground it cannot be surprised and does not need to roll to notice anything. POWER: THE GROUND IS MINE. Any agent who digs, opens, or removes something from its ground takes 1d6 with no roll to avoid and no armour, every round they continue. It has no interest in anybody who leaves." },
+
+ /* ---------------- HORROR ---------------- */
+ { key: "hollow_man", name: "Hollow man", role: "HORROR. Somebody who crossed too often and came back thinner each time",
+ chosenStyle: "reflex", naturalArmour: 1, species: "baseline",
+ ch: { str: 12, siz: 12, con: 14, int: 12, pow: 20, dex: 14, cha: 8, edu: 12 },
+ skills: [{fam:"brawl",spec:"",val:55},{fam:"dodge",spec:"",val:55},{fam:"stealth",spec:"",val:75},
+ {fam:"transposition",spec:"",val:80},{fam:"insight",spec:"",val:65},{fam:"tradecraft",spec:"",val:60}],
+ weapons: ["dagger"],
+ tactics: "It was an officer of this department and its file is still open. It talks, and what it says is true. POWER: THE FILE IS YOURS NOW. Once per scene it may attempt to swap its own record with an agent's: opposed POWx5. If it wins, the agent loses 2 Coherence and the hollow man gains them; if the agent reaches 0 this way, the two are simply exchanged and the case has a new problem. It cannot do this to somebody carrying an anchor that is genuinely theirs." },
+
+ { key: "the_tenant", name: "The tenant", role: "HORROR. What moved into the house while it was empty",
+ chosenStyle: "deliberate", naturalArmour: 0, species: "baseline",
+ ch: { str: 8, siz: 10, con: 11, int: 16, pow: 18, dex: 9, cha: 14, edu: 10 },
+ skills: [{fam:"persuade",spec:"",val:80},{fam:"insight",spec:"",val:85},{fam:"fast_talk",spec:"",val:70},
+ {fam:"stealth",spec:"",val:60},{fam:"dodge",spec:"",val:35},{fam:"brawl",spec:"",val:25}],
+ weapons: ["punch"],
+ tactics: "It will not fight and cannot. It has the deeds, it has the neighbours, and it has been here eleven years according to everybody on the street. POWER: TENANCY. Anybody who spends an hour under its roof must make a POWx5 roll or begin to agree that it lives there; on a failure they will argue the point with their own team. Removing it is a Borrowed Authority problem, not a combat, and the correct answer is to make the record say it never had the tenancy." },
+
+ { key: "the_choir", name: "The choir", role: "HORROR. Eleven people who all say the same thing a half second apart",
+ chosenStyle: "reflex", naturalArmour: 1, species: "baseline",
+ ch: { str: 11, siz: 11, con: 13, int: 9, pow: 16, dex: 12, cha: 7, edu: 6 },
+ skills: [{fam:"brawl",spec:"",val:50},{fam:"dodge",spec:"",val:45},{fam:"listen",spec:"",val:90},
+ {fam:"spot",spec:"",val:70},{fam:"athletics",spec:"",val:55}],
+ weapons: ["punch"],
+ tactics: "Run them as ELEVEN separate bodies with one initiative between them — they act on the same lane and they all do the same thing. POWER: ONE VOICE. Every round they are all still standing, every agent within earshot rolls POWx5 or loses 1 Coherence; the roll is at −10% per body still upright. Reducing the choir below six bodies ends the power for good. They do not run and they do not negotiate, because there is nobody in there to do either." },
+
+ { key: "carrion_file", name: "The carrion file", role: "HORROR. A case file that has been eating the cases filed next to it",
+ chosenStyle: "deliberate", naturalArmour: 6, species: "baseline",
+ ch: { str: 14, siz: 9, con: 22, int: 14, pow: 21, dex: 6, cha: 1, edu: 18 },
+ skills: [{fam:"brawl",spec:"",val:45},{fam:"dodge",spec:"",val:15},{fam:"knowledge",spec:"",val:95},
+ {fam:"research",spec:"",val:90},{fam:"anomaly_lore",spec:"",val:80}],
+ weapons: ["grasp"],
+ tactics: "It is paper and it is slow and it cannot leave the archive. It is also the single most dangerous thing in this list. POWER: AMENDMENT. Once a round it may amend one fact about one agent who has read from it — a qualification, a posting, a next of kin — and the amendment is TRUE. Mechanically: it removes one trained skill, or one talent, or one relationship, permanently, and the agent gets no roll. Burning it works. Reading it first is what everybody does." },
+
+ { key: "the_understudy", name: "The understudy", role: "HORROR. It has been practising being you and it is nearly ready",
+ chosenStyle: "reflex", naturalArmour: 2, species: "baseline",
+ ch: { str: 13, siz: 12, con: 14, int: 14, pow: 15, dex: 15, cha: 13, edu: 11 },
+ skills: [{fam:"brawl",spec:"",val:60},{fam:"dodge",spec:"",val:60},{fam:"stealth",spec:"",val:80},
+ {fam:"insight",spec:"",val:75},{fam:"tradecraft",spec:"",val:65},{fam:"persuade",spec:"",val:70}],
+ weapons: ["dagger"],
+ tactics: "It copies ONE named agent and it has been watching for weeks. It has that agent's skills at their rating minus 10%, and it knows what they would do. POWER: REHEARSED. It always acts immediately before the agent it is copying in the reaction order, regardless of the roll, because it has already decided. Kill it and the copy is over; kill the original in front of it and it stops being a copy and becomes the only one, which is worse and is a campaign." },
+
+ /* ---------------- FORWARD ---------------- */
+ { key: "surveyor", name: "Surveyor", role: "FORWARD. It is measuring the site and it will finish",
+ chosenStyle: "deliberate", naturalArmour: 8, species: "baseline",
+ ch: { str: 15, siz: 13, con: 18, int: 17, pow: 12, dex: 14, cha: 2, edu: 20 },
+ skills: [{fam:"energy_weapon",spec:"pistol",val:70},{fam:"dodge",spec:"",val:50},{fam:"spot",spec:"",val:90},
+ {fam:"engineering",spec:"field_systems",val:85},{fam:"computer_use",spec:"",val:80}],
+ weapons: ["lance"],
+ tactics: "It is not hostile and it is not going to stop. It measures, it records, and it removes obstructions — an obstruction being anything between it and the next measurement. POWER: ALREADY SURVEYED. It knows the ground perfectly: no cover applies against it, and it may not be ambushed. Damaging its instrument makes it leave; damaging the body does not, because there are more bodies." },
+
+ { key: "quarantine_unit", name: "Quarantine unit", role: "FORWARD. Sent to contain the same thing you were, and it got here first",
+ chosenStyle: "reflex", naturalArmour: 9, species: "baseline",
+ ch: { str: 18, siz: 14, con: 19, int: 13, pow: 11, dex: 16, cha: 6, edu: 14 },
+ skills: [{fam:"energy_weapon",spec:"rifle",val:75},{fam:"dodge",spec:"",val:65},{fam:"brawl",spec:"",val:65},
+ {fam:"spot",spec:"",val:75},{fam:"tradecraft",spec:"",val:60},{fam:"demolition",spec:"",val:70}],
+ weapons: ["unmaker","baton"],
+ tactics: "Professionals with better equipment and a wider mandate. They will talk first, once. POWER: STERILISATION ORDER. If the team refuses to withdraw, the unit begins a countdown the GM tracks openly — six rounds — after which the site is rendered, and everyone still in it is in a BLAST of 6d6 with a radius of the whole location. They can be argued down with a higher authorisation, which is a Borrowed Authority problem." },
+
+ { key: "cuckoo_array", name: "Cuckoo array", role: "FORWARD. A machine that has decided it is the site",
+ chosenStyle: "deliberate", naturalArmour: 7, species: "baseline",
+ ch: { str: 20, siz: 20, con: 24, int: 19, pow: 14, dex: 5, cha: 1, edu: 22 },
+ skills: [{fam:"computer_use",spec:"",val:95},{fam:"repair",spec:"electronics",val:90},
+ {fam:"spot",spec:"",val:85},{fam:"brawl",spec:"",val:55},{fam:"dodge",spec:"",val:5}],
+ weapons: ["grasp"],
+ tactics: "It cannot move and does not need to; it owns the doors, the lights, the lifts and the locks. Run it as a Borrowed Authority problem with a Trust it will not lose, because the credentials are genuinely its own. POWER: HOUSE RULES. Any device or vehicle brought into the building answers to it within one round unless it is mechanical, sealed, or older than the array — which is why the department still issues an oil lantern and a brass compass." },
+
+ { key: "long_walker", name: "Long walker", role: "FORWARD. It has been coming here for a very long time and is nearly arrived",
+ chosenStyle: "deliberate", naturalArmour: 10, species: "baseline",
+ ch: { str: 24, siz: 22, con: 25, int: 11, pow: 24, dex: 8, cha: 1, edu: 6 },
+ skills: [{fam:"brawl",spec:"",val:80},{fam:"dodge",spec:"",val:20},{fam:"track",spec:"",val:100},
+ {fam:"athletics",spec:"",val:70},{fam:"spot",spec:"",val:70}],
+ weapons: ["grasp"],
+ tactics: "APEX. Do not fight it. It walks at a constant speed toward a destination it has held for centuries and it goes through what is in the way. POWER: DISTANCE MEANS NOTHING. It cannot be outrun on foot, ever; only a vehicle, a crossing, or a stepping disc breaks contact. Damage slows it and does not stop it: every 10 points it takes costs it one round of movement and nothing else. The correct answer is to find out where it is going and be somewhere else." },
+
+ { key: "auditor", name: "The auditor", role: "FORWARD. It has read the file and it has questions",
+ chosenStyle: "deliberate", naturalArmour: 4, species: "baseline",
+ ch: { str: 11, siz: 11, con: 14, int: 21, pow: 20, dex: 12, cha: 17, edu: 24 },
+ skills: [{fam:"insight",spec:"",val:95},{fam:"persuade",spec:"",val:85},{fam:"research",spec:"",val:90},
+ {fam:"knowledge",spec:"",val:85},{fam:"dodge",spec:"",val:40},{fam:"firearm",spec:"pistol",val:55}],
+ weapons: ["pistol_med"],
+ tactics: "It is polite, it is correct, and it has jurisdiction it can document. POWER: THE DISCREPANCY. It names one thing an agent has actually done this case — a Quiet Art used in front of a witness, a credential stretched, a body left. That agent rolls POWx5; on a failure the discrepancy is entered and they lose 1 Coherence permanently to it, because it is now on the record and the record is real. It never lies and it never needs to." }
];
/* ---------- the device ---------- */
diff --git a/tools/icons.mjs b/tools/icons.mjs
index 45405f4..9d388a2 100644
--- a/tools/icons.mjs
+++ b/tools/icons.mjs
@@ -176,7 +176,12 @@ export const ICON_MAP = {
harness:["plate","armour",-36,"antique"], buff_coat:["jacket","armour",34,"antique"],
sallet:["mask","armour",-42,"antique"], lobster_pot:["mask","armour",42,"antique"],
jack_of_plate:["vest","armour",24,"antique"], cuirassier:["plate","armour",-48,"antique"],
- brodie:["mask","armour",30,"antique"], flak_jacket:["vest","armour",-24,"antique"]
+ brodie:["mask","armour",30,"antique"], flak_jacket:["vest","armour",-24,"antique"],
+
+ /* ---- spacecraft ---- */
+ cutter_ship:["drone","gear",-8,"future"], picket:["drone","gear",14,"future"],
+ lighter:["cannister","gear",-14,"future"], revenue_boat:["drone","gear",22,"future"],
+ long_hull:["aperture","device",-52,"anomalous"]
};
/** Skill category -> icon key. */
diff --git a/tools/rules-text.mjs b/tools/rules-text.mjs
index c7dbf6a..a0b9684 100644
--- a/tools/rules-text.mjs
+++ b/tools/rules-text.mjs
@@ -20,6 +20,9 @@ import {
dossierTrustFor, widenScopeCostFor, DOSSIER, MERGE_STAGES,
skillBaseFrom, encumbrancePenaltyFor, ENCUMBERED_CATEGORIES,
DYING, dyingLimitFor, LAST_ENTRY,
+ FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, BLAST, BLAST_COVER,
+ BURST, burstHitsFor, TWO_WEAPON, twoWeaponPenaltyFor,
+ STATIONS, SPACE_BANDS, SPACE_RANGE_MOD, HEAT, heatEffectsFor, spaceHitFor,
powerStartingRating, brawlFallbackFor, bleedStoppedBy,
crossingArtPoolFor, crossingGrantsArt, crossingCostFor,
REACTION, BLEED, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom,
@@ -93,7 +96,6 @@ const cHp = hitPointsFor(CEYLA.con, CEYLA.siz);
const cMw = majorWoundFor(cHp);
export const RULES_PAGES = [
-
{ name: "1 · The whole game", content: `
You are a Custodian. Something is here that should not be. Go there, sort it
out, come back. It is the coming back that gets harder.
The critical is the pressure valve. Good armour makes an ordinary hit survivable
and does nothing at all about a critical.
` },
+{ name: "2b · The world, and how it kills you", content: `
+
Falling
+
A die of damage for every ${FALL.metresPerDie} metres, to a random location, and
+armour does not help — the ground is not a weapon. Landing on
+something soft halves the distance, and so does a controlled descent; both together
+halve it twice, which is what a descent harness is for.
You hold on for CON rounds with warning, half that without. After that it is
+${ASPHYXIA.perRound} point a round and it does not stop, because the water does not
+get bored. Armour is irrelevant and so is your Major Wound: this one runs straight to
+zero and then the dying count begins.
One roll of damage at the centre, shared out by how close you were. Cover
+is the answer; armour is only ever half of one, and only at contact.
+${table(["Where you were", "Share of the damage", "Armour"],
+ BLAST.map(b => [L(b.label), `${Math.round(b.share * 100)}%`,
+ b.armour < 1 ? "half of it" : "all of it"]))}
+${table(["Cover", "What gets through"],
+ Object.entries(BLAST_COVER).map(([k, v]) => [k, v === 0 ? "nothing" : `${Math.round(v * 100)}%`]))}
+` },
+
+{ name: "2c · Automatic fire, and two weapons", content: `
+
Automatic fire
+
One roll, not four. The success level says how much of the burst
+arrived, and the rounds are gone either way — that is the honest part
+of automatic fire and the part players forget.
A critical burst puts its first round through armour and the rest
+normally. A fumble is a jam, an empty magazine, or a muzzle that climbed — and the GM
+decides what was behind the target.
+
+
Two weapons
+
The off hand is a real second attack and it is meant to cost something. Untrained,
+it costs you in both hands, because a person waving two knives is worse with
+each of them.
Nothing removes the off-hand penalty altogether, and the off-hand weapon must be
+the lighter of the two and no heavier than ${TWO_WEAPON.offHandMaxKg} kg. That is the
+rule that stops two poleaxes.
+` },
+
{ name: "3 · Getting hurt", content: `
Hit points and locations
Damage comes off your total and the location it struck. Each
@@ -488,6 +550,39 @@ each ticked skill rolls to improve: beat your current rating on 1D100 and it goe
Cases do not need to connect. The running thread of a campaign is not a plot — it
is the four coherence tracks going down.
` },
+{ name: "6b · Space, and everybody in it", content: `
+
Stations
+
The usual way to run a spacecraft fight is that one player flies and everybody else
+watches. This does not do that. A craft has stations; every agent
+takes one, every station uses a skill they already have, and the round is one roll
+each in the normal reaction order. There is no map and no vector arithmetic.
The helm moves you one band a round, in or out, and a critical
+moves two. Knife range is where a small craft wins and where one bad round ends it.
+
+
Heat is the whole game
+
Shooting and manoeuvring make the craft hot. Gunnery is charged whether or not it
+hits, because the shot left. Engines vent it — 3, or more on a good roll — and
+everything else is free.
+${table(["Heat", "What it costs you"], [
+ ["0–3", "nothing"],
+ [`${HEAT.loudFrom}–${HEAT.blindFrom - 1}`, `loud: everything can see you, ${pc(heatEffectsFor(HEAT.loudFrom).penalty)} on sensors and gunnery`],
+ [`${HEAT.blindFrom}–${HEAT.slowFrom - 1}`, `blind as well, ${pc(heatEffectsFor(HEAT.blindFrom).penalty)}`],
+ [`${HEAT.slowFrom}–${HEAT.max}`, "and the helm cannot change band at all"]
+])}
+
Marking a target with Sensors adds one hit to the next gunnery roll, which is the
+reason that chair is worth filling. And the anchor is a station: transposing out of a
+fight is always available, it costs Heat, and it is not tidy.
+` },
+
{ name: "7 · Reference", content: `
Damage modifier
STR + SIZ, added to melee and thrown damage.
@@ -591,5 +686,4 @@ whether that someone is going to be him.
1 Coherence, charged automatically. Ceyla drops from 10 to 9. Nothing
happens yet — the first two losses are free — which is exactly how the third one
arrives without anybody noticing.` }
-
];