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

@ -233,6 +233,11 @@ Hooks.once('init', () => {
return handlebarsRegistration(); return handlebarsRegistration();
}); });
Hooks.on('i18nInit', () => {
// Setup homebrew resources
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).refreshConfig();
});
Hooks.on('setup', () => { Hooks.on('setup', () => {
CONFIG.statusEffects = [ CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),

View file

@ -74,6 +74,15 @@
"invalidDrop": "You can only drop Actor entities to summon.", "invalidDrop": "You can only drop Actor entities to summon.",
"chatMessageTitle": "Test2", "chatMessageTitle": "Test2",
"chatMessageHeaderTitle": "Summoning" "chatMessageHeaderTitle": "Summoning"
},
"transform": {
"name": "Transform",
"tooltip": "Transform one actor into another",
"noTransformActor": "There is no assigned actor to transform into",
"transformActorMissing": "The assigned actor to transform into does not exist. It was probably deleted or moved in/out of a compendium",
"canvasError": "There is no active scene.",
"prototypeError": "You can only use a transform action from a Token",
"actorLinkError": "You cannot transform a token with Actor Link set to true"
} }
}, },
"Config": { "Config": {
@ -129,6 +138,12 @@
}, },
"summon": { "summon": {
"dropSummonsHere": "Drop Summons Here" "dropSummonsHere": "Drop Summons Here"
},
"transform": {
"dropTransformHere": "Drop Transform Here",
"actorIsMissing": "The linked actor is missing. You should delete this link.",
"clearHitPoints": "Clear Hitpoints",
"clearStress": "Clear Stress"
} }
} }
}, },
@ -1031,7 +1046,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": {
@ -1166,12 +1182,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"
} }
}, },
@ -1294,6 +1310,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."
}, },
@ -2268,6 +2285,7 @@
"identify": "Identity", "identify": "Identity",
"imagePath": "Image Path", "imagePath": "Image Path",
"inactiveEffects": "Inactive Effects", "inactiveEffects": "Inactive Effects",
"initial": "Initial",
"inventory": "Inventory", "inventory": "Inventory",
"itemResource": "Item Resource", "itemResource": "Item Resource",
"itemQuantity": "Item Quantity", "itemQuantity": "Item Quantity",
@ -2559,6 +2577,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"
@ -2627,6 +2649,14 @@
"title": "Triggers" "title": "Triggers"
} }
}, },
"Metagaming": {
"FIELDS": {
"hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
}
}
},
"Homebrew": { "Homebrew": {
"newDowntimeMove": "Downtime Move", "newDowntimeMove": "Downtime Move",
"downtimeMove": "Downtime Move", "downtimeMove": "Downtime Move",
@ -2641,6 +2671,8 @@
"resetMovesText": "Are you sure you want to reset?", "resetMovesText": "Are you sure you want to reset?",
"deleteItemTitle": "Delete Homebrew Item", "deleteItemTitle": "Delete Homebrew Item",
"deleteItemText": "Are you sure you want to delete the item?", "deleteItemText": "Are you sure you want to delete the item?",
"deleteResourceTitle": "Delete Homebrew Resource",
"deleteResourceText": "Are you sure you want to delete the resource?",
"FIELDS": { "FIELDS": {
"maxFear": { "label": "Max Fear" }, "maxFear": { "label": "Max Fear" },
"maxHope": { "label": "Max Hope" }, "maxHope": { "label": "Max Hope" },
@ -2649,6 +2681,13 @@
"label": "Max Cards in Loadout", "label": "Max Cards in Loadout",
"hint": "Set to blank or 0 for unlimited maximum" "hint": "Set to blank or 0 for unlimited maximum"
}, },
"resources": {
"resources": {
"value": { "label": "Icon" },
"isIcon": { "label": "Font Awesome Icon" },
"noColorFilter": { "label": "Disable Color Filter" }
}
},
"maxDomains": { "label": "Max Class Domains", "hint": "Max domains you can set on a class" } "maxDomains": { "label": "Max Class Domains", "hint": "Max domains you can set on a class" }
}, },
"currency": { "currency": {
@ -2677,6 +2716,13 @@
"adversaryType": { "adversaryType": {
"title": "Custom Adversary Types", "title": "Custom Adversary Types",
"newType": "Adversary Type" "newType": "Adversary Type"
},
"resources": {
"typeTitle": "{type} Resources",
"filledIcon": "Filled Icon",
"emptyIcon": "Empty Icon",
"resourceIdentifier": "Resource Identifier",
"setResourceIdentifier": "Set Resource Identifier"
} }
}, },
"Menu": { "Menu": {
@ -2686,6 +2732,11 @@
"label": "Configure Automation", "label": "Configure Automation",
"hint": "Various settings automating resource management and more" "hint": "Various settings automating resource management and more"
}, },
"metagaming": {
"name": "Metagaming Settings",
"label": "Configure Metagaming",
"hint": "Various settings controlling the flow of information to players"
},
"homebrew": { "homebrew": {
"name": "Homebrew Settings", "name": "Homebrew Settings",
"label": "Configure Homebrew", "label": "Configure Homebrew",
@ -2809,7 +2860,7 @@
"title": "Domain Card" "title": "Domain Card"
}, },
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Roll"
}, },
"effectSummary": { "effectSummary": {
"title": "Effects Applied", "title": "Effects Applied",
@ -2824,7 +2875,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"
}, },
@ -2990,7 +3041,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

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

View file

