[PR] [Feature] 590 - Daggerheart Menu (#1007)

* Added menu with refresh tools

* Replaced menu icon
This commit is contained in:
WBHarry 2025-09-07 00:30:29 +02:00 committed by GitHub
parent eefb28c312
commit f1b6d3851d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 730 additions and 350 deletions

View file

@ -6,5 +6,6 @@ export * as scene from './scene/_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';

View file

@ -536,7 +536,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
static async viewCompendium(event, target) {
const type = target.dataset.compendium ?? target.dataset.type;
const presets = {
const presets = {
folder: type,
render: {
noFolder: true

View file

@ -1 +1 @@
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';
export { default as DhSceneConfigSettings } from './sceneConfigSettings.mjs';

View file

@ -1,25 +1,24 @@
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
constructor(options, ...args) {
super(options, ...args);
}
static buildParts() {
const { footer, ...parts } = super.PARTS;
const tmpParts = {
...parts,
dh: { template: "systems/daggerheart/templates/scene/dh-config.hbs" },
footer
constructor(options, ...args) {
super(options, ...args);
}
return tmpParts;
}
static PARTS = DhSceneConfigSettings.buildParts();
static buildParts() {
const { footer, ...parts } = super.PARTS;
const tmpParts = {
...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
footer
};
return tmpParts;
}
static buildTabs() {
super.TABS.sheet.tabs.push({ id: "dh", icon: "fa-solid" });
return super.TABS;
}
static PARTS = DhSceneConfigSettings.buildParts();
static TABS = DhSceneConfigSettings.buildTabs();
static buildTabs() {
super.TABS.sheet.tabs.push({ id: 'dh', icon: 'fa-solid' });
return super.TABS;
}
}
static TABS = DhSceneConfigSettings.buildTabs();
}

View file

@ -33,7 +33,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/settings/appearance-settings/main.hbs' },
diceSoNice: { template: 'systems/daggerheart/templates/settings/appearance-settings/diceSoNice.hbs' },
footer: { template: "templates/generic/form-footer.hbs" }
footer: { template: 'templates/generic/form-footer.hbs' }
};
/** @inheritdoc */
@ -41,7 +41,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
general: {
tabs: [
{ id: 'main', label: 'DAGGERHEART.GENERAL.Tabs.general' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' },
{ id: 'diceSoNice', label: 'DAGGERHEART.SETTINGS.Menu.appearance.diceSoNice.title' }
],
initial: 'main'
},
@ -73,7 +73,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
/** @inheritdoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
if (!game.modules.get('dice-so-nice')?.active){
if (!game.modules.get('dice-so-nice')?.active) {
delete parts.diceSoNice;
delete parts.tabs;
}
@ -83,7 +83,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
/**@inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (options.isFirstRender) this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
if (options.isFirstRender)
this.setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance);
context.setting = this.setting;
context.fields = this.setting.schema.fields;
@ -99,18 +100,17 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
const partContext = await super._preparePartContext(partId, context, options);
if (partId in context.tabs) partContext.tab = partContext.tabs[partId];
switch (partId) {
case "diceSoNice":
case 'diceSoNice':
await this.prepareDiceSoNiceContext(partContext);
break;
case "footer":
case 'footer':
partContext.buttons = [
{ type: "button", action: "reset", icon: "fa-solid fa-arrow-rotate-left", label: "Reset" },
{ type: "submit", icon: "fa-solid fa-floppy-disk", label: "Save Changes" }
{ type: 'button', action: 'reset', icon: 'fa-solid fa-arrow-rotate-left', label: 'Reset' },
{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: 'Save Changes' }
];
break;
}
return partContext;
}
/**
@ -120,32 +120,44 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @protected
*/
async prepareDiceSoNiceContext(context) {
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce((acc, [k, v]) => ({
...acc,
[k]: v.name
}), {});
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce((acc, v) => ({
...acc,
[v.id]: v.description
}), {});
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce((acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}), {});
context.diceSoNiceSystems = Object.fromEntries([...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name]));
context.diceSoNiceTextures = Object.entries(game.dice3d.exports.TEXTURELIST).reduce(
(acc, [k, v]) => ({
...acc,
[k]: v.name
}),
{}
);
context.diceSoNiceColorsets = Object.values(game.dice3d.exports.COLORSETS).reduce(
(acc, v) => ({
...acc,
[v.id]: v.description
}),
{}
);
context.diceSoNiceMaterials = Object.keys(game.dice3d.DiceFactory.material_options).reduce(
(acc, key) => ({
...acc,
[key]: `DICESONICE.Material${key.capitalize()}`
}),
{}
);
context.diceSoNiceSystems = Object.fromEntries(
[...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name])
);
foundry.utils.mergeObject(context.dsnTabs, [
"hope",
"fear",
"advantage",
"disadvantage",
].reduce((acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields,
}
}), {}));
foundry.utils.mergeObject(
context.dsnTabs,
['hope', 'fear', 'advantage', 'disadvantage'].reduce(
(acc, key) => ({
...acc,
[key]: {
values: this.setting.diceSoNice[key],
fields: this.setting.schema.getField(`diceSoNice.${key}`).fields
}
}),
{}
)
);
}
/**
@ -169,7 +181,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
* @type {ApplicationClickAction}
*/
static async #onPreview(_, target) {
const formData = new foundry.applications.ux.FormDataExtended(target.closest("form"));
const formData = new foundry.applications.ux.FormDataExtended(target.closest('form'));
const { diceSoNice } = foundry.utils.expandObject(formData.object);
const { key } = target.dataset;
const faces = ['advantage', 'disadvantage'].includes(key) ? 'd6' : 'd12';
@ -181,10 +193,10 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App
}
/**
* Reset the form back to default values.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
* Reset the form back to default values.
* @this {DHAppearanceSettings}
* @type {ApplicationClickAction}
*/
static async #onReset() {
this.setting = new this.setting.constructor();
this.render({ force: false });

