diff --git a/lang/en.json b/lang/en.json index 3b8f31cd..1aa43b5b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1042,7 +1042,8 @@ }, "vulnerable": { "name": "Vulnerable", - "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again." + "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again.", + "autoAppliedByLabel": "Max Stress" } }, "CountdownType": { @@ -1177,12 +1178,12 @@ }, "far": { "name": "Far", - "description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility check to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.", + "description": "means a distance where one can see the appearance of a person or object, but probably not in great detail-- across a small battlefield or down a large corridor. This is usually about 30-100 feet away. While under danger, a PC will likely have to make an Agility roll to get here safely. Anything on a battle map that is within the length of a standard piece of paper (~10-11 inches) can usually be considered far.", "short": "Far" }, "veryFar": { "name": "Very Far", - "description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility check to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.", + "description": "means a distance where you can see the shape of a person or object, but probably not make outany details-- across a large battlefield or down a long street, generally about 100-300 feet away. While under danger, a PC likely has to make an Agility roll to get here safely. Anything on a battle map that is beyond far distance, but still within sight of the characters can usually be considered very far.", "short": "V. Far" } }, @@ -1305,6 +1306,7 @@ "triggerTexts": { "strangePatternsContentTitle": "Matched {nr} times.", "strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.", + "strangePatternsActionExplanation": "Left click to increase, right click to decrease", "ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?", "ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you." }, @@ -2570,6 +2572,10 @@ "gm": { "label": "GM" }, "players": { "label": "Players" } }, + "vulnerableAutomation": { + "label": "Vulnerable Automation", + "hint": "Automatically apply the Vulnerable condition when a actor reaches max stress" + }, "countdownAutomation": { "label": "Countdown Automation", "hint": "Automatically progress countdowns based on their progression settings" @@ -2820,7 +2826,7 @@ "title": "Domain Card" }, "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "abilityCheckTitle": "{ability} Roll" }, "effectSummary": { "title": "Effects Applied", @@ -2835,7 +2841,7 @@ "selectLeader": "Select a Leader", "selectMember": "Select a Member", "rerollTitle": "Reroll Group Roll", - "rerollContent": "Are you sure you want to reroll your {trait} check?", + "rerollContent": "Are you sure you want to reroll your {trait} roll?", "rerollTooltip": "Reroll", "wholePartySelected": "The whole party is selected" }, @@ -3001,7 +3007,8 @@ "tokenActorMissing": "{name} is missing an Actor", "tokenActorsMissing": "[{names}] missing Actors", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", - "knowTheTide": "Know The Tide gained a token" + "knowTheTide": "Know The Tide gained a token", + "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}" }, "Sidebar": { "actorDirectory": { diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index bfad899b..0779bf58 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -187,6 +187,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli }); } + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -227,6 +228,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli } }); + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -246,6 +248,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli await this.settings.updateSource({ [`${path}.${id}`]: _del }); + + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 75173f8d..2bd7d5b9 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -4,6 +4,43 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac constructor(options) { super(options); + this.changeChoices = DhActiveEffectConfig.getChangeChoices(); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'sheet', 'dh-style'] + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, + tabs: { template: 'templates/generic/tab-navigation.hbs' }, + details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, + settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, + changes: { + template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', + templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'], + scrollable: ['ol[data-changes]'] + }, + footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } + }; + + static TABS = { + sheet: { + tabs: [ + { id: 'details', icon: 'fa-solid fa-book' }, + { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, + { id: 'changes', icon: 'fa-solid fa-gears' } + ], + initial: 'details', + labelPrefix: 'EFFECT.TABS' + } + }; + + /** + * Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs + * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} + */ + static getChangeChoices() { const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; const getAllLeaves = (root, group, parentPath = '') => { @@ -23,7 +60,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac return leaves; }; - this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { + return Object.keys(game.system.api.models.actors).reduce((acc, key) => { if (ignoredActorKeys.includes(key)) return acc; const model = game.system.api.models.actors[key]; @@ -62,35 +99,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac }, []); } - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'sheet', 'dh-style'] - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, - tabs: { template: 'templates/generic/tab-navigation.hbs' }, - details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, - settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, - changes: { - template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', - templates: ['systems/daggerheart/templates/sheets/activeEffect/change.hbs'], - scrollable: ['ol[data-changes]'] - }, - footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } - }; - - static TABS = { - sheet: { - tabs: [ - { id: 'details', icon: 'fa-solid fa-book' }, - { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, - { id: 'changes', icon: 'fa-solid fa-gears' } - ], - initial: 'details', - labelPrefix: 'EFFECT.TABS' - } - }; - _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); const changeChoices = this.changeChoices; diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs index fe36e37f..12ac90d1 100644 --- a/module/applications/sheets-configs/setting-active-effect-config.mjs +++ b/module/applications/sheets-configs/setting-active-effect-config.mjs @@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi super({}); this.effect = foundry.utils.deepClone(effect); - const ignoredActorKeys = ['config', 'DhEnvironment']; - this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { - if (!ignoredActorKeys.includes(key)) { - const model = game.system.api.models.actors[key]; - const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); - const group = game.i18n.localize(model.metadata.label); - const choices = CONFIG.Token.documentClass - .getTrackedAttributeChoices(attributes, model) - .map(x => ({ ...x, group: group })); - acc.push(...choices); - } - return acc; - }, []); + this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices(); } static DEFAULT_OPTIONS = { diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index e8ff7818..fb790f7f 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App return context; } - static async updateData(event, element, formData) { + static async updateData(_event, _element, formData) { const data = foundry.utils.expandObject(formData.object); - foundry.utils.mergeObject(this.move, data); + await this.updateMove({ + [`${this.movePath}`]: data + }); this.render(); } @@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App } ); - await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); - + await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action }); this.render(); } @@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); if (!updatedEffect) return; - await this.settings.updateSource({ + await this.updateMove({ [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); return acc; }, []) }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } else { const action = this.move.actions.get(id); @@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App : existingEffectIndex === -1 ? [...currentEffects, effectData] : currentEffects.with(existingEffectIndex, effectData); - await this.settings.updateSource({ + await this.updateMove({ [`${this.movePath}.effects`]: updatedEffects }); } - await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); + await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove }); + this.render(); return updatedEffects; }).render(true); @@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App }); } } - await this.settings.updateSource({ + await this.updateMove({ [this.movePath]: { effects: move.effects.filter(x => x.id !== id), actions: move.actions } }); } else { - await this.settings.updateSource({ [`${this.actionsPath}.${target.dataset.id}`]: _del }); + await this.updateMove({ [`${this.actionsPath}.${target.dataset.id}`]: _del }); } - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } - static async addEffect(_, target) { + static async addEffect() { const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); - await this.settings.updateSource({ + + await this.updateMove({ [`${this.movePath}.effects`]: [ ...currentEffects, game.system.api.data.activeEffects.BaseEffect.getDefaultObject() ] }); - - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } + async updateMove(update) { + await this.settings.updateSource(update); + this.move = foundry.utils.getProperty(this.settings, this.movePath); + } + static resetMoves() {} _filterTabs(tabs) { diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index d78519cb..1b1722db 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import DhpActor from '../../../documents/actor.mjs'; -import DHItem from '../../../documents/item.mjs'; export default class Party extends DHBaseActorSheet { constructor(options) { @@ -269,15 +268,6 @@ export default class Party extends DHBaseActorSheet { ).render({ force: true }); } - /** - * Get the set of ContextMenu options for Consumable and Loot. - * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance - * @this {CharacterSheet} - * @protected - */ - static #getItemContextOptions() { - return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); - } /* -------------------------------------------- */ /* Filter Tracking */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 85ecd616..6f994faf 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { ], dragDrop: [ { dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }, - { dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null } + { dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null } ] }; @@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { value: context.source.system.gold[key] }; } - context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled); + context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled); } return context; @@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { currency }); if (quantity) { - originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) }); + originActor.update({ + [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) + }); this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity }); } return; @@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { /* Handling transfer of inventoryItems */ if (item.system.metadata.isInventoryItem) { + if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) { + return ui.notifications.error( + game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', { + user: game.user.name, + target: this.document.name + }) + ); + } + if (item.system.metadata.isQuantifiable) { const actorItem = originActor.items.get(data.originId); const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({ @@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { }); if (quantityTransfered) { - if (quantityTransfered === actorItem.system.quantity) { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); - } else { - await actorItem.update({ - 'system.quantity': actorItem.system.quantity - quantityTransfered - }); - } - const existingItem = this.document.items.find(x => itemIsIdentical(x, item)); if (existingItem) { await existingItem.update({ @@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { } ]); } + + if (quantityTransfered === actorItem.system.quantity) { + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); + } else { + await actorItem.update({ + 'system.quantity': actorItem.system.quantity - quantityTransfered + }); + } } } else { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); await this.document.createEmbeddedDocuments('Item', [item.toObject()]); + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); } } } @@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { */ async _onDragStart(event) { // Handle drag/dropping currencies - const currencyEl = event.currentTarget.closest(".currency[data-currency]"); + const currencyEl = event.currentTarget.closest('.currency[data-currency]'); if (currencyEl) { const currency = currencyEl.dataset.currency; const data = { type: 'Currency', currency, originActor: this.document.uuid }; @@ -359,8 +370,8 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); return; - } - + } + const item = await getDocFromElement(event.target); if (item) { const dragData = { diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index a16a1490..26ae484b 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -1,4 +1,4 @@ -import { expireActiveEffects, refreshIsAllowed } from '../../../helpers/utils.mjs'; +import { RefreshFeatures } from '../../../helpers/utils.mjs'; const { HandlebarsApplicationMixin } = foundry.applications.api; const { AbstractSidebarTab } = foundry.applications.sidebar; @@ -54,75 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract return context; } - async getRefreshables(types) { - const refreshedActors = {}; - for (let actor of game.actors) { - if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { - expireActiveEffects(actor, types); - - const updates = {}; - for (let item of actor.items) { - if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) { - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - const increasing = - item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; - updates[item.id].system = { - ...updates[item.id].system, - 'resource.value': increasing - ? 0 - : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) - }; - } - if (item.system.metadata?.hasActions) { - const refreshTypes = new Set(); - const actions = item.system.actions.filter(action => { - if (refreshIsAllowed(types, action.uses.recovery)) { - refreshTypes.add(action.uses.recovery); - return true; - } - - return false; - }); - if (actions.length === 0) continue; - - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - ...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - updates[item.id].system = { - ...updates[item.id].system, - ...actions.reduce( - (acc, action) => { - acc.actions[action.id] = { 'uses.value': 0 }; - return acc; - }, - { actions: updates[item.id].system.actions ?? {} } - ) - }; - } - } - - for (let key in updates) { - const update = updates[key]; - await actor.items.get(key).update(update); - } - } - } - - return refreshedActors; - } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -135,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract static async #refreshActors() { const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); - await this.getRefreshables(refreshKeys); - const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', '); - ui.notifications.info( - game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { - types: `[${types}]` - }) - ); + await RefreshFeatures(refreshKeys); + this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); - - const cls = getDocumentClass('ChatMessage'); - const msg = { - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', - { - types: types - } - ), - title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), - speaker: cls.getSpeaker() - }; - - cls.create(msg); - this.render(); } } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 685b725f..8ec7034a 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -206,7 +206,8 @@ export const conditions = () => ({ id: 'vulnerable', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', img: 'icons/magic/control/silhouette-fall-slip-prone.webp', - description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description' + description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description', + autoApplyFlagId: 'auto-vulnerable' }, hidden: { id: 'hidden', diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 7d80e597..77328987 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -467,9 +467,7 @@ export const allArmorFeatures = () => { }; export const orderedArmorFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .armorFeatures; - const allFeatures = { ...armorFeatures, ...homebrewFeatures }; + const allFeatures = allArmorFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { @@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => { }; export const orderedWeaponFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .weaponFeatures; - const allFeatures = { ...weaponFeatures, ...homebrewFeatures }; + const allFeatures = allWeaponFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs index 4b927aed..c8bf8448 100644 --- a/module/data/actor/creature.mjs +++ b/module/data/actor/creature.mjs @@ -17,4 +17,45 @@ export default class DhCreature extends BaseDataActor { }) }; } + + get isAutoVulnerableActive() { + const vulnerableAppliedByOther = this.parent.effects.some( + x => x.statuses.has('vulnerable') && !x.flags.daggerheart?.autoApplyFlagId + ); + return !vulnerableAppliedByOther; + } + + async _preUpdate(changes, options, userId) { + const allowed = await super._preUpdate(changes, options, userId); + if (allowed === false) return; + + const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + if ( + automationSettings.vulnerableAutomation && + this.parent.type !== 'companion' && + changes.system?.resources?.stress?.value + ) { + const { name, description, img, autoApplyFlagId } = CONFIG.DH.GENERAL.conditions().vulnerable; + const autoEffects = this.parent.effects.filter( + x => x.flags.daggerheart?.autoApplyFlagId === autoApplyFlagId + ); + if (changes.system.resources.stress.value >= this.resources.stress.max) { + if (!autoEffects.length) + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.localize(name), + description: game.i18n.localize(description), + img: img, + statuses: ['vulnerable'], + flags: { daggerheart: { autoApplyFlagId } } + } + ]); + } else if (this.resources.stress.value >= this.resources.stress.max) { + this.parent.deleteEmbeddedDocuments( + 'ActiveEffect', + autoEffects.map(x => x.id) + ); + } + } + } } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index f6b67ff2..264d7f93 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem { armorFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allArmorFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -58,7 +56,7 @@ export default class DHArmor extends AttachableItem { async getDescriptionData() { const baseDescription = this.description; const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); - const features = this.armorFeatures.map(x => allFeatures[x.value]); + const features = this.armorFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/armor/description.hbs', diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 389dc1c5..bb2e10d5 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem { weaponFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allWeaponFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -121,7 +119,7 @@ export default class DHWeapon extends AttachableItem { const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label); const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); - const features = this.weaponFeatures.map(x => allFeatures[x.value]); + const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/weapon/description.hbs', diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index ee4f3b49..ab86351c 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -75,7 +75,7 @@ export default class RegisteredTriggers extends Map { unregisterSceneEnvironmentTriggers(flagSystemData) { const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); for (const environment of sceneData.sceneEnvironments) { - if (environment.pack) continue; + if (!environment || environment.pack) continue; this.unregisterItemTriggers(environment.system.features); } } diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index edc4eb28..35e87327 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel { label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' }) }), + vulnerableAutomation: new fields.BooleanField({ + initial: true, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.vulnerableAutomation.label' + }), countdownAutomation: new fields.BooleanField({ required: true, initial: true, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index e1acaf40..b322aae7 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,4 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; +import { parseRallyDice } from '../helpers/utils.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; @@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll { static async buildPost(roll, config, message) { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) - : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); + : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC); if (game.modules.get('dice-so-nice')?.active) { const pool = foundry.dice.terms.PoolTerm.fromRolls( Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) @@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll { chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, chatMessage.blind ); + config.mute = true; } await super.buildPost(roll, config, message); - if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); + if (config.source?.message) { + chatMessage.update({ 'system.damage': config.damage }); + + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } } static unifyDamageRoll(rolls) { @@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll { // Bardic Rally const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); if (rallyChoices.length) { diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 83b5559f..01452512 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,6 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; -import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; @@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll { setRallyChoices() { return this.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index cb51a255..f0538bff 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -934,10 +934,23 @@ export default class DhpActor extends Actor { /** Get active effects */ getActiveEffects() { + const conditions = CONFIG.DH.GENERAL.conditions(); const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); + const autoVulnerableActive = this.system.isAutoVulnerableActive; return this.effects .filter(x => !x.disabled) .reduce((acc, effect) => { + /* Could be generalized if needed. Currently just related to Vulnerable */ + const isAutoVulnerableEffect = + effect.flags.daggerheart?.autoApplyFlagId === conditions.vulnerable.autoApplyFlagId; + if (isAutoVulnerableEffect) { + if (!autoVulnerableActive) return acc; + + effect.appliedBy = game.i18n.localize('DAGGERHEART.CONFIG.Condition.vulnerable.autoAppliedByLabel'); + effect.isLockedCondition = true; + effect.condition = 'vulnerable'; + } + acc.push(effect); const currentStatusActiveEffects = acc.filter( diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index e9d65777..7e4d794b 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {} }), maxTags: typeof maxTags === 'function' ? maxTags() : maxTags, dropdown: { + searchKeys: ['value', 'name'], mapValueTo: 'name', - searchKeys: ['value'], enabled: 0, maxItems: 100, closeOnSelect: true, @@ -479,7 +479,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) { case CONFIG.DH.GENERAL.refreshTypes.scene.id: case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.longRest.id: - return allowedTypes.includes(typeToCheck); + return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck); case CONFIG.DH.GENERAL.refreshTypes.shortRest.id: return allowedTypes.some( x => @@ -602,3 +602,123 @@ export function calculateExpectedValue(formulaOrTerms) { : [formulaOrTerms]; return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); } + +export function parseRallyDice(value, effect) { + const legacyStartsWithPrefix = value.toLowerCase().startsWith('d'); + const workingValue = legacyStartsWithPrefix ? value.slice(1) : value; + const dataParsedValue = itemAbleRollParse(workingValue, effect.parent); + + return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`; +} +/** + * Refreshes character and/or adversary resources. + * @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes + * @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary. + * @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh + * @return { Actor[] } The actors that had their features refreshed + */ +export async function RefreshFeatures( + refreshTypes = [], + actorTypes = ['character', 'adversary'], + sendNotificationMessage = true, + sendRefreshMessage = true +) { + const refreshedActors = {}; + for (let actor of game.actors) { + if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) { + expireActiveEffects(actor, refreshTypes); + + const updates = {}; + for (let item of actor.items) { + if ( + item.system.metadata?.hasResource && + refreshIsAllowed(refreshTypes, item.system.resource?.recovery) + ) { + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + const increasing = + item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; + updates[item.id].system = { + ...updates[item.id].system, + 'resource.value': increasing + ? 0 + : game.system.api.documents.DhActiveEffect.effectSafeEval( + Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + ) + }; + } + if (item.system.metadata?.hasActions) { + const usedTypes = new Set(); + const actions = item.system.actions.filter(action => { + if (refreshIsAllowed(refreshTypes, action.uses.recovery)) { + usedTypes.add(action.uses.recovery); + return true; + } + + return false; + }); + if (actions.length === 0) continue; + + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + ...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + updates[item.id].system = { + ...updates[item.id].system, + ...actions.reduce( + (acc, action) => { + acc.actions[action.id] = { 'uses.value': 0 }; + return acc; + }, + { actions: updates[item.id].system.actions ?? {} } + ) + }; + } + } + + for (let key in updates) { + const update = updates[key]; + await actor.items.get(key).update(update); + } + } + } + + const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', '); + + if (sendNotificationMessage) { + ui.notifications.info( + game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { + types: `[${types}]` + }) + ); + } + + if (sendRefreshMessage) { + const cls = getDocumentClass('ChatMessage'); + const msg = { + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', + { + types: types + } + ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), + speaker: cls.getSpeaker() + }; + + cls.create(msg); + } + + return refreshedActors; +} diff --git a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json index b7830722..c4dd83a7 100644 --- a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json +++ b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json @@ -20,10 +20,6 @@ { "type": "class", "item": "Compendium.daggerheart.classes.Item.PydiMnNCKpd44SGS" - }, - { - "type": "class", - "item": "Compendium.daggerheart.classes.Item.TVeEyqmPPiRa2r3i" } ], "subclasses": [ diff --git a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json index e8d4c3c9..e2a0b5bb 100644 --- a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json +++ b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json @@ -63,7 +63,7 @@ { "key": "system.bonuses.rally", "mode": 2, - "value": "d6", + "value": "6 + min((floor(@system.levelData.level.current / 5)*2), 2)", "priority": null } ], diff --git a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json b/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json deleted file mode 100644 index 46717fcb..00000000 --- a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "folder": "C9y59fIkq50d3SyD", - "name": "Rally (Level 5)", - "type": "feature", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "system": { - "description": "
Once per session, describe how you rally the party and give yourself and each of your allies a Rally Die. At level 1, your Rally Die is a d6. A PC can spend their Rally Die to roll it, adding the result to their action roll, reaction roll, damage roll, or to clear a number of Stress equal to the result. At the end of each session, clear all unspent Rally Dice. At level 5, your Rally Die increases to a d8.
", - "resource": null, - "actions": { - "Z1KWFrpXOqZWuZD1": { - "type": "effect", - "_id": "Z1KWFrpXOqZWuZD1", - "systemPath": "actions", - "description": "", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "1", - "recovery": "session" - }, - "effects": [ - { - "_id": "8CFxYJV8zE6Wabwj", - "onSave": false - } - ], - "target": { - "type": "any", - "amount": null - }, - "name": "Rally your Allies", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "range": "" - } - }, - "originItemType": null, - "originId": null, - "attribution": { - "source": "Daggerheart SRD", - "page": 9, - "artist": "" - } - }, - "effects": [ - { - "name": "Rally (Level 5)", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "origin": "Compendium.daggerheart.classes.Item.oxv0m8AFUQVFKtZ4", - "transfer": false, - "_id": "8CFxYJV8zE6Wabwj", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "changes": [ - { - "key": "system.bonuses.rally", - "mode": 2, - "value": "d8", - "priority": null - } - ], - "disabled": false, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "", - "tint": "#ffffff", - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!TVeEyqmPPiRa2r3i.8CFxYJV8zE6Wabwj" - } - ], - "flags": {}, - "ownership": { - "default": 0, - "LgnbNMLaxandgMQq": 3 - }, - "_id": "TVeEyqmPPiRa2r3i", - "sort": 300000, - "_key": "!items!TVeEyqmPPiRa2r3i" -} diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index 95f42c06..953b3a2c 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -85,7 +85,7 @@ { "trigger": "dualityRoll", "triggeringActorType": "self", - "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n