Merge branch 'main' into feature/tag-team-rework

This commit is contained in:
WBHarry 2026-03-11 21:07:56 +01:00
commit 43114187b9
96 changed files with 1942 additions and 664 deletions

View file

@ -1,4 +1,5 @@
export { default as DhAppearanceSettings } from './appearanceSettings.mjs';
export { default as DhAutomationSettings } from './automationSettings.mjs';
export { default as DhHomebrewSettings } from './homebrewSettings.mjs';
export { default as DhMetagamingSettings } from './metagamingSettings.mjs';
export { default as DhVariantRuleSettings } from './variantRuleSettings.mjs';

View file

@ -31,8 +31,8 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
};
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' },

View file

@ -1,4 +1,5 @@
import { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -44,6 +45,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType,
addResource: this.addResource,
removeResource: this.removeResource,
resetResourceImage: this.resetResourceImage,
save: this.save,
resetTokenSizes: this.resetTokenSizes,
reset: this.reset
@ -56,6 +60,10 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' },
resources: {
template: 'systems/daggerheart/templates/settings/homebrew-settings/resources.hbs',
scrollable: ['.resource-types-container']
},
itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
@ -64,7 +72,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */
static TABS = {
main: {
tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }],
tabs: [
{ id: 'settings' },
{ id: 'domains' },
{ id: 'types' },
{ id: 'resources' },
{ id: 'itemFeatures' },
{ id: 'downtime' }
],
initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}
@ -77,9 +92,17 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const element of htmlElement.querySelectorAll('.path-field input'))
element.addEventListener('change', this.toggleResourceIsIcon.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
context.schemaFields = context.settingFields.schema.fields;
return context;
}
@ -103,6 +126,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null;
break;
case 'resources':
break;
case 'downtime':
context.restOptions = {
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
@ -124,6 +149,33 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
async toggleResourceIsIcon(event) {
const element = event.target.closest('.resource-icon-container');
const { actorType, resourceKey, imageKey } = element.dataset;
const current = this.settings.resources[actorType].resources[resourceKey].images[imageKey];
await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]: {
isIcon: !current.isIcon,
value: ''
}
});
this.render();
}
static async resetResourceImage(_event, button) {
const element = button.closest('.resource-icon-container');
const { actorType, resourceKey, imageKey } = element.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]:
Resource.getDefaultImageData(imageKey)
});
this.render();
}
static async changeCurrencyIcon(_, target) {
const type = target.dataset.currency;
const currentIcon = this.settings.currency[type].icon;
@ -187,6 +239,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
});
}
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
@ -227,6 +280,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
}
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
@ -246,6 +300,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({
[`${path}.-=${id}`]: null
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
@ -462,6 +518,58 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render();
}
static async addResource(_, target) {
const { actorType } = target.dataset;
const content = new foundry.data.fields.StringField({
label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.resourceIdentifier'),
required: true
}).toFormGroup({}, { name: 'identifier', localize: true }).outerHTML;
async function callback(_, button) {
const identifier = button.form.elements.identifier.value;
if (!identifier) return;
const sluggedIdentifier = slugify(identifier);
await this.settings.updateSource({
[`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier)
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
await foundry.applications.api.DialogV2.prompt({
content: content,
rejectClose: false,
modal: true,
ok: { callback: callback.bind(this) },
window: {
title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.setResourceIdentifier')
},
position: { width: 400 }
});
}
static async removeResource(_, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteResourceTitle`)
},
content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteResourceText')
});
if (!confirmed) return;
const { actorType, resourceKey } = target.dataset;
await this.settings.updateSource({
[`resources.${actorType}.resources.-=${resourceKey}`]: null
});
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close();

View file

@ -0,0 +1,62 @@
import { DhMetagaming } from '../../data/settings/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DhMetagamingSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhMetagaming(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-metagaming-settings',
classes: ['daggerheart', 'dh-style', 'dialog', 'setting'],
position: { width: '600', height: 'auto' },
window: {
icon: 'fa-solid fa-eye-low-vision'
},
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/settings/metagaming-settings/header.hbs' },
general: { template: 'systems/daggerheart/templates/settings/metagaming-settings/general.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/metagaming-settings/footer.hbs' }
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(_event, _element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async reset() {
this.settings = new DhMetagaming();
this.render();
}
static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, this.settings.toObject());
this.close();
}
}

View file

@ -28,6 +28,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
removeEffect: this.removeEffect,
addElement: this.addElement,
removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect,
addDamage: this.addDamage,
removeDamage: this.removeDamage,
@ -41,7 +42,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
submitOnChange: true,
closeOnSubmit: false
},
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }]
dragDrop: [{ dragSelector: null, dropSelector: '[data-is-drop-zone]', handlers: ['_onDrop'] }]
};
static PARTS = {
@ -120,6 +121,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this));
});
htmlElement.querySelectorAll('.transform-resource input').forEach(element => {
element.addEventListener('change', this.updateTransformResource.bind(this));
});
}
async _prepareContext(_options) {
@ -133,6 +138,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.summons.push({ actor, count: summon.count });
}
if (context.source.transform) {
const actor = await foundry.utils.fromUuid(context.source.transform.actorUUID);
context.transform = {
...context.source.transform,
actor:
actor ??
(context.source.transform.actorUUID && !actor
? { error: game.i18n.localize('DAGGERHEART.ACTIONS.Settings.transform.actorIsMissing') }
: null)
};
}
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
@ -266,6 +283,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
if (doc) return doc.sheet.render({ force: true });
}
static async removeTransformActor() {
const data = this.action.toObject();
data.transform.actorUUID = null;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
static addDamage(_event) {
if (!this.action.damage.parts) return;
const data = this.action.toObject(),
@ -346,6 +369,14 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
updateTransformResource(event) {
event.stopPropagation();
const data = this.action.toObject();
data.transform.resourceRefresh[event.target.dataset.resource] = event.target.checked;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
/** Specific implementation in extending classes **/
static async addEffect(_event) {}
static removeEffect(_event, _button) {}
@ -364,6 +395,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return;
}
const dropZone = event.target.closest('[data-is-drop-zone]');
if (!dropZone) return;
switch (dropZone.id) {
case 'summon-drop-zone':
return this.onSummonDrop(data);
case 'transform-drop-zone':
return this.onTransformDrop(data);
}
}
async onSummonDrop(data) {
const actionData = this.action.toObject();
let countvalue = 1;
for (const entry of actionData.summon) {
@ -380,4 +423,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
async onTransformDrop(data) {
const actionData = this.action.toObject();
actionData.transform.actorUUID = data.uuid;
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) });
}
}