View file

@ -416,11 +416,14 @@ export default function DHApplicationMixin(Base) {
icon: 'fa-solid fa-ban',
condition: target => {
const doc = getDocFromElementSync(target);
return doc && doc.system?.actions?.some(a => a.type === "beastform");
return doc && doc.system?.actions?.some(a => a.type === 'beastform');
},
callback: async target => game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(await getDocFromElement(target))
callback: async target =>
game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(
await getDocFromElement(target)
)
});
options.unshift({
name: 'DAGGERHEART.GENERAL.damage',
icon: 'fa-solid fa-explosion',
@ -433,7 +436,7 @@ export default function DHApplicationMixin(Base) {
action = doc?.system?.attack ?? doc;
const config = action.prepareConfig(event);
config.hasRoll = false;
return action && action.workflow.get("damage").execute(config, null, true);
return action && action.workflow.get('damage').execute(config, null, true);
}
});

View file

@ -0,0 +1,2 @@
export { default as DaggerheartMenu } from './tabs/daggerheartMenu.mjs';
export { default as DhSidebar } from './sidebar.mjs';

View file

@ -0,0 +1,33 @@
export default class DhSidebar extends Sidebar {
/** @override */
static TABS = {
...super.TABS,
daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg'
}
};
/** @override */
static PARTS = {
tabs: {
id: 'tabs',
template: 'systems/daggerheart/templates/sidebar/tabs.hbs'
}
};
/** @override */
async _prepareTabContext(context, options) {
context.tabs = Object.entries(this.constructor.TABS).reduce((obj, [k, v]) => {
let { documentName, gmOnly, tooltip, icon, img } = v;
if (gmOnly && !game.user.isGM) return obj;
if (documentName) {
tooltip ??= getDocumentClass(documentName).metadata.labelPlural;
icon ??= CONFIG[documentName]?.sidebarIcon;
}
obj[k] = { tooltip, icon, img };
obj[k].active = this.tabGroups.primary === k;
return obj;
}, {});
}
}

View file

@ -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();
}
}

View file

@ -112,7 +112,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (event.currentTarget.dataset.directDamage) {
const config = action.prepareConfig(event);
config.hasRoll = false;
action.workflow.get("damage").execute(config, null, true);
action.workflow.get('damage').execute(config, null, true);
} else action.use(event);
}

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent, socketEvent } from "../../systemRegistration/socket.mjs";
import { emitAsGM, GMUpdateEvent, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -106,6 +106,10 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
}
async updateFear(value) {
return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value);
return emitAsGM(
GMUpdateEvent.UpdateFear,
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
value
);
}
}

View file

