Character sheet: reload button for ranged weapons
A reload icon sits beside the ammo count in the readied-weapon strip and the weapons table, for any weapon with a magazine. It tops the weapon up from its reserve (rules.reloadFor, spot-checked), posts the new count and the weapon's reload time to chat, and warns when the weapon is already full or the reserve is empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e0deb2ef0
commit
750cbd257d
@@ -217,6 +217,7 @@
|
|||||||
"RINGBRP.Sheet.Damage": "Damage",
|
"RINGBRP.Sheet.Damage": "Damage",
|
||||||
"RINGBRP.Sheet.Range": "Range",
|
"RINGBRP.Sheet.Range": "Range",
|
||||||
"RINGBRP.Sheet.Ammo": "Ammo",
|
"RINGBRP.Sheet.Ammo": "Ammo",
|
||||||
|
"RINGBRP.Sheet.ReloadHint": "Reload from reserve",
|
||||||
"RINGBRP.Sheet.Points": "pts",
|
"RINGBRP.Sheet.Points": "pts",
|
||||||
"RINGBRP.Sheet.Integrity": "Integrity",
|
"RINGBRP.Sheet.Integrity": "Integrity",
|
||||||
"RINGBRP.Sheet.ClearWound": "Clear this wound",
|
"RINGBRP.Sheet.ClearWound": "Clear this wound",
|
||||||
@@ -387,6 +388,8 @@
|
|||||||
"RINGBRP.Notify.NoSkill": "Nothing on this sheet knows how to use {name}.",
|
"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.OutOfRange": "Too far away to hit. Get closer or use something with more reach.",
|
||||||
"RINGBRP.Notify.Empty": "Empty. Reload before firing.",
|
"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.NoLeg": "No unresolved phase remains.",
|
||||||
"RINGBRP.Notify.BadDamage": "Damage was not a number — the hit was not applied. See the console.",
|
"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.",
|
"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.Adds": "adds {dice} damage",
|
||||||
"RINGBRP.Fire.Second": "Two shots",
|
"RINGBRP.Fire.Second": "Two shots",
|
||||||
"RINGBRP.Fire.SecondEffect": "both hurried",
|
"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.Reloading": "{name} — reloading, {n} round(s)",
|
||||||
"RINGBRP.Fire.slow.Label": "Slow",
|
"RINGBRP.Fire.slow.Label": "Slow",
|
||||||
"RINGBRP.Fire.single.Label": "Single",
|
"RINGBRP.Fire.single.Label": "Single",
|
||||||
|
|||||||
+25
-1
@@ -35,7 +35,7 @@ import {
|
|||||||
RETURN_CLOCK, returnPenaltyFor, returnIsDifficult, extractionCostFor,
|
RETURN_CLOCK, returnPenaltyFor, returnIsDifficult, extractionCostFor,
|
||||||
FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, fireBandFor,
|
FALL, fallDamageFor, ASPHYXIA, breathRoundsFor, FIRE, fireBandFor,
|
||||||
BLAST, blastBandFor, blastShareFor, BLAST_COVER, blastAfterCover,
|
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,
|
BURST_HITS, LAMP, lampReachFor, lampCovers,
|
||||||
CONTACT_OUTCOMES, contactDeltaFor, impressionDeltaFor,
|
CONTACT_OUTCOMES, contactDeltaFor, impressionDeltaFor,
|
||||||
COVERAGE, coverageFor, armourCoversKind, locationArmourFor,
|
COVERAGE, coverageFor, armourCoversKind, locationArmourFor,
|
||||||
@@ -5005,6 +5005,9 @@ class RingBRPCharacterSheet extends ActorSheet {
|
|||||||
el.addEventListener("click", () => this._onRollLocation()));
|
el.addEventListener("click", () => this._onRollLocation()));
|
||||||
root.querySelectorAll("[data-action='roll-attack']").forEach(el =>
|
root.querySelectorAll("[data-action='roll-attack']").forEach(el =>
|
||||||
el.addEventListener("click", ev => this._onRollAttack(ev)));
|
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
|
// Hovering a limb highlights its row and vice versa, so the picture and the
|
||||||
// numbers are obviously the same thing.
|
// numbers are obviously the same thing.
|
||||||
const link = (sel, on) => root.querySelectorAll(sel).forEach(el => {
|
const link = (sel, on) => root.querySelectorAll(sel).forEach(el => {
|
||||||
@@ -5287,6 +5290,27 @@ class RingBRPCharacterSheet extends ActorSheet {
|
|||||||
return res;
|
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: `<div class="ringbrp-chat"><strong>${
|
||||||
|
game.i18n.format("RINGBRP.Fire.Reloaded", {
|
||||||
|
name: weapon.name, loaded: r.loaded, capacity: ammo.capacity,
|
||||||
|
reserve: r.reserve, n: Number(ammo.reloadRounds) || 1 })}</strong></div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _onRollDamage(ev) {
|
async _onRollDamage(ev) {
|
||||||
const weapon = this._itemFrom(ev);
|
const weapon = this._itemFrom(ev);
|
||||||
if (!weapon) return;
|
if (!weapon) return;
|
||||||
|
|||||||
@@ -1077,6 +1077,18 @@ export function burstRoundsSpent(size, loaded) {
|
|||||||
return Math.min(Number(loaded) || 0, b.rounds);
|
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. */
|
/** 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"];
|
export const BURST_FUMBLE = ["RINGBRP.Burst.Jam", "RINGBRP.Burst.Empty", "RINGBRP.Burst.Wild"];
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,8 @@
|
|||||||
.rb-weapontable .rb-col-ready { width: 40px; text-align: center; }
|
.rb-weapontable .rb-col-ready { width: 40px; text-align: center; }
|
||||||
.rb-weapontable td { vertical-align: middle; }
|
.rb-weapontable td { vertical-align: middle; }
|
||||||
.rb-weapontable .rb-equip { font-size: 15px; }
|
.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 {
|
.rb-weapon-icon {
|
||||||
width: 40px; height: 40px; margin-right: 9px;
|
width: 40px; height: 40px; margin-right: 9px;
|
||||||
border: 1px solid var(--rb-hairline); background: var(--rb-void);
|
border: 1px solid var(--rb-hairline); background: var(--rb-void);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<a data-action="roll-attack">{{w.name}}</a>
|
<a data-action="roll-attack">{{w.name}}</a>
|
||||||
<span class="rb-strip-dmg" data-action="roll-damage">{{#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}}</span>
|
<span class="rb-strip-dmg" data-action="roll-damage">{{#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}}</span>
|
||||||
{{#if w.system.rangeSummary}}<span class="rb-strip-range">{{w.system.rangeSummary}}</span>{{/if}}
|
{{#if w.system.rangeSummary}}<span class="rb-strip-range">{{w.system.rangeSummary}}</span>{{/if}}
|
||||||
{{#if w.system.ammo.capacity}}<span class="rb-strip-ammo">{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}</span>{{/if}}
|
{{#if w.system.ammo.capacity}}<span class="rb-strip-ammo">{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}</span><a class="rb-reload" data-action="reload-weapon" data-tooltip="{{localize 'RINGBRP.Sheet.ReloadHint'}} ({{localize 'RINGBRP.Item.Reserve'}} {{w.system.ammo.reserve}})"><i class="fa-solid fa-rotate-right"></i></a>{{/if}}
|
||||||
</span>
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="rb-strip-empty">{{localize "RINGBRP.Sheet.NothingReadied"}}</span>
|
<span class="rb-strip-empty">{{localize "RINGBRP.Sheet.NothingReadied"}}</span>
|
||||||
@@ -239,7 +239,7 @@
|
|||||||
<td class="rb-td-icon"><img class="rb-li-icon rb-weapon-icon" src="{{w.img}}" alt=""/><a data-action="roll-attack" class="rb-weapon-name">{{w.name}}</a></td>
|
<td class="rb-td-icon"><img class="rb-li-icon rb-weapon-icon" src="{{w.img}}" alt=""/><a data-action="roll-attack" class="rb-weapon-name">{{w.name}}</a></td>
|
||||||
<td><a data-action="roll-damage" class="rb-weapon-dmg">{{#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}}</a></td>
|
<td><a data-action="roll-damage" class="rb-weapon-dmg">{{#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}}</a></td>
|
||||||
<td>{{w.system.rangeSummary}}</td>
|
<td>{{w.system.rangeSummary}}</td>
|
||||||
<td>{{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}}{{/if}}</td>
|
<td>{{#if w.system.ammo.capacity}}{{w.system.ammo.loaded}}/{{w.system.ammo.capacity}} <a class="rb-reload" data-action="reload-weapon" data-tooltip="{{localize 'RINGBRP.Sheet.ReloadHint'}} ({{localize 'RINGBRP.Item.Reserve'}} {{w.system.ammo.reserve}})"><i class="fa-solid fa-rotate-right"></i></a>{{/if}}</td>
|
||||||
<td class="rb-col-num">{{w.system.massKg}} kg</td>
|
<td class="rb-col-num">{{w.system.massKg}} kg</td>
|
||||||
<td class="rb-actions">
|
<td class="rb-actions">
|
||||||
<a data-action="item-edit"><i class="fas fa-edit"></i></a>
|
<a data-action="item-edit"><i class="fas fa-edit"></i></a>
|
||||||
|
|||||||
@@ -320,6 +320,10 @@ const checks = [
|
|||||||
["burstHitsFor(critical, full) capped", RULES.burstHitsFor("critical","full"), 7],
|
["burstHitsFor(critical, full) capped", RULES.burstHitsFor("critical","full"), 7],
|
||||||
["burstHitsFor(success, short)", RULES.burstHitsFor("success","short"), 1],
|
["burstHitsFor(success, short)", RULES.burstHitsFor("success","short"), 1],
|
||||||
["burstHitsFor(failure, long)", RULES.burstHitsFor("failure","long"), 0],
|
["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],
|
["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.
|
// 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],
|
["burstSizesFor(slow) is empty", RULES.burstSizesFor("slow").length, 0],
|
||||||
|
|||||||
Reference in New Issue
Block a user