View file

@ -4,6 +4,42 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
constructor(options) {
super(options);
this.changeChoices = DhActiveEffectConfig.getChangeChoices();
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'sheet', 'dh-style']
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' },
tabs: { template: 'templates/generic/tab-navigation.hbs' },
details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] },
settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' },
changes: {
template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
/**
* Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs
* @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]}
*/
static getChangeChoices() {
const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty'];
const getAllLeaves = (root, group, parentPath = '') => {
@ -23,7 +59,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return leaves;
};
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
return Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (ignoredActorKeys.includes(key)) return acc;
const model = game.system.api.models.actors[key];
@ -62,34 +98,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',
scrollable: ['ol[data-changes]']
},
footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' }
};
static TABS = {
sheet: {
tabs: [
{ id: 'details', icon: 'fa-solid fa-book' },
{ id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' },
{ id: 'changes', icon: 'fa-solid fa-gears' }
],
initial: 'details',
labelPrefix: 'EFFECT.TABS'
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const changeChoices = this.changeChoices;

View file

@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi
super({});
this.effect = foundry.utils.deepClone(effect);
const ignoredActorKeys = ['config', 'DhEnvironment'];
this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => {
if (!ignoredActorKeys.includes(key)) {
const model = game.system.api.models.actors[key];
const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model);
const group = game.i18n.localize(model.metadata.label);
const choices = CONFIG.Token.documentClass
.getTrackedAttributeChoices(attributes, model)
.map(x => ({ ...x, group: group }));
acc.push(...choices);
}
return acc;
}, []);
this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices();
}
static DEFAULT_OPTIONS = {

View file

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

View file

@ -32,6 +32,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty
},
@ -225,6 +226,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'loadout':
await this._prepareLoadoutContext(context, options);
break;
@ -239,6 +243,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
return context;
}
async _prepareHeaderContext(context, _options) {
context.hasExtraResources = Object.keys(CONFIG.DH.RESOURCE.character.all).some(
key => !CONFIG.DH.RESOURCE.character.base[key]
);
}
/**
* Prepare render context for the Loadout part.
* @param {ApplicationRenderContext} context
@ -922,6 +932,78 @@ export default class CharacterSheet extends DHBaseActorSheet {
});
}
static async #toggleResourceManagement(event, button) {
event.stopPropagation();
const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container');
if (existingTooltip) {
game.tooltip.dismissLockedTooltips();
return;
}
const extraResources = Object.values(CONFIG.DH.RESOURCE.character.all).reduce((acc, resource) => {
if (CONFIG.DH.RESOURCE.character.base[resource.id]) return acc;
const resourceData = this.document.system.resources[resource.id];
acc[resource.id] = {
id: resource.id,
label: game.i18n.localize(resource.label),
value: resourceData.value,
max: resourceData.max,
fullIcon: resource.images?.full ?? { value: 'fa-solid fa-circle', isIcon: true },
emptyIcon: resource.images?.empty ?? { value: 'fa-regular fa-circle', isIcon: true }
};
return acc;
}, {});
const html = document.createElement('div');
html.innerHTML = await foundry.applications.handlebars.renderTemplate(
`systems/daggerheart/templates/ui/tooltip/resourceManagement.hbs`,
{
resources: extraResources
}
);
const target = button.closest('.resource-section');
game.tooltip.dismissLockedTooltips();
game.tooltip.activate(target, {
html,
locked: true,
cssClass: 'bordered-tooltip',
direction: 'DOWN',
noOffset: true
});
const resourceManager = target.querySelector('.resource-manager');
resourceManager.classList.toggle('inverted');
Hooks.once(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed, () => {
resourceManager.classList.toggle('inverted');
});
for (const element of html.querySelectorAll('.resource-value'))
element.addEventListener('click', this.onUpdateResource.bind(this));
}
async onUpdateResource(event) {
const target = event.target.closest('.resource-value');
const { resource, value: textValue } = target.dataset;
const inputValue = Number.parseInt(textValue);
const decreasing = inputValue <= this.document.system.resources[resource].value;
const value = decreasing ? inputValue - 1 : inputValue;
await this.document.update({ [`system.resources.${resource}.value`]: value }, { render: false });
/* Update resource symbols */
const section = target.closest('.resource-section');
for (const element of section.querySelectorAll('.resource-value')) {
const showFull = Number.parseInt(element.dataset.value) <= value;
element.querySelector('.full').classList.toggle('hidden', !showFull);
element.querySelector('.empty').classList.toggle('hidden', showFull);
}
}
/**
* Open the downtime application.
* @type {ApplicationClickAction}

View file

@ -264,15 +264,6 @@ export default class Party extends DHBaseActorSheet {
).render({ force: true });
}
/**
* Get the set of ContextMenu options for Consumable and Loot.
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet}
* @protected
*/
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
}
/* -------------------------------------------- */
/* Filter Tracking */
/* -------------------------------------------- */

