diff --git a/langs/en-ca.json b/langs/en-ca.json index d606190..cad8c6a 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -17,6 +17,7 @@ "RipCrypt": { "sheet-names": { "AllItemsSheetV1": "RipCrypt Item Sheet", + "ArmourSheet": "Armour Sheet", "CombinedHeroSheet": "Hero Sheet", "StatsCardV1": "Hero Stat Card", "CraftCardV1": "Hero Craft Card", @@ -157,6 +158,7 @@ } }, "Apps": { + "damage-reduction": "@RipCrypt.common.damage reduction", "traits-range": "@RipCrypt.common.traits & @RipCrypt.common.range", "grit-skills": "@RipCrypt.common.abilities.grit Skills", "gait-skills": "@RipCrypt.common.abilities.gait Skills", @@ -184,7 +186,8 @@ "star-button-tooltip": "Add Star", "unstar-button": "Remove {name} as a starred ammo", "unstar-button-tooltip": "Remove Star" - } + }, + "protects-the-location": "Protects your {part}" }, "notifs": { "error": { diff --git a/module/Apps/ActorSheets/CombinedHeroSheet.mjs b/module/Apps/ActorSheets/CombinedHeroSheet.mjs index 75bde71..3c18491 100644 --- a/module/Apps/ActorSheets/CombinedHeroSheet.mjs +++ b/module/Apps/ActorSheets/CombinedHeroSheet.mjs @@ -106,7 +106,6 @@ export class CombinedHeroSheet extends GenericAppMixin(HandlebarsApplicationMixi }; }; - Logger.debug(`Context keys:`, Object.keys(ctx)); return ctx; }; // #endregion diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 1fe5edf..0be30e4 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -4,7 +4,8 @@ import { RichEditor } from "./RichEditor.mjs"; import { toBoolean } from "../consts.mjs"; /** - * A mixin that takes the class from HandlebarsApplicationMixin and + * A mixin that takes the class from HandlebarsApplicationMixin and combines it + * with utility functions / data that is used across all RipCrypt applications */ export function GenericAppMixin(HandlebarsApp) { class GenericRipCryptApp extends HandlebarsApp { @@ -91,8 +92,9 @@ export function GenericAppMixin(HandlebarsApp) { ctx.meta.idp = this.document?.uuid ?? this.id; if (this.document) { ctx.meta.limited = this.document.limited; - ctx.meta.editable = ctx.editable; - } + ctx.meta.editable = this.isEditable || game.user.isGM; + ctx.meta.embedded = this.document.isEmbedded; + }; delete ctx.editable; return ctx; diff --git a/module/Apps/ItemSheets/ArmourSheet.mjs b/module/Apps/ItemSheets/ArmourSheet.mjs new file mode 100644 index 0000000..9201e16 --- /dev/null +++ b/module/Apps/ItemSheets/ArmourSheet.mjs @@ -0,0 +1,135 @@ +import { filePath } from "../../consts.mjs"; +import { gameTerms } from "../../gameTerms.mjs"; +import { GenericAppMixin } from "../GenericApp.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ItemSheetV2 } = foundry.applications.sheets; +const { getProperty, hasProperty, setProperty } = foundry.utils; + +export class ArmourSheet extends GenericAppMixin(HandlebarsApplicationMixin(ItemSheetV2)) { + + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt--item`, + `ArmourSheet`, + ], + position: { + width: `auto`, + height: `auto`, + }, + window: { + resizable: false, + }, + form: { + submitOnChange: true, + closeOnSubmit: false, + }, + }; + + static PARTS = { + header: { + template: filePath(`templates/Apps/partials/item-header.hbs`), + }, + content: { + template: filePath(`templates/Apps/ArmourSheet/content.hbs`), + }, + }; + // #endregion + + // #region Lifecycle + async _onRender() { + // remove the flag if it exists when we render the sheet + delete this.document?.system?.forceRerender; + }; + + /** + * Used to make it so that items that don't get updated because of the + * _preUpdate hook removing/changing the data submitted, can still get + * re-rendered when the diff is empty. If the document does get updated, + * this rerendering does not happen. + * + * @override + */ + async _processSubmitData(...args) { + await super._processSubmitData(...args); + + if (this.document.system.forceRerender) { + await this.render(false); + }; + }; + + /** + * Customize how form data is extracted into an expanded object. + * @param {SubmitEvent|null} event The originating form submission event + * @param {HTMLFormElement} form The form element that was submitted + * @param {FormDataExtended} formData Processed data for the submitted form + * @returns {object} An expanded object of processed form data + * @throws {Error} Subclasses may throw validation errors here to prevent form submission + * @protected + */ + _processFormData(event, form, formData) { + const data = super._processFormData(event, form, formData); + + if (hasProperty(data, `system.location`)) { + let locations = getProperty(data, `system.location`); + locations = locations.filter(value => value != null); + setProperty(data, `system.location`, locations); + }; + + return data; + }; + // #endregion + + // #region Data Prep + async _preparePartContext(partId, _, opts) { + const ctx = await super._preparePartContext(partId, {}, opts); + + ctx.item = this.document; + ctx.system = this.document.system; + + switch (partId) { + case `content`: { + this._prepareContentContext(ctx, opts); + break; + }; + }; + + return ctx; + }; + + async _prepareContentContext(ctx) { + ctx.weights = [ + { + label: `RipCrypt.common.empty`, + value: null, + }, + ...Object.values(gameTerms.WeightRatings).map(opt => ({ + label: `RipCrypt.common.weightRatings.${opt}`, + value: opt, + })), + ]; + + ctx.accesses = [ + { + label: `RipCrypt.common.empty`, + value: ``, + }, + ...gameTerms.Access.map(opt => ({ + label: `RipCrypt.common.accessLevels.${opt}`, + value: opt, + })), + ]; + + ctx.protects = { + head: this.document.system.location.has(gameTerms.Anatomy.HEAD), + body: this.document.system.location.has(gameTerms.Anatomy.BODY), + arms: this.document.system.location.has(gameTerms.Anatomy.ARMS), + legs: this.document.system.location.has(gameTerms.Anatomy.LEGS), + }; + }; + // #endregion + + // #region Actions + // #endregion +}; diff --git a/module/Apps/components/ArmourSummary.mjs b/module/Apps/components/ArmourSummary.mjs new file mode 100644 index 0000000..15ddf16 --- /dev/null +++ b/module/Apps/components/ArmourSummary.mjs @@ -0,0 +1,56 @@ +import { filePath } from "../../consts.mjs"; +import { StyledShadowElement } from "./mixins/StyledShadowElement.mjs"; + +const { renderTemplate } = foundry.applications.handlebars; + +export class ArmourSummary extends StyledShadowElement(HTMLElement) { + static elementName = `armour-summary`; + static formAssociated = false; + + /* Stuff for the mixin to use */ + static _stylePath = `css/components/armour-summary.css`; + #container; + + get type() { + return this.getAttribute(`type`) ?? `hero`; + }; + + set type(newValue) { + this.setAttribute(`type`, newValue); + }; + + _mounted = false; + async connectedCallback() { + super.connectedCallback(); + if (this._mounted) { return }; + + /* + This converts all of the double-dash prefixed properties on the element to + CSS variables so that they don't all need to be provided by doing style="" + */ + for (const attrVar of this.attributes) { + if (attrVar.name?.startsWith(`var:`)) { + const prop = attrVar.name.replace(`var:`, ``); + this.style.setProperty(`--` + prop, attrVar.value); + }; + }; + + this.#container = document.createElement(`div`); + this.#container.classList = `person`; + + this.#container.innerHTML = await renderTemplate( + filePath(`templates/components/armour-summary.hbs`), + { type: this.type }, + ); + + this._shadow.appendChild(this.#container); + + this._mounted = true; + }; + + disconnectedCallback() { + super.disconnectedCallback(); + if (!this._mounted) { return }; + this._mounted = false; + }; +}; diff --git a/module/Apps/components/_index.mjs b/module/Apps/components/_index.mjs index 3568481..8400462 100644 --- a/module/Apps/components/_index.mjs +++ b/module/Apps/components/_index.mjs @@ -1,9 +1,11 @@ +import { ArmourSummary } from "./ArmourSummary.mjs"; import { Logger } from "../../utils/Logger.mjs"; import { RipCryptBorder } from "./RipCryptBorder.mjs"; import { RipCryptIcon } from "./Icon.mjs"; import { RipCryptSVGLoader } from "./svgLoader.mjs"; const components = [ + ArmourSummary, RipCryptIcon, RipCryptSVGLoader, RipCryptBorder, diff --git a/module/data/Item/Armour.mjs b/module/data/Item/Armour.mjs index 2669b4c..726c929 100644 --- a/module/data/Item/Armour.mjs +++ b/module/data/Item/Armour.mjs @@ -5,7 +5,7 @@ import { Logger } from "../../utils/Logger.mjs"; import { requiredInteger } from "../helpers.mjs"; const { fields } = foundry.data; -const { hasProperty, diffObject, mergeObject } = foundry.utils; +const { getProperty, diffObject, mergeObject } = foundry.utils; /** Used for Armour and Shields */ export class ArmourData extends CommonItemData { @@ -19,16 +19,16 @@ export class ArmourData extends CommonItemData { blank: false, trim: true, nullable: false, + required: true, options: Object.values(gameTerms.Anatomy), }), { nullable: false, - required: true, + initial: [], }, ), equipped: new fields.BooleanField({ initial: false, - required: true, nullable: false, }), weight: new fields.StringField({ @@ -59,7 +59,7 @@ export class ArmourData extends CommonItemData { const diff = diffObject(this.parent._source, changes); let valid = await super._preUpdate(changes, options, user); - if (hasProperty(diff, `system.equipped`) && !this._canEquip()) { + if (getProperty(diff, `system.equipped`) && !this._canEquip()) { ui.notifications.error( localizer( `RipCrypt.notifs.error.cannot-equip`, @@ -80,7 +80,10 @@ export class ArmourData extends CommonItemData { return valid; }; - /** Used to tell the preUpdate logic whether or not to prevent the */ + /** + * Used to tell the preUpdate logic whether or not to prevent the item from + * being equipped or not. + */ _canEquip() { const parent = this.parent; if (!parent.isEmbedded || !(parent.parent instanceof Actor)) { @@ -94,7 +97,7 @@ export class ArmourData extends CommonItemData { }; const slots = parent.parent.system.equippedArmour ?? {}; - Logger.debug(`slots`, slots); + for (const locationTag of this.location) { if (slots[locationTag.toLowerCase()] != null) { Logger.error(`Unable to equip multiple items in the same slot`); @@ -110,90 +113,4 @@ export class ArmourData extends CommonItemData { return [...this.location].join(`, `); }; // #endregion - - // #region Sheet Data - getFormFields(_ctx) { - const fields = [ - { - id: `quantity`, - type: `integer`, - label: `RipCrypt.common.quantity`, - path: `system.quantity`, - value: this.quantity, - min: 0, - }, - { - id: `access`, - type: `dropdown`, - label: `RipCrypt.common.access`, - path: `system.access`, - value: this.access, - limited: false, - options: [ - { - label: `RipCrypt.common.empty`, - value: ``, - }, - ...gameTerms.Access.map(opt => ({ - label: `RipCrypt.common.accessLevels.${opt}`, - value: opt, - })), - ], - }, - { - id: `cost`, - type: `cost`, - label: `RipCrypt.common.cost`, - gold: this.cost.gold, - silver: this.cost.silver, - copper: this.cost.copper, - }, - { - id: `weight`, - type: `dropdown`, - label: `RipCrypt.common.weightRating`, - path: `system.weight`, - value: this.weight, - options: [ - { - label: `RipCrypt.common.empty`, - value: null, - }, - ...Object.values(gameTerms.WeightRatings).map(opt => ({ - label: `RipCrypt.common.weightRatings.${opt}`, - value: opt, - })), - ], - }, - { - id: `location`, - type: `string-set`, - label: `RipCrypt.common.location`, - placeholder: `RipCrypt.Apps.location-placeholder`, - path: `system.location`, - value: this.locationString, - }, - { - id: `protection`, - type: `integer`, - label: `RipCrypt.common.protection`, - value: this.protection, - path: `system.protection`, - min: 0, - }, - ]; - - if (this.parent.isEmbedded) { - fields.push({ - id: `equipped`, - type: `boolean`, - label: `RipCrypt.common.equipped`, - value: this.equipped, - path: `system.equipped`, - }); - }; - - return fields; - }; - // #endregion }; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 0bbabd1..548e8e2 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,5 +1,6 @@ // Applications import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; +import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs"; import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; @@ -37,7 +38,6 @@ import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; const { Items, Actors } = foundry.documents.collections; -const { ItemSheet, ActorSheet } = foundry.appv1.sheets; Hooks.once(`init`, () => { Logger.log(`Initializing`); @@ -74,10 +74,6 @@ Hooks.once(`init`, () => { // #endregion // #region Sheets - // Unregister core sheets - Items.unregisterSheet(`core`, ItemSheet); - Actors.unregisterSheet(`core`, ActorSheet); - // #region Actors Actors.registerSheet(game.system.id, CombinedHeroSheet, { makeDefault: true, @@ -114,6 +110,16 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.AllItemsSheetV1`, themes: AllItemSheetV1.themes, }); + + Items.registerSheet(game.system.id, ArmourSheet, { + makeDefault: true, + types: [`armour`, `shield`], + label: `RipCrypt.sheet-names.ArmourSheet`, + themes: ArmourSheet.themes, + }); + Items.unregisterSheet(game.system.id, AllItemSheetV1, { + types: [`armour`, `shield`], + }); // #endregion // #endregion diff --git a/templates/Apps/AllItemSheetV1/content.hbs b/templates/Apps/AllItemSheetV1/content.hbs index 0fb9599..523c483 100644 --- a/templates/Apps/AllItemSheetV1/content.hbs +++ b/templates/Apps/AllItemSheetV1/content.hbs @@ -1,6 +1,6 @@