diff --git a/assets/icons/dice/duality/DualityBW.png b/assets/icons/dice/duality/DualityBW.png new file mode 100644 index 00000000..5f94e99f Binary files /dev/null and b/assets/icons/dice/duality/DualityBW.png differ diff --git a/assets/icons/dice/duality/DualityBW.svg b/assets/icons/dice/duality/DualityBW.svg new file mode 100644 index 00000000..9e716fd6 --- /dev/null +++ b/assets/icons/dice/duality/DualityBW.svg @@ -0,0 +1,59 @@ + + + + + + + diff --git a/daggerheart.mjs b/daggerheart.mjs index d99bc1f0..36cca4ba 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, @@ -144,6 +142,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; @@ -170,10 +169,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(); @@ -246,29 +247,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 81a5d314..b3a7c425 100755 --- a/lang/en.json +++ b/lang/en.json @@ -332,7 +332,8 @@ "label": { "label": "Label", "hint": "Used for custom" }, "value": { "label": "Value" } } - } + }, + "type": { "label": "Countdown Type" } } } }, @@ -347,6 +348,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}?" @@ -1898,6 +1919,10 @@ } } }, + "SpotlightRequests": { + "singular": "Spotlight Request", + "plural": "Spotlight Requests" + }, "Tabs": { "details": "Details", "attack": "Attack", @@ -2046,6 +2071,7 @@ "range": "Range", "reactionRoll": "Reaction Roll", "recovery": "Recovery", + "refresh": "Refresh", "reroll": "Reroll", "rerollThing": "Reroll {thing}", "resource": "Resource", @@ -2298,6 +2324,9 @@ "label": "Apply Effects", "hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions." } + }, + "summaryMessages": { + "label": "Summary Messages" } }, "defeated": { @@ -2419,6 +2448,7 @@ "action": { "title": "Action" }, + "appliedTo": "Applied To", "applyEffect": { "title": "Apply Effects - {name}" }, @@ -2428,6 +2458,11 @@ "rollHealing": "Roll Healing", "applyEffect": "Apply Effects" }, + "clearResource": "Clear {quantity} {resource}", + "damageSummary": { + "title": "Damage Applied", + "healingTitle": "Healing Applied" + }, "damageRoll": { "title": "Damage - {damage}", "dealDamageToTargets": "Damage Hit Targets", @@ -2449,12 +2484,16 @@ "dualityRoll": { "abilityCheckTitle": "{ability} Check" }, + "effectSummary": { + "title": "Effects Applied" + }, "featureTitle": "Class Feature", "healingRoll": { "title": "Heal - {damage}", "heal": "Heal", "applyHealing": "Apply Healing" }, + "markResource": "Mark {quantity} {resource}", "refreshMessage": { "title": "Feature Refresh", "header": "Refreshed" @@ -2475,6 +2514,11 @@ "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, + "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", @@ -2584,7 +2628,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/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'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index bb5e04a1..0efde2de 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -16,7 +16,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.action = config.data.attack?._id == config.source.action ? config.data.attack - : this.item.system.actions.get(config.source.action); + : this.item.system.actionsList?.find(a => a.id === config.source.action); } } 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/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index bdd9fa68..dd845d34 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -99,6 +99,7 @@ export default function DHApplicationMixin(Base) { deleteDoc: DHSheetV2.#deleteDoc, toChat: DHSheetV2.#toChat, useItem: DHSheetV2.#useItem, + viewItem: DHSheetV2.#viewItem, toggleEffect: DHSheetV2.#toggleEffect, toggleExtended: DHSheetV2.#toggleExtended, addNewItem: DHSheetV2.#addNewItem, @@ -203,11 +204,19 @@ export default function DHApplicationMixin(Base) { this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]); } + /** @inheritdoc */ + async _renderHTML(context, options) { + const rendered = await super._renderHTML(context, options); + for (const result of Object.values(rendered)) { + await this.#prepareInventoryDescription(result); + } + return rendered; + } + /**@inheritdoc */ async _onRender(context, options) { await super._onRender(context, options); this._createTagifyElements(this.options.tagifyConfigs); - await this.#prepareInventoryDescription(context); } /* -------------------------------------------- */ @@ -215,8 +224,8 @@ export default function DHApplicationMixin(Base) { /* -------------------------------------------- */ /**@inheritdoc */ - _syncPartState(partId, newElement, priorElement, state) { - super._syncPartState(partId, newElement, priorElement, state); + _preSyncPartState(partId, newElement, priorElement, state) { + super._preSyncPartState(partId, newElement, priorElement, state); for (const el of priorElement.querySelectorAll('.extensible.extended')) { const { actionId, itemUuid } = el.parentElement.dataset; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; @@ -496,11 +505,12 @@ export default function DHApplicationMixin(Base) { /** * Prepares and enriches an inventory item or action description for display. + * @param {HTMLElement} element the element to enrich the inventory items of * @returns {Promise} */ - async #prepareInventoryDescription(context) { + async #prepareInventoryDescription(element) { // Get all inventory item elements with a data-item-uuid attribute - const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); + const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]'); for (const el of inventoryItems) { // Get the doc uuid from the element const { itemUuid } = el?.dataset || {}; @@ -701,7 +711,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #toChat(_event, target) { - let doc = await getDocFromElement(target); + const doc = await getDocFromElement(target); return doc.toChat(doc.uuid); } @@ -710,10 +720,19 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #useItem(event, target) { - let doc = await getDocFromElement(target); + const doc = await getDocFromElement(target); await doc.use(event); } + /** + * View an item by opening its sheet + * @type {ApplicationClickAction} + */ + static async #viewItem(_, target) { + const doc = await getDocFromElement(target); + await doc.sheet.render({ force: true }); + } + /** * Toggle a ActiveEffect * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index f719b6d1..21ccbbf1 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -66,7 +66,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { /**@inheritdoc */ async _prepareContext(options) { - const context = super._prepareContext(options); + const context = await super._prepareContext(options); context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) .hideAttribution; diff --git a/module/applications/sidebar/sidebar.mjs b/module/applications/sidebar/sidebar.mjs index fad39ac5..ae28d56c 100644 --- a/module/applications/sidebar/sidebar.mjs +++ b/module/applications/sidebar/sidebar.mjs @@ -1,10 +1,49 @@ -export default class DhSidebar extends Sidebar { +export default class DhSidebar extends foundry.applications.sidebar.Sidebar { /** @override */ static TABS = { - ...super.TABS, + chat: { + documentName: 'ChatMessage' + }, + combat: { + documentName: 'Combat' + }, + scenes: { + documentName: 'Scene', + gmOnly: true + }, + actors: { + documentName: 'Actor' + }, + items: { + documentName: 'Item' + }, + journal: { + documentName: 'JournalEntry', + tooltip: 'SIDEBAR.TabJournal' + }, + tables: { + documentName: 'RollTable' + }, + cards: { + documentName: 'Cards' + }, + macros: { + documentName: 'Macro' + }, + playlists: { + documentName: 'Playlist' + }, + compendium: { + tooltip: 'SIDEBAR.TabCompendium', + icon: 'fa-solid fa-book-atlas' + }, daggerheartMenu: { tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title', img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' + }, + settings: { + tooltip: 'SIDEBAR.TabSettings', + icon: 'fa-solid fa-gears' } }; diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 4094cc24..a1367aac 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -28,8 +28,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract title: 'SIDEBAR.TabSettings' }, actions: { - selectRefreshable: DaggerheartMenu.selectRefreshable, - refreshActors: DaggerheartMenu.refreshActors + selectRefreshable: DaggerheartMenu.#selectRefreshable, + refreshActors: DaggerheartMenu.#refreshActors, + editCountdowns: DaggerheartMenu.#editCountdowns } }; @@ -123,13 +124,13 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract /* Application Clicks Actions */ /* -------------------------------------------- */ - static async selectRefreshable(_event, button) { + static async #selectRefreshable(_event, button) { const { type } = button.dataset; this.refreshSelections[type].selected = !this.refreshSelections[type].selected; this.render(); } - static async refreshActors() { + static async #refreshActors() { const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); await this.getRefreshables(refreshKeys); const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', '); @@ -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 85c77cd3..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: { @@ -36,10 +34,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 +123,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 +152,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 } }); @@ -168,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/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index a00f8edc..33995aa9 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -16,6 +16,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this.selectedMenu = { path: [], data: null }; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.presets = {}; + this.compendiumBrowserTypeKey = 'compendiumBrowserDefault'; } /** @inheritDoc */ @@ -84,10 +85,26 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { /** @inheritDoc */ async _preRender(context, options) { this.presets = options.presets ?? {}; + const noFolder = this.presets?.render?.noFolder; + if (noFolder === true) { + this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder'; + } + const lite = this.presets?.render?.lite; + if (lite === true) { + this.compendiumBrowserTypeKey = 'compendiumBrowserLite'; + } + const userPresetPosition = game.user.getFlag( + CONFIG.DH.id, + CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position + ); - const width = this.presets?.render?.noFolder === true || this.presets?.render?.lite === true ? 600 : 850; - if (this.rendered) this.setPosition({ width }); - else options.position.width = width; + options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position; + + if (!userPresetPosition) { + const width = noFolder === true || lite === true ? 600 : 850; + if (this.rendered) this.setPosition({ width }); + else options.position.width = width; + } await super._preRender(context, options); } @@ -113,6 +130,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { }); } + _onPosition(position) { + game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position); + } + _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index c2a6dff2..12ab3ee1 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -8,8 +8,21 @@ export const encounterCountdown = { position: 'countdown-encounter-position' }; +export const compendiumBrowserDefault = { + position: 'compendium-browser-default-position' +}; + +export const compendiumBrowserNoFolder = { + position: 'compendium-browser-no-folder-position' +}; + +export const compendiumBrowserLite = { + position: 'compendium-browser-lite-position' +}; + 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/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index e870172f..2c3e1dfb 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -370,19 +370,6 @@ export const typeConfig = { label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait' } ], - filters: [] - }, - beastforms: { - columns: [ - { - key: 'system.tier', - label: 'DAGGERHEART.GENERAL.Tiers.singular' - }, - { - key: 'system.mainTrait', - label: 'DAGGERHEART.GENERAL.Trait.single' - } - ], filters: [ { key: 'system.linkedClass.uuid', diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 7e301906..67046248 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,9 +1,11 @@ import DHAbilityUse from './abilityUse.mjs'; import DHActorRoll from './actorRoll.mjs'; +import DHSystemMessage from './systemMessage.mjs'; export const config = { abilityUse: DHAbilityUse, adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, - dualityRoll: DHActorRoll + dualityRoll: DHActorRoll, + systemMessage: DHSystemMessage }; diff --git a/module/data/chat-message/systemMessage.mjs b/module/data/chat-message/systemMessage.mjs new file mode 100644 index 00000000..cd2a06a1 --- /dev/null +++ b/module/data/chat-message/systemMessage.mjs @@ -0,0 +1,9 @@ +export default class DHSystemMessage extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + useTitle: new fields.BooleanField({ initial: true }) + }; + } +} 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/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/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index 2d2a38df..656edee3 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -103,7 +103,7 @@ export default class CostField extends fields.ArrayField { static calcCosts(costs) { const resources = CostField.getResources.call(this, costs); let filteredCosts = costs; - if (this.parent.metadata.isQuantifiable && this.parent.consumeOnUse === false) { + if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) { filteredCosts = filteredCosts.filter(c => c.key !== 'quantity'); } diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 26d720c0..43623c94 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -81,6 +81,9 @@ export default class DamageField extends fields.SchemaField { static async applyDamage(config, targets = null, force = false) { targets ??= config.targets.filter(target => target.hit); if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return; + + const targetDamage = []; + const damagePromises = []; for (let target of targets) { const actor = fromUuidSync(target.actorId); if (!actor) continue; @@ -95,9 +98,45 @@ export default class DamageField extends fields.SchemaField { }); } - if (config.hasHealing) actor.takeHealing(config.damage); - else actor.takeDamage(config.damage, config.isDirect); + if (config.hasHealing) + damagePromises.push( + actor + .takeHealing(config.damage) + .then(updates => targetDamage.push({ token: actor.token ?? actor.prototypeToken, updates })) + ); + else + damagePromises.push( + actor + .takeDamage(config.damage, config.isDirect) + .then(updates => targetDamage.push({ token: actor.token ?? actor.prototypeToken, updates })) + ); } + + Promise.all(damagePromises).then(async _ => { + const summaryMessageSettings = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Automation + ).summaryMessages; + if (!summaryMessageSettings.damage) return; + + const cls = getDocumentClass('ChatMessage'); + const msg = { + type: 'systemMessage', + user: game.user.id, + speaker: cls.getSpeaker(), + title: game.i18n.localize( + `DAGGERHEART.UI.Chat.damageSummary.${config.hasHealing ? 'healingTitle' : 'title'}` + ), + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/damageSummary.hbs', + { + targets: targetDamage + } + ) + }; + + cls.create(msg); + }); } /** diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 0f205d72..887607ba 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -46,17 +46,48 @@ export default class EffectsField extends fields.ArrayField { */ static async applyEffects(targets) { if (!this.effects?.length || !targets?.length) return; + let effects = this.effects; - targets.forEach(async token => { + const messageTargets = []; + targets.forEach(async baseToken => { if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (!effects.length) return; + + const token = canvas.tokens.get(baseToken.id); + if (!token) return; + messageTargets.push(token.document); + effects.forEach(async e => { - const actor = canvas.tokens.get(token.id)?.actor, - effect = this.item.effects.get(e._id); - if (!actor || !effect) return; - await EffectsField.applyEffect(effect, actor); + const effect = this.item.effects.get(e._id); + if (!token.actor || !effect) return; + await EffectsField.applyEffect(effect, token.actor); }); }); + + if (messageTargets.length === 0) return; + + const summaryMessageSettings = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Automation + ).summaryMessages; + if (!summaryMessageSettings.effects) return; + + const cls = getDocumentClass('ChatMessage'); + const msg = { + type: 'systemMessage', + user: game.user.id, + speaker: cls.getSpeaker(), + title: game.i18n.localize('DAGGERHEART.UI.Chat.effectSummary.title'), + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/effectSummary.hbs', + { + effects: this.effects.map(e => this.item.effects.get(e._id)), + targets: messageTargets + } + ) + }; + + cls.create(msg); } /** diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index beefac0b..c54a1c31 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -2,6 +2,10 @@ export default class DhAutomation extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; return { + summaryMessages: new fields.SchemaField({ + damage: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.GENERAL.damage' }), + effects: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.GENERAL.Effect.plural' }) + }), hopeFear: new fields.SchemaField({ gm: new fields.BooleanField({ required: true, diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 0057fc98..b3505447 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -501,6 +501,7 @@ export default class DhpActor extends Actor { /**@inheritdoc */ getRollData() { const rollData = super.getRollData(); + rollData.name = this.name; rollData.system = this.system.getRollData(); rollData.prof = this.system.proficiency ?? 1; rollData.cast = this.system.spellcastModifier ?? 1; @@ -598,6 +599,8 @@ export default class DhpActor extends Actor { await this.modifyResource(updates); if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, updates) === false) return null; + + return updates; } calculateDamage(baseDamage, type) { @@ -646,6 +649,8 @@ export default class DhpActor extends Actor { await this.modifyResource(updates); if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null; + + return updates; } async modifyResource(resources) { diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index edf2491d..3a5fef38 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -143,6 +143,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { html.querySelectorAll('.button-target-selection').forEach(element => { element.addEventListener('click', this.onTargetSelection.bind(this)); }); + + html.querySelectorAll('.token-target-container').forEach(element => { + element.addEventListener('pointerover', this.hoverTarget); + element.addEventListener('pointerout', this.unhoverTarget); + element.addEventListener('click', this.clickTarget); + }); } async onRollDamage(event) { diff --git a/module/enrichers/DamageEnricher.mjs b/module/enrichers/DamageEnricher.mjs index a52c4b31..e3f9c42a 100644 --- a/module/enrichers/DamageEnricher.mjs +++ b/module/enrichers/DamageEnricher.mjs @@ -1,29 +1,8 @@ +import { parseInlineParams } from './parser.mjs'; + export default function DhDamageEnricher(match, _options) { - const parts = match[1].split('|').map(x => x.trim()); - - let value = null, - type = null, - inline = false; - - parts.forEach(part => { - const split = part.split(':').map(x => x.toLowerCase().trim()); - if (split.length === 2) { - switch (split[0]) { - case 'value': - value = split[1]; - break; - case 'type': - type = split[1]; - break; - case 'inline': - inline = true; - break; - } - } - }); - - if (!value || !value) return match[0]; - + const { value, type, inline } = parseInlineParams(match[1]); + if (!value || !type) return match[0]; return getDamageMessage(value, type, inline, match[0]); } diff --git a/module/enrichers/LookupEnricher.mjs b/module/enrichers/LookupEnricher.mjs new file mode 100644 index 00000000..3566e112 --- /dev/null +++ b/module/enrichers/LookupEnricher.mjs @@ -0,0 +1,8 @@ +import { parseInlineParams } from './parser.mjs'; + +export default function DhLookupEnricher(match, { rollData }) { + const results = parseInlineParams(match[1], { first: 'formula' }); + const element = document.createElement('span'); + element.textContent = Roll.replaceFormulaData(String(results.formula), rollData); + return element; +} diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs index 15936b29..4c98e196 100644 --- a/module/enrichers/TemplateEnricher.mjs +++ b/module/enrichers/TemplateEnricher.mjs @@ -1,55 +1,25 @@ +import { parseInlineParams } from './parser.mjs'; + export default function DhTemplateEnricher(match, _options) { - const parts = match[1].split('|').map(x => x.trim()); - - let type = null, - range = null, - angle = CONFIG.MeasuredTemplate.defaults.angle, - direction = 0, - inline = false; - - 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(CONFIG.DH.GENERAL.templateTypes).find( - x => x.toLowerCase() === split[1] - ); - type = matchedType; - break; - case 'range': - if (Number.isNaN(Number(split[1]))) { - const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find( - x => x.id.toLowerCase() === split[1] || x.short === split[1] - ); - range = matchedRange?.id; - } else { - range = split[1]; - } - break; - case 'inline': - inline = true; - break; - case 'angle': - angle = split[1]; - break; - case 'direction': - direction = split[1]; - break; - } - } - }); - - if (!type || !range) return match[0]; + const params = parseInlineParams(match[1]); + const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params; + const direction = Number(params.direction) || 0; + const range = + params.range && Number.isNaN(params.range) + ? Object.values(CONFIG.DH.GENERAL.templateRanges).find( + x => x.id.toLowerCase() === split[1] || x.short === split[1] + )?.id + : params.range; + if (!(type in CONFIG.MeasuredTemplate.types) || !range) return match[0]; const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`); - - const rangeDisplay = Number.isNaN(Number(range)) ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`) : range; + const rangeDisplay = Number.isNaN(Number(range)) + ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`) + : range; let angleDisplay = ''; if (angle != CONFIG.MeasuredTemplate.defaults.angle) { angleDisplay = 'angle:' + angle; - } let directionDisplay = ''; if (direction != 0) { @@ -95,8 +65,9 @@ export const renderMeasuredTemplate = async event => { let baseDistance = range; if (Number.isNaN(Number(range))) { - baseDistance = - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[range]; + baseDistance = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[ + range + ]; } const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance; diff --git a/module/enrichers/_module.mjs b/module/enrichers/_module.mjs index deec4250..794756f4 100644 --- a/module/enrichers/_module.mjs +++ b/module/enrichers/_module.mjs @@ -2,6 +2,7 @@ import { default as DhDamageEnricher, renderDamageButton } from './DamageEnriche import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; import { default as DhEffectEnricher } from './EffectEnricher.mjs'; import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; +import { default as DhLookupEnricher } from './LookupEnricher.mjs'; export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; @@ -21,6 +22,10 @@ export const enricherConfig = [ { pattern: /@Template\[(.*)\]({.*})?/g, enricher: DhTemplateEnricher + }, + { + pattern: /@Lookup\[(.*)\]({.*})?/g, + enricher: DhLookupEnricher } ]; diff --git a/module/enrichers/parser.mjs b/module/enrichers/parser.mjs new file mode 100644 index 00000000..365caec9 --- /dev/null +++ b/module/enrichers/parser.mjs @@ -0,0 +1,20 @@ +/** + * @param {string} paramString The parameter inside the brackets of something like @Template[] to parse + * @param {Object} options + * @param {string} options.first If set, the first parameter is treated as a value with this as its key + * @returns {Record | null} + */ +export function parseInlineParams(paramString, { first } = {}) { + const parts = paramString.split('|').map(x => x.trim()); + const params = {}; + for (const [idx, param] of parts.entries()) { + if (first && idx === 0) { + params[first] = param; + } else { + const parts = param.split(':'); + params[parts[0]] = parts.length > 1 ? parts[1] : true; + } + } + + return params; +} diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 26e86012..847b04ce 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -15,7 +15,7 @@ export default class RegisterHandlebarsHelpers { setVar: this.setVar, empty: this.empty, pluralize: this.pluralize, - log: this.log + positive: this.positive }); } static add(a, b) { @@ -91,7 +91,7 @@ export default class RegisterHandlebarsHelpers { return game.i18n.localize(key); } - static log(test) { - return test; + static positive(a) { + return Math.abs(Number(a)); } } diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 24047827..dd19e429 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -29,7 +29,6 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', - 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 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 68392381..ac61238f 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -30,6 +30,7 @@ export const GMUpdateEvent = { UpdateEffect: 'DhGMUpdateEffect', UpdateSetting: 'DhGMUpdateSetting', UpdateFear: 'DhGMUpdateFear', + UpdateCountdowns: 'DhGMUpdateCountdowns', UpdateSaveMessage: 'DhGMUpdateSaveMessage' }; @@ -66,6 +67,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); @@ -90,14 +95,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/global/chat.less b/styles/less/global/chat.less index 95ea956f..3f83294a 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -61,6 +61,7 @@ width: 40px; height: 40px; object-fit: cover; + object-position: top center; } .message-sub-header-container { diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index e221f4e7..1f5840af 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -26,7 +26,7 @@ &:not(.single-img) { .inventory-item-header:hover { - .img-portait { + .img-portait:has(.roll-img) { .roll-img { opacity: 1; } @@ -96,7 +96,9 @@ } .roll-img { + object-fit: contain; opacity: 0; + padding: 2px; } } @@ -307,12 +309,14 @@ background-color: @dark-blue; bottom: 0; mask-image: linear-gradient(180deg, transparent 0%, black 20%); + border-radius: 0 0 6px 6px; .card-name { font-style: normal; font-weight: 400; font-size: var(--font-size-12); line-height: 15px; + text-align: center; color: @beige; } diff --git a/styles/less/global/tab-description.less b/styles/less/global/tab-description.less index 4d81f2f2..be95ef6d 100644 --- a/styles/less/global/tab-description.less +++ b/styles/less/global/tab-description.less @@ -1,13 +1,16 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.daggerheart.dh-style { - .tab.active.description { - overflow-y: hidden !important; - height: -webkit-fill-available !important; - - fieldset { - height: -webkit-fill-available; - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.daggerheart.dh-style { + .tab.active.description { + display: flex; + flex-direction: column; + height: -webkit-fill-available !important; + overflow-y: hidden !important; + padding-top: 10px; + + prose-mirror.active + .artist-attribution { + display: none; + } + } +} diff --git a/styles/less/ui/chat/damage-summary.less b/styles/less/ui/chat/damage-summary.less new file mode 100644 index 00000000..02fdbadf --- /dev/null +++ b/styles/less/ui/chat/damage-summary.less @@ -0,0 +1,87 @@ +@import '../../utils/colors.less'; + +#interface.theme-light { + .daggerheart.chat.damage-summary .token-target-container { + &:hover { + background: @dark-blue-10; + } + + header { + .actor-name { + color: @dark; + } + + &::after { + background: @dark-blue; + } + } + } +} + +.daggerheart.chat.damage-summary { + display: flex; + flex-direction: column; + gap: 5px; + padding: 0; + + .token-target-container { + display: flex; + flex-direction: column; + gap: 2px; + cursor: pointer; + transition: all 0.3s ease; + border-radius: 6px; + + &:hover { + background: @golden-10; + } + + header { + display: flex; + align-items: center; + gap: 5px; + pointer-events: none; + position: relative; + margin-bottom: 10px; + + img { + width: 40px; + height: 40px; + padding: 0; + border-radius: 50%; + } + + .actor-name { + margin: 0; + color: @beige; + font-size: var(--font-size-20); + padding: 8px; + } + + &::after { + content: ''; + position: absolute; + bottom: -10px; + background: @golden; + mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%); + height: 2px; + width: 100%; + } + } + + .damage-container { + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + pointer-events: none; + margin-top: 5px; + list-style: disc; + + .damage-row { + padding: 0 2px; + gap: 4px; + } + } + } +} diff --git a/styles/less/ui/chat/effect-summary.less b/styles/less/ui/chat/effect-summary.less new file mode 100644 index 00000000..9bea1fd9 --- /dev/null +++ b/styles/less/ui/chat/effect-summary.less @@ -0,0 +1,166 @@ +@import '../../utils/colors.less'; + +#interface.theme-light { + .daggerheart.chat.effect-summary { + .effect-header, + .actor-header { + &::before, + &::after { + height: 2px; + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @dark-blue 100%); + } + + &::after { + background: linear-gradient(90deg, @dark-blue 0%, rgba(0, 0, 0, 0) 100%); + } + + span { + color: @dark; + } + } + + .token-target-container, + .effect-target-container { + .effect-label .title, + .title { + color: @dark-blue; + } + + .effect-label { + border-color: @dark-blue; + } + + &:hover { + background: @dark-blue-10; + } + } + } +} + +.daggerheart.chat.effect-summary { + display: flex; + flex-direction: column; + + .effect-header, + .actor-header { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 5px; + + &::before, + &::after { + content: ''; + flex: 1; + height: 2px; + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @golden 100%); + } + + &::after { + background: linear-gradient(90deg, @golden 0%, rgba(0, 0, 0, 0) 100%); + } + + span { + color: @beige; + padding: 0 10px; + white-space: nowrap; + } + } + + .effects-container { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-bottom: 8px; + } + + .targets-container { + display: flex; + flex-direction: column; + gap: 5px; + } + + .token-target-container { + display: flex; + align-items: center; + gap: 13px; + border-radius: 6px; + padding: 0 2px; + border-radius: 6px; + background: transparent; + transition: all 0.3s ease; + padding: 5px; + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + background: @golden-10; + } + + img { + width: 40px; + height: 40px; + border-radius: 50%; + pointer-events: none; + } + + .title { + font-size: var(--font-size-20); + color: @golden; + font-weight: 700; + margin: 0; + pointer-events: none; + } + } + + details[open] { + .fa-chevron-down { + transform: rotate(180deg); + transition: all 0.3s ease; + } + } + + .effect-target-container { + width: 100%; + transition: all 0.3s ease; + cursor: pointer; + + &:hover { + background: @golden-10; + } + + .fa-chevron-down { + transition: all 0.3s ease; + margin-left: auto; + } + + .effect-label { + display: flex; + flex-direction: row; + align-items: center; + margin: 8px 8px 0; + padding-bottom: 5px; + width: -webkit-fill-available; + gap: 13px; + border-bottom: 1px solid @golden; + + .effect-img { + width: 40px; + height: 40px; + border-radius: 3px; + object-fit: cover; + } + + .title { + font-size: var(--font-size-20); + color: @golden; + font-weight: 700; + margin: 0; + } + } + + .description { + padding: 8px; + } + } +} diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less new file mode 100644 index 00000000..9051cccb --- /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..296ef325 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -1,7 +1,9 @@ @import './chat/ability-use.less'; @import './chat/action.less'; @import './chat/chat.less'; +@import './chat/damage-summary.less'; @import './chat/downtime.less'; +@import './chat/effect-summary.less'; @import './chat/refresh-message.less'; @import './chat/sheet.less'; @@ -13,6 +15,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/system.json b/system.json index b37c7f1e..8831b42a 100644 --- a/system.json +++ b/system.json @@ -270,7 +270,8 @@ "adversaryRoll": {}, "damageRoll": {}, "abilityUse": {}, - "tagTeam": {} + "tagTeam": {}, + "systemMessage": {} } }, "background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png", 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 @@
- + {{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true }}
- {{#each ownership.players 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|}}
- {{tier.name}} + {{localize tier.name}} {{#each tier.groups}}
diff --git a/templates/settings/automation-settings/general.hbs b/templates/settings/automation-settings/general.hbs index 211ee68e..8921ab6a 100644 --- a/templates/settings/automation-settings/general.hbs +++ b/templates/settings/automation-settings/general.hbs @@ -7,7 +7,11 @@ {{formGroup settingFields.schema.fields.hopeFear.fields.gm value=settingFields._source.hopeFear.gm localize=true}} {{formGroup settingFields.schema.fields.hopeFear.fields.players value=settingFields._source.hopeFear.players localize=true}} - +
+
+ + {{formGroup settingFields.schema.fields.summaryMessages.fields.damage value=settingFields._source.summaryMessages.damage localize=true}} + {{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}}
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index 1dd92128..ae95b7af 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -19,6 +19,6 @@
- {{item.name}} + {{item.name}} \ No newline at end of file diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index cee14ac5..e365fb6a 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -23,7 +23,13 @@ Parameters: (hasProperty item "toChat" ) "toChat" "editDoc" ) }}' {{#unless hideTooltip}} {{#if (eq type 'attack' )}} data-tooltip="#attack#{{item.actor.uuid}}" {{else}} data-tooltip="#item#{{item.uuid}}" {{/if}} {{/unless}}> - d20 + {{#if (or item.system.actionsList.size item.system.actionsList.length item.actionType)}} + {{#if @root.isNPC}} + d20 + {{else}} + 2d12 + {{/if}} + {{/if}} {{!-- Name & Tags --}} diff --git a/templates/sheets/global/tabs/tab-description.hbs b/templates/sheets/global/tabs/tab-description.hbs index 6d0669e0..71995a51 100755 --- a/templates/sheets/global/tabs/tab-description.hbs +++ b/templates/sheets/global/tabs/tab-description.hbs @@ -3,10 +3,7 @@ data-tab='{{tabs.description.id}}' data-group='{{tabs.description.group}}' > -
- {{localize "DAGGERHEART.GENERAL.description"}} - {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} -
+ {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} {{#if (and showAttribution document.system.attribution.artist)}} diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs index 6f31f165..137f2e3d 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"}} @@ -16,7 +18,9 @@ {{/each}}
- + - \ No newline at end of file + + + diff --git a/templates/ui/chat/damageSummary.hbs b/templates/ui/chat/damageSummary.hbs new file mode 100644 index 00000000..aa8246e1 --- /dev/null +++ b/templates/ui/chat/damageSummary.hbs @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/templates/ui/chat/effectSummary.hbs b/templates/ui/chat/effectSummary.hbs new file mode 100644 index 00000000..1b7badb2 --- /dev/null +++ b/templates/ui/chat/effectSummary.hbs @@ -0,0 +1,33 @@ +
+
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} +
+
+ {{#each effects}} +
+ + +

{{this.name}}

+ +
+
+ {{{this.description}}} +
+
+ {{/each}} +
+ + {{#if targets}} +
+ {{localize "DAGGERHEART.UI.Chat.appliedTo"}} +
+
+ {{#each targets}} +
+ +

{{this.name}}

+
+ {{/each}} +
+ {{/if}} +
\ No newline at end of file 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}} diff --git a/templates/ui/combatTracker/combatTrackerHeader.hbs b/templates/ui/combatTracker/combatTrackerHeader.hbs index 9ecff9e6..fe7d33a0 100644 --- a/templates/ui/combatTracker/combatTrackerHeader.hbs +++ b/templates/ui/combatTracker/combatTrackerHeader.hbs @@ -67,7 +67,6 @@ {{!-- Combat Controls --}}
{{#if hasCombat}} -
+
+ + +
+ {{#if isGM}} +
+ + +
+ {{/if}} +
+ +
+ {{#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 diff --git a/templates/ui/countdowns.hbs b/templates/ui/countdowns.hbs index 11a112ae..122b36e6 100644 --- a/templates/ui/countdowns.hbs +++ b/templates/ui/countdowns.hbs @@ -1,45 +1,33 @@
- {{#if simple}} - - {{else}} -
-
- {{#if canCreate}}{{/if}} - {{#if isGM}}{{/if}} -
- -
- {{#each source.countdowns}} -
- - {{this.name}} - {{#if this.canEdit}}{{/if}} - {{#if @root.isGM}}{{/if}} - - - -
- -
- {{formGroup @root.countdownFields.name name=(concat @root.base ".countdowns." @key ".name") value=this.name localize=true disabled=(not this.canEdit)}} -
- {{formGroup @root.countdownFields.progress.fields.current name=(concat @root.base ".countdowns." @key ".progress.current") value=this.progress.current localize=true disabled=(not this.canEdit)}} - {{formGroup @root.countdownFields.progress.fields.max name=(concat @root.base ".countdowns." @key ".progress.max") value=this.progress.max localize=true disabled=(not this.canEdit)}} -
- {{formGroup @root.countdownFields.progress.fields.type.fields.value name=(concat @root.base ".countdowns." @key ".progress.type.value") value=this.progress.type.value localize=true localize=true disabled=(not this.canEdit)}} +
+ {{#each countdowns as | countdown id |}} +
+
+ +
+ {{#unless ../iconOnly}}{{/unless}} +
+ {{#if countdown.editable}}{{/if}} +
+ {{countdown.progress.current}}/{{countdown.progress.max}}
+ {{#if countdown.editable}}{{/if}}
-
- {{/each}} +
+
+ {{#if (and @root.isGM (not ../iconOnly))}} + {{#if (gt countdown.playerAccess.length 0)}} +
+ {{#each countdown.playerAccess as |player|}} +
+ {{/each}} +
+ {{/if}} + {{#if countdown.noPlayerAccess}} +
+ {{/if}} + {{/if}}
- - {{/if}} + {{/each}} + \ No newline at end of file