Attack card: humanoid and quadruped damage buttons

The single Roll damage button becomes two, with person and paw icons. Each
rolls damage and then a hit location. A targeted token still uses its own
anatomy; with no target the blow is placed on the chosen body plan and
shown, with nothing applied. Both buttons disable after either is used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-16 18:12:48 +01:00
co-authored by Claude Opus 5
parent 197bdf9fb0
commit 1e0deb2ef0
5 changed files with 39 additions and 6 deletions
+5
View File
@@ -468,6 +468,7 @@
"RINGBRP.Loc.Header": "Hit location", "RINGBRP.Loc.Header": "Hit location",
"RINGBRP.Loc.Grounded": "{name} comes down. A flyer with a hole in a wing is a ground animal &mdash; {plan} now, and it fights as one for the rest of the scene.", "RINGBRP.Loc.Grounded": "{name} comes down. A flyer with a hole in a wing is a ground animal &mdash; {plan} now, and it fights as one for the rest of the scene.",
"RINGBRP.Loc.NoTarget": "No target — damage rolled, but nothing was hit. Target a token and roll damage again to place the blow.", "RINGBRP.Loc.NoTarget": "No target — damage rolled, but nothing was hit. Target a token and roll damage again to place the blow.",
"RINGBRP.Loc.NoTargetPlaced": "No target: the blow is placed but nothing is applied. Target a token to take the damage off.",
"RINGBRP.Loc.RollHint": "Roll 1D20 for a hit location on this anatomy", "RINGBRP.Loc.RollHint": "Roll 1D20 for a hit location on this anatomy",
"RINGBRP.Tab.Anatomy": "Anatomy", "RINGBRP.Tab.Anatomy": "Anatomy",
"RINGBRP.Sheet.LocationNote": "A blow meeting a location's maximum disables it; twice that destroys it. Damage also comes off general hit points, so the two readings always agree.", "RINGBRP.Sheet.LocationNote": "A blow meeting a location's maximum disables it; twice that destroys it. Damage also comes off general hit points, so the two readings always agree.",
@@ -735,6 +736,10 @@
"RINGBRP.React.ReadinessSkill": "from training", "RINGBRP.React.ReadinessSkill": "from training",
"RINGBRP.React.ReadinessInt": "from wits", "RINGBRP.React.ReadinessInt": "from wits",
"RINGBRP.Chat.RollDamage": "Roll damage", "RINGBRP.Chat.RollDamage": "Roll damage",
"RINGBRP.Chat.RollDamageHumanoid": "Humanoid",
"RINGBRP.Chat.RollDamageQuadruped": "Quadruped",
"RINGBRP.Chat.DamageHumanoidHint": "Roll damage, then a hit location on a humanoid. A targeted token uses its own anatomy.",
"RINGBRP.Chat.DamageQuadrupedHint": "Roll damage, then a hit location on a quadruped. A targeted token uses its own anatomy.",
"RINGBRP.Chat.DamageByBand": "by range band", "RINGBRP.Chat.DamageByBand": "by range band",
"RINGBRP.Chat.DamageCritical": "maximum damage, armour ignored", "RINGBRP.Chat.DamageCritical": "maximum damage, armour ignored",
"RINGBRP.Chat.DamageSpecial": "armour counts half", "RINGBRP.Chat.DamageSpecial": "armour counts half",
+22 -3
View File
@@ -5950,7 +5950,8 @@ function damageTargetFor(explicit = null) {
* token and the blow lands somewhere. * token and the blow lands somewhere.
*/ */
export async function rollWeaponDamage({ actor = null, weapon, level = "success", band = null, export async function rollWeaponDamage({ actor = null, weapon, level = "success", band = null,
bonus = "", target = null, locate = true } = {}) { bonus = "", target = null, locate = true,
plan = null } = {}) {
if (!weapon) return null; if (!weapon) return null;
const d = weapon.system?.damage ?? {}; const d = weapon.system?.damage ?? {};
let formula = d.formula || "1d6"; let formula = d.formula || "1d6";
@@ -6004,6 +6005,21 @@ export async function rollWeaponDamage({ actor = null, weapon, level = "success"
if (victim) { if (victim) {
await hitLocationRoll({ target: victim, damage: roll.total, await hitLocationRoll({ target: victim, damage: roll.total,
mode: locationModeFor(weapon), level }); mode: locationModeFor(weapon), level });
} else if (plan) {
// The attack card's humanoid / quadruped buttons. With nobody targeted the blow
// is still placed on the body plan the player chose, so the GM can narrate it,
// but nothing is applied. A target always uses its own sheet's anatomy instead,
// because its location hit points are keyed to that table.
const mode = locationModeFor(weapon);
const r = await new Roll("1d20").evaluate();
const loc = locationFor(r.total, plan, mode);
const content = await rbRenderTemplate("systems/ringbrp/templates/chat-location.hbs", {
roll: r.total, mode, preview: true,
locationLabel: game.i18n.localize(loc.label),
speciesLabel: game.i18n.localize(RINGBRP.locationTables[plan]?.label ?? ""),
damage: roll.total
});
await r.toMessage({ speaker: ChatMessage.getSpeaker({ actor: actor ?? undefined }), content });
} else { } else {
// Say so rather than failing quietly. A GM who meant to target somebody needs // Say so rather than failing quietly. A GM who meant to target somebody needs
// to know the blow went nowhere, and one who did not is told how to make it. // to know the blow went nowhere, and one who did not is told how to make it.
@@ -6490,8 +6506,11 @@ async function _onChatDamage(ev) {
: game.actors.get(f.actorId); : game.actors.get(f.actorId);
const weapon = actor?.items?.get(f.weaponId); const weapon = actor?.items?.get(f.weaponId);
if (!weapon) return ui.notifications?.warn(game.i18n.localize("RINGBRP.Notify.NoWeapon")); if (!weapon) return ui.notifications?.warn(game.i18n.localize("RINGBRP.Notify.NoWeapon"));
btn.disabled = true; // one damage roll per hit // One damage roll per hit, whichever body plan was chosen.
await rollWeaponDamage({ actor, weapon, level: f.level }); btn.closest(".rb-chat-damage")?.querySelectorAll("[data-action='chat-damage']")
.forEach(b => { b.disabled = true; });
const plan = btn.dataset.plan in RINGBRP.locationTables ? btn.dataset.plan : null;
await rollWeaponDamage({ actor, weapon, level: f.level, plan });
} }
// v13 renamed this and warns on every message if the old name is registered, so // v13 renamed this and warns on every message if the old name is registered, so
+1
View File
@@ -1324,6 +1324,7 @@
} }
.rb-dmg-btn:hover:not(:disabled) { background: var(--rb-yellow); color: var(--rb-panel); } .rb-dmg-btn:hover:not(:disabled) { background: var(--rb-yellow); color: var(--rb-panel); }
.rb-dmg-btn:disabled { opacity: 0.45; cursor: default; } .rb-dmg-btn:disabled { opacity: 0.45; cursor: default; }
.rb-dmg-btn i { margin-right: 4px; }
.rb-dmg-note { font-size: 0.75rem; font-style: italic; color: var(--rb-muted, #9fb0d9); } .rb-dmg-note { font-size: 0.75rem; font-style: italic; color: var(--rb-muted, #9fb0d9); }
/* Carried but not worn — the ARMOUR box says why it reads zero */ /* Carried but not worn — the ARMOUR box says why it reads zero */
+2 -1
View File
@@ -8,8 +8,9 @@
<div class="rb-chat-result"> <div class="rb-chat-result">
<span class="rb-loc-name">{{locationLabel}}</span> <span class="rb-loc-name">{{locationLabel}}</span>
<span class="rb-loc-dmg">{{damage}}</span> <span class="rb-loc-dmg">{{damage}}</span>
<span class="rb-loc-of">{{taken}} / {{max}}</span> {{#unless preview}}<span class="rb-loc-of">{{taken}} / {{max}}</span>{{/unless}}
</div> </div>
{{#if effect}}<p class="rb-loc-effect">{{effect}}</p>{{/if}} {{#if effect}}<p class="rb-loc-effect">{{effect}}</p>{{/if}}
{{#if preview}}<p class="rb-loc-effect">{{localize "RINGBRP.Loc.NoTargetPlaced"}}</p>{{/if}}
{{#if majorWound}}<p class="rb-wound-loss">{{localize "RINGBRP.Wound.Header"}}</p>{{/if}} {{#if majorWound}}<p class="rb-wound-loss">{{localize "RINGBRP.Wound.Header"}}</p>{{/if}}
</div> </div>
+9 -2
View File
@@ -22,8 +22,15 @@
</div> </div>
{{#if damage}} {{#if damage}}
<div class="rb-chat-damage"> <div class="rb-chat-damage">
<button type="button" class="rb-dmg-btn" data-action="chat-damage"> <button type="button" class="rb-dmg-btn" data-action="chat-damage" data-plan="baseline"
{{localize "RINGBRP.Chat.RollDamage"}} <strong>{{damage.label}}</strong> title="{{localize "RINGBRP.Chat.DamageHumanoidHint"}}">
<i class="fa-solid fa-person" aria-hidden="true"></i>
{{localize "RINGBRP.Chat.RollDamageHumanoid"}} <strong>{{damage.label}}</strong>
</button>
<button type="button" class="rb-dmg-btn" data-action="chat-damage" data-plan="quadruped"
title="{{localize "RINGBRP.Chat.DamageQuadrupedHint"}}">
<i class="fa-solid fa-paw" aria-hidden="true"></i>
{{localize "RINGBRP.Chat.RollDamageQuadruped"}} <strong>{{damage.label}}</strong>
</button> </button>
{{#if damage.critical}}<span class="rb-dmg-note">{{localize "RINGBRP.Chat.DamageCritical"}}</span>{{/if}} {{#if damage.critical}}<span class="rb-dmg-note">{{localize "RINGBRP.Chat.DamageCritical"}}</span>{{/if}}
{{#if damage.special}}<span class="rb-dmg-note">{{localize "RINGBRP.Chat.DamageSpecial"}}</span>{{/if}} {{#if damage.special}}<span class="rb-dmg-note">{{localize "RINGBRP.Chat.DamageSpecial"}}</span>{{/if}}