diff --git a/lang/en.json b/lang/en.json
index cf8b6f3..5b3c5a5 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -217,6 +217,7 @@
"RINGBRP.Sheet.Damage": "Damage",
"RINGBRP.Sheet.Range": "Range",
"RINGBRP.Sheet.Ammo": "Ammo",
+ "RINGBRP.Sheet.ReloadHint": "Reload from reserve",
"RINGBRP.Sheet.Points": "pts",
"RINGBRP.Sheet.Integrity": "Integrity",
"RINGBRP.Sheet.ClearWound": "Clear this wound",
@@ -387,6 +388,8 @@
"RINGBRP.Notify.NoSkill": "Nothing on this sheet knows how to use {name}.",
"RINGBRP.Notify.OutOfRange": "Too far away to hit. Get closer or use something with more reach.",
"RINGBRP.Notify.Empty": "Empty. Reload before firing.",
+ "RINGBRP.Notify.ReloadFull": "{name} is already fully loaded.",
+ "RINGBRP.Notify.NoReserve": "No reserve ammunition left for {name}.",
"RINGBRP.Notify.NoLeg": "No unresolved phase remains.",
"RINGBRP.Notify.BadDamage": "Damage was not a number — the hit was not applied. See the console.",
"RINGBRP.Notify.BlowStopped": "That blow was stopped. There is no damage to roll.",
@@ -855,6 +858,7 @@
"RINGBRP.Fire.Adds": "adds {dice} damage",
"RINGBRP.Fire.Second": "Two shots",
"RINGBRP.Fire.SecondEffect": "both hurried",
+ "RINGBRP.Fire.Reloaded": "{name} reloaded: {loaded}/{capacity}, {reserve} in reserve. Takes {n} round(s).",
"RINGBRP.Fire.Reloading": "{name} — reloading, {n} round(s)",
"RINGBRP.Fire.slow.Label": "Slow",
"RINGBRP.Fire.single.Label": "Single",
diff --git a/ringbrp.mjs b/ringbrp.mjs
index 95a5910..b572ab8 100644
--- a/ringbrp.mjs
+++ b/ringbrp.mjs
@@ -35,7 +35,7 @@ import {
RETURN_CLOCK, returnPenaltyFor, returnIsDifficult, extractionCostFor,
FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, fireBandFor,
BLAST, blastBandFor, blastShareFor, BLAST_COVER, blastAfterCover,
- BURST, burstHitsFor, burstRoundsSpent, BURST_FUMBLE, BURST_ONE_MAJOR_WOUND,
+ BURST, burstHitsFor, burstRoundsSpent, BURST_FUMBLE, BURST_ONE_MAJOR_WOUND, reloadFor,
BURST_HITS, LAMP, lampReachFor, lampCovers,
CONTACT_OUTCOMES, contactDeltaFor, impressionDeltaFor,
COVERAGE, coverageFor, armourCoversKind, locationArmourFor,
@@ -5005,6 +5005,9 @@ class RingBRPCharacterSheet extends ActorSheet {
el.addEventListener("click", () => this._onRollLocation()));
root.querySelectorAll("[data-action='roll-attack']").forEach(el =>
el.addEventListener("click", ev => this._onRollAttack(ev)));
+ root.querySelectorAll("[data-action='reload-weapon']").forEach(el =>
+ el.addEventListener("click", ev => { ev.preventDefault(); ev.stopPropagation();
+ this._onReloadWeapon(ev); }));
// Hovering a limb highlights its row and vice versa, so the picture and the
// numbers are obviously the same thing.
const link = (sel, on) => root.querySelectorAll(sel).forEach(el => {
@@ -5287,6 +5290,27 @@ class RingBRPCharacterSheet extends ActorSheet {
return res;
}
+ /** Top the weapon up from its reserve and say how long it takes. */
+ async _onReloadWeapon(ev) {
+ const weapon = this._itemFrom(ev);
+ const ammo = weapon?.system?.ammo;
+ if (!(ammo?.capacity > 0)) return;
+ const r = reloadFor(ammo.capacity, ammo.loaded, ammo.reserve);
+ if (r.moved <= 0) {
+ const key = (Number(ammo.loaded) || 0) >= ammo.capacity ? "RINGBRP.Notify.ReloadFull"
+ : "RINGBRP.Notify.NoReserve";
+ return ui.notifications.warn(game.i18n.format(key, { name: weapon.name }));
+ }
+ await weapon.update({ "system.ammo.loaded": r.loaded, "system.ammo.reserve": r.reserve });
+ await ChatMessage.create({
+ speaker: ChatMessage.getSpeaker({ actor: this.actor }),
+ content: `
${
+ game.i18n.format("RINGBRP.Fire.Reloaded", {
+ name: weapon.name, loaded: r.loaded, capacity: ammo.capacity,
+ reserve: r.reserve, n: Number(ammo.reloadRounds) || 1 })}
`
+ });
+ }
+
async _onRollDamage(ev) {
const weapon = this._itemFrom(ev);
if (!weapon) return;
diff --git a/rules.mjs b/rules.mjs
index 1883d53..fa5a3f3 100644
--- a/rules.mjs
+++ b/rules.mjs
@@ -1077,6 +1077,18 @@ export function burstRoundsSpent(size, loaded) {
return Math.min(Number(loaded) || 0, b.rounds);
}
+/**
+ * A reload tops the weapon up from its reserve. Whatever the magazine has room for
+ * comes out of the reserve, and no more than the reserve holds.
+ */
+export function reloadFor(capacity, loaded, reserve) {
+ const cap = Math.max(0, Number(capacity) || 0);
+ const have = Math.min(cap, Math.max(0, Number(loaded) || 0));
+ const spare = Math.max(0, Number(reserve) || 0);
+ const moved = Math.min(cap - have, spare);
+ return { loaded: have + moved, reserve: spare - moved, moved };
+}
+
/** A fumbled burst is the interesting one: it is a jam, or it is worse. */
export const BURST_FUMBLE = ["RINGBRP.Burst.Jam", "RINGBRP.Burst.Empty", "RINGBRP.Burst.Wild"];
diff --git a/styles/ringbrp.css b/styles/ringbrp.css
index f683431..a7a55bf 100644
--- a/styles/ringbrp.css
+++ b/styles/ringbrp.css
@@ -714,6 +714,8 @@
.rb-weapontable .rb-col-ready { width: 40px; text-align: center; }
.rb-weapontable td { vertical-align: middle; }
.rb-weapontable .rb-equip { font-size: 15px; }
+.rb-reload { margin-left: 4px; font-size: 11px; color: var(--rb-muted); cursor: pointer; }
+.rb-reload:hover { color: var(--rb-yellow); }
.rb-weapon-icon {
width: 40px; height: 40px; margin-right: 9px;
border: 1px solid var(--rb-hairline); background: var(--rb-void);
diff --git a/templates/character-sheet.hbs b/templates/character-sheet.hbs
index dc06fa6..45152e9 100644
--- a/templates/character-sheet.hbs
+++ b/templates/character-sheet.hbs
@@ -115,7 +115,7 @@
{{w.name}}
{{#if (rbEq w.system.damage.mode "byBand")}}{{w.system.damage.short}}/{{w.system.damage.medium}}/{{w.system.damage.long}}{{else}}{{w.system.damage.formula}}{{/if}}
{{#if w.system.rangeSummary}}{{w.system.rangeSummary}}{{/if}}
- {{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}{{/if}}
+ {{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}{{/if}}
{{else}}
{{localize "RINGBRP.Sheet.NothingReadied"}}
@@ -239,7 +239,7 @@
{{w.name}} |
{{#if (rbEq w.system.damage.mode "byBand")}}{{w.system.damage.short}}/{{w.system.damage.medium}}/{{w.system.damage.long}}{{else}}{{w.system.damage.formula}}{{/if}} |
{{w.system.rangeSummary}} |
- {{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}{{/if}} |
+ {{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}} {{/if}} |
{{w.system.massKg}} kg |
diff --git a/tools/check-rules.mjs b/tools/check-rules.mjs
index ede5ba3..e348251 100644
--- a/tools/check-rules.mjs
+++ b/tools/check-rules.mjs
@@ -320,6 +320,10 @@ const checks = [
["burstHitsFor(critical, full) capped", RULES.burstHitsFor("critical","full"), 7],
["burstHitsFor(success, short)", RULES.burstHitsFor("success","short"), 1],
["burstHitsFor(failure, long)", RULES.burstHitsFor("failure","long"), 0],
+ ["reloadFor(6 cap, 2 loaded, 12)", JSON.stringify(RULES.reloadFor(6,2,12)), '{"loaded":6,"reserve":8,"moved":4}'],
+ ["reloadFor(6 cap, 0 loaded, 3)", JSON.stringify(RULES.reloadFor(6,0,3)), '{"loaded":3,"reserve":0,"moved":3}'],
+ ["reloadFor(full)", RULES.reloadFor(6,6,12).moved, 0],
+ ["reloadFor(no reserve)", RULES.reloadFor(6,1,0).moved, 0],
["burstRoundsSpent(long, 4 loaded)", RULES.burstRoundsSpent("long",4), 4],
// The mode is the gate. A musket may not rip a burst however the API is called.
["burstSizesFor(slow) is empty", RULES.burstSizesFor("slow").length, 0],
|