View file

@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC;
if (context.systemFields.attack)
if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
}
// Create fake fields for actor configurable max resource value.
const resourceConfig = CONFIG.DH.RESOURCE[this.actor.type]?.all;
if (resourceConfig) {
const relevant = ['hitPoints', 'stress'].filter(r => r in resourceConfig);
context.resources = relevant.map(key => {
const data = this.actor._source.system.resources[key];
const config = resourceConfig[key];
return {
label: config.label,
name: `system.resources.${key}.max`,
value: data.max ?? config.max,
tooltip: key === 'hitPoints' ? game.i18n.localize('DAGGERHEART.UI.Tooltip.maxHPClassBound') : null,
field: new foundry.data.fields.NumberField({
initial: config.max,
integer: true,
label: game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', {
thing: game.i18n.localize(config.label)
})
})
};
});
}
return context;
}

View file

@ -691,6 +691,9 @@ export default function DHApplicationMixin(Base) {
case 'weapon':
presets.folder = 'equipments.folders.weapons';
break;
case 'feature':
presets.folder = 'features';
break;
case 'domainCard':
presets.folder = 'domains';
presets.filter = {

View file

@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
],
dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
{ dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null }
]
};
@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
value: context.source.system.gold[key]
};
}
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled);
context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled);
}
return context;
@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
currency
});
if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
originActor.update({
[`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity)
});
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
}
return;
@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Handling transfer of inventoryItems */
if (item.system.metadata.isInventoryItem) {
if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) {
return ui.notifications.error(
game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', {
user: game.user.name,
target: this.document.name
})
);
}
if (item.system.metadata.isQuantifiable) {
const actorItem = originActor.items.get(data.originId);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
});
if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
const existingItem = this.document.items.find(x => itemIsIdentical(x, item));
if (existingItem) {
await existingItem.update({
@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
]);
}
if (quantityTransfered === actorItem.system.quantity) {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
} else {
await actorItem.update({
'system.quantity': actorItem.system.quantity - quantityTransfered
});
}
}
} else {
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
await this.document.createEmbeddedDocuments('Item', [item.toObject()]);
await originActor.deleteEmbeddedDocuments('Item', [data.originId]);
}
}
}
@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
*/
async _onDragStart(event) {
// Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]");
const currencyEl = event.currentTarget.closest('.currency[data-currency]');
if (currencyEl) {
const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid };
@ -359,8 +370,8 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
return;
}
}
const item = await getDocFromElement(event.target);
if (item) {
const dragData = {

View file

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

View file

@ -251,6 +251,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) {
if (["weapon", "armor"].includes(item.type)) {
item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
item.system,
);
}
item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));

View file

@ -11,3 +11,4 @@ export * as settingsConfig from './settingsConfig.mjs';
export * as systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
export * as triggerConfig from './triggerConfig.mjs';
export * as resourceConfig from './resourceConfig.mjs';

View file

@ -35,6 +35,12 @@ export const actionTypes = {
icon: 'fa-ghost',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.summon.tooltip'
},
transform: {
id: 'transform',
name: 'DAGGERHEART.ACTIONS.TYPES.transform.name',
icon: 'fa-dragon',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.transform.tooltip'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.ACTIONS.TYPES.effect.name',

View file

@ -55,24 +55,6 @@ export const abilities = {
}
};
export const scrollingTextResource = {
hitPoints: {
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
reversed: true
},
stress: {
label: 'DAGGERHEART.GENERAL.stress',
reversed: true
},
hope: {
label: 'DAGGERHEART.GENERAL.hope'
},
armor: {
label: 'DAGGERHEART.GENERAL.armor',
reversed: true
}
};
export const featureProperties = {
agility: {
name: 'DAGGERHEART.CONFIG.Traits.agility.name',
@ -506,8 +488,8 @@ export const subclassFeatureLabels = {
* @property {number[]} damage
*/
/**
* @type {Record<string, Record<2 | 3 | 4, TierData>}
/**
* @type {Record<string, Record<2 | 3 | 4, TierData>}
* Scaling data used to change an adversary's tier. Each rank is applied incrementally.
*/
export const adversaryScalingData = {
@ -518,7 +500,7 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 1,
stress: 2,
attack: 2,
attack: 2
},
3: {
difficulty: 2,
@ -526,7 +508,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2,
attack: 2
},
4: {
difficulty: 2,
@ -534,7 +516,7 @@ export const adversaryScalingData = {
severeThreshold: 25,
hp: 1,
stress: 0,
attack: 2,
attack: 2
}
},
horde: {
@ -544,7 +526,7 @@ export const adversaryScalingData = {
severeThreshold: 8,
hp: 2,
stress: 0,
attack: 0,
attack: 0
},
3: {
difficulty: 2,
@ -552,7 +534,7 @@ export const adversaryScalingData = {
severeThreshold: 12,
hp: 0,
stress: 1,
attack: 1,
attack: 1
},
4: {
difficulty: 2,
@ -560,7 +542,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 2,
stress: 0,
attack: 0,
attack: 0
}
},
leader: {
@ -570,7 +552,7 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 0,
stress: 0,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -578,7 +560,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 1,
stress: 0,
attack: 2,
attack: 2
},
4: {
difficulty: 2,
@ -586,7 +568,7 @@ export const adversaryScalingData = {
severeThreshold: 25,
hp: 1,
stress: 1,
attack: 3,
attack: 3
}
},
minion: {
@ -596,7 +578,7 @@ export const adversaryScalingData = {
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -604,7 +586,7 @@ export const adversaryScalingData = {
severeThreshold: 0,
hp: 0,
stress: 1,
attack: 1,
attack: 1
},
4: {
difficulty: 2,
@ -612,7 +594,7 @@ export const adversaryScalingData = {
severeThreshold: 0,
hp: 0,
stress: 0,
attack: 1,
attack: 1
}
},
ranged: {
@ -622,7 +604,7 @@ export const adversaryScalingData = {
severeThreshold: 6,
hp: 1,
stress: 0,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -630,7 +612,7 @@ export const adversaryScalingData = {
severeThreshold: 14,
hp: 1,
stress: 1,
attack: 2,
attack: 2
},
4: {
difficulty: 2,
@ -638,7 +620,7 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
attack: 1
}
},
skulk: {
@ -648,7 +630,7 @@ export const adversaryScalingData = {
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -656,7 +638,7 @@ export const adversaryScalingData = {
severeThreshold: 12,
hp: 1,
stress: 1,
attack: 1,
attack: 1
},
4: {
difficulty: 2,
@ -664,7 +646,7 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
attack: 1
}
},
solo: {
@ -674,7 +656,7 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 0,
stress: 1,
attack: 2,
attack: 2
},
3: {
difficulty: 2,
@ -682,7 +664,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 2,
stress: 1,
attack: 2,
attack: 2
},
4: {
difficulty: 2,
@ -690,7 +672,7 @@ export const adversaryScalingData = {
severeThreshold: 25,
hp: 0,
stress: 1,
attack: 3,
attack: 3
}
},
standard: {
@ -700,7 +682,7 @@ export const adversaryScalingData = {
severeThreshold: 8,
hp: 0,
stress: 0,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -708,7 +690,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 1,
stress: 1,
attack: 1,
attack: 1
},
4: {
difficulty: 2,
@ -716,7 +698,7 @@ export const adversaryScalingData = {
severeThreshold: 15,
hp: 0,
stress: 1,
attack: 1,
attack: 1
}
},
support: {
@ -726,7 +708,7 @@ export const adversaryScalingData = {
severeThreshold: 8,
hp: 1,
stress: 1,
attack: 1,
attack: 1
},
3: {
difficulty: 2,
@ -734,7 +716,7 @@ export const adversaryScalingData = {
severeThreshold: 12,
hp: 0,
stress: 0,
attack: 1,
attack: 1
},
4: {
difficulty: 2,
@ -742,27 +724,27 @@ export const adversaryScalingData = {
severeThreshold: 10,
hp: 1,
stress: 1,
attack: 1,
attack: 1
}
}
};
/**
/**
* Scaling data used for an adversary's damage.
* Tier 4 is missing certain adversary types and therefore skews upwards.
* We manually set tier 4 data to hopefully lead to better results
*/
export const adversaryExpectedDamage = {
basic: {
1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
4: { mean: 26, deviation: 5.2 }
},
minion: {
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
2: { mean: 5, deviation: 0.816496580927726 },
3: { mean: 6.5, deviation: 2.1213203435596424 },
4: { mean: 11, deviation: 1 }
}
basic: {
1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
4: { mean: 26, deviation: 5.2 }
},
minion: {
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
2: { mean: 5, deviation: 0.816496580927726 },
3: { mean: 6.5, deviation: 2.1213203435596424 },
4: { mean: 11, deviation: 1 }
}
};

View file

@ -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',

View file

@ -1,3 +1,4 @@
export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle'
effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
};

View file

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

View file

@ -0,0 +1,88 @@
/**
* Full custom typing:
* id
* initial
* max
* reverse
* label
* images {
* full { value, isIcon, noColorFilter }
* empty { value, isIcon noColorFilter }
* }
*/
const characterBaseResources = Object.freeze({
hitPoints: {
id: 'hitPoints',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
maxLabel: 'DAGGERHEART.ACTORS.Character.maxHPBonus'
},
stress: {
id: 'stress',
initial: 0,
max: 6,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
},
hope: {
id: 'hope',
initial: 2,
reverse: false,
label: 'DAGGERHEART.GENERAL.hope'
}
});
const adversaryBaseResources = Object.freeze({
hitPoints: {
id: 'hitPoints',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.HitPoints.plural',
maxLabel: 'DAGGERHEART.ACTORS.Character.maxHPBonus'
},
stress: {
id: 'stress',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
}
});
const companionBaseResources = Object.freeze({
stress: {
id: 'stress',
initial: 0,
max: 0,
reverse: true,
label: 'DAGGERHEART.GENERAL.stress'
},
hope: {
id: 'hope',
initial: 0,
reverse: false,
label: 'DAGGERHEART.GENERAL.hope'
}
});
export const character = {
base: characterBaseResources,
custom: {}, // module stuff goes here
all: { ...characterBaseResources },
};
export const adversary = {
base: adversaryBaseResources,
custom: {}, // module stuff goes here
all: { ...adversaryBaseResources },
};
export const companion = {
base: companionBaseResources,
custom: {}, // module stuff goes here
all: { ...companionBaseResources },
};

View file

@ -3,6 +3,10 @@ export const menu = {
Name: 'GameSettingsAutomation',
Icon: 'fa-solid fa-robot'
},
Metagaming: {
Name: 'GameSettingsMetagaming',
Icon: 'fa-solid fa-eye-low-vision'
},
Homebrew: {
Name: 'GameSettingsHomebrew',
Icon: 'fa-solid fa-flask-vial'
@ -19,6 +23,7 @@ export const menu = {
export const gameSettings = {
Automation: 'Automation',
Metagaming: 'Metagaming',
Homebrew: 'Homebrew',
appearance: 'Appearance',
variantRules: 'VariantRules',

View file

@ -2,6 +2,7 @@ import * as GENERAL from './generalConfig.mjs';
import * as DOMAIN from './domainConfig.mjs';
import * as ENCOUNTER from './encounterConfig.mjs';
import * as ACTOR from './actorConfig.mjs';
import * as RESOURCE from './resourceConfig.mjs';
import * as ITEM from './itemConfig.mjs';
import * as SETTINGS from './settingsConfig.mjs';
import * as EFFECTS from './effectConfig.mjs';
@ -19,6 +20,7 @@ export const SYSTEM = {
GENERAL,
DOMAIN,
ACTOR,
RESOURCE,
ITEM,
SETTINGS,
EFFECTS,

View file

@ -7,6 +7,7 @@ import EffectAction from './effectAction.mjs';
import HealingAction from './healingAction.mjs';
import MacroAction from './macroAction.mjs';
import SummonAction from './summonAction.mjs';
import TransformAction from './transformAction.mjs';
export const actionsTypes = {
base: BaseAction,
@ -17,5 +18,6 @@ export const actionsTypes = {
summon: SummonAction,
effect: EffectAction,
macro: MacroAction,
beastform: BeastformAction
beastform: BeastformAction,
transform: TransformAction
};

View file

@ -197,7 +197,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
async executeWorkflow(config) {
for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return;
if ((await part.execute(config)) === false) return;
if ((await part.execute(config)) === false) return false;
if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return;
}
}
@ -224,7 +224,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
}
// Execute the Action Worflow in order based of schema fields
await this.executeWorkflow(config);
const result = await this.executeWorkflow(config);
if (result === false) return;
await config.resourceUpdates.updateResources();
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;

View file

@ -0,0 +1,5 @@
import DHBaseAction from './baseAction.mjs';
export default class DHTransformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'transform'];
}

View file

@ -2,7 +2,7 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-set
import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
import { bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
@ -65,10 +65,6 @@ export default class DhpAdversary extends DhCreature {
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
})
}),
resources: new fields.SchemaField({
hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true),
stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true)
}),
rules: new fields.SchemaField({
...commonActorRules()
}),
@ -191,6 +187,7 @@ export default class DhpAdversary extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
this.attack.roll.isStandardAttack = true;
}

View file

@ -213,7 +213,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
const textData = Object.keys(changes.system.resources).reduce((acc, key) => {
const resource = changes.system.resources[key];
if (resource.value !== undefined && resource.value !== this.resources[key].value) {
acc.push(getScrollTextData(this.resources, resource, key));
acc.push(getScrollTextData(this.parent, resource, key));
}
return acc;

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
@ -27,28 +27,6 @@ export default class DhCharacter extends DhCreature {
return {
...super.defineSchema(),
resources: new fields.SchemaField({
hitPoints: resourceField(
0,
0,
'DAGGERHEART.GENERAL.HitPoints.plural',
true,
'DAGGERHEART.ACTORS.Character.maxHPBonus'
),
stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.SchemaField(
{
value: new fields.NumberField({
initial: 2,
min: 0,
integer: true,
label: 'DAGGERHEART.GENERAL.hope'
}),
isReversed: new fields.BooleanField({ initial: false })
},
{ label: 'DAGGERHEART.GENERAL.hope' }
)
}),
traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
@ -609,6 +587,7 @@ export default class DhCharacter extends DhCreature {
}
prepareBaseData() {
super.prepareBaseData();
this.evasion += this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current;
@ -680,6 +659,7 @@ export default class DhCharacter extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
let baseHope = this.resources.hope.value;
if (this.companion) {
for (let levelKey in this.companion.system.levelData.levelups) {
@ -699,6 +679,7 @@ export default class DhCharacter extends DhCreature {
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.resources.armor = {
label: 'DAGGERHEART.GENERAL.armor',
value: this.armor?.system?.marks?.value ?? 0,
max: this.armorScore,
isReversed: true

View file

@ -4,7 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs';
import { resourceField, bonusField } from '../fields/actorField.mjs';
import { bonusField } from '../fields/actorField.mjs';
export default class DhCompanion extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -26,10 +26,6 @@ export default class DhCompanion extends DhCreature {
return {
...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }),
resources: new fields.SchemaField({
stress: resourceField(3, 0, 'DAGGERHEART.GENERAL.stress', true),
hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' })
}),
evasion: new fields.NumberField({
required: true,
min: 1,
@ -127,6 +123,7 @@ export default class DhCompanion extends DhCreature {
}
prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
for (let levelKey in this.levelData.levelups) {
@ -161,6 +158,7 @@ export default class DhCompanion extends DhCreature {
}
prepareDerivedData() {
super.prepareDerivedData();
/* Partner Related Setup */
if (this.partner) {
this.levelData.level.changed = this.partner.system.levelData.level.current;

View file

@ -1,3 +1,4 @@
import { ResourcesField } from '../fields/actorField.mjs';
import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor {
@ -7,6 +8,7 @@ export default class DhCreature extends BaseDataActor {
return {
...super.defineSchema(),
resources: new ResourcesField(this.metadata.type),
advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint'
@ -17,4 +19,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

@ -10,3 +10,4 @@ export { default as DamageField } from './damageField.mjs';
export { default as RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs';
export { default as SummonField } from './summonField.mjs';
export { default as TransformField } from './transformField.mjs';

View file

@ -0,0 +1,103 @@
const fields = foundry.data.fields;
export default class DHSummonField extends fields.SchemaField {
/**
* Action Workflow order
*/
static order = 130;
constructor(options = {}, context = {}) {
const transformFields = {
actorUUID: new fields.DocumentUUIDField({
type: 'Actor',
required: true
}),
resourceRefresh: new fields.SchemaField({
hitPoints: new fields.BooleanField({ initial: true }),
stress: new fields.BooleanField({ initial: true })
})
};
super(transformFields, options, context);
}
static async execute() {
if (!this.transform.actorUUID) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.noTransformActor'));
return false;
}
const baseActor = await foundry.utils.fromUuid(this.transform.actorUUID);
if (!baseActor) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.transformActorMissing'));
return false;
}
if (!canvas.scene) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.canvasError'));
return false;
}
if (this.actor.prototypeToken.actorLink) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.actorLinkError'));
return false;
}
if (!this.actor.token) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.prototypeError'));
return false;
}
const actor = await DHSummonField.getWorldActor(baseActor);
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const tokenSize = actor?.system.metadata.usesSize ? tokenSizes[actor.system.size] : actor.prototypeToken.width;
await this.actor.token.update(
{ ...actor.prototypeToken.toJSON(), actorId: actor.id, width: tokenSize, height: tokenSize },
{ diff: false, recursive: false, noHook: true }
);
if (this.actor.token.combatant) {
this.actor.token.combatant.update({ actorId: actor.id, img: actor.prototypeToken.texture.src });
}
const marks = { hitPoints: 0, stress: 0 };
if (!this.transform.resourceRefresh.hitPoints) {
marks.hitPoints = Math.min(
this.actor.system.resources.hitPoints.value,
this.actor.token.actor.system.resources.hitPoints.max - 1
);
}
if (!this.transform.resourceRefresh.stress) {
marks.stress = Math.min(
this.actor.system.resources.stress.value,
this.actor.token.actor.system.resources.stress.max - 1
);
}
if (marks.hitPoints || marks.stress) {
this.actor.token.actor.update({
'system.resources': {
hitPoints: { value: marks.hitPoints },
stress: { value: marks.stress }
}
});
}
const prevPosition = { ...this.actor.sheet.position };
this.actor.sheet.close();
this.actor.token.actor.sheet.render({ force: true, position: prevPosition });
}
/* Check for any available instances of the actor present in the world, or create a world actor based on compendium */
static async getWorldActor(baseActor) {
if (!baseActor.inCompendium) return baseActor;
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
if (worldActorCopy) return worldActorCopy;
}
const worldActor = await game.system.api.documents.DhpActor.create(baseActor.toObject());
return worldActor;
}
}

View file

@ -6,22 +6,6 @@ const attributeField = label =>
tierMarked: new fields.BooleanField({ initial: false })
});
const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) =>
new fields.SchemaField(
{
value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }),
max: new fields.NumberField({
initial: max,
integer: true,
label:
maxLabel ??
game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) })
}),
isReversed: new fields.BooleanField({ initial: reverse })
},
{ label }
);
const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({
cost: new fields.NumberField({
@ -37,4 +21,67 @@ const bonusField = label =>
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` })
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };
/**
* Field used for actor resources. It is a resource that validates dynamically based on the config.
* Because "max" may be defined during runtime, we don't attempt to clamp the maximum value.
*/
class ResourcesField extends fields.TypedObjectField {
constructor(actorType) {
super(
new fields.SchemaField({
value: new fields.NumberField({ min: 0, initial: 0, integer: true }),
// Some resources allow changing max. A null max means its the default
max: new fields.NumberField({ initial: null, integer: true, nullable: true })
})
);
this.actorType = actorType;
}
getInitialValue() {
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
return Object.values(resources).reduce((result, resource) => {
result[resource.id] = {
value: resource.initial,
max: null
};
return result;
}, {});
}
_validateKey(key) {
return key in CONFIG.DH.RESOURCE[this.actorType].all;
}
_cleanType(value, options) {
value = super._cleanType(value, options);
// If not partial, ensure all data exists
if (!options.partial) {
value = foundry.utils.mergeObject(this.getInitialValue(), value);
}
return value;
}
/** Initializes the original source data, returning prepared data */
initialize(...args) {
const data = super.initialize(...args);
const resources = CONFIG.DH.RESOURCE[this.actorType].all;
for (const [key, value] of Object.entries(data)) {
// TypedObjectField only calls _validateKey when persisting, so we also call it here
if (!this._validateKey(key)) {
delete value[key];
continue;
}
// Add basic prepared data.
const resource = resources[key];
value.label = resource.label;
value.isReversed = resources[key].reverse;
value.max = typeof resource.max === 'number' ? value.max ?? resource.max : null;
}
return data;
}
}
export { attributeField, ResourcesField, stressDamageReductionRule, bonusField };

