Compare commits

...

4 commits

14 changed files with 195 additions and 202 deletions

View file

@ -1031,7 +1031,8 @@
}, },
"vulnerable": { "vulnerable": {
"name": "Vulnerable", "name": "Vulnerable",
"description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable cant 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 cant be made to take the condition again.",
"autoAppliedByLabel": "Max Stress"
} }
}, },
"CountdownType": { "CountdownType": {
@ -2556,6 +2557,10 @@
"gm": { "label": "GM" }, "gm": { "label": "GM" },
"players": { "label": "Players" } "players": { "label": "Players" }
}, },
"vulnerableAutomation": {
"label": "Vulnerable Automation",
"hint": "Automatically apply the Vulnerable condition when a actor reaches max stress"
},
"countdownAutomation": { "countdownAutomation": {
"label": "Countdown Automation", "label": "Countdown Automation",
"hint": "Automatically progress countdowns based on their progression settings" "hint": "Automatically progress countdowns based on their progression settings"

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs'; import { RefreshFeatures } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar; const { AbstractSidebarTab } = foundry.applications.sidebar;
@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
return context; 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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
static async #refreshActors() { static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys); await RefreshFeatures(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); 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(); this.render();
} }
} }

View file

@ -202,7 +202,8 @@ export const conditions = () => ({
id: 'vulnerable', id: 'vulnerable',
name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name',
img: 'icons/magic/control/silhouette-fall-slip-prone.webp', 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: { hidden: {
id: 'hidden', id: 'hidden',

View file

@ -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)
);
}
}
}
} }

View file

@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' 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({ countdownAutomation: new fields.BooleanField({
required: true, required: true,
initial: true, initial: true,

View file

@ -1,4 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
@ -197,7 +198,7 @@ export default class DamageRoll extends DHRoll {
// Bardic Rally // Bardic Rally
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); 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; return a;
}, []); }, []);
if (rallyChoices.length) { if (rallyChoices.length) {

View file

@ -1,6 +1,6 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.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 { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll {
setRallyChoices() { setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => { return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); 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; return a;
}, []); }, []);
} }

View file

@ -934,10 +934,23 @@ export default class DhpActor extends Actor {
/** Get active effects */ /** Get active effects */
getActiveEffects() { getActiveEffects() {
const conditions = CONFIG.DH.GENERAL.conditions();
const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status]));
const autoVulnerableActive = this.system.isAutoVulnerableActive;
return this.effects return this.effects
.filter(x => !x.disabled) .filter(x => !x.disabled)
.reduce((acc, effect) => { .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); acc.push(effect);
const currentStatusActiveEffects = acc.filter( const currentStatusActiveEffects = acc.filter(

View file

@ -472,7 +472,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) {
case CONFIG.DH.GENERAL.refreshTypes.scene.id: case CONFIG.DH.GENERAL.refreshTypes.scene.id:
case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.session.id:
case CONFIG.DH.GENERAL.refreshTypes.longRest.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: case CONFIG.DH.GENERAL.refreshTypes.shortRest.id:
return allowedTypes.some( return allowedTypes.some(
x => x =>
@ -557,3 +557,121 @@ export function calculateExpectedValue(formulaOrTerms) {
: [formulaOrTerms]; : [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); 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;
}

View file

@ -20,10 +20,6 @@
{ {
"type": "class", "type": "class",
"item": "Compendium.daggerheart.classes.Item.PydiMnNCKpd44SGS" "item": "Compendium.daggerheart.classes.Item.PydiMnNCKpd44SGS"
},
{
"type": "class",
"item": "Compendium.daggerheart.classes.Item.TVeEyqmPPiRa2r3i"
} }
], ],
"subclasses": [ "subclasses": [

View file

@ -63,7 +63,7 @@
{ {
"key": "system.bonuses.rally", "key": "system.bonuses.rally",
"mode": 2, "mode": 2,
"value": "d6", "value": "6 + min((floor(@system.levelData.level.current / 5)*2), 2)",
"priority": null "priority": null
} }
], ],

View file

@ -1,99 +0,0 @@
{
"folder": "C9y59fIkq50d3SyD",
"name": "Rally (Level 5)",
"type": "feature",
"img": "icons/tools/instruments/drum-hand-tan.webp",
"system": {
"description": "<p>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.</p>",
"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"
}

View file

@ -2,7 +2,7 @@
"id": "daggerheart", "id": "daggerheart",
"title": "Daggerheart", "title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system", "description": "An unofficial implementation of the Daggerheart system",
"version": "1.7.3", "version": "1.8.0",
"compatibility": { "compatibility": {
"minimum": "13.346", "minimum": "13.346",
"verified": "13.351", "verified": "13.351",

View file

@ -14,6 +14,7 @@
{{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}} {{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}}
</div> </div>
{{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.countdownAutomation value=settingFields._source.countdownAutomation localize=true}}
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}}
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}