Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More advantage mode coverage #4890

Merged
merged 3 commits into from
Jan 2, 2025
Merged
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
127 changes: 99 additions & 28 deletions module/data/fields/advantage-mode-field.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
/**
* @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.
*/

/**
* @typedef AdvantageModeCounts
* @property {number} count The number of applications of this mode.
* @property {boolean} [suppressed] Whether this mode is suppressed.
*/

/**
* Subclass of NumberField that tracks the number of changes made to a roll mode.
*/
export default class AdvantageModeField extends foundry.data.fields.NumberField {
/** @inheritDoc */
static get _defaults() {
return foundry.utils.mergeObject(super._defaults, {
choices: [-1, 0, 1],
choices: AdvantageModeField.#values,
initial: 0,
label: "DND5E.AdvantageMode"
});
Expand All @@ -14,46 +27,104 @@ export default class AdvantageModeField extends foundry.data.fields.NumberField
/* -------------------------------------------- */

/**
* Number of advantage modifications.
* @type {number}
* Allowed advantage mode values.
* @type {number[]}
*/
#advantage;
static #values = [-1, 0, 1];

/* -------------------------------------------- */
/* Active Effect Integration */
/* -------------------------------------------- */

/**
* Number of disadvantage modifications.
* @type {number}
*/
#disadvantage;
/** @override */
_applyChangeAdd(value, delta, model, change) {
// Add a source of advantage or disadvantage.
if ( (delta !== -1) && (delta !== 1) ) return value;
const counts = this.constructor.getCounts(model, change);
if ( delta === 1 ) counts.advantages.count++;
else counts.disadvantages.count++;
return this.constructor.resolveMode(model, change, counts);
}

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

/** @inheritDoc */
initialize(value, model, options={}) {
this.#advantage = Number(value === 1);
this.#disadvantage = Number(value === -1);
return value;
/** @override */
_applyChangeDowngrade(value, delta, model, change) {
// Downgrade the roll so that it can no longer benefit from advantage.
if ( (delta !== -1) && (delta !== 0) ) return value;
const counts = this.constructor.getCounts(model, change);
counts.advantages.suppressed = true;
if ( delta === -1 ) counts.disadvantages.count++;
return this.constructor.resolveMode(model, change, counts);
}

/* -------------------------------------------- */
/* Active Effect Integration */

/** @override */
_applyChangeMultiply(value, delta, model, change) {
return value;
}

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

/** @override */
applyChange(value, model, change) {
const delta = this._castChangeDelta(change.value);
if ( change.mode === CONST.ACTIVE_EFFECT_MODES.CUSTOM ) {
return this._applyChangeCustom(value, delta, model, change);
_applyChangeOverride(value, delta, model, change) {
// Force a given roll mode.
if ( (delta === -1) || (delta === 0) || (delta === 1) ) {
this.constructor.getCounts(model, change).override = delta;
return delta;
}
switch (delta) {
case 1:
this.#advantage++;
break;
case -1:
this.#disadvantage++;
break;
}
return Math.sign(this.#advantage) - Math.sign(this.#disadvantage);
return value;
}

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

/** @override */
_applyChangeUpgrade(value, delta, model, change) {
// Upgrade the roll so that it can no longer be penalised by disadvantage.
if ( (delta !== 1) && (delta !== 0) ) return value;
const counts = this.constructor.getCounts(model, change);
counts.disadvantages.suppressed = true;
if ( delta === 1 ) counts.advantages.count++;
return this.constructor.resolveMode(model, change, counts);
}

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

/**
* Retrieve the advantage/disadvantage counts from the model.
* @param {DataModel} model The model the change is applied to.
* @param {EffectChangeData} change The change to apply.
* @returns {AdvantageModeData}
*/
static getCounts(model, change) {
const parentKey = change.key.substring(0, change.key.lastIndexOf("."));
const roll = foundry.utils.getProperty(model, parentKey) ?? {};
return roll.modeCounts ??= {
override: null,
advantages: { count: 0, suppressed: false },
disadvantages: { count: 0, suppressed: false }
};
}

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

/**
* Resolve multiple sources of advantage and disadvantage into a single roll mode per the game rules.
* @param {DataModel} model The model the change is applied to.
* @param {EffectChangeData} change The change to applied.
* @param {AdvantageModeData} [counts] The current advantage/disadvantage counts.
* @returns {number} An integer in the interval [-1, 1], indicating advantage (1),
* disadvantage (-1), or neither (0).
*/
static resolveMode(model, change, counts) {
const { override, advantages, disadvantages } = counts ?? this.getCounts(model, change);
if ( override !== null ) return override;
const src = foundry.utils.getProperty(model._source, change.key) ?? 0;
const advantageCount = advantages.suppressed ? 0 : advantages.count + Number(src === 1);
const disadvantageCount = disadvantages.suppressed ? 0 : disadvantages.count + Number(src === -1);
return Math.sign(advantageCount) - Math.sign(disadvantageCount);
}
}