Skip to content

Commit

Permalink
[foundryvtt#3911] Add SaveActivity data and sheet
Browse files Browse the repository at this point in the history
Adds a new activity type for making saves and applying damage.
This involves refactoring out some code from the attack & summons
sheets into `ActivitySheet` for some shared behavior.

Also reworks the damage parts list and the applied effects into a
`.separated-list` like on the summons sheet.

Note: Does not contain any functionality for rolling saves or damage.
  • Loading branch information
arbron committed Aug 5, 2024
1 parent b94e0ac commit db0ca19
Show file tree
Hide file tree
Showing 21 changed files with 487 additions and 165 deletions.
1 change: 1 addition & 0 deletions icons/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The dnd5e system for Foundry Virtual Tabletop includes icon artwork licensed fro
/svg/vehicle.svg - "Ship's wheel" by Delapouite under CC BY 3.0
/svg/versatile.svg - "Swiss army knife" by Delapouite under CC BY 3.0
/svg/activity/attack.svg - "Sword clash" by Lorc under CC BY 3.0
/svg/activity/save.svg - "Shield reflect" by Lorc under CC BY 3.0
/svg/activity/summon.svg - "Pentagram rose" by Lorc under CC BY 3.0
/svg/activity/utility.svg - "Spanner" by Lorc under CC BY 3.0
/svg/damage/acid.svg - "Fizzling flask" by Lorc under CC BY 3.0
Expand Down
6 changes: 6 additions & 0 deletions icons/svg/activity/save.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,46 @@
"DND5E.MovementUnits": "Units",
"DND5E.NewDay": "Is New Day?",
"DND5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",

"DND5E.SAVE": {
"Title": {
"one": "Save",
"other": "Saves"
},
"FIELDS": {
"ability": {
"label": "Challenge Ability",
"hint": "Ability that must be rolled to attempt to save."
},
"damage": {
"label": "Save Damage",
"onSave": {
"label": "Damage on Save",
"hint": "How much damage should be applied on a successful save?",
"Full": "Full Damage",
"Half": "Half Damage",
"None": "No Damage"
}
},
"save": {
"label": "Save Details",
"dc": {
"label": "Difficulty Class",
"calculation": {
"label": "DC Calculation",
"hint": "Method or ability used to calculate the difficulty class."
},
"formula": {
"label": "DC Formula",
"hint": "Custom formula or flat value for defining the save DC."
},
"CustomFormula": "Custom Formula",
"DefaultFormula": "8 + Ability + Proficiency"
}
}
}
},

"DND5E.SaveBonus": "Saving Throw Bonus",
"DND5E.SaveGlobalBonusHint": "This bonus applies to all saving throws made by this actor.",
"DND5E.Scroll": {
Expand Down
34 changes: 21 additions & 13 deletions less/v2/activities.less
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,11 @@
margin-block-end: -3px;
}
}
}

/* ----------------------------------------- */
/* Summon Activity */
/* ----------------------------------------- */

.summon-activity.sheet {
.separated-list {
.details { align-items: stretch; }
.content-link, .drop-area, .name {
flex: 0 0 175px;
display: flex;
align-items: center;
align-content: center;
}
.content-link {
display: block;
align-content: center;
overflow: hidden;
text-overflow: ellipsis;
}
Expand All @@ -52,6 +40,26 @@
border-radius: 4px;
padding-inline: 4px;
}
.gold-icon {
flex: 0 0 32px;
width: 32px;
height: 32px;
}
}
}

/* ----------------------------------------- */
/* Summon Activity */
/* ----------------------------------------- */

