From 45a322f4180d6f58e0430e48a6e5a5269162bd84 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 6 Sep 2025 19:12:15 +0200 Subject: [PATCH] Added homebrew for armor and weapon fatures --- daggerheart.mjs | 8 +- lang/en.json | 7 +- .../settings/homebrewSettings.mjs | 65 +++-- .../applications/sheets-configs/_module.mjs | 3 +- .../sheets-configs/action-config.mjs | 2 +- .../sheets-configs/activeEffectConfig.mjs | 7 + .../setting-active-effect-config.mjs | 227 ++++++++++++++++++ ...eConfig.mjs => setting-feature-config.mjs} | 68 +++++- module/applications/sheets/items/armor.mjs | 2 +- module/applications/sheets/items/weapon.mjs | 2 +- module/config/itemConfig.mjs | 56 +++++ module/data/activeEffect/baseEffect.mjs | 20 ++ module/data/item/armor.mjs | 26 +- module/data/item/weapon.mjs | 18 +- module/data/settings/Homebrew.mjs | 30 ++- .../settings/downtime-config/effects.hbs | 15 ++ templates/settings/downtime-config/main.hbs | 10 +- .../homebrew-settings/itemFeatures.hbs | 35 +++ templates/sheets/activeEffect/changes.hbs | 36 +-- templates/sheets/activeEffect/settings.hbs | 8 +- 20 files changed, 564 insertions(+), 81 deletions(-) create mode 100644 module/applications/sheets-configs/setting-active-effect-config.mjs rename module/applications/sheets-configs/{downtimeConfig.mjs => setting-feature-config.mjs} (66%) create mode 100644 templates/settings/downtime-config/effects.hbs create mode 100644 templates/settings/homebrew-settings/itemFeatures.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 1c4c2a85..c6bcdb1a 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -1,5 +1,6 @@ import { SYSTEM } from './module/config/system.mjs'; import * as applications from './module/applications/_module.mjs'; +import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; @@ -26,6 +27,7 @@ Hooks.once('init', () => { CONFIG.DH = SYSTEM; game.system.api = { applications, + data, models, documents, dice, @@ -162,7 +164,7 @@ Hooks.on('ready', async () => { if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ui.resources.render({ force: true }); - if(!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) + if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) ui.compendiumBrowser = new applications.ui.ItemBrowser(); registerCountdownHooks(); @@ -309,5 +311,5 @@ Hooks.on('moveToken', async (movedToken, data) => { } }); -Hooks.on("renderCompendiumDirectory", (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); -Hooks.on("renderDocumentDirectory", (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); +Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); +Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); diff --git a/lang/en.json b/lang/en.json index 6068d48e..b81b24e0 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1912,7 +1912,8 @@ "downtime": "Downtime", "roll": "Roll", "rules": "Rules", - "types": "Types" + "types": "Types", + "itemFeatures": "Item Features" }, "Tiers": { "singular": "Tier", @@ -1929,6 +1930,7 @@ "amount": "Amount", "any": "Any", "armor": "Armor", + "armorFeatures": "Armor Features", "armors": "Armors", "armorScore": "Armor Score", "activeEffects": "Active Effects", @@ -2041,6 +2043,7 @@ "used": "Used", "uses": "Uses", "value": "Value", + "weaponFeatures": "Weapon Features", "weapons": "Weapons", "withThing": "With {thing}" }, @@ -2248,7 +2251,9 @@ }, "Homebrew": { "newDowntimeMove": "Downtime Move", + "newFeature": "New ItemFeature", "downtimeMoves": "Downtime Moves", + "itemFeatures": "Item Features", "nrChoices": "# Moves Per Rest", "resetMovesTitle": "Reset {type} Downtime Moves", "resetMovesText": "Are you sure you want to reset?", diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index c2ac4a89..e880f7ee 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -53,6 +53,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli settings: { template: 'systems/daggerheart/templates/settings/homebrew-settings/settings.hbs' }, domains: { template: 'systems/daggerheart/templates/settings/homebrew-settings/domains.hbs' }, types: { template: 'systems/daggerheart/templates/settings/homebrew-settings/types.hbs' }, + itemTypes: { template: 'systems/daggerheart/templates/settings/homebrew-settings/itemFeatures.hbs' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } }; @@ -60,7 +61,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli /** @inheritdoc */ static TABS = { main: { - tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'downtime' }], + tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'itemFeatures' }, { id: 'downtime' }], initial: 'settings', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -115,33 +116,53 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli } static async addItem(_, target) { - await this.settings.updateSource({ - [`restMoves.${target.dataset.type}.moves.${foundry.utils.randomID()}`]: { - name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), - img: 'icons/magic/life/cross-worn-green.webp', - description: '', - actions: [] - } - }); + const { type } = target.dataset; + if (['shortRest', 'longRest'].includes(type)) { + await this.settings.updateSource({ + [`restMoves.${type}.moves.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newDowntimeMove'), + img: 'icons/magic/life/cross-worn-green.webp', + description: '', + actions: [] + } + }); + } else if (['armorFeatures', 'weaponFeatures'].includes(type)) { + await this.settings.updateSource({ + [`itemFeatures.${type}.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.newFeature'), + img: 'icons/magic/life/cross-worn-green.webp', + description: '', + actions: [], + effects: [] + } + }); + } + this.render(); } static async editItem(_, target) { - const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id]; - const path = `restMoves.${target.dataset.type}.moves.${target.dataset.id}`; - const editedMove = await game.system.api.applications.sheetConfigs.DowntimeConfig.configure( - move, - path, - this.settings - ); - if (!editedMove) return; + const { type, id } = target.dataset; + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves.${id}` : `itemFeatures.${type}.${id}`; + const featureBase = isDowntime ? this.settings.restMoves[type].moves[id] : this.settings.itemFeatures[type][id]; - await this.updateAction.bind(this)(editedMove, target.dataset.type, target.dataset.id); + const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( + featureBase, + path, + this.settings, + { hasIcon: isDowntime, hasEffects: !isDowntime } + ); + if (!editedBase) return; + + await this.updateAction.bind(this)(editedBase, target.dataset.type, target.dataset.id); } async updateAction(data, type, id) { + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; await this.settings.updateSource({ - [`restMoves.${type}.moves.${id}`]: { + [`${path}.${id}`]: { actions: data.actions, name: data.name, icon: data.icon, @@ -149,12 +170,16 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli description: data.description } }); + this.render(); } static async removeItem(_, target) { + const { type, id } = target.dataset; + const isDowntime = ['shortRest', 'longRest'].includes(type); + const path = isDowntime ? `restMoves.${type}.moves` : `itemFeatures.${type}`; await this.settings.updateSource({ - [`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null + [`${path}.-=${id}`]: null }); this.render(); } diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index ed062163..a8a625d0 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -2,7 +2,8 @@ export { default as ActionConfig } from './action-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; -export { default as DowntimeConfig } from './downtimeConfig.mjs'; +export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs'; +export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; export { default as DhTokenConfig } from './token-config.mjs'; diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 96b6cc48..0f92baa1 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -138,7 +138,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { }; } - if (this.action.parent.metadata.isQuantifiable) { + if (this.action.parent.metadata?.isQuantifiable) { options.quantity = { label: 'DAGGERHEART.GENERAL.itemQuantity', group: 'Global' diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 25f7d2b5..6a466c55 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -96,6 +96,13 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac }); } + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.systemFields = context.document.system.schema.fields; + + return context; + } + async _preparePartContext(partId, context) { const partContext = await super._preparePartContext(partId, context); switch (partId) { diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs new file mode 100644 index 00000000..9b57b47a --- /dev/null +++ b/module/applications/sheets-configs/setting-active-effect-config.mjs @@ -0,0 +1,227 @@ +import autocomplete from 'autocompleter'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(effect) { + super({}); + + this.effect = foundry.utils.deepClone(effect); + const ignoredActorKeys = ['config', 'DhEnvironment']; + this.changeChoices = Object.keys(game.system.api.models.actors).reduce((acc, key) => { + if (!ignoredActorKeys.includes(key)) { + const model = game.system.api.models.actors[key]; + const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model); + const group = game.i18n.localize(model.metadata.label); + const choices = CONFIG.Token.documentClass + .getTrackedAttributeChoices(attributes, model) + .map(x => ({ ...x, group: group })); + acc.push(...choices); + } + return acc; + }, []); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config'], + tag: 'form', + position: { + width: 560 + }, + form: { + submitOnChange: false, + closeOnSubmit: false, + handler: SettingActiveEffectConfig.#onSubmit + }, + actions: { + editImage: SettingActiveEffectConfig.#editImage, + addChange: SettingActiveEffectConfig.#addChange, + deleteChange: SettingActiveEffectConfig.#deleteChange + } + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, + tabs: { template: 'templates/generic/tab-navigation.hbs' }, + details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, + settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, + changes: { + template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', + scrollable: ['ol[data-changes]'] + }, + footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } + }; + + static TABS = { + sheet: { + tabs: [ + { id: 'details', icon: 'fa-solid fa-book' }, + { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, + { id: 'changes', icon: 'fa-solid fa-gears' } + ], + initial: 'details', + labelPrefix: 'EFFECT.TABS' + } + }; + + /**@inheritdoc */ + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.source = this.effect; + context.fields = game.system.api.documents.DhActiveEffect.schema.fields; + context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields; + + return context; + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + const changeChoices = this.changeChoices; + + htmlElement.querySelectorAll('.effect-change-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(changeChoices); + } else { + text = text.toLowerCase(); + var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (item, search) { + const label = game.i18n.localize(item.label); + const matchIndex = label.toLowerCase().indexOf(search); + + const beforeText = label.slice(0, matchIndex); + const matchText = label.slice(matchIndex, matchIndex + search.length); + const after = label.slice(matchIndex + search.length, label.length); + + const element = document.createElement('li'); + element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + if (item.hint) { + element.dataset.tooltip = game.i18n.localize(item.hint); + } + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: function (item) { + element.value = `system.${item.value}`; + }, + click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, + minLength: 0 + }); + }); + } + + async _preparePartContext(partId, context) { + if (partId in context.tabs) context.tab = context.tabs[partId]; + switch (partId) { + case 'details': + context.isActorEffect = false; + context.isItemEffect = true; + const useGeneric = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.appearance + ).showGenericStatusEffects; + if (!useGeneric) { + context.statuses = Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({ + value: status.id, + label: game.i18n.localize(status.name) + })); + } + break; + case 'changes': + context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => { + modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`); + return modes; + }, {}); + + context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES; + break; + } + + return context; + } + + static async #onSubmit(event, form, formData) { + this.data = foundry.utils.expandObject(formData.object); + this.close(); + } + + /** + * Edit a Document image. + * @this {DocumentSheetV2} + * @type {ApplicationClickAction} + */ + static async #editImage(_event, target) { + if (target.nodeName !== 'IMG') { + throw new Error('The editImage action is available only for IMG elements.'); + } + + const attr = target.dataset.edit; + const current = foundry.utils.getProperty(this.effect, attr); + const fp = new FilePicker.implementation({ + current, + type: 'image', + callback: path => (target.src = path), + position: { + top: this.position.top + 40, + left: this.position.left + 10 + } + }); + + await fp.browse(); + } + + /** + * Add a new change to the effect's changes array. + * @this {ActiveEffectConfig} + * @type {ApplicationClickAction} + */ + static async #addChange() { + const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); + const changes = Object.values(submitData.changes ?? {}); + changes.push({}); + + this.effect.changes = changes; + this.render(); + } + + /** + * Delete a change from the effect's changes array. + * @this {ActiveEffectConfig} + * @type {ApplicationClickAction} + */ + static async #deleteChange(event) { + const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); + const changes = Object.values(submitData.changes); + const row = event.target.closest('li'); + const index = Number(row.dataset.index) || 0; + changes.splice(index, 1); + + this.effect.changes = changes; + this.render(); + } + + static async configure(effect, options = {}) { + return new Promise(resolve => { + const app = new this(effect, options); + app.addEventListener('close', () => resolve(app.data), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets-configs/downtimeConfig.mjs b/module/applications/sheets-configs/setting-feature-config.mjs similarity index 66% rename from module/applications/sheets-configs/downtimeConfig.mjs rename to module/applications/sheets-configs/setting-feature-config.mjs index 80aab900..e775f93d 100644 --- a/module/applications/sheets-configs/downtimeConfig.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -3,8 +3,8 @@ import DHActionConfig from './action-config.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export default class DowntimeConfig extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(move, movePath, settings, options) { +export default class SettingFeatureConfig extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(move, movePath, settings, optionalParts, options) { super(options); this.move = move; @@ -12,6 +12,10 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati this.movePath = movePath; this.actionsPath = `${movePath}.actions`; this.settings = settings; + + const { hasIcon, hasEffects } = optionalParts; + this.hasIcon = hasIcon; + this.hasEffects = hasEffects; } get title() { @@ -30,6 +34,7 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, + addEffect: this.addEffect, resetMoves: this.resetMoves, saveForm: this.saveForm }, @@ -41,13 +46,14 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, main: { template: 'systems/daggerheart/templates/settings/downtime-config/main.hbs' }, actions: { template: 'systems/daggerheart/templates/settings/downtime-config/actions.hbs' }, + effects: { template: 'systems/daggerheart/templates/settings/downtime-config/effects.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/downtime-config/footer.hbs' } }; /** @inheritdoc */ static TABS = { primary: { - tabs: [{ id: 'main' }, { id: 'actions' }], + tabs: [{ id: 'main' }, { id: 'actions' }, { id: 'effects' }], initial: 'main', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -55,6 +61,9 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati async _prepareContext(_options) { const context = await super._prepareContext(_options); + context.tabs = this._filterTabs(context.tabs); + context.hasIcon = this.hasIcon; + context.hasEffects = this.hasEffects; context.move = this.move; context.move.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML( context.move.description @@ -130,13 +139,30 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati } static async editItem(_, target) { - const actionId = target.dataset.id; - const action = this.move.actions.get(actionId); - await new DHActionConfig(action, async updatedMove => { - await this.settings.updateSource({ [`${this.actionsPath}.${actionId}`]: updatedMove }); + const { type, id } = target.dataset; + if (type === 'effect') { + const effectIndex = this.move.effects.findIndex(x => x.id === id); + const effect = this.move.effects[effectIndex]; + const updatedEffect = + await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); + if (!updatedEffect) return; + + await this.settings.updateSource({ + [`${this.movePath}.effects`]: this.move.effects.reduce((acc, effect, index) => { + acc.push(index === effectIndex ? { ...updatedEffect, id: effect.id } : effect); + return acc; + }, []) + }); this.move = foundry.utils.getProperty(this.settings, this.movePath); this.render(); - }).render(true); + } else { + const action = this.move.actions.get(id); + await new DHActionConfig(action, async updatedMove => { + await this.settings.updateSource({ [`${this.actionsPath}.${id}`]: updatedMove }); + this.move = foundry.utils.getProperty(this.settings, this.movePath); + this.render(); + }).render(true); + } } static async removeItem(_, target) { @@ -145,16 +171,38 @@ export default class DowntimeConfig extends HandlebarsApplicationMixin(Applicati this.render(); } + static async addEffect(_, target) { + const currentEffects = foundry.utils.getProperty(this.settings, `${this.movePath}.effects`); + await this.settings.updateSource({ + [`${this.movePath}.effects`]: [ + ...currentEffects, + game.system.api.data.activeEffects.BaseEffect.getDefaultObject() + ] + }); + + this.move = foundry.utils.getProperty(this.settings, this.movePath); + this.render(); + } + static resetMoves() {} + _filterTabs(tabs) { + return this.hasEffects + ? tabs + : Object.keys(tabs).reduce((acc, key) => { + if (key !== 'effects') acc[key] = tabs[key]; + return acc; + }, {}); + } + /** @override */ _onClose(options = {}) { if (!options.submitted) this.move = null; } - static async configure(move, movePath, settings, options = {}) { + static async configure(move, movePath, settings, optionalParts, options = {}) { return new Promise(resolve => { - const app = new this(move, movePath, settings, options); + const app = new this(move, movePath, settings, optionalParts, options); app.addEventListener('close', () => resolve(app.move), { once: true }); app.render({ force: true }); }); diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index bdc482c3..2550b415 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -8,7 +8,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { tagifyConfigs: [ { selector: '.features-input', - options: () => CONFIG.DH.ITEM.armorFeatures, + options: () => CONFIG.DH.ITEM.orderedArmorFeatures(), callback: ArmorSheet.#onFeatureSelect } ] diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index 2533287b..f5c7dddf 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -8,7 +8,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { tagifyConfigs: [ { selector: '.features-input', - options: () => CONFIG.DH.ITEM.weaponFeatures, + options: () => CONFIG.DH.ITEM.orderedWeaponFeatures(), callback: WeaponSheet.#onFeatureSelect } ] diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 7e9f75ea..d815181b 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -452,6 +452,34 @@ export const armorFeatures = { } }; +export const allArmorFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .armorFeatures; + return { + ...armorFeatures, + ...Object.keys(homebrewFeatures).reduce((acc, key) => { + const feature = homebrewFeatures[key]; + acc[key] = { ...feature, label: feature.name }; + return acc; + }, {}) + }; +}; + +export const orderedArmorFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .armorFeatures; + const allFeatures = { ...armorFeatures, ...homebrewFeatures }; + const all = Object.keys(allFeatures).map(key => { + const feature = allFeatures[key]; + return { + ...feature, + id: key, + label: feature.label ?? feature.name + }; + }); + return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label))); +}; + export const weaponFeatures = { barrier: { label: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.name', @@ -1383,6 +1411,34 @@ export const weaponFeatures = { } }; +export const allWeaponFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .weaponFeatures; + return { + ...weaponFeatures, + ...Object.keys(homebrewFeatures).reduce((acc, key) => { + const feature = homebrewFeatures[key]; + acc[key] = { ...feature, label: feature.name }; + return acc; + }, {}) + }; +}; + +export const orderedWeaponFeatures = () => { + const homebrewFeatures = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).itemFeatures + .weaponFeatures; + const allFeatures = { ...weaponFeatures, ...homebrewFeatures }; + const all = Object.keys(allFeatures).map(key => { + const feature = allFeatures[key]; + return { + ...feature, + id: key, + label: feature.label ?? feature.name + }; + }); + return Object.values(all).sort((a, b) => game.i18n.localize(a.label).localeCompare(game.i18n.localize(b.label))); +}; + export const featureTypes = { ancestry: { id: 'ancestry', diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index 0ac87de0..770e3462 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -30,4 +30,24 @@ export default class BaseEffect extends foundry.abstract.TypeDataModel { }) }; } + + static getDefaultObject() { + return { + name: 'New Effect', + id: foundry.utils.randomID(), + disabled: false, + img: 'icons/magic/life/heart-cross-blue.webp', + description: '', + statuses: [], + changes: [], + system: { + rangeDependence: { + enabled: false, + type: CONFIG.DH.GENERAL.rangeInclusion.withinRange.id, + target: CONFIG.DH.GENERAL.otherTargetTypes.hostile.id, + range: CONFIG.DH.GENERAL.range.melee.id + } + } + }; + } } diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 7f70d3f7..ca1ca004 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -1,5 +1,4 @@ import AttachableItem from './attachableItem.mjs'; -import { armorFeatures } from '../../config/itemConfig.mjs'; export default class DHArmor extends AttachableItem { /** @inheritDoc */ @@ -25,7 +24,7 @@ export default class DHArmor extends AttachableItem { new fields.SchemaField({ value: new fields.StringField({ required: true, - choices: CONFIG.DH.ITEM.armorFeatures, + choices: CONFIG.DH.ITEM.allArmorFeatures, blank: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), @@ -60,13 +59,14 @@ export default class DHArmor extends AttachableItem { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; + const changedArmorFeatures = changes.system?.armorFeatures ?? []; + const removedFeatures = this.armorFeatures.filter(x => changedArmorFeatures.every(y => y.value !== x.value)); if (changes.system?.armorFeatures) { - const removed = this.armorFeatures.filter(x => !changes.system.armorFeatures.includes(x)); - const added = changes.system.armorFeatures.filter(x => !this.armorFeatures.includes(x)); + const added = changedArmorFeatures.filter(x => this.armorFeatures.every(y => y.value !== x.value)); const effectIds = []; const actionIds = []; - for (var feature of removed) { + for (var feature of removedFeatures) { effectIds.push(...feature.effectIds); actionIds.push(...feature.actionIds); } @@ -76,8 +76,9 @@ export default class DHArmor extends AttachableItem { return acc; }, {}); + const allFeatures = CONFIG.DH.ITEM.allArmorFeatures(); for (const feature of added) { - const featureData = armorFeatures[feature.value]; + const featureData = allFeatures[feature.value]; if (featureData.effects?.length > 0) { const embeddedItems = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -91,7 +92,7 @@ export default class DHArmor extends AttachableItem { } const newActions = {}; - if (featureData.actions?.length > 0) { + if (featureData.actions?.length > 0 || featureData.actions?.size > 0) { for (let action of featureData.actions) { const embeddedEffects = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -110,10 +111,12 @@ export default class DHArmor extends AttachableItem { { ...cls.getSourceConfig(this), ...action, + type: action.type, _id: actionId, name: game.i18n.localize(action.name), description: game.i18n.localize(action.description), - effects: embeddedEffects.map(x => ({ _id: x.id })) + effects: embeddedEffects.map(x => ({ _id: x.id })), + systemPath: 'actions' }, { parent: this } ); @@ -126,6 +129,10 @@ export default class DHArmor extends AttachableItem { } } + _onUpdate(a, b, c) { + super._onUpdate(a, b, c); + } + /** * Generates a list of localized tags based on this item's type-specific properties. * @returns {string[]} An array of localized tag strings. @@ -145,7 +152,8 @@ export default class DHArmor extends AttachableItem { */ _getLabels() { const labels = []; - if(this.baseScore) labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`) + if (this.baseScore) + labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`); return labels; } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 07a5c028..b2d937b5 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -39,7 +39,7 @@ export default class DHWeapon extends AttachableItem { new fields.SchemaField({ value: new fields.StringField({ required: true, - choices: CONFIG.DH.ITEM.weaponFeatures, + choices: CONFIG.DH.ITEM.allWeaponFeatures, blank: true }), effectIds: new fields.ArrayField(new fields.StringField({ required: true })), @@ -116,13 +116,14 @@ export default class DHWeapon extends AttachableItem { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; + const changedWeaponFeatures = changes.system?.weaponFeatures ?? []; + const removedFeatures = this.weaponFeatures.filter(x => changedWeaponFeatures.every(y => y.value !== x.value)); if (changes.system?.weaponFeatures) { - const removed = this.weaponFeatures.filter(x => !changes.system.weaponFeatures.includes(x)); - const added = changes.system.weaponFeatures.filter(x => !this.weaponFeatures.includes(x)); + const added = changedWeaponFeatures.filter(x => this.weaponFeatures.every(y => y.value !== x.value)); const removedEffectsUpdate = []; const removedActionsUpdate = []; - for (let weaponFeature of removed) { + for (let weaponFeature of removedFeatures) { removedEffectsUpdate.push(...weaponFeature.effectIds); removedActionsUpdate.push(...weaponFeature.actionIds); } @@ -133,8 +134,9 @@ export default class DHWeapon extends AttachableItem { return acc; }, {}); + const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures(); for (let weaponFeature of added) { - const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value]; + const featureData = allFeatures[weaponFeature.value]; if (featureData.effects?.length > 0) { const embeddedItems = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -148,7 +150,7 @@ export default class DHWeapon extends AttachableItem { } const newActions = {}; - if (featureData.actions?.length > 0) { + if (featureData.actions?.length > 0 || featureData.actions?.size > 0) { for (let action of featureData.actions) { const embeddedEffects = await this.parent.createEmbeddedDocuments( 'ActiveEffect', @@ -170,10 +172,12 @@ export default class DHWeapon extends AttachableItem { { ...cls.getSourceConfig(this), ...action, + type: action.type, _id: actionId, name: game.i18n.localize(action.name), description: game.i18n.localize(action.description), - effects: embeddedEffects.map(x => ({ _id: x.id })) + effects: embeddedEffects.map(x => ({ _id: x.id })), + systemPath: 'actions' }, { parent: this } ); diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 0719b085..ca44a3ed 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -115,7 +115,35 @@ export default class DhHomebrew extends foundry.abstract.DataModel { label: new fields.StringField({ required: true, label: 'DAGGERHEART.GENERAL.label' }), description: new fields.StringField() }) - ) + ), + itemFeatures: new fields.SchemaField({ + weaponFeatures: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + img: new fields.FilePathField({ + initial: 'icons/magic/life/cross-worn-green.webp', + categories: ['IMAGE'], + base64: false + }), + description: new fields.HTMLField(), + actions: new ActionsField(), + effects: new fields.ArrayField(new fields.ObjectField()) + }) + ), + armorFeatures: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + img: new fields.FilePathField({ + initial: 'icons/magic/life/cross-worn-green.webp', + categories: ['IMAGE'], + base64: false + }), + description: new fields.HTMLField(), + actions: new ActionsField(), + effects: new fields.ArrayField(new fields.ObjectField()) + }) + ) + }) }; } } diff --git a/templates/settings/downtime-config/effects.hbs b/templates/settings/downtime-config/effects.hbs new file mode 100644 index 00000000..f09fdb05 --- /dev/null +++ b/templates/settings/downtime-config/effects.hbs @@ -0,0 +1,15 @@ +
+
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + +
+ {{#each move.effects}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" id=this.id type="effect" }} + {{/each}} +
+
+
\ No newline at end of file diff --git a/templates/settings/downtime-config/main.hbs b/templates/settings/downtime-config/main.hbs index f8a972c7..7f681368 100644 --- a/templates/settings/downtime-config/main.hbs +++ b/templates/settings/downtime-config/main.hbs @@ -3,11 +3,13 @@ data-tab='{{tabs.main.id}}' data-group='{{tabs.main.group}}' > -
- {{localize "Icon"}} + {{#if hasIcon}} +
+ {{localize "Icon"}} - -
+ +
+ {{/if}}
{{localize "Description"}} diff --git a/templates/settings/homebrew-settings/itemFeatures.hbs b/templates/settings/homebrew-settings/itemFeatures.hbs new file mode 100644 index 00000000..1f8595de --- /dev/null +++ b/templates/settings/homebrew-settings/itemFeatures.hbs @@ -0,0 +1,35 @@ +
+
+
+ + {{localize "DAGGERHEART.GENERAL.weaponFeatures"}} + + + + +
+ {{#each settingFields._source.itemFeatures.weaponFeatures as |feature id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="weaponFeatures" id=id }} + {{/each}} +
+
+ +
+ + {{localize "DAGGERHEART.GENERAL.armorFeatures"}} + + + + +
+ {{#each settingFields._source.itemFeatures.armorFeatures as |feature id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="armorFeatures" id=id }} + {{/each}} +
+
+
+
\ No newline at end of file diff --git a/templates/sheets/activeEffect/changes.hbs b/templates/sheets/activeEffect/changes.hbs index 9cf137f0..75f49e4a 100644 --- a/templates/sheets/activeEffect/changes.hbs +++ b/templates/sheets/activeEffect/changes.hbs @@ -8,24 +8,24 @@
    {{#each source.changes as |change i|}} - {{#with ../fields.changes.element.fields as |changeFields|}} -
  1. -
    - -
    -
    - {{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}} -
    -
    - {{formInput changeFields.value name=(concat "changes." i ".value") value=change.value}} -
    -
    - {{formInput changeFields.priority name=(concat "changes." i ".priority") value=change.priority - placeholder=(lookup ../../priorities change.mode)}} -
    -
    -
  2. - {{/with}} + {{#with ../fields.changes.element.fields as |changeFields|}} +
  3. +
    + +
    +
    + {{formInput changeFields.mode name=(concat "changes." i ".mode") value=change.mode choices=@root.modes}} +
    +
    + {{formInput changeFields.value name=(concat "changes." i ".value") value=change.value}} +
    +
    + {{formInput changeFields.priority name=(concat "changes." i ".priority") value=change.priority + placeholder=(lookup ../../priorities change.mode)}} +
    +
    +
  4. + {{/with}} {{/each}}
\ No newline at end of file diff --git a/templates/sheets/activeEffect/settings.hbs b/templates/sheets/activeEffect/settings.hbs index 33e1d1b9..cf98c786 100644 --- a/templates/sheets/activeEffect/settings.hbs +++ b/templates/sheets/activeEffect/settings.hbs @@ -2,10 +2,10 @@
{{localize "DAGGERHEART.ACTIVEEFFECT.Config.rangeDependence.title"}} - {{formGroup document.system.schema.fields.rangeDependence.fields.enabled value=source.system.rangeDependence.enabled localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.type value=source.system.rangeDependence.type localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} - {{formGroup document.system.schema.fields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }} + {{formGroup systemFields.rangeDependence.fields.enabled value=source.system.rangeDependence.enabled localize=true }} + {{formGroup systemFields.rangeDependence.fields.type value=source.system.rangeDependence.type localize=true }} + {{formGroup systemFields.rangeDependence.fields.target value=source.system.rangeDependence.target localize=true }} + {{formGroup systemFields.rangeDependence.fields.range value=source.system.rangeDependence.range localize=true }}