From db0ca19d54ea0a3f8e9fb379aa416b709cdb5595 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Fri, 2 Aug 2024 12:32:45 -0700 Subject: [PATCH] [#3911] Add `SaveActivity` data and sheet 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. --- icons/LICENSE | 1 + icons/svg/activity/save.svg | 6 + lang/en.json | 40 ++++ less/v2/activities.less | 34 ++-- .../applications/activity/activity-sheet.mjs | 175 ++++++++++++++++-- module/applications/activity/attack-sheet.mjs | 68 +------ module/applications/activity/save-sheet.mjs | 65 +++++++ module/applications/activity/summon-sheet.mjs | 54 +----- module/config.mjs | 3 + module/data/activity/_module.mjs | 1 + module/data/activity/base-activity.mjs | 16 +- module/data/activity/save-data.mjs | 69 +++++++ module/documents/activity/_module.mjs | 1 + module/documents/activity/save.mjs | 27 +++ templates/activity/parts/activity-effects.hbs | 41 +++- templates/activity/parts/damage-parts.hbs | 21 ++- templates/activity/parts/save-damage.hbs | 5 + templates/activity/parts/save-details.hbs | 13 ++ .../activity/parts/save-effect-settings.hbs | 3 + templates/activity/parts/summon-profiles.hbs | 4 +- templates/activity/save-effect.hbs | 5 + 21 files changed, 487 insertions(+), 165 deletions(-) create mode 100644 icons/svg/activity/save.svg create mode 100644 module/applications/activity/save-sheet.mjs create mode 100644 module/data/activity/save-data.mjs create mode 100644 module/documents/activity/save.mjs create mode 100644 templates/activity/parts/save-damage.hbs create mode 100644 templates/activity/parts/save-details.hbs create mode 100644 templates/activity/parts/save-effect-settings.hbs create mode 100644 templates/activity/save-effect.hbs diff --git a/icons/LICENSE b/icons/LICENSE index 1fc2839393..4208bec0aa 100644 --- a/icons/LICENSE +++ b/icons/LICENSE @@ -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 diff --git a/icons/svg/activity/save.svg b/icons/svg/activity/save.svg new file mode 100644 index 0000000000..c345b16a0f --- /dev/null +++ b/icons/svg/activity/save.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lang/en.json b/lang/en.json index 04f9cec4f7..111f9f18d3 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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": { diff --git a/less/v2/activities.less b/less/v2/activities.less index 2e51986a26..e7d64bf0ca 100644 --- a/less/v2/activities.less +++ b/less/v2/activities.less @@ -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; } @@ -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; diff --git a/module/applications/activity/activity-sheet.mjs b/module/applications/activity/activity-sheet.mjs index b9fdeb5ddd..fa7fddc818 100644 --- a/module/applications/activity/activity-sheet.mjs +++ b/module/applications/activity/activity-sheet.mjs @@ -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, @@ -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", @@ -99,6 +110,14 @@ export default class ActivitySheet extends Application5e { /* -------------------------------------------- */ + /** + * Expanded states for additional settings sections. + * @type {Map} + */ + #expandedSections = new Map(); + + /* -------------------------------------------- */ + /** * Is this Activity sheet visible to the current user? * @type {boolean} @@ -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; @@ -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; } @@ -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; @@ -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); @@ -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} @@ -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} @@ -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 */ /* -------------------------------------------- */ @@ -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)); @@ -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; } diff --git a/module/applications/activity/attack-sheet.mjs b/module/applications/activity/attack-sheet.mjs index 9c8edb2bdb..9f7ef8afd6 100644 --- a/module/applications/activity/attack-sheet.mjs +++ b/module/applications/activity/attack-sheet.mjs @@ -7,11 +7,7 @@ export default class AttackSheet extends ActivitySheet { /** @inheritDoc */ static DEFAULT_OPTIONS = { - classes: ["attack-activity"], - actions: { - addDamagePart: AttackSheet.#addDamagePart, - deleteDamagePart: AttackSheet.#deleteDamagePart - } + classes: ["attack-activity"] }; /* -------------------------------------------- */ @@ -52,27 +48,6 @@ export default class AttackSheet extends ActivitySheet { ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({ value, label: config.label })) ]; - const denominationOptions = [ - { value: "", label: "" }, - ...CONFIG.DND5E.dieSteps.map(value => ({ value, label: `d${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) => ({ - 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 context; } @@ -95,45 +70,4 @@ export default class AttackSheet extends ActivitySheet { return context; } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** - * 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) { - this.activity.update({ "damage.parts": [...this.activity.toObject().damage.parts, {}] }); - } - - /* -------------------------------------------- */ - - /** - * 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) { - const parts = this.activity.toObject().damage.parts; - parts.splice(target.closest("[data-index]").dataset.index, 1); - this.activity.update({ "damage.parts": parts }); - } - - /* -------------------------------------------- */ - /* Form Handling */ - /* -------------------------------------------- */ - - /** @inheritDoc */ - _prepareSubmitData(event, formData) { - const submitData = super._prepareSubmitData(event, formData); - if ( foundry.utils.hasProperty(submitData, "damage.parts") ) { - submitData.damage.parts = Object.values(submitData.damage.parts); - } - return submitData; - } } diff --git a/module/applications/activity/save-sheet.mjs b/module/applications/activity/save-sheet.mjs new file mode 100644 index 0000000000..8d684a55f4 --- /dev/null +++ b/module/applications/activity/save-sheet.mjs @@ -0,0 +1,65 @@ +import ActivitySheet from "./activity-sheet.mjs"; + +/** + * Sheet for the attack activity. + */ +export default class AttackSheet extends ActivitySheet { + + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ["save-activity"] + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static PARTS = { + ...super.PARTS, + effect: { + template: "systems/dnd5e/templates/activity/save-effect.hbs", + templates: [ + ...super.PARTS.effect.templates, + "systems/dnd5e/templates/activity/parts/damage-parts.hbs", + "systems/dnd5e/templates/activity/parts/save-damage.hbs", + "systems/dnd5e/templates/activity/parts/save-details.hbs", + "systems/dnd5e/templates/activity/parts/save-effect-settings.hbs" + ] + } + }; + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @override */ + _prepareAppliedEffectContext(context, effect) { + effect.additionalSettings = "systems/dnd5e/templates/activity/parts/save-effect-settings.hbs"; + return effect; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _prepareEffectContext(context) { + context = await super._prepareEffectContext(context); + + context.abilityOptions = Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({ + value, label: config.label + })); + context.calculationOptions = [ + { value: "custom", label: game.i18n.localize("DND5E.SAVE.FIELDS.save.dc.CustomFormula") }, + // TODO: Have an
here would be great! + { value: "spellcasting", label: game.i18n.localize("DND5E.SpellAbility") }, + ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({ + value, label: config.label, group: game.i18n.localize("DND5E.Abilities") + })) + ]; + context.onSaveOptions = [ + { value: "none", label: game.i18n.localize("DND5E.SAVE.FIELDS.damage.onSave.None") }, + { value: "half", label: game.i18n.localize("DND5E.SAVE.FIELDS.damage.onSave.Half") }, + { value: "full", label: game.i18n.localize("DND5E.SAVE.FIELDS.damage.onSave.Full") } + ]; + + return context; + } +} diff --git a/module/applications/activity/summon-sheet.mjs b/module/applications/activity/summon-sheet.mjs index 61ff4ccfe3..3c7e789de3 100644 --- a/module/applications/activity/summon-sheet.mjs +++ b/module/applications/activity/summon-sheet.mjs @@ -10,8 +10,7 @@ export default class SummonSheet extends ActivitySheet { classes: ["summon-activity"], actions: { addProfile: SummonSheet.#addProfile, - deleteProfile: SummonSheet.#deleteProfile, - toggleCollapsed: SummonSheet.#toggleCollapsed + deleteProfile: SummonSheet.#deleteProfile } }; @@ -35,6 +34,11 @@ export default class SummonSheet extends ActivitySheet { /* -------------------------------------------- */ + /** @inheritDoc */ + static CLEAN_ARRAYS = [...super.CLEAN_ARRAYS, "profiles"]; + + /* -------------------------------------------- */ + /** @override */ tabGroups = { sheet: "identity", @@ -42,16 +46,6 @@ export default class SummonSheet extends ActivitySheet { effect: "profiles" }; - /* -------------------------------------------- */ - /* Properties */ - /* -------------------------------------------- */ - - /** - * Expanded states for each profile. - * @type {Map} - */ - #expandedProfiles = new Map(); - /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ @@ -73,7 +67,6 @@ export default class SummonSheet extends ActivitySheet { ]; context.profiles = this.activity.profiles.map((data, index) => ({ data, index, - collapsed: this.#expandedProfiles.get(data._id) ? "" : "collapsed", fields: this.activity.schema.fields.profiles.element.fields, prefix: `profiles.${index}.`, source: context.source.profiles[index] ?? data, @@ -145,23 +138,6 @@ export default class SummonSheet extends ActivitySheet { this.activity.update({ profiles }); } - /* -------------------------------------------- */ - - /** - * 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.#expandedProfiles.set( - target.closest("[data-profile-id]").dataset.profileId, - !event.currentTarget.classList.contains("collapsed") - ); - } - /* -------------------------------------------- */ /* Drag & Drop */ /* -------------------------------------------- */ @@ -190,22 +166,4 @@ export default class SummonSheet extends ActivitySheet { // Otherwise create a new profile else this.activity.update({ profiles: [...this.activity.toObject().profiles, { uuid: actor.uuid }] }); } - - /* -------------------------------------------- */ - /* Form Handling */ - /* -------------------------------------------- */ - - /** - * Perform any pre-processing of the form data to prepare it for updating. - * @param {SubmitEvent} event Triggering submit event. - * @param {FormDataExtended} formData Data from the submitted form. - * @returns {object} - */ - _prepareSubmitData(event, formData) { - const submitData = foundry.utils.expandObject(formData.object); - if ( foundry.utils.hasProperty(submitData, "profiles") ) { - submitData.profiles = Object.values(submitData.profiles); - } - return submitData; - } } diff --git a/module/config.mjs b/module/config.mjs index 9204826147..e42ad3fac6 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -3236,6 +3236,9 @@ DND5E.activityTypes = { attack: { documentClass: activities.AttackActivity }, + save: { + documentClass: activities.SaveActivity + }, summon: { documentClass: activities.SummonActivity }, diff --git a/module/data/activity/_module.mjs b/module/data/activity/_module.mjs index c5a7fac54f..08e8eaeee2 100644 --- a/module/data/activity/_module.mjs +++ b/module/data/activity/_module.mjs @@ -1,5 +1,6 @@ export {default as BaseActivityData} from "./base-activity.mjs"; export {default as AttackActivityData} from "./attack-data.mjs"; +export {default as SaveActivityData} from "./save-data.mjs"; export {default as SummonActivityData} from "./summon-data.mjs"; export {default as UtilityActivityData} from "./utility-data.mjs"; diff --git a/module/data/activity/base-activity.mjs b/module/data/activity/base-activity.mjs index 2777e1dd44..5d72213001 100644 --- a/module/data/activity/base-activity.mjs +++ b/module/data/activity/base-activity.mjs @@ -117,9 +117,7 @@ export default class BaseActivityData extends foundry.abstract.DataModel { units: new StringField({ initial: "inst" }), special: new StringField() }), - effects: new ArrayField(new SchemaField({ - id: new DocumentIdField() - })), + effects: new ArrayField(new SchemaField(this.defineEffectSchema())), range: new SchemaField({ value: new FormulaField({ deterministic: true }), units: new StringField(), @@ -147,6 +145,18 @@ export default class BaseActivityData extends foundry.abstract.DataModel { }; } + /* -------------------------------------------- */ + + /** + * Return the fields that will be included in the schema for applied effects. + * @returns {Record} + */ + static defineEffectSchema() { + return { + id: new DocumentIdField() + }; + } + /* -------------------------------------------- */ /* Data Preparation */ /* -------------------------------------------- */ diff --git a/module/data/activity/save-data.mjs b/module/data/activity/save-data.mjs new file mode 100644 index 0000000000..338403e7f3 --- /dev/null +++ b/module/data/activity/save-data.mjs @@ -0,0 +1,69 @@ +import FormulaField from "../fields/formula-field.mjs"; +import DamageField from "../shared/damage-field.mjs"; +import BaseActivityData from "./base-activity.mjs"; + +const { ArrayField, BooleanField, SchemaField, StringField } = foundry.data.fields; + +/** + * @typedef {EffectApplicationData} SaveEffectApplicationData + * @property {string} onSave Should this effect still be applied on a successful save? + */ + +/** + * Data model for an save activity. + * + * @property {string} ability Ability used to make the attack and determine damage. + * @property {object} damage + * @property {string} damage.onSave How much damage is done on a successful save? + * @property {DamageData[]} damage.parts Parts of damage to inflict. + * @property {SaveEffectApplicationData[]} effects Linked effects that can be applied. + * @property {object} save + * @property {object} save.dc + * @property {string} save.dc.calculation Method or ability used to calculate the difficulty class. + * @property {string} save.dc.formula Custom DC formula or flat value. + */ +export default class SaveActivityData extends BaseActivityData { + /** @inheritDoc */ + static defineSchema() { + return { + ...super.defineSchema(), + ability: new StringField({ initial: () => Object.keys(CONFIG.DND5E.abilities)[0] }), + damage: new SchemaField({ + onSave: new StringField(), + parts: new ArrayField(new DamageField()) + }), + save: new SchemaField({ + dc: new SchemaField({ + calculation: new StringField(), + formula: new FormulaField({ deterministic: true }) + }) + }) + }; + } + + /* -------------------------------------------- */ + + /** + * Return the fields that will be included in the schema for applied effects. + * @returns {Record} + */ + static defineEffectSchema() { + return { + ...super.defineEffectSchema(), + onSave: new BooleanField() + }; + } + + /* -------------------------------------------- */ + /* Data Preparation */ + /* -------------------------------------------- */ + + /** + * Prepare data related to this activity. + */ + prepareData() { + super.prepareData(); + if ( !this.damage.onSave ) this.damage.onSave = this.isSpell && (this.item.system.level === 0) ? "none" : "half"; + if ( !this.save.dc.calculation ) this.save.dc.calculation = this.isSpell ? "spellcasting" : "custom"; + } +} diff --git a/module/documents/activity/_module.mjs b/module/documents/activity/_module.mjs index 42c6943f10..2d3b497ae4 100644 --- a/module/documents/activity/_module.mjs +++ b/module/documents/activity/_module.mjs @@ -1,5 +1,6 @@ export {default as ActivityMixin} from "./mixin.mjs"; export {default as AttackActivity} from "./attack.mjs"; +export {default as SaveActivity} from "./save.mjs"; export {default as SummonActivity} from "./summon.mjs"; export {default as UtilityActivity} from "./utility.mjs"; diff --git a/module/documents/activity/save.mjs b/module/documents/activity/save.mjs new file mode 100644 index 0000000000..35d3356707 --- /dev/null +++ b/module/documents/activity/save.mjs @@ -0,0 +1,27 @@ +import SaveSheet from "../../applications/activity/save-sheet.mjs"; +import SaveActivityData from "../../data/activity/save-data.mjs"; +import ActivityMixin from "./mixin.mjs"; + +/** + * Activity for making saving throws and rolling damage. + */ +export default class SaveActivity extends ActivityMixin(SaveActivityData) { + /* -------------------------------------------- */ + /* Model Configuration */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, "DND5E.SAVE"]; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static metadata = Object.freeze( + foundry.utils.mergeObject(super.metadata, { + type: "save", + img: "systems/dnd5e/icons/svg/activity/save.svg", + title: "DND5E.SAVE.Title.one", + sheetClass: SaveSheet + }, { inplace: false }) + ); +} diff --git a/templates/activity/parts/activity-effects.hbs b/templates/activity/parts/activity-effects.hbs index 6ff9a9d86b..653b0d3c31 100644 --- a/templates/activity/parts/activity-effects.hbs +++ b/templates/activity/parts/activity-effects.hbs @@ -3,14 +3,10 @@ {{ selectOptions allEffects }} -
    - {{#each activity.effects}} -
  • - {{ effect.name }} - {{{ dnd5e-linkForUuid effect.uuid }}} - +
      + {{#each appliedEffects}} +
    • + {{> ".effect" }}
    • {{/each}}
    @@ -18,3 +14,32 @@ {{ localize "DND5E.EFFECT.Action.Create" }} + +{{#*inline ".effect"}} +
    + {{ effect.name }} + {{{ dnd5e-linkForUuid effect.uuid }}} +
    + +
    +
    + +{{#if additionalSettings}} +
    + +
    +
    + {{> (lookup . "additionalSettings") }} +
    +
    +
    +{{/if}} +{{/inline}} diff --git a/templates/activity/parts/damage-parts.hbs b/templates/activity/parts/damage-parts.hbs index 7fe3ae7321..acdb8aad51 100644 --- a/templates/activity/parts/damage-parts.hbs +++ b/templates/activity/parts/damage-parts.hbs @@ -1,10 +1,13 @@ -
      +
        {{#each damageParts}}
      • -
        +
        + {{ formInput fields.custom.fields.enabled name=(concat prefix "custom.enabled") + value=data.custom.enabled }} {{#unless data.custom.enabled}} {{ formField fields.number name=(concat prefix "number") value=data.number stacked=true hint=false }} + d {{ formField fields.denomination name=(concat prefix "denomination") value=data.denomination options=denominationOptions stacked=true hint=false }} + @@ -20,9 +23,16 @@ {{/unless}} - {{ formField fields.custom.fields.enabled name=(concat prefix "custom.enabled") - value=data.custom.enabled stacked=true hint=false }} + +
        + +
        + {{ formField fields.types name=(concat prefix "types") value=data.types options=typeOptions hint=false }} {{#if canScale}} @@ -40,9 +50,6 @@ {{/with}} {{/if}} -
      • {{/each}}
      diff --git a/templates/activity/parts/save-damage.hbs b/templates/activity/parts/save-damage.hbs new file mode 100644 index 0000000000..0e94167694 --- /dev/null +++ b/templates/activity/parts/save-damage.hbs @@ -0,0 +1,5 @@ +
      + {{ localize "DND5E.SAVE.FIELDS.damage.label" }} + {{ formField fields.damage.fields.onSave value=source.damage.onSave options=onSaveOptions }} + {{> "systems/dnd5e/templates/activity/parts/damage-parts.hbs"}} +
      diff --git a/templates/activity/parts/save-details.hbs b/templates/activity/parts/save-details.hbs new file mode 100644 index 0000000000..3dc3eab0fb --- /dev/null +++ b/templates/activity/parts/save-details.hbs @@ -0,0 +1,13 @@ +
      + {{ localize "DND5E.SAVE.FIELDS.save.label" }} + {{ formField fields.ability value=source.ability options=abilityOptions }} + {{#with fields.save.fields.dc.fields as |fields|}} + {{ formField fields.calculation value=../source.save.dc.calculation options=../calculationOptions }} + {{#if (eq ../source.save.dc.calculation "custom")}} + {{ formField fields.formula value=../source.save.dc.formula }} + {{else}} + {{ formField fields.formula name="" value=(localize "DND5E.SAVE.FIELDS.save.dc.DefaultFormula") disabled=true }} + + {{/if}} + {{/with}} +
      diff --git a/templates/activity/parts/save-effect-settings.hbs b/templates/activity/parts/save-effect-settings.hbs new file mode 100644 index 0000000000..3242fdb6e5 --- /dev/null +++ b/templates/activity/parts/save-effect-settings.hbs @@ -0,0 +1,3 @@ +
      + {{ formField fields.onSave name=(concat prefix "onSave") value=data.onSave }} +
      diff --git a/templates/activity/parts/summon-profiles.hbs b/templates/activity/parts/summon-profiles.hbs index 59e5e359ad..74eb8b9650 100644 --- a/templates/activity/parts/summon-profiles.hbs +++ b/templates/activity/parts/summon-profiles.hbs @@ -7,7 +7,7 @@ {{/with}}
        {{#each profiles}} -
      • +
      • {{> ".profile" }}
      • {{else}} @@ -50,7 +50,7 @@ -