.summon-activity.sheet {
.content-link, .drop-area, .name {
flex: 0 0 175px;
display: flex;
align-items: center;
align-content: center;
}
.separated-list {
input::placeholder { opacity: .5; }
.details > label {
display: flex;
Expand Down
175 changes: 158 additions & 17 deletions module/applications/activity/activity-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ export default class ActivitySheet extends Application5e {
},
actions: {
addConsumption: ActivitySheet.#addConsumption,
addDamagePart: ActivitySheet.#addDamagePart,
addEffect: ActivitySheet.#addEffect,
addRecovery: ActivitySheet.#addRecovery,
deleteConsumption: ActivitySheet.#deleteConsumption,
deleteDamagePart: ActivitySheet.#deleteDamagePart,
deleteEffect: ActivitySheet.#deleteEffect,
deleteRecovery: ActivitySheet.#deleteRecovery
deleteRecovery: ActivitySheet.#deleteRecovery,
toggleCollapsed: ActivitySheet.#toggleCollapsed
},
form: {
handler: ActivitySheet.#onSubmitForm,
Expand Down Expand Up @@ -73,6 +76,14 @@ export default class ActivitySheet extends Application5e {

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

/**
* Key paths to the parts of the submit data stored in arrays that will need special handling on submission.
* @type {string[]}
*/
static CLEAN_ARRAYS = ["consumption.targets", "damage.parts", "effects", "uses.recovery"];

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

/** @override */
tabGroups = {
sheet: "identity",
Expand All @@ -99,6 +110,14 @@ export default class ActivitySheet extends Application5e {

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

/**
* Expanded states for additional settings sections.
* @type {Map<string, boolean>}
*/
#expandedSections = new Map();

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

/**
* Is this Activity sheet visible to the current user?
* @type {boolean}
Expand Down Expand Up @@ -169,8 +188,9 @@ export default class ActivitySheet extends Application5e {

/**
* Prepare rendering context for the activation tab.
* @param {object} context Context being prepared.
* @returns {object}
* @param {ApplicationRenderContext} context Context being prepared.
* @returns {ApplicationRenderContext}
* @protected
*/
async _prepareActivationContext(context) {
context.tab = context.tabs.activation;
Expand Down Expand Up @@ -278,17 +298,83 @@ export default class ActivitySheet extends Application5e {
/* -------------------------------------------- */

/**
* Prepare rendering context for the effect tab.
* @param {object} context Context being prepared.
* Prepare a specific applied effect if present in the activity data.
* @param {ApplicationRenderContext} context Context being prepared.
* @param {object} effect Applied effect context being prepared.
* @returns {object}
* @protected
*/
_prepareAppliedEffectContext(context, effect) {
return effect;
}

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

/**
* Prepare a specific damage part if present in the activity data.
* @param {ApplicationRenderContext} context Context being prepared.
* @param {object} part Damage part context being prepared.
* @returns {object}
* @protected
*/
_prepareDamagePartContext(context, part) {
return part;
}

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

/**
* Prepare rendering context for the effect tab.
* @param {ApplicationRenderContext} context Context being prepared.
* @returns {ApplicationRenderContext}
* @protected
*/
async _prepareEffectContext(context) {
context.tab = context.tabs.effect;

const appliedEffects = new Set(context.activity.effects?.map(e => e.id) ?? []);
context.allEffects = this.item.effects.map(effect => ({
value: effect.id, label: effect.name, selected: appliedEffects.has(effect.id)
}));
if ( context.activity.effects ) {
const appliedEffects = new Set(context.activity.effects?.map(e => e.id) ?? []);
context.allEffects = this.item.effects.map(effect => ({
value: effect.id, label: effect.name, selected: appliedEffects.has(effect.id)
}));
context.appliedEffects = context.activity.effects.map((data, index) => {
const effect = {
data,
effect: data.effect,
fields: this.activity.schema.fields.effects.element.fields,
prefix: `effects.${index}.`,
source: context.source.effects[index] ?? data,
additionalSettings: null
};
return this._prepareAppliedEffectContext(context, effect);
});
}

if ( context.activity.damage?.parts ) {
const denominationOptions = [
{ value: "", label: "" },
...CONFIG.DND5E.dieSteps.map(value => ({ value, label: value }))
];
const scalingOptions = [
{ value: "", label: game.i18n.localize("DND5E.DAMAGE.Scaling.None") },
...Object.entries(CONFIG.DND5E.damageScalingModes).map(([value, config]) => ({ value, label: config.label }))
];
context.damageParts = context.activity.damage.parts.map((data, index) => {
const part = {
data,
fields: this.activity.schema.fields.damage.fields.parts.element.fields,
prefix: `damage.parts.${index}.`,
source: context.source.damage.parts[index] ?? data,
canScale: this.activity.canScaleDamage,
denominationOptions,
scalingOptions,
typeOptions: Object.entries(CONFIG.DND5E.damageTypes).map(([value, config]) => ({
value, label: config.label, selected: data.types.has(value)
}))
};
return this._prepareDamagePartContext(context, part);
});
}

return context;
}
Expand All @@ -297,8 +383,9 @@ export default class ActivitySheet extends Application5e {

/**
* Prepare rendering context for the identity tab.
* @param {object} context Context being prepared.
* @returns {object}
* @param {ApplicationRenderContext} context Context being prepared.
* @returns {ApplicationRenderContext}
* @protected
*/
async _prepareIdentityContext(context) {
context.tab = context.tabs.identity;
Expand Down Expand Up @@ -385,6 +472,17 @@ export default class ActivitySheet extends Application5e {

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

/** @inheritDoc */
_onRender(context, options) {
super._onRender(context, options);
for ( const element of this.element.querySelectorAll("[data-expand-id]") ) {
element.querySelector(".collapsible")?.classList
.toggle("collapsed", !this.#expandedSections.get(element.dataset.expandId));
}
}

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

/** @override */
_onClose(_options) {
this.activity.constructor._unregisterApp(this.activity, this);
Expand Down Expand Up @@ -423,6 +521,19 @@ export default class ActivitySheet extends Application5e {

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

/**
* Handle adding a new entry to the damage parts list.
* @this {ActivityConfig}
* @param {Event} event Triggering click event.
* @param {HTMLElement} target Button that was clicked.
*/
static #addDamagePart(event, target) {
if ( !this.activity.damage?.parts ) return;
this.activity.update({ "damage.parts": [...this.activity.toObject().damage.parts, {}] });
}

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

/**
* Handle creating a new active effect and adding it to the applied effects list.
* @this {ActivityConfig}
Expand Down Expand Up @@ -478,6 +589,21 @@ export default class ActivitySheet extends Application5e {

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

/**
* Handle removing an entry from the damage parts list.
* @this {ActivityConfig}
* @param {Event} event Triggering click event.
* @param {HTMLElement} target Button that was clicked.
*/
static #deleteDamagePart(event, target) {
if ( !this.activity.damage?.parts ) return;
const parts = this.activity.toObject().damage.parts;
parts.splice(target.closest("[data-index]").dataset.index, 1);
this.activity.update({ "damage.parts": parts });
}

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

/**
* Handle deleting an active effect and removing it from the applied effects list.
* @this {ActivityConfig}
Expand Down Expand Up @@ -507,6 +633,23 @@ export default class ActivitySheet extends Application5e {
this.activity.update({ "uses.recovery": recovery });
}

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

/**
* Handle toggling the collapsed state of an additional settings section.
* @this {ActivityConfig}
* @param {Event} event Triggering click event.
* @param {HTMLElement} target Button that was clicked.
*/
static #toggleCollapsed(event, target) {
if ( event.target.closest(".collapsible-content") ) return;
target.classList.toggle("collapsed");
this.#expandedSections.set(
target.closest("[data-expand-id]")?.dataset.expandId,
!event.currentTarget.classList.contains("collapsed")
);
}

/* -------------------------------------------- */
/* Form Handling */
/* -------------------------------------------- */
Expand All @@ -532,6 +675,10 @@ export default class ActivitySheet extends Application5e {
*/
_prepareSubmitData(event, formData) {
const submitData = foundry.utils.expandObject(formData.object);
for ( const keyPath of this.constructor.CLEAN_ARRAYS ) {
const data = foundry.utils.getProperty(submitData, keyPath);
if ( data ) foundry.utils.setProperty(submitData, keyPath, Object.values(data));
}
if ( foundry.utils.hasProperty(submitData, "appliedEffects") ) {
const effects = submitData.effects ?? this.activity.toObject().effects;
submitData.effects = effects.filter(e => submitData.appliedEffects.includes(e.id));
Expand All @@ -540,12 +687,6 @@ export default class ActivitySheet extends Application5e {
submitData.effects.push({ id });
}
}
if ( foundry.utils.hasProperty(submitData, "consumption.targets") ) {
submitData.consumption.targets = Object.values(submitData.consumption.targets);
}
if ( foundry.utils.hasProperty(submitData, "uses.recovery") ) {
submitData.uses.recovery = Object.values(submitData.uses.recovery);
}
return submitData;
}

Expand Down
Loading

0 comments on commit db0ca19

Please sign in to comment.