diff --git a/README.md b/README.md index dc86103..4244471 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ changing underneath a running server. | | | |---|---| | **Roles** | 25 postings, each with a key characteristic, trained skills, talents and its own kit | -| **Catalogue** | 175 items — 118 gear, 30 weapons, 13 armour, 14 vehicles | +| **Catalogue** | 229 items — 123 gear, 62 weapons, 25 armour, 19 vehicles | | **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything | -| **Icons** | 231, all generated from `tools/icons.mjs`; no external art | +| **Icons** | 285, all generated from `tools/icons.mjs`; no external art | | **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, a starter case | -| **Rules journal** | 8 pages, generated from `rules.mjs` so the book cannot drift from the code | +| **Rules journal** | 12 pages, generated from `rules.mjs` so the book cannot drift from the code | --- @@ -92,6 +92,16 @@ Only a critical stops a critical. **Access is a record.** There is no hacking skill and nothing to break into — see below. +**A weapon's fire mode is a decision.** Slow weapons shoot better and then make you +work; fast ones offer a second shot at the cost of both; rapid ones fire bursts; beams +can be held on the target for damage instead of hits. A weapon that cannot fire a +burst cannot be made to. + +**Sights and lamps apply themselves.** A sight is graded by range band — a telescopic +sight is a penalty inside ten metres — and a carried lamp buys the dark back to dim, +never to daylight, and tells everyone where you are. Neither is a thing the player +selects: both are facts about what is in the pack. + --- ## Borrowed Authority — the access subsystem @@ -129,7 +139,7 @@ and nothing in this game spends anything to converge them back. ## Repository layout ``` -ringbrp.mjs the system: sheets, engine, hooks (~4,900 lines) +ringbrp.mjs the system: sheets, engine, hooks (~5,500 lines) rules.mjs THE SINGLE AUTHORITY for every rule formula system.json Foundry manifest template.json data model for every Actor and Item type @@ -163,7 +173,7 @@ one set of numbers and played under another; this makes that impossible to ship. The build runs all three guards before it writes anything: ``` -check-rules: 40 rules, 12 files scanned, 132 formulas verified -check-kits: 25 roles, 162 catalogue items, every kit key resolves -check-lang: 738 keys, no leaf/branch collisions +check-rules: 76 rules, 12 files scanned, 230 formulas verified +check-kits: 25 roles, 211 catalogue items, every kit key resolves +check-lang: 878 keys, no leaf/branch collisions ``` diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 070e3d1..9792145 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -4211,3 +4211,82 @@ is the retuned extraction rule working end to end. all four agents. Only the Field Lead ever had it — the other three had never been awarded it, and the probe could not tell the difference between a talent removed and a talent never held. The amendments actually made were three, not four. + +## Fire modes, fitted kit, and a rail instead of a strip + +Four requests, and the first of them turned over the usual stone. + +**R-231. Every weapon has carried a fire mode since the packs were first built, and +nothing has ever read it.** A musket could rip a ten-round burst; "rapid" bought a +weapon precisely nothing. `FIRE_MODE` now gates the burst rules and carries its own +attack modifier, so the mode is a decision rather than a label: **slow** +10% and then +you work for the next one, **single** the baseline, **fast** a second shot at −20% to +*both*, **rapid** bursts, **beam** +5% and it can be held. The gate is checked inside +`burstAttack` rather than only in the dialog, so no macro or harness can route round it. + +**R-232. `fast` was not a fire mode at all.** Five weapons carried it — longbow, sling, +javelin, the Webley and the Lee-Enfield — and the system's list of modes did not +include it, so the item sheet's dropdown had no matching option and re-saving one of +those weapons silently blanked its mode. The list was hand-written next to the rule +instead of derived from it. It is derived now, and the guard that should have caught +this was the one I had to fix in R-236. + +**R-233. `burstAttack` was reachable from nothing.** The automatic-fire rules shipped +complete and could not be used from any sheet. It is now the resolution path the fire +selector chooses, and it takes the dialog's whole modifier stack rather than rebuilding +its own — the first version charged the player twice for the same burst. + +**R-234. Any weapon with a Coherence cost could not be attacked with at all.** The +handler read the weapon, awaited the Coherence charge, and then read the weapon +*again* — but `currentTarget` is only set while an event is being dispatched, so the +second read returned null and the attack died on a TypeError with nothing shown to the +player. The Redaction lance, the Unmaker and every piece of transposed materiel were +unusable, and the failure was completely silent. Resolve once, before anything is +awaited. + +**R-235. The damage card was the last one wearing Foundry's stock dice styling.** It +posted a bare Roll and let the core template dress it — grey boxes on a white panel in +the middle of a navy sheet. It renders through the module's own card now, with the Roll +still riding on the message so the dice stay inspectable and Dice So Nice still +animates. + +**R-236. The duplicate-rule guard reported an honest table as a copy.** `FIRE_MODE` has +`slow` and `fast`; so does the entirely unrelated `speedBands` vocabulary, and two +shared key names was enough to fail the build. Every real instance this guard has ever +caught duplicated the *whole* table, so it now wants a majority of the keys — and +prints partial overlaps rather than swallowing them. + +**Sights and lamps.** A sight is not a flat bonus, which is why most systems end up +with everybody carrying the same scope. It is graded by range band, so **a telescopic +sight is a −10% liability inside ten metres** and worth +20% at extreme, and it only +counts if it *fits* — the scope and the thermal sight are rifle furniture. Because the +band is already known by the time the modifier is wanted, the sight applies itself: +there is nothing to select and nothing to forget. The dark now costs something, a +carried lamp buys it back to dim and never to daylight, and the card says plainly that +the lamp shows them exactly where you are. + +**R-237. "Automatic" that needs a re-import is not automatic.** Agents created before +those fields existed carried a Field lamp whose `lightSource` was the template default +of false — the lamp sat in the pack doing nothing. A migration stamps fitted gear from +the compendium on load, matched on `flags.ringbrp.key` rather than on display name, +because the fuzzy-name match this project used before once mapped a climbing kit to +breaching charges. + +**My own error, caught by playing it.** I first costed a sustained beam as +`multiplier × energyPerShot`, which made a locked beam cost 60 charge from a 20-charge +cell — unfirable, and the failure was a warning the player would read as a bug. The +magazine is spent one per shot everywhere else in the system, so the cost is in shots: +a locked beam burns six. Worth noting that `energyPerShot` itself is written by the +pack builder and **read by nothing** — the same defect again, left alone here because +wiring it would change every energy weapon's economy. + +**The tabs.** Eight of them across the top of an 820px sheet compressed to 11px type +and were genuinely hard to read. They are a rail down the side now: one line each, +larger type, a solid block for the active tab, and counts as badges. The sheet is 152px +wider to pay for the rail so nothing else got tighter, and the skills list is driven by +the space actually available rather than by a window-width breakpoint that was +measuring the wrong box. + +Also noted, not fixed: fourteen rules are imported into `ringbrp.mjs` and read nowhere +in it. They are live rules with real readers in the rulebook generator and the guards, +so this is dead *imports* rather than dead rules — but it is the same smell. diff --git a/icons/sight_irons.svg b/icons/sight_irons.svg new file mode 100644 index 0000000..448c432 --- /dev/null +++ b/icons/sight_irons.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/icons/sight_reflex.svg b/icons/sight_reflex.svg new file mode 100644 index 0000000..3bc49e5 --- /dev/null +++ b/icons/sight_reflex.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/icons/sight_scope.svg b/icons/sight_scope.svg new file mode 100644 index 0000000..419181e --- /dev/null +++ b/icons/sight_scope.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/icons/sight_thermal.svg b/icons/sight_thermal.svg new file mode 100644 index 0000000..8727f1c --- /dev/null +++ b/icons/sight_thermal.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/icons/weapon_lamp.svg b/icons/weapon_lamp.svg new file mode 100644 index 0000000..8e73833 --- /dev/null +++ b/icons/weapon_lamp.svg @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 9845edd..f5aeb7b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -836,5 +836,45 @@ "RINGBRP.Salvage.Title": "Salvage", "RINGBRP.Salvage.Found": "Left lying about — {era}", "RINGBRP.Salvage.None": "Nothing of that era in the catalogue ({era}).", - "RINGBRP.Gen.Cancel": "Cancel" + "RINGBRP.Gen.Cancel": "Cancel", + "RINGBRP.Fire.Shot": "Single shot", + "RINGBRP.Fire.Rounds": "{n} rounds, {hits} connect", + "RINGBRP.Fire.Adds": "adds {dice} damage", + "RINGBRP.Fire.Second": "Two shots", + "RINGBRP.Fire.SecondEffect": "both hurried", + "RINGBRP.Fire.Reloading": "{name} — reloading, {n} round(s)", + "RINGBRP.Fire.slow.Label": "Slow", + "RINGBRP.Fire.single.Label": "Single", + "RINGBRP.Fire.fast.Label": "Fast", + "RINGBRP.Fire.rapid.Label": "Rapid", + "RINGBRP.Fire.beam.Label": "Beam", + "RINGBRP.Fire.slow.Note": "One shot, then work. Nothing about this weapon rewards haste, and it is fired by somebody who knows that.", + "RINGBRP.Fire.single.Note": "One aimed shot a round. The ordinary case, and the baseline everything else is measured against.", + "RINGBRP.Fire.fast.Note": "The action will give you a second shot if you will accept both of them being worse.", + "RINGBRP.Fire.rapid.Note": "Bursts. One roll, and how well it went decides how much of the burst arrived.", + "RINGBRP.Fire.beam.Note": "No recoil, no lead, no drop — and it can be held on the target for damage instead of hits.", + "RINGBRP.Sustain.tap": "Tap", + "RINGBRP.Sustain.hold": "Held beam", + "RINGBRP.Sustain.lock": "Locked beam", + "RINGBRP.Sight.none": "No sight", + "RINGBRP.Sight.irons": "Aperture sights", + "RINGBRP.Sight.reflex": "Reflex sight", + "RINGBRP.Sight.telescopic": "Telescopic sight", + "RINGBRP.Sight.thermal": "Thermal sight", + "RINGBRP.Sight.LampEffect": "lit, and visible", + "RINGBRP.Sight.Revealed": "{name} shows them exactly where you are", + "RINGBRP.Lighting.lit": "Lit", + "RINGBRP.Lighting.dim": "Dim", + "RINGBRP.Lighting.dark": "Dark", + "RINGBRP.Lighting.pitch": "Pitch dark", + "RINGBRP.Dialog.Fire": "Fire", + "RINGBRP.Dialog.Lighting": "Light", + "RINGBRP.Dialog.Fitted": "Applies itself", + "RINGBRP.Notify.NoBurst": "{name} cannot fire a burst.", + "RINGBRP.Notify.NotEnoughCharge": "Not enough charge to hold the beam that long.", + "RINGBRP.Notify.SecondUsed": "That second shot has already been taken this round.", + "RINGBRP.Chat.Damage": "damage", + "RINGBRP.Chat.Sustained": "sustained", + "RINGBRP.Chat.DamageMod": "damage mod", + "RINGBRP.Notify.GearFitted": "Fitted {n} item(s) that were carried before sights and lamps did anything." } diff --git a/packs/armour/000003.log b/packs/armour/000003.log new file mode 100644 index 0000000..5b23416 Binary files /dev/null and b/packs/armour/000003.log differ diff --git a/packs/armour/000005.ldb b/packs/armour/000005.ldb deleted file mode 100644 index b406921..0000000 Binary files a/packs/armour/000005.ldb and /dev/null differ diff --git a/packs/armour/000006.log b/packs/armour/000006.log deleted file mode 100644 index 9d3f468..0000000 Binary files a/packs/armour/000006.log and /dev/null differ diff --git a/packs/armour/CURRENT b/packs/armour/CURRENT index cacca75..1a84852 100644 --- a/packs/armour/CURRENT +++ b/packs/armour/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/armour/LOG b/packs/armour/LOG index 45ee788..481972f 100644 --- a/packs/armour/LOG +++ b/packs/armour/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.417544 17416f000 Recovering log #3 -2026/08/11-08:06:46.417572 17416f000 Level-0 table #5: started -2026/08/11-08:06:46.417699 17416f000 Level-0 table #5: 3269 bytes OK -2026/08/11-08:06:46.417964 17416f000 Delete type=0 #3 -2026/08/11-08:06:46.417996 17416f000 Delete type=3 #2 +2026/08/11-08:56:03.947107 16e763000 Delete type=3 #1 diff --git a/packs/armour/LOG.old b/packs/armour/LOG.old deleted file mode 100644 index f2eefe9..0000000 --- a/packs/armour/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.255757 171d3f000 Delete type=3 #1 diff --git a/packs/armour/MANIFEST-000002 b/packs/armour/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/armour/MANIFEST-000002 differ diff --git a/packs/armour/MANIFEST-000004 b/packs/armour/MANIFEST-000004 deleted file mode 100644 index 1faa5e9..0000000 Binary files a/packs/armour/MANIFEST-000004 and /dev/null differ diff --git a/packs/expedition/000003.log b/packs/expedition/000003.log new file mode 100644 index 0000000..263295b Binary files /dev/null and b/packs/expedition/000003.log differ diff --git a/packs/expedition/000005.ldb b/packs/expedition/000005.ldb deleted file mode 100644 index 4ff460d..0000000 Binary files a/packs/expedition/000005.ldb and /dev/null differ diff --git a/packs/expedition/000006.log b/packs/expedition/000006.log deleted file mode 100644 index 268b187..0000000 Binary files a/packs/expedition/000006.log and /dev/null differ diff --git a/packs/expedition/CURRENT b/packs/expedition/CURRENT index cacca75..1a84852 100644 --- a/packs/expedition/CURRENT +++ b/packs/expedition/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/expedition/LOG b/packs/expedition/LOG index 43baa34..a59eb01 100644 --- a/packs/expedition/LOG +++ b/packs/expedition/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.424344 175993000 Recovering log #3 -2026/08/11-08:06:46.424368 175993000 Level-0 table #5: started -2026/08/11-08:06:46.424513 175993000 Level-0 table #5: 4129 bytes OK -2026/08/11-08:06:46.424759 175993000 Delete type=0 #3 -2026/08/11-08:06:46.424801 175993000 Delete type=3 #2 +2026/08/11-08:56:03.980151 16f77b000 Delete type=3 #1 diff --git a/packs/expedition/LOG.old b/packs/expedition/LOG.old deleted file mode 100644 index f9488bd..0000000 --- a/packs/expedition/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.282139 171d3f000 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..349523a Binary files /dev/null and b/packs/gear/000003.log differ diff --git a/packs/gear/000005.ldb b/packs/gear/000005.ldb deleted file mode 100644 index fedde60..0000000 Binary files a/packs/gear/000005.ldb and /dev/null differ diff --git a/packs/gear/000006.log b/packs/gear/000006.log deleted file mode 100644 index 4b150b6..0000000 Binary files a/packs/gear/000006.log and /dev/null differ diff --git a/packs/gear/CURRENT b/packs/gear/CURRENT index cacca75..1a84852 100644 --- a/packs/gear/CURRENT +++ b/packs/gear/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/gear/LOG b/packs/gear/LOG index b0ae8ed..7a5bd92 100644 --- a/packs/gear/LOG +++ b/packs/gear/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.418514 17497b000 Recovering log #3 -2026/08/11-08:06:46.418608 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.418879 17497b000 Level-0 table #5: 27954 bytes OK -2026/08/11-08:06:46.419164 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.419211 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.949621 16f77b000 Delete type=3 #1 diff --git a/packs/gear/LOG.old b/packs/gear/LOG.old deleted file mode 100644 index dd3db13..0000000 --- a/packs/gear/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.257347 170d27000 Delete type=3 #1 diff --git a/packs/gear/MANIFEST-000002 b/packs/gear/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/gear/MANIFEST-000002 differ diff --git a/packs/gear/MANIFEST-000004 b/packs/gear/MANIFEST-000004 deleted file mode 100644 index 97d9316..0000000 Binary files a/packs/gear/MANIFEST-000004 and /dev/null differ diff --git a/packs/npcs/000003.log b/packs/npcs/000003.log new file mode 100644 index 0000000..c13442d Binary files /dev/null and b/packs/npcs/000003.log differ diff --git a/packs/npcs/000005.ldb b/packs/npcs/000005.ldb deleted file mode 100644 index 434f292..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 a8f297d..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 db03d48..28f4fea 100644 --- a/packs/npcs/LOG +++ b/packs/npcs/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.407760 17497b000 Recovering log #3 -2026/08/11-08:06:46.408666 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.410003 17497b000 Level-0 table #5: 223489 bytes OK -2026/08/11-08:06:46.410364 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.410398 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.966944 16ef6f000 Delete type=3 #1 diff --git a/packs/npcs/LOG.old b/packs/npcs/LOG.old deleted file mode 100644 index 0863b55..0000000 --- a/packs/npcs/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.270516 17051b000 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 c64c94d..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 2844da0..208cfc8 100644 --- a/packs/powers/LOG +++ b/packs/powers/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.415299 17416f000 Recovering log #3 -2026/08/11-08:06:46.415324 17416f000 Level-0 table #5: started -2026/08/11-08:06:46.415442 17416f000 Level-0 table #5: 3295 bytes OK -2026/08/11-08:06:46.415703 17416f000 Delete type=0 #3 -2026/08/11-08:06:46.415749 17416f000 Delete type=3 #2 +2026/08/11-08:56:03.974409 16e763000 Delete type=3 #1 diff --git a/packs/powers/LOG.old b/packs/powers/LOG.old deleted file mode 100644 index 79d7e77..0000000 --- a/packs/powers/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.277189 170d27000 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..5ed9a47 Binary files /dev/null and b/packs/pregens/000003.log differ diff --git a/packs/pregens/000005.ldb b/packs/pregens/000005.ldb deleted file mode 100644 index d646217..0000000 Binary files a/packs/pregens/000005.ldb and /dev/null differ diff --git a/packs/pregens/000006.log b/packs/pregens/000006.log deleted file mode 100644 index 71e3acd..0000000 Binary files a/packs/pregens/000006.log and /dev/null differ diff --git a/packs/pregens/CURRENT b/packs/pregens/CURRENT index cacca75..1a84852 100644 --- a/packs/pregens/CURRENT +++ b/packs/pregens/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/pregens/LOG b/packs/pregens/LOG index dde3e2b..0888ece 100644 --- a/packs/pregens/LOG +++ b/packs/pregens/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.405358 17497b000 Recovering log #3 -2026/08/11-08:06:46.405609 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.406137 17497b000 Level-0 table #5: 57435 bytes OK -2026/08/11-08:06:46.406463 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.406496 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.956900 16ff87000 Delete type=3 #1 diff --git a/packs/pregens/LOG.old b/packs/pregens/LOG.old deleted file mode 100644 index d41e962..0000000 --- a/packs/pregens/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.262897 171d3f000 Delete type=3 #1 diff --git a/packs/pregens/MANIFEST-000002 b/packs/pregens/MANIFEST-000002 new file mode 100644 index 0000000..bbbc585 Binary files /dev/null and b/packs/pregens/MANIFEST-000002 differ diff --git a/packs/pregens/MANIFEST-000004 b/packs/pregens/MANIFEST-000004 deleted file mode 100644 index a27f077..0000000 Binary files a/packs/pregens/MANIFEST-000004 and /dev/null differ diff --git a/packs/rules/000003.log b/packs/rules/000003.log new file mode 100644 index 0000000..347e0ec 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 e811734..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 72eda75..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 bbc40ff..d9c2440 100644 --- a/packs/rules/LOG +++ b/packs/rules/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.421106 17497b000 Recovering log #3 -2026/08/11-08:06:46.421169 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.421431 17497b000 Level-0 table #5: 28916 bytes OK -2026/08/11-08:06:46.421607 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.421634 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.978547 16ff87000 Delete type=3 #1 diff --git a/packs/rules/LOG.old b/packs/rules/LOG.old deleted file mode 100644 index d977710..0000000 --- a/packs/rules/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.280795 171533000 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 2c6a051..0000000 Binary files a/packs/rules/MANIFEST-000004 and /dev/null differ diff --git a/packs/skills/000003.log b/packs/skills/000003.log new file mode 100644 index 0000000..e23ff19 Binary files /dev/null and b/packs/skills/000003.log differ diff --git a/packs/skills/000005.ldb b/packs/skills/000005.ldb deleted file mode 100644 index d613d26..0000000 Binary files a/packs/skills/000005.ldb and /dev/null differ diff --git a/packs/skills/000006.log b/packs/skills/000006.log deleted file mode 100644 index aa7dd7d..0000000 Binary files a/packs/skills/000006.log and /dev/null differ diff --git a/packs/skills/CURRENT b/packs/skills/CURRENT index cacca75..1a84852 100644 --- a/packs/skills/CURRENT +++ b/packs/skills/CURRENT @@ -1 +1 @@ -MANIFEST-000004 +MANIFEST-000002 diff --git a/packs/skills/LOG b/packs/skills/LOG index 409b3dc..6e545bf 100644 --- a/packs/skills/LOG +++ b/packs/skills/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.413121 17416f000 Recovering log #3 -2026/08/11-08:06:46.413179 17416f000 Level-0 table #5: started -2026/08/11-08:06:46.413357 17416f000 Level-0 table #5: 7486 bytes OK -2026/08/11-08:06:46.413587 17416f000 Delete type=0 #3 -2026/08/11-08:06:46.413622 17416f000 Delete type=3 #2 +2026/08/11-08:56:03.938794 16e763000 Delete type=3 #1 diff --git a/packs/skills/LOG.old b/packs/skills/LOG.old deleted file mode 100644 index 713f3ca..0000000 --- a/packs/skills/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.250466 17051b000 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 159e9eb..f3f0f86 100644 --- a/packs/slice/LOG +++ b/packs/slice/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.422268 17416f000 Recovering log #3 -2026/08/11-08:06:46.422303 17416f000 Level-0 table #5: started -2026/08/11-08:06:46.422611 17416f000 Level-0 table #5: 16892 bytes OK -2026/08/11-08:06:46.422814 17416f000 Delete type=0 #3 -2026/08/11-08:06:46.422839 17416f000 Delete type=3 #2 +2026/08/11-08:56:03.972924 16ef6f000 Delete type=3 #1 diff --git a/packs/slice/LOG.old b/packs/slice/LOG.old deleted file mode 100644 index 13d38fd..0000000 --- a/packs/slice/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.275784 170d27000 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 78d3500..31ac16d 100644 --- a/packs/tables/LOG +++ b/packs/tables/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.423379 175187000 Recovering log #3 -2026/08/11-08:06:46.423401 175187000 Level-0 table #5: started -2026/08/11-08:06:46.423489 175187000 Level-0 table #5: 3739 bytes OK -2026/08/11-08:06:46.423704 175187000 Delete type=0 #3 -2026/08/11-08:06:46.423739 175187000 Delete type=3 #2 +2026/08/11-08:56:03.971218 16e763000 Delete type=3 #1 diff --git a/packs/tables/LOG.old b/packs/tables/LOG.old deleted file mode 100644 index d87b0ac..0000000 --- a/packs/tables/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.274259 171d3f000 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 3e9a074..730ff0a 100644 --- a/packs/talents/LOG +++ b/packs/talents/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.414303 17497b000 Recovering log #3 -2026/08/11-08:06:46.414338 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.414485 17497b000 Level-0 table #5: 5770 bytes OK -2026/08/11-08:06:46.414718 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.414751 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.942298 16ff87000 Delete type=3 #1 diff --git a/packs/talents/LOG.old b/packs/talents/LOG.old deleted file mode 100644 index 06e0c9d..0000000 --- a/packs/talents/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.252690 17051b000 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 58e94ca..3c3d6b6 100644 --- a/packs/vehicles/LOG +++ b/packs/vehicles/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.419951 17416f000 Recovering log #3 -2026/08/11-08:06:46.419979 17416f000 Level-0 table #5: started -2026/08/11-08:06:46.420174 17416f000 Level-0 table #5: 6939 bytes OK -2026/08/11-08:06:46.420458 17416f000 Delete type=0 #3 -2026/08/11-08:06:46.420501 17416f000 Delete type=3 #2 +2026/08/11-08:56:03.951942 16e763000 Delete type=3 #1 diff --git a/packs/vehicles/LOG.old b/packs/vehicles/LOG.old deleted file mode 100644 index eb3225d..0000000 --- a/packs/vehicles/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.258984 171533000 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 c53e472..7d206d0 100644 --- a/packs/weapons/LOG +++ b/packs/weapons/LOG @@ -1,5 +1 @@ -2026/08/11-08:06:46.416237 17497b000 Recovering log #3 -2026/08/11-08:06:46.416299 17497b000 Level-0 table #5: started -2026/08/11-08:06:46.416505 17497b000 Level-0 table #5: 14767 bytes OK -2026/08/11-08:06:46.416784 17497b000 Delete type=0 #3 -2026/08/11-08:06:46.416833 17497b000 Delete type=3 #2 +2026/08/11-08:56:03.945178 16ef6f000 Delete type=3 #1 diff --git a/packs/weapons/LOG.old b/packs/weapons/LOG.old deleted file mode 100644 index 161ec1b..0000000 --- a/packs/weapons/LOG.old +++ /dev/null @@ -1 +0,0 @@ -2026/08/11-08:06:24.254252 17051b000 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 85ecab3..25d4e49 100644 --- a/ringbrp.mjs +++ b/ringbrp.mjs @@ -32,6 +32,11 @@ import { FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, fireBandFor, BLAST, blastBandFor, blastShareFor, BLAST_COVER, blastAfterCover, BURST, burstHitsFor, burstRoundsSpent, BURST_FUMBLE, BURST_ONE_MAJOR_WOUND, + BURST_HITS, + FIRE_MODE, fireModeFor, burstSizesFor, fireModeAttackFor, secondShotPenaltyFor, + SUSTAIN, sustainDamageFor, sustainChargeFor, + SIGHT, sightFor, sightBonusFor, sightSeesInDark, + LIGHTING, lightingModFor, SPACE_BANDS, spaceBandRank, spaceBandAfter, STATIONS, HEAT, heatEffectsFor, spaceRangeModFor, spaceHitFor, craftConditionFor, TWO_WEAPON, twoWeaponPenaltyFor, canPairWeapons, @@ -285,7 +290,14 @@ RINGBRP.locationEffects = { head: { disabled: "RINGBRP.LocEffect.head", destroyed: "RINGBRP.LocEffect.headX" } }; -RINGBRP.fireModes = ["", "single", "rapid", "slow", "beam"]; +// Not a copy — rules.mjs owns the modes; this is the keyed view the item sheet wants, +// with a blank at the front for melee. The hand-written list that used to live here +// was missing `fast`, which five catalogue weapons already carried: the select had no +// matching option, so opening one of those items and saving it blanked its fire mode. +RINGBRP.fireModes = ["", ...Object.keys(FIRE_MODE)]; +RINGBRP.sights = SIGHT; +RINGBRP.lighting = LIGHTING; +RINGBRP.sustain = SUSTAIN; RINGBRP.weaponClasses = ["melee", "thrown", "bow", "firearm", "energy", "stun", "special"]; RINGBRP.damageTags = ["kinetic", "energy", "monofilament", "disintegrator", "explosive", "blunt", "edged", "stun"]; RINGBRP.traditions = ["tradition_home", "tradition_rival_a", "tradition_rival_b", "tradition_local", "tradition_builder", "tradition_precursor"]; @@ -842,6 +854,46 @@ export function gearBonusFor(actor, skillFamilyId) { }; } +/** + * The sight actually bolted to this weapon, decided by what the agent is carrying. + * + * The point of grading sights by range band is that they apply themselves: there is + * nothing for the player to select and nothing to forget, because the band is already + * known by the time the modifier is wanted. What this has to get right is FIT — a + * telescopic sight on a service pistol is not a thing, so an optic declares what it + * fits and is matched against the weapon's own specialisation. + * + * Only carried optics count, and the best single one applies: taping three sights to + * a rifle is not three bonuses. + */ +export function sightForWeapon(actor, weapon) { + if (!actor || !weapon?.system?.isRanged) return { id: "none", source: "" }; + const spec = weapon.system.skillSpecialisationId || ""; + let best = { id: "none", source: "", worth: 0 }; + for (const it of actor.items ?? []) { + if (it.system?.carried === false) continue; + const o = it.system?.optic; + const kind = o?.kind; + if (!kind || !SIGHT[kind]) continue; + const fits = o.fits || "any"; + if (fits !== "any" && fits !== spec) continue; + // "Best" is the most it is ever worth, which keeps the choice stable across + // bands — a scope does not stop being the scope you fitted at close range. + const worth = Math.max(...Object.values(sightFor(kind).bands)); + if (worth > best.worth) best = { id: kind, source: it.name, worth }; + } + return { id: best.id, source: best.source }; +} + +/** Is this agent carrying something lit? Lamps are gear, not a checkbox. */ +export function carriedLight(actor) { + for (const it of actor?.items ?? []) { + if (it.system?.carried === false) continue; + if (it.system?.lightSource) return { on: true, source: it.name }; + } + return { on: false, source: "" }; +} + /** * Core skill roll. Everything in the system funnels through here so that the * band arithmetic exists in exactly one place. @@ -2319,9 +2371,18 @@ export async function detonate({ formula = "4d6", radius = 6, targets = [] } = { */ export async function burstAttack(actor, weapon, target, { size = "short", difficulty = "average", - situational = 0 } = {}) { + situational = 0, + sizeModApplied = false, + flavourLines = [] } = {}) { if (!actor || !weapon) return null; const b = BURST[size] ?? BURST.short; + // The mode is the gate, and it is checked HERE rather than only in the dialog, so + // that no caller — a macro, a playtest harness, the API — can have a muzzle-loader + // produce automatic fire. + if (!burstSizesFor(weapon.system?.fireMode).includes(b.id)) { + ui.notifications?.warn(game.i18n.format("RINGBRP.Notify.NoBurst", { name: weapon.name })); + return null; + } const loaded = Number(weapon.system?.ammo?.loaded) || 0; if (loaded <= 0) { ui.notifications?.warn(game.i18n.localize("RINGBRP.Notify.Empty")); @@ -2333,9 +2394,11 @@ export async function burstAttack(actor, weapon, target, { size = "short", const res = await skillRoll({ actor, label: game.i18n.format("RINGBRP.Burst.Label", { name: weapon.name, size: game.i18n.localize(b.label) }), - rating, difficulty, situational: situational + b.mod, + // When the sheet built the stack it already counted the burst's own modifier; + // adding it again here charged the player twice for the same decision. + rating, difficulty, situational: situational + (sizeModApplied ? 0 : b.mod), categoryId: "ranged", skillFamilyId: weapon.system.skillFamilyId, weapon, - flavourLines: [game.i18n.format("RINGBRP.Burst.Spends", { n: spent })] + flavourLines: [...flavourLines, game.i18n.format("RINGBRP.Burst.Spends", { n: spent })] }); if (!res) return null; // The rounds go whether or not anything is hit. That is automatic fire. @@ -2909,10 +2972,14 @@ export async function defend(actor, { type = "dodge", weapon = null, difficulty const BLOW_TTL_MS = 120000; let lastBlow = null; -export async function recordBlow({ attacker, level, weapon = null, targets = [] } = {}) { +export async function recordBlow({ attacker, level, weapon = null, targets = [], + bonus = "" } = {}) { if (!attacker || !["critical", "special", "success"].includes(level)) return null; + // `bonus` is extra damage dice the ATTACK earned rather than the weapon carrying — + // a sustained beam. It rides on the blow because the damage roll happens later and + // separately, and otherwise the choice that paid -25% to accuracy would be lost. const blow = { attackerId: attacker.id, attackerName: attacker.name, level, - weaponName: weapon?.name ?? null, targets, at: Date.now() }; + weaponName: weapon?.name ?? null, targets, bonus, at: Date.now() }; lastBlow = blow; if (game.combat) await game.combat.setFlag("ringbrp", "standingBlow", blow); return blow; @@ -4542,7 +4609,9 @@ class RingBRPCharacterSheet extends ActorSheet { return foundry.utils.mergeObject(super.defaultOptions, { classes: ["ringbrp", "sheet", "actor", "character"], template: "systems/ringbrp/templates/character-sheet.hbs", - width: 820, + // The tab rail takes 152px the body used to have. Paying for it here keeps the + // skills list at two columns and every other tab exactly as roomy as before. + width: 972, height: 880, tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "skills" }] }); @@ -4879,7 +4948,12 @@ class RingBRPCharacterSheet extends ActorSheet { } _itemFrom(ev) { - const id = ev.currentTarget.closest("[data-item-id]")?.dataset.itemId; + // currentTarget is only set while the event is being dispatched. A handler that + // awaits anything and then calls this again gets null and throws — which is + // exactly what happened to every Coherence-costing weapon. Fall back to the + // target, which survives, so a second call after an await still resolves. + const from = ev.currentTarget ?? ev.target; + const id = from?.closest("[data-item-id]")?.dataset.itemId; return id ? this.actor.items.get(id) : null; } @@ -4971,13 +5045,16 @@ class RingBRPCharacterSheet extends ActorSheet { } async _onRollAttack(ev) { - // F5: transposed materiel charges its Coherence cost when it is actually used. - const _w = this._itemFrom(ev); - if (_w && Number(_w.system?.coherence ?? _w.system?.coherenceCost)) { - await useTransposed(this.actor, _w); - } + // Resolve the weapon ONCE, before anything is awaited. Reading it a second time + // after the Coherence charge was how the Redaction lance, the Unmaker and every + // other piece of transposed materiel became unusable: the event had gone stale + // by then and the attack died on a TypeError with nothing shown to the player. const weapon = this._itemFrom(ev); if (!weapon) return; + // F5: transposed materiel charges its Coherence cost when it is actually used. + if (Number(weapon.system?.coherence ?? weapon.system?.coherenceCost)) { + await useTransposed(this.actor, weapon); + } const sys = this.actor.system; const rating = this.actor.skillRating(weapon.system.skillFamilyId, weapon.system.skillSpecialisationId); if (rating === null) { @@ -4990,27 +5067,70 @@ class RingBRPCharacterSheet extends ActorSheet { ui.notifications.warn(game.i18n.localize("RINGBRP.Notify.OutOfRange")); return; } + // A burst is a different resolution, not a modified one: one roll whose BAND + // decides how much of it arrived. burstAttack owns that, and takes the whole + // modifier stack this dialog built rather than rebuilding its own. + if (opts.burst) { + const target = [...(game.user.targets ?? [])][0]?.actor ?? null; + return burstAttack(this.actor, weapon, target, + { size: opts.burst, difficulty: opts.difficulty, + situational: opts.situational, sizeModApplied: true, flavourLines: opts.lines }); + } + // Ammunition (spec §4.5) if (weapon.system.ammo?.capacity > 0) { if ((weapon.system.ammo.loaded ?? 0) <= 0) { ui.notifications.warn(game.i18n.localize("RINGBRP.Notify.Empty")); return; } - await weapon.update({ "system.ammo.loaded": weapon.system.ammo.loaded - 1 }); + const cost = opts.sustain ? sustainChargeFor(opts.sustain) : 1; + if ((weapon.system.ammo.loaded ?? 0) < cost) { + ui.notifications.warn(game.i18n.localize("RINGBRP.Notify.NotEnoughCharge")); + return; + } + await weapon.update({ "system.ammo.loaded": weapon.system.ammo.loaded - cost }); + } + + // The second shot a fast action gives you. Both shots are worse, which is the + // whole cost, and it is once per round — which is what `secondAttackUsed` was + // reset for every round without anything ever setting it. + const shots = opts.second ? 2 : 1; + if (opts.second) { + if (this.actor.system.combat?.secondAttackUsed) { + ui.notifications.warn(game.i18n.localize("RINGBRP.Notify.SecondUsed")); + return; + } + await this.actor.update({ "system.combat.secondAttackUsed": true }); + } + + let res = null; + for (let i = 0; i < shots; i++) { + res = await skillRoll({ + actor: this.actor, + label: shots > 1 ? `${weapon.name} (${i + 1}/${shots})` : weapon.name, + rating, + difficulty: opts.difficulty, + situational: opts.situational, + flavourLines: opts.lines, + categoryId: weapon.system?.isRanged ? "ranged" : "melee", + weapon + }); + // Leave the blow standing so whoever defends knows what quality it was. + if (res) await recordBlow({ attacker: this.actor, level: res.level, weapon, + bonus: opts.sustain ? sustainDamageFor(opts.sustain) : "", + targets: [...(game.user.targets ?? [])].map(t => t.id) }); + } + + // A slow weapon costs its reload after the shot, not before it. This is the + // other half of why it is worth +10%: you get one, and then you work for it. + if (opts.reloads && weapon.system.ammo?.capacity > 0) { + const n = Number(weapon.system.ammo.reloadRounds) || 1; + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + content: `
Every weapon has a fire mode, and it is a decision rather than a +drawback. The trade runs one way: the faster a weapon puts rounds out, the less each +individual shot is worth, and the slower it is, the more the one shot you get is worth +taking properly.
+${table(["Mode", "To hit", "What it offers"], + Object.values(FIRE_MODE).map(m => [L(m.label), pc(m.attack), + m.burst.length ? "bursts" + : m.sustain ? "the beam can be held on the target" + : m.second ? `a second shot, at ${pc(SECOND_SHOT)} to both` + : m.reloads ? "one shot, then work" + : "one aimed shot a round"]))} +A weapon that cannot fire a burst cannot be made to. A muzzle-loader +has no automatic setting however the button is pressed.
+ +A beam does not fire discrete rounds, so counting hits is the wrong shape for it. +What a beam can do instead is stay on the target, and that buys damage +— the one firing choice in this game that adds dice rather than adding rolls. It is +paid for twice, in accuracy and in charge.
+${table(["", "To hit", "Damage", "Charge"], + Object.values(SUSTAIN).map(s => [L(s.label), pc(s.mod), + s.bonus ? `+${s.bonus}` : "—", `×${s.charge}`]))} + +A sight is not a flat bonus, which is why most games end up with everybody carrying +the same scope. A sight is ground for a distance and is a liability at the +others. You never choose to apply it: the range band is already known by the +time the modifier is wanted, so a carried sight applies itself.
+${table(["Sight", "Short", "Medium", "Long", "Extreme"], + Object.values(SIGHT).filter(s => s.id !== "none").map(s => [L(s.label), + pc(s.bands.short), pc(s.bands.medium), pc(s.bands.long), pc(s.bands.extreme)]))} +The telescopic sight is a penalty inside ten metres. That is the +whole point of it, and it is why nobody sensibly carries all four. A sight only counts +if it fits — the scope and the thermal sight are rifle furniture and do nothing +bolted to a service pistol.
+ +Most crossings do not come with the lights on.
+${table(["", "To hit"], Object.values(LIGHTING).map(l => [L(l.label), pc(l.mod)]))} +A carried lamp buys the dark back to ${L(LIGHTING[LIGHT_FLOOR].label)} +and never better — and it is a position. The card will say so. A thermal sight removes +the dark outright and does not announce you, which is most of what you are +paying its weight for.
+One roll, not four. The success level says how much of the burst arrived, and the rounds are gone either way — that is the honest part