diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index e8c1a16..ea90482 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -29,10 +29,6 @@ jobs: if: ${{ steps.check-tag.outputs.exists == 'true' }} run: exit 1 - - name: Ensure there are specific files to release - if: ${{ vars.files_to_release == '' }} - run: exit 1 - # Compile the stuff that needs to be compiled - run: npm run build - run: node scripts/buildCompendia.mjs @@ -43,10 +39,10 @@ jobs: - name: Update the download property in the manifest id: manifest-update - run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/${{ vars.zip_name }}.zip"' > system.json + run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"' > system.json - name: Create the zip - run: zip -r ${{ vars.zip_name || 'release' }}.zip ${{ vars.files_to_release }} + run: zip -r release.zip langs module styles templates system.json README.md - name: Create the draft release uses: ncipollo/release-action@v1 diff --git a/.gitignore b/.gitignore index 1726f87..b22745b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.styles \ No newline at end of file +deprecated diff --git a/.promo/hjonk-samples.png b/.promo/hjonk-samples.png new file mode 100644 index 0000000..7151a06 Binary files /dev/null and b/.promo/hjonk-samples.png differ diff --git a/README.md b/README.md index f530123..8d4cb08 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,3 @@ This is an intentionally bare-bones system that features a text-only character sheet, allowing the playing of games that may not otherwise have a Foundry system implementation. - -## Features -There are not too many features included in this system for things like automation -as it's meant to be used to mostly play rules-light games. However there are some -special features that can be enabled on a per-world basis using a world script -to enable feature flags that you want. - -### List of Feature Flags -| Flag | Description -| - | - -| `ROLL_MODE_CONTENT` | Allows players, GMs, and macros to send "blind" chat messages where only the GM gets to see the content. -| `STORABLE_SHEET_SIZE` | Makes it so that certain sheets are able to have their size saved, so that it resumes that size when opened. - -### Example Feature Flag -In order to change these flags, you must make a world script that has something -like the below code in it: - -```js -// v- this is the name of the flag from the table above -taf.FEATURES.STORABLE_SHEET_SIZE = true; -``` \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 544b98e..454af9e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,10 +20,6 @@ export default [ Handlebars: `readonly`, Hooks: `readonly`, ui: `readonly`, - Actor: `readonly`, - Actors: `readonly`, - Item: `readonly`, - Items: `readonly`, ActorSheet: `readonly`, ItemSheet: `readonly`, foundry: `readonly`, @@ -31,7 +27,8 @@ export default [ ActiveEffect: `readonly`, Dialog: `readonly`, renderTemplate: `readonly`, - TextEditor: `readonly`, + fromUuid: `readonly`, + fromUuidSync: `readonly`, }, }, }, diff --git a/langs/en-ca.json b/langs/en-ca.json new file mode 100644 index 0000000..99de0b3 --- /dev/null +++ b/langs/en-ca.json @@ -0,0 +1,18 @@ +{ + "TYPES": { + "Actor": { + "player": "Player" + } + }, + "taf": { + "settings": { + "canPlayersManageAttributes": { + "name": "Players Can Manage Attributes", + "hint": "This allows players who have edit access to a document to be able to edit what attributes those characters have via the attribute editor" + } + }, + "sheet-names": { + "PlayerSheet": "Player Sheet" + } + } +} diff --git a/module/apps/AttributeManager.mjs b/module/apps/AttributeManager.mjs new file mode 100644 index 0000000..105b5bf --- /dev/null +++ b/module/apps/AttributeManager.mjs @@ -0,0 +1,171 @@ +import { __ID__, filePath } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { toID } from "../utils/toID.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { deepClone, diffObject, randomID, setProperty } = foundry.utils; + +export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) { + + // #region Options + static DEFAULT_OPTIONS = { + tag: `form`, + classes: [ + __ID__, + `AttributeManager`, + ], + position: { + width: 400, + height: 350, + }, + window: { + resizable: true, + }, + form: { + submitOnChange: false, + closeOnSubmit: true, + handler: this.#onSubmit, + }, + actions: { + addNew: this.#addNew, + removeAttribute: this.#remove, + }, + }; + + static PARTS = { + attributes: { + template: filePath(`templates/AttributeManager/attribute-list.hbs`), + }, + controls: { + template: filePath(`templates/AttributeManager/controls.hbs`), + }, + }; + // #endregion Options + + // #region Instance Data + /** @type {string | null} */ + #doc = null; + + #attributes; + + constructor({ document , ...options } = {}) { + super(options); + this.#doc = document; + this.#attributes = deepClone(document.system.attr); + }; + + get title() { + return `Attributes: ${this.#doc.name}`; + }; + // #endregion Instance Data + + // #region Lifecycle + async _onRender(context, options) { + await super._onRender(context, options); + + const elements = this.element + .querySelectorAll(`[data-bind]`); + for (const input of elements) { + input.addEventListener(`change`, this.#bindListener.bind(this)); + }; + }; + // #endregion Lifecycle + + // #region Data Prep + async _preparePartContext(partId) { + const ctx = {}; + + ctx.actor = this.#doc; + + switch (partId) { + case `attributes`: { + await this._prepareAttributeContext(ctx); + }; + }; + + return ctx; + }; + + async _prepareAttributeContext(ctx) { + const attrs = []; + for (const [id, data] of Object.entries(this.#attributes)) { + if (data == null) { continue }; + attrs.push({ + id, + name: data.name, + isRange: data.isRange, + isNew: data.isNew ?? false, + }); + }; + ctx.attrs = attrs; + }; + // #endregion Data Prep + + // #region Actions + /** + * @param {Event} event + */ + async #bindListener(event) { + const target = event.target; + const data = target.dataset; + const binding = data.bind; + + let value = target.value; + switch (target.type) { + case `checkbox`: { + value = target.checked; + }; + }; + + Logger.debug(`Updating ${binding} value to ${value}`); + setProperty(this.#attributes, binding, value); + await this.render(); + }; + + /** @this {AttributeManager} */ + static async #addNew() { + const id = randomID(); + this.#attributes[id] = { + name: ``, + isRange: false, + isNew: true, + }; + await this.render({ parts: [ `attributes` ]}); + }; + + /** @this {AttributeManager} */ + static async #remove($e, element) { + const attribute = element.closest(`[data-attribute]`)?.dataset.attribute; + if (!attribute) { return }; + delete this.#attributes[attribute]; + this.#attributes[`-=${attribute}`] = null; + await this.render({ parts: [ `attributes` ] }); + }; + + /** @this {AttributeManager} */ + static async #onSubmit() { + const entries = Object.entries(this.#attributes) + .map(([id, attr]) => { + if (attr == null) { + return [ id, attr ]; + }; + + if (attr.isNew) { + delete attr.isNew; + return [ toID(attr.name), attr ]; + }; + + return [ id, attr ]; + }); + const data = Object.fromEntries(entries); + + const diff = diffObject( + this.#doc.system.attr, + data, + { inner: false, deletionKeys: true }, + ); + + await this.#doc.update({ "system.attr": diff }); + }; + // #endregion Actions +}; diff --git a/module/apps/PlayerSheet.mjs b/module/apps/PlayerSheet.mjs new file mode 100644 index 0000000..dfcef25 --- /dev/null +++ b/module/apps/PlayerSheet.mjs @@ -0,0 +1,111 @@ +import { __ID__, filePath } from "../consts.mjs"; +import { AttributeManager } from "./AttributeManager.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ActorSheetV2 } = foundry.applications.sheets; + +export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) { + + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + __ID__, + `PlayerSheet`, + ], + position: { + width: 575, + height: 740, + }, + window: { + resizable: true, + }, + form: { + submitOnChange: true, + closeOnSubmit: false, + }, + actions: { + manageAttributes: this.#manageAttributes, + }, + }; + + static PARTS = { + header: { template: filePath(`templates/PlayerSheet/header.hbs`) }, + attributes: { template: filePath(`templates/PlayerSheet/attributes.hbs`) }, + content: { template: filePath(`templates/PlayerSheet/content.hbs`) }, + }; + // #endregion Options + + // #region Lifecycle + _getHeaderControls() { + const controls = super._getHeaderControls(); + + controls.push({ + icon: `fa-solid fa-at`, + label: `Manage Attributes`, + action: `manageAttributes`, + visible: () => { + const isGM = game.user.isGM; + const allowPlayerEdits = game.settings.get(__ID__, `canPlayersManageAttributes`); + const editable = this.isEditable; + return isGM || (allowPlayerEdits && editable); + }, + }); + + return controls; + }; + // #endregion Lifecycle + + // #region Data Prep + async _preparePartContext(partID) { + let ctx = { + actor: this.actor, + system: this.actor.system, + editable: this.isEditable, + }; + + switch (partID) { + case `attributes`: { + await this._prepareAttributes(ctx); + break; + }; + case `content`: { + await this._prepareContent(ctx); + break; + }; + }; + + return ctx; + }; + + async _prepareAttributes(ctx) { + ctx.hasAttributes = this.actor.system.hasAttributes; + + const attrs = []; + for (const [id, data] of Object.entries(this.actor.system.attr)) { + attrs.push({ + ...data, + id, + path: `system.attr.${id}`, + }); + }; + ctx.attrs = attrs.toSorted((a, b) => a.name.localeCompare(b.name)); + }; + + async _prepareContent(ctx) { + const TextEditor = foundry.applications.ux.TextEditor.implementation; + ctx.enriched = { + system: { + content: await TextEditor.enrichHTML(this.actor.system.content), + }, + }; + }; + // #endregion Data Prep + + // #region Actions + /** @this {PlayerSheet} */ + static async #manageAttributes() { + const app = new AttributeManager({ document: this.actor }); + await app.render({ force: true }); + }; + // #endregion Actions +}; diff --git a/module/consts.mjs b/module/consts.mjs new file mode 100644 index 0000000..0d33f25 --- /dev/null +++ b/module/consts.mjs @@ -0,0 +1,9 @@ +export const __ID__ = `taf`; + +// MARK: filePath +export function filePath(path) { + if (path.startsWith(`/`)) { + path = path.slice(1); + }; + return `systems/${__ID__}/${path}`; +}; diff --git a/module/data/Player.mjs b/module/data/Player.mjs new file mode 100644 index 0000000..9b449a1 --- /dev/null +++ b/module/data/Player.mjs @@ -0,0 +1,29 @@ +export class PlayerData extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + return { + content: new fields.HTMLField({ + blank: true, + trim: true, + initial: ``, + }), + attr: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ blank: false, trim: true }), + value: new fields.NumberField({ min: 0, initial: 0, integer: true, nullable: false }), + max: new fields.NumberField({ min: 0, initial: null, integer: true, nullable: true }), + isRange: new fields.BooleanField({ initial: false, nullable: false }), + }), + { + initial: {}, + nullable: false, + required: true, + }, + ), + }; + }; + + get hasAttributes() { + return Object.keys(this.attr).length > 0; + }; +}; diff --git a/module/documents/Actor.mjs b/module/documents/Actor.mjs new file mode 100644 index 0000000..292cdf3 --- /dev/null +++ b/module/documents/Actor.mjs @@ -0,0 +1,28 @@ +import { Logger } from "../utils/Logger.mjs"; + +const { Actor } = foundry.documents; + +export class TAFActor extends Actor { + async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) { + Logger.table({ attribute, value, isDelta, isBar }); + const attr = foundry.utils.getProperty(this.system, attribute); + const current = isBar ? attr.value : attr; + const update = isDelta ? current + value : value; + if ( update === current ) { + return this; + }; + + // Determine the updates to make to the actor data + let updates; + if (isBar) { + updates = {[`system.${attribute}.value`]: Math.clamp(update, 0, attr.max)}; + } else { + updates = {[`system.${attribute}`]: update}; + }; + + // Allow a hook to override these changes + const allowed = Hooks.call(`modifyTokenAttribute`, {attribute, value, isDelta, isBar}, updates, this); + + return allowed !== false ? this.update(updates) : this; + } +}; diff --git a/module/documents/Token.mjs b/module/documents/Token.mjs new file mode 100644 index 0000000..b6baf2d --- /dev/null +++ b/module/documents/Token.mjs @@ -0,0 +1,111 @@ +import { Logger } from "../utils/Logger.mjs"; + +const { TokenDocument } = foundry.documents; +const { getProperty, getType, hasProperty, isSubclass } = foundry.utils; + +export class TAFTokenDocument extends TokenDocument { + + /** + * @override + * This override's purpose is to make it so that Token attributes and bars can + * be accessed from the data model's values directly instead of relying on only + * the schema, which doesn't account for my TypedObjectField of attributes. + */ + static getTrackedAttributes(data, _path = []) { + + // Case 1 - Infer attributes from schema structure. + if ( (data instanceof foundry.abstract.DataModel) || isSubclass(data, foundry.abstract.DataModel) ) { + return this._getTrackedAttributesFromObject(data, _path); + } + if ( data instanceof foundry.data.fields.SchemaField ) { + return this._getTrackedAttributesFromSchema(data, _path); + } + + // Case 2 - Infer attributes from object structure. + if ( [`Object`, `Array`].includes(getType(data)) ) { + return this._getTrackedAttributesFromObject(data, _path); + } + + // Case 3 - Retrieve explicitly configured attributes. + if ( !data || (typeof data === `string`) ) { + const config = this._getConfiguredTrackedAttributes(data); + if ( config ) { + return config; + } + data = undefined; + } + + // Track the path and record found attributes + if ( data !== undefined ) { + return {bar: [], value: []}; + } + + // Case 4 - Infer attributes from system template. + const bar = new Set(); + const value = new Set(); + for ( const [type, model] of Object.entries(game.model.Actor) ) { + const dataModel = CONFIG.Actor.dataModels?.[type]; + const inner = this.getTrackedAttributes(dataModel ?? model, _path); + inner.bar.forEach(attr => bar.add(attr.join(`.`))); + inner.value.forEach(attr => value.add(attr.join(`.`))); + } + + return { + bar: Array.from(bar).map(attr => attr.split(`.`)), + value: Array.from(value).map(attr => attr.split(`.`)), + }; + }; + + /** + * @override + */ + getBarAttribute(barName, {alternative} = {}) { + const attribute = alternative || this[barName]?.attribute; + Logger.log(barName, attribute); + if (!attribute || !this.actor) { + return null; + }; + const system = this.actor.system; + + // Get the current attribute value + const data = getProperty(system, attribute); + if (data == null) { + return null; + }; + + if (Number.isNumeric(data)) { + let editable = hasProperty(system, attribute); + return { + type: `value`, + attribute, + value: Number(data), + editable, + }; + }; + + if (`value` in data && `max` in data) { + let editable = hasProperty(system, `${attribute}.value`); + const isRange = getProperty(system, `${attribute}.isRange`); + if (isRange) { + return { + type: `bar`, + attribute, + value: parseInt(data.value || 0), + max: parseInt(data.max || 0), + editable, + }; + } else { + return { + type: `value`, + attribute: `${attribute}.value`, + value: Number(data.value), + editable, + }; + }; + }; + + // Otherwise null + return null; + }; + +}; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs new file mode 100644 index 0000000..442d916 --- /dev/null +++ b/module/hooks/init.mjs @@ -0,0 +1,36 @@ +// Apps +import { PlayerSheet } from "../apps/PlayerSheet.mjs"; + +// Data Models +import { PlayerData } from "../data/Player.mjs"; + +// Documents +import { TAFActor } from "../documents/Actor.mjs"; +import { TAFTokenDocument } from "../documents/Token.mjs"; + +// Settings +import { registerWorldSettings } from "../settings/world.mjs"; + +// Utils +import { __ID__ } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; + +Hooks.on(`init`, () => { + Logger.debug(`Initializing`); + + CONFIG.Token.documentClass = TAFTokenDocument; + CONFIG.Actor.documentClass = TAFActor; + + CONFIG.Actor.dataModels.player = PlayerData; + + foundry.documents.collections.Actors.registerSheet( + __ID__, + PlayerSheet, + { + makeDefault: true, + label: `taf.sheet-names.PlayerSheet`, + }, + ); + + registerWorldSettings(); +}); diff --git a/module/main.mjs b/module/main.mjs new file mode 100644 index 0000000..8f3b5cc --- /dev/null +++ b/module/main.mjs @@ -0,0 +1 @@ +import "./hooks/init.mjs"; diff --git a/module/settings/world.mjs b/module/settings/world.mjs new file mode 100644 index 0000000..78462d7 --- /dev/null +++ b/module/settings/world.mjs @@ -0,0 +1,12 @@ +import { __ID__ } from "../consts.mjs"; + +export function registerWorldSettings() { + game.settings.register(__ID__, `canPlayersManageAttributes`, { + name: `taf.settings.canPlayersManageAttributes.name`, + hint: `taf.settings.canPlayersManageAttributes.hint`, + config: true, + type: Boolean, + default: false, + scope: `world`, + }); +}; diff --git a/src/utils/logger.mjs b/module/utils/Logger.mjs similarity index 71% rename from src/utils/logger.mjs rename to module/utils/Logger.mjs index 5f9b37e..70c6481 100644 --- a/src/utils/logger.mjs +++ b/module/utils/Logger.mjs @@ -12,10 +12,10 @@ const augmentedProps = new Set([ ]); /** @type {Console} */ -globalThis.Logger = new Proxy(console, { +export const Logger = new Proxy(console, { get(target, prop, _receiver) { if (augmentedProps.has(prop)) { - return (...args) => target[prop](game.system.id, `|`, ...args); + return target[prop].bind(target, game.system.id, `|`); }; return target[prop]; }, diff --git a/module/utils/toID.mjs b/module/utils/toID.mjs new file mode 100644 index 0000000..384ed22 --- /dev/null +++ b/module/utils/toID.mjs @@ -0,0 +1,13 @@ +/** + * A helper method that converts an arbitrary string into a format that can be + * used as an object key easily. + * + * @param {string} text The text to convert + * @returns The converted ID + */ +export function toID(text) { + return text + .toLowerCase() + .replace(/\s/g, `_`) + .replace(/\W/g, ``); +}; diff --git a/scripts/macros/rollDice.mjs b/scripts/macros/rollDice.mjs deleted file mode 100644 index b2d99b2..0000000 --- a/scripts/macros/rollDice.mjs +++ /dev/null @@ -1,77 +0,0 @@ -async function rollDice() { - const sidesOnDice = 6; - - const answers = await DialogManager.ask({ - id: `eat-the-reich-dice-pool`, - question: `Set up your dice pool:`, - inputs: [ - { - key: `statBase`, - inputType: `number`, - defaultValue: 2, - label: `Number of Dice`, - autofocus: true, - }, - { - key: `successThreshold`, - inputType: `number`, - defaultValue: 3, - label: `Success Threshold (d${sidesOnDice} > X)`, - }, - { - key: `critsEnabled`, - inputType: `checkbox`, - defaultValue: true, - label: `Enable Criticals`, - }, - ], - }); - const { statBase, successThreshold, critsEnabled } = answers; - let rollMode = game.settings.get(`core`, `rollMode`); - - - - let successes = 0; - let critsOnly = 0; - const results = []; - for (let i = statBase; i > 0; i--) { - let r = new Roll(`1d${sidesOnDice}`); - await r.evaluate(); - let classes = `roll die d6`; - - // Determine the success count and class modifications for the chat - if (r.total > successThreshold) { - successes++; - } - else { - classes += ` failure` - } - if (r.total === sidesOnDice && critsEnabled) { - successes++; - critsOnly++; - classes += ` success`; - } - - results.push(`
  • ${r.total}
  • `); - } - - let content = `Rolls:
      ${results.join(``)}

    Successes: ${successes}
    Crits: ${critsOnly}`; - - - if (rollMode === CONST.DICE_ROLL_MODES.BLIND) { - ui.notifications.warn(`Cannot make a blind roll from the macro, rolling with mode "Private GM Roll" instead`); - rollMode = CONST.DICE_ROLL_MODES.PRIVATE; - } - - const chatData = ChatMessage.applyRollMode( - { - title: `Dice Pool`, - content, - }, - rollMode, - ); - - await ChatMessage.implementation.create(chatData); -} - -rollDice() \ No newline at end of file diff --git a/src/components/_index.mjs b/src/components/_index.mjs deleted file mode 100644 index 44133d9..0000000 --- a/src/components/_index.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { SystemIcon } from "./icon.mjs"; -import { SystemIncrementer } from "./incrementer.mjs"; -import { SystemRange } from "./range.mjs"; - -/** - * A list of element classes to register, expects all of them to have a static - * property of "elementName" that is the namespaced name that the component will - * be registered under. Any elements that are formAssociated have their name added - * to the "CONFIG.CACHE.componentListeners" array and should be listened to for - * "change" events in sheets. - */ -const components = [ - SystemIcon, - SystemIncrementer, - SystemRange, -]; - -export function registerCustomComponents() { - (CONFIG.CACHE ??= {}).componentListeners ??= []; - for (const component of components) { - if (!window.customElements.get(component.elementName)) { - console.debug(`${game.system.id} | Registering component "${component.elementName}"`); - window.customElements.define( - component.elementName, - component, - ); - if (component.formAssociated) { - CONFIG.CACHE.componentListeners.push(component.elementName); - } - }; - }; -}; diff --git a/src/components/icon.mjs b/src/components/icon.mjs deleted file mode 100644 index 430076d..0000000 --- a/src/components/icon.mjs +++ /dev/null @@ -1,125 +0,0 @@ -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 -*/ -export class SystemIcon extends StyledShadowElement(HTMLElement) { - static elementName = `dd-icon`; - static formAssociated = false; - - /* Stuff for the mixin to use */ - static _stylePath = ``; - - - static _cache = new Map(); - #container; - /** @type {null | string} */ - _name; - /** @type {null | string} */ - _path; - - /* Stored IDs for all of the hooks that are in this component */ - #svgHmr; - - constructor() { - super(); - // this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true }); - - this.#container = document.createElement(`div`); - this._shadow.appendChild(this.#container); - }; - - _mounted = false; - async connectedCallback() { - super.connectedCallback(); - if (this._mounted) { return } - - this._name = this.getAttribute(`name`); - this._path = this.getAttribute(`path`); - - /* - 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); - }; - }; - - /* - Try to retrieve the icon if it isn't present, try the path then default to - the slot content, as then we can have a default per-icon usage - */ - let content; - if (this._name) { - content = await this.#getIcon(`./systems/dotdungeon/assets/${this._name}.svg`); - }; - - if (this._path && !content) { - content = await this.#getIcon(this._path); - }; - - if (content) { - this.#container.appendChild(content.cloneNode(true)); - }; - - /* - This is so that when we get an HMR event from Foundry we can appropriately - handle it using our logic to update the component and the icon cache. - */ - if (game.settings.get(game.system.id, `devMode`)) { - this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => { - if (this._name === iconName || this._path?.endsWith(data.path)) { - const svg = this.#parseSVG(data.content); - this.constructor._cache.set(iconName, svg); - this.#container.replaceChildren(svg.cloneNode(true)); - }; - }); - }; - - this._mounted = true; - }; - - disconnectedCallback() { - super.disconnectedCallback(); - if (!this._mounted) { return } - - Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr); - - this._mounted = false; - }; - - async #getIcon(path) { - // Cache hit! - if (this.constructor._cache.has(path)) { - Logger.debug(`Icon ${path} cache hit`); - return this.constructor._cache.get(path); - }; - - const r = await fetch(path); - switch (r.status) { - case 200: - case 201: - break; - default: - Logger.error(`Failed to fetch icon: ${path}`); - return; - }; - - Logger.debug(`Adding icon ${path} to the cache`); - const svg = this.#parseSVG(await r.text()); - this.constructor._cache.set(path, svg); - return svg; - }; - - /** Takes an SVG string and returns it as a DOM node */ - #parseSVG(content) { - const temp = document.createElement(`div`); - temp.innerHTML = content; - return temp.querySelector(`svg`); - }; -}; diff --git a/src/components/incrementer.mjs b/src/components/incrementer.mjs deleted file mode 100644 index cde1cec..0000000 --- a/src/components/incrementer.mjs +++ /dev/null @@ -1,153 +0,0 @@ -import { StyledShadowElement } from "./mixins/Styles.mjs"; -import { SystemIcon } from "./icon.mjs"; - -/** -Attributes: -@property {string} name - The path to the value to update -@property {number} value - The actual value of the input -@property {number} min - The minimum value of the input -@property {number} max - The maximum value of the input -@property {number?} smallStep - The step size used for the buttons and arrow keys -@property {number?} largeStep - The step size used for the buttons + Ctrl and page up / down - -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) -*/ -export class SystemIncrementer extends StyledShadowElement(HTMLElement) { - static elementName = `dd-incrementer`; - static formAssociated = true; - - static _stylePath = `v1/components/incrementer.scss`; - - _internals; - #input; - - _min; - _max; - _smallStep; - _largeStep; - - 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() { - return this.getAttribute(`value`); - }; - set value(value) { - this.setAttribute(`value`, value); - }; - - get type() { - return `number`; - } - - connectedCallback() { - super.connectedCallback(); - this.replaceChildren(); - - // Attribute parsing / registration - const value = this.getAttribute(`value`); - this._min = parseInt(this.getAttribute(`min`) ?? 0); - this._max = parseInt(this.getAttribute(`max`) ?? 0); - this._smallStep = parseInt(this.getAttribute(`smallStep`) ?? 1); - this._largeStep = parseInt(this.getAttribute(`largeStep`) ?? 5); - - this._internals.ariaValueMin = this._min; - this._internals.ariaValueMax = this._max; - - const container = document.createElement(`div`); - - // The input that the user can see / modify - const input = document.createElement(`input`); - this.#input = input; - input.type = `number`; - input.ariaHidden = true; - input.min = this.getAttribute(`min`); - input.max = this.getAttribute(`max`); - input.addEventListener(`change`, this.#updateValue.bind(this)); - input.value = value; - - // plus button - const increment = document.createElement(SystemIcon.elementName); - increment.setAttribute(`name`, `ui/plus`); - increment.setAttribute(`var:size`, `0.75rem`); - increment.setAttribute(`var:fill`, `currentColor`); - increment.ariaHidden = true; - increment.classList.value = `increment`; - increment.addEventListener(`mousedown`, this.#increment.bind(this)); - - // minus button - const decrement = document.createElement(SystemIcon.elementName); - decrement.setAttribute(`name`, `ui/minus`); - decrement.setAttribute(`var:size`, `0.75rem`); - decrement.setAttribute(`var:fill`, `currentColor`); - decrement.ariaHidden = true; - decrement.classList.value = `decrement`; - decrement.addEventListener(`mousedown`, this.#decrement.bind(this)); - - // Construct the DOM - container.appendChild(decrement); - container.appendChild(input); - container.appendChild(increment); - 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() { - let value = parseInt(this.#input.value); - if (this.getAttribute(`min`)) { - value = Math.max(this._min, value); - } - if (this.getAttribute(`max`)) { - value = Math.min(this._max, value); - } - this.#input.value = value; - this.value = value; - this.dispatchEvent(new Event(`change`, { bubbles: true })); - }; - - /** @param {Event} $e */ - #increment($e) { - $e.preventDefault(); - let value = parseInt(this.#input.value); - value += $e.ctrlKey ? this._largeStep : this._smallStep; - this.#input.value = value; - this.#updateValue(); - }; - - /** @param {Event} $e */ - #decrement($e) { - $e.preventDefault(); - let value = parseInt(this.#input.value); - value -= $e.ctrlKey ? this._largeStep : this._smallStep; - this.#input.value = value; - this.#updateValue(); - }; -}; diff --git a/src/components/mixins/Styles.mjs b/src/components/mixins/Styles.mjs deleted file mode 100644 index b76dd29..0000000 --- a/src/components/mixins/Styles.mjs +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @param {HTMLElement} Base - */ -export function StyledShadowElement(Base) { - return class extends Base { - /** - * The path to the CSS that is loaded - * @type {string} - */ - static _stylePath; - - /** - * The stringified CSS to use - * @type {string} - */ - static _styles; - - /** - * The HTML element of the stylesheet - * @type {HTMLStyleElement} - */ - _style; - - /** @type {ShadowRoot} */ - _shadow; - - /** - * The hook ID for this element's CSS hot reload - * @type {number} - */ - #cssHmr; - - constructor() { - super(); - - this._shadow = this.attachShadow({ mode: `open` }); - this._style = document.createElement(`style`); - this._shadow.appendChild(this._style); - }; - - #mounted = false; - connectedCallback() { - if (this.#mounted) { return } - - this._getStyles(); - - if (game.settings.get(`dotdungeon`, `devMode`)) { - this.#cssHmr = Hooks.on(`dd-hmr:css`, (data) => { - if (data.path.endsWith(this.constructor._stylePath)) { - this._style.innerHTML = data.content; - }; - }); - }; - - this.#mounted = true; - }; - - disconnectedCallback() { - if (!this.#mounted) { return } - if (this.#cssHmr != null) { - Hooks.off(`dd-hmr:css`, this.#cssHmr); - this.#cssHmr = null; - }; - this.#mounted = false; - }; - - _getStyles() { - if (this.constructor._styles) { - this._style.innerHTML = this.constructor._styles; - } else { - fetch(`./systems/dotdungeon/.styles/${this.constructor._stylePath}`) - .then(r => r.text()) - .then(t => { - this.constructor._styles = t; - this._style.innerHTML = t; - }); - } - }; - }; -}; diff --git a/src/components/range.mjs b/src/components/range.mjs deleted file mode 100644 index f40b985..0000000 --- a/src/components/range.mjs +++ /dev/null @@ -1,138 +0,0 @@ -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 SystemRange - 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/src/consts.mjs b/src/consts.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/src/documents/ActiveEffect/_proxy.mjs b/src/documents/ActiveEffect/_proxy.mjs deleted file mode 100644 index 6f75118..0000000 --- a/src/documents/ActiveEffect/_proxy.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs"; - -/** - * An object of Foundry-types to in-code Document classes. - */ -const classes = {}; - -/** The class that will be used if no type-specific class is defined */ -const defaultClass = ActiveEffect; - -export const ActiveEffectProxy = createDocumentProxy(defaultClass, classes); diff --git a/src/documents/Actor/Player/Document.mjs b/src/documents/Actor/Player/Document.mjs deleted file mode 100644 index 287e30f..0000000 --- a/src/documents/Actor/Player/Document.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export class Player extends Actor { - getRollData() { - return this.system; - }; -}; diff --git a/src/documents/Actor/Player/Model.mjs b/src/documents/Actor/Player/Model.mjs deleted file mode 100644 index b141450..0000000 --- a/src/documents/Actor/Player/Model.mjs +++ /dev/null @@ -1,12 +0,0 @@ -export class PlayerData extends foundry.abstract.TypeDataModel { - static defineSchema() { - const fields = foundry.data.fields; - return { - content: new fields.HTMLField({ - blank: true, - trim: true, - initial: ``, - }), - }; - }; -}; diff --git a/src/documents/Actor/_proxy.mjs b/src/documents/Actor/_proxy.mjs deleted file mode 100644 index 6e7eb18..0000000 --- a/src/documents/Actor/_proxy.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs"; - -/** - * An object of Foundry-types to in-code Document classes. - */ -const classes = {}; - -/** The class that will be used if no type-specific class is defined */ -const defaultClass = Actor; - -export const ActorProxy = createDocumentProxy(defaultClass, classes); diff --git a/src/documents/ChatMessage/_proxy.mjs b/src/documents/ChatMessage/_proxy.mjs deleted file mode 100644 index 44b44c7..0000000 --- a/src/documents/ChatMessage/_proxy.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs"; - -/** - * An object of Foundry-types to in-code Document classes. - */ -const classes = {}; - -/** The class that will be used if no type-specific class is defined */ -const defaultClass = ChatMessage; - -export const ChatMessageProxy = createDocumentProxy(defaultClass, classes); diff --git a/src/documents/Item/_proxy.mjs b/src/documents/Item/_proxy.mjs deleted file mode 100644 index 8f01bc6..0000000 --- a/src/documents/Item/_proxy.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs"; - -/** - * An object of Foundry-types to in-code Document classes. - */ -const classes = {}; - -/** The class that will be used if no type-specific class is defined */ -const defaultClass = Item; - -export const ItemProxy = createDocumentProxy(defaultClass, classes); diff --git a/src/helpers/_index.mjs b/src/helpers/_index.mjs deleted file mode 100644 index 8d5ed41..0000000 --- a/src/helpers/_index.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { handlebarsLocalizer, localizer } from "../utils/localizer.mjs"; -import { options } from "./options.mjs"; - -export function registerHandlebarsHelpers() { - const helperPrefix = game.system.id; - - return { - // MARK: Complex helpers - [`${helperPrefix}-i18n`]: handlebarsLocalizer, - [`${helperPrefix}-options`]: options, - - // MARK: Simple helpers - [`${helperPrefix}-stringify`]: v => JSON.stringify(v, null, ` `), - [`${helperPrefix}-empty`]: v => v.length == 0, - [`${helperPrefix}-set-has`]: (s, k) => s.has(k), - [`${helperPrefix}-empty-state`]: (v) => v ?? localizer(`${game.system.id}.common.empty`), - }; -}; diff --git a/src/helpers/options.mjs b/src/helpers/options.mjs deleted file mode 100644 index b0df8d0..0000000 --- a/src/helpers/options.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { localizer } from "../utils/localizer.mjs"; - -/** - * @typedef {object} Option - * @property {string} [label] - * @property {string|number} value - * @property {boolean} [disabled] - */ - -/** - * @param {string | number} selected - * @param {Array`, - ); - }; - return htmlOptions.join(`\n`); -}; diff --git a/src/hooks/hotReload.mjs b/src/hooks/hotReload.mjs deleted file mode 100644 index 0320dd8..0000000 --- a/src/hooks/hotReload.mjs +++ /dev/null @@ -1,18 +0,0 @@ -const loaders = { - svg(data) { - const iconName = data.path.split(`/`).slice(-1)[0].slice(0, -4); - Logger.debug(`hot-reloading icon: ${iconName}`); - Hooks.call(`${game.system.id}-hmr:svg`, iconName, data); - }, - js() {window.location.reload()}, - mjs() {window.location.reload()}, - css(data) { - Logger.debug(`Hot-reloading CSS: ${data.path}`); - Hooks.call(`${game.system.id}-hmr:css`, data); - }, -}; - -Hooks.on(`hotReload`, async (data) => { - if (!loaders[data.extension]) {return} - return loaders[data.extension](data); -}); diff --git a/src/hooks/renderChatMessage.mjs b/src/hooks/renderChatMessage.mjs deleted file mode 100644 index 496a135..0000000 --- a/src/hooks/renderChatMessage.mjs +++ /dev/null @@ -1,18 +0,0 @@ -Hooks.on(`renderChatMessage`, (msg, html) => { - - // Short-Circuit when the flag isn't set for the message - if (msg.getFlag(`taf`, `rollModedContent`)) { - return; - } - - const featureFlagEnabled = taf.FEATURES.ROLL_MODE_CONTENT; - - const contentElement = html.find(`.message-content`)[0]; - let content = contentElement.innerHTML; - if (featureFlagEnabled && msg.blind && !game.user.isGM) { - content = content.replace(/-=.*?=-/gm, `??`); - } else { - content = content.replace(/-=|=-/gm, ``); - } - contentElement.innerHTML = content; -}); diff --git a/src/main.mjs b/src/main.mjs deleted file mode 100644 index 0314ffe..0000000 --- a/src/main.mjs +++ /dev/null @@ -1,64 +0,0 @@ -// Document Imports -import { ActiveEffectProxy } from "./documents/ActiveEffect/_proxy.mjs"; -import { ActorProxy } from "./documents/Actor/_proxy.mjs"; -import { ChatMessageProxy } from "./documents/ChatMessage/_proxy.mjs"; -import { ItemProxy } from "./documents/Item/_proxy.mjs"; - -// DataModel Imports -import { PlayerData } from "./documents/Actor/Player/Model.mjs"; - -// Hook Imports -import "./hooks/renderChatMessage.mjs"; -import "./hooks/hotReload.mjs"; - -// Misc Imports -import "./utils/globalTaf.mjs"; -import "./utils/logger.mjs"; -import "./utils/DialogManager.mjs"; -import { registerCustomComponents } from "./components/_index.mjs"; -import { registerHandlebarsHelpers } from "./helpers/_index.mjs"; -import { registerSettings } from "./settings/_index.mjs"; -import { registerSheets } from "./sheets/_index.mjs"; - -// MARK: init hook -Hooks.once(`init`, () => { - Logger.info(`Initializing`); - CONFIG.ActiveEffect.legacyTransferral = false; - - registerSettings(); - - // Data Models - CONFIG.Actor.dataModels.player = PlayerData; - - // Update document classes - CONFIG.Actor.documentClass = ActorProxy; - CONFIG.Item.documentClass = ItemProxy; - CONFIG.ActiveEffect.documentClass = ActiveEffectProxy; - CONFIG.ChatMessage.documentClass = ChatMessageProxy; - registerSheets(); - - registerHandlebarsHelpers(); - - registerCustomComponents(); -}); - - -// MARK: ready hook -Hooks.once(`ready`, () => { - Logger.info(`Ready`); - - let defaultTab = game.settings.get(game.system.id, `defaultTab`); - if (defaultTab) { - if (!ui.sidebar?.tabs?.[defaultTab]) { - Logger.error(`Couldn't find a sidebar tab with ID:`, defaultTab); - } else { - Logger.debug(`Switching sidebar tab to:`, defaultTab); - ui.sidebar.tabs[defaultTab].activate(); - }; - }; - - if (game.settings.get(game.system.id, `devMode`)) { - console.log(`%cFeature Flags:`, `color: #00aa00; font-style: bold; font-size: 1.5rem;`); - Logger.table(taf.FEATURES); - }; -}); diff --git a/src/settings/_index.mjs b/src/settings/_index.mjs deleted file mode 100644 index 0f65987..0000000 --- a/src/settings/_index.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { registerClientSettings } from "./client_settings.mjs"; -import { registerDevSettings } from "./dev_settings.mjs"; -import { registerWorldSettings } from "./world_settings.mjs"; - -export function registerSettings() { - Logger.debug(`Registering settings`); - registerClientSettings(); - registerWorldSettings(); - registerDevSettings(); -}; diff --git a/src/settings/client_settings.mjs b/src/settings/client_settings.mjs deleted file mode 100644 index 7dd2708..0000000 --- a/src/settings/client_settings.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export function registerClientSettings() { -}; diff --git a/src/settings/dev_settings.mjs b/src/settings/dev_settings.mjs deleted file mode 100644 index 80235b1..0000000 --- a/src/settings/dev_settings.mjs +++ /dev/null @@ -1,25 +0,0 @@ -export function registerDevSettings() { - const isLocalhost = window.location.hostname === `localhost`; - - game.settings.register(game.system.id, `devMode`, { - name: `Dev Mode?`, - scope: `client`, - type: Boolean, - config: isLocalhost, - default: false, - requiresReload: false, - }); - - game.settings.register(game.system.id, `defaultTab`, { - name: `Default Sidebar Tab`, - scope: `client`, - type: String, - config: isLocalhost, - requiresReload: false, - onChange(value) { - if (!ui.sidebar.tabs[value]) { - ui.notifications.warn(`"${value}" cannot be found in the sidebar tabs, it may not work at reload.`); - } - }, - }); -}; diff --git a/src/settings/world_settings.mjs b/src/settings/world_settings.mjs deleted file mode 100644 index eb3aad7..0000000 --- a/src/settings/world_settings.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export function registerWorldSettings() { -}; diff --git a/src/sheets/Player/v1.mjs b/src/sheets/Player/v1.mjs deleted file mode 100644 index 8e775f1..0000000 --- a/src/sheets/Player/v1.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { SizeStorable } from "../mixins/SizeStorable.mjs"; - -export class PlayerSheetv1 extends SizeStorable(ActorSheet) { - static get defaultOptions() { - let opts = foundry.utils.mergeObject( - super.defaultOptions, - { - template: `systems/${game.system.id}/templates/Player/v1/main.hbs`, - classes: [], - }, - ); - opts.classes = [`actor--player`, `style-v1`]; - return opts; - }; - - async getData() { - const ctx = {}; - - ctx.editable = this.isEditable; - - const actor = ctx.actor = this.actor; - ctx.system = actor.system; - ctx.enriched = { system: {} }; - ctx.enriched.system.content = await TextEditor.enrichHTML(actor.system.content); - - return ctx; - }; -} diff --git a/src/sheets/_index.mjs b/src/sheets/_index.mjs deleted file mode 100644 index 073a018..0000000 --- a/src/sheets/_index.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { PlayerSheetv1 } from "./Player/v1.mjs"; - -export function registerSheets() { - Logger.debug(`Registering sheets`); - - Actors.registerSheet(game.system.id, PlayerSheetv1, { - makeDefault: true, - types: [`player`], - label: `Hello`, - }); -}; diff --git a/src/sheets/mixins/SizeStorable.mjs b/src/sheets/mixins/SizeStorable.mjs deleted file mode 100644 index 38ba887..0000000 --- a/src/sheets/mixins/SizeStorable.mjs +++ /dev/null @@ -1,125 +0,0 @@ -import { DialogManager } from "../../utils/DialogManager.mjs"; - -/** - * This mixin allows making a class so that it can store the width/height data - * to the sheet or localhost in order to make using the text sheets a lil nicer. - * - * @param {ActorSheet|ItemSheet} cls The Sheet class to augment - * @returns The augmented class - */ -export function SizeStorable(cls) { - - // Don't augment class when the feature isn't enabled - if (!taf.FEATURES.STORABLE_SHEET_SIZE) { - return cls; - } - - return class SizeStorableClass extends cls { - constructor(doc, opts) { - - /* - Find the saved size of the sheet, it takes the following order of precedence - from highest to lowest: - - Locally saved - - Default values on actor - - Default values from constructor - */ - /** @type {string|undefined} */ - let size = localStorage.getItem(`${game.system.id}.size:${doc.uuid}`); - size ??= doc.getFlag(game.system.id, `size`); - - // Apply the saved value to the options - if (size) { - const [ width, height ] = size.split(`,`); - opts.width = width; - opts.height = height; - }; - - super(doc, opts); - }; - - get hasLocalSize() { - return localStorage.getItem(`${game.system.id}.size:${this.object.uuid}`) != null; - }; - - get hasGlobalSize() { - return this.object.getFlag(game.system.id, `size`) != null; - }; - - _getHeaderButtons() { - return [ - { - class: `size-save`, - icon: `fa-solid fa-floppy-disk`, - label: `Save Size`, - onclick: () => { - - const buttons = { - saveGlobal: { - label: `Save Global Size`, - callback: () => { - this.object.setFlag( - game.system.id, - `size`, - `${this.position.width},${this.position.height}`, - ); - }, - }, - saveLocal: { - label: `Save For Me Only`, - callback: () => { - localStorage.setItem( - `${game.system.id}.size:${this.object.uuid}`, - `${this.position.width},${this.position.height}`, - ); - }, - }, - }; - - // Add resets if there is a size already - if (this.hasGlobalSize) { - buttons.resetGlobal = { - label: `Reset Global Size`, - callback: () => { - this.object.unsetFlag(game.system.id, `size`); - }, - }; - }; - - if (this.hasLocalSize) { - buttons.resetLocal = { - label: `Reset Size For Me Only`, - callback: () => { - localStorage.removeItem(`${game.system.id}.size:${this.object.uuid}`); - }, - }; - }; - - // When a non-GM is using this system, we only want to save local sizes - if (!game.user.isGM) { - delete buttons.saveGlobal; - delete buttons.resetGlobal; - }; - - DialogManager.createOrFocus( - `${this.object.uuid}:size-save`, - { - title: `Save size of sheet: ${this.title}`, - content: `Saving the size of this sheet will cause it to open at the size it is when you press the save button`, - buttons, - render: (html) => { - const el = html[2]; - el.style = `display: grid; grid-template-columns: 1fr 1fr; gap: 8px;`; - }, - }, - { - jQuery: true, - }, - ); - }, - }, - ...super._getHeaderButtons(), - ]; - }; - }; -}; diff --git a/src/utils/DialogManager.mjs b/src/utils/DialogManager.mjs deleted file mode 100644 index 6413abf..0000000 --- a/src/utils/DialogManager.mjs +++ /dev/null @@ -1,178 +0,0 @@ -import { localizer } from "./localizer.mjs"; - -/** - * A utility class that allows managing Dialogs that are created for various - * purposes such as deleting items, help popups, etc. This is a singleton class - * that upon instantiating after the first time will just return the first instance - */ -export class DialogManager { - - /** @type {Map} */ - static #dialogs = new Map(); - - /** - * Focuses a dialog if it already exists, or creates a new one and renders it. - * - * @param {string} dialogId The ID to associate with the dialog, should be unique - * @param {object} data The data to pass to the Dialog constructor - * @param {DialogOptions} opts The options to pass to the Dialog constructor - * @returns {Dialog} The Dialog instance - */ - static async createOrFocus(dialogId, data, opts = {}) { - if (DialogManager.#dialogs.has(dialogId)) { - const dialog = DialogManager.#dialogs.get(dialogId); - dialog.bringToTop(); - return dialog; - }; - - /* - This makes sure that if I provide a close function as a part of the data, - that the dialog still gets removed from the set once it's closed, otherwise - it could lead to dangling references that I don't care to keep. Or if I don't - provide the close function, it just sets the function as there isn't anything - extra that's needed to be called. - */ - if (data?.close) { - const provided = data.close; - data.close = () => { - DialogManager.#dialogs.delete(dialogId); - provided(); - }; - } else { - data.close = () => DialogManager.#dialogs.delete(dialogId); - }; - - // Create the Dialog with the modified data - const dialog = new Dialog(data, opts); - DialogManager.#dialogs.set(dialogId, dialog); - dialog.render(true); - return dialog; - }; - - /** - * Closes a dialog if it is rendered - * - * @param {string} dialogId The ID of the dialog to close - */ - static async close(dialogId) { - const dialog = DialogManager.#dialogs.get(dialogId); - dialog?.close(); - }; - - static async helpDialog( - helpId, - helpContent, - helpTitle = `dotdungeon.common.help`, - localizationData = {}, - ) { - DialogManager.createOrFocus( - helpId, - { - title: localizer(helpTitle, localizationData), - content: localizer(helpContent, localizationData), - buttons: {}, - }, - { resizable: true }, - ); - }; - - /** - * Asks the user to provide a simple piece of information, this is primarily - * intended to be used within macros so that it can have better info gathering - * as needed. This returns an object of input keys/labels to the value the user - * input for that label, if there is only one input, this will return the value - * without an object wrapper, allowing for easier access. - */ - static async ask(data, opts = {}) { - if (!data.id) { - throw new Error(`Asking the user for input must contain an ID`); - } - if (!data.inputs.length) { - throw new Error(`Must include at least one input specification when prompting the user`); - } - - let autofocusClaimed = false; - for (const i of data.inputs) { - i.id ??= foundry.utils.randomID(16); - i.inputType ??= `text`; - - // Only ever allow one input to claim autofocus - i.autofocus &&= !autofocusClaimed; - autofocusClaimed ||= i.autofocus; - - // Set the value's attribute name if it isn't specified explicitly - if (!i.valueAttribute) { - switch (i.inputType) { - case `checkbox`: - i.valueAttribute = `checked`; - break; - default: - i.valueAttribute = `value`; - }; - }; - }; - - opts.jQuery = true; - data.default ??= `confirm`; - data.title ??= `System Question`; - - data.content = await renderTemplate( - `systems/${game.system.id}/templates/Dialogs/ask.hbs`, - data, - ); - - return new Promise((resolve, reject) => { - DialogManager.createOrFocus( - data.id, - { - ...data, - buttons: { - confirm: { - label: `Confirm`, - callback: (html) => { - const answers = {}; - - /* - Retrieve the answer for every input provided using the ID - determined during initial data prep, and assign the value - to the property of the label in the object. - */ - for (const i of data.inputs) { - const element = html.find(`#${i.id}`)[0]; - let value = element.value; - switch (i.inputType) { - case `number`: - value = parseFloat(value); - break; - case `checkbox`: - value = element.checked; - break; - } - Logger.debug(`Ask response: ${value} (type: ${typeof value})`); - answers[i.key ?? i.label] = value; - if (data.inputs.length === 1) { - resolve(value); - return; - } - } - - resolve(answers); - }, - }, - cancel: { - label: `Cancel`, - callback: () => reject(`User cancelled the prompt`), - }, - }, - }, - opts, - ); - }); - }; - - static get size() { - return DialogManager.#dialogs.size; - } -}; - -globalThis.DialogManager = DialogManager; diff --git a/src/utils/createDocumentProxy.mjs b/src/utils/createDocumentProxy.mjs deleted file mode 100644 index 08af7e6..0000000 --- a/src/utils/createDocumentProxy.mjs +++ /dev/null @@ -1,39 +0,0 @@ -export function createDocumentProxy(defaultClass, classes) { - // eslint-disable-next-line func-names - return new Proxy(function () {}, { - construct(_target, args) { - const [data] = args; - - if (!classes[data.type]) { - return new defaultClass(...args); - } - - return new classes[data.type](...args); - }, - get(_target, prop, _receiver) { - - if ([`create`, `createDocuments`].includes(prop)) { - return (data, options) => { - if (data.constructor === Array) { - return data.map(i => this.constructor.create(i, options)); - } - - if (!classes[data.type]) { - return defaultClass.create(data, options); - } - - return classes[data.type].create(data, options); - }; - }; - - if (prop == Symbol.hasInstance) { - return (instance) => { - if (instance instanceof defaultClass) {return true} - return Object.values(classes).some(i => instance instanceof i); - }; - }; - - return defaultClass[prop]; - }, - }); -}; diff --git a/src/utils/feature_flags/rollModeMessageContent.mjs b/src/utils/feature_flags/rollModeMessageContent.mjs deleted file mode 100644 index 67ed613..0000000 --- a/src/utils/feature_flags/rollModeMessageContent.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export function hideMessageText(content) { - const hideContent = taf.FEATURES.ROLL_MODE_CONTENT; - if (hideContent) { - return `-=${content}=-`; - } - return content; -}; diff --git a/src/utils/globalTaf.mjs b/src/utils/globalTaf.mjs deleted file mode 100644 index 899328e..0000000 --- a/src/utils/globalTaf.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { hideMessageText } from "./feature_flags/rollModeMessageContent.mjs"; - -Object.defineProperty( - globalThis, - `taf`, - { - value: Object.freeze({ - utils: Object.freeze({ - hideMessageText, - }), - FEATURES: Object.preventExtensions({ - ROLL_MODE_CONTENT: false, - STORABLE_SHEET_SIZE: false, - }), - }), - writable: false, - }, -); diff --git a/src/utils/localizer.mjs b/src/utils/localizer.mjs deleted file mode 100644 index 916e54d..0000000 --- a/src/utils/localizer.mjs +++ /dev/null @@ -1,45 +0,0 @@ -/** A handlebars helper that utilizes the recursive localizer */ -export function handlebarsLocalizer(key, ...args) { - let data = args[0]; - if (args.length === 1) { data = args[0].hash } - if (key instanceof Handlebars.SafeString) {key = key.toString()} - const localized = localizer(key, data); - return localized; -}; - -/** - * A localizer that allows recursively localizing strings so that localized strings - * that want to use other localized strings can. - * - * @param {string} key The localization key to retrieve - * @param {object?} args The arguments provided to the localizer for replacement - * @param {number?} depth The current depth of the localizer - * @returns The localized string - */ -export function localizer(key, args = {}, depth = 0) { - /** @type {string} */ - let localized = game.i18n.format(key, args); - const subkeys = localized.matchAll(/@(?[a-zA-Z.]+)/gm); - - // Short-cut to help prevent infinite recursion - if (depth > 10) { - return localized; - }; - - /* - Helps prevent localization on the same key so that we aren't doing excess work. - */ - const localizedSubkeys = new Map(); - for (const match of subkeys) { - const subkey = match.groups.key; - if (localizedSubkeys.has(subkey)) {continue} - localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1)); - }; - - return localized.replace( - /@(?[a-zA-Z.]+)/gm, - (_fullMatch, subkey) => { - return localizedSubkeys.get(subkey); - }, - ); -}; diff --git a/styles/Apps/AttributeManager.css b/styles/Apps/AttributeManager.css new file mode 100644 index 0000000..a36d35c --- /dev/null +++ b/styles/Apps/AttributeManager.css @@ -0,0 +1,33 @@ +.taf.AttributeManager { + .attributes { + display: flex; + flex-direction: column; + gap: 8px; + } + + .attribute { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 8px; + padding: 8px; + border: 1px solid rebeccapurple; + border-radius: 4px; + + label { + display: flex; + flex-direction: row; + align-items: center; + } + } + + .controls { + display: flex; + flex-direction: row; + gap: 8px; + + button { + flex-grow: 1; + } + } +} diff --git a/styles/Apps/PlayerSheet.css b/styles/Apps/PlayerSheet.css new file mode 100644 index 0000000..cb81dbd --- /dev/null +++ b/styles/Apps/PlayerSheet.css @@ -0,0 +1,57 @@ +.taf.PlayerSheet { + .sheet-header, fieldset, .content { + border-radius: 8px; + border: 1px solid rebeccapurple; + } + + .sheet-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + padding: 4px; + + img { + border-radius: 4px; + } + } + + .attributes { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; + gap: 0.5rem; + } + + .attr-range { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + width: 100px; + + > input { + text-align: center; + } + } + + .content { + flex-grow: 1; + overflow: hidden; + --table-row-color-odd: var(--table-header-bg-color); + + &:not(:has(> prose-mirror)) { + padding: 0.5rem; + } + } + + prose-mirror { + height: 100%; + + menu { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } +} diff --git a/styles/Apps/common.css b/styles/Apps/common.css new file mode 100644 index 0000000..ce56fdd --- /dev/null +++ b/styles/Apps/common.css @@ -0,0 +1,8 @@ +.taf { + > .window-content { + padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/styles/elements/headers.css b/styles/elements/headers.css new file mode 100644 index 0000000..2f59e8c --- /dev/null +++ b/styles/elements/headers.css @@ -0,0 +1,5 @@ +.taf > .window-content { + h1, h2, h3, h4, h5, h6 { + margin: 0; + } +} diff --git a/styles/elements/input.css b/styles/elements/input.css new file mode 100644 index 0000000..9b72811 --- /dev/null +++ b/styles/elements/input.css @@ -0,0 +1,6 @@ +.taf > .window-content input { + &.large { + --input-height: 2.5rem; + font-size: 1.75rem; + } +} diff --git a/styles/elements/p.css b/styles/elements/p.css new file mode 100644 index 0000000..160b0d0 --- /dev/null +++ b/styles/elements/p.css @@ -0,0 +1,9 @@ +.taf > .window-content p { + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } +} diff --git a/styles/elements/prose-mirror.css b/styles/elements/prose-mirror.css new file mode 100644 index 0000000..8b6189d --- /dev/null +++ b/styles/elements/prose-mirror.css @@ -0,0 +1,12 @@ +.taf > .window-content prose-mirror { + background: var(--prosemirror-background); + + .editor-content { + padding: 0 8px 8px; + } + + .tableWrapper th, + .tableWrapper td { + border-color: rebeccapurple; + } +} diff --git a/styles/main.css b/styles/main.css new file mode 100644 index 0000000..a07aa4f --- /dev/null +++ b/styles/main.css @@ -0,0 +1,16 @@ +@layer resets, themes, elements, components, partials, apps, exceptions; + +/* Themes */ +@import url("./themes/dark.css") layer(themes); +@import url("./themes/light.css") layer(themes); + +/* Elements */ +@import url("./elements/headers.css") layer(elements); +@import url("./elements/input.css") layer(elements); +@import url("./elements/p.css") layer(elements); +@import url("./elements/prose-mirror.css") layer(elements); + +/* Apps */ +@import url("./Apps/common.css") layer(apps); +@import url("./Apps/PlayerSheet.css") layer(apps); +@import url("./Apps/AttributeManager.css") layer(apps); diff --git a/styles/resets/inputs.css b/styles/resets/inputs.css new file mode 100644 index 0000000..aa6e6b7 --- /dev/null +++ b/styles/resets/inputs.css @@ -0,0 +1,5 @@ +.taf > .window-content { + button, input { + all: initial; + } +} diff --git a/styles/root.scss b/styles/root.scss deleted file mode 100644 index 423849d..0000000 --- a/styles/root.scss +++ /dev/null @@ -1 +0,0 @@ -@use "./v1/index.scss"; \ No newline at end of file diff --git a/styles/themes/dark.css b/styles/themes/dark.css new file mode 100644 index 0000000..85103f7 --- /dev/null +++ b/styles/themes/dark.css @@ -0,0 +1,3 @@ +.theme-dark { + --prosemirror-background: var(--color-cool-5); +} diff --git a/styles/themes/light.css b/styles/themes/light.css new file mode 100644 index 0000000..fee3812 --- /dev/null +++ b/styles/themes/light.css @@ -0,0 +1,3 @@ +.theme-light { + --prosemirror-background: white; +} diff --git a/styles/v1/Dialog.scss b/styles/v1/Dialog.scss deleted file mode 100644 index 28f133f..0000000 --- a/styles/v1/Dialog.scss +++ /dev/null @@ -1,12 +0,0 @@ -.dialog-content:not(:only-child) { - margin-bottom: 8px; -} - -.dialog-content { - p { - margin: 0; - } - .prompt { - margin-top: 8px; - } -} diff --git a/styles/v1/components/common.scss b/styles/v1/components/common.scss deleted file mode 100644 index 59f812d..0000000 --- a/styles/v1/components/common.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Disclaimer: This CSS is used by a custom web component and is scoped to JUST -// the corresponding web component. This should only be imported by web component -// style files. - -:host { - display: inline-block; -} diff --git a/styles/v1/components/icon.scss b/styles/v1/components/icon.scss deleted file mode 100644 index 59a68a9..0000000 --- a/styles/v1/components/icon.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Disclaimer: This CSS is used by a custom web component and is scoped to JUST -the corresponding web component. Importing this into other files is forbidden -*/ - -$default-size: 1rem; - -@use "./common.scss"; - -div { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; -} - -svg { - width: var(--size, $default-size); - height: var(--size, $default-size); - fill: var(--fill); - stroke: var(--stroke); -} \ No newline at end of file diff --git a/styles/v1/components/incrementer.scss b/styles/v1/components/incrementer.scss deleted file mode 100644 index fee073a..0000000 --- a/styles/v1/components/incrementer.scss +++ /dev/null @@ -1,53 +0,0 @@ -/* -Disclaimer: This CSS is used by a custom web component and is scoped to JUST -the corresponding web component. Importing this into other files is forbidden -*/ - -$default-border-radius: 4px; -$default-height: 1.5rem; - -@use "./common.scss"; - -div { - display: grid; - grid-template-columns: var(--height, $default-height) var(--width, 50px) var(--height, $default-height); - grid-template-rows: var(--height, 1fr); - border-radius: var(--border-radius, $default-border-radius); -} - -span, input { - border: none; - outline: none; - background: none; - color: inherit; -} - -input { - font-family: var(--font-family, inherit); - text-align: center; - font-size: var(--font-size, inherit); - padding: 2px 4px; - - &::-webkit-inner-spin-button, &::-webkit-outer-spin-button { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - margin: 0 - } -} - -.increment, .decrement { - aspect-ratio: 1 / 1; - padding: 0; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -} - -.increment { - border-radius: 0 var(--border-radius, $default-border-radius) var(--border-radius, 4px) 0; -} -.decrement { - border-radius: var(--border-radius, $default-border-radius) 0 0 var(--border-radius, $default-border-radius); -} diff --git a/styles/v1/index.scss b/styles/v1/index.scss deleted file mode 100644 index c4a7503..0000000 --- a/styles/v1/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Styling version 1 - -@use "./Dialog.scss"; - -@use "./player/root.scss"; diff --git a/styles/v1/player/root.scss b/styles/v1/player/root.scss deleted file mode 100644 index d2df5d9..0000000 --- a/styles/v1/player/root.scss +++ /dev/null @@ -1,42 +0,0 @@ -.actor--player.style-v1 { - --header-size: 75px; - - form { - display: flex; - flex-direction: column; - gap: 8px; - } - - .header-row { - display: flex; - flex-direction: row; - border-radius: 4px; - border: 1px solid var(--color-underline-header); - } - - .avatar { - --size: var(--header-size); - width: var(--size); - height: var(--size); - border: none; - border-right: 1px solid var(--color-underline-header); - } - - .actor-name { - height: var(--header-size); - padding: 8px 1rem; - font-size: clamp(1rem, 2rem, calc(var(--header-size) - 16px)); - border: none; - } - - prose-mirror { - --menu-background: rgba(0, 0, 0, 0.1); - flex-grow: 1; - border: 1px solid var(--color-underline-header); - border-radius: 4px; - - .editor-container { - height: auto; - } - } -} \ No newline at end of file diff --git a/system.json b/system.json index d1561a6..024c8a6 100644 --- a/system.json +++ b/system.json @@ -1,15 +1,15 @@ { "id": "taf", "title": "Text-Based Actors", - "description": "", + "description": "An intentionally minimalist system that enables you to play rules-light games without any hassle!", "version": "2.0.0", - "download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/dotdungeon.zip", + "download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/release.zip", "manifest": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/system.json", "url": "https://github.com/Oliver-Akins/Text-Actors-Foundry", "compatibility": { - "minimum": 12, - "verified": 12, - "maximum": 12 + "minimum": 13, + "verified": 13, + "maximum": 13 }, "authors": [ { @@ -18,12 +18,21 @@ } ], "esmodules": [ - "src/main.mjs" + "./module/main.mjs" ], "styles": [ - ".styles/root.css" + { + "src": "./styles/main.css", + "layer": "system" + } + ], + "languages": [ + { + "lang": "en", + "name": "English (Canadian)", + "path": "langs/en-ca.json" + } ], - "packs": [], "documentTypes": { "Actor": { "player": { diff --git a/taf.lock b/taf.lock new file mode 100644 index 0000000..82ef623 --- /dev/null +++ b/taf.lock @@ -0,0 +1 @@ +🔒 \ No newline at end of file diff --git a/templates/AttributeManager/attribute-list.hbs b/templates/AttributeManager/attribute-list.hbs new file mode 100644 index 0000000..f15d9a9 --- /dev/null +++ b/templates/AttributeManager/attribute-list.hbs @@ -0,0 +1,35 @@ +
    + {{#each attrs as |attr|}} +
    + {{#if attr.isNew}} + + {{else}} + {{ attr.name }} + {{/if}} + + +
    + {{else}} +

    No attributes yet

    + {{/each}} +
    diff --git a/templates/AttributeManager/controls.hbs b/templates/AttributeManager/controls.hbs new file mode 100644 index 0000000..59dbb9b --- /dev/null +++ b/templates/AttributeManager/controls.hbs @@ -0,0 +1,13 @@ +
    + + +
    diff --git a/templates/Dialogs/ask.hbs b/templates/Dialogs/ask.hbs deleted file mode 100644 index a37f56e..0000000 --- a/templates/Dialogs/ask.hbs +++ /dev/null @@ -1,27 +0,0 @@ -
    -

    - {{ question }} -

    - {{#each inputs as | i | }} -
    - - - {{#if i.details}} -

    - {{{ i.details }}} -

    - {{/if}} -
    - {{/each}} -
    diff --git a/templates/Player/v1/main.hbs b/templates/Player/v1/main.hbs deleted file mode 100644 index 64e24d8..0000000 --- a/templates/Player/v1/main.hbs +++ /dev/null @@ -1,29 +0,0 @@ -
    -
    - - -
    - {{#if editable}} - - {{{enriched.system.content}}} - - {{else}} - {{{enriched.system.content}}} - {{/if}} -
    diff --git a/templates/PlayerSheet/attributes.hbs b/templates/PlayerSheet/attributes.hbs new file mode 100644 index 0000000..80d6085 --- /dev/null +++ b/templates/PlayerSheet/attributes.hbs @@ -0,0 +1,32 @@ +{{#if hasAttributes}} +
    + {{#each attrs as | attr |}} +
    + + {{ attr.name }} + +
    + + {{#if attr.isRange}} + + + {{/if}} +
    +
    + {{/each}} +
    +{{else}} +