diff --git a/lang/en.json b/lang/en.json index 82193133..c9d21944 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1031,7 +1031,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": { @@ -2556,6 +2557,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" diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index b29437bf..26ae484b 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -1,4 +1,4 @@ -import { refreshIsAllowed } from '../../../helpers/utils.mjs'; +import { RefreshFeatures } from '../../../helpers/utils.mjs'; const { HandlebarsApplicationMixin } = foundry.applications.api; const { AbstractSidebarTab } = foundry.applications.sidebar; @@ -54,73 +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) { - 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 */ /* -------------------------------------------- */ @@ -133,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 a38d1c8a..d46db23a 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -202,7 +202,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/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/settings/Automation.mjs b/module/data/settings/Automation.mjs index e9952b1c..20fe0baf 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 0c9d5e41..ef5f9434 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'; @@ -197,7 +198,7 @@ export default class DamageRoll extends DHRoll { // Bardic Rally const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.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 cafe0cff..9037250a 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.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 e8bea0bf..787797ff 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 57badd89..c8b62ff6 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -472,7 +472,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 => @@ -557,3 +557,121 @@ 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) { + 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/system.json b/system.json index 40de5fa1..fc5e1615 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.7.3", + "version": "1.8.0", "compatibility": { "minimum": "13.346", "verified": "13.351", diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index c5f4d871..65bafab8 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -14,6 +14,7 @@ {{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}} + {{formGroup settingFields.schema.fields.vulnerableAutomation value=settingFields._source.vulnerableAutomation localize=true}} {{formGroup settingFields.schema.fields.countdownAutomation value=settingFields._source.countdownAutomation localize=true}} {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}