Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion module/_types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@
* @property {string} [exclusiveGroup] Any status effects with the same group will not be able to be applied at
* the same time through the token HUD (multiple statuses applied through
* other effects can still coexist).
* @property {number} [cover] The level of cover this condition provides.
* @property {number} [coverBonus] A bonus this condition provides to AC and dexterity saving throws.
* @property {boolean} [neverBlockMovement] If true, a token with this status will not block movement for other tokens.
*/
Expand Down Expand Up @@ -632,7 +633,8 @@
* @property {string} uuid The UUID of the target.
* @property {string} img The target's image.
* @property {string} name The target's name.
* @property {number} ac The target's armor class, if applicable.
* @property {number|null} ac The target's armor class, if applicable.
* @property {number} cover The target's level of cover.
*/

/* -------------------------------------------- */
Expand Down
5 changes: 4 additions & 1 deletion module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3836,20 +3836,23 @@ DND5E.statusEffects = {
img: "systems/dnd5e/icons/svg/statuses/cover-half.svg",
order: 2,
exclusiveGroup: "cover",
cover: 0.5,
coverBonus: 2
},
coverThreeQuarters: {
name: "EFFECT.DND5E.StatusThreeQuartersCover",
img: "systems/dnd5e/icons/svg/statuses/cover-three-quarters.svg",
order: 3,
exclusiveGroup: "cover",
cover: 0.75,
coverBonus: 5
},
coverTotal: {
name: "EFFECT.DND5E.StatusTotalCover",
img: "systems/dnd5e/icons/svg/statuses/cover-total.svg",
order: 4,
exclusiveGroup: "cover"
exclusiveGroup: "cover",
cover: 1
},
dead: {
name: "EFFECT.DND5E.StatusDead",
Expand Down
3 changes: 3 additions & 0 deletions module/dice/_types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@
* @property {boolean} [advantage] Does this roll potentially have advantage?
* @property {boolean} [disadvantage] Does this roll potentially have disadvantage?
* @property {D20Roll.ADV_MODE} [advantageMode] Final advantage mode.
* @property {boolean} [autoFailure] Force the roll to fail, including on a critical success.
* @property {number} [criticalSuccess] The value of the d20 die to be considered a critical success.
* @property {number} [criticalFailure] The value of the d20 die to be considered a critical failure.
* @property {boolean} [elvenAccuracy] Use three dice when rolling with advantage.
* @property {boolean} [halflingLucky] Add a re-roll once modifier to the d20 die.
* @property {boolean} [ignoreTotalCover=false] Ignore total cover when making an attack roll.
* @property {number} [maximum] Maximum number the d20 die can roll.
* @property {number} [minimum] Minimum number the d20 die can roll.
*/
Expand All @@ -76,6 +78,7 @@
* @typedef {D20RollProcessConfiguration} AttackRollProcessConfiguration
* @property {Item5e|boolean} [ammunition] Specific ammunition to consume, or `false` to prevent any ammo usage.
* @property {WeaponAttackMode} [attackMode] Mode to use for making the attack and rolling damage.
* @property {boolean} [ignoreTotalCover=false] Ignore total cover when making an attack roll.
* @property {string} [mastery] Weapon mastery option to use.
*/

Expand Down
19 changes: 19 additions & 0 deletions module/dice/d20-roll.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default class D20Roll extends BasicRoll {
config.options.criticalFailure ??= CONFIG.Dice.D20Die.CRITICAL_FAILURE_TOTAL;
config.options.elvenAccuracy ??= process.elvenAccuracy;
config.options.halflingLucky ??= process.halflingLucky;
config.options.ignoreTotalCover ??= process.ignoreTotalCover ?? false;
config.options.reliableTalent ??= process.reliableTalent;
config.options.target ??= process.target;
return new this(formula, config.data, config.options);
Expand Down Expand Up @@ -170,6 +171,24 @@ export default class D20Roll extends BasicRoll {

/* -------------------------------------------- */

/** @override */
get isFailure() {
if ( !this._evaluated ) return;
if ( this.options.autoFailure ) return true;
return super.isFailure;
}

/* -------------------------------------------- */

/** @override */
get isSuccess() {
if ( !this._evaluated ) return;
if ( this.options.autoFailure ) return false;
return super.isSuccess;
}

/* -------------------------------------------- */

/**
* Does this roll start with a d20?
* @type {boolean}
Expand Down
7 changes: 6 additions & 1 deletion module/documents/activity/attack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ export default class AttackActivity extends ActivityMixin(BaseAttackActivityData
}, message);

const rolls = await CONFIG.Dice.D20Roll.buildConfigure(rollConfig, dialogConfig, messageConfig);
if ( (targets.length === 1) && (targets[0].cover === CONFIG.DND5E.statusEffects.coverTotal?.cover) ) {
for ( const roll of rolls ) {
if ( !roll.options.ignoreTotalCover ) roll.options.autoFailure = true;
}
}
await CONFIG.Dice.D20Roll.buildEvaluate(rolls, rollConfig, messageConfig);
if ( !rolls.length ) return null;
for ( const key of ["ammunition", "attackMode", "mastery"] ) {
Expand Down Expand Up @@ -310,7 +315,7 @@ export default class AttackActivity extends ActivityMixin(BaseAttackActivityData
: actor.items.get(lastAttack.getFlag("dnd5e", "roll.ammunition"));
}

const isCritical = lastAttack?.rolls[0]?.isCritical;
const isCritical = lastAttack?.rolls[0]?.isCritical && !lastAttack.rolls[0].isFailure;
const dialogConfig = {};
if ( isCritical ) dialogConfig.options = { defaultButton: "critical" };

Expand Down
10 changes: 10 additions & 0 deletions module/documents/actor/actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ export default class Actor5e extends SystemDocumentMixin(Actor) {

/* -------------------------------------------- */

/**
* Determine the highest level of cover affecting this actor.
* @type {number}
*/
get coverLevel() {
return Math.max(0, ...Array.from(this.statuses, id => CONFIG.DND5E.statusEffects[id]?.cover ?? 0));
}

/* -------------------------------------------- */

/**
* Highest ability associated with a spellcasting class.
* @type {string}
Expand Down
9 changes: 6 additions & 3 deletions module/documents/chat-message.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export default class ChatMessage5e extends ChatMessage {
if ( d20Roll.isSuccess || forceSuccess ) total.classList.add("success");
else total.classList.add("failure");
}
if ( canCrit && d20Roll.isCritical ) total.classList.add("critical");
if ( canCrit && d20Roll.isCritical && !d20Roll.isFailure ) total.classList.add("critical");
if ( canCrit && d20Roll.isFumble && !forceSuccess ) total.classList.add("fumble");

const icons = document.createElement("div");
Expand Down Expand Up @@ -460,8 +460,11 @@ export default class ChatMessage5e extends ChatMessage {
</div>
`;
const evaluation = tray.querySelector("ul");
const rows = targets.map(({ name, ac, uuid }) => {
const isMiss = !attackRoll.isCritical && ((attackRoll.total < ac) || attackRoll.isFumble);
const rows = targets.map(({ name, ac, cover=0, uuid }) => {
const totalCover = (cover === CONFIG.DND5E.statusEffects.coverTotal?.cover) && !attackRoll.options.ignoreTotalCover;
const isMiss = totalCover || (!attackRoll.isCritical && (attackRoll.isFumble
|| (Number.isNumeric(ac) && (attackRoll.total < ac))));
if ( totalCover ) ac = null;
if ( !game.user.isGM && (visibility !== "all") ) ac = "";
const li = document.createElement("li");
Object.assign(li.dataset, { uuid, miss: isMiss });
Expand Down
8 changes: 3 additions & 5 deletions module/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -765,11 +765,9 @@ export function getTargetDescriptors(tokens=game.user.targets) {
const targets = new Map();
for ( const token of tokens ) {
const { name } = token;
const { img, system, uuid, statuses } = token.actor ?? {};
if ( uuid ) {
const ac = statuses.has("coverTotal") ? null : system.attributes?.ac?.value;
targets.set(uuid, { name, img, uuid, ac: ac ?? null });
}
const actor = token.actor;
const { img, system, uuid } = actor ?? {};
if ( uuid ) targets.set(uuid, { name, img, uuid, ac: system.attributes?.ac?.value ?? null, cover: actor.coverLevel ?? 0 });
}
return Array.from(targets.values());
}
Expand Down