diff --git a/daggerheart.mjs b/daggerheart.mjs
index f5f5e303..d7aba401 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,
diff --git a/lang/en.json b/lang/en.json
index 8de962bd..85e941dc 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -1917,6 +1917,7 @@
"roll": "Roll",
"rules": "Rules",
"types": "Types",
+ "itemFeatures": "Item Features",
"questions": "Questions"
},
"Tiers": {
@@ -1934,6 +1935,7 @@
"amount": "Amount",
"any": "Any",
"armor": "Armor",
+ "armorFeatures": "Armor Features",
"armors": "Armors",
"armorScore": "Armor Score",
"activeEffects": "Active Effects",
@@ -2046,6 +2048,7 @@
"used": "Used",
"uses": "Uses",
"value": "Value",
+ "weaponFeatures": "Weapon Features",
"weapons": "Weapons",
"withThing": "With {thing}"
},
@@ -2276,7 +2279,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 @@
+