From 218f180fa013fdf0cd0d4d86d068af5f43c15269 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:13:51 +0200 Subject: [PATCH 1/6] [Fix] DiceSoNice DamageRolls (#1026) * Fixed so the 3d damage dice can be seen by everyone when not whispered * Fixed typo --- module/dice/damageRoll.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 427b6273..aa9e1d94 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -37,7 +37,13 @@ export default class DamageRoll extends DHRoll { Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) ), diceRoll = Roll.fromTerms([pool]); - await game.dice3d.showForRoll(diceRoll, game.user, true, chatMessage.whisper, chatMessage.blind); + await game.dice3d.showForRoll( + diceRoll, + game.user, + true, + chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, + chatMessage.blind + ); } await super.buildPost(roll, config, message); if (config.source?.message) { From 60b55619e1035e5a10509e22055af079a76ac946 Mon Sep 17 00:00:00 2001 From: Chris Ryan <73275196+chrisryan10@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:05:09 +1000 Subject: [PATCH 2/6] [PR] [Feature] 652 Allow override range measurement settings (#1030) * Look for rangeMeasurementSettingsOverride on the scene to switch off DH global range measurement settings. * Part progress on adding config tab to scene config * Hard coded template; no value applied/saved * Flag fix * Use the flags setting * Clean up * Remove import * Better initialisation of PARTS and TABS * Fix localisation --------- Co-authored-by: Chris Ryan Co-authored-by: WBHarry --- daggerheart.mjs | 5 ++++ lang/en.json | 11 +++++++ module/applications/_module.mjs | 1 + module/applications/scene/_module.mjs | 1 + .../scene/sceneConfigSettings.mjs | 25 ++++++++++++++++ module/canvas/placeables/measuredTemplate.mjs | 30 +++++++++++++------ module/canvas/placeables/ruler.mjs | 6 ++-- module/canvas/placeables/tokenRuler.mjs | 6 ++-- module/systemRegistration/handlebars.mjs | 3 ++ templates/scene/dh-config.hbs | 9 ++++++ 10 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 module/applications/scene/_module.mjs create mode 100644 module/applications/scene/sceneConfigSettings.mjs create mode 100644 templates/scene/dh-config.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 795764cc..9ed5feb1 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -145,6 +145,11 @@ Hooks.once('init', () => { // Make Compendium Dialog resizable foundry.applications.sidebar.apps.Compendium.DEFAULT_OPTIONS.window.resizable = true; + DocumentSheetConfig.registerSheet(foundry.documents.Scene, SYSTEM.id, applications.scene.DhSceneConfigSettings, { + makeDefault: true, + label: 'Daggerheart' + }); + settingsRegistration.registerDHSettings(); RegisterHandlebarsHelpers.registerHelpers(); diff --git a/lang/en.json b/lang/en.json index a7a615f5..d8a4b08f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -26,6 +26,14 @@ "CONTROLS": { "inFront": "In Front" }, + "SCENE": { + "TABS": { + "SHEET": { + "dh": "Daggerheart" + } + } + }, + "DAGGERHEART": { "ACTIONS": { "TYPES": { @@ -2283,6 +2291,9 @@ "ResetSettings": { "resetConfirmationTitle": "Reset Settings", "resetConfirmationText": "Are you sure you want to reset the {settings}?" + }, + "Scene": { + "rangeMeasurementOverride": "Override Global Range Measurement Settings" } }, "UI": { diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index d4ceb229..3dd0c78f 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -2,6 +2,7 @@ export * as characterCreation from './characterCreation/_module.mjs'; export * as dialogs from './dialogs/_module.mjs'; export * as hud from './hud/_module.mjs'; export * as levelup from './levelup/_module.mjs'; +export * as scene from './scene/_module.mjs'; export * as settings from './settings/_module.mjs'; export * as sheets from './sheets/_module.mjs'; export * as sheetConfigs from './sheets-configs/_module.mjs'; diff --git a/module/applications/scene/_module.mjs b/module/applications/scene/_module.mjs new file mode 100644 index 00000000..6dc59081 --- /dev/null +++ b/module/applications/scene/_module.mjs @@ -0,0 +1 @@ +export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs'; \ No newline at end of file diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs new file mode 100644 index 00000000..2ad7421a --- /dev/null +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -0,0 +1,25 @@ +export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig { + constructor(options, ...args) { + super(options, ...args); + } + + static buildParts() { + const { footer, ...parts } = super.PARTS; + const tmpParts = { + ...parts, + dh: { template: "systems/daggerheart/templates/scene/dh-config.hbs" }, + footer + } + return tmpParts; + } + + static PARTS = DhSceneConfigSettings.buildParts(); + + static buildTabs() { + super.TABS.sheet.tabs.push({ id: "dh", icon: "fa-solid" }); + return super.TABS; + } + + static TABS = DhSceneConfigSettings.buildTabs(); + +} \ No newline at end of file diff --git a/module/canvas/placeables/measuredTemplate.mjs b/module/canvas/placeables/measuredTemplate.mjs index c9950650..49142685 100644 --- a/module/canvas/placeables/measuredTemplate.mjs +++ b/module/canvas/placeables/measuredTemplate.mjs @@ -10,29 +10,41 @@ export default class DhMeasuredTemplate extends foundry.canvas.placeables.Measur const splitRulerText = this.ruler.text.split(' '); if (splitRulerText.length > 0) { const rulerValue = Number(splitRulerText[0]); - const vagueLabel = this.constructor.getDistanceLabel(rulerValue, rangeMeasurementSettings); - this.ruler.text = vagueLabel; + const result = this.constructor.getRangeLabels(rulerValue, rangeMeasurementSettings); + this.ruler.text = result.distance + result.units ? (' ' + result.units) : ''; } } } - static getDistanceLabel(distance, settings) { + static getRangeLabels(distance, settings) { + let result = { distance: '', units: null } + const rangeMeasurementOverride = canvas.scene.flags.daggerheart?.rangeMeasurementOverride; + + if (rangeMeasurementOverride === true) { + result.distance = distance; + result.units = canvas.scene?.grid?.units; + return result + } if (distance <= settings.melee) { - return game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name'); + result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.melee.name'); + return result; } if (distance <= settings.veryClose) { - return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name'); + result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryClose.name'); + return result; } if (distance <= settings.close) { - return game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name'); + result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.close.name'); + return result; } if (distance <= settings.far) { - return game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name'); + result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.far.name'); + return result; } if (distance > settings.far) { - return game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name'); + result.distance = game.i18n.localize('DAGGERHEART.CONFIG.Range.veryFar.name'); } - return ''; + return result; } } diff --git a/module/canvas/placeables/ruler.mjs b/module/canvas/placeables/ruler.mjs index 6585a1cd..6e2f220d 100644 --- a/module/canvas/placeables/ruler.mjs +++ b/module/canvas/placeables/ruler.mjs @@ -8,9 +8,9 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler { const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; if (range.enabled) { - const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); - context.cost = { total: distance, units: null }; - context.distance = { total: distance, units: null }; + const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range); + context.cost = { total: result.distance, units: result.units }; + context.distance = { total: result.distance, units: result.units }; } return context; diff --git a/module/canvas/placeables/tokenRuler.mjs b/module/canvas/placeables/tokenRuler.mjs index ff8fc0d5..056953f8 100644 --- a/module/canvas/placeables/tokenRuler.mjs +++ b/module/canvas/placeables/tokenRuler.mjs @@ -8,9 +8,9 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke const range = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; if (range.enabled) { - const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); - context.cost = { total: distance, units: null }; - context.distance = { total: distance, units: null }; + const result = DhMeasuredTemplate.getRangeLabels(waypoint.measurement.distance.toNearest(0.01), range); + context.cost = { total: result.distance, units: result.units }; + context.distance = { total: result.distance, units: result.units }; } return context; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index ff741b91..cb7be42a 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -35,5 +35,8 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', + + 'systems/daggerheart/templates/scene/dh-config.hbs', + ]); }; diff --git a/templates/scene/dh-config.hbs b/templates/scene/dh-config.hbs new file mode 100644 index 00000000..0d20c302 --- /dev/null +++ b/templates/scene/dh-config.hbs @@ -0,0 +1,9 @@ +
+
+
+ + +
+
+
\ No newline at end of file From 523ecb506b8f4b91d97f71b2859c7b36c36b0b79 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:38:07 +0200 Subject: [PATCH 3/6] [Fix] ItemLink Fix (#1032) * . * . * Removed outcommented code * Raised to minor version * Added confirmation on import of old character data --- daggerheart.mjs | 3 ++ lang/en.json | 4 +- .../sheets/api/application-mixin.mjs | 1 - module/applications/sheets/api/base-item.mjs | 9 ++-- module/config/settingsConfig.mjs | 3 +- module/data/actor/character.mjs | 12 ++--- module/data/item/base.mjs | 50 ++++++------------- module/data/item/feature.mjs | 18 ------- module/documents/actor.mjs | 42 +++++++++++----- module/helpers/utils.mjs | 11 ++++ module/systemRegistration/_module.mjs | 1 + module/systemRegistration/migrations.mjs | 41 +++++++++++++++ module/systemRegistration/settings.mjs | 6 +++ system.json | 2 +- 14 files changed, 122 insertions(+), 81 deletions(-) create mode 100644 module/systemRegistration/migrations.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 795764cc..96a6783b 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -13,6 +13,7 @@ import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs' import { registerCountdownHooks } from './module/data/countdowns.mjs'; import { handlebarsRegistration, + runMigrations, settingsRegistration, socketRegistration } from './module/systemRegistration/_module.mjs'; @@ -168,6 +169,8 @@ Hooks.on('ready', async () => { game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.welcomeMessage, true); } } + + runMigrations(); }); Hooks.once('dicesoniceready', () => {}); diff --git a/lang/en.json b/lang/en.json index 7daf6b14..6a34037d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -193,7 +193,9 @@ "companionLevelup": { "confirmTitle": "Companion Levelup", "confirmText": "Would you like to level up your companion {name} by {levelChange} levels at this time? (You can do it manually later)" - } + }, + "InvalidOldCharacterImportTitle": "Old Character Import", + "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?" }, "Companion": { "FIELDS": { diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index ab1c9ab2..bdcee27a 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -591,7 +591,6 @@ export default function DHApplicationMixin(Base) { if (featureOnCharacter) { systemData = { originItemType: this.document.type, - originId: this.document.id, identifier: this.document.system.isMulticlass ? 'multiclass' : null }; } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 9d7df6ee..e722b72e 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -149,12 +149,12 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { const { type } = target.dataset; const cls = foundry.documents.Item.implementation; + const multiclass = this.document.system.isMulticlass ? 'multiclass' : null; let systemData = {}; if (this.document.parent?.type === 'character') { systemData = { originItemType: this.document.type, - originId: this.document.id, - identifier: this.document.system.isMulticlass ? 'multiclass' : null + identifier: multiclass ?? type }; } @@ -275,14 +275,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { if (this.document.parent?.type === 'character') { const itemData = item.toObject(); + const multiclass = this.document.system.isMulticlass ? 'multiclass' : null; item = await cls.create( { ...itemData, + _stats: { compendiumSource: this.document.uuid }, system: { ...itemData.system, originItemType: this.document.type, - originId: this.document.id, - identifier: this.document.system.isMulticlass ? 'multiclass' : null + identifier: multiclass ?? target.dataset.type } }, { parent: this.document.parent } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index dd8aeffe..76234a97 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -26,5 +26,6 @@ export const gameSettings = { Fear: 'ResourcesFear' }, LevelTiers: 'LevelTiers', - Countdowns: 'Countdowns' + Countdowns: 'Countdowns', + LastMigrationVersion: 'LastMigrationVersion' }; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index bb6a8c65..7dd7993c 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -444,16 +444,12 @@ export default class DhCharacter extends BaseDataActor { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { if (this.class.subclass) { const subclassState = this.class.subclass.system.featureState; - const subclass = - item.system.identifier === 'multiclass' ? this.multiclass.subclass : this.class.subclass; - const featureType = subclass - ? (subclass.system.features.find(x => x.item?.uuid === item.uuid)?.type ?? null) - : null; if ( - featureType === CONFIG.DH.ITEM.featureSubTypes.foundation || - (featureType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) || - (featureType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) + item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.foundation || + (item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.specialization && + subclassState >= 2) || + (item.system.identifier === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) ) { subclassFeatures.push(item); } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index f0b22b44..f333537b 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -144,50 +144,30 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { } if (this.actor && this.actor.type === 'character' && this.features) { - const featureUpdates = {}; + const features = []; for (let f of this.features) { const fBase = f.item ?? f; const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid); - const createData = foundry.utils.mergeObject( - feature.toObject(), - { - system: { - originItemType: this.parent.type, - originId: data._id, - identifier: this.isMulticlass ? 'multiclass' : null - } - }, - { inplace: false } + const multiclass = this.isMulticlass ? 'multiclass' : null; + features.push( + foundry.utils.mergeObject( + feature.toObject(), + { + _stats: { compendiumSource: fBase.uuid }, + system: { + originItemType: this.parent.type, + identifier: multiclass ?? (f.item ? f.type : null) + } + }, + { inplace: false } + ) ); - const [doc] = await this.actor.createEmbeddedDocuments('Item', [createData]); - - if (!featureUpdates.features) - featureUpdates.features = this.features.map(x => (x.item ? { ...x, item: x.item.uuid } : x.uuid)); - - if (f.item) { - const existingFeature = featureUpdates.features.find(x => x.item === f.item.uuid); - existingFeature.item = doc.uuid; - } else { - const replaceIndex = featureUpdates.features.findIndex(x => x === f.uuid); - featureUpdates.features.splice(replaceIndex, 1, doc.uuid); - } } - await this.updateSource(featureUpdates); + await this.actor.createEmbeddedDocuments('Item', features); } } - async _preDelete() { - if (!this.actor || this.actor.type !== 'character') return; - - const items = this.actor.items.filter(item => item.system.originId === this.parent.id); - if (items.length > 0) - await this.actor.deleteEmbeddedDocuments( - 'Item', - items.map(x => x.id) - ); - } - async _preUpdate(changed, options, userId) { const allowed = await super._preUpdate(changed, options, userId); if (allowed === false) return false; diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 13c1d149..6e1aab41 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,5 +1,4 @@ import BaseDataItem from './base.mjs'; -import { ActionField, ActionsField } from '../fields/actionField.mjs'; export default class DHFeature extends BaseDataItem { /** @inheritDoc */ @@ -30,24 +29,7 @@ export default class DHFeature extends BaseDataItem { nullable: true, initial: null }), - originId: new fields.StringField({ nullable: true, initial: null }), identifier: new fields.StringField() }; } - - get spellcastingModifier() { - let traitValue = 0; - if (this.actor && this.originId && ['class', 'subclass'].includes(this.originItemType)) { - if (this.originItemType === 'subclass') { - traitValue = - this.actor.system.traits[this.actor.items.get(this.originId).system.spellcastingTrait]?.value ?? 0; - } else { - const { value: multiclass, subclass } = this.actor.system.multiclass; - const selectedSubclass = multiclass?.id === this.originId ? subclass : this.actor.system.class.subclass; - traitValue = this.actor.system.traits[selectedSubclass.system.spellcastingTrait]?.value ?? 0; - } - } - - return traitValue; - } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 4a6d2d67..e1dd93af 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,7 +1,7 @@ import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -import { damageKeyToNumber } from '../helpers/utils.mjs'; +import { damageKeyToNumber, versionCompare } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; export default class DhpActor extends Actor { @@ -27,7 +27,7 @@ export default class DhpActor extends Actor { /** @inheritDoc */ static migrateData(source) { - if(source.system?.attack && !source.system.attack.type) source.system.attack.type = "attack"; + if (source.system?.attack && !source.system.attack.type) source.system.attack.type = 'attack'; return super.migrateData(source); } @@ -571,19 +571,15 @@ export default class DhpActor extends Actor { if (armorSlotResult) { const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult; updates.find(u => u.key === 'hitPoints').value = modifiedDamage; - if(armorSpent) { + if (armorSpent) { const armorUpdate = updates.find(u => u.key === 'armor'); - if(armorUpdate) - armorUpdate.value += armorSpent; - else - updates.push({ value: armorSpent, key: 'armor' }); + if (armorUpdate) armorUpdate.value += armorSpent; + else updates.push({ value: armorSpent, key: 'armor' }); } - if(stressSpent) { + if (stressSpent) { const stressUpdate = updates.find(u => u.key === 'stress'); - if(stressUpdate) - stressUpdate.value += stressSpent; - else - updates.push({ value: stressSpent, key: 'stress' }); + if (stressUpdate) stressUpdate.value += stressSpent; + else updates.push({ value: stressSpent, key: 'stress' }); } } } @@ -753,4 +749,26 @@ export default class DhpActor extends Actor { } } } + + /** @inheritdoc */ + async importFromJSON(json) { + if (!this.type === 'character') return await super.importFromJSON(json); + + if (!CONST.WORLD_DOCUMENT_TYPES.includes(this.documentName)) { + throw new Error('Only world Documents may be imported'); + } + + const parsedJSON = JSON.parse(json); + if (versionCompare(parsedJSON._stats.systemVersion, '1.1.0')) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.ACTORS.Character.InvalidOldCharacterImportTitle') + }, + content: game.i18n.localize('DAGGERHEART.ACTORS.Character.InvalidOldCharacterImportText') + }); + if (!confirmed) return; + } + + return await super.importFromJSON(json); + } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 6f4e5a26..c8f4186f 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -420,3 +420,14 @@ export async function createEmbeddedItemsWithEffects(actor, baseData) { export const slugify = name => { return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', ''); }; + +export const versionCompare = (current, target) => { + const currentSplit = current.split('.').map(x => Number.parseInt(x)); + const targetSplit = target.split('.').map(x => Number.parseInt(x)); + for (var i = 0; i < currentSplit.length; i++) { + if (currentSplit[i] < targetSplit[i]) return true; + if (currentSplit[i] > targetSplit[i]) return false; + } + + return false; +}; diff --git a/module/systemRegistration/_module.mjs b/module/systemRegistration/_module.mjs index 38dcac4f..483a6b37 100644 --- a/module/systemRegistration/_module.mjs +++ b/module/systemRegistration/_module.mjs @@ -1,3 +1,4 @@ export { preloadHandlebarsTemplates as handlebarsRegistration } from './handlebars.mjs'; export * as settingsRegistration from './settings.mjs'; export * as socketRegistration from './socket.mjs'; +export { runMigrations } from './migrations.mjs'; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs new file mode 100644 index 00000000..e84018fa --- /dev/null +++ b/module/systemRegistration/migrations.mjs @@ -0,0 +1,41 @@ +import { versionCompare } from '../helpers/utils.mjs'; + +export async function runMigrations() { + let lastMigrationVersion = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion); + if (!lastMigrationVersion) lastMigrationVersion = '1.0.6'; + + if (versionCompare(lastMigrationVersion, '1.1.0')) { + const compendiumActors = []; + for (let pack of game.packs) { + const documents = await pack.getDocuments(); + compendiumActors.push(...documents.filter(x => x.type === 'character')); + } + + [...compendiumActors, ...game.actors].forEach(actor => { + const items = actor.items.reduce((acc, item) => { + if (item.type === 'feature') { + const { originItemType, isMulticlass, identifier } = item.system; + const base = originItemType + ? actor.items.find( + x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass) + ) + : null; + if (base) { + const feature = base.system.features.find(x => x.item && x.item.uuid === item.uuid); + if (feature && identifier !== 'multiclass') { + acc.push({ _id: item.id, system: { identifier: feature.type } }); + } + } + } + + return acc; + }, []); + + actor.updateEmbeddedDocuments('Item', items); + }); + + lastMigrationVersion = '1.1.0'; + } + + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); +} diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 3d9ffc19..4828ebb0 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -91,6 +91,12 @@ const registerMenus = () => { }; const registerNonConfigSettings = () => { + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, { + scope: 'world', + config: false, + type: String + }); + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers, { scope: 'world', config: false, diff --git a/system.json b/system.json index e6b7650f..d72c53d5 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.0.6", + "version": "1.1.0", "compatibility": { "minimum": "13", "verified": "13.347", From 888cf9172bb69c0d444201527c408c9d6f53a825 Mon Sep 17 00:00:00 2001 From: Luiz HD Costa Date: Thu, 21 Aug 2025 22:35:36 -0300 Subject: [PATCH 4/6] [Community PR] Localize hardcoded text (#1002) * Localize remaining hardcoded user-facing strings * Introduce pluralize helper for localizing strings * Localize missing strings from ItemBrowser --- lang/en.json | 39 +++++- module/applications/ui/itemBrowser.mjs | 15 ++- module/config/itemBrowserConfig.mjs | 128 +++++++++---------- module/data/fields/action/costField.mjs | 2 +- module/data/fields/action/usesField.mjs | 2 +- module/helpers/handlebarsHelper.mjs | 21 ++- templates/sheets/actors/character/header.hbs | 2 +- templates/ui/chat/parts/target-part.hbs | 6 +- templates/ui/itemBrowser/itemBrowser.hbs | 14 +- 9 files changed, 147 insertions(+), 82 deletions(-) diff --git a/lang/en.json b/lang/en.json index 63b18219..1bddbad3 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2347,6 +2347,42 @@ "playerMessage": "{user} rerolled their {name}" } }, + "ItemBrowser": { + "title": "Daggerheart Compendium Browser", + "hint": "Select a Folder in sidebar to start browsing through the compendium", + "searchPlaceholder": "Search...", + "columnName": "Name", + "tooltipFilters": "Filters", + "tooltipErase": "Erase", + "difficultyMin": "Difficulty (Min)", + "difficultyMax": "Difficulty (Max)", + "hitPointsMin": "Hit Points (Min)", + "hitPointsMax": "Hit Points (Max)", + "stressMin": "Stress (Min)", + "stressMax": "Stress (Max)", + "armorScoreMin": "Armor Score (Min)", + "armorScoreMax": "Armor Score (Max)", + "levelMin": "Level (Min)", + "levelMax": "Level (Max)", + "recallCostMin": "Recall Cost (Min)", + "recallCostMax": "Recall Cost (Max)", + "evasionMin": "Evasion (Min)", + "evasionMax": "Evasion (Max)", + "subtype": "Subtype", + "folders": { + "adversaries": "Adversaries", + "ancestries": "Ancestries", + "equipment": "Equipment", + "classes": "Classes", + "subclasses": "Subclasses", + "domainCards": "Domain Cards", + "communities": "Communities", + "environments": "Environments", + "beastforms": "Beastforms", + "features": "Features", + "items": "Items" + } + }, "Notifications": { "adversaryMissing": "The linked adversary doesn't exist in the world.", "beastformInapplicable": "A beastform can only be applied to a Character.", @@ -2406,7 +2442,8 @@ "beastformEquipWeapon": "You cannot use weapons while in a Beastform.", "loadoutMaxReached": "You've reached maximum loadout. Move atleast one domain card to the vault, or increase the limit in homebrew settings if desired.", "domainMaxReached": "You've reached the maximum domains for the class. Increase the limit in homebrew settings if desired.", - "insufficientResources": "You have insufficient resources", + "insufficientResources": "You don't have enough resources to use that action.", + "actionNoUsesRemaining": "That action doesn't have remaining uses.", "multiclassAlreadyPresent": "You already have a class and multiclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice" diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index f0ad98db..3a31bd4e 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -154,7 +154,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { Object.values(config).forEach(c => { const folder = { id: c.id, - label: c.label, + label: game.i18n.localize(c.label), selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id }; folder.folders = c.folders @@ -173,11 +173,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { folderPath = `${compendium}.folders.${folderId}`, folderData = foundry.utils.getProperty(config, folderPath); + const columns = ItemBrowser.getFolderConfig(folderData).map(col => ({ + ...col, + label: game.i18n.localize(col.label) + })); + this.selectedMenu = { path: folderPath.split('.'), data: { ...folderData, - columns: ItemBrowser.getFolderConfig(folderData) + columns: columns } }; @@ -237,6 +242,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { else if (typeof f.choices === 'function') { f.choices = f.choices(); } + + // Clear field label so template uses our custom label parameter + if (f.field && f.label) { + f.field.label = undefined; + } + f.name ??= f.key; f.value = this.presets?.filter?.[f.name]?.value ?? null; }); diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index 6e3c0dea..0cb42d2c 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -3,63 +3,63 @@ export const typeConfig = { columns: [ { key: "system.tier", - label: "Tier" + label: "DAGGERHEART.GENERAL.Tiers.singular" }, { key: "system.type", - label: "Type" + label: "DAGGERHEART.GENERAL.type" } ], filters: [ { key: "system.tier", - label: "Tier", + label: "DAGGERHEART.GENERAL.Tiers.singular", field: 'system.api.models.actors.DhAdversary.schema.fields.tier' }, { key: "system.type", - label: "Type", + label: "DAGGERHEART.GENERAL.type", field: 'system.api.models.actors.DhAdversary.schema.fields.type' }, { key: "system.difficulty", name: "difficulty.min", - label: "Difficulty (Min)", + label: "DAGGERHEART.UI.ItemBrowser.difficultyMin", field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', operator: "gte" }, { key: "system.difficulty", name: "difficulty.max", - label: "Difficulty (Max)", + label: "DAGGERHEART.UI.ItemBrowser.difficultyMax", field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', operator: "lte" }, { key: "system.resources.hitPoints.max", name: "hp.min", - label: "Hit Points (Min)", + label: "DAGGERHEART.UI.ItemBrowser.hitPointsMin", field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', operator: "gte" }, { key: "system.resources.hitPoints.max", name: "hp.max", - label: "Hit Points (Max)", + label: "DAGGERHEART.UI.ItemBrowser.hitPointsMax", field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', operator: "lte" }, { key: "system.resources.stress.max", name: "stress.min", - label: "Stress (Min)", + label: "DAGGERHEART.UI.ItemBrowser.stressMin", field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', operator: "gte" }, { key: "system.resources.stress.max", name: "stress.max", - label: "Stress (Max)", + label: "DAGGERHEART.UI.ItemBrowser.stressMax", field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', operator: "lte" }, @@ -69,70 +69,70 @@ export const typeConfig = { columns: [ { key: "type", - label: "Type" + label: "DAGGERHEART.GENERAL.type" }, { key: "system.secondary", - label: "Subtype", + label: "DAGGERHEART.UI.ItemBrowser.subtype", format: (isSecondary) => isSecondary ? "secondary" : (isSecondary === false ? "primary" : '-') }, { key: "system.tier", - label: "Tier" + label: "DAGGERHEART.GENERAL.Tiers.singular" } ], filters: [ { key: "type", - label: "Type", + label: "DAGGERHEART.GENERAL.type", choices: () => CONFIG.Item.documentClass.TYPES.filter(t => ["armor", "weapon", "consumable", "loot"].includes(t)).map(t => ({ value: t, label: t })) }, { key: "system.secondary", - label: "Subtype", + label: "DAGGERHEART.UI.ItemBrowser.subtype", choices: [ - { value: false, label: "Primary Weapon"}, - { value: true, label: "Secondary Weapon"} + { value: false, label: "DAGGERHEART.ITEMS.Weapon.primaryWeapon" }, + { value: true, label: "DAGGERHEART.ITEMS.Weapon.secondaryWeapon" } ] }, { key: "system.tier", - label: "Tier", - choices: [{ value: "1", label: "1"}, { value: "2", label: "2"}, { value: "3", label: "3"}, { value: "4", label: "4"}] + label: "DAGGERHEART.GENERAL.Tiers.singular", + choices: [{ value: "1", label: "1" }, { value: "2", label: "2" }, { value: "3", label: "3" }, { value: "4", label: "4" }] }, { key: "system.burden", - label: "Burden", + label: "DAGGERHEART.GENERAL.burden", field: 'system.api.models.items.DHWeapon.schema.fields.burden' }, { key: "system.attack.roll.trait", - label: "Trait", + label: "DAGGERHEART.GENERAL.Trait.single", field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait' }, { key: "system.attack.range", - label: "Range", + label: "DAGGERHEART.GENERAL.range", field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range' }, { key: "system.baseScore", name: "armor.min", - label: "Armor Score (Min)", + label: "DAGGERHEART.UI.ItemBrowser.armorScoreMin", field: 'system.api.models.items.DHArmor.schema.fields.baseScore', operator: "gte" }, { key: "system.baseScore", name: "armor.max", - label: "Armor Score (Max)", + label: "DAGGERHEART.UI.ItemBrowser.armorScoreMax", field: 'system.api.models.items.DHArmor.schema.fields.baseScore', operator: "lte" }, { key: "system.itemFeatures", - label: "Features", - choices: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k,v]) => ({ value: k, label: v.label})), + label: "DAGGERHEART.GENERAL.features", + choices: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k, v]) => ({ value: k, label: v.label })), operator: "contains3" } ] @@ -149,54 +149,54 @@ export const typeConfig = { columns: [ { key: "system.type", - label: "Type" + label: "DAGGERHEART.GENERAL.type" }, { key: "system.domain", - label: "Domain" + label: "DAGGERHEART.GENERAL.Domain.single" }, { key: "system.level", - label: "Level" + label: "DAGGERHEART.GENERAL.level" } ], filters: [ { key: "system.type", - label: "Type", + label: "DAGGERHEART.GENERAL.type", field: 'system.api.models.items.DHDomainCard.schema.fields.type' }, { key: "system.domain", - label: "Domain", + label: "DAGGERHEART.GENERAL.Domain.single", field: 'system.api.models.items.DHDomainCard.schema.fields.domain', operator: "contains2" }, { key: "system.level", name: "level.min", - label: "Level (Min)", + label: "DAGGERHEART.UI.ItemBrowser.levelMin", field: 'system.api.models.items.DHDomainCard.schema.fields.level', operator: "gte" }, { key: "system.level", name: "level.max", - label: "Level (Max)", + label: "DAGGERHEART.UI.ItemBrowser.levelMax", field: 'system.api.models.items.DHDomainCard.schema.fields.level', operator: "lte" }, { key: "system.recallCost", name: "recall.min", - label: "Recall Cost (Min)", + label: "DAGGERHEART.UI.ItemBrowser.recallCostMin", field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', operator: "gte" }, { key: "system.recallCost", name: "recall.max", - label: "Recall Cost (Max)", + label: "DAGGERHEART.UI.ItemBrowser.recallCostMax", field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', operator: "lte" } @@ -206,50 +206,50 @@ export const typeConfig = { columns: [ { key: "system.evasion", - label: "Evasion" + label: "DAGGERHEART.GENERAL.evasion" }, { key: "system.hitPoints", - label: "Hit Points" + label: "DAGGERHEART.GENERAL.HitPoints.plural" }, { key: "system.domains", - label: "Domains" + label: "DAGGERHEART.GENERAL.Domain.plural" } ], filters: [ { key: "system.evasion", name: "evasion.min", - label: "Evasion (Min)", + label: "DAGGERHEART.UI.ItemBrowser.evasionMin", field: 'system.api.models.items.DHClass.schema.fields.evasion', operator: "gte" }, { key: "system.evasion", name: "evasion.max", - label: "Evasion (Max)", + label: "DAGGERHEART.UI.ItemBrowser.evasionMax", field: 'system.api.models.items.DHClass.schema.fields.evasion', operator: "lte" }, { key: "system.hitPoints", name: "hp.min", - label: "Hit Points (Min)", + label: "DAGGERHEART.UI.ItemBrowser.hitPointsMin", field: 'system.api.models.items.DHClass.schema.fields.hitPoints', operator: "gte" }, { key: "system.hitPoints", name: "hp.max", - label: "Hit Points (Max)", + label: "DAGGERHEART.UI.ItemBrowser.hitPointsMax", field: 'system.api.models.items.DHClass.schema.fields.hitPoints', operator: "lte" }, { key: "system.domains", - label: "Domains", - choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label})), + label: "DAGGERHEART.GENERAL.Domain.plural", + choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label })), operator: "contains2" } ] @@ -258,14 +258,14 @@ export const typeConfig = { columns: [ { key: "id", - label: "Class", + label: "TYPES.Item.class", format: (id) => { return ""; } }, { key: "system.spellcastingTrait", - label: "Spellcasting Trait" + label: "DAGGERHEART.ITEMS.Subclass.spellcastingTrait" } ], filters: [] @@ -274,22 +274,22 @@ export const typeConfig = { columns: [ { key: "system.tier", - label: "Tier" + label: "DAGGERHEART.GENERAL.Tiers.singular" }, { key: "system.mainTrait", - label: "Main Trait" + label: "DAGGERHEART.GENERAL.Trait.single" } ], filters: [ { key: "system.tier", - label: "Tier", + label: "DAGGERHEART.GENERAL.Tiers.singular", field: 'system.api.models.items.DHBeastform.schema.fields.tier' }, { key: "system.mainTrait", - label: "Main Trait", + label: "DAGGERHEART.GENERAL.Trait.single", field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait' } ] @@ -304,20 +304,20 @@ export const compendiumConfig = { "adversaries": { id: "adversaries", keys: ["adversaries"], - label: "Adversaries", + label: "DAGGERHEART.UI.ItemBrowser.folders.adversaries", type: ["adversary"], listType: "adversaries" }, "ancestries": { id: "ancestries", keys: ["ancestries"], - label: "Ancestries", + label: "DAGGERHEART.UI.ItemBrowser.folders.ancestries", type: ["ancestry"], folders: { "features": { id: "features", keys: ["ancestries"], - label: "Features", + label: "DAGGERHEART.UI.ItemBrowser.folders.features", type: ["feature"] } } @@ -325,26 +325,26 @@ export const compendiumConfig = { "equipments": { id: "equipments", keys: ["armors", "weapons", "consumables", "loot"], - label: "Equipment", + label: "DAGGERHEART.UI.ItemBrowser.folders.equipment", type: ["armor", "weapon", "consumable", "loot"], listType: "items" }, "classes": { id: "classes", keys: ["classes"], - label: "Classes", + label: "DAGGERHEART.UI.ItemBrowser.folders.classes", type: ["class"], folders: { "features": { id: "features", keys: ["classes"], - label: "Features", + label: "DAGGERHEART.UI.ItemBrowser.folders.features", type: ["feature"] }, "items": { id: "items", keys: ["classes"], - label: "Items", + label: "DAGGERHEART.UI.ItemBrowser.folders.items", type: ["armor", "weapon", "consumable", "loot"], listType: "items" } @@ -354,27 +354,27 @@ export const compendiumConfig = { "subclasses": { id: "subclasses", keys: ["subclasses"], - label: "Subclasses", + label: "DAGGERHEART.UI.ItemBrowser.folders.subclasses", type: ["subclass"], listType: "subclasses" }, "domains": { id: "domains", keys: ["domains"], - label: "Domain Cards", + label: "DAGGERHEART.UI.ItemBrowser.folders.domainCards", type: ["domainCard"], listType: "cards" }, "communities": { id: "communities", keys: ["communities"], - label: "Communities", + label: "DAGGERHEART.UI.ItemBrowser.folders.communities", type: ["community"], folders: { "features": { id: "features", keys: ["communities"], - label: "Features", + label: "DAGGERHEART.UI.ItemBrowser.folders.features", type: ["feature"] } } @@ -382,20 +382,20 @@ export const compendiumConfig = { "environments": { id: "environments", keys: ["environments"], - label: "Environments", + label: "DAGGERHEART.UI.ItemBrowser.folders.environments", type: ["environment"] }, "beastforms": { id: "beastforms", keys: ["beastforms"], - label: "Beastforms", + label: "DAGGERHEART.UI.ItemBrowser.folders.beastforms", type: ["beastform"], listType: "beastforms", folders: { "features": { id: "features", keys: ["beastforms"], - label: "Features", + label: "DAGGERHEART.UI.ItemBrowser.folders.features", type: ["feature"] } } diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index f4d942b1..c224fff0 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -25,7 +25,7 @@ export default class CostField extends fields.ArrayField { config.costs = CostField.calcCosts.call(this, costs); const hasCost = CostField.hasCost.call(this, config.costs); if (config.isFastForward && !hasCost) - return ui.notifications.warn("You don't have the resources to use that action."); + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources')); return hasCost; } diff --git a/module/data/fields/action/usesField.mjs b/module/data/fields/action/usesField.mjs index 3993ca3b..5c2bfb61 100644 --- a/module/data/fields/action/usesField.mjs +++ b/module/data/fields/action/usesField.mjs @@ -25,7 +25,7 @@ export default class UsesField extends fields.SchemaField { if (uses && !uses.value) uses.value = 0; config.uses = uses; const hasUses = UsesField.hasUses.call(this, config.uses); - if (config.isFastForward && !hasUses) return ui.notifications.warn("That action doesn't have remaining uses."); + if (config.isFastForward && !hasUses) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining')); return hasUses; } diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 171255e2..83220307 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -13,7 +13,8 @@ export default class RegisterHandlebarsHelpers { hasProperty: foundry.utils.hasProperty, getProperty: foundry.utils.getProperty, setVar: this.setVar, - empty: this.empty + empty: this.empty, + pluralize: this.pluralize }); } static add(a, b) { @@ -64,7 +65,7 @@ export default class RegisterHandlebarsHelpers { return isNumerical ? (!result ? 0 : Number(result)) : result; } - static setVar(name, value, context) { + static setVar(name, value) { this[name] = value; } @@ -72,4 +73,20 @@ export default class RegisterHandlebarsHelpers { if (!(typeof object === 'object')) return true; return Object.keys(object).length === 0; } + + /** + * Pluralize helper that returns the appropriate localized string based on count + * @param {number} count - The number to check for plurality + * @param {string} baseKey - The base localization key (e.g., "DAGGERHEART.GENERAL.Target") + * @returns {string} The localized singular or plural string + * + * Usage: {{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}} + * Returns: "Target" if count is exactly 1, "Targets" if count is 0, 2+, or invalid + */ + static pluralize(count, baseKey) { + const numericCount = Number(count); + const isSingular = !isNaN(numericCount) && numericCount === 1; + const key = isSingular ? `${baseKey}.single` : `${baseKey}.plural`; + return game.i18n.localize(key); + } } diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 88a72fb8..e19c1dea 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -6,7 +6,7 @@ type='text' name='name' value='{{document.name}}' - placeholder='Actor Name' + placeholder='{{localize "DAGGERHEART.GENERAL.actorName"}}' /> diff --git a/templates/ui/chat/parts/target-part.hbs b/templates/ui/chat/parts/target-part.hbs index 82c71456..af7e93b0 100644 --- a/templates/ui/chat/parts/target-part.hbs +++ b/templates/ui/chat/parts/target-part.hbs @@ -1,11 +1,11 @@
-
Target
+
{{pluralize currentTargets.length "DAGGERHEART.GENERAL.Target"}}
{{#if (or (and targets.length (or (gt targetShort.hit 0) (gt targetShort.miss 0))) (and hasSave pendingSaves))}}
{{#if (or (gt targetShort.hit 0) (gt targetShort.miss 0))}} -
{{targetShort.hit}} {{#if (gt targetShort.hit 1)}}{{localize "DAGGERHEART.GENERAL.hit.single"}}{{else}}{{localize "DAGGERHEART.GENERAL.hit.plural"}}{{/if}}
-
{{targetShort.miss}} {{#if (gt targetShort.miss 1)}}{{localize "DAGGERHEART.GENERAL.miss.single"}}{{else}}{{localize "DAGGERHEART.GENERAL.miss.plural"}}{{/if}}
+
{{targetShort.hit}} {{pluralize targetShort.hit "DAGGERHEART.GENERAL.hit"}}
+
{{targetShort.miss}} {{pluralize targetShort.miss "DAGGERHEART.GENERAL.miss"}}
{{/if}} {{#if (and hasSave pendingSaves)}}
{{/if}}
diff --git a/templates/ui/itemBrowser/itemBrowser.hbs b/templates/ui/itemBrowser/itemBrowser.hbs index 000e4c03..ca0def19 100644 --- a/templates/ui/itemBrowser/itemBrowser.hbs +++ b/templates/ui/itemBrowser/itemBrowser.hbs @@ -17,12 +17,12 @@
- +
{{#if fieldFilter.length}} - + {{/if}} - +
@@ -55,9 +55,9 @@ {{#if menu.data.columns.length}}
-
Name
+
{{localize 'DAGGERHEART.UI.ItemBrowser.columnName'}}
{{#each menu.data.columns}} - {{label}} + {{localize label}} {{/each}}
{{/if}} @@ -82,8 +82,8 @@ {{!--
--}} {{else}}
-

Daggerheart Compendium Browser

- Select a Folder in sidebar to start browsing trought the compendium +

{{localize "DAGGERHEART.UI.ItemBrowser.title"}}

+ {{localize "DAGGERHEART.UI.ItemBrowser.hint"}}
{{/if}}
\ No newline at end of file From ee786544c75a970dc34d2364db600b932f6a72b7 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:22:47 +0200 Subject: [PATCH 5/6] Fix double damageKeyToNumber (#1044) --- module/documents/actor.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 0a68e9d1..4ef8b3b0 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,7 +1,7 @@ import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -import { createScrollText, damageKeyToNumber, damageKeyToNumber, versionCompare } from '../helpers/utils.mjs'; +import { createScrollText, damageKeyToNumber, versionCompare } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; export default class DhpActor extends Actor { From d5f7e17339dd0b24490be12064fe06f4fbebdf1c Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:18:25 +0200 Subject: [PATCH 6/6] Added Custom Adversary Types (#1048) --- lang/en.json | 7 ++- .../settings/homebrewSettings.mjs | 54 +++++++++++++++++-- .../applications/sheets/actors/adversary.mjs | 4 ++ module/config/actorConfig.mjs | 5 ++ module/data/actor/adversary.mjs | 2 +- module/data/settings/Homebrew.mjs | 7 +++ styles/less/ui/index.less | 1 + .../ui/settings/homebrew-settings/types.less | 52 ++++++++++++++++++ .../settings/homebrew-settings/types.hbs | 28 ++++++++++ templates/sheets/actors/adversary/header.hbs | 4 +- templates/ui/tooltip/adversary.hbs | 2 +- 11 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 styles/less/ui/settings/homebrew-settings/types.less create mode 100644 templates/settings/homebrew-settings/types.hbs diff --git a/lang/en.json b/lang/en.json index 1bddbad3..1c614fc2 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1900,7 +1900,8 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", - "rules": "Rules" + "rules": "Rules", + "types": "Types" }, "Tiers": { "singular": "Tier", @@ -2230,6 +2231,10 @@ "deleteDomain": "Delete Domain", "deleteDomainText": "Are you sure you want to delete the {name} domain? It will be immediately removed from all Actors in this world where it's currently used. Compendiums are not cleared.", "duplicateDomain": "There is already a domain with this identification." + }, + "adversaryType": { + "title": "Custom Adversary Types", + "newType": "Adversary Type" } }, "Menu": { diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 3fa42afd..c2ac4a89 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -1,5 +1,6 @@ import { DhHomebrew } from '../../data/settings/_module.mjs'; import { slugify } from '../../helpers/utils.mjs'; + const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) { @@ -10,11 +11,14 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).toObject() ); - this.selected = { - domain: null - }; + this.selected = this.#getDefaultAdversaryType(); } + #getDefaultAdversaryType = () => ({ + domain: null, + adversaryType: null + }); + get title() { return game.i18n.localize('DAGGERHEART.SETTINGS.Menu.title'); } @@ -35,6 +39,9 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli addDomain: this.addDomain, toggleSelectedDomain: this.toggleSelectedDomain, deleteDomain: this.deleteDomain, + addAdversaryType: this.addAdversaryType, + deleteAdversaryType: this.deleteAdversaryType, + selectAdversaryType: this.selectAdversaryType, save: this.save, reset: this.reset }, @@ -45,6 +52,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, 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' }, downtime: { template: 'systems/daggerheart/templates/settings/homebrew-settings/downtime.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/homebrew-settings/footer.hbs' } }; @@ -52,12 +60,19 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli /** @inheritdoc */ static TABS = { main: { - tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'downtime' }], + tabs: [{ id: 'settings' }, { id: 'domains' }, { id: 'types' }, { id: 'downtime' }], initial: 'settings', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; + changeTab(tab, group, options) { + super.changeTab(tab, group, options); + this.selected = this.#getDefaultAdversaryType(); + + this.render(); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); context.settingFields = this.settings; @@ -79,6 +94,11 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli context.configDomains = CONFIG.DH.DOMAIN.domains; context.homebrewDomains = this.settings.domains; break; + case 'types': + context.selectedAdversaryType = this.selected.adversaryType + ? { id: this.selected.adversaryType, ...this.settings.adversaryTypes[this.selected.adversaryType] } + : null; + break; } return context; @@ -301,6 +321,32 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async addAdversaryType(_, target) { + const newId = foundry.utils.randomID(); + await this.settings.updateSource({ + [`adversaryTypes.${newId}`]: { + id: newId, + label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.adversaryType.newType') + } + }); + + this.selected.adversaryType = newId; + this.render(); + } + + static async deleteAdversaryType(_, target) { + const { key } = target.dataset; + await this.settings.updateSource({ [`adversaryTypes.-=${key}`]: null }); + + this.selected.adversaryType = this.selected.adversaryType === key ? null : this.selected.adversaryType; + this.render(); + } + + static async selectAdversaryType(_, target) { + this.selected.adversaryType = this.selected.adversaryType === target.dataset.type ? null : target.dataset.type; + this.render(); + } + static async save() { await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); this.close(); diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index b47cb85a..f575a2f2 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -56,6 +56,7 @@ export default class AdversarySheet extends DHBaseActorSheet { async _prepareContext(options) { const context = await super._prepareContext(options); context.systemFields.attack.fields = this.document.system.attack.schema.fields; + return context; } @@ -65,6 +66,9 @@ export default class AdversarySheet extends DHBaseActorSheet { switch (partId) { case 'header': await this._prepareHeaderContext(context, options); + + const adversaryTypes = CONFIG.DH.ACTOR.allAdversaryTypes(); + context.adversaryType = game.i18n.localize(adversaryTypes[this.document.system.type].label); break; case 'notes': await this._prepareNotesContext(context, options); diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 6453cd78..55f03789 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -157,6 +157,11 @@ export const adversaryTypes = { } }; +export const allAdversaryTypes = () => ({ + ...adversaryTypes, + ...game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).adversaryTypes +}); + export const environmentTypes = { exploration: { label: 'DAGGERHEART.CONFIG.EnvironmentType.exploration.label', diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 80bcb43e..ba0693f7 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -27,7 +27,7 @@ export default class DhpAdversary extends BaseDataActor { }), type: new fields.StringField({ required: true, - choices: CONFIG.DH.ACTOR.adversaryTypes, + choices: CONFIG.DH.ACTOR.allAdversaryTypes, initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id }), motivesAndTactics: new fields.StringField(), diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index e18fee39..0719b085 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -108,6 +108,13 @@ export default class DhHomebrew extends foundry.abstract.DataModel { }), description: new fields.HTMLField() }) + ), + adversaryTypes: new fields.TypedObjectField( + new fields.SchemaField({ + id: new fields.StringField({ required: true }), + label: new fields.StringField({ required: true, label: 'DAGGERHEART.GENERAL.label' }), + description: new fields.StringField() + }) ) }; } diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 4a93feb6..49d1e009 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -21,3 +21,4 @@ @import './settings/settings.less'; @import './settings/homebrew-settings/domains.less'; +@import './settings/homebrew-settings/types.less'; diff --git a/styles/less/ui/settings/homebrew-settings/types.less b/styles/less/ui/settings/homebrew-settings/types.less new file mode 100644 index 00000000..d09431f7 --- /dev/null +++ b/styles/less/ui/settings/homebrew-settings/types.less @@ -0,0 +1,52 @@ +.theme-light .daggerheart.dh-style.setting.homebrew-settings .types.tab { + .adversary-types-container .adversary-type-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } +} + +.daggerheart.dh-style.setting.homebrew-settings { + .types.tab { + .adversary-types-container { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + + .adversary-type-container { + height: 2em; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border: 1px solid; + border-radius: 6px; + padding: 0 8px; + border: 1px solid light-dark(@dark-blue, @golden); + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + cursor: pointer; + opacity: 0.6; + + &:hover { + opacity: 1; + } + + &.active { + opacity: 1; + background: var(--color-warm-2); + } + } + } + + .type-edit-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + + textarea { + width: 100%; + } + } + } +} diff --git a/templates/settings/homebrew-settings/types.hbs b/templates/settings/homebrew-settings/types.hbs new file mode 100644 index 00000000..f9d3bba3 --- /dev/null +++ b/templates/settings/homebrew-settings/types.hbs @@ -0,0 +1,28 @@ +
+
+ + {{localize "DAGGERHEART.SETTINGS.Homebrew.adversaryType.title"}} + + + +
+ {{#each settingFields.adversaryTypes as |type key|}} +
+ {{type.label}} +
+
+ {{/each}} +
+ + {{#if selectedAdversaryType}} +
+ {{formGroup settingFields.schema.fields.adversaryTypes.element.fields.label name=(concat "adversaryTypes." selectedAdversaryType.id ".label") value=selectedAdversaryType.label localize=true }} + +
+ {{/if}} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/header.hbs b/templates/sheets/actors/adversary/header.hbs index 8411dd93..e6f829b8 100644 --- a/templates/sheets/actors/adversary/header.hbs +++ b/templates/sheets/actors/adversary/header.hbs @@ -13,9 +13,7 @@
- - {{localize (concat 'DAGGERHEART.CONFIG.AdversaryType.' source.system.type '.label')}} - + {{adversaryType}}
{{#if (eq source.system.type 'horde')}}
diff --git a/templates/ui/tooltip/adversary.hbs b/templates/ui/tooltip/adversary.hbs index b400bd29..86c399c5 100644 --- a/templates/ui/tooltip/adversary.hbs +++ b/templates/ui/tooltip/adversary.hbs @@ -12,7 +12,7 @@
- {{#with (lookup config.ACTOR.adversaryTypes item.system.type) as | type |}} + {{#with (lookup adversaryTypes item.system.type) as | type |}}
{{localize type.label}}
{{/with}}