diff --git a/daggerheart.mjs b/daggerheart.mjs index 05b57ac9..5960c6b1 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -233,6 +233,11 @@ Hooks.once('init', () => { return handlebarsRegistration(); }); +Hooks.on('i18nInit', () => { + // Setup homebrew resources + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).refreshConfig(); +}); + Hooks.on('setup', () => { CONFIG.statusEffects = [ ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), diff --git a/lang/en.json b/lang/en.json index 2eef200f..9c74a351 100755 --- a/lang/en.json +++ b/lang/en.json @@ -74,6 +74,15 @@ "invalidDrop": "You can only drop Actor entities to summon.", "chatMessageTitle": "Test2", "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": { @@ -129,6 +138,12 @@ }, "summon": { "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": { "name": "Vulnerable", - "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again." + "description": "While a creature is Vulnerable, all rolls targeting them have advantage.\nA creature who is already Vulnerable can’t be made to take the condition again.", + "autoAppliedByLabel": "Max Stress" } }, "CountdownType": { @@ -1166,12 +1182,12 @@ }, "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" }, "veryFar": { "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" } }, @@ -1294,6 +1310,7 @@ "triggerTexts": { "strangePatternsContentTitle": "Matched {nr} times.", "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?", "ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you." }, @@ -2268,6 +2285,7 @@ "identify": "Identity", "imagePath": "Image Path", "inactiveEffects": "Inactive Effects", + "initial": "Initial", "inventory": "Inventory", "itemResource": "Item Resource", "itemQuantity": "Item Quantity", @@ -2559,6 +2577,10 @@ "gm": { "label": "GM" }, "players": { "label": "Players" } }, + "vulnerableAutomation": { + "label": "Vulnerable Automation", + "hint": "Automatically apply the Vulnerable condition when a actor reaches max stress" + }, "countdownAutomation": { "label": "Countdown Automation", "hint": "Automatically progress countdowns based on their progression settings" @@ -2627,6 +2649,14 @@ "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": { "newDowntimeMove": "Downtime Move", "downtimeMove": "Downtime Move", @@ -2641,6 +2671,8 @@ "resetMovesText": "Are you sure you want to reset?", "deleteItemTitle": "Delete Homebrew 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": { "maxFear": { "label": "Max Fear" }, "maxHope": { "label": "Max Hope" }, @@ -2649,6 +2681,13 @@ "label": "Max Cards in Loadout", "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" } }, "currency": { @@ -2677,6 +2716,13 @@ "adversaryType": { "title": "Custom Adversary Types", "newType": "Adversary Type" + }, + "resources": { + "typeTitle": "{type} Resources", + "filledIcon": "Filled Icon", + "emptyIcon": "Empty Icon", + "resourceIdentifier": "Resource Identifier", + "setResourceIdentifier": "Set Resource Identifier" } }, "Menu": { @@ -2686,6 +2732,11 @@ "label": "Configure Automation", "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": { "name": "Homebrew Settings", "label": "Configure Homebrew", @@ -2809,7 +2860,7 @@ "title": "Domain Card" }, "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "abilityCheckTitle": "{ability} Roll" }, "effectSummary": { "title": "Effects Applied", @@ -2824,7 +2875,7 @@ "selectLeader": "Select a Leader", "selectMember": "Select a Member", "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", "wholePartySelected": "The whole party is selected" }, @@ -2990,7 +3041,8 @@ "tokenActorMissing": "{name} is missing an Actor", "tokenActorsMissing": "[{names}] missing Actors", "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": { "actorDirectory": { diff --git a/module/applications/settings/_module.mjs b/module/applications/settings/_module.mjs index b565777c..28972397 100644 --- a/module/applications/settings/_module.mjs +++ b/module/applications/settings/_module.mjs @@ -1,4 +1,5 @@ export { default as DhAppearanceSettings } from './appearanceSettings.mjs'; export { default as DhAutomationSettings } from './automationSettings.mjs'; export { default as DhHomebrewSettings } from './homebrewSettings.mjs'; +export { default as DhMetagamingSettings } from './metagamingSettings.mjs'; export { default as DhVariantRuleSettings } from './variantRuleSettings.mjs'; diff --git a/module/applications/settings/automationSettings.mjs b/module/applications/settings/automationSettings.mjs index e720733e..52def7db 100644 --- a/module/applications/settings/automationSettings.mjs +++ b/module/applications/settings/automationSettings.mjs @@ -31,8 +31,8 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App }; static PARTS = { - tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, rules: { template: 'systems/daggerheart/templates/settings/automation-settings/deathMoves.hbs' }, roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' }, diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 51bf439e..9cc0ecb1 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -1,4 +1,5 @@ import { DhHomebrew } from '../../data/settings/_module.mjs'; +import { Resource } from '../../data/settings/Homebrew.mjs'; import { slugify } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -44,6 +45,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli addAdversaryType: this.addAdversaryType, deleteAdversaryType: this.deleteAdversaryType, selectAdversaryType: this.selectAdversaryType, + addResource: this.addResource, + removeResource: this.removeResource, + resetResourceImage: this.resetResourceImage, save: this.save, resetTokenSizes: this.resetTokenSizes, reset: this.reset @@ -56,6 +60,10 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' }, + resources: { + template: 'systems/daggerheart/templates/settings/homebrew-settings/resources.hbs', + scrollable: ['.resource-types-container'] + }, itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } @@ -64,7 +72,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli /** @inheritdoc */ static TABS = { main: { - tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }], + tabs: [ + { id: 'settings' }, + { id: 'domains' }, + { id: 'types' }, + { id: 'resources' }, + { id: 'itemFeatures' }, + { id: 'downtime' } + ], initial: 'settings', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -77,9 +92,17 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const element of htmlElement.querySelectorAll('.path-field input')) + element.addEventListener('change', this.toggleResourceIsIcon.bind(this)); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); context.settingFields = this.settings; + context.schemaFields = context.settingFields.schema.fields; return context; } @@ -103,6 +126,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli ? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] } : null; break; + case 'resources': + break; case 'downtime': context.restOptions = { shortRest: CONFIG.DH.GENERAL.defaultRestOptions.shortRest(), @@ -124,6 +149,33 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + async toggleResourceIsIcon(event) { + const element = event.target.closest('.resource-icon-container'); + const { actorType, resourceKey, imageKey } = element.dataset; + + const current = this.settings.resources[actorType].resources[resourceKey].images[imageKey]; + await this.settings.updateSource({ + [`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]: { + isIcon: !current.isIcon, + value: '' + } + }); + + this.render(); + } + + static async resetResourceImage(_event, button) { + const element = button.closest('.resource-icon-container'); + const { actorType, resourceKey, imageKey } = element.dataset; + + await this.settings.updateSource({ + [`resources.${actorType}.resources.${resourceKey}.images.${imageKey}`]: + Resource.getDefaultImageData(imageKey) + }); + + this.render(); + } + static async changeCurrencyIcon(_, target) { const type = target.dataset.currency; const currentIcon = this.settings.currency[type].icon; @@ -187,6 +239,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli }); } + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -227,6 +280,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli } }); + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -246,6 +300,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli await this.settings.updateSource({ [`${path}.-=${id}`]: null }); + + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.render(); } @@ -462,6 +518,58 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async addResource(_, target) { + const { actorType } = target.dataset; + const content = new foundry.data.fields.StringField({ + label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.resourceIdentifier'), + required: true + }).toFormGroup({}, { name: 'identifier', localize: true }).outerHTML; + + async function callback(_, button) { + const identifier = button.form.elements.identifier.value; + if (!identifier) return; + + const sluggedIdentifier = slugify(identifier); + + await this.settings.updateSource({ + [`resources.${actorType}.resources.${sluggedIdentifier}`]: Resource.getDefaultResourceData(identifier) + }); + + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); + this.render(); + } + + await foundry.applications.api.DialogV2.prompt({ + content: content, + rejectClose: false, + modal: true, + ok: { callback: callback.bind(this) }, + window: { + title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.resources.setResourceIdentifier') + }, + position: { width: 400 } + }); + } + + static async removeResource(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize(`DAGGERHEART.SETTINGS.Homebrew.deleteResourceTitle`) + }, + content: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.deleteResourceText') + }); + + if (!confirmed) return; + + const { actorType, resourceKey } = target.dataset; + await this.settings.updateSource({ + [`resources.${actorType}.resources.-=${resourceKey}`]: null + }); + + game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); + this.render(); + } + static async save() { await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.close(); diff --git a/module/applications/settings/metagamingSettings.mjs b/module/applications/settings/metagamingSettings.mjs new file mode 100644 index 00000000..c29a021b --- /dev/null +++ b/module/applications/settings/metagamingSettings.mjs @@ -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(); + } +} diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 34543086..ce0e7e24 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -28,6 +28,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) removeEffect: this.removeEffect, addElement: this.addElement, removeElement: this.removeElement, + removeTransformActor: this.removeTransformActor, editEffect: this.editEffect, addDamage: this.addDamage, removeDamage: this.removeDamage, @@ -41,7 +42,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) submitOnChange: true, closeOnSubmit: false }, - dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }] + dragDrop: [{ dragSelector: null, dropSelector: '[data-is-drop-zone]', handlers: ['_onDrop'] }] }; static PARTS = { @@ -120,6 +121,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => { element.addEventListener('change', this.updateSummonCount.bind(this)); }); + + htmlElement.querySelectorAll('.transform-resource input').forEach(element => { + element.addEventListener('change', this.updateTransformResource.bind(this)); + }); } async _prepareContext(_options) { @@ -133,6 +138,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.summons.push({ actor, count: summon.count }); } + if (context.source.transform) { + const actor = await foundry.utils.fromUuid(context.source.transform.actorUUID); + context.transform = { + ...context.source.transform, + actor: + actor ?? + (context.source.transform.actorUUID && !actor + ? { error: game.i18n.localize('DAGGERHEART.ACTIONS.Settings.transform.actorIsMissing') } + : null) + }; + } + context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; @@ -266,6 +283,12 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) if (doc) return doc.sheet.render({ force: true }); } + static async removeTransformActor() { + const data = this.action.toObject(); + data.transform.actorUUID = null; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + static addDamage(_event) { if (!this.action.damage.parts) return; const data = this.action.toObject(), @@ -346,6 +369,14 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + updateTransformResource(event) { + event.stopPropagation(); + + const data = this.action.toObject(); + data.transform.resourceRefresh[event.target.dataset.resource] = event.target.checked; + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + /** Specific implementation in extending classes **/ static async addEffect(_event) {} static removeEffect(_event, _button) {} @@ -364,6 +395,18 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return; } + const dropZone = event.target.closest('[data-is-drop-zone]'); + if (!dropZone) return; + + switch (dropZone.id) { + case 'summon-drop-zone': + return this.onSummonDrop(data); + case 'transform-drop-zone': + return this.onTransformDrop(data); + } + } + + async onSummonDrop(data) { const actionData = this.action.toObject(); let countvalue = 1; for (const entry of actionData.summon) { @@ -380,4 +423,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) actionData.summon.push({ actorUUID: data.uuid, count: countvalue }); await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); } + + async onTransformDrop(data) { + const actionData = this.action.toObject(); + actionData.transform.actorUUID = data.uuid; + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + } } diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 28db0efe..1f2ba5e0 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -4,6 +4,42 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac constructor(options) { super(options); + this.changeChoices = DhActiveEffectConfig.getChangeChoices(); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'sheet', 'dh-style'] + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, + tabs: { template: 'templates/generic/tab-navigation.hbs' }, + details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, + settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, + changes: { + template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', + scrollable: ['ol[data-changes]'] + }, + footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } + }; + + static TABS = { + sheet: { + tabs: [ + { id: 'details', icon: 'fa-solid fa-book' }, + { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, + { id: 'changes', icon: 'fa-solid fa-gears' } + ], + initial: 'details', + labelPrefix: 'EFFECT.TABS' + } + }; + + /** + * Get ChangeChoices for the changes autocomplete. Static for use in this class aswell as in settings-active-effect-config.mjs + * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} + */ + static getChangeChoices() { const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; const getAllLeaves = (root, group, parentPath = '') => { @@ -23,7 +59,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac return leaves; }; - this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { + return Object.keys(game.system.api.models.actors).reduce((acc, key) => { if (ignoredActorKeys.includes(key)) return acc; const model = game.system.api.models.actors[key]; @@ -62,34 +98,6 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac }, []); } - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'sheet', 'dh-style'] - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, - tabs: { template: 'templates/generic/tab-navigation.hbs' }, - details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, - settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, - changes: { - template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', - scrollable: ['ol[data-changes]'] - }, - footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } - }; - - static TABS = { - sheet: { - tabs: [ - { id: 'details', icon: 'fa-solid fa-book' }, - { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, - { id: 'changes', icon: 'fa-solid fa-gears' } - ], - initial: 'details', - labelPrefix: 'EFFECT.TABS' - } - }; - _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); const changeChoices = this.changeChoices; diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs index fe36e37f..12ac90d1 100644 --- a/module/applications/sheets-configs/setting-active-effect-config.mjs +++ b/module/applications/sheets-configs/setting-active-effect-config.mjs @@ -7,19 +7,7 @@ export default class SettingActiveEffectConfig extends HandlebarsApplicationMixi super({}); this.effect = foundry.utils.deepClone(effect); - const ignoredActorKeys = ['config', 'DhEnvironment']; - this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { - if (!ignoredActorKeys.includes(key)) { - const model = game.system.api.models.actors[key]; - const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); - const group = game.i18n.localize(model.metadata.label); - const choices = CONFIG.Token.documentClass - .getTrackedAttributeChoices(attributes, model) - .map(x => ({ ...x, group: group })); - acc.push(...choices); - } - return acc; - }, []); + this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices(); } static DEFAULT_OPTIONS = { diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index e8bf6109..28e686ea 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -73,9 +73,11 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App return context; } - static async updateData(event, element, formData) { + static async updateData(_event, _element, formData) { const data = foundry.utils.expandObject(formData.object); - foundry.utils.mergeObject(this.move, data); + await this.updateMove({ + [`${this.movePath}`]: data + }); this.render(); } @@ -135,9 +137,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App } ); - await this.settings.updateSource({ [`${this.actionsPath}.${action.id}`]: action }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); - + await this.updateMove({ [`${this.actionsPath}.${action.id}`]: action }); this.render(); } @@ -150,13 +150,12 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); if (!updatedEffect) return; - await this.settings.updateSource({ + await this.updateMove({ [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); return acc; }, []) }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } else { const action = this.move.actions.get(id); @@ -171,13 +170,13 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App : existingEffectIndex === -1 ? [...currentEffects, effectData] : currentEffects.with(existingEffectIndex, effectData); - await this.settings.updateSource({ + await this.updateMove({ [`${this.movePath}.effects`]: updatedEffects }); } - await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); - this.move = foundry.utils.getProperty(this.settings, this.movePath); + await this.updateMove({ [`${this.actionsPath}.${id}`]: updatedMove }); + this.render(); return updatedEffects; }).render(true); @@ -199,33 +198,36 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App }); } } - await this.settings.updateSource({ + await this.updateMove({ [this.movePath]: { effects: move.effects.filter(x => x.id !== id), actions: move.actions } }); } else { - await this.settings.updateSource({ [`${this.actionsPath}.-=${target.dataset.id}`]: null }); + await this.updateMove({ [`${this.actionsPath}.-=${target.dataset.id}`]: null }); } - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } - static async addEffect(_, target) { + static async addEffect() { const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); - await this.settings.updateSource({ + + await this.updateMove({ [`${this.movePath}.effects`]: [ ...currentEffects, game.system.api.data.activeEffects.BaseEffect.getDefaultObject() ] }); - - this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); } + async updateMove(update) { + await this.settings.updateSource(update); + this.move = foundry.utils.getProperty(this.settings, this.movePath); + } + static resetMoves() {} _filterTabs(tabs) { diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8cddd948..86ce4233 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -32,6 +32,7 @@ export default class CharacterSheet extends DHBaseActorSheet { handleResourceDice: CharacterSheet.#handleResourceDice, advanceResourceDie: CharacterSheet.#advanceResourceDie, cancelBeastform: CharacterSheet.#cancelBeastform, + toggleResourceManagement: CharacterSheet.#toggleResourceManagement, useDowntime: this.useDowntime, viewParty: CharacterSheet.#viewParty }, @@ -225,6 +226,9 @@ export default class CharacterSheet extends DHBaseActorSheet { async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { + case 'header': + await this._prepareHeaderContext(context, options); + break; case 'loadout': await this._prepareLoadoutContext(context, options); break; @@ -239,6 +243,12 @@ export default class CharacterSheet extends DHBaseActorSheet { return context; } + async _prepareHeaderContext(context, _options) { + context.hasExtraResources = Object.keys(CONFIG.DH.RESOURCE.character.all).some( + key => !CONFIG.DH.RESOURCE.character.base[key] + ); + } + /** * Prepare render context for the Loadout part. * @param {ApplicationRenderContext} context @@ -922,6 +932,78 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } + static async #toggleResourceManagement(event, button) { + event.stopPropagation(); + const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container'); + if (existingTooltip) { + game.tooltip.dismissLockedTooltips(); + return; + } + + const extraResources = Object.values(CONFIG.DH.RESOURCE.character.all).reduce((acc, resource) => { + if (CONFIG.DH.RESOURCE.character.base[resource.id]) return acc; + + const resourceData = this.document.system.resources[resource.id]; + acc[resource.id] = { + id: resource.id, + label: game.i18n.localize(resource.label), + value: resourceData.value, + max: resourceData.max, + fullIcon: resource.images?.full ?? { value: 'fa-solid fa-circle', isIcon: true }, + emptyIcon: resource.images?.empty ?? { value: 'fa-regular fa-circle', isIcon: true } + }; + + return acc; + }, {}); + + const html = document.createElement('div'); + html.innerHTML = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/resourceManagement.hbs`, + { + resources: extraResources + } + ); + + const target = button.closest('.resource-section'); + + game.tooltip.dismissLockedTooltips(); + game.tooltip.activate(target, { + html, + locked: true, + cssClass: 'bordered-tooltip', + direction: 'DOWN', + noOffset: true + }); + + const resourceManager = target.querySelector('.resource-manager'); + resourceManager.classList.toggle('inverted'); + + Hooks.once(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed, () => { + resourceManager.classList.toggle('inverted'); + }); + + for (const element of html.querySelectorAll('.resource-value')) + element.addEventListener('click', this.onUpdateResource.bind(this)); + } + + async onUpdateResource(event) { + const target = event.target.closest('.resource-value'); + const { resource, value: textValue } = target.dataset; + + const inputValue = Number.parseInt(textValue); + const decreasing = inputValue <= this.document.system.resources[resource].value; + const value = decreasing ? inputValue - 1 : inputValue; + await this.document.update({ [`system.resources.${resource}.value`]: value }, { render: false }); + + /* Update resource symbols */ + const section = target.closest('.resource-section'); + for (const element of section.querySelectorAll('.resource-value')) { + const showFull = Number.parseInt(element.dataset.value) <= value; + element.querySelector('.full').classList.toggle('hidden', !showFull); + element.querySelector('.empty').classList.toggle('hidden', showFull); + } + } + /** * Open the downtime application. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 9e533572..f9c0dfd9 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -264,15 +264,6 @@ export default class Party extends DHBaseActorSheet { ).render({ force: true }); } - /** - * Get the set of ContextMenu options for Consumable and Loot. - * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance - * @this {CharacterSheet} - * @protected - */ - static #getItemContextOptions() { - return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); - } /* -------------------------------------------- */ /* Filter Tracking */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/actor-setting.mjs b/module/applications/sheets/api/actor-setting.mjs index d8cfb40f..738f7002 100644 --- a/module/applications/sheets/api/actor-setting.mjs +++ b/module/applications/sheets/api/actor-setting.mjs @@ -44,8 +44,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee const context = await super._prepareContext(options); context.isNPC = this.actor.isNPC; - if (context.systemFields.attack) + if (context.systemFields.attack) { context.systemFields.attack.fields = this.actor.system.attack.schema.fields; + } + + // Create fake fields for actor configurable max resource value. + const resourceConfig = CONFIG.DH.RESOURCE[this.actor.type]?.all; + if (resourceConfig) { + const relevant = ['hitPoints', 'stress'].filter(r => r in resourceConfig); + context.resources = relevant.map(key => { + const data = this.actor._source.system.resources[key]; + const config = resourceConfig[key]; + return { + label: config.label, + name: `system.resources.${key}.max`, + value: data.max ?? config.max, + tooltip: key === 'hitPoints' ? game.i18n.localize('DAGGERHEART.UI.Tooltip.maxHPClassBound') : null, + field: new foundry.data.fields.NumberField({ + initial: config.max, + integer: true, + label: game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { + thing: game.i18n.localize(config.label) + }) + }) + }; + }); + } return context; } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 449880fb..b18176ec 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -691,6 +691,9 @@ export default function DHApplicationMixin(Base) { case 'weapon': presets.folder = 'equipments.folders.weapons'; break; + case 'feature': + presets.folder = 'features'; + break; case 'domainCard': presets.folder = 'domains'; presets.filter = { diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 85ecd616..6f994faf 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -36,7 +36,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { ], dragDrop: [ { dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }, - { dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null } + { dragSelector: '.currency[data-currency] .drag-handle', dropSelector: null } ] }; @@ -92,7 +92,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { value: context.source.system.gold[key] }; } - context.inventory.hasCurrency = Object.values(context.inventory.currencies).some((c) => c.enabled); + context.inventory.hasCurrency = Object.values(context.inventory.currencies).some(c => c.enabled); } return context; @@ -270,7 +270,9 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { currency }); if (quantity) { - originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) }); + originActor.update({ + [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) + }); this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity }); } return; @@ -292,6 +294,15 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { /* Handling transfer of inventoryItems */ if (item.system.metadata.isInventoryItem) { + if (!this.document.testUserPermission(game.user, 'OWNER', { exact: true })) { + return ui.notifications.error( + game.i18n.format('DAGGERHEART.UI.Notifications.lackingItemTransferPermission', { + user: game.user.name, + target: this.document.name + }) + ); + } + if (item.system.metadata.isQuantifiable) { const actorItem = originActor.items.get(data.originId); const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({ @@ -300,14 +311,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { }); if (quantityTransfered) { - if (quantityTransfered === actorItem.system.quantity) { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); - } else { - await actorItem.update({ - 'system.quantity': actorItem.system.quantity - quantityTransfered - }); - } - const existingItem = this.document.items.find(x => itemIsIdentical(x, item)); if (existingItem) { await existingItem.update({ @@ -325,10 +328,18 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { } ]); } + + if (quantityTransfered === actorItem.system.quantity) { + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); + } else { + await actorItem.update({ + 'system.quantity': actorItem.system.quantity - quantityTransfered + }); + } } } else { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); await this.document.createEmbeddedDocuments('Item', [item.toObject()]); + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); } } } @@ -339,7 +350,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { */ async _onDragStart(event) { // Handle drag/dropping currencies - const currencyEl = event.currentTarget.closest(".currency[data-currency]"); + const currencyEl = event.currentTarget.closest('.currency[data-currency]'); if (currencyEl) { const currency = currencyEl.dataset.currency; const data = { type: 'Currency', currency, originActor: this.document.uuid }; @@ -359,8 +370,8 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); return; - } - + } + const item = await getDocFromElement(event.target); if (item) { const dragData = { diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index b29437bf..26ae484b 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -1,4 +1,4 @@ -import { refreshIsAllowed } from '../../../helpers/utils.mjs'; +import { RefreshFeatures } from '../../../helpers/utils.mjs'; const { HandlebarsApplicationMixin } = foundry.applications.api; const { AbstractSidebarTab } = foundry.applications.sidebar; @@ -54,73 +54,6 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract return context; } - async getRefreshables(types) { - const refreshedActors = {}; - for (let actor of game.actors) { - if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { - const updates = {}; - for (let item of actor.items) { - if (item.system.metadata?.hasResource && refreshIsAllowed(types, item.system.resource?.recovery)) { - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - const increasing = - item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; - updates[item.id].system = { - ...updates[item.id].system, - 'resource.value': increasing - ? 0 - : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) - }; - } - if (item.system.metadata?.hasActions) { - const refreshTypes = new Set(); - const actions = item.system.actions.filter(action => { - if (refreshIsAllowed(types, action.uses.recovery)) { - refreshTypes.add(action.uses.recovery); - return true; - } - - return false; - }); - if (actions.length === 0) continue; - - if (!refreshedActors[actor.id]) - refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; - refreshedActors[actor.id].refreshed.add( - ...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) - ); - - if (!updates[item.id]?.system) updates[item.id] = { system: {} }; - - updates[item.id].system = { - ...updates[item.id].system, - ...actions.reduce( - (acc, action) => { - acc.actions[action.id] = { 'uses.value': 0 }; - return acc; - }, - { actions: updates[item.id].system.actions ?? {} } - ) - }; - } - } - - for (let key in updates) { - const update = updates[key]; - await actor.items.get(key).update(update); - } - } - } - - return refreshedActors; - } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -133,30 +66,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract static async #refreshActors() { const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); - await this.getRefreshables(refreshKeys); - const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', '); - ui.notifications.info( - game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { - types: `[${types}]` - }) - ); + await RefreshFeatures(refreshKeys); + this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); - - const cls = getDocumentClass('ChatMessage'); - const msg = { - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', - { - types: types - } - ), - title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), - speaker: cls.getSpeaker() - }; - - cls.create(msg); - this.render(); } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 2d882eba..f6235101 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -251,6 +251,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { /* If any noticeable slowdown occurs, consider replacing with enriching description on clicking to expand descriptions */ for (const item of this.items) { + if (["weapon", "armor"].includes(item.type)) { + item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs', + item.system, + ); + } item.system.enrichedDescription = (await item.system.getEnrichedDescription?.()) ?? (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); diff --git a/module/config/_module.mjs b/module/config/_module.mjs index 560f3fec..7a725f54 100644 --- a/module/config/_module.mjs +++ b/module/config/_module.mjs @@ -11,3 +11,4 @@ export * as settingsConfig from './settingsConfig.mjs'; export * as systemConfig from './system.mjs'; export * as itemBrowserConfig from './itemBrowserConfig.mjs'; export * as triggerConfig from './triggerConfig.mjs'; +export * as resourceConfig from './resourceConfig.mjs'; diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index c9b70193..0b1bf91d 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -35,6 +35,12 @@ export const actionTypes = { icon: 'fa-ghost', tooltip: 'DAGGERHEART.ACTIONS.TYPES.summon.tooltip' }, + transform: { + id: 'transform', + name: 'DAGGERHEART.ACTIONS.TYPES.transform.name', + icon: 'fa-dragon', + tooltip: 'DAGGERHEART.ACTIONS.TYPES.transform.tooltip' + }, effect: { id: 'effect', name: 'DAGGERHEART.ACTIONS.TYPES.effect.name', diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index ac55117a..aa7c3cf7 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -55,24 +55,6 @@ export const abilities = { } }; -export const scrollingTextResource = { - hitPoints: { - label: 'DAGGERHEART.GENERAL.HitPoints.plural', - reversed: true - }, - stress: { - label: 'DAGGERHEART.GENERAL.stress', - reversed: true - }, - hope: { - label: 'DAGGERHEART.GENERAL.hope' - }, - armor: { - label: 'DAGGERHEART.GENERAL.armor', - reversed: true - } -}; - export const featureProperties = { agility: { name: 'DAGGERHEART.CONFIG.Traits.agility.name', @@ -506,8 +488,8 @@ export const subclassFeatureLabels = { * @property {number[]} damage */ -/** - * @type {Record} +/** + * @type {Record} * Scaling data used to change an adversary's tier. Each rank is applied incrementally. */ export const adversaryScalingData = { @@ -518,7 +500,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 2, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -526,7 +508,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -534,7 +516,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 0, - attack: 2, + attack: 2 } }, horde: { @@ -544,7 +526,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 2, stress: 0, - attack: 0, + attack: 0 }, 3: { difficulty: 2, @@ -552,7 +534,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -560,7 +542,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 0, - attack: 0, + attack: 0 } }, leader: { @@ -570,7 +552,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -578,7 +560,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -586,7 +568,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 1, - attack: 3, + attack: 3 } }, minion: { @@ -596,7 +578,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -604,7 +586,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -612,7 +594,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 } }, ranged: { @@ -622,7 +604,7 @@ export const adversaryScalingData = { severeThreshold: 6, hp: 1, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -630,7 +612,7 @@ export const adversaryScalingData = { severeThreshold: 14, hp: 1, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -638,7 +620,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, skulk: { @@ -648,7 +630,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -656,7 +638,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -664,7 +646,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, solo: { @@ -674,7 +656,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 1, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -682,7 +664,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -690,7 +672,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 0, stress: 1, - attack: 3, + attack: 3 } }, standard: { @@ -700,7 +682,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -708,7 +690,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -716,7 +698,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 0, stress: 1, - attack: 1, + attack: 1 } }, support: { @@ -726,7 +708,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -734,7 +716,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -742,27 +724,27 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } } }; -/** +/** * Scaling data used for an adversary's damage. * Tier 4 is missing certain adversary types and therefore skews upwards. * We manually set tier 4 data to hopefully lead to better results */ export const adversaryExpectedDamage = { - basic: { - 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, - 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, - 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, - 4: { mean: 26, deviation: 5.2 } - }, - minion: { - 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, - 2: { mean: 5, deviation: 0.816496580927726 }, - 3: { mean: 6.5, deviation: 2.1213203435596424 }, - 4: { mean: 11, deviation: 1 } - } + basic: { + 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, + 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, + 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, + 4: { mean: 26, deviation: 5.2 } + }, + minion: { + 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, + 2: { mean: 5, deviation: 0.816496580927726 }, + 3: { mean: 6.5, deviation: 2.1213203435596424 }, + 4: { mean: 11, deviation: 1 } + } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 862625cb..70558c27 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -202,7 +202,8 @@ export const conditions = () => ({ id: 'vulnerable', name: 'DAGGERHEART.CONFIG.Condition.vulnerable.name', img: 'icons/magic/control/silhouette-fall-slip-prone.webp', - description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description' + description: 'DAGGERHEART.CONFIG.Condition.vulnerable.description', + autoApplyFlagId: 'auto-vulnerable' }, hidden: { id: 'hidden', diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs index 9140ea0a..61ba594c 100644 --- a/module/config/hooksConfig.mjs +++ b/module/config/hooksConfig.mjs @@ -1,3 +1,4 @@ export const hooksConfig = { - effectDisplayToggle: 'DHEffectDisplayToggle' + effectDisplayToggle: 'DHEffectDisplayToggle', + lockedTooltipDismissed: 'DHLockedTooltipDismissed' }; diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 7d80e597..77328987 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -467,9 +467,7 @@ export const allArmorFeatures = () => { }; export const orderedArmorFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .armorFeatures; - const allFeatures = { ...armorFeatures, ...homebrewFeatures }; + const allFeatures = allArmorFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { @@ -1404,9 +1402,7 @@ export const allWeaponFeatures = () => { }; export const orderedWeaponFeatures = () => { - const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures - .weaponFeatures; - const allFeatures = { ...weaponFeatures, ...homebrewFeatures }; + const allFeatures = allWeaponFeatures(); const all = Object.keys(allFeatures).map(key => { const feature = allFeatures[key]; return { diff --git a/module/config/resourceConfig.mjs b/module/config/resourceConfig.mjs new file mode 100644 index 00000000..f7d56b44 --- /dev/null +++ b/module/config/resourceConfig.mjs @@ -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 }, +}; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index d3f752bb..38ffcf3b 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -3,6 +3,10 @@ export const menu = { Name: 'GameSettingsAutomation', Icon: 'fa-solid fa-robot' }, + Metagaming: { + Name: 'GameSettingsMetagaming', + Icon: 'fa-solid fa-eye-low-vision' + }, Homebrew: { Name: 'GameSettingsHomebrew', Icon: 'fa-solid fa-flask-vial' @@ -19,6 +23,7 @@ export const menu = { export const gameSettings = { Automation: 'Automation', + Metagaming: 'Metagaming', Homebrew: 'Homebrew', appearance: 'Appearance', variantRules: 'VariantRules', diff --git a/module/config/system.mjs b/module/config/system.mjs index 47a41e8d..31dba518 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -2,6 +2,7 @@ import * as GENERAL from './generalConfig.mjs'; import * as DOMAIN from './domainConfig.mjs'; import * as ENCOUNTER from './encounterConfig.mjs'; import * as ACTOR from './actorConfig.mjs'; +import * as RESOURCE from './resourceConfig.mjs'; import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; @@ -19,6 +20,7 @@ export const SYSTEM = { GENERAL, DOMAIN, ACTOR, + RESOURCE, ITEM, SETTINGS, EFFECTS, diff --git a/module/data/action/_module.mjs b/module/data/action/_module.mjs index 9cfc48cb..043b039c 100644 --- a/module/data/action/_module.mjs +++ b/module/data/action/_module.mjs @@ -7,6 +7,7 @@ import EffectAction from './effectAction.mjs'; import HealingAction from './healingAction.mjs'; import MacroAction from './macroAction.mjs'; import SummonAction from './summonAction.mjs'; +import TransformAction from './transformAction.mjs'; export const actionsTypes = { base: BaseAction, @@ -17,5 +18,6 @@ export const actionsTypes = { summon: SummonAction, effect: EffectAction, macro: MacroAction, - beastform: BeastformAction + beastform: BeastformAction, + transform: TransformAction }; diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index c75cb8b8..9309137e 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -197,7 +197,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel async executeWorkflow(config) { for (const [key, part] of this.workflow) { if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return; - if ((await part.execute(config)) === false) return; + if ((await part.execute(config)) === false) return false; if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return; } } @@ -224,7 +224,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } // Execute the Action Worflow in order based of schema fields - await this.executeWorkflow(config); + const result = await this.executeWorkflow(config); + if (result === false) return; + await config.resourceUpdates.updateResources(); if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; diff --git a/module/data/action/transformAction.mjs b/module/data/action/transformAction.mjs new file mode 100644 index 00000000..7e552902 --- /dev/null +++ b/module/data/action/transformAction.mjs @@ -0,0 +1,5 @@ +import DHBaseAction from './baseAction.mjs'; + +export default class DHTransformAction extends DHBaseAction { + static extraSchemas = [...super.extraSchemas, 'transform']; +} diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 0a446c15..2053ac99 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -2,7 +2,7 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-set import { ActionField } from '../fields/actionField.mjs'; import { commonActorRules } from './base.mjs'; import DhCreature from './creature.mjs'; -import { resourceField, bonusField } from '../fields/actorField.mjs'; +import { bonusField } from '../fields/actorField.mjs'; import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; @@ -65,10 +65,6 @@ export default class DhpAdversary extends DhCreature { label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' }) }), - resources: new fields.SchemaField({ - hitPoints: resourceField(0, 0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), - stress: resourceField(0, 0, 'DAGGERHEART.GENERAL.stress', true) - }), rules: new fields.SchemaField({ ...commonActorRules() }), @@ -191,6 +187,7 @@ export default class DhpAdversary extends DhCreature { } prepareDerivedData() { + super.prepareDerivedData(); this.attack.roll.isStandardAttack = true; } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 5e16bac9..e2f910a0 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -213,7 +213,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { const textData = Object.keys(changes.system.resources).reduce((acc, key) => { const resource = changes.system.resources[key]; if (resource.value !== undefined && resource.value !== this.resources[key].value) { - acc.push(getScrollTextData(this.resources, resource, key)); + acc.push(getScrollTextData(this.parent, resource, key)); } return acc; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10fba63c..68f7f3a8 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import DhLevelData from '../levelData.mjs'; import { commonActorRules } from './base.mjs'; import DhCreature from './creature.mjs'; -import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; +import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; @@ -27,28 +27,6 @@ export default class DhCharacter extends DhCreature { return { ...super.defineSchema(), - resources: new fields.SchemaField({ - hitPoints: resourceField( - 0, - 0, - 'DAGGERHEART.GENERAL.HitPoints.plural', - true, - 'DAGGERHEART.ACTORS.Character.maxHPBonus' - ), - stress: resourceField(6, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: new fields.SchemaField( - { - value: new fields.NumberField({ - initial: 2, - min: 0, - integer: true, - label: 'DAGGERHEART.GENERAL.hope' - }), - isReversed: new fields.BooleanField({ initial: false }) - }, - { label: 'DAGGERHEART.GENERAL.hope' } - ) - }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'), @@ -609,6 +587,7 @@ export default class DhCharacter extends DhCreature { } prepareBaseData() { + super.prepareBaseData(); this.evasion += this.class.value?.system?.evasion ?? 0; const currentLevel = this.levelData.level.current; @@ -680,6 +659,7 @@ export default class DhCharacter extends DhCreature { } prepareDerivedData() { + super.prepareDerivedData(); let baseHope = this.resources.hope.value; if (this.companion) { for (let levelKey in this.companion.system.levelData.levelups) { @@ -699,6 +679,7 @@ export default class DhCharacter extends DhCreature { this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.resources.armor = { + label: 'DAGGERHEART.GENERAL.armor', value: this.armor?.system?.marks?.value ?? 0, max: this.armorScore, isReversed: true diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index 6f51b593..7a8f0e64 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -4,7 +4,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import { ActionField } from '../fields/actionField.mjs'; import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; -import { resourceField, bonusField } from '../fields/actorField.mjs'; +import { bonusField } from '../fields/actorField.mjs'; export default class DhCompanion extends DhCreature { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Companion']; @@ -26,10 +26,6 @@ export default class DhCompanion extends DhCreature { return { ...super.defineSchema(), partner: new ForeignDocumentUUIDField({ type: 'Actor' }), - resources: new fields.SchemaField({ - stress: resourceField(3, 0, 'DAGGERHEART.GENERAL.stress', true), - hope: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.hope' }) - }), evasion: new fields.NumberField({ required: true, min: 1, @@ -127,6 +123,7 @@ export default class DhCompanion extends DhCreature { } prepareBaseData() { + super.prepareBaseData(); this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; for (let levelKey in this.levelData.levelups) { @@ -161,6 +158,7 @@ export default class DhCompanion extends DhCreature { } prepareDerivedData() { + super.prepareDerivedData(); /* Partner Related Setup */ if (this.partner) { this.levelData.level.changed = this.partner.system.levelData.level.current; diff --git a/module/data/actor/creature.mjs b/module/data/actor/creature.mjs index 4b927aed..601068ad 100644 --- a/module/data/actor/creature.mjs +++ b/module/data/actor/creature.mjs @@ -1,3 +1,4 @@ +import { ResourcesField } from '../fields/actorField.mjs'; import BaseDataActor from './base.mjs'; export default class DhCreature extends BaseDataActor { @@ -7,6 +8,7 @@ export default class DhCreature extends BaseDataActor { return { ...super.defineSchema(), + resources: new ResourcesField(this.metadata.type), advantageSources: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' @@ -17,4 +19,45 @@ export default class DhCreature extends BaseDataActor { }) }; } + + get isAutoVulnerableActive() { + const vulnerableAppliedByOther = this.parent.effects.some( + x => x.statuses.has('vulnerable') && !x.flags.daggerheart?.autoApplyFlagId + ); + return !vulnerableAppliedByOther; + } + + async _preUpdate(changes, options, userId) { + const allowed = await super._preUpdate(changes, options, userId); + if (allowed === false) return; + + const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + if ( + automationSettings.vulnerableAutomation && + this.parent.type !== 'companion' && + changes.system?.resources?.stress?.value + ) { + const { name, description, img, autoApplyFlagId } = CONFIG.DH.GENERAL.conditions().vulnerable; + const autoEffects = this.parent.effects.filter( + x => x.flags.daggerheart?.autoApplyFlagId === autoApplyFlagId + ); + if (changes.system.resources.stress.value >= this.resources.stress.max) { + if (!autoEffects.length) + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + name: game.i18n.localize(name), + description: game.i18n.localize(description), + img: img, + statuses: ['vulnerable'], + flags: { daggerheart: { autoApplyFlagId } } + } + ]); + } else if (this.resources.stress.value >= this.resources.stress.max) { + this.parent.deleteEmbeddedDocuments( + 'ActiveEffect', + autoEffects.map(x => x.id) + ); + } + } + } } diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs index 0bdffca2..fa3b1cd5 100644 --- a/module/data/fields/action/_module.mjs +++ b/module/data/fields/action/_module.mjs @@ -10,3 +10,4 @@ export { default as DamageField } from './damageField.mjs'; export { default as RollField } from './rollField.mjs'; export { default as MacroField } from './macroField.mjs'; export { default as SummonField } from './summonField.mjs'; +export { default as TransformField } from './transformField.mjs'; diff --git a/module/data/fields/action/transformField.mjs b/module/data/fields/action/transformField.mjs new file mode 100644 index 00000000..07b32166 --- /dev/null +++ b/module/data/fields/action/transformField.mjs @@ -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; + } +} diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index db1faad4..65e7abbf 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -6,22 +6,6 @@ const attributeField = label => tierMarked: new fields.BooleanField({ initial: false }) }); -const resourceField = (max = 0, initial = 0, label, reverse = false, maxLabel) => - new fields.SchemaField( - { - value: new fields.NumberField({ initial: initial, min: 0, integer: true, label }), - max: new fields.NumberField({ - initial: max, - integer: true, - label: - maxLabel ?? - game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) - }), - isReversed: new fields.BooleanField({ initial: reverse }) - }, - { label } - ); - const stressDamageReductionRule = localizationPath => new fields.SchemaField({ cost: new fields.NumberField({ @@ -37,4 +21,67 @@ const bonusField = label => dice: new fields.ArrayField(new fields.StringField(), { label: `${game.i18n.localize(label)} Dice` }) }); -export { attributeField, resourceField, stressDamageReductionRule, bonusField }; +/** + * Field used for actor resources. It is a resource that validates dynamically based on the config. + * Because "max" may be defined during runtime, we don't attempt to clamp the maximum value. + */ +class ResourcesField extends fields.TypedObjectField { + constructor(actorType) { + super( + new fields.SchemaField({ + value: new fields.NumberField({ min: 0, initial: 0, integer: true }), + // Some resources allow changing max. A null max means its the default + max: new fields.NumberField({ initial: null, integer: true, nullable: true }) + }) + ); + this.actorType = actorType; + } + + getInitialValue() { + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + return Object.values(resources).reduce((result, resource) => { + result[resource.id] = { + value: resource.initial, + max: null + }; + return result; + }, {}); + } + + _validateKey(key) { + return key in CONFIG.DH.RESOURCE[this.actorType].all; + } + + _cleanType(value, options) { + value = super._cleanType(value, options); + + // If not partial, ensure all data exists + if (!options.partial) { + value = foundry.utils.mergeObject(this.getInitialValue(), value); + } + + return value; + } + + /** Initializes the original source data, returning prepared data */ + initialize(...args) { + const data = super.initialize(...args); + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + for (const [key, value] of Object.entries(data)) { + // TypedObjectField only calls _validateKey when persisting, so we also call it here + if (!this._validateKey(key)) { + delete value[key]; + continue; + } + + // Add basic prepared data. + const resource = resources[key]; + value.label = resource.label; + value.isReversed = resources[key].reverse; + value.max = typeof resource.max === 'number' ? value.max ?? resource.max : null; + } + return data; + } +} + +export { attributeField, ResourcesField, stressDamageReductionRule, bonusField }; diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 2d31c290..0958a9f3 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -23,9 +23,7 @@ export default class DHArmor extends AttachableItem { armorFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allArmorFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -58,7 +56,7 @@ export default class DHArmor extends AttachableItem { async getDescriptionData() { const baseDescription = this.description; const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); - const features = this.armorFeatures.map(x => allFeatures[x.value]); + const features = this.armorFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/armor/description.hbs', diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 84f39103..6f3256e3 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -224,7 +224,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const armorChanged = changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value; if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') { - const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); + const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor'); options.scrollingTextData = [armorData]; } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 5c6f8514..bb7fde0a 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -38,9 +38,7 @@ export default class DHWeapon extends AttachableItem { weaponFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ - required: true, - choices: CONFIG.DH.ITEM.allWeaponFeatures, - blank: true + required: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) @@ -114,24 +112,14 @@ export default class DHWeapon extends AttachableItem { async getDescriptionData() { const baseDescription = this.description; - const tier = game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`); - const trait = game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.attack.roll.trait].label); - const range = game.i18n.localize(`DAGGERHEART.CONFIG.Range.${this.attack.range}.name`); - const damage = Roll.replaceFormulaData(this.attack.damageFormula, this.parent.parent ?? this.parent); - const burden = game.i18n.localize(CONFIG.DH.GENERAL.burden[this.burden].label); - const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); - const features = this.weaponFeatures.map(x => allFeatures[x.value]); + const features = this.weaponFeatures.map(x => allFeatures[x.value]).filter(x => x); const prefix = await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/sheets/items/weapon/description.hbs', { - features, - tier, - trait, - range, - damage, - burden + item: this, + features } ); diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index e9952b1c..20fe0baf 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -18,6 +18,10 @@ export default class DhAutomation extends foundry.abstract.DataModel { label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' }) }), + vulnerableAutomation: new fields.BooleanField({ + initial: true, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.vulnerableAutomation.label' + }), countdownAutomation: new fields.BooleanField({ required: true, initial: true, diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index b8804fa7..9b4ee2cf 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -145,6 +145,16 @@ export default class DhHomebrew extends foundry.abstract.DataModel { description: new fields.StringField() }) ), + resources: new fields.TypedObjectField( + new fields.SchemaField({ + resources: new fields.TypedObjectField(new fields.EmbeddedDataField(Resource)) + }), + { + initial: { + character: { resources: {} } + } + } + ), itemFeatures: new fields.SchemaField({ weaponFeatures: new fields.TypedObjectField( new fields.SchemaField({ @@ -185,4 +195,117 @@ export default class DhHomebrew extends foundry.abstract.DataModel { } return source; } + + /** Invoked by the setting when data changes */ + handleChange() { + if (this.maxFear) { + if (ui.resources) ui.resources.render({ force: true }); + } + + this.refreshConfig(); + this.#resetActors(); + } + + /** Update config values based on homebrew data. Make sure the references don't change */ + refreshConfig() { + for (const [actorType, actorData] of Object.entries(this.resources)) { + const config = CONFIG.DH.RESOURCE[actorType]; + for (const key of Object.keys(config.all)) { + delete config.all[key]; + } + Object.assign(config.all, { + ...Object.entries(actorData.resources).reduce((result, [key, value]) => { + result[key] = value.toObject(); + result[key].id = key; + return result; + }, {}), + ...config.custom, + ...config.base, + }); + } + } + + /** + * Triggers a reset and non-forced re-render on all given actors (if given) + * or all world actors and actors in all scenes to show immediate results for a changed setting. + */ + #resetActors() { + const actors = new Set( + [ + game.actors.contents, + game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? []) + ].flat() + ); + for (const actor of actors) { + for (const app of Object.values(actor.apps)) { + for (const element of app.element?.querySelectorAll('prose-mirror.active')) { + element.open = false; // This triggers a save + } + } + + actor.reset(); + actor.render(); + } + } } + +export class Resource extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + initial: new fields.NumberField({ + required: true, + integer: true, + initial: 0, + min: 0, + label: 'DAGGERHEART.GENERAL.initial' + }), + max: new fields.NumberField({ + nullable: true, + initial: null, + min: 0, + label: 'DAGGERHEART.GENERAL.max' + }), + label: new fields.StringField({ label: 'DAGGERHEART.GENERAL.label' }), + images: new fields.SchemaField({ + full: imageIconField('fa solid fa-circle'), + empty: imageIconField('fa-regular fa-circle') + }) + }; + } + + static getDefaultResourceData = label => { + const images = Resource.schema.fields.images.getInitialValue(); + return { + initial: 0, + max: 0, + label: label ?? '', + images + }; + }; + + static getDefaultImageData = imageKey => { + return Resource.schema.fields.images.fields[imageKey].getInitialValue(); + }; +} + +const imageIconField = defaultValue => + new foundry.data.fields.SchemaField( + { + value: new foundry.data.fields.StringField({ + initial: defaultValue, + label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.value.label' + }), + isIcon: new foundry.data.fields.BooleanField({ + required: true, + initial: true, + label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.isIcon.label' + }), + noColorFilter: new foundry.data.fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.SETTINGS.Homebrew.FIELDS.resources.resources.noColorFilter.label' + }) + }, + { required: true } + ); diff --git a/module/data/settings/Metagaming.mjs b/module/data/settings/Metagaming.mjs new file mode 100644 index 00000000..2bb5afdf --- /dev/null +++ b/module/data/settings/Metagaming.mjs @@ -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' + }) + }; + } +} diff --git a/module/data/settings/_module.mjs b/module/data/settings/_module.mjs index db1fbfec..45405ba5 100644 --- a/module/data/settings/_module.mjs +++ b/module/data/settings/_module.mjs @@ -1,4 +1,5 @@ export { default as DhAppearance } from './Appearance.mjs'; export { default as DhAutomation } from './Automation.mjs'; export { default as DhHomebrew } from './Homebrew.mjs'; +export { default as DhMetagaming } from './Metagaming.mjs'; export { default as DhVariantRules } from './VariantRules.mjs'; diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index cd26eb21..ef5f9434 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,4 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; +import { parseRallyDice } from '../helpers/utils.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; @@ -33,7 +34,7 @@ export default class DamageRoll extends DHRoll { static async buildPost(roll, config, message) { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) - : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); + : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC); if (game.modules.get('dice-so-nice')?.active) { const pool = foundry.dice.terms.PoolTerm.fromRolls( Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) @@ -46,9 +47,14 @@ export default class DamageRoll extends DHRoll { chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, chatMessage.blind ); + config.mute = true; } await super.buildPost(roll, config, message); - if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); + if (config.source?.message) { + chatMessage.update({ 'system.damage': config.damage }); + + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } } static unifyDamageRoll(rolls) { @@ -192,7 +198,7 @@ export default class DamageRoll extends DHRoll { // Bardic Rally const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); if (rallyChoices.length) { diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 05ebe81f..f3527c02 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -143,8 +143,10 @@ export default class DHRoll extends Roll { /** @inheritDoc */ async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) { if (!this._evaluated) return; + + const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); - return foundry.applications.handlebars.renderTemplate(template, chatData); + return foundry.applications.handlebars.renderTemplate(template, { ...chatData, metagamingSettings }); } /** @inheritDoc */ diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 6433ab94..d16de9c9 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,6 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; -import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; @@ -68,7 +68,7 @@ export default class DualityRoll extends D20Roll { setRallyChoices() { return this.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); - if (change) a.push({ value: c.id, label: change.value }); + if (change) a.push({ value: c.id, label: parseRallyDice(change.value, c) }); return a; }, []); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 6af56e00..c87ce400 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -959,10 +959,23 @@ export default class DhpActor extends Actor { /** Get active effects */ getActiveEffects() { + const conditions = CONFIG.DH.GENERAL.conditions(); const statusMap = new Map(foundry.CONFIG.statusEffects.map(status => [status.id, status])); + const autoVulnerableActive = this.system.isAutoVulnerableActive; return this.effects .filter(x => !x.disabled) .reduce((acc, effect) => { + /* Could be generalized if needed. Currently just related to Vulnerable */ + const isAutoVulnerableEffect = + effect.flags.daggerheart?.autoApplyFlagId === conditions.vulnerable.autoApplyFlagId; + if (isAutoVulnerableEffect) { + if (!autoVulnerableActive) return acc; + + effect.appliedBy = game.i18n.localize('DAGGERHEART.CONFIG.Condition.vulnerable.autoAppliedByLabel'); + effect.isLockedCondition = true; + effect.condition = 'vulnerable'; + } + acc.push(effect); const currentStatusActiveEffects = acc.filter( diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 668ad06b..53921329 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -68,8 +68,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { document = fromUuidSync(uuid); if (!document) return; - e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER')); e.setAttribute('data-use-perm', document.testUserPermission(game.user, 'OWNER')); + + const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming); + if (settings.hideObserverPermissionInChat) + e.setAttribute('data-view-perm', document.testUserPermission(game.user, 'OBSERVER')); }); if (this.isContentVisible) { diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index c4b52bb5..4793f1f7 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -3,6 +3,7 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { #wide = false; #bordered = false; + #active = false; async activate(element, options = {}) { const { TextEditor } = foundry.applications.ux; @@ -168,7 +169,100 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti } } - super.activate(element, { ...options, html: html }); + this.baseActivate(element, { ...options, html: html }); + } + + /* Need to pass more options to _setAnchor, so have to copy whole foundry method >_< */ + async baseActivate(element, options) { + let { text, direction, cssClass, locked = false, html, content } = options; + if (content && !html) { + foundry.utils.logCompatibilityWarning( + 'The content option has been deprecated in favor of the html option', + { since: 13, until: 15, once: true } + ); + html = content; + } + if (text && html) throw new Error('Cannot provide both text and html options to TooltipManager#activate.'); + // Deactivate currently active element + this.deactivate(); + // Check if the element still exists in the DOM. + if (!document.body.contains(element)) return; + // Mark the new element as active + this.#active = true; + this.element = element; + element.setAttribute('aria-describedby', 'tooltip'); + html ||= element.dataset.tooltipHtml; + if (html) { + if (typeof html === 'string') this.tooltip.innerHTML = foundry.utils.cleanHTML(html); + else { + this.tooltip.innerHTML = ''; // Clear existing HTML + this.tooltip.appendChild(html); + } + } else { + text ||= element.dataset.tooltipText; + if (text) this.tooltip.textContent = text; + else { + text = element.dataset.tooltip; + // Localized message should be safe + if (game.i18n.has(text)) this.tooltip.innerHTML = game.i18n.localize(text); + else this.tooltip.innerHTML = foundry.utils.cleanHTML(text); + } + } + + // Activate display of the tooltip + this.tooltip.removeAttribute('class'); + this.tooltip.classList.add('active', 'themed', 'theme-dark'); + this.tooltip.showPopover(); + cssClass ??= element.closest('[data-tooltip-class]')?.dataset.tooltipClass; + if (cssClass) this.tooltip.classList.add(...cssClass.split(' ')); + + // Set tooltip position + direction ??= element.closest('[data-tooltip-direction]')?.dataset.tooltipDirection; + if (!direction) direction = this._determineDirection(); + this._setAnchor(direction, options); + + if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip(); + } + + _setAnchor(direction, options) { + const directions = this.constructor.TOOLTIP_DIRECTIONS; + const pad = this.constructor.TOOLTIP_MARGIN_PX; + const pos = this.element.getBoundingClientRect(); + + const { innerHeight, innerWidth } = this.tooltip.ownerDocument.defaultView; + const tooltipPadding = 16; + const horizontalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetWidth / 2 - pos.width / 2; + const verticalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetHeight / 2 - pos.height / 2; + + const style = {}; + switch (direction) { + case directions.DOWN: + style.textAlign = 'center'; + style.left = pos.left - horizontalOffset; + style.top = pos.bottom + pad; + break; + case directions.LEFT: + style.textAlign = 'left'; + style.right = innerWidth - pos.left + pad; + style.top = pos.top - verticalOffset; + break; + case directions.RIGHT: + style.textAlign = 'right'; + style.left = pos.right + pad; + style.top = pos.top - verticalOffset; + break; + case directions.UP: + style.textAlign = 'center'; + style.left = pos.left - horizontalOffset; + style.bottom = innerHeight - pos.top + pad; + break; + case directions.CENTER: + style.textAlign = 'center'; + style.left = pos.left - horizontalOffset; + style.top = pos.top - verticalOffset; + break; + } + return this._setStyle(style); } _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) { @@ -270,6 +364,12 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti return clone; } + /**@inheritdoc */ + dismissLockedTooltips() { + super.dismissLockedTooltips(); + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.lockedTooltipDismissed); + } + /** Get HTML for Battlepoints tooltip */ async getBattlepointHTML(combatId) { const combat = game.combats.get(combatId); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 4ecc7809..5704b891 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -119,8 +119,8 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {} }), maxTags: typeof maxTags === 'function' ? maxTags() : maxTags, dropdown: { + searchKeys: ['value', 'name'], mapValueTo: 'name', - searchKeys: ['value'], enabled: 0, maxItems: 100, closeOnSelect: true, @@ -378,17 +378,18 @@ export const arraysEqual = (a, b) => export const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value)); -export function getScrollTextData(resources, resource, key) { - const { reversed, label } = CONFIG.DH.ACTOR.scrollingTextResource[key]; +export function getScrollTextData(actor, resource, key) { const { BOTTOM, TOP } = CONST.TEXT_ANCHOR_POINTS; + + const resources = actor.system.resources; const increased = resources[key].value < resource.value; const value = -1 * (resources[key].value - resource.value); + const { label, isReversed } = resources[key]; const text = `${game.i18n.localize(label)} ${value.signedString()}`; - - const stroke = increased ? (reversed ? 0xffffff : 0x000000) : reversed ? 0x000000 : 0xffffff; - const fill = increased ? (reversed ? 0x0032b1 : 0xffe760) : reversed ? 0xffe760 : 0x0032b1; - const direction = increased ? (reversed ? BOTTOM : TOP) : reversed ? TOP : BOTTOM; + const stroke = increased ? (isReversed ? 0xffffff : 0x000000) : isReversed ? 0x000000 : 0xffffff; + const fill = increased ? (isReversed ? 0x0032b1 : 0xffe760) : isReversed ? 0xffe760 : 0x0032b1; + const direction = increased ? (isReversed ? BOTTOM : TOP) : isReversed ? TOP : BOTTOM; return { text, stroke, fill, direction }; } @@ -472,7 +473,7 @@ export function refreshIsAllowed(allowedTypes, typeToCheck) { case CONFIG.DH.GENERAL.refreshTypes.scene.id: case CONFIG.DH.GENERAL.refreshTypes.session.id: case CONFIG.DH.GENERAL.refreshTypes.longRest.id: - return allowedTypes.includes(typeToCheck); + return allowedTypes.includes?.(typeToCheck) ?? allowedTypes.has(typeToCheck); case CONFIG.DH.GENERAL.refreshTypes.shortRest.id: return allowedTypes.some( x => @@ -557,3 +558,121 @@ export function calculateExpectedValue(formulaOrTerms) { : [formulaOrTerms]; return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); } + +export function parseRallyDice(value, effect) { + const legacyStartsWithPrefix = value.toLowerCase().startsWith('d'); + const workingValue = legacyStartsWithPrefix ? value.slice(1) : value; + const dataParsedValue = itemAbleRollParse(workingValue, effect.parent); + + return `d${game.system.api.documents.DhActiveEffect.effectSafeEval(dataParsedValue)}`; +} +/** + * Refreshes character and/or adversary resources. + * @param { string[] } refreshTypes Which type of features to refresh using IDs from CONFIG.DH.GENERAL.refreshTypes + * @param { string[] = ['character', 'adversary'] } actorTypes Which actor types should refresh their features. Defaults to character and adversary. + * @param { boolean = true } sendRefreshMessage If a chat message should be created detailing the refresh + * @return { Actor[] } The actors that had their features refreshed + */ +export async function RefreshFeatures( + refreshTypes = [], + actorTypes = ['character', 'adversary'], + sendNotificationMessage = true, + sendRefreshMessage = true +) { + const refreshedActors = {}; + for (let actor of game.actors) { + if (actorTypes.includes(actor.type) && actor.prototypeToken.actorLink) { + const updates = {}; + for (let item of actor.items) { + if ( + item.system.metadata?.hasResource && + refreshIsAllowed(refreshTypes, item.system.resource?.recovery) + ) { + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + const increasing = + item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; + updates[item.id].system = { + ...updates[item.id].system, + 'resource.value': increasing + ? 0 + : game.system.api.documents.DhActiveEffect.effectSafeEval( + Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + ) + }; + } + if (item.system.metadata?.hasActions) { + const usedTypes = new Set(); + const actions = item.system.actions.filter(action => { + if (refreshIsAllowed(refreshTypes, action.uses.recovery)) { + usedTypes.add(action.uses.recovery); + return true; + } + + return false; + }); + if (actions.length === 0) continue; + + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + ...usedTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + updates[item.id].system = { + ...updates[item.id].system, + ...actions.reduce( + (acc, action) => { + acc.actions[action.id] = { 'uses.value': 0 }; + return acc; + }, + { actions: updates[item.id].system.actions ?? {} } + ) + }; + } + } + + for (let key in updates) { + const update = updates[key]; + await actor.items.get(key).update(update); + } + } + } + + const types = refreshTypes.map(x => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[x].label)).join(', '); + + if (sendNotificationMessage) { + ui.notifications.info( + game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { + types: `[${types}]` + }) + ); + } + + if (sendRefreshMessage) { + const cls = getDocumentClass('ChatMessage'); + const msg = { + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', + { + types: types + } + ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), + speaker: cls.getSpeaker() + }; + + cls.create(msg); + } + + return refreshedActors; +} diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index ad8c741a..9ccb16f4 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -17,9 +17,10 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/global/partials/resource-section/dice-value.hbs', 'systems/daggerheart/templates/sheets/global/partials/resource-section/die.hbs', 'systems/daggerheart/templates/sheets/global/partials/resource-bar.hbs', + 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', + 'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs', - 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs', 'systems/daggerheart/templates/actionTypes/damage.hbs', 'systems/daggerheart/templates/actionTypes/resource.hbs', @@ -33,6 +34,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/actionTypes/beastform.hbs', 'systems/daggerheart/templates/actionTypes/countdown.hbs', 'systems/daggerheart/templates/actionTypes/summon.hbs', + 'systems/daggerheart/templates/actionTypes/transform.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index c4acf7ed..8440e3fe 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -1,10 +1,11 @@ import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; import DhCountdowns from '../data/countdowns.mjs'; -import { DhAppearance, DhAutomation, DhHomebrew, DhVariantRules } from '../data/settings/_module.mjs'; +import { DhAppearance, DhAutomation, DhHomebrew, DhMetagaming, DhVariantRules } from '../data/settings/_module.mjs'; import { DhAppearanceSettings, DhAutomationSettings, DhHomebrewSettings, + DhMetagamingSettings, DhVariantRuleSettings } from '../applications/settings/_module.mjs'; import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; @@ -38,17 +39,18 @@ const registerMenuSettings = () => { type: DhAutomation }); + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, { + scope: 'world', + config: false, + type: DhMetagaming + }); + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, { scope: 'world', config: false, type: DhHomebrew, onChange: value => { - if (value.maxFear) { - if (ui.resources) ui.resources.render({ force: true }); - } - - // Some homebrew settings may change sheets in various ways, so trigger a re-render - resetActors(); + value.handleChange(); } }); @@ -76,6 +78,16 @@ const registerMenus = () => { type: DhAutomationSettings, restricted: true }); + + game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Metagaming.Name, { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.name'), + label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.label'), + hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.metagaming.hint'), + icon: CONFIG.DH.SETTINGS.menu.Metagaming.Icon, + type: DhMetagamingSettings, + restricted: true + }); + game.settings.registerMenu(CONFIG.DH.id, CONFIG.DH.SETTINGS.menu.Homebrew.Name, { name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.name'), label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.homebrew.label'), @@ -144,30 +156,8 @@ const registerNonConfigSettings = () => { }); game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { - scope: 'client', + scope: 'world', config: false, type: CompendiumBrowserSettings }); }; - -/** - * Triggers a reset and non-forced re-render on all given actors (if given) - * or all world actors and actors in all scenes to show immediate results for a changed setting. - */ -function resetActors(actors) { - actors ??= [ - game.actors.contents, - game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? []) - ].flat(); - actors = new Set(actors); - for (const actor of actors) { - for (const app of Object.values(actor.apps)) { - for (const element of app.element?.querySelectorAll('prose-mirror.active')) { - element.open = false; // This triggers a save - } - } - - actor.reset(); - actor.render(); - } -} diff --git a/src/packs/adversaries/adversary_Fallen_Warlord__Realm_Breaker_hxZ0sgoFJubh5aj6.json b/src/packs/adversaries/adversary_Fallen_Warlord__Realm_Breaker_hxZ0sgoFJubh5aj6.json index b2cdc489..8f3865e9 100644 --- a/src/packs/adversaries/adversary_Fallen_Warlord__Realm_Breaker_hxZ0sgoFJubh5aj6.json +++ b/src/packs/adversaries/adversary_Fallen_Warlord__Realm_Breaker_hxZ0sgoFJubh5aj6.json @@ -717,7 +717,35 @@ "system": { "description": "

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.

", "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, "originId": null, "featureForm": "reaction" diff --git a/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json b/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json index b23da064..7f9deb6c 100644 --- a/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json +++ b/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json @@ -846,7 +846,37 @@ "system": { "description": "

When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.pMuXGCSOQaxpi5tb]{Ashen Tyrant} and immediately spotlight them.

", "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, "originId": null, "featureForm": "reaction" diff --git a/src/packs/adversaries/adversary_Volcanic_Dragon__Obsidian_Predator_ladm7wykhZczYzrQ.json b/src/packs/adversaries/adversary_Volcanic_Dragon__Obsidian_Predator_ladm7wykhZczYzrQ.json index 2e2adbdd..5f32aae5 100644 --- a/src/packs/adversaries/adversary_Volcanic_Dragon__Obsidian_Predator_ladm7wykhZczYzrQ.json +++ b/src/packs/adversaries/adversary_Volcanic_Dragon__Obsidian_Predator_ladm7wykhZczYzrQ.json @@ -742,7 +742,37 @@ "system": { "description": "

When the @Lookup[@name] marks their last HP, replace them with the @UUID[Compendium.daggerheart.adversaries.Actor.eArAPuB38CNR0ZIM]{Molten Scourge} and immediately spotlight them.

", "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, "originId": null, "featureForm": "reaction" diff --git a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json index b7830722..c4dd83a7 100644 --- a/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json +++ b/src/packs/classes/class_Bard_vegl3bFOq3pcFTWT.json @@ -20,10 +20,6 @@ { "type": "class", "item": "Compendium.daggerheart.classes.Item.PydiMnNCKpd44SGS" - }, - { - "type": "class", - "item": "Compendium.daggerheart.classes.Item.TVeEyqmPPiRa2r3i" } ], "subclasses": [ diff --git a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json index e8d4c3c9..e2a0b5bb 100644 --- a/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json +++ b/src/packs/classes/feature_Rally_PydiMnNCKpd44SGS.json @@ -63,7 +63,7 @@ { "key": "system.bonuses.rally", "mode": 2, - "value": "d6", + "value": "6 + min((floor(@system.levelData.level.current / 5)*2), 2)", "priority": null } ], diff --git a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json b/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json deleted file mode 100644 index 46717fcb..00000000 --- a/src/packs/classes/feature_Rally__Level_5__TVeEyqmPPiRa2r3i.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "folder": "C9y59fIkq50d3SyD", - "name": "Rally (Level 5)", - "type": "feature", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "system": { - "description": "

Once per session, describe how you rally the party and give yourself and each of your allies a Rally Die. At level 1, your Rally Die is a d6. A PC can spend their Rally Die to roll it, adding the result to their action roll, reaction roll, damage roll, or to clear a number of Stress equal to the result. At the end of each session, clear all unspent Rally Dice. At level 5, your Rally Die increases to a d8.

", - "resource": null, - "actions": { - "Z1KWFrpXOqZWuZD1": { - "type": "effect", - "_id": "Z1KWFrpXOqZWuZD1", - "systemPath": "actions", - "description": "", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "1", - "recovery": "session" - }, - "effects": [ - { - "_id": "8CFxYJV8zE6Wabwj", - "onSave": false - } - ], - "target": { - "type": "any", - "amount": null - }, - "name": "Rally your Allies", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "range": "" - } - }, - "originItemType": null, - "originId": null, - "attribution": { - "source": "Daggerheart SRD", - "page": 9, - "artist": "" - } - }, - "effects": [ - { - "name": "Rally (Level 5)", - "img": "icons/tools/instruments/drum-hand-tan.webp", - "origin": "Compendium.daggerheart.classes.Item.oxv0m8AFUQVFKtZ4", - "transfer": false, - "_id": "8CFxYJV8zE6Wabwj", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "changes": [ - { - "key": "system.bonuses.rally", - "mode": 2, - "value": "d8", - "priority": null - } - ], - "disabled": false, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "", - "tint": "#ffffff", - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!TVeEyqmPPiRa2r3i.8CFxYJV8zE6Wabwj" - } - ], - "flags": {}, - "ownership": { - "default": 0, - "LgnbNMLaxandgMQq": 3 - }, - "_id": "TVeEyqmPPiRa2r3i", - "sort": 300000, - "_key": "!items!TVeEyqmPPiRa2r3i" -} diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index 95f42c06..953b3a2c 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -85,7 +85,7 @@ { "trigger": "dualityRoll", "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
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
\n \n \n
\n
`;\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
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}
\n
\n \n \n
\n
`;\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;" } ] } diff --git a/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json b/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json index 8cdb62b0..16753e1e 100644 --- a/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json +++ b/src/packs/domains/domainCard_Wrangle_9DwSxHoUwl8Kxj3n.json @@ -53,7 +53,7 @@ "difficulty": null, "damageMod": "none" }, - "name": "Agility Check", + "name": "Agility Roll", "img": "icons/skills/melee/sword-engraved-glow-purple.webp", "range": "close" } diff --git a/styles/less/global/global.less b/styles/less/global/global.less index fb995b8c..b9af67c0 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -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 */ diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 9045baf5..4bd4d0bb 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -103,10 +103,9 @@ display: flex; align-items: center; justify-content: end; - gap: 8px; a { - width: 15px; + width: 20px; text-align: center; } @@ -139,24 +138,6 @@ 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 { @@ -275,8 +256,10 @@ grid-area: controls; align-self: start; padding-top: 0.3125rem; - gap: 4px; margin-bottom: -1px; + a { + width: 18px; + } } > .item-labels { align-self: start; @@ -334,6 +317,27 @@ 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 { display: flex; flex-direction: column; diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less index 9be56c8e..5c21dc60 100644 --- a/styles/less/sheets/actions/actions.less +++ b/styles/less/sheets/actions/actions.less @@ -4,48 +4,78 @@ display: flex; flex-direction: column; 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; align-items: center; gap: 5px; - border-radius: 3px; - .actor-summon-name { - flex: 2; - 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; - } + img { + height: 40px; } } - .summon-dragger { + .actor-drop-controls { + flex: 1; 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); + gap: 5px; + + &.transform { + justify-content: flex-end; + } + + .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 { diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index bf6393f4..23db088a 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -183,6 +183,11 @@ } } + .domain-details { + display: flex; + flex-direction: column; + } + .level-details { align-self: center; } diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 5e8ef002..31fd4256 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -133,8 +133,19 @@ padding: 0; margin-bottom: 15px; - .hope-section { + .resource-section { + display: flex; + align-items: center; + gap: 4px; margin-right: 20px; + + .resource-manager { + transition: all 0.1s ease; + + &.inverted { + transform: rotate(180deg); + } + } } .downtime-section { diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 1e723ed7..e9ef9147 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -158,7 +158,7 @@ .daggerheart, #chat-notifications { .chat-message { - --text-color: @golden; + --text-color: light-dark(@dark-blue, @golden); --bg-color: @golden-40; [data-use-perm='false'] { @@ -233,7 +233,7 @@ font-family: @font-subtitle; font-size: var(--font-size-18); font-weight: bold; - color: light-dark(@dark-blue, var(--text-color)); + color: var(--text-color); margin-bottom: -2px; } @@ -450,6 +450,10 @@ .target-data { flex: 1; + + .target-name { + text-align: left; + } } .target-save { diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 065e43c5..5a6e5878 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -27,6 +27,7 @@ @import './settings/settings.less'; @import './settings/homebrew-settings/domains.less'; @import './settings/homebrew-settings/types.less'; +@import './settings/homebrew-settings/resources.less'; @import './sidebar/tabs.less'; @import './sidebar/daggerheartMenu.less'; diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index b395f8c8..f558a0ba 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -304,7 +304,15 @@ padding: 0 0 0 50px; display: flex; 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 { font-size: var(--font-size-32); @@ -350,6 +358,7 @@ .filter-content, .item-desc { display: grid; + opacity: 0; grid-template-rows: 0fr; transition: all 0.3s ease-in-out; width: 100%; @@ -378,8 +387,8 @@ } .expanded + .extensible { + opacity: 1; grid-template-rows: 1fr; - padding-top: 10px; } .welcome-message { diff --git a/styles/less/ui/settings/homebrew-settings/resources.less b/styles/less/ui/settings/homebrew-settings/resources.less new file mode 100644 index 00000000..5333e54d --- /dev/null +++ b/styles/less/ui/settings/homebrew-settings/resources.less @@ -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%; + } + } +} diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index 0bd1b71e..c6c40f78 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -4,3 +4,5 @@ @import './tooltip/domain-cards.less'; @import './autocomplete/autocomplete.less'; + +@import './tooltip/resource-management.less'; diff --git a/styles/less/ux/tooltip/resource-management.less b/styles/less/ux/tooltip/resource-management.less new file mode 100644 index 00000000..ff1f4dd2 --- /dev/null +++ b/styles/less/ux/tooltip/resource-management.less @@ -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; + } + } + } + } +} diff --git a/system.json b/system.json index fb23ad7b..34d7e438 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.7.2", + "version": "1.9.0", "compatibility": { "minimum": "13.346", "verified": "13.351", diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index 429977d9..18fed2e2 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -1,19 +1,19 @@ -
+
{{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}}
    {{#each @root.summons as |summon index|}} -
  • -
    +
  • +

    {{summon.actor.name}}

    -
    +
    @@ -43,7 +43,7 @@
  • {{/each}} -
    +
    {{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}}
diff --git a/templates/actionTypes/transform.hbs b/templates/actionTypes/transform.hbs new file mode 100644 index 00000000..ae17862a --- /dev/null +++ b/templates/actionTypes/transform.hbs @@ -0,0 +1,62 @@ +
+ + {{localize "DAGGERHEART.ACTIONS.TYPES.transform.name"}} + + +
+ {{#if transform.actor}} +
+ {{#if transform.actor.error}} +
{{transform.actor.error}}
+ {{else}} +
+ +

+ {{transform.actor.name}} +

+
+ {{/if}} + +
+
+ {{#unless transform.actor.error}} + + + + {{/unless}} + + + +
+
+
+ + {{/if}} + + {{#unless transform.actor}} +
+ {{localize "DAGGERHEART.ACTIONS.Settings.transform.dropTransformHere"}} +
+ {{/unless}} + +
+
+ + {{localize "DAGGERHEART.ACTIONS.Settings.transform.clearHitPoints"}} +
+
+ + {{localize "DAGGERHEART.ACTIONS.Settings.transform.clearStress"}} +
+
+
+
\ No newline at end of file diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index c5f4d871..65bafab8 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -14,6 +14,7 @@ {{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}} + {{formGroup settingFields.schema.fields.vulnerableAutomation value=settingFields._source.vulnerableAutomation localize=true}} {{formGroup settingFields.schema.fields.countdownAutomation value=settingFields._source.countdownAutomation localize=true}} {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} diff --git a/templates/settings/homebrew-settings/resources.hbs b/templates/settings/homebrew-settings/resources.hbs new file mode 100644 index 00000000..7f3dee3e --- /dev/null +++ b/templates/settings/homebrew-settings/resources.hbs @@ -0,0 +1,78 @@ +
+
+ {{#each settingFields.resources as |type key|}} +
+ + {{localize "DAGGERHEART.SETTINGS.Homebrew.resources.typeTitle" type=(localize (concat "TYPES.Actor." key))}} + + + +
+
+ {{#each type.resources as |resource key|}} +
+ {{resource.label}} + + {{formField @root.schemaFields.resources.element.fields.resources.element.fields.label value=resource.label name=(concat "resources." @../key ".resources." key ".label") classes="vertical" localize=true }} + +
+ {{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 }} +
+ +
+
+ {{#with @root.schemaFields.resources.element.fields.resources.element.fields.images.fields.full.fields}} +
+
+ {{localize "DAGGERHEART.SETTINGS.Homebrew.resources.filledIcon"}} + +
+
+
+ {{#if ../images.full.isIcon}} + {{formGroup this.value value=../images.full.value name=(concat "resources." @../key ".resources." key ".images.full.value") localize=true }} + {{else}} +
+ +
+ {{/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 }} +
+ {{/with}} +
+
+ {{#with @root.schemaFields.resources.element.fields.resources.element.fields.images.fields.empty.fields}} +
+
+ {{localize "DAGGERHEART.SETTINGS.Homebrew.resources.emptyIcon"}} + +
+
+
+ {{#if ../images.empty.isIcon}} + {{formGroup this.value value=../images.empty.value name=(concat "resources." @../key ".resources." key ".images.empty.value") localize=true }} + {{else}} +
+ +
+ {{/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 }} +
+ {{/with}} +
+
+
+ {{/each}} +
+
+
+ {{/each}} +
+
\ No newline at end of file diff --git a/templates/settings/metagaming-settings/footer.hbs b/templates/settings/metagaming-settings/footer.hbs new file mode 100644 index 00000000..54939c17 --- /dev/null +++ b/templates/settings/metagaming-settings/footer.hbs @@ -0,0 +1,10 @@ +
+ + +
\ No newline at end of file diff --git a/templates/settings/metagaming-settings/general.hbs b/templates/settings/metagaming-settings/general.hbs new file mode 100644 index 00000000..7f36715e --- /dev/null +++ b/templates/settings/metagaming-settings/general.hbs @@ -0,0 +1,3 @@ +
+ {{formGroup settingFields.schema.fields.hideObserverPermissionInChat value=settingFields._source.hideObserverPermissionInChat localize=true}} +
\ No newline at end of file diff --git a/templates/settings/metagaming-settings/header.hbs b/templates/settings/metagaming-settings/header.hbs new file mode 100644 index 00000000..e0f72ee8 --- /dev/null +++ b/templates/settings/metagaming-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{localize 'DAGGERHEART.SETTINGS.Menu.metagaming.name'}}

+
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index e94f4328..1bdd0304 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -11,4 +11,5 @@ {{#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.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}} \ No newline at end of file diff --git a/templates/sheets-settings/adversary-settings/details.hbs b/templates/sheets-settings/adversary-settings/details.hbs index 065ebe74..dc2fd386 100644 --- a/templates/sheets-settings/adversary-settings/details.hbs +++ b/templates/sheets-settings/adversary-settings/details.hbs @@ -20,15 +20,11 @@
- {{localize "DAGGERHEART.GENERAL.HitPoints.plural"}} - {{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}} - {{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.max.label")}} -
-
- {{localize "DAGGERHEART.GENERAL.stress"}} - {{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")}} -
+ {{localize "DAGGERHEART.GENERAL.Resource.plural"}} + {{#each resources as |resource|}} + {{formGroup resource.field value=resource.value name=resource.name}} + {{/each}} +
@@ -36,4 +32,4 @@ {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}} {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}}
- \ No newline at end of file + diff --git a/templates/sheets-settings/character-settings/details.hbs b/templates/sheets-settings/character-settings/details.hbs index 3f9247e0..4bda501e 100644 --- a/templates/sheets-settings/character-settings/details.hbs +++ b/templates/sheets-settings/character-settings/details.hbs @@ -22,15 +22,12 @@ {{localize 'DAGGERHEART.GENERAL.basics'}}
- {{formGroup systemFields.resources.fields.hitPoints.fields.value value=document._source.system.resources.hitPoints.value localize=true}} - - {{formGroup systemFields.resources.fields.hitPoints.fields.max value=document._source.system.resources.hitPoints.max localize=true}} - + {{#each resources as |resource|}} + + {{formGroup resource.field value=resource.value name=resource.name}} + + {{/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.proficiency value=document._source.system.proficiency localize=true}} @@ -39,4 +36,4 @@
- \ No newline at end of file + diff --git a/templates/sheets-settings/companion-settings/details.hbs b/templates/sheets-settings/companion-settings/details.hbs index 88878d67..6a602d38 100644 --- a/templates/sheets-settings/companion-settings/details.hbs +++ b/templates/sheets-settings/companion-settings/details.hbs @@ -7,8 +7,9 @@ {{localize 'DAGGERHEART.GENERAL.basics'}}
{{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}} - {{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}} + {{#each resources as |resource|}} + {{formGroup resource.field value=resource.value name=resource.name}} + {{/each}}
@@ -19,4 +20,4 @@
- \ No newline at end of file + diff --git a/templates/sheets/actors/character/features.hbs b/templates/sheets/actors/character/features.hbs index acabd37e..3e942468 100644 --- a/templates/sheets/actors/character/features.hbs +++ b/templates/sheets/actors/character/features.hbs @@ -4,19 +4,21 @@ {{#each document.system.sheetLists as |category|}} {{#if (eq category.type 'feature' )}} {{> 'daggerheart.inventory-items' - title=category.title - type='feature' - collection=category.values - canCreate=true - showActions=true + title=category.title + type='feature' + actorType='character' + collection=category.values + canCreate=true + showActions=true }} {{else if category.values}} {{> 'daggerheart.inventory-items' - title=category.title - type='feature' - collection=category.values - canCreate=false - showActions=true + title=category.title + type='feature' + actorType='character' + collection=category.values + canCreate=false + showActions=true }} {{/if}} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index d2c01f3c..06f464fa 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -65,22 +65,25 @@
-
-

{{localize "DAGGERHEART.GENERAL.hope"}}

- {{#times document.system.resources.hope.max}} - - {{#if (gte ../document.system.resources.hope.value (add this 1))}} - - {{else}} - - {{/if}} - - {{/times}} - {{#times document.system.scars}} - - - - {{/times}} +
+
+

{{localize "DAGGERHEART.GENERAL.hope"}}

+ {{#times document.system.resources.hope.max}} + + {{#if (gte ../document.system.resources.hope.value (add this 1))}} + + {{else}} + + {{/if}} + + {{/times}} + {{#times document.system.scars}} + + + + {{/times}} + {{#if hasExtraResources}}{{/if}} +
{{#if document.system.class.value}}
diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index ae95b7af..54e44e64 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,5 +1,9 @@
  • + + {{item.system.recallCost}} + +
    @@ -109,7 +99,7 @@ Parameters: {{else if (eq type 'armor')}} - + {{/if}} {{#if (eq type 'domainCard')}} @@ -125,7 +115,7 @@ Parameters: {{/if}} {{#if (hasProperty item "toChat")}} - + {{/if}} {{else}} @@ -138,7 +128,7 @@ Parameters: {{/unless}} {{#unless hideContextMenu}} - + {{/unless}} {{/if}} diff --git a/templates/sheets/global/partials/item-tags.hbs b/templates/sheets/global/partials/item-tags.hbs new file mode 100644 index 00000000..b30fcbf2 --- /dev/null +++ b/templates/sheets/global/partials/item-tags.hbs @@ -0,0 +1,8 @@ +
    + {{#each _getTags as |tag|}} +
    + {{tag}} +
    + {{/each}} + {{#if @partial-block}}{{> @partial-block}}{{/if}} +
    \ No newline at end of file diff --git a/templates/sheets/items/armor/description.hbs b/templates/sheets/items/armor/description.hbs index c234fa10..af2698ef 100644 --- a/templates/sheets/items/armor/description.hbs +++ b/templates/sheets/items/armor/description.hbs @@ -1,19 +1,6 @@
    -
    -
    -

    {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}

    - {{item.system.baseThresholds.major}}/{{item.system.baseThresholds.severe}} -
    - -
    -

    {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}

    - {{item.system.baseScore}} -
    -
    - {{#if features.length}}
    -

    {{localize "DAGGERHEART.GENERAL.features"}}

    {{#each features as | feature |}}
    {{localize feature.label}}: {{{localize feature.description}}}
    {{/each}} diff --git a/templates/sheets/items/weapon/description.hbs b/templates/sheets/items/weapon/description.hbs index d8e128e7..af2698ef 100644 --- a/templates/sheets/items/weapon/description.hbs +++ b/templates/sheets/items/weapon/description.hbs @@ -1,36 +1,6 @@
    -
    -
    -

    {{localize "DAGGERHEART.GENERAL.Tiers.singular"}}

    - {{tier}} -
    - -
    -

    {{localize "DAGGERHEART.GENERAL.Trait.single"}}

    - {{trait}} -
    - -
    -

    {{localize "DAGGERHEART.GENERAL.range"}}

    - {{range}} -
    -
    - -
    -
    -

    {{localize "DAGGERHEART.GENERAL.damage"}}

    - {{damage}} -
    - -
    -

    {{localize "DAGGERHEART.GENERAL.burden"}}

    - {{burden}} -
    -
    - {{#if features.length}}
    -

    {{localize "DAGGERHEART.GENERAL.features"}}

    {{#each features as | feature |}}
    {{localize feature.label}}: {{{localize feature.description}}}
    {{/each}} diff --git a/templates/ui/chat/parts/target-part.hbs b/templates/ui/chat/parts/target-part.hbs index 477445a3..9a545926 100644 --- a/templates/ui/chat/parts/target-part.hbs +++ b/templates/ui/chat/parts/target-part.hbs @@ -1,6 +1,6 @@
    {{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}}
    - {{#if isGM}} + {{#if (or isGM (not metagamingSettings.hideObserverPermissionInChat))}}
    {{#if (or (gt targetShort.hit 0) (gt targetShort.miss 0))}} diff --git a/templates/ui/itemBrowser/itemContainer.hbs b/templates/ui/itemBrowser/itemContainer.hbs index 0040a692..8dd75156 100644 --- a/templates/ui/itemBrowser/itemContainer.hbs +++ b/templates/ui/itemBrowser/itemContainer.hbs @@ -10,7 +10,10 @@
    - {{{system.enrichedDescription}}} + + {{{system.enrichedTags}}} + {{{system.enrichedDescription}}} +
    {{/each}} \ No newline at end of file diff --git a/templates/ui/tooltip/domainCard.hbs b/templates/ui/tooltip/domainCard.hbs index 9388f1a4..0380a523 100644 --- a/templates/ui/tooltip/domainCard.hbs +++ b/templates/ui/tooltip/domainCard.hbs @@ -5,7 +5,7 @@ - {{#with (lookup config.DOMAIN.domains item.system.domain) as | domain |}} + {{#with (lookup allDomains item.system.domain) as | domain |}} {{/with}} diff --git a/templates/ui/tooltip/resourceManagement.hbs b/templates/ui/tooltip/resourceManagement.hbs new file mode 100644 index 00000000..b9817e6e --- /dev/null +++ b/templates/ui/tooltip/resourceManagement.hbs @@ -0,0 +1,21 @@ +
    + {{#each resources as |resource|}} +
    +

    {{resource.label}}

    + {{#times resource.max}} + + {{#if resource.fullIcon.isIcon}} + + {{else}} + + {{/if}} + {{#if resource.emptyIcon.isIcon}} + + {{else}} + + {{/if}} + + {{/times}} +
    + {{/each}} +
    \ No newline at end of file