diff --git a/langs/en-ca.2.json b/langs/en-ca.2.json index dce0efa..f8a1ab5 100644 --- a/langs/en-ca.2.json +++ b/langs/en-ca.2.json @@ -101,6 +101,32 @@ "title": "Delete Effect", "content": "
Are you sure you would like to delete the active effect: {name}
" } + }, + "settings": { + "showAvatarOnSheet": { + "name": "Show Avatar On Player Sheet", + "description": "Determines whether or not to show the avatar to you on the Player Character sheets, turning this off will replace the image with a file picker so that you can still change the image from the character sheet." + }, + "playersCanChangeGroup": { + "name": "Allow Players to Change Group", + "description": "Setting this to true allows non-GM players to modify the group that the Actor belongs to. While this is disabled the GM will still be able to modify each player's group by editing the character sheet." + }, + "resourcesOrSupplies": { + "name": "Use Resources or Supplies", + "description": "Determines which term to use for the objects that allow travelling into the next hex tile. This is because of the", + "option": { + "supplies": "Supplies", + "resources": "Resources" + } + }, + "openEmbeddedOnCreate": { + "name": "Edit Custom Items Immediately When Created", + "description": "Tells the character sheets that have \"Add\" buttons to open the Item's sheet when you create the new item so that you can immediately edit it without needing to click more buttons." + }, + "aspectLimit": { + "name": "Character Aspect Limit", + "description": "Limit how many Aspects a single character can have." + } } }, "TYPES": { diff --git a/module/components/icon.mjs b/module/components/icon.mjs index 8c70d40..82c8e99 100644 --- a/module/components/icon.mjs +++ b/module/components/icon.mjs @@ -4,6 +4,8 @@ import { StyledShadowElement } from "./mixins/Styles.mjs"; Attributes: @property {string} name - The name of the icon, takes precedence over the path @property {string} path - The path of the icon file + +@extends {HTMLElement} */ export class DotDungeonIcon extends StyledShadowElement(HTMLElement) { static elementName = `dd-icon`; diff --git a/module/components/incrementer.mjs b/module/components/incrementer.mjs index 68e426a..0c15387 100644 --- a/module/components/incrementer.mjs +++ b/module/components/incrementer.mjs @@ -13,8 +13,14 @@ Attributes: Styling: - `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem) - `--width`: Controls the width of the number input (default 50px) + +@extends {HTMLElement} */ -export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) { +export class DotDungeonIncrementer +extends StyledShadowElement( + HTMLElement, + { mode: `open`, delegatesFocus: true } +) { static elementName = `dd-incrementer`; static formAssociated = true; @@ -38,14 +44,14 @@ export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) { get form() { return this._internals.form; - } + }; get name() { return this.getAttribute(`name`); - } + }; set name(value) { this.setAttribute(`name`, value); - } + }; get value() { return this.getAttribute(`value`); @@ -56,7 +62,7 @@ export class DotDungeonIncrementer extends StyledShadowElement(HTMLElement) { get type() { return `number`; - } + }; connectedCallback() { super.connectedCallback(); diff --git a/module/components/index.mjs b/module/components/index.mjs index f4d39e9..7d870b0 100644 --- a/module/components/index.mjs +++ b/module/components/index.mjs @@ -1,9 +1,11 @@ import { DotDungeonIncrementer } from "./incrementer.mjs"; import { DotDungeonIcon } from "./icon.mjs"; +import { DotDungeonRange } from "./range.mjs"; const components = [ DotDungeonIcon, DotDungeonIncrementer, + DotDungeonRange, ]; export function registerCustomComponents() { diff --git a/module/components/mixins/Styles.mjs b/module/components/mixins/Styles.mjs index 33d5eb5..bbceaad 100644 --- a/module/components/mixins/Styles.mjs +++ b/module/components/mixins/Styles.mjs @@ -1,7 +1,7 @@ /** * @param {HTMLElement} Base */ -export function StyledShadowElement(Base) { +export function StyledShadowElement(Base, shadowOptions = { mode: `open` }) { return class extends Base { /** * The path to the CSS that is loaded @@ -33,7 +33,7 @@ export function StyledShadowElement(Base) { constructor() { super(); - this._shadow = this.attachShadow({ mode: `open` }); + this._shadow = this.attachShadow(shadowOptions); this._style = document.createElement(`style`); this._shadow.appendChild(this._style); }; diff --git a/module/components/range.mjs b/module/components/range.mjs new file mode 100644 index 0000000..db286fe --- /dev/null +++ b/module/components/range.mjs @@ -0,0 +1,139 @@ +import { DotDungeonIcon } from "./icon.mjs"; +import { StyledShadowElement } from "./mixins/Styles.mjs"; + +/** +Attributes: +@property {string} name - The path to the value to update in the datamodel +@property {number} value - The actual value of the input +@property {number} max - The maximum value that this range has + +@extends {HTMLElement} +*/ +export class DotDungeonRange +extends StyledShadowElement( + HTMLElement, + { mode: `open`, delegatesFocus: true } +) { + static elementName = `dd-range`; + static formAssociated = true; + + static observedAttributes = [`max`]; + + static _stylePath = `v3/components/range.css`; + + _internals; + #input; + + constructor() { + super(); + + // Form internals + this._internals = this.attachInternals(); + this._internals.role = `spinbutton`; + }; + + get form() { + return this._internals.form; + }; + + get name() { + return this.getAttribute(`name`); + }; + set name(value) { + this.setAttribute(`name`, value); + }; + + get value() { + try { + return parseInt(this.getAttribute(`value`)); + } catch { + throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`); + }; + }; + set value(value) { + this.setAttribute(`value`, value); + }; + + get max() { + try { + return parseInt(this.getAttribute(`max`)); + } catch { + throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`); + }; + }; + set max(value) { + this.setAttribute(`max`, value); + }; + + get type() { + return `number`; + }; + + connectedCallback() { + super.connectedCallback(); + + // Attribute validation + if (!this.hasAttribute(`max`)) { + throw new Error(`dotdungeon | Cannot have a range without a maximum value`); + }; + + // Keyboard accessible input for the thing + this.#input = document.createElement(`input`); + this.#input.type = `number`; + this.#input.min = 0; + this.#input.max = this.max; + this.#input.value = this.value; + this.#input.addEventListener(`change`, () => { + const inputValue = parseInt(this.#input.value); + if (inputValue === this.value) return; + this._updateValue.bind(this)(Math.sign(this.value - inputValue)); + this._updateValue(Math.sign(this.value - inputValue)); + }); + this._shadow.appendChild(this.#input); + + // Shadow-DOM construction + this._elements = new Array(this.max); + const container = document.createElement(`div`); + container.classList.add(`container`); + + // Creating the node for filled content + const filledContainer = document.createElement(`div`); + filledContainer.classList.add(`range-increment`, `filled`); + const filledNode = this.querySelector(`[slot="filled"]`); + if (filledNode) filledContainer.appendChild(filledNode); + + const emptyContainer = document.createElement(`div`); + emptyContainer.classList.add(`range-increment`, `empty`); + const emptyNode = this.querySelector(`[slot="empty"]`); + if (emptyNode) emptyContainer.appendChild(emptyNode); + + this._elements.fill(filledContainer, 0, this.value); + this._elements.fill(emptyContainer, this.value); + container.append(...this._elements.map((slot, i) => { + const node = slot.cloneNode(true); + node.setAttribute(`data-index`, i + 1); + node.addEventListener(`click`, () => { + const filled = node.classList.contains(`filled`); + this._updateValue(filled ? -1 : 1); + }); + return node; + })); + this._shadow.appendChild(container); + + /* + This converts all of the namespace 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); + }; + }; + }; + + _updateValue(delta) { + this.value += delta; + this.dispatchEvent(new Event(`change`, { bubbles: true })); + }; +}; diff --git a/module/config.mjs b/module/config.mjs index 56a0fb4..d1fa118 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -68,6 +68,11 @@ export const itemFilters = [ `service`, ]; +export const invalidActiveEffectTargets = new Set([ + `system.uses_inventory_slot`, + `system.quantity_affects_used_capacity`, +]); + export default { stats, statDice, @@ -86,4 +91,5 @@ export default { syncDice, localizerConfig, itemFilters, + invalidActiveEffectTargets, }; diff --git a/module/documents/ActiveEffect/GenericActiveEffect.mjs b/module/documents/ActiveEffect/GenericActiveEffect.mjs index 8ee70f3..47f10d3 100644 --- a/module/documents/ActiveEffect/GenericActiveEffect.mjs +++ b/module/documents/ActiveEffect/GenericActiveEffect.mjs @@ -1,7 +1,25 @@ +import { invalidActiveEffectTargets } from "../../config.mjs"; + export class DotDungeonActiveEffect extends ActiveEffect { // Invert the logic of the disabled property so it's easier to modify via // embedded controls get enabled() { return !this.disabled }; set enabled(newValue) { this.disabled = !newValue }; + + apply(object, change) { + if (invalidActiveEffectTargets.has(change.key)) return; + + change.value = change.value.replace( + /@(?