diff --git a/daggerheart.mjs b/daggerheart.mjs index eff0fcd4..47c53fe4 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -216,46 +216,27 @@ Hooks.on('chatMessage', (_, message) => { }) : game.i18n.localize('DAGGERHEART.General.Duality'); - const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`; - const advantageRoll = `${advantageState === true ? '+d6' : advantageState === false ? '-d6' : ''}`; - const attributeRoll = `${trait?.value ? `${trait.value > 0 ? `+${trait.value}` : `${trait.value}`}` : ''}`; - const roll = await Roll.create(`${hopeAndFearRoll}${advantageRoll}${attributeRoll}`).evaluate(); - - setDiceSoNiceForDualityRoll(roll, advantageState); - - resolve({ - roll, - trait: trait - ? { - value: trait.value, - label: `${game.i18n.localize(abilities[traitValue].label)} ${trait.value >= 0 ? `+` : ``}${trait.value}` - } - : undefined, - title - }); - }).then(async ({ roll, trait, title }) => { - const cls = getDocumentClass('ChatMessage'); - const systemData = new DHDualityRoll({ + const config = { title: title, - origin: target?.id, - roll: roll, - modifiers: trait ? [trait] : [], - hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, - fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, - advantage: advantageState !== null ? { dice: 'd6', value: roll.dice[2].total } : undefined, - advantageState - }); - - const msgData = { - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', - rolls: [roll] + roll: { + trait: traitValue + }, + data: { + traits: { + [traitValue]: trait + } + }, + source: target, + hasSave: false, + dialog: { configure: false }, + evaluate: true, + advantage: rollCommand.advantage == true, + disadvantage: rollCommand.disadvantage == true }; - cls.create(msgData); + await CONFIG.Dice.daggerheart['DualityRoll'].build(config); + + resolve(); }); } diff --git a/lang/en.json b/lang/en.json index d908a091..3cea26a4 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1316,7 +1316,8 @@ } }, "Experiences": "Experiences", - "Level": "Level" + "Level": "Level", + "noPartner": "No Partner selected" }, "Adversary": { "FIELDS": { diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index e16a66fe..a1574a33 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -1,6 +1,6 @@ export { default as DhCharacterSheet } from './sheets/actors/character.mjs'; export { default as DhpAdversarySheet } from './sheets/actors/adversary.mjs'; -export { default as DhCompanionSheet } from './sheets/companion.mjs'; +export { default as DhCompanionSheet } from './sheets/actors/companion.mjs'; export { default as DhpClassSheet } from './sheets/items/class.mjs'; export { default as DhpSubclass } from './sheets/items/subclass.mjs'; export { default as DhpFeatureSheet } from './sheets/items/feature.mjs'; @@ -19,3 +19,4 @@ export { default as DhBeastform } from './sheets/items/beastform.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; export * as api from './sheets/api/_modules.mjs'; +export * as ux from './ux/_module.mjs'; diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 1328de57..ef76d18f 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -1,6 +1,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { async renderHTML() { - if(this.system.messageTemplate) this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, this.system); + if (this.system.messageTemplate) + this.content = await foundry.applications.handlebars.renderTemplate( + this.system.messageTemplate, + this.system + ); /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); @@ -14,7 +18,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { break; case -1: html.classList.add('fear'); - break; + break; default: html.classList.add('critical'); break; diff --git a/module/applications/roll.mjs b/module/applications/roll.mjs index 509402f2..321ead6f 100644 --- a/module/applications/roll.mjs +++ b/module/applications/roll.mjs @@ -1,6 +1,7 @@ import DHDamageRoll from '../data/chat-message/damageRoll.mjs'; import D20RollDialog from '../dialogs/d20RollDialog.mjs'; import DamageDialog from '../dialogs/damageDialog.mjs'; +import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; /* - Damage & other resources roll @@ -96,7 +97,8 @@ export class DHRoll extends Roll { } static applyKeybindings(config) { - config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); + if (config.event) + config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); } formatModifier(modifier) { @@ -176,18 +178,27 @@ export class D20Roll extends DHRoll { } static applyKeybindings(config) { - const keys = { - normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey, - advantage: config.event.altKey, - disadvantage: config.event.ctrlKey + let keys = { + normal: true, + advantage: false, + disadvantage: false }; + if (config.event) { + keys = { + normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey, + advantage: config.event.altKey, + disadvantage: config.event.ctrlKey + }; + } + // Should the roll configuration dialog be displayed? config.dialog.configure ??= !Object.values(keys).some(k => k); // Determine advantage mode - const advantage = config.roll.advantage === this.ADV_MODE.ADVANTAGE || keys.advantage; - const disadvantage = config.roll.advantage === this.ADV_MODE.DISADVANTAGE || keys.disadvantage; + const advantage = config.roll.advantage === this.ADV_MODE.ADVANTAGE || keys.advantage || config.advantage; + const disadvantage = + config.roll.advantage === this.ADV_MODE.DISADVANTAGE || keys.disadvantage || config.disadvantage; if (advantage && !disadvantage) config.roll.advantage = this.ADV_MODE.ADVANTAGE; else if (!advantage && disadvantage) config.roll.advantage = this.ADV_MODE.DISADVANTAGE; else config.roll.advantage = this.ADV_MODE.NORMAL; @@ -254,6 +265,18 @@ export class D20Roll extends DHRoll { }); } + static async buildEvaluate(roll, config = {}, message = {}) { + if (config.evaluate !== false) await roll.evaluate(); + const advantageState = + config.roll.advantage == this.ADV_MODE.ADVANTAGE + ? true + : config.roll.advantage == this.ADV_MODE.DISADVANTAGE + ? false + : null; + setDiceSoNiceForDualityRoll(roll, advantageState); + this.postEvaluate(roll, config); + } + static postEvaluate(roll, config = {}) { super.postEvaluate(roll, config); if (config.targets?.length) { diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index cbe8edb5..ec957651 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -6,6 +6,7 @@ import DaggerheartSheet from '.././daggerheart-sheet.mjs'; import { abilities } from '../../../config/actorConfig.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation.mjs'; +import FilterMenu from '../../ux/filter-menu.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; const { TextEditor } = foundry.applications.ux; @@ -215,6 +216,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { await super._onFirstRender(context, options); this._createContextMenues(); + this._createFilterMenus(); } /** @inheritDoc */ @@ -369,7 +371,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } /* -------------------------------------------- */ - /* Search Filter */ + /* Filter Tracking */ /* -------------------------------------------- */ /** @@ -379,12 +381,33 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { #search = {}; /** - * Track which item IDs are currently displayed due to a search filter. - * @type {{ inventory: Set, loadout: Set }} + * The currently active search filter. + * @type {FilterMenu} + */ + #menu = {}; + + /** + * Tracks which item IDs are currently displayed, organized by filter type and section. + * @type {{ + * inventory: { + * search: Set, + * menu: Set + * }, + * loadout: { + * search: Set, + * menu: Set + * }, + * }} */ #filteredItems = { - inventory: new Set(), - loadout: new Set() + inventory: { + search: new Set(), + menu: new Set() + }, + loadout: { + search: new Set(), + menu: new Set() + } }; /** @@ -432,15 +455,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { * @protected */ _onSearchFilterInventory(event, query, rgx, html) { - this.#filteredItems.inventory.clear(); + this.#filteredItems.inventory.search.clear(); - for (const ul of html.querySelectorAll('.items-list')) { - for (const li of ul.querySelectorAll('.inventory-item')) { - const item = this.document.items.get(li.dataset.itemId); - const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); - if (match) this.#filteredItems.inventory.add(item.id); - li.hidden = !match; - } + for (const li of html.querySelectorAll('.inventory-item')) { + const item = this.document.items.get(li.dataset.itemId); + const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); + const { menu } = this.#filteredItems.inventory; + li.hidden = !(menu.has(item.id) && matchesSearch); } } @@ -453,15 +475,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { * @protected */ _onSearchFilterCard(event, query, rgx, html) { - this.#filteredItems.loadout.clear(); + this.#filteredItems.loadout.search.clear(); - const elements = html.querySelectorAll('.items-list .inventory-item, .card-list .card-item'); - - for (const li of elements) { + for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { const item = this.document.items.get(li.dataset.itemId); - const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); - if (match) this.#filteredItems.loadout.add(item.id); - li.hidden = !match; + const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (matchesSearch) this.#filteredItems.loadout.search.add(item.id); + const { menu } = this.#filteredItems.loadout; + li.hidden = !(menu.has(item.id) && matchesSearch); } } @@ -477,6 +498,102 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.document.diceRoll(config); } + /* -------------------------------------------- */ + /* Filter Menus */ + /* -------------------------------------------- */ + + _createFilterMenus() { + //Menus could be a application option if needed + const menus = [ + { + key: 'inventory', + container: '[data-application-part="inventory"]', + content: '.items-section', + callback: this._onMenuFilterInventory.bind(this), + target: '.filter-button', + filters: FilterMenu.invetoryFilters + }, + { + key: 'loadout', + container: '[data-application-part="loadout"]', + content: '.items-section', + callback: this._onMenuFilterLoadout.bind(this), + target: '.filter-button', + filters: FilterMenu.cardsFilters + } + ]; + + menus.forEach(m => { + const container = this.element.querySelector(m.container); + this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, { + contentSelector: m.content + }); + }); + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters + */ + _onMenuFilterInventory(event, html, filters) { + this.#filteredItems.inventory.menu.clear(); + + for (const li of html.querySelectorAll('.inventory-item')) { + const item = this.document.items.get(li.dataset.itemId); + + const matchesMenu = + filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); + if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id); + + const { search } = this.#filteredItems.inventory; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters + */ + _onMenuFilterLoadout(event, html, filters) { + this.#filteredItems.loadout.menu.clear(); + + for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { + const item = this.document.items.get(li.dataset.itemId); + + const matchesMenu = + filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); + if (matchesMenu) this.#filteredItems.loadout.menu.add(item.id); + + const { search } = this.#filteredItems.loadout; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + /* -------------------------------------------- */ + + async mapFeatureType(data, configType) { + return await Promise.all( + data.map(async x => { + const abilities = x.system.abilities + ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) + : []; + + return { + ...x, + uuid: x.uuid, + system: { + ...x.system, + abilities: abilities, + type: game.i18n.localize(configType[x.system.type ?? x.type].label) + } + }; + }) + ); + } + static async toggleMarks(_, button) { const markValue = Number.parseInt(button.dataset.value); const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs new file mode 100644 index 00000000..89939e9a --- /dev/null +++ b/module/applications/sheets/actors/companion.mjs @@ -0,0 +1,108 @@ +import DaggerheartSheet from '../daggerheart-sheet.mjs'; +import DHCompanionSettings from '../applications/companion-settings.mjs'; + +const { ActorSheetV2 } = foundry.applications.sheets; +export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'companion'], + position: { width: 300 }, + actions: { + viewActor: this.viewActor, + openSettings: this.openSettings, + useItem: this.useItem, + toChat: this.toChat + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + } + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/actors/companion/header.hbs' }, + details: { template: 'systems/daggerheart/templates/sheets/actors/companion/details.hbs' }, + effects: { template: 'systems/daggerheart/templates/sheets/actors/companion/effects.hbs' } + }; + + static TABS = { + details: { + active: true, + cssClass: '', + group: 'primary', + id: 'details', + icon: null, + label: 'DAGGERHEART.General.tabs.details' + }, + effects: { + active: false, + cssClass: '', + group: 'primary', + id: 'effects', + icon: null, + label: 'DAGGERHEART.Sheets.PC.Tabs.effects' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.document; + context.tabs = super._getTabs(this.constructor.TABS); + + return context; + } + + static async updateForm(event, _, formData) { + await this.document.update(formData.object); + this.render(); + } + + static async viewActor(_, button) { + const target = button.closest('[data-item-uuid]'); + const actor = await foundry.utils.fromUuid(target.dataset.itemUuid); + if (!actor) return; + + actor.sheet.render(true); + } + + getAction(element) { + const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, + item = this.document.system.actions.find(x => x.id === itemId); + return item; + } + + static async useItem(event) { + const action = this.getAction(event) ?? this.actor.system.attack; + action.use(event); + } + + static async toChat(event, button) { + if (button?.dataset?.type === 'experience') { + const experience = this.document.system.experiences[button.dataset.uuid]; + const cls = getDocumentClass('ChatMessage'); + const systemData = { + name: game.i18n.localize('DAGGERHEART.General.Experience.Single'), + description: `${experience.name} ${experience.total < 0 ? experience.total : `+${experience.total}`}` + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } else { + const item = this.getAction(event) ?? this.document.system.attack; + item.toChat(this.document.id); + } + } + + static async openSettings() { + await new DHCompanionSettings(this.document).render(true); + } +} diff --git a/module/applications/sheets/applications/companion-settings.mjs b/module/applications/sheets/applications/companion-settings.mjs new file mode 100644 index 00000000..89d20a07 --- /dev/null +++ b/module/applications/sheets/applications/companion-settings.mjs @@ -0,0 +1,160 @@ +import { GMUpdateEvent, socketEvent } from '../../../helpers/socket.mjs'; +import DhCompanionlevelUp from '../../levelup/companionLevelup.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class DHCompanionSettings extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor) { + super({}); + + this.actor = actor; + } + + get title() { + return `${game.i18n.localize('DAGGERHEART.Sheets.TABS.settings')}`; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dh-style', 'dialog', 'companion-settings'], + window: { + icon: 'fa-solid fa-wrench', + resizable: false + }, + position: { width: 455, height: 'auto' }, + actions: { + levelUp: this.levelUp + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + } + }; + + static PARTS = { + header: { + id: 'header', + template: 'systems/daggerheart/templates/sheets/applications/companion-settings/header.hbs' + }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + details: { + id: 'details', + template: 'systems/daggerheart/templates/sheets/applications/companion-settings/details.hbs' + }, + experiences: { + id: 'experiences', + template: 'systems/daggerheart/templates/sheets/applications/companion-settings/experiences.hbs' + }, + attack: { + id: 'attack', + template: 'systems/daggerheart/templates/sheets/applications/companion-settings/attack.hbs' + } + }; + + static TABS = { + details: { + active: true, + cssClass: '', + group: 'primary', + id: 'details', + icon: null, + label: 'DAGGERHEART.General.tabs.details' + }, + experiences: { + active: false, + cssClass: '', + group: 'primary', + id: 'experiences', + icon: null, + label: 'DAGGERHEART.General.tabs.experiences' + }, + attack: { + active: false, + cssClass: '', + group: 'primary', + id: 'attack', + icon: null, + label: 'DAGGERHEART.General.tabs.attack' + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelector('.partner-value')?.addEventListener('change', this.onPartnerChange.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.actor; + context.tabs = this._getTabs(this.constructor.TABS); + context.systemFields = this.actor.system.schema.fields; + context.systemFields.attack.fields = this.actor.system.attack.schema.fields; + context.isNPC = true; + context.playerCharacters = game.actors + .filter( + x => + x.type === 'character' && + (x.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) || + this.document.system.partner?.uuid === x.uuid) + ) + .map(x => ({ key: x.uuid, name: x.name })); + + return context; + } + + _getTabs(tabs) { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + } + + return tabs; + } + + async onPartnerChange(event) { + const partnerDocument = event.target.value + ? await foundry.utils.fromUuid(event.target.value) + : this.actor.system.partner; + const partnerUpdate = { 'system.companion': event.target.value ? this.actor.uuid : null }; + + if (!partnerDocument.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateDocument, + uuid: partnerDocument.uuid, + update: update + } + }); + } else { + await partnerDocument.update(partnerUpdate); + } + + await this.actor.update({ 'system.partner': event.target.value }); + + if (!event.target.value) { + await this.actor.updateLevel(1); + } + + this.render(); + } + + async viewActor(_, button) { + const target = button.closest('[data-item-uuid]'); + const actor = await foundry.utils.fromUuid(target.dataset.itemUuid); + if (!actor) return; + + actor.sheet.render(true); + } + + static async levelUp() { + new DhCompanionlevelUp(this.actor).render(true); + } + + static async updateForm(event, _, formData) { + await this.actor.update(formData.object); + this.render(); + } +} diff --git a/module/applications/sheets/companion.mjs b/module/applications/sheets/companion.mjs deleted file mode 100644 index 46080814..00000000 --- a/module/applications/sheets/companion.mjs +++ /dev/null @@ -1,86 +0,0 @@ -import { GMUpdateEvent, socketEvent } from '../../helpers/socket.mjs'; -import DhCompanionlevelUp from '../levelup/companionLevelup.mjs'; -import DaggerheartSheet from './daggerheart-sheet.mjs'; - -const { ActorSheetV2 } = foundry.applications.sheets; -export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'companion'], - position: { width: 700, height: 1000 }, - actions: { - attackRoll: this.attackRoll, - levelUp: this.levelUp - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } - }; - - static PARTS = { - sidebar: { template: 'systems/daggerheart/templates/sheets/actors/companion/tempMain.hbs' } - }; - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - htmlElement.querySelector('.partner-value')?.addEventListener('change', this.onPartnerChange.bind(this)); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.playerCharacters = game.actors - .filter( - x => - x.type === 'character' && - (x.ownership.default === 3 || - x.ownership[game.user.id] === 3 || - this.document.system.partner?.uuid === x.uuid) - ) - .map(x => ({ key: x.uuid, name: x.name })); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - - async onPartnerChange(event) { - const partnerDocument = event.target.value - ? await foundry.utils.fromUuid(event.target.value) - : this.document.system.partner; - const partnerUpdate = { 'system.companion': event.target.value ? this.document.uuid : null }; - - if (!partnerDocument.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) { - await game.socket.emit(`system.${SYSTEM.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateDocument, - uuid: partnerDocument.uuid, - update: update - } - }); - } else { - await partnerDocument.update(partnerUpdate); - } - - await this.document.update({ 'system.partner': event.target.value }); - - if (!event.target.value) { - await this.document.updateLevel(1); - } - } - - static async attackRoll(event) { - this.actor.system.attack.use(event); - } - - static async levelUp() { - new DhCompanionlevelUp(this.document).render(true); - } -} diff --git a/module/applications/ux/_module.mjs b/module/applications/ux/_module.mjs new file mode 100644 index 00000000..a16a4d6f --- /dev/null +++ b/module/applications/ux/_module.mjs @@ -0,0 +1 @@ +export { default as FilterMenu } from './filter-menu.mjs'; diff --git a/module/applications/ux/filter-menu.mjs b/module/applications/ux/filter-menu.mjs new file mode 100644 index 00000000..0973a358 --- /dev/null +++ b/module/applications/ux/filter-menu.mjs @@ -0,0 +1,237 @@ +/** + * @typedef {Object} FilterItem + * @property {string} group - The group name this filter belongs to (e.g., "Type"). + * @property {string} name - The display name of the filter (e.g., "Weapons"). + * @property {import("@client/applications/ux/search-filter.mjs").FieldFilter} filter - The filter condition. + */ + +export default class FilterMenu extends foundry.applications.ux.ContextMenu { + /** + * Filter Menu + * @param {HTMLElement} container - Container element + * @param {string} selector - CSS selector for menu targets + * @param {Array} menuItems - Array of menu entries + * @param {Function} callback - Callback when filters change + * @param {Object} [options] - Additional options + */ + constructor(container, selector, menuItems, callback, options = {}) { + // Set default options + const mergedOptions = { + eventName: 'click', + fixed: true, + ...options + }; + + super(container, selector, menuItems, mergedOptions); + + // Initialize filter states + this.menuItems = menuItems.map(item => ({ + ...item, + enabled: false + })); + + this.callback = callback; + this.contentElement = container.querySelector(mergedOptions.contentSelector); + + const syntheticEvent = { + type: 'pointerdown', + bubbles: true, + cancelable: true, + pointerType: 'mouse', + isPrimary: true, + button: 0 + }; + + this.callback(syntheticEvent, this.contentElement, this.getActiveFilterData()); + } + + /** @inheritdoc */ + async render(target, options = {}) { + await super.render(target, { ...options, animate: false }); + + // Create menu structure + const menu = document.createElement('menu'); + menu.className = 'filter-menu'; + + // Group items by their group property + const groups = this.#groupItems(this.menuItems); + + // Create sections for each group + for (const [groupName, items] of Object.entries(groups)) { + if (!items.length) continue; + + const section = this.#createSection(groupName, items); + menu.appendChild(section); + } + + // Update menu and set position + this.element.replaceChildren(menu); + + menu.addEventListener('click', this.#handleClick.bind(this)); + + this._setPosition(this.element, target, options); + + if (options.animate !== false) await this._animate(true); + return this._onRender(options); + } + + /** + * Groups an array of items by their `group`. + * @param {Array} items - The array of items to group. Each item is expected to have an optional `group` property. + * @returns {Object>} An object where keys are group names and values are arrays of items belonging to each group. + */ + #groupItems(items) { + return items.reduce((groups, item) => { + const group = item.group ?? '_none'; + groups[group] = groups[group] || []; + groups[group].push(item); + return groups; + }, {}); + } + + /** + * Creates a DOM section element for a group of items with corresponding filter buttons. + * @param {string} groupName - The name of the group, used as the section label. + * @param {Array} items - The items to create buttons for. Each item should have: + * @returns {HTMLDivElement} The section DOM element containing the label and buttons. + */ + #createSection(groupName, items) { + const section = document.createElement('fieldset'); + section.className = 'filter-section'; + + const header = document.createElement('legend'); + header.textContent = groupName; + section.appendChild(header); + + const buttons = document.createElement('div'); + buttons.className = 'filter-buttons'; + + items.forEach(item => { + const button = document.createElement('button'); + button.className = `filter-button ${item.enabled ? 'active' : ''}`; + button.textContent = item.name; + item.element = button; + buttons.appendChild(button); + }); + + section.appendChild(buttons); + return section; + } + + /** + * Get filter data from active filters + * @returns {Array} Array of filter configurations + */ + getActiveFilterData() { + return this.menuItems.filter(item => item.enabled).map(item => item.filter); + } + + /** + * Handles click events on filter buttons. + * Toggles the active state of the clicked button and updates the corresponding item's `enabled` state. + * Then triggers the provided callback with the event, the content element, and the current active filter data. + * @param {PointerEvent} event - The click event triggered by interacting with a filter button. + * @returns {void} + */ + #handleClick(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.target.closest('.filter-button'); + if (!button) return; + + const clickedItem = this.menuItems.find(item => item.element === button); + if (!clickedItem) return; + + const isActive = button.classList.toggle('active'); + clickedItem.enabled = isActive; + + const filters = this.getActiveFilterData(); + + if (filters.length > 0) { + this.target.classList.add('fa-beat', 'active'); + } else { + this.target.classList.remove('fa-beat', 'active'); + } + + this.callback(event, this.contentElement, filters); + } + + /** + * Generate and return a sorted array of inventory filters. + * @returns {Array} An array of filter objects, sorted by name within each group. + */ + static get invetoryFilters() { + const { OPERATORS } = foundry.applications.ux.SearchFilter; + + const typesFilters = Object.entries(CONFIG.Item.dataModels) + .filter(([, { metadata }]) => metadata.isInventoryItem) + .map(([type, { metadata }]) => ({ + group: game.i18n.localize('Type'), + name: game.i18n.localize(metadata.label), + filter: { + field: 'type', + operator: OPERATORS.EQUALS, + value: type + } + })); + + const burdenFilter = Object.values(CONFIG.daggerheart.GENERAL.burden).map(({ value, label }) => ({ + group: game.i18n.localize('DAGGERHEART.Sheets.Weapon.Burden'), + name: game.i18n.localize(label), + filter: { + field: 'system.burden', + operator: OPERATORS.EQUALS, + value: value + } + })); + + const damageTypeFilter = Object.values(CONFIG.daggerheart.GENERAL.damageTypes).map(({ id, abbreviation }) => ({ + group: 'Damage Type', //TODO localize + name: game.i18n.localize(abbreviation), + filter: { + field: 'system.damage.type', + operator: OPERATORS.EQUALS, + value: id + } + })); + + return [ + ...game.i18n.sortObjects(typesFilters, 'name'), + ...game.i18n.sortObjects(burdenFilter, 'name'), + ...game.i18n.sortObjects(damageTypeFilter, 'name') + ]; + } + + /** + * Generate and return a sorted array of inventory filters. + * @returns {Array} An array of filter objects, sorted by name within each group. + */ + static get cardsFilters() { + const { OPERATORS } = foundry.applications.ux.SearchFilter; + + const typesFilters = Object.values(CONFIG.daggerheart.DOMAIN.cardTypes).map(({ id, label }) => ({ + group: game.i18n.localize('Type'), + name: game.i18n.localize(label), + filter: { + field: 'system.type', + operator: OPERATORS.EQUALS, + value: id + } + })); + + const domainFilter = Object.values(CONFIG.daggerheart.DOMAIN.domains).map(({ id, label }) => ({ + group: game.i18n.localize('DAGGERHEART.Sheets.DomainCard.Domain'), + name: game.i18n.localize(label), + filter: { + field: 'system.domain', + operator: OPERATORS.EQUALS, + value: id + } + })); + + const sort = arr => game.i18n.sortObjects(arr, 'name'); + + return [...sort(typesFilters), ...sort(domainFilter)]; + } +} diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index abc38e93..b1254f87 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -47,6 +47,7 @@ export default class DhCompanion extends BaseDataActor { attack: new ActionField({ initial: { name: 'Attack', + img: 'icons/creatures/claws/claw-bear-paw-swipe-brown.webp', _id: foundry.utils.randomID(), systemPath: 'attack', type: 'attack', @@ -57,7 +58,8 @@ export default class DhCompanion extends BaseDataActor { }, roll: { type: 'weapon', - bonus: 0 + bonus: 0, + trait: 'instinct' }, damage: { parts: [ @@ -77,8 +79,10 @@ export default class DhCompanion extends BaseDataActor { }; } - get attackBonus() { - return this.attack.roll.bonus ?? 0; + get traits() { + return { + instinct: { total: this.attack.roll.bonus } + }; } prepareBaseData() { diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 541b76d0..2bb4084f 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -126,6 +126,7 @@ export default class DhpActor extends Actor { } } }); + this.sheet.render(); if (this.system.companion) { this.system.companion.updateLevel(newLevel); @@ -307,6 +308,7 @@ export default class DhpActor extends Actor { } } }); + this.sheet.render(); if (this.system.companion) { this.system.companion.updateLevel(this.system.levelData.level.changed); @@ -338,7 +340,7 @@ export default class DhpActor extends Actor { } get rollClass() { - return CONFIG.Dice.daggerheart[this.type === 'character' ? 'DualityRoll' : 'D20Roll']; + return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll']; } getRollData() { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index af8cfeec..88bb7f3c 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -124,13 +124,13 @@ export const getCommandTarget = () => { export const setDiceSoNiceForDualityRoll = (rollResult, advantageState) => { const diceSoNicePresets = getDiceSoNicePresets(); - rollResult.dice[0].options.appearance = diceSoNicePresets.hope; - rollResult.dice[1].options.appearance = diceSoNicePresets.fear; + rollResult.dice[0].options = { appearance: diceSoNicePresets.hope }; + rollResult.dice[1].options = { appearance: diceSoNicePresets.fear }; //diceSoNicePresets.fear; if (rollResult.dice[2]) { if (advantageState === true) { - rollResult.dice[2].options.appearance = diceSoNicePresets.advantage; + rollResult.dice[2].options = { appearance: diceSoNicePresets.advantage }; } else if (advantageState === false) { - rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage; + rollResult.dice[2].options = { appearance: diceSoNicePresets.disadvantage }; } } }; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index cee88d55..6319f3ea 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -4206,6 +4206,10 @@ div.daggerheart.views.multiclass { .application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar input:placeholder { color: light-dark(#18162e50, #efe6d850); } +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar input::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; +} .application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar .icon { align-content: center; height: 32px; @@ -5032,13 +5036,248 @@ div.daggerheart.views.multiclass { .application.daggerheart.dh-style.views.beastform-selection footer button { flex: 1; } -.application.sheet.daggerheart.actor.dh-style.companion .profile { - height: 80px; - width: 80px; +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; } -.application.sheet.daggerheart.actor.dh-style.companion .temp-container { +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .profile { + height: 235px; + width: 100%; + object-fit: cover; + cursor: pointer; + mask-image: linear-gradient(0deg, transparent 0%, black 10%); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .actor-name { + display: flex; + align-items: center; position: relative; - top: 32px; + top: -30px; + gap: 20px; + padding: 0 20px; + margin-bottom: -30px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .actor-name input[type='text'] { + font-size: 24px; + height: 32px; + text-align: center; + border: 1px solid transparent; + outline: 2px solid transparent; + transition: all 0.3s ease; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .actor-name input[type='text']:hover { + outline: 2px solid light-dark(#222, #f3c267); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section { + display: flex; + gap: 5px; + justify-content: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-number { + justify-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-number .status-value { + position: relative; + display: flex; + width: 50px; + height: 40px; + border: 1px solid light-dark(#18162e, #f3c267); + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 0 6px; + font-size: 1.5rem; + align-items: center; + justify-content: center; + background: light-dark(transparent, #18162e); + z-index: 2; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-number .status-value.armor-slots { + width: 80px; + height: 30px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-number .status-label { + padding: 2px 10px; + width: 100%; + border-radius: 3px; + background: light-dark(#18162e, #f3c267); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-number .status-label h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + font-size: 12px; + color: light-dark(#efe6d8, #18162e); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar { + position: relative; + width: 100px; + height: 40px; + justify-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-label { + position: relative; + top: 40px; + height: 22px; + width: 79px; + clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); + background: light-dark(#18162e, #f3c267); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-label h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + color: light-dark(#efe6d8, #18162e); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value { + position: absolute; + display: flex; + padding: 0 6px; + font-size: 1.5rem; + align-items: center; + width: 100px; + height: 40px; + justify-content: center; + text-align: center; + z-index: 2; + color: #efe6d8; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value input[type='number'] { + background: transparent; + font-size: 1.5rem; + width: 40px; + height: 30px; + text-align: center; + border: none; + outline: 2px solid transparent; + color: #efe6d8; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value input[type='number'].bar-input { + padding: 0; + color: #efe6d8; + backdrop-filter: none; + background: transparent; + transition: all 0.3s ease; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value input[type='number'].bar-input:hover, +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value input[type='number'].bar-input:focus { + background: rgba(24, 22, 46, 0.33); + backdrop-filter: blur(9.5px); +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .status-value .bar-label { + width: 40px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar { + position: absolute; + appearance: none; + width: 100px; + height: 40px; + border: 1px solid light-dark(#18162e, #f3c267); + border-radius: 6px; + z-index: 1; + background: #18162e; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar::-webkit-progress-bar { + border: none; + background: #18162e; + border-radius: 6px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar::-webkit-progress-value { + background: linear-gradient(15deg, #46140a 0%, #be0000 42%, #fcb045 100%); + border-radius: 6px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar.stress-color::-webkit-progress-value { + background: linear-gradient(15deg, #823b01 0%, #fc8e45 65%, #be0000 100%); + border-radius: 6px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar::-moz-progress-bar { + background: linear-gradient(15deg, #46140a 0%, #be0000 42%, #fcb045 100%); + border-radius: 6px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .status-bar .progress-bar.stress-color::-moz-progress-bar { + background: linear-gradient(15deg, #823b01 0%, #fc8e45 65%, #be0000 100%); + border-radius: 6px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .status-section .level-up-label { + font-size: 24px; + padding-top: 8px; +} +.application.sheet.daggerheart.actor.dh-style.companion .companion-header-sheet .companion-navigation { + display: flex; + gap: 8px; + align-items: center; + width: 100%; +} +.application.sheet.daggerheart.actor.dh-style.companion .partner-section, +.application.sheet.daggerheart.actor.dh-style.companion .attack-section { + display: flex; + flex-direction: column; + align-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .partner-section .title, +.application.sheet.daggerheart.actor.dh-style.companion .attack-section .title { + display: flex; + gap: 15px; + align-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .partner-section .title h3, +.application.sheet.daggerheart.actor.dh-style.companion .attack-section .title h3 { + font-size: 20px; +} +.application.sheet.daggerheart.actor.dh-style.companion .partner-section .items-list, +.application.sheet.daggerheart.actor.dh-style.companion .attack-section .items-list { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .partner-placeholder { + display: flex; + opacity: 0.6; + text-align: center; + font-style: italic; + justify-content: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .experience-list { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + margin-top: 10px; + align-items: center; +} +.application.sheet.daggerheart.actor.dh-style.companion .experience-list .experience-row { + display: flex; + gap: 5px; + width: 250px; + align-items: center; + justify-content: space-between; +} +.application.sheet.daggerheart.actor.dh-style.companion .experience-list .experience-row .experience-name { + width: 180px; + text-align: start; + font-size: 14px; + font-family: 'Montserrat', sans-serif; + color: light-dark(#222, #efe6d8); +} +.application.sheet.daggerheart.actor.dh-style.companion .experience-list .experience-value { + height: 25px; + width: 35px; + font-size: 14px; + font-family: 'Montserrat', sans-serif; + color: light-dark(#222, #efe6d8); + align-content: center; + text-align: center; + background: url(../assets/svg/experience-shield.svg) no-repeat; +} +.theme-light .application.sheet.daggerheart.actor.dh-style.companion .experience-list .experience-value { + background: url('../assets/svg/experience-shield-light.svg') no-repeat; +} +.theme-light .application.sheet.daggerheart.actor.dh-style.companion { + background: url('../assets/parchments/dh-parchment-light.png'); +} +.theme-dark .application.sheet.daggerheart.actor.dh-style.companion { + background-image: url('../assets/parchments/dh-parchment-dark.png'); } .application.sheet.daggerheart.actor.dh-style.adversary .window-content { overflow: auto; @@ -5333,6 +5572,19 @@ div.daggerheart.views.multiclass { box-shadow: none; outline: 2px solid light-dark(#222, #efe6d8); } +.application.dh-style input[type='text']:disabled[type='text'], +.application.dh-style input[type='number']:disabled[type='text'], +.application.dh-style input[type='text']:disabled[type='number'], +.application.dh-style input[type='number']:disabled[type='number'] { + outline: 2px solid transparent; + cursor: not-allowed; +} +.application.dh-style input[type='text']:disabled[type='text']:hover, +.application.dh-style input[type='number']:disabled[type='text']:hover, +.application.dh-style input[type='text']:disabled[type='number']:hover, +.application.dh-style input[type='number']:disabled[type='number']:hover { + background: transparent; +} .application.dh-style input[type='checkbox']:checked::after { color: light-dark(#222, #f3c267); } @@ -5356,6 +5608,16 @@ div.daggerheart.views.multiclass { .application.dh-style button.glow { animation: glow 0.75s infinite alternate; } +.application.dh-style button:disabled { + background: light-dark(transparent, #f3c267); + color: light-dark(#18162e, #18162e); + opacity: 0.6; + cursor: not-allowed; +} +.application.dh-style button:disabled:hover { + background: light-dark(transparent, #f3c267); + color: light-dark(#18162e, #18162e); +} .application.dh-style select { background: light-dark(transparent, transparent); color: light-dark(#222, #efe6d8); @@ -6139,6 +6401,44 @@ div.daggerheart.views.multiclass { .application prose-mirror .editor-content h3 { font-size: 24px; } +.filter-menu { + width: auto; +} +.filter-menu fieldset.filter-section { + align-items: center; + margin: 5px; + border-radius: 6px; + border-color: light-dark(#18162e, #f3c267); + padding: 5px; +} +.filter-menu fieldset.filter-section legend { + font-family: 'Montserrat', sans-serif; + font-weight: bold; + color: light-dark(#18162e, #f3c267); + font-size: var(--font-size-12); +} +.filter-menu fieldset.filter-section .filter-buttons { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + gap: 5px; +} +.filter-menu fieldset.filter-section .filter-buttons button { + background: light-dark(rgba(0, 0, 0, 0.3), #18162e); + color: light-dark(#18162e, #f3c267); + outline: none; + box-shadow: none; + border: 1px solid light-dark(#18162e, #18162e); + padding: 0 0.2rem; + font-size: var(--font-size-12); +} +.filter-menu fieldset.filter-section .filter-buttons button:hover { + background: light-dark(transparent, #f3c267); + color: light-dark(#18162e, #18162e); +} +.filter-menu fieldset.filter-section .filter-buttons button.active { + animation: glow 0.75s infinite alternate; +} .daggerheart { /* Flex */ /****/ diff --git a/styles/daggerheart.less b/styles/daggerheart.less index b915d44a..38654759 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -42,6 +42,8 @@ @import './less/applications//beastform.less'; +@import './less/actors/companion/header.less'; +@import './less/actors/companion/details.less'; @import './less/actors/companion/sheet.less'; @import './less/actors/adversary.less'; @@ -67,6 +69,8 @@ @import './less/global/inventory-item.less'; @import './less/global/inventory-fieldset-items.less'; @import './less/global/prose-mirror.less'; +@import './less/global/filter-menu.less'; + @import '../node_modules/@yaireo/tagify/dist/tagify.css'; .daggerheart { diff --git a/styles/less/actors/character/loadout.less b/styles/less/actors/character/loadout.less index bf1474a2..72393597 100644 --- a/styles/less/actors/character/loadout.less +++ b/styles/less/actors/character/loadout.less @@ -30,6 +30,11 @@ &:placeholder { color: light-dark(@dark-blue-50, @beige-50); } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } } .icon { diff --git a/styles/less/actors/companion/details.less b/styles/less/actors/companion/details.less new file mode 100644 index 00000000..4da8d126 --- /dev/null +++ b/styles/less/actors/companion/details.less @@ -0,0 +1,75 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.companion { + .partner-section, + .attack-section { + display: flex; + flex-direction: column; + align-items: center; + + .title { + display: flex; + gap: 15px; + align-items: center; + + h3 { + font-size: 20px; + } + } + .items-list { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + } + + .partner-placeholder { + display: flex; + opacity: 0.6; + text-align: center; + font-style: italic; + justify-content: center; + } + + .experience-list { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + margin-top: 10px; + align-items: center; + + .experience-row { + display: flex; + gap: 5px; + width: 250px; + align-items: center; + justify-content: space-between; + + .experience-name { + width: 180px; + text-align: start; + font-size: 14px; + font-family: @font-body; + color: light-dark(@dark, @beige); + } + } + + .experience-value { + height: 25px; + width: 35px; + font-size: 14px; + font-family: @font-body; + color: light-dark(@dark, @beige); + align-content: center; + text-align: center; + background: url(../assets/svg/experience-shield.svg) no-repeat; + + .theme-light & { + background: url('../assets/svg/experience-shield-light.svg') no-repeat; + } + } + } +} diff --git a/styles/less/actors/companion/header.less b/styles/less/actors/companion/header.less new file mode 100644 index 00000000..daa20e93 --- /dev/null +++ b/styles/less/actors/companion/header.less @@ -0,0 +1,197 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.companion { + .companion-header-sheet { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + + .profile { + height: 235px; + width: 100%; + object-fit: cover; + cursor: pointer; + mask-image: linear-gradient(0deg, transparent 0%, black 10%); + } + + .actor-name { + display: flex; + align-items: center; + position: relative; + top: -30px; + gap: 20px; + padding: 0 20px; + margin-bottom: -30px; + + input[type='text'] { + font-size: 24px; + height: 32px; + text-align: center; + border: 1px solid transparent; + outline: 2px solid transparent; + transition: all 0.3s ease; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + } + } + + .status-section { + display: flex; + gap: 5px; + justify-content: center; + + .status-number { + justify-items: center; + + .status-value { + position: relative; + display: flex; + width: 50px; + height: 40px; + border: 1px solid light-dark(@dark-blue, @golden); + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 0 6px; + font-size: 1.5rem; + align-items: center; + justify-content: center; + background: light-dark(transparent, @dark-blue); + z-index: 2; + + &.armor-slots { + width: 80px; + height: 30px; + } + } + + .status-label { + padding: 2px 10px; + width: 100%; + border-radius: 3px; + background: light-dark(@dark-blue, @golden); + + h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + font-size: 12px; + color: light-dark(@beige, @dark-blue); + } + } + } + + .status-bar { + position: relative; + width: 100px; + height: 40px; + justify-items: center; + + .status-label { + position: relative; + top: 40px; + height: 22px; + width: 79px; + clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); + background: light-dark(@dark-blue, @golden); + + h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + color: light-dark(@beige, @dark-blue); + } + } + .status-value { + position: absolute; + display: flex; + padding: 0 6px; + font-size: 1.5rem; + align-items: center; + width: 100px; + height: 40px; + justify-content: center; + text-align: center; + z-index: 2; + color: @beige; + + input[type='number'] { + background: transparent; + font-size: 1.5rem; + width: 40px; + height: 30px; + text-align: center; + border: none; + outline: 2px solid transparent; + color: @beige; + + &.bar-input { + padding: 0; + color: @beige; + backdrop-filter: none; + background: transparent; + transition: all 0.3s ease; + + &:hover, + &:focus { + background: @semi-transparent-dark-blue; + backdrop-filter: blur(9.5px); + } + } + } + + .bar-label { + width: 40px; + } + } + .progress-bar { + position: absolute; + appearance: none; + width: 100px; + height: 40px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + background: @dark-blue; + + &::-webkit-progress-bar { + border: none; + background: @dark-blue; + border-radius: 6px; + } + &::-webkit-progress-value { + background: @gradient-hp; + border-radius: 6px; + } + &.stress-color::-webkit-progress-value { + background: @gradient-stress; + border-radius: 6px; + } + &::-moz-progress-bar { + background: @gradient-hp; + border-radius: 6px; + } + &.stress-color::-moz-progress-bar { + background: @gradient-stress; + border-radius: 6px; + } + } + } + + .level-up-label { + font-size: 24px; + padding-top: 8px; + } + } + + .companion-navigation { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + } + } +} diff --git a/styles/less/actors/companion/sheet.less b/styles/less/actors/companion/sheet.less index 1beb28a7..db221597 100644 --- a/styles/less/actors/companion/sheet.less +++ b/styles/less/actors/companion/sheet.less @@ -1,11 +1,18 @@ .application.sheet.daggerheart.actor.dh-style.companion { - .profile { - height: 80px; - width: 80px; + .theme-light & { + background: url('../assets/parchments/dh-parchment-light.png'); + } + .theme-dark & { + background-image: url('../assets/parchments/dh-parchment-dark.png'); } - .temp-container { - position: relative; - top: 32px; - } + // .profile { + // height: 80px; + // width: 80px; + // } + + // .temp-container { + // position: relative; + // top: 32px; + // } } diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 1f6e5988..fcf9d353 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -23,6 +23,16 @@ box-shadow: none; outline: 2px solid light-dark(@dark, @beige); } + + &:disabled[type='text'], + &:disabled[type='number'] { + outline: 2px solid transparent; + cursor: not-allowed; + + &:hover { + background: transparent; + } + } } input[type='checkbox'] { @@ -52,6 +62,18 @@ &.glow { animation: glow 0.75s infinite alternate; } + + &:disabled { + background: light-dark(transparent, @golden); + color: light-dark(@dark-blue, @dark-blue); + opacity: 0.6; + cursor: not-allowed; + + &:hover { + background: light-dark(transparent, @golden); + color: light-dark(@dark-blue, @dark-blue); + } + } } select { diff --git a/styles/less/global/filter-menu.less b/styles/less/global/filter-menu.less new file mode 100644 index 00000000..09962afb --- /dev/null +++ b/styles/less/global/filter-menu.less @@ -0,0 +1,47 @@ +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.filter-menu { + width: auto; + + fieldset.filter-section { + align-items: center; + margin: 5px; + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + padding: 5px; + + legend { + font-family: @font-body; + font-weight: bold; + color: light-dark(@dark-blue, @golden); + font-size: var(--font-size-12); + } + + .filter-buttons { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + gap: 5px; + + button { + background: light-dark(@light-black, @dark-blue); + color: light-dark(@dark-blue, @golden); + outline: none; + box-shadow: none; + border: 1px solid light-dark(@dark-blue, @dark-blue); + padding: 0 0.2rem; + font-size: var(--font-size-12); + + &:hover { + background: light-dark(transparent, @golden); + color: light-dark(@dark-blue, @dark-blue); + } + + &.active { + animation: glow 0.75s infinite alternate; + } + } + } + } +} diff --git a/templates/chat/duality-roll.hbs b/templates/chat/duality-roll.hbs index 9a530649..ff1e9894 100644 --- a/templates/chat/duality-roll.hbs +++ b/templates/chat/duality-roll.hbs @@ -123,7 +123,7 @@
- {{> 'systems/daggerheart/templates/chat/parts/damage-chat.hbs' damage=damage noTitle=true}} + {{!-- {{> 'systems/daggerheart/templates/chat/parts/damage-chat.hbs' damage=damage noTitle=true}} --}}
diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index 3f4b98be..be8bb251 100644 --- a/templates/sheets/actors/character/inventory.hbs +++ b/templates/sheets/actors/character/inventory.hbs @@ -10,7 +10,9 @@ - + + +
diff --git a/templates/sheets/actors/character/loadout.hbs b/templates/sheets/actors/character/loadout.hbs index de63323c..c9c9d4b2 100644 --- a/templates/sheets/actors/character/loadout.hbs +++ b/templates/sheets/actors/character/loadout.hbs @@ -10,7 +10,9 @@
- + + + + + \ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/attack.hbs b/templates/sheets/applications/companion-settings/attack.hbs new file mode 100644 index 00000000..f0a26769 --- /dev/null +++ b/templates/sheets/applications/companion-settings/attack.hbs @@ -0,0 +1,21 @@ +
+
+ {{localize "DAGGERHEART.General.basics"}} + {{formGroup systemFields.attack.fields.img value=document.system.attack.img label="Image Path" name="system.attack.img"}} + {{formGroup systemFields.attack.fields.name value=document.system.attack.name label="Attack Name" name="system.attack.name"}} +
+
+ {{localize "DAGGERHEART.Sheets.Adversary.Attack"}} + {{formField systemFields.attack.fields.range value=document.system.attack.range label="Range" name="system.attack.range" localize=true}} + {{#if systemFields.attack.fields.target.fields}} + {{ formField systemFields.attack.fields.target.fields.type value=document.system.attack.target.type label="Target" name="system.attack.target.type" localize=true }} + {{#if (and document.system.attack.target.type (not (eq document.system.attack.target.type 'self')))}} + {{ formField systemFields.attack.fields.target.fields.amount value=document.system.attack.target.amount label="Amount" name="system.attack.target.amount" }} + {{/if}} + {{/if}} +
+
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/details.hbs b/templates/sheets/applications/companion-settings/details.hbs new file mode 100644 index 00000000..d047752c --- /dev/null +++ b/templates/sheets/applications/companion-settings/details.hbs @@ -0,0 +1,23 @@ +
+
+ {{localize 'DAGGERHEART.General.basics'}} +
+ {{formGroup systemFields.evasion.fields.value value=document.system.evasion.value localize=true}} + {{formGroup systemFields.resources.fields.stress.fields.value value=document.system.resources.stress.value label='Current Stress'}} + {{formGroup systemFields.resources.fields.stress.fields.max value=document.system.resources.stress.max label='Max Stress'}} +
+
+
+ + +
+
+
+ +
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/experiences.hbs b/templates/sheets/applications/companion-settings/experiences.hbs new file mode 100644 index 00000000..124a1a86 --- /dev/null +++ b/templates/sheets/applications/companion-settings/experiences.hbs @@ -0,0 +1,18 @@ +
+
+ {{localize tabs.experiences.label}} +
    + {{#each document.system.experiences as |experience key|}} +
  • + + +
  • + {{/each}} +
+
+ +
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/header.hbs b/templates/sheets/applications/companion-settings/header.hbs new file mode 100644 index 00000000..0978f2c3 --- /dev/null +++ b/templates/sheets/applications/companion-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{document.name}}

+
\ No newline at end of file diff --git a/templates/sheets/global/partials/inventory-item.hbs b/templates/sheets/global/partials/inventory-item.hbs index 061cb125..9b1c392b 100644 --- a/templates/sheets/global/partials/inventory-item.hbs +++ b/templates/sheets/global/partials/inventory-item.hbs @@ -1,7 +1,11 @@ -
  • +
  • -
    {{item.name}}
    + {{#if isCompanion}} + {{item.name}} + {{else}} +
    {{item.name}}
    + {{/if}} {{#if (eq type 'weapon')}}
    {{#if isSidebar}} @@ -114,6 +118,11 @@ {{#unless hideControls}} {{#if isActor}}
    + {{#if (eq type 'actor')}} + + + + {{/if}} {{#if (eq type 'adversary')}}