Compare commits

..

16 commits

Author SHA1 Message Date
WBHarry
876e496d24 Merged with main 2026-03-07 01:39:52 +01:00
WBHarry
f1f5102af1 Raised version 2026-03-07 01:33:45 +01:00
WBHarry
a4f8c67707
Added vulnerable condition automation (#1704) 2026-03-07 01:32:36 +01:00
WBHarry
83c3da0130
Fixed so that Rally dice works as a single active effect for bard (#1708) 2026-03-07 00:08:03 +01:00
WBHarry
92d8c2ca18 Moved RefreshFeatures to utils and added a proper description 2026-03-07 00:06:12 +01:00
WBHarry
17aa0680d2
[Fix] 1697 - DamageRolls DiceSoNice (#1706)
* .

* .

* .
2026-03-06 13:07:55 +01:00
WBHarry
5732639391
. (#1705) 2026-03-06 11:07:52 +01:00
WBHarry
9bfe3505bf
[Fix] 1696 - Homebrew Fixes (#1707)
* SettingsActiveEffectConfig was out of date, making it error

* Other two fixes

* .

* .
2026-03-06 11:03:55 +01:00
WBHarry
9cba77ec11
[Fix] 1687 - Item Transfer Without Permissions (#1691)
* Fixed so that transfering items completly aborts if lacking permissions

* .
2026-03-05 22:14:07 +01:00
Carlos Fernandez
0675e1f019
Add recall cost to domain cards in grid view (#1700) 2026-03-05 21:31:49 +01:00
Carlos Fernandez
1212bd01f8
Increase the click area of sidebar and inventory control buttons (#1703) 2026-03-05 21:30:31 +01:00
Psitacus
3267f3f531
fix instances of rolls being called checks (#1702)
Co-authored-by: Psitacus <walther.johnson@ucalgary.ca>
2026-03-05 09:56:35 +01:00
WBHarry
986544a653
[Fix] 1689 - Missing Feature Errors (#1690)
* Fixed so that weaponfeatures and armorFeatures are tolerant of features having been removed

* .
2026-03-04 22:16:11 +10:00
WBHarry
5459581f7f
Fixed styling in firefox (#1692) 2026-03-04 01:10:40 +01:00
WBHarry
0d0b5125ba
[Fix] 1683 - Strange Patterns Explanation (#1693)
* Added an explanation text to Strange Patterns trigger dialog

* Update lang/en.json

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>

---------

Co-authored-by: Chris Ryan <73275196+chrisryan10@users.noreply.github.com>
2026-03-02 09:37:33 +01:00
WBHarry
c48842dd2d Fixed error on deleting a sceneEnvironment item 2026-02-26 20:04:59 +01:00
30 changed files with 347 additions and 321 deletions

View file

@ -1042,7 +1042,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": {
@ -1177,12 +1178,12 @@
}, },
"far": { "far": {
"name": "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" "short": "Far"
}, },
"veryFar": { "veryFar": {
"name": "Very Far", "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" "short": "V. Far"
} }
}, },
@ -1305,6 +1306,7 @@
"triggerTexts": { "triggerTexts": {
"strangePatternsContentTitle": "Matched {nr} times.", "strangePatternsContentTitle": "Matched {nr} times.",
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.", "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?", "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." "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" }, "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"
@ -2820,7 +2826,7 @@
"title": "Domain Card" "title": "Domain Card"
}, },
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Roll"
}, },
"effectSummary": { "effectSummary": {
"title": "Effects Applied", "title": "Effects Applied",
@ -2835,7 +2841,7 @@
"selectLeader": "Select a Leader", "selectLeader": "Select a Leader",
"selectMember": "Select a Member", "selectMember": "Select a Member",
"rerollTitle": "Reroll Group Roll", "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", "rerollTooltip": "Reroll",
"wholePartySelected": "The whole party is selected" "wholePartySelected": "The whole party is selected"
}, },
@ -3001,7 +3007,8 @@
"tokenActorMissing": "{name} is missing an Actor", "tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors", "tokenActorsMissing": "[{names}] missing Actors",
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "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": { "Sidebar": {
"actorDirectory": { "actorDirectory": {

View file

@ -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(); 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(); this.render();
} }
@ -246,6 +248,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({ await this.settings.updateSource({
[`${path}.${id}`]: _del [`${path}.${id}`]: _del
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render(); this.render();
} }

View file

@ -4,6 +4,43 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
constructor(options) { constructor(options) {
super(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 ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const getAllLeaves = (root, group, parentPath = '') => { const getAllLeaves = (root, group, parentPath = '') => {
@ -23,7 +60,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return leaves; 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; if (ignoredActorKeys.includes(key)) return acc;
const model = game.system.api.models.actors[key]; 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) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices; const changeChoices = this.changeChoices;

View file

@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
super({}); super({});
this.effect = foundry.utils.deepClone(effect); this.effect = foundry.utils.deepClone(effect);
const ignoredActorKeys = ['config', 'DhEnvironment']; this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
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;
}, []);
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {

View file

@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
return context; return context;
} }
static async updateData(event, element, formData) { static async updateData(_event, _element, formData) {
const data = foundry.utils.expandObject(formData.object); const data = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.move, data); await this.updateMove({
[`${this.movePath}`]: data
});
this.render(); this.render();
} }
@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
} }
); );
await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action }); await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect);
if (!updatedEffect) return; if (!updatedEffect) return;
await this.settings.updateSource({ await this.updateMove({
[`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => {
acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect);
return acc; return acc;
}, []) }, [])
}); });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} else { } else {
const action = this.move.actions.get(id); const action = this.move.actions.get(id);
@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
: existingEffectIndex === -1 : existingEffectIndex === -1
? [...currentEffects, effectData] ? [...currentEffects, effectData]
: currentEffects.with(existingEffectIndex, effectData); : currentEffects.with(existingEffectIndex, effectData);
await this.settings.updateSource({ await this.updateMove({
[`${this.movePath}.effects`]: updatedEffects [`${this.movePath}.effects`]: updatedEffects
}); });
} }
await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
return updatedEffects; return updatedEffects;
}).render(true); }).render(true);
@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App
}); });
} }
} }
await this.settings.updateSource({ await this.updateMove({
[this.movePath]: { [this.movePath]: {
effects: move.effects.filter(x => x.id !== id), effects: move.effects.filter(x => x.id !== id),
actions: move.actions actions: move.actions
} }
}); });
} else { } 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(); this.render();
} }
static async addEffect(_, target) { static async addEffect() {
const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`);
await this.settings.updateSource({
await this.updateMove({
[`${this.movePath}.effects`]: [ [`${this.movePath}.effects`]: [
...currentEffects, ...currentEffects,
game.system.api.data.activeEffects.BaseEffect.getDefaultObject() game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
] ]
}); });
this.move = foundry.utils.getProperty(this.settings, this.movePath);
this.render(); this.render();
} }
async updateMove(update) {
await this.settings.updateSource(update);
this.move = foundry.utils.getProperty(this.settings, this.movePath);
}
static resetMoves() {} static resetMoves() {}
_filterTabs(tabs) { _filterTabs(tabs) {

View file

@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
import DhpActor from '../../../documents/actor.mjs'; import DhpActor from '../../../documents/actor.mjs';
import DHItem from '../../../documents/item.mjs';
export default class Party extends DHBaseActorSheet { export default class Party extends DHBaseActorSheet {
constructor(options) { constructor(options) {
@ -269,15 +268,6 @@ export default class Party extends DHBaseActorSheet {
).render({ force: true }); ).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 */ /* Filter Tracking */
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
], ],
dragDrop: [ dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }, { 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] 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; return context;
@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
currency currency
}); });
if (quantity) { 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 }); this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
} }
return; return;
@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Handling transfer of inventoryItems */ /* Handling transfer of inventoryItems */
if (item.system.metadata.isInventoryItem) { 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) { if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId); const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({ 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) {
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)); const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) { if (existingItem) {
await existingItem.update({ 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 { } else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]); 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) { async _onDragStart(event) {
// Handle drag/dropping currencies // Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]"); const currencyEl = event.currentTarget.closest('.currency[data-currency]');
if (currencyEl) { if (currencyEl) {
const currency = currencyEl.dataset.currency; const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid }; 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.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
return; return;
} }
const item = await getDocFromElement(event.target); const item = await getDocFromElement(event.target);
if (item) { if (item) {
const dragData = { const dragData = {

View file

@ -1,4 +1,4 @@
import { expireActiveEffects, 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,75 +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) {
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 */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -135,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

@ -206,7 +206,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

@ -467,9 +467,7 @@ export const allArmorFeatures = () => {
}; };
export const orderedArmorFeatures = () => { export const orderedArmorFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures const allFeatures = allArmorFeatures();
.armorFeatures;
const allFeatures = { ...armorFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => { const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key]; const feature = allFeatures[key];
return { return {
@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => {
}; };
export const orderedWeaponFeatures = () => { export const orderedWeaponFeatures = () => {
const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures const allFeatures = allWeaponFeatures();
.weaponFeatures;
const allFeatures = { ...weaponFeatures, ...homebrewFeatures };
const all = Object.keys(allFeatures).map(key => { const all = Object.keys(allFeatures).map(key => {
const feature = allFeatures[key]; const feature = allFeatures[key];
return { return {

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

@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem {
armorFeatures: new fields.ArrayField( armorFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true
choices: CONFIG.DH.ITEM.allArmorFeatures,
blank: true
}), }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })), effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: 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() { async getDescriptionData() {
const baseDescription = this.description; const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); 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( const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/armor/description.hbs', 'systems/daggerheart/templates/sheets/items/armor/description.hbs',

View file

@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem {
weaponFeatures: new fields.ArrayField( weaponFeatures: new fields.ArrayField(
new fields.SchemaField({ new fields.SchemaField({
value: new fields.StringField({ value: new fields.StringField({
required: true, required: true
choices: CONFIG.DH.ITEM.allWeaponFeatures,
blank: true
}), }),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })), effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: 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 burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); 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( const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs', 'systems/daggerheart/templates/sheets/items/weapon/description.hbs',

View file

@ -75,7 +75,7 @@ export default class RegisteredTriggers extends Map {
unregisterSceneEnvironmentTriggers(flagSystemData) { unregisterSceneEnvironmentTriggers(flagSystemData) {
const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData); const sceneData = new game.system.api.data.scenes.DHScene(flagSystemData);
for (const environment of sceneData.sceneEnvironments) { for (const environment of sceneData.sceneEnvironments) {
if (environment.pack) continue; if (!environment || environment.pack) continue;
this.unregisterItemTriggers(environment.system.features); this.unregisterItemTriggers(environment.system.features);
} }
} }

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';
@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll {
static async buildPost(roll, config, message) { static async buildPost(roll, config, message) {
const chatMessage = config.source?.message const chatMessage = config.source?.message
? ui.chat.collection.get(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) { if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls( const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) 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.whisper?.length > 0 ? chatMessage.whisper : null,
chatMessage.blind chatMessage.blind
); );
config.mute = true;
} }
await super.buildPost(roll, config, message); 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) { static unifyDamageRoll(rolls) {
@ -192,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.system.changes.find(ch => ch.key === 'system.bonuses.rally'); 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; 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.system.changes.find(ch => ch.key === 'system.bonuses.rally'); 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; 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

@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
}), }),
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags, maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
dropdown: { dropdown: {
searchKeys: ['value', 'name'],
mapValueTo: 'name', mapValueTo: 'name',
searchKeys: ['value'],
enabled: 0, enabled: 0,
maxItems: 100, maxItems: 100,
closeOnSelect: true, closeOnSelect: true,
@ -479,7 +479,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 =>
@ -602,3 +602,123 @@ 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) {
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;
}

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

@ -85,7 +85,7 @@
{ {
"trigger": "dualityRoll", "trigger": "dualityRoll",
"triggeringActorType": "self", "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 <div><div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}</div>\n <div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}</div>\n <div class=\"flexrow\" style=\"gap: 8px;\">\n <button type=\"button\" id=\"hopeButton\">\n <i class=\"fa-solid fa-hands-holding\"></i>\n <label>0</label>\n </button>\n <button type=\"button\" id=\"stressButton\">\n <i class=\"fa-solid fa-bolt-lightning\"></i>\n <label>0</label>\n </button>\n </div>\n</div>`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;" "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 <div><div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}</div>\n <div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}</div>\n<div>${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}</div>\n <div class=\"flexrow\" style=\"gap: 8px;\">\n <button type=\"button\" id=\"hopeButton\">\n <i class=\"fa-solid fa-hands-holding\"></i>\n <label>0</label>\n </button>\n <button type=\"button\" id=\"stressButton\">\n <i class=\"fa-solid fa-bolt-lightning\"></i>\n <label>0</label>\n </button>\n </div>\n</div>`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;"
} }
] ]
} }

View file

@ -53,7 +53,7 @@
"difficulty": null, "difficulty": null,
"damageMod": "none" "damageMod": "none"
}, },
"name": "Agility Check", "name": "Agility Roll",
"img": "icons/skills/melee/sword-engraved-glow-purple.webp", "img": "icons/skills/melee/sword-engraved-glow-purple.webp",
"range": "close" "range": "close"
} }

View file

@ -103,10 +103,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: end; justify-content: end;
gap: 8px;
a { a {
width: 15px; width: 20px;
text-align: center; text-align: center;
} }
@ -275,8 +274,10 @@
grid-area: controls; grid-area: controls;
align-self: start; align-self: start;
padding-top: 0.3125rem; padding-top: 0.3125rem;
gap: 4px;
margin-bottom: -1px; margin-bottom: -1px;
a {
width: 18px;
}
} }
> .item-labels { > .item-labels {
align-self: start; align-self: start;
@ -334,6 +335,27 @@
border-radius: 6px; border-radius: 6px;
} }
.recall-cost {
position: absolute;
right: 4px;
top: 4px;
width: 1.75em;
height: 1.75em;
align-items: center;
background: @dark-blue;
border-radius: 50%;
border: 1px solid @golden;
color: @golden;
display: flex;
justify-content: center;
padding-top: 0.1em; // compensate for font
i {
font-size: 0.68em;
}
}
.card-label { .card-label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -183,6 +183,11 @@
} }
} }
.domain-details {
display: flex;
flex-direction: column;
}
.level-details { .level-details {
align-self: center; align-self: center;
} }

View file

@ -158,7 +158,7 @@
.daggerheart, .daggerheart,
#chat-notifications { #chat-notifications {
.chat-message { .chat-message {
--text-color: @golden; --text-color: light-dark(@dark-blue, @golden);
--bg-color: @golden-40; --bg-color: @golden-40;
[data-use-perm='false'] { [data-use-perm='false'] {
@ -233,7 +233,7 @@
font-family: @font-subtitle; font-family: @font-subtitle;
font-size: var(--font-size-18); font-size: var(--font-size-18);
font-weight: bold; font-weight: bold;
color: light-dark(@dark-blue, var(--text-color)); color: var(--text-color);
margin-bottom: -2px; margin-bottom: -2px;
} }

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.autoExpireActiveEffects value=settingFields._source.autoExpireActiveEffects localize=true}} {{formGroup settingFields.schema.fields.autoExpireActiveEffects value=settingFields._source.autoExpireActiveEffects 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}}

View file

@ -1,5 +1,9 @@
<li class="card-item" data-item-uuid="{{item.uuid}}" data-type="domainCard"> <li class="card-item" data-item-uuid="{{item.uuid}}" data-type="domainCard">
<img src="{{item.img}}" data-action="useItem" class="card-img" /> <img src="{{item.img}}" data-action="useItem" class="card-img" />
<span class="item-icon recall-cost">
<span class="recall-value">{{item.system.recallCost}}</span>
<i class="fa-solid fa-bolt"></i>
</span>
<div class="card-label"> <div class="card-label">
<div <div
class="menu {{#if item.system.resource}}resource-menu{{/if}} {{#if (eq item.system.resource.type 'diceValue')}}dice-menu{{/if}}"> class="menu {{#if item.system.resource}}resource-menu{{/if}} {{#if (eq item.system.resource.type 'diceValue')}}dice-menu{{/if}}">

View file

@ -109,7 +109,7 @@ Parameters:
{{else if (eq type 'armor')}} {{else if (eq type 'armor')}}
<a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem" <a class="{{#unless item.system.equipped}}unequipped{{/unless}}" data-action="toggleEquipItem"
data-tooltip="DAGGERHEART.UI.Tooltip.{{ifThen item.system.equipped 'unequip' 'equip' }}"> data-tooltip="DAGGERHEART.UI.Tooltip.{{ifThen item.system.equipped 'unequip' 'equip' }}">
<i class="fa-solid fa-shield"></i> <i class="fa-solid fa-fw fa-shield"></i>
</a> </a>
{{/if}} {{/if}}
{{#if (eq type 'domainCard')}} {{#if (eq type 'domainCard')}}
@ -125,7 +125,7 @@ Parameters:
{{/if}} {{/if}}
{{#if (hasProperty item "toChat")}} {{#if (hasProperty item "toChat")}}
<a data-action="toChat" data-tooltip="DAGGERHEART.UI.Tooltip.sendToChat"> <a data-action="toChat" data-tooltip="DAGGERHEART.UI.Tooltip.sendToChat">
<i class="fa-regular fa-message"></i> <i class="fa-regular fa-fw fa-message"></i>
</a> </a>
{{/if}} {{/if}}
{{else}} {{else}}
@ -138,7 +138,7 @@ Parameters:
{{/unless}} {{/unless}}
{{#unless hideContextMenu}} {{#unless hideContextMenu}}
<a data-action="triggerContextMenu" data-tooltip="DAGGERHEART.UI.Tooltip.moreOptions"> <a data-action="triggerContextMenu" data-tooltip="DAGGERHEART.UI.Tooltip.moreOptions">
<i class="fa-solid fa-ellipsis-vertical"></i> <i class="fa-solid fa-fw fa-ellipsis-vertical"></i>
</a> </a>
{{/unless}} {{/unless}}
{{/if}} {{/if}}