From 7802d18a4d69ec8f85f166f24b84079cac793bcc Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:01:59 +0200 Subject: [PATCH 01/13] 144 - Update System Settings (#145) * Updated SystemSettings to V2 and organized some * Corrected distance measuring labels * Raised system.json foundry version --- daggerheart.mjs | 2 +- lang/en.json | 51 ++- module/applications/resources.mjs | 4 +- module/applications/settings.mjs | 423 +++++------------- module/applications/settings/_module.mjs | 13 + .../settings/appearanceSettings.mjs | 19 +- .../settings/automationSettings.mjs | 59 +++ .../settings/homebrewSettings.mjs | 60 +++ .../settings/rangeMeasurementSettings.mjs | 59 +++ module/applications/sheets/character.mjs | 22 +- module/config/generalConfig.mjs | 6 + module/config/settingsConfig.mjs | 24 +- module/data/settings/Appearance.mjs | 10 +- module/data/settings/Automation.mjs | 11 + module/data/settings/Homebrew.mjs | 19 + module/data/settings/RangeMeasurement.mjs | 17 + module/data/settings/VariantRules.mjs | 20 +- module/data/settings/_module.mjs | 7 + module/ui/ruler.mjs | 12 +- module/ui/tokenRuler.mjs | 12 +- styles/daggerheart.css | 25 ++ styles/daggerheart.less | 1 + styles/settings.less | 31 ++ system.json | 2 +- templates/settings/appearance-settings.hbs | 2 + templates/settings/automation-settings.hbs | 26 ++ templates/settings/homebrew-settings.hbs | 25 ++ .../settings/range-measurement-settings.hbs | 22 + templates/settings/variant-rules.hbs | 4 +- templates/views/automation-settings.hbs | 19 - templates/views/homebrew-settings.hbs | 13 - templates/views/range-settings.hbs | 44 -- 32 files changed, 594 insertions(+), 470 deletions(-) create mode 100644 module/applications/settings/_module.mjs create mode 100644 module/applications/settings/automationSettings.mjs create mode 100644 module/applications/settings/homebrewSettings.mjs create mode 100644 module/applications/settings/rangeMeasurementSettings.mjs create mode 100644 module/data/settings/Automation.mjs create mode 100644 module/data/settings/Homebrew.mjs create mode 100644 module/data/settings/RangeMeasurement.mjs create mode 100644 module/data/settings/_module.mjs create mode 100644 styles/settings.less create mode 100644 templates/settings/automation-settings.hbs create mode 100644 templates/settings/homebrew-settings.hbs create mode 100644 templates/settings/range-measurement-settings.hbs delete mode 100644 templates/views/automation-settings.hbs delete mode 100644 templates/views/homebrew-settings.hbs delete mode 100644 templates/views/range-settings.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 96b407d3..6ef550ce 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -106,7 +106,7 @@ Hooks.once('init', () => { Hooks.on('ready', () => { ui.resources = new CONFIG.ui.resources(); - if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear) !== 'hide') + if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ui.resources.render({ force: true }); document.body.classList.toggle( 'theme-colorful', diff --git a/lang/en.json b/lang/en.json index 71dbd08a..1417e905 100755 --- a/lang/en.json +++ b/lang/en.json @@ -24,10 +24,7 @@ "Automation": { "Name": "Automation Settings", "Label": "Configure Automation", - "Hint": "Various settings automating resource management and more", - "HopeLabel": "Hope", - "FearLabel": "Fear", - "ActionPointsLabel": "Action Points" + "Hint": "Various settings automating resource management and more" }, "Homebrew": { "Name": "Homebrew Settings", @@ -38,13 +35,7 @@ "Range": { "Name": "Range Settings", "Label": "Configure Range Handling", - "Hint": "System ruler setup for displaying ranges in Daggerheart", - "EnabledLabel": "Enabled", - "MeleeLabel": "Melee", - "VeryCloseLabel": "Very Close", - "CloseLabel": "Close", - "FarLabel": "Far", - "VeryFarLabel": "Very Far" + "Hint": "System ruler setup for displaying ranges in Daggerheart" }, "Appearance": { "title": "Appearance Settings", @@ -69,18 +60,36 @@ "actionTokens": "Action Tokens" } }, - "Automation": { - "Hope": { - "Name": "Hope", - "Hint": "Automatically increase a character's hope on a hope duality roll result." + "Appearance": { + "FIELDS": { + "displayFear": { "label": "Fear Display" } }, + "FearDisplay": { + "Token": "Tokens", + "Bar": "Bar", + "Hide": "Hide" + } + }, + "Automation": { "Fear": { "Name": "Fear", "Hint": "Automatically increase the GM's fear pool on a fear duality roll result." }, - "ActionPoints": { - "Name": "Action Points", - "Hint": "Automatically give and take Action Points as combatants take their turns." + "FIELDS": { + "hope": { + "label": "Hope", + "hint": "Automatically increase a character's hope on a hope duality roll result." + }, + "actionPoints": { + "label": "Action Points", + "hint": "Automatically give and take Action Points as combatants take their turns." + } + } + }, + "Homebrew": { + "FIELDS": { + "maxFear": { "label": "Max Fear" }, + "traitArray": { "label": "Initial Trait Modifiers" } } }, "Resources": { @@ -113,6 +122,10 @@ "hint": "Give each player action tokens to use in combat" }, "FIELDS": { + "actionTokens": { + "enabled": { "label": "Enabled" }, + "tokens": { "label": "Tokens" } + }, "useCoins": { "label": "Use Coins", "hint": "test" @@ -163,7 +176,9 @@ "OK": "OK", "Cancel": "Cancel", "Or": "Or", + "Enabled": "Enabled", "Description": "Description", + "Modifier": "Modifier", "Features": "Features", "proficiency": "Proficiency", "unarmored": "Unarmored", diff --git a/module/applications/resources.mjs b/module/applications/resources.mjs index bbd47fc5..3b8c6ce6 100644 --- a/module/applications/resources.mjs +++ b/module/applications/resources.mjs @@ -50,7 +50,7 @@ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) } get maxFear() { - return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); + return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).maxFear; } /* -------------------------------------------- */ @@ -59,7 +59,7 @@ export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) /** @override */ async _prepareContext(_options) { - const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), + const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear, current = this.currentFear, max = this.maxFear, percent = (current / max) * 100, diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index 8f5b3446..cb1aa1bd 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,178 +1,121 @@ -import { DualityRollColor } from '../config/settingsConfig.mjs'; import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; -import DhAppearance from '../data/settings/Appearance.mjs'; -import DHAppearanceSettings from './settings/appearanceSettings.mjs'; -import DhVariantRules from '../data/settings/VariantRules.mjs'; -import DHVariantRuleSettings from './settings/variantRuleSettings.mjs'; - -class DhpAutomationSettings extends FormApplication { - constructor(object = {}, options = {}) { - super(object, options); - } - - static get defaultOptions() { - const defaults = super.defaultOptions; - const overrides = { - height: 'auto', - width: 400, - id: 'daggerheart-automation-settings', - template: 'systems/daggerheart/templates/views/automation-settings.hbs', - closeOnSubmit: true, - submitOnChange: false, - classes: ['daggerheart', 'views', 'settings'] - }; - - const mergedOptions = foundry.utils.mergeObject(defaults, overrides); - - return mergedOptions; - } - - async getData() { - const context = super.getData(); - context.settings = SYSTEM.SETTINGS.gameSettings.Automation; - context.hope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); - context.actionPoints = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints); - - return context; - } - - activateListeners(html) { - super.activateListeners(html); - } - - async _updateObject(_, formData) { - const data = foundry.utils.expandObject(formData); - const updateSettingsKeys = Object.keys(data); - for (var i = 0; i < updateSettingsKeys.length; i++) { - await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]); - } - } -} - -class DhpHomebrewSettings extends FormApplication { - constructor(object = {}, options = {}) { - super(object, options); - } - - static get defaultOptions() { - const defaults = super.defaultOptions; - const overrides = { - height: 'auto', - width: 400, - id: 'daggerheart-homebrew-settings', - template: 'systems/daggerheart/templates/views/homebrew-settings.hbs', - closeOnSubmit: true, - submitOnChange: false, - classes: ['daggerheart', 'views', 'settings'] - }; - - const mergedOptions = foundry.utils.mergeObject(defaults, overrides); - - return mergedOptions; - } - - async getData() { - const context = super.getData(); - context.settings = SYSTEM.SETTINGS.gameSettings.General; - context.abilityArray = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray); - - return context; - } - - activateListeners(html) { - super.activateListeners(html); - } - - async _updateObject(_, formData) { - const data = foundry.utils.expandObject(formData); - const updateSettingsKeys = Object.keys(data); - for (var i = 0; i < updateSettingsKeys.length; i++) { - await game.settings.set(SYSTEM.id, updateSettingsKeys[i], data[updateSettingsKeys[i]]); - } - } -} - -class DhpRangeSettings extends FormApplication { - constructor(object = {}, options = {}) { - super(object, options); - - this.range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement); - } - - static get defaultOptions() { - const defaults = super.defaultOptions; - const overrides = { - height: 'auto', - width: 400, - id: 'daggerheart-range-settings', - template: 'systems/daggerheart/templates/views/range-settings.hbs', - closeOnSubmit: false, - submitOnChange: true, - classes: ['daggerheart', 'views', 'settings'] - }; - - const mergedOptions = foundry.utils.mergeObject(defaults, overrides); - - return mergedOptions; - } - - async getData() { - const context = super.getData(); - context.settings = SYSTEM.SETTINGS.gameSettings.General; - context.range = this.range; - context.disabled = - context.range.enabled && - [ - context.range.melee, - context.range.veryClose, - context.range.close, - context.range.far, - context.range.veryFar - ].some(x => x === null || x === false); - - return context; - } - - activateListeners(html) { - super.activateListeners(html); - - html.find('.range-reset').click(this.reset.bind(this)); - html.find('.save').click(this.save.bind(this)); - html.find('.close').click(this.close.bind(this)); - } - - async _updateObject(_, formData) { - const data = foundry.utils.expandObject(formData, { disabled: true }); - this.range = foundry.utils.mergeObject(this.range, data); - this.render(true); - } - - reset() { - this.range = { - enabled: false, - melee: 5, - veryClose: 15, - close: 30, - far: 60, - veryFar: 120 - }; - this.render(true); - } - - async save() { - await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, this.range); - this.close(); - } -} +import { + DhAppearance, + DhAutomation, + DhHomebrew, + DhRangeMeasurement, + DhVariantRules +} from '../data/settings/_module.mjs'; +import { + DhAppearanceSettings, + DhAutomationSettings, + DhHomebrewSettings, + DhRangeMeasurementSettings, + DhVariantRuleSettings +} from './settings/_module.mjs'; export const registerDHSettings = () => { - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray, { - name: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.General.AbilityArray.Hint'), + registerMenuSettings(); + registerMenus(); + registerNonConfigSettings(); +}; + +const registerMenuSettings = () => { + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, { scope: 'world', config: false, - type: String, - default: '[2,1,1,0,0,-1]' + type: DhVariantRules + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation, { + scope: 'world', + config: false, + type: DhAutomation + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew, { + scope: 'world', + config: false, + type: DhHomebrew, + onChange: value => { + if (value.maxFear) { + if (ui.resources) ui.resources.render({ force: true }); + } + } + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { + scope: 'client', + config: false, + type: DhAppearance, + onChange: value => { + if (value.displayFear) { + if (ui.resources) { + if (value.displayFear === 'hide') ui.resources.close({ allowed: true }); + else ui.resources.render({ force: true }); + } + } + } + }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement, { + scope: 'client', + config: false, + type: DhRangeMeasurement + }); +}; + +const registerMenus = () => { + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Hint'), + icon: SYSTEM.SETTINGS.menu.Automation.Icon, + type: DhAutomationSettings, + restricted: true + }); + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Homebrew.Name, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Hint'), + icon: SYSTEM.SETTINGS.menu.Homebrew.Icon, + type: DhHomebrewSettings, + restricted: true + }); + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Range.Name, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Name'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Hint'), + icon: SYSTEM.SETTINGS.menu.Range.Icon, + type: DhRangeMeasurementSettings, + restricted: true + }); + + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.title'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.hint'), + icon: 'fa-solid fa-palette', + type: DhAppearanceSettings, + restricted: false + }); + + game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, { + name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'), + label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'), + hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'), + icon: SYSTEM.SETTINGS.menu.VariantRules.Icon, + type: DhVariantRuleSettings, + restricted: false + }); +}; + +const registerNonConfigSettings = () => { + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, { + scope: 'world', + config: false, + type: DhLevelTiers, + default: defaultLevelTiers }); game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, { @@ -187,144 +130,4 @@ export const registerDHSettings = () => { ui.combat.render({ force: true }); } }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear, { - name: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.Resources.MaxFear.Hint'), - scope: 'world', - config: true, - type: Number, - default: 12, - onChange: () => { - if (ui.resources) ui.resources.render({ force: true }); - } - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear, { - name: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.Resources.DisplayFear.Hint'), - scope: 'client', - config: true, - type: String, - choices: { - token: 'Tokens', - bar: 'Bar', - hide: 'Hide' - }, - default: 'token', - onChange: value => { - if (ui.resources) { - if (value === 'hide') ui.resources.close({ allowed: true }); - else ui.resources.render({ force: true }); - } - } - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope, { - name: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.Automation.Hope.Hint'), - scope: 'world', - config: false, - type: Boolean, - default: false - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints, { - name: game.i18n.localize('DAGGERHEART.Settings.Automation.ActionPoints.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.Automation.ActionPoints.Hint'), - scope: 'world', - config: false, - type: Boolean, - default: true - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement, { - name: game.i18n.localize('DAGGERHEART.Settings.General.RangeMeasurement.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.General.RangeMeasurement.Hint'), - scope: 'world', - config: false, - type: Object, - default: { - enabled: true, - melee: 5, - veryClose: 15, - close: 30, - far: 60, - veryFar: 120 - } - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, { - scope: 'world', - config: false, - type: DhVariantRules, - default: DhVariantRules.defaultSchema - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { - scope: 'client', - config: false, - type: DhAppearance, - default: DhAppearance.defaultSchema - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.DualityRollColor, { - name: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Name'), - hint: game.i18n.localize('DAGGERHEART.Settings.DualityRollColor.Hint'), - scope: 'world', - config: false, - type: Number, - choices: Object.values(DualityRollColor), - default: DualityRollColor.colorful.value - }); - - game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers, { - scope: 'world', - config: false, - type: DhLevelTiers, - default: defaultLevelTiers - }); - - game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Automation.Name, { - name: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'), - label: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Label'), - hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Hint'), - icon: SYSTEM.SETTINGS.menu.Automation.Icon, - type: DhpAutomationSettings, - restricted: true - }); - game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Homebrew.Name, { - name: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name'), - label: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Label'), - hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Hint'), - icon: SYSTEM.SETTINGS.menu.Homebrew.Icon, - type: DhpHomebrewSettings, - restricted: true - }); - game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.Range.Name, { - name: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Name'), - label: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Label'), - hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Range.Hint'), - icon: SYSTEM.SETTINGS.menu.Range.Icon, - type: DhpRangeSettings, - restricted: true - }); - - game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { - name: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.title'), - label: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.label'), - hint: game.i18n.localize('DAGGERHEART.Settings.Menu.Appearance.hint'), - icon: 'fa-solid fa-palette', - type: DHAppearanceSettings, - restricted: false - }); - - game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, { - name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'), - label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'), - hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'), - icon: SYSTEM.SETTINGS.menu.VariantRules.Icon, - type: DHVariantRuleSettings, - restricted: false - }); }; diff --git a/module/applications/settings/_module.mjs b/module/applications/settings/_module.mjs new file mode 100644 index 00000000..b5b5748d --- /dev/null +++ b/module/applications/settings/_module.mjs @@ -0,0 +1,13 @@ +import DhAppearanceSettings from './appearanceSettings.mjs'; +import DhAutomationSettings from './automationSettings.mjs'; +import DhHomebrewSettings from './homebrewSettings.mjs'; +import DhRangeMeasurementSettings from './rangeMeasurementSettings.mjs'; +import DhVariantRuleSettings from './variantRuleSettings.mjs'; + +export { + DhAppearanceSettings, + DhAutomationSettings, + DhHomebrewSettings, + DhRangeMeasurementSettings, + DhVariantRuleSettings +}; diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index 5797836b..c11efd0f 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -54,20 +54,11 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App static async save() { await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, this.settings.toObject()); - /* const reload = await foundry.applications.api.DialogV2.confirm({ - id: 'reload-world-confirm', - modal: true, - rejectClose: false, - window: { title: 'SETTINGS.ReloadPromptTitle' }, - position: { width: 400 }, - content: `

${game.i18n.localize('SETTINGS.ReloadPromptBody')}

` - }); - - if (reload) { - await game.socket.emit('reload'); - foundry.utils.debouncedReload(); - } */ - document.body.classList.toggle('theme-colorful', game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === DualityRollColor.colorful.value); + document.body.classList.toggle( + 'theme-colorful', + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === + DualityRollColor.colorful.value + ); this.close(); } diff --git a/module/applications/settings/automationSettings.mjs b/module/applications/settings/automationSettings.mjs new file mode 100644 index 00000000..6de21efd --- /dev/null +++ b/module/applications/settings/automationSettings.mjs @@ -0,0 +1,59 @@ +import { DhAutomation } from '../../data/settings/_module.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DhAutomationSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super({}); + + this.settings = new DhAutomation( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).toObject() + ); + } + + get title() { + return game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'daggerheart-automation-settings', + classes: ['daggerheart', 'setting', 'dh-style'], + position: { width: '600', height: 'auto' }, + actions: { + reset: this.reset, + save: this.save + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + main: { + template: 'systems/daggerheart/templates/settings/automation-settings.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.settingFields = this.settings; + + return context; + } + + static async updateData(event, element, formData) { + const updatedSettings = foundry.utils.expandObject(formData.object); + + await this.settings.updateSource(updatedSettings); + this.render(); + } + + static async reset() { + this.settings = new DhAutomation(); + this.render(); + } + + static async save() { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation, this.settings.toObject()); + this.close(); + } +} diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs new file mode 100644 index 00000000..e1b60726 --- /dev/null +++ b/module/applications/settings/homebrewSettings.mjs @@ -0,0 +1,60 @@ +import { DhHomebrew } from '../../data/settings/_module.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DhAutomationSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super({}); + + this.settings = new DhHomebrew(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).toObject()); + } + + get title() { + return game.i18n.localize('DAGGERHEART.Settings.Menu.Homebrew.Name'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'daggerheart-homebrew-settings', + classes: ['daggerheart', 'setting', 'dh-style'], + position: { width: '600', height: 'auto' }, + actions: { + reset: this.reset, + save: this.save + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + main: { + template: 'systems/daggerheart/templates/settings/homebrew-settings.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.settingFields = this.settings; + + return context; + } + + static async updateData(event, element, formData) { + const updatedSettings = foundry.utils.expandObject(formData.object); + + await this.settings.updateSource({ + ...updatedSettings, + traitArray: Object.values(updatedSettings.traitArray) + }); + this.render(); + } + + static async reset() { + this.settings = new DhHomebrew(); + this.render(); + } + + static async save() { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew, this.settings.toObject()); + this.close(); + } +} diff --git a/module/applications/settings/rangeMeasurementSettings.mjs b/module/applications/settings/rangeMeasurementSettings.mjs new file mode 100644 index 00000000..8e90b462 --- /dev/null +++ b/module/applications/settings/rangeMeasurementSettings.mjs @@ -0,0 +1,59 @@ +import { DhRangeMeasurement } from '../../data/settings/_module.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DhRangeMeasurementSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super({}); + + this.settings = new DhRangeMeasurement( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement).toObject() + ); + } + + get title() { + return game.i18n.localize('DAGGERHEART.Settings.Menu.Automation.Name'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + id: 'daggerheart-automation-settings', + classes: ['daggerheart', 'setting', 'dh-style'], + position: { width: '600', height: 'auto' }, + actions: { + reset: this.reset, + save: this.save + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + main: { + template: 'systems/daggerheart/templates/settings/range-measurement-settings.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.settingFields = this.settings; + + return context; + } + + static async updateData(event, element, formData) { + const updatedSettings = foundry.utils.expandObject(formData.object); + + await this.settings.updateSource(updatedSettings); + this.render(); + } + + static async reset() { + this.settings = new DhRangeMeasurement(); + this.render(); + } + + static async save() { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement, this.settings.toObject()); + this.close(); + } +} diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index bea918e6..fe0beec9 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -165,18 +165,18 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { context.config = SYSTEM; const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); - context.abilityScoreArray = JSON.parse( - await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) - ).reduce((acc, x) => { - const selectedIndex = selectedAttributes.indexOf(x); - if (selectedIndex !== -1) { - selectedAttributes.splice(selectedIndex, 1); - } else { - acc.push({ name: x, value: x }); - } + context.abilityScoreArray = await game.settings + .get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew) + .traitArray.reduce((acc, x) => { + const selectedIndex = selectedAttributes.indexOf(x); + if (selectedIndex !== -1) { + selectedAttributes.splice(selectedIndex, 1); + } else { + acc.push({ name: x, value: x }); + } - return acc; - }, []); + return acc; + }, []); if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 978e32cb..76fd12a0 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -337,3 +337,9 @@ export const rollTypes = { label: 'DAGGERHEART.RollTypes.ability.name' } }; + +export const fearDisplay = { + token: { value: 'token', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Token' }, + bar: { value: 'bar', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Bar' }, + hide: { value: 'hide', label: 'DAGGERHEART.Settings.Appearance.FearDisplay.Hide' } +}; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 3a33e61b..be793199 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -18,23 +18,15 @@ export const menu = { }; export const gameSettings = { - Automation: { - Hope: 'AutomationHope', - ActionPoints: 'AutomationActionPoints' - }, - Resources: { - Fear: 'ResourcesFear', - MaxFear: 'ResourcesMaxFear', - DisplayFear: 'DisplayFear' - }, - General: { - AbilityArray: 'AbilityArray', - RangeMeasurement: 'RangeMeasurement' - }, - DualityRollColor: 'DualityRollColor', - LevelTiers: 'LevelTiers', + Automation: 'Automation', + Homebrew: 'Homebrew', + RangeMeasurement: 'RangeMeasurement', appearance: 'Appearance', - variantRules: 'VariantRules' + variantRules: 'VariantRules', + Resources: { + Fear: 'ResourcesFear' + }, + LevelTiers: 'LevelTiers' }; export const DualityRollColor = { diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index fdbd925c..6382b613 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -1,7 +1,15 @@ +import { fearDisplay } from '../../config/generalConfig.mjs'; + export default class DhAppearance extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { + displayFear: new fields.StringField({ + required: true, + choices: fearDisplay, + initial: fearDisplay.token.value, + label: 'DAGGERHEART.Settings.Appearance.FIELDS.displayFear.label' + }), dualityColorScheme: new fields.StringField({ required: true, choices: DualityRollColor, @@ -35,8 +43,6 @@ export default class DhAppearance extends foundry.abstract.DataModel { }) }; } - - static defaultSchema = {}; } export const DualityRollColor = { diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs new file mode 100644 index 00000000..85ed8bd4 --- /dev/null +++ b/module/data/settings/Automation.mjs @@ -0,0 +1,11 @@ +export default class DhAutomation extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.Automation']; // Doesn't work for some reason + + static defineSchema() { + const fields = foundry.data.fields; + return { + hope: new fields.BooleanField({ required: true, initial: false }), + actionPoints: new fields.BooleanField({ required: true, initial: false }) + }; + } +} diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs new file mode 100644 index 00000000..92999c1a --- /dev/null +++ b/module/data/settings/Homebrew.mjs @@ -0,0 +1,19 @@ +export default class DhHomebrew extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.Homebrew']; // Doesn't work for some reason + + static defineSchema() { + const fields = foundry.data.fields; + return { + maxFear: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 12, + label: 'DAGGERHEART.Settings.Homebrew.FIELDS.maxFear.label' + }), + traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), { + initial: () => [2, 1, 1, 0, 0, -1] + }) + }; + } +} diff --git a/module/data/settings/RangeMeasurement.mjs b/module/data/settings/RangeMeasurement.mjs new file mode 100644 index 00000000..5d3e1125 --- /dev/null +++ b/module/data/settings/RangeMeasurement.mjs @@ -0,0 +1,17 @@ +export default class DhRangeMeasurement extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + enabled: new fields.BooleanField({ required: true, initial: false, label: 'DAGGERHEART.General.Enabled' }), + melee: new fields.NumberField({ required: true, initial: 5, label: 'DAGGERHEART.Range.melee.name' }), + veryClose: new fields.NumberField({ + required: true, + initial: 15, + label: 'DAGGERHEART.Range.veryClose.name' + }), + close: new fields.NumberField({ required: true, initial: 30, label: 'DAGGERHEART.Range.close.name' }), + far: new fields.NumberField({ required: true, initial: 60, label: 'DAGGERHEART.Range.far.name' }), + veryFar: new fields.NumberField({ required: true, initial: 120, label: 'DAGGERHEART.Range.veryFar.name' }) + }; + } +} diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs index 7d28a1d7..25e012bc 100644 --- a/module/data/settings/VariantRules.mjs +++ b/module/data/settings/VariantRules.mjs @@ -5,12 +5,22 @@ export default class DhVariantRules extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { actionTokens: new fields.SchemaField({ - enabled: new fields.BooleanField({ required: true, initial: false }), - tokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) + enabled: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.Settings.VariantRules.FIELDS.actionTokens.enabled.label' + }), + tokens: new fields.NumberField({ + required: true, + integer: true, + initial: 3, + label: 'DAGGERHEART.Settings.VariantRules.FIELDS.actionTokens.tokens.label' + }) }), - useCoins: new fields.BooleanField({ initial: false }) + useCoins: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.Settings.VariantRules.FIELDS.useCoins.label' + }) }; } - - static defaultSchema = {}; } diff --git a/module/data/settings/_module.mjs b/module/data/settings/_module.mjs new file mode 100644 index 00000000..dc99ed36 --- /dev/null +++ b/module/data/settings/_module.mjs @@ -0,0 +1,7 @@ +import DhAppearance from './Appearance.mjs'; +import DhAutomation from './Automation.mjs'; +import DhHomebrew from './Homebrew.mjs'; +import DhRangeMeasurement from './RangeMeasurement.mjs'; +import DhVariantRules from './VariantRules.mjs'; + +export { DhAppearance, DhAutomation, DhHomebrew, DhRangeMeasurement, DhVariantRules }; diff --git a/module/ui/ruler.mjs b/module/ui/ruler.mjs index 2c6848d2..2041f7be 100644 --- a/module/ui/ruler.mjs +++ b/module/ui/ruler.mjs @@ -3,7 +3,7 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler { const context = super._getWaypointLabelContext(waypoint, state); if (!context) return; - const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement); + const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement); if (range.enabled) { const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range); @@ -16,19 +16,19 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler { #getRangeLabel(distance, settings) { if (distance <= settings.melee) { - return game.i18n.localize('DAGGERHEART.Range.Melee.Name'); + return game.i18n.localize('DAGGERHEART.Range.melee.name'); } if (distance <= settings.veryClose) { - return game.i18n.localize('DAGGERHEART.Range.VeryClose.Name'); + return game.i18n.localize('DAGGERHEART.Range.veryClose.name'); } if (distance <= settings.close) { - return game.i18n.localize('DAGGERHEART.Range.Close.Name'); + return game.i18n.localize('DAGGERHEART.Range.close.name'); } if (distance <= settings.far) { - return game.i18n.localize('DAGGERHEART.Range.Far.Name'); + return game.i18n.localize('DAGGERHEART.Range.far.name'); } if (distance <= settings.veryFar) { - return game.i18n.localize('DAGGERHEART.Range.VeryFar.Name'); + return game.i18n.localize('DAGGERHEART.Range.veryFar.name'); } } } diff --git a/module/ui/tokenRuler.mjs b/module/ui/tokenRuler.mjs index 4faa516e..aed8859c 100644 --- a/module/ui/tokenRuler.mjs +++ b/module/ui/tokenRuler.mjs @@ -3,7 +3,7 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke const context = super._getWaypointLabelContext(waypoint, state); if (!context) return; - const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.RangeMeasurement); + const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement); if (range.enabled) { const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range); @@ -16,19 +16,19 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke #getRangeLabel(distance, settings) { if (distance <= settings.melee) { - return game.i18n.localize('DAGGERHEART.Range.Melee.Name'); + return game.i18n.localize('DAGGERHEART.Range.melee.name'); } if (distance <= settings.veryClose) { - return game.i18n.localize('DAGGERHEART.Range.VeryClose.Name'); + return game.i18n.localize('DAGGERHEART.Range.veryClose.name'); } if (distance <= settings.close) { - return game.i18n.localize('DAGGERHEART.Range.Close.Name'); + return game.i18n.localize('DAGGERHEART.Range.close.name'); } if (distance <= settings.far) { - return game.i18n.localize('DAGGERHEART.Range.Far.Name'); + return game.i18n.localize('DAGGERHEART.Range.far.name'); } if (distance <= settings.veryFar) { - return game.i18n.localize('DAGGERHEART.Range.VeryFar.Name'); + return game.i18n.localize('DAGGERHEART.Range.veryFar.name'); } } } diff --git a/styles/daggerheart.css b/styles/daggerheart.css index e5d27cb5..fee3a88b 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2816,6 +2816,31 @@ div.daggerheart.views.multiclass { #resources:has(.fear-bar) { min-width: 200px; } +.daggerheart.dh-style.setting .settings-col { + display: flex; + flex-direction: column; + gap: 4px; +} +.daggerheart.dh-style.setting .trait-array-container { + display: flex; + justify-content: space-evenly; + gap: 8px; + margin-bottom: 16px; +} +.daggerheart.dh-style.setting .trait-array-container .trait-array-item { + position: relative; + display: flex; + justify-content: center; +} +.daggerheart.dh-style.setting .trait-array-container .trait-array-item label { + position: absolute; + top: -7px; + font-size: 12px; + font-variant: petite-caps; +} +.daggerheart.dh-style.setting .trait-array-container .trait-array-item input { + text-align: center; +} .application.sheet.daggerheart.actor.dh-style.adversary .window-content { overflow: auto; } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 0b0f3926..9965015b 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -11,6 +11,7 @@ @import './levelup.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import './resources.less'; +@import './settings.less'; // new styles imports @import './less/actors/character.less'; diff --git a/styles/settings.less b/styles/settings.less new file mode 100644 index 00000000..e37f9891 --- /dev/null +++ b/styles/settings.less @@ -0,0 +1,31 @@ +.daggerheart.dh-style.setting { + .settings-col { + display: flex; + flex-direction: column; + gap: 4px; + } + + .trait-array-container { + display: flex; + justify-content: space-evenly; + gap: 8px; + margin-bottom: 16px; + + .trait-array-item { + position: relative; + display: flex; + justify-content: center; + + label { + position: absolute; + top: -7px; + font-size: 12px; + font-variant: petite-caps; + } + + input { + text-align: center; + } + } + } +} diff --git a/system.json b/system.json index 35f69cde..c0af75d1 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "0.0.1", "compatibility": { "minimum": "13", - "verified": "13.344", + "verified": "13.345", "maximum": "13" }, "authors": [ diff --git a/templates/settings/appearance-settings.hbs b/templates/settings/appearance-settings.hbs index e0a16235..6464f9c9 100644 --- a/templates/settings/appearance-settings.hbs +++ b/templates/settings/appearance-settings.hbs @@ -1,4 +1,6 @@
+ {{formGroup settingFields.schema.fields.displayFear value=settingFields._source.displayFear localize=true}} +
{{localize "DAGGERHEART.Settings.Menu.Appearance.duality"}} diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings.hbs new file mode 100644 index 00000000..bd50c548 --- /dev/null +++ b/templates/settings/automation-settings.hbs @@ -0,0 +1,26 @@ +
+
+ +
+ {{formInput settingFields.schema.fields.hope value=settingFields._source.hope }} +
+
+
+ +
+ {{formInput settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints }} +
+
+ +
+ + +
+
+ \ No newline at end of file diff --git a/templates/settings/homebrew-settings.hbs b/templates/settings/homebrew-settings.hbs new file mode 100644 index 00000000..aa866378 --- /dev/null +++ b/templates/settings/homebrew-settings.hbs @@ -0,0 +1,25 @@ +
+ {{formGroup settingFields.schema.fields.maxFear value=settingFields._source.maxFear localize=true}} + +

{{localize "DAGGERHEART.Settings.Homebrew.FIELDS.traitArray.label"}}

+
+ {{#each settingFields._source.traitArray as |trait index|}} +
+ + +
+ {{/each}} +
+ +
+ + +
+
+ \ No newline at end of file diff --git a/templates/settings/range-measurement-settings.hbs b/templates/settings/range-measurement-settings.hbs new file mode 100644 index 00000000..36357d57 --- /dev/null +++ b/templates/settings/range-measurement-settings.hbs @@ -0,0 +1,22 @@ +
+
+ {{formGroup settingFields.schema.fields.enabled value=settingFields._source.enabled localize=true}} + {{formGroup settingFields.schema.fields.melee value=settingFields._source.melee localize=true}} + {{formGroup settingFields.schema.fields.veryClose value=settingFields._source.veryClose localize=true}} + {{formGroup settingFields.schema.fields.close value=settingFields._source.close localize=true}} + {{formGroup settingFields.schema.fields.far value=settingFields._source.far localize=true}} + {{formGroup settingFields.schema.fields.veryFar value=settingFields._source.veryFar localize=true}} +
+ +
+ + +
+
+ \ No newline at end of file diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs index 2c4d7d30..f4fa3fdf 100644 --- a/templates/settings/variant-rules.hbs +++ b/templates/settings/variant-rules.hbs @@ -3,8 +3,8 @@
- {{formInput settingFields.schema.fields.actionTokens.fields.enabled value=settingFields._source.actionTokens.enabled}} - {{formInput settingFields.schema.fields.actionTokens.fields.tokens value=settingFields._source.actionTokens.tokens disabled=(not settingFields._source.actionTokens.enabled)}} + {{formGroup settingFields.schema.fields.actionTokens.fields.enabled value=settingFields._source.actionTokens.enabled localize=true}} + {{formGroup settingFields.schema.fields.actionTokens.fields.tokens value=settingFields._source.actionTokens.tokens disabled=(not settingFields._source.actionTokens.enabled) localize=true}}
diff --git a/templates/views/automation-settings.hbs b/templates/views/automation-settings.hbs deleted file mode 100644 index de4f1131..00000000 --- a/templates/views/automation-settings.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -
- -
-
-
- -
- -
-
- - -
\ No newline at end of file diff --git a/templates/views/homebrew-settings.hbs b/templates/views/homebrew-settings.hbs deleted file mode 100644 index 6d9c9a90..00000000 --- a/templates/views/homebrew-settings.hbs +++ /dev/null @@ -1,13 +0,0 @@ -
-
- -
- -
-
- - -
\ No newline at end of file diff --git a/templates/views/range-settings.hbs b/templates/views/range-settings.hbs deleted file mode 100644 index fec4f7fb..00000000 --- a/templates/views/range-settings.hbs +++ /dev/null @@ -1,44 +0,0 @@ -
-
- -
- - -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- - -
\ No newline at end of file From 96ed90b5fcb2c81a65cd4f48db430ade09e96671 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:33:34 +0200 Subject: [PATCH 02/13] 146 - Measured Templates (#147) * Added DhMeasuredTemplate * Added TemplateEnricher for Template buttons in text --- daggerheart.mjs | 56 ++++++++++++---------- module/config/generalConfig.mjs | 7 +++ module/enrichers/DualityRollEnricher.mjs | 25 +++++++++- module/enrichers/TemplateEnricher.mjs | 60 ++++++++++++++++++++++++ module/enrichers/_module.mjs | 4 ++ module/placeables/_module.mjs | 3 ++ module/placeables/measuredTemplate.mjs | 35 ++++++++++++++ module/ui/ruler.mjs | 22 ++------- module/ui/tokenRuler.mjs | 22 ++------- 9 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 module/enrichers/TemplateEnricher.mjs create mode 100644 module/enrichers/_module.mjs create mode 100644 module/placeables/_module.mjs create mode 100644 module/placeables/measuredTemplate.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 6ef550ce..c5c685cd 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -9,12 +9,15 @@ import { registerDHSettings } from './module/applications/settings.mjs'; import DhpChatLog from './module/ui/chatLog.mjs'; import DhpRuler from './module/ui/ruler.mjs'; import DhpTokenRuler from './module/ui/tokenRuler.mjs'; -import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; +import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.mjs'; import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs'; +import { DhMeasuredTemplate } from './module/placeables/_module.mjs'; +import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; +import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; globalThis.SYSTEM = SYSTEM; @@ -27,16 +30,26 @@ Hooks.once('init', () => { documents }; - CONFIG.TextEditor.enrichers.push({ - pattern: /\[\[\/dr\s?(.*?)\]\]/g, - enricher: dualityRollEnricher - }); + CONFIG.TextEditor.enrichers.push( + ...[ + { + pattern: /\[\[\/dr\s?(.*?)\]\]/g, + enricher: DhDualityRollEnricher + }, + { + pattern: /^@Template\[(.*)\]$/g, + enricher: DhTemplateEnricher + } + ] + ); CONFIG.statusEffects = Object.values(SYSTEM.GENERAL.conditions).map(x => ({ ...x, name: game.i18n.localize(x.name) })); + CONFIG.MeasuredTemplate.objectClass = DhMeasuredTemplate; + CONFIG.Item.documentClass = documents.DhpItem; //Registering the Item DataModel @@ -141,43 +154,34 @@ Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { } }); -const renderDualityButton = async event => { - const button = event.currentTarget, - traitValue = button.dataset.trait?.toLowerCase(), - target = getCommandTarget(); - if (!target) return; - - const config = { - event: event, - title: button.dataset.title, - roll: { - modifier: traitValue ? target.system.traits[traitValue].value : null, - label: button.dataset.label, - type: button.dataset.actionType ?? null // Need check - }, - chatMessage: { - template: 'systems/daggerheart/templates/chat/duality-roll.hbs' - } - }; - await target.diceRoll(config); -}; - Hooks.on('renderChatMessageHTML', (_, element) => { element .querySelectorAll('.duality-roll-button') .forEach(element => element.addEventListener('click', renderDualityButton)); + + element + .querySelectorAll('.measured-template-button') + .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); }); Hooks.on('renderJournalEntryPageProseMirrorSheet', (_, element) => { element .querySelectorAll('.duality-roll-button') .forEach(element => element.addEventListener('click', renderDualityButton)); + + element + .querySelectorAll('.measured-template-button') + .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); }); Hooks.on('renderHandlebarsApplication', (_, element) => { element .querySelectorAll('.duality-roll-button') .forEach(element => element.addEventListener('click', renderDualityButton)); + + element + .querySelectorAll('.measured-template-button') + .forEach(element => element.addEventListener('click', renderMeasuredTemplate)); }); Hooks.on('chatMessage', (_, message) => { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 76fd12a0..2b0e11b9 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -1,35 +1,42 @@ export const range = { self: { + id: 'self', + short: 's', label: 'DAGGERHEART.Range.self.name', description: 'DAGGERHEART.Range.self.description', distance: 0 }, melee: { id: 'melee', + short: 'm', label: 'DAGGERHEART.Range.melee.name', description: 'DAGGERHEART.Range.melee.description', distance: 1 }, veryClose: { id: 'veryClose', + short: 'vc', label: 'DAGGERHEART.Range.veryClose.name', description: 'DAGGERHEART.Range.veryClose.description', distance: 3 }, close: { id: 'close', + short: 'c', label: 'DAGGERHEART.Range.close.name', description: 'DAGGERHEART.Range.close.description', distance: 10 }, far: { id: 'far', + short: 'f', label: 'DAGGERHEART.Range.far.name', description: 'DAGGERHEART.Range.far.description', distance: 20 }, veryFar: { id: 'veryFar', + short: 'vf', label: 'DAGGERHEART.Range.veryFar.name', description: 'DAGGERHEART.Range.veryFar.description', distance: 30 diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 01fbe1af..61884acc 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -1,7 +1,7 @@ import { abilities } from '../config/actorConfig.mjs'; -import { rollCommandToJSON } from '../helpers/utils.mjs'; +import { getCommandTarget, rollCommandToJSON } from '../helpers/utils.mjs'; -export function dualityRollEnricher(match, _options) { +export default function DhDualityRollEnricher(match, _options) { const roll = rollCommandToJSON(match[1]); if (!roll) return match[0]; @@ -39,3 +39,24 @@ export function getDualityMessage(roll) { return dualityElement; } + +export const renderDualityButton = async event => { + const button = event.currentTarget, + traitValue = button.dataset.trait?.toLowerCase(), + target = getCommandTarget(); + if (!target) return; + + const config = { + event: event, + title: button.dataset.title, + roll: { + modifier: traitValue ? target.system.traits[traitValue].value : null, + label: button.dataset.label, + type: button.dataset.actionType ?? null // Need check + }, + chatMessage: { + template: 'systems/daggerheart/templates/chat/duality-roll.hbs' + } + }; + await target.diceRoll(config); +}; diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs new file mode 100644 index 00000000..50b27068 --- /dev/null +++ b/module/enrichers/TemplateEnricher.mjs @@ -0,0 +1,60 @@ +import { range as configRange } from '../config/generalConfig.mjs'; + +export default function DhTemplateEnricher(match, _options) { + const parts = match[1].split('|').map(x => x.trim()); + + let type = null, + range = null; + + parts.forEach(part => { + const split = part.split(':').map(x => x.toLowerCase().trim()); + if (split.length === 2) { + switch (split[0]) { + case 'type': + const matchedType = Object.values(CONST.MEASURED_TEMPLATE_TYPES).find( + x => x.toLowerCase() === split[1] + ); + type = matchedType; + break; + case 'range': + const matchedRange = Object.values(configRange).find( + x => x.id.toLowerCase() === split[1] || x.short === split[1] + ); + range = matchedRange?.id; + break; + } + } + }); + + if (!type || !range) return match[0]; + + const templateElement = document.createElement('span'); + templateElement.innerHTML = ` + + `; + + return templateElement; +} + +export const renderMeasuredTemplate = async event => { + const button = event.currentTarget, + type = button.dataset.type, + range = button.dataset.range; + + if (!type || !range || !game.canvas.scene) return; + + const distance = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement)[range]; + const { width, height } = game.canvas.scene.dimensions; + canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [ + { + x: width / 2, + y: height / 2, + t: type, + distance: distance, + width: type === CONST.MEASURED_TEMPLATE_TYPES.RAY ? 5 : undefined, + angle: type === CONST.MEASURED_TEMPLATE_TYPES.CONE ? CONFIG.MeasuredTemplate.defaults.angle : undefined + } + ]); +}; diff --git a/module/enrichers/_module.mjs b/module/enrichers/_module.mjs new file mode 100644 index 00000000..4ba1bdb6 --- /dev/null +++ b/module/enrichers/_module.mjs @@ -0,0 +1,4 @@ +import DhDualityRollEnricher from './DualityRollEnricher.mjs'; +import DhTemplateEnricher from './TemplateEnricher.mjs'; + +export { DhDualityRollEnricher, DhTemplateEnricher }; diff --git a/module/placeables/_module.mjs b/module/placeables/_module.mjs new file mode 100644 index 00000000..52e3dffe --- /dev/null +++ b/module/placeables/_module.mjs @@ -0,0 +1,3 @@ +import DhMeasuredTemplate from './measuredTemplate.mjs'; + +export { DhMeasuredTemplate }; diff --git a/module/placeables/measuredTemplate.mjs b/module/placeables/measuredTemplate.mjs new file mode 100644 index 00000000..ee21af6d --- /dev/null +++ b/module/placeables/measuredTemplate.mjs @@ -0,0 +1,35 @@ +export default class DhMeasuredTemplate extends MeasuredTemplate { + _refreshRulerText() { + super._refreshRulerText(); + + const rangeMeasurementSettings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement); + if (rangeMeasurementSettings.enabled) { + 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; + } + } + } + + static getDistanceLabel(distance, settings) { + if (distance <= settings.melee) { + return game.i18n.localize('DAGGERHEART.Range.melee.name'); + } + if (distance <= settings.veryClose) { + return game.i18n.localize('DAGGERHEART.Range.veryClose.name'); + } + if (distance <= settings.close) { + return game.i18n.localize('DAGGERHEART.Range.close.name'); + } + if (distance <= settings.far) { + return game.i18n.localize('DAGGERHEART.Range.far.name'); + } + if (distance <= settings.veryFar) { + return game.i18n.localize('DAGGERHEART.Range.veryFar.name'); + } + + return ''; + } +} diff --git a/module/ui/ruler.mjs b/module/ui/ruler.mjs index 2041f7be..655c9c61 100644 --- a/module/ui/ruler.mjs +++ b/module/ui/ruler.mjs @@ -1,3 +1,5 @@ +import DhMeasuredTemplate from '../placeables/measuredTemplate.mjs'; + export default class DhpRuler extends foundry.canvas.interaction.Ruler { _getWaypointLabelContext(waypoint, state) { const context = super._getWaypointLabelContext(waypoint, state); @@ -6,29 +8,11 @@ export default class DhpRuler extends foundry.canvas.interaction.Ruler { const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement); if (range.enabled) { - const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range); + const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); context.cost = { total: distance, units: null }; context.distance = { total: distance, units: null }; } return context; } - - #getRangeLabel(distance, settings) { - if (distance <= settings.melee) { - return game.i18n.localize('DAGGERHEART.Range.melee.name'); - } - if (distance <= settings.veryClose) { - return game.i18n.localize('DAGGERHEART.Range.veryClose.name'); - } - if (distance <= settings.close) { - return game.i18n.localize('DAGGERHEART.Range.close.name'); - } - if (distance <= settings.far) { - return game.i18n.localize('DAGGERHEART.Range.far.name'); - } - if (distance <= settings.veryFar) { - return game.i18n.localize('DAGGERHEART.Range.veryFar.name'); - } - } } diff --git a/module/ui/tokenRuler.mjs b/module/ui/tokenRuler.mjs index aed8859c..5c5a275e 100644 --- a/module/ui/tokenRuler.mjs +++ b/module/ui/tokenRuler.mjs @@ -1,3 +1,5 @@ +import DhMeasuredTemplate from '../placeables/measuredTemplate.mjs'; + export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.TokenRuler { _getWaypointLabelContext(waypoint, state) { const context = super._getWaypointLabelContext(waypoint, state); @@ -6,29 +8,11 @@ export default class DhpTokenRuler extends foundry.canvas.placeables.tokens.Toke const range = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.RangeMeasurement); if (range.enabled) { - const distance = this.#getRangeLabel(waypoint.measurement.distance.toNearest(0.01), range); + const distance = DhMeasuredTemplate.getDistanceLabel(waypoint.measurement.distance.toNearest(0.01), range); context.cost = { total: distance, units: null }; context.distance = { total: distance, units: null }; } return context; } - - #getRangeLabel(distance, settings) { - if (distance <= settings.melee) { - return game.i18n.localize('DAGGERHEART.Range.melee.name'); - } - if (distance <= settings.veryClose) { - return game.i18n.localize('DAGGERHEART.Range.veryClose.name'); - } - if (distance <= settings.close) { - return game.i18n.localize('DAGGERHEART.Range.close.name'); - } - if (distance <= settings.far) { - return game.i18n.localize('DAGGERHEART.Range.far.name'); - } - if (distance <= settings.veryFar) { - return game.i18n.localize('DAGGERHEART.Range.veryFar.name'); - } - } } From a0a5196825bfe962af805433914bdedf970c12cb Mon Sep 17 00:00:00 2001 From: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:47:50 -0300 Subject: [PATCH 03/13] Bug/124 foreigndocumentuuidfields fail initial initialization (#150) * FIX: Remove the psudo-documents because they will be used. * FIX: ForeignDocumentUUIDField initialize like a getter FEAT: ForeignDocumentUUIDArrayField created and used * REFACTOR: prettier format --------- Co-authored-by: Joaquin Pereyra --- module/applications/_module.mjs | 4 +- .../sheets/pseudo-documents/_module.mjs | 1 - .../pseudo-documents-sheet.mjs | 66 ------ module/config/pseudoConfig.mjs | 17 -- module/config/system.mjs | 2 - module/data/_module.mjs | 3 - module/data/fields/_module.mjs | 2 +- .../fields/foreignDocumentUUIDArrayField.mjs | 20 ++ .../data/fields/foreignDocumentUUIDField.mjs | 4 +- module/data/fields/pseudoDocumentsField.mjs | 56 ----- module/data/item/base.mjs | 2 - module/data/item/class.mjs | 20 +- module/data/item/subclass.mjs | 4 +- module/data/pseudo-documents/_module.mjs | 2 - module/data/pseudo-documents/base/base.mjs | 213 ------------------ .../pseudo-documents/base/pseudoDocument.mjs | 59 ----- .../base/sheetManagementMixin.mjs | 158 ------------- .../data/pseudo-documents/feature/_module.mjs | 2 - .../feature/baseFeatureData.mjs | 24 -- .../feature/weaponFeature.mjs | 6 - 20 files changed, 33 insertions(+), 632 deletions(-) delete mode 100644 module/applications/sheets/pseudo-documents/_module.mjs delete mode 100644 module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs delete mode 100644 module/config/pseudoConfig.mjs create mode 100644 module/data/fields/foreignDocumentUUIDArrayField.mjs delete mode 100644 module/data/fields/pseudoDocumentsField.mjs delete mode 100644 module/data/pseudo-documents/_module.mjs delete mode 100644 module/data/pseudo-documents/base/base.mjs delete mode 100644 module/data/pseudo-documents/base/pseudoDocument.mjs delete mode 100644 module/data/pseudo-documents/base/sheetManagementMixin.mjs delete mode 100644 module/data/pseudo-documents/feature/_module.mjs delete mode 100644 module/data/pseudo-documents/feature/baseFeatureData.mjs delete mode 100644 module/data/pseudo-documents/feature/weaponFeature.mjs diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 5ee13189..65eafe09 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -12,6 +12,4 @@ export { default as DhpWeapon } from './sheets/items/weapon.mjs'; export { default as DhpArmor } from './sheets/items/armor.mjs'; export { default as DhpChatMessage } from './chatMessage.mjs'; export { default as DhpEnvironment } from './sheets/environment.mjs'; -export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; - -export * as pseudoDocumentSheet from './sheets/pseudo-documents/_module.mjs'; +export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; \ No newline at end of file diff --git a/module/applications/sheets/pseudo-documents/_module.mjs b/module/applications/sheets/pseudo-documents/_module.mjs deleted file mode 100644 index 9dc4d356..00000000 --- a/module/applications/sheets/pseudo-documents/_module.mjs +++ /dev/null @@ -1 +0,0 @@ -export {default as PseudoDocumentSheet }from "./pseudo-documents-sheet.mjs"; \ No newline at end of file diff --git a/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs b/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs deleted file mode 100644 index 487da420..00000000 --- a/module/applications/sheets/pseudo-documents/pseudo-documents-sheet.mjs +++ /dev/null @@ -1,66 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class PseudoDocumentSheet extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(options) { - super(options); - this.#pseudoDocument = options.document; - } - - /** - * The UUID of the associated pseudo-document - * @type {string} - */ - get pseudoUuid() { - return this.pseudoDocument.uuid; - } - - #pseudoDocument; - - /** - * The pseudo-document instance this sheet represents - * @type {object} - */ - get pseudoDocument() { - return this.#pseudoDocument; - } - - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet'], - position: { width: 600 }, - form: { - handler: PseudoDocumentSheet.#onSubmitForm, - submitOnChange: true, - closeOnSubmit: false - }, - dragDrop: [{ dragSelector: null, dropSelector: null }], - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/pseudo-documents/header.hbs' }, - }; - - /** @inheritDoc */ - async _prepareContext(options) { - const context = await super._prepareContext(options); - const document = this.pseudoDocument; - return Object.assign(context, { - document, - source: document._source, - editable: this.isEditable, - user: game.user, - rootId: this.id, - }); - } - - /** - * Form submission handler - * @param {SubmitEvent | Event} event - The originating form submission or input change event - * @param {HTMLFormElement} form - The form element that was submitted - * @param {foundry.applications.ux.FormDataExtended} formData - Processed data for the submitted form - */ - static async #onSubmitForm(event, form, formData) { - const submitData = foundry.utils.expandObject(formData.object); - await this.pseudoDocument.update(submitData); - } -} diff --git a/module/config/pseudoConfig.mjs b/module/config/pseudoConfig.mjs deleted file mode 100644 index 297d42cf..00000000 --- a/module/config/pseudoConfig.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import { pseudoDocuments } from "../data/_module.mjs"; -import { pseudoDocumentSheet } from "../applications/_module.mjs"; - -//CONFIG.daggerheart.pseudoDocuments -export default { - sheetClass: pseudoDocumentSheet.PseudoDocumentSheet, - feature: { - label: "DAGGERHEART.Feature.Label", - documentClass: pseudoDocuments.feature.BaseFeatureData, - types: { - weapon: { - label: "DAGGERHEART.Feature.Weapon.Label", - documentClass: pseudoDocuments.feature.WeaponFeature, - } - } - } -}; \ No newline at end of file diff --git a/module/config/system.mjs b/module/config/system.mjs index be2fc1aa..b9755317 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -5,7 +5,6 @@ import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; -import pseudoDocuments from "./pseudoConfig.mjs"; export const SYSTEM_ID = 'daggerheart'; @@ -18,5 +17,4 @@ export const SYSTEM = { SETTINGS, EFFECTS, ACTIONS, - pseudoDocuments }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index e43ddb99..4284bc41 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,5 +1,3 @@ -export { default as DhClass } from './item/class.mjs'; -export { default as DhSubclass } from './item/subclass.mjs'; export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; @@ -8,4 +6,3 @@ export * as items from './item/_module.mjs'; export { actionsTypes } from './action/_module.mjs'; export * as messages from './chat-message/_modules.mjs'; export * as fields from './fields/_module.mjs'; -export * as pseudoDocuments from './pseudo-documents/_module.mjs'; diff --git a/module/data/fields/_module.mjs b/module/data/fields/_module.mjs index 3a573a0b..682ff1c4 100644 --- a/module/data/fields/_module.mjs +++ b/module/data/fields/_module.mjs @@ -1,3 +1,3 @@ export { default as FormulaField } from './formulaField.mjs'; export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs'; -export { default as PseudoDocumentsField } from './pseudoDocumentsField.mjs'; +export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs'; diff --git a/module/data/fields/foreignDocumentUUIDArrayField.mjs b/module/data/fields/foreignDocumentUUIDArrayField.mjs new file mode 100644 index 00000000..1cf120d8 --- /dev/null +++ b/module/data/fields/foreignDocumentUUIDArrayField.mjs @@ -0,0 +1,20 @@ +import ForeignDocumentUUIDField from './foreignDocumentUUIDField.mjs'; +/** + * A subclass of {@link foundry.data.fields.ArrayField} that defines an array of foreign document UUID references. + */ +export default class ForeignDocumentUUIDArrayField extends foundry.data.fields.ArrayField { + /** + * @param {foundry.data.types.DocumentUUIDFieldOptions} [fieldOption] - Options to configure each individual ForeignDocumentUUIDField. + * @param {foundry.data.types.ArrayFieldOptions} [options] - Options to configure the array behavior + * @param {foundry.data.types.DataFieldContext} [context] - Optional context for schema processing + */ + constructor(fieldOption = {}, options = {}, context = {}) { + super(new ForeignDocumentUUIDField(fieldOption), options, context); + } + + /** @inheritdoc */ + initialize(value, model, options = {}) { + const v = super.initialize(value, model, options); + return () => v.map(entry => (typeof entry === 'function' ? entry() : entry)); + } +} diff --git a/module/data/fields/foreignDocumentUUIDField.mjs b/module/data/fields/foreignDocumentUUIDField.mjs index 29cfaff3..0efa60da 100644 --- a/module/data/fields/foreignDocumentUUIDField.mjs +++ b/module/data/fields/foreignDocumentUUIDField.mjs @@ -23,7 +23,7 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume /**@override */ initialize(value, _model, _options = {}) { if (this.idOnly) return value; - return (() => { + return () => { try { const doc = fromUuidSync(value); return doc; @@ -31,7 +31,7 @@ export default class ForeignDocumentUUIDField extends foundry.data.fields.Docume console.error(error); return value ?? null; } - })(); + }; } /**@override */ diff --git a/module/data/fields/pseudoDocumentsField.mjs b/module/data/fields/pseudoDocumentsField.mjs deleted file mode 100644 index 0f48af5b..00000000 --- a/module/data/fields/pseudoDocumentsField.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import PseudoDocument from '../pseudo-documents/base/pseudoDocument.mjs'; - -const { TypedObjectField, TypedSchemaField } = foundry.data.fields; - -/** - * @typedef _PseudoDocumentsFieldOptions - * @property {Number} [max] - The maximum amount of elements (default: `Infinity`) - * @property {String[]} [validTypes] - Allowed pseudo-documents types (default: `[]`) - * @property {Function} [validateKey] - callback for validate keys of the object; - - * @typedef {foundry.data.types.DataFieldOptions & _PseudoDocumentsFieldOptions} PseudoDocumentsFieldOptions - */ -export default class PseudoDocumentsField extends TypedObjectField { - /** - * @param {PseudoDocument} model - The PseudoDocument of each entry in this collection. - * @param {PseudoDocumentsFieldOptions} [options] - Options which configure the behavior of the field - * @param {foundry.data.types.DataFieldContext} [context] - Additional context which describes the field - */ - constructor(model, options = {}, context = {}) { - options.validateKey ||= key => foundry.data.validators.isValidId(key); - if (!foundry.utils.isSubclass(model, PseudoDocument)) throw new Error('The model must be a PseudoDocument'); - - const allTypes = model.TYPES; - - const filteredTypes = options.validTypes - ? Object.fromEntries( - Object.entries(allTypes).filter(([key]) => options.validTypes.includes(key)) - ) - : allTypes; - - const field = new TypedSchemaField(filteredTypes); - super(field, options, context); - } - - /** @inheritdoc */ - static get _defaults() { - return Object.assign(super._defaults, { - max: Infinity, - validTypes: [] - }); - } - - /** @override */ - _validateType(value, options = {}) { - if (Object.keys(value).length > this.max) throw new Error(`cannot have more than ${this.max} elements`); - return super._validateType(value, options); - } - - /** @override */ - initialize(value, model, options = {}) { - if (!value) return; - value = super.initialize(value, model, options); - const collection = new foundry.utils.Collection(Object.values(value).map(d => [d._id, d])); - return collection; - } -} diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 3dd174d7..6f557866 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -5,7 +5,6 @@ * @property {string} type - The system type that this data model represents. * @property {boolean} hasDescription - Indicates whether items of this type have description field * @property {boolean} isQuantifiable - Indicates whether items of this type have quantity field - * @property {Record} embedded - Record of document names of pseudo-documents and the path to the collection */ const fields = foundry.data.fields; @@ -18,7 +17,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { type: "base", hasDescription: false, isQuantifiable: false, - embedded: {}, }; } diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 47acb712..e796dd75 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -1,5 +1,6 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ActionField from '../fields/actionField.mjs'; export default class DHClass extends BaseDataItem { @@ -18,23 +19,16 @@ export default class DHClass extends BaseDataItem { return { ...super.defineSchema(), domains: new fields.ArrayField(new fields.StringField(), { max: 2 }), - classItems: new fields.ArrayField(new ForeignDocumentUUIDField({ type: 'Item' })), + classItems: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + evasion: new fields.NumberField({ initial: 0, integer: true }), hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()), classFeatures: new foundry.data.fields.ArrayField(new ActionField()), - subclasses: new fields.ArrayField( - new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) - ), + subclasses: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), inventory: new fields.SchemaField({ - take: new fields.ArrayField( - new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) - ), - choiceA: new fields.ArrayField( - new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) - ), - choiceB: new fields.ArrayField( - new ForeignDocumentUUIDField({ type: 'Item', required: false, nullable: true, initial: undefined }) - ) + take: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + choiceA: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + choiceB: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), }), characterGuide: new fields.SchemaField({ suggestedTraits: new fields.SchemaField({ diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 1e236ff4..bb473db1 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,11 +1,11 @@ import ActionField from '../fields/actionField.mjs'; -import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; const featureSchema = () => { return new foundry.data.fields.SchemaField({ name: new foundry.data.fields.StringField({ required: true }), - effects: new foundry.data.fields.ArrayField(new ForeignDocumentUUIDField({ type: 'ActiveEffect' })), + effects: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), actions: new foundry.data.fields.ArrayField(new ActionField()) }); }; diff --git a/module/data/pseudo-documents/_module.mjs b/module/data/pseudo-documents/_module.mjs deleted file mode 100644 index 6f50a137..00000000 --- a/module/data/pseudo-documents/_module.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export { default as base } from './base/pseudoDocument.mjs'; -export * as feature from './feature/_module.mjs'; diff --git a/module/data/pseudo-documents/base/base.mjs b/module/data/pseudo-documents/base/base.mjs deleted file mode 100644 index 4d5313ba..00000000 --- a/module/data/pseudo-documents/base/base.mjs +++ /dev/null @@ -1,213 +0,0 @@ -/** - * @typedef {object} PseudoDocumentMetadata - * @property {string} name - The document name of this pseudo-document - * @property {Record} embedded - Record of document names and their collection paths - * @property {typeof foundry.applications.api.ApplicationV2} [sheetClass] - The class used to render this pseudo-document - * @property {string} defaultArtwork - The default image used for newly created documents - */ - -/** - * @class Base class for pseudo-documents - * @extends {foundry.abstract.DataModel} - */ -export default class BasePseudoDocument extends foundry.abstract.DataModel { - /** - * Pseudo-document metadata. - * @returns {PseudoDocumentMetadata} - */ - static get metadata() { - return { - name: '', - embedded: {}, - defaultArtwork: foundry.documents.Item.DEFAULT_ICON, - sheetClass: CONFIG.daggerheart.pseudoDocuments.sheetClass, - }; - } - - /** @override */ - static LOCALIZATION_PREFIXES = ['DOCUMENT']; - - /** @inheritdoc */ - static defineSchema() { - const { fields } = foundry.data; - - return { - _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), - name: new fields.StringField({ required: true, blank: false, textSearch: true }), - img: new fields.FilePathField({ categories: ['IMAGE'], initial: this.metadata.defaultArtwork }), - description: new fields.HTMLField({ textSearch: true }) - }; - } - - /* -------------------------------------------- */ - /* Instance Properties */ - /* -------------------------------------------- */ - - /** - * The id of this pseudo-document. - * @type {string} - */ - get id() { - return this._id; - } - - /* -------------------------------------------- */ - - /** - * The uuid of this document. - * @type {string} - */ - get uuid() { - let parent = this.parent; - while (!(parent instanceof BasePseudoDocument) && !(parent instanceof foundry.abstract.Document)) - parent = parent.parent; - return [parent.uuid, this.constructor.metadata.name, this.id].join('.'); - } - - /* -------------------------------------------- */ - - /** - * The parent document of this pseudo-document. - * @type {foundry.abstract.Document} - */ - get document() { - let parent = this; - while (!(parent instanceof foundry.abstract.Document)) parent = parent.parent; - return parent; - } - - /* -------------------------------------------- */ - - /** - * Item to which this PseudoDocument belongs, if applicable. - * @type {foundry.documents.Item|null} - */ - get item() { - return this.parent?.parent instanceof Item ? this.parent.parent : null; - } - - /* -------------------------------------------- */ - - /** - * Actor to which this PseudoDocument's item belongs, if the item is embedded. - * @type {foundry.documents.Actor|null} - */ - get actor() { - return this.item?.parent ?? null; - } - - /* -------------------------------------------- */ - - /** - * The property path to this pseudo document relative to its parent document. - * @type {string} - */ - get fieldPath() { - const fp = this.schema.fieldPath; - let path = fp.slice(0, fp.lastIndexOf('element') - 1); - - if (this.parent instanceof BasePseudoDocument) { - path = [this.parent.fieldPath, this.parent.id, path].join('.'); - } - - return path; - } - - /* -------------------------------------------- */ - /* Embedded Document Methods */ - /* -------------------------------------------- */ - - /** - * Retrieve an embedded pseudo-document. - * @param {string} embeddedName The document name of the embedded pseudo-document. - * @param {string} id The id of the embedded pseudo-document. - * @param {object} [options] Retrieval options. - * @param {boolean} [options.strinct] Throw an error if the embedded pseudo-document does not exist? - * @returns {PseudoDocument|null} - */ - getEmbeddedDocument(embeddedName, id, { strict = false } = {}) { - const embeds = this.constructor.metadata.embedded ?? {}; - if (embeddedName in embeds) { - return foundry.utils.getProperty(this, embeds[embeddedName]).get(id, { strict }) ?? null; - } - return null; - } - - /* -------------------------------------------- */ - /* CRUD Operations */ - /* -------------------------------------------- */ - - /** - * Does this pseudo-document exist in the document's source? - * @type {boolean} - */ - get isSource() { - const source = foundry.utils.getProperty(this.document._source, this.fieldPath); - if (foundry.utils.getType(source) !== 'Object') { - throw new Error('Source is not an object!'); - } - return this.id in source; - } - - /** - * Create a new instance of this pseudo-document. - * @param {object} [data] The data used for the creation. - * @param {object} operation The context of the update operation. - * @param {foundry.abstract.Document} operation.parent The parent of this document. - * @returns {Promise} A promise that resolves to the updated document. - */ - static async create(data = {}, { parent, ...operation } = {}) { - if (!parent) { - throw new Error('A parent document must be specified for the creation of a pseudo-document!'); - } - const id = - operation.keepId && foundry.data.validators.isValidId(data._id) ? data._id : foundry.utils.randomID(); - - const fieldPath = parent.system.constructor.metadata.embedded?.[this.metadata.name]; - if (!fieldPath) { - throw new Error( - `A ${parent.documentName} of type '${parent.type}' does not support ${this.metadata.name}!` - ); - } - - const update = { [`system.${fieldPath}.${id}`]: { ...data, _id: id } }; - const updatedParent = await parent.update(update, operation); - return foundry.utils.getProperty(updatedParent, `system.${fieldPath}.${id}`); - } - - /** - * Delete this pseudo-document. - * @param {object} [operation] The context of the operation. - * @returns {Promise} A promise that resolves to the updated document. - */ - async delete(operation = {}) { - if (!this.isSource) throw new Error('You cannot delete a non-source pseudo-document!'); - const update = { [`${this.fieldPath}.-=${this.id}`]: null }; - return this.document.update(update, operation); - } - - /** - * Duplicate this pseudo-document. - * @returns {Promise} A promise that resolves to the updated document. - */ - async duplicate() { - if (!this.isSource) throw new Error('You cannot duplicate a non-source pseudo-document!'); - const docData = foundry.utils.mergeObject(this.toObject(), { - name: game.i18n.format('DOCUMENT.CopyOf', { name: this.name }) - }); - return this.constructor.create(docData, { parent: this.document }); - } - - /** - * Update this pseudo-document. - * @param {object} [change] The change to perform. - * @param {object} [operation] The context of the operation. - * @returns {Promise} A promise that resolves to the updated document. - */ - async update(change = {}, operation = {}) { - if (!this.isSource) throw new Error('You cannot update a non-source pseudo-document!'); - const path = [this.fieldPath, this.id].join('.'); - const update = { [path]: change }; - return this.document.update(update, operation); - } -} diff --git a/module/data/pseudo-documents/base/pseudoDocument.mjs b/module/data/pseudo-documents/base/pseudoDocument.mjs deleted file mode 100644 index 2db23ef5..00000000 --- a/module/data/pseudo-documents/base/pseudoDocument.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import BasePseudoDocument from './base.mjs'; -import SheetManagementMixin from './sheetManagementMixin.mjs'; - -/** @extends BasePseudoDocument */ -export default class PseudoDocument extends SheetManagementMixin(BasePseudoDocument) { - static get TYPES() { - const { types } = CONFIG.daggerheart.pseudoDocuments[this.metadata.name]; - const typeEntries = Object.entries(types).map(([key, { documentClass }]) => [key, documentClass]); - return (this._TYPES ??= Object.freeze(Object.fromEntries(typeEntries))); - } - - static _TYPES; - - /** - * The type of this shape. - * @type {string} - */ - static TYPE = ''; - - /* -------------------------------------------- */ - - static getTypesChoices(validTypes) { - const { types } = CONFIG.daggerheart.pseudoDocuments[model.metadata.name]; - const typeEntries = Object.entries(types) - .map(([key, { label }]) => [key, label]) - .filter(([key]) => !validTypes || validTypes.includes(key)); - - return Object.entries(typeEntries); - } - - /* -------------------------------------------- */ - - /** @override */ - static defineSchema() { - const { fields } = foundry.data; - - return Object.assign(super.defineSchema(), { - type: new fields.StringField({ - required: true, - blank: false, - initial: this.TYPE, - validate: value => value === this.TYPE, - validationError: `must be equal to "${this.TYPE}"` - }) - }); - } - - /** @inheritdoc */ - static async create(data = {}, { parent, ...operation } = {}) { - data = foundry.utils.deepClone(data); - if (!data.type) data.type = Object.keys(this.TYPES)[0]; - if (!(data.type in this.TYPES)) { - throw new Error( - `The '${data.type}' type is not a valid type for a '${this.metadata.documentName}' pseudo-document!` - ); - } - return super.create(data, { parent, ...operation }); - } -} diff --git a/module/data/pseudo-documents/base/sheetManagementMixin.mjs b/module/data/pseudo-documents/base/sheetManagementMixin.mjs deleted file mode 100644 index 796faf51..00000000 --- a/module/data/pseudo-documents/base/sheetManagementMixin.mjs +++ /dev/null @@ -1,158 +0,0 @@ -import BasePseudoDocument from './base.mjs'; -const { ApplicationV2 } = foundry.applications.api; - -/** - * A mixin that adds sheet management capabilities to pseudo-documents - * @template {typeof BasePseudoDocument} T - * @param {T} Base - * @returns {T & typeof PseudoDocumentWithSheets} - */ -export default function SheetManagementMixin(Base) { - class PseudoDocumentWithSheets extends Base { - /** - * Reference to the sheet of this pseudo-document. - * @type {ApplicationV2|null} - */ - get sheet() { - if (this._sheet) return this._sheet; - const cls = this.constructor.metadata.sheetClass ?? ApplicationV2; - - if (!foundry.utils.isSubclass(cls, ApplicationV2)) { - return void ui.notifications.error( - 'Daggerheart | Error on PseudoDocument | sheetClass must be ApplicationV2' - ); - } - - const sheet = new cls({ document: this }); - this._sheet = sheet; - return sheet; - } - - /* -------------------------------------------- */ - /* Static Properties */ - /* -------------------------------------------- */ - - /** - * Set of apps what should be re-render. - * @type {Set} - * @internal - */ - _apps = new Set(); - - /* -------------------------------------------- */ - - /** - * Existing sheets of a specific type for a specific document. - * @type {ApplicationV2 | null} - */ - _sheet = null; - - /* -------------------------------------------- */ - /* Display Methods */ - /* -------------------------------------------- */ - - /** - * Render all the Application instances which are connected to this PseudoDocument. - * @param {ApplicationRenderOptions} [options] Rendering options. - */ - render(options) { - for (const app of this._apps ?? []) { - app.render({ window: { title: app.title }, ...options }); - } - } - - /* -------------------------------------------- */ - - /** - * Register an application to respond to updates to a certain document. - * @param {ApplicationV2} app Application to update. - * @internal - */ - _registerApp(app) { - this._apps.add(app); - } - - /* -------------------------------------------- */ - - /** - * Remove an application from the render registry. - * @param {ApplicationV2} app Application to stop watching. - */ - _unregisterApp(app) { - this._apps.delete(app); - } - - /* -------------------------------------------- */ - /* Drag and Drop */ - /* -------------------------------------------- */ - - /** - * Serialize salient information for this PseudoDocument when dragging it. - * @returns {object} An object of drag data. - */ - toDragData() { - const dragData = { type: this.documentName, data: this.toObject() }; - if (this.id) dragData.uuid = this.uuid; - return dragData; - } - - /* -------------------------------------------- */ - /* Dialog Methods */ - /* -------------------------------------------- */ - - /** - * Spawn a dialog for creating a new PseudoDocument. - * @param {object} [data] Data to pre-populate the document with. - * @param {object} context - * @param {foundry.documents.Item} context.parent A parent for the document. - * @param {string[]|null} [context.types] A list of types to restrict the choices to, or null for no restriction. - * @returns {Promise} - */ - static async createDialog(data = {}, { parent, types = null, ...options } = {}) { - // TODO - } - - /** - * Present a Dialog form to confirm deletion of this PseudoDocument. - * @param {object} [options] - Additional options passed to `DialogV2.confirm`; - * @returns {Promise} A Promise which resolves to the deleted PseudoDocument. - */ - async deleteDialog(options = {}) { - const type = game.i18n.localize(this.constructor.metadata.label); - const content = options.content ?? `

- ${game.i18n.localize("AreYouSure")} - ${game.i18n.format("SIDEBAR.DeleteWarning", { type })} -

`; - - return foundry.applications.api.DialogV2.confirm({ - content, - yes: { callback: () => this.delete(operation) }, - window: { - icon: "fa-solid fa-trash", - title: `${game.i18n.format("DOCUMENT.Delete", { type })}: ${this.name}` - }, - ...options - }); - } - - /** - * Gets the default new name for a Document - * @param {object} collection - Collection of Documents - * @returns {string} - */ - static defaultName(collection) { - const documentName = this.metadata.name; - const takenNames = new Set(); - for (const document of collection) takenNames.add(document.name); - - const config = CONFIG.daggerheart.pseudoDocuments[documentName]; - const baseName = game.i18n.localize(config.label); - let name = baseName; - let index = 1; - while (takenNames.has(name)) name = `${baseName} (${++index})`; - return name; - } - } - - return PseudoDocumentWithSheets; -} diff --git a/module/data/pseudo-documents/feature/_module.mjs b/module/data/pseudo-documents/feature/_module.mjs deleted file mode 100644 index 794f5d27..00000000 --- a/module/data/pseudo-documents/feature/_module.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export { default as BaseFeatureData } from './baseFeatureData.mjs'; -export { default as WeaponFeature } from './weaponFeature.mjs'; diff --git a/module/data/pseudo-documents/feature/baseFeatureData.mjs b/module/data/pseudo-documents/feature/baseFeatureData.mjs deleted file mode 100644 index 61d1468d..00000000 --- a/module/data/pseudo-documents/feature/baseFeatureData.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import PseudoDocument from '../base/pseudoDocument.mjs'; - -export default class BaseFeatureData extends PseudoDocument { - /**@inheritdoc */ - static get metadata() { - return foundry.utils.mergeObject( - super.metadata, - { - name: 'feature', - embedded: {}, - //sheetClass: null //TODO: define feature-sheet - }, - { inplace: false } - ); - } - - static defineSchema() { - const { fields } = foundry.data; - const schema = super.defineSchema(); - return Object.assign(schema, { - subtype: new fields.StringField({ initial: 'test' }) - }); - } -} diff --git a/module/data/pseudo-documents/feature/weaponFeature.mjs b/module/data/pseudo-documents/feature/weaponFeature.mjs deleted file mode 100644 index c8039ede..00000000 --- a/module/data/pseudo-documents/feature/weaponFeature.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import BaseFeatureData from './baseFeatureData.mjs'; - -export default class WeaponFeature extends BaseFeatureData { - /**@override */ - static TYPE = 'weapon'; -} From f6e077b290e68ebc05df09d8f4bd8266092c816e Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Wed, 18 Jun 2025 20:57:32 +0200 Subject: [PATCH 04/13] 148 - Character Setup (#151) * Added a character setup dialog * Added optional equipment tab * Removed temp button to open character setup * Cleaned up footer from commented code --- daggerheart.mjs | 13 +- lang/en.json | 57 +- module/applications/characterCreation.mjs | 496 ++++++++++++++++++ .../sheets/activeEffectConfig.mjs | 2 +- module/applications/sheets/items/class.mjs | 25 +- module/config/domainConfig.mjs | 18 +- module/data/actor/character.mjs | 12 +- module/data/item/subclass.mjs | 2 +- module/dialogs/selectDialog.mjs | 97 ---- module/placeables/measuredTemplate.mjs | 2 +- styles/characterCreation.less | 398 ++++++++++++++ styles/daggerheart.css | 339 ++++++++++++ styles/daggerheart.less | 1 + templates/components/card-preview.hbs | 5 +- templates/sheets/items/armor/settings.hbs | 1 - templates/views/characterCreation/footer.hbs | 4 + templates/views/characterCreation/tabs.hbs | 13 + .../characterCreation/tabs/equipment.hbs | 107 ++++ .../views/characterCreation/tabs/setup.hbs | 101 ++++ .../views/characterCreation/tabs/story.hbs | 9 + 20 files changed, 1560 insertions(+), 142 deletions(-) create mode 100644 module/applications/characterCreation.mjs delete mode 100644 module/dialogs/selectDialog.mjs create mode 100644 styles/characterCreation.less create mode 100644 templates/views/characterCreation/footer.hbs create mode 100644 templates/views/characterCreation/tabs.hbs create mode 100644 templates/views/characterCreation/tabs/equipment.hbs create mode 100644 templates/views/characterCreation/tabs/setup.hbs create mode 100644 templates/views/characterCreation/tabs/story.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index c5c685cd..b4caca2a 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -77,14 +77,19 @@ Hooks.once('init', () => { Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; - DocumentSheetConfig.unregisterSheet( + foundry.applications.apps.DocumentSheetConfig.unregisterSheet( CONFIG.ActiveEffect.documentClass, 'core', foundry.applications.sheets.ActiveEffectConfig ); - DocumentSheetConfig.registerSheet(CONFIG.ActiveEffect.documentClass, SYSTEM.id, applications.DhActiveEffectConfig, { - makeDefault: true - }); + foundry.applications.apps.DocumentSheetConfig.registerSheet( + CONFIG.ActiveEffect.documentClass, + SYSTEM.id, + applications.DhActiveEffectConfig, + { + makeDefault: true + } + ); CONFIG.Combat.dataModels = { base: models.DhCombat diff --git a/lang/en.json b/lang/en.json index 1417e905..3c739537 100755 --- a/lang/en.json +++ b/lang/en.json @@ -367,39 +367,39 @@ } }, "Domains": { - "Arcana": { + "arcana": { "label": "Arcana", "Description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." }, - "Blade": { + "blade": { "label": "Blade", "Description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death." }, - "Bone": { + "bone": { "label": "Bone", "Description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training." }, - "Codex": { + "codex": { "label": "Codex", "Description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge." }, - "Grace": { + "grace": { "label": "Grace", "Description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language." }, - "Midnight": { + "midnight": { "label": "Midnight", "Description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas." }, - "Sage": { + "sage": { "label": "Sage", "Description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator." }, - "Splendor": { + "splendor": { "label": "Splendor", "Description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life." }, - "Valor": { + "valor": { "label": "Valor", "Description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others." } @@ -417,6 +417,45 @@ "requestingSpotlight": "Requesting The Spotlight", "combatStarted": "Active" }, + "CharacterCreation": { + "Title": "{actor} - Character Setup", + "TraitIncreases": "Trait Increases", + "SuggestedTraits": "Suggested Traits", + "InitialExperiences": "Initial Experiences", + "Heritage": "Heritage", + "SelectAncestry": "Select Ancestry", + "SelectCommunity": "Select Community", + "SelectClass": "Select Class", + "SelectSubclass": "Select Subclass", + "SelectArmor": "Select Armor", + "SelectPrimaryWeapon": "Select Primary Weapon", + "SelectSecondaryWeapon": "Select Secondary Weapon", + "SuggestedArmor": "Suggested Armor", + "SuggestedWeapons": "Suggested Weapon", + "SuggestedPrimaryWeapon": "Suggested Primary Weapon", + "SuggestedSecondaryWeapon": "Suggested Secondary Weapon", + "StartingItems": "Starting Items", + "Choice": "Choice", + "NewExperience": "New Experience..", + "FinishCreation": "Finish Character Setup", + "Tabs": { + "Optional": "Optional", + "Setup": "Setup", + "Equipment": "Equipment", + "Story": "Story" + }, + "Notifications": { + "SubclassNotInClass": "This subclass does not belong to your selected class.", + "MissingClass": "You don't have a class selected yet.", + "WrongDomain": "The card isn't from one of your class domains.", + "CardTooHighLevel": "The card is too high level!", + "DuplicateCard": "You cannot select the same card more than once.", + "NotPrimary": "The weapon is not a primary weapon!", + "NotSecondary": "The weapon is not a secondary weapon!", + "ItemTooHighTier": "The item must be from Tier1", + "PrimaryIsTwoHanded": "Cannot select a secondary weapon with a two-handed primary!" + } + }, "LevelUp": { "Options": { "trait": "Gain a +1 bonus to two unmarked character traits and mark them.", diff --git a/module/applications/characterCreation.mjs b/module/applications/characterCreation.mjs new file mode 100644 index 00000000..d10bfe17 --- /dev/null +++ b/module/applications/characterCreation.mjs @@ -0,0 +1,496 @@ +import { abilities } from '../config/actorConfig.mjs'; +import { burden } from '../config/generalConfig.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DhCharacterCreation extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(character) { + super({}); + + this.character = character; + + this.setup = { + traits: this.character.system.traits, + ancestry: this.character.system.ancestry ?? {}, + community: this.character.system.community ?? {}, + class: this.character.system.class?.value ?? {}, + subclass: this.character.system.class?.subclass ?? {}, + experiences: { + [foundry.utils.randomID()]: { description: '', value: 2 }, + [foundry.utils.randomID()]: { description: '', value: 2 } + }, + domainCards: { + [foundry.utils.randomID()]: {}, + [foundry.utils.randomID()]: {} + }, + visibility: 1 + }; + + this.equipment = { + armor: {}, + primaryWeapon: {}, + secondaryWeapon: {}, + inventory: { + take: {}, + choiceA: {}, + choiceB: {} + } + }; + + this._dragDrop = this._createDragDropHandlers(); + } + + get title() { + return game.i18n.format('DAGGERHEART.CharacterCreation.Title', { actor: this.character.name }); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'character-creation'], + position: { width: 800, height: 'auto' }, + actions: { + viewCompendium: this.viewCompendium, + viewItem: this.viewItem, + useSuggestedTraits: this.useSuggestedTraits, + equipmentChoice: this.equipmentChoice, + finish: this.finish + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [ + { dragSelector: null, dropSelector: '.ancestry-card' }, + { dragSelector: null, dropSelector: '.community-card' }, + { dragSelector: null, dropSelector: '.class-card' }, + { dragSelector: null, dropSelector: '.subclass-card' }, + { dragSelector: null, dropSelector: '.domain-card' }, + { dragSelector: null, dropSelector: '.armor-card' }, + { dragSelector: null, dropSelector: '.primary-weapon-card' }, + { dragSelector: null, dropSelector: '.secondary-weapon-card' }, + { dragSelector: '.suggestion-inner-container', dropSelector: '.selections-container' } + ] + }; + + static PARTS = { + tabs: { template: 'systems/daggerheart/templates/views/characterCreation/tabs.hbs' }, + setup: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/setup.hbs' }, + equipment: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/equipment.hbs' }, + // story: { template: 'systems/daggerheart/templates/views/characterCreation/tabs/story.hbs' }, + footer: { template: 'systems/daggerheart/templates/views/characterCreation/footer.hbs' } + }; + + static TABS = { + setup: { + active: true, + cssClass: '', + group: 'primary', + id: 'setup', + label: 'DAGGERHEART.CharacterCreation.Tabs.Setup' + }, + equipment: { + active: false, + cssClass: '', + group: 'primary', + id: 'equipment', + label: 'DAGGERHEART.CharacterCreation.Tabs.Equipment', + optional: true + } + // story: { + // active: false, + // cssClass: '', + // group: 'primary', + // id: 'story', + // label: 'DAGGERHEART.CharacterCreation.Tabs.Story', + // optional: true + // } + }; + + _getTabs(tabs) { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + + switch (v.id) { + case 'setup': + const classFinished = this.setup.class.uuid && this.setup.subclass.uuid; + const heritageFinished = this.setup.ancestry.uuid && this.setup.community.uuid; + const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null); + const experiencesFinished = Object.values(this.setup.experiences).every(x => x.description); + const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid); + v.finished = + classFinished && + heritageFinished && + traitsFinished && + experiencesFinished && + domainCardsFinished; + break; + case 'equipment': + const armorFinished = this.equipment.armor?.uuid; + const primaryFinished = this.equipment.primaryWeapon?.uuid; + const secondaryFinished = + this.equipment.secondaryWeapon?.uuid || + (primaryFinished && this.equipment.primaryWeapon.system.burden == burden.twoHanded.value); + const choiceAFinished = this.equipment.inventory.choiceA?.uuid; + const choiceBFinished = this.equipment.inventory.choiceB?.uuid; + + v.finished = + armorFinished && primaryFinished && secondaryFinished && choiceAFinished && choiceBFinished; + } + } + + tabs.equipment.cssClass = tabs.setup.finished ? tabs.equipment.cssClass : 'disabled'; + // tabs.story.cssClass = tabs.setup.finished ? tabs.story.cssClass : 'disabled'; + + return tabs; + } + + changeTab(tab, group, options) { + super.changeTab(tab, group, options); + + for (var listTab of Object.keys(this.constructor.TABS)) { + const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`); + if (listTab === tab) { + marker.classList.add('active'); + } else { + marker.classList.remove('active'); + } + } + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + this._dragDrop.forEach(d => d.bind(htmlElement)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.tabs = this._getTabs(this.constructor.TABS); + + return context; + } + + async _preparePartContext(partId, context) { + switch (partId) { + case 'setup': + const availableTraitModifiers = game.settings + .get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew) + .traitArray.map(trait => ({ key: trait, name: trait })); + for (let trait of Object.values(this.setup.traits).filter(x => x.value !== null)) { + const index = availableTraitModifiers.findIndex(x => x.key === trait.value); + if (index !== -1) { + availableTraitModifiers.splice(index, 1); + } + } + + context.suggestedTraits = this.setup.class.system + ? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => { + const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; + return `${game.i18n.localize(`DAGGERHEART.Abilities.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; + }) + : []; + context.traits = { + values: Object.keys(this.setup.traits).map(traitKey => { + const trait = this.setup.traits[traitKey]; + const options = [...availableTraitModifiers]; + if (trait.value !== null && !options.some(x => x.key === trait.value)) + options.push({ key: trait.value, name: trait.value }); + + return { + ...trait, + key: traitKey, + name: game.i18n.localize(abilities[traitKey].label), + options: options + }; + }) + }; + context.traits.nrTotal = Object.keys(context.traits.values).length; + context.traits.nrSelected = Object.values(context.traits.values).reduce( + (acc, trait) => acc + (trait.value !== null ? 1 : 0), + 0 + ); + + context.experience = { + values: this.setup.experiences, + nrTotal: Object.keys(this.setup.experiences).length, + nrSelected: Object.values(this.setup.experiences).reduce( + (acc, exp) => acc + (exp.description ? 1 : 0), + 0 + ) + }; + + context.ancestry = { ...this.setup.ancestry, compendium: 'ancestries' }; + context.community = { ...this.setup.community, compendium: 'communities' }; + context.class = { ...this.setup.class, compendium: 'classes' }; + context.subclass = { ...this.setup.subclass, compendium: 'subclasses' }; + context.domainCards = Object.keys(this.setup.domainCards).reduce((acc, x) => { + acc[x] = { ...this.setup.domainCards[x], compendium: 'domains' }; + return acc; + }, {}); + + context.visibility = this.setup.visibility; + break; + case 'equipment': + const suggestions = await this.getEquipmentSuggestions( + this.equipment.inventory.choiceA, + this.equipment.inventory.choiceB + ); + context.armor = { + ...this.equipment.armor, + suggestion: { ...suggestions.armor, taken: suggestions.armor?.uuid === this.equipment.armor?.uuid }, + compendium: 'armors' + }; + context.primaryWeapon = { + ...this.equipment.primaryWeapon, + suggestion: { + ...suggestions.primaryWeapon, + taken: suggestions.primaryWeapon?.uuid === this.equipment.primaryWeapon?.uuid + }, + compendium: 'weapons' + }; + context.secondaryWeapon = { + ...this.equipment.secondaryWeapon, + suggestion: { + ...suggestions.secondaryWeapon, + taken: suggestions.secondaryWeapon?.uuid === this.equipment.secondaryWeapon?.uuid + }, + disabled: this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value, + compendium: 'weapons' + }; + context.inventory = { + take: suggestions.inventory.take, + choiceA: { suggestions: suggestions.inventory.choiceA, compendium: 'consumables' }, + choiceB: { suggestions: suggestions.inventory.choiceB, compendium: 'general-items' } + }; + + break; + } + + return context; + } + + static async updateForm(event, _, formData) { + this.setup = foundry.utils.mergeObject(this.setup, formData.object); + + this.setup.visibility = this.getUpdateVisibility(); + this.render(); + } + + getUpdateVisibility() { + switch (this.setup.visibility) { + case 5: + return 5; + case 4: + return Object.values(this.setup.experiences).every(x => x.description) ? 5 : 4; + case 3: + return Object.values(this.setup.traits).every(x => x.value !== null) ? 4 : 3; + case 2: + return this.setup.ancestry.uuid && this.setup.community.uuid ? 3 : 2; + case 1: + return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1; + } + } + + async getEquipmentSuggestions(choiceA, choiceB) { + if (!this.setup.class.uuid) return { inventory: { take: [] } }; + + const { inventory, characterGuide } = this.setup.class.system; + return { + armor: characterGuide.suggestedArmor ?? null, + primaryWeapon: characterGuide.suggestedPrimaryWeapon ?? null, + secondaryWeapon: + { ...characterGuide.suggestedSecondaryWeapon, uuid: characterGuide.suggestedSecondaryWeapon.uuid } ?? + null, + inventory: { + take: inventory.take ?? [], + choiceA: + inventory.choiceA?.map(x => ({ ...x, uuid: x.uuid, selected: x.uuid === choiceA?.uuid })) ?? [], + choiceB: inventory.choiceB?.map(x => ({ ...x, uuid: x.uuid, selected: x.uuid === choiceB?.uuid })) ?? [] + } + }; + } + + _createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.callbacks = { + dragstart: this._onDragStart.bind(this), + drop: this._onDrop.bind(this) + }; + return new foundry.applications.ux.DragDrop.implementation(d); + }); + } + + static async viewCompendium(_, target) { + (await game.packs.get(`daggerheart.${target.dataset.compendium}`))?.render(true); + } + + static async viewItem(_, target) { + (await foundry.utils.fromUuid(target.dataset.uuid)).sheet.render(true); + } + + static useSuggestedTraits() { + this.setup.traits = Object.keys(this.setup.traits).reduce((acc, traitKey) => { + acc[traitKey] = { + ...this.setup.traits[traitKey], + value: this.setup.class.system.characterGuide.suggestedTraits[traitKey] + }; + return acc; + }, {}); + + this.setup.visibility = this.getUpdateVisibility(); + this.render(); + } + + static async equipmentChoice(_, target) { + this.equipment.inventory[target.dataset.path] = await foundry.utils.fromUuid(target.dataset.uuid); + this.render(); + } + + static async finish() { + const embeddedAncestries = await this.character.createEmbeddedDocuments('Item', [this.setup.ancestry]); + const embeddedCommunities = await this.character.createEmbeddedDocuments('Item', [this.setup.community]); + await this.character.createEmbeddedDocuments('Item', [this.setup.class]); + await this.character.createEmbeddedDocuments('Item', [this.setup.subclass]); + await this.character.createEmbeddedDocuments('Item', Object.values(this.setup.domainCards)); + + if (this.equipment.armor.uuid) + await this.character.createEmbeddedDocuments('Item', [ + { ...this.equipment.armor, system: { ...this.equipment.armor.system, equipped: true } } + ]); + if (this.equipment.primaryWeapon.uuid) + await this.character.createEmbeddedDocuments('Item', [ + { ...this.equipment.primaryWeapon, system: { ...this.equipment.primaryWeapon.system, equipped: true } } + ]); + if (this.equipment.secondaryWeapon.uuid) + await this.character.createEmbeddedDocuments('Item', [ + { + ...this.equipment.secondaryWeapon, + system: { ...this.equipment.secondaryWeapon.system, equipped: true } + } + ]); + if (this.equipment.inventory.choiceA.uuid) + await this.character.createEmbeddedDocuments('Item', [this.equipment.inventory.choiceA]); + if (this.equipment.inventory.choiceB.uuid) + await this.character.createEmbeddedDocuments('Item', [this.equipment.inventory.choiceB]); + await this.character.createEmbeddedDocuments('Item', this.setup.class.system.inventory.take); + + await this.character.update({ + system: { + traits: this.setup.traits, + experiences: this.setup.experiences, + ancestry: embeddedAncestries[0].uuid, + community: embeddedCommunities[0].uuid + } + }); + + this.close(); + } + + async _onDragStart(event) { + const target = event.currentTarget; + + event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset)); + event.dataTransfer.setDragImage(target, 60, 0); + } + + async _onDrop(event) { + const data = TextEditor.getDragEventData(event); + const item = await foundry.utils.fromUuid(data.uuid); + if (item.type === 'ancestry' && event.target.closest('.ancestry-card')) { + this.setup.ancestry = { ...item, uuid: item.uuid }; + } else if (item.type === 'community' && event.target.closest('.community-card')) { + this.setup.community = { ...item, uuid: item.uuid }; + } else if (item.type === 'class' && event.target.closest('.class-card')) { + this.setup.class = { ...item, uuid: item.uuid }; + this.setup.subclass = {}; + this.setup.domainCards = { + [foundry.utils.randomID()]: {}, + [foundry.utils.randomID()]: {} + }; + } else if (item.type === 'subclass' && event.target.closest('.subclass-card')) { + if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.SubclassNotInClass') + ); + return; + } + + this.setup.subclass = { ...item, uuid: item.uuid }; + } else if (item.type === 'domainCard' && event.target.closest('.domain-card')) { + if (!this.setup.class.uuid) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.MissingClass')); + return; + } + + if (!this.setup.class.system.domains.includes(item.system.domain)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.WrongDomain')); + return; + } + + if (item.system.level > 1) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.CardTooHighLevel') + ); + return; + } + + if (Object.values(this.setup.domainCards).some(card => card.uuid === item.uuid)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.DuplicateCard')); + return; + } + + this.setup.domainCards[event.target.closest('.domain-card').dataset.card] = { ...item, uuid: item.uuid }; + } else if (item.type === 'armor' && event.target.closest('.armor-card')) { + if (item.system.tier > 1) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier') + ); + return; + } + + this.equipment.armor = { ...item, uuid: item.uuid }; + } else if (item.type === 'weapon' && event.target.closest('.primary-weapon-card')) { + if (item.system.secondary) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.NotPrimary')); + return; + } + + if (item.system.tier > 1) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier') + ); + return; + } + + this.equipment.primaryWeapon = { ...item, uuid: item.uuid }; + } else if (item.type === 'weapon' && event.target.closest('.secondary-weapon-card')) { + if (this.equipment.primaryWeapon?.system?.burden === burden.twoHanded.value) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.PrimaryIsTwoHanded') + ); + return; + } + + if (!item.system.secondary) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.NotSecondary')); + return; + } + + if (item.system.tier > 1) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.CharacterCreation.Notifications.ItemTooHighTier') + ); + return; + } + + this.equipment.secondaryWeapon = { ...item, uuid: item.uuid }; + } else { + return; + } + + this.setup.visibility = this.getUpdateVisibility(); + this.render(); + } +} diff --git a/module/applications/sheets/activeEffectConfig.mjs b/module/applications/sheets/activeEffectConfig.mjs index 585086a1..6a629583 100644 --- a/module/applications/sheets/activeEffectConfig.mjs +++ b/module/applications/sheets/activeEffectConfig.mjs @@ -1,4 +1,4 @@ -export default class DhActiveEffectConfig extends ActiveEffectConfig { +export default class DhActiveEffectConfig extends foundry.applications.sheets.ActiveEffectConfig { static DEFAULT_OPTIONS = { classes: ['daggerheart', 'sheet', 'dh-style'] }; diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 54f29361..a4659598 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -221,46 +221,53 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { async _onDrop(event) { const data = TextEditor.getDragEventData(event); const item = await fromUuid(data.uuid); + const target = event.target.closest('fieldset.drop-section'); if (item.type === 'subclass') { await this.document.update({ 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] }); } else if (item.type === 'weapon') { - if (event.currentTarget.classList.contains('primary-weapon-section')) { + if (target.classList.contains('primary-weapon-section')) { if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) await this.document.update({ 'system.characterGuide.suggestedPrimaryWeapon': item.uuid }); - } else if (event.currentTarget.classList.contains('secondary-weapon-section')) { + } else if (target.classList.contains('secondary-weapon-section')) { if (!this.document.system.characterGuide.suggestedSecondaryWeapon && item.system.secondary) await this.document.update({ 'system.characterGuide.suggestedSecondaryWeapon': item.uuid }); } } else if (item.type === 'armor') { - if (event.currentTarget.classList.contains('armor-section')) { + if (target.classList.contains('armor-section')) { if (!this.document.system.characterGuide.suggestedArmor) await this.document.update({ 'system.characterGuide.suggestedArmor': item.uuid }); } - } else if (event.currentTarget.classList.contains('choice-a-section')) { + } else if (target.classList.contains('choice-a-section')) { if (item.type === 'miscellaneous' || item.type === 'consumable') { if (this.document.system.inventory.choiceA.length < 2) await this.document.update({ - 'system.inventory.choiceA': [...this.document.system.inventory.choiceA, item.uuid] + 'system.inventory.choiceA': [ + ...this.document.system.inventory.choiceA.map(x => x.uuid), + item.uuid + ] }); } } else if (item.type === 'miscellaneous') { - if (event.currentTarget.classList.contains('take-section')) { + if (target.classList.contains('take-section')) { if (this.document.system.inventory.take.length < 3) await this.document.update({ - 'system.inventory.take': [...this.document.system.inventory.take, item.uuid] + 'system.inventory.take': [...this.document.system.inventory.take.map(x => x.uuid), item.uuid] }); - } else if (event.currentTarget.classList.contains('choice-b-section')) { + } else if (target.classList.contains('choice-b-section')) { if (this.document.system.inventory.choiceB.length < 2) await this.document.update({ - 'system.inventory.choiceB': [...this.document.system.inventory.choiceB, item.uuid] + 'system.inventory.choiceB': [ + ...this.document.system.inventory.choiceB.map(x => x.uuid), + item.uuid + ] }); } } diff --git a/module/config/domainConfig.mjs b/module/config/domainConfig.mjs index a32091c2..3ecb0bd1 100644 --- a/module/config/domainConfig.mjs +++ b/module/config/domainConfig.mjs @@ -1,56 +1,56 @@ export const domains = { arcana: { id: 'arcana', - label: 'DAGGERHEART.Domains.Arcana.label', + label: 'DAGGERHEART.Domains.arcana.label', src: 'icons/magic/symbols/circled-gem-pink.webp', description: 'DAGGERHEART.Domains.Arcana' }, blade: { id: 'blade', - label: 'DAGGERHEART.Domains.Blade.label', + label: 'DAGGERHEART.Domains.blade.label', src: 'icons/weapons/swords/sword-broad-crystal-paired.webp', description: 'DAGGERHEART.Domains.Blade' }, bone: { id: 'bone', - label: 'DAGGERHEART.Domains.Bone.label', + label: 'DAGGERHEART.Domains.bone.label', src: 'icons/skills/wounds/bone-broken-marrow-red.webp', description: 'DAGGERHEART.Domains.Bone' }, codex: { id: 'codex', - label: 'DAGGERHEART.Domains.Codex.label', + label: 'DAGGERHEART.Domains.codex.label', src: 'icons/sundries/books/book-embossed-jewel-gold-purple.webp', description: 'DAGGERHEART.Domains.Codex' }, grace: { id: 'grace', - label: 'DAGGERHEART.Domains.Grace.label', + label: 'DAGGERHEART.Domains.grace.label', src: 'icons/skills/movement/feet-winged-boots-glowing-yellow.webp', description: 'DAGGERHEART.Domains.Grace' }, midnight: { id: 'midnight', - label: 'DAGGERHEART.Domains.Midnight.label', + label: 'DAGGERHEART.Domains.midnight.label', src: 'icons/environment/settlement/watchtower-castle-night.webp', background: 'systems/daggerheart/assets/backgrounds/MidnightBackground.webp', description: 'DAGGERHEART.Domains.Midnight' }, sage: { id: 'sage', - label: 'DAGGERHEART.Domains.Sage.label', + label: 'DAGGERHEART.Domains.sage.label', src: 'icons/sundries/misc/pipe-wooden-straight-brown.webp', description: 'DAGGERHEART.Domains.Sage' }, splendor: { id: 'splendor', - label: 'DAGGERHEART.Domains.Splendor.label', + label: 'DAGGERHEART.Domains.splendor.label', src: 'icons/magic/control/control-influence-crown-gold.webp', description: 'DAGGERHEART.Domains.Splendor' }, valor: { id: 'valor', - label: 'DAGGERHEART.Domains.Valor.label', + label: 'DAGGERHEART.Domains.valor.label', src: 'icons/magic/control/control-influence-rally-purple.webp', description: 'DAGGERHEART.Domains.Valor' } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 1ebe217c..8219f89e 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -5,7 +5,7 @@ import BaseDataActor from './base.mjs'; const attributeField = () => new foundry.data.fields.SchemaField({ - value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + value: new foundry.data.fields.NumberField({ initial: null, integer: true }), bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }), tierMarked: new foundry.data.fields.BooleanField({ initial: false }) }); @@ -54,13 +54,7 @@ export default class DhCharacter extends BaseDataActor { description: new fields.StringField({}), value: new fields.NumberField({ integer: true, initial: 0 }), bonus: new fields.NumberField({ integer: true, initial: 0 }) - }), - { - initial: { - [foundry.utils.randomID()]: { description: '', value: 2 }, - [foundry.utils.randomID()]: { description: '', value: 2 } - } - } + }) ), gold: new fields.SchemaField({ coins: new fields.NumberField({ initial: 0, integer: true }), @@ -235,7 +229,7 @@ export default class DhCharacter extends BaseDataActor { for (var traitKey in this.traits) { var trait = this.traits[traitKey]; - trait.total = trait.value + trait.bonus; + trait.total = (trait.value ?? 0) + trait.bonus; } for (var experienceKey in this.experiences) { diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index bb473db1..75345625 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -64,7 +64,7 @@ export default class DHSubclass extends BaseDataItem { } else if (subclassData) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected')); return false; - } else if (classData.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) { + } else if (classData.system.subclasses.every(x => x.uuid !== data.uuid ?? `Item.${data._id}`)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassNotInClass')); return false; } diff --git a/module/dialogs/selectDialog.mjs b/module/dialogs/selectDialog.mjs deleted file mode 100644 index 484979cc..00000000 --- a/module/dialogs/selectDialog.mjs +++ /dev/null @@ -1,97 +0,0 @@ -export default class SelectDialog extends Dialog { - constructor(data, options) { - super(options); - - this.data = { - title: data.title, - buttons: data.buttons, - content: foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/dialog/item-select.hbs', - { - items: data.choices - } - ) - }; - - this.actor = data.actor; - this.actionCostMax = data.actionCostMax; - this.nrChoices = data.nrChoices; - this.validate = data.validate; - } - - async getData(options = {}) { - let buttons = Object.keys(this.data.buttons).reduce((obj, key) => { - let b = this.data.buttons[key]; - b.cssClass = (this.data.default === key ? [key, 'default', 'bright'] : [key]).join(' '); - if (b.condition !== false) obj[key] = b; - return obj; - }, {}); - - const content = await this.data.content; - - return { - content: content, - buttons: buttons - }; - } - - activateListeners(html) { - super.activateListeners(html); - $(html).find('.item-button').click(this.selectChoice); - } - - selectChoice = async event => { - if (this.validate) { - if (!this.validate(event.currentTarget.dataset.validateProp)) { - return; - } - } - - event.currentTarget.classList.toggle('checked'); - $(event.currentTarget).find('i')[0].classList.toggle('checked'); - - const buttons = $(this.element[0]).find('button.checked'); - if (buttons.length === this.nrChoices) { - $(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = false; - } else { - $(event.currentTarget).closest('.window-content').find('.confirm')[0].disabled = true; - } - }; - - /** - * - * @param {*} data - * choices, actor, title, cancelMessage, nrChoices, validate - * @returns - */ - static async selectItem(data) { - return this.wait({ - title: data.title ?? 'Selection', - buttons: { - no: { - icon: '', - label: game.i18n.localize('DAGGERHEART.General.Cancel'), - callback: _ => { - if (data.cancelMessage) { - ChatMessage.create({ content: data.cancelMessage }); - } - return []; - } - }, - confirm: { - icon: '', - label: game.i18n.localize('DAGGERHEART.General.OK'), - callback: html => { - const buttons = $(html).find('button.checked'); - return buttons.map(key => Number.parseInt(buttons[key].dataset.index)).toArray(); - }, - disabled: true - } - }, - choices: data.choices, - actor: data.actor, - nrChoices: data.nrChoices ?? 1, - validate: data.validate - }); - } -} diff --git a/module/placeables/measuredTemplate.mjs b/module/placeables/measuredTemplate.mjs index ee21af6d..385cf81e 100644 --- a/module/placeables/measuredTemplate.mjs +++ b/module/placeables/measuredTemplate.mjs @@ -1,4 +1,4 @@ -export default class DhMeasuredTemplate extends MeasuredTemplate { +export default class DhMeasuredTemplate extends foundry.canvas.placeables.MeasuredTemplate { _refreshRulerText() { super._refreshRulerText(); diff --git a/styles/characterCreation.less b/styles/characterCreation.less new file mode 100644 index 00000000..3e4b42d9 --- /dev/null +++ b/styles/characterCreation.less @@ -0,0 +1,398 @@ +.daggerheart.dh-style.dialog.character-creation { + .window-content { + gap: 16px; + } + + .tab-navigation { + nav { + flex: 1; + + a { + flex: 1; + text-align: center; + display: flex; + justify-content: center; + position: relative; + + &.disabled { + opacity: 0.4; + } + + .nav-section-text { + position: relative; + display: flex; + align-items: center; + } + + .finish-marker { + position: absolute; + align-self: center; + top: -8px; + padding: 4px; + border: 1px solid; + border-radius: 50%; + height: 16px; + width: 16px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-cool-4); + content: ''; + + &.active { + background-color: var(--color-warm-2); + } + } + + .descriptor { + position: absolute; + bottom: -8px; + font-size: 12px; + border-radius: 8px; + width: 56px; + text-align: center; + line-height: 1; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + background-image: url(../assets/parchments/dh-parchment-light.png); + } + } + } + } + + .main-selections-container { + display: flex; + flex-direction: column; + gap: 4px; + + .selections-container { + width: 140px; + display: flex; + flex-direction: column; + text-align: center; + + .card-preview-container { + border-color: light-dark(@dark-blue, @golden); + } + } + + .selections-outer-container { + display: flex; + justify-content: space-evenly; + height: 210px; + } + + .section-container { + border-radius: 8px; + border-color: light-dark(@dark-blue, @golden); + + legend { + margin-left: auto; + margin-right: auto; + font-size: 28px; + font-weight: bold; + padding: 0 8px; + } + + .section-inner-container { + position: relative; + border-radius: 8px; + border-color: light-dark(@dark-blue, @golden); + display: flex; + justify-content: center; + + legend { + font-size: 20px; + } + + .action-button { + position: absolute; + bottom: -8px; + height: 16px; + width: 110px; + min-height: unset; + border: 1px solid light-dark(@dark-blue, @golden); + color: light-dark(@dark, @beige); + background-color: var(--color-warm-3); + + &:hover { + background-color: var(--color-warm-2); + filter: drop-shadow(0 0 3px var(--color-warm-2)); + } + } + } + } + + .traits-container { + text-align: center; + display: flex; + gap: 16px; + + .suggested-traits-container { + display: flex; + flex-wrap: wrap; + width: 176px; + gap: 4px; + margin-bottom: 8px; + + .suggested-trait-container { + width: 56px; + white-space: nowrap; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } + + .traits-inner-container { + display: flex; + justify-content: space-evenly; + gap: 8px; + + .trait-container { + border: 1px solid light-dark(@dark-blue, @golden); + padding: 0 4px; + } + } + } + + .experiences-inner-container { + display: flex; + justify-content: space-evenly; + text-align: center; + + .experience-container { + position: relative; + display: flex; + align-items: center; + + .experience-description { + border-color: light-dark(@dark-blue, @golden); + padding-right: 24px; + } + + .experience-value { + position: absolute; + right: 0; + width: 22px; + border-left: 1px solid light-dark(@dark-blue, @golden); + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .creation-action-footer { + display: flex; + align-items: center; + gap: 32px; + + .footer-section { + display: flex; + align-items: center; + gap: 32px; + + nav { + flex: 1; + gap: 8px; + border: 0; + + a { + flex: 1; + text-align: center; + display: flex; + justify-content: center; + position: relative; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + + .nav-section-text { + position: relative; + display: flex; + align-items: center; + } + + .finish-marker { + position: absolute; + align-self: center; + top: -10px; + padding: 4px; + border: 1px solid; + border-radius: 50%; + height: 20px; + width: 20px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-cool-4); + content: ''; + + &.finished { + background-color: var(--color-warm-2); + } + } + + .descriptor { + position: absolute; + bottom: -8px; + font-size: 12px; + border-radius: 8px; + width: 56px; + text-align: center; + line-height: 1; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + background-image: url(../assets/parchments/dh-parchment-light.png); + } + } + } + + button { + flex: 1; + height: 100%; + white-space: nowrap; + } + } + } + + .main-equipment-selection { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 16px; + + &.triple { + grid-template-columns: 1fr 1fr 1fr; + } + } + + .equipment-selection { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 8px; + + legend { + margin-left: auto; + margin-right: auto; + font-size: 28px; + font-weight: bold; + padding: 0 8px; + white-space: nowrap; + } + + .equipment-subsection { + display: flex; + align-items: start; + gap: 32px; + } + + .equipment-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + } + + .simple-equipment-container { + display: flex; + flex-direction: column; + justify-content: space-evenly; + gap: 8px; + height: 100%; + + .simple-equipment { + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 8px; + position: relative; + display: flex; + justify-content: center; + + &.selectable { + cursor: pointer; + } + + &.inactive { + opacity: 0.4; + } + + label { + position: absolute; + top: -8px; + font-size: 12px; + white-space: nowrap; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + background-image: url('../assets/parchments/dh-parchment-light.png'); + padding: 0 2px; + } + + img { + width: 60px; + height: 60px; + border-radius: 8px; + } + } + } + + .suggestion-container { + position: relative; + display: flex; + justify-content: center; + height: min-content; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 8px; + + legend { + margin-left: auto; + margin-right: auto; + font-size: 12px; + } + + .suggestion-inner-container { + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding: 6px; + cursor: grab; + + &.taken { + opacity: 0.4; + } + + label { + position: absolute; + top: -2px; + font-size: 12px; + } + + img { + width: 120px; + } + } + } + } + } + + .creation-action-footer { + display: flex; + align-items: center; + gap: 32px; + + button { + flex: 1; + height: 100%; + white-space: nowrap; + } + } +} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index fee3a88b..a7a0cdc3 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2489,6 +2489,345 @@ div.daggerheart.views.multiclass { .item-button .item-icon.checked { opacity: 1; } +.daggerheart.dh-style.dialog.character-creation .window-content { + gap: 16px; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav { + flex: 1; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a { + flex: 1; + text-align: center; + display: flex; + justify-content: center; + position: relative; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a.disabled { + opacity: 0.4; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a .nav-section-text { + position: relative; + display: flex; + align-items: center; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a .finish-marker { + position: absolute; + align-self: center; + top: -8px; + padding: 4px; + border: 1px solid; + border-radius: 50%; + height: 16px; + width: 16px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-cool-4); + content: ''; +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a .finish-marker.active { + background-color: var(--color-warm-2); +} +.daggerheart.dh-style.dialog.character-creation .tab-navigation nav a .descriptor { + position: absolute; + bottom: -8px; + font-size: 12px; + border-radius: 8px; + width: 56px; + text-align: center; + line-height: 1; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + color: light-dark(#efe6d8, #222); + background-image: url(../assets/parchments/dh-parchment-light.png); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container { + display: flex; + flex-direction: column; + gap: 4px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .selections-container { + width: 140px; + display: flex; + flex-direction: column; + text-align: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .selections-container .card-preview-container { + border-color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .selections-outer-container { + display: flex; + justify-content: space-evenly; + height: 210px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container { + border-radius: 8px; + border-color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container legend { + margin-left: auto; + margin-right: auto; + font-size: 28px; + font-weight: bold; + padding: 0 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container .section-inner-container { + position: relative; + border-radius: 8px; + border-color: light-dark(#18162e, #f3c267); + display: flex; + justify-content: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container .section-inner-container legend { + font-size: 20px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container .section-inner-container .action-button { + position: absolute; + bottom: -8px; + height: 16px; + width: 110px; + min-height: unset; + border: 1px solid light-dark(#18162e, #f3c267); + color: light-dark(#222, #efe6d8); + background-color: var(--color-warm-3); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .section-container .section-inner-container .action-button:hover { + background-color: var(--color-warm-2); + filter: drop-shadow(0 0 3px var(--color-warm-2)); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .traits-container { + text-align: center; + display: flex; + gap: 16px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .traits-container .suggested-traits-container { + display: flex; + flex-wrap: wrap; + width: 176px; + gap: 4px; + margin-bottom: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .traits-container .suggested-traits-container .suggested-trait-container { + width: 56px; + white-space: nowrap; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + color: light-dark(#efe6d8, #222); + background-image: url('../assets/parchments/dh-parchment-light.png'); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .traits-container .traits-inner-container { + display: flex; + justify-content: space-evenly; + gap: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .traits-container .traits-inner-container .trait-container { + border: 1px solid light-dark(#18162e, #f3c267); + padding: 0 4px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .experiences-inner-container { + display: flex; + justify-content: space-evenly; + text-align: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .experiences-inner-container .experience-container { + position: relative; + display: flex; + align-items: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .experiences-inner-container .experience-container .experience-description { + border-color: light-dark(#18162e, #f3c267); + padding-right: 24px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .experiences-inner-container .experience-container .experience-value { + position: absolute; + right: 0; + width: 22px; + border-left: 1px solid light-dark(#18162e, #f3c267); + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer { + display: flex; + align-items: center; + gap: 32px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section { + display: flex; + align-items: center; + gap: 32px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav { + flex: 1; + gap: 8px; + border: 0; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav a { + flex: 1; + text-align: center; + display: flex; + justify-content: center; + position: relative; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav a .nav-section-text { + position: relative; + display: flex; + align-items: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav a .finish-marker { + position: absolute; + align-self: center; + top: -10px; + padding: 4px; + border: 1px solid; + border-radius: 50%; + height: 20px; + width: 20px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-cool-4); + content: ''; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav a .finish-marker.finished { + background-color: var(--color-warm-2); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section nav a .descriptor { + position: absolute; + bottom: -8px; + font-size: 12px; + border-radius: 8px; + width: 56px; + text-align: center; + line-height: 1; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + color: light-dark(#efe6d8, #222); + background-image: url(../assets/parchments/dh-parchment-light.png); +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .creation-action-footer .footer-section button { + flex: 1; + height: 100%; + white-space: nowrap; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .main-equipment-selection { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 16px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .main-equipment-selection.triple { + grid-template-columns: 1fr 1fr 1fr; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection legend { + margin-left: auto; + margin-right: auto; + font-size: 28px; + font-weight: bold; + padding: 0 8px; + white-space: nowrap; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .equipment-subsection { + display: flex; + align-items: start; + gap: 32px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .equipment-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container { + display: flex; + flex-direction: column; + justify-content: space-evenly; + gap: 8px; + height: 100%; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container .simple-equipment { + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 8px; + position: relative; + display: flex; + justify-content: center; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container .simple-equipment.selectable { + cursor: pointer; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container .simple-equipment.inactive { + opacity: 0.4; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container .simple-equipment label { + position: absolute; + top: -8px; + font-size: 12px; + white-space: nowrap; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + color: light-dark(#efe6d8, #222); + background-image: url('../assets/parchments/dh-parchment-light.png'); + padding: 0 2px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .simple-equipment-container .simple-equipment img { + width: 60px; + height: 60px; + border-radius: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container { + position: relative; + display: flex; + justify-content: center; + height: min-content; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 8px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container legend { + margin-left: auto; + margin-right: auto; + font-size: 12px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container .suggestion-inner-container { + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding: 6px; + cursor: grab; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container .suggestion-inner-container.taken { + opacity: 0.4; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container .suggestion-inner-container label { + position: absolute; + top: -2px; + font-size: 12px; +} +.daggerheart.dh-style.dialog.character-creation .main-selections-container .equipment-selection .suggestion-container .suggestion-inner-container img { + width: 120px; +} +.daggerheart.dh-style.dialog.character-creation .creation-action-footer { + display: flex; + align-items: center; + gap: 32px; +} +.daggerheart.dh-style.dialog.character-creation .creation-action-footer button { + flex: 1; + height: 100%; + white-space: nowrap; +} .theme-light .daggerheart.levelup .tiers-container .tier-container { background-image: url('../assets/parchments/dh-parchment-light.png'); } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 9965015b..3ad972fc 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -8,6 +8,7 @@ @import './application.less'; @import './sheets/sheets.less'; @import './dialog.less'; +@import './characterCreation.less'; @import './levelup.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import './resources.less'; diff --git a/templates/components/card-preview.hbs b/templates/components/card-preview.hbs index ba817371..da2abb77 100644 --- a/templates/components/card-preview.hbs +++ b/templates/components/card-preview.hbs @@ -1,4 +1,7 @@ -
+
{{#if this.img}}
{{this.name}}
diff --git a/templates/sheets/items/armor/settings.hbs b/templates/sheets/items/armor/settings.hbs index 5f3d749b..6a2341d9 100644 --- a/templates/sheets/items/armor/settings.hbs +++ b/templates/sheets/items/armor/settings.hbs @@ -3,7 +3,6 @@ data-tab='{{tabs.settings.id}}' data-group='{{tabs.settings.group}}' > -
{{localize tabs.settings.label}} {{localize "DAGGERHEART.Tiers.singular"}} diff --git a/templates/views/characterCreation/footer.hbs b/templates/views/characterCreation/footer.hbs new file mode 100644 index 00000000..3cc3aa5e --- /dev/null +++ b/templates/views/characterCreation/footer.hbs @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/templates/views/characterCreation/tabs.hbs b/templates/views/characterCreation/tabs.hbs new file mode 100644 index 00000000..44065d4e --- /dev/null +++ b/templates/views/characterCreation/tabs.hbs @@ -0,0 +1,13 @@ +
+ + + +
\ No newline at end of file diff --git a/templates/views/characterCreation/tabs/equipment.hbs b/templates/views/characterCreation/tabs/equipment.hbs new file mode 100644 index 00000000..75f2631e --- /dev/null +++ b/templates/views/characterCreation/tabs/equipment.hbs @@ -0,0 +1,107 @@ +
+
+
+
+ {{localize "DAGGERHEART.CharacterCreation.SuggestedArmor"}} + +
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" armor }} + {{localize "DAGGERHEART.CharacterCreation.SelectArmor"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ {{#if armor.suggestion}} +
+ {{armor.suggestion.name}} +
+ +
+
+ {{/if}} +
+ +
+ {{localize "DAGGERHEART.CharacterCreation.SuggestedWeapons"}} + +
+
+
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" primaryWeapon }} + {{localize "DAGGERHEART.CharacterCreation.SelectPrimaryWeapon"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ {{#if primaryWeapon.suggestion}} +
+ {{primaryWeapon.suggestion.name}} +
+ +
+
+ {{/if}} +
+
+
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" secondaryWeapon }} + {{localize "DAGGERHEART.CharacterCreation.SelectSecondaryWeapon"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ {{#if secondaryWeapon.suggestion}} +
+ {{secondaryWeapon.suggestion.name}} +
+ +
+
+ {{/if}} +
+
+
+
+
+
+ {{localize "DAGGERHEART.CharacterCreation.StartingItems"}} + +
+ {{#each inventory.take}} +
+ + +
+ {{/each}} +
+
+ +
+ {{localize "DAGGERHEART.CharacterCreation.Choice"}} + +
+ {{#each inventory.choiceA.suggestions}} +
+ + +
+ {{/each}} +
+
+ +
+ {{localize "DAGGERHEART.CharacterCreation.Choice"}} + +
+ {{#each inventory.choiceB.suggestions}} +
+ + +
+ {{/each}} +
+
+
+
+
\ No newline at end of file diff --git a/templates/views/characterCreation/tabs/setup.hbs b/templates/views/characterCreation/tabs/setup.hbs new file mode 100644 index 00000000..4d6e638c --- /dev/null +++ b/templates/views/characterCreation/tabs/setup.hbs @@ -0,0 +1,101 @@ +
+
+
+ {{localize "TYPES.Item.class"}} +
+
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" class }} + {{localize "DAGGERHEART.CharacterCreation.SelectClass"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ +
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" subclass disabled=(not class.img) }} + {{localize "DAGGERHEART.CharacterCreation.SelectSubclass"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+
+
+ + {{#if (gte visibility 2)}} +
+ {{localize "DAGGERHEART.CharacterCreation.Heritage"}} +
+
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" ancestry }} + {{localize "DAGGERHEART.CharacterCreation.SelectAncestry"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ +
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" community }} + {{localize "DAGGERHEART.CharacterCreation.SelectCommunity"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+
+
+ {{/if}} + + {{#if (gte visibility 3)}} +
+ {{localize "DAGGERHEART.CharacterCreation.TraitIncreases"}} {{traits.nrSelected}}/{{traits.nrTotal}} +
+
+ {{localize "DAGGERHEART.CharacterCreation.SuggestedTraits"}} +
+ {{#each suggestedTraits}} +
{{this}}
+ {{/each}} +
+ +
+
+ {{#each traits.values}} +
+
{{this.name}}
+ +
+ {{/each}} +
+
+
+ {{/if}} + + {{#if (gte visibility 4)}} +
+ {{localize "DAGGERHEART.CharacterCreation.InitialExperiences"}} {{experience.nrSelected}}/{{experience.nrTotal}} +
+ {{#each experience.values as |experience id|}} +
+ +
{{signedNumber this.value}}
+
+ {{/each}} +
+
+ {{/if}} + + {{#if (gte visibility 5)}} +
+ {{localize "TYPES.Item.domainCard"}} +
+ {{#each domainCards as |domainCard id|}} +
+ {{#> "systems/daggerheart/templates/components/card-preview.hbs" domainCard }} + {{#each @root.class.system.domains }} +
{{localize (concat "DAGGERHEART.Domains." this ".label")}}
+ {{/each}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
+ {{/each}} +
+
+ {{/if}} +
+
\ No newline at end of file diff --git a/templates/views/characterCreation/tabs/story.hbs b/templates/views/characterCreation/tabs/story.hbs new file mode 100644 index 00000000..c2fb7400 --- /dev/null +++ b/templates/views/characterCreation/tabs/story.hbs @@ -0,0 +1,9 @@ +
+
+ Story +
+
\ No newline at end of file From 3464717958a301fd1603beafcf90b03b8b68aa07 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:36:09 +0200 Subject: [PATCH 05/13] 152 - Improve Rest Options (#154) * Fixed up downtime dialogs and data model * Added homebrew settings without action handling for now * Added NrChoices to homebrew --- daggerheart.mjs | 3 +- lang/en.json | 69 ++++++--- module/applications/downtime.mjs | 78 ++++++---- .../components/settingsActionsView.mjs | 144 ++++++++++++++++++ .../settings/homebrewSettings.mjs | 105 ++++++++++++- module/config/generalConfig.mjs | 80 +++++++--- module/data/actor/character.mjs | 29 ++-- module/data/settings/Homebrew.mjs | 36 +++++ module/ui/chatLog.mjs | 17 +++ styles/application.less | 30 +++- styles/daggerheart.css | 104 ++++++++++++- styles/settings.less | 91 +++++++++++ templates/chat/downtime.hbs | 19 +-- .../components/action-view-footer.hbs | 3 + .../components/action-view-header.hbs | 6 + templates/settings/components/action-view.hbs | 18 +++ .../components/settings-item-line.hbs | 22 +++ templates/settings/homebrew-settings.hbs | 46 ++++++ templates/views/downtime.hbs | 22 ++- 19 files changed, 799 insertions(+), 123 deletions(-) create mode 100644 module/applications/settings/components/settingsActionsView.mjs create mode 100644 templates/settings/components/action-view-footer.hbs create mode 100644 templates/settings/components/action-view-header.hbs create mode 100644 templates/settings/components/action-view.hbs create mode 100644 templates/settings/components/settings-item-line.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index b4caca2a..8c329868 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -297,6 +297,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/views/actionTypes/roll.hbs', 'systems/daggerheart/templates/views/actionTypes/cost.hbs', 'systems/daggerheart/templates/views/actionTypes/range-target.hbs', - 'systems/daggerheart/templates/views/actionTypes/effect.hbs' + 'systems/daggerheart/templates/views/actionTypes/effect.hbs', + 'systems/daggerheart/templates/settings/components/settings-item-line.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index 3c739537..868ee74b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -87,6 +87,11 @@ } }, "Homebrew": { + "NewDowntimeMove": "Downtime Move", + "DowntimeMoves": "Downtime Moves", + "NrChoices": "# Moves Per Rest", + "ResetMovesTitle": "Reset {type} Downtime Moves", + "ResetMovesText": "Are you sure you want to reset?", "FIELDS": { "maxFear": { "label": "Max Fear" }, "traitArray": { "label": "Initial Trait Modifiers" } @@ -499,29 +504,51 @@ } }, "Downtime": { - "TendToWounds": { - "Name": "Tend to Wounds", - "Description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead." + "DowntimeHeader": "Downtime Moves ({current}/{max})", + "ShortRest": { + "title": "Short Rest", + "TendToWounds": { + "Name": "Tend to Wounds", + "Description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead." + }, + "ClearStress": { + "Name": "Clear Stress", + "Description": "Describe how you blow off steam or pull yourself together, then clear a number of Stress equal to 1d4 + your tier." + }, + "RepairArmor": { + "Name": "Repair Armor", + "Description": "Describe how you quickly repair your armor, then clear a number of Armor Slots equal to 1d4 + your tier. You can do this to an ally's armor instead." + }, + "Prepare": { + "Name": "Prepare", + "Description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." + } }, - "ClearStress": { - "Name": "Clear Stress", - "Description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress." + "LongRest": { + "title": "Long Rest", + "TendToWounds": { + "Name": "Tend to Wounds", + "Description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead." + }, + "ClearStress": { + "Name": "Clear Stress", + "Description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress." + }, + "RepairArmor": { + "Name": "Repair Armor", + "Description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally's armor instead." + }, + "Prepare": { + "Name": "Prepare", + "Description": "Describe how you are preparing for the next day's adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope." + }, + "WorkOnAProject": { + "Name": "Work on a Project", + "Description": "Establish or continue work on a project." + } }, - "RepairArmor": { - "Name": "Repair Armor", - "Description": "Describe how you spend time repairing your armor and clear all of its Armor Slots. You may also do this to an ally’s armor instead." - }, - "Prepare": { - "Name": "Prepare", - "Description": "Describe how you are preparing for the next day’s adventure, then gain a Hope. If you choose to Prepare with one or more members of your party, you may each take two Hope." - }, - "WorkOnAProject": { - "Name": "Work on a Project", - "Description": "Establish or continue work on a project. The GM might ask for a roll to determine how much to tick down on the completion track." - }, - "Custom": { - "NamePlaceholder": "Custom Activity...", - "Placeholder": "A custom downtime activity description..." + "Notifications": { + "NoMoreMoves": "You cannot select any more downtime moves" } }, "DeathMoves": { diff --git a/module/applications/downtime.mjs b/module/applications/downtime.mjs index 49d8b1ab..d0fc34b6 100644 --- a/module/applications/downtime.mjs +++ b/module/applications/downtime.mjs @@ -1,3 +1,5 @@ +import { actionsTypes } from '../data/_module.mjs'; + const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV2) { @@ -5,25 +7,25 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV super({}); this.actor = actor; - this.selectedActivity = null; this.shortrest = shortrest; - this.customActivity = SYSTEM.GENERAL.downtime.custom; + const options = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).restMoves; + this.moveData = shortrest ? options.shortRest : options.longRest; } get title() { - return `${this.actor.name} - ${this.shortrest ? 'Short Rest' : 'Long Rest'}`; + return ''; } static DEFAULT_OPTIONS = { tag: 'form', classes: ['daggerheart', 'views', 'downtime'], - position: { width: 800, height: 'auto' }, + position: { width: 680, height: 'auto' }, actions: { - selectActivity: this.selectActivity, + selectMove: this.selectMove, takeDowntime: this.takeDowntime }, - form: { handler: this.updateData, submitOnChange: true } + form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } }; static PARTS = { @@ -33,51 +35,63 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV } }; + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement + .querySelectorAll('.activity-image') + .forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this))); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); context.selectedActivity = this.selectedActivity; - context.options = this.shortrest ? SYSTEM.GENERAL.downtime.shortRest : SYSTEM.GENERAL.downtime.longRest; - context.customActivity = this.customActivity; - - context.disabledDowntime = - !this.selectedActivity || - (this.selectedActivity.id === this.customActivity.id && - (!this.customActivity.name || !this.customActivity.description)); + context.moveData = this.moveData; + context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); + context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices; return context; } - static selectActivity(_, button) { - const activity = button.dataset.activity; - this.selectedActivity = - activity === this.customActivity.id - ? this.customActivity - : this.shortrest - ? SYSTEM.GENERAL.downtime.shortRest[activity] - : SYSTEM.GENERAL.downtime.longRest[activity]; + static selectMove(_, button) { + const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); + if (nrSelected === this.moveData.nrChoices) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Downtime.Notifications.NoMoreMoves')); + return; + } + + const move = button.dataset.move; + this.moveData.moves[move].selected = this.moveData.moves[move].selected + ? this.moveData.moves[move].selected + 1 + : 1; + + this.render(); + } + + deselectMove(event) { + const move = event.currentTarget.dataset.move; + this.moveData.moves[move].selected = this.moveData.moves[move].selected + ? this.moveData.moves[move].selected - 1 + : 0; this.render(); } static async takeDowntime() { - const refreshedFeatures = this.shortrest - ? this.actor.system.refreshableFeatures.shortRest - : [...this.actor.system.refreshableFeatures.shortRest, ...this.actor.system.refreshableFeatures.longRest]; - for (var feature of refreshedFeatures) { - await feature.system.refresh(); - } + const moves = Object.values(this.moveData.moves).filter(x => x.selected); const cls = getDocumentClass('ChatMessage'); const msg = new cls({ user: game.user.id, + system: { + moves: moves, + actor: this.actor.uuid + }, content: await foundry.applications.handlebars.renderTemplate( 'systems/daggerheart/templates/chat/downtime.hbs', { - player: this.actor.name, - title: game.i18n.localize(this.selectedActivity.name), - img: this.selectedActivity.img, - description: game.i18n.localize(this.selectedActivity.description), - refreshedFeatures: refreshedFeatures + title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.Downtime.${this.shortRest ? 'ShortRest' : 'LongRest'}.title`)}`, + moves: moves } ) }); diff --git a/module/applications/settings/components/settingsActionsView.mjs b/module/applications/settings/components/settingsActionsView.mjs new file mode 100644 index 00000000..9b223ec5 --- /dev/null +++ b/module/applications/settings/components/settingsActionsView.mjs @@ -0,0 +1,144 @@ +import { actionsTypes } from '../../../data/_module.mjs'; +import DHActionConfig from '../../config/Action.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(resolve, reject, title, name, img, description, actions) { + super({}); + + this.resolve = resolve; + this.reject = reject; + this.viewTitle = title; + this.name = name; + this.img = img; + this.description = description; + this.actions = actions; + } + + get title() { + return this.viewTitle; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'setting', 'dh-style'], + position: { width: '400', height: 'auto' }, + actions: { + editImage: this.onEditImage, + addItem: this.addItem, + editItem: this.editItem, + removeItem: this.removeItem, + resetMoves: this.resetMoves, + saveForm: this.saveForm + }, + form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/settings/components/action-view-header.hbs' }, + main: { + template: 'systems/daggerheart/templates/settings/components/action-view.hbs' + }, + footer: { template: 'systems/daggerheart/templates/settings/components/action-view-footer.hbs' } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.name = this.name; + context.img = this.img; + context.description = this.description; + context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description); + context.actions = this.actions; + + return context; + } + + static async updateData(event, element, formData) { + const { name, img, description } = foundry.utils.expandObject(formData.object); + this.name = name; + this.description = description; + + this.render(); + } + + static async saveForm(event) { + this.resolve({ + name: this.name, + img: this.img, + description: this.description, + actions: this.actions + }); + this.close(true); + } + + static close(fromSave) { + if (!fromSave) { + this.reject(); + } + + super.close(); + } + + static onEditImage() { + const fp = new FilePicker({ + current: this.img, + type: 'image', + callback: async path => { + this.img = path; + this.render(); + }, + top: this.position.top + 40, + left: this.position.left + 10 + }); + return fp.browse(); + } + + async selectActionType() { + const content = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/views/actionType.hbs', + { types: SYSTEM.ACTIONS.actionTypes } + ), + title = 'Select Action Type', + type = 'form', + data = {}; + return Dialog.prompt({ + title, + label: title, + content, + type, + callback: html => { + const form = html[0].querySelector('form'), + fd = new foundry.applications.ux.FormDataExtended(form); + foundry.utils.mergeObject(data, fd.object, { inplace: true }); + return data; + }, + rejectClose: false + }); + } + + static async addItem() { + const actionType = await this.selectActionType(); + const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, + action = new cls({ + _id: foundry.utils.randomID(), + type: actionType.type, + name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), + ...cls.getSourceConfig(this.document) + }); + + this.actions.push(action); + this.render(); + } + + static async editItem(_, button) { + await new DHActionConfig(this.actions[button.dataset.id]).render(true); + } + + static removeItem(event, button) { + this.actions = this.actions.filter((_, index) => index !== Number.parseInt(button.dataset.id)); + this.render(); + } + + static resetMoves() {} +} diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index e1b60726..d59bc35c 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -1,8 +1,9 @@ import { DhHomebrew } from '../../data/settings/_module.mjs'; +import DhSettingsActionView from './components/settingsActionsView.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export default class DhAutomationSettings extends HandlebarsApplicationMixin(ApplicationV2) { +export default class DhHomebrewSettings extends HandlebarsApplicationMixin(ApplicationV2) { constructor() { super({}); @@ -19,7 +20,10 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App classes: ['daggerheart', 'setting', 'dh-style'], position: { width: '600', height: 'auto' }, actions: { - reset: this.reset, + addItem: this.addItem, + editItem: this.editItem, + removeItem: this.removeItem, + resetMoves: this.resetMoves, save: this.save }, form: { handler: this.updateData, submitOnChange: true } @@ -48,8 +52,101 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App this.render(); } - static async reset() { - this.settings = new DhHomebrew(); + 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: [] + } + }); + this.render(); + } + + static async editItem(_, target) { + const move = this.settings.restMoves[target.dataset.type].moves[target.dataset.id]; + new Promise((resolve, reject) => { + new DhSettingsActionView( + resolve, + reject, + game.i18n.localize('DAGGERHEART.Settings.Homebrew.DowntimeMoves'), + move.name, + move.img, + move.description, + move.actions + ).render(true); + }).then(data => this.updateAction.bind(this)(data, target.dataset.type, target.dataset.id)); + } + + async updateAction(data, type, id) { + await this.settings.updateSource({ + [`restMoves.${type}.moves.${id}`]: { + name: data.name, + img: data.img, + description: data.description + } + }); + this.render(); + } + + static async removeItem(_, target) { + await this.settings.updateSource({ + [`restMoves.${target.dataset.type}.moves.-=${target.dataset.id}`]: null + }); + this.render(); + } + + static async resetMoves(_, target) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.Settings.Homebrew.ResetMovesTitle', { + type: game.i18n.localize( + `DAGGERHEART.Downtime.${target.dataset.type === 'shortRest' ? 'ShortRest' : 'LongRest'}.title` + ) + }) + }, + content: game.i18n.localize('DAGGERHEART.Settings.Homebrew.ResetMovesText') + }); + + if (!confirmed) return; + + const fields = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew).schema.fields; + + const removeUpdate = Object.keys(this.settings.restMoves[target.dataset.type].moves).reduce((acc, key) => { + acc[`-=${key}`] = null; + + return acc; + }, {}); + + const updateBase = + target.dataset.type === 'shortRest' + ? fields.restMoves.fields.shortRest.fields + : fields.restMoves.fields.longRest.fields; + const update = { + nrChoices: updateBase.nrChoices.initial, + moves: Object.keys(updateBase.moves.initial).reduce((acc, key) => { + const move = updateBase.moves.initial[key]; + acc[key] = { + ...move, + name: game.i18n.localize(move.name), + description: game.i18n.localize(move.description) + }; + + return acc; + }, {}) + }; + + await this.settings.updateSource({ + [`restMoves.${target.dataset.type}`]: { + ...update, + moves: { + ...removeUpdate, + ...update.moves + } + } + }); + this.render(); } diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 2b0e11b9..96545cf3 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -101,65 +101,99 @@ export const conditions = { } }; -export const downtime = { - shortRest: { +export const defaultRestOptions = { + shortRest: () => ({ tendToWounds: { id: 'tendToWounds', - name: 'DAGGERHEART.Downtime.TendToWounds.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Name'), img: 'icons/magic/life/cross-worn-green.webp', - description: 'DAGGERHEART.Downtime.TendToWounds.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Description'), + actions: [ + { + type: 'healing', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.TendToWounds.Name'), + img: 'icons/magic/life/cross-worn-green.webp', + actionType: 'action', + healing: { + type: 'health', + value: { + custom: { + enabled: true, + formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? + } + } + } + } + ] }, clearStress: { id: 'clearStress', - name: 'DAGGERHEART.Downtime.ClearStress.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Name'), img: 'icons/magic/perception/eye-ringed-green.webp', - description: 'DAGGERHEART.Downtime.ClearStress.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Description'), + actions: [ + { + type: 'healing', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.ClearStress.Name'), + img: 'icons/magic/perception/eye-ringed-green.webp', + actionType: 'action', + healing: { + type: 'stress', + value: { + custom: { + enabled: true, + formula: '1d4 + 1' // should be 1d4 + {tier}. How to use the roll param? + } + } + } + } + ] }, repairArmor: { id: 'repairArmor', - name: 'DAGGERHEART.Downtime.RepairArmor.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.RepairArmor.Name'), img: 'icons/skills/trades/smithing-anvil-silver-red.webp', - description: 'DAGGERHEART.Downtime.RepairArmor.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.RepairArmor.Description') }, prepare: { id: 'prepare', - name: 'DAGGERHEART.Downtime.Prepare.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.Prepare.Name'), img: 'icons/skills/trades/academics-merchant-scribe.webp', - description: 'DAGGERHEART.Downtime.Prepare.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.ShortRest.Prepare.Description') } - }, - longRest: { + }), + longRest: () => ({ tendToWounds: { id: 'tendToWounds', - name: 'DAGGERHEART.Downtime.TendToWounds.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.TendToWounds.Name'), img: 'icons/magic/life/cross-worn-green.webp', - description: 'DAGGERHEART.Downtime.TendToWounds.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.TendToWounds.Description') }, clearStress: { id: 'clearStress', - name: 'DAGGERHEART.Downtime.ClearStress.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.ClearStress.Name'), img: 'icons/magic/perception/eye-ringed-green.webp', - description: 'DAGGERHEART.Downtime.ClearStress.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.ClearStress.Description') }, repairArmor: { id: 'repairArmor', - name: 'DAGGERHEART.Downtime.RepairArmor.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.RepairArmor.Name'), img: 'icons/skills/trades/smithing-anvil-silver-red.webp', - description: 'DAGGERHEART.Downtime.RepairArmor.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.RepairArmor.Description') }, prepare: { id: 'prepare', - name: 'DAGGERHEART.Downtime.Prepare.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.Prepare.Name'), img: 'icons/skills/trades/academics-merchant-scribe.webp', - description: 'DAGGERHEART.Downtime.Prepare.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.Prepare.Description') }, workOnAProject: { id: 'workOnAProject', - name: 'DAGGERHEART.Downtime.WorkOnAProject.Name', + name: game.i18n.localize('DAGGERHEART.Downtime.LongRest.WorkOnAProject.Name'), img: 'icons/skills/social/thumbsup-approval-like.webp', - description: 'DAGGERHEART.Downtime.WorkOnAProject.Description' + description: game.i18n.localize('DAGGERHEART.Downtime.LongRest.WorkOnAProject.Description') } - }, + }), custom: { id: 'customActivity', name: '', diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 8219f89e..273b7a72 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -87,6 +87,14 @@ export default class DhCharacter extends BaseDataActor { }; } + get tier() { + return this.levelData.level.current === 1 + ? 1 + : Object.values(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers).tiers).find( + tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end + ).tier; + } + get ancestry() { return this.parent.items.find(x => x.type === 'ancestry') ?? null; } @@ -134,19 +142,6 @@ export default class DhCharacter extends BaseDataActor { : null; } - get refreshableFeatures() { - return this.parent.items.reduce( - (acc, x) => { - if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) { - acc[x.system.refreshData.type].push(x); - } - - return acc; - }, - { shortRest: [], longRest: [] } - ); - } - static async unequipBeforeEquip(itemToEquip) { const primary = this.primaryWeapon, secondary = this.secondaryWeapon; @@ -242,6 +237,14 @@ export default class DhCharacter extends BaseDataActor { this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; this.proficiency.total = this.proficiency.value + this.proficiency.bonus; } + + getRollData() { + const data = super.getRollData(); + return { + ...data, + tier: this.tier + }; + } } class DhPCLevelData extends foundry.abstract.DataModel { diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 92999c1a..3aef56d6 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -1,3 +1,5 @@ +import { defaultRestOptions } from '../../config/generalConfig.mjs'; + export default class DhHomebrew extends foundry.abstract.DataModel { static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.Homebrew']; // Doesn't work for some reason @@ -13,6 +15,40 @@ export default class DhHomebrew extends foundry.abstract.DataModel { }), traitArray: new fields.ArrayField(new fields.NumberField({ required: true, integer: true }), { initial: () => [2, 1, 1, 0, 0, -1] + }), + restMoves: new fields.SchemaField({ + longRest: new fields.SchemaField({ + nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }), + moves: 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 fields.ArrayField(new fields.ObjectField()) + }), + { initial: defaultRestOptions.longRest() } + ) + }), + shortRest: new fields.SchemaField({ + nrChoices: new fields.NumberField({ required: true, integer: true, min: 1, initial: 2 }), + moves: 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 fields.ArrayField(new fields.ObjectField()) + }), + { initial: defaultRestOptions.shortRest() } + ) + }) }) }; } diff --git a/module/ui/chatLog.mjs b/module/ui/chatLog.mjs index 1b575aa8..75ad4bf7 100644 --- a/module/ui/chatLog.mjs +++ b/module/ui/chatLog.mjs @@ -1,3 +1,5 @@ +import { actionsTypes } from '../data/_module.mjs'; + export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor() { super(); @@ -38,6 +40,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.ability-use-button').forEach(element => element.addEventListener('click', event => this.abilityUseButton.bind(this)(event, data.message)) ); + html.querySelectorAll('.action-use-button').forEach(element => + element.addEventListener('click', event => this.actionUseButton.bind(this)(event, data.message)) + ); }; setupHooks() { @@ -137,4 +142,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const actor = game.actors.get(message.system.origin); await actor.useAction(action); }; + + actionUseButton = async (_, message) => { + const parent = await foundry.utils.fromUuid(message.system.actor); + const testAction = Object.values(message.system.moves)[0].actions[0]; + const cls = actionsTypes[testAction.type]; + const action = new cls( + { ...testAction, _id: foundry.utils.randomID(), name: game.i18n.localize(testAction.name) }, + { parent: parent } + ); + + action.use(); + }; } diff --git a/styles/application.less b/styles/application.less index 5319a35e..8cdbf1ac 100644 --- a/styles/application.less +++ b/styles/application.less @@ -134,6 +134,12 @@ div.daggerheart.views.multiclass { } .downtime-container { + .downtime-header { + margin: 0; + color: light-dark(@dark-blue, @golden); + text-align: center; + } + .activity-container { display: flex; align-items: center; @@ -150,12 +156,32 @@ div.daggerheart.views.multiclass { } .activity-image { - width: 120px; + width: 80px; + position: relative; + display: flex; + justify-content: center; + margin-right: 8px; border: 2px solid black; border-radius: 50%; - margin-right: 8px; cursor: pointer; + .activity-select-label { + position: absolute; + top: -9px; + font-size: 14px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + background-image: url(../assets/parchments/dh-parchment-light.png); + padding: 0 8px; + line-height: 1; + font-weight: bold; + } + + img { + border-radius: 50%; + } + &:hover, &.selected { filter: drop-shadow(0 0 6px gold); diff --git a/styles/daggerheart.css b/styles/daggerheart.css index a7a0cdc3..e87b899a 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1887,6 +1887,11 @@ div.daggerheart.views.multiclass { .daggerheart.views.levelup .levelup-section .levelup-container .levelup-inner-container .levelup-posttext { padding: 8px 0; } +.daggerheart.views .downtime-container .downtime-header { + margin: 0; + color: light-dark(#18162e, #f3c267); + text-align: center; +} .daggerheart.views .downtime-container .activity-container { display: flex; align-items: center; @@ -1902,12 +1907,30 @@ div.daggerheart.views.multiclass { font-weight: bold; } .daggerheart.views .downtime-container .activity-container .activity-title .activity-image { - width: 120px; + width: 80px; + position: relative; + display: flex; + justify-content: center; + margin-right: 8px; border: 2px solid black; border-radius: 50%; - margin-right: 8px; cursor: pointer; } +.daggerheart.views .downtime-container .activity-container .activity-title .activity-image .activity-select-label { + position: absolute; + top: -9px; + font-size: 14px; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + color: light-dark(#efe6d8, #222); + background-image: url(../assets/parchments/dh-parchment-light.png); + padding: 0 8px; + line-height: 1; + font-weight: bold; +} +.daggerheart.views .downtime-container .activity-container .activity-title .activity-image img { + border-radius: 50%; +} .daggerheart.views .downtime-container .activity-container .activity-title .activity-image:hover, .daggerheart.views .downtime-container .activity-container .activity-title .activity-image.selected { filter: drop-shadow(0 0 6px gold); @@ -3155,6 +3178,83 @@ div.daggerheart.views.multiclass { #resources:has(.fear-bar) { min-width: 200px; } +.daggerheart.dh-style.setting fieldset { + display: flex; + flex-direction: column; + gap: 4px; +} +.daggerheart.dh-style.setting fieldset.two-columns { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 10px; +} +.daggerheart.dh-style.setting fieldset.two-columns.even { + grid-template-columns: 1fr 1fr; +} +.daggerheart.dh-style.setting .setting-group-field { + white-space: nowrap; + display: flex; + align-items: center; + gap: 8px; +} +.daggerheart.dh-style.setting .settings-items { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.dh-style.setting .settings-items .settings-item { + display: flex; + align-items: center; + justify-content: space-between; + border: 1px solid; + border-radius: 8px; + padding: 0 8px 0 0; +} +.daggerheart.dh-style.setting .settings-items .settings-item .settings-sub-item { + display: flex; + align-items: center; + gap: 8px; +} +.daggerheart.dh-style.setting .settings-items .settings-item .settings-sub-item img { + width: 60px; + border-radius: 8px 0 0 8px; +} +.daggerheart.dh-style.setting .settings-items .settings-item .settings-sub-item i { + font-size: 18px; +} +.daggerheart.dh-style.setting .settings-item-header { + display: flex; + align-items: center; +} +.daggerheart.dh-style.setting .settings-item-header .profile { + height: 100px; + width: 100px; + object-fit: cover; + box-sizing: border-box; + cursor: pointer; +} +.daggerheart.dh-style.setting .settings-item-header .item-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + text-align: center; + width: 80%; +} +.daggerheart.dh-style.setting .settings-item-header .item-info .item-name input[type='text'] { + font-size: 32px; + height: 42px; + text-align: center; + width: 90%; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; +} +.daggerheart.dh-style.setting .settings-item-header .item-info .item-name input[type='text']:hover[type='text'], +.daggerheart.dh-style.setting .settings-item-header .item-info .item-name input[type='text']:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(#18162e, #f3c267); +} .daggerheart.dh-style.setting .settings-col { display: flex; flex-direction: column; diff --git a/styles/settings.less b/styles/settings.less index e37f9891..d3e63a2d 100644 --- a/styles/settings.less +++ b/styles/settings.less @@ -1,4 +1,95 @@ .daggerheart.dh-style.setting { + fieldset { + display: flex; + flex-direction: column; + gap: 4px; + + &.two-columns { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 10px; + + &.even { + grid-template-columns: 1fr 1fr; + } + } + } + + .setting-group-field { + white-space: nowrap; + display: flex; + align-items: center; + gap: 8px; + } + + .settings-items { + display: flex; + flex-direction: column; + gap: 8px; + + .settings-item { + display: flex; + align-items: center; + justify-content: space-between; + border: 1px solid; + border-radius: 8px; + padding: 0 8px 0 0; + + .settings-sub-item { + display: flex; + align-items: center; + gap: 8px; + + img { + width: 60px; + border-radius: 8px 0 0 8px; + } + + i { + font-size: 18px; + } + } + } + } + + .settings-item-header { + display: flex; + align-items: center; + + .profile { + height: 100px; + width: 100px; + object-fit: cover; + box-sizing: border-box; + cursor: pointer; + } + + .item-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + text-align: center; + width: 80%; + + .item-name input[type='text'] { + font-size: 32px; + height: 42px; + text-align: center; + width: 90%; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; + + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(@dark-blue, @golden); + } + } + } + } + .settings-col { display: flex; flex-direction: column; diff --git a/templates/chat/downtime.hbs b/templates/chat/downtime.hbs index 2c7c200e..98a7a227 100644 --- a/templates/chat/downtime.hbs +++ b/templates/chat/downtime.hbs @@ -1,16 +1,11 @@

-
{{this.player}} {{localize "DAGGERHEART.Chat.Downtime.Title"}}
-
{{this.title}}
+
{{title}}

- -
{{{this.description}}}
-
-
Refreshed Features
- {{#each this.refreshedFeatures}} -
-
{{this.name}}
-
- {{/each}} -
+ {{#each moves}} + {{this.name}} + +
{{{this.description}}}
+ {{#if (gt this.actions.length 0)}}{{/if}} + {{/each}}
\ No newline at end of file diff --git a/templates/settings/components/action-view-footer.hbs b/templates/settings/components/action-view-footer.hbs new file mode 100644 index 00000000..309f383f --- /dev/null +++ b/templates/settings/components/action-view-footer.hbs @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/templates/settings/components/action-view-header.hbs b/templates/settings/components/action-view-header.hbs new file mode 100644 index 00000000..a5269faf --- /dev/null +++ b/templates/settings/components/action-view-header.hbs @@ -0,0 +1,6 @@ +
+ +
+

+
+
\ No newline at end of file diff --git a/templates/settings/components/action-view.hbs b/templates/settings/components/action-view.hbs new file mode 100644 index 00000000..6c01b072 --- /dev/null +++ b/templates/settings/components/action-view.hbs @@ -0,0 +1,18 @@ +
+
+ {{localize "Description"}} + + + {{{ enrichedDescription }}} + +
+ {{!--
+ {{localize "Actions"}} + +
+ {{#each this.actions as |action index|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" action type="actions" id=index }} + {{/each}} +
+
--}} +
\ No newline at end of file diff --git a/templates/settings/components/settings-item-line.hbs b/templates/settings/components/settings-item-line.hbs new file mode 100644 index 00000000..039301ec --- /dev/null +++ b/templates/settings/components/settings-item-line.hbs @@ -0,0 +1,22 @@ +
+
+ +
{{this.name}}
+
+ +
\ No newline at end of file diff --git a/templates/settings/homebrew-settings.hbs b/templates/settings/homebrew-settings.hbs index aa866378..2e6ddb10 100644 --- a/templates/settings/homebrew-settings.hbs +++ b/templates/settings/homebrew-settings.hbs @@ -10,6 +10,52 @@
{{/each}}
+ +
+ {{localize "DAGGERHEART.Settings.Homebrew.DowntimeMoves"}} + +
+ + {{localize "DAGGERHEART.Downtime.LongRest.title"}} + + + + +
+ +
+ +
+
+ +
+ {{#each settingFields._source.restMoves.longRest.moves as |move id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="longRest" id=id }} + {{/each}} +
+
+ +
+ + {{localize "DAGGERHEART.Downtime.ShortRest.title"}} + + + + +
+ +
+ +
+
+ +
+ {{#each settingFields._source.restMoves.shortRest.moves as |move id|}} + {{> "systems/daggerheart/templates/settings/components/settings-item-line.hbs" this type="shortRest" id=id }} + {{/each}} +
+
+
- + +
\ No newline at end of file From c15d55a50537861fc37be6a6e2c2e2149ab98af0 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:37:22 +0200 Subject: [PATCH 06/13] 120 - Countdowns (#158) * Added the shell of the Countdown application * Added countdown automation * Fixed overflow layout and added confirmation on countdown removal * Added ownership to countdowns --- daggerheart.mjs | 56 ++-- lang/en.json | 43 +++ module/applications/_module.mjs | 2 +- module/applications/chatMessage.mjs | 4 +- module/applications/countdowns.mjs | 343 ++++++++++++++++++++ module/applications/ownershipSelection.mjs | 72 ++++ module/applications/settings.mjs | 7 + module/applications/sheets/character.mjs | 6 +- module/config/generalConfig.mjs | 14 + module/config/hooksConfig.mjs | 4 + module/config/settingsConfig.mjs | 3 +- module/config/system.mjs | 4 +- module/data/chat-message/_modules.mjs | 25 +- module/data/countdowns.mjs | 139 ++++++++ module/data/item/base.mjs | 71 ++-- module/data/item/class.mjs | 10 +- module/data/item/consumable.mjs | 10 +- module/data/item/miscellaneous.mjs | 6 +- module/data/item/subclass.mjs | 2 +- module/data/settings/Automation.mjs | 3 +- module/documents/actor.mjs | 4 +- module/helpers/socket.mjs | 58 +++- module/ui/combatTracker.mjs | 11 +- styles/chat.less | 21 +- styles/countdown.less | 137 ++++++++ styles/daggerheart.css | 151 ++++++++- styles/daggerheart.less | 2 + styles/ownershipSelection.less | 22 ++ styles/ui.less | 46 ++- templates/settings/automation-settings.hbs | 6 + templates/ui/combat/combatTrackerHeader.hbs | 13 +- templates/views/countdowns.hbs | 42 +++ templates/views/ownershipSelection.hbs | 22 ++ 33 files changed, 1222 insertions(+), 137 deletions(-) create mode 100644 module/applications/countdowns.mjs create mode 100644 module/applications/ownershipSelection.mjs create mode 100644 module/config/hooksConfig.mjs create mode 100644 module/data/countdowns.mjs create mode 100644 styles/countdown.less create mode 100644 styles/ownershipSelection.less create mode 100644 templates/views/countdowns.hbs create mode 100644 templates/views/ownershipSelection.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 8c329868..2b25ce77 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -4,7 +4,7 @@ import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import DhCombatTracker from './module/ui/combatTracker.mjs'; -import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs'; +import { handleSocketEvent, registerSocketHooks } from './module/helpers/socket.mjs'; import { registerDHSettings } from './module/applications/settings.mjs'; import DhpChatLog from './module/ui/chatLog.mjs'; import DhpRuler from './module/ui/ruler.mjs'; @@ -13,11 +13,13 @@ import { DhDualityRollEnricher, DhTemplateEnricher } from './module/enrichers/_m import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.mjs'; +import { NarrativeCountdowns, registerCountdownApplicationHooks } from './module/applications/countdowns.mjs'; import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; import { DualityRollColor } from './module/data/settings/Appearance.mjs'; import { DhMeasuredTemplate } from './module/placeables/_module.mjs'; import { renderDualityButton } from './module/enrichers/DualityRollEnricher.mjs'; import { renderMeasuredTemplate } from './module/enrichers/TemplateEnricher.mjs'; +import { registerCountdownHooks } from './module/data/countdowns.mjs'; globalThis.SYSTEM = SYSTEM; @@ -126,39 +128,20 @@ Hooks.on('ready', () => { ui.resources = new CONFIG.ui.resources(); if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') ui.resources.render({ force: true }); + document.body.classList.toggle( 'theme-colorful', game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).dualityColorScheme === DualityRollColor.colorful.value ); + + registerCountdownHooks(); + registerSocketHooks(); + registerCountdownApplicationHooks(); }); Hooks.once('dicesoniceready', () => {}); -Hooks.on(socketEvent.GMUpdate, async (action, uuid, update) => { - if (game.user.isGM) { - const document = uuid ? await fromUuid(uuid) : null; - switch (action) { - case GMUpdateEvent.UpdateDocument: - if (document && update) { - await document.update(update); - } - break; - case GMUpdateEvent.UpdateFear: - if (game.user.isGM) { - await game.settings.set( - SYSTEM.id, - SYSTEM.SETTINGS.gameSettings.Resources.Fear, - Math.max(Math.min(update, 6), 0) - ); - Hooks.callAll(socketEvent.DhpFearUpdate); - await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate }); - } - break; - } - } -}); - Hooks.on('renderChatMessageHTML', (_, element) => { element .querySelectorAll('.duality-roll-button') @@ -264,6 +247,29 @@ Hooks.on('chatMessage', (_, message) => { } }); +Hooks.on('renderJournalDirectory', async (tab, html, _, options) => { + if (tab.id === 'journal') { + if (options.parts && !options.parts.includes('footer')) return; + + const buttons = tab.element.querySelector('.directory-footer.action-buttons'); + const title = game.i18n.format('DAGGERHEART.Countdown.Title', { + type: game.i18n.localize('DAGGERHEART.Countdown.Types.narrative') + }); + buttons.insertAdjacentHTML( + 'afterbegin', + ` + ` + ); + + buttons.querySelector('#narrative-countdown-button').onclick = async () => { + new NarrativeCountdowns().open(); + }; + } +}); + const preloadHandlebarsTemplates = async function () { return foundry.applications.handlebars.loadTemplates([ 'systems/daggerheart/templates/sheets/parts/attributes.hbs', diff --git a/lang/en.json b/lang/en.json index 868ee74b..e4ad9cd5 100755 --- a/lang/en.json +++ b/lang/en.json @@ -83,6 +83,10 @@ "actionPoints": { "label": "Action Points", "hint": "Automatically give and take Action Points as combatants take their turns." + }, + "countdowns": { + "label": "Countdowns", + "hint": "Automatically progress non-custom countdowns" } } }, @@ -1027,6 +1031,45 @@ "Title": "Downtime" } }, + "Countdown": { + "FIELDS": { + "countdowns": { + "element": { + "name": { "label": "Name" }, + "progress": { + "current": { "label": "Current" }, + "max": { "label": "Max" }, + "type": { + "value": { "label": "Value" }, + "label": { "label": "Label", "hint": "Used for custom" } + } + } + } + } + }, + "Type": { + "Spotlight": "Spotlight", + "Custom": "Custom", + "CharacterAttack": "Character Attack" + }, + "NewCountdown": "New Countdown", + "AddCountdown": "Add Countdown", + "RemoveCountdownTitle": "Remove Countdown", + "RemoveCountdownText": "Are you sure you want to remove the countdown: {name}?", + "OpenOwnership": "Edit Player Ownership", + "Title": "{type} Countdowns", + "Types": { + "narrative": "Narrative", + "encounter": "Encounter" + }, + "Notifications": { + "LimitedOwnershipMaximise": "You don't have permission to enter edit view" + } + }, + "OwnershipSelection": { + "Title": "Ownership Selection - {name}", + "Default": "Default Ownership" + }, "Sheets": { "PC": { "Name": "Name", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 65eafe09..b1a1d59e 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -12,4 +12,4 @@ export { default as DhpWeapon } from './sheets/items/weapon.mjs'; export { default as DhpArmor } from './sheets/items/armor.mjs'; export { default as DhpChatMessage } from './chatMessage.mjs'; export { default as DhpEnvironment } from './sheets/environment.mjs'; -export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; \ No newline at end of file +export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 4850a0d2..e7bcd269 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -10,9 +10,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); - if ( - this.type === 'dualityRoll' - ) { + if (this.type === 'dualityRoll') { html.classList.add('duality'); const dualityResult = this.system.dualityResult; if (dualityResult === DHDualityRoll.dualityResult.hope) html.classList.add('hope'); diff --git a/module/applications/countdowns.mjs b/module/applications/countdowns.mjs new file mode 100644 index 00000000..354541fe --- /dev/null +++ b/module/applications/countdowns.mjs @@ -0,0 +1,343 @@ +import { countdownTypes } from '../config/generalConfig.mjs'; +import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs'; +import OwnershipSelection from './ownershipSelection.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(basePath) { + super({}); + + this.basePath = basePath; + } + + get title() { + return game.i18n.format('DAGGERHEART.Countdown.Title', { + type: game.i18n.localize(`DAGGERHEART.Countdown.Types.${this.basePath}`) + }); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'countdown'], + tag: 'form', + position: { width: 740, height: 700 }, + window: { + frame: true, + title: 'Countdowns', + resizable: true, + minimizable: true + }, + actions: { + addCountdown: this.addCountdown, + removeCountdown: this.removeCountdown, + editImage: this.onEditImage, + openOwnership: this.openOwnership, + openCountdownOwnership: this.openCountdownOwnership + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + countdowns: { + template: 'systems/daggerheart/templates/views/countdowns.hbs', + scrollable: ['.expanded-view'] + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { + element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, true)); + element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, false)); + }); + } + + async _onFirstRender(context, options) { + super._onFirstRender(context, options); + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + const countdownData = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + + context.isGM = game.user.isGM; + context.base = this.basePath; + + context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + context.source = { + ...countdownData, + countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { + const countdown = countdownData.countdowns[key]; + + const ownershipValue = countdown.playerOwnership[game.user.id].value; + if (ownershipValue > CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { + acc[key] = { ...countdown, canEdit: ownershipValue === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }; + } + + return acc; + }, {}) + }; + context.systemFields = countdownData.schema.fields; + context.countdownFields = context.systemFields.countdowns.element.fields; + context.minimized = this.minimized || _options.isFirstRender; + + return context; + } + + static async updateData(event, _, formData) { + const data = foundry.utils.expandObject(formData.object); + const newSetting = foundry.utils.mergeObject( + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns).toObject(), + data + ); + + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, newSetting); + this.render(); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateSetting, + uuid: SYSTEM.SETTINGS.gameSettings.Countdowns, + update: newSetting + } + }); + } + } + + async minimize() { + await super.minimize(); + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + async maximize() { + if (this.minimized) { + const settings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + if (settings.playerOwnership[game.user.id].value <= CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED) { + ui.notifications.info(game.i18n.localize('DAGGERHEART.Countdown.Notifications.LimitedOwnership')); + return; + } + + this.element.querySelector('.expanded-view').classList.toggle('hidden'); + this.element.querySelector('.minimized-view').classList.toggle('hidden'); + } + + await super.maximize(); + } + + async updateSetting(update) { + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update); + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.Refresh, + data: { + refreshType: RefreshType.Countdown, + application: `${this.basePath}-countdowns` + } + }); + + this.render(); + } else { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateSetting, + uuid: SYSTEM.SETTINGS.gameSettings.Countdowns, + update: update, + refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` } + } + }); + } + } + + static onEditImage(_, target) { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + const current = setting.countdowns[target.dataset.countdown].img; + const fp = new FilePicker({ + current, + type: 'image', + callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown), + top: this.position.top + 40, + left: this.position.left + 10 + }); + return fp.browse(); + } + + async updateImage(path, countdown) { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.countdowns.${countdown}.img`]: path + }); + + await this.updateSetting(setting); + } + + static openOwnership(_, target) { + new Promise((resolve, reject) => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; + const ownership = { default: setting.ownership.default, players: setting.playerOwnership }; + new OwnershipSelection(resolve, reject, this.title, ownership).render(true); + }).then(async ownership => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.ownership`]: ownership + }); + + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting.toObject()); + this.render(); + }); + } + + static openCountdownOwnership(_, target) { + const countdownId = target.dataset.countdown; + new Promise((resolve, reject) => { + const countdown = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath] + .countdowns[countdownId]; + const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership }; + new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true); + }).then(async ownership => { + const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await setting.updateSource({ + [`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership + }); + + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, setting); + this.render(); + }); + } + + async updateCountdownValue(event, increase) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown]; + + if (countdown.playerOwnership[game.user.id] < CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) { + return; + } + + const currentValue = countdown.progress.current; + + if (increase && currentValue === countdown.progress.max) return; + if (!increase && currentValue === 0) return; + + await countdownSetting.updateSource({ + [`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase + ? currentValue + 1 + : currentValue - 1 + }); + + await this.updateSetting(countdownSetting.toObject()); + } + + static async addCountdown() { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + await countdownSetting.updateSource({ + [`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: { + name: game.i18n.localize('DAGGERHEART.Countdown.NewCountdown'), + ownership: game.user.isGM + ? {} + : { + players: { + [game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER } + } + } + } + }); + + await this.updateSetting(countdownSetting.toObject()); + } + + static async removeCountdown(_, target) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name; + + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.Countdown.RemoveCountdownTitle') + }, + content: game.i18n.format('DAGGERHEART.Countdown.RemoveCountdownText', { name: countdownName }) + }); + if (!confirmed) return; + + await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null }); + + await this.updateSetting(countdownSetting.toObject()); + } + + async open() { + await this.render(true); + if ( + Object.keys(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns) + .length > 0 + ) { + this.minimize(); + } + } +} + +export class NarrativeCountdowns extends Countdowns { + constructor() { + super('narrative'); + } + + static DEFAULT_OPTIONS = { + id: 'narrative-countdowns' + }; +} + +export class EncounterCountdowns extends Countdowns { + constructor() { + super('encounter'); + } + + static DEFAULT_OPTIONS = { + id: 'encounter-countdowns' + }; +} + +export const registerCountdownApplicationHooks = () => { + const updateCountdowns = async shouldIncrease => { + if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).countdowns) { + const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns); + for (let countdownCategoryKey in countdownSetting) { + const countdownCategory = countdownSetting[countdownCategoryKey]; + for (let countdownKey in countdownCategory.countdowns) { + const countdown = countdownCategory.countdowns[countdownKey]; + + if (shouldIncrease(countdown)) { + await countdownSetting.updateSource({ + [`${countdownCategoryKey}.countdowns.${countdownKey}.progress.current`]: + countdown.progress.current + 1 + }); + await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, countdownSetting); + foundry.applications.instances.get(`${countdownCategoryKey}-countdowns`)?.render(); + } + } + } + } + }; + + Hooks.on(SYSTEM.HOOKS.characterAttack, async () => { + updateCountdowns(countdown => { + return ( + countdown.progress.type.value === countdownTypes.characterAttack.id && + countdown.progress.current < countdown.progress.max + ); + }); + }); + + Hooks.on(SYSTEM.HOOKS.spotlight, async () => { + updateCountdowns(countdown => { + return ( + countdown.progress.type.value === countdownTypes.spotlight.id && + countdown.progress.current < countdown.progress.max + ); + }); + }); +}; diff --git a/module/applications/ownershipSelection.mjs b/module/applications/ownershipSelection.mjs new file mode 100644 index 00000000..b8de22f8 --- /dev/null +++ b/module/applications/ownershipSelection.mjs @@ -0,0 +1,72 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(resolve, reject, name, ownership) { + super({}); + + this.resolve = resolve; + this.reject = reject; + this.name = name; + this.ownership = ownership; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'ownership-selection'], + position: { + width: 600, + height: 'auto' + }, + form: { handler: this.updateData } + }; + + static PARTS = { + selection: { + template: 'systems/daggerheart/templates/views/ownershipSelection.hbs' + } + }; + + get title() { + return game.i18n.format('DAGGERHEART.OwnershipSelection.Title', { name: this.name }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ + value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], + label: game.i18n.localize(`OWNERSHIP.${level}`) + })); + context.ownership = { + default: this.ownership.default, + players: Object.keys(this.ownership.players).reduce((acc, x) => { + const user = game.users.get(x); + if (!user.isGM) { + acc[x] = { + img: user.character?.img, + name: user.name, + ownership: this.ownership.players[x].value + }; + } + + return acc; + }, {}) + }; + + return context; + } + + static async updateData(event, _, formData) { + const { ownership } = foundry.utils.expandObject(formData.object); + + this.resolve(ownership); + this.close(true); + } + + async close(fromSave) { + if (!fromSave) { + this.reject(); + } + + await super.close(); + } +} diff --git a/module/applications/settings.mjs b/module/applications/settings.mjs index cb1aa1bd..8cfc5161 100644 --- a/module/applications/settings.mjs +++ b/module/applications/settings.mjs @@ -1,4 +1,5 @@ import { defaultLevelTiers, DhLevelTiers } from '../data/levelTier.mjs'; +import DhCountdowns from '../data/countdowns.mjs'; import { DhAppearance, DhAutomation, @@ -130,4 +131,10 @@ const registerNonConfigSettings = () => { ui.combat.render({ force: true }); } }); + + game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, { + scope: 'world', + config: false, + type: DhCountdowns + }); }; diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index fe0beec9..0c98d2bc 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -371,7 +371,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { static async attackRoll(event, button) { const weapon = await fromUuid(button.dataset.weapon); if (!weapon) return; - weapon.use(event); + + const wasUsed = await weapon.use(event); + if (wasUsed) { + Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + } } static openLevelUp() { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 96545cf3..279300fc 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -364,6 +364,20 @@ export const abilityCosts = { } }; +export const countdownTypes = { + spotlight: { + id: 'spotlight', + label: 'DAGGERHEART.Countdown.Type.Spotlight' + }, + characterAttack: { + id: 'characterAttack', + label: 'DAGGERHEART.Countdown.Type.CharacterAttack' + }, + custom: { + id: 'custom', + label: 'DAGGERHEART.Countdown.Type.Custom' + } +}; export const rollTypes = { weapon: { id: 'weapon', diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs new file mode 100644 index 00000000..8410c0de --- /dev/null +++ b/module/config/hooksConfig.mjs @@ -0,0 +1,4 @@ +export const hooks = { + characterAttack: 'characterAttackHook', + spotlight: 'spotlightHook' +}; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index be793199..95a3de0e 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -26,7 +26,8 @@ export const gameSettings = { Resources: { Fear: 'ResourcesFear' }, - LevelTiers: 'LevelTiers' + LevelTiers: 'LevelTiers', + Countdowns: 'Countdowns' }; export const DualityRollColor = { diff --git a/module/config/system.mjs b/module/config/system.mjs index b9755317..41d67154 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -3,6 +3,7 @@ import * as DOMAIN from './domainConfig.mjs'; import * as ACTOR from './actorConfig.mjs'; import * as ITEM from './itemConfig.mjs'; import * as SETTINGS from './settingsConfig.mjs'; +import { hooks as HOOKS } from './hooksConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; @@ -15,6 +16,7 @@ export const SYSTEM = { ACTOR, ITEM, SETTINGS, + HOOKS, EFFECTS, - ACTIONS, + ACTIONS }; diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 0432a789..7ee3d0b6 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,18 +1,13 @@ -import DHAbilityUse from "./abilityUse.mjs"; -import DHAdversaryRoll from "./adversaryRoll.mjs"; -import DHDamageRoll from "./damageRoll.mjs"; -import DHDualityRoll from "./dualityRoll.mjs"; +import DHAbilityUse from './abilityUse.mjs'; +import DHAdversaryRoll from './adversaryRoll.mjs'; +import DHDamageRoll from './damageRoll.mjs'; +import DHDualityRoll from './dualityRoll.mjs'; -export { - DHAbilityUse, - DHAdversaryRoll, - DHDamageRoll, - DHDualityRoll, -} +export { DHAbilityUse, DHAdversaryRoll, DHDamageRoll, DHDualityRoll }; export const config = { - abilityUse: DHAbilityUse, - adversaryRoll: DHAdversaryRoll, - damageRoll: DHDamageRoll, - dualityRoll: DHDualityRoll, -}; \ No newline at end of file + abilityUse: DHAbilityUse, + adversaryRoll: DHAdversaryRoll, + damageRoll: DHDamageRoll, + dualityRoll: DHDualityRoll +}; diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs new file mode 100644 index 00000000..fec2b790 --- /dev/null +++ b/module/data/countdowns.mjs @@ -0,0 +1,139 @@ +import { countdownTypes } from '../config/generalConfig.mjs'; +import { RefreshType, socketEvent } from '../helpers/socket.mjs'; + +export default class DhCountdowns extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + narrative: new fields.EmbeddedDataField(DhCountdownData), + encounter: new fields.EmbeddedDataField(DhCountdownData) + }; + } + + static CountdownCategories = { narrative: 'narrative', combat: 'combat' }; +} + +class DhCountdownData extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Countdown']; // Nots ure why this won't work. Setting labels manually for now + + static defineSchema() { + const fields = foundry.data.fields; + return { + countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), + ownership: new fields.SchemaField({ + default: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE + }), + players: new fields.TypedObjectField( + new fields.SchemaField({ + type: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + }) + ) + }) + }; + } + + get playerOwnership() { + return Array.from(game.users).reduce((acc, user) => { + acc[user.id] = { + value: user.isGM + ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER + : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 + ? this.ownership.players[user.id].type + : this.ownership.default, + isGM: user.isGM + }; + + return acc; + }, {}); + } +} + +class DhCountdown extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + name: new fields.StringField({ + required: true, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.name.label' + }), + img: new fields.FilePathField({ + categories: ['IMAGE'], + base64: false, + initial: 'icons/magic/time/hourglass-yellow-green.webp' + }), + ownership: new fields.SchemaField({ + default: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE + }), + players: new fields.TypedObjectField( + new fields.SchemaField({ + type: new fields.NumberField({ + required: true, + choices: Object.values(CONST.DOCUMENT_OWNERSHIP_LEVELS), + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + }) + ) + }), + progress: new fields.SchemaField({ + current: new fields.NumberField({ + required: true, + integer: true, + initial: 0, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.current.label' + }), + max: new fields.NumberField({ + required: true, + integer: true, + initial: 1, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.max.label' + }), + type: new fields.SchemaField({ + value: new fields.StringField({ + required: true, + choices: countdownTypes, + initial: countdownTypes.spotlight.id, + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.value.label' + }), + label: new fields.StringField({ + label: 'DAGGERHEART.Countdown.FIELDS.countdowns.element.progress.type.label.label' + }) + }) + }) + }; + } + + get playerOwnership() { + return Array.from(game.users).reduce((acc, user) => { + acc[user.id] = { + value: user.isGM + ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER + : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 + ? this.ownership.players[user.id].type + : this.ownership.default, + isGM: user.isGM + }; + + return acc; + }, {}); + } +} + +export const registerCountdownHooks = () => { + Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { + if (refreshType === RefreshType.Countdown) { + foundry.applications.instances.get(application)?.render(); + return false; + } + }); +}; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 6f557866..492fcfe1 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -10,45 +10,44 @@ const fields = foundry.data.fields; export default class BaseDataItem extends foundry.abstract.TypeDataModel { - /** @returns {ItemDataModelMetadata}*/ - static get metadata() { - return { - label: "Base Item", - type: "base", - hasDescription: false, - isQuantifiable: false, - }; - } + /** @returns {ItemDataModelMetadata}*/ + static get metadata() { + return { + label: 'Base Item', + type: 'base', + hasDescription: false, + isQuantifiable: false + }; + } - /** @inheritDoc */ - static defineSchema() { - const schema = {}; + /** @inheritDoc */ + static defineSchema() { + const schema = {}; - if (this.metadata.hasDescription) - schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); - if (this.metadata.isQuantifiable) - schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); + if (this.metadata.isQuantifiable) + schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); - return schema; - } + return schema; + } - /** - * Convenient access to the item's actor, if it exists. - * @returns {foundry.documents.Actor | null} - */ - get actor() { - return this.parent.actor; - } + /** + * Convenient access to the item's actor, if it exists. + * @returns {foundry.documents.Actor | null} + */ + get actor() { + return this.parent.actor; + } - /** - * Obtain a data object used to evaluate any dice rolls associated with this Item Type - * @param {object} [options] - Options which modify the getRollData method. - * @returns {object} - */ - getRollData(options = {}) { - const actorRollData = this.actor?.getRollData() ?? {}; - const data = { ...actorRollData, item: { ...this } }; - return data; - } -} \ No newline at end of file + /** + * Obtain a data object used to evaluate any dice rolls associated with this Item Type + * @param {object} [options] - Options which modify the getRollData method. + * @returns {object} + */ + getRollData(options = {}) { + const actorRollData = this.actor?.getRollData() ?? {}; + const data = { ...actorRollData, item: { ...this } }; + return data; + } +} diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index e796dd75..cd69648d 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -19,16 +19,16 @@ export default class DHClass extends BaseDataItem { return { ...super.defineSchema(), domains: new fields.ArrayField(new fields.StringField(), { max: 2 }), - classItems: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + classItems: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), evasion: new fields.NumberField({ initial: 0, integer: true }), hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()), classFeatures: new foundry.data.fields.ArrayField(new ActionField()), - subclasses: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), inventory: new fields.SchemaField({ - take: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), - choiceA: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), - choiceB: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), + choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), + choiceB: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }) }), characterGuide: new fields.SchemaField({ suggestedTraits: new fields.SchemaField({ diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index aff7eea0..6c8df798 100644 --- a/module/data/item/consumable.mjs +++ b/module/data/item/consumable.mjs @@ -1,14 +1,14 @@ -import BaseDataItem from "./base.mjs"; +import BaseDataItem from './base.mjs'; import ActionField from '../fields/actionField.mjs'; export default class DHConsumable extends BaseDataItem { - /** @inheritDoc */ + /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.consumable", - type: "consumable", + label: 'TYPES.Item.consumable', + type: 'consumable', hasDescription: true, - isQuantifiable: true, + isQuantifiable: true }); } diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index 71daad57..d7687dc7 100644 --- a/module/data/item/miscellaneous.mjs +++ b/module/data/item/miscellaneous.mjs @@ -5,10 +5,10 @@ export default class DHMiscellaneous extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.miscellaneous", - type: "miscellaneous", + label: 'TYPES.Item.miscellaneous', + type: 'miscellaneous', hasDescription: true, - isQuantifiable: true, + isQuantifiable: true }); } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 75345625..61eec6c4 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -5,7 +5,7 @@ import BaseDataItem from './base.mjs'; const featureSchema = () => { return new foundry.data.fields.SchemaField({ name: new foundry.data.fields.StringField({ required: true }), - effects: new ForeignDocumentUUIDArrayField({type: 'Item', required: false}), + effects: new ForeignDocumentUUIDArrayField({ type: 'ActiveEffect', required: false }), actions: new foundry.data.fields.ArrayField(new ActionField()) }); }; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index 85ed8bd4..bf2aed4b 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -5,7 +5,8 @@ export default class DhAutomation extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { hope: new fields.BooleanField({ required: true, initial: false }), - actionPoints: new fields.BooleanField({ required: true, initial: false }) + actionPoints: new fields.BooleanField({ required: true, initial: false }), + countdowns: new fields.BooleanField({ requireD: true, initial: false }) }; } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index fea4a426..6b9571a9 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -301,7 +301,7 @@ export default class DhpActor extends Actor { ); if (this.type === 'character') { - const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); + const automateHope = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope; if (automateHope && result.hopeUsed) { await this.update({ @@ -330,7 +330,7 @@ export default class DhpActor extends Actor { hope = roll.dice[0].results[0].result; fear = roll.dice[1].results[0].result; if ( - game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope) && + game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation).hope && config.roll.type === 'action' ) { if (hope > fear) { diff --git a/module/helpers/socket.mjs b/module/helpers/socket.mjs index fd768d34..6cd041aa 100644 --- a/module/helpers/socket.mjs +++ b/module/helpers/socket.mjs @@ -1,20 +1,68 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { switch (action) { case socketEvent.GMUpdate: - Hooks.callAll(socketEvent.GMUpdate, data.action, data.uuid, data.update); + Hooks.callAll(socketEvent.GMUpdate, data); break; case socketEvent.DhpFearUpdate: Hooks.callAll(socketEvent.DhpFearUpdate); break; + case socketEvent.Refresh: + Hooks.call(socketEvent.Refresh, data); + break; } } export const socketEvent = { - GMUpdate: 'DhpGMUpdate', - DhpFearUpdate: 'DhpFearUpdate' + GMUpdate: 'DhGMUpdate', + Refresh: 'DhRefresh', + DhpFearUpdate: 'DhFearUpdate' }; export const GMUpdateEvent = { - UpdateDocument: 'DhpGMUpdateDocument', - UpdateFear: 'DhpUpdateFear' + UpdateDocument: 'DhGMUpdateDocument', + UpdateSetting: 'DhGMUpdateSetting', + UpdateFear: 'DhGMUpdateFear' +}; + +export const RefreshType = { + Countdown: 'DhCoundownRefresh' +}; + +export const registerSocketHooks = () => { + Hooks.on(socketEvent.GMUpdate, async data => { + if (game.user.isGM) { + const document = data.uuid ? await fromUuid(data.uuid) : null; + switch (data.action) { + case GMUpdateEvent.UpdateDocument: + if (document && data.update) { + await document.update(data.update); + } + break; + case GMUpdateEvent.UpdateSetting: + if (game.user.isGM) { + await game.settings.set(SYSTEM.id, data.uuid, data.update); + } + break; + case GMUpdateEvent.UpdateFear: + if (game.user.isGM) { + await game.settings.set( + SYSTEM.id, + SYSTEM.SETTINGS.gameSettings.Resources.Fear, + Math.max(Math.min(data.update, 6), 0) + ); + Hooks.callAll(socketEvent.DhpFearUpdate); + await game.socket.emit(`system.${SYSTEM.id}`, { action: socketEvent.DhpFearUpdate }); + } + break; + } + + if (data.refresh) { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.Refresh, + data: data.refresh + }); + Hooks.call(socketEvent.Refresh, data.refresh); + } + } + }); }; diff --git a/module/ui/combatTracker.mjs b/module/ui/combatTracker.mjs index 8b71f627..d6d64f98 100644 --- a/module/ui/combatTracker.mjs +++ b/module/ui/combatTracker.mjs @@ -1,9 +1,12 @@ +import { EncounterCountdowns } from '../applications/countdowns.mjs'; + export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { static DEFAULT_OPTIONS = { actions: { requestSpotlight: this.requestSpotlight, toggleSpotlight: this.toggleSpotlight, - setActionTokens: this.setActionTokens + setActionTokens: this.setActionTokens, + openCountdowns: this.openCountdowns } }; @@ -83,6 +86,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C .map(x => x.id) .indexOf(combatantId); + if (this.viewed.turn !== toggleTurn) Hooks.callAll(SYSTEM.HOOKS.spotlight, {}); + await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn }); await combatant.update({ 'system.spotlight.requesting': false }); } @@ -97,4 +102,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C await combatant.update({ 'system.actionTokens': newIndex }); this.render(); } + + static openCountdowns() { + new EncounterCountdowns().open(); + } } diff --git a/styles/chat.less b/styles/chat.less index 883af72b..c1de3106 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -1,5 +1,7 @@ .chat-message { - .duality-modifiers, .duality-result, .dice-title { + .duality-modifiers, + .duality-result, + .dice-title { display: none; } } @@ -67,7 +69,8 @@ align-items: center; justify-content: center; position: relative; - &.hope, &.fear { + &.hope, + &.fear { .dice-wrapper { clip-path: polygon( 50% 0%, @@ -335,8 +338,10 @@ > * { padding: 0 8px; } - .message-content { - .duality-modifiers, .duality-result, .dice-title { + .message-content { + .duality-modifiers, + .duality-result, + .dice-title { display: flex; } .duality-modifiers { @@ -364,7 +369,7 @@ } .dice-result { .duality-modifiers { - display: flex; // Default => display: none; + display: flex; // Default => display: none; gap: 2px; margin-bottom: 4px; .duality-modifier { @@ -375,7 +380,9 @@ font-size: 12px; } } - .dice-formula, > .dice-total, .part-header { + .dice-formula, + > .dice-total, + .part-header { display: none; } .dice-tooltip { @@ -384,7 +391,7 @@ .tooltip-part { display: flex; align-items: end; - gap: .25rem; + gap: 0.25rem; .dice { .dice-rolls { margin-bottom: 0; diff --git a/styles/countdown.less b/styles/countdown.less new file mode 100644 index 00000000..ef5279aa --- /dev/null +++ b/styles/countdown.less @@ -0,0 +1,137 @@ +.daggerheart.dh-style.countdown { + overflow: hidden; + + fieldset { + align-items: center; + margin-top: 5px; + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + + legend { + font-family: @font-body; + font-weight: bold; + color: light-dark(@dark-blue, @golden); + + a { + text-shadow: none; + } + } + } + + &.minimized { + height: auto !important; + max-height: unset !important; + max-width: 740px !important; + width: auto !important; + + .window-content { + display: flex; + padding: 4px 8px; + justify-content: center; + } + + .minimized-view { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .mini-countdown-container { + width: fit-content; + display: flex; + align-items: center; + gap: 8px; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 0 4px 0 0; + background-image: url('../assets/parchments/dh-parchment-light.png'); + color: light-dark(@beige, @dark); + cursor: pointer; + + &.disabled { + cursor: initial; + } + + img { + width: 30px; + height: 30px; + border-radius: 6px 0 0 6px; + } + + .mini-countdown-name { + white-space: nowrap; + } + + .mini-countdown-value { + } + } + } + } + + .hidden { + display: none; + } + + .window-content { + > div { + height: 100%; + + .expanded-view { + height: 100%; + display: flex; + flex-direction: column; + + .countdowns-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + overflow: auto; + max-height: 100%; + + .countdown-fieldset { + width: 340px; + height: min-content; + position: relative; + + .ownership-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 18px; + } + + .countdown-container { + display: flex; + align-items: center; + gap: 16px; + + img { + width: 150px; + height: 150px; + cursor: pointer; + + &.disabled { + cursor: initial; + } + } + + .countdown-inner-container { + display: flex; + flex-direction: column; + gap: 4px; + + .countdown-value-container { + display: flex; + gap: 4px; + + input { + max-width: 80px; + } + } + } + } + } + } + } + } + } +} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index e87b899a..1adab278 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1297,25 +1297,38 @@ .combat-sidebar .encounter-controls.combat { justify-content: space-between; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls { + display: flex; + align-items: center; + gap: 8px; +} +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container { + display: flex; + gap: 2px; +} +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container { display: flex; position: relative; align-items: center; justify-content: center; color: black; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice { - height: 24px; +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .dice { + height: 22px; + width: 22px; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .encounter-control-fear { position: absolute; font-size: 16px; } -.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter { +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-fear-dice-container .encounter-control-fear-container .encounter-control-counter { position: absolute; right: -10px; color: var(--color-text-secondary); } +.combat-sidebar .encounter-controls.combat .encounter-fear-controls .encounter-countdowns { + color: var(--content-link-icon-color); +} .combat-sidebar .encounter-controls.combat .control-buttons { width: min-content; } @@ -3064,6 +3077,24 @@ div.daggerheart.views.multiclass { .daggerheart.levelup .levelup-footer { display: flex; } +.daggerheart.views.ownership-selection .ownership-outer-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container { + display: flex; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + padding: 0 4px 0 0; + align-items: center; + gap: 8px; +} +.daggerheart.views.ownership-selection .ownership-outer-container .ownership-container img { + height: 40px; + width: 40px; + border-radius: 6px 0 0 6px; +} :root { --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --fear-animation: background 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease; @@ -3178,6 +3209,116 @@ div.daggerheart.views.multiclass { #resources:has(.fear-bar) { min-width: 200px; } +.daggerheart.dh-style.countdown { + overflow: hidden; +} +.daggerheart.dh-style.countdown fieldset { + align-items: center; + margin-top: 5px; + border-radius: 6px; + border-color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.countdown fieldset legend { + font-family: 'Montserrat', sans-serif; + font-weight: bold; + color: light-dark(#18162e, #f3c267); +} +.daggerheart.dh-style.countdown fieldset legend a { + text-shadow: none; +} +.daggerheart.dh-style.countdown.minimized { + height: auto !important; + max-height: unset !important; + max-width: 740px !important; + width: auto !important; +} +.daggerheart.dh-style.countdown.minimized .window-content { + display: flex; + padding: 4px 8px; + justify-content: center; +} +.daggerheart.dh-style.countdown.minimized .minimized-view { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container { + width: fit-content; + display: flex; + align-items: center; + gap: 8px; + border: 2px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + padding: 0 4px 0 0; + background-image: url('../assets/parchments/dh-parchment-light.png'); + color: light-dark(#efe6d8, #222); + cursor: pointer; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container.disabled { + cursor: initial; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container img { + width: 30px; + height: 30px; + border-radius: 6px 0 0 6px; +} +.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container .mini-countdown-name { + white-space: nowrap; +} +.daggerheart.dh-style.countdown .hidden { + display: none; +} +.daggerheart.dh-style.countdown .window-content > div { + height: 100%; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view { + height: 100%; + display: flex; + flex-direction: column; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container { + display: flex; + gap: 8px; + flex-wrap: wrap; + overflow: auto; + max-height: 100%; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset { + width: 340px; + height: min-content; + position: relative; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .ownership-button { + position: absolute; + top: 8px; + right: 8px; + font-size: 18px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container { + display: flex; + align-items: center; + gap: 16px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container img { + width: 150px; + height: 150px; + cursor: pointer; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container img.disabled { + cursor: initial; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container { + display: flex; + flex-direction: column; + gap: 4px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container .countdown-value-container { + display: flex; + gap: 4px; +} +.daggerheart.dh-style.countdown .window-content > div .expanded-view .countdowns-container .countdown-fieldset .countdown-container .countdown-inner-container .countdown-value-container input { + max-width: 80px; +} .daggerheart.dh-style.setting fieldset { display: flex; flex-direction: column; diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 3ad972fc..d5e7f76a 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -10,8 +10,10 @@ @import './dialog.less'; @import './characterCreation.less'; @import './levelup.less'; +@import './ownershipSelection.less'; @import '../node_modules/@yaireo/tagify/dist/tagify.css'; @import './resources.less'; +@import './countdown.less'; @import './settings.less'; // new styles imports diff --git a/styles/ownershipSelection.less b/styles/ownershipSelection.less new file mode 100644 index 00000000..f5093bee --- /dev/null +++ b/styles/ownershipSelection.less @@ -0,0 +1,22 @@ +.daggerheart.views.ownership-selection { + .ownership-outer-container { + display: flex; + flex-direction: column; + gap: 8px; + + .ownership-container { + display: flex; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 0 4px 0 0; + align-items: center; + gap: 8px; + + img { + height: 40px; + width: 40px; + border-radius: 6px 0 0 6px; + } + } + } +} diff --git a/styles/ui.less b/styles/ui.less index c54b0b3b..09a3511f 100644 --- a/styles/ui.less +++ b/styles/ui.less @@ -2,26 +2,42 @@ .encounter-controls.combat { justify-content: space-between; - .encounter-control-fear-container { + .encounter-fear-controls { display: flex; - position: relative; align-items: center; - justify-content: center; - color: black; + gap: 8px; - .dice { - height: 24px; + .encounter-fear-dice-container { + display: flex; + gap: 2px; + + .encounter-control-fear-container { + display: flex; + position: relative; + align-items: center; + justify-content: center; + color: black; + + .dice { + height: 22px; + width: 22px; + } + + .encounter-control-fear { + position: absolute; + font-size: 16px; + } + + .encounter-control-counter { + position: absolute; + right: -10px; + color: var(--color-text-secondary); + } + } } - .encounter-control-fear { - position: absolute; - font-size: 16px; - } - - .encounter-control-counter { - position: absolute; - right: -10px; - color: var(--color-text-secondary); + .encounter-countdowns { + color: var(--content-link-icon-color); } } diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings.hbs index bd50c548..8db980c7 100644 --- a/templates/settings/automation-settings.hbs +++ b/templates/settings/automation-settings.hbs @@ -11,6 +11,12 @@ {{formInput settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints }} +
+ +
+ {{formInput settingFields.schema.fields.countdowns value=settingFields._source.countdowns }} +
+