diff --git a/README.md b/README.md index c22d425..819c84b 100644 --- a/README.md +++ b/README.md @@ -187,12 +187,12 @@ check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resol check-lang: OK — en.json, 1018 keys, no leaf/branch collisions check-templates: OK — 19 templates compile check-behaviour: OK — 76 behavioural tests -check-scenarios: OK — 11 scenario files, 79 mechanic tags, every skill named resolves against 62 catalogue entries -check-rollable: OK — 78 rolls across 11 scenario files, every one reachable at 25% or better by somebody present +check-scenarios: OK — 12 scenario files, 79 mechanic tags, every skill named resolves against 62 catalogue entries +check-rollable: OK — 78 rolls across 12 scenario files, every one reachable at 25% or better by somebody present check-creatures: OK — 102 actor specs across 8 sources, every characteristic, skill, kit key, talent, species and style resolves, no duplicate keys check-anatomy: OK — 5 body plans, 36 locations, d20 complete in both modes, every location drawn, every kind handled and destructible, 20 plan changes carried 432 wounds with their severity intact, 1 flyer(s) with a survivable landing check-lethality: OK — 47 creatures, every one fighting exactly as recorded against the frozen party -check-focus: OK — focus fire measured against 31 packs, helps in all of them, every gain above its own noise +check-focus: OK — focus fire measured against 30 packs, helps in all of them, 28 of them by more than their own noise check-bestiary: OK — 47 creatures, the document matches the game ``` diff --git a/docs/BESTIARY.md b/docs/BESTIARY.md index 9d6ec5f..7e0d860 100644 --- a/docs/BESTIARY.md +++ b/docs/BESTIARY.md @@ -11,42 +11,42 @@ Sorted by how much of a four-agent party each one puts on the floor, alone. **So | | Creature | Essence | SIZ | Arm | HP | Attack | Down of 4 | |---|---|---|---|---|---|---|---| -| | **Quarantine unit** | Sent to contain the same thing you were, and it got here first | 14 | 9 | 17 | Unmaker 75% | 3.71 | -| | **The supporter** | The winged beast off a borough coat of arms, come down off it | 24 | 9 | 30 | Grasping limb 75% | 3.38 | -| | **Long walker** | It has been coming here for a very long time and is nearly arrived | 22 | 10 | 24 | Grasping limb 80% | 3.18 | -| | **The stanchion** | Something was driven through the floor here to keep the door from shutting | 21 | 12 | 23 | Grasping limb 45% | 2.29 | -| | **Nuckelavee** | It came up out of the firth and everything it breathes on dies | 20 | 5 | 20 | Grasping limb 85% | 1.70 | -| | **The arrears** | Something was borrowed from the far side in 1968 and this is the collection | 19 | 7 | 20 | Grasping limb 80% | 1.62 | -| | **Surveyor** | It is measuring the site and it will finish | 13 | 8 | 16 | Redaction lance 70% | 1.39 | -| V | **The courier** | A Vesh carrying something across, and it will not put it down | 16 | 6 | 18 | Grasping limb 80% | 1.37 | -| | **The apple tree man** | The oldest tree in the orchard, and it has been owed a drink since 1961 | 19 | 8 | 22 | Grasping limb 55% | 1.04 | -| | **Cuckoo array** | A machine that has decided it is the site | 20 | 7 | 22 | Grasping limb 55% | 0.78 | -| | **Lost property** | It has been collecting for ninety years and it is not only umbrellas now | 18 | 7 | 21 | Grasping limb 60% | 0.78 | -| V | **The yield** | It is here for the harvest and the harvest is annual | 18 | 5 | 18 | Grasping limb 75% | 0.72 | -| C | **The relay** | Cadence machinery holding one end of a crossing open | 17 | 8 | 20 | Grasping limb 55% | 0.60 | -| | **Barghest** | The black dog on the road out of the village | 15 | 3 | 16 | Grasping limb 75% | 0.43 | -| | **Redcap** | The small old man in the ruin, and what he dyes his hat with | 7 | 2 | 13 | Halberd 80% | 0.34 | -| | **Spriggan** | The small ugly thing guarding the barrow, until it is not small | 16 | 4 | 17 | Cudgel 55% | 0.29 | -| | **Church grim** | What was buried at the north door to keep the ground | 14 | 5 | 17 | Grasping limb 70% | 0.28 | -| | **Kelpie** | The horse standing at the water that is not a horse | 18 | 2 | 17 | Grasping limb 65% | 0.24 | -| V | **Precedent** | A Vesh that has already attended the incident you are about to file | 14 | 3 | 15 | Grasping limb 60% | 0.17 | -| | **The waiting room** | You will be seen shortly | 17 | 6 | 19 | Grasping limb 40% | 0.17 | -| | **Revenant** | What is left when something comes through without a body to come through in | 13 | 4 | 15 | Grasping limb 65% | 0.16 | -| V | **The census** | A Vesh counting what is on this side, and it has got to you | 13 | 4 | 15 | Grasping limb 50% | 0.15 | -| | **Grindylow** | What is in the still water at the bottom of the lock | 11 | 3 | 14 | Grasping limb 70% | 0.13 | -| | **The auditor** | It has read the file and it has questions | 11 | 4 | 13 | Service pistol 55% | 0.12 | -| C | **The overwriter** | It is correcting the record and it does not know the record is us | 15 | 6 | 18 | Grasping limb 35% | 0.12 | -| | **The predecessor** | Whoever had this desk before you never quite handed over | 12 | 2 | 14 | Service pistol 60% | 0.09 | -| | **Cleaner (marksman)** | The one holding the high ground | 12 | 0 | 12 | Marksman rifle 55% | 0.08 | -| | **The night registry** | The filing that gets done after everyone goes home | 13 | 3 | 16 | Grasping limb 50% | 0.08 | -| | **Duplicate (of an agent)** | The single most expensive thing a team can meet | 12 | 0 | 13 | Service pistol 50% | 0.06 | -| | **The Keepers (waystation)** | Scavengers who bury what comes through — and the only real information network on the far side | 12 | 0 | 13 | Utility knife 55% | 0.04 | -| | **The carrion file** | A case file that has been eating the cases filed next to it | 9 | 6 | 16 | Grasping limb 45% | 0.04 | -| | **The locum** | Covering a post that nobody appointed and nobody can close | 12 | 1 | 13 | Service pistol 50% | 0.04 | -| | **Cleaner (contractor)** | Human, armed, and working for whoever got here first | 12 | 0 | 12 | Service pistol 45% | 0.03 | -| | **The Quiet Neighbours** | Near-world locals who have lived beside the threshold for generations | 8 | 0 | 10 | Improvised spear 50% | 0.02 | -| V | **The margin** | It lives in the space around what was written down | 8 | 2 | 11 | Grasping limb 40% | 0.02 | -| | **The understudy** | It has been practising being you and it is nearly ready | 12 | 2 | 13 | Punch / kick 60% | 0.01 | +| | **Quarantine unit** | Sent to contain the same thing you were, and it got here first | 14 | 9 | 17 | Unmaker 75% | 3.69 | +| | **Long walker** | It has been coming here for a very long time and is nearly arrived | 22 | 10 | 24 | Grasping limb 80% | 3.32 | +| | **The stanchion** | Something was driven through the floor here to keep the door from shutting | 21 | 12 | 23 | Grasping limb 45% | 2.52 | +| | **The supporter** | The winged beast off a borough coat of arms, come down off it | 24 | 9 | 30 | Grasping limb 75% | 2.16 | +| | **Nuckelavee** | It came up out of the firth and everything it breathes on dies | 20 | 5 | 20 | Grasping limb 85% | 1.98 | +| | **The arrears** | Something was borrowed from the far side in 1968 and this is the collection | 19 | 7 | 20 | Grasping limb 80% | 1.98 | +| V | **The courier** | A Vesh carrying something across, and it will not put it down | 16 | 6 | 18 | Grasping limb 80% | 1.73 | +| | **Surveyor** | It is measuring the site and it will finish | 13 | 8 | 16 | Redaction lance 70% | 1.56 | +| | **The apple tree man** | The oldest tree in the orchard, and it has been owed a drink since 1961 | 19 | 8 | 22 | Grasping limb 55% | 1.37 | +| | **Lost property** | It has been collecting for ninety years and it is not only umbrellas now | 18 | 7 | 21 | Grasping limb 60% | 1.06 | +| | **Cuckoo array** | A machine that has decided it is the site | 20 | 7 | 22 | Grasping limb 55% | 1.02 | +| V | **The yield** | It is here for the harvest and the harvest is annual | 18 | 5 | 18 | Grasping limb 75% | 1.01 | +| C | **The relay** | Cadence machinery holding one end of a crossing open | 17 | 8 | 20 | Grasping limb 55% | 0.94 | +| | **Barghest** | The black dog on the road out of the village | 15 | 3 | 16 | Grasping limb 75% | 0.64 | +| | **Redcap** | The small old man in the ruin, and what he dyes his hat with | 7 | 2 | 13 | Halberd 80% | 0.55 | +| | **Church grim** | What was buried at the north door to keep the ground | 14 | 5 | 17 | Grasping limb 70% | 0.49 | +| | **Spriggan** | The small ugly thing guarding the barrow, until it is not small | 16 | 4 | 17 | Cudgel 55% | 0.49 | +| | **Kelpie** | The horse standing at the water that is not a horse | 18 | 2 | 17 | Grasping limb 65% | 0.38 | +| V | **Precedent** | A Vesh that has already attended the incident you are about to file | 14 | 3 | 15 | Grasping limb 60% | 0.30 | +| | **The waiting room** | You will be seen shortly | 17 | 6 | 19 | Grasping limb 40% | 0.30 | +| | **Revenant** | What is left when something comes through without a body to come through in | 13 | 4 | 15 | Grasping limb 65% | 0.27 | +| V | **The census** | A Vesh counting what is on this side, and it has got to you | 13 | 4 | 15 | Grasping limb 50% | 0.27 | +| C | **The overwriter** | It is correcting the record and it does not know the record is us | 15 | 6 | 18 | Grasping limb 35% | 0.25 | +| | **Grindylow** | What is in the still water at the bottom of the lock | 11 | 3 | 14 | Grasping limb 70% | 0.24 | +| | **The auditor** | It has read the file and it has questions | 11 | 4 | 13 | Service pistol 55% | 0.17 | +| | **The night registry** | The filing that gets done after everyone goes home | 13 | 3 | 16 | Grasping limb 50% | 0.14 | +| | **The predecessor** | Whoever had this desk before you never quite handed over | 12 | 2 | 14 | Service pistol 60% | 0.13 | +| | **Cleaner (marksman)** | The one holding the high ground | 12 | 0 | 12 | Marksman rifle 55% | 0.11 | +| | **The carrion file** | A case file that has been eating the cases filed next to it | 9 | 6 | 16 | Grasping limb 45% | 0.11 | +| | **The locum** | Covering a post that nobody appointed and nobody can close | 12 | 1 | 13 | Service pistol 50% | 0.09 | +| | **Duplicate (of an agent)** | The single most expensive thing a team can meet | 12 | 0 | 13 | Service pistol 50% | 0.08 | +| | **The Keepers (waystation)** | Scavengers who bury what comes through — and the only real information network on the far side | 12 | 0 | 13 | Utility knife 55% | 0.06 | +| | **The understudy** | It has been practising being you and it is nearly ready | 12 | 2 | 13 | Punch / kick 60% | 0.05 | +| V | **The margin** | It lives in the space around what was written down | 8 | 2 | 11 | Grasping limb 40% | 0.05 | +| | **The Quiet Neighbours** | Near-world locals who have lived beside the threshold for generations | 8 | 0 | 10 | Improvised spear 50% | 0.04 | +| | **Cleaner (contractor)** | Human, armed, and working for whoever got here first | 12 | 0 | 12 | Service pistol 45% | 0.04 | | | **Boggart** | The thing in the house that was tolerable until it was not | 5 | 0 | 9 | Punch / kick 35% | 0.00 | | | **Hollow man** | Somebody who crossed too often and came back thinner each time | 12 | 1 | 13 | Punch / kick 55% | 0.00 | | | **The tenant** | What moved into the house while it was empty | 10 | 0 | 11 | Punch / kick 25% | 0.00 | @@ -87,12 +87,12 @@ rule for holding an action. Arranging the party so its best weapon fires last wa measured in the simulator and came to nothing distinguishable from noise. **What you can do is kill them one at a time.** Against a pack, every agent putting -their attacks into the same creature beats spreading them — in **every one** of the -31 fights in this book whose outcome was ever in doubt, without exception, and by more -than the measurement’s own noise each time. It is worth about +their attacks into the same creature beats spreading them — in 30 of the +30 fights in this book whose outcome was ever in doubt, and by more +than the measurement’s own noise in 28 of them. It is worth about **8 points** of win rate against a pair and -**14 points** against three or more; the best case in the book is -6 × The margin, which goes 42.0% to 61.1%. Most of that is not the ladder +**12 points** against three or more; the best case in the book is +3 × Spriggan, which goes 38.9% to 55.5%. Most of that is not the ladder above: a creature that is dead stops attacking, and one on half its hit points does not attack any less. Held at a fixed pack size the dodge ladder is worth a point or two. @@ -130,7 +130,7 @@ Older than the department and largely uninterested in it. Most of these are arra **ARGENT AND GULES.** While it stands on ground the borough holds, it takes half damage from anything the borough owns — which includes the department's vehicles and any weapon signed out of Stores. WING IT. -*Measured alone against the frozen party: 3.38 of 4 down, 69.3% wiped, 17 rounds.* +*Measured alone against the frozen party: 2.16 of 4 down, 25.1% wiped, 9 rounds.* ### Nuckelavee @@ -144,7 +144,7 @@ Older than the department and largely uninterested in it. Most of these are arra **THE BLIGHT.** Everything within ten metres withers — crops, stock, and on a failed CONx5 each round, people, who take 1 point that armour does not stop. It will not cross running fresh water and it knows that about itself, which makes every stream on the map a tactical feature. -*Measured alone against the frozen party: 1.70 of 4 down, 16% wiped, 7 rounds.* +*Measured alone against the frozen party: 1.98 of 4 down, 20.3% wiped, 7 rounds.* ### The apple tree man @@ -158,7 +158,7 @@ Older than the department and largely uninterested in it. Most of these are arra **THE RECKONING.** It knows what is buried in its orchard, all of it, and will say so in exchange for the debt being paid — cider poured on the roots, and somebody has to sing. It cannot be made to answer any other way; threats bore it, being nineteen feet of apple wood. -*Measured alone against the frozen party: 1.04 of 4 down, 2.1% wiped, 10 rounds.* +*Measured alone against the frozen party: 1.37 of 4 down, 3.7% wiped, 11 rounds.* ### Barghest @@ -174,7 +174,7 @@ Older than the department and largely uninterested in it. Most of these are arra **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. -*Measured alone against the frozen party: 0.43 of 4 down, 0.4% wiped, 4 rounds.* +*Measured alone against the frozen party: 0.64 of 4 down, 0.7% wiped, 4 rounds.* ### Redcap @@ -188,21 +188,7 @@ Older than the department and largely uninterested in it. Most of these are arra **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. -*Measured alone against the frozen party: 0.34 of 4 down, 0.1% wiped, 3 rounds.* - -### Spriggan - -*The small ugly thing guarding the barrow, until it is not small* - -| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | -|---|---|---|---|---|---|---|---|---|---| -| 21 | 18 | 16 | 9 | 15 | 13 | 4 | 17 | 9 | 7 | - -**Attacks with** Cudgel at 55%, 1d6+1 · **dodges** 45% - -**WHAT IT REALLY IS.** The characteristics above are the LARGE form. Until the barrow is disturbed run it at SIZ 6 and STR 8, harmless and mostly comic; the change is one round and there is no roll to resist it. - -*Measured alone against the frozen party: 0.29 of 4 down, 0.1% wiped, 4 rounds.* +*Measured alone against the frozen party: 0.55 of 4 down, 0.7% wiped, 3 rounds.* ### Church grim @@ -218,7 +204,21 @@ Older than the department and largely uninterested in it. Most of these are arra **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. -*Measured alone against the frozen party: 0.28 of 4 down, 0.1% wiped, 4 rounds.* +*Measured alone against the frozen party: 0.49 of 4 down, 0.4% wiped, 4 rounds.* + +### Spriggan + +*The small ugly thing guarding the barrow, until it is not small* + +| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | +|---|---|---|---|---|---|---|---|---|---| +| 21 | 18 | 16 | 9 | 15 | 13 | 4 | 17 | 9 | 7 | + +**Attacks with** Cudgel at 55%, 1d6+1 · **dodges** 45% + +**WHAT IT REALLY IS.** The characteristics above are the LARGE form. Until the barrow is disturbed run it at SIZ 6 and STR 8, harmless and mostly comic; the change is one round and there is no roll to resist it. + +*Measured alone against the frozen party: 0.49 of 4 down, 0.3% wiped, 4 rounds.* ### Kelpie @@ -234,7 +234,7 @@ Older than the department and largely uninterested in it. Most of these are arra **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. -*Measured alone against the frozen party: 0.24 of 4 down, 0.1% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.38 of 4 down, 0.1% wiped, 3 rounds.* ### Grindylow @@ -248,7 +248,7 @@ Older than the department and largely uninterested in it. Most of these are arra **THE PULL.** Against a target in water it attacks at +40% and a hit means grappled, not damaged; a grappled target is under and rolling CONx5 each round to keep breathing. Out of water it is slow, half-blind and frankly pitiable — the entire fight is about whether anybody is standing where they should not be. -*Measured alone against the frozen party: 0.13 of 4 down, 0% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.24 of 4 down, 0% wiped, 3 rounds.* ### Boggart @@ -290,7 +290,7 @@ Older than the department and largely uninterested in it. Most of these are arra **THE SHORTER WAY.** Every ten minutes of following, each agent rolls Navigate; on a failure the party is one step further into water, bog or a drop they cannot see. Three failures and somebody is in it. -*Measured alone against the frozen party: 0.00 of 4 down, 0% wiped, 1 rounds.* +*Measured alone against the frozen party: 0.00 of 4 down, 0% wiped, 2 rounds.* ### Knockers @@ -340,7 +340,7 @@ The department's own. Every one is a piece of administration that kept going aft **UNCLAIMED.** Anything an agent sets down in the room is the room's after one round, and cannot be picked up again without a STR roll against STR 20. That includes a rifle put down to open a door, and it will include a person who sits down. -*Measured alone against the frozen party: 0.78 of 4 down, 0.9% wiped, 8 rounds.* +*Measured alone against the frozen party: 1.06 of 4 down, 2.1% wiped, 8 rounds.* ### The waiting room @@ -354,21 +354,7 @@ The department's own. Every one is a piece of administration that kept going aft **SHORTLY.** Every agent who sits down rolls POWx5 at the end of each scene; on a failure they do not want to leave yet, and will explain reasonably why not. Three failures and they are one of the four. -*Measured alone against the frozen party: 0.17 of 4 down, 0% wiped, 6 rounds.* - -### The predecessor - -*Whoever had this desk before you never quite handed over* - -| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | -|---|---|---|---|---|---|---|---|---|---| -| 13 | 15 | 12 | 17 | 20 | 13 | 2 | 14 | 7 | 7 | - -**Attacks with** Service pistol at 60%, 1d8+2 · **dodges** 50% - -**HANDOVER.** Once a round it performs a task that agent was about to perform, correctly and first. Mechanically that agent loses their action; the thing still gets done, which is what makes it hard to object to. - -*Measured alone against the frozen party: 0.09 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.30 of 4 down, 0.1% wiped, 6 rounds.* ### The night registry @@ -382,7 +368,21 @@ The department's own. Every one is a piece of administration that kept going aft **CORRECTLY FILED.** Anything left in the building overnight is put where it belongs — which is not where anybody left it, and for a person means the personnel files. An agent who sleeps in the building rolls POWx5; on a failure they wake up on the record as staff from 1961, with that year's clearances and none of their own. -*Measured alone against the frozen party: 0.08 of 4 down, 0% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.14 of 4 down, 0% wiped, 3 rounds.* + +### The predecessor + +*Whoever had this desk before you never quite handed over* + +| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | +|---|---|---|---|---|---|---|---|---|---| +| 13 | 15 | 12 | 17 | 20 | 13 | 2 | 14 | 7 | 7 | + +**Attacks with** Service pistol at 60%, 1d8+2 · **dodges** 50% + +**HANDOVER.** Once a round it performs a task that agent was about to perform, correctly and first. Mechanically that agent loses their action; the thing still gets done, which is what makes it hard to object to. + +*Measured alone against the frozen party: 0.13 of 4 down, 0% wiped, 2 rounds.* ### The carrion file @@ -396,7 +396,7 @@ The department's own. Every one is a piece of administration that kept going aft **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. -*Measured alone against the frozen party: 0.04 of 4 down, 0% wiped, 5 rounds.* +*Measured alone against the frozen party: 0.11 of 4 down, 0% wiped, 5 rounds.* ### The locum @@ -410,7 +410,7 @@ The department's own. Every one is a piece of administration that kept going aft **OF COURSE.** It may assert one credential per scene and it is believed — by NPCs automatically, and by an agent unless they beat it on Insight against its Persuade. It is not hostile until challenged in front of witnesses, at which point it becomes extremely hostile and extremely plausible about it. -*Measured alone against the frozen party: 0.04 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.09 of 4 down, 0% wiped, 2 rounds.* ### The understudy @@ -424,7 +424,7 @@ The department's own. Every one is a piece of administration that kept going aft **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. -*Measured alone against the frozen party: 0.01 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.05 of 4 down, 0% wiped, 2 rounds.* ### Hollow man @@ -532,7 +532,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **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. -*Measured alone against the frozen party: 3.71 of 4 down, 88.8% wiped, 8 rounds.* +*Measured alone against the frozen party: 3.69 of 4 down, 87.4% wiped, 7 rounds.* ### Long walker @@ -546,7 +546,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **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. -*Measured alone against the frozen party: 3.18 of 4 down, 64.1% wiped, 15 rounds.* +*Measured alone against the frozen party: 3.32 of 4 down, 68% wiped, 14 rounds.* ### The stanchion @@ -560,7 +560,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **DRIVEN.** While it stands the crossing beside it cannot be closed by any means, including the Quiet Arts. It has 12 natural armour, so small arms do literally nothing and the party will need the cutter, the demolition charge, or an idea. -*Measured alone against the frozen party: 2.29 of 4 down, 31.9% wiped, 19 rounds.* +*Measured alone against the frozen party: 2.52 of 4 down, 37.4% wiped, 19 rounds.* ### The arrears @@ -574,21 +574,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **THE PRINCIPAL.** It knows precisely what the department took and from whom; the debt is real and the file confirming it exists in the Registry. Settling it ends the encounter instantly and costs the department something the players will not want to give. -*Measured alone against the frozen party: 1.62 of 4 down, 8.9% wiped, 11 rounds.* - -### Surveyor - -*It is measuring the site and it will finish* - -| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | -|---|---|---|---|---|---|---|---|---|---| -| 15 | 18 | 13 | 17 | 12 | 14 | 8 | 16 | 8 | 7 | - -**Attacks with** Redaction lance at 70%, 1d4+6 · **dodges** 50% - -**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. - -*Measured alone against the frozen party: 1.39 of 4 down, 4.8% wiped, 10 rounds.* +*Measured alone against the frozen party: 1.98 of 4 down, 12.8% wiped, 12 rounds.* ### The courier @@ -604,7 +590,21 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **THE CONSIGNMENT.** Everything it carries is in the single grasp limb and it will not use that limb to fight while it is carrying — so it attacks with the brace limb at HALF the rating above until the consignment is taken from it. Disable the grasp limb and it drops the consignment and fights properly, at full rating and furious. -*Measured alone against the frozen party: 1.37 of 4 down, 6.6% wiped, 9 rounds.* +*Measured alone against the frozen party: 1.73 of 4 down, 9.4% wiped, 10 rounds.* + +### Surveyor + +*It is measuring the site and it will finish* + +| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | +|---|---|---|---|---|---|---|---|---|---| +| 15 | 18 | 13 | 17 | 12 | 14 | 8 | 16 | 8 | 7 | + +**Attacks with** Redaction lance at 70%, 1d4+6 · **dodges** 50% + +**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. + +*Measured alone against the frozen party: 1.56 of 4 down, 6.3% wiped, 11 rounds.* ### Cuckoo array @@ -618,7 +618,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **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. -*Measured alone against the frozen party: 0.78 of 4 down, 1.5% wiped, 8 rounds.* +*Measured alone against the frozen party: 1.02 of 4 down, 1.7% wiped, 8 rounds.* ### The yield @@ -634,7 +634,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **THE COUNT.** It takes four. Not five, not three — if the party stops it taking a fourth it will keep working until it has one, and it will not leave without them. -*Measured alone against the frozen party: 0.72 of 4 down, 0.9% wiped, 6 rounds.* +*Measured alone against the frozen party: 1.01 of 4 down, 2.9% wiped, 6 rounds.* ### The relay @@ -650,7 +650,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **WHILE QUORATE.** The crossing holds while three or more bodies stand. Take it to two and the crossing begins to close over a minute; take it to one and it closes now, with whatever is halfway through still halfway through. -*Measured alone against the frozen party: 0.60 of 4 down, 0.5% wiped, 10 rounds.* +*Measured alone against the frozen party: 0.94 of 4 down, 1.4% wiped, 10 rounds.* ### Precedent @@ -666,7 +666,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **COUNTERSIGNED.** Any document an agent files about this scene is found to already bear a second signature, in the agent's own hand, dated earlier. The agent gets no roll; the paperwork is simply already done and already wrong. -*Measured alone against the frozen party: 0.17 of 4 down, 0% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.30 of 4 down, 0.1% wiped, 4 rounds.* ### The census @@ -682,21 +682,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **ENUMERATED.** When it counts an agent — one round, no roll, it simply looks — that agent is on a list on the far side. Thereafter anything from the far side knows where they are, permanently, and the department cannot get them off it. -*Measured alone against the frozen party: 0.15 of 4 down, 0% wiped, 4 rounds.* - -### The auditor - -*It has read the file and it has questions* - -| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | -|---|---|---|---|---|---|---|---|---|---| -| 11 | 14 | 11 | 21 | 20 | 12 | 4 | 13 | 7 | 7 | - -**Attacks with** Service pistol at 55%, 1d8+2 · **dodges** 40% - -**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. - -*Measured alone against the frozen party: 0.12 of 4 down, 0% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.27 of 4 down, 0.1% wiped, 4 rounds.* ### The overwriter @@ -712,7 +698,21 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **THE CORRECTION.** Once a round it corrects one FACT the party is relying on — an address, a name in a file, which door the key opens, who signed the warrant. The correction is retroactive and true, and no roll resists it; the party simply finds the world has always been slightly different. -*Measured alone against the frozen party: 0.12 of 4 down, 0% wiped, 6 rounds.* +*Measured alone against the frozen party: 0.25 of 4 down, 0% wiped, 6 rounds.* + +### The auditor + +*It has read the file and it has questions* + +| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | +|---|---|---|---|---|---|---|---|---|---| +| 11 | 14 | 11 | 21 | 20 | 12 | 4 | 13 | 7 | 7 | + +**Attacks with** Service pistol at 55%, 1d8+2 · **dodges** 40% + +**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. + +*Measured alone against the frozen party: 0.17 of 4 down, 0% wiped, 3 rounds.* ### The margin @@ -728,7 +728,7 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **NOT MENTIONED.** It can only be perceived by an agent who is NOT currently looking for it; anybody actively searching rolls Spot at −50%, and anybody who has stopped searching sees it immediately. It steals documents and it does not steal anything else. -*Measured alone against the frozen party: 0.02 of 4 down, 0% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.05 of 4 down, 0% wiped, 3 rounds.* --- @@ -746,7 +746,7 @@ People, mostly — contractors, locals, and the ones who were already here. **Attacks with** Grasping limb at 65%, 1d6+1 · **dodges** 40% -*Measured alone against the frozen party: 0.16 of 4 down, 0.1% wiped, 3 rounds.* +*Measured alone against the frozen party: 0.27 of 4 down, 0.1% wiped, 3 rounds.* ### Cleaner (marksman) @@ -758,7 +758,7 @@ People, mostly — contractors, locals, and the ones who were already here. **Attacks with** Marksman rifle at 55%, 1d10+2 · **dodges** 35% -*Measured alone against the frozen party: 0.08 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.11 of 4 down, 0% wiped, 2 rounds.* ### Duplicate (of an agent) @@ -770,7 +770,7 @@ People, mostly — contractors, locals, and the ones who were already here. **Attacks with** Service pistol at 50%, 1d8+2 · **dodges** 45% -*Measured alone against the frozen party: 0.06 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.08 of 4 down, 0% wiped, 2 rounds.* ### The Keepers (waystation) @@ -782,6 +782,18 @@ People, mostly — contractors, locals, and the ones who were already here. **Attacks with** Utility knife at 55%, 1d4+2 · **dodges** 45% +*Measured alone against the frozen party: 0.06 of 4 down, 0% wiped, 2 rounds.* + +### The Quiet Neighbours + +*Near-world locals who have lived beside the threshold for generations* + +| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | +|---|---|---|---|---|---|---|---|---|---| +| 9 | 12 | 8 | 11 | 11 | 16 | 0 | 10 | 5 | 7 | + +**Attacks with** Improvised spear at 50%, 1d8+1 · **dodges** 68% + *Measured alone against the frozen party: 0.04 of 4 down, 0% wiped, 2 rounds.* ### Cleaner (contractor) @@ -794,19 +806,7 @@ People, mostly — contractors, locals, and the ones who were already here. **Attacks with** Service pistol at 45%, 1d8+2 · **dodges** 40% -*Measured alone against the frozen party: 0.03 of 4 down, 0% wiped, 2 rounds.* - -### The Quiet Neighbours - -*Near-world locals who have lived beside the threshold for generations* - -| STR | CON | SIZ | INT | POW | DEX | Armour | HP | Major wound | Locations | -|---|---|---|---|---|---|---|---|---|---| -| 9 | 12 | 8 | 11 | 11 | 16 | 0 | 10 | 5 | 7 | - -**Attacks with** Improvised spear at 50%, 1d8+1 · **dodges** 68% - -*Measured alone against the frozen party: 0.02 of 4 down, 0% wiped, 2 rounds.* +*Measured alone against the frozen party: 0.04 of 4 down, 0% wiped, 2 rounds.* --- diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 784295e..9e4972f 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -5703,3 +5703,65 @@ points check-focus records for this creature, seen once instead of averaged. The defence ladder is legible in the log too — `Ben → Redcap A: 26 SUCCESS — dodged (10 vs 45, -30 for defences already spent)` — a 75% dodge meeting the second attack of the round at 45. + +## R-264 — the harness was fighting a different animal + +Reading a narrated supporter fight (R-263, the first time any fight in this project could +be read rather than totalled) showed it being hit on `armR` and `head`. It has wings. It +has never had arms. Three defects, all in the pipeline every published number comes from. + +**1. It located hits by species, not body plan.** `simulate.mjs` read `defender.species` +where the game writes `spec.bodyPlan ?? spec.species ?? "baseline"` into `speciesProfile` +(build-packs.mjs:294). Four creatures were fought human-shaped: the barghest, the kelpie +and the church grim on two legs with arms, and **the supporter with no wings at all**, so +the fight its own tactics call the one the agents can win could not occur in a measured +fight. + +**2. Armour was one scalar for the whole creature.** `worn + naturalArmour`, applied +everywhere, and the harness's locations had no armour field at all. So bare wings, the +grounded halving and the vital exemption — R-254 and R-255, most of a session's design — +were invisible to every number. Worn armour was summed the same way, which put a stab +vest on a cleaner's head. + +**3. Nothing was ever grounded.** A wing could be ruined and the creature kept flying, +because the harness never changed a body mid-fight. + +**The fix, in the game's own order.** Locate first, then apply what that location carries: +`locationArmourFor(slot.armour + naturalArmourOn(slot, exposedHideFor(hide, grounded, +kind)), worn, kind)`, every term imported. A ruined wing now calls `groundedPlanFor`, and +the wounds carry across by severity through `remapLocationDamage` — the same function the +actor's `_preUpdate` uses. + +**A bug of mine that no guard would have caught.** `remapLocationDamage` returns +`{ damage, moved, rescaled }` and my first draft passed the whole object where a damage +map was expected, so **every wound a creature was carrying was forgiven the moment it came +down**. check-lethality would have passed it happily: fewer wounds means a longer fight, +which reads as a number moving, and the whole point of this commit is that numbers move. +Found by probing a landing by hand, not by the suite. The engine destructures it the same +way at ringbrp.mjs:462, which is where I should have looked first. + +**What it cost. 20 of 47 creatures moved, 18 deadlier and 2 less.** + +| | wiped | down of 4 | +|---|---|---| +| **The supporter** | 69.3% → **25.1%** | 3.38 → 2.16 | +| The stanchion | 31.9% → 37.4% | 2.29 → 2.52 | +| Nuckelavee | 16% → 20.3% | 1.70 → 1.98 | +| Long walker | 64.1% → 68% | 3.18 → 3.32 | + +The supporter falls from second deadliest in the book to fourth, and that is the correct +direction: it was being measured as a 30-hit-point creature in uniform armour 9 that could +not be grounded, and it is actually a creature you can put on the floor, after which most +of it is armour 4. The small rises everywhere else are the party's own armour no longer +covering locations it never protected — a torso vest stops covering a head. + +Focus fire was re-recorded too: 30 of 47 measurable now rather than 31, worth 8 points +against a pair and 12 against three or more, best case 3 × Spriggan at 38.9% → 55.5%. + +**And check-focus caught the page overclaiming, which is what it is for.** Its claim-check +failed on the new numbers: focus fire still helps in all 30, but only 28 clear their own +noise where all 31 once did, and the page said "each time". The guard was asserting more +than the page needs to: the bestiary prints the above-noise COUNT from the artifact and so +cannot overstate it however it moves, and what the page actually asserts outright is that +concentrating fire helps. The guard now checks exactly that, and the page rewrote itself +to "in 28 of them" on regeneration. diff --git a/system.json b/system.json index 2c96728..edc5ace 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "ringbrp", "title": "The Custodians", "description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.", - "version": "1.7.17", + "version": "1.8.0", "compatibility": { "minimum": 13, "verified": "14.364" diff --git a/tools/check-focus.mjs b/tools/check-focus.mjs index 3cfd7ff..7d67d6e 100644 --- a/tools/check-focus.mjs +++ b/tools/check-focus.mjs @@ -184,12 +184,17 @@ if (problems.length || added.length || removed.length) { /* The claim the bestiary makes, restated as a test rather than as prose: concentrating a round's attacks is supposed to help against every pack whose fight is in doubt. If it ever stops doing that, the page is wrong and should be rewritten, not re-recorded. */ -if (helped !== checked || aboveNoise !== checked) { - console.error(`check-focus: FAILED — focus fire helped ${helped} of ${checked} ` - + `(${aboveNoise} beyond their own noise). docs/BESTIARY.md claims it helps in every ` - + `fight that is in doubt, and that is no longer true.`); +/* Two different claims, and only one of them is the page's. The bestiary states the + above-noise COUNT from this artifact, so it cannot overstate that however the number + moves — R-264 dropped it from 31 of 31 to 28 of 30 and the page rewrote itself. What + the page does assert outright is that concentrating fire helps, and that is what fails + here. A failure means rewrite the advice; it is not a baseline to re-record. */ +if (helped !== checked) { + console.error(`check-focus: FAILED — focus fire helped only ${helped} of ${checked} packs. ` + + `docs/BESTIARY.md advises concentrating fire against every pack whose fight is in ` + + `doubt, and that is no longer true. Rewrite the advice rather than re-recording.`); process.exit(1); } -console.log(`check-focus: OK — focus fire measured against ${checked} packs, helps in all of them, ` - + `every gain above its own noise`); +console.log(`check-focus: OK — focus fire measured against ${checked} packs, helps in all of them` + + `, ${aboveNoise} of them by more than their own noise`); diff --git a/tools/focus-baseline.json b/tools/focus-baseline.json index 1b5274d..c150248 100644 --- a/tools/focus-baseline.json +++ b/tools/focus-baseline.json @@ -20,66 +20,66 @@ "creatures": { "quiet_neighbours_npc": { "n": 6, - "spread": 60.5, - "focus": 70.1, - "gain": 9.5, - "noise": 1.5 + "spread": 47.4, + "focus": 55.7, + "gain": 8.3, + "noise": 2.5 }, "keepers": { - "n": 6, - "spread": 43.2, - "focus": 58, - "gain": 14.8, - "noise": 4.5 + "n": 5, + "spread": 59.9, + "focus": 70, + "gain": 10.1, + "noise": 0.5 }, "cleaner": { "n": 6, - "spread": 54.6, - "focus": 68.5, - "gain": 13.9, - "noise": 6 + "spread": 57.8, + "focus": 71, + "gain": 13.2, + "noise": 6.8 }, "cleaner_marksman": { "n": 5, - "spread": 46.6, - "focus": 61.5, - "gain": 14.9, - "noise": 1 + "spread": 51.2, + "focus": 62.1, + "gain": 10.9, + "noise": 0.7 }, "revenant": { - "n": 4, - "spread": 32, - "focus": 49.3, - "gain": 17.3, - "noise": 0.7 + "n": 3, + "spread": 63.1, + "focus": 73.3, + "gain": 10.2, + "noise": 1.6 }, "duplicate": { "n": 5, - "spread": 46, - "focus": 61.6, - "gain": 15.6, - "noise": 2.7 + "spread": 58.4, + "focus": 69.3, + "gain": 10.9, + "noise": 3.2 }, "barghest": { "n": 3, - "spread": 31.1, - "focus": 44, - "gain": 12.9, - "noise": 2 + "spread": 26.2, + "focus": 36.8, + "gain": 10.5, + "noise": 2.2 }, "kelpie": { "n": 3, - "spread": 62.8, - "focus": 75.6, + "spread": 54.6, + "focus": 67.3, "gain": 12.8, - "noise": 2.7 + "noise": 1.8 }, "redcap": { "n": 3, - "spread": 40.6, - "focus": 53.8, - "gain": 13.2, - "noise": 3.3 + "spread": 31.4, + "focus": 44, + "gain": 12.6, + "noise": 2 }, "boggart": { "n": 2, @@ -87,54 +87,54 @@ }, "churchgrim": { "n": 3, - "spread": 54.6, - "focus": 67.2, - "gain": 12.5, + "spread": 42.1, + "focus": 54.7, + "gain": 12.6, "noise": 3.4 }, "supporter": { "n": 2, - "pinned": 0.3 + "pinned": 3.8 }, "hollow_man": { - "n": 2, - "pinned": 100 + "n": 6, + "pinned": 92.4 }, "the_tenant": { "n": 2, "pinned": 100 }, "the_choir": { - "n": 2, - "pinned": 100 + "n": 6, + "pinned": 99.1 }, "precedent": { "n": 3, - "spread": 71.2, - "focus": 81.9, - "gain": 10.7, - "noise": 0.9 + "spread": 59.2, + "focus": 71.1, + "gain": 11.9, + "noise": 0.7 }, "carrion_file": { - "n": 5, - "spread": 29.9, - "focus": 44.1, - "gain": 14.2, - "noise": 2.6 + "n": 4, + "spread": 45.8, + "focus": 58.4, + "gain": 12.7, + "noise": 3.6 }, "the_understudy": { - "n": 6, - "spread": 57, - "focus": 72.6, - "gain": 15.6, - "noise": 2.5 + "n": 5, + "spread": 60.2, + "focus": 71.8, + "gain": 11.7, + "noise": 0.8 }, "surveyor": { "n": 2, - "spread": 29.8, - "focus": 36.3, - "gain": 6.5, - "noise": 4.5 + "spread": 27.5, + "focus": 34.5, + "gain": 7, + "noise": 3.7 }, "quarantine_unit": { "n": 2, @@ -142,21 +142,21 @@ }, "cuckoo_array": { "n": 2, - "spread": 65.3, - "focus": 73.3, + "spread": 58.3, + "focus": 66.2, "gain": 7.9, - "noise": 4.8 + "noise": 2.1 }, "long_walker": { "n": 2, - "pinned": 0.9 + "pinned": 0.6 }, "auditor": { "n": 4, - "spread": 58.7, - "focus": 71.8, - "gain": 13.1, - "noise": 2.1 + "spread": 52, + "focus": 64.5, + "gain": 12.5, + "noise": 1.6 }, "washer_ford": { "n": 2, @@ -167,11 +167,11 @@ "pinned": 100 }, "grindylow": { - "n": 4, - "spread": 41.9, - "focus": 56.7, - "gain": 14.8, - "noise": 3.2 + "n": 3, + "spread": 70.6, + "focus": 80.1, + "gain": 9.4, + "noise": 2.9 }, "knockers": { "n": 2, @@ -179,125 +179,122 @@ }, "spriggan": { "n": 3, - "spread": 50.4, - "focus": 63.8, - "gain": 13.4, + "spread": 38.9, + "focus": 55.5, + "gain": 16.6, "noise": 3.4 }, "nuckelavee": { "n": 2, - "pinned": 11 + "pinned": 8.5 }, "hedley_kow": { - "n": 2, - "pinned": 100 + "n": 6, + "pinned": 97.6 }, "apple_man": { "n": 2, - "spread": 45.4, - "focus": 51.9, - "gain": 6.5, - "noise": 2.1 + "spread": 36.8, + "focus": 42.5, + "gain": 5.7, + "noise": 7.2 }, "the_committee": { "n": 2, "pinned": 100 }, "night_registry": { - "n": 5, - "spread": 39.5, - "focus": 56.1, - "gain": 16.6, - "noise": 1.2 + "n": 4, + "spread": 62.1, + "focus": 76.2, + "gain": 14, + "noise": 2.5 }, "the_locum": { "n": 5, - "spread": 59.4, - "focus": 71.6, + "spread": 49.3, + "focus": 61.5, "gain": 12.2, - "noise": 1.3 + "noise": 2.4 }, "lost_property": { "n": 2, - "spread": 65.8, - "focus": 71.7, - "gain": 5.9, - "noise": 1.7 + "spread": 54.9, + "focus": 64.1, + "gain": 9.2, + "noise": 2.1 }, "the_minutes": { "n": 2, "pinned": 100 }, "waiting_room": { - "n": 4, - "spread": 29.7, - "focus": 41.6, - "gain": 11.9, - "noise": 5.6 + "n": 3, + "spread": 63.2, + "focus": 71.8, + "gain": 8.6, + "noise": 2.1 }, "the_predecessor": { - "n": 5, - "spread": 32.2, - "focus": 45.5, - "gain": 13.2, - "noise": 3 + "n": 4, + "spread": 55.5, + "focus": 69.2, + "gain": 13.7, + "noise": 6.7 }, "switchboard": { "n": 6, - "pinned": 99.5 + "pinned": 86.1 }, "the_census": { - "n": 4, - "spread": 34, - "focus": 49, - "gain": 15, - "noise": 2.6 + "n": 3, + "spread": 64.3, + "focus": 75.3, + "gain": 11, + "noise": 4.2 }, "the_courier": { "n": 2, - "spread": 23.7, - "focus": 33.5, - "gain": 9.8, - "noise": 1.4 + "spread": 17.5, + "focus": 25.2, + "gain": 7.7, + "noise": 4 }, "the_relay": { "n": 2, - "spread": 63.1, - "focus": 70.3, - "gain": 7.1, - "noise": 4.7 + "spread": 52.6, + "focus": 58, + "gain": 5.4, + "noise": 6.7 }, "the_stanchion": { "n": 2, - "pinned": 6.6 + "pinned": 4 }, "the_yield": { "n": 2, - "spread": 57.2, - "focus": 67.8, - "gain": 10.6, - "noise": 4.8 + "spread": 48.7, + "focus": 62, + "gain": 13.3, + "noise": 3.8 }, "the_overwriter": { - "n": 4, - "spread": 31.7, - "focus": 44, - "gain": 12.3, - "noise": 0.8 + "n": 3, + "spread": 62.6, + "focus": 71.7, + "gain": 9.2, + "noise": 3.9 }, "the_arrears": { "n": 2, - "spread": 19.3, - "focus": 25.4, - "gain": 6.1, - "noise": 2.6 + "pinned": 14.3 }, "the_margin": { - "n": 6, - "spread": 42, - "focus": 61.1, - "gain": 19.1, - "noise": 5.4 + "n": 5, + "spread": 46.7, + "focus": 59.6, + "gain": 12.8, + "noise": 6.2 } } } diff --git a/tools/lethality-baseline.json b/tools/lethality-baseline.json index 3613ff0..d4b01f1 100644 --- a/tools/lethality-baseline.json +++ b/tools/lethality-baseline.json @@ -12,47 +12,47 @@ "creatures": { "quiet_neighbours_npc": { "wipe": 0, - "down": 0.02, + "down": 0.04, "rounds": 2 }, "keepers": { "wipe": 0, - "down": 0.04, + "down": 0.06, "rounds": 2 }, "cleaner": { "wipe": 0, - "down": 0.03, + "down": 0.04, "rounds": 2 }, "cleaner_marksman": { "wipe": 0, - "down": 0.08, + "down": 0.11, "rounds": 2 }, "revenant": { "wipe": 0.1, - "down": 0.16, + "down": 0.27, "rounds": 3 }, "duplicate": { "wipe": 0, - "down": 0.06, + "down": 0.08, "rounds": 2 }, "barghest": { - "wipe": 0.4, - "down": 0.43, + "wipe": 0.7, + "down": 0.64, "rounds": 4 }, "kelpie": { "wipe": 0.1, - "down": 0.24, + "down": 0.38, "rounds": 3 }, "redcap": { - "wipe": 0.1, - "down": 0.34, + "wipe": 0.7, + "down": 0.55, "rounds": 3 }, "boggart": { @@ -61,14 +61,14 @@ "rounds": 2 }, "churchgrim": { - "wipe": 0.1, - "down": 0.28, + "wipe": 0.4, + "down": 0.49, "rounds": 4 }, "supporter": { - "wipe": 69.3, - "down": 3.38, - "rounds": 17 + "wipe": 25.1, + "down": 2.16, + "rounds": 9 }, "hollow_man": { "wipe": 0, @@ -86,43 +86,43 @@ "rounds": 2 }, "precedent": { - "wipe": 0, - "down": 0.17, - "rounds": 3 + "wipe": 0.1, + "down": 0.3, + "rounds": 4 }, "carrion_file": { "wipe": 0, - "down": 0.04, + "down": 0.11, "rounds": 5 }, "the_understudy": { "wipe": 0, - "down": 0.01, + "down": 0.05, "rounds": 2 }, "surveyor": { - "wipe": 4.8, - "down": 1.39, - "rounds": 10 + "wipe": 6.3, + "down": 1.56, + "rounds": 11 }, "quarantine_unit": { - "wipe": 88.8, - "down": 3.71, - "rounds": 8 + "wipe": 87.4, + "down": 3.69, + "rounds": 7 }, "cuckoo_array": { - "wipe": 1.5, - "down": 0.78, + "wipe": 1.7, + "down": 1.02, "rounds": 8 }, "long_walker": { - "wipe": 64.1, - "down": 3.18, - "rounds": 15 + "wipe": 68, + "down": 3.32, + "rounds": 14 }, "auditor": { "wipe": 0, - "down": 0.12, + "down": 0.17, "rounds": 3 }, "washer_ford": { @@ -133,11 +133,11 @@ "lantern_man": { "wipe": 0, "down": 0, - "rounds": 1 + "rounds": 2 }, "grindylow": { "wipe": 0, - "down": 0.13, + "down": 0.24, "rounds": 3 }, "knockers": { @@ -146,13 +146,13 @@ "rounds": 2 }, "spriggan": { - "wipe": 0.1, - "down": 0.29, + "wipe": 0.3, + "down": 0.49, "rounds": 4 }, "nuckelavee": { - "wipe": 16, - "down": 1.7, + "wipe": 20.3, + "down": 1.98, "rounds": 7 }, "hedley_kow": { @@ -161,9 +161,9 @@ "rounds": 2 }, "apple_man": { - "wipe": 2.1, - "down": 1.04, - "rounds": 10 + "wipe": 3.7, + "down": 1.37, + "rounds": 11 }, "the_committee": { "wipe": 0, @@ -172,17 +172,17 @@ }, "night_registry": { "wipe": 0, - "down": 0.08, + "down": 0.14, "rounds": 3 }, "the_locum": { "wipe": 0, - "down": 0.04, + "down": 0.09, "rounds": 2 }, "lost_property": { - "wipe": 0.9, - "down": 0.78, + "wipe": 2.1, + "down": 1.06, "rounds": 8 }, "the_minutes": { @@ -191,13 +191,13 @@ "rounds": 1 }, "waiting_room": { - "wipe": 0, - "down": 0.17, + "wipe": 0.1, + "down": 0.3, "rounds": 6 }, "the_predecessor": { "wipe": 0, - "down": 0.09, + "down": 0.13, "rounds": 2 }, "switchboard": { @@ -206,43 +206,43 @@ "rounds": 4 }, "the_census": { - "wipe": 0, - "down": 0.15, + "wipe": 0.1, + "down": 0.27, "rounds": 4 }, "the_courier": { - "wipe": 6.6, - "down": 1.37, - "rounds": 9 + "wipe": 9.4, + "down": 1.73, + "rounds": 10 }, "the_relay": { - "wipe": 0.5, - "down": 0.6, + "wipe": 1.4, + "down": 0.94, "rounds": 10 }, "the_stanchion": { - "wipe": 31.9, - "down": 2.29, + "wipe": 37.4, + "down": 2.52, "rounds": 19 }, "the_yield": { - "wipe": 0.9, - "down": 0.72, + "wipe": 2.9, + "down": 1.01, "rounds": 6 }, "the_overwriter": { "wipe": 0, - "down": 0.12, + "down": 0.25, "rounds": 6 }, "the_arrears": { - "wipe": 8.9, - "down": 1.62, - "rounds": 11 + "wipe": 12.8, + "down": 1.98, + "rounds": 12 }, "the_margin": { "wipe": 0, - "down": 0.02, + "down": 0.05, "rounds": 3 } } diff --git a/tools/playthrough.mjs b/tools/playthrough.mjs index a254f0f..9d23892 100644 --- a/tools/playthrough.mjs +++ b/tools/playthrough.mjs @@ -38,7 +38,10 @@ if (!spec) { } const party = base.party.map(k => ROSTER.find(r => r.key === k || r.key === "pc_" + k)); const rec = focus.creatures[KEY]; -const N = rec?.n ?? 1; +/* A pinned row records the pack size that was TRIED, not one worth playing: the header + above promises solo for those, and a tool that contradicts its own documentation is the + defect this file exists to help find. Solo also matches what check-lethality records. */ +const N = rec?.gain !== undefined ? rec.n : 1; /* The same generator shape the guards use, so a seed here names the same fight there. */ const mk = seed => { let s = seed >>> 0; return () => ((s = (1664525 * s + 1013904223) >>> 0) / 4294967296); }; @@ -61,7 +64,18 @@ function play(mode) { return; } if (e.kind === "turn") return; + if (e.kind === "grounded") { + lines.push(` >> ${nameOf(e.defender)} COMES DOWN — it is a ${e.plan} now.` + + ` Hide ${e.hide} everywhere, ${e.vital} on the chest, and the wings are gone.`); + return; + } const a = nameOf(e.attacker), d = nameOf(e.defender); + if (e.kind === "stopped") { + lines.push(` ${a} → ${d}: ${e.roll} ${e.level.toUpperCase()} · ${e.location?.id ?? "?"}` + + ` · ${e.rolled} vs armour ${e.armour}${e.effectiveArmour !== e.armour ? `→${e.effectiveArmour}` : ""}` + + ` — stopped dead`); + return; + } if (e.kind === "miss") lines.push(` ${a} → ${d}: ${e.roll} vs ${e.target} — ${e.level}`); else if (e.kind === "dodged") @@ -69,7 +83,7 @@ function play(mode) { + ` (${e.dodgeRoll} vs ${e.dodgeTarget}${e.penalty ? `, ${e.penalty} for defences already spent` : ""})`); else if (e.kind === "hit") lines.push(` ${a} → ${d}: ${e.roll} ${e.level.toUpperCase()}${e.landing !== e.level ? ` → lands ${e.landing}` : ""}` - + ` · ${e.rolled} vs armour ${e.armour}${e.effectiveArmour !== e.armour ? `→${e.effectiveArmour}` : ""}` + + ` · ${e.location?.id ?? "?"} · ${e.rolled} vs armour ${e.armour}${e.effectiveArmour !== e.armour ? `→${e.effectiveArmour}` : ""}` + ` = ${e.taken} · ${d} ${hp(e.defender)}` + `${e.majorWound ? " · MAJOR WOUND" : ""}${e.destroyed ? " · DESTROYED" : e.disabled ? " · disabled" : ""}` + `${e.defender.hp <= 0 ? " ** DOWN **" : ""}`); diff --git a/tools/simulate.mjs b/tools/simulate.mjs index 1c2ce2e..021bdf7 100644 --- a/tools/simulate.mjs +++ b/tools/simulate.mjs @@ -50,7 +50,8 @@ import { resolveBands, gradeRoll, defenceOutcomeFor, defencePenaltyFor, conditionFor, dyingLimitFor, REACTION, locationFor, resolveLocationHit, locationsFor, locationMaxHp, - locationEffectsFor, woundPenaltyFrom, armourAgainst, damageAfterArmour + locationEffectsFor, woundPenaltyFrom, armourAgainst, damageAfterArmour, + exposedHideFor, naturalArmourOn, locationArmourFor, groundedPlanFor, remapLocationDamage } from "../rules.mjs"; import { WEAPONS, ARMOURS, NPCS, PREGENS } from "./content.mjs"; import { ROSTER } from "./roster.mjs"; @@ -187,17 +188,29 @@ export function buildCombatant(rawSpec, { side = "?" } = {}) { return { w, rating, dmg, usesMod: w.mod !== false, expected: hitChance * meanDmg }; }).filter(Boolean).sort((a, b) => b.expected - a.expected); - const worn = (spec.armour ?? []).reduce((t, k) => t + (ARMOUR.get(k)?.points ?? 0), 0); + /* R-264. Worn armour is PIECES, not a total: a stab vest covers a torso, and summing + its points into one number put it on the wearer's head. Coverage comes from the + catalogue exactly as build-packs reads it. */ + const worn = (spec.armour ?? []).map(k => ARMOUR.get(k)).filter(Boolean) + .map(a => ({ points: Number(a.points) || 0, coverage: a.covers ?? "all" })); - /* Every location this species has, each with its own share of the hit points. The - share comes from the creature's OWN table, so a cadence gets five bodies at 0.28 - apiece and no vital at all, and a vesh gets a ridge instead of a head. */ + /* R-264. The BODY PLAN, not the species — `spec.bodyPlan ?? spec.species ?? "baseline"` + is what build-packs:294 writes to the actor's speciesProfile, and this harness read + `species` alone. Four creatures were fought human-shaped: the barghest, the kelpie + and the church grim on two legs with arms, and the supporter with no wings at all, + so the one fight its own tactics call winnable could not happen in a measured fight. + + Every location carries its own share of the hit points, plus the two fields the hide + rules need: `bare` (a wing carries no hide) and any armour the table gives it. */ const species = spec.species ?? "baseline"; - const locations = locationsFor(species).map(l => ({ - id: l.id, kind: l.kind, - max: locationMaxHp(hp, l.frac), - taken: 0, disabled: false, destroyed: false - })); + const plan = spec.bodyPlan ?? species; + const mkLocations = (planId, tookBefore = {}) => locationsFor(planId).map(l => { + const max = locationMaxHp(hp, l.frac); + const taken = Number(tookBefore[l.id]) || 0; + return { id: l.id, kind: l.kind, bare: !!l.bare, armour: Number(l.armour) || 0, + max, taken, disabled: taken >= max, destroyed: taken >= max * 2 }; + }); + const locations = mkLocations(plan); return { side, key: spec.key, name: spec.name, @@ -207,7 +220,13 @@ export function buildCombatant(rawSpec, { side = "?" } = {}) { majorWounds: 0, dmgMod: parseDice(damageModifierFor((ch.str ?? 0) + (ch.siz ?? 0))), reactionBase: reactionBaseFrom(ch.dex, readiness), - armour: worn + (Number(spec.naturalArmour) || 0), + /* Kept as the creature's overall hide for reporting; what a blow actually meets is + computed per location by armourAt(), because a wing is bare and a grounded belly + is not the back it was drawn with. */ + naturalArmour: Number(spec.naturalArmour) || 0, + armour: (Number(spec.naturalArmour) || 0) + worn.reduce((t, p) => t + p.points, 0), + worn, + plan, grounded: false, mkLocations, dodge: skills.get("dodge:") ?? 0, arms, locations, @@ -229,6 +248,41 @@ const isFighting = c => conditionOf(c) === "up"; /* ---------------------------------------------------------------- one fight */ +/* R-264. What a blow actually meets at one location: the creature's own hide, less + whatever the posture and the location take off it, plus every worn piece that reaches + that kind. Every term comes from rules.mjs — check-rules forbids restating any of it + here, which is how R-257 ended. */ +function armourAt(c, slot) { + return locationArmourFor( + slot.armour + naturalArmourOn(slot, exposedHideFor(c.naturalArmour, c.grounded, slot.kind)), + c.worn, slot.kind); +} + +/* R-264. A flyer with a hole in a wing is a quadruped — the rule the supporter's tactics + call "the fight the agents can actually win", which no measured fight could reach + because this harness never changed a creature's body mid-fight. Wounds carry across by + SEVERITY through remapLocationDamage, the same function the actor's _preUpdate uses, so + a ruined wing arrives as a ruined foreleg rather than being quietly forgiven. */ +function groundIfWinged(c, slot, say) { + if (c.grounded || slot.kind !== "wing" || !(slot.disabled || slot.destroyed)) return; + const landed = groundedPlanFor(c.plan); + if (!landed || landed === c.plan) return; + const took = Object.fromEntries(c.locations.filter(l => l.taken > 0).map(l => [l.id, l.taken])); + /* remapLocationDamage returns { damage, moved, rescaled }, and the first draft of this + handed the whole object to mkLocations — so every wound the creature was carrying was + silently forgiven the moment it came down. No guard would have caught it: fewer wounds + just means a longer fight, which reads as a number moving. The actor's _preUpdate + destructures it the same way at ringbrp.mjs:462. */ + const { damage: carried } = remapLocationDamage(took, c.plan, landed, c.hpMax); + c.locations = c.mkLocations(landed, carried); + c.plan = landed; + c.grounded = true; + c.wounded = locationEffectsFor(c.locations); + say({ kind: "grounded", defender: c, plan: landed, + hide: exposedHideFor(c.naturalArmour, true, "leg"), + vital: exposedHideFor(c.naturalArmour, true, "vital") }); +} + /* R-263. `say` is an optional narration sink, passed down from runFight and defaulting to a no-op. It reports what the simulator already decided — it never touches rng, so a narrated fight and a silent one are the same fight, which is the only way a play-by-play @@ -280,13 +334,25 @@ function attack(rng, attacker, defender, say = () => {}) { if (arm.usesMod) dmg += rollDice(rng, attacker.dmgMod); dmg = Math.max(1, dmg); // R-56: a hit that connects always does something - // R-257: armourAgainst and damageAfterArmour come from rules.mjs, the same two the - // game applies. This harness used to restate both inline; they agreed, which is - // exactly how a rule change reaches the game and not the numbers that describe it. - const armour = armourAgainst(landing, defender.armour); + /* R-264. WHERE IT LANDS IS DECIDED BEFORE WHAT STOPS IT, because that is the order the + game resolves in and the two cannot be separated: a wing carries no hide, a grounded + belly carries half, and a chest carries all of it whatever posture it is in. This + harness used to apply one armour number for the whole creature and then locate the + blow afterwards, so the entire design of R-254 and R-255 was invisible to every + number it produced. It also returned early when armour stopped a blow, before the + location was rolled at all. */ + const loc = locationFor(d(rng, 20), defender.plan, mode); + const slot = defender.locations.find(l => l.id === loc.id) ?? defender.locations[0]; + const metArmour = armourAt(defender, slot); // before this blow changes anything + const armour = armourAgainst(landing, metArmour); const taken = damageAfterArmour(dmg, armour); - if (taken <= 0) return; + if (taken <= 0) { + say({ kind: "stopped", attacker, defender, arm, roll, level, landing, + rolled: dmg, armour: metArmour, effectiveArmour: armour, + location: slot, locationLabel: loc.label }); + return; + } /* Locate it. 1d20 against the defender's own species table, then the damage goes to that location AND to general hit points — the two systems agree rather than @@ -298,8 +364,6 @@ function attack(rng, attacker, defender, say = () => {}) { everything, and it degrades toward useless rather than dying. A Vesh, conversely, has a ridge for a vital that is a LONG target rather than a small one. None of that could be measured before, because damage was never located. */ - const loc = locationFor(d(rng, 20), defender.species, mode); - const slot = defender.locations.find(l => l.id === loc.id) ?? defender.locations[0]; const res = resolveLocationHit({ damage: taken, locationMax: slot.max, @@ -317,9 +381,13 @@ function attack(rng, attacker, defender, say = () => {}) { defender.wounded = locationEffectsFor(defender.locations); say({ kind: "hit", attacker, defender, arm, roll, level, landing, - rolled: dmg, armour: defender.armour, effectiveArmour: armour, taken, + rolled: dmg, armour: metArmour, effectiveArmour: armour, taken, location: slot, locationLabel: loc.label, majorWound: res.majorWound, disabled: res.disabled, destroyed: res.destroyed }); + + // After the blow is reported, not before it: the wing is ruined by a hit the reader + // has to have seen first. + groundIfWinged(defender, slot, say); } /** One fight to a conclusion. Returns which side stood, and how long it took. */