diff --git a/daggerheart.mjs b/daggerheart.mjs index 9239d338..872da5f3 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,7 +3,7 @@ import * as applications from './module/applications/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; -import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs'; +import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs'; @@ -20,6 +20,7 @@ import { placeables } from './module/canvas/_module.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; import { registerDHActorHooks } from './module/documents/actor.mjs'; import './node_modules/@yaireo/tagify/dist/tagify.css'; +import { renderDamageButton } from './module/enrichers/DamageEnricher.mjs'; Hooks.once('init', () => { CONFIG.DH = SYSTEM; @@ -29,18 +30,7 @@ Hooks.once('init', () => { documents }; - CONFIG.TextEditor.enrichers.push( - ...[ - { - pattern: /\[\[\/dr\s?(.*?)\]\]/g, - enricher: DhDualityRollEnricher - }, - { - pattern: /^@Template\[(.*)\]$/g, - enricher: DhTemplateEnricher - } - ] - ); + CONFIG.TextEditor.enrichers.push(...enricherConfig); CONFIG.statusEffects = [ ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), @@ -178,33 +168,15 @@ Hooks.on('ready', () => { Hooks.once('dicesoniceready', () => {}); Hooks.on('renderChatMessageHTML', (_, element) => { - element - .querySelectorAll('.duality-roll-button') - .forEach(element => element.addEventListener('click', renderDualityButton)); - - element - .querySelectorAll('.measured-template-button') - .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); + enricherRenderSetup(element); }); Hooks.on('renderJournalEntryPageProseMirrorSheet', (_, element) => { - element - .querySelectorAll('.duality-roll-button') - .forEach(element => element.addEventListener('click', renderDualityButton)); - - element - .querySelectorAll('.measured-template-button') - .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); + enricherRenderSetup(element); }); Hooks.on('renderHandlebarsApplication', (_, element) => { - element - .querySelectorAll('.duality-roll-button') - .forEach(element => element.addEventListener('click', renderDualityButton)); - - element - .querySelectorAll('.measured-template-button') - .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); + enricherRenderSetup(element); }); Hooks.on('chatMessage', (_, message) => { diff --git a/lang/en.json b/lang/en.json index 4d3927bc..ae6ccdf4 100755 --- a/lang/en.json +++ b/lang/en.json @@ -13,6 +13,9 @@ "armor": "Armor", "beastform": "Beastform" }, + "ActiveEffect": { + "beastform": "Beastform" + }, "Actor": { "character": "Character", "companion": "Companion", @@ -110,8 +113,25 @@ "horderHp": "Horde/HP" }, "Character": { + "advantageSources": { + "label": "Advantage Sources", + "hint": "Add single words or short text as reminders and hints of what a character has advantage on." + }, "age": "Age", "companionFeatures": "Companion Features", + "contextMenu": { + "consume": "Consume Item", + "equip": "Equip", + "sendToChat": "Send To Chat", + "toLoadout": "Send to Loadout", + "toVault": "Send to Vault", + "unequip": "Unequip", + "useItem": "Use Item" + }, + "disadvantageSources": { + "label": "Disadvantage Sources", + "hint": "Add single words or short text as reminders and hints of what a character has disadvantage on." + }, "faith": "Faith", "levelUp": "You can level up", "pronouns": "Pronouns", @@ -572,6 +592,11 @@ "description": "You reduce incoming magic damage by your Armor Score before applying it to your damage thresholds." } }, + "BeastformType": { + "normal": "Normal", + "evolved": "Evolved", + "hybrid": "Hybrid" + }, "Burden": { "oneHanded": "One-Handed", "twoHanded": "Two-Handed" @@ -1019,6 +1044,7 @@ }, "Advantage": { "full": "Advantage", + "plural": "Advantages", "short": "Adv" }, "Adversary": { @@ -1202,6 +1228,11 @@ "hint": "The cost in stress you can pay to reduce minor damage to none." } } + }, + "attack": { + "damage": { + "value": { "label": "Base Attack: Damage" } + } } }, "Tabs": { @@ -1235,14 +1266,19 @@ "recovery": "Recovery", "setup": "Setup", "equipment": "Equipment", - "attachments": "Attachments" - }, - "Tiers": { - "singular": "Tier", + "attachments": "Attachments", + "advanced": "Advanced", "tier1": "Tier 1", "tier2": "Tier 2", "tier3": "Tier 3", - "tier4": "Tier 4" + "tier4": "tier 4" + }, + "Tiers": { + "singular": "Tier", + "1": "Tier 1", + "2": "Tier 2", + "3": "Tier 3", + "4": "Tier 4" }, "Trait": { "single": "Trait", @@ -1278,7 +1314,7 @@ "features": "Features", "formula": "Formula", "healing": "Healing", - "hitPoints": { + "HitPoints": { "single": "Hit Point", "plural": "Hit Points", "short": "HP" @@ -1318,6 +1354,8 @@ "total": "Total", "true": "True", "type": "Type", + "unarmed": "Unarmed", + "unarmedStrike": "Unarmed Strike", "unarmored": "Unarmored", "use": "Use", "used": "Used", @@ -1351,7 +1389,9 @@ }, "Beastform": { "FIELDS": { + "beastformType": { "label": "Beastform Type" }, "tier": { "label": "Tier" }, + "mainTrait": { "label": "Main Trait" }, "examples": { "label": "Examples" }, "advantageOn": { "label": "Gain Advantage On" }, "tokenImg": { "label": "Token Image" }, @@ -1360,12 +1400,28 @@ "placeholder": "Using character dimensions", "height": { "label": "Height" }, "width": { "label": "Width" } + }, + "evolved": { + "maximumTier": { "label": "Maximum Tier" }, + "mainTraitBonus": { "label": "Main Trait Bonus" } + }, + "hybrid": { + "beastformOptions": { "label": "Nr Beastforms" }, + "advantages": { "label": "Nr Advantages" }, + "features": { "label": "Nr Features" } } }, + "attackName": "Beast Attack", + "beastformEffect": "Beastform Transformation", "dialogTitle": "Beastform Selection", "tokenTitle": "Beastform Token", "transform": "Transform", - "beastformEffect": "Beastform Transformation" + "evolve": "Evolve", + "evolvedFeatureTitle": "Evolved", + "evolvedDrag": "Drag a form here to evolve it.", + "hybridize": "Hybridize", + "hybridizeFeatureTitle": "Hybrid Features", + "hybridizeDrag": "Drag a form here to hybridize it." }, "Class": { "hopeFeatures": "Hope Features", @@ -1609,7 +1665,11 @@ "featureNotSecondary": "This feature is used as something else than a Secondary feature and cannot be used here.", "featureNotFoundation": "This feature is used as something else than a Foundation feature and cannot be used here.", "featureNotSpecialization": "This feature is used as something else than a Specialization feature and cannot be used here.", - "featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here." + "featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here.", + "beastformMissingEffect": "The Beastform is missing a Beastform Effect. Cannot be used.", + "beastformToManyAdvantages": "You cannot select any more advantages.", + "beastformToManyFeatures": "You cannot select any more features.", + "beastformEquipWeapon": "You cannot use weapons while in a Beastform." }, "Tooltip": { "disableEffect": "Disable Effect", @@ -1624,6 +1684,7 @@ "sendToLoadout": "Send to Loadout", "makeDeathMove": "Make a Death Move", "rangeAndTarget": "Range & Target", + "dragApplyEffect": "Drag effect to apply it to an actor", "appliedEvenIfSuccessful": "Applied even if save succeeded" } } diff --git a/module/applications/dialogs/beastformDialog.mjs b/module/applications/dialogs/beastformDialog.mjs index 367311b0..1d6725ad 100644 --- a/module/applications/dialogs/beastformDialog.mjs +++ b/module/applications/dialogs/beastformDialog.mjs @@ -6,6 +6,10 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat this.configData = configData; this.selected = null; + this.evolved = { form: null }; + this.hybrid = { forms: {}, advantages: {}, features: {} }; + + this._dragDrop = this._createDragDropHandlers(); } static DEFAULT_OPTIONS = { @@ -17,13 +21,16 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat }, actions: { selectBeastform: this.selectBeastform, + toggleHybridFeature: this.toggleHybridFeature, + toggleHybridAdvantage: this.toggleHybridAdvantage, submitBeastform: this.submitBeastform }, form: { handler: this.updateBeastform, submitOnChange: true, submitOnClose: false - } + }, + dragDrop: [{ dragSelector: '.beastform-container', dropSelector: '.advanced-form-container' }] }; get title() { @@ -32,36 +39,217 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat /** @override */ static PARTS = { - beastform: { - template: 'systems/daggerheart/templates/dialogs/beastformDialog.hbs' + tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' }, + beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' }, + advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' }, + footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' } + }; + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], + initial: '1', + labelPrefix: 'DAGGERHEART.GENERAL.Tiers' } }; + changeTab(tab, group, options) { + super.changeTab(tab, group, options); + + this.render(); + } + + _createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.callbacks = { + dragstart: this._onDragStart.bind(this), + drop: this._onDrop.bind(this) + }; + return new foundry.applications.ux.DragDrop.implementation(d); + }); + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + this._dragDrop.forEach(d => d.bind(htmlElement)); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.beastformTiers = game.items.reduce((acc, x) => { - const tier = CONFIG.DH.GENERAL.tiers[x.system.tier]; - if (x.type !== 'beastform' || tier.value > this.configData.tierLimit) return acc; + context.selected = this.selected; + context.selectedBeastformEffect = this.selected?.effects?.find?.(x => x.type === 'beastform'); - if (!acc[tier.value]) acc[tier.value] = { label: game.i18n.localize(tier.label), values: {} }; - acc[tier.value].values[x.uuid] = { selected: this.selected == x.uuid, value: x }; + context.evolved = this.evolved; + context.hybridForms = Object.keys(this.hybrid.forms).reduce((acc, formKey) => { + if (!this.hybrid.forms[formKey]) { + acc[formKey] = null; + } else { + const data = this.hybrid.forms[formKey].toObject(); + acc[formKey] = { + ...data, + system: { + ...data.system, + features: this.hybrid.forms[formKey].system.features.map(feature => ({ + ...feature.toObject(), + uuid: feature.uuid, + selected: Boolean(this.hybrid.features?.[formKey]?.[feature.uuid]) + })), + advantageOn: Object.keys(data.system.advantageOn).reduce((acc, key) => { + acc[key] = { + ...data.system.advantageOn[key], + selected: Boolean(this.hybrid.advantages?.[formKey]?.[key]) + }; + return acc; + }, {}) + } + }; + } return acc; - }, {}); // Also get from compendium when added - context.canSubmit = this.selected; + }, {}); + + const maximumDragTier = Math.max( + this.selected?.system?.evolved?.maximumTier ?? 0, + this.selected?.system?.hybrid?.maximumTier ?? 0 + ); + + const compendiumBeastforms = await game.packs.get(`daggerheart.beastforms`)?.getDocuments(); + const beastformTiers = [...(compendiumBeastforms ? compendiumBeastforms : []), ...game.items].reduce( + (acc, x) => { + const tier = CONFIG.DH.GENERAL.tiers[x.system.tier]; + if (x.type !== 'beastform' || tier.id > this.configData.tierLimit) return acc; + + if (!acc[tier.id]) acc[tier.id] = { label: game.i18n.localize(tier.label), values: {} }; + + acc[tier.id].values[x.uuid] = { + selected: this.selected?.uuid == x.uuid, + value: x, + draggable: + !['evolved', 'hybrid'].includes(x.system.beastformType) && maximumDragTier + ? x.system.tier <= maximumDragTier + : false + }; + + return acc; + }, + {} + ); + + context.tier = beastformTiers[this.tabGroups.primary]; + context.tierKey = this.tabGroups.primary; + + context.canSubmit = this.canSubmit(); return context; } + canSubmit() { + if (this.selected) { + switch (this.selected.system.beastformType) { + case 'normal': + return true; + case 'evolved': + return this.evolved.form; + case 'hybrid': + const selectedAdvantages = Object.values(this.hybrid.advantages).reduce( + (acc, form) => acc + Object.values(form).length, + 0 + ); + const selectedFeatures = Object.values(this.hybrid.features).reduce( + (acc, form) => acc + Object.values(form).length, + 0 + ); + + const advantagesSelected = selectedAdvantages === this.selected.system.hybrid.advantages; + const featuresSelected = selectedFeatures === this.selected.system.hybrid.features; + return advantagesSelected && featuresSelected; + } + } + + return false; + } + static updateBeastform(event, _, formData) { this.selected = foundry.utils.mergeObject(this.selected, formData.object); this.render(); } - static selectBeastform(_, target) { - this.selected = this.selected === target.dataset.uuid ? null : target.dataset.uuid; + static async selectBeastform(_, target) { + this.element.querySelectorAll('.beastform-container ').forEach(element => { + if (element.dataset.uuid === target.dataset.uuid && this.selected?.uuid !== target.dataset.uuid) { + element.classList.remove('inactive'); + } else { + element.classList.add('inactive'); + } + }); + + const uuid = this.selected?.uuid === target.dataset.uuid ? null : target.dataset.uuid; + this.selected = uuid ? await foundry.utils.fromUuid(uuid) : null; + + if (this.selected) { + if (this.selected.system.beastformType !== 'evolved') this.evolved.form = null; + if (this.selected.system.beastformType !== 'hybrid') { + this.hybrid.forms = {}; + this.hybrid.advantages = {}; + this.hybrid.features = {}; + } else { + this.hybrid.forms = [...Array(this.selected.system.hybrid.beastformOptions).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = null; + return acc; + }, {}); + } + } + + this.render(); + } + + static toggleHybridFeature(_, button) { + const current = this.hybrid.features[button.dataset.form]; + if (!current) this.hybrid.features[button.dataset.form] = {}; + + if (this.hybrid.features[button.dataset.form][button.id]) + delete this.hybrid.features[button.dataset.form][button.id]; + else { + const currentFeatures = Object.values(this.hybrid.features).reduce( + (acc, form) => acc + Object.values(form).length, + 0 + ); + if (currentFeatures === this.selected.system.hybrid.features) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyFeatures')); + return; + } + + const feature = this.hybrid.forms[button.dataset.form].system.features.find(x => x.uuid === button.id); + this.hybrid.features[button.dataset.form][button.id] = feature; + } + + this.render(); + } + + static toggleHybridAdvantage(_, button) { + const current = this.hybrid.advantages[button.dataset.form]; + if (!current) this.hybrid.advantages[button.dataset.form] = {}; + + if (this.hybrid.advantages[button.dataset.form][button.id]) + delete this.hybrid.advantages[button.dataset.form][button.id]; + else { + const currentAdvantages = Object.values(this.hybrid.advantages).reduce( + (acc, form) => acc + Object.values(form).length, + 0 + ); + if (currentAdvantages === this.selected.system.hybrid.advantages) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyAdvantages')); + return; + } + + const advantage = this.hybrid.forms[button.dataset.form].system.advantageOn[button.id]; + this.hybrid.advantages[button.dataset.form][button.id] = advantage; + } + this.render(); } @@ -71,14 +259,60 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat /** @override */ _onClose(options = {}) { - if (!options.submitted) this.config = false; + if (!options.submitted) this.selected = null; } static async configure(configData) { return new Promise(resolve => { const app = new this(configData); - app.addEventListener('close', () => resolve(app.selected), { once: true }); + app.addEventListener( + 'close', + () => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid }), + { once: true } + ); app.render({ force: true }); }); } + + async _onDragStart(event) { + const target = event.currentTarget; + const abort = () => event.preventDefault(); + if (!this.selected) abort(); + + const draggedForm = await foundry.utils.fromUuid(target.dataset.uuid); + if (['evolved', 'hybrid'].includes(draggedForm.system.beastformType)) abort(); + + if (this.selected.system.beastformType === 'evolved') { + if (draggedForm.system.tier > this.selected.system.evolved.maximumTier) abort(); + } + if (this.selected.system.beastformType === 'hybrid') { + if (draggedForm.system.tier > this.selected.system.hybrid.maximumTier) abort(); + } + + event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset)); + event.dataTransfer.setDragImage(target, 60, 0); + } + + async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.getDragEventData(event); + const item = await fromUuid(data.uuid); + if (!item) return; + + if (event.target.closest('.advanced-form-container.evolved')) { + this.evolved.form = item; + } else { + const hybridContainer = event.target.closest('.advanced-form-container.hybridized'); + if (hybridContainer) { + const existingId = Object.keys(this.hybrid.forms).find( + key => this.hybrid.forms[key]?.uuid === item.uuid + ); + if (existingId) this.hybrid.forms[existingId] = null; + + this.hybrid.forms[hybridContainer.id] = item; + } + } + + this.render(); + } } diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 1b61b96d..78452054 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -61,7 +61,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application static updateRollConfiguration(_event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); - foundry.utils.mergeObject(this.config.roll, rest.roll) + foundry.utils.mergeObject(this.config.roll, rest.roll); this.config.selectedRollMode = rest.selectedRollMode; this.render(); diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 658cef96..3e3bde44 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -10,7 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.reject = reject; this.actor = actor; this.damage = damage; - + const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true); const maxArmorMarks = canApplyArmor ? Math.min( diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index db3c4d27..3f915e41 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -114,7 +114,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; context.tierOptions = [ - { key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.tier1') }, + { key: 1, label: game.i18n.localize('DAGGERHEART.GENERAL.Tiers.1') }, ...Object.values(settingsTiers).map(x => ({ key: x.tier, label: x.name })) ]; diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index c249d6d5..7a91b272 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -70,9 +70,9 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { } /** - * + * * @type {ApplicationClickAction} - * @returns + * @returns */ static async #deleteAdversary(_event, target) { const doc = getDocFromElement(target); diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index e6508695..f6324881 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -8,7 +8,7 @@ export default class AdversarySheet extends DHBaseActorSheet { position: { width: 660, height: 766 }, window: { resizable: true }, actions: { - reactionRoll: AdversarySheet.#reactionRoll, + reactionRoll: AdversarySheet.#reactionRoll }, window: { resizable: true diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8f157c96..4fe4b5e3 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -619,6 +619,12 @@ export default class CharacterSheet extends DHBaseActorSheet { await item.update({ 'system.equipped': true }); break; case 'weapon': + if (this.document.effects.find(x => x.type === 'beastform')) { + return ui.notifications.warn( + game.i18n.localize('DAGGERHEART.UI.Notifications.beastformEquipWeapon') + ); + } + await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); await item.update({ 'system.equipped': true }); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index b343eb62..65a43123 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -84,24 +84,26 @@ export default function DHApplicationMixin(Base) { useItem: DHSheetV2.#useItem, useAction: DHSheetV2.#useAction, toggleEffect: DHSheetV2.#toggleEffect, - toggleExtended: DHSheetV2.#toggleExtended, + toggleExtended: DHSheetV2.#toggleExtended }, - contextMenus: [{ - handler: DHSheetV2.#getEffectContextOptions, - selector: '[data-item-uuid][data-type="effect"]', - options: { - parentClassHooks: false, - fixed: true + contextMenus: [ + { + handler: DHSheetV2.#getEffectContextOptions, + selector: '[data-item-uuid][data-type="effect"]', + options: { + parentClassHooks: false, + fixed: true + } }, - }, - { - handler: DHSheetV2.#getActionContextOptions, - selector: '[data-item-uuid][data-type="action"]', - options: { - parentClassHooks: false, - fixed: true + { + handler: DHSheetV2.#getActionContextOptions, + selector: '[data-item-uuid][data-type="action"]', + options: { + parentClassHooks: false, + fixed: true + } } - }], + ], dragDrop: [], tagifyConfigs: [] }; @@ -132,13 +134,13 @@ export default function DHApplicationMixin(Base) { /**@inheritdoc */ _syncPartState(partId, newElement, priorElement, state) { super._syncPartState(partId, newElement, priorElement, state); - for (const el of priorElement.querySelectorAll(".extensible.extended")) { + for (const el of priorElement.querySelectorAll('.extensible.extended')) { const { actionId, itemUuid } = el.parentElement.dataset; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; const newExtensible = newElement.querySelector(selector); if (!newExtensible) continue; - newExtensible.classList.add("extended"); + newExtensible.classList.add('extended'); const descriptionElement = newExtensible.querySelector('.invetory-description'); if (descriptionElement) { this.#prepareInventoryDescription(newExtensible, descriptionElement); @@ -209,14 +211,14 @@ export default function DHApplicationMixin(Base) { * @param {DragEvent} event * @protected */ - _onDragStart(event) { } + _onDragStart(event) {} /** * Handle drop event. * @param {DragEvent} event * @protected */ - _onDrop(event) { } + _onDrop(event) {} /* -------------------------------------------- */ /* Context Menu */ @@ -251,7 +253,7 @@ export default function DHApplicationMixin(Base) { icon: 'fa-regular fa-lightbulb', condition: target => getDocFromElement(target).disabled, callback: target => getDocFromElement(target).update({ disabled: false }) - }, + } ].map(option => ({ ...option, name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`, @@ -269,7 +271,7 @@ export default function DHApplicationMixin(Base) { */ static #getActionContextOptions() { /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ - const getAction = (target) => { + const getAction = target => { const { actionId } = target.closest('[data-action-id]').dataset; const { actions, attack } = this.document.system; return attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); @@ -279,30 +281,31 @@ export default function DHApplicationMixin(Base) { { name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', icon: 'fa-solid fa-burst', - condition: this.document instanceof foundry.documents.Actor || + condition: + this.document instanceof foundry.documents.Actor || (this.document instanceof foundry.documents.Item && this.document.parent), - callback: (target, event) => getAction(target).use(event), + callback: (target, event) => getAction(target).use(event) }, { name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', icon: 'fa-solid fa-message', - callback: (target) => getAction(target).toChat(this.document.id), + callback: target => getAction(target).toChat(this.document.id) }, { name: 'CONTROLS.CommonEdit', icon: 'fa-solid fa-pen-to-square', - callback: (target) => new DHActionConfig(getAction(target)).render({ force: true }) + callback: target => new DHActionConfig(getAction(target)).render({ force: true }) }, { name: 'CONTROLS.CommonDelete', icon: 'fa-solid fa-trash', - condition: (target) => { + condition: target => { const { actionId } = target.closest('[data-action-id]').dataset; const { attack } = this.document.system; - return attack?.id !== actionId + return attack?.id !== actionId; }, - callback: async (target) => { - const action = getAction(target) + callback: async target => { + const action = getAction(target); const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { @@ -310,12 +313,14 @@ export default function DHApplicationMixin(Base) { name: action.name }) }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { + name: action.name + }) }); if (!confirmed) return; return this.document.update({ - 'system.actions': this.document.system.actions.filter((a) => a.id !== action.id) + 'system.actions': this.document.system.actions.filter(a => a.id !== action.id) }); } } @@ -337,35 +342,38 @@ export default function DHApplicationMixin(Base) { name: 'CONTROLS.CommonEdit', icon: 'fa-solid fa-pen-to-square', callback: target => getDocFromElement(target).sheet.render({ force: true }) - }, + } ]; - if (usable) options.unshift({ - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', - icon: 'fa-solid fa-burst', - callback: (target, event) => getDocFromElement(target).use(event), - }); + if (usable) + options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', + icon: 'fa-solid fa-burst', + callback: (target, event) => getDocFromElement(target).use(event) + }); - if (toChat) options.unshift({ - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', - icon: 'fa-solid fa-message', - callback: (target) => getDocFromElement(target).toChat(this.document.id), - }); + if (toChat) + options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', + icon: 'fa-solid fa-message', + callback: target => getDocFromElement(target).toChat(this.document.id) + }); - if (deletable) options.push({ - name: 'CONTROLS.CommonDelete', - icon: 'fa-solid fa-trash', - callback: (target, event) => { - const doc = getDocFromElement(target); - if (event.shiftKey) return doc.delete(); - else return doc.deleteDialog(); - } - }) + if (deletable) + options.push({ + name: 'CONTROLS.CommonDelete', + icon: 'fa-solid fa-trash', + callback: (target, event) => { + const doc = getDocFromElement(target); + if (event.shiftKey) return doc.delete(); + else return doc.deleteDialog(); + } + }); return options.map(option => ({ ...option, icon: `` - })) + })); } /* -------------------------------------------- */ @@ -400,17 +408,20 @@ export default function DHApplicationMixin(Base) { const doc = itemUuid ? getDocFromElement(extensibleElement) : this.document.system.attack?.id === actionId - ? this.document.system.attack - : this.document.system.actions?.find(a => a.id === actionId); + ? this.document.system.attack + : this.document.system.actions?.find(a => a.id === actionId); if (!doc) return; const description = doc.system?.description ?? doc.description; const isAction = !!actionId; - descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(description, { - relativeTo: isAction ? doc.parent : doc, - rollData: doc.getRollData?.(), - secrets: isAction ? doc.parent.isOwner : doc.isOwner - }); + descriptionElement.innerHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML( + description, + { + relativeTo: isAction ? doc.parent : doc, + rollData: doc.getRollData?.(), + secrets: isAction ? doc.parent.isOwner : doc.isOwner + } + ); } /* -------------------------------------------- */ @@ -427,26 +438,28 @@ export default function DHApplicationMixin(Base) { const parent = parentIsItem && documentClass === 'Item' ? null : this.document; if (type === 'action') { - const { type: actionType } = await foundry.applications.api.DialogV2.input({ - window: { title: 'Select Action Type' }, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/actionTypes/actionType.hbs', - { types: CONFIG.DH.ACTIONS.actionTypes } - ), - ok: { - label: game.i18n.format('DOCUMENT.Create', { - type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single') - }), - } - }) ?? {}; + const { type: actionType } = + (await foundry.applications.api.DialogV2.input({ + window: { title: 'Select Action Type' }, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/actionTypes/actionType.hbs', + { types: CONFIG.DH.ACTIONS.actionTypes } + ), + ok: { + label: game.i18n.format('DOCUMENT.Create', { + type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single') + }) + } + })) ?? {}; if (!actionType) return; - const cls = game.system.api.models.actions.actionsTypes[actionType] - const action = new cls({ - _id: foundry.utils.randomID(), - type: actionType, - name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), - ...cls.getSourceConfig(this.document) - }, + const cls = game.system.api.models.actions.actionsTypes[actionType]; + const action = new cls( + { + _id: foundry.utils.randomID(), + type: actionType, + name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), + ...cls.getSourceConfig(this.document) + }, { parent: this.document } @@ -456,14 +469,13 @@ export default function DHApplicationMixin(Base) { force: true }); return action; - } else { const cls = getDocumentClass(documentClass); const data = { name: cls.defaultName({ type, parent }), - type, - } - if (inVault) data["system.inVault"] = true; + type + }; + if (inVault) data['system.inVault'] = true; if (disabled) data.disabled = true; const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); @@ -474,7 +486,6 @@ export default function DHApplicationMixin(Base) { } return doc; } - } /** @@ -489,7 +500,7 @@ export default function DHApplicationMixin(Base) { const { actionId } = target.closest('[data-action-id]').dataset; const { actions, attack } = this.document.system; const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - new DHActionConfig(action).render({ force: true }) + new DHActionConfig(action).render({ force: true }); } /** @@ -500,8 +511,8 @@ export default function DHApplicationMixin(Base) { const doc = getDocFromElement(target); if (doc) { - if (event.shiftKey) return doc.delete() - else return await doc.deleteDialog() + if (event.shiftKey) return doc.delete(); + else return await doc.deleteDialog(); } // TODO: REDO this @@ -524,7 +535,7 @@ export default function DHApplicationMixin(Base) { } return await this.document.update({ - 'system.actions': actions.filter((a) => a.id !== action.id) + 'system.actions': actions.filter(a => a.id !== action.id) }); } @@ -555,7 +566,7 @@ export default function DHApplicationMixin(Base) { const { actionId } = target.closest('[data-action-id]').dataset; const { actions, attack } = this.document.system; doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - if(this.document instanceof foundry.documents.Item && !this.document.parent) return; + if (this.document instanceof foundry.documents.Item && !this.document.parent) return; } await doc.use(event); @@ -573,7 +584,6 @@ export default function DHApplicationMixin(Base) { await action.use(event); } - /** * Toggle a ActiveEffect * @type {ApplicationClickAction} @@ -604,7 +614,6 @@ export default function DHApplicationMixin(Base) { const descriptionElement = extensible?.querySelector('.invetory-description'); if (t && !!descriptionElement) this.#prepareInventoryDescription(extensible, descriptionElement); } - } return DHSheetV2; diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 56f00e9b..346a0ab6 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -22,7 +22,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { }, actions: { openSettings: DHBaseActorSheet.#openSettings, - sendExpToChat: DHBaseActorSheet.#sendExpToChat, + sendExpToChat: DHBaseActorSheet.#sendExpToChat }, contextMenus: [ { @@ -59,7 +59,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { return context; } - /**@inheritdoc */ async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); @@ -81,7 +80,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { async _prepareEffectsContext(context, _options) { context.effects = { actives: [], - inactives: [], + inactives: [] }; for (const effect of this.actor.allApplicableEffects()) { @@ -104,7 +103,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 766b443c..1888be9e 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -72,10 +72,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { secrets: this.item.isOwner }); break; - case "effects": - await this._prepareEffectsContext(context, options) + case 'effects': + await this._prepareEffectsContext(context, options); break; - case "features": + case 'features': context.isGM = game.user.isGM; break; } @@ -93,7 +93,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { async _prepareEffectsContext(context, _options) { context.effects = { actives: [], - inactives: [], + inactives: [] }; for (const effect of this.item.effects) { @@ -113,30 +113,30 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { * @protected */ static #getFeatureContextOptions() { - const options = this._getContextMenuCommonOptions({ usable: true, toChat: true, deletable: false }) - options.push( - { - name: 'CONTROLS.CommonDelete', - icon: '', - callback: async (target) => { - const feature = getDocFromElement(target); - if (!feature) return; - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize(`TYPES.Item.feature`), - name: feature.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name }) - }); - if (!confirmed) return; - await this.document.update({ - 'system.features': this.document.system.toObject().features.filter(uuid => uuid !== feature.uuid) - }); - }, + const options = this._getContextMenuCommonOptions({ usable: true, toChat: true, deletable: false }); + options.push({ + name: 'CONTROLS.CommonDelete', + icon: '', + callback: async target => { + const feature = getDocFromElement(target); + if (!feature) return; + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`TYPES.Item.feature`), + name: feature.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { + name: feature.name + }) + }); + if (!confirmed) return; + await this.document.update({ + 'system.features': this.document.system.toObject().features.filter(uuid => uuid !== feature.uuid) + }); } - ) + }); return options; } @@ -153,7 +153,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { const actionIndex = button.closest('[data-index]').dataset.index; const action = this.document.system.actions[actionIndex]; - if(!event.shiftKey) { + if (!event.shiftKey) { const confirmed = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { @@ -166,7 +166,6 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { if (!confirmed) return; } - await this.document.update({ 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) }); @@ -180,9 +179,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { const { type } = target.dataset; const cls = foundry.documents.Item.implementation; const feature = await cls.create({ - type: 'feature', - name: cls.defaultName({ type: 'feature' }), - "system.subType": CONFIG.DH.ITEM.featureSubTypes[type] + 'type': 'feature', + 'name': cls.defaultName({ type: 'feature' }), + 'system.subType': CONFIG.DH.ITEM.featureSubTypes[type] }); await this.document.update({ 'system.features': [...this.document.system.features, feature].map(f => f.uuid) @@ -198,9 +197,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); await feature.update({ 'system.subType': null }); await this.document.update({ - 'system.features': this.document.system.features - .map(x => x.uuid) - .filter(uuid => uuid !== feature.uuid) + 'system.features': this.document.system.features.map(x => x.uuid).filter(uuid => uuid !== feature.uuid) }); } diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index 80bbe139..bdc482c3 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -31,7 +31,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/applications/sheets/items/beastform.mjs b/module/applications/sheets/items/beastform.mjs index db769e94..8894b694 100644 --- a/module/applications/sheets/items/beastform.mjs +++ b/module/applications/sheets/items/beastform.mjs @@ -1,4 +1,5 @@ import DHBaseItemSheet from '../api/base-item.mjs'; +import Tagify from '@yaireo/tagify'; export default class BeastformSheet extends DHBaseItemSheet { /**@inheritdoc */ @@ -15,6 +16,7 @@ export default class BeastformSheet extends DHBaseItemSheet { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs', scrollable: ['.features'] }, + advanced: { template: 'systems/daggerheart/templates/sheets/items/beastform/advanced.hbs' }, effects: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] @@ -23,9 +25,77 @@ export default class BeastformSheet extends DHBaseItemSheet { static TABS = { primary: { - tabs: [{ id: 'settings' }, { id: 'features' }, { id: 'effects' }], + tabs: [{ id: 'settings' }, { id: 'features' }, { id: 'advanced' }, { id: 'effects' }], initial: 'settings', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + const advantageOnInput = htmlElement.querySelector('.advantageon-input'); + if (advantageOnInput) { + const tagifyElement = new Tagify(advantageOnInput, { + tagTextProp: 'name', + templates: { + tag(tagData) { + return ` + +
+ ${tagData[this.settings.tagTextProp] || tagData.value} + ${tagData.src ? `` : ''} +
+
`; + } + } + }); + tagifyElement.on('add', this.advantageOnAdd.bind(this)); + tagifyElement.on('remove', this.advantageOnRemove.bind(this)); + } + } + + /**@inheritdoc */ + async _preparePartContext(partId, context, options) { + await super._preparePartContext(partId, context, options); + + switch (partId) { + case 'settings': + context.advantageOn = JSON.stringify( + Object.keys(context.document.system.advantageOn).map(key => ({ + value: key, + name: context.document.system.advantageOn[key].value + })) + ); + break; + case 'effects': + context.effects.actives = context.effects.actives.map(effect => { + const data = effect.toObject(); + data.id = effect.id; + if (effect.type === 'beastform') data.mandatory = true; + + return data; + }); + break; + } + + return context; + } + + async advantageOnAdd(event) { + await this.document.update({ + [`system.advantageOn.${foundry.utils.randomID()}`]: { value: event.detail.data.value } + }); + } + + async advantageOnRemove(event) { + await this.document.update({ + [`system.advantageOn.-=${event.detail.data.value}`]: null + }); + } } diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 11feb1b2..1520844b 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -9,7 +9,7 @@ export default class ClassSheet extends DHBaseItemSheet { position: { width: 700 }, actions: { removeItemFromCollection: ClassSheet.#removeItemFromCollection, - removeSuggestedItem: ClassSheet.#removeSuggestedItem, + removeSuggestedItem: ClassSheet.#removeSuggestedItem }, tagifyConfigs: [ { diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index 3fb725d7..2533287b 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -31,7 +31,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 13bfb5f9..41f9ef20 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -206,21 +206,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (!confirm) return; } } - + if (targets.length === 0) return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); for (let target of targets) { let damages = foundry.utils.deepClone(message.system.damage?.roll ?? message.system.roll); - if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) { + if ( + message.system.onSave && + message.system.targets.find(t => t.id === target.id)?.saved?.success === true + ) { const mod = CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1; - Object.entries(damages).forEach(([k,v]) => { + Object.entries(damages).forEach(([k, v]) => { v.total = 0; v.parts.forEach(part => { part.total = Math.ceil(part.total * mod); v.total += part.total; - }) - }) + }); + }); } target.actor.takeDamage(damages); @@ -233,7 +236,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (targets.length === 0) return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - + for (var target of targets) { target.actor.takeHealing(message.system.roll); } @@ -294,15 +297,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo await actor.useAction(action); }; - actionUseButton = async (_, message) => { + actionUseButton = async (event, message) => { + const { moveIndex, actionIndex } = event.currentTarget.dataset; const parent = await foundry.utils.fromUuid(message.system.actor); - const actionType = Object.values(message.system.moves)[0].actions[0]; - const cls = CONFIG.DH.ACTIONS.actionTypes[actionType.type]; + const actionType = message.system.moves[moveIndex].actions[actionIndex]; + const cls = game.system.api.models.actions.actionsTypes[actionType.type]; const action = new cls( { ...actionType, _id: foundry.utils.randomID(), name: game.i18n.localize(actionType.name) }, - { parent: parent } + { parent: parent.system } ); - action.use(); + action.use(event); }; } diff --git a/module/applications/ui/hotbar.mjs b/module/applications/ui/hotbar.mjs index b4ebc05c..9d102c6c 100644 --- a/module/applications/ui/hotbar.mjs +++ b/module/applications/ui/hotbar.mjs @@ -99,7 +99,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar { async createItemMacro(data, slot) { const macro = await Macro.implementation.create({ - name: `${game.i18n.localize('Display')} ${name}`, + name: data.name, type: CONST.MACRO_TYPES.SCRIPT, img: data.img, command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");` @@ -109,7 +109,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar { async createActionMacro(data, slot) { const macro = await Macro.implementation.create({ - name: `${game.i18n.localize('Display')} ${name}`, + name: data.data.name, type: CONST.MACRO_TYPES.SCRIPT, img: data.data.img, command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");` @@ -119,7 +119,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar { async createAttackMacro(data, slot) { const macro = await Macro.implementation.create({ - name: `${game.i18n.localize('Display')} ${name}`, + name: data.name, type: CONST.MACRO_TYPES.SCRIPT, img: data.img, command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");` diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index d05dc81c..edf3a9f3 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -1,5 +1,6 @@ export const abilities = { agility: { + id: 'agility', label: 'DAGGERHEART.CONFIG.Traits.agility.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.agility.verb.sprint', @@ -8,6 +9,7 @@ export const abilities = { ] }, strength: { + id: 'strength', label: 'DAGGERHEART.CONFIG.Traits.strength.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.strength.verb.lift', @@ -16,6 +18,7 @@ export const abilities = { ] }, finesse: { + id: 'finesse', label: 'DAGGERHEART.CONFIG.Traits.finesse.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.finesse.verb.control', @@ -24,6 +27,7 @@ export const abilities = { ] }, instinct: { + id: 'instinct', label: 'DAGGERHEART.CONFIG.Traits.instinct.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.instinct.verb.perceive', @@ -32,6 +36,7 @@ export const abilities = { ] }, presence: { + id: 'presence', label: 'DAGGERHEART.CONFIG.Traits.presence.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.presence.verb.charm', @@ -40,6 +45,7 @@ export const abilities = { ] }, knowledge: { + id: 'knowledge', label: 'DAGGERHEART.CONFIG.Traits.knowledge.name', verbs: [ 'DAGGERHEART.CONFIG.Traits.knowledge.verb.recall', diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index efe8a016..390435b4 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -145,11 +145,11 @@ export const defaultRestOptions = { img: 'icons/magic/life/cross-worn-green.webp', actionType: 'action', healing: { - type: 'health', + applyTo: healingTypes.hitPoints.id, value: { custom: { enabled: true, - formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? + formula: '1d4 + @tier' } } } @@ -169,11 +169,11 @@ export const defaultRestOptions = { img: 'icons/magic/perception/eye-ringed-green.webp', actionType: 'action', healing: { - type: 'stress', + applyTo: healingTypes.stress.id, value: { custom: { enabled: true, - formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? + formula: '1d4 + @tier' } } } @@ -186,7 +186,23 @@ export const defaultRestOptions = { icon: 'fa-solid fa-hammer', img: 'icons/skills/trades/smithing-anvil-silver-red.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'), - actions: [] + actions: [ + { + type: 'healing', + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'), + img: 'icons/skills/trades/smithing-anvil-silver-red.webp', + actionType: 'action', + healing: { + applyTo: healingTypes.armorStack.id, + value: { + custom: { + enabled: true, + formula: '1d4 + @tier' + } + } + } + } + ] }, prepare: { id: 'prepare', @@ -263,25 +279,21 @@ export const deathMoves = { }; export const tiers = { - tier1: { - id: 'tier1', - label: 'DAGGERHEART.GENERAL.Tiers.tier1', - value: 1 + 1: { + id: 1, + label: 'DAGGERHEART.GENERAL.Tiers.1' }, - tier2: { - id: 'tier2', - label: 'DAGGERHEART.GENERAL.Tiers.tier2', - value: 2 + 2: { + id: 2, + label: 'DAGGERHEART.GENERAL.Tiers.2' }, - tier3: { - id: 'tier3', - label: 'DAGGERHEART.GENERAL.Tiers.tier3', - value: 3 + 3: { + id: 3, + label: 'DAGGERHEART.GENERAL.Tiers.3' }, - tier4: { - id: 'tier4', - label: 'DAGGERHEART.GENERAL.Tiers.tier4', - value: 4 + 4: { + id: 4, + label: 'DAGGERHEART.GENERAL.Tiers.4' } }; diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 6b28a1ae..9deed7f1 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1346,3 +1346,18 @@ export const itemResourceTypes = { label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue' } }; + +export const beastformTypes = { + normal: { + id: 'normal', + label: 'DAGGERHEART.CONFIG.BeastformType.normal' + }, + evolved: { + id: 'evolved', + label: 'DAGGERHEART.CONFIG.BeastformType.evolved' + }, + hybrid: { + id: 'hybrid', + label: 'DAGGERHEART.CONFIG.BeastformType.hybrid' + } +}; diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index e72dc54a..1d2b204b 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -113,7 +113,7 @@ export class DHResourceData extends foundry.abstract.DataModel { }), value: new fields.EmbeddedDataField(DHActionDiceData), valueAlt: new fields.EmbeddedDataField(DHActionDiceData) - } + }; } } @@ -134,6 +134,6 @@ export class DHDamageData extends DHResourceData { label: 'Type' } ) - } + }; } } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index f0176d78..ad442951 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -163,7 +163,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } getRollData(data = {}) { - if(!this.actor) return null; + if (!this.actor) return null; const actorData = this.actor.getRollData(false); // Add Roll results to RollDatas @@ -178,8 +178,8 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } async use(event, ...args) { - if(!this.actor) throw new Error("An Action can't be used outside of an Actor context."); - + if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); + const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave); // Prepare base Config const initConfig = this.initActionConfig(event); diff --git a/module/data/action/beastformAction.mjs b/module/data/action/beastformAction.mjs index 98d8053f..2116662c 100644 --- a/module/data/action/beastformAction.mjs +++ b/module/data/action/beastformAction.mjs @@ -10,10 +10,10 @@ export default class DhBeastformAction extends DHBaseAction { const abort = await this.handleActiveTransformations(); if (abort) return; - const beastformUuid = await BeastformDialog.configure(beastformConfig); - if (!beastformUuid) return; + const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig); + if (!selected) return; - await this.transform(beastformUuid); + await this.transform(selected, evolved, hybrid); } prepareBeastformConfig(config) { @@ -29,21 +29,48 @@ export default class DhBeastformAction extends DHBaseAction { }; } - async transform(beastformUuid) { - const beastform = await foundry.utils.fromUuid(beastformUuid); - this.actor.createEmbeddedDocuments('Item', [beastform.toObject()]); + async transform(selectedForm, evolvedData, hybridData) { + const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject(); + const beastformEffect = formData.effects.find(x => x.type === 'beastform'); + if (!beastformEffect) { + ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect'); + return; + } + + if (evolvedData?.form) { + const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform'); + if (!evolvedForm) { + ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect'); + return; + } + + beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes]; + formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)]; + } + + if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) { + formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => { + Object.keys(formCategory).forEach(advantageKey => { + advantages[advantageKey] = formCategory[advantageKey]; + }); + return advantages; + }, {}); + formData.system.features = [ + ...formData.system.features, + ...Object.values(hybridData.features).flatMap(x => Object.keys(x)) + ]; + } + + this.actor.createEmbeddedDocuments('Item', [formData]); } async handleActiveTransformations() { const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform'); - if (beastformEffects.length > 0) { - for (let effect of beastformEffects) { - await effect.delete(); - } - - return true; - } - - return false; + const existingEffects = beastformEffects.length > 0; + await this.actor.deleteEmbeddedDocuments( + 'ActiveEffect', + beastformEffects.map(x => x.id) + ); + return existingEffects; } } diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 13461d30..988e1844 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -22,31 +22,32 @@ export default class DHDamageAction extends DHBaseAction { formatFormulas(formulas, systemData) { const formattedFormulas = []; formulas.forEach(formula => { - if (isNaN(formula.formula)) formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData)); - const same = formattedFormulas.find(f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo); - if(same) - same.formula += ` + ${formula.formula}`; - else - formattedFormulas.push(formula); - }) + if (isNaN(formula.formula)) + formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData)); + const same = formattedFormulas.find( + f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo + ); + if (same) same.formula += ` + ${formula.formula}`; + else formattedFormulas.push(formula); + }); return formattedFormulas; } async rollDamage(event, data) { const systemData = data.system ?? data; - + let formulas = this.damage.parts.map(p => ({ formula: this.getFormulaValue(p, data).getFormula(this.actor), damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type, applyTo: p.applyTo })); - if(!formulas.length) return; + if (!formulas.length) return; formulas = this.formatFormulas(formulas, systemData); const config = { - title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: this.name }), + title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: game.i18n.localize(this.name) }), roll: formulas, targets: systemData.targets.filter(t => t.hit) ?? data.targets, hasSave: this.hasSave, diff --git a/module/data/action/healingAction.mjs b/module/data/action/healingAction.mjs index e0a74f27..b7cc4a75 100644 --- a/module/data/action/healingAction.mjs +++ b/module/data/action/healingAction.mjs @@ -16,11 +16,13 @@ export default class DHHealingAction extends DHBaseAction { async rollHealing(event, data) { const systemData = data.system ?? data; - let formulas = [{ - formula: this.getFormulaValue(data).getFormula(this.actor), - applyTo: this.healing.applyTo - }]; - + let formulas = [ + { + formula: this.getFormulaValue(data).getFormula(this.actor), + applyTo: this.healing.applyTo + } + ]; + const config = { title: game.i18n.format('DAGGERHEART.UI.Chat.healingRoll.title', { healing: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[this.healing.applyTo].label) diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 6445f65d..4e6fa104 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -26,6 +26,13 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel { }; } + async _onCreate() { + if (this.parent.parent?.type === 'character') { + this.parent.parent.system.primaryWeapon?.update?.({ 'system.equipped': false }); + this.parent.parent.system.secondayWeapon?.update?.({ 'system.equipped': false }); + } + } + async _preDelete() { if (this.parent.parent.type === 'character') { const update = { diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 00a19d05..7360e42f 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -18,10 +18,11 @@ export default class DhpAdversary extends BaseDataActor { const fields = foundry.data.fields; return { ...super.defineSchema(), - tier: new fields.StringField({ + tier: new fields.NumberField({ required: true, + integer: true, choices: CONFIG.DH.GENERAL.tiers, - initial: CONFIG.DH.GENERAL.tiers.tier1.id + initial: CONFIG.DH.GENERAL.tiers[1].id }), type: new fields.StringField({ required: true, @@ -52,7 +53,7 @@ export default class DhpAdversary extends BaseDataActor { }) }), resources: new fields.SchemaField({ - hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true), + hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), stress: resourceField(0, 'DAGGERHEART.GENERAL.stress', true) }), attack: new ActionField({ diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index c484465a..34a1f525 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -3,6 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import DhLevelData from '../levelData.mjs'; import BaseDataActor from './base.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; +import ActionField from '../fields/actionField.mjs'; export default class DhCharacter extends BaseDataActor { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; @@ -21,7 +22,7 @@ export default class DhCharacter extends BaseDataActor { return { ...super.defineSchema(), resources: new fields.SchemaField({ - hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.hitPoints.plural', true), + hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true), hope: resourceField(6, 'DAGGERHEART.GENERAL.hope') }), @@ -87,8 +88,45 @@ export default class DhCharacter extends BaseDataActor { value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) }), - advantageSources: new fields.ArrayField(new fields.StringField()), - disadvantageSources: new fields.ArrayField(new fields.StringField()), + attack: new ActionField({ + initial: { + name: 'Attack', + img: 'icons/skills/melee/unarmed-punch-fist-yellow-red.webp', + _id: foundry.utils.randomID(), + systemPath: 'attack', + type: 'attack', + range: 'melee', + target: { + type: 'any', + amount: 1 + }, + roll: { + type: 'attack', + trait: 'strength' + }, + damage: { + parts: [ + { + type: ['physical'], + value: { + custom: { + enabled: true, + formula: '@system.rules.attack.damage.value' + } + } + } + ] + } + } + }), + advantageSources: new fields.ArrayField(new fields.StringField(), { + label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', + hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' + }), + disadvantageSources: new fields.ArrayField(new fields.StringField(), { + label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label', + hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint' + }), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ @@ -198,6 +236,15 @@ export default class DhCharacter extends BaseDataActor { magical: new fields.BooleanField({ initial: false }), physical: new fields.BooleanField({ initial: false }) }), + attack: new fields.SchemaField({ + damage: new fields.SchemaField({ + value: new fields.StringField({ + required: true, + initial: '@profd4', + label: 'DAGGERHEART.GENERAL.Rules.attack.damage.value.label' + }) + }) + }), weapon: new fields.SchemaField({ /* Unimplemented -> Should remove the lowest damage dice from weapon damage @@ -277,6 +324,24 @@ export default class DhCharacter extends BaseDataActor { return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); } + get activeBeastform() { + return this.parent.effects.find(x => x.type === 'beastform'); + } + + get usedUnarmed() { + const primaryWeaponEquipped = this.primaryWeapon?.system?.equipped; + const secondaryWeaponEquipped = this.secondaryWeapon?.system?.equipped; + return !primaryWeaponEquipped && !secondaryWeaponEquipped + ? { + ...this.attack, + id: this.attack.id, + name: this.activeBeastform ? 'DAGGERHEART.ITEMS.Beastform.attackName' : this.attack.name, + img: this.activeBeastform ? 'icons/creatures/claws/claw-straight-brown.webp' : this.attack.img, + actor: this.parent + } + : null; + } + get sheetLists() { const ancestryFeatures = [], communityFeatures = [], @@ -457,9 +522,6 @@ export default class DhCharacter extends BaseDataActor { const data = super.getRollData(); return { ...data, - ...this.resources.tokens, - ...this.resources.dice, - ...this.bonuses, tier: this.tier, level: this.levelData.level.current }; diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index 2db1f039..e9a484b3 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -18,10 +18,11 @@ export default class DhEnvironment extends BaseDataActor { const fields = foundry.data.fields; return { ...super.defineSchema(), - tier: new fields.StringField({ + tier: new fields.NumberField({ required: true, + integer: true, choices: CONFIG.DH.GENERAL.tiers, - initial: CONFIG.DH.GENERAL.tiers.tier1.id + initial: CONFIG.DH.GENERAL.tiers[1].id }), type: new fields.StringField({ choices: CONFIG.DH.ACTOR.environmentTypes }), impulses: new fields.StringField(), diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index d19521d0..226504df 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -19,10 +19,16 @@ export default class DHBeastform extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - tier: new fields.StringField({ + beastformType: new fields.StringField({ required: true, + choices: CONFIG.DH.ITEM.beastformTypes, + initial: CONFIG.DH.ITEM.beastformTypes.normal.id + }), + tier: new fields.NumberField({ + required: true, + integer: true, choices: CONFIG.DH.GENERAL.tiers, - initial: CONFIG.DH.GENERAL.tiers.tier1.id + initial: CONFIG.DH.GENERAL.tiers[1].id }), tokenImg: new fields.FilePathField({ initial: 'icons/svg/mystery-man.svg', @@ -38,9 +44,40 @@ export default class DHBeastform extends BaseDataItem { height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), + mainTrait: new fields.StringField({ + required: true, + choices: CONFIG.DH.ACTOR.abilities, + initial: CONFIG.DH.ACTOR.abilities.agility.id + }), examples: new fields.StringField(), - advantageOn: new fields.StringField(), - features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) + advantageOn: new fields.TypedObjectField( + new fields.SchemaField({ + value: new fields.StringField() + }) + ), + features: new ForeignDocumentUUIDArrayField({ type: 'Item' }), + evolved: new fields.SchemaField({ + maximumTier: new fields.NumberField({ + integer: true, + choices: CONFIG.DH.GENERAL.tiers + }), + mainTraitBonus: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 0 + }) + }), + hybrid: new fields.SchemaField({ + maximumTier: new fields.NumberField({ + integer: true, + choices: CONFIG.DH.GENERAL.tiers, + label: 'DAGGERHEART.ITEMS.Beastform.FIELDS.evolved.maximumTier.label' + }), + beastformOptions: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }), + advantages: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }), + features: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }) + }) }; } @@ -69,7 +106,16 @@ export default class DHBeastform extends BaseDataItem { const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); await beastformEffect.updateSource({ - changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }], + changes: [ + ...beastformEffect.changes, + { + key: 'system.advantageSources', + mode: 2, + value: Object.values(this.advantageOn) + .map(x => x.value) + .join(', ') + } + ], system: { characterTokenData: { tokenImg: this.parent.parent.prototypeToken.texture.src, diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index b8a9ab81..4016a4c7 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -24,7 +24,7 @@ export default class DHClass extends BaseDataItem { integer: true, min: 1, initial: 5, - label: 'DAGGERHEART.GENERAL.hitPoints.plural' + label: 'DAGGERHEART.GENERAL.HitPoints.plural' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }), diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 09dd716e..95cba5ca 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -92,7 +92,7 @@ export default class D20Roll extends DHRoll { configureModifiers() { this.applyAdvantage(); - + this.baseTerms = foundry.utils.deepClone(this.dice); this.options.roll.modifiers = this.applyBaseBonus(); @@ -147,8 +147,7 @@ export default class D20Roll extends DHRoll { const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; target.hit = this.isCritical || roll.total >= difficulty; }); - } else if (config.roll.difficulty) - data.success = roll.isCritical || roll.total >= config.roll.difficulty; + } else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty; data.advantage = { type: config.roll.advantage, dice: roll.dAdvantage?.denomination, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index aa1c4c86..34fe77d7 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -11,8 +11,8 @@ export default class DamageRoll extends DHRoll { static DefaultDialog = DamageDialog; static async buildEvaluate(roll, config = {}, message = {}) { - if ( config.evaluate !== false ) { - for ( const roll of config.roll ) await roll.roll.evaluate(); + if (config.evaluate !== false) { + for (const roll of config.roll) await roll.roll.evaluate(); } roll._evaluated = true; const parts = config.roll.map(r => this.postEvaluate(r)); @@ -27,7 +27,7 @@ export default class DamageRoll extends DHRoll { roll: roll.roll, type: config.type, modifierTotal: this.calculateTotalModifiers(roll.roll) - } + }; } static async buildPost(roll, config, message) { @@ -46,33 +46,33 @@ export default class DamageRoll extends DHRoll { resource.total += r.total; resource.parts.push(r); unified[r.applyTo] = resource; - }) + }); return unified; } static formatGlobal(rolls) { let formula, total; const applyTo = new Set(rolls.flatMap(r => r.applyTo)); - if(applyTo.size > 1) { + if (applyTo.size > 1) { const data = {}; rolls.forEach(r => { - if(data[r.applyTo]) { - data[r.applyTo].formula += ` + ${r.formula}` ; - data[r.applyTo].total += r.total ; + if (data[r.applyTo]) { + data[r.applyTo].formula += ` + ${r.formula}`; + data[r.applyTo].total += r.total; } else { data[r.applyTo] = { formula: r.formula, total: r.total - } + }; } }); - formula = Object.entries(data).reduce((a, [k,v]) => a + ` ${k}: ${v.formula}`, ''); - total = Object.entries(data).reduce((a, [k,v]) => a + ` ${k}: ${v.total}`, ''); + formula = Object.entries(data).reduce((a, [k, v]) => a + ` ${k}: ${v.formula}`, ''); + total = Object.entries(data).reduce((a, [k, v]) => a + ` ${k}: ${v.total}`, ''); } else { formula = rolls.map(r => r.formula).join(' + '); - total = rolls.reduce((a,c) => a + c.total, 0) + total = rolls.reduce((a, c) => a + c.total, 0); } - return {formula, total} + return { formula, total }; } applyBaseBonus(part) { @@ -94,17 +94,17 @@ export default class DamageRoll extends DHRoll { } constructFormula(config) { - this.options.roll.forEach( part => { - part.roll = new Roll(part.formula); - this.constructFormulaPart(config, part) - }) + this.options.roll.forEach(part => { + part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); + this.constructFormulaPart(config, part); + }); return this.options.roll; } constructFormulaPart(config, part) { part.roll.terms = Roll.parse(part.roll.formula, config.data); - if(part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { part.modifiers = this.applyBaseBonus(part); this.addModifiers(part); part.modifiers?.forEach(m => { @@ -118,7 +118,7 @@ export default class DamageRoll extends DHRoll { ...this.constructor.parse(part.extraFormula, this.options.data) ); } - + if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { const tmpRoll = Roll.fromTerms(part.roll.terms)._evaluateSync({ maximize: true }), criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll); diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index cb1007e3..c50c126a 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -57,13 +57,14 @@ export default class DHRoll extends Roll { // Create Chat Message if (config.source?.message) { - if(Object.values(config.roll)?.length) { - const pool = foundry.dice.terms.PoolTerm.fromRolls(Object.values(config.roll).flatMap(r => r.parts.map(p => p.roll))); + if (Object.values(config.roll)?.length) { + const pool = foundry.dice.terms.PoolTerm.fromRolls( + Object.values(config.roll).flatMap(r => r.parts.map(p => p.roll)) + ); roll = Roll.fromTerms([pool]); } if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); - } else - config.message = await this.toMessage(roll, config); + } else config.message = await this.toMessage(roll, config); } static postEvaluate(roll, config = {}) { @@ -76,7 +77,7 @@ export default class DHRoll extends Roll { formula: d.formula, results: d.results })) - } + }; } static async toMessage(roll, config) { diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index e36d2427..142f21e8 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -63,24 +63,22 @@ export default class DualityRoll extends D20Roll { } setRallyChoices() { - return this.data?.parent?.effects.reduce((a,c) => { - const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); - if(change) a.push({ value: c.id, label: change.value }); - return a; - }, []); + return this.data?.parent?.effects.reduce((a, c) => { + const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); + if (change) a.push({ value: c.id, label: change.value }); + return a; + }, []); } get dRally() { - if(!this.rallyFaces) return null; - if(this.hasDisadvantage || this.hasAdvantage) - return this.dice[3]; - else - return this.dice[2]; + if (!this.rallyFaces) return null; + if (this.hasDisadvantage || this.hasAdvantage) return this.dice[3]; + else return this.dice[2]; } get rallyFaces() { const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label; - return rallyChoice ? this.getFaces(rallyChoice) : null; + return rallyChoice ? this.getFaces(rallyChoice) : null; } get isCritical() { @@ -129,13 +127,13 @@ export default class DualityRoll extends D20Roll { if (this.hasAdvantage || this.hasDisadvantage) { const dieFaces = this.advantageFaces, advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber }); - if(this.advantageNumber > 1) advDie.modifiers = ['kh']; + if (this.advantageNumber > 1) advDie.modifiers = ['kh']; this.terms.push( new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), advDie ); } - if(this.rallyFaces) + if (this.rallyFaces) this.terms.push( new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), new foundry.dice.terms.Die({ faces: this.rallyFaces }) @@ -162,7 +160,7 @@ export default class DualityRoll extends D20Roll { static postEvaluate(roll, config = {}) { const data = super.postEvaluate(roll, config); - + data.hope = { dice: roll.dHope.denomination, value: roll.dHope.total @@ -181,7 +179,7 @@ export default class DualityRoll extends D20Roll { label: roll.totalLabel }; - if(roll._rallyIndex && roll.data?.parent) + if (roll._rallyIndex && roll.data?.parent) roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]); setDiceSoNiceForDualityRoll(roll, data.advantage.type); diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e6ae781a..490f53eb 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -370,6 +370,7 @@ export default class DhpActor extends Actor { getRollData() { const rollData = super.getRollData(); + rollData.system = this.system.getRollData(); rollData.prof = this.system.proficiency ?? 1; rollData.cast = this.system.spellcastModifier ?? 1; return rollData; @@ -403,24 +404,28 @@ export default class DhpActor extends Actor { Object.entries(damages).forEach(([key, damage]) => { damage.parts.forEach(part => { - if(part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) + if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) part.total = this.calculateDamage(part.total, part.damageTypes); const update = updates.find(u => u.key === key); - if(update) { + if (update) { update.value += part.total; update.damageTypes.add(...new Set(part.damageTypes)); - } else updates.push({ value: part.total, key, damageTypes: new Set(part.damageTypes) }) - }) + } else updates.push({ value: part.total, key, damageTypes: new Set(part.damageTypes) }); + }); }); if (Hooks.call(`${CONFIG.DH.id}.postCalculateDamage`, this, damages) === false) return null; - if(!updates.length) return; + if (!updates.length) return; const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); - if(hpDamage) { + if (hpDamage) { hpDamage.value = this.convertDamageToThreshold(hpDamage.value); - if (this.type === 'character' && this.system.armor && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { + if ( + this.type === 'character' && + this.system.armor && + this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes) + ) { const armorStackResult = await this.owner.query('armorStack', { actorId: this.uuid, damage: hpDamage.value, @@ -436,18 +441,19 @@ export default class DhpActor extends Actor { } } } - - updates.forEach( u => - u.value = u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value + + updates.forEach( + u => + (u.value = + u.key === 'fear' || this.system?.resources?.[u.key]?.isReversed === false ? u.value * -1 : u.value) ); - + await this.modifyResource(updates); if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, damages) === false) return null; } calculateDamage(baseDamage, type) { - if (this.canResist(type, 'immunity')) return 0; if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); @@ -472,12 +478,12 @@ export default class DhpActor extends Actor { } async takeHealing(resources) { - const updates = Object.entries(resources).map(([key, value]) => ( - { - key: key, - value: !(key === 'fear' || this.system?.resources?.[key]?.isReversed === false) ? value.total * -1 : value.total - } - )) + const updates = Object.entries(resources).map(([key, value]) => ({ + key: key, + value: !(key === 'fear' || this.system?.resources?.[key]?.isReversed === false) + ? value.total * -1 + : value.total + })); await this.modifyResource(updates); } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index f48af563..a8105eb2 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -16,7 +16,21 @@ export default class DHToken extends TokenDocument { }); bars.sort((a, b) => a.label.compare(b.label)); - const invalidAttributes = ['gold', 'levelData', 'actions']; + const invalidAttributes = [ + 'gold', + 'levelData', + 'actions', + 'biography', + 'class', + 'multiclass', + 'companion', + 'notes', + 'partner', + 'description', + 'impulses', + 'tier', + 'type' + ]; const values = attributes.value.reduce((acc, v) => { const a = v.join('.'); if (invalidAttributes.some(x => a.startsWith(x))) return acc; @@ -38,9 +52,11 @@ export default class DHToken extends TokenDocument { for (const [name, field] of Object.entries(schema.fields)) { const p = _path.concat([name]); if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p); + if (field instanceof foundry.data.fields.StringField) attributes.value.push(p); if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p); const isSchema = field instanceof foundry.data.fields.SchemaField; const isModel = field instanceof foundry.data.fields.EmbeddedDataField; + if (isSchema || isModel) { const schema = isModel ? field.model.schema : field; const isBar = schema.has && schema.has('value') && schema.has('max'); diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 005f77bd..8e1c729e 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -1,5 +1,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { async activate(element, options = {}) { + const { TextEditor } = foundry.applications.ux; + let html = options.html; if (element.dataset.tooltip?.startsWith('#item#')) { const splitValues = element.dataset.tooltip.slice(6).split('#action#'); @@ -10,10 +12,16 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti const item = actionId ? baseItem.system.actions.find(x => x.id === actionId) : baseItem; if (item) { const type = actionId ? 'action' : item.type; + const description = await TextEditor.enrichHTML(item.system.description); + for (let feature of item.system.features) { + feature.system.enrichedDescription = await TextEditor.enrichHTML(feature.system.description); + } + html = await foundry.applications.handlebars.renderTemplate( `systems/daggerheart/templates/ui/tooltip/${type}.hbs`, { item: item, + description: description, config: CONFIG.DH } ); @@ -22,6 +30,26 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti options.direction = this._determineItemTooltipDirection(element); } } else { + const attack = element.dataset.tooltip?.startsWith('#attack#'); + if (attack) { + const actorUuid = element.dataset.tooltip.slice(8); + const actor = await foundry.utils.fromUuid(actorUuid); + const attack = actor.system.attack; + + const description = await TextEditor.enrichHTML(attack.description); + html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/attack.hbs`, + { + attack: attack, + description: description, + parent: actor, + config: CONFIG.DH + } + ); + + this.tooltip.innerHTML = html; + } + const shortRest = element.dataset.tooltip?.startsWith('#shortRest#'); const longRest = element.dataset.tooltip?.startsWith('#longRest#'); if (shortRest || longRest) { @@ -29,11 +57,14 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti const downtimeOptions = shortRest ? CONFIG.DH.GENERAL.defaultRestOptions.shortRest() : CONFIG.DH.GENERAL.defaultRestOptions.longRest(); + const move = downtimeOptions[key]; + const description = await TextEditor.enrichHTML(move.description); html = await foundry.applications.handlebars.renderTemplate( `systems/daggerheart/templates/ui/tooltip/downtime.hbs`, { - move: move + move: move, + description: description } ); diff --git a/module/enrichers/DamageEnricher.mjs b/module/enrichers/DamageEnricher.mjs new file mode 100644 index 00000000..918edc39 --- /dev/null +++ b/module/enrichers/DamageEnricher.mjs @@ -0,0 +1,80 @@ +export default function DhDamageEnricher(match, _options) { + const parts = match[1].split('|').map(x => x.trim()); + + let value = null, + type = null; + + parts.forEach(part => { + const split = part.split(':').map(x => x.toLowerCase().trim()); + if (split.length === 2) { + switch (split[0]) { + case 'value': + value = split[1]; + break; + case 'type': + type = split[1]; + break; + } + } + }); + + if (!value || !value) return match[0]; + + return getDamageMessage(value, type, match[0]); +} + +function getDamageMessage(damage, type, defaultElement) { + const typeIcons = type + .replace('[', '') + .replace(']', '') + .split(',') + .map(x => x.trim()) + .map(x => { + return CONFIG.DH.GENERAL.damageTypes[x]?.icon ?? null; + }) + .filter(x => x); + + if (!typeIcons.length) return defaultElement; + + const iconNodes = typeIcons.map(x => ``).join(''); + + const dualityElement = document.createElement('span'); + dualityElement.innerHTML = ` + + `; + + return dualityElement; +} + +export const renderDamageButton = async event => { + const button = event.currentTarget, + value = button.dataset.value, + type = button.dataset.type + .replace('[', '') + .replace(']', '') + .split(',') + .map(x => x.trim()); + + const config = { + event: event, + title: game.i18n.localize('Damage Roll'), + data: { bonuses: [] }, + source: {}, + roll: [ + { + formula: value, + applyTo: CONFIG.DH.GENERAL.healingTypes.hitPoints.id, + type: type + } + ] + }; + + CONFIG.Dice.daggerheart.DamageRoll.build(config); +}; diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 094a6948..a2da2805 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -8,7 +8,7 @@ export default function DhDualityRollEnricher(match, _options) { return getDualityMessage(roll); } -export function getDualityMessage(roll) { +function getDualityMessage(roll) { const traitLabel = roll.trait && abilities[roll.trait] ? game.i18n.format('DAGGERHEART.GENERAL.check', { diff --git a/module/enrichers/EffectEnricher.mjs b/module/enrichers/EffectEnricher.mjs new file mode 100644 index 00000000..69b89ef2 --- /dev/null +++ b/module/enrichers/EffectEnricher.mjs @@ -0,0 +1,19 @@ +export default async function DhEffectEnricher(match, _options) { + const effect = await foundry.utils.fromUuid(match[1]); + if (!effect) return match[0]; + + const dualityElement = document.createElement('span'); + dualityElement.innerHTML = ` + + + ${effect.name} + + `; + + return dualityElement; +} diff --git a/module/enrichers/_module.mjs b/module/enrichers/_module.mjs index 1907f117..abfd8158 100644 --- a/module/enrichers/_module.mjs +++ b/module/enrichers/_module.mjs @@ -1,2 +1,43 @@ -export { default as DhDualityRollEnricher } from './DualityRollEnricher.mjs'; -export { default as DhTemplateEnricher } from './TemplateEnricher.mjs'; +import { default as DhDamageEnricher, renderDamageButton } from './DamageEnricher.mjs'; +import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; +import { default as DhEffectEnricher } from './EffectEnricher.mjs'; +import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; + +export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; + +export const enricherConfig = [ + { + pattern: /^@Damage\[(.*)\]$/g, + enricher: DhDamageEnricher + }, + { + pattern: /\[\[\/dr\s?(.*?)\]\]/g, + enricher: DhDualityRollEnricher + }, + { + pattern: /^@Effect\[(.*)\]$/g, + enricher: DhEffectEnricher + }, + { + pattern: /^@Template\[(.*)\]$/g, + enricher: DhTemplateEnricher + } +]; + +export const enricherRenderSetup = element => { + element + .querySelectorAll('.enriched-damage-button') + .forEach(element => element.addEventListener('click', renderDamageButton)); + + element + .querySelectorAll('.duality-roll-button') + .forEach(element => element.addEventListener('click', renderDualityButton)); + + element + .querySelectorAll('.measured-template-button') + .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); + + // element + // .querySelectorAll('.enriched-effect') + // .forEach(element => element.addEventListener('dragstart', dragEnrichedEffect)); +}; diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 0a57a6a6..0c919191 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -9,7 +9,7 @@ export default class RegisterHandlebarsHelpers { damageFormula: this.damageFormula, damageSymbols: this.damageSymbols, rollParsed: this.rollParsed, - hasProperty: foundry.utils.hasProperty, + hasProperty: foundry.utils.hasProperty }); } static add(a, b) { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 2aacd5c7..7e73695e 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,80 +1,10 @@ import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs'; import Tagify from '@yaireo/tagify'; -export const loadCompendiumOptions = async compendiums => { - const compendiumValues = []; - - for (var compendium of compendiums) { - const values = await getCompendiumOptions(compendium); - compendiumValues.push(values); - } - - return compendiumValues; -}; - -const getCompendiumOptions = async compendium => { - const compendiumPack = await game.packs.get(compendium); - - const values = []; - for (var value of compendiumPack.index) { - const document = await compendiumPack.getDocument(value._id); - values.push(document); - } - - return values; -}; - -export const getWidthOfText = (txt, fontsize, allCaps, bold) => { - const text = allCaps ? txt.toUpperCase() : txt; - if (getWidthOfText.c === undefined) { - getWidthOfText.c = document.createElement('canvas'); - getWidthOfText.ctx = getWidthOfText.c.getContext('2d'); - } - var fontspec = `${bold ? 'bold' : ''} ${fontsize}px` + ' ' + 'Signika, sans-serif'; - if (getWidthOfText.ctx.font !== fontspec) getWidthOfText.ctx.font = fontspec; - - return getWidthOfText.ctx.measureText(text).width; -}; - -export const padArray = (arr, len, fill) => { - return arr.concat(Array(len).fill(fill)).slice(0, len); -}; - -export const getTier = (level, asNr) => { - switch (Math.floor((level + 1) / 3)) { - case 1: - return asNr ? 1 : 'tier1'; - case 2: - return asNr ? 2 : 'tier2'; - case 3: - return asNr ? 3 : 'tier3'; - default: - return asNr ? 0 : 'tier0'; - } -}; - export const capitalize = string => { return string.charAt(0).toUpperCase() + string.slice(1); }; -export const getPathValue = (path, entity, numeric) => { - const pathValue = foundry.utils.getProperty(entity, path); - if (pathValue) return numeric ? Number.parseInt(pathValue) : pathValue; - - return numeric ? Number.parseInt(path) : path; -}; - -export const generateId = (title, length) => { - const id = title - .split(' ') - .map((w, i) => { - const p = w.slugify({ replacement: '', strict: true }); - return i ? p.titleCase() : p; - }) - .join(''); - return Number.isNumeric(length) ? id.slice(0, length).padEnd(length, '0') : id; -}; - export function rollCommandToJSON(text) { if (!text) return {}; @@ -311,7 +241,6 @@ export function getDocFromElement(element) { return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; } - export const itemAbleRollParse = (value, actor, item) => { if (!value) return value; @@ -325,13 +254,7 @@ export const itemAbleRollParse = (value, actor, item) => { }; export const arraysEqual = (a, b) => - a.length === b.length && - [...new Set([...a, ...b])].every( - v => a.filter(e => e === v).length === b.filter(e => e === v).length - ); + a.length === b.length && + [...new Set([...a, ...b])].every(v => a.filter(e => e === v).length === b.filter(e => e === v).length); -export const setsEqual = (a, b) => - a.size === b.size && - [...a].every( - value => b.has(value) - ); \ No newline at end of file +export const setsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value)); diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index ec5f2df1..b15bf820 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -1,8 +1,9 @@ export const preloadHandlebarsTemplates = async function () { foundry.applications.handlebars.loadTemplates({ - 'daggerheart.inventory-items': 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs', - 'daggerheart.inventory-item': 'systems/daggerheart/templates/sheets/global/partials/inventory-item-V2.hbs', - }) + 'daggerheart.inventory-items': + 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs', + 'daggerheart.inventory-item': 'systems/daggerheart/templates/sheets/global/partials/inventory-item-V2.hbs' + }); return foundry.applications.handlebars.loadTemplates([ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs', 'systems/daggerheart/templates/sheets/global/partials/action-item.hbs', @@ -27,6 +28,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-chat.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-chat.hbs', + 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs' ]); diff --git a/src/packs/beastforms/beastform_Agile_Scout_6tr99y6wHaJJYy3J.json b/src/packs/beastforms/beastform_Agile_Scout_6tr99y6wHaJJYy3J.json new file mode 100644 index 00000000..0885d9f3 --- /dev/null +++ b/src/packs/beastforms/beastform_Agile_Scout_6tr99y6wHaJJYy3J.json @@ -0,0 +1,160 @@ +{ + "name": "Agile Scout", + "type": "beastform", + "img": "icons/creatures/mammals/goat-horned-blue.webp", + "system": { + "beastformType": "normal", + "tier": 1, + "tokenImg": "icons/creatures/mammals/goat-horned-blue.webp", + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": { + "height": null, + "width": null + }, + "mainTrait": "agility", + "advantageOn": { + "tXlf8FvWrgGqfaJu": { + "value": "deceit" + }, + "lp1gv9iNUCpC2Fli": { + "value": "locate" + }, + "GxDMKUpOeDHzyhAT": { + "value": "sneak" + } + }, + "features": [ + "Compendium.daggerheart.beastforms.Item.sef9mwD2eRLZ64oV", + "Compendium.daggerheart.beastforms.Item.9ryNrYWjNtOT6DXN" + ], + "evolved": { + "mainTraitBonus": 0 + }, + "hybrid": { + "beastformOptions": 2, + "advantages": 2, + "features": 2 + }, + "examples": "Fox, Mouse, Weasel, etc." + }, + "effects": [ + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "m098fyKkAjTFZ6UJ", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [ + { + "key": "system.traits.agility.value", + "mode": 2, + "value": "1", + "priority": null + }, + { + "key": "system.evasion", + "mode": 2, + "value": "2", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "lastModifiedBy": null + }, + "_key": "!items.effects!6tr99y6wHaJJYy3J.m098fyKkAjTFZ6UJ" + }, + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "5mi9ku2R4paP579i", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [], + "disabled": false, + "duration": { + "startTime": null, + "combat": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976985351, + "modifiedTime": 1752976985351, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!items.effects!6tr99y6wHaJJYy3J.5mi9ku2R4paP579i" + } + ], + "folder": null, + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976985346, + "modifiedTime": 1752976987362, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "6tr99y6wHaJJYy3J", + "sort": 100000, + "_key": "!items!6tr99y6wHaJJYy3J" +} diff --git a/src/packs/beastforms/beastform_Household_Friend_uxBugKULjn7O1KQc.json b/src/packs/beastforms/beastform_Household_Friend_uxBugKULjn7O1KQc.json new file mode 100644 index 00000000..288afead --- /dev/null +++ b/src/packs/beastforms/beastform_Household_Friend_uxBugKULjn7O1KQc.json @@ -0,0 +1,166 @@ +{ + "name": "Household Friend", + "type": "beastform", + "img": "icons/creatures/mammals/cat-hunched-glowing-red.webp", + "system": { + "beastformType": "normal", + "tier": 1, + "tokenImg": "icons/creatures/mammals/cat-hunched-glowing-red.webp", + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": { + "height": null, + "width": null + }, + "mainTrait": "instinct", + "advantageOn": { + "u0mzlWihDHITgh1x": { + "value": "climb" + }, + "m53oFXA2SA5jAjWc": { + "value": "locate" + }, + "XLPn5Egg9mIuLyVP": { + "value": "protect" + } + }, + "features": [ + "Compendium.daggerheart.beastforms.Item.0tlnxIxlIw2hl1UE", + "Compendium.daggerheart.beastforms.Item.9ryNrYWjNtOT6DXN" + ], + "evolved": { + "mainTraitBonus": 0 + }, + "hybrid": { + "beastformOptions": 2, + "advantages": 2, + "features": 2 + }, + "examples": "Cat, Dog, Rabbit, etc." + }, + "effects": [ + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "d25tcdgssnDvekKR", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [ + { + "key": "system.rules.attack.trait", + "mode": 5, + "value": "instinct", + "priority": null + }, + { + "key": "system.rules.attack.range", + "mode": 5, + "value": "melee", + "priority": null + }, + { + "key": "system.rules.attack.damage.value", + "mode": 5, + "value": "1d8", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "lastModifiedBy": null + }, + "_key": "!items.effects!uxBugKULjn7O1KQc.d25tcdgssnDvekKR" + }, + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "idqGh9sm1zBLME1O", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [], + "disabled": false, + "duration": { + "startTime": null, + "combat": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976986453, + "modifiedTime": 1752976986453, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!items.effects!uxBugKULjn7O1KQc.idqGh9sm1zBLME1O" + } + ], + "folder": null, + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976986449, + "modifiedTime": 1752976987362, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "uxBugKULjn7O1KQc", + "sort": 300000, + "_key": "!items!uxBugKULjn7O1KQc" +} diff --git a/src/packs/beastforms/beastform_Legendary_Beast_mERwC7aMDoIKfZTf.json b/src/packs/beastforms/beastform_Legendary_Beast_mERwC7aMDoIKfZTf.json new file mode 100644 index 00000000..4588bc0a --- /dev/null +++ b/src/packs/beastforms/beastform_Legendary_Beast_mERwC7aMDoIKfZTf.json @@ -0,0 +1,154 @@ +{ + "name": "Legendary Beast", + "type": "beastform", + "img": "icons/creatures/magical/spirit-undead-horned-blue.webp", + "system": { + "beastformType": "evolved", + "tier": 3, + "tokenImg": "icons/creatures/magical/spirit-undead-horned-blue.webp", + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": { + "height": null, + "width": null + }, + "mainTrait": "agility", + "advantageOn": {}, + "features": [], + "evolved": { + "mainTraitBonus": 1, + "maximumTier": 1 + }, + "hybrid": { + "beastformOptions": 2, + "advantages": 2, + "features": 2 + }, + "examples": "" + }, + "effects": [ + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "cKAoI5JqYOtGBscd", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [ + { + "key": "system.bonuses.damage.physical.bonus", + "mode": 2, + "value": "6", + "priority": null + }, + { + "key": "system.bonuses.damage.magical.bonus", + "mode": 2, + "value": "6", + "priority": null + }, + { + "key": "system.evasion", + "mode": 2, + "value": "2", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

Pick a Tier 1 Beastform option and become a larger, more powerful version of that creature. While you’re in this form, you retain all traits and features from the original form and gain the following bonuses:

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "lastModifiedBy": null + }, + "_key": "!items.effects!mERwC7aMDoIKfZTf.cKAoI5JqYOtGBscd" + }, + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "LltLvTqjhk9RseV8", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [], + "disabled": false, + "duration": { + "startTime": null, + "combat": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976989252, + "modifiedTime": 1752976989252, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!items.effects!mERwC7aMDoIKfZTf.LltLvTqjhk9RseV8" + } + ], + "folder": null, + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976989245, + "modifiedTime": 1752976989245, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "mERwC7aMDoIKfZTf", + "sort": 0, + "_key": "!items!mERwC7aMDoIKfZTf" +} diff --git a/src/packs/beastforms/beastform_Mighty_Strider_LF68kGAcOTZQ81GB.json b/src/packs/beastforms/beastform_Mighty_Strider_LF68kGAcOTZQ81GB.json new file mode 100644 index 00000000..04049a81 --- /dev/null +++ b/src/packs/beastforms/beastform_Mighty_Strider_LF68kGAcOTZQ81GB.json @@ -0,0 +1,166 @@ +{ + "name": "Mighty Strider", + "type": "beastform", + "img": "icons/creatures/mammals/bull-horned-blue.webp", + "system": { + "beastformType": "normal", + "tier": 2, + "tokenImg": "icons/creatures/mammals/bull-horned-blue.webp", + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": { + "height": null, + "width": null + }, + "mainTrait": "agility", + "advantageOn": { + "Xxr01TwSerOS8qsd": { + "value": "leap" + }, + "cASut9AUij2Uf4zm": { + "value": "navigate" + }, + "XJie4FhCSwUCg9uN": { + "value": "sprint" + } + }, + "features": [ + "Compendium.daggerheart.beastforms.Item.YSolAjtv6Sfnai98", + "Compendium.daggerheart.beastforms.Item.P6tWFIZzXWyekw6r" + ], + "evolved": { + "mainTraitBonus": 0 + }, + "hybrid": { + "beastformOptions": 2, + "advantages": 2, + "features": 2 + }, + "examples": "Camel, Horse, Zebra, etc." + }, + "effects": [ + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "1KdhXARm6rg2fg22", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [ + { + "key": "system.rules.attack.damage.value", + "mode": 5, + "value": "d8 + 1", + "priority": null + }, + { + "key": "system.rules.attack.trait", + "mode": 5, + "value": "agility", + "priority": null + }, + { + "key": "system.rules.attack.range", + "mode": 5, + "value": "melee", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "lastModifiedBy": null + }, + "_key": "!items.effects!LF68kGAcOTZQ81GB.1KdhXARm6rg2fg22" + }, + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "ngEREmS8hzNyshak", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [], + "disabled": false, + "duration": { + "startTime": null, + "combat": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976987358, + "modifiedTime": 1752976987358, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!items.effects!LF68kGAcOTZQ81GB.ngEREmS8hzNyshak" + } + ], + "folder": null, + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976987354, + "modifiedTime": 1752976987362, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "LF68kGAcOTZQ81GB", + "sort": 200000, + "_key": "!items!LF68kGAcOTZQ81GB" +} diff --git a/src/packs/beastforms/beastform_Mythic_Hybrid_VI1DyowECDCDdsC1.json b/src/packs/beastforms/beastform_Mythic_Hybrid_VI1DyowECDCDdsC1.json new file mode 100644 index 00000000..0117a189 --- /dev/null +++ b/src/packs/beastforms/beastform_Mythic_Hybrid_VI1DyowECDCDdsC1.json @@ -0,0 +1,148 @@ +{ + "name": "Mythic Hybrid", + "type": "beastform", + "img": "icons/creatures/magical/humanoid-silhouette-glowing-pink.webp", + "system": { + "beastformType": "hybrid", + "tier": 4, + "tokenImg": "icons/creatures/magical/humanoid-silhouette-glowing-pink.webp", + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": { + "height": null, + "width": null + }, + "mainTrait": "strength", + "advantageOn": {}, + "features": [], + "evolved": { + "mainTraitBonus": 0 + }, + "hybrid": { + "beastformOptions": 3, + "advantages": 5, + "features": 3, + "maximumTier": 3 + }, + "examples": "" + }, + "effects": [ + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "dvqS0a0ur8xM2swY", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [ + { + "key": "system.traits.strength.value", + "mode": 2, + "value": "3", + "priority": null + }, + { + "key": "system.evasion", + "mode": 2, + "value": "2", + "priority": null + } + ], + "disabled": false, + "duration": { + "startTime": null, + "combat": null, + "seconds": null, + "rounds": null, + "turns": null, + "startRound": null, + "startTurn": null + }, + "description": "

To transform into this creature, mark 2 additional Stress. Choose any three Beastform options from Tiers 1–3. Choose a total of five advantages and three features from those options.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "lastModifiedBy": null + }, + "_key": "!items.effects!VI1DyowECDCDdsC1.dvqS0a0ur8xM2swY" + }, + { + "type": "beastform", + "name": "Beastform Transformation", + "img": "icons/creatures/abilities/paw-print-pair-purple.webp", + "_id": "xSMy4BO5PuqASEKW", + "system": { + "characterTokenData": { + "tokenImg": null, + "tokenRingImg": "icons/svg/mystery-man.svg", + "tokenSize": {} + }, + "advantageOn": [], + "featureIds": [], + "effectIds": [] + }, + "changes": [], + "disabled": false, + "duration": { + "startTime": null, + "combat": null + }, + "description": "", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976990470, + "modifiedTime": 1752976990470, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!items.effects!VI1DyowECDCDdsC1.xSMy4BO5PuqASEKW" + } + ], + "folder": null, + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976990466, + "modifiedTime": 1752976990466, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "VI1DyowECDCDdsC1", + "sort": 0, + "_key": "!items!VI1DyowECDCDdsC1" +} diff --git a/src/packs/beastforms/feature_Agile_sef9mwD2eRLZ64oV.json b/src/packs/beastforms/feature_Agile_sef9mwD2eRLZ64oV.json new file mode 100644 index 00000000..70fd6e11 --- /dev/null +++ b/src/packs/beastforms/feature_Agile_sef9mwD2eRLZ64oV.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Agile", + "img": "icons/skills/movement/arrow-upward-blue.webp", + "system": { + "description": "

Your movement is silent, and you can spend a Hope to move up to Far range without rolling.

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976877948, + "modifiedTime": 1752976906072, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "sef9mwD2eRLZ64oV", + "sort": 100000, + "_key": "!items!sef9mwD2eRLZ64oV" +} diff --git a/src/packs/beastforms/feature_Carrier_YSolAjtv6Sfnai98.json b/src/packs/beastforms/feature_Carrier_YSolAjtv6Sfnai98.json new file mode 100644 index 00000000..0ecd47f5 --- /dev/null +++ b/src/packs/beastforms/feature_Carrier_YSolAjtv6Sfnai98.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Carrier", + "img": "icons/creatures/abilities/bull-head-horns-glowing.webp", + "system": { + "description": "

You can carry up to two willing allies with you when you move.

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976866309, + "modifiedTime": 1752976907469, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "YSolAjtv6Sfnai98", + "sort": 200000, + "_key": "!items!YSolAjtv6Sfnai98" +} diff --git a/src/packs/beastforms/feature_Companion_0tlnxIxlIw2hl1UE.json b/src/packs/beastforms/feature_Companion_0tlnxIxlIw2hl1UE.json new file mode 100644 index 00000000..d7f35b73 --- /dev/null +++ b/src/packs/beastforms/feature_Companion_0tlnxIxlIw2hl1UE.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Companion", + "img": "icons/magic/life/heart-hand-gold-green-light.webp", + "system": { + "description": "

When you Help an Ally, you can roll a d8 as your advantage die.

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976848672, + "modifiedTime": 1752976908157, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "0tlnxIxlIw2hl1UE", + "sort": 0, + "_key": "!items!0tlnxIxlIw2hl1UE" +} diff --git a/src/packs/beastforms/feature_Evolved_MG21w4u5wXSGZ5WB.json b/src/packs/beastforms/feature_Evolved_MG21w4u5wXSGZ5WB.json new file mode 100644 index 00000000..520283bb --- /dev/null +++ b/src/packs/beastforms/feature_Evolved_MG21w4u5wXSGZ5WB.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Evolved", + "img": "icons/creatures/abilities/dragon-breath-purple.webp", + "system": { + "description": "

Pick a Tier 1 Beastform option and become a larger, more powerful version of that creature. While you’re in this form, you retain all traits and features from the original form and gain the following bonuses:

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976852417, + "modifiedTime": 1752976908700, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "MG21w4u5wXSGZ5WB", + "sort": 50000, + "_key": "!items!MG21w4u5wXSGZ5WB" +} diff --git a/src/packs/beastforms/feature_Fragile_9ryNrYWjNtOT6DXN.json b/src/packs/beastforms/feature_Fragile_9ryNrYWjNtOT6DXN.json new file mode 100644 index 00000000..beae6eb2 --- /dev/null +++ b/src/packs/beastforms/feature_Fragile_9ryNrYWjNtOT6DXN.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Fragile", + "img": "icons/magic/life/heart-broken-red.webp", + "system": { + "description": "

When you take Major or greater damage, you drop out of Beastform.

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976855944, + "modifiedTime": 1752976909267, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "9ryNrYWjNtOT6DXN", + "sort": 150000, + "_key": "!items!9ryNrYWjNtOT6DXN" +} diff --git a/src/packs/beastforms/feature_Trample_P6tWFIZzXWyekw6r.json b/src/packs/beastforms/feature_Trample_P6tWFIZzXWyekw6r.json new file mode 100644 index 00000000..9e1ca0c5 --- /dev/null +++ b/src/packs/beastforms/feature_Trample_P6tWFIZzXWyekw6r.json @@ -0,0 +1,34 @@ +{ + "type": "feature", + "name": "Trample", + "img": "icons/creatures/mammals/ox-bull-horned-glowing-orange.webp", + "system": { + "description": "

Mark a Stress to move up to Close range in a straight line and make an attack against all targets within Melee range of the line. Targets you succeed against take [[/r d8+1]] physical damage using your Proficiency and are temporarily Vulnerable.

", + "resource": null, + "originItemType": null, + "subType": null, + "originId": null, + "actions": [] + }, + "effects": [], + "folder": "uU8bIoZvXge0rLaU", + "ownership": { + "default": 0, + "k0gmQFlvrPvlTtbh": 3 + }, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976867812, + "modifiedTime": 1752976976609, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_id": "P6tWFIZzXWyekw6r", + "sort": -100000, + "_key": "!items!P6tWFIZzXWyekw6r" +} diff --git a/src/packs/beastforms/folders_Beastform_Features_uU8bIoZvXge0rLaU.json b/src/packs/beastforms/folders_Beastform_Features_uU8bIoZvXge0rLaU.json new file mode 100644 index 00000000..74cd56a6 --- /dev/null +++ b/src/packs/beastforms/folders_Beastform_Features_uU8bIoZvXge0rLaU.json @@ -0,0 +1,23 @@ +{ + "type": "Item", + "folder": null, + "name": "Beastform Features", + "color": null, + "sorting": "a", + "_id": "uU8bIoZvXge0rLaU", + "description": "", + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null, + "duplicateSource": null, + "exportSource": null, + "coreVersion": "13.346", + "systemId": "daggerheart", + "systemVersion": "0.0.1", + "createdTime": 1752976835537, + "modifiedTime": 1752976835537, + "lastModifiedBy": "k0gmQFlvrPvlTtbh" + }, + "_key": "!folders!uU8bIoZvXge0rLaU" +} diff --git a/styles/less/dialog/beastform/beastform-container.less b/styles/less/dialog/beastform/beastform-container.less deleted file mode 100644 index bf0d0cae..00000000 --- a/styles/less/dialog/beastform/beastform-container.less +++ /dev/null @@ -1,46 +0,0 @@ -@import '../../utils/colors.less'; - -.application.daggerheart.dh-style.views.beastform-selection { - .beastforms-container { - display: flex; - flex-direction: column; - gap: 4px; - - .beastforms-tier { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 4px; - - .beastform-container { - position: relative; - display: flex; - justify-content: center; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - cursor: pointer; - - &.inactive { - opacity: 0.4; - } - - img { - width: 100%; - border-radius: 6px; - } - - .beastform-title { - position: absolute; - top: 4px; - display: flex; - flex-wrap: wrap; - font-size: 16px; - margin: 0 4px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - color: light-dark(@beige, @dark); - background-image: url('../assets/parchments/dh-parchment-light.png'); - } - } - } - } -} diff --git a/styles/less/dialog/beastform/sheet.less b/styles/less/dialog/beastform/sheet.less index bd757b87..ce2e990d 100644 --- a/styles/less/dialog/beastform/sheet.less +++ b/styles/less/dialog/beastform/sheet.less @@ -1,15 +1,208 @@ @import '../../utils/colors.less'; @import '../../utils/mixin.less'; -.appTheme({ - &.beastform-selection { - .beastforms-container .beastforms-tier .beastform-container .beastform-title { - background-image: url('../assets/parchments/dh-parchment-dark.png'); +.theme-light .application.daggerheart.dh-style.views.beastform-selection .beastforms-outer-container { + .beastform-title { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + + .advanced-container { + .advanced-forms-container { + .advanced-form-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + + .hybrid-data-wrapper .hybrid-data-container .hybrid-data-inner-container .hybrid-data { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } + .form-features .form-feature { + background-image: url('../assets/parchments/dh-parchment-light.png'); } } -}, {}); +} .application.daggerheart.dh-style.views.beastform-selection { + .beastform-nav { + nav { + flex: 1; + + a { + white-space: nowrap; + } + } + } + + .beastform-title { + position: absolute; + top: 4px; + padding: 0 2px; + display: flex; + flex-wrap: wrap; + text-align: center; + font-size: 16px; + margin: 0 4px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + } + + .beastforms-tier { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 4px; + + .beastform-container { + position: relative; + display: flex; + justify-content: center; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + cursor: pointer; + width: 120px; + height: 120px; + + &.inactive { + opacity: 0.4; + cursor: default; + } + + &.draggable { + cursor: pointer; + filter: drop-shadow(0 0 15px light-dark(@dark-blue, @golden)); + } + + img { + width: 100%; + border-radius: 6px; + } + } + } + + .advanced-container { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 12px; + transition: width 0.3s ease; + + h2 { + margin: 0; + } + + .advanced-forms-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 16px; + } + + .advanced-form-container { + position: relative; + display: flex; + justify-content: center; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + cursor: pointer; + width: 120px; + height: 120px; + align-items: center; + text-align: center; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + &.hybridized { + flex-direction: column; + justify-content: start; + padding-top: 4px; + height: 200px; + width: 100%; + overflow: hidden; + + &.empty { + justify-content: center; + } + + .beastform-title { + position: initial; + } + } + + .empty-form { + display: flex; + flex-direction: column; + align-items: center; + + i { + font-size: 24px; + } + } + + .beastform-title-wrapper { + height: 44px; + } + + .hybrid-data-wrapper { + overflow: auto; + + .hybrid-data-container { + display: flex; + flex-direction: column; + gap: 2px; + padding: 0 4px; + + label { + font-weight: bold; + } + + .hybrid-data-inner-container { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 4px; + + .hybrid-data { + padding: 0 2px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + opacity: 0.4; + + &.active { + opacity: 1; + } + } + } + } + } + } + + .form-features { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0 16px; + margin: 8px 0; + + .form-feature { + display: flex; + flex-direction: column; + gap: 4px; + padding: 0 4px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + h4 { + text-align: center; + margin: 0; + } + } + } + } + footer { margin-top: 8px; display: flex; diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index f3e86518..66fd981d 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -12,7 +12,6 @@ @import './downtime/downtime-container.less'; -@import './beastform/beastform-container.less'; @import './beastform/sheet.less'; @import './character-creation/creation-action-footer.less'; diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 89370ec6..3470de37 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -18,6 +18,7 @@ @import './actors/environment/header.less'; @import './actors/environment/sheet.less'; +@import './items/beastform.less'; @import './items/class.less'; @import './items/domain-card.less'; @import './items/feature.less'; diff --git a/styles/less/sheets/items/beastform.less b/styles/less/sheets/items/beastform.less new file mode 100644 index 00000000..162c4925 --- /dev/null +++ b/styles/less/sheets/items/beastform.less @@ -0,0 +1,9 @@ +.application.sheet.daggerheart.dh-style.beastform { + .settings.tab { + .advantage-on-section { + display: flex; + flex-direction: column; + margin-top: 10px; + } + } +} diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 6b5db1b9..c2c72d48 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -21,13 +21,8 @@ width: 80px; } - .downtime-refresh-container { - margin-top: @fullMargin; + .action-use-button { width: 100%; - - .refresh-title { - font-weight: bold; - } } } @@ -365,6 +360,7 @@ width: 80px; } } + button { &.inner-button { --button-size: 1.25rem; diff --git a/styles/less/ui/chat/sheet.less b/styles/less/ui/chat/sheet.less index 58281690..1c41f97e 100644 --- a/styles/less/ui/chat/sheet.less +++ b/styles/less/ui/chat/sheet.less @@ -4,6 +4,27 @@ .dice-title { display: none; } + + .message-content { + .enriched-effect { + display: flex; + align-items: center; + border: 1px solid black; + width: fit-content; + padding: 0 2px 0 0; + border-radius: 6px; + color: @dark; + background-image: url(../assets/parchments/dh-parchment-light.png); + + &:hover { + text-shadow: none; + } + + span { + white-space: nowrap; + } + } + } } fieldset.daggerheart.chat { diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index 0060f74b..e6b660a5 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -74,6 +74,22 @@ } } + .tooltip-chips { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: 8px; + + .tooltip-chip { + font-size: 18px; + padding: 2px 4px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + } + .tooltip-tags { width: 100%; display: flex; diff --git a/system.json b/system.json index bfa25df3..eaef71c1 100644 --- a/system.json +++ b/system.json @@ -144,6 +144,15 @@ "type": "Item", "private": false, "flags": {} + }, + { + "name": "beastforms", + "label": "Beastforms", + "system": "daggerheart", + "path": "packs/beastforms.db", + "type": "Item", + "private": false, + "flags": {} } ], "packFolders": [ @@ -157,7 +166,7 @@ "name": "Character Options", "sorting": "m", "color": "#000000", - "packs": ["ancestries", "communities", "classes", "subclasses", "domains"] + "packs": ["ancestries", "communities", "classes", "subclasses", "domains", "beastforms"] }, { "name": "Items", diff --git a/templates/dialogs/beastform/advanced.hbs b/templates/dialogs/beastform/advanced.hbs new file mode 100644 index 00000000..78a690da --- /dev/null +++ b/templates/dialogs/beastform/advanced.hbs @@ -0,0 +1,73 @@ +
+ {{#if (eq selected.system.beastformType 'evolved')}} +

{{localize "DAGGERHEART.ITEMS.Beastform.evolve"}}

+ +
+ {{#if selectedBeastformEffect}} +
+

{{localize "DAGGERHEART.ITEMS.Beastform.evolvedFeatureTitle"}}

+
{{{selectedBeastformEffect.description}}}
+
+ {{/if}} +
+ +
+ {{#if evolved.form}} +
{{concat (localize "DAGGERHEART.CONFIG.BeastformType.evolved") " " evolved.form.name}}
+ + {{else}} +
+ + +
+ {{/if}} +
+ {{/if}} + {{#if (eq selected.system.beastformType 'hybrid')}} +

{{localize "DAGGERHEART.ITEMS.Beastform.hybridize"}}

+ +
+ {{#if selectedBeastformEffect}} +
+

{{localize "DAGGERHEART.ITEMS.Beastform.hybridizeFeatureTitle"}}

+
{{{selectedBeastformEffect.description}}}
+
+ {{/if}} +
+ +
+ {{#each hybridForms as | form key |}} +
+ {{#if form}} +
+
{{form.name}}
+
+
+
+ +
+ {{#each form.system.features as | feature |}} +
{{feature.name}}
+ {{/each}} +
+
+
+ +
+ {{#each form.system.advantageOn as | advantage id |}} +
{{advantage.value}}
+ {{/each}} +
+
+
+ {{else}} +
+ + +
+ {{/if}} +
+ {{/each}} +
+ {{/if}} +
\ No newline at end of file diff --git a/templates/dialogs/beastform/beastformTier.hbs b/templates/dialogs/beastform/beastformTier.hbs new file mode 100644 index 00000000..db307158 --- /dev/null +++ b/templates/dialogs/beastform/beastformTier.hbs @@ -0,0 +1,8 @@ +
+ {{#each tier.values as |form uuid|}} +
+ +
{{form.value.name}}
+
+ {{/each}} +
\ No newline at end of file diff --git a/templates/dialogs/beastform/footer.hbs b/templates/dialogs/beastform/footer.hbs new file mode 100644 index 00000000..8ad53964 --- /dev/null +++ b/templates/dialogs/beastform/footer.hbs @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/templates/dialogs/beastform/tabs.hbs b/templates/dialogs/beastform/tabs.hbs new file mode 100644 index 00000000..25bb2180 --- /dev/null +++ b/templates/dialogs/beastform/tabs.hbs @@ -0,0 +1,11 @@ +
+ +
\ No newline at end of file diff --git a/templates/dialogs/beastformDialog.hbs b/templates/dialogs/beastformDialog.hbs deleted file mode 100644 index 05f87b89..00000000 --- a/templates/dialogs/beastformDialog.hbs +++ /dev/null @@ -1,18 +0,0 @@ -
-
- {{#each beastformTiers as |tier tierKey|}} -
- {{tier.label}} - {{#each tier.values as |form uuid|}} -
- -
{{form.value.name}}
-
- {{/each}} -
- {{/each}} -
- -
\ 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 2fc61e2c..e3ecf859 100644 --- a/templates/sheets-settings/adversary-settings/details.hbs +++ b/templates/sheets-settings/adversary-settings/details.hbs @@ -19,7 +19,7 @@
- {{localize "DAGGERHEART.GENERAL.hitPoints.plural"}} + {{localize "DAGGERHEART.GENERAL.HitPoints.plural"}} {{formGroup systemFields.resources.fields.hitPoints.fields.value value=document.system.resources.hitPoints.value label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.resources.hitPoints.value.label")}} {{formGroup systemFields.resources.fields.hitPoints.fields.max value=document.system.resources.hitPoints.max}}
diff --git a/templates/sheets/actors/adversary/header.hbs b/templates/sheets/actors/adversary/header.hbs index e7625f16..a7cfa3a6 100644 --- a/templates/sheets/actors/adversary/header.hbs +++ b/templates/sheets/actors/adversary/header.hbs @@ -20,7 +20,7 @@ {{#if (eq source.system.type 'horde')}}
{{source.system.hordeHp}} - /{{localize "DAGGERHEART.GENERAL.hitPoints.short"}} + /{{localize "DAGGERHEART.GENERAL.HitPoints.short"}}
{{/if}}
diff --git a/templates/sheets/actors/adversary/sidebar.hbs b/templates/sheets/actors/adversary/sidebar.hbs index 8eb4dcb0..131fb33b 100644 --- a/templates/sheets/actors/adversary/sidebar.hbs +++ b/templates/sheets/actors/adversary/sidebar.hbs @@ -17,7 +17,7 @@
-

{{localize 'DAGGERHEART.GENERAL.attack.hitPoints.short'}}

+

{{localize 'DAGGERHEART.GENERAL.HitPoints.short'}}

@@ -92,7 +92,7 @@
-

{{localize DAGGERHEART.GENERAL.experience.plural}}

+

{{localize "DAGGERHEART.GENERAL.experience.plural"}}

@@ -113,6 +113,6 @@
- +
\ No newline at end of file diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 05280127..b9ac28a6 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -47,7 +47,7 @@

{{document.system.proficiency}}

-

{{localize "DAGGERHEART.GENERAL.proficienc"}}

+

{{localize "DAGGERHEART.GENERAL.proficiency"}}

@@ -95,6 +95,9 @@