@ -31,8 +31,8 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App
}; };
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.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' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' },
rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' }, rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' },
roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.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 { DhHomebrew } from '../../data/settings/_module.mjs';
import { Resource } from '../../data/settings/Homebrew.mjs';
import { slugify } from '../../helpers/utils.mjs'; import { slugify } from '../../helpers/utils.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -44,6 +45,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
addAdversaryType: this.addAdversaryType, addAdversaryType: this.addAdversaryType,
deleteAdversaryType: this.deleteAdversaryType, deleteAdversaryType: this.deleteAdversaryType,
selectAdversaryType: this.selectAdversaryType, selectAdversaryType: this.selectAdversaryType,
addResource: this.addResource,
removeResource: this.removeResource,
resetResourceImage: this.resetResourceImage,
save: this.save, save: this.save,
resetTokenSizes: this.resetTokenSizes, resetTokenSizes: this.resetTokenSizes,
reset: this.reset reset: this.reset
@ -56,6 +60,10 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' },
domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' },
types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.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' }, itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' },
downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' },
footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' }
@ -64,7 +72,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
/** @inheritdoc */ /** @inheritdoc */
static TABS = { static TABS = {
main: { 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', initial: 'settings',
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
} }
@ -77,9 +92,17 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); 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) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.settingFields = this.settings; context.settingFields = this.settings;
context.schemaFields = context.settingFields.schema.fields;
return context; return context;
} }
@ -103,6 +126,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] } ? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] }
: null; : null;
break; break;
case 'resources':
break;
case 'downtime': case 'downtime':
context.restOptions = { context.restOptions = {
shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(), shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(),
@ -124,6 +149,33 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); 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) { static async changeCurrencyIcon(_, target) {
const type = target.dataset.currency; const type = target.dataset.currency;
const currentIcon = this.settings.currency[type].icon; 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(); 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(); this.render();
} }
@ -246,6 +300,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
await this.settings.updateSource({ await this.settings.updateSource({
[`${path}.-=${id}`]: null [`${path}.-=${id}`]: null
}); });
game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.render(); this.render();
} }
@ -462,6 +518,58 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli
this.render(); 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() { static async save() {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject());
this.close(); 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, removeEffect: this.removeEffect,
addElement: this.addElement, addElement: this.addElement,
removeElement: this.removeElement, removeElement: this.removeElement,
removeTransformActor: this.removeTransformActor,
editEffect: this.editEffect, editEffect: this.editEffect,
addDamage: this.addDamage, addDamage: this.addDamage,
removeDamage: this.removeDamage, removeDamage: this.removeDamage,
@ -41,7 +42,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false closeOnSubmit: false
}, },
dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }] dragDrop: [{ dragSelector: null, dropSelector: '[data-is-drop-zone]', handlers: ['_onDrop'] }]
}; };
static PARTS = { static PARTS = {
@ -120,6 +121,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => { htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => {
element.addEventListener('change', this.updateSummonCount.bind(this)); element.addEventListener('change', this.updateSummonCount.bind(this));
}); });
htmlElement.querySelectorAll('.transform-resource input').forEach(element => {
element.addEventListener('change', this.updateTransformResource.bind(this));
});
} }
async _prepareContext(_options) { async _prepareContext(_options) {
@ -133,6 +138,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.summons.push({ actor, count: summon.count }); 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.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH; context.config = CONFIG.DH;
@ -266,6 +283,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
if (doc) return doc.sheet.render({ force: true }); 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) { static addDamage(_event) {
if (!this.action.damage.parts) return; if (!this.action.damage.parts) return;
const data = this.action.toObject(), 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) }); 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 **/ /** Specific implementation in extending classes **/
static async addEffect(_event) {} static async addEffect(_event) {}
static removeEffect(_event, _button) {} static removeEffect(_event, _button) {}
@ -364,6 +395,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
return; 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(); const actionData = this.action.toObject();
let countvalue = 1; let countvalue = 1;
for (const entry of actionData.summon) { 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 }); actionData.summon.push({ actorUUID: data.uuid, count: countvalue });
await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); 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) { 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',
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 +59,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,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) { _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}`]: null }); await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null });
} }
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

@ -32,6 +32,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
handleResourceDice: CharacterSheet.#handleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice,
advanceResourceDie: CharacterSheet.#advanceResourceDie, advanceResourceDie: CharacterSheet.#advanceResourceDie,
cancelBeastform: CharacterSheet.#cancelBeastform, cancelBeastform: CharacterSheet.#cancelBeastform,
toggleResourceManagement: CharacterSheet.#toggleResourceManagement,
useDowntime: this.useDowntime, useDowntime: this.useDowntime,
viewParty: CharacterSheet.#viewParty viewParty: CharacterSheet.#viewParty
}, },
@ -225,6 +226,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'header':
await this._prepareHeaderContext(context, options);
break;
case 'loadout': case 'loadout':
await this._prepareLoadoutContext(context, options); await this._prepareLoadoutContext(context, options);
break; break;
@ -239,6 +243,12 @@ export default class CharacterSheet extends DHBaseActorSheet {
return context; 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. * Prepare render context for the Loadout part.
* @param {ApplicationRenderContext} context * @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. * Open the downtime application.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -264,15 +264,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

@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
context.isNPC = this.actor.isNPC; context.isNPC = this.actor.isNPC;
if (context.systemFields.attack) if (context.systemFields.attack) {
context.systemFields.attack.fields = this.actor.system.attack.schema.fields; 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; return context;
} }

View file

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

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

View file

@ -1,4 +1,4 @@
import { refreshIsAllowed } from '../../../helpers/utils.mjs'; import { RefreshFeatures } from '../../../helpers/utils.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { AbstractSidebarTab } = foundry.applications.sidebar; const { AbstractSidebarTab } = foundry.applications.sidebar;
@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
return context; return context;
} }
async getRefreshables(types) {
const refreshedActors = {};
for (let actor of game.actors) {
if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
};
}
if (item.system.metadata?.hasActions) {
const refreshTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(types, action.uses.recovery)) {
refreshTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
return refreshedActors;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
static async #refreshActors() { static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys); await RefreshFeatures(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
this.render(); this.render();
} }
} }

View file

@ -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 */ /* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */
for (const item of this.items) { 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 = item.system.enrichedDescription =
(await item.system.getEnrichedDescription?.()) ?? (await item.system.getEnrichedDescription?.()) ??
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); (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 systemConfig from './system.mjs';
export * as itemBrowserConfig from './itemBrowserConfig.mjs'; export * as itemBrowserConfig from './itemBrowserConfig.mjs';
export * as triggerConfig from './triggerConfig.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', icon: 'fa-ghost',
tooltip: 'DAGGERHEART.ACTIONS.TYPES.summon.tooltip' 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: { effect: {
id: 'effect', id: 'effect',
name: 'DAGGERHEART.ACTIONS.TYPES.effect.name', 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 = { export const featureProperties = {
agility: { agility: {
name: 'DAGGERHEART.CONFIG.Traits.agility.name', name: 'DAGGERHEART.CONFIG.Traits.agility.name',
@ -518,7 +500,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 1, hp: 1,
stress: 2, stress: 2,
attack: 2, attack: 2
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -526,7 +508,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 1, hp: 1,
stress: 0, stress: 0,
attack: 2, attack: 2
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -534,7 +516,7 @@ export const adversaryScalingData = {
severeThreshold: 25, severeThreshold: 25,
hp: 1, hp: 1,
stress: 0, stress: 0,
attack: 2, attack: 2
} }
}, },
horde: { horde: {
@ -544,7 +526,7 @@ export const adversaryScalingData = {
severeThreshold: 8, severeThreshold: 8,
hp: 2, hp: 2,
stress: 0, stress: 0,
attack: 0, attack: 0
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -552,7 +534,7 @@ export const adversaryScalingData = {
severeThreshold: 12, severeThreshold: 12,
hp: 0, hp: 0,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -560,7 +542,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 2, hp: 2,
stress: 0, stress: 0,
attack: 0, attack: 0
} }
}, },
leader: { leader: {
@ -570,7 +552,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 0, hp: 0,
stress: 0, stress: 0,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -578,7 +560,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 1, hp: 1,
stress: 0, stress: 0,
attack: 2, attack: 2
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -586,7 +568,7 @@ export const adversaryScalingData = {
severeThreshold: 25, severeThreshold: 25,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 3, attack: 3
} }
}, },
minion: { minion: {
@ -596,7 +578,7 @@ export const adversaryScalingData = {
severeThreshold: 0, severeThreshold: 0,
hp: 0, hp: 0,
stress: 0, stress: 0,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -604,7 +586,7 @@ export const adversaryScalingData = {
severeThreshold: 0, severeThreshold: 0,
hp: 0, hp: 0,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -612,7 +594,7 @@ export const adversaryScalingData = {
severeThreshold: 0, severeThreshold: 0,
hp: 0, hp: 0,
stress: 0, stress: 0,
attack: 1, attack: 1
} }
}, },
ranged: { ranged: {
@ -622,7 +604,7 @@ export const adversaryScalingData = {
severeThreshold: 6, severeThreshold: 6,
hp: 1, hp: 1,
stress: 0, stress: 0,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -630,7 +612,7 @@ export const adversaryScalingData = {
severeThreshold: 14, severeThreshold: 14,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 2, attack: 2
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -638,7 +620,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
} }
}, },
skulk: { skulk: {
@ -648,7 +630,7 @@ export const adversaryScalingData = {
severeThreshold: 8, severeThreshold: 8,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -656,7 +638,7 @@ export const adversaryScalingData = {
severeThreshold: 12, severeThreshold: 12,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -664,7 +646,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
} }
}, },
solo: { solo: {
@ -674,7 +656,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 0, hp: 0,
stress: 1, stress: 1,
attack: 2, attack: 2
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -682,7 +664,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 2, hp: 2,
stress: 1, stress: 1,
attack: 2, attack: 2
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -690,7 +672,7 @@ export const adversaryScalingData = {
severeThreshold: 25, severeThreshold: 25,
hp: 0, hp: 0,
stress: 1, stress: 1,
attack: 3, attack: 3
} }
}, },
standard: { standard: {
@ -700,7 +682,7 @@ export const adversaryScalingData = {
severeThreshold: 8, severeThreshold: 8,
hp: 0, hp: 0,
stress: 0, stress: 0,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -708,7 +690,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -716,7 +698,7 @@ export const adversaryScalingData = {
severeThreshold: 15, severeThreshold: 15,
hp: 0, hp: 0,
stress: 1, stress: 1,
attack: 1, attack: 1
} }
}, },
support: { support: {
@ -726,7 +708,7 @@ export const adversaryScalingData = {
severeThreshold: 8, severeThreshold: 8,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
}, },
3: { 3: {
difficulty: 2, difficulty: 2,
@ -734,7 +716,7 @@ export const adversaryScalingData = {
severeThreshold: 12, severeThreshold: 12,
hp: 0, hp: 0,
stress: 0, stress: 0,
attack: 1, attack: 1
}, },
4: { 4: {
difficulty: 2, difficulty: 2,
@ -742,7 +724,7 @@ export const adversaryScalingData = {
severeThreshold: 10, severeThreshold: 10,
hp: 1, hp: 1,
stress: 1, stress: 1,
attack: 1, attack: 1
} }
} }
}; };
@ -753,16 +735,16 @@ export const adversaryScalingData = {
* We manually set tier 4 data to hopefully lead to better results * We manually set tier 4 data to hopefully lead to better results
*/ */
export const adversaryExpectedDamage = { export const adversaryExpectedDamage = {
basic: { basic: {
1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, 1: { mean: 7.321428571428571, deviation: 1.962519002770912 },
2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 },
3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, 3: { mean: 15.722222222222221, deviation: 2.486565208464823 },
4: { mean: 26, deviation: 5.2 } 4: { mean: 26, deviation: 5.2 }
}, },
minion: { minion: {
1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 },
2: { mean: 5, deviation: 0.816496580927726 }, 2: { mean: 5, deviation: 0.816496580927726 },
3: { mean: 6.5, deviation: 2.1213203435596424 }, 3: { mean: 6.5, deviation: 2.1213203435596424 },
4: { mean: 11, deviation: 1 } 4: { mean: 11, deviation: 1 }
} }
}; };

View file

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

View file

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

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

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

View file

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

View file

@ -7,6 +7,7 @@ import EffectAction from './effectAction.mjs';
import HealingAction from './healingAction.mjs'; import HealingAction from './healingAction.mjs';
import MacroAction from './macroAction.mjs'; import MacroAction from './macroAction.mjs';
import SummonAction from './summonAction.mjs'; import SummonAction from './summonAction.mjs';
import TransformAction from './transformAction.mjs';
export const actionsTypes = { export const actionsTypes = {
base: BaseAction, base: BaseAction,
@ -17,5 +18,6 @@ export const actionsTypes = {
summon: SummonAction, summon: SummonAction,
effect: EffectAction, effect: EffectAction,
macro: MacroAction, 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) { async executeWorkflow(config) {
for (const [key, part] of this.workflow) { for (const [key, part] of this.workflow) {
if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return; 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; 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 // 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(); await config.resourceUpdates.updateResources();
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; 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 { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs'; import { commonActorRules } from './base.mjs';
import DhCreature from './creature.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 { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';
@ -65,10 +65,6 @@ export default class DhpAdversary extends DhCreature {
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' 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({ rules: new fields.SchemaField({
...commonActorRules() ...commonActorRules()
}), }),
@ -191,6 +187,7 @@ export default class DhpAdversary extends DhCreature {
} }
prepareDerivedData() { prepareDerivedData() {
super.prepareDerivedData();
this.attack.roll.isStandardAttack = true; 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 textData = Object.keys(changes.system.resources).reduce((acc, key) => {
const resource = changes.system.resources[key]; const resource = changes.system.resources[key];
if (resource.value !== undefined && resource.value !== this.resources[key].value) { 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; return acc;

View file

@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import DhLevelData from '../levelData.mjs'; import DhLevelData from '../levelData.mjs';
import { commonActorRules } from './base.mjs'; import { commonActorRules } from './base.mjs';
import DhCreature from './creature.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 { ActionField } from '../fields/actionField.mjs';
import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs';
@ -27,28 +27,6 @@ export default class DhCharacter extends DhCreature {
return { return {
...super.defineSchema(), ...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({ traits: new fields.SchemaField({
agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'),
strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'), strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'),
@ -609,6 +587,7 @@ export default class DhCharacter extends DhCreature {
} }
prepareBaseData() { prepareBaseData() {
super.prepareBaseData();
this.evasion += this.class.value?.system?.evasion ?? 0; this.evasion += this.class.value?.system?.evasion ?? 0;
const currentLevel = this.levelData.level.current; const currentLevel = this.levelData.level.current;
@ -680,6 +659,7 @@ export default class DhCharacter extends DhCreature {
} }
prepareDerivedData() { prepareDerivedData() {
super.prepareDerivedData();
let baseHope = this.resources.hope.value; let baseHope = this.resources.hope.value;
if (this.companion) { if (this.companion) {
for (let levelKey in this.companion.system.levelData.levelups) { 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.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.resources.armor = { this.resources.armor = {
label: 'DAGGERHEART.GENERAL.armor',
value: this.armor?.system?.marks?.value ?? 0, value: this.armor?.system?.marks?.value ?? 0,
max: this.armorScore, max: this.armorScore,
isReversed: true isReversed: true

View file

@ -4,7 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import { ActionField } from '../fields/actionField.mjs'; import { ActionField } from '../fields/actionField.mjs';
import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import { adjustDice, adjustRange } from '../../helpers/utils.mjs';
import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.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 { export default class DhCompanion extends DhCreature {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion'];
@ -26,10 +26,6 @@ export default class DhCompanion extends DhCreature {
return { return {
...super.defineSchema(), ...super.defineSchema(),
partner: new ForeignDocumentUUIDField({ type: 'Actor' }), 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({ evasion: new fields.NumberField({
required: true, required: true,
min: 1, min: 1,
@ -127,6 +123,7 @@ export default class DhCompanion extends DhCreature {
} }
prepareBaseData() { prepareBaseData() {
super.prepareBaseData();
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;
for (let levelKey in this.levelData.levelups) { for (let levelKey in this.levelData.levelups) {
@ -161,6 +158,7 @@ export default class DhCompanion extends DhCreature {
} }
prepareDerivedData() { prepareDerivedData() {
super.prepareDerivedData();
/* Partner Related Setup */ /* Partner Related Setup */
if (this.partner) { if (this.partner) {
this.levelData.level.changed = this.partner.system.levelData.level.current; 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'; import BaseDataActor from './base.mjs';
export default class DhCreature extends BaseDataActor { export default class DhCreature extends BaseDataActor {
@ -7,6 +8,7 @@ export default class DhCreature extends BaseDataActor {
return { return {
...super.defineSchema(), ...super.defineSchema(),
resources: new ResourcesField(this.metadata.type),
advantageSources: new fields.ArrayField(new fields.StringField(), { advantageSources: new fields.ArrayField(new fields.StringField(), {
label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', label: 'DAGGERHEART.ACTORS.Character.advantageSources.label',
hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' 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 RollField } from './rollField.mjs';
export { default as MacroField } from './macroField.mjs'; export { default as MacroField } from './macroField.mjs';
export { default as SummonField } from './summonField.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 }) 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 => const stressDamageReductionRule = localizationPath =>
new fields.SchemaField({ new fields.SchemaField({
cost: new fields.NumberField({ cost: new fields.NumberField({
@ -37,4 +21,67 @@ const bonusField = label =>
dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` }) 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( 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

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

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 }))
@ -114,24 +112,14 @@ export default class DHWeapon extends AttachableItem {
async getDescriptionData() { async getDescriptionData() {
const baseDescription = this.description; 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 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',
{ {
features, item: this,
tier, features
trait,
range,
damage,
burden
} }
); );

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

@ -145,6 +145,16 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
description: new fields.StringField() 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({ itemFeatures: new fields.SchemaField({
weaponFeatures: new fields.TypedObjectField( weaponFeatures: new fields.TypedObjectField(
new fields.SchemaField({ new fields.SchemaField({
@ -185,4 +195,117 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
} }
return source; 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 DhAppearance } from './Appearance.mjs';
export { default as DhAutomation } from './Automation.mjs'; export { default as DhAutomation } from './Automation.mjs';
export { default as DhHomebrew } from './Homebrew.mjs'; export { default as DhHomebrew } from './Homebrew.mjs';
export { default as DhMetagaming } from './Metagaming.mjs';
export { default as DhVariantRules } from './VariantRules.mjs'; export { default as DhVariantRules } from './VariantRules.mjs';

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.changes.find(ch => ch.key === 'system.bonuses.rally'); const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value }); if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a; return a;
}, []); }, []);
if (rallyChoices.length) { if (rallyChoices.length) {

View file

@ -143,8 +143,10 @@ export default class DHRoll extends Roll {
/** @inheritDoc */ /** @inheritDoc */
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) { async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
if (!this._evaluated) return; 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 }); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, chatData); return foundry.applications.handlebars.renderTemplate(template, { ...chatData, metagamingSettings });
} }
/** @inheritDoc */ /** @inheritDoc */

