Death at zero, and a case brief that answers the useful questions
R-223: nothing happened at 0 hit points. An agent was marked down and lay there forever; the only mention of death in the system was flavour text on a destroyed head. Zero now starts a clock whose length is the agent's own Major Wound threshold, ticked from the same hook that charges blood loss. First Aid or Medicine stabilises and stops the count; healing above zero clears it; heal() refuses the dead. On death, Coherence decides what the record says — from a proper death in service down to not being recorded as dead at all. Case generator: four phases instead of five (the arc drew two interchangeable middles that read as padding). The brief now leads with Where / When / Classification / Phases, states the opposition in full, and has a "How you get back" section. place and duration were generated and never printed. The same facts are on the case file itself. 300 seeds checked for object leaks; one found and fixed (group taboo/desire are objects with .text). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a360d77b83
commit
89ce557355
@@ -3799,3 +3799,64 @@ floor the old ramp produced.
|
||||
**R-222 confirmed across the board.** Every agent came out of this case with ticks —
|
||||
Dodge, First Aid, Firearm (Pistol), Firearm (Rifle), Navigate — where before the fix
|
||||
a case produced none at all from phase work.
|
||||
|
||||
## Zero hit points now means something
|
||||
|
||||
**R-223 · nothing happened at zero.** An agent reduced to 0 hit points was marked down
|
||||
and then lay there indefinitely. Bleeding deliberately stops at zero, no rule advanced
|
||||
anything, and the only mention of death in the entire system was flavour text on a
|
||||
destroyed head location. A game with hit locations, Major Wounds and bleeding had no
|
||||
way for anyone to die.
|
||||
|
||||
Hit points are not taken below zero — every damage path clamps there and unpicking
|
||||
that would touch everything. Zero starts a clock instead:
|
||||
|
||||
above 2 up
|
||||
2 or below DOWN — out of the fight, still conscious, still yours
|
||||
0 DYING — unconscious, no actions, no defences, the count begins
|
||||
count met DEAD
|
||||
|
||||
The count is the agent's own **Major Wound threshold**, so the number that already
|
||||
decides how much damage they take standing up also decides how long they have on the
|
||||
floor, with a floor of two rounds. It ticks from the same end-of-round hook that
|
||||
charges blood loss, so nobody has to remember it — which is exactly why bleeding
|
||||
worked and dying did not exist.
|
||||
|
||||
**Stabilising** takes First Aid or Medicine and stops the count. It does not put
|
||||
anyone back on their feet: still zero, still unconscious, until somebody heals them
|
||||
above it, which clears the count automatically. A FUMBLED attempt over a dying agent
|
||||
costs them a round they do not have. `heal()` now refuses the dead, which is the one
|
||||
place that had to say so because every medical path routes through it.
|
||||
|
||||
**The last entry.** A Custodian's death is a filing question. How well filed they were
|
||||
decides what the record says: at Coherence 7+ a death in service with a form and a
|
||||
pension; at 4–6 two dates of death on file and Registry asking which is correct; at
|
||||
1–3 a file that will not close, with correspondence still arriving and being answered;
|
||||
at 0 they are not recorded as dead at all but filed as transferred, to a section the
|
||||
department cannot produce an address for.
|
||||
|
||||
Four rules, ten spot-checks, on the sheet as a banner in all three states, and
|
||||
documented in the rulebook. 44 rules, 148 formulas.
|
||||
|
||||
## The case generator, simplified and made to say the useful things
|
||||
|
||||
**Four phases, not five.** The arc drew TWO interchangeable middle phases, which read
|
||||
as padding — two seeds in a row produced "The search" then "Inside" with nothing to
|
||||
tell them apart. One middle phase: Approach, the thing in the way, the job, and out.
|
||||
|
||||
**The brief now answers the operational questions first.** It opened on a
|
||||
classification note and never stated where the site was, how long the team had, what
|
||||
was likely to be in the way, or how they were getting out — all of which the generator
|
||||
already knew and threw away. `place` and `duration` were generated and never printed.
|
||||
The brief now leads with a table of Where / When / Classification / Phases, then the
|
||||
job, then what is reported, then a full section on the opposition — numbers, tier,
|
||||
motive, tactics, and what might work instead of shooting — then **How you get back**:
|
||||
the extraction ground, the Transposition roll, the classification modifier, and the
|
||||
warning that a bad crossing is the only way an Art is ever acquired.
|
||||
|
||||
The same four facts are now on the CASE FILE ITSELF, so a GM does not have to open the
|
||||
journal to answer "where are we and how long have we got".
|
||||
|
||||
Checked across 300 seeds for `[object Object]`, `undefined` and `NaN` in every
|
||||
generated page: clean. One real leak found and fixed on the way — the group's taboo
|
||||
and desire are objects with a `.text`, and the brief was printing the object.
|
||||
|
||||
@@ -624,6 +624,28 @@
|
||||
"RINGBRP.Wound.variant.mobility": "mobility",
|
||||
"RINGBRP.Heal.Bleeds": "{name} bleeds {amount}.",
|
||||
"RINGBRP.Heal.BledOut": "bled out",
|
||||
"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.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.",
|
||||
"RINGBRP.Death.Entry.queried": "Recorded, and queried. Two dates of death are on file and Registry has asked which is correct. Nobody has answered.",
|
||||
"RINGBRP.Death.Entry.unclosed": "The file will not close. Correspondence continues to arrive for them, and it is answered.",
|
||||
"RINGBRP.Death.Entry.transferred": "Not recorded as dead at all. They are filed as transferred, to a section the department cannot produce an address for. Somebody will meet them again.",
|
||||
"RINGBRP.Death.Stabilising": "Stabilising {name}",
|
||||
"RINGBRP.Death.Stabilised": "{name} is stable. Still at zero, still unconscious, but the count has stopped.",
|
||||
"RINGBRP.Death.NotStabilised": "It does not hold. {name} is still going.",
|
||||
"RINGBRP.Death.TooLate": "They are dead. There is nothing medical left to do.",
|
||||
"RINGBRP.Death.NotDying": "They are not dying. Ordinary First Aid will serve.",
|
||||
"RINGBRP.Death.WrongSkill": "Stabilising takes one of: {list}.",
|
||||
"RINGBRP.Condition.up": "up",
|
||||
"RINGBRP.Condition.down": "down",
|
||||
"RINGBRP.Condition.dying": "DYING",
|
||||
"RINGBRP.Condition.dead": "DEAD",
|
||||
"RINGBRP.Sheet.Stabilise": "Stabilise",
|
||||
"RINGBRP.Heal.BleedStopped": "The bleeding is staunched. {name} is no longer losing blood.",
|
||||
"RINGBRP.Heal.Medicine": "Medicine",
|
||||
"RINGBRP.React.ReadinessSkill": "from training",
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.033759 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.033780 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.033929 175517000 Level-0 table #5: 1902 bytes OK
|
||||
2026/08/10-21:10:35.034226 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.034283 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.167695 172fa3000 Recovering log #3
|
||||
2026/08/10-22:10:05.167719 172fa3000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.167831 172fa3000 Level-0 table #5: 1902 bytes OK
|
||||
2026/08/10-22:10:05.168131 172fa3000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.168177 172fa3000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.742472 172977000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.857234 172fe3000 Delete type=3 #1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.042390 175d23000 Recovering log #3
|
||||
2026/08/10-21:10:35.042410 175d23000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.042554 175d23000 Level-0 table #5: 4129 bytes OK
|
||||
2026/08/10-21:10:35.042821 175d23000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.042870 175d23000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.175210 172797000 Recovering log #3
|
||||
2026/08/10-22:10:05.175229 172797000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.175333 172797000 Level-0 table #5: 4129 bytes OK
|
||||
2026/08/10-22:10:05.175537 172797000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.175564 172797000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.766828 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.883542 172fe3000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.034917 175d23000 Recovering log #3
|
||||
2026/08/10-21:10:35.035011 175d23000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.035308 175d23000 Level-0 table #5: 27954 bytes OK
|
||||
2026/08/10-21:10:35.035591 175d23000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.035641 175d23000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.168741 172797000 Recovering log #3
|
||||
2026/08/10-22:10:05.168838 172797000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.169097 172797000 Level-0 table #5: 27954 bytes OK
|
||||
2026/08/10-22:10:05.169298 172797000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.169329 172797000 Delete type=3 #2
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.744343 17419b000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.859443 1727d7000 Delete type=3 #1
|
||||
|
||||
Binary file not shown.
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.025087 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.025512 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.026438 175517000 Level-0 table #5: 60660 bytes OK
|
||||
2026/08/10-21:10:35.026776 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.026817 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.160943 173fbb000 Recovering log #3
|
||||
2026/08/10-22:10:05.161166 173fbb000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.161661 173fbb000 Level-0 table #5: 60660 bytes OK
|
||||
2026/08/10-22:10:05.161946 173fbb000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.162005 173fbb000 Delete type=3 #2
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.754647 172977000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.870862 172fe3000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.031149 1744ff000 Recovering log #3
|
||||
2026/08/10-21:10:35.031171 1744ff000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.031288 1744ff000 Level-0 table #5: 3295 bytes OK
|
||||
2026/08/10-21:10:35.031521 1744ff000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.031553 1744ff000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.165461 172fa3000 Recovering log #3
|
||||
2026/08/10-22:10:05.165486 172fa3000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.165592 172fa3000 Level-0 table #5: 3295 bytes OK
|
||||
2026/08/10-22:10:05.165831 172fa3000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.165879 172fa3000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.760755 17419b000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.877345 172fe3000 Delete type=3 #1
|
||||
|
||||
Binary file not shown.
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.021326 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.021545 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.022420 175517000 Level-0 table #5: 54921 bytes OK
|
||||
2026/08/10-21:10:35.022844 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.022893 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.158567 173fbb000 Recovering log #3
|
||||
2026/08/10-22:10:05.158795 173fbb000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.159257 173fbb000 Level-0 table #5: 54921 bytes OK
|
||||
2026/08/10-22:10:05.159544 173fbb000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.159580 173fbb000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.750048 17419b000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.866210 171fcb000 Delete type=3 #1
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.037687 175d23000 Recovering log #3
|
||||
2026/08/10-21:10:35.037745 175d23000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.038003 175d23000 Level-0 table #5: 23316 bytes OK
|
||||
2026/08/10-21:10:35.038279 175d23000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.038312 175d23000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.171339 172797000 Recovering log #3
|
||||
2026/08/10-22:10:05.171403 172797000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.171720 172797000 Level-0 table #5: 24552 bytes OK
|
||||
2026/08/10-22:10:05.172072 172797000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.172108 172797000 Delete type=3 #2
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.765165 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.881569 172fe3000 Delete type=3 #1
|
||||
|
||||
Binary file not shown.
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.028347 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.028398 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.028587 175517000 Level-0 table #5: 6679 bytes OK
|
||||
2026/08/10-21:10:35.028935 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.029009 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.163199 173fbb000 Recovering log #3
|
||||
2026/08/10-22:10:05.163244 173fbb000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.163412 173fbb000 Level-0 table #5: 6679 bytes OK
|
||||
2026/08/10-22:10:05.163630 173fbb000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.163662 173fbb000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.736502 172977000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.849645 171fcb000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.039746 1744ff000 Recovering log #3
|
||||
2026/08/10-21:10:35.039788 1744ff000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.040037 1744ff000 Level-0 table #5: 16892 bytes OK
|
||||
2026/08/10-21:10:35.040363 1744ff000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.040421 1744ff000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.172842 173fbb000 Recovering log #3
|
||||
2026/08/10-22:10:05.172885 173fbb000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.173125 173fbb000 Level-0 table #5: 16892 bytes OK
|
||||
2026/08/10-22:10:05.173406 173fbb000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.173455 173fbb000 Delete type=3 #2
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.759088 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.875668 1727d7000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.041184 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.041207 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.041346 175517000 Level-0 table #5: 3739 bytes OK
|
||||
2026/08/10-21:10:35.041618 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.041664 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.174148 172fa3000 Recovering log #3
|
||||
2026/08/10-22:10:05.174171 172fa3000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.174281 172fa3000 Level-0 table #5: 3739 bytes OK
|
||||
2026/08/10-22:10:05.174533 172fa3000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.174562 172fa3000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.757057 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.873729 171fcb000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.029938 174d0b000 Recovering log #3
|
||||
2026/08/10-21:10:35.029970 174d0b000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.030128 174d0b000 Level-0 table #5: 5105 bytes OK
|
||||
2026/08/10-21:10:35.030410 174d0b000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.030446 174d0b000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.164350 1737af000 Recovering log #3
|
||||
2026/08/10-22:10:05.164377 1737af000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.164513 1737af000 Level-0 table #5: 5105 bytes OK
|
||||
2026/08/10-22:10:05.164819 1737af000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.164862 1737af000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.739179 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.853599 172fe3000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.036441 175517000 Recovering log #3
|
||||
2026/08/10-21:10:35.036467 175517000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.036615 175517000 Level-0 table #5: 5012 bytes OK
|
||||
2026/08/10-21:10:35.036909 175517000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.036961 175517000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.170089 172fa3000 Recovering log #3
|
||||
2026/08/10-22:10:05.170122 172fa3000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.170277 172fa3000 Level-0 table #5: 5012 bytes OK
|
||||
2026/08/10-22:10:05.170539 172fa3000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.170582 172fa3000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.746192 173183000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.861512 1737ef000 Delete type=3 #1
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
2026/08/10-21:10:35.032367 175d23000 Recovering log #3
|
||||
2026/08/10-21:10:35.032410 175d23000 Level-0 table #5: started
|
||||
2026/08/10-21:10:35.032599 175d23000 Level-0 table #5: 7240 bytes OK
|
||||
2026/08/10-21:10:35.032954 175d23000 Delete type=0 #3
|
||||
2026/08/10-21:10:35.033016 175d23000 Delete type=3 #2
|
||||
2026/08/10-22:10:05.166515 172797000 Recovering log #3
|
||||
2026/08/10-22:10:05.166560 172797000 Level-0 table #5: started
|
||||
2026/08/10-22:10:05.166726 172797000 Level-0 table #5: 7240 bytes OK
|
||||
2026/08/10-22:10:05.166989 172797000 Delete type=0 #3
|
||||
2026/08/10-22:10:05.167039 172797000 Delete type=3 #2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/10-21:10:26.740769 17419b000 Delete type=3 #1
|
||||
2026/08/10-22:09:56.855434 1727d7000 Delete type=3 #1
|
||||
|
||||
+158
-2
@@ -27,6 +27,7 @@ import {
|
||||
encumbrancePenaltyFor, ENCUMBERED_CATEGORIES, freeCarryFor,
|
||||
defencePenaltyFor, defenceTypeAllowed, DEFENCE_STEP,
|
||||
defenceOutcomeFor, levelRank, LEVEL_LADDER, skillBaseFrom,
|
||||
DYING, dyingLimitFor, conditionFor, lastEntryFor, LAST_ENTRY,
|
||||
brawlFallbackFor, BRAWL_FALLBACK,
|
||||
SCOPE_TIERS, scopeRank, scopeStanceFor, scrutinyFor, RED_FLAG_STEP,
|
||||
authorityOutcomeFor, trustAfter, TRUST_MAX, DOSSIER, dossierTrustFor,
|
||||
@@ -698,6 +699,13 @@ class RingBRPActor extends Actor {
|
||||
sys.disabledLocations = sys.locations.filter(l => l.disabled).length;
|
||||
|
||||
sys.isDown = (sys.hp.value <= 2) || !!sys.wounded?.defenceOnly || !!sys.wounded?.unconscious;
|
||||
// Up / down / dying / dead, from the one rule, so nothing re-derives it.
|
||||
const dy = sys.dying ?? { rounds: 0, stabilised: false };
|
||||
sys.condition = (sys.dying?.dead) ? "dead" : conditionFor({
|
||||
hp: sys.hp.value, dyingRounds: dy.rounds, majorWound: sys.majorWound?.value,
|
||||
stabilised: dy.stabilised, unconscious: sys.wounded?.unconscious
|
||||
});
|
||||
sys.dyingLimit = dyingLimitFor(sys.majorWound?.value);
|
||||
}
|
||||
|
||||
_prepareExpedition() {
|
||||
@@ -1870,6 +1878,120 @@ async function promptAuthorityUse(authority) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dying, and death */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Advance the count on an agent at zero. Called from the same end-of-round hook that
|
||||
* charges blood loss, so nobody has to remember it — which is the whole reason
|
||||
* bleeding works and dying did not exist.
|
||||
*/
|
||||
export async function tickDying(actor) {
|
||||
if (!actor) return null;
|
||||
const sys = actor.system;
|
||||
if (sys.dying?.dead) return null;
|
||||
if ((Number(sys.hp?.value) || 0) > 0) return null; // not at zero
|
||||
if (sys.dying?.stabilised) return null; // held
|
||||
|
||||
const rounds = (Number(sys.dying?.rounds) || 0) + DYING.perRound;
|
||||
const limit = dyingLimitFor(sys.majorWound?.value);
|
||||
await actor.update({ "system.dying.rounds": rounds });
|
||||
|
||||
if (rounds < limit) {
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `<div class="ringbrp-chat rb-dying"><strong>${
|
||||
game.i18n.format("RINGBRP.Death.Dying", { name: actor.name })}</strong> ${
|
||||
game.i18n.format("RINGBRP.Death.Count", { n: rounds, of: limit })}</div>`
|
||||
});
|
||||
return { state: "dying", rounds, limit };
|
||||
}
|
||||
return died(actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last entry.
|
||||
*
|
||||
* A Custodian's death is a filing question, because everything here is. How well
|
||||
* filed they were decides what the record ends up saying — and the badly filed are
|
||||
* not recorded as dead at all, which becomes somebody's problem later.
|
||||
*/
|
||||
export async function died(actor) {
|
||||
if (!actor || actor.system.dying?.dead) return null;
|
||||
const coherence = Number(actor.system?.coherence?.value) || 0;
|
||||
const entry = lastEntryFor(coherence);
|
||||
await actor.update({
|
||||
"system.dying.dead": true,
|
||||
"system.dying.lastEntry": entry.id,
|
||||
"system.hp.value": 0
|
||||
});
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `<div class="ringbrp-chat rb-dead">`
|
||||
+ `<h3 class="rb-chat-title">${game.i18n.format("RINGBRP.Death.Died", { name: actor.name })}</h3>`
|
||||
+ `<p class="rb-chat-flavour">${game.i18n.format("RINGBRP.Death.Coherence", { n: coherence })}</p>`
|
||||
+ `<p><strong>${game.i18n.localize("RINGBRP.Death.TheEntry")}</strong> ${game.i18n.localize(entry.label)}</p>`
|
||||
+ `</div>`
|
||||
});
|
||||
return { state: "dead", entry: entry.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stabilise somebody at zero. Stops the count; it does not put them back on their
|
||||
* feet. They are still at zero and still unconscious until somebody heals them.
|
||||
*/
|
||||
export async function stabilise(medic, patient, { familyId = "first_aid",
|
||||
difficulty = "average" } = {}) {
|
||||
if (!medic || !patient) return null;
|
||||
const sys = patient.system;
|
||||
if (sys.dying?.dead) {
|
||||
ui.notifications?.warn(game.i18n.localize("RINGBRP.Death.TooLate"));
|
||||
return null;
|
||||
}
|
||||
if ((Number(sys.hp?.value) || 0) > 0) {
|
||||
ui.notifications?.info(game.i18n.localize("RINGBRP.Death.NotDying"));
|
||||
return null;
|
||||
}
|
||||
if (!DYING.stabiliseWith.includes(familyId)) {
|
||||
ui.notifications?.warn(game.i18n.format("RINGBRP.Death.WrongSkill",
|
||||
{ list: DYING.stabiliseWith.join(", ") }));
|
||||
return null;
|
||||
}
|
||||
const res = await skillRoll({
|
||||
actor: medic, label: game.i18n.format("RINGBRP.Death.Stabilising", { name: patient.name }),
|
||||
rating: medic.skillRating(familyId) ?? 0, difficulty,
|
||||
categoryId: "mental", skillFamilyId: familyId, printedBase: 0
|
||||
});
|
||||
if (!res) return null;
|
||||
const ok = ["critical", "special", "success"].includes(res.level);
|
||||
if (ok) await patient.update({ "system.dying.stabilised": true });
|
||||
// A fumble over a dying person costs them a round of the count they do not have.
|
||||
else if (res.level === "fumble") {
|
||||
const rounds = (Number(sys.dying?.rounds) || 0) + 1;
|
||||
await patient.update({ "system.dying.rounds": rounds });
|
||||
if (rounds >= dyingLimitFor(sys.majorWound?.value)) await died(patient);
|
||||
}
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor: medic }),
|
||||
content: `<div class="ringbrp-chat ${ok ? "rb-stable" : "rb-dying"}">${
|
||||
game.i18n.format(ok ? "RINGBRP.Death.Stabilised" : "RINGBRP.Death.NotStabilised",
|
||||
{ name: patient.name })}</div>`
|
||||
});
|
||||
return { ...res, stabilised: ok };
|
||||
}
|
||||
|
||||
/** Healing above zero clears the count. Nobody should have to clear it by hand. */
|
||||
export async function reviveIfHealed(actor) {
|
||||
if (!actor) return null;
|
||||
if ((Number(actor.system?.hp?.value) || 0) <= 0) return null;
|
||||
if (!(Number(actor.system?.dying?.rounds) || 0) && !actor.system?.dying?.stabilised) return null;
|
||||
await actor.update({ "system.dying.rounds": 0, "system.dying.stabilised": false });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Borrowed Authority */
|
||||
/* -------------------------------------------- */
|
||||
@@ -2135,6 +2257,12 @@ RINGBRP.healing = {
|
||||
*/
|
||||
export async function heal(actor, amount, { source = "natural", locationId = "" } = {}) {
|
||||
if (!actor || !(amount > 0)) return null;
|
||||
// The dead do not heal. Everything else in the medical chain routes through here,
|
||||
// so this is the one place that has to say so.
|
||||
if (actor.system?.dying?.dead) {
|
||||
ui.notifications?.warn(game.i18n.localize("RINGBRP.Death.TooLate"));
|
||||
return null;
|
||||
}
|
||||
const max = Number(actor.system.hp?.max) || 0;
|
||||
const before = Number(actor.system.hp?.value) || 0;
|
||||
const after = Math.min(max, before + amount);
|
||||
@@ -2145,6 +2273,10 @@ export async function heal(actor, amount, { source = "natural", locationId = ""
|
||||
// the stored value untouched — a location healed to zero silently kept its
|
||||
// damage, while a partial heal appeared to work. Fully-mended locations must be
|
||||
// removed with the "-=" deletion form.
|
||||
if (after > 0) {
|
||||
upd["system.dying.rounds"] = 0;
|
||||
upd["system.dying.stabilised"] = false;
|
||||
}
|
||||
const dmg = foundry.utils.deepClone(actor.system.locationDamage ?? {});
|
||||
let pool = after - before;
|
||||
const order = locationId ? [locationId, ...Object.keys(dmg).filter(k => k !== locationId)]
|
||||
@@ -2571,7 +2703,17 @@ export async function generateMissionCase({ seed = null, notify = true, ...opts
|
||||
forcedEncounterId: p.revealsGap ? p.reveal : null
|
||||
}))
|
||||
},
|
||||
notes: `<p><strong>${m.codename}</strong> — ${m.classification.name}. ${m.place}.</p>`
|
||||
// The case file itself should answer where, when, and what the job is without
|
||||
// anyone opening the journal. It used to carry one line of codename and place.
|
||||
notes: `<p><strong>${m.codename}</strong> — ${m.classification.name}.</p>`
|
||||
+ `<p><strong>Where:</strong> ${m.place}<br/>`
|
||||
+ `<strong>When:</strong> ${m.duration}<br/>`
|
||||
+ `<strong>Job:</strong> ${m.resolution}</p>`
|
||||
+ `<p><strong>Expect:</strong> about ${m.adversary.count} ${m.adversary.name} (${m.adversary.tier}). `
|
||||
+ `${m.adversary.leverage}</p>`
|
||||
+ `<p><strong>Way back:</strong> ${m.phases[m.phases.length - 1].terrain}. `
|
||||
+ `Transposition per agent`
|
||||
+ `${m.crossingModifier ? `, ${m.crossingModifier}% for the classification` : ""}.</p>`
|
||||
+ `<p>${m.anomaly.brief}</p>`,
|
||||
// A case with nobody assigned cannot resolve a phase: resolveLegDialog needs
|
||||
// a roster to pick a lead from and bails silently without one. But the old
|
||||
@@ -4009,6 +4151,15 @@ class RingBRPCharacterSheet extends ActorSheet {
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(it)), it, { functions: fns });
|
||||
});
|
||||
ctx.wounds = sys.wounds ?? [];
|
||||
ctx.condition = sys.condition ?? "up";
|
||||
ctx.conditionLabel = game.i18n.localize(`RINGBRP.Condition.${ctx.condition}`);
|
||||
ctx.dyingNow = ctx.condition === "dying";
|
||||
ctx.deadNow = ctx.condition === "dead";
|
||||
ctx.dyingRounds = Number(sys.dying?.rounds) || 0;
|
||||
ctx.dyingLimit = sys.dyingLimit ?? 0;
|
||||
ctx.stabilised = !!sys.dying?.stabilised;
|
||||
ctx.lastEntry = sys.dying?.lastEntry
|
||||
? game.i18n.localize(`RINGBRP.Death.Entry.${sys.dying.lastEntry}`) : "";
|
||||
ctx.styleLabel = game.i18n.localize(RINGBRP.marksmanship[sys.marksmanship?.style]?.label ?? "");
|
||||
// The dropdown offered a choice with no stated consequence — two words and no
|
||||
// hint of what either buys you. Built from RINGBRP.marksmanship so the sheet
|
||||
@@ -5174,6 +5325,7 @@ Hooks.once("init", () => {
|
||||
resolveContact, newEncounter, claimRewards, activeRewardEffects,
|
||||
defenceOutcomeFor, levelRank, recordBlow, standingBlow, amendBlow,
|
||||
addDossierItem, useAuthority, widenScope, rehabilitate,
|
||||
tickDying, died, stabilise, reviveIfHealed, conditionFor, dyingLimitFor,
|
||||
scopeStanceFor, dossierTrustFor, mergeStageFor, SCOPE_TIERS,
|
||||
heal, firstAid, medicalCare, applyBleeding, stopBleeding, bleedRateFor, BLEED,
|
||||
defend, defencePenaltyFor,
|
||||
@@ -5285,7 +5437,11 @@ Hooks.on("updateCombat", async (combat, changed) => {
|
||||
// elapsed yet, and starting combat used to cost a bleeding agent a free tick.
|
||||
if ("round" in changed && changed.round > 1) {
|
||||
for (const c of combat.combatants) {
|
||||
if (c.actor && c.actor.type !== "expedition") await applyBleeding(c.actor);
|
||||
if (!c.actor || c.actor.type === "expedition") continue;
|
||||
await applyBleeding(c.actor);
|
||||
// Bleeding stops AT zero by design; the count that starts there is what
|
||||
// carries on. Same hook, so neither has to be remembered.
|
||||
await tickDying(c.actor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,70 @@ export function defencePenaltyFor(defencesUsed = 0) {
|
||||
return DEFENCE_STEP * Math.max(0, Number(defencesUsed) || 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dying, and death */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* WHAT HAPPENS AT ZERO.
|
||||
*
|
||||
* Nothing did. An agent reduced to 0 hit points was marked down and then lay there
|
||||
* indefinitely: bleeding deliberately stops at 0, no rule advanced anything, and the
|
||||
* only mention of death in the whole system was flavour text on a destroyed head.
|
||||
* A game with hit locations, Major Wounds and bleeding had no way for anyone to die.
|
||||
*
|
||||
* Hit points are NOT taken below zero — every damage path clamps there, and unpicking
|
||||
* that would touch everything. Instead, zero starts a clock:
|
||||
*
|
||||
* above 2 you are up
|
||||
* 2 or below DOWN — out of the fight, still conscious, still yours
|
||||
* 0 DYING — unconscious, no actions, no defences, and a count begins
|
||||
* count met DEAD
|
||||
*
|
||||
* The count is the agent's own Major Wound threshold, so a solid Custodian has longer
|
||||
* on the floor than a slight one, which is the same number already deciding how much
|
||||
* damage they can take standing up. First Aid or Medicine STABILISES: the count stops
|
||||
* and they stay at 0, unconscious, until somebody heals them above it.
|
||||
*/
|
||||
export const DYING = { perRound: 1, minCount: 2, stabiliseWith: ["first_aid", "medicine"] };
|
||||
|
||||
/** How many rounds this agent has at zero before the count is met. */
|
||||
export function dyingLimitFor(majorWound) {
|
||||
return Math.max(DYING.minCount, Number(majorWound) || 0);
|
||||
}
|
||||
|
||||
/** The four states, from one place, so nothing has to re-derive them. */
|
||||
export function conditionFor({ hp = 0, dyingRounds = 0, majorWound = 0,
|
||||
stabilised = false, unconscious = false } = {}) {
|
||||
const h = Number(hp) || 0;
|
||||
if (h <= 0) {
|
||||
if (!stabilised && (Number(dyingRounds) || 0) >= dyingLimitFor(majorWound)) return "dead";
|
||||
return "dying";
|
||||
}
|
||||
if (h <= 2 || unconscious) return "down";
|
||||
return "up";
|
||||
}
|
||||
|
||||
/**
|
||||
* THE LAST ENTRY.
|
||||
*
|
||||
* A Custodian's death is a filing question, because everything here is. How well
|
||||
* filed they were when they died decides what the record ends up saying, and the
|
||||
* badly filed do not get recorded as dead at all — which is the department's
|
||||
* problem, and then somebody else's.
|
||||
*/
|
||||
export const LAST_ENTRY = [
|
||||
{ atLeast: 7, id: "recorded", label: "RINGBRP.Death.Entry.recorded" },
|
||||
{ atLeast: 4, id: "queried", label: "RINGBRP.Death.Entry.queried" },
|
||||
{ atLeast: 1, id: "unclosed", label: "RINGBRP.Death.Entry.unclosed" },
|
||||
{ atLeast: 0, id: "transferred", label: "RINGBRP.Death.Entry.transferred" }
|
||||
];
|
||||
|
||||
export function lastEntryFor(coherence) {
|
||||
const c = Number(coherence) || 0;
|
||||
return LAST_ENTRY.find(e => c >= e.atLeast) ?? LAST_ENTRY[LAST_ENTRY.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* HOW HARD A CASE PHASE IS.
|
||||
*
|
||||
|
||||
@@ -1242,3 +1242,22 @@
|
||||
|
||||
/* The case generator sits with the others but is the one that makes a whole case. */
|
||||
.rb-genbtn-case { border-color: var(--rb-yellow); color: var(--rb-yellow); }
|
||||
|
||||
/* --- Dying and dead ------------------------------------------------------- */
|
||||
.rb-dyingbar, .rb-deadbar {
|
||||
display: flex; gap: 10px; align-items: baseline;
|
||||
padding: 6px 10px; margin: 0 0 8px;
|
||||
font-family: var(--rb-body); font-size: 11px;
|
||||
border-left: 3px solid var(--rb-red, #ff7a5c); background: var(--rb-deep);
|
||||
}
|
||||
.rb-dyingbar.rb-stable { border-left-color: var(--rb-yellow); }
|
||||
.rb-deadbar { border-left-color: var(--rb-white); }
|
||||
.rb-dyingbar-title {
|
||||
font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--rb-red, #ff7a5c);
|
||||
}
|
||||
.rb-dyingbar.rb-stable .rb-dyingbar-title { color: var(--rb-yellow); }
|
||||
.rb-deadbar .rb-dyingbar-title { color: var(--rb-white); }
|
||||
.rb-dying { border-left: 3px solid var(--rb-red, #ff7a5c); padding-left: 8px; }
|
||||
.rb-stable { border-left: 3px solid var(--rb-yellow); padding-left: 8px; }
|
||||
.rb-dead { border-left: 3px solid var(--rb-white); padding-left: 8px; }
|
||||
|
||||
+14
-2
@@ -82,7 +82,13 @@
|
||||
},
|
||||
"pursuits": [],
|
||||
"biography": "",
|
||||
"rank": ""
|
||||
"rank": "",
|
||||
"dying": {
|
||||
"rounds": 0,
|
||||
"stabilised": false,
|
||||
"dead": false,
|
||||
"lastEntry": ""
|
||||
}
|
||||
},
|
||||
"npc": {
|
||||
"templates": [
|
||||
@@ -96,7 +102,13 @@
|
||||
"naturalArmour": {
|
||||
"value": 0
|
||||
},
|
||||
"tactics": ""
|
||||
"tactics": "",
|
||||
"dying": {
|
||||
"rounds": 0,
|
||||
"stabilised": false,
|
||||
"dead": false,
|
||||
"lastEntry": ""
|
||||
}
|
||||
},
|
||||
"expedition": {
|
||||
"supply": {
|
||||
|
||||
@@ -118,6 +118,23 @@
|
||||
<section class="sheet-body rb-body">
|
||||
|
||||
<div class="tab" data-group="primary" data-tab="skills">
|
||||
{{#if dyingNow}}
|
||||
<div class="rb-dyingbar{{#if stabilised}} rb-stable{{/if}}">
|
||||
<span class="rb-dyingbar-title">{{localize "RINGBRP.Condition.dying"}}</span>
|
||||
{{#if stabilised}}
|
||||
<span>{{localize "RINGBRP.Death.StableTag"}}</span>
|
||||
{{else}}
|
||||
<span>{{localize "RINGBRP.Death.CountTag"}} {{dyingRounds}} / {{dyingLimit}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if deadNow}}
|
||||
<div class="rb-deadbar">
|
||||
<span class="rb-dyingbar-title">{{localize "RINGBRP.Condition.dead"}}</span>
|
||||
<span>{{lastEntry}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if impaired.active}}
|
||||
<div class="rb-woundbar" data-tooltip="{{localize 'RINGBRP.Sheet.WoundBarHint'}}">
|
||||
<span class="rb-woundbar-title"><i class="fas fa-triangle-exclamation"></i>
|
||||
|
||||
@@ -197,6 +197,17 @@ const checks = [
|
||||
["defenceTypeAllowed('','parry')", RULES.defenceTypeAllowed("","parry"), "parry"],
|
||||
// defences are graded: a defence stops its own quality or worse, and only
|
||||
// shaves a better blow by as many steps as it was outclassed by
|
||||
// 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],
|
||||
["conditionFor(hp 8)", RULES.conditionFor({hp:8}), "up"],
|
||||
["conditionFor(hp 2) is down", RULES.conditionFor({hp:2}), "down"],
|
||||
["conditionFor(hp 0) is dying", RULES.conditionFor({hp:0,majorWound:6}), "dying"],
|
||||
["conditionFor(count met) is dead", RULES.conditionFor({hp:0,dyingRounds:6,majorWound:6}), "dead"],
|
||||
["stabilised never reaches dead", RULES.conditionFor({hp:0,dyingRounds:9,majorWound:6,stabilised:true}), "dying"],
|
||||
["lastEntryFor(10)", RULES.lastEntryFor(10).id, "recorded"],
|
||||
["lastEntryFor(3)", RULES.lastEntryFor(3).id, "unclosed"],
|
||||
["lastEntryFor(0)", RULES.lastEntryFor(0).id, "transferred"],
|
||||
// the case ramp: attrition accumulates and stops; only Containment is Difficult
|
||||
["phaseRampFor(0) is the way in", RULES.phaseRampFor(0,"approach").difficulty, "average"],
|
||||
["phaseRampFor(0) modifier", RULES.phaseRampFor(0,"approach").modifier, 0],
|
||||
|
||||
+48
-12
@@ -94,7 +94,10 @@ function buildPhases(r, { anomaly, gap }) {
|
||||
// extraction, and "getting out with it" presumed the it had been obtained
|
||||
// offscreen: the generator produced an approach, not a session. The phase where
|
||||
// they do the thing they were sent to do is the one the case is about.
|
||||
const middle = some(PHASE_KINDS.filter(k => !["approach", "extraction", "containment"].includes(k.id)), 2, r);
|
||||
// ONE middle phase, not two. A five-phase arc with two interchangeable middles
|
||||
// read as padding — the case is Approach, the thing in the way, the job, and out.
|
||||
// Four phases is a session; five was a session and a half with a repeat in it.
|
||||
const middle = some(PHASE_KINDS.filter(k => !["approach", "extraction", "containment"].includes(k.id)), 1, r);
|
||||
const kinds = [
|
||||
PHASE_KINDS.find(k => k.id === "approach"),
|
||||
...middle,
|
||||
@@ -244,22 +247,55 @@ const ul = xs => `<ul>${xs.map(x => `<li>${esc(x)}</li>`).join("")}</ul>`;
|
||||
|
||||
/** The player-facing brief. Everything here is true as far as the agency knows. */
|
||||
export function briefPage(m) {
|
||||
// The brief used to open on a classification note and never say where the site
|
||||
// was, how long the team had, what was likely to be in the way, or how they were
|
||||
// getting out — all of which the generator already knew. An operational brief
|
||||
// answers those four first and explains itself afterwards.
|
||||
const ex = m.phases[m.phases.length - 1];
|
||||
return `
|
||||
<p><strong>Classification ${esc(m.classification.name)}.</strong> <em>${esc(m.classification.note)}</em></p>
|
||||
<h2>The brief</h2>
|
||||
<p>${esc(m.anomaly.brief)}</p>
|
||||
<p><strong>Containment.</strong> ${esc(m.anomaly.containment)}</p>
|
||||
<table class="rb-brief-head"><tbody>
|
||||
<tr><th>Where</th><td><strong>${esc(m.place)}</strong></td></tr>
|
||||
<tr><th>When</th><td><strong>${esc(m.duration)}</strong> — that is the window, not an estimate.</td></tr>
|
||||
<tr><th>Classification</th><td><strong>${esc(m.classification.name)}</strong> — ${esc(m.classification.note)}</td></tr>
|
||||
<tr><th>Phases</th><td>${m.phases.length}: ${m.phases.map(p => esc(p.name)).join(" → ")}</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h2>What you are being sent to do</h2>
|
||||
<p>${esc(m.resolution)}</p>
|
||||
<h2>Known complication</h2>
|
||||
<p>${esc(m.complication)}</p>
|
||||
<h2>Reported signs</h2>
|
||||
<p><strong>${esc(m.resolution)}</strong></p>
|
||||
|
||||
<h2>What is reported</h2>
|
||||
<p>${esc(m.anomaly.brief)}</p>
|
||||
<p><strong>Why it cannot be left.</strong> ${esc(m.anomaly.containment)}</p>
|
||||
<p><strong>Signs you have the right site:</strong></p>
|
||||
${ul(m.anomaly.signs)}
|
||||
<h2>Starting state</h2>
|
||||
|
||||
<h2>What is likely to be in your way</h2>
|
||||
<p><strong>${esc(m.adversary.name)}</strong> — expect around ${m.adversary.count}, tier
|
||||
<strong>${esc(m.adversary.tier)}</strong>.</p>
|
||||
<p><strong>Why they care.</strong> ${esc(m.adversary.motive)}</p>
|
||||
<p><strong>How they will go about it.</strong> ${esc(m.adversary.tactics)}</p>
|
||||
<p><strong>What might work instead of shooting.</strong> ${esc(m.adversary.leverage)}</p>
|
||||
<p><strong>Also on the ground:</strong> ${esc(m.group.name)} — ${esc(m.group.description)}</p>
|
||||
<p><strong>Do not:</strong> ${esc(m.group.taboo?.text ?? m.group.taboo)}
|
||||
${m.group.taboo?.penaltyIfViolated ? `(${m.group.taboo.penaltyIfViolated}% if you do)` : ""}</p>
|
||||
<p><strong>They want:</strong> ${esc(m.group.desire?.text ?? m.group.desire)}</p>
|
||||
<p><strong>Known complication.</strong> ${esc(m.complication)}</p>
|
||||
|
||||
<h2>How you get back</h2>
|
||||
<ul>
|
||||
<li>Extraction is <strong>${esc(ex.terrain)}</strong>.</li>
|
||||
<li>Return is a <strong>Transposition</strong> roll per agent. Anyone may aim for
|
||||
somebody else instead of themselves.</li>
|
||||
<li>Classification modifier on the way home: <strong>${m.crossingModifier ? `${m.crossingModifier}%` : "none"}</strong>.</li>
|
||||
<li>A rough return costs Coherence. A crossing that goes badly wrong is the only way
|
||||
an Art is ever acquired.</li>
|
||||
</ul>
|
||||
|
||||
<h2>What you start with</h2>
|
||||
<ul>
|
||||
<li><strong>Containment ${m.tracks.containment} · Resources ${m.tracks.resources} · Clearance ${m.tracks.clearance}</strong></li>
|
||||
<li>Destination knowledge: <strong>rumoured</strong> — every phase this journey is Difficult.</li>
|
||||
<li>Return modifier for the classification: <strong>${m.crossingModifier ? `${m.crossingModifier}%` : "none"}</strong></li>
|
||||
<li>Phases cost <strong>${m.phaseCost} Resources</strong> in total if nothing goes wrong. It will.</li>
|
||||
<li>Ground is <strong>rumoured</strong>, not known.</li>
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SCOPE_TIERS, scopeStanceFor, scrutinyFor, authorityOutcomeFor,
|
||||
dossierTrustFor, widenScopeCostFor, DOSSIER, MERGE_STAGES,
|
||||
skillBaseFrom, encumbrancePenaltyFor, ENCUMBERED_CATEGORIES,
|
||||
DYING, dyingLimitFor, LAST_ENTRY,
|
||||
powerStartingRating, brawlFallbackFor, bleedStoppedBy,
|
||||
crossingArtPoolFor, crossingGrantsArt, crossingCostFor,
|
||||
REACTION, BLEED, CIRCUMSTANCE, RANGE_LADDER, rangeBandFrom,
|
||||
@@ -309,6 +310,33 @@ ${table(["1D10", "Wound", "What it does to you"], woundRows)}
|
||||
<p>These stack, with each other and with a shattered limb, and they are applied to
|
||||
every roll you make. The Skills tab shows a red bar when you are carrying any.</p>
|
||||
|
||||
<h2>Zero hit points</h2>
|
||||
<p>Hit points do not go below zero. Zero starts a clock instead.</p>
|
||||
${table(["Hit points", "You are"], [
|
||||
["above 2", "<strong>up</strong> — no restriction from injury alone"],
|
||||
["2 or below", "<strong>down</strong> — out of the fight, still conscious, still yours"],
|
||||
["0", "<strong>dying</strong> — unconscious, no actions, no defences, and the count begins"]
|
||||
])}
|
||||
<p>Each round at zero adds ${DYING.perRound} to the count. When the count reaches your
|
||||
own <strong>Major Wound</strong> threshold you are <strong>dead</strong> — so the same
|
||||
number that decides how much damage you can take standing up also decides how long you
|
||||
have on the floor. A minimum of ${DYING.minCount} rounds applies however slight you are.</p>
|
||||
<p><strong>Stabilising</strong> takes ${DYING.stabiliseWith.join(" or ")} and stops the
|
||||
count. It does not put you back on your feet: you are still at zero and still
|
||||
unconscious until somebody heals you above it. Healing above zero clears the count.
|
||||
A <em>fumbled</em> attempt over a dying agent costs them a round they do not have.</p>
|
||||
|
||||
<h3>The last entry</h3>
|
||||
<p>A Custodian's death is a filing question, because everything here is. What the
|
||||
record ends up saying depends on how well filed they were when it happened.</p>
|
||||
${table(["Coherence at death", "What the record says"],
|
||||
LAST_ENTRY.map((e, i) => {
|
||||
const upper = i === 0 ? "10" : String(LAST_ENTRY[i - 1].atLeast - 1);
|
||||
return [e.atLeast === 0 ? "0" : `${e.atLeast}–${upper}`, L(e.label)];
|
||||
}))}
|
||||
<p>The badly filed are not recorded as dead at all. That is the department's problem
|
||||
first, and then somebody else's.</p>
|
||||
|
||||
<h2>Bleeding</h2>
|
||||
${table(["Wound", "Per round", "Stopped by"], Object.entries(BLEED).map(([k, v]) =>
|
||||
[L(`RINGBRP.Wound.${k}.title`), `${v.perRound} hp`,
|
||||
|
||||
Reference in New Issue
Block a user