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
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@
"DND5E.AdvancementTitle": "Advancement",
"DND5E.Advantage": "Advantage",
"DND5E.AdvantageMode": "Advantage Mode",
"DND5E.AdvantageModeManual": "Manual Configuration",
"DND5E.Age": "Age",
"DND5E.Alignment": "Alignment",
"DND5E.AlignmentCE": "Chaotic Evil",
Expand Down
15 changes: 10 additions & 5 deletions module/data/actor/templates/attributes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export default class AttributesFields {

// Set stealth disadvantage
if ( armors[0]?.system.properties.has("stealthDisadvantage") && this.skills ) {
AdvantageModeField.setMode(this, "skills.ste.roll.mode", -1);
AdvantageModeField.setMode(this, "skills.ste.roll.mode", -1, { source: { label: armors[0].name } });
}

ac.label = !["custom", "flat"].includes(ac.calc) ? CONFIG.DND5E.armorClasses[ac.calc]?.label : null;
Expand Down Expand Up @@ -389,11 +389,16 @@ export default class AttributesFields {
init.prof = new Proficiency(prof, alert ? 1 : (joat || ra) ? 0.5 : 0, !ra);

// Adjust rolling mode
if ( (flags.remarkableAthlete && !isLegacy) || this.parent.hasConditionEffect("initiativeAdvantage") ) {
AdvantageModeField.setMode(this, "attributes.init.roll.mode", 1);
if ( flags.remarkableAthlete && !isLegacy ) {
AdvantageModeField.setMode(this, "attributes.init.roll.mode", 1, { source: { label: _loc("DND5E.FlagsRemarkableAthlete") } });
}
if ( this.parent.hasConditionEffect("initiativeDisadvantage") ) {
AdvantageModeField.setMode(this, "attributes.init.roll.mode", -1);
const initiativeAdvantage = this.parent.hasConditionEffect("initiativeAdvantage", { label: true });
if ( initiativeAdvantage ) {
AdvantageModeField.setMode(this, "attributes.init.roll.mode", 1, { source: { label: initiativeAdvantage } });
}
const initiativeDisadvantage = this.parent.hasConditionEffect("initiativeDisadvantage", { label: true });
if ( initiativeDisadvantage ) {
AdvantageModeField.setMode(this, "attributes.init.roll.mode", -1, { source: { label: initiativeDisadvantage } });
}

// Total initiative includes all numeric terms
Expand Down
21 changes: 12 additions & 9 deletions module/data/actor/templates/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,20 @@ export default class CommonTemplate extends ActorDataModel.mixin(CurrencyTemplat

// Adjust rolling mode
const isPhysicalAbility = CONFIG.DND5E.abilities[id]?.type === "physical";
if ( this.parent.hasConditionEffect("abilityCheckDisadvantage")
|| (isPhysicalAbility && this.parent.hasConditionEffect("physicalCheckDisadvantage")) ) {
AdvantageModeField.setMode(this, `abilities.${id}.check.roll.mode`, -1);
const checkDisadvantage = this.parent.hasConditionEffect("abilityCheckDisadvantage", { label: true })
|| (isPhysicalAbility && this.parent.hasConditionEffect("physicalCheckDisadvantage", { label: true }));
if ( checkDisadvantage ) {
AdvantageModeField.setMode(this, `abilities.${id}.check.roll.mode`, -1, { source: { label: checkDisadvantage } });
}
if ( (id === "dex") && this.parent.hasConditionEffect("dexteritySaveAdvantage") ) {
AdvantageModeField.setMode(this, `abilities.${id}.save.roll.mode`, 1);
const saveAdvantage = (id === "dex") && this.parent.hasConditionEffect("dexteritySaveAdvantage", { label: true });
if ( saveAdvantage ) {
AdvantageModeField.setMode(this, `abilities.${id}.save.roll.mode`, 1, { source: { label: saveAdvantage } });
}
if ( this.parent.hasConditionEffect("abilitySaveDisadvantage")
|| (isPhysicalAbility && this.parent.hasConditionEffect("physicalSaveDisadvantage"))
|| ((id === "dex") && this.parent.hasConditionEffect("dexteritySaveDisadvantage")) ) {
AdvantageModeField.setMode(this, `abilities.${id}.save.roll.mode`, -1);
const saveDisadvantage = this.parent.hasConditionEffect("abilitySaveDisadvantage", { label: true })
|| (isPhysicalAbility && this.parent.hasConditionEffect("physicalSaveDisadvantage", { label: true }))
|| ((id === "dex") && this.parent.hasConditionEffect("dexteritySaveDisadvantage", { label: true }));
if ( saveDisadvantage ) {
AdvantageModeField.setMode(this, `abilities.${id}.save.roll.mode`, -1, { source: { label: saveDisadvantage } });
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion module/data/actor/templates/creature.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export default class CreatureTemplate extends CommonTemplate {
const isLegacy = game.settings.get("dnd5e", "rulesVersion") === "legacy";
if ( flags.remarkableAthlete
&& CONFIG.DND5E.characterFlags.remarkableAthlete.skills.includes(skillId) && !isLegacy ) {
AdvantageModeField.setMode(this, `skills.${skillId}.roll.mode`, 1);
AdvantageModeField.setMode(this, `skills.${skillId}.roll.mode`, 1, { source: { label: _loc("DND5E.FlagsRemarkableAthlete") } });
}

// Compute passive bonus
Expand Down
18 changes: 13 additions & 5 deletions module/data/fields/_types.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/**
* @typedef AdvantageModeData
* @property {number|null} override Whether the mode has been entirely overridden.
* @property {AdvantageModeCounts} advantages The advantage counts.
* @property {AdvantageModeCounts} disadvantages The disadvantage counts.
* @property {number|null} override Whether the mode has been entirely overridden.
* @property {AdvantageModeSource|null} [overrideSource] Source responsible for the override.
* @property {AdvantageModeCounts} advantages The advantage counts.
* @property {AdvantageModeCounts} disadvantages The disadvantage counts.
*/

/**
* @typedef AdvantageModeCounts
* @property {number} count The number of applications of this mode.
* @property {boolean} [suppressed] Whether this mode is suppressed.
* @property {number} count The number of applications of this mode.
* @property {boolean} [suppressed] Whether this mode is suppressed.
* @property {AdvantageModeSource[]} [sources] Sources responsible for each count.
*/

/**
* @typedef AdvantageModeSource
* @property {string} [label] A pre-localized label for this source.
* @property {ActiveEffect5e} [effect] Active Effect responsible for this change.
*/

/**
Expand Down
63 changes: 51 additions & 12 deletions module/data/fields/advantage-mode-field.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @import { AdvantageModeData } from "./_types.mjs";
* @import { AdvantageModeData, AdvantageModeSource } from "./_types.mjs";
*/

/**
Expand Down Expand Up @@ -32,8 +32,14 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
// Add a source of advantage or disadvantage.
if ( (delta !== -1) && (delta !== 1) ) return value;
const counts = this.constructor.getCounts(model, change.key);
if ( delta === 1 ) counts.advantages.count++;
else counts.disadvantages.count++;
const source = change.source ?? (change.effect ? { effect: change.effect } : null);
if ( delta === 1 ) {
counts.advantages.count++;
if ( source ) counts.advantages.sources.push(source);
} else {
counts.disadvantages.count++;
if ( source ) counts.disadvantages.sources.push(source);
}
return this.constructor.resolveMode(model, change, counts);
}

Expand All @@ -45,7 +51,11 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
if ( (delta !== -1) && (delta !== 0) ) return value;
const counts = this.constructor.getCounts(model, change.key);
counts.advantages.suppressed = true;
if ( delta === -1 ) counts.disadvantages.count++;
if ( delta === -1 ) {
counts.disadvantages.count++;
const source = change.source ?? (change.effect ? { effect: change.effect } : null);
if ( source ) counts.disadvantages.sources.push(source);
}
return this.constructor.resolveMode(model, change, counts);
}

Expand All @@ -62,7 +72,9 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
_applyChangeOverride(value, delta, model, change) {
// Force a given roll mode.
if ( (delta === -1) || (delta === 0) || (delta === 1) ) {
this.constructor.getCounts(model, change.key).override = delta;
const counts = this.constructor.getCounts(model, change.key);
counts.override = delta;
counts.overrideSource = change.source ?? (change.effect ? { effect: change.effect } : null);
return delta;
}
return value;
Expand All @@ -76,14 +88,39 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
if ( (delta !== 1) && (delta !== 0) ) return value;
const counts = this.constructor.getCounts(model, change);
counts.disadvantages.suppressed = true;
if ( delta === 1 ) counts.advantages.count++;
if ( delta === 1 ) {
counts.advantages.count++;
const source = change.source ?? (change.effect ? { effect: change.effect } : null);
if ( source ) counts.advantages.sources.push(source);
}
return this.constructor.resolveMode(model, change, counts);
}

/* -------------------------------------------- */
/* Helpers */
/* -------------------------------------------- */

/**
* Collect attribution sources from one or more roll mode fields.
* @param {DataModel} model The model containing the fields.
* @param {string[]} keyPaths Paths to the individual fields to collect from.
* @returns {{value: number, source: AdvantageModeSource|null}[]}
*/
static collectSources(model, keyPaths) {
const result = [];
for ( const keyPath of keyPaths ) {
const counts = this.getCounts(model, keyPath);
const src = foundry.utils.getProperty(model._source, keyPath) ?? 0;
if ( src ) result.push({ value: src, source: null });
if ( counts.override !== null ) result.push({ value: counts.override, source: counts.overrideSource });
for ( const source of counts.advantages.sources ) result.push({ value: 1, source });
for ( const source of counts.disadvantages.sources ) result.push({ value: -1, source });
}
return result;
}

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

/**
* Retrieve the counts from several advantage mode fields and determine the final advantage mode.
* @param {DataModel} model The model containing the fields.
Expand Down Expand Up @@ -129,8 +166,9 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
const roll = foundry.utils.getProperty(model, parentKey) ?? {};
return roll.modeCounts ??= {
override: null,
advantages: { count: 0, suppressed: false },
disadvantages: { count: 0, suppressed: false }
overrideSource: null,
advantages: { count: 0, suppressed: false, sources: [] },
disadvantages: { count: 0, suppressed: false, sources: [] }
};
}

Expand Down Expand Up @@ -163,18 +201,19 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
* @param {number} value An integer in the interval [-1, 1], indicating advantage (1),
* disadvantage (-1), or neither (0).
* @param {object} [options={}]
* @param {boolean} [options.override=false] Override the mode rather than following the normal advantage rules.
* @returns {number} Final advantage value.
* @param {boolean} [options.override=false] Override the mode rather than following the normal advantage rules.
* @param {AdvantageModeSource} [options.source] Source responsible for this change, kept for attribution.
* @returns {number} Final advantage value.
*/
static setMode(model, keyPath, value, { override=false }={}) {
static setMode(model, keyPath, value, { override=false, source=null }={}) {
const field = keyPath.startsWith("system.") ? model.system.schema.getField(keyPath.slice(7))
: model.schema.getField(keyPath);
if ( !field ) {
console.error(`No field found at "${keyPath}" to apply advantage to.`);
return 0;
}
const type = override ? "override" : "add";
const change = { key: keyPath, value, type };
const change = { key: keyPath, value, type, source };
const final = field.applyChange(foundry.utils.getProperty(model, keyPath), model, change);
foundry.utils.setProperty(model, keyPath, final);
return final;
Expand Down
38 changes: 34 additions & 4 deletions module/documents/actor/actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,12 @@ export default class Actor5e extends SystemDocumentMixin(Actor) {

/**
* Is this actor under the effect of this property from some status or due to its level of exhaustion?
* @param {string} key A key in `DND5E.conditionEffects`.
* @returns {boolean} Whether the actor is affected.
* @param {string} key A key in `DND5E.conditionEffects`.
* @param {object} [options={}]
* @param {boolean} [options.label=false] Return the responsible status label rather than a boolean.
* @returns {boolean|string} Whether the actor is affected, or the label of the responsible status.
*/
hasConditionEffect(key) {
hasConditionEffect(key, { label=false }={}) {
const props = CONFIG.DND5E.conditionEffects[key] ?? new Set();
const level = this.system.attributes?.exhaustion ?? null;
const imms = this.system.traits?.ci?.value ?? new Set();
Expand All @@ -547,10 +549,15 @@ export default class Actor5e extends SystemDocumentMixin(Actor) {
};
const applyDodging = !statuses.has("incapacitated")
&& !(CONFIG.DND5E.conditionEffects.noMovement?.some(isActiveSource) ?? false);
return props.some(k => {
const match = props.find(k => {
if ( (k === "dodging") && !applyDodging ) return false;
return isActiveSource(k);
});
if ( !label ) return !!match;
if ( !match ) return null;
const [, n] = match.match(/^exhaustion-(\d+)$/) ?? [];
return n ? _loc("DND5E.ExhaustionLevel", { n: Number(n) })
: _loc(CONFIG.DND5E.conditionTypes[match]?.name ?? match);
}

/* -------------------------------------------- */
Expand Down Expand Up @@ -2746,6 +2753,29 @@ export default class Actor5e extends SystemDocumentMixin(Actor) {
return attributions;
}

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

/**
* Break down the sources of advantage and disadvantage for one or more roll mode fields.
* @param {string|string[]} keyPaths Path(s) to the roll mode field(s).
* @returns {AttributionDescription[]} Individual contributions to this roll's mode.
* @protected
*/
_prepareRollModeAttributions(keyPaths) {
keyPaths = Array.isArray(keyPaths) ? keyPaths : [keyPaths];
const unknown = _loc("COMMON.Unknown");
return AdvantageModeField.collectSources(this.system, keyPaths).map(({ value, source }) => {
if ( !source ) return { value, label: _loc("DND5E.AdvantageModeManual"), document: null };
if ( source.effect ) {
let label = source.effect.sourceName;
if ( !source.effect.origin || (source.effect.origin === this.uuid)
|| (label === unknown) ) label = source.effect.name;
return { value, label, document: source.effect };
}
return { value, label: source.label, document: null };
});
}

/* -------------------------------------------- */
/* Conversion & Transformation */
/* -------------------------------------------- */
Expand Down