diff --git a/lang/en.json b/lang/en.json index 39f808e17c..b61a261d97 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2119,6 +2119,13 @@ "SETTINGS.5eAutoCollapseCardN": "Collapse Item Cards in Chat", "SETTINGS.5eAutoSpellTemplateL": "When a spell is cast, defaults to begin the process to create the corresponding Measured Template if any (requires TRUSTED or higher player role)", "SETTINGS.5eAutoSpellTemplateN": "Always place Spell Template", +"SETTINGS.5eAttackRollVisibility": { + "Name": "Attack Result Visibility", + "Hint": "Control visibility of attack roll results in chat cards for players.", + "All": "Show results & target ACs", + "HideAC": "Show only results", + "None": "Hide all" +}, "SETTINGS.5eChallengeVisibility": { "Name": "Challenge Visibility", "Hint": "Control what roll DCs are visible to the players and whether successes/failures are highlighted.", diff --git a/module/documents/chat-message.mjs b/module/documents/chat-message.mjs index e17b27b554..93036ad32a 100644 --- a/module/documents/chat-message.mjs +++ b/module/documents/chat-message.mjs @@ -172,6 +172,7 @@ export default class ChatMessage5e extends ChatMessage { if ( !this.isContentVisible || !this.rolls.length ) return; const originatingMessage = game.messages.get(this.getFlag("dnd5e", "originatingMessage")) ?? this; const displayChallenge = originatingMessage?.shouldDisplayChallenge; + const displayAttackResult = game.user.isGM || (game.settings.get("dnd5e", "attackRollVisibility") !== "none"); /** * Create an icon to indicate success or failure. @@ -202,7 +203,9 @@ export default class ChatMessage5e extends ChatMessage { if ( !total ) continue; // Only attack rolls and death saves can crit or fumble. const canCrit = ["attack", "death"].includes(this.getFlag("dnd5e", "roll.type")); - if ( d.options.target && displayChallenge ) { + const isAttack = this.getFlag("dnd5e", "roll.type") === "attack"; + const showResult = isAttack ? displayAttackResult : displayChallenge; + if ( d.options.target && showResult ) { if ( d20Roll.total >= d.options.target ) total.classList.add("success"); else total.classList.add("failure"); } @@ -359,7 +362,9 @@ export default class ChatMessage5e extends ChatMessage { _enrichAttackTargets(html) { const attackRoll = this.rolls[0]; const targets = this.getFlag("dnd5e", "targets"); - if ( !game.user.isGM || !(attackRoll instanceof dnd5e.dice.D20Roll) || !targets?.length ) return; + const visibility = game.settings.get("dnd5e", "attackRollVisibility"); + const isVisible = game.user.isGM || (visibility !== "none"); + if ( !isVisible || !(attackRoll instanceof dnd5e.dice.D20Roll) || !targets?.length ) return; const tray = document.createElement("div"); tray.classList.add("dnd5e2"); tray.innerHTML = ` @@ -376,15 +381,18 @@ export default class ChatMessage5e extends ChatMessage { `; const evaluation = tray.querySelector("ul"); evaluation.innerHTML = targets.map(({ name, ac, uuid }) => { + if ( !game.user.isGM && (visibility !== "all") ) ac = ""; const isMiss = !attackRoll.isCritical && ((attackRoll.total < ac) || attackRoll.isFumble); return [`
  • ${name}
    + ${ac ? `
    ${ac}
    + ` : ""}
  • `, isMiss]; }).sort((a, b) => (a[1] === b[1]) ? 0 : a[1] ? 1 : -1).reduce((str, [li]) => str + li, ""); diff --git a/module/settings.mjs b/module/settings.mjs index 8466795ccb..13428a1c9e 100644 --- a/module/settings.mjs +++ b/module/settings.mjs @@ -28,6 +28,20 @@ export function registerSystemSettings() { } }); + game.settings.register("dnd5e", "attackRollVisibility", { + name: "SETTINGS.5eAttackRollVisibility.Name", + hint: "SETTINGS.5eAttackRollVisibility.Hint", + scope: "world", + config: true, + default: "none", + type: String, + choices: { + all: "SETTINGS.5eAttackRollVisibility.All", + hideAC: "SETTINGS.5eAttackRollVisibility.HideAC", + none: "SETTINGS.5eAttackRollVisibility.None" + } + }); + // Encumbrance tracking game.settings.register("dnd5e", "encumbrance", { name: "SETTINGS.5eEncumbrance.Name",