View file

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

View file

@ -224,7 +224,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const armorChanged =
changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value;
if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') {
const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor');
const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor');
options.scrollingTextData = [armorData];
}

View file

@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem {
weaponFeatures: new fields.ArrayField(
new fields.SchemaField({
value: new fields.StringField({
required: true,
choices: CONFIG.DH.ITEM.allWeaponFeatures,
blank: true
required: true
}),
effectIds: new fields.ArrayField(new fields.StringField({ required: true })),
actionIds: new fields.ArrayField(new fields.StringField({ required: true }))
@ -114,24 +112,14 @@ export default class DHWeapon extends AttachableItem {
async getDescriptionData() {
const baseDescription = this.description;
const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`);
const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label);
const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`);
const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent);
const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label);
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]);
const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x);
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{
features,
tier,
trait,
range,
damage,
burden
item: this,
features
}
);

View file

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

View file

@ -145,6 +145,16 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
description: new fields.StringField()
})
),
resources: new fields.TypedObjectField(
new fields.SchemaField({
resources: new fields.TypedObjectField(new fields.EmbeddedDataField(Resource))
}),
{
initial: {
character: { resources: {} }
}
}
),
itemFeatures: new fields.SchemaField({
weaponFeatures: new fields.TypedObjectField(
new fields.SchemaField({
@ -185,4 +195,117 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
}
return source;
}
/** Invoked by the setting when data changes */
handleChange() {
if (this.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
}
this.refreshConfig();
this.#resetActors();
}
/** Update config values based on homebrew data. Make sure the references don't change */
refreshConfig() {
for (const [actorType, actorData] of Object.entries(this.resources)) {
const config = CONFIG.DH.RESOURCE[actorType];
for (const key of Object.keys(config.all)) {
delete config.all[key];
}
Object.assign(config.all, {
...Object.entries(actorData.resources).reduce((result, [key, value]) => {
result[key] = value.toObject();
result[key].id = key;
return result;
}, {}),
...config.custom,
...config.base,
});
}
}
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
#resetActors() {
const actors = new Set(
[
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat()
);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}
}
export class Resource extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
initial: new fields.NumberField({
required: true,
integer: true,
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.initial'
}),
max: new fields.NumberField({
nullable: true,
initial: null,
min: 0,
label: 'DAGGERHEART.GENERAL.max'
}),
label: new fields.StringField({ label: 'DAGGERHEART.GENERAL.label' }),
images: new fields.SchemaField({
full: imageIconField('fa solid fa-circle'),
empty: imageIconField('fa-regular fa-circle')
})
};
}
static getDefaultResourceData = label => {
const images = Resource.schema.fields.images.getInitialValue();
return {
initial: 0,
max: 0,
label: label ?? '',
images
};
};
static getDefaultImageData = imageKey => {
return Resource.schema.fields.images.fields[imageKey].getInitialValue();
};
}
const imageIconField = defaultValue =>
new foundry.data.fields.SchemaField(
{
value: new foundry.data.fields.StringField({
initial: defaultValue,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.value.label'
}),
isIcon: new foundry.data.fields.BooleanField({
required: true,
initial: true,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.isIcon.label'
}),
noColorFilter: new foundry.data.fields.BooleanField({
required: true,
initial: false,
label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.noColorFilter.label'
})
},
{ required: true }
);