@ -84,12 +84,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/** @inheritDoc */
async _preRender(context, options) {
this.presets = options.presets ?? {};
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;
if (this.rendered) this.setPosition({ width });
else options.position.width = width;
await super._preRender(context, options);
}
@ -100,15 +98,18 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.element
.querySelectorAll('[data-action="selectFolder"]')
.forEach(element => element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.')));
.forEach(element =>
element.classList.toggle('is-selected', element.dataset.folderId === this.selectedMenu.path.join('.'))
);
this._createSearchFilter();
this.element.classList.toggle('lite', this.presets?.render?.lite === true);
this.element.classList.toggle('no-folder', this.presets?.render?.noFolder === true);
this.element.classList.toggle('no-filter', this.presets?.render?.noFilter === true);
this.element.querySelectorAll('.folder-list > [data-action="selectFolder"]').forEach(element => {
element.hidden = this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
element.hidden =
this.presets.render?.folders?.length && !this.presets.render.folders.includes(element.dataset.folderId);
});
}
@ -157,7 +158,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
: [];
folders.push(folder);
});
folders.sort((a, b) => a.label.localeCompare(b.label))
folders.sort((a, b) => a.label.localeCompare(b.label));
return folders;
}
@ -181,8 +182,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
await this.render({ force: true, presets: this.presets });
if(this.selectedMenu?.data?.type?.length)
this.loadItems();
if (this.selectedMenu?.data?.type?.length) this.loadItems();
}
_replaceHTML(result, content, options) {
@ -194,53 +194,54 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
let loadTimeout = this.toggleLoader(true);
const promises = [];
game.packs.forEach(pack => {
promises.push(
new Promise(async resolve => {
const items = await pack.getDocuments({ type__in: this.selectedMenu?.data?.type });
resolve(items);
})
)
);
});
Promise.all(promises).then(async result => {
this.items = ItemBrowser.sortBy(result.flatMap(r => r), 'name');
this.items = ItemBrowser.sortBy(
result.flatMap(r => r),
'name'
);
this.fieldFilter = this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(
([k, v]) => {
const filter = this.fieldFilter.find(c => c.name === k)
if(filter) filter.value = v.value;
}
);
Object.entries(this.presets.filter).forEach(([k, v]) => {
const filter = this.fieldFilter.find(c => c.name === k);
if (filter) filter.value = v.value;
});
// await this._onInputFilterBrowser();
}
const filterList = await foundry.applications.handlebars.renderTemplate('systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
const filterList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/filterContainer.hbs',
{
fieldFilter: this.fieldFilter,
presets: this.presets,
formatChoices: this.formatChoices
}
);
this.element.querySelector('.filter-content .wrapper').innerHTML = filterList;
const filterContainer = this.element.querySelector('.filter-header > [data-action="expandContent"]');
if(this.fieldFilter.length === 0)
filterContainer.setAttribute('disabled', '');
else
filterContainer.removeAttribute('disabled');
const itemList = await foundry.applications.handlebars.renderTemplate('systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
if (this.fieldFilter.length === 0) filterContainer.setAttribute('disabled', '');
else filterContainer.removeAttribute('disabled');
const itemList = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs',
{
items: this.items,
menu: this.selectedMenu,
formatLabel: this.formatLabel
}
);
this.element.querySelector('.item-list').innerHTML = itemList;
this._createFilterInputs();
@ -255,7 +256,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
toggleLoader(state) {
const container = this.element.querySelector('.item-list');
return setTimeout(() => {
container.classList.toggle("loader", state);
container.classList.toggle('loader', state);
}, 100);
}
@ -376,7 +377,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
for (const li of html.querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID);
if(!item) continue;
if (!item) continue;
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.browser.search.add(item.id);
const { input } = this.#filteredItems.browser;
@ -399,7 +400,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
item = this.items.find(i => i.uuid === itemUUID);
if (!item) continue;
const matchesMenu =
this.fieldFilter.length === 0 ||
this.fieldFilter.every(
@ -503,19 +504,19 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
_canDragStart() {
return true;
}
static injectSidebarButton(html) {
if(!game.user.isGM) return;
if (!game.user.isGM) return;
const sectionId = html.dataset.tab,
menus = {
menus = {
actors: {
folder: "adversaries",
folder: 'adversaries',
render: {
folders: ["adversaries", "characters", "environments"]
folders: ['adversaries', 'characters', 'environments']
}
},
items: {
folder: "equipments",
folder: 'equipments',
render: {
noFolder: true
}
@ -523,20 +524,20 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
compendium: {}
};
if(Object.keys(menus).includes(sectionId)) {
const headerActions = html.querySelector(".header-actions");
if (Object.keys(menus).includes(sectionId)) {
const headerActions = html.querySelector('.header-actions');
const button = document.createElement("button");
button.type = "button";
button.classList.add("open-compendium-browser");
const button = document.createElement('button');
button.type = 'button';
button.classList.add('open-compendium-browser');
button.innerHTML = `
<i class="fa-solid fa-book-atlas"></i>
${game.i18n.localize("DAGGERHEART.UI.Tooltip.compendiumBrowser")}
${game.i18n.localize('DAGGERHEART.UI.Tooltip.compendiumBrowser')}
`;
button.addEventListener("click", event => {
button.addEventListener('click', event => {
ui.compendiumBrowser.open(menus[sectionId]);
});
headerActions.append(button);
}
}