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
91 changes: 46 additions & 45 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,48 +288,6 @@
}
},

"DND5E.ACTIVEEFFECT": {
"AttributeKeyTooltip": "For a list of common keys and their accepted values, see <a href=\"{url}\">the wiki</a>.",
"ChangeType": {
"Advantage": {
"Label": "Advantage Mode"
},
"Bonus": {
"Label": "Bonus"
},
"Group": {
"Custom": "Custom",
"Rules": "Rules",
"Standard": "Standard"
},
"Maximum": {
"Label": "Roll Maximum"
},
"Minimum": {
"Label": "Roll Minimum"
}
},
"Expired": "Expired",
"Expiry": {
"GROUPS": {
"General": "General",
"Specific": "Specific"
},
"SourceStart": "Start of Source's Next Turn",
"SourceEnd": "End of Source's Next Turn",
"TargetStart": "Start of Target's Next Turn",
"TargetEnd": "End of Target's Next Turn",
"YourStart": "Start of Your Next Turn",
"YourEnd": "End of Your Next Turn"
},
"FIELDS": {
"statuses": {
"label": "Status Conditions",
"hint": "While affected by this Active Effect, the target will be treated as having these additional status conditions."
}
}
},

"DND5E.ACTIVITY": {
"Action": {
"Create": "Create Activity",
Expand Down Expand Up @@ -2582,6 +2540,10 @@
"label": "Concentration",
"hint": "Creature must maintain concentration while active."
},
"expiry": {
"label": "Expiry Event",
"hint": "Event that triggers the end of the usage."
},
"override": {
"label": "Override Duration",
"hint": "Use these duration values instead of the item's when using this activity."
Expand Down Expand Up @@ -2637,6 +2599,7 @@
"Ownership": "Effects cannot be applied to tokens you are not the owner of."
}
},
"AttributeKeyTooltip": "For a list of common keys and their accepted values, see <a href=\"{url}\">the wiki</a>.",
"BASE": {
"FIELDS": {
"changes": {
Expand Down Expand Up @@ -2681,14 +2644,52 @@
},
"Change": {
"Configuration": "Change Configuration",
"Group": {
"Custom": "Custom",
"Rules": "Rules",
"Standard": "Standard"
},
"Label": "Change",
"Title": "{effect} Change #{number}"
"Title": "{effect} Change #{number}",
"Type": {
"Advantage": {
"Label": "Advantage Mode"
},
"Bonus": {
"Label": "Bonus"
},
"Maximum": {
"Label": "Roll Maximum"
},
"Minimum": {
"Label": "Roll Minimum"
}
}
},
"DropHint": "Drop an Active Effect here",
"Empty": "No associated effects. Use the <i class=\"fas fa-plus\"></i> button above to create one, or select an existing effect from the drop-down.",
"Expired": "Expired",
"Expiry": {
"LongRest": "End of Long Rest",
"ShortRest": "End of Short Rest"
"Group": {
"General": "General",
"Specific": "Specific"
},
"Type": {
"LongRest": "End of Long Rest",
"ShortRest": "End of Short Rest",
"SourceEnd": "End of Source's Next Turn",
"SourceStart": "Start of Source's Next Turn",
"TargetEnd": "End of Target's Next Turn",
"TargetStart": "Start of Target's Next Turn",
"YourEnd": "End of Your Next Turn",
"YourStart": "Start of Your Next Turn"
}
},
"FIELDS": {
"statuses": {
"label": "Status Conditions",
"hint": "While affected by this Active Effect, the target will be treated as having these additional status conditions."
}
},
"Label": "Available Effects",
"New": "New Effect",
Expand Down
15 changes: 3 additions & 12 deletions module/applications/active-effect/active-effect-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,9 @@ export default class ActiveEffectSheet5e extends ApplicationV2Mixin(ActiveEffect
* @protected
*/
async _prepareDurationContext(context, options) {
const general = _loc("DND5E.ACTIVEEFFECT.Expiry.GROUPS.General");
const specific = _loc("DND5E.ACTIVEEFFECT.Expiry.GROUPS.Specific");

for ( const [expiry, label] of Object.entries(context.expiryEvents) ) {
context.expiryEvents[expiry] = { group: general, label };
}
for ( const expiry of dnd5e.documents.ActiveEffect5e.PSEUDO_EXPIRIES ) {
context.expiryEvents[expiry] = {
group: specific,
label: _loc(`DND5E.ACTIVEEFFECT.Expiry.${expiry.capitalize()}`)
};
}
context.expiryEvents = Object.fromEntries(
ActiveEffect.implementation.expiryOptions.map(({ value, ...data }) => [value, data])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be alphabetised?

);
return context;
}

Expand Down
4 changes: 2 additions & 2 deletions module/applications/active-effect/effect-change-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default class EffectChangeConfig extends DocumentSheet5e {
context.defaultPriority = ActiveEffect.CHANGE_TYPES[context.source?.type]?.defaultPriority;
context.fields = this.effect.system.schema.fields.changes.element.fields;

context.hintText = _loc("DND5E.ACTIVEEFFECT.AttributeKeyTooltip", {
context.hintText = _loc("DND5E.EFFECT.AttributeKeyTooltip", {
url: this.effect.type === "enchantment"
? "https://github.com/foundryvtt/dnd5e/wiki/Enchantment"
: "https://github.com/foundryvtt/dnd5e/wiki/Active-Effect-Guide"
Expand All @@ -119,7 +119,7 @@ export default class EffectChangeConfig extends DocumentSheet5e {
context.typeOptions = Object.entries(ActiveEffect.CHANGE_TYPES)
.map(([value, { group, label }]) => ({ value, label: _loc(label), group: _loc(
CONFIG.ActiveEffect.changeTypes[value]?.group
?? `DND5E.ACTIVEEFFECT.ChangeType.Group.${value in CONST.ACTIVE_EFFECT_CHANGE_TYPES ? "Standard" : "Custom"}`
?? `DND5E.EFFECT.Change.Group.${value in CONST.ACTIVE_EFFECT_CHANGE_TYPES ? "Standard" : "Custom"}`
) }))
.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));

Expand Down
1 change: 1 addition & 0 deletions module/applications/activity/activity-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export default class ActivitySheet extends PseudoDocumentSheet {
value, label, group: _loc("DND5E.RangeDistance")
}))
];
context.expiryOptions = ActiveEffect.implementation.expiryOptions;

// Consumption targets
const canScale = this.activity.canConfigureScaling;
Expand Down
21 changes: 7 additions & 14 deletions module/applications/components/effect-application.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,33 +195,27 @@ export default class EffectApplicationElement extends TargetedApplicationMixin(C
throw new Error(_loc("DND5E.EFFECT.Application.Warning.Ownership"));
}

const effectFlags = {
// Get any effect changes provided by the activity & add flags
const changes = this.chatMessage.getAssociatedActivity({ scaled: true })
?.getAppliedEffectChanges(effect, { chatMessage: this.chatMessage, target: actor }) ?? {};
foundry.utils.mergeObject(changes, {
flags: {
dnd5e: {
dependentOn: concentration?.uuid,
scaling: this.chatMessage.system.scaling,
spellLevel: this.chatMessage.system.spellLevel
}
}
};

// Inherit the activity's duration only when the applied effect has a duration expiry and no explicit duration of
// its own.
let durationOverride = {};
if ( !Number.isFinite(effect.duration.value) && effect.expirySupportsDuration() ) {
const effectDuration = this.chatMessage.getAssociatedActivity({ scaled: true })?.duration.getEffectData();
if ( !foundry.utils.isEmpty(effectDuration) ) durationOverride = { duration: effectDuration };
}
});

// Enable an existing effect on the target if it originated from this effect
const existingEffect = effect.inCompendium ? actor.effects.find(e => e._stats.compendiumSource === effect.uuid)
: actor.effects.find(e => e.origin === origin.uuid);
if ( existingEffect ) {
return existingEffect.update(foundry.utils.mergeObject({
...durationOverride,
disabled: false,
start: effect.constructor.getEffectStart()
}, effectFlags));
}, changes));
}

if ( !game.user.isGM && concentration && !concentration.isOwner ) {
Expand All @@ -231,15 +225,14 @@ export default class EffectApplicationElement extends TargetedApplicationMixin(C
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...effect.toObject(),
...durationOverride,
disabled: false,
transfer: false,
origin: origin.uuid,
_stats: {
[effect.inCompendium ? "compendiumSource" : "duplicateSource"]: effect.uuid,
[effect.inCompendium ? "duplicateSource" : "compendiumSource"]: null
}
}, effectFlags);
}, changes);
return await ActiveEffect.implementation.create(effectData, { parent: actor });
}