View file

@ -0,0 +1,12 @@
export default class DhMetagaming extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
hideObserverPermissionInChat: new fields.BooleanField({
initial: false,
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint'
})
};
}
}

View file

@ -1,4 +1,5 @@
export { default as DhAppearance } from './Appearance.mjs';
export { default as DhAutomation } from './Automation.mjs';
export { default as DhHomebrew } from './Homebrew.mjs';
export { default as DhMetagaming } from './Metagaming.mjs';
export { default as DhVariantRules } from './VariantRules.mjs';

View file

@ -1,4 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs';
@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll {
static async buildPost(roll, config, message) {
const chatMessage = config.source?.message
? ui.chat.collection.get(config.source.message)
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode);
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC);
if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))
@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll {
chatMessage.whisper?.length > 0 ? chatMessage.whisper : null,
chatMessage.blind
);
config.mute = true;
}
await super.buildPost(roll, config, message);
if (config.source?.message) chatMessage.update({ 'system.damage': config.damage });
if (config.source?.message) {
chatMessage.update({ 'system.damage': config.damage });
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
}
static unifyDamageRoll(rolls) {
@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll {
// Bardic Rally
const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.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) {

View file

@ -143,8 +143,10 @@ export default class DHRoll extends Roll {
/** @inheritDoc */
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if (!this._evaluated) return;
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData);
return foundry.applications.handlebars.renderTemplate(template, { ...chatData, metagamingSettings });
}
/** @inheritDoc */

View file

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

View file

@ -959,10 +959,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(

View file

@ -68,8 +68,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
document = fromUuidSync(uuid);
if (!document) return;
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER'));
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
if (settings.hideObserverPermissionInChat)
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
});
if (this.isContentVisible) {

View file

@ -3,6 +3,7 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter
export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager {
#wide = false;
#bordered = false;
#active = false;
async activate(element, options = {}) {
const { TextEditor } = foundry.applications.ux;
@ -168,7 +169,100 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
}
}
super.activate(element, { ...options, html: html });
this.baseActivate(element, { ...options, html: html });
}
/* Need to pass more options to _setAnchor, so have to copy whole foundry method >_< */
async baseActivate(element, options) {
let { text, direction, cssClass, locked = false, html, content } = options;
if (content && !html) {
foundry.utils.logCompatibilityWarning(
'The content option has been deprecated in favor of the html option',
{ since: 13, until: 15, once: true }
);
html = content;
}
if (text && html) throw new Error('Cannot provide both text and html options to TooltipManager#activate.');
// Deactivate currently active element
this.deactivate();
// Check if the element still exists in the DOM.
if (!document.body.contains(element)) return;
// Mark the new element as active
this.#active = true;
this.element = element;
element.setAttribute('aria-describedby', 'tooltip');
html ||= element.dataset.tooltipHtml;
if (html) {
if (typeof html === 'string') this.tooltip.innerHTML = foundry.utils.cleanHTML(html);
else {
this.tooltip.innerHTML = ''; // Clear existing HTML
this.tooltip.appendChild(html);
}
} else {
text ||= element.dataset.tooltipText;
if (text) this.tooltip.textContent = text;
else {
text = element.dataset.tooltip;
// Localized message should be safe
if (game.i18n.has(text)) this.tooltip.innerHTML = game.i18n.localize(text);
else this.tooltip.innerHTML = foundry.utils.cleanHTML(text);
}
}
// Activate display of the tooltip
this.tooltip.removeAttribute('class');
this.tooltip.classList.add('active', 'themed', 'theme-dark');
this.tooltip.showPopover();
cssClass ??= element.closest('[data-tooltip-class]')?.dataset.tooltipClass;
if (cssClass) this.tooltip.classList.add(...cssClass.split(' '));
// Set tooltip position
direction ??= element.closest('[data-tooltip-direction]')?.dataset.tooltipDirection;
if (!direction) direction = this._determineDirection();
this._setAnchor(direction, options);
if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip();
}
_setAnchor(direction, options) {
const directions = this.constructor.TOOLTIP_DIRECTIONS;
const pad = this.constructor.TOOLTIP_MARGIN_PX;
const pos = this.element.getBoundingClientRect();
const { innerHeight, innerWidth } = this.tooltip.ownerDocument.defaultView;
const tooltipPadding = 16;
const horizontalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetWidth / 2 - pos.width / 2;
const verticalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetHeight / 2 - pos.height / 2;
const style = {};
switch (direction) {
case directions.DOWN:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.top = pos.bottom + pad;
break;
case directions.LEFT:
style.textAlign = 'left';
style.right = innerWidth - pos.left + pad;
style.top = pos.top - verticalOffset;
break;
case directions.RIGHT:
style.textAlign = 'right';
style.left = pos.right + pad;
style.top = pos.top - verticalOffset;
break;
case directions.UP:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.bottom = innerHeight - pos.top + pad;
break;
case directions.CENTER:
style.textAlign = 'center';
style.left = pos.left - horizontalOffset;
style.top = pos.top - verticalOffset;
break;
}
return this._setStyle(style);
}
_determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
@ -270,6 +364,12 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
return clone;
}
/**@inheritdoc */
dismissLockedTooltips() {
super.dismissLockedTooltips();
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed);
}
/** Get HTML for Battlepoints tooltip */
async getBattlepointHTML(combatId) {
const combat = game.combats.get(combatId);

View file

@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {}
}),
maxTags: typeof maxTags === 'function' ? maxTags() : maxTags,
dropdown: {
searchKeys: ['value', 'name'],
mapValueTo: 'name',
searchKeys: ['value'],
enabled: 0,
maxItems: 100,
closeOnSelect: true,
@ -378,17 +378,18 @@ export const arraysEqual = (a, b) =>
export const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));
export function getScrollTextData(resources, resource, key) {
const { reversed, label } = CONFIG.DH.ACTOR.scrollingTextResource[key];
export function getScrollTextData(actor, resource, key) {
const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS;
const resources = actor.system.resources;
const increased = resources[key].value < resource.value;
const value = -1 * (resources[key].value - resource.value);
const { label, isReversed } = resources[key];
const text = `${game.i18n.localize(label)} ${value.signedString()}`;
const stroke = increased ? (reversed ? 0xffffff : 0x000000) : reversed ? 0x000000 : 0xffffff;
const fill = increased ? (reversed ? 0x0032b1 : 0xffe760) : reversed ? 0xffe760 : 0x0032b1;
const direction = increased ? (reversed ? BOTTOM : TOP) : reversed ? TOP : BOTTOM;
const stroke = increased ? (isReversed ? 0xffffff : 0x000000) : isReversed ? 0x000000 : 0xffffff;
const fill = increased ? (isReversed ? 0x0032b1 : 0xffe760) : isReversed ? 0xffe760 : 0x0032b1;
const direction = increased ? (isReversed ? BOTTOM : TOP) : isReversed ? TOP : BOTTOM;
return { text, stroke, fill, direction };
}
@ -472,7 +473,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 +558,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;
}

