diff --git a/lang/en.json b/lang/en.json index e088df6..cf8b6f3 100644 --- a/lang/en.json +++ b/lang/en.json @@ -468,6 +468,7 @@ "RINGBRP.Loc.Header": "Hit location", "RINGBRP.Loc.Grounded": "{name} comes down. A flyer with a hole in a wing is a ground animal — {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.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.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.", @@ -735,6 +736,10 @@ "RINGBRP.React.ReadinessSkill": "from training", "RINGBRP.React.ReadinessInt": "from wits", "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.DamageCritical": "maximum damage, armour ignored", "RINGBRP.Chat.DamageSpecial": "armour counts half", diff --git a/ringbrp.mjs b/ringbrp.mjs index 92e7205..95a5910 100644 --- a/ringbrp.mjs +++ b/ringbrp.mjs @@ -5950,7 +5950,8 @@ function damageTargetFor(explicit = null) { * token and the blow lands somewhere. */ 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; const d = weapon.system?.damage ?? {}; let formula = d.formula || "1d6"; @@ -6004,6 +6005,21 @@ export async function rollWeaponDamage({ actor = null, weapon, level = "success" if (victim) { await hitLocationRoll({ target: victim, damage: roll.total, 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 { // 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. @@ -6490,8 +6506,11 @@ async function _onChatDamage(ev) { : game.actors.get(f.actorId); const weapon = actor?.items?.get(f.weaponId); if (!weapon) return ui.notifications?.warn(game.i18n.localize("RINGBRP.Notify.NoWeapon")); - btn.disabled = true; // one damage roll per hit - await rollWeaponDamage({ actor, weapon, level: f.level }); + // One damage roll per hit, whichever body plan was chosen. + 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 diff --git a/styles/ringbrp.css b/styles/ringbrp.css index c053c48..f683431 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -1324,6 +1324,7 @@ } .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 i { margin-right: 4px; } .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 */ diff --git a/templates/chat-location.hbs b/templates/chat-location.hbs index b79eabc..fe2d0c3 100644 --- a/templates/chat-location.hbs +++ b/templates/chat-location.hbs @@ -8,8 +8,9 @@
{{locationLabel}} {{damage}} - {{taken}} / {{max}} + {{#unless preview}}{{taken}} / {{max}}{{/unless}}
{{#if effect}}

{{effect}}

{{/if}} + {{#if preview}}

{{localize "RINGBRP.Loc.NoTargetPlaced"}}

{{/if}} {{#if majorWound}}

{{localize "RINGBRP.Wound.Header"}}

{{/if}} diff --git a/templates/chat-roll.hbs b/templates/chat-roll.hbs index 03c850d..c146c51 100644 --- a/templates/chat-roll.hbs +++ b/templates/chat-roll.hbs @@ -22,8 +22,15 @@ {{#if damage}}
- + {{#if damage.critical}}{{localize "RINGBRP.Chat.DamageCritical"}}{{/if}} {{#if damage.special}}{{localize "RINGBRP.Chat.DamageSpecial"}}{{/if}}