From d4f80b6fa1cabb8d659a1f361dd1f304425523d4 Mon Sep 17 00:00:00 2001
From: IrkTheImp <41175833+IrkTheImp@users.noreply.github.com>
Date: Sat, 25 Oct 2025 11:59:25 -0500
Subject: [PATCH 1/3] [Feature] 789 - spotlight order (#1222)
* order spotlight requests
* add spotlight requests section to combat tracker
---
lang/en.json | 4 ++++
module/applications/ui/combatTracker.mjs | 22 +++++++++++++++++---
module/data/combatant.mjs | 3 ++-
templates/ui/combatTracker/combatTracker.hbs | 3 +++
4 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/lang/en.json b/lang/en.json
index e40b035c..38a498fe 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -1884,6 +1884,10 @@
}
}
},
+ "SpotlightRequests": {
+ "singular": "Spotlight Request",
+ "plural": "Spotlight Requests"
+ },
"Tabs": {
"details": "Details",
"attack": "Attack",
diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs
index 85c77cd3..101e20d6 100644
--- a/module/applications/ui/combatTracker.mjs
+++ b/module/applications/ui/combatTracker.mjs
@@ -36,10 +36,21 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = context.turns?.filter(x => !x.isNPC) ?? [];
+ const spotlightRequests = characters
+ ?.filter(x => !x.isNPC)
+ .filter(x => x.system.spotlight.requestOrderIndex > 0)
+ .sort((a, b) => {
+ const valueA = a.system.spotlight.requestOrderIndex;
+ const valueB = b.system.spotlight.requestOrderIndex;
+
+ return valueA - valueB;
+ });
+
Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
- characters
+ characters: characters?.filter(x => !x.isNPC).filter(x => x.system.spotlight.requestOrderIndex == 0),
+ spotlightRequests
});
}
@@ -114,7 +125,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async setCombatantSpotlight(combatantId) {
const update = {
system: {
- 'spotlight.requesting': false
+ 'spotlight.requesting': false,
+ 'spotlight.requestOrderIndex': 0
}
};
const combatant = this.viewed.combatants.get(combatantId);
@@ -142,11 +154,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
}
static async requestSpotlight(_, target) {
+ const characters = this.viewed.turns?.filter(x => !x.isNPC) ?? [];
+ const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex);
+ const maxRequestIndex = Math.max(...orderValues);
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({
'system.spotlight': {
- requesting: !combatant.system.spotlight.requesting
+ requesting: !combatant.system.spotlight.requesting,
+ requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
}
});
diff --git a/module/data/combatant.mjs b/module/data/combatant.mjs
index bb54c798..6deade9f 100644
--- a/module/data/combatant.mjs
+++ b/module/data/combatant.mjs
@@ -3,7 +3,8 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields;
return {
spotlight: new fields.SchemaField({
- requesting: new fields.BooleanField({ required: true, initial: false })
+ requesting: new fields.BooleanField({ required: true, initial: false }),
+ requestOrderIndex: new fields.NumberField({ required: true, integer: true, initial: 0 })
}),
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
};
diff --git a/templates/ui/combatTracker/combatTracker.hbs b/templates/ui/combatTracker/combatTracker.hbs
index 8ad4f7d5..91f7786b 100644
--- a/templates/ui/combatTracker/combatTracker.hbs
+++ b/templates/ui/combatTracker/combatTracker.hbs
@@ -1,4 +1,7 @@
+ {{#if (gt this.spotlightRequests.length 0)}}
+ {{> 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.GENERAL.SpotlightRequests.plural") turns=this.spotlightRequests}}
+ {{/if}}
{{#if (gt this.characters.length 0)}}
{{> 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.GENERAL.Character.plural") turns=this.characters}}
{{/if}}
From 07cdcf2d78452536c56164068d84faf71608556e Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Sun, 26 Oct 2025 12:57:46 +0100
Subject: [PATCH 2/3] Improved order of steps (#1232)
---
.../characterCreation/characterCreation.mjs | 38 +++++++++----------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs
index 490294cd..aa764c56 100644
--- a/module/applications/characterCreation/characterCreation.mjs
+++ b/module/applications/characterCreation/characterCreation.mjs
@@ -83,9 +83,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
+ class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
- class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
@@ -95,6 +95,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
};
static TABS = {
+ class: {
+ active: false,
+ cssClass: '',
+ group: 'setup',
+ id: 'class',
+ label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
+ },
ancestry: {
active: true,
cssClass: '',
@@ -109,13 +116,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
},
- class: {
- active: false,
- cssClass: '',
- group: 'setup',
- id: 'class',
- label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
- },
traits: {
active: false,
cssClass: '',
@@ -156,10 +156,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : '';
switch (v.id) {
- case 'community':
+ case 'ancestry':
v.disabled = this.setup.visibility < 2;
break;
- case 'class':
+ case 'community':
v.disabled = this.setup.visibility < 3;
break;
case 'traits':
@@ -192,7 +192,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}
async _prepareContext(_options) {
- this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry';
+ this.tabGroups.setup = this.tabGroups.setup ?? 'class';
const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS);
@@ -266,13 +266,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.isLastTab = this.tabGroups.setup === 'equipment';
switch (this.tabGroups.setup) {
case null:
- case 'ancestry':
+ case 'class':
context.nextDisabled = this.setup.visibility === 1;
break;
- case 'community':
+ case 'ancestry':
context.nextDisabled = this.setup.visibility === 2;
break;
- case 'class':
+ case 'community':
context.nextDisabled = this.setup.visibility === 3;
break;
case 'traits':
@@ -363,11 +363,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3:
- return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3;
+ return this.setup.community.uuid ? 4 : 3;
case 2:
- return this.setup.community.uuid ? 3 : 2;
+ return this.setup.primaryAncestry.uuid ? 3 : 2;
case 1:
- return this.setup.primaryAncestry.uuid ? 2 : 1;
+ return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
}
}
@@ -473,10 +473,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() {
switch (this.setup.visibility) {
case 2:
- this.tabGroups.setup = 'community';
+ this.tabGroups.setup = 'ancestry';
break;
case 3:
- this.tabGroups.setup = 'class';
+ this.tabGroups.setup = 'community';
break;
case 4:
this.tabGroups.setup = 'traits';
From 906c7ac853ece6470e3a59d24ac42427f92099a0 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Mon, 27 Oct 2025 22:24:38 +0100
Subject: [PATCH 3/3] [Feature] 613 - Countdown Improvements (#1184)
* Added CountdownEdit view
* Added countdowns UI element
* .
* Fixed migration of countdowns
* .
* .
* style countdown interface, application and ownership dialog
* fix buttons height in ownsership selection
* .
* Added coloured pips to UI cooldowns to signify player visibility if not every player has it
* .
* Added max-height and overflow
* Sync countdown current with max when equal (#1221)
* Update module/applications/ui/countdownEdit.mjs
Co-authored-by: Carlos Fernandez
* .
---------
Co-authored-by: moliloo
Co-authored-by: Carlos Fernandez
---
daggerheart.mjs | 30 +-
lang/en.json | 31 +-
.../dialogs/ownershipSelection.mjs | 67 +--
.../sidebar/tabs/daggerheartMenu.mjs | 7 +-
module/applications/ui/_module.mjs | 3 +-
module/applications/ui/combatTracker.mjs | 6 -
module/applications/ui/countdownEdit.mjs | 199 ++++++++
module/applications/ui/countdowns.mjs | 459 ++++++------------
module/config/flagsConfig.mjs | 3 +-
module/config/generalConfig.mjs | 27 ++
module/data/countdowns.mjs | 119 ++++-
module/systemRegistration/migrations.mjs | 31 ++
module/systemRegistration/socket.mjs | 10 +-
styles/less/ui/countdown/countdown-edit.less | 142 ++++++
styles/less/ui/countdown/countdown.less | 176 +++++--
styles/less/ui/index.less | 1 +
.../ownership-selection.less | 22 +-
styles/less/ui/sidebar/daggerheartMenu.less | 14 +-
templates/dialogs/ownershipSelection.hbs | 24 +-
templates/levelup/tabs/viewMode.hbs | 2 +-
templates/sidebar/daggerheart-menu/main.hbs | 4 +
.../ui/combatTracker/combatTrackerHeader.hbs | 1 -
templates/ui/countdown-edit.hbs | 76 +++
templates/ui/countdowns.hbs | 68 ++-
24 files changed, 1024 insertions(+), 498 deletions(-)
create mode 100644 module/applications/ui/countdownEdit.mjs
create mode 100644 styles/less/ui/countdown/countdown-edit.less
create mode 100644 templates/ui/countdown-edit.hbs
diff --git a/daggerheart.mjs b/daggerheart.mjs
index d7aba401..e079703a 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -8,10 +8,8 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
-import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
-import { registerCountdownHooks } from './module/data/countdowns.mjs';
import {
handlebarsRegistration,
runMigrations,
@@ -140,6 +138,7 @@ Hooks.once('init', () => {
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker;
+ CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager;
@@ -166,10 +165,12 @@ Hooks.on('ready', async () => {
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
ui.resources.render({ force: true });
+ ui.countdowns = new CONFIG.ui.countdowns();
+ ui.countdowns.render({ force: true });
+
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser();
- registerCountdownHooks();
socketRegistration.registerSocketHooks();
registerRollDiceHooks();
socketRegistration.registerUserQueries();
@@ -242,29 +243,6 @@ 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.APPLICATIONS.Countdown.title', {
- type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
- });
- buttons.insertAdjacentHTML(
- 'afterbegin',
- `
- `
- );
-
- buttons.querySelector('#narrative-countdown-button').onclick = async () => {
- new NarrativeCountdowns().open();
- };
- }
-});
-
Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return;
diff --git a/lang/en.json b/lang/en.json
index 38a498fe..c21c1d5b 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,26 @@
"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",
+ "hideNewCountdowns": "Hide New Countdowns"
+ },
+ "DaggerheartMenu": {
+ "title": "GM Tools",
+ "countdowns": "Edit Countdowns"
+ },
"DeleteConfirmation": {
"title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?"
@@ -2455,6 +2476,11 @@
"playerMessage": "{user} rerolled their {name}"
}
},
+ "Countdowns": {
+ "title": "Countdowns",
+ "toggleIconMode": "Toggle Icon Only",
+ "noPlayerAccess": "This countdown isn't visible to any players"
+ },
"ItemBrowser": {
"title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium",
@@ -2562,7 +2588,8 @@
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}",
- "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class."
+ "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
+ "gmRequired": "This action requires an online GM"
},
"Sidebar": {
"daggerheartMenu": {
diff --git a/module/applications/dialogs/ownershipSelection.mjs b/module/applications/dialogs/ownershipSelection.mjs
index e4a7e628..049f4d99 100644
--- a/module/applications/dialogs/ownershipSelection.mjs
+++ b/module/applications/dialogs/ownershipSelection.mjs
@@ -1,18 +1,20 @@
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 = {
tag: 'form',
- classes: ['daggerheart', 'views', 'ownership-selection'],
+ classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
+ window: {
+ icon: 'fa-solid fa-users'
+ },
position: {
width: 600,
height: 'auto'
@@ -30,43 +32,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..35a58566 100644
--- a/module/applications/ui/_module.mjs
+++ b/module/applications/ui/_module.mjs
@@ -1,6 +1,7 @@
+export { default as CountdownEdit } from './countdownEdit.mjs';
+export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
-export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs';
diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs
index 101e20d6..b70f8c71 100644
--- a/module/applications/ui/combatTracker.mjs
+++ b/module/applications/ui/combatTracker.mjs
@@ -1,5 +1,3 @@
-import { EncounterCountdowns } from '../ui/countdowns.mjs';
-
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
@@ -184,8 +182,4 @@ 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/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs
new file mode 100644
index 00000000..2098fb10
--- /dev/null
+++ b/module/applications/ui/countdownEdit.mjs
@@ -0,0 +1,199 @@
+import { DhCountdown } from '../../data/countdowns.mjs';
+import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.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();
+ this.currentEditCountdown = null;
+ this.hideNewCountdowns = false;
+ }
+
+ static DEFAULT_OPTIONS = {
+ classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'],
+ tag: 'form',
+ position: { width: 600 },
+ window: {
+ title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title',
+ 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', '.edit-content']
+ }
+ };
+
+ async _prepareContext(_options) {
+ const context = await super._prepareContext(_options);
+ context.isGM = game.user.isGM;
+ context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
+ context.defaultOwnership = this.data.defaultOwnership;
+ context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
+ context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes;
+ context.hideNewCountdowns = this.hideNewCountdowns;
+ 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;
+ }
+
+ /** @override */
+ async _postRender(_context, _options) {
+ if (this.currentEditCountdown) {
+ setTimeout(() => {
+ const input = this.element.querySelector(
+ `.countdown-edit-container[data-id="${this.currentEditCountdown}"] input`
+ );
+ if (input) {
+ input.select();
+ this.currentEditCountdown = null;
+ }
+ }, 100);
+ }
+ }
+
+ canPerformEdit() {
+ if (game.user.isGM) return true;
+
+ if (!game.users.activeGM) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
+ return false;
+ }
+
+ return true;
+ }
+
+ async updateSetting(update) {
+ const noGM = !game.users.find(x => x.isGM && x.active);
+ if (noGM) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
+ return;
+ }
+
+ await this.data.updateSource(update);
+ await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
+ refreshType: RefreshType.Countdown
+ });
+
+ this.render();
+ }
+
+ static async updateData(_event, _, formData) {
+ const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object);
+
+ // Sync current and max if max is changing and they were equal before
+ for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) {
+ const existing = this.data.countdowns[id];
+ const wasEqual = existing && existing.progress.current === existing.progress.max;
+ if (wasEqual && countdown.progress.max !== existing.progress.max) {
+ countdown.progress.current = countdown.progress.max;
+ } else {
+ countdown.progress.current = Math.min(countdown.progress.current, countdown.progress.max);
+ }
+ }
+
+ this.hideNewCountdowns = hideNewCountdowns;
+ this.updateSetting(settingsData);
+ }
+
+ async gmSetSetting(data) {
+ await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
+ game.socket.emit(`system.${CONFIG.DH.id}`, {
+ action: socketEvent.Refresh,
+ data: { refreshType: RefreshType.Countdown }
+ });
+ Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
+ }
+
+ static #addCountdown() {
+ const id = foundry.utils.randomID();
+ this.editingCountdowns.add(id);
+ this.currentEditCountdown = id;
+ this.updateSetting({
+ [`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns)
+ });
+ }
+
+ 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.currentEditCountdown = 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(event, button) {
+ const { countdownId } = button.dataset;
+
+ if (!event.shiftKey) {
+ 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/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs
index 5e3ad1ab..07eac74b 100644
--- a/module/applications/ui/countdowns.mjs
+++ b/module/applications/ui/countdowns.mjs
@@ -1,355 +1,218 @@
-import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
-import constructHTMLButton from '../../helpers/utils.mjs';
-import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
+import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
-class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
- constructor(basePath) {
- super({});
+/**
+ * A UI element which displays the countdowns in this world.
+ *
+ * @extends ApplicationV2
+ * @mixes HandlebarsApplication
+ */
- this.basePath = basePath;
- }
-
- get title() {
- return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
- type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`)
- });
+export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
+ constructor(options = {}) {
+ super(options);
+
+ this.sidebarCollapsed = true;
+ this.setupHooks();
}
+ /** @inheritDoc */
static DEFAULT_OPTIONS = {
- classes: ['daggerheart', 'dh-style', 'countdown'],
- tag: 'form',
- position: { width: 740, height: 700 },
+ id: 'countdowns',
+ tag: 'div',
+ classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
window: {
+ icon: 'fa-solid fa-clock-rotate-left',
frame: true,
- title: 'Countdowns',
- resizable: true,
+ title: 'DAGGERHEART.UI.Countdowns.title',
+ positioned: false,
+ resizable: false,
minimizable: false
},
actions: {
- addCountdown: this.addCountdown,
- removeCountdown: this.removeCountdown,
- editImage: this.onEditImage,
- openOwnership: this.openOwnership,
- openCountdownOwnership: this.openCountdownOwnership,
- toggleSimpleView: this.toggleSimpleView
+ toggleViewMode: DhCountdowns.#toggleViewMode,
+ decreaseCountdown: (_, target) => this.editCountdown(false, target),
+ increaseCountdown: (_, target) => this.editCountdown(true, target)
},
- form: { handler: this.updateData, submitOnChange: true }
- };
-
- static PARTS = {
- countdowns: {
- template: 'systems/daggerheart/templates/ui/countdowns.hbs',
- scrollable: ['.expanded-view']
+ position: {
+ width: 400,
+ height: 222,
+ top: 50
}
};
- _attachPartListeners(partId, htmlElement, options) {
- super._attachPartListeners(partId, htmlElement, options);
+ /** @override */
+ static PARTS = {
+ resources: {
+ root: true,
+ template: 'systems/daggerheart/templates/ui/countdowns.hbs'
+ }
+ };
- htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => {
- element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false));
- element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
- });
- }
-
- async _preFirstRender(context, options) {
- options.position =
- game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
- Countdowns.DEFAULT_OPTIONS.position;
-
- const viewSetting =
- game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
- this.simpleView =
- game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
- context.simple = this.simpleView;
- }
-
- _onPosition(position) {
- game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
+ get element() {
+ return document.body.querySelector('.daggerheart.dh-style.countdowns');
}
+ /**@inheritdoc */
async _renderFrame(options) {
const frame = await super._renderFrame(options);
- if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
- const button = constructHTMLButton({
- label: '',
- classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
- dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' }
- });
- this.window.controls.after(button);
- }
+ const iconOnly =
+ game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
+ CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
+ if (iconOnly) frame.classList.add('icon-only');
+ else frame.classList.remove('icon-only');
+
+ const header = frame.querySelector('.window-header');
+ header.querySelector('button[data-action="close"]').remove();
+
+ const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
+ const minimizeButton = ``;
+ header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame;
}
- testUserPermission(level, exact, altSettings) {
- if (game.user.isGM) return true;
-
- const settings =
- altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
- const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
- const userAllowed = exact
- ? settings.playerOwnership[game.user.id]?.value === level
- : settings.playerOwnership[game.user.id]?.value >= level;
- return defaultAllowed || userAllowed;
- }
-
- async _prepareContext(_options) {
- const context = await super._prepareContext(_options);
- const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[
- this.basePath
- ];
-
+ /** @override */
+ async _prepareContext(options) {
+ const context = await super._prepareContext(options);
context.isGM = game.user.isGM;
- context.base = this.basePath;
+ context.sidebarCollapsed = this.sidebarCollapsed;
+ context.iconOnly =
+ game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
+ CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
- context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
- context.source = {
- ...countdownData,
- countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
- const countdown = countdownData.countdowns[key];
+ const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
+ context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => {
+ const countdown = setting.countdowns[key];
+ const ownership = DhCountdowns.#getPlayerOwnership(game.user, setting, countdown);
+ if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc;
- if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
- acc[key] = {
- ...countdown,
- canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
- };
+ const playersWithAccess = game.users.reduce((acc, user) => {
+ const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
+ if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
+ acc.push(user);
}
-
return acc;
- }, {})
- };
- context.systemFields = countdownData.schema.fields;
- context.countdownFields = context.systemFields.countdowns.element.fields;
- context.simple = this.simpleView;
+ }, []);
+ const nonGmPlayers = game.users.filter(x => !x.isGM);
+ acc[key] = {
+ ...countdown,
+ editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
+ playerAccess: playersWithAccess.length !== nonGmPlayers.length ? playersWithAccess : [],
+ noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0
+ };
+ return acc;
+ }, {});
return context;
}
- static async updateData(event, _, formData) {
- const data = foundry.utils.expandObject(formData.object);
- const newSetting = foundry.utils.mergeObject(
- game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(),
- data
- );
+ static #getPlayerOwnership(user, setting, countdown) {
+ const playerOwnership = countdown.ownership[user.id];
+ return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
+ ? setting.defaultOwnership
+ : playerOwnership;
+ }
- if (game.user.isGM) {
- await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting);
- this.render();
- } else {
- await game.socket.emit(`system.${CONFIG.DH.id}`, {
- action: socketEvent.GMUpdate,
- data: {
- action: GMUpdateEvent.UpdateSetting,
- uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
- update: newSetting
- }
- });
+ toggleCollapsedPosition = async (_, collapsed) => {
+ this.sidebarCollapsed = collapsed;
+ if (!collapsed) this.element.classList.add('expanded');
+ else this.element.classList.remove('expanded');
+ };
+
+ cooldownRefresh = ({ refreshType }) => {
+ if (refreshType === RefreshType.Countdown) this.render();
+ };
+
+ static canPerformEdit() {
+ if (game.user.isGM) return true;
+
+ const noGM = !game.users.find(x => x.isGM && x.active);
+ if (noGM) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
+ return false;
}
+
+ return true;
}
- async updateSetting(update) {
- if (game.user.isGM) {
- await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update);
- await game.socket.emit(`system.${CONFIG.DH.id}`, {
- action: socketEvent.Refresh,
- data: {
- refreshType: RefreshType.Countdown,
- application: `${this.basePath}-countdowns`
- }
- });
+ static async #toggleViewMode() {
+ const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
+ const appMode = CONFIG.DH.GENERAL.countdownAppMode;
+ const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
+ await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
- this.render();
- } else {
- await game.socket.emit(`system.${CONFIG.DH.id}`, {
- action: socketEvent.GMUpdate,
- data: {
- action: GMUpdateEvent.UpdateSetting,
- uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
- update: update,
- refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
- }
- });
- }
- }
-
- static onEditImage(_, target) {
- const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
- const current = setting.countdowns[target.dataset.countdown].img;
- const fp = new foundry.applications.apps.FilePicker.implementation({
- 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(CONFIG.DH.id, CONFIG.DH.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(CONFIG.DH.id, CONFIG.DH.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(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
- await setting.updateSource({
- [`${this.basePath}.ownership`]: ownership
- });
-
- await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
- this.render();
- });
- }
-
- static openCountdownOwnership(_, target) {
- const countdownId = target.dataset.countdown;
- new Promise((resolve, reject) => {
- const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.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(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
- await setting.updateSource({
- [`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
- });
-
- await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
- this.render();
- });
- }
-
- static async toggleSimpleView() {
- this.simpleView = !this.simpleView;
- await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
+ if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
+ else this.element.classList.remove('icon-only');
this.render();
}
- async updateCountdownValue(event, increase) {
- const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
- const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
+ static async editCountdown(increase, target) {
+ if (!DhCountdowns.canPerformEdit()) return;
- if (!this.testUserPermission(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
+ const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
+ const countdown = settings.countdowns[target.id];
+ const newCurrent = increase
+ ? Math.min(countdown.progress.current + 1, countdown.progress.max)
+ : Math.max(countdown.progress.current - 1, 0);
+ await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
+ await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
+ refreshType: RefreshType.Countdown
});
-
- await this.updateSetting(countdownSetting.toObject());
}
- static async addCountdown() {
+ static async gmSetSetting(data) {
+ await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data),
+ game.socket.emit(`system.${CONFIG.DH.id}`, {
+ action: socketEvent.Refresh,
+ data: { refreshType: RefreshType.Countdown }
+ });
+ Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
+ }
+
+ setupHooks() {
+ Hooks.on('collapseSidebar', this.toggleCollapsedPosition.bind());
+ Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
+ }
+
+ close(options) {
+ Hooks.off('collapseSidebar', this.toggleCollapsedPosition);
+ Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
+ super.close(options);
+ }
+
+ static async updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
- await countdownSetting.updateSource({
- [`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: {
- name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
- ownership: game.user.isGM
- ? {}
- : {
- players: {
- [game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
- }
- }
+ const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
+ const countdown = countdownSetting.countdowns[key];
+ if (countdown.progress.type === progressType && countdown.progress.current > 0) {
+ acc.push(key);
}
- });
- await this.updateSetting(countdownSetting.toObject());
- }
+ return acc;
+ }, []);
- static async removeCountdown(_, target) {
- const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.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.APPLICATIONS.Countdown.removeCountdownTitle')
- },
- content: game.i18n.format('DAGGERHEART.APPLICATIONS.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(CONFIG.DH.id, CONFIG.DH.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 async function updateCountdowns(progressType) {
- const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
- const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
- return foundry.utils.mergeObject(
- update,
- Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
- const countdown = countdownSetting[typeKey].countdowns[countdownKey];
- if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
- acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
+ const countdownData = countdownSetting.toObject();
+ await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, {
+ ...countdownData,
+ countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
+ const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
+ if (updatedCountdowns.includes(key)) {
+ countdown.progress.current -= 1;
}
+ acc[key] = countdown;
return acc;
}, {})
- );
- }, {});
+ });
- await countdownSetting.updateSource(update);
- await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting);
-
- const data = { refreshType: RefreshType.Countdown };
- await game.socket.emit(`system.${CONFIG.DH.id}`, {
- action: socketEvent.Refresh,
- data
- });
- Hooks.callAll(socketEvent.Refresh, data);
+ const data = { refreshType: RefreshType.Countdown };
+ await game.socket.emit(`system.${CONFIG.DH.id}`, {
+ action: socketEvent.Refresh,
+ data
+ });
+ Hooks.callAll(socketEvent.Refresh, data);
+ }
}
diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs
index 32088bc1..12ab3ee1 100644
--- a/module/config/flagsConfig.mjs
+++ b/module/config/flagsConfig.mjs
@@ -23,5 +23,6 @@ export const compendiumBrowserLite = {
export const itemAttachmentSource = 'attachmentSource';
export const userFlags = {
- welcomeMessage: 'welcome-message'
+ welcomeMessage: 'welcome-message',
+ countdownMode: 'countdown-mode'
};
diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs
index 7afcbdea..900dcfbc 100644
--- a/module/config/generalConfig.mjs
+++ b/module/config/generalConfig.mjs
@@ -650,3 +650,30 @@ 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'
+ }
+};
+
+export const countdownAppMode = {
+ textIcon: 'text-icon',
+ iconOnly: 'icon-only'
+};
diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs
index 62036c38..6db4cbeb 100644
--- a/module/data/countdowns.mjs
+++ b/module/data/countdowns.mjs
@@ -1,25 +1,28 @@
-import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
-
export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() {
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 {
- countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
+ countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)),
ownership: new fields.SchemaField({
default: new fields.NumberField({
required: true,
@@ -56,7 +59,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
}
}
-class DhCountdown extends foundry.abstract.DataModel {
+/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
+class DhOldCountdown extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
@@ -129,17 +133,88 @@ class DhCountdown extends foundry.abstract.DataModel {
}
}
-export const registerCountdownHooks = () => {
- Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => {
- if (refreshType === RefreshType.Countdown) {
- if (application) {
- foundry.applications.instances.get(application)?.render();
- } else {
- foundry.applications.instances.get('narrative-countdowns')?.render();
- foundry.applications.instances.get('encounter-countdowns')?.render();
- }
+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'
+ }),
+ img: new fields.FilePathField({
+ categories: ['IMAGE'],
+ base64: false,
+ initial: 'icons/magic/time/hourglass-yellow-green.webp'
+ }),
+ ownership: new fields.TypedObjectField(
+ new fields.NumberField({
+ required: true,
+ choices: CONFIG.DH.GENERAL.simpleOwnershiplevels,
+ initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
+ })
+ ),
+ progress: new fields.SchemaField({
+ current: new fields.NumberField({
+ required: true,
+ integer: true,
+ initial: 1,
+ label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label'
+ }),
+ max: new fields.NumberField({
+ required: true,
+ integer: true,
+ initial: 1,
+ label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.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'
+ })
+ })
+ };
+ }
- return false;
- }
- });
-};
+ static defaultCountdown(type, playerHidden) {
+ const ownership = playerHidden
+ ? game.users.reduce((acc, user) => {
+ if (!user.isGM) {
+ acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
+ }
+ return acc;
+ }, {})
+ : undefined;
+
+ 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',
+ ownership: ownership,
+ progress: {
+ current: 1,
+ max: 1
+ }
+ };
+ }
+
+ 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;
+ }, {});
+ }
+}
diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs
index 015c19cb..e27fa3bd 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,36 @@ export async function runMigrations() {
await pack.configure({ locked: true });
}
+ /* Migrate old countdown structure */
+ const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
+ const getCountdowns = (data, type) => {
+ return Object.keys(data.countdowns).reduce((acc, key) => {
+ const countdown = data.countdowns[key];
+ acc[key] = {
+ ...countdown,
+ type: type,
+ ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => {
+ acc[key] = countdown.ownership.players[key].type;
+ return acc;
+ }, {}),
+ progress: {
+ ...countdown.progress,
+ type: countdown.progress.type.value
+ }
+ };
+
+ return acc;
+ }, {});
+ };
+
+ await countdownSettings.updateSource({
+ countdowns: {
+ ...getCountdowns(countdownSettings.narrative, CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id),
+ ...getCountdowns(countdownSettings.encounter, CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id)
+ }
+ });
+ await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings);
+
lastMigrationVersion = '1.2.0';
}
diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs
index f75c7b36..14b4cec1 100644
--- a/module/systemRegistration/socket.mjs
+++ b/module/systemRegistration/socket.mjs
@@ -25,6 +25,7 @@ export const GMUpdateEvent = {
UpdateEffect: 'DhGMUpdateEffect',
UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear',
+ UpdateCountdowns: 'DhGMUpdateCountdowns',
UpdateSaveMessage: 'DhGMUpdateSaveMessage'
};
@@ -60,6 +61,10 @@ export const registerSocketHooks = () => {
)
);
break;
+ case GMUpdateEvent.UpdateCountdowns:
+ await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update);
+ Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
+ break;
case GMUpdateEvent.UpdateSaveMessage:
const action = await fromUuid(data.update.action),
message = game.messages.get(data.update.message);
@@ -84,14 +89,15 @@ export const registerUserQueries = () => {
CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery;
};
-export const emitAsGM = async (eventName, callback, update, uuid = null) => {
+export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => {
if (!game.user.isGM) {
return await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: eventName,
uuid,
- update
+ update,
+ refresh
}
});
} else return callback(update);
diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less
new file mode 100644
index 00000000..1460c6ef
--- /dev/null
+++ b/styles/less/ui/countdown/countdown-edit.less
@@ -0,0 +1,142 @@
+@import '../../utils/colors.less';
+@import '../../utils/fonts.less';
+
+.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: 2fr 1fr 144px;
+ gap: 8px;
+
+ .hide-tools {
+ white-space: nowrap;
+ flex-wrap: nowrap;
+ display: flex;
+ align-items: center;
+
+ input {
+ position: relative;
+ top: 2px;
+ }
+ }
+
+ .header-main-button {
+ height: 32px;
+ 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;
+ overflow-y: auto;
+ overflow-x: hidden;
+ max-height: 500px;
+ scrollbar-width: thin;
+ scrollbar-color: light-dark(@dark-blue, @golden) transparent;
+
+ .countdown-edit-container {
+ display: grid;
+ grid-template-columns: 48px 1fr 64px;
+ align-items: center;
+ gap: 8px;
+
+ img {
+ width: 52px;
+ height: 52px;
+ border-radius: 6px;
+ }
+
+ .countdown-edit-text {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+
+ .countdown-edit-subtext {
+ display: flex;
+ gap: 10px;
+
+ .countdown-edit-sub-tag {
+ padding: 3px 5px;
+ font-size: var(--font-size-12);
+ font: @font-body;
+
+ background: light-dark(@dark-15, @beige-15);
+ border: 1px solid light-dark(@dark, @beige);
+ border-radius: 3px;
+ }
+ }
+ }
+
+ .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 {
+ min-width: 2.5rem;
+ }
+ }
+
+ input,
+ select {
+ background: light-dark(@beige, @dark-blue);
+ color: light-dark(@dark, @beige);
+ }
+ }
+ }
+ }
+}
diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less
index 5e46989a..9fa42ec7 100644
--- a/styles/less/ui/countdown/countdown.less
+++ b/styles/less/ui/countdown/countdown.less
@@ -1,60 +1,130 @@
@import '../../utils/colors.less';
@import '../../utils/fonts.less';
-.daggerheart.dh-style.countdown {
- fieldset {
- align-items: center;
- margin-top: 5px;
- border-radius: 6px;
- border-color: light-dark(@dark-blue, @golden);
+.theme-dark {
+ .daggerheart.dh-style.countdowns {
+ background-image: url(../assets/parchments/dh-parchment-dark.png);
- legend {
- font-weight: bold;
- color: light-dark(@dark-blue, @golden);
-
- a {
- text-shadow: none;
- }
+ .window-header {
+ background-image: url(../assets/parchments/dh-parchment-dark.png);
+ }
+ }
+}
+
+.daggerheart.dh-style.countdowns {
+ z-index: var(--z-index-ui) !important;
+ border: 0;
+ border-radius: 4px;
+ box-shadow: none;
+ width: 300px;
+ top: 16px;
+ right: 64px;
+ transition:
+ right ease 250ms,
+ opacity var(--ui-fade-duration) ease,
+ opacity var(--ui-fade-duration);
+
+ .window-title {
+ font-family: @font-body;
+ }
+
+ &.expanded {
+ right: 364px;
+ }
+
+ &.icon-only {
+ width: 180px;
+ min-width: 180px;
+ }
+
+ .window-header {
+ cursor: default;
+ border-bottom: 0;
+ }
+
+ .window-content {
+ padding-top: 4px;
+ padding-bottom: 16px;
+ overflow: auto;
+ max-height: 312px;
+
+ .countdowns-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .countdown-container {
+ display: flex;
+ justify-content: space-between;
+
+ &.icon-only {
+ gap: 8px;
+
+ .countdown-main-container {
+ .countdown-content {
+ justify-content: center;
+
+ .countdown-tools {
+ gap: 8px;
+ }
+ }
+ }
+ }
+
+ .countdown-main-container {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ img {
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ }
+
+ .countdown-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .countdown-tools {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ .progress-tag {
+ border: 1px solid;
+ border-radius: 4px;
+ padding: 2px 4px;
+ background-color: light-dark(@beige, @dark-blue);
+ }
+ }
+ }
+ }
+
+ .countdown-access-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-auto-rows: min-content;
+ width: 38px;
+ gap: 4px;
+
+ .countdown-access {
+ height: 10px;
+ width: 10px;
+ border-radius: 50%;
+ border: 1px solid light-dark(@dark-blue, @beige-80);
+ content: '';
+ }
+ }
+
+ .countdown-no-access-container {
+ width: 38px;
+ display: flex;
+ align-items: center;
+ 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;
- }
}
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/ownership-selection/ownership-selection.less b/styles/less/ui/ownership-selection/ownership-selection.less
index 56fddd4f..76ae0930 100644
--- a/styles/less/ui/ownership-selection/ownership-selection.less
+++ b/styles/less/ui/ownership-selection/ownership-selection.less
@@ -6,9 +6,15 @@
flex-direction: column;
gap: 8px;
+ .ownership-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 10px;
+ }
+
.ownership-container {
display: flex;
- border: 2px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
padding: 0 4px 0 0;
align-items: center;
@@ -17,12 +23,24 @@
img {
height: 40px;
width: 40px;
- border-radius: 6px 0 0 6px;
+ border-radius: 50%;
+ }
+
+ span {
+ flex: 3;
}
select {
+ flex: 1;
margin: 4px 0;
}
}
+
+ footer {
+ margin-top: 10px;
+ button {
+ height: 32px;
+ }
+ }
}
}
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..b16e5d75 100644
--- a/templates/dialogs/ownershipSelection.hbs
+++ b/templates/dialogs/ownershipSelection.hbs
@@ -2,20 +2,22 @@
- {{#each ownership.players as |player id|}}
-
-

-
{{player.name}}
-
-
- {{/each}}
+
+ {{#each ownership as |player id|}}
+ -
+
+ {{player.name}}
+
+
+ {{/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|}}