View file

@ -17,9 +17,10 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/sheets/global/partials/resource-section/dice-value.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-section/die.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-bar.hbs',
'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs',
'systems/daggerheart/templates/components/card-preview.hbs',
'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs',
'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs',
'systems/daggerheart/templates/actionTypes/damage.hbs',
'systems/daggerheart/templates/actionTypes/resource.hbs',
@ -33,6 +34,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/actionTypes/beastform.hbs',
'systems/daggerheart/templates/actionTypes/countdown.hbs',
'systems/daggerheart/templates/actionTypes/summon.hbs',
'systems/daggerheart/templates/actionTypes/transform.hbs',
'systems/daggerheart/templates/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',

View file

@ -1,10 +1,11 @@
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhCountdowns from '../data/countdowns.mjs';
import { DhAppearance, DhAutomation, DhHomebrew, DhVariantRules } from '../data/settings/_module.mjs';
import { DhAppearance, DhAutomation, DhHomebrew, DhMetagaming, DhVariantRules } from '../data/settings/_module.mjs';
import {
DhAppearanceSettings,
DhAutomationSettings,
DhHomebrewSettings,
DhMetagamingSettings,
DhVariantRuleSettings
} from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
@ -38,17 +39,18 @@ const registerMenuSettings = () => {
type: DhAutomation
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, {
scope: 'world',
config: false,
type: DhMetagaming
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, {
scope: 'world',
config: false,
type: DhHomebrew,
onChange: value => {
if (value.maxFear) {
if (ui.resources) ui.resources.render({ force: true });
}
// Some homebrew settings may change sheets in various ways, so trigger a re-render
resetActors();
value.handleChange();
}
});
@ -76,6 +78,16 @@ const registerMenus = () => {
type: DhAutomationSettings,
restricted: true
});
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Metagaming.Name, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.label'),
hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.hint'),
icon: CONFIG.DH.SETTINGS.menu.Metagaming.Icon,
type: DhMetagamingSettings,
restricted: true
});
game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.label'),
@ -144,30 +156,8 @@ const registerNonConfigSettings = () => {
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'client',
scope: 'world',
config: false,
type: CompendiumBrowserSettings
});
};
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
function resetActors(actors) {
actors ??= [
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat();
actors = new Set(actors);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}