diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index e32ddac..c343fbb 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -4521,3 +4521,52 @@ on the first attempt and landed on the second, staunched the bleeding, healed 3, `reviveIfHealed` took Plated out of the dying state without being asked. My harness then called `tickDying` on a revived character and read `.state` off the null it correctly returned — the rule was right and the probe was wrong. + +## Shields — armour with a switch + +A shield is the one piece of armour you *hold*, and it protects you exactly as much as +you are currently holding it up. Everything interesting follows from that switch +having a cost, so the design is: raised it is armour and a defence, lowered it is +weight, and the price of keeping it up is that it occupies a hand and you are shooting +around it. + +* **Raised** — its points apply to what it covers, and it is the only armour you can + also defend with, on a new **Shield** skill. +* **Lowered** — nothing. Verified: heater shield slung reads 0 on every location, + raised reads 4 on torso and both arms and 0 on legs and head. +* **The cost** — −20% to your own ranged attacks while it is up, printed on + the card as plainly as anything else: *"Heater shield −20%"*. That is what stops + "always raised" being the only answer; it is obviously right for a containment + officer and obviously wrong for a marksman. +* **Only one goes up.** Raising a second lowers the first. Two shields is not a stance. + +Seven of them, from a 2-point buckler to a 6-point ballistic shield, with the pavise +and the barrier plate covering legs as well because they are walls you carry. + +**`degradation` finally does something.** It has been on every armour item since the +packs were first built and was read by nothing. Shields wear: a blow costs the shield a +point for every step the attack stood above an ordinary success, so an ordinary hit +costs it nothing, a special costs 1 and a critical costs 2. At its limit it comes apart +— no points, nothing to parry with, and it drops out of your hand and refuses to be +raised again. + +### Two defects found by testing it rather than reading it + +**R-244. Every shield defence quietly rolled Dodge.** I added `"shield"` as a third +defence type and passed it through `defenceTypeAllowed`, which returned it unchanged — +so `kind` was `"shield"`, neither of the `kind === "parry"` branches matched, and the +function fell through to the dodge branch. It was invisible in the chat card and only +showed up as a statistical smell: eight rolls at a supposed 85% produced six failures. +Taking a blow on the shield **is** a parry — it locks with parries and is not a dodge — +so the shield is a flag on a parry rather than a type of its own. + +**R-245. The wear rule was unreachable.** I first wrote it to fire only when the shield +turned a blow aside *completely*. But under graded defences only a critical fully stops +a critical, so a shield would have gone its entire service life without a scratch. Wear +is for what the shield **absorbed**: a shield that drags a critical down to a special +has very much taken that blow, and that is exactly when shields splinter. Ten forced +criticals produced zero wear under the first rule; under the corrected one the shield +went 0 → 2 → 4 → broken in three absorbed blows. + +Both were mine, both were introduced in this change, and both were found by driving the +real functions rather than by reading the diff. diff --git a/icons/ballistic_shield.svg b/icons/ballistic_shield.svg new file mode 100644 index 0000000..b112d3e --- /dev/null +++ b/icons/ballistic_shield.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/barrier_field.svg b/icons/barrier_field.svg new file mode 100644 index 0000000..1e99a1a --- /dev/null +++ b/icons/barrier_field.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/buckler.svg b/icons/buckler.svg new file mode 100644 index 0000000..1fdd799 --- /dev/null +++ b/icons/buckler.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/heater.svg b/icons/heater.svg new file mode 100644 index 0000000..5d919fb --- /dev/null +++ b/icons/heater.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/pavise.svg b/icons/pavise.svg new file mode 100644 index 0000000..d3e954a --- /dev/null +++ b/icons/pavise.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/riot_shield.svg b/icons/riot_shield.svg new file mode 100644 index 0000000..86e06f2 --- /dev/null +++ b/icons/riot_shield.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/targe.svg b/icons/targe.svg new file mode 100644 index 0000000..cf67082 --- /dev/null +++ b/icons/targe.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index b694455..8cc9d88 100644 --- a/lang/en.json +++ b/lang/en.json @@ -893,5 +893,19 @@ "RINGBRP.Coverage.head": "head", "RINGBRP.Sheet.Unarmoured": "bare", "RINGBRP.Sheet.ByLocation": "by location", - "RINGBRP.Item.Coverage": "Covers" + "RINGBRP.Item.Coverage": "Covers", + "RINGBRP.Shield.Raised": "raised — {n} points where it covers", + "RINGBRP.Shield.InTheWay": "raised — you are shooting around it ({mod}%)", + "RINGBRP.Shield.Wear": "{name} takes the blow — {n}/{max}", + "RINGBRP.Shield.Broken": "{name} comes apart. {n}/{max} — it is firewood.", + "RINGBRP.Sheet.RaiseHint": "Raise or lower this shield. Raised it protects what it covers and you can defend with it; lowered it is weight.", + "RINGBRP.Sheet.ShieldUp": "raised", + "RINGBRP.Sheet.ShieldDown": "slung", + "RINGBRP.Sheet.ShieldBroken": "broken", + "RINGBRP.Sheet.ShieldHint": "Take the blow on the shield. It wears against anything better than an ordinary hit.", + "RINGBRP.Notify.ShieldBroken": "{name} is broken. It cannot be raised.", + "RINGBRP.Item.IsShield": "Is a shield", + "RINGBRP.Item.Raised": "Raised", + "RINGBRP.Item.Wear": "Wear", + "RINGBRP.Item.WearMax": "Breaks at" } diff --git a/packs/armour/000003.log b/packs/armour/000003.log new file mode 100644 index 0000000..9b6d27d Binary files /dev/null and b/packs/armour/000003.log differ diff --git a/packs/armour/000005.ldb b/packs/armour/000005.ldb deleted file mode 100644 index 35545db..0000000 Binary files a/packs/armour/000005.ldb and /dev/null differ diff --git a/packs/armour/000006.log b/packs/armour/000006.log deleted file mode 100644 index c6bd211..0000000 Binary files a/packs/armour/000006.log and /dev/null differ diff --git a/packs/armour/CURRENT b/packs/armour/CURRENT index cacca75..1a84852 100644 --- a/packs/armour/CURRENT +++ b/packs/armour/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/armour/LOG b/packs/armour/LOG index b6656cb..ff9cf85 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.815927 175f73000 Recovering log #3 -2026/08/11-10:31:15.816054 175f73000 Level-0 table #5: started -2026/08/11-10:31:15.816169 175f73000 Level-0 table #5: 3399 bytes OK -2026/08/11-10:31:15.816387 175f73000 Delete type=0 #3 -2026/08/11-10:31:15.816415 175f73000 Delete type=3 #2 +2026/08/11-18:22:31.919810 17373b000 Delete type=3 #1 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old deleted file mode 100644 index 10115e3..0000000 --- a/packs/armour/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.321526 1714a3000 Delete type=3 #1 diff --git a/packs/armour/MANIFEST-000002 b/packs/armour/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/armour/MANIFEST-000002 differ diff --git a/packs/armour/MANIFEST-000004 b/packs/armour/MANIFEST-000004 deleted file mode 100644 index 819d483..0000000 Binary files a/packs/armour/MANIFEST-000004 and /dev/null differ diff --git a/packs/expedition/000003.log b/packs/expedition/000003.log new file mode 100644 index 0000000..263295b Binary files /dev/null and b/packs/expedition/000003.log differ diff --git a/packs/expedition/000005.ldb b/packs/expedition/000005.ldb deleted file mode 100644 index 4ff460d..0000000 Binary files a/packs/expedition/000005.ldb and /dev/null differ diff --git a/packs/expedition/000006.log b/packs/expedition/000006.log deleted file mode 100644 index 268b187..0000000 Binary files a/packs/expedition/000006.log and /dev/null differ diff --git a/packs/expedition/CURRENT b/packs/expedition/CURRENT index cacca75..1a84852 100644 --- a/packs/expedition/CURRENT +++ b/packs/expedition/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index e2c93cc..0861364 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.827053 176f8b000 Recovering log #3 -2026/08/11-10:31:15.827202 176f8b000 Level-0 table #5: started -2026/08/11-10:31:15.827356 176f8b000 Level-0 table #5: 4129 bytes OK -2026/08/11-10:31:15.827638 176f8b000 Delete type=0 #3 -2026/08/11-10:31:15.827685 176f8b000 Delete type=3 #2 +2026/08/11-18:22:31.954955 173f47000 Delete type=3 #1 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old deleted file mode 100644 index 8713030..0000000 --- a/packs/expedition/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.359914 170c97000 Delete type=3 #1 diff --git a/packs/expedition/MANIFEST-000002 b/packs/expedition/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/expedition/MANIFEST-000002 differ diff --git a/packs/expedition/MANIFEST-000004 b/packs/expedition/MANIFEST-000004 deleted file mode 100644 index 47fbcd4..0000000 Binary files a/packs/expedition/MANIFEST-000004 and /dev/null differ diff --git a/packs/gear/000003.log b/packs/gear/000003.log new file mode 100644 index 0000000..629ba6d Binary files /dev/null and b/packs/gear/000003.log differ diff --git a/packs/gear/000005.ldb b/packs/gear/000005.ldb deleted file mode 100644 index d9bac03..0000000 Binary files a/packs/gear/000005.ldb and /dev/null differ diff --git a/packs/gear/000006.log b/packs/gear/000006.log deleted file mode 100644 index d1deb9b..0000000 Binary files a/packs/gear/000006.log and /dev/null differ diff --git a/packs/gear/CURRENT b/packs/gear/CURRENT index cacca75..1a84852 100644 --- a/packs/gear/CURRENT +++ b/packs/gear/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/gear/LOG b/packs/gear/LOG index 198b123..90a702f 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.817069 177797000 Recovering log #3 -2026/08/11-10:31:15.817858 177797000 Level-0 table #5: started -2026/08/11-10:31:15.818239 177797000 Level-0 table #5: 31614 bytes OK -2026/08/11-10:31:15.818545 177797000 Delete type=0 #3 -2026/08/11-10:31:15.818601 177797000 Delete type=3 #2 +2026/08/11-18:22:31.921916 172723000 Delete type=3 #1 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old deleted file mode 100644 index c8ab21b..0000000 --- a/packs/gear/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.323743 171caf000 Delete type=3 #1 diff --git a/packs/gear/MANIFEST-000002 b/packs/gear/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/gear/MANIFEST-000002 differ diff --git a/packs/gear/MANIFEST-000004 b/packs/gear/MANIFEST-000004 deleted file mode 100644 index f7cd73c..0000000 Binary files a/packs/gear/MANIFEST-000004 and /dev/null differ diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log new file mode 100644 index 0000000..09b2bf6 Binary files /dev/null and b/packs/npcs/000003.log differ diff --git a/packs/npcs/000005.ldb b/packs/npcs/000005.ldb deleted file mode 100644 index ab23f79..0000000 Binary files a/packs/npcs/000005.ldb and /dev/null differ diff --git a/packs/npcs/000006.log b/packs/npcs/000006.log deleted file mode 100644 index ed99ff7..0000000 Binary files a/packs/npcs/000006.log and /dev/null differ diff --git a/packs/npcs/CURRENT b/packs/npcs/CURRENT index cacca75..1a84852 100644 --- a/packs/npcs/CURRENT +++ b/packs/npcs/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/npcs/LOG b/packs/npcs/LOG index a035e49..b32283b 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.803065 177797000 Recovering log #3 -2026/08/11-10:31:15.804714 177797000 Level-0 table #5: started -2026/08/11-10:31:15.806503 177797000 Level-0 table #5: 223539 bytes OK -2026/08/11-10:31:15.806843 177797000 Delete type=0 #3 -2026/08/11-10:31:15.806889 177797000 Delete type=3 #2 +2026/08/11-18:22:31.938436 172723000 Delete type=3 #1 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old deleted file mode 100644 index f263391..0000000 --- a/packs/npcs/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.341619 170c97000 Delete type=3 #1 diff --git a/packs/npcs/MANIFEST-000002 b/packs/npcs/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/npcs/MANIFEST-000002 differ diff --git a/packs/npcs/MANIFEST-000004 b/packs/npcs/MANIFEST-000004 deleted file mode 100644 index 9793e35..0000000 Binary files a/packs/npcs/MANIFEST-000004 and /dev/null differ diff --git a/packs/powers/000003.log b/packs/powers/000003.log new file mode 100644 index 0000000..d723043 Binary files /dev/null and b/packs/powers/000003.log differ diff --git a/packs/powers/000005.ldb b/packs/powers/000005.ldb deleted file mode 100644 index 7f540c6..0000000 Binary files a/packs/powers/000005.ldb and /dev/null differ diff --git a/packs/powers/000006.log b/packs/powers/000006.log deleted file mode 100644 index 43e306f..0000000 Binary files a/packs/powers/000006.log and /dev/null differ diff --git a/packs/powers/CURRENT b/packs/powers/CURRENT index cacca75..1a84852 100644 --- a/packs/powers/CURRENT +++ b/packs/powers/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/powers/LOG b/packs/powers/LOG index 44e2293..5b533e1 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.812365 175f73000 Recovering log #3 -2026/08/11-10:31:15.812699 175f73000 Level-0 table #5: started -2026/08/11-10:31:15.812860 175f73000 Level-0 table #5: 3295 bytes OK -2026/08/11-10:31:15.813123 175f73000 Delete type=0 #3 -2026/08/11-10:31:15.813163 175f73000 Delete type=3 #2 +2026/08/11-18:22:31.946701 172723000 Delete type=3 #1 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old deleted file mode 100644 index 9e996b0..0000000 --- a/packs/powers/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.352685 170c97000 Delete type=3 #1 diff --git a/packs/powers/MANIFEST-000002 b/packs/powers/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/powers/MANIFEST-000002 differ diff --git a/packs/powers/MANIFEST-000004 b/packs/powers/MANIFEST-000004 deleted file mode 100644 index 20de082..0000000 Binary files a/packs/powers/MANIFEST-000004 and /dev/null differ diff --git a/packs/pregens/000003.log b/packs/pregens/000003.log new file mode 100644 index 0000000..712264f Binary files /dev/null and b/packs/pregens/000003.log differ diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb deleted file mode 100644 index 4f39ac7..0000000 Binary files a/packs/pregens/000005.ldb and /dev/null differ diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log deleted file mode 100644 index 327442d..0000000 Binary files a/packs/pregens/000006.log and /dev/null differ diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT index cacca75..1a84852 100644 --- a/packs/pregens/CURRENT +++ b/packs/pregens/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index 67b8b8a..8380368 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.799594 177797000 Recovering log #3 -2026/08/11-10:31:15.800464 177797000 Level-0 table #5: started -2026/08/11-10:31:15.800988 177797000 Level-0 table #5: 58133 bytes OK -2026/08/11-10:31:15.801383 177797000 Delete type=0 #3 -2026/08/11-10:31:15.801428 177797000 Delete type=3 #2 +2026/08/11-18:22:31.928648 172723000 Delete type=3 #1 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old deleted file mode 100644 index b460cd9..0000000 --- a/packs/pregens/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.331121 1724bb000 Delete type=3 #1 diff --git a/packs/pregens/MANIFEST-000002 b/packs/pregens/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/pregens/MANIFEST-000002 differ diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004 deleted file mode 100644 index ca194a0..0000000 Binary files a/packs/pregens/MANIFEST-000004 and /dev/null differ diff --git a/packs/rules/000003.log b/packs/rules/000003.log new file mode 100644 index 0000000..916d086 Binary files /dev/null and b/packs/rules/000003.log differ diff --git a/packs/rules/000005.ldb b/packs/rules/000005.ldb deleted file mode 100644 index 1147e24..0000000 Binary files a/packs/rules/000005.ldb and /dev/null differ diff --git a/packs/rules/000006.log b/packs/rules/000006.log deleted file mode 100644 index 5e876ff..0000000 Binary files a/packs/rules/000006.log and /dev/null differ diff --git a/packs/rules/CURRENT b/packs/rules/CURRENT index cacca75..1a84852 100644 --- a/packs/rules/CURRENT +++ b/packs/rules/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/rules/LOG b/packs/rules/LOG index 6dd2eaa..2e749bc 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.822560 176f8b000 Recovering log #3 -2026/08/11-10:31:15.823036 176f8b000 Level-0 table #5: started -2026/08/11-10:31:15.823351 176f8b000 Level-0 table #5: 30975 bytes OK -2026/08/11-10:31:15.823593 176f8b000 Delete type=0 #3 -2026/08/11-10:31:15.823625 176f8b000 Delete type=3 #2 +2026/08/11-18:22:31.951456 172f2f000 Delete type=3 #1 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old deleted file mode 100644 index 11bb770..0000000 --- a/packs/rules/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.357907 170c97000 Delete type=3 #1 diff --git a/packs/rules/MANIFEST-000002 b/packs/rules/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/rules/MANIFEST-000002 differ diff --git a/packs/rules/MANIFEST-000004 b/packs/rules/MANIFEST-000004 deleted file mode 100644 index e2e1bf1..0000000 Binary files a/packs/rules/MANIFEST-000004 and /dev/null differ diff --git a/packs/skills/000003.log b/packs/skills/000003.log new file mode 100644 index 0000000..2a55440 Binary files /dev/null and b/packs/skills/000003.log differ diff --git a/packs/skills/000005.ldb b/packs/skills/000005.ldb deleted file mode 100644 index d613d26..0000000 Binary files a/packs/skills/000005.ldb and /dev/null differ diff --git a/packs/skills/000006.log b/packs/skills/000006.log deleted file mode 100644 index aa7dd7d..0000000 Binary files a/packs/skills/000006.log and /dev/null differ diff --git a/packs/skills/CURRENT b/packs/skills/CURRENT index cacca75..1a84852 100644 --- a/packs/skills/CURRENT +++ b/packs/skills/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/skills/LOG b/packs/skills/LOG index 4ecde2e..221a782 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.809540 175f73000 Recovering log #3 -2026/08/11-10:31:15.809752 175f73000 Level-0 table #5: started -2026/08/11-10:31:15.809950 175f73000 Level-0 table #5: 7486 bytes OK -2026/08/11-10:31:15.810216 175f73000 Delete type=0 #3 -2026/08/11-10:31:15.810251 175f73000 Delete type=3 #2 +2026/08/11-18:22:31.912459 172f2f000 Delete type=3 #1 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old deleted file mode 100644 index c4bf034..0000000 --- a/packs/skills/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.314145 1724bb000 Delete type=3 #1 diff --git a/packs/skills/MANIFEST-000002 b/packs/skills/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/skills/MANIFEST-000002 differ diff --git a/packs/skills/MANIFEST-000004 b/packs/skills/MANIFEST-000004 deleted file mode 100644 index 792e8a1..0000000 Binary files a/packs/skills/MANIFEST-000004 and /dev/null differ diff --git a/packs/slice/000003.log b/packs/slice/000003.log new file mode 100644 index 0000000..4a86fe9 Binary files /dev/null and b/packs/slice/000003.log differ diff --git a/packs/slice/000005.ldb b/packs/slice/000005.ldb deleted file mode 100644 index 4851c77..0000000 Binary files a/packs/slice/000005.ldb and /dev/null differ diff --git a/packs/slice/000006.log b/packs/slice/000006.log deleted file mode 100644 index c7d9b16..0000000 Binary files a/packs/slice/000006.log and /dev/null differ diff --git a/packs/slice/CURRENT b/packs/slice/CURRENT index cacca75..1a84852 100644 --- a/packs/slice/CURRENT +++ b/packs/slice/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/slice/LOG b/packs/slice/LOG index be57eab..8638976 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.824364 17677f000 Recovering log #3 -2026/08/11-10:31:15.824536 17677f000 Level-0 table #5: started -2026/08/11-10:31:15.824835 17677f000 Level-0 table #5: 16892 bytes OK -2026/08/11-10:31:15.825060 17677f000 Delete type=0 #3 -2026/08/11-10:31:15.825097 17677f000 Delete type=3 #2 +2026/08/11-18:22:31.945043 17373b000 Delete type=3 #1 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old deleted file mode 100644 index 3be9995..0000000 --- a/packs/slice/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.350616 1714a3000 Delete type=3 #1 diff --git a/packs/slice/MANIFEST-000002 b/packs/slice/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/slice/MANIFEST-000002 differ diff --git a/packs/slice/MANIFEST-000004 b/packs/slice/MANIFEST-000004 deleted file mode 100644 index 0552c3a..0000000 Binary files a/packs/slice/MANIFEST-000004 and /dev/null differ diff --git a/packs/tables/000003.log b/packs/tables/000003.log new file mode 100644 index 0000000..f125b7d Binary files /dev/null and b/packs/tables/000003.log differ diff --git a/packs/tables/000005.ldb b/packs/tables/000005.ldb deleted file mode 100644 index 64baa1c..0000000 Binary files a/packs/tables/000005.ldb and /dev/null differ diff --git a/packs/tables/000006.log b/packs/tables/000006.log deleted file mode 100644 index d5e06f3..0000000 Binary files a/packs/tables/000006.log and /dev/null differ diff --git a/packs/tables/CURRENT b/packs/tables/CURRENT index cacca75..1a84852 100644 --- a/packs/tables/CURRENT +++ b/packs/tables/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/tables/LOG b/packs/tables/LOG index f29fd17..6590e5d 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.825859 175f73000 Recovering log #3 -2026/08/11-10:31:15.825973 175f73000 Level-0 table #5: started -2026/08/11-10:31:15.826082 175f73000 Level-0 table #5: 3739 bytes OK -2026/08/11-10:31:15.826284 175f73000 Delete type=0 #3 -2026/08/11-10:31:15.826314 175f73000 Delete type=3 #2 +2026/08/11-18:22:31.943087 17373b000 Delete type=3 #1 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old deleted file mode 100644 index f1ac225..0000000 --- a/packs/tables/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.347262 1714a3000 Delete type=3 #1 diff --git a/packs/tables/MANIFEST-000002 b/packs/tables/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/tables/MANIFEST-000002 differ diff --git a/packs/tables/MANIFEST-000004 b/packs/tables/MANIFEST-000004 deleted file mode 100644 index 004ff30..0000000 Binary files a/packs/tables/MANIFEST-000004 and /dev/null differ diff --git a/packs/talents/000003.log b/packs/talents/000003.log new file mode 100644 index 0000000..443b7ea Binary files /dev/null and b/packs/talents/000003.log differ diff --git a/packs/talents/000005.ldb b/packs/talents/000005.ldb deleted file mode 100644 index fb653de..0000000 Binary files a/packs/talents/000005.ldb and /dev/null differ diff --git a/packs/talents/000006.log b/packs/talents/000006.log deleted file mode 100644 index 29003f7..0000000 Binary files a/packs/talents/000006.log and /dev/null differ diff --git a/packs/talents/CURRENT b/packs/talents/CURRENT index cacca75..1a84852 100644 --- a/packs/talents/CURRENT +++ b/packs/talents/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/talents/LOG b/packs/talents/LOG index 5bf1264..23ce093 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.810995 177797000 Recovering log #3 -2026/08/11-10:31:15.811100 177797000 Level-0 table #5: started -2026/08/11-10:31:15.811254 177797000 Level-0 table #5: 5770 bytes OK -2026/08/11-10:31:15.811516 177797000 Delete type=0 #3 -2026/08/11-10:31:15.811553 177797000 Delete type=3 #2 +2026/08/11-18:22:31.915613 173f47000 Delete type=3 #1 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old deleted file mode 100644 index 7f324bc..0000000 --- a/packs/talents/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.318325 1724bb000 Delete type=3 #1 diff --git a/packs/talents/MANIFEST-000002 b/packs/talents/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/talents/MANIFEST-000002 differ diff --git a/packs/talents/MANIFEST-000004 b/packs/talents/MANIFEST-000004 deleted file mode 100644 index f8c6b47..0000000 Binary files a/packs/talents/MANIFEST-000004 and /dev/null differ diff --git a/packs/vehicles/000003.log b/packs/vehicles/000003.log new file mode 100644 index 0000000..9950ce5 Binary files /dev/null and b/packs/vehicles/000003.log differ diff --git a/packs/vehicles/000005.ldb b/packs/vehicles/000005.ldb deleted file mode 100644 index f6b2cd4..0000000 Binary files a/packs/vehicles/000005.ldb and /dev/null differ diff --git a/packs/vehicles/000006.log b/packs/vehicles/000006.log deleted file mode 100644 index 47800e2..0000000 Binary files a/packs/vehicles/000006.log and /dev/null differ diff --git a/packs/vehicles/CURRENT b/packs/vehicles/CURRENT index cacca75..1a84852 100644 --- a/packs/vehicles/CURRENT +++ b/packs/vehicles/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/vehicles/LOG b/packs/vehicles/LOG index fec31df..924b9a0 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.819847 17677f000 Recovering log #3 -2026/08/11-10:31:15.820254 17677f000 Level-0 table #5: started -2026/08/11-10:31:15.821322 17677f000 Level-0 table #5: 6939 bytes OK -2026/08/11-10:31:15.821576 17677f000 Delete type=0 #3 -2026/08/11-10:31:15.821605 17677f000 Delete type=3 #2 +2026/08/11-18:22:31.924112 172723000 Delete type=3 #1 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old deleted file mode 100644 index e1200c4..0000000 --- a/packs/vehicles/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.325901 170c97000 Delete type=3 #1 diff --git a/packs/vehicles/MANIFEST-000002 b/packs/vehicles/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/vehicles/MANIFEST-000002 differ diff --git a/packs/vehicles/MANIFEST-000004 b/packs/vehicles/MANIFEST-000004 deleted file mode 100644 index d4d73b7..0000000 Binary files a/packs/vehicles/MANIFEST-000004 and /dev/null differ diff --git a/packs/weapons/000003.log b/packs/weapons/000003.log new file mode 100644 index 0000000..e1488ed Binary files /dev/null and b/packs/weapons/000003.log differ diff --git a/packs/weapons/000005.ldb b/packs/weapons/000005.ldb deleted file mode 100644 index 742beba..0000000 Binary files a/packs/weapons/000005.ldb and /dev/null differ diff --git a/packs/weapons/000006.log b/packs/weapons/000006.log deleted file mode 100644 index 4d427a6..0000000 Binary files a/packs/weapons/000006.log and /dev/null differ diff --git a/packs/weapons/CURRENT b/packs/weapons/CURRENT index cacca75..1a84852 100644 --- a/packs/weapons/CURRENT +++ b/packs/weapons/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/weapons/LOG b/packs/weapons/LOG index 5fcd2da..6ff2401 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -1,5 +1 @@ -2026/08/11-10:31:15.814063 177797000 Recovering log #3 -2026/08/11-10:31:15.814421 177797000 Level-0 table #5: started -2026/08/11-10:31:15.814700 177797000 Level-0 table #5: 14767 bytes OK -2026/08/11-10:31:15.814977 177797000 Delete type=0 #3 -2026/08/11-10:31:15.815010 177797000 Delete type=3 #2 +2026/08/11-18:22:31.917625 172f2f000 Delete type=3 #1 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old deleted file mode 100644 index bac01ca..0000000 --- a/packs/weapons/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-10:29:47.320066 170c97000 Delete type=3 #1 diff --git a/packs/weapons/MANIFEST-000002 b/packs/weapons/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/weapons/MANIFEST-000002 differ diff --git a/packs/weapons/MANIFEST-000004 b/packs/weapons/MANIFEST-000004 deleted file mode 100644 index 6a582af..0000000 Binary files a/packs/weapons/MANIFEST-000004 and /dev/null differ diff --git a/ringbrp.mjs b/ringbrp.mjs index 36b7e43..75ebeec 100644 --- a/ringbrp.mjs +++ b/ringbrp.mjs @@ -34,6 +34,7 @@ import { BURST, burstHitsFor, burstRoundsSpent, BURST_FUMBLE, BURST_ONE_MAJOR_WOUND, BURST_HITS, LAMP, lampReachFor, lampCovers, COVERAGE, coverageFor, armourCoversKind, locationArmourFor, + SHIELD, shieldArmourFor, shieldRangedPenaltyFor, shieldWearFor, shieldBroken, FIRE_MODE, fireModeFor, burstSizesFor, fireModeAttackFor, secondShotPenaltyFor, SUSTAIN, sustainDamageFor, sustainChargeFor, SIGHT, sightFor, sightBonusFor, sightSeesInDark, @@ -666,10 +667,30 @@ class RingBRPActor extends Actor { // Every piece used to add to every location, so a Brodie helmet protected your // legs. Pieces now declare coverage and the total is computed per location. const naturalArmour = Number(sys.naturalArmour?.value) || 0; + // A shield is armour you HOLD: its points count only while it is raised and only + // while it is still a shield. Lowered or broken it is weight and nothing else. const wornArmour = this.items.filter(i => i.type === "armour" && i.system.equipped) - .map(i => ({ name: i.name, points: Number(i.system.points) || 0, - coverage: i.system.coverage || "all" })); + .map(i => { + const isShield = !!i.system.shield; + const broken = isShield && shieldBroken(i.system.degradation?.current, + i.system.degradation?.max); + const pts = Number(i.system.points) || 0; + return { name: i.name, shield: isShield, raised: !!i.system.raised, broken, + points: isShield ? shieldArmourFor(pts, { raised: !!i.system.raised, broken }) + : pts, + coverage: i.system.coverage || "all" }; + }) + .filter(p => p.points > 0 || !p.shield); sys.wornArmour = wornArmour; + // The shield in hand, if there is one up. Everything else reads this. + { + const up = this.items.find(i => i.type === "armour" && i.system.shield + && i.system.equipped && i.system.raised + && !shieldBroken(i.system.degradation?.current, i.system.degradation?.max)); + sys.shieldUp = up ? { id: up.id, name: up.name, points: Number(up.system.points) || 0, + wear: Number(up.system.degradation?.current) || 0, + wearMax: Number(up.system.degradation?.max) || 0 } : null; + } // --- hit locations (spec §3.6) --- const speciesId = sys.speciesProfile || "baseline"; @@ -2967,6 +2988,13 @@ export async function medicalCare(medic, patient, { difficulty = "average" } = { * Returns the roll result plus whether the attack was turned aside, so a caller * can decide what that means: a dodge avoids the blow, a parry stops it. */ +/** The shield this actor currently has up, as the item — null if none, or if broken. */ +function this_shield(actor) { + return actor?.items?.find(i => i.type === "armour" && i.system.shield + && i.system.equipped && i.system.raised + && !shieldBroken(i.system.degradation?.current, i.system.degradation?.max)) ?? null; +} + export async function defend(actor, { type = "dodge", weapon = null, difficulty = "average", against = null } = {}) { if (!actor) return null; @@ -2976,15 +3004,28 @@ export async function defend(actor, { type = "dodge", weapon = null, difficulty const incoming = against ?? standingBlow(actor)?.level ?? "success"; const combat = actor.system.combat ?? {}; const used = Number(combat.defencesThisRound) || 0; - const kind = defenceTypeAllowed(combat.defenceTypeLock, type); - if (kind !== type) { + // Taking it on the shield IS a parry — it locks with parries and it is not a dodge. + // What differs is the skill it rolls, so the shield is a flag rather than a third + // defence type: passing "shield" through defenceTypeAllowed produced a `kind` the + // branches below did not recognise, and every shield defence quietly rolled Dodge. + const wantsShield = type === "shield"; + const asked = wantsShield ? "parry" : type; + const kind = defenceTypeAllowed(combat.defenceTypeLock, asked); + if (kind !== asked) { ui.notifications?.info(game.i18n.format("RINGBRP.Notify.DefenceLocked", { kind: game.i18n.localize(`RINGBRP.Defence.${kind}`) })); } - // Dodge is its own skill; a parry uses the weapon you are holding. + // Dodge is its own skill; a parry uses the weapon you are holding — or the shield, + // which is the whole point of a shield and the only armour you can defend with. let rating = 0, label = "", famId = "", catId = "phys"; - if (kind === "parry" && weapon) { + const shieldItem = (kind === "parry" && (wantsShield || !weapon)) + ? this_shield(actor) : null; + if (kind === "parry" && shieldItem) { + rating = actor.skillRating(SHIELD.parryFamily, SHIELD.parrySpec) ?? 0; + label = game.i18n.format("RINGBRP.Defence.ParryWith", { name: shieldItem.name }); + famId = SHIELD.parryFamily; catId = "melee"; + } else if (kind === "parry" && weapon) { rating = actor.skillRating(weapon.system.skillFamilyId, weapon.system.skillSpecialisationId) ?? 0; label = game.i18n.format("RINGBRP.Defence.ParryWith", { name: weapon.name }); famId = weapon.system.skillFamilyId; catId = "melee"; @@ -3008,6 +3049,25 @@ export async function defend(actor, { type = "dodge", weapon = null, difficulty "system.combat.defenceTypeLock": kind }); const outcome = defenceOutcomeFor(incoming, res.level); + // Shields break. Wear is taken for what the shield ABSORBED, not only for what it + // stopped outright — a shield that turns a critical down to a special has very much + // taken that blow, and it is exactly then that shields splinter. Requiring a clean + // stop made this unreachable: only a critical defence fully stops a critical, so a + // shield would have gone its entire life without a scratch. + if (shieldItem && (outcome.turnedAside || outcome.steps > 0)) { + const wear = shieldWearFor(incoming); + if (wear) { + const cur = (Number(shieldItem.system.degradation?.current) || 0) + wear; + await shieldItem.update({ "system.degradation.current": cur }); + const max = Number(shieldItem.system.degradation?.max) || 0; + const broken = shieldBroken(cur, max); + await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), + content: `
${ + game.i18n.format(broken ? "RINGBRP.Shield.Broken" : "RINGBRP.Shield.Wear", + { name: shieldItem.name, n: cur, max })}
` }); + if (broken) await shieldItem.update({ "system.raised": false }); + } + } // The blow now stands at whatever survived the defence, so the attacker's damage // roll uses the reduced level and a stopped blow can no longer roll damage at all. await amendBlow(outcome.turnedAside ? { stopped: true } : { level: outcome.landsAt }); @@ -4708,7 +4768,15 @@ class RingBRPCharacterSheet extends ActorSheet { ctx.weapons = this.actor.items.filter(i => i.type === "weapon"); ctx.equippedWeapons = ctx.weapons.filter(w => w.system.equipped); - ctx.armours = this.actor.items.filter(i => i.type === "armour"); + // A view wrapper rather than the documents themselves: the template needs two + // derived flags and mutating a live Item to carry them is a good way to have + // them turn up somewhere they were never meant to be. + ctx.armours = this.actor.items.filter(i => i.type === "armour").map(i => ({ + id: i.id, name: i.name, img: i.img, system: i.system, + raisedNow: !!(i.system.shield && i.system.raised), + brokenNow: !!(i.system.shield && shieldBroken(i.system.degradation?.current, + i.system.degradation?.max)) + })); // Transport is listed but never weighed: a vehicle is not carried, it carries. // Access the team holds. Kept beside the gear because that is what it is: a // thing you carry, spend, and can lose. @@ -5009,6 +5077,23 @@ class RingBRPCharacterSheet extends ActorSheet { el.addEventListener("click", ev => this._onItemCreate(ev))); root.querySelectorAll("[data-action='wound-clear']").forEach(el => el.addEventListener("click", ev => this._onWoundClear(ev))); + root.querySelectorAll("[data-action='toggle-raised']").forEach(el => + el.addEventListener("click", async ev => { + const it = this._itemFrom(ev); + if (!it) return; + if (shieldBroken(it.system.degradation?.current, it.system.degradation?.max)) { + return ui.notifications?.warn(game.i18n.format("RINGBRP.Notify.ShieldBroken", + { name: it.name })); + } + // Only one shield goes up. Two shields is not a stance, it is a costume. + if (!it.system.raised) { + for (const other of this.actor.items) { + if (other.id !== it.id && other.type === "armour" && other.system.shield + && other.system.raised) await other.update({ "system.raised": false }); + } + } + await it.update({ "system.raised": !it.system.raised, "system.equipped": true }); + })); root.querySelectorAll("[data-action='toggle-equipped']").forEach(el => el.addEventListener("click", ev => { const it = this._itemFrom(ev); @@ -5687,6 +5772,7 @@ async function promptAttackOptions({ weapon, actor }) { // selects, because both are facts about the pack rather than about the shot. const sight = sightForWeapon(actor, weapon); const lamp = lightOn(actor); + const shieldUp = actor.system.shieldUp; const kit = []; if (sight.id !== "none") { const bands = sightFor(sight.id).bands; @@ -5695,6 +5781,13 @@ async function promptAttackOptions({ weapon, actor }) { .map(([b, v]) => `${v > 0 ? "+" : ""}${v}% ${game.i18n.localize(`RINGBRP.Range.${b}`)}`) .join(" · ") }); } + if (shieldUp && weapon.system.isRanged) { + kit.push({ name: shieldUp.name, effect: game.i18n.format("RINGBRP.Shield.InTheWay", + { mod: shieldRangedPenaltyFor(true) }) }); + } else if (shieldUp) { + kit.push({ name: shieldUp.name, effect: game.i18n.format("RINGBRP.Shield.Raised", + { n: shieldUp.points }) }); + } if (lamp.on) { kit.push({ name: lamp.source, effect: game.i18n.format( lamp.own ? "RINGBRP.Sight.LampOwn" : "RINGBRP.Sight.LampNear", @@ -5780,6 +5873,13 @@ async function promptAttackOptions({ weapon, actor }) { if (lit.revealed) lines.push(game.i18n.format("RINGBRP.Sight.Revealed", { name: lamp.source || game.i18n.localize("RINGBRP.Sight.SomeLamp") })); + // You are shooting around your own shield. This is what it costs, and it + // is the reason "always raised" is not the only answer. + if (shieldUp && weapon.system.isRanged) { + const sp = shieldRangedPenaltyFor(true); + mod += sp; + lines.push(`${shieldUp.name} ${sp}%`); + } const cv = RINGBRP.cover[cover]?.mod ?? 0; if (cv) { mod += cv; lines.push(`${game.i18n.localize(RINGBRP.cover[cover].label)} ${cv}%`); } if (moved && styleCfg.moveAndFire) { diff --git a/rules.mjs b/rules.mjs index 4f08b4f..35efc71 100644 --- a/rules.mjs +++ b/rules.mjs @@ -782,6 +782,63 @@ export function locationArmourFor(base, pieces, kind) { return n; } +/** + * SHIELDS. + * + * A shield is the only armour that is not simply worn: it is armour you HOLD, and it + * protects you exactly as much as you are currently holding it up. So it is armour + * with a switch, and everything interesting about it follows from that switch having + * a cost. + * + * Raised, it adds its points to what it covers and it is the one piece of armour you + * can also defend with. Lowered — slung, or because both your hands are full — it is + * weight and nothing else. + * + * The cost is that it occupies a hand and you are shooting around your own shield. + * That is what stops "always raised" from being the only answer: it is obviously + * right for a containment officer and obviously wrong for a marksman. + */ +export const SHIELD = { + ranged: -20, // your own shooting, from behind your own shield + coverage: "torsoArms", // what a raised shield presents, unless it says otherwise + parryFamily: "melee_weapon", + parrySpec: "shield" +}; + +/** A shield's points count only while it is up and only while it is still a shield. */ +export function shieldArmourFor(points, { raised = false, broken = false } = {}) { + return (raised && !broken) ? (Number(points) || 0) : 0; +} + +/** Shooting past your own shield. Nothing if it is down. */ +export function shieldRangedPenaltyFor(raised) { + return raised ? SHIELD.ranged : 0; +} + +/** + * Shields break. `degradation` has been on every armour item since the packs were + * first built and was read by nothing; this is what it was for. + * + * Wear is taken for stopping something that was genuinely trying — an ordinary blow + * turned aside costs the shield nothing, because otherwise every fight is arithmetic + * about splinters. + */ +// Derived from the ladder every other band decision uses rather than written out as +// a second table keyed by the same five names — which is both a duplicate vocabulary +// and, as the guard pointed out, indistinguishable from one. +export const SHIELD_WEAR_FROM = "success"; + +export function shieldWearFor(attackLevel) { + return Math.max(0, levelRank(attackLevel) - levelRank(SHIELD_WEAR_FROM)); +} + +/** At its limit it is firewood: no points, and nothing to parry with. */ +export function shieldBroken(current, max) { + const c = Number(current) || 0, m = Number(max) || 0; + if (m <= 0) return false; + return c >= m; +} + /** A lamp never makes it daylight. This is the best it gets you. */ export const LIGHT_FLOOR = "dim"; diff --git a/styles/ringbrp.css b/styles/ringbrp.css index ab019d2..4b88fdd 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -423,6 +423,26 @@ .ringbrp-sheet .rb-equip:hover { color: var(--rb-white); } .ringbrp-sheet .rb-equip.on { color: var(--rb-yellow); } +/* ============================================================ + Shields — armour with a switch, so the switch has to be visible + ============================================================ */ + +.ringbrp-sheet .rb-raise { color: var(--rb-muted); margin-left: 2px; } +.ringbrp-sheet .rb-raise:hover { color: var(--rb-white); } +.ringbrp-sheet .rb-raise.on { color: var(--rb-yellow); } +.rb-shield-up { background: rgba(255, 214, 0, .07); } +.rb-shield-up .rb-strong { color: var(--rb-yellow); } +.rb-shield-state { + font-family: var(--rb-display); font-size: 9px; letter-spacing: .08em; + text-transform: uppercase; color: var(--rb-muted); + border: 1px solid var(--rb-hairline); padding: 0 5px; +} +.rb-shield-up .rb-shield-state { color: var(--rb-void); background: var(--rb-yellow); border-color: var(--rb-yellow); } +.rb-shield-wear { font-family: var(--rb-mono); font-size: 10px; } +.rb-shield-broken { opacity: .55; } +.rb-shield-broken .rb-shield-state { color: var(--rb-danger); border-color: var(--rb-danger); } +.rb-cbtn-shield { border-color: var(--rb-yellow-dim); color: var(--rb-yellow); } + /* ============================================================ Coherence, promoted to the vitals — it is a survival number ============================================================ */ diff --git a/template.json b/template.json index 59fac7b..bc9a91f 100644 --- a/template.json +++ b/template.json @@ -235,6 +235,8 @@ ], "points": 1, "coverage": "all", + "shield": false, + "raised": false, "ignoredBy": [], "degradation": { "current": 0, diff --git a/templates/character-sheet.hbs b/templates/character-sheet.hbs index cb050a5..3fb2a61 100644 --- a/templates/character-sheet.hbs +++ b/templates/character-sheet.hbs @@ -234,10 +234,19 @@
    {{#each armours as |a|}} -
  1. +
  2. + {{#if a.system.shield}} + + {{/if}} {{a.name}} {{a.system.points}} {{localize "RINGBRP.Sheet.Points"}} + {{#if a.system.shield}} + {{#if a.brokenNow}}{{localize "RINGBRP.Sheet.ShieldBroken"}}{{else if a.raisedNow}}{{localize "RINGBRP.Sheet.ShieldUp"}}{{else}}{{localize "RINGBRP.Sheet.ShieldDown"}}{{/if}} + {{a.system.degradation.current}}/{{a.system.degradation.max}} + {{/if}} + {{localize (rbConcat "RINGBRP.Coverage." a.system.coverage)}} {{a.system.massKg}} kg @@ -251,6 +260,10 @@ data-tooltip="{{localize 'RINGBRP.Sheet.DefendHint'}}">{{localize "RINGBRP.Defence.dodge"}} {{localize "RINGBRP.Defence.parry"}} + {{#if sys.shieldUp}} + {{sys.shieldUp.name}} {{sys.shieldUp.points}} + {{/if}} {{#if sys.combat.defencesThisRound}} {{sys.combat.defencesThisRound}} {{localize "RINGBRP.Sheet.ThisRound"}} · {{localize "RINGBRP.Sheet.NextAt"}} {{defenceNext}}% diff --git a/templates/item-sheet.hbs b/templates/item-sheet.hbs index 87b027e..7e4a9f5 100644 --- a/templates/item-sheet.hbs +++ b/templates/item-sheet.hbs @@ -279,6 +279,12 @@ {{#each RINGBRP.coverage as |c key|}}{{/each}} + + {{#if sys.shield}} + + + + {{/if}}

    {{localize "RINGBRP.Item.ArmourNote"}}

    {{/if}} diff --git a/tools/build-packs.mjs b/tools/build-packs.mjs index 0c4869f..88872b2 100644 --- a/tools/build-packs.mjs +++ b/tools/build-packs.mjs @@ -16,6 +16,7 @@ import { SKILL_CATALOGUE, WEAPONS, ARMOURS, GEAR, VEHICLES, PREGENS, NPCS, DEVICES, TALENTS, SITE_TABLE, HAZARD_TABLE, hitPoints, majorWound, damageModifier, styleFor } from "./content.mjs"; +import { SHIELD } from "../rules.mjs"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const PACKS = path.join(ROOT, "packs"); @@ -100,8 +101,9 @@ function armourItem(a) { coherenceCost: a.coherence ?? 0, description: "", massKg: a.mass, quantity: 1, equipped: false, carried: true, traditionIds: [a.trad ?? "tradition_local"], - points: a.points, coverage: a.covers ?? "all", - ignoredBy: [], degradation: { current: 0, max: 4 } + points: a.points, coverage: a.covers ?? (a.shield ? SHIELD.coverage : "all"), + shield: !!a.shield, raised: false, + ignoredBy: [], degradation: { current: 0, max: a.tough ?? 4 } }, effects: [], ...BASE(), flags: { ringbrp: { key: a.key, era: a.era ?? "modern" } } diff --git a/tools/check-rules.mjs b/tools/check-rules.mjs index 5f95965..ea3e87f 100644 --- a/tools/check-rules.mjs +++ b/tools/check-rules.mjs @@ -284,6 +284,22 @@ const checks = [ ["sightFor(nonsense) is none", RULES.sightFor("wat").id, "none"], ["sightSeesInDark(thermal)", RULES.sightSeesInDark("thermal"), true], ["sightSeesInDark(telescopic)", RULES.sightSeesInDark("telescopic"), false], + // A shield is armour with a switch, and the switch is the whole rule. + ["shieldArmourFor(raised)", RULES.shieldArmourFor(3,{raised:true}), 3], + ["shieldArmourFor(lowered)", RULES.shieldArmourFor(3,{raised:false}), 0], + ["shieldArmourFor(broken)", RULES.shieldArmourFor(3,{raised:true,broken:true}),0], + ["shieldArmourFor(default down)", RULES.shieldArmourFor(3), 0], + ["shieldRangedPenaltyFor(raised)", RULES.shieldRangedPenaltyFor(true), -20], + ["shieldRangedPenaltyFor(lowered)", RULES.shieldRangedPenaltyFor(false), 0], + // An ordinary blow turned aside costs the shield nothing; a critical costs it two. + ["shieldWearFor(critical)", RULES.shieldWearFor("critical"), 2], + ["shieldWearFor(special)", RULES.shieldWearFor("special"), 1], + ["shieldWearFor(success)", RULES.shieldWearFor("success"), 0], + ["shieldWearFor(nonsense)", RULES.shieldWearFor("wat"), 0], + ["shieldWearFor(fumble)", RULES.shieldWearFor("fumble"), 0], + ["shieldBroken(4,4)", RULES.shieldBroken(4,4), true], + ["shieldBroken(3,4)", RULES.shieldBroken(3,4), false], + ["shieldBroken(0,0) never breaks", RULES.shieldBroken(0,0), false], // A helmet does not protect your legs. Coverage, and what it adds up to. ["armourCoversKind(head, head)", RULES.armourCoversKind("head","head"), true], ["armourCoversKind(head, leg)", RULES.armourCoversKind("head","leg"), false], diff --git a/tools/content.mjs b/tools/content.mjs index 2f10158..dbe4b12 100644 --- a/tools/content.mjs +++ b/tools/content.mjs @@ -31,6 +31,7 @@ export const SKILL_CATALOGUE = [ // else fits an existing specialisation: a mace is a club, a dirk is a knife. ["melee_weapon", "polearm", "melee", 5, "Melee Weapon (Polearm)"], ["melee_weapon", "flail", "melee", 5, "Melee Weapon (Flail)"], + ["melee_weapon", "shield", "melee", 5, "Shield"], ["variable_sword", "", "melee", 5, "Variable Sword"], // ranged ["firearm", "pistol", "ranged", 5, "Firearm (Pistol)"], @@ -181,6 +182,18 @@ export const WEAPONS = [ /* ---------- armour (v0.5 §4.8) ---------- */ export const ARMOURS = [ + /* --- shields: armour you hold up, and only armour while you are holding it --- * + * A shield is the one piece of armour you can also defend with, and the one that + * stops protecting you the moment your hands are full. It occupies a hand and you + * are shooting around it, which is what stops "always raised" being the answer. */ + { key: "buckler", name: "Buckler", points: 2, shield: true, tough: 3, mass: 1.2, era: "antique", trad: "tradition_local", desc: "A fist-sized iron plate. It does not cover you; it goes where the blow is, and that is a skill." }, + { key: "targe", name: "Targe", points: 3, shield: true, tough: 4, mass: 2.6, era: "antique", trad: "tradition_local", desc: "Wood, hide and a boss. Light enough to keep up all day, which is the whole argument for it." }, + { key: "heater", name: "Heater shield", points: 4, shield: true, tough: 5, mass: 3.8, era: "antique", trad: "tradition_local", desc: "Shaped to cover a man on foot from shoulder to thigh. Everything after this is heavier." }, + { key: "pavise", name: "Pavise", points: 5, shield: true, tough: 6, covers: "torsoLimbs", mass: 7.5, era: "antique", trad: "tradition_local", desc: "A wall you carry. Covers all of you and you will not be doing anything else." }, + { key: "riot_shield", name: "Riot shield", points: 3, shield: true, tough: 5, mass: 4.2, trad: "tradition_agency", desc: "Polycarbonate, agency issue, with a window you can see the thing through. Stores has forty of them and no explanation." }, + { key: "ballistic_shield", name: "Ballistic shield", points: 6, shield: true, tough: 6, mass: 9.0, trad: "tradition_agency", desc: "Rated for a rifle round and weighed accordingly. Two hands on the grip means no hands on anything else." }, + { key: "barrier_field", name: "Barrier plate", points: 5, shield: true, tough: 8, covers: "torsoLimbs", mass: 2.0, era: "future", trad: "tradition_transposed", desc: "It is not there until it is raised, and then it is very much there. Nobody in Stores will say what happens when it fails." }, + { key: "jacket", name: "Reinforced jacket", points: 1, covers: "torsoArms", mass: 2, trad: "tradition_agency" }, { key: "stab_vest", name: "Stab vest", points: 2, covers: "torso", mass: 3, trad: "tradition_agency" }, { key: "soft_armour", name: "Concealed soft armour", points: 3, covers: "torso", mass: 3.5, trad: "tradition_agency" }, diff --git a/tools/icons.mjs b/tools/icons.mjs index 9280348..f5d6452 100644 --- a/tools/icons.mjs +++ b/tools/icons.mjs @@ -110,6 +110,11 @@ export const ICON_MAP = { lodestone:["compass","gear",-62,"antique"], sand_glass:["pocketwatch","gear",62,"antique"], /* ---- future stores ---- */ + // shields — a disc for the round ones, a bar for the big square ones + buckler:["disc","armour",22,"antique"], targe:["disc","armour",-14,"antique"], + heater:["shield","armour",6,"antique"], pavise:["shield","armour",-26,"antique"], + riot_shield:["shield","armour",30], ballistic_shield:["shield","armour",-8], + barrier_field:["shield","armour",44,"future"], wound_foam:["kit","gear",-16,"future"], cold_light:["bolt","gear",-20,"future"], // Fitted to the weapon. Lenses for the three optical sights, so they read as a set; // the thermal one is a future-era lens and the lamp is a bolt like the other lights. diff --git a/tools/make-icons.mjs b/tools/make-icons.mjs index 1d67500..6833aec 100644 --- a/tools/make-icons.mjs +++ b/tools/make-icons.mjs @@ -96,6 +96,11 @@ const G = { /* ---------------- armour ---------------- */ vest: { base: `M40 20h48l10 18v56a10 10 0 0 1-10 10H40a10 10 0 0 1-10-10V38z`, detail: `M64 20v84 M38 46h20M74 46h20` , stroke: ["detail"] }, + // A shield read from the front: a heater outline with a boss and a grip line. + shield: { base: `M64 14l38 12v34c0 30-16 48-38 58-22-10-38-28-38-58V26z`, + detail: `M64 14v104 M30 52h68`, + accent: `M64 56m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0`, + stroke: ["detail"] }, plate: { base: `M64 12l40 14v40c0 28-18 44-40 50-22-6-40-22-40-50V26z`, detail: `M64 26l26 10v30c0 18-12 28-26 32-14-4-26-14-26-32V36z` }, jacket: { base: `M42 22h44l12 16v62H30V38z`, detail: `M64 22v78 M30 54h68`, stroke: ["detail"] }, diff --git a/tools/rules-text.mjs b/tools/rules-text.mjs index a2c0d74..0df43eb 100644 --- a/tools/rules-text.mjs +++ b/tools/rules-text.mjs @@ -23,6 +23,7 @@ import { FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, BLAST, BLAST_COVER, BURST, burstHitsFor, TWO_WEAPON, twoWeaponPenaltyFor, FIRE_MODE, SECOND_SHOT, SUSTAIN, SIGHT, LIGHTING, LIGHT_FLOOR, LAMP, + SHIELD, shieldWearFor, COVERAGE, STATIONS, SPACE_BANDS, SPACE_RANGE_MOD, HEAT, heatEffectsFor, spaceHitFor, powerStartingRating, brawlFallbackFor, bleedStoppedBy, crossingArtPoolFor, crossingGrantsArt, crossingCostFor, @@ -368,6 +369,22 @@ whole point of it, and it is why nobody sensibly carries all four. A sight only if it fits — the scope and the thermal sight are rifle furniture and do nothing bolted to a service pistol.

    +

    Shields

    +

    A shield is the one piece of armour you hold, and it protects you exactly +as much as you are currently holding it up. Raised, it adds its points +to what it covers and it is the only armour you can also defend with — a parry on the +Shield skill. Lowered, it is weight.

    +

    What stops "always raised" being the only answer is that it occupies a hand and you +are shooting around your own shield: ${pc(SHIELD.ranged)} to your own ranged attacks +while it is up. It is obviously right for a containment officer and obviously wrong for +a marksman, which is the point.

    +

    Shields break. A blow the shield absorbed — stopped +outright, or merely dragged down a step — costs it a point of wear for every step the +attack stood above an ordinary success — so an ordinary blow costs +it nothing, a special costs ${shieldWearFor("special")}, and a critical costs +${shieldWearFor("critical")}. At its limit it is firewood: no points, nothing to parry +with, and it drops out of your hand.

    +

    The dark

    Most crossings do not come with the lights on.

    ${table(["", "To hit"], Object.values(LIGHTING).map(l => [L(l.label), pc(l.mod)]))}