View file

@ -1,6 +1,6 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import D20Roll from './d20Roll.mjs'; import D20Roll from './d20Roll.mjs';
import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll {
setRallyChoices() { setRallyChoices() {
return this.data?.parent?.appliedEffects.reduce((a, c) => { return this.data?.parent?.appliedEffects.reduce((a, c) => {
const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
if (change) a.push({ value: c.id, label: change.value }); if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) });
return a; return a;
}, []); }, []);
} }

View file

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

@ -68,8 +68,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
document = fromUuidSync(uuid); document = fromUuidSync(uuid);
if (!document) return; if (!document) return;
e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER'));
e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER')); 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) { if (this.isContentVisible) {

View file

@ -3,6 +3,7 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter
export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager {
#wide = false; #wide = false;
#bordered = false; #bordered = false;
#active = false;
async activate(element, options = {}) { async activate(element, options = {}) {
const { TextEditor } = foundry.applications.ux; 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) { _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) {
@ -270,6 +364,12 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
return clone; return clone;
} }
/**@inheritdoc */
dismissLockedTooltips() {
super.dismissLockedTooltips();
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed);
}
/** Get HTML for Battlepoints tooltip */ /** Get HTML for Battlepoints tooltip */
async getBattlepointHTML(combatId) { async getBattlepointHTML(combatId) {
const combat = game.combats.get(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, 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,
@ -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 const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));
export function getScrollTextData(resources, resource, key) { export function getScrollTextData(actor, resource, key) {
const { reversed, label } = CONFIG.DH.ACTOR.scrollingTextResource[key];
const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS; const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS;
const resources = actor.system.resources;
const increased = resources[key].value < resource.value; const increased = resources[key].value < resource.value;
const value = -1 * (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 text = `${game.i18n.localize(label)} ${value.signedString()}`;
const stroke = increased ? (isReversed ? 0xffffff : 0x000000) : isReversed ? 0x000000 : 0xffffff;
const stroke = increased ? (reversed ? 0xffffff : 0x000000) : reversed ? 0x000000 : 0xffffff; const fill = increased ? (isReversed ? 0x0032b1 : 0xffe760) : isReversed ? 0xffe760 : 0x0032b1;
const fill = increased ? (reversed ? 0x0032b1 : 0xffe760) : reversed ? 0xffe760 : 0x0032b1; const direction = increased ? (isReversed ? BOTTOM : TOP) : isReversed ? TOP : BOTTOM;
const direction = increased ? (reversed ? BOTTOM : TOP) : reversed ? TOP : BOTTOM;
return { text, stroke, fill, direction }; 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.scene.id:
case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.session.id:
case CONFIG.DH.GENERAL.refreshTypes.longRest.id: case CONFIG.DH.GENERAL.refreshTypes.longRest.id:
return allowedTypes.includes(typeToCheck); return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck);
case CONFIG.DH.GENERAL.refreshTypes.shortRest.id: case CONFIG.DH.GENERAL.refreshTypes.shortRest.id:
return allowedTypes.some( return allowedTypes.some(
x => x =>
@ -557,3 +558,121 @@ export function calculateExpectedValue(formulaOrTerms) {
: [formulaOrTerms]; : [formulaOrTerms];
return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0);
} }
export function parseRallyDice(value, effect) {
const legacyStartsWithPrefix = value.toLowerCase().startsWith('d');
const workingValue = legacyStartsWithPrefix ? value.slice(1) : value;
const dataParsedValue = itemAbleRollParse(workingValue, effect.parent);
return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`;
}
/**
* Refreshes character and/or adversary resources.
* @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes
* @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary.
* @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh
* @return { Actor[] } The actors that had their features refreshed
*/
export async function RefreshFeatures(
refreshTypes = [],
actorTypes = ['character', 'adversary'],
sendNotificationMessage = true,
sendRefreshMessage = true
) {
const refreshedActors = {};
for (let actor of game.actors) {
if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) {
const updates = {};
for (let item of actor.items) {
if (
item.system.metadata?.hasResource &&
refreshIsAllowed(refreshTypes, item.system.resource?.recovery)
) {
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label)
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
const increasing =
item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id;
updates[item.id].system = {
...updates[item.id].system,
'resource.value': increasing
? 0
: game.system.api.documents.DhActiveEffect.effectSafeEval(
Roll.replaceFormulaData(item.system.resource.max, actor.getRollData())
)
};
}
if (item.system.metadata?.hasActions) {
const usedTypes = new Set();
const actions = item.system.actions.filter(action => {
if (refreshIsAllowed(refreshTypes, action.uses.recovery)) {
usedTypes.add(action.uses.recovery);
return true;
}
return false;
});
if (actions.length === 0) continue;
if (!refreshedActors[actor.id])
refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() };
refreshedActors[actor.id].refreshed.add(
...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label))
);
if (!updates[item.id]?.system) updates[item.id] = { system: {} };
updates[item.id].system = {
...updates[item.id].system,
...actions.reduce(
(acc, action) => {
acc.actions[action.id] = { 'uses.value': 0 };
return acc;
},
{ actions: updates[item.id].system.actions ?? {} }
)
};
}
}
for (let key in updates) {
const update = updates[key];
await actor.items.get(key).update(update);
}
}
}
const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', ');
if (sendNotificationMessage) {
ui.notifications.info(
game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', {
types: `[${types}]`
})
);
}
if (sendRefreshMessage) {
const cls = getDocumentClass('ChatMessage');
const msg = {
user: game.user.id,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/refreshMessage.hbs',
{
types: types
}
),
title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'),
speaker: cls.getSpeaker()
};
cls.create(msg);
}
return refreshedActors;
}

View file

@ -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/dice-value.hbs',
'systems/daggerheart/templates/sheets/global/partials/resource-section/die.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/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/components/card-preview.hbs',
'systems/daggerheart/templates/levelup/parts/selectable-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/ui/combatTracker/combatTrackerSection.hbs',
'systems/daggerheart/templates/actionTypes/damage.hbs', 'systems/daggerheart/templates/actionTypes/damage.hbs',
'systems/daggerheart/templates/actionTypes/resource.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/beastform.hbs',
'systems/daggerheart/templates/actionTypes/countdown.hbs', 'systems/daggerheart/templates/actionTypes/countdown.hbs',
'systems/daggerheart/templates/actionTypes/summon.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/settings/components/settings-item-line.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',

View file

@ -1,10 +1,11 @@
import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs';
import DhCountdowns from '../data/countdowns.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 { import {
DhAppearanceSettings, DhAppearanceSettings,
DhAutomationSettings, DhAutomationSettings,
DhHomebrewSettings, DhHomebrewSettings,
DhMetagamingSettings,
DhVariantRuleSettings DhVariantRuleSettings
} from '../applications/settings/_module.mjs'; } from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
@ -38,17 +39,18 @@ const registerMenuSettings = () => {
type: DhAutomation 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, { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, {
scope: 'world', scope: 'world',
config: false, config: false,
type: DhHomebrew, type: DhHomebrew,
onChange: value => { onChange: value => {
if (value.maxFear) { value.handleChange();
if (ui.resources) ui.resources.render({ force: true });
}
// Some homebrew settings may change sheets in various ways, so trigger a re-render
resetActors();
} }
}); });
@ -76,6 +78,16 @@ const registerMenus = () => {
type: DhAutomationSettings, type: DhAutomationSettings,
restricted: true 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, { game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Homebrew.Name, {
name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'), name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'),
label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.label'), 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, { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'client', scope: 'world',
config: false, config: false,
type: CompendiumBrowserSettings 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();
}
}

View file

@ -717,7 +717,35 @@
"system": { "system": {
"description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.RXkZTwBRi4dJ3JE5]{Fallen Warlord: Undefeated Champion} and immediately spotlight them.</p>", "description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.RXkZTwBRi4dJ3JE5]{Fallen Warlord: Undefeated Champion} and immediately spotlight them.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {
"gP426WmWbtrZEWCD": {
"type": "transform",
"_id": "gP426WmWbtrZEWCD",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"cost": [],
"uses": {
"value": null,
"max": null,
"recovery": null,
"consumeOnSuccess": false
},
"transform": {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.RXkZTwBRi4dJ3JE5",
"resourceRefresh": {
"hitPoints": true,
"stress": true
}
}
}
},
"originItemType": null, "originItemType": null,
"originId": null, "originId": null,
"featureForm": "reaction" "featureForm": "reaction"

View file

@ -846,7 +846,37 @@
"system": { "system": {
"description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.pMuXGCSOQaxpi5tb]{Ashen Tyrant} and immediately spotlight them.</p>", "description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.pMuXGCSOQaxpi5tb]{Ashen Tyrant} and immediately spotlight them.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {
"cFqFjemAfAjB0OB0": {
"type": "transform",
"_id": "cFqFjemAfAjB0OB0",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"transform": {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.pMuXGCSOQaxpi5tb",
"resourceRefresh": {
"hitPoints": true,
"stress": true
}
},
"name": "Transform",
"range": ""
}
},
"originItemType": null, "originItemType": null,
"originId": null, "originId": null,
"featureForm": "reaction" "featureForm": "reaction"

View file

@ -742,7 +742,37 @@
"system": { "system": {
"description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.eArAPuB38CNR0ZIM]{Molten Scourge} and immediately spotlight them.</p>", "description": "<p>When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.eArAPuB38CNR0ZIM]{Molten Scourge} and immediately spotlight them.</p>",
"resource": null, "resource": null,
"actions": {}, "actions": {
"OxGkCGgIl4vGFufD": {
"type": "transform",
"_id": "OxGkCGgIl4vGFufD",
"systemPath": "actions",
"baseAction": false,
"description": "",
"chatDisplay": true,
"originItem": {
"type": "itemCollection"
},
"actionType": "action",
"triggers": [],
"cost": [],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"transform": {
"actorUUID": "Compendium.daggerheart.adversaries.Actor.eArAPuB38CNR0ZIM",
"resourceRefresh": {
"hitPoints": true,
"stress": true
}
},
"name": "Transform",
"range": ""
}
},
"originItemType": null, "originItemType": null,
"originId": null, "originId": null,
"featureForm": "reaction" "featureForm": "reaction"

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

@ -73,6 +73,24 @@
} }
} }
} }
.item-tags {
display: flex;
gap: 10px;
.tag {
align-items: center;
background: light-dark(@dark-15, @beige-15);
border-radius: 3px;
border: 1px solid light-dark(@dark, @beige);
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-size: var(--font-size-12);
justify-content: start;
padding: 3px 5px;
}
}
} }
/* TODO: Remove me when this issue is resolved https://github.com/foundryvtt/foundryvtt/issues/13734 */ /* TODO: Remove me when this issue is resolved https://github.com/foundryvtt/foundryvtt/issues/13734 */

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;
} }
@ -139,24 +138,6 @@
display: none; display: none;
} }
} }
.item-tags {
display: flex;
gap: 10px;
.tag {
align-items: center;
background: light-dark(@dark-15, @beige-15);
border-radius: 3px;
border: 1px solid light-dark(@dark, @beige);
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-size: var(--font-size-12);
justify-content: start;
padding: 3px 5px;
}
}
} }
.item-resource { .item-resource {
@ -275,8 +256,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 +317,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

@ -4,48 +4,78 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
}
.actor-summon-line { .transform-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
.transform-resources {
display: flex;
flex-direction: column;
.transform-resource {
display: flex;
align-items: center;
gap: 2px;
.resource-title {
font-size: var(--font-size-18);
}
}
}
}
.actor-drop-line {
display: flex;
align-items: center;
gap: 5px;
border-radius: 3px;
.actor-drop-name {
flex: 2;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
border-radius: 3px;
.actor-summon-name { img {
flex: 2; height: 40px;
display: flex;
align-items: center;
gap: 5px;
img {
height: 40px;
}
}
.actor-summon-controls {
flex: 1;
display: flex;
align-items: center;
gap: 5px;
.controls {
display: flex;
gap: 5px;
}
} }
} }
.summon-dragger { .actor-drop-controls {
flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 5px;
box-sizing: border-box;
height: 40px; &.transform {
margin-top: 10px; justify-content: flex-end;
border: 1px dashed light-dark(@dark-blue-50, @beige-50); }
border-radius: 3px;
color: light-dark(@dark-blue-50, @beige-50); .controls {
display: flex;
gap: 5px;
}
} }
.actor-drop-hint {
flex: none;
}
}
.drop-dragger {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: 40px;
margin-top: 10px;
border: 1px dashed light-dark(@dark-blue-50, @beige-50);
border-radius: 3px;
color: light-dark(@dark-blue-50, @beige-50);
} }
.trigger-data { .trigger-data {

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

@ -133,8 +133,19 @@
padding: 0; padding: 0;
margin-bottom: 15px; margin-bottom: 15px;
.hope-section { .resource-section {
display: flex;
align-items: center;
gap: 4px;
margin-right: 20px; margin-right: 20px;
.resource-manager {
transition: all 0.1s ease;
&.inverted {
transform: rotate(180deg);
}
}
} }
.downtime-section { .downtime-section {

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;
} }
@ -450,6 +450,10 @@
.target-data { .target-data {
flex: 1; flex: 1;
.target-name {
text-align: left;
}
} }
.target-save { .target-save {

View file

@ -27,6 +27,7 @@
@import './settings/settings.less'; @import './settings/settings.less';
@import './settings/homebrew-settings/domains.less'; @import './settings/homebrew-settings/domains.less';
@import './settings/homebrew-settings/types.less'; @import './settings/homebrew-settings/types.less';
@import './settings/homebrew-settings/resources.less';
@import './sidebar/tabs.less'; @import './sidebar/tabs.less';
@import './sidebar/daggerheartMenu.less'; @import './sidebar/daggerheartMenu.less';

View file

@ -304,7 +304,15 @@
padding: 0 0 0 50px; padding: 0 0 0 50px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px;
.item-description-outer-container:has(div, p) {
margin-top: 8px;
}
/* Some items don't include an outer container, so we attempt a catch-all */
> *:last-child {
padding-bottom: 6px;
}
h1 { h1 {
font-size: var(--font-size-32); font-size: var(--font-size-32);
@ -350,6 +358,7 @@
.filter-content, .filter-content,
.item-desc { .item-desc {
display: grid; display: grid;
opacity: 0;
grid-template-rows: 0fr; grid-template-rows: 0fr;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
width: 100%; width: 100%;
@ -378,8 +387,8 @@
} }
.expanded + .extensible { .expanded + .extensible {
opacity: 1;
grid-template-rows: 1fr; grid-template-rows: 1fr;
padding-top: 10px;
} }
.welcome-message { .welcome-message {

View file

@ -0,0 +1,87 @@
.daggerheart.dh-style.setting.homebrew-settings .resources.tab {
.resource-types-container {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
max-height: 570px;
fieldset legend {
display: flex;
align-items: center;
gap: 4px;
}
.resource-type-container {
width: 100%;
.resources-container {
display: flex;
flex-direction: column;
gap: 4px;
.resource-container {
.resource-icons-container {
display: flex;
justify-content: space-between;
gap: 8px;
width: 100%;
.resource-icon-container {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
.resource-icon-title-container {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&::before,
&::after {
color: @dark-blue;
content: '';
flex: 1;
height: 2px;
}
&::before {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @golden 100%);
}
&::after {
background: linear-gradient(90deg, @golden 0%, rgba(0, 0, 0, 0) 100%);
}
.resource-icon-title {
font-size: var(--font-size-16);
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
color: light-dark(@dark-blue, @golden);
i {
font-size: 14px;
}
}
}
}
}
}
}
}
}
.two-columns {
width: 100%;
}
.form-group.vertical {
> * {
flex: 0 0 100%;
}
}
}

View file

@ -4,3 +4,5 @@
@import './tooltip/domain-cards.less'; @import './tooltip/domain-cards.less';
@import './autocomplete/autocomplete.less'; @import './autocomplete/autocomplete.less';
@import './tooltip/resource-management.less';

View file

@ -0,0 +1,56 @@
.bordered-tooltip.locked-tooltip .daggerheart.resource-management-container,
#tooltip .daggerheart.resource-management-container {
display: flex;
flex-direction: column;
gap: 16px;
.resource-section {
position: relative;
display: flex;
gap: 10px;
background-color: light-dark(transparent, @dark-blue);
color: light-dark(@dark-blue, @golden);
padding: 5px 10px;
border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
align-items: center;
width: fit-content;
height: 30px;
h4 {
font-family: var(--dh-font-body, 'Montserrat'), sans-serif;
font-size: var(--font-size-14);
font-weight: bold;
text-transform: uppercase;
color: light-dark(@dark-blue, @golden);
margin: 0;
}
.resource-value {
display: flex;
cursor: pointer;
.hidden {
display: none;
}
img {
width: 14px;
height: 14px;
&.empty {
opacity: 0.4;
}
&.filter {
filter: @golden-filter;
}
&.non-transparent {
border-radius: 50%;
border: 1px solid @golden;
}
}
}
}
}

View file

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

View file

@ -1,19 +1,19 @@
<fieldset class="one-column" id="summon-drop-zone" data-key="summon"> <fieldset class="one-column" id="summon-drop-zone" data-is-drop-zone="true" data-key="summon">
<legend> <legend>
{{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}}
</legend> </legend>
<ul class="actor-summon-items"> <ul class="actor-summon-items">
{{#each @root.summons as |summon index|}} {{#each @root.summons as |summon index|}}
<li class="actor-summon-line"> <li class="actor-drop-line">
<div class="actor-summon-name"> <div class="actor-drop-name">
<img class="image" src="{{summon.actor.img}}" /> <img class="image" src="{{summon.actor.img}}" />
<h4 class="h4"> <h4 class="h4">
{{summon.actor.name}} {{summon.actor.name}}
</h4> </h4>
</div> </div>
<div class="actor-summon-controls"> <div class="actor-drop-controls">
<div class="form-group summon-count-wrapper" data-index="{{index}}"> <div class="form-group summon-count-wrapper" data-index="{{index}}">
<div class="form-fields"> <div class="form-fields">
<input type="text" value="{{summon.count}}" /> <input type="text" value="{{summon.count}}" />
@ -43,7 +43,7 @@
</div> </div>
</li> </li>
{{/each}} {{/each}}
<div class="summon-dragger"> <div class="drop-dragger">
<span>{{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}}</span> <span>{{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}}</span>
</div> </div>
</ul> </ul>

View file

@ -0,0 +1,62 @@
<fieldset class="one-column" id="transform-drop-zone" data-is-drop-zone="true" data-key="transform">
<legend>
{{localize "DAGGERHEART.ACTIONS.TYPES.transform.name"}}
</legend>
<div class="transform-container">
{{#if transform.actor}}
<div class="actor-drop-line">
{{#if transform.actor.error}}
<div class="hint actor-drop-hint">{{transform.actor.error}}</div>
{{else}}
<div class="actor-drop-name">
<img class="image" src="{{transform.actor.img}}" />
<h4 class="h4">
{{transform.actor.name}}
</h4>
</div>
{{/if}}
<div class="actor-drop-controls transform">
<div class="controls">
{{#unless transform.actor.error}}
<a
class='effect-control'
data-action='editDoc'
data-item-uuid="{{transform.actor.uuid}}"
data-tooltip='{{localize "DAGGERHEART.UI.Tooltip.openItemWorld"}}'
>
<i class="fa-solid fa-globe"></i>
</a>
{{/unless}}
<a
class='effect-control'
data-action='removeTransformActor'
data-tooltip='{{localize "CONTROLS.CommonDelete"}}'
>
<i class='fas fa-trash'></i>
</a>
</div>
</div>
</div>
<line-div></line-div>
{{/if}}
{{#unless transform.actor}}
<div class="drop-dragger">
<span>{{localize "DAGGERHEART.ACTIONS.Settings.transform.dropTransformHere"}}</span>
</div>
{{/unless}}
<div class="transform-resources">
<div class="transform-resource">
<input type="checkbox" data-resource="hitPoints" {{checked transform.resourceRefresh.hitPoints}}/>
<span class="resource-title">{{localize "DAGGERHEART.ACTIONS.Settings.transform.clearHitPoints"}}</span>
</div>
<div class="transform-resource">
<input type="checkbox" data-resource="stress" {{checked transform.resourceRefresh.stress}}/>
<span class="resource-title">{{localize "DAGGERHEART.ACTIONS.Settings.transform.clearStress"}}</span>
</div>
</div>
</div>
</fieldset>

View file

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

View file

@ -0,0 +1,78 @@
<section
class="tab {{tabs.resources.cssClass}} {{tabs.resources.id}} scrollable"
data-tab="{{tabs.resources.id}}"
data-group="{{tabs.resources.group}}"
>
<div class="resource-types-container">
{{#each settingFields.resources as |type key|}}
<fieldset>
<legend>
{{localize "DAGGERHEART.SETTINGS.Homebrew.resources.typeTitle" type=(localize (concat "TYPES.Actor." key))}}
<a data-action="addResource" data-actor-type="{{key}}"><i class="fa-solid fa-plus"></i></a>
</legend>
<div class="resource-type-container">
<div class="resources-container">
{{#each type.resources as |resource key|}}
<fieldset class="resource-container">
<legend>{{resource.label}}<a data-action="removeResource" data-actor-type="{{@../key}}" data-resource-key="{{key}}"><i class="fa-solid fa-trash"></i></a></legend>
{{formField @root.schemaFields.resources.element.fields.resources.element.fields.label value=resource.label name=(concat "resources." @../key ".resources." key ".label") classes="vertical" localize=true }}
<div class="two-columns even">
{{formField @root.schemaFields.resources.element.fields.resources.element.fields.initial value=resource.initial name=(concat "resources." @../key ".resources." key ".initial") classes="vertical" localize=true }}
{{formField @root.schemaFields.resources.element.fields.resources.element.fields.max value=resource.max name=(concat "resources." @../key ".resources." key ".max") classes="vertical" localize=true }}
</div>
<div class="resource-icons-container">
<div class="resource-icon-container" data-actor-type="{{@../key}}" data-resource-key="{{key}}" data-image-key="full">
{{#with @root.schemaFields.resources.element.fields.resources.element.fields.images.fields.full.fields}}
<div class="resource-icon-title-container">
<div class="resource-icon-title">
<span>{{localize "DAGGERHEART.SETTINGS.Homebrew.resources.filledIcon"}}</span>
<a data-action="resetResourceImage"><i class="fa-solid fa-arrow-rotate-left"></i></a>
</div>
</div>
<div class="resource-icon-content">
{{#if ../images.full.isIcon}}
{{formGroup this.value value=../images.full.value name=(concat "resources." @../key ".resources." key ".images.full.value") localize=true }}
{{else}}
<div class="form-fields">
<file-picker name="{{concat "resources." @../key ".resources." key ".images.full.value"}}" value="{{../images.full.value}}" type="image"></file-picker>
</div>
{{/if}}
{{formGroup this.isIcon value=../images.full.isIcon name="" classes="path-field" localize=true }}
{{formGroup this.noColorFilter value=../images.full.noColorFilter name=(concat "resources." @../key ".resources." key ".images.full.noColorFilter") localize=true }}
</div>
{{/with}}
</div>
<div class="resource-icon-container" data-actor-type="{{@../key}}" data-resource-key="{{key}}" data-image-key="empty">
{{#with @root.schemaFields.resources.element.fields.resources.element.fields.images.fields.empty.fields}}
<div class="resource-icon-title-container">
<div class="resource-icon-title">
<span>{{localize "DAGGERHEART.SETTINGS.Homebrew.resources.emptyIcon"}}</span>
<a data-action="resetResourceImage"><i class="fa-solid fa-arrow-rotate-left"></i></a>
</div>
</div>
<div class="resource-icon-content">
{{#if ../images.empty.isIcon}}
{{formGroup this.value value=../images.empty.value name=(concat "resources." @../key ".resources." key ".images.empty.value") localize=true }}
{{else}}
<div class="form-fields">
<file-picker name="{{concat "resources." @../key ".resources." key ".images.empty.value"}}" value="{{../images.empty.value}}" type="image"></file-picker>
</div>
{{/if}}
{{formGroup this.isIcon value=resource.images.empty.isIcon name="" classes="path-field" localize=true }}
{{formGroup this.noColorFilter value=resource.images.empty.noColorFilter name=(concat "resources." @../key ".resources." key ".images.empty.noColorFilter") localize=true }}
</div>
{{/with}}
</div>
</div>
</fieldset>
{{/each}}
</div>
</div>
</fieldset>
{{/each}}
</div>
</section>

View file

@ -0,0 +1,10 @@
<footer class="form-footer">
<button data-action="reset">
<i class="fa-solid fa-arrow-rotate-left"></i>
<span>{{localize "Reset"}}</span>
</button>
<button data-action="save" >
<i class="fa-solid fa-floppy-disk"></i>
<span>{{localize "Save Changes"}}</span>
</button>
</footer>

View file

@ -0,0 +1,3 @@
<div>
{{formGroup settingFields.schema.fields.hideObserverPermissionInChat value=settingFields._source.hideObserverPermissionInChat localize=true}}
</div>

View file

@ -0,0 +1,3 @@
<header class="dialog-header">
<h1>{{localize 'DAGGERHEART.SETTINGS.Menu.metagaming.name'}}</h1>
</header>

View file

@ -11,4 +11,5 @@
{{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}}
{{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}}
{{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}}
{{#if fields.transform}}{{> 'systems/daggerheart/templates/actionTypes/transform.hbs' fields=fields.transform.fields source=source.transform}}{{/if}}
</section> </section>

View file

@ -20,15 +20,11 @@
<div class="fieldsets-section"> <div class="fieldsets-section">
<fieldset class="flex"> <fieldset class="flex">
<legend>{{localize "DAGGERHEART.GENERAL.HitPoints.plural"}}</legend> <legend>{{localize "DAGGERHEART.GENERAL.Resource.plural"}}</legend>
{{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}} {{#each resources as |resource|}}
{{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.max.label")}} {{formGroup resource.field value=resource.value name=resource.name}}
</fieldset> {{/each}}
<fieldset class="flex"> </fieldset>
<legend>{{localize "DAGGERHEART.GENERAL.stress"}}</legend>
{{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.stress.value.label")}}
{{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.stress.max.label")}}
</fieldset>
</div> </div>
<fieldset class="flex"> <fieldset class="flex">

View file

@ -22,15 +22,12 @@
<legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend> <legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend>
<div class="two-columns even"> <div class="two-columns even">
{{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value localize=true}} {{#each resources as |resource|}}
<span data-tooltip-text="{{localize "DAGGERHEART.UI.Tooltip.maxHPClassBound"}}"> <span {{#if resource.tooltip}}data-tooltip-text="{{resource.tooltip}}"{{/if}}>
{{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max localize=true}} {{formGroup resource.field value=resource.value name=resource.name}}
</span> </span>
{{/each}}
{{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value localize=true}}
{{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max localize=true}}
{{formGroup systemFields.resources.fields.hope.fields.value value=document._source.system.resources.hope.value localize=true}}
{{formGroup systemFields.scars value=document._source.system.scars localize=true}} {{formGroup systemFields.scars value=document._source.system.scars localize=true}}
{{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}} {{formGroup systemFields.proficiency value=document._source.system.proficiency localize=true}}

View file

@ -7,8 +7,9 @@
<legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend> <legend>{{localize 'DAGGERHEART.GENERAL.basics'}}</legend>
<div class="nest-inputs"> <div class="nest-inputs">
{{formGroup systemFields.evasion value=document._source.system.evasion localize=true}} {{formGroup systemFields.evasion value=document._source.system.evasion localize=true}}
{{formGroup systemFields.resources.fields.stress.fields.value value=document._source.system.resources.stress.value label='DAGGERHEART.ACTORS.Companion.FIELDS.resources.stress.currentStress.label' localize=true}} {{#each resources as |resource|}}
{{formGroup systemFields.resources.fields.stress.fields.max value=document._source.system.resources.stress.max label='DAGGERHEART.ACTORS.Companion.FIELDS.resources.stress.maxStress.label' localize=true}} {{formGroup resource.field value=resource.value name=resource.name}}
{{/each}}
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-fields"> <div class="form-fields">

View file

@ -4,19 +4,21 @@
{{#each document.system.sheetLists as |category|}} {{#each document.system.sheetLists as |category|}}
{{#if (eq category.type 'feature' )}} {{#if (eq category.type 'feature' )}}
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title=category.title title=category.title
type='feature' type='feature'
collection=category.values actorType='character'
canCreate=true collection=category.values
showActions=true canCreate=true
showActions=true
}} }}
{{else if category.values}} {{else if category.values}}
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title=category.title title=category.title
type='feature' type='feature'
collection=category.values actorType='character'
canCreate=false collection=category.values
showActions=true canCreate=false
showActions=true
}} }}
{{/if}} {{/if}}

View file

@ -65,22 +65,25 @@
</div> </div>
<div class="character-row"> <div class="character-row">
<div class="hope-section"> <div class="resource-section">
<h4>{{localize "DAGGERHEART.GENERAL.hope"}}</h4> <div class="hope-section">
{{#times document.system.resources.hope.max}} <h4>{{localize "DAGGERHEART.GENERAL.hope"}}</h4>
<span class='hope-value' data-action='toggleHope' data-value="{{add this 1}}"> {{#times document.system.resources.hope.max}}
{{#if (gte ../document.system.resources.hope.value (add this 1))}} <span class='hope-value' data-action='toggleHope' data-value="{{add this 1}}">
<i class='fa-solid fa-diamond'></i> {{#if (gte ../document.system.resources.hope.value (add this 1))}}
{{else}} <i class='fa-solid fa-diamond'></i>
<i class='fa-regular fa-circle'></i> {{else}}
{{/if}} <i class='fa-regular fa-circle'></i>
</span> {{/if}}
{{/times}} </span>
{{#times document.system.scars}} {{/times}}
<span class='hope-value scar'> {{#times document.system.scars}}
<i class='fa-regular fa-ban'></i> <span class='hope-value scar'>
</span> <i class='fa-regular fa-ban'></i>
{{/times}} </span>
{{/times}}
{{#if hasExtraResources}}<a type="button" class="resource-manager" data-action="toggleResourceManagement"><i class="fa-solid fa-angle-down"></i></a>{{/if}}
</div>
</div> </div>
{{#if document.system.class.value}} {{#if document.system.class.value}}
<div class="domains-section"> <div class="domains-section">

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

@ -28,7 +28,7 @@ Parameters:
<legend> <legend>
{{localize title}} {{localize title}}
{{#if canCreate}} {{#if canCreate}}
<a data-action="{{ifThen (or (eq type 'effect') (eq type 'feature') (eq type 'action')) 'createDoc' 'addNewItem' }}" data-document-class="{{ifThen (eq type 'effect') 'ActiveEffect' 'Item' }}" <a data-action="{{ifThen (or (eq type 'effect') (and (eq type 'feature') (not (eq actorType 'character'))) (eq type 'action')) 'createDoc' 'addNewItem' }}" data-document-class="{{ifThen (eq type 'effect') 'ActiveEffect' 'Item' }}"
data-type="{{ifThen (eq type 'effect') 'base' type}}" data-type="{{ifThen (eq type 'effect') 'base' type}}"
{{#if inVault}}data-in-vault="{{inVault}}"{{/if}} {{#if inVault}}data-in-vault="{{inVault}}"{{/if}}
{{#if disabled}} data-disabled="{{disabled}}"{{/if}} {{#if disabled}} data-disabled="{{disabled}}"{{/if}}

View file

@ -45,30 +45,20 @@ Parameters:
<span class="item-name">{{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}<span class="expanded-icon"><i class="fa-solid fa-expand"></i></span>{{/unless}}</span> <span class="item-name">{{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}<span class="expanded-icon"><i class="fa-solid fa-expand"></i></span>{{/unless}}</span>
{{!-- Tags Start --}} {{!-- Tags Start --}}
{{#with item}} {{#if (not ../hideTags)}}
{{#if (not ../hideTags)}} {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item }}
<div class="item-tags"> {{#if (eq ../type 'feature')}}
{{#each this._getTags as |tag|}} {{#if (or (eq @root.document.type 'adversary') (eq @root.document.type 'environment'))}}
<div class="tag"> {{#if system.featureForm}}
{{tag}} <div class="tag feature-form">
</div> <span class="recall-value">{{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}}</span>
{{/each}} </div>
{{/if}}
{{!-- Feature Form Tag Start --}}
{{#if (eq ../type 'feature')}}
{{#if (or (eq @root.document.type 'adversary') (eq @root.document.type 'environment'))}}
{{#if system.featureForm}}
<div class="tag feature-form">
<span class="recall-value">{{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}}</span>
</div>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/ "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}}
{{/if}} {{/if}}
{{!-- Feature Form Tag End --}}
</div>
{{/if}}
{{/with}}
{{!--Tags End --}} {{!--Tags End --}}
</div> </div>
@ -109,7 +99,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 +115,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 +128,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}}

View file

@ -0,0 +1,8 @@
<div class="item-tags">
{{#each _getTags as |tag|}}
<div class="tag">
{{tag}}
</div>
{{/each}}
{{#if @partial-block}}{{> @partial-block}}{{/if}}
</div>

View file

@ -1,19 +1,6 @@
<div class="item-description-outer-container"> <div class="item-description-outer-container">
<div class="two-columns">
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}</h4>
<span>{{item.system.baseThresholds.major}}/{{item.system.baseThresholds.severe}}</span>
</div>
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}</h4>
<span>{{item.system.baseScore}}</span>
</div>
</div>
{{#if features.length}} {{#if features.length}}
<div class="item-description-container"> <div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.features"}}</h4>
{{#each features as | feature |}} {{#each features as | feature |}}
<div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div> <div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div>
{{/each}} {{/each}}

View file

@ -1,36 +1,6 @@
<div class="item-description-outer-container"> <div class="item-description-outer-container">
<div class="three-columns">
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.Tiers.singular"}}</h4>
<span>{{tier}}</span>
</div>
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.Trait.single"}}</h4>
<span>{{trait}}</span>
</div>
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.range"}}</h4>
<span>{{range}}</span>
</div>
</div>
<div class="three-columns">
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.damage"}}</h4>
<span>{{damage}}</span>
</div>
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.burden"}}</h4>
<span>{{burden}}</span>
</div>
</div>
{{#if features.length}} {{#if features.length}}
<div class="item-description-container"> <div class="item-description-container">
<h4>{{localize "DAGGERHEART.GENERAL.features"}}</h4>
{{#each features as | feature |}} {{#each features as | feature |}}
<div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div> <div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div>
{{/each}} {{/each}}

View file

@ -1,6 +1,6 @@
<div class="roll-part target-section dice-roll" data-action="expandRoll"> <div class="roll-part target-section dice-roll" data-action="expandRoll">
<div class="roll-part-header"><div><span>{{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}}</span></div></div> <div class="roll-part-header"><div><span>{{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}}</span></div></div>
{{#if isGM}} {{#if (or isGM (not metagamingSettings.hideObserverPermissionInChat))}}
<div class="roll-part-extra on-reduced"> <div class="roll-part-extra on-reduced">
<div class="wrapper"> <div class="wrapper">
{{#if (or (gt targetShort.hit 0) (gt targetShort.miss 0))}} {{#if (or (gt targetShort.hit 0) (gt targetShort.miss 0))}}

View file

@ -10,7 +10,10 @@
</div> </div>
</div> </div>
<div class="item-desc extensible"> <div class="item-desc extensible">
<span class="wrapper">{{{system.enrichedDescription}}}</span> <span class="wrapper">
{{{system.enrichedTags}}}
{{{system.enrichedDescription}}}
</span>
</div> </div>
</div> </div>
{{/each}} {{/each}}

View file

@ -5,7 +5,7 @@
<i class="fa-solid fa-bolt"></i> <i class="fa-solid fa-bolt"></i>
</span> </span>
<span class="item-icon"> <span class="item-icon">
{{#with (lookup config.DOMAIN.domains item.system.domain) as | domain |}} {{#with (lookup allDomains item.system.domain) as | domain |}}
<img src="{{domain.src}}" alt=""> <img src="{{domain.src}}" alt="">
{{/with}} {{/with}}
</span> </span>

View file

@ -0,0 +1,21 @@
<div class="daggerheart resource-management-container">
{{#each resources as |resource|}}
<div class="resource-section {{resource.resourceClass}}">
<h4>{{resource.label}}</h4>
{{#times resource.max}}
<span class='resource-value' data-action='toggleResource' data-value="{{add this 1}}" data-resource="{{resource.id}}">
{{#if resource.fullIcon.isIcon}}
<i class='{{resource.fullIcon.value}} full {{#unless (gte ../value (add this 1))}}hidden{{/unless}}'></i>
{{else}}
<img src="{{resource.fullIcon.value}}" class="full {{#unless resource.fullIcon.noColorFilter}}filter{{else}}non-transparent{{/unless}} {{#unless (gte ../value (add this 1))}}hidden{{/unless}}" />
{{/if}}
{{#if resource.emptyIcon.isIcon}}
<i class='{{resource.emptyIcon.value}} empty {{#if (gte ../value (add this 1))}}hidden{{/if}}'></i>
{{else}}
<img src="{{resource.emptyIcon.value}}" class="empty {{#unless resource.fullIcon.noColorFilter}}filter{{else}}non-transparent{{/unless}} {{#if (gte ../value (add this 1))}}hidden{{/if}}" />
{{/if}}
</span>
{{/times}}
</div>
{{/each}}
</div>