diff --git a/daggerheart.mjs b/daggerheart.mjs index a2f41735..f25077d6 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -134,6 +134,8 @@ Hooks.once('init', () => { CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.hotbar = applications.ui.DhHotbar; + CONFIG.ui.sidebar = applications.sidebar.DhSidebar; + CONFIG.ui.daggerheartMenu = applications.sidebar.DaggerheartMenu; CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.ui.resources = applications.ui.DhFearTracker; diff --git a/lang/en.json b/lang/en.json index 1088a94f..b8602f37 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2314,6 +2314,10 @@ "heal": "Heal", "applyHealing": "Apply Healing" }, + "refreshMessage": { + "title": "Feature Refresh", + "header": "Refreshed" + }, "reroll": { "confirmTitle": "Reroll Dice", "confirmText": "Are you sure you want to reroll?" @@ -2384,7 +2388,15 @@ "insufficientResources": "You have insufficient resources", "multiclassAlreadyPresent": "You already have a class and multiclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass", - "noDiceSystem": "Your selected dice {system} does not have a {faces} dice" + "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", + "gmMenuRefresh": "You refreshed all actions and resources {types}" + }, + "Sidebar": { + "daggerheartMenu": { + "title": "Daggerheart Menu", + "startSession": "Start Session", + "startScene": "Start Scene" + } }, "Tooltip": { "disableEffect": "Disable Effect", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index d4ceb229..39320cff 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -5,5 +5,6 @@ export * as levelup from './levelup/_module.mjs'; export * as settings from './settings/_module.mjs'; export * as sheets from './sheets/_module.mjs'; export * as sheetConfigs from './sheets-configs/_module.mjs'; +export * as sidebar from './sidebar/_module.mjs'; export * as ui from './ui/_module.mjs'; export * as ux from './ux/_module.mjs'; diff --git a/module/applications/sidebar/_module.mjs b/module/applications/sidebar/_module.mjs new file mode 100644 index 00000000..f19f697c --- /dev/null +++ b/module/applications/sidebar/_module.mjs @@ -0,0 +1,2 @@ +export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs'; +export { default as DhSidebar } from './sidebar.mjs'; diff --git a/module/applications/sidebar/sidebar.mjs b/module/applications/sidebar/sidebar.mjs new file mode 100644 index 00000000..2a25e02a --- /dev/null +++ b/module/applications/sidebar/sidebar.mjs @@ -0,0 +1,9 @@ +export default class DhSidebar extends Sidebar { + static TABS = { + ...super.TABS, + daggerheartMenu: { + tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title', + icon: 'fa-solid fa-bars' + } + }; +} diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs new file mode 100644 index 00000000..cf7aeae3 --- /dev/null +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -0,0 +1,160 @@ +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { AbstractSidebarTab } = foundry.applications.sidebar; +/** + * The daggerheart menu tab. + * @extends {AbstractSidebarTab} + * @mixes HandlebarsApplication + */ +export default class DaggerheartMenu extends HandlebarsApplicationMixin(AbstractSidebarTab) { + constructor(options) { + super(options); + + this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + } + + static #defaultRefreshSelections() { + return { + session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') }, + scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') }, + longRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.longrest') }, + shortRest: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.shortrest') } + }; + } + + /** @override */ + static DEFAULT_OPTIONS = { + classes: ['dh-style'], + window: { + title: 'SIDEBAR.TabSettings' + }, + actions: { + selectRefreshable: DaggerheartMenu.#selectRefreshable, + refreshActors: DaggerheartMenu.#refreshActors + } + }; + + /** @override */ + static tabName = 'daggerheartMenu'; + + /** @override */ + static PARTS = { + main: { template: 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs' } + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.refreshables = this.refreshSelections; + context.disableRefresh = Object.values(this.refreshSelections).every(x => !x.selected); + + return context; + } + + async getRefreshables(types) { + const refreshedActors = {}; + for (let actor of game.actors) { + if (['character', 'adversary'].includes(actor.type) && actor.prototypeToken.actorLink) { + const updates = {}; + for (let item of actor.items) { + if (item.system.metadata.hasResource && types.includes(item.system.resource?.recovery)) { + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[item.system.resource.recovery].label) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + const increasing = + item.system.resource.progression === CONFIG.DH.ITEM.itemResourceProgression.increasing.id; + updates[item.id].system = { + ...updates[item.id].system, + 'resource.value': increasing + ? 0 + : Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + }; + } + if (item.system.metadata.hasActions) { + const refreshTypes = new Set(); + const actions = item.system.actions.filter(action => { + if (types.includes(action.uses.recovery)) { + refreshTypes.add(action.uses.recovery); + return true; + } + + return false; + }); + if (actions.length === 0) continue; + + if (!refreshedActors[actor.id]) + refreshedActors[actor.id] = { name: actor.name, img: actor.img, refreshed: new Set() }; + refreshedActors[actor.id].refreshed.add( + ...refreshTypes.map(type => game.i18n.localize(CONFIG.DH.GENERAL.refreshTypes[type].label)) + ); + + if (!updates[item.id]?.system) updates[item.id] = { system: {} }; + + updates[item.id].system = { + ...updates[item.id].system, + ...actions.reduce( + (acc, action) => { + acc.actions[action.id] = { 'uses.value': 0 }; + return acc; + }, + { actions: updates[item.id].system.actions ?? {} } + ) + }; + } + } + + for (let key in updates) { + const update = updates[key]; + await actor.items.get(key).update(update); + } + } + } + + return refreshedActors; + } + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + static async #selectRefreshable(_event, button) { + const { type } = button.dataset; + this.refreshSelections[type].selected = !this.refreshSelections[type].selected; + this.render(); + } + + 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(', '); + ui.notifications.info( + game.i18n.format('DAGGERHEART.UI.Notifications.gmMenuRefresh', { + types: `[${types}]` + }) + ); + this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + + const cls = getDocumentClass('ChatMessage'); + const msg = { + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/refreshMessage.hbs', + { + types: types + } + ), + title: game.i18n.localize('DAGGERHEART.UI.Chat.refreshMessage.title'), + speaker: cls.getSpeaker() + }; + + cls.create(msg); + + this.render(); + } +} diff --git a/styles/less/ui/chat/refresh-message.less b/styles/less/ui/chat/refresh-message.less new file mode 100644 index 00000000..2fce189b --- /dev/null +++ b/styles/less/ui/chat/refresh-message.less @@ -0,0 +1,13 @@ +.daggerheart.chat.refresh-message { + header { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + + .subtitle { + font-size: 18; + font-weight: bold; + } + } +} diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 4a93feb6..8fc41734 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -2,6 +2,7 @@ @import './chat/action.less'; @import './chat/chat.less'; @import './chat/downtime.less'; +@import './chat/refresh-message.less'; @import './chat/sheet.less'; @import './combat-sidebar/combat-sidebar.less'; @@ -21,3 +22,5 @@ @import './settings/settings.less'; @import './settings/homebrew-settings/domains.less'; + +@import './sidebar/daggerheartMenu.less'; diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less new file mode 100644 index 00000000..e975954c --- /dev/null +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -0,0 +1,38 @@ +.tab.sidebar-tab.daggerheartMenu-sidebar { + padding: 0 4px; + + .menu-refresh-container { + display: flex; + flex-direction: column; + gap: 8px; + + .menu-refresh-inner-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + + .experience-chip { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 5px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + .label { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 17px; + } + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + } + } + } +} diff --git a/templates/sidebar/daggerheart-menu/main.hbs b/templates/sidebar/daggerheart-menu/main.hbs new file mode 100644 index 00000000..6f31f165 --- /dev/null +++ b/templates/sidebar/daggerheart-menu/main.hbs @@ -0,0 +1,22 @@ +