Expand Down
35 changes: 25 additions & 10 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3924,31 +3924,31 @@ DND5E.bloodied = {
*/
DND5E.activeEffectChangeTypes = Object.freeze({
"dnd5e.advantage": {
label: "DND5E.ACTIVEEFFECT.ChangeType.Advantage.Label",
label: "DND5E.EFFECT.Change.Type.Advantage.Label",
defaultPriority: 100,
handler: ActiveEffect5e._applyChangeRule,
group: "DND5E.ACTIVEEFFECT.ChangeType.Group.Rules",
group: "DND5E.EFFECT.Change.Group.Rules",
skipConditions: true
},
"dnd5e.bonus": {
label: "DND5E.ACTIVEEFFECT.ChangeType.Bonus.Label",
label: "DND5E.EFFECT.Change.Type.Bonus.Label",
defaultPriority: 100,
handler: ActiveEffect5e._applyChangeRule,
group: "DND5E.ACTIVEEFFECT.ChangeType.Group.Rules",
group: "DND5E.EFFECT.Change.Group.Rules",
skipConditions: true
},
"dnd5e.maximum": {
label: "DND5E.ACTIVEEFFECT.ChangeType.Maximum.Label",
label: "DND5E.EFFECT.Change.Type.Maximum.Label",
defaultPriority: 100,
handler: ActiveEffect5e._applyChangeRule,
group: "DND5E.ACTIVEEFFECT.ChangeType.Group.Rules",
group: "DND5E.EFFECT.Change.Group.Rules",
skipConditions: true
},
"dnd5e.minimum": {
label: "DND5E.ACTIVEEFFECT.ChangeType.Minimum.Label",
label: "DND5E.EFFECT.Change.Type.Minimum.Label",
defaultPriority: 100,
handler: ActiveEffect5e._applyChangeRule,
group: "DND5E.ACTIVEEFFECT.ChangeType.Group.Rules",
group: "DND5E.EFFECT.Change.Group.Rules",
skipConditions: true
}
});
Expand Down Expand Up @@ -4723,10 +4723,25 @@ DND5E.calendarDeltasRecoveryMapping = new Map([
* @enum {string}
*/
DND5E.expiryEvents = Object.seal({
longRest: "DND5E.EFFECT.Expiry.LongRest",
shortRest: "DND5E.EFFECT.Expiry.ShortRest"
longRest: "DND5E.EFFECT.Expiry.Type.LongRest",
shortRest: "DND5E.EFFECT.Expiry.Type.ShortRest"
});

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

/**
* System-specific "expiry" choices which do not require registration or custom expiry events, and instead
* are handled dynamically in `ActiveEffect#isExpiryEvent`.
* @type {Record<string>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @type {Record<string>}
* @type {Record<string, string>}

*/
DND5E.pseudoExpiryEvents = Object.seal({
sourceStart: "DND5E.EFFECT.Expiry.Type.SourceStart",
sourceEnd: "DND5E.EFFECT.Expiry.Type.SourceEnd",
targetStart: "DND5E.EFFECT.Expiry.Type.TargetStart",
targetEnd: "DND5E.EFFECT.Expiry.Type.TargetEnd"
});
preLocalize("pseudoExpiryEvents");

/* -------------------------------------------- */
/* Requests */
/* -------------------------------------------- */
Expand Down
1 change: 1 addition & 0 deletions module/data/item/spell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ export default class SpellData extends ItemDataModel.mixin(ActivitiesTemplate, I
return { value, label, group: "DND5E.DurationPermanent" };
})
];
context.expiryOptions = ActiveEffect.implementation.expiryOptions;

// Targets
context.targetTypes = [
Expand Down
1 change: 1 addition & 0 deletions module/data/shared/_types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* @typedef DurationData
* @property {string} value Scalar value for the activity's duration.
* @property {string} units Units that are used for the duration.
* @property {string} expiry Active effect expiry event.
* @property {string} special Description of any special duration details.
*/

Expand Down
27 changes: 15 additions & 12 deletions module/data/shared/duration-field.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class DurationField extends SchemaField {
fields = {
value: new FormulaField({ deterministic: true }),
units: new StringField({ required: true, blank: false, initial: "inst" }),
expiry: new StringField(),
special: new StringField(),
...fields
};
Expand Down Expand Up @@ -56,21 +57,23 @@ export default class DurationField extends SchemaField {
/**
* Create duration data usable for an active effect based on this duration.
* @this {DurationData}
* @returns {EffectDurationData}
* @returns {Partial<EffectDurationData>}
*/
static getEffectDuration() {
if ( !Number.isNumeric(this.value) ) return {};
const { value, units } = this;
let { expiry, value, units } = this;
if ( !Number.isNumeric(value) ) return expiry ? { expiry, value: null } : {};
if ( expiry && !ActiveEffect.implementation.expirySupportsDuration(expiry) ) return { expiry, value: null };
switch ( units ) {
case "turn": return { value, units: "turns" };
case "round": return { value, units: "rounds" };
case "second": return { value, units: "seconds" };
case "minute": return { value, units: "minutes" };
case "hour": return { value, units: "hours" };
case "day": return { value, units: "days" };
case "month": return { value, units: "months" };
case "year": return { value, units: "years" };
default: return {};
case "turn": units = "turns"; break;
case "round": units = "rounds"; break;
case "second": units = "seconds"; break;
case "minute": units = "minutes"; break;
case "hour": units = "hours"; break;
case "day": units = "days" ; break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case "day": units = "days" ; break;
case "day": units = "days"; break;

case "month": units = "months"; break;
case "year": units = "years"; break;
default: return expiry ? { expiry, value: null } : {};
}
return { expiry, value, units };
}
}
Loading