diff --git a/README.md b/README.md index d08af2b9..9c58fece 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,8 @@ # Daggerheart -#### For Foundry VTT +This is a repo for a Foundry VTT implementation of daggerheart. It is not associated with Critical Role or Darrington Press. -This is a repo for a Foundry VTT implementation of daggerheart. It is not associated with critical role -or darrington press. - -# Table Of Contents - -- [Overview](#overview) -- [Developer Guide](#developer-guide) - -# Overview - -# Developer Guide - -#### Coding Practises -##### Style Code -The project is set up for Prettify. Make sure you've run `npm install` since it was added. -There is a pre-commit hook that will automatically run prettify on the files you've changed whenever you do a commit to maintain the formating. - -##### Branches And Pull Requests - -During pre-release development, we are making use of `main` as the development branch. Once release is getting closer we will instead be making a `dev` branch to base development from to make `main` more stable. - -When you work on an issue or feature, start from `main` and make a new branch. Branches should be topically named and with the associated Issue number if it relates to an Issue. EX: `#6/Level-Up-Bugginess`. - ---- - -Once you're finished with an issue or feature, open a Pull Request on Github for that branch. - -The Reviewers Team will be approving submissions. This is mainly since we have a wide spread of experience with system building and the system itself, and we do want the system to become something great. As time goes on, more collaborators are likely to be added as reviewers. - -#### Setup +## Setup - Open a terminal in the directory with the repo `cd //` - NOTE: The repo should be placed in the system files are or somewhere else and a link (if on linux) is placed in the system directory @@ -52,4 +23,8 @@ The Reviewers Team will be approving submissions. This is mainly since we have a Now you should be able to build the app using `npm start` [Foundry VTT Website][1] -[1]: https://foundryvtt.com/ +[1]: https://foundryvtt.com/ + +## Contributing + +Looking to contribute to the project? Look no further, check out our [contributing guide](contributing.md), and keep the [Code of Conduct](coc.md) in mind when working on things. \ No newline at end of file diff --git a/assets/icons/domains/arcana.svg b/assets/icons/domains/arcana.svg new file mode 100644 index 00000000..f9e31ea9 --- /dev/null +++ b/assets/icons/domains/arcana.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/blade.svg b/assets/icons/domains/blade.svg new file mode 100644 index 00000000..8bde2bfb --- /dev/null +++ b/assets/icons/domains/blade.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/bone.svg b/assets/icons/domains/bone.svg new file mode 100644 index 00000000..0daedd76 --- /dev/null +++ b/assets/icons/domains/bone.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/codex.svg b/assets/icons/domains/codex.svg new file mode 100644 index 00000000..5df38f71 --- /dev/null +++ b/assets/icons/domains/codex.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/grace.svg b/assets/icons/domains/grace.svg new file mode 100644 index 00000000..2fd82418 --- /dev/null +++ b/assets/icons/domains/grace.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/midnight.svg b/assets/icons/domains/midnight.svg new file mode 100644 index 00000000..56439c07 --- /dev/null +++ b/assets/icons/domains/midnight.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/sage.svg b/assets/icons/domains/sage.svg new file mode 100644 index 00000000..6d1659eb --- /dev/null +++ b/assets/icons/domains/sage.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/splendor.svg b/assets/icons/domains/splendor.svg new file mode 100644 index 00000000..106e60ab --- /dev/null +++ b/assets/icons/domains/splendor.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/domains/valor.svg b/assets/icons/domains/valor.svg new file mode 100644 index 00000000..4d650974 --- /dev/null +++ b/assets/icons/domains/valor.svg @@ -0,0 +1 @@ + diff --git a/coc.md b/coc.md new file mode 100644 index 00000000..a0b32d87 --- /dev/null +++ b/coc.md @@ -0,0 +1,66 @@ +# Code of Conduct + +## 💚 Our Commitment + +As contributors and maintainers of **Foundryborne**, we are dedicated to fostering a safe, inclusive, and welcoming environment for everyone — regardless of background, identity, or experience level. + +We want this project to be a space where *everyone* feels comfortable sharing ideas, collaborating, and learning together. + +--- + +## 🤝 Our Standards + +We expect all participants in our community (including issues, pull requests, discussions, and other communications) to: + +- Be respectful and considerate +- Use inclusive and welcoming language +- Assume good intentions and clarify misunderstandings +- Offer constructive feedback +- Accept feedback graciously +- Avoid demeaning, discriminatory, or harassing behavior or speech + +Unacceptable behaviors include: + +- Personal attacks +- Trolling, baiting, or inflammatory comments +- Harassment in any form (public or private) +- Publishing private information (doxing) without consent +- Disruptive behavior that derails conversations or community efforts + +--- + +## 📣 Reporting Issues + +If you experience or witness unacceptable behavior, please report it by contacting a maintainer privately. You can also open a confidential issue or reach out through our community platform (e.g., Discord or email — TBD). + +We take all reports seriously and will treat them with the confidentiality and respect they deserve. + +--- + +## 🛠 Enforcement + +Project maintainers are responsible for clarifying standards of acceptable behavior and may take appropriate and fair corrective action in response to any instance of unacceptable behavior. + +This may include warnings, temporary or permanent bans, or removing content that violates the Code of Conduct. + +--- + +## 🌍 Scope + +This Code of Conduct applies to: + +- All project spaces, including GitHub repositories, discussions, issues, and pull requests +- Any official community channels associated with the project (e.g., Discord, forums, etc.) +- Both public and private interactions when representing the project + +--- + +## 🙏 Attribution + +This Code of Conduct is based on the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. + +For questions or feedback, feel free to open a discussion or contact the maintainers. + +--- + +> Let's keep **Foundryborne** a friendly and collaborative space for TTRPG lovers, devs, and curious frogs alike. 🐸 diff --git a/contributing.md b/contributing.md new file mode 100644 index 00000000..b6f4097e --- /dev/null +++ b/contributing.md @@ -0,0 +1,72 @@ +# Contributing to Foundryborne + +Welcome! This is a community-driven project to bring [Daggerheart](https://www.daggerheart.com/) to [FoundryVTT](https://foundryvtt.com/) as a full system. We're excited to have you here and appreciate your interest in contributing. + +--- + +## 🤝 How to Contribute + +We welcome contributions of all kinds: + +- Bug reports +- Feature suggestions +- Code contributions +- UI/UX mockups +- Documentation improvements +- Questions and discussions + +Please be respectful and collaborative — we’re all here to build something great together. + +--- + +## 🧭 General Guidelines + +- **Use GitHub Issues** to report bugs or propose features +- **Start a Discussion** for larger ideas or questions +- **Open a Pull Request** once you've confirmed your work aligns with project direction +- **Keep things modular and maintainable** — if you're not sure how to structure something, ask! +- **Orient your code on existing examples**, and feel free to suggest a standard if it makes things clearer + +--- + +## 🗂️ Project Structure + +Please try to follow the general logic of the existing code when submitting PRs. + +We encourage contributors to leave comments or open Discussions when proposing structural or organizational changes. + +--- + +## 🧾 Issue & PR Best Practices + +**For Issues:** +- Use clear, descriptive titles +- Provide a concise explanation of the problem or idea +- Include reproduction steps or example scenarios if it's a bug +- Add screenshots or logs if helpful + +**For Pull Requests:** +- Use a clear title summarizing the change +- Provide a brief description of what your code does and why +- Link to any related Issues +- Keep PRs focused — smaller is better + +--- + +## 🔖 Labels and Boards + +We use GitHub labels to help organize contributions. If your issue or PR relates to a specific category, feel free to tag it. There is also a GitHub Project Board to help track active work and priorities. + +--- + +## 📣 Communication + +Discussions are currently happening on GitHub — in Issues, PRs, and [GitHub Discussions](https://github.com/Foundryborne/daggerheart/discussions). You're welcome to use any of these, though we may consolidate to one in the future. + +--- + +## 🤗 Thank You! + +Whether you're fixing a typo or designing entire mechanics — every contribution matters. Thank you for helping bring *Daggerheart* to life in FoundryVTT through **Foundryborne**! + +🐸🛠️ diff --git a/daggerheart.mjs b/daggerheart.mjs index c954f264..c08fed77 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -122,6 +122,7 @@ Hooks.once('init', () => { CONFIG.Token.rulerClass = DhpTokenRuler; CONFIG.ui.resources = Resources; + CONFIG.ux.ContextMenu = applications.DhContextMenu; game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent); diff --git a/lang/en.json b/lang/en.json index b3640203..77a14f75 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1106,6 +1106,13 @@ } }, "Sheets": { + "TABS": { + "features": "Features", + "effects": "Effects", + "settings": "Settings", + "actions": "Actions", + "description": "Description" + }, "PC": { "Name": "Name", "Pronouns": "Pronouns", @@ -1127,6 +1134,17 @@ "biography": "Biography", "effects": "Effects" }, + "ContextMenu": { + "UseItem": "Use", + "Equip": "Equip", + "Unequip": "Unequip", + "Edit": "Edit", + "Delete": "Delete", + "ToLoadout": "Send to Loadout", + "ToVault": "Send to Vault", + "Consume": "Consume Item", + "SendToChat": "Send To Chat" + }, "Armor": { "Title": "Active Armor" }, @@ -1323,6 +1341,7 @@ "Severe": "Severe" }, "Evasion": "Evasion", + "HitPoints": "Hit Points", "ClassFeatures": "Class Features", "Subclasses": "Subclasses", "Guide": { diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index b1a1d59e..c9f5ddc6 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -13,3 +13,6 @@ export { default as DhpArmor } from './sheets/items/armor.mjs'; export { default as DhpChatMessage } from './chatMessage.mjs'; export { default as DhpEnvironment } from './sheets/environment.mjs'; export { default as DhActiveEffectConfig } from './sheets/activeEffectConfig.mjs'; +export { default as DhContextMenu } from './contextMenu.mjs'; + +export * as api from './sheets/api/_modules.mjs'; diff --git a/module/applications/contextMenu.mjs b/module/applications/contextMenu.mjs new file mode 100644 index 00000000..ff171bfe --- /dev/null +++ b/module/applications/contextMenu.mjs @@ -0,0 +1,35 @@ +export default class DhContextMenu extends ContextMenu { + constructor(container, selector, menuItems, options) { + super(container, selector, menuItems, options); + + /** @deprecated since v13 until v15 */ + this.#jQuery = options.jQuery; + } + + #jQuery; + + activateListeners(menu) { + menu.addEventListener('click', this.#onClickItem.bind(this)); + } + + #onClickItem(event) { + event.preventDefault(); + event.stopPropagation(); + const element = event.target.closest('.context-item'); + if (!element) return; + const item = this.menuItems.find(i => i.element === element); + item?.callback(this.#jQuery ? $(this.target) : this.target, event); + this.close(); + } + + static triggerContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + const { clientX, clientY } = event; + const selector = "[data-item-id]"; + const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); + target?.dispatchEvent(new PointerEvent("contextmenu", { + view: window, bubbles: true, cancelable: true, clientX, clientY + })); + } +} diff --git a/module/applications/countdowns.mjs b/module/applications/countdowns.mjs index 8f7c1cbf..0eac145f 100644 --- a/module/applications/countdowns.mjs +++ b/module/applications/countdowns.mjs @@ -48,8 +48,8 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { super._attachPartListeners(partId, htmlElement, options); htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { - element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, true)); - element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, false)); + element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false)); + element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true)); }); } diff --git a/module/applications/sheets/api/_modules.mjs b/module/applications/sheets/api/_modules.mjs new file mode 100644 index 00000000..4316da2a --- /dev/null +++ b/module/applications/sheets/api/_modules.mjs @@ -0,0 +1,3 @@ +export { default as DHApplicationMixin } from './application-mixin.mjs'; +export { default as DHBaseItemSheet } from './base-item.mjs'; +export { default as DHHeritageSheet } from './heritage-sheet.mjs'; diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs new file mode 100644 index 00000000..2dcfff6e --- /dev/null +++ b/module/applications/sheets/api/application-mixin.mjs @@ -0,0 +1,204 @@ +const { HandlebarsApplicationMixin } = foundry.applications.api; +import { tagifyElement } from '../../../helpers/utils.mjs'; + +/** + * @typedef {object} DragDropConfig + * @property {string} [dragSelector] - A CSS selector that identifies draggable elements. + * @property {string} [dropSelector] - A CSS selector that identifies drop targets. + * + * @typedef {Object} TagOption + * @property {string} label + * @property {string} [src] + * + * @typedef {object} TagifyConfig + * @property {String} selector - The CSS selector for get the element to transform into a tag input + * @property {Record | (() => Record)} options - Available tag options as key-value pairs + * @property {TagChangeCallback} callback - Callback function triggered when tags change + * @property {TagifyOptions} [tagifyOptions={}] - Additional configuration for Tagify + * + * @callback TagChangeCallback + * @param {Array<{value: string, name: string, src?: string}>} selectedOptions - Current selected tags + * @param {{option: string, removed: boolean}} change - What changed (added/removed tag) + * @param {HTMLElement} inputElement - Original input element + * + * + * @typedef {Object} TagifyOptions + * @property {number} [maxTags] - Maximum number of allowed tags + * + * @typedef {import("@client/applications/api/handlebars-application.mjs").HandlebarsRenderOptions} HandlebarsRenderOptions + * @typedef {foundry.applications.types.ApplicationConfiguration & HandlebarsRenderOptions & { dragDrop?: DragDropConfig[], tagifyConfigs?: TagifyConfig[] }} DHSheetV2Configuration + */ + +/** + * @template {Constructor} BaseDocumentSheet + * @param {BaseDocumentSheet} Base - The base class to extend. + * @returns {BaseDocumentSheet} + */ +export default function DHApplicationMixin(Base) { + class DHSheetV2 extends HandlebarsApplicationMixin(Base) { + /** + * @param {DHSheetV2Configuration} [options={}] + */ + constructor(options = {}) { + super(options); + /** + * @type {foundry.applications.ux.DragDrop[]} + * @private + */ + this._dragDrop = this._createDragDropHandlers(); + } + + /** + * The default options for the sheet. + * @type {DHSheetV2Configuration} + */ + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'sheet', 'dh-style'], + position: { + width: 480, + height: 'auto' + }, + actions: { + addEffect: DHSheetV2.#addEffect, + editEffect: DHSheetV2.#editEffect, + removeEffect: DHSheetV2.#removeEffect + }, + dragDrop: [], + tagifyConfigs: [] + }; + + /* -------------------------------------------- */ + + /**@inheritdoc */ + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + this._dragDrop.forEach(d => d.bind(htmlElement)); + } + + /**@inheritdoc */ + async _onRender(context, options) { + await super._onRender(context, options); + this._createTagifyElements(this.options.tagifyConfigs); + } + + /** + * Creates Tagify elements from configuration objects + * @param {TagifyConfig[]} tagConfigs - Array of Tagify configuration objects + * @throws {TypeError} If tagConfigs is not an array + * @throws {Error} If required properties are missing in config objects + * @param {TagifyConfig[]} tagConfigs + */ + _createTagifyElements(tagConfigs) { + if (!Array.isArray(tagConfigs)) throw new TypeError('tagConfigs must be an array'); + + tagConfigs.forEach(config => { + try { + const { selector, options, callback, tagifyOptions = {} } = config; + + // Validate required fields + if (!selector || !options || !callback) { + throw new Error('Invalid TagifyConfig - missing required properties', config); + } + + // Find target element + const element = this.element.querySelector(selector); + if (!element) { + throw new Error(`Element not found with selector: ${selector}`); + } + // Resolve dynamic options if function provided + const resolvedOptions = typeof options === 'function' ? options.call(this) : options; + + // Initialize Tagify + tagifyElement(element, resolvedOptions, callback.bind(this), tagifyOptions); + } catch (error) { + console.error('Error initializing Tagify:', error); + } + }); + } + + /* -------------------------------------------- */ + /* Drag and Drop */ + /* -------------------------------------------- */ + + /** + * Creates drag-drop handlers from the configured options. + * @returns {foundry.applications.ux.DragDrop[]} + * @private + */ + _createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.callbacks = { + drop: this._onDrop.bind(this) + }; + return new foundry.applications.ux.DragDrop.implementation(d); + }); + } + + /** + * Handle drop event. + * @param {DragEvent} event + * @protected + */ + _onDrop(event) { } + + /* -------------------------------------------- */ + /* Prepare Context */ + /* -------------------------------------------- */ + + /** + * Prepare the template context. + * @param {object} options + * @param {string} [objectPath='document'] + * @returns {Promise} + * @inheritdoc + */ + async _prepareContext(options, objectPath = 'document') { + const context = await super._prepareContext(options); + context.config = CONFIG.daggerheart; + context.source = this[objectPath]; + context.fields = this[objectPath].schema.fields; + context.systemFields = this[objectPath].system ? this[objectPath].system.schema.fields : {}; + return context; + } + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + /** + * Renders an ActiveEffect's sheet sheet. + * @param {PointerEvent} event - The originating click event + * @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"] + */ + static async #addEffect() { + const cls = foundry.documents.ActiveEffect; + await cls.create( + { + name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize(cls.metadata.label) }) + }, + { parent: this.document } + ); + } + + /** + * Renders an ActiveEffect's sheet sheet. + * @param {PointerEvent} event - The originating click event + * @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"] + */ + static async #editEffect(_event, button) { + const effect = this.document.effects.get(button.dataset.effect); + effect.sheet.render({ force: true }); + } + + /** + * Delete an ActiveEffect from the item. + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"] + */ + static async #removeEffect(_event, button) { + await this.document.effects.get(button.dataset.effect).delete(); + } + } + + return DHSheetV2; +} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs new file mode 100644 index 00000000..5528f8fc --- /dev/null +++ b/module/applications/sheets/api/base-item.mjs @@ -0,0 +1,148 @@ +import DHApplicationMixin from './application-mixin.mjs'; +import { actionsTypes } from '../../../data/_module.mjs'; +import DHActionConfig from '../../config/Action.mjs'; + +const { ItemSheetV2 } = foundry.applications.sheets; + +/** + * A base item sheet extending {@link ItemSheetV2} via {@link DHApplicationMixin} + * @extends ItemSheetV2 + * @mixes DHSheetV2 + */ +export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ['item'], + position: { width: 600 }, + form: { + submitOnChange: true + }, + actions: { + addAction: DHBaseItemSheet.#addAction, + editAction: DHBaseItemSheet.#editAction, + removeAction: DHBaseItemSheet.#removeAction + } + }; + + /* -------------------------------------------- */ + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' + } + }; + + /* -------------------------------------------- */ + /* Prepare Context */ + /* -------------------------------------------- */ + + /**@inheritdoc */ + async _preparePartContext(partId, context) { + await super._preparePartContext(partId, context); + const { TextEditor } = foundry.applications.ux; + + switch (partId) { + case 'description': + const value = foundry.utils.getProperty(this.document, "system.description") ?? ""; + context.enrichedDescription = await TextEditor.enrichHTML(value, { + relativeTo: this.item, + rollData: this.item.getRollData(), + secrets: this.item.isOwner + }) + break; + } + + return context; + } + + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + /** + * Render a dialog prompting the user to select an action type. + * + * @returns {Promise} An object containing the selected action type. + */ + static async selectActionType() { + const content = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/views/actionType.hbs', + { types: SYSTEM.ACTIONS.actionTypes } + ), + title = 'Select Action Type', //useless var + type = 'form', + data = {}; //useless var + //TODO: use DialogV2 + return Dialog.prompt({ + title, + label: title, + content, + type, //this prop is useless + callback: html => { + const form = html[0].querySelector('form'), + fd = new foundry.applications.ux.FormDataExtended(form); + foundry.utils.mergeObject(data, fd.object, { inplace: true }); + // if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name); + return data; + }, + rejectClose: false + }); + } + + /** + * Add a new action to the item, prompting the user for its type. + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} _button - The capturing HTML element which defines the [data-action="addAction"] + */ + static async #addAction(_event, _button) { + const actionType = await DHBaseItemSheet.selectActionType(); + try { + const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, + action = new cls( + { + _id: foundry.utils.randomID(), + type: actionType.type, + name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), + ...cls.getSourceConfig(this.document) + }, + { + parent: this.document + } + ); + await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); + await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render({ + force: true + }); + } catch (error) { + console.log(error); + } + } + + /** + * Edit an existing action on the item + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} button - The capturing HTML element which defines the [data-action="editAction"] + */ + static async #editAction(_event, button) { + const action = this.document.system.actions[button.dataset.index]; + await new DHActionConfig(action).render({ force: true }); + } + + /** + * Remove an action from the item. + * @param {PointerEvent} event - The originating click event + * @param {HTMLElement} button - The capturing HTML element which defines the [data-action="removeAction"] + */ + static async #removeAction(event, button) { + event.stopPropagation(); + await this.document.update({ + 'system.actions': this.document.system.actions.filter( + (_, index) => index !== Number.parseInt(button.dataset.index) + ) + }); + } +} diff --git a/module/applications/sheets/api/heritage-sheet.mjs b/module/applications/sheets/api/heritage-sheet.mjs new file mode 100644 index 00000000..4938f5b0 --- /dev/null +++ b/module/applications/sheets/api/heritage-sheet.mjs @@ -0,0 +1,31 @@ +import DHBaseItemSheet from './base-item.mjs'; + +export default class DHHeritageSheet extends DHBaseItemSheet { + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + position: { width: 450, height: 700 } + }; + + /**@override */ + static PARTS = { + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + actions: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', + scrollable: ['.actions'] + }, + effects: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', + scrollable: ['.effects'] + } + }; + + /** @override*/ + static TABS = { + primary: { + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' + } + }; +} diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index e5fed0f1..55190217 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -25,14 +25,13 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { toggleStress: this.toggleStress, toggleHope: this.toggleHope, toggleGold: this.toggleGold, - attackRoll: this.attackRoll, + toggleLoadoutView: this.toggleLoadoutView, useDomainCard: this.useDomainCard, - removeCard: this.removeDomainCard, selectClass: this.selectClass, selectSubclass: this.selectSubclass, selectAncestry: this.selectAncestry, selectCommunity: this.selectCommunity, - // viewObject: this.viewObject, + viewObject: this.viewObject, useItem: this.useItem, useFeature: this.useFeature, takeShortRest: this.takeShortRest, @@ -43,12 +42,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { makeDeathMove: this.makeDeathMove, itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1), itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1), - useAbility: this.useAbility, + toChat: this.toChat, useAdvancementCard: this.useAdvancementCard, useAdvancementAbility: this.useAdvancementAbility, toggleEquipItem: this.toggleEquipItem, + toggleVault: this.toggleVault, levelManagement: this.levelManagement, - editImage: this._onEditImage + editImage: this._onEditImage, + triggerContextMenu: this.triggerContextMenu }, window: { resizable: true @@ -58,11 +59,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { submitOnChange: true, closeOnSubmit: false }, - dragDrop: [ - { dragSelector: null, dropSelector: '.weapon-section' }, - { dragSelector: null, dropSelector: '.armor-section' }, - { dragSelector: '.item-list .item', dropSelector: null } - ] + dragDrop: [] }; static PARTS = { @@ -214,14 +211,96 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { return { primary: primaryTabs, secondary: secondaryTabs }; } + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + + this._createContextMenues(); + } + + _createContextMenues() { + const allOptions = { + useItem: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.UseItem', + icon: '', + condition: el => { + const item = this.getItem(el); + return !['class', 'subclass'].includes(item.type); + }, + callback: (button, event) => this.constructor.useItem.bind(this)(event, button) + }, + equip: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.Equip', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['weapon', 'armor'].includes(item.type) && !item.system.equipped; + }, + callback: this.constructor.toggleEquipItem.bind(this) + }, + unequip: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.Unequip', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['weapon', 'armor'].includes(item.type) && item.system.equipped; + }, + callback: this.constructor.toggleEquipItem.bind(this) + }, + sendToLoadout: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToLoadout', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['domainCard'].includes(item.type) && item.system.inVault; + }, + callback: this.constructor.toggleVault.bind(this) + }, + sendToVault: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.ToVault', + icon: '', + condition: el => { + const item = this.getItem(el); + return ['domainCard'].includes(item.type) && !item.system.inVault; + }, + callback: this.constructor.toggleVault.bind(this) + }, + sendToChat: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.SendToChat', + icon: '', + callback: this.constructor.toChat.bind(this) + }, + edit: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.Edit', + icon: '', + callback: this.constructor.viewObject.bind(this) + }, + delete: { + name: 'DAGGERHEART.Sheets.PC.ContextMenu.Delete', + icon: '', + callback: this.constructor.deleteItem.bind(this) + } + }; + + this._createContextMenu(() => Object.values(allOptions), `[data-item-id]`, { + parentClassHooks: false, + fixed: true + }); + } + _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); htmlElement.querySelector('.level-value')?.addEventListener('change', this.onLevelChange.bind(this)); - // To Remove when ContextMenu Handler is made - htmlElement - .querySelectorAll('[data-item-id]') - .forEach(element => element.addEventListener('contextmenu', this.editItem.bind(this))); + } + + getItem(element) { + const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, + item = this.document.items.get(itemId); + return item; + } + + static triggerContextMenu(event, button) { + return CONFIG.ux.ContextMenu.triggerContextMenu(event); } static _onEditImage() { @@ -408,14 +487,10 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { await this.document.update({ [update]: newValue }); } - static async attackRoll(event, button) { - const weapon = await fromUuid(button.dataset.weapon); - if (!weapon) return; - - const wasUsed = await weapon.use(event); - if (wasUsed) { - Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); - } + static async toggleLoadoutView(_, button) { + const newAbilityView = !(button.dataset.value === 'true'); + await game.user.setFlag(SYSTEM.id, SYSTEM.FLAGS.displayDomainCardsAsList, newAbilityView); + this.render(); } static levelManagement() { @@ -439,8 +514,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { new DhlevelUp(this.document).render(true); } - static async useDomainCard(_, button) { - const card = this.document.items.find(x => x.uuid === button.dataset.key); + static async useDomainCard(event, button) { + const card = this.getItem(event); + if (!card) return; const cls = getDocumentClass('ChatMessage'); const systemData = { @@ -464,13 +540,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async removeDomainCard(_, button) { - if (button.dataset.type === 'domainCard') { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - await card.delete(); - } - } - static async selectClass() { (await game.packs.get('daggerheart.classes'))?.render(true); } @@ -505,27 +574,23 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { (await game.packs.get('daggerheart.communities'))?.render(true); } - static useItem(event) { - const uuid = event.target.closest('[data-item-id]').dataset.itemId, - item = this.document.items.find(i => i.uuid === uuid); - item.use(event); + static async useItem(event, button) { + const item = this.getItem(button); + if (!item) return; + const wasUsed = await item.use(event); + if (wasUsed && item.type === 'weapon') { + Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + } } - /* static async viewObject(_, button) { - const object = await fromUuid(button.dataset.value); - if (!object) return; - - const tab = button.dataset.tab; - if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab; - - if (object.sheet.editMode) object.sheet.editMode = false; - - object.sheet.render(true); - } */ + static async viewObject(event, button) { + const item = this.getItem(event); + if (!item) return; + item.sheet.render(true); + } editItem(event) { - const uuid = event.target.closest('[data-item-id]').dataset.itemId, - item = this.document.items.find(i => i.uuid === uuid); + const item = this.getItem(event); if (!item) return; if (item.sheet.editMode) item.sheet.editMode = false; @@ -566,33 +631,29 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { static async makeDeathMove() { if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { await new DhpDeathMove(this.document).render(true); - await this.minimize(); } } - async itemUpdate(event) { - const name = event.currentTarget.dataset.item; - const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); - await item.update({ [name]: event.currentTarget.value }); - } - async onLevelChange(event) { await this.document.updateLevel(Number(event.currentTarget.value)); this.render(); } - static async deleteItem(_, button) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + static async deleteItem(event, button) { + const item = this.getItem(event); + if (!item) return; await item.delete(); } static async setItemQuantity(button, value) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + const item = this.getItem(button); + if (!item) return; await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); } - static async useFeature(_, button) { - const item = await fromUuid(button.dataset.id); + static async useFeature(event, button) { + const item = this.getItem(event); + if (!item) return; const cls = getDocumentClass('ChatMessage'); const systemData = { @@ -616,35 +677,30 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async useAbility(_, button) { - const item = await fromUuid(button.dataset.feature); - const type = button.dataset.type; + 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.description} ${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 + ) + }); - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: - type === 'ancestry' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') - : type === 'community' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') - : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - img: item.img, - name: item.name, - description: item.system.description, - actions: [] - }; - 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()); + cls.create(msg.toObject()); + } else { + const item = this.getItem(event); + if (!item) return; + item.toChat(this.document.id); + } } static async useAdvancementCard(_, button) { @@ -699,8 +755,9 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { cls.create(msg.toObject()); } - static async toggleEquipItem(_, button) { - const item = this.document.items.get(button.id); + static async toggleEquipItem(event, button) { + const item = this.getItem(event); + if (!item) return; if (item.system.equipped) { await item.update({ 'system.equipped': false }); return; @@ -724,6 +781,13 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } + static async toggleVault(event, button) { + const itemId = event.target.closest('[data-item-id]').dataset.itemId, + item = this.document.items.get(itemId); + if (!item) return; + await item.update({ 'system.inVault': !item.system.inVault }); + } + async _onDragStart(_, event) { super._onDragStart(event); } diff --git a/module/applications/sheets/item.mjs b/module/applications/sheets/item.mjs deleted file mode 100644 index 0b0800f7..00000000 --- a/module/applications/sheets/item.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import DhpApplicationMixin from './daggerheart-sheet.mjs'; -import DHActionConfig from '../config/Action.mjs'; -import { actionsTypes } from '../../data/_module.mjs'; - -export default function DHItemMixin(Base) { - return class DHItemSheetV2 extends DhpApplicationMixin(Base) { - constructor(options = {}) { - super(options); - } - - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style'], - position: { width: 600 }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - }, - actions: { - addAction: this.addAction, - editAction: this.editAction, - removeAction: this.removeAction - } - }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - actions: { - active: false, - cssClass: '', - group: 'primary', - id: 'actions', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.config = CONFIG.daggerheart; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - - static async selectActionType() { - const content = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/views/actionType.hbs', - { types: SYSTEM.ACTIONS.actionTypes } - ), - title = 'Select Action Type', - type = 'form', - data = {}; - return Dialog.prompt({ - title, - label: title, - content, - type, - callback: html => { - const form = html[0].querySelector('form'), - fd = new foundry.applications.ux.FormDataExtended(form); - foundry.utils.mergeObject(data, fd.object, { inplace: true }); - // if (!data.name?.trim()) data.name = game.i18n.localize(SYSTEM.ACTIONS.actionTypes[data.type].name); - return data; - }, - rejectClose: false - }); - } - - static async addAction() { - const actionType = await DHItemSheetV2.selectActionType(); - try { - const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, - action = new cls( - { - _id: foundry.utils.randomID(), - type: actionType.type, - name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), - ...cls.getSourceConfig(this.document) - }, - { - parent: this.document - } - ); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render(true); - } catch (error) { - console.log(error); - } - } - - static async editAction(event, button) { - const index = event.target.closest('[data-index]').dataset.index, - action = this.document.system.actions[index]; - await new DHActionConfig(action).render(true); - } - - static async removeAction(event, button) { - event.stopPropagation(); - const action = event.target.closest('[data-index]').dataset.index; - await this.document.update({ - 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(action) - ) - }); - } - }; -} diff --git a/module/applications/sheets/items/ancestry.mjs b/module/applications/sheets/items/ancestry.mjs index 3d20df7a..3636ea62 100644 --- a/module/applications/sheets/items/ancestry.mjs +++ b/module/applications/sheets/items/ancestry.mjs @@ -1,11 +1,12 @@ -import DHHeritageSheetV2 from './heritage.mjs'; +import DHHeritageSheet from '../api/heritage-sheet.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class AncestrySheet extends DHHeritageSheetV2(ItemSheetV2) { +export default class AncestrySheet extends DHHeritageSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['ancestry'] }; + /**@inheritdoc */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/ancestry/header.hbs' }, ...super.PARTS diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index 32c482ba..4e53d8fa 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -1,14 +1,20 @@ -import { armorFeatures } from '../../../config/itemConfig.mjs'; -import { tagifyElement } from '../../../helpers/utils.mjs'; -import DHItemSheetV2 from '../item.mjs'; +import DHBaseItemSheet from '../api/base-item.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) { +export default class ArmorSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['armor'], - dragDrop: [{ dragSelector: null, dropSelector: null }] + dragDrop: [{ dragSelector: null, dropSelector: null }], + tagifyConfigs: [ + { + selector: '.features-input', + options: () => CONFIG.daggerheart.ITEM.armorFeatures, + callback: ArmorSheet.#onFeatureSelect + } + ] }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/armor/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, @@ -23,8 +29,9 @@ export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) { } }; + /**@inheritdoc */ async _preparePartContext(partId, context) { - super._preparePartContext(partId, context); + await super._preparePartContext(partId, context); switch (partId) { case 'settings': @@ -35,15 +42,11 @@ export default class ArmorSheet extends DHItemSheetV2(ItemSheetV2) { return context; } - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - const featureInput = htmlElement.querySelector('.features-input'); - tagifyElement(featureInput, armorFeatures, this.onFeatureSelect.bind(this)); - } - - async onFeatureSelect(features) { - await this.document.update({ 'system.features': features.map(x => ({ value: x.value })) }); - this.render(true); + /** + * Callback function used by `tagifyElement`. + * @param {Array} selectedOptions - The currently selected tag objects. + */ + static async #onFeatureSelect(selectedOptions) { + await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); } } diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index a4659598..efe556fa 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -1,33 +1,29 @@ +import DHBaseItemSheet from '../api/base-item.mjs'; import { actionsTypes } from '../../../data/_module.mjs'; -import { tagifyElement } from '../../../helpers/utils.mjs'; import DHActionConfig from '../../config/Action.mjs'; -import DaggerheartSheet from '../daggerheart-sheet.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; const { TextEditor } = foundry.applications.ux; -export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { +export default class ClassSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'class'], + classes: ['class'], position: { width: 700 }, actions: { - removeSubclass: this.removeSubclass, - viewSubclass: this.viewSubclass, + removeItemFromCollection: ClassSheet.#removeItemFromCollection, + removeSuggestedItem: ClassSheet.#removeSuggestedItem, + viewDoc: ClassSheet.#viewDoc, addFeature: this.addFeature, editFeature: this.editFeature, - deleteFeature: this.deleteFeature, - removeItem: this.removeItem, - viewItem: this.viewItem, - removePrimaryWeapon: this.removePrimaryWeapon, - removeSecondaryWeapon: this.removeSecondaryWeapon, - removeArmor: this.removeArmor - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false + deleteFeature: this.deleteFeature }, + tagifyConfigs: [ + { + selector: '.domain-input', + options: () => CONFIG.daggerheart.DOMAIN.domains, + callback: ClassSheet.#onDomainSelect + } + ], dragDrop: [ { dragSelector: '.suggested-item', dropSelector: null }, { dragSelector: null, dropSelector: '.take-section' }, @@ -40,9 +36,11 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { ] }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/class/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, features: { template: 'systems/daggerheart/templates/sheets/items/class/features.hbs', scrollable: ['.features'] @@ -53,170 +51,33 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { } }; + /** @inheritdoc */ static TABS = { - features: { - active: true, - cssClass: '', - group: 'primary', - id: 'features', - icon: null, - label: 'DAGGERHEART.Sheets.Class.Tabs.Features' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Class.Tabs.settings' + primary: { + tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' } }; - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - const domainInput = htmlElement.querySelector('.domain-input'); - tagifyElement(domainInput, SYSTEM.DOMAIN.domains, this.onDomainSelect.bind(this)); - } - + /**@inheritdoc */ async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = super._getTabs(this.constructor.TABS); context.domains = this.document.system.domains; - return context; } - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); + /* -------------------------------------------- */ + + /** + * Callback function used by `tagifyElement`. + * @param {Array} selectedOptions - The currently selected tag objects. + */ + static async #onDomainSelect(selectedOptions) { + await this.document.update({ 'system.domains': selectedOptions.map(x => x.value) }); } - onAddTag(e) { - if (e.detail.index === 2) { - ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.ClassCanOnlyHaveTwoDomains')); - } - } - - async onDomainSelect(domains) { - await this.document.update({ 'system.domains': domains.map(x => x.value) }); - this.render(true); - } - - static async removeSubclass(_, button) { - await this.document.update({ - 'system.subclasses': this.document.system.subclasses.filter(x => x.uuid !== button.dataset.subclass) - }); - } - - static async viewSubclass(_, button) { - const subclass = await fromUuid(button.dataset.subclass); - subclass.sheet.render(true); - } - - static async deleteFeature(_, button) { - await this.document.update({ - 'system.features': this.document.system.features.map(x => x.uuid).filter(x => x !== button.dataset.feature) - }); - } - - static async editFeature(_, button) { - const feature = await fromUuid(button.dataset.feature); - feature.sheet.render(true); - } - - static async removeItem(event, button) { - event.stopPropagation(); - const type = button.dataset.type; - const path = `system.inventory.${type}`; - await this.document.update({ - [path]: this.document.system.inventory[type].filter(x => x.uuid !== button.dataset.item) - }); - } - - static async viewItem(_, button) { - const item = await fromUuid(button.dataset.item); - item.sheet.render(true); - } - - static async removePrimaryWeapon(event) { - event.stopPropagation(); - await this.document.update({ 'system.characterGuide.suggestedPrimaryWeapon': null }, { diff: false }); - } - - static async removeSecondaryWeapon(event) { - event.stopPropagation(); - await this.document.update({ 'system.characterGuide.suggestedSecondaryWeapon': null }, { diff: false }); - } - - static async removeArmor(event) { - event.stopPropagation(); - await this.document.update({ 'system.characterGuide.suggestedArmor': null }, { diff: false }); - } - - async selectActionType() { - const content = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/views/actionType.hbs', - { types: SYSTEM.ACTIONS.actionTypes } - ), - title = 'Select Action Type', - type = 'form', - data = {}; - return Dialog.prompt({ - title, - label: title, - content, - type, - callback: html => { - const form = html[0].querySelector('form'), - fd = new foundry.applications.ux.FormDataExtended(form); - foundry.utils.mergeObject(data, fd.object, { inplace: true }); - - return data; - }, - rejectClose: false - }); - } - - getActionPath(type) { - return type === 'hope' ? 'hopeFeatures' : 'classFeatures'; - } - - static async addFeature(_, target) { - const actionPath = this.getActionPath(target.dataset.type); - const actionType = await this.selectActionType(); - const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, - action = new cls( - { - _id: foundry.utils.randomID(), - systemPath: actionPath, - type: actionType.type, - name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), - ...cls.getSourceConfig(this.document) - }, - { - parent: this.document - } - ); - await this.document.update({ [`system.${actionPath}`]: [...this.document.system[actionPath], action] }); - } - - static async editFeature(_, target) { - const action = this.document.system[this.getActionPath(target.dataset.type)].find( - x => x._id === target.dataset.feature - ); - await new DHActionConfig(action).render(true); - } - - static async deleteFeature(_, target) { - const actionPath = this.getActionPath(target.dataset.type); - await this.document.update({ - [`system.${actionPath}`]: this.document.system[actionPath].filter( - action => action._id !== target.dataset.feature - ) - }); - } + /* -------------------------------------------- */ async _onDrop(event) { const data = TextEditor.getDragEventData(event); @@ -272,4 +133,107 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { } } } + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + /** + * Removes an item from an class collection by UUID. + * @param {PointerEvent} event - The originating click event + * @param {HTMLElement} element - The capturing HTML element which defines the [data-action="removeItemFromCollection"] + */ + static async #removeItemFromCollection(_event, element) { + const { uuid, target } = element.dataset; + const prop = foundry.utils.getProperty(this.document.system, target); + await this.document.update({ [target]: prop.filter(i => i.uuid !== uuid) }); + } + + /** + * Removes an suggested item from the class. + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} element - The capturing HTML element which defines the [data-action="removeSuggestedItem"] + */ + static async #removeSuggestedItem(_event, element) { + const { target } = element.dataset; + await this.document.update({ [`system.characterGuide.${target}`]: null }); + } + + /** + * Open the sheet of a item by UUID. + * @param {PointerEvent} _event - + * @param {HTMLElement} button + */ + static async #viewDoc(_event, button) { + const doc = await fromUuid(button.dataset.uuid); + doc.sheet.render({ force: true }); + } + + //TODO: redo this + async selectActionType() { + const content = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/views/actionType.hbs', + { types: SYSTEM.ACTIONS.actionTypes } + ), + title = 'Select Action Type', + type = 'form', + data = {}; + return Dialog.prompt({ + title, + label: title, + content, + type, + callback: html => { + const form = html[0].querySelector('form'), + fd = new foundry.applications.ux.FormDataExtended(form); + foundry.utils.mergeObject(data, fd.object, { inplace: true }); + + return data; + }, + rejectClose: false + }); + } + + //TODO: redo this + getActionPath(type) { + return type === 'hope' ? 'hopeFeatures' : 'classFeatures'; + } + + //TODO: redo this + static async addFeature(_, target) { + const actionPath = this.getActionPath(target.dataset.type); + const actionType = await this.selectActionType(); + const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, + action = new cls( + { + _id: foundry.utils.randomID(), + systemPath: actionPath, + type: actionType.type, + name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), + ...cls.getSourceConfig(this.document) + }, + { + parent: this.document + } + ); + await this.document.update({ [`system.${actionPath}`]: [...this.document.system[actionPath], action] }); + } + + //TODO: redo this + static async editFeature(_, target) { + const action = this.document.system[this.getActionPath(target.dataset.type)].find( + x => x._id === target.dataset.feature + ); + await new DHActionConfig(action).render(true); + } + + //TODO: redo this + static async deleteFeature(_, target) { + const actionPath = this.getActionPath(target.dataset.type); + await this.document.update({ + [`system.${actionPath}`]: this.document.system[actionPath].filter( + action => action._id !== target.dataset.feature + ) + }); + } } diff --git a/module/applications/sheets/items/community.mjs b/module/applications/sheets/items/community.mjs index 890ff605..92c3731e 100644 --- a/module/applications/sheets/items/community.mjs +++ b/module/applications/sheets/items/community.mjs @@ -1,11 +1,12 @@ -import DHHeritageSheetV2 from './heritage.mjs'; +import DHHeritageSheet from '../api/heritage-sheet.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class CommunitySheet extends DHHeritageSheetV2(ItemSheetV2) { +export default class CommunitySheet extends DHHeritageSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['community'] }; + /**@inheritdoc */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' }, ...super.PARTS diff --git a/module/applications/sheets/items/consumable.mjs b/module/applications/sheets/items/consumable.mjs index 815c6b9b..f6fb1ba5 100644 --- a/module/applications/sheets/items/consumable.mjs +++ b/module/applications/sheets/items/consumable.mjs @@ -1,12 +1,13 @@ -import DHItemSheetV2 from '../item.mjs'; +import DHBaseItemSheet from '../api/base-item.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class ConsumableSheet extends DHItemSheetV2(ItemSheetV2) { +export default class ConsumableSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['consumable'], position: { width: 550 } }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/consumable/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, diff --git a/module/applications/sheets/items/domainCard.mjs b/module/applications/sheets/items/domainCard.mjs index dd951be5..d7a70741 100644 --- a/module/applications/sheets/items/domainCard.mjs +++ b/module/applications/sheets/items/domainCard.mjs @@ -1,59 +1,37 @@ -import DHItemSheetV2 from '../item.mjs'; +import DHBaseItemSheet from '../api/base-item.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) { +export default class DomainCardSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['domain-card'], - position: { width: 450, height: 700 }, - actions: { - addEffect: this.addEffect, - editEffect: this.editEffect, - removeEffect: this.removeEffect + position: { width: 450, height: 700 } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'effects' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' } }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/domainCard/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, - settings: { - template: 'systems/daggerheart/templates/sheets/items/domainCard/settings.hbs', - scrollable: ['.settings'] - }, actions: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', scrollable: ['.actions'] }, + settings: { + template: 'systems/daggerheart/templates/sheets/items/domainCard/settings.hbs', + scrollable: ['.settings'] + }, effects: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] } }; - - static TABS = { - ...super.TABS, - effects: { - active: false, - cssClass: '', - group: 'primary', - id: 'effects', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Effects' - } - }; - - static async addEffect() { - await this.document.createEmbeddedDocuments('ActiveEffect', [ - { name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') } - ]); - } - - static async editEffect(_, target) { - const effect = this.document.effects.get(target.dataset.effect); - effect.sheet.render(true); - } - - static async removeEffect(_, target) { - await this.document.effects.get(target.dataset.effect).delete(); - } } diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index 40fa9b02..655e4542 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -1,17 +1,11 @@ -import DHItemSheetV2 from '../item.mjs'; - -const { ItemSheetV2 } = foundry.applications.sheets; -export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) { - constructor(options = {}) { - super(options); - - this.selectedEffectType = null; - } +import DHBaseItemSheet from '../api/base-item.mjs'; +export default class FeatureSheet extends DHBaseItemSheet { + /** @inheritDoc */ static DEFAULT_OPTIONS = { id: 'daggerheart-feature', classes: ['feature'], - position: { width: 600, height: 600 }, + position: { height: 600 }, window: { resizable: true }, actions: { addEffect: this.addEffect, @@ -19,6 +13,7 @@ export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) { } }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, @@ -37,58 +32,86 @@ export default class FeatureSheet extends DHItemSheetV2(ItemSheetV2) { } }; + /** + * Internally tracks the selected effect type from the select. + * @type {String} + * @private + */ + _selectedEffectType; + + /**@override */ static TABS = { - ...super.TABS, - effects: { - active: false, - cssClass: '', - group: 'primary', - id: 'effects', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Effects' + primary: { + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'effects' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' } }; + /* -------------------------------------------- */ + + /**@inheritdoc*/ _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); - $(htmlElement).find('.effect-select').on('change', this.effectSelect.bind(this)); + if (partId === 'effects') + htmlElement.querySelector('.effect-select')?.addEventListener('change', this._effectSelect.bind(this)); } + /** + * Handles selection of a new effect type. + * @param {Event} event - Change Event + */ + _effectSelect(event) { + const value = event.currentTarget.value; + this._selectedEffectType = value; + this.render({ parts: ['effects'] }); + } + + /* -------------------------------------------- */ + + /**@inheritdoc */ async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = super._getTabs(this.constructor.TABS); - context.generalConfig = SYSTEM.GENERAL; - context.itemConfig = SYSTEM.ITEM; - context.properties = SYSTEM.ACTOR.featureProperties; - context.dice = SYSTEM.GENERAL.diceTypes; - context.selectedEffectType = this.selectedEffectType; - context.effectConfig = SYSTEM.EFFECTS; + context.properties = CONFIG.daggerheart.ACTOR.featureProperties; + context.dice = CONFIG.daggerheart.GENERAL.diceTypes; + context.effectConfig = CONFIG.daggerheart.EFFECTS; + + context.selectedEffectType = this._selectedEffectType; return context; } - effectSelect(event) { - this.selectedEffectType = event.currentTarget.value; - this.render(true); - } + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ - static async addEffect() { - if (!this.selectedEffectType) return; - - const { id, name, ...rest } = SYSTEM.EFFECTS.effectTypes[this.selectedEffectType]; - const update = { - [foundry.utils.randomID()]: { - type: this.selectedEffectType, + /** + * Adds a new effect to the item, based on the selected effect type. + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} _target - The capturing HTML element which defines the [data-action] + * @returns + */ + static async addEffect(_event, _target) { + const type = this._selectedEffectType; + if (!type) return; + const { id, name, ...rest } = CONFIG.daggerheart.EFFECTS.effectTypes[type]; + await this.item.update({ + [`system.effects.${foundry.utils.randomID()}`]: { + type, value: '', ...rest } - }; - await this.item.update({ 'system.effects': update }); + }); } - static async removeEffect(_, button) { - const path = `system.effects.-=${button.dataset.effect}`; + /** + * Removes an effect from the item. + * @param {PointerEvent} _event - The originating click event + * @param {HTMLElement} target - The capturing HTML element which defines the [data-action] + * @returns + */ + static async removeEffect(_event, target) { + const path = `system.effects.-=${target.dataset.effect}`; await this.item.update({ [path]: null }); } } diff --git a/module/applications/sheets/items/heritage.mjs b/module/applications/sheets/items/heritage.mjs deleted file mode 100644 index 44d950ca..00000000 --- a/module/applications/sheets/items/heritage.mjs +++ /dev/null @@ -1,147 +0,0 @@ -import { actionsTypes } from '../../../data/_module.mjs'; -import DHActionConfig from '../../config/Action.mjs'; -import DHItemMixin from '../item.mjs'; - -export default function DHHeritageMixin(Base) { - return class DHHeritageSheetV2 extends DHItemMixin(Base) { - static DEFAULT_OPTIONS = { - tag: 'form', - position: { width: 450, height: 700 }, - actions: { - addAction: this.addAction, - editAction: this.editAction, - removeAction: this.removeAction, - addEffect: this.addEffect, - editEffect: this.editEffect, - removeEffect: this.removeEffect - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - } - }; - - static PARTS = { - tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, - description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, - actions: { - template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', - scrollable: ['.actions'] - }, - effects: { - template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', - scrollable: ['.effects'] - } - }; - - static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - actions: { - active: false, - cssClass: '', - group: 'primary', - id: 'actions', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Actions' - }, - effects: { - active: false, - cssClass: '', - group: 'primary', - id: 'effects', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.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(); - } - - async selectActionType() { - const content = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/views/actionType.hbs', - { types: SYSTEM.ACTIONS.actionTypes } - ), - title = 'Select Action Type', - type = 'form', - data = {}; - return Dialog.prompt({ - title, - label: title, - content, - type, - callback: html => { - const form = html[0].querySelector('form'), - fd = new foundry.applications.ux.FormDataExtended(form); - foundry.utils.mergeObject(data, fd.object, { inplace: true }); - return data; - }, - rejectClose: false - }); - } - - static async addAction() { - const actionType = await this.selectActionType(); - const cls = actionsTypes[actionType?.type] ?? actionsTypes.attack, - action = new cls( - { - _id: foundry.utils.randomID(), - type: actionType.type, - name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType.type].name), - ...cls.getSourceConfig(this.document) - }, - { - parent: this.document - } - ); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - } - - static async editAction(_, button) { - const action = this.document.system.actions[button.dataset.index]; - await new DHActionConfig(action).render(true); - } - - static async removeAction(event, button) { - event.stopPropagation(); - await this.document.update({ - 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(button.dataset.index) - ) - }); - } - - static async addEffect() { - await this.document.createEmbeddedDocuments('ActiveEffect', [ - { name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') } - ]); - } - - static async editEffect(_, target) { - const effect = this.document.effects.get(target.dataset.effect); - effect.sheet.render(true); - } - - static async removeEffect(_, target) { - await this.document.effects.get(target.dataset.effect).delete(); - } - }; -} diff --git a/module/applications/sheets/items/miscellaneous.mjs b/module/applications/sheets/items/miscellaneous.mjs index dd22d216..62df4e8c 100644 --- a/module/applications/sheets/items/miscellaneous.mjs +++ b/module/applications/sheets/items/miscellaneous.mjs @@ -1,12 +1,13 @@ -import DHItemSheetV2 from '../item.mjs'; +import DHBaseItemSheet from '../api/base-item.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class MiscellaneousSheet extends DHItemSheetV2(ItemSheetV2) { +export default class MiscellaneousSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['miscellaneous'], position: { width: 550 } }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/miscellaneous/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index e6e9725f..841c3eb2 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -1,26 +1,21 @@ +import DHBaseItemSheet from '../api/base-item.mjs'; import { actionsTypes } from '../../../data/_module.mjs'; import DHActionConfig from '../../config/Action.mjs'; -import DhpApplicationMixin from '../daggerheart-sheet.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class SubclassSheet extends DhpApplicationMixin(ItemSheetV2) { +export default class SubclassSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'item', 'dh-style', 'subclass'], + classes: ['subclass'], position: { width: 600 }, window: { resizable: false }, actions: { addFeature: this.addFeature, editFeature: this.editFeature, deleteFeature: this.deleteFeature - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false } }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/subclass/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, @@ -35,46 +30,16 @@ export default class SubclassSheet extends DhpApplicationMixin(ItemSheetV2) { } }; + /** @inheritdoc */ static TABS = { - description: { - active: true, - cssClass: '', - group: 'primary', - id: 'description', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Description' - }, - features: { - active: false, - cssClass: '', - group: 'primary', - id: 'features', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Features' - }, - settings: { - active: false, - cssClass: '', - group: 'primary', - id: 'settings', - icon: null, - label: 'DAGGERHEART.Sheets.Feature.Tabs.Settings' + primary: { + tabs: [{ id: 'description' }, { id: 'features' }, { id: 'settings' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.Sheets.TABS' } }; - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.config = CONFIG.daggerheart; - context.tabs = super._getTabs(this.constructor.TABS); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } + //TODO redo everything below this message static addFeature(_, target) { if (target.dataset.type === 'action') this.addAction(target.dataset.level); diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index b381e8bf..5f2a12d7 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -1,13 +1,19 @@ -import { weaponFeatures } from '../../../config/itemConfig.mjs'; -import { tagifyElement } from '../../../helpers/utils.mjs'; -import DHItemSheetV2 from '../item.mjs'; +import DHBaseItemSheet from '../api/base-item.mjs'; -const { ItemSheetV2 } = foundry.applications.sheets; -export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { +export default class WeaponSheet extends DHBaseItemSheet { + /**@inheritdoc */ static DEFAULT_OPTIONS = { - classes: ['weapon'] + classes: ['weapon'], + tagifyConfigs: [ + { + selector: '.features-input', + options: () => CONFIG.daggerheart.ITEM.weaponFeatures, + callback: WeaponSheet.#onFeatureSelect + } + ] }; + /**@override */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/weapon/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, @@ -22,6 +28,7 @@ export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { } }; + /**@inheritdoc */ async _preparePartContext(partId, context) { super._preparePartContext(partId, context); @@ -34,15 +41,11 @@ export default class WeaponSheet extends DHItemSheetV2(ItemSheetV2) { return context; } - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - const featureInput = htmlElement.querySelector('.features-input'); - tagifyElement(featureInput, weaponFeatures, this.onFeatureSelect.bind(this)); - } - - async onFeatureSelect(features) { - await this.document.update({ 'system.features': features.map(x => ({ value: x.value })) }); - this.render(true); + /** + * Callback function used by `tagifyElement`. + * @param {Array} selectedOptions - The currently selected tag objects. + */ + static async #onFeatureSelect(selectedOptions) { + await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); } } diff --git a/module/config/domainConfig.mjs b/module/config/domainConfig.mjs index 3ecb0bd1..136b077e 100644 --- a/module/config/domainConfig.mjs +++ b/module/config/domainConfig.mjs @@ -2,56 +2,55 @@ export const domains = { arcana: { id: 'arcana', label: 'DAGGERHEART.Domains.arcana.label', - src: 'icons/magic/symbols/circled-gem-pink.webp', + src: 'systems/daggerheart/assets/icons/domains/arcana.svg', description: 'DAGGERHEART.Domains.Arcana' }, blade: { id: 'blade', label: 'DAGGERHEART.Domains.blade.label', - src: 'icons/weapons/swords/sword-broad-crystal-paired.webp', + src: 'systems/daggerheart/assets/icons/domains/blade.svg', description: 'DAGGERHEART.Domains.Blade' }, bone: { id: 'bone', label: 'DAGGERHEART.Domains.bone.label', - src: 'icons/skills/wounds/bone-broken-marrow-red.webp', + src: 'systems/daggerheart/assets/icons/domains/bone.svg', description: 'DAGGERHEART.Domains.Bone' }, codex: { id: 'codex', label: 'DAGGERHEART.Domains.codex.label', - src: 'icons/sundries/books/book-embossed-jewel-gold-purple.webp', + src: 'systems/daggerheart/assets/icons/domains/codex.svg', description: 'DAGGERHEART.Domains.Codex' }, grace: { id: 'grace', label: 'DAGGERHEART.Domains.grace.label', - src: 'icons/skills/movement/feet-winged-boots-glowing-yellow.webp', + src: 'systems/daggerheart/assets/icons/domains/grace.svg', description: 'DAGGERHEART.Domains.Grace' }, midnight: { id: 'midnight', label: 'DAGGERHEART.Domains.midnight.label', - src: 'icons/environment/settlement/watchtower-castle-night.webp', - background: 'systems/daggerheart/assets/backgrounds/MidnightBackground.webp', + src: 'systems/daggerheart/assets/icons/domains/midnight.svg', description: 'DAGGERHEART.Domains.Midnight' }, sage: { id: 'sage', label: 'DAGGERHEART.Domains.sage.label', - src: 'icons/sundries/misc/pipe-wooden-straight-brown.webp', + src: 'systems/daggerheart/assets/icons/domains/sage.svg', description: 'DAGGERHEART.Domains.Sage' }, splendor: { id: 'splendor', label: 'DAGGERHEART.Domains.splendor.label', - src: 'icons/magic/control/control-influence-crown-gold.webp', + src: 'systems/daggerheart/assets/icons/domains/splendor.svg', description: 'DAGGERHEART.Domains.Splendor' }, valor: { id: 'valor', label: 'DAGGERHEART.Domains.valor.label', - src: 'icons/magic/control/control-influence-rally-purple.webp', + src: 'systems/daggerheart/assets/icons/domains/valor.svg', description: 'DAGGERHEART.Domains.Valor' } }; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 654df3d8..1df2176d 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -36,7 +36,10 @@ export default class DhCharacter extends BaseDataActor { return { resources: new fields.SchemaField({ - hitPoints: resourceField(6), + hitPoints: new fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }) + }), stress: resourceField(6), hope: resourceField(6), tokens: new fields.ObjectField(), @@ -287,7 +290,7 @@ export default class DhCharacter extends BaseDataActor { this.rules.maxArmorMarked.total = this.rules.maxArmorMarked.value + this.rules.maxArmorMarked.bonus; this.armorScore = this.armor ? this.armor.system.baseScore + (this.bonuses.armorScore ?? 0) : 0; - this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + this.resources.hitPoints.bonus; + this.resources.hitPoints.maxTotal = (this.class.value?.system?.hitPoints ?? 0) + this.resources.hitPoints.bonus; this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; this.proficiency.total = this.proficiency.value + this.proficiency.bonus; diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index cd69648d..46ec6ff8 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -20,8 +20,14 @@ export default class DHClass extends BaseDataItem { ...super.defineSchema(), domains: new fields.ArrayField(new fields.StringField(), { max: 2 }), classItems: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), - - evasion: new fields.NumberField({ initial: 0, integer: true }), + hitPoints: new fields.NumberField({ + required: true, + integer: true, + min: 1, + initial: 5, + label: 'DAGGERHEART.Sheets.Class.HitPoints' + }), + evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.Sheets.Class.Evasion' }), hopeFeatures: new foundry.data.fields.ArrayField(new ActionField()), classFeatures: new foundry.data.fields.ArrayField(new ActionField()), subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 54759542..195b9c27 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -131,14 +131,35 @@ export default class DhpItem extends Item { action = await this.selectActionDialog(); } if (action) response = action.use(event); - // Check Target - // If action.roll => Roll Dialog - // Else If action.cost => Cost Dialog - // Then - // Apply Cost - // Apply Effect } - // Display Item Card in chat return response; } + + async toChat(origin) { + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: + this.type === 'ancestry' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') + : this.type === 'community' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: origin, + img: this.img, + name: this.name, + description: this.system.description, + actions: [] + }; + 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()); + } } diff --git a/styles/chat.less b/styles/chat.less index 9af413a0..15ff39e7 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -306,6 +306,7 @@ fieldset.daggerheart.chat { h2 { width: 100%; text-align: center; + margin: 0; } } diff --git a/styles/daggerheart.css b/styles/daggerheart.css index fb76a078..fca82604 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -1618,6 +1618,7 @@ fieldset.daggerheart.chat { .daggerheart.chat.domain-card .domain-card-title h2 { width: 100%; text-align: center; + margin: 0; } .daggerheart.chat.domain-card .ability-card-footer { display: flex; @@ -3778,13 +3779,35 @@ div.daggerheart.views.multiclass { .theme-light .application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet { background: transparent; } -.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet img { +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait { + position: relative; height: 235px; width: 275px; border-bottom: 1px solid light-dark(#18162e, #f3c267); cursor: pointer; +} +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait img { + height: 235px; + width: 275px; object-fit: cover; } +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait .death-roll-btn { + display: none; +} +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait.death-roll { + filter: grayscale(1); +} +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait.death-roll .death-roll-btn { + display: flex; + position: absolute; + top: 30%; + right: 30%; + font-size: 6rem; + color: #efe6d8; +} +.application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .portrait.death-roll .death-roll-btn:hover { + text-shadow: 0 0 8px #efe6d8; +} .application.sheet.daggerheart.actor.dh-style.character .character-sidebar-sheet .info-section { position: relative; display: flex; diff --git a/styles/less/actors/character/sidebar.less b/styles/less/actors/character/sidebar.less index 4f8a1f7e..bde3996c 100644 --- a/styles/less/actors/character/sidebar.less +++ b/styles/less/actors/character/sidebar.less @@ -12,12 +12,39 @@ background: transparent; } - img { + .portrait { + position: relative; height: 235px; width: 275px; border-bottom: 1px solid light-dark(@dark-blue, @golden); cursor: pointer; - object-fit: cover; + + img { + height: 235px; + width: 275px; + object-fit: cover; + } + + .death-roll-btn { + display: none; + } + + &.death-roll { + filter: grayscale(1); + + .death-roll-btn { + display: flex; + position: absolute; + top: 30%; + right: 30%; + font-size: 6rem; + color: @beige; + + &:hover { + text-shadow: 0 0 8px @beige; + } + } + } } .info-section { diff --git a/templates/chat/ability-use.hbs b/templates/chat/ability-use.hbs index 51d56ace..4ebaa293 100644 --- a/templates/chat/ability-use.hbs +++ b/templates/chat/ability-use.hbs @@ -1,6 +1,5 @@
-
{{title}}

{{name}}

diff --git a/templates/sheets/actors/character/loadout.hbs b/templates/sheets/actors/character/loadout.hbs index a03f393c..375581f3 100644 --- a/templates/sheets/actors/character/loadout.hbs +++ b/templates/sheets/actors/character/loadout.hbs @@ -23,6 +23,6 @@
{{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(localize 'DAGGERHEART.Sheets.PC.Tabs.Loadout') type='domainCard' isGlassy=true cardView='list'}} - {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(localize 'DAGGERHEART.Sheets.PC.Tabs.Vault') type='domainCard' isGlassy=true cardView='list'}} + {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(localize 'DAGGERHEART.Sheets.PC.Tabs.Vault') type='domainCard' isVault=true isGlassy=true cardView='list'}}
\ No newline at end of file diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 8e4891dc..4060caeb 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -1,5 +1,9 @@