diff --git a/lang/en.json b/lang/en.json index 685f1266..a978cd16 100755 --- a/lang/en.json +++ b/lang/en.json @@ -331,7 +331,8 @@ "label": { "label": "Label", "hint": "Used for custom" }, "value": { "label": "Value" } } - } + }, + "type": { "label": "Countdown Type" } } } }, @@ -346,6 +347,25 @@ "encounter": "Encounter" } }, + "CountdownEdit": { + "title": "Countdown Edit", + "viewTitle": "Countdowns", + "editTitle": "Edit Countdowns", + "newCountdown": "New Countdown", + "removeCountdownTitle": "Remove Countdown", + "removeCountdownText": "Are you sure you want to remove the countdown: {name}?", + "current": "Current", + "max": "Max", + "currentCountdownValue": "Current: {value}", + "currentCountdownMax": "Max: {value}", + "category": "Category", + "type": "Type", + "defaultOwnershipTooltip": "The default player ownership of countdowns" + }, + "DaggerheartMenu": { + "title": "GM Tools", + "countdowns": "Edit Countdowns" + }, "DeleteConfirmation": { "title": "Delete {type} - {name}", "text": "Are you sure you want to delete {name}?" diff --git a/module/applications/dialogs/ownershipSelection.mjs b/module/applications/dialogs/ownershipSelection.mjs index e4a7e628..a6608c9c 100644 --- a/module/applications/dialogs/ownershipSelection.mjs +++ b/module/applications/dialogs/ownershipSelection.mjs @@ -1,13 +1,12 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(resolve, reject, name, ownership) { + constructor(name, ownership, defaultOwnership) { super({}); - this.resolve = resolve; - this.reject = reject; this.name = name; - this.ownership = ownership; + this.ownership = foundry.utils.deepClone(ownership); + this.defaultOwnership = defaultOwnership; } static DEFAULT_OPTIONS = { @@ -30,43 +29,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); } + getOwnershipData(id) { + return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT; + } + 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 ?? 'icons/svg/cowled.svg', - name: user.name, - ownership: this.ownership.players[x].value - }; - } + context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels; + context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels; + context.defaultOwnership = this.defaultOwnership; + context.ownership = game.users.reduce((acc, user) => { + if (!user.isGM) { + acc[user.id] = { + ...user, + img: user.character?.img ?? 'icons/svg/cowled.svg', + ownership: this.getOwnershipData(user.id) + }; + } - return acc; - }, {}) - }; + return acc; + }, {}); return context; } static async updateData(event, _, formData) { - const { ownership } = foundry.utils.expandObject(formData.object); - - this.resolve(ownership); - this.close(true); + const data = foundry.utils.expandObject(formData.object); + this.close(data); } - async close(fromSave) { - if (!fromSave) { - this.reject(); + async close(data) { + if (data) { + this.saveData = data; } await super.close(); } + + static async configure(name, ownership, defaultOwnership) { + return new Promise(resolve => { + const app = new this(name, ownership, defaultOwnership); + app.addEventListener('close', () => resolve(app.saveData), { once: true }); + app.render({ force: true }); + }); + } } diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index cf7aeae3..1fb5d09f 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -29,7 +29,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract }, actions: { selectRefreshable: DaggerheartMenu.#selectRefreshable, - refreshActors: DaggerheartMenu.#refreshActors + refreshActors: DaggerheartMenu.#refreshActors, + editCountdowns: DaggerheartMenu.#editCountdowns } }; @@ -157,4 +158,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract this.render(); } + + static async #editCountdowns() { + new game.system.api.applications.ui.CountdownEdit().render(true); + } } diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index 815fc4e7..7645219e 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -1,3 +1,4 @@ +export { default as CountdownEdit } from './countdownEdit.mjs'; export { default as DhChatLog } from './chatLog.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs'; export * as DhCountdowns from './countdowns.mjs'; diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs new file mode 100644 index 00000000..5f80ae10 --- /dev/null +++ b/module/applications/ui/countdownEdit.mjs @@ -0,0 +1,129 @@ +import { DhCountdown } from '../../data/countdowns.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) { + constructor() { + super(); + + this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + this.editingCountdowns = new Set(); + } + + get title() { + return game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.title'); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'countdown-edit'], + tag: 'form', + position: { width: 600 }, + window: { icon: 'fa-solid fa-clock-rotate-left' }, + actions: { + addCountdown: CountdownEdit.#addCountdown, + toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit, + editCountdownImage: CountdownEdit.#editCountdownImage, + editCountdownOwnership: CountdownEdit.#editCountdownOwnership, + removeCountdown: CountdownEdit.#removeCountdown + }, + form: { handler: this.updateData, submitOnChange: true } + }; + + static PARTS = { + countdowns: { + template: 'systems/daggerheart/templates/ui/countdown-edit.hbs', + scrollable: ['.expanded-view'] + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels; + context.defaultOwnership = this.data.defaultOwnership; + context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes; + context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes; + context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => { + const countdown = this.data.countdowns[key]; + acc[key] = { + ...countdown, + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name), + progress: { + ...countdown.progress, + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label) + }, + editing: this.editingCountdowns.has(key) + }; + + return acc; + }, {}); + + return context; + } + + async updateSetting(update) { + await this.data.updateSource(update); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, this.data); + this.render(); + } + + static async updateData(_event, _, formData) { + this.updateSetting(foundry.utils.expandObject(formData.object)); + } + + static #addCountdown() { + this.updateSetting({ + [`countdowns.${foundry.utils.randomID()}`]: DhCountdown.defaultCountdown() + }); + } + + static #editCountdownImage(_, target) { + const countdown = this.data.countdowns[target.id]; + const fp = new foundry.applications.apps.FilePicker.implementation({ + current: countdown.img, + type: 'image', + callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }), + top: this.position.top + 40, + left: this.position.left + 10 + }); + return fp.browse(); + } + + static #toggleCountdownEdit(_, button) { + const { countdownId } = button.dataset; + + const isEditing = this.editingCountdowns.has(countdownId); + if (isEditing) this.editingCountdowns.delete(countdownId); + else this.editingCountdowns.add(countdownId); + + this.render(); + } + + static async #editCountdownOwnership(_, button) { + const countdown = this.data.countdowns[button.dataset.countdownId]; + const data = await game.system.api.applications.dialogs.OwnershipSelection.configure( + countdown.name, + countdown.ownership, + this.data.defaultOwnership + ); + if (!data) return; + + this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data }); + } + + static async #removeCountdown(_, button) { + const { countdownId } = button.dataset; + + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle') + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', { + name: this.data.countdowns[countdownId].name + }) + }); + if (!confirmed) return; + + if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId); + this.updateSetting({ [`countdowns.-=${countdownId}`]: null }); + } +} diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 7afcbdea..12c7d3c0 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -650,3 +650,25 @@ export const fearDisplay = { bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } }; + +export const basicOwnershiplevels = { + 0: { value: 0, label: 'OWNERSHIP.NONE' }, + 2: { value: 2, label: 'OWNERSHIP.OBSERVER' }, + 3: { value: 3, label: 'OWNERSHIP.OWNER' } +}; + +export const simpleOwnershiplevels = { + [-1]: { value: -1, label: 'OWNERSHIP.INHERIT' }, + ...basicOwnershiplevels +}; + +export const countdownBaseTypes = { + narrative: { + id: 'narrative', + name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative' + }, + encounter: { + id: 'encounter', + name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter' + } +}; diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 62036c38..397e87c1 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -5,17 +5,22 @@ export default class DhCountdowns extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { + /* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */ narrative: new fields.EmbeddedDataField(DhCountdownData), - encounter: new fields.EmbeddedDataField(DhCountdownData) + encounter: new fields.EmbeddedDataField(DhCountdownData), + /**/ + countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), + defaultOwnership: new fields.NumberField({ + required: true, + choices: CONFIG.DH.GENERAL.basicOwnershiplevels, + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER + }) }; } - - static CountdownCategories = { narrative: 'narrative', combat: 'combat' }; } +/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */ class DhCountdownData extends foundry.abstract.DataModel { - static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now - static defineSchema() { const fields = foundry.data.fields; return { @@ -56,10 +61,15 @@ class DhCountdownData extends foundry.abstract.DataModel { } } -class DhCountdown extends foundry.abstract.DataModel { +export class DhCountdown extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { + type: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.countdownBaseTypes, + label: 'DAGGERHEART.GENERAL.type' + }), name: new fields.StringField({ required: true, label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label' @@ -69,22 +79,13 @@ class DhCountdown extends foundry.abstract.DataModel { base64: false, initial: 'icons/magic/time/hourglass-yellow-green.webp' }), - ownership: new fields.SchemaField({ - default: new fields.NumberField({ + ownership: new fields.TypedObjectField( + 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 - }) - }) - ) - }), + choices: CONFIG.DH.GENERAL.simpleOwnershiplevels, + initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }) + ), progress: new fields.SchemaField({ current: new fields.NumberField({ required: true, @@ -98,21 +99,28 @@ class DhCountdown extends foundry.abstract.DataModel { initial: 1, label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label' }), - type: new fields.SchemaField({ - value: new fields.StringField({ - required: true, - choices: CONFIG.DH.GENERAL.countdownTypes, - initial: CONFIG.DH.GENERAL.countdownTypes.custom.id, - label: 'DAGGERHEART.GENERAL.type' - }), - label: new fields.StringField({ - label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.type.label.label' - }) + type: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.custom.id, + label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label' }) }) }; } + static defaultCountdown(type) { + return { + type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id, + name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), + img: 'icons/magic/time/hourglass-yellow-green.webp', + progress: { + current: 1, + max: 1 + } + }; + } + get playerOwnership() { return Array.from(game.users).reduce((acc, user) => { acc[user.id] = { diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index 015c19cb..e4a2abc4 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -97,6 +97,7 @@ export async function runMigrations() { } if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) { + /* Migrate old action costs */ const lockedPacks = []; const compendiumItems = []; for (let pack of game.packs) { @@ -148,6 +149,14 @@ export async function runMigrations() { await pack.configure({ locked: true }); } + /* Migrate old countdown structure */ + const { narrative, encounter } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); + if (narrative) { + narrative.countdowns; + } + if (encounter) { + } + lastMigrationVersion = '1.2.0'; } diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less new file mode 100644 index 00000000..ff856249 --- /dev/null +++ b/styles/less/ui/countdown/countdown-edit.less @@ -0,0 +1,117 @@ +.theme-light .daggerheart.application.dh-style.countdown-edit { + background-image: url('../assets/parchments/dh-parchment-light.png'); +} + +.daggerheart.application.dh-style.countdown-edit { + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + + .edit-container { + display: flex; + flex-direction: column; + gap: 8px; + + h2 { + text-align: center; + color: light-dark(@dark, @golden); + } + + .header-tools { + display: grid; + grid-template-columns: 1fr 144px; + gap: 8px; + + .header-main-button { + flex: 1; + } + + .default-ownership-tools { + display: flex; + align-items: center; + gap: 8px; + + select { + flex: 1; + background: light-dark(@beige, @dark-blue); + } + } + } + + .edit-content { + display: flex; + flex-direction: column; + gap: 8px; + + .countdown-edit-container { + display: grid; + grid-template-columns: 48px 1fr 64px; + align-items: center; + gap: 8px; + + &.viewing { + padding: 0 16px; + } + + img { + width: 52px; + height: 52px; + } + + .countdown-edit-text { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + + .countdown-edit-subtext { + display: flex; + gap: 8px; + + .countdown-edit-sub-tag { + border: 1px solid; + border-radius: 4px; + padding: 2px 4px; + background: light-dark(@beige, @dark-blue); + } + } + } + + .countdown-edit-tools { + display: flex; + gap: 8px; + + &.same-row { + margin-top: 17.5px; + } + + a { + font-size: 16px; + } + } + } + + .countdown-edit-subrow { + display: flex; + gap: 16px; + margin: 0 72px 0 56px; + } + + .countdown-edit-input { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + + &.tiny { + flex: 0; + } + + input, + select { + background: light-dark(@beige, @dark-blue); + } + } + } + } +} diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 8b0c53f6..0a89afc3 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -13,6 +13,7 @@ @import './item-browser/item-browser.less'; @import './countdown/countdown.less'; +@import './countdown/countdown-edit.less'; @import './countdown/sheet.less'; @import './ownership-selection/ownership-selection.less'; diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index e975954c..80eda9a1 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -1,5 +1,17 @@ .tab.sidebar-tab.daggerheartMenu-sidebar { - padding: 0 4px; + padding: 4px; + + div[data-application-part] { + display: flex; + flex-direction: column; + gap: 8px; + } + + h2 { + margin-top: 8px; + text-align: center; + font-weight: bold; + } .menu-refresh-container { display: flex; diff --git a/templates/dialogs/ownershipSelection.hbs b/templates/dialogs/ownershipSelection.hbs index 43711c07..46b11066 100644 --- a/templates/dialogs/ownershipSelection.hbs +++ b/templates/dialogs/ownershipSelection.hbs @@ -2,17 +2,17 @@
- + {{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true }}
- {{#each ownership.players as |player id|}} + {{#each ownership as |player id|}}
{{player.name}}
- + {{selectOptions @root.ownershipOptions selected=player.ownership labelAttr="label" valueAttr="value" localize=true }}
{{/each}} diff --git a/templates/levelup/tabs/viewMode.hbs b/templates/levelup/tabs/viewMode.hbs index 12e7cbcd..b41623d7 100644 --- a/templates/levelup/tabs/viewMode.hbs +++ b/templates/levelup/tabs/viewMode.hbs @@ -2,7 +2,7 @@
{{#each this.tiers as |tier key|}}
- {{tier.name}} + {{localize tier.name}} {{#each tier.groups}}
diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs index 6f31f165..448fba3f 100644 --- a/templates/sidebar/daggerheart-menu/main.hbs +++ b/templates/sidebar/daggerheart-menu/main.hbs @@ -1,4 +1,6 @@
+

{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.title"}}

+
{{localize "Refresh Features"}} @@ -19,4 +21,6 @@
+ +
\ No newline at end of file diff --git a/templates/ui/countdown-edit.hbs b/templates/ui/countdown-edit.hbs new file mode 100644 index 00000000..25791a64 --- /dev/null +++ b/templates/ui/countdown-edit.hbs @@ -0,0 +1,68 @@ +
+
+

{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle"}}

+ +
+ +
+ + +
+
+ +
+ {{#each countdowns as | countdown id | }} +
+ + {{#unless countdown.editing}} +
+

{{countdown.name}}

+
+
{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownValue" value=countdown.progress.current}}
+
{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownMax" value=countdown.progress.max}}
+
{{countdown.typeName}}
+
{{countdown.progress.typeName}}
+
+
+ {{else}} +
+ + +
+ {{/unless}} +
+ + + +
+
+ {{#if countdown.editing}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {{/if}} + {{/each}} +
+
+
\ No newline at end of file