From 8c5d7b64697076d10fe5336d3d0cecad611a34cb Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Feb 2025 18:36:26 -0700 Subject: [PATCH 001/146] Add autocomplete for the rc-border element --- .vscode/ripcrypt.html-data.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.vscode/ripcrypt.html-data.json b/.vscode/ripcrypt.html-data.json index 628a94a..3efb728 100644 --- a/.vscode/ripcrypt.html-data.json +++ b/.vscode/ripcrypt.html-data.json @@ -26,6 +26,20 @@ { "name": "var:stroke-width", "description": "The stroke width of the icon, must be a valid CSS unit" }, { "name": "var:stroke-linejoin", "description": "The stroke linejoin of the icon, must be a valid CSS value" } ] + }, + { + "name": "rc-border", + "description": "Creates a stylized border in the same sort of design that the published RipCrypt book uses", + "attributes": [ + { "name": "var:vertical-displacement", "description": "How much vertical displacement the title receives, defaults to 12.5px" }, + { "name": "var:padding", "description": "How much padding the border container has" }, + { "name": "var:border-color", "description": "The CSS value that is used as the colour of the border" }, + { "name": "var:padding-top", "description": "How much padding the top of the border element has, if not provided, defaults to the value of vertical displacement plus 4px" }, + { "name": "var:margin-top", "description": "How much margin the top of the border element has, if not provided, defaults to the value of vertical displacement" }, + { "name": "var:border-mask", "description": "The CSS colour used to mask out the border element, if not provided defaults to the --base-background CSS variable"}, + { "name": "var:title-height", "description": "The CSS height for the title, defaults to 20px" }, + { "name": "var:title-background", "description": "The CSS colour to make the title element, defaults to var:border-color" } + ] } ] } \ No newline at end of file From de6ded9a3956ec3f34cabf6065ded2c2ff26e0ce Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Feb 2025 18:38:07 -0700 Subject: [PATCH 002/146] Allow currency to be tracked on tokens if for some reason you want that --- module/data/Actor/Hero.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/module/data/Actor/Hero.mjs b/module/data/Actor/Hero.mjs index ab5d3d5..89dc9a1 100644 --- a/module/data/Actor/Hero.mjs +++ b/module/data/Actor/Hero.mjs @@ -19,6 +19,9 @@ export class HeroData extends foundry.abstract.TypeDataModel { `level.glory`, `level.step`, `level.rank`, + `coin.gold`, + `coin.silver`, + `coin.copper`, ], }; }; From 14b6f8513727b587f80287ce5b2381e6dd2eb905 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Feb 2025 18:38:57 -0700 Subject: [PATCH 003/146] Make a helper function for token bars that only store the current value in the DB --- module/data/Actor/Hero.mjs | 10 ++-------- module/data/helpers.mjs | 11 +++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/module/data/Actor/Hero.mjs b/module/data/Actor/Hero.mjs index 89dc9a1..2f0dc15 100644 --- a/module/data/Actor/Hero.mjs +++ b/module/data/Actor/Hero.mjs @@ -1,3 +1,4 @@ +import { derivedMaximumBar } from "../helpers.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { sumReduce } from "../../utils/sumReduce.mjs"; @@ -59,14 +60,7 @@ export class HeroData extends foundry.abstract.TypeDataModel { nullable: false, }), }), - guts: new fields.SchemaField({ - value: new fields.NumberField({ - min: 0, - initial: 5, - integer: true, - nullable: false, - }), - }), + guts: derivedMaximumBar(0, 5), coin: new fields.SchemaField({ gold: new fields.NumberField({ initial: 5, diff --git a/module/data/helpers.mjs b/module/data/helpers.mjs index 2321503..e7c705f 100644 --- a/module/data/helpers.mjs +++ b/module/data/helpers.mjs @@ -19,6 +19,17 @@ export function barAttribute(min, initial, max = undefined) { }); }; +export function derivedMaximumBar(min, initial) { + return new fields.SchemaField({ + value: new fields.NumberField({ + min, + initial, + integer: true, + nullable: false, + }), + }); +}; + export function optionalInteger({min, initial = null, max} = {}) { return new fields.NumberField({ min, From dc5bf7aa0742205fbac62fc7c7673389cb0534e9 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Feb 2025 19:21:09 -0700 Subject: [PATCH 004/146] Remove the v12 compatibility layer that isn't needed --- module/settings/userSettings.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/module/settings/userSettings.mjs b/module/settings/userSettings.mjs index 6dca4c4..0470ec3 100644 --- a/module/settings/userSettings.mjs +++ b/module/settings/userSettings.mjs @@ -1,10 +1,8 @@ export function registerUserSettings() { - const userScope = game.release.generation >= 13 ? `user` : `client`; - game.settings.register(`ripcrypt`, `abbrAccess`, { name: `RipCrypt.setting.abbrAccess.name`, hint: `RipCrypt.setting.abbrAccess.hint`, - scope: userScope, + scope: `user`, type: Boolean, config: true, default: false, @@ -14,7 +12,7 @@ export function registerUserSettings() { game.settings.register(`ripcrypt`, `condensedRange`, { name: `RipCrypt.setting.condensedRange.name`, hint: `RipCrypt.setting.condensedRange.hint`, - scope: userScope, + scope: `user`, type: Boolean, config: true, default: true, From 00228d3aae8afb8ee709d5f8155476ac80d545f5 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Feb 2025 19:21:35 -0700 Subject: [PATCH 005/146] Begin adding cost to items --- langs/en-ca.json | 1 + module/data/Item/Ammo.mjs | 8 +++++ module/data/Item/Common.mjs | 7 +++- module/handlebarHelpers/inputs/currency.mjs | 33 +++++++++++++++++++ module/handlebarHelpers/inputs/formFields.mjs | 2 ++ module/handlebarHelpers/inputs/groupInput.mjs | 2 +- templates/Apps/AllItemSheetV1/style.css | 5 +++ 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 module/handlebarHelpers/inputs/currency.mjs diff --git a/langs/en-ca.json b/langs/en-ca.json index ac5a6bb..a91a488 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -51,6 +51,7 @@ "fract": "Fract", "focus": "Focus" }, + "cost": "Cost", "currency": { "gold": "Gold", "silver": "Silver", diff --git a/module/data/Item/Ammo.mjs b/module/data/Item/Ammo.mjs index a3aa54b..566afc7 100644 --- a/module/data/Item/Ammo.mjs +++ b/module/data/Item/Ammo.mjs @@ -26,6 +26,14 @@ export class AmmoData extends CommonItemData { value: this.quantity, min: 0, }, + { + id: `cost`, + type: `cost`, + label: `RipCrypt.common.cost`, + gold: this.cost.gold, + silver: this.cost.silver, + copper: this.cost.copper, + }, { id: `access`, type: `dropdown`, diff --git a/module/data/Item/Common.mjs b/module/data/Item/Common.mjs index fe60d96..bf37c0e 100644 --- a/module/data/Item/Common.mjs +++ b/module/data/Item/Common.mjs @@ -1,5 +1,5 @@ +import { optionalInteger, requiredInteger } from "../helpers.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { requiredInteger } from "../helpers.mjs"; const { fields } = foundry.data; @@ -14,6 +14,11 @@ export class CommonItemData extends foundry.abstract.TypeDataModel { trim: true, choices: gameTerms.Access, }), + cost: new fields.SchemaField({ + gold: optionalInteger(), + silver: optionalInteger(), + copper: optionalInteger(), + }), }; }; diff --git a/module/handlebarHelpers/inputs/currency.mjs b/module/handlebarHelpers/inputs/currency.mjs new file mode 100644 index 0000000..12b6ca8 --- /dev/null +++ b/module/handlebarHelpers/inputs/currency.mjs @@ -0,0 +1,33 @@ +import { groupInput } from "./groupInput.mjs"; + +export function costInput(input, data) { + return groupInput({ + title: input.label, + fields: [ + { + id: input.id + `-gold`, + type: `integer`, + label: `RipCrypt.common.currency.gold`, + value: input.gold, + path: `system.cost.gold`, + limited: input.limited, + }, + { + id: input.id + `-silver`, + type: `integer`, + label: `RipCrypt.common.currency.silver`, + value: input.silver, + path: `system.cost.silver`, + limited: input.limited, + }, + { + id: input.id + `-copper`, + type: `integer`, + label: `RipCrypt.common.currency.copper`, + value: input.copper, + path: `system.cost.copper`, + limited: input.limited, + }, + ], + }, data); +}; diff --git a/module/handlebarHelpers/inputs/formFields.mjs b/module/handlebarHelpers/inputs/formFields.mjs index 518b054..d8cd18e 100644 --- a/module/handlebarHelpers/inputs/formFields.mjs +++ b/module/handlebarHelpers/inputs/formFields.mjs @@ -1,5 +1,6 @@ import { barInput } from "./barInput.mjs"; import { booleanInput } from "./booleanInput.mjs"; +import { costInput } from "./currency.mjs"; import { dropdownInput } from "./dropdownInput.mjs"; import { groupInput } from "./groupInput.mjs"; import { numberInput } from "./numberInput.mjs"; @@ -18,6 +19,7 @@ const inputTypes = { boolean: booleanInput, group: groupInput, text: textInput, + cost: costInput, }; const typesToSanitize = new Set([ `string`, `number` ]); diff --git a/module/handlebarHelpers/inputs/groupInput.mjs b/module/handlebarHelpers/inputs/groupInput.mjs index d6a5c45..59f26e8 100644 --- a/module/handlebarHelpers/inputs/groupInput.mjs +++ b/module/handlebarHelpers/inputs/groupInput.mjs @@ -16,7 +16,7 @@ export function groupInput(input, data) { data-input-type="group" var:border-color="${input.borderColor ?? `var(--accent-1)`}" var:vertical-displacement="${input.verticalDisplacement ?? `12px`}" - var:padding-top="${input.paddingTop ?? `16px`}" + var:padding-top="${input.paddingTop ?? `20px`}" >
${title}
diff --git a/templates/Apps/AllItemSheetV1/style.css b/templates/Apps/AllItemSheetV1/style.css index 59089ef..2170d31 100644 --- a/templates/Apps/AllItemSheetV1/style.css +++ b/templates/Apps/AllItemSheetV1/style.css @@ -83,6 +83,11 @@ margin: 0 auto; } + hr:has(+ [data-input-type="group"]), + [data-input-type="group"] + hr { + display: none; + }; + label, .label { display: flex; align-items: center; From c7342b6402fcec6a822e1cb12d41ddf05873b5bc Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 27 Feb 2025 22:56:36 -0700 Subject: [PATCH 006/146] Begin work on the updated delve dice HUD that is better in every way than the other version --- module/Apps/DelveDiceHUD.mjs | 106 ++++++++++++++++++ module/hooks/init.mjs | 3 + module/hooks/ready.mjs | 2 + module/settings/metaSettings.mjs | 17 ++- templates/Apps/DelveDiceHUD/difficulty.hbs | 3 + templates/Apps/DelveDiceHUD/fateCompass.hbs | 3 + templates/Apps/DelveDiceHUD/style.css | 18 +++ templates/Apps/DelveDiceHUD/tour/current.hbs | 3 + templates/Apps/DelveDiceHUD/tour/next.hbs | 7 ++ templates/Apps/DelveDiceHUD/tour/previous.hbs | 7 ++ templates/Apps/apps.css | 1 + templates/css/elements/button.css | 2 +- templates/css/themes/dark.css | 4 + 13 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 module/Apps/DelveDiceHUD.mjs create mode 100644 templates/Apps/DelveDiceHUD/difficulty.hbs create mode 100644 templates/Apps/DelveDiceHUD/fateCompass.hbs create mode 100644 templates/Apps/DelveDiceHUD/style.css create mode 100644 templates/Apps/DelveDiceHUD/tour/current.hbs create mode 100644 templates/Apps/DelveDiceHUD/tour/next.hbs create mode 100644 templates/Apps/DelveDiceHUD/tour/previous.hbs diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs new file mode 100644 index 0000000..0122aea --- /dev/null +++ b/module/Apps/DelveDiceHUD.mjs @@ -0,0 +1,106 @@ +import { filePath } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +const conditions = [ + { label: `RipCrypt.common.difficulties.easy`, value: 4 }, + { label: `RipCrypt.common.difficulties.normal`, value: 5 }, + { label: `RipCrypt.common.difficulties.tough`, value: 6 }, + { label: `RipCrypt.common.difficulties.hard`, value: 7 }, +]; + +export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { + // #region Options + static DEFAULT_OPTIONS = { + id: `ripcrypt-delve-dice`, + tag: `aside`, + classes: [ + `ripcrypt`, + `ripcrypt--DelveDiceHUD` + ], + window: { + frame: false, + positioned: false, + }, + actions: { + tourDelta: this.#tourDelta, + }, + }; + + static PARTS = { + previousTour: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/previous.hbs`), + }, + difficulty: { + template: filePath(`templates/Apps/DelveDiceHUD/difficulty.hbs`), + }, + fateCompass: { + template: filePath(`templates/Apps/DelveDiceHUD/fateCompass.hbs`), + }, + currentTour: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/current.hbs`), + }, + nextTour: { + template: filePath(`templates/Apps/DelveDiceHUD/tour/next.hbs`), + }, + }; + // #endregion + + // #region Lifecycle + /** + * Injects the element into the Foundry UI in the top middle + */ + _insertElement(element) { + const existing = document.getElementById(element.id); + if (existing) { + existing.replaceWith(element); + } else { + const parent = document.getElementById(`ui-top`); + parent.prepend(element); + }; + }; + + async _onRender(context, options) { + await super._onRender(context, options); + + // Shortcut because users can't edit + if (!game.user.isGM) { return }; + }; + + async _preparePartContext(partId, ctx, opts) { + ctx = await super._preparePartContext(partId, ctx, opts); + ctx.meta ??= {}; + + ctx.meta.editable = game.user.isGM; + + switch (partId) { + case `currentTour`: { + await this._prepareTourContext(ctx); + break; + }; + case `difficulty`: { + await this._prepareDifficultyContext(ctx); + break; + }; + }; + + Logger.log(`${partId} Context`, ctx); + return ctx; + }; + + async _prepareTourContext(ctx) { + ctx.tour = game.settings.get(`ripcrypt`, `sandsOfFate`); + }; + + async _prepareDifficultyContext(ctx) { + ctx.dc = game.settings.get(`ripcrypt`, `dc`); + } + // #endregion + + // #region Actions + static async #tourDelta() { + ui.notifications.info(`Button Clicked!`, { console: false }); + }; + // #endregion +}; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 812c6e2..4005cc5 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,6 +1,7 @@ // Applications import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; +import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; import { DelveTourApp } from "../Apps/DelveTourApp.mjs"; import { HeroSkillsCardV1 } from "../Apps/ActorSheets/HeroSkillsCardV1.mjs"; import { HeroSummaryCardV1 } from "../Apps/ActorSheets/HeroSummaryCardV1.mjs"; @@ -39,6 +40,8 @@ Hooks.once(`init`, () => { CONFIG.Combat.initiative.decimals = 2; CONFIG.ui.crypt = DelveTourApp; + CONFIG.ui.delveDice = DelveDiceHUD; + // globalThis.delveDice = new DelveDiceHUD(); // #region Settings registerMetaSettings(); diff --git a/module/hooks/ready.mjs b/module/hooks/ready.mjs index c71bc25..c6c0167 100644 --- a/module/hooks/ready.mjs +++ b/module/hooks/ready.mjs @@ -23,6 +23,8 @@ Hooks.once(`ready`, () => { ui.crypt.render({ force: true }); }; + ui.delveDice.render({ force: true }); + // MARK: 1-time updates if (!game.settings.get(`ripcrypt`, `firstLoadFinished`)) { // Update the turnMarker to be the RipCrypt defaults diff --git a/module/settings/metaSettings.mjs b/module/settings/metaSettings.mjs index 7694088..77bc3dc 100644 --- a/module/settings/metaSettings.mjs +++ b/module/settings/metaSettings.mjs @@ -5,20 +5,25 @@ export function registerMetaSettings() { config: false, requiresReload: false, onChange: () => { - ui.crypt.render({ parts: [ `delveConditions` ]}); + ui.delveDice.render({ parts: [`difficulty`] }); }, }); + game.settings.register(`ripcrypt`, `sandsOfFate`, { + scope: `world`, + type: Number, + initial: 8, + config: false, + requiresReload: false, + onChange: async () => {}, + }); + game.settings.register(`ripcrypt`, `currentFate`, { scope: `world`, type: String, config: false, requiresReload: false, - onChange: async () => { - await ui.crypt.render({ parts: [ `fate` ] }); - await game.combat.setupTurns(); - await ui.combat.render({ parts: [ `tracker` ] }); - }, + onChange: async () => {}, }); game.settings.register(`ripcrypt`, `whoFirst`, { diff --git a/templates/Apps/DelveDiceHUD/difficulty.hbs b/templates/Apps/DelveDiceHUD/difficulty.hbs new file mode 100644 index 0000000..29b591f --- /dev/null +++ b/templates/Apps/DelveDiceHUD/difficulty.hbs @@ -0,0 +1,3 @@ +
+ Difficulty: {{dc}} +
diff --git a/templates/Apps/DelveDiceHUD/fateCompass.hbs b/templates/Apps/DelveDiceHUD/fateCompass.hbs new file mode 100644 index 0000000..3ad015d --- /dev/null +++ b/templates/Apps/DelveDiceHUD/fateCompass.hbs @@ -0,0 +1,3 @@ +
+ North +
\ No newline at end of file diff --git a/templates/Apps/DelveDiceHUD/style.css b/templates/Apps/DelveDiceHUD/style.css new file mode 100644 index 0000000..a7ab971 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/style.css @@ -0,0 +1,18 @@ +#ripcrypt-delve-dice { + display: grid; + grid-template-columns: max-content 1fr 1fr 1fr max-content; + gap: 8px; + padding: 4px 1.5rem; + background: var(--DelveDice-background); + align-items: center; + justify-items: center; + pointer-events: all; + + border-radius: 0 0 999px 999px; + + button { + &:hover { + cursor: pointer; + } + } +} diff --git a/templates/Apps/DelveDiceHUD/tour/current.hbs b/templates/Apps/DelveDiceHUD/tour/current.hbs new file mode 100644 index 0000000..12c795e --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/current.hbs @@ -0,0 +1,3 @@ +
+ The Hourglass +
diff --git a/templates/Apps/DelveDiceHUD/tour/next.hbs b/templates/Apps/DelveDiceHUD/tour/next.hbs new file mode 100644 index 0000000..1e85007 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/next.hbs @@ -0,0 +1,7 @@ + diff --git a/templates/Apps/DelveDiceHUD/tour/previous.hbs b/templates/Apps/DelveDiceHUD/tour/previous.hbs new file mode 100644 index 0000000..d498a99 --- /dev/null +++ b/templates/Apps/DelveDiceHUD/tour/previous.hbs @@ -0,0 +1,7 @@ + diff --git a/templates/Apps/apps.css b/templates/Apps/apps.css index bcf722e..9f6a4f6 100644 --- a/templates/Apps/apps.css +++ b/templates/Apps/apps.css @@ -1,6 +1,7 @@ @import url("./AllItemSheetV1/style.css"); @import url("./CombinedHeroSheet/style.css"); @import url("./CryptApp/style.css"); +@import url("./DelveDiceHUD/style.css"); @import url("./DicePool/style.css"); @import url("./HeroSummaryCardV1/style.css"); @import url("./HeroSkillsCardV1/style.css"); diff --git a/templates/css/elements/button.css b/templates/css/elements/button.css index 2d0b6bb..c9e0bab 100644 --- a/templates/css/elements/button.css +++ b/templates/css/elements/button.css @@ -1,4 +1,4 @@ -.ripcrypt > .window-content button { +.ripcrypt button { all: revert; outline: none; border: none; diff --git a/templates/css/themes/dark.css b/templates/css/themes/dark.css index b33c069..aa4a8a4 100644 --- a/templates/css/themes/dark.css +++ b/templates/css/themes/dark.css @@ -41,4 +41,8 @@ --pill-input-background: var(--accent-2); --pill-input-disabled-text: white; --pill-input-disabled-background: black; + + /* Custom HUD Components */ + --DelveDice-background: var(--accent-1); + --DelveDice-text: white; } From 77979f55506071e1353ddc3092153b1476911c7f Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 28 Feb 2025 16:17:16 -0700 Subject: [PATCH 007/146] Add arrow icons for the HUD controls --- assets/_credit.txt | 5 +++++ assets/icons/arrow-compass.svg | 3 +++ assets/icons/arrow-left.svg | 3 +++ assets/icons/arrow-right.svg | 3 +++ 4 files changed, 14 insertions(+) create mode 100644 assets/icons/arrow-compass.svg create mode 100644 assets/icons/arrow-left.svg create mode 100644 assets/icons/arrow-right.svg diff --git a/assets/_credit.txt b/assets/_credit.txt index 39124c6..43cf3fb 100644 --- a/assets/_credit.txt +++ b/assets/_credit.txt @@ -11,6 +11,11 @@ ARISO: Abdulloh Fauzan: - icons/info-circle.svg (https://thenounproject.com/icon/information-4176576/) : Rights Purchased +QOLBIN SALIIM: + - icons/arrow-left.svg (https://thenounproject.com/icon/arrow-1933583/) : Rights Purchased + - icons/arrow-right.svg (https://thenounproject.com/icon/arrow-1933581/) : Rights Purchased + - icons/arrow-compass.svg (https://thenounproject.com/icon/arrow-2052607/) : Rights Purchased + Soetarman Atmodjo: - icons/roll.svg (https://thenounproject.com/icon/dice-5195278/) : Rights Purchased diff --git a/assets/icons/arrow-compass.svg b/assets/icons/arrow-compass.svg new file mode 100644 index 0000000..b1e8a40 --- /dev/null +++ b/assets/icons/arrow-compass.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000..e1a347e --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000..a477835 --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + From 3d6710dd189e657251c254b8907b200aaf5d99e5 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 28 Feb 2025 16:18:31 -0700 Subject: [PATCH 008/146] Get the fate compass looking good --- templates/Apps/DelveDiceHUD/fateCompass.hbs | 17 ++++++++-- templates/Apps/DelveDiceHUD/style.css | 36 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/templates/Apps/DelveDiceHUD/fateCompass.hbs b/templates/Apps/DelveDiceHUD/fateCompass.hbs index 3ad015d..19ea83c 100644 --- a/templates/Apps/DelveDiceHUD/fateCompass.hbs +++ b/templates/Apps/DelveDiceHUD/fateCompass.hbs @@ -1,3 +1,16 @@
- North -
\ No newline at end of file +
+
+ N + W + + E + S +
+
+
diff --git a/templates/Apps/DelveDiceHUD/style.css b/templates/Apps/DelveDiceHUD/style.css index a7ab971..0e4a226 100644 --- a/templates/Apps/DelveDiceHUD/style.css +++ b/templates/Apps/DelveDiceHUD/style.css @@ -15,4 +15,40 @@ cursor: pointer; } } + + #fate-compass { + width: 100%; + height: 100%; + overflow: visible; + position: relative; + + .compass-container { + position: absolute; + background: var(--DelveDice-background); + border-radius: 0 0 999px 999px; + padding: 4px; + width: 100%; + } + + .compass { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(3, minmax(0, 1fr)); + grid-template-areas: + ". N ." + "W A E" + ". S ."; + align-items: center; + justify-items: center; + background: var(--accent-2); + border-radius: 999px; + aspect-ratio: 1; + } + + .compass-pointer { + grid-area: A; + transition: 500ms transform; + transform: rotate(-90deg); /* North by default */ + } + } } From 6ae412c787a51cb0e72a2b44c354d1769d88bca3 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 28 Feb 2025 16:47:41 -0700 Subject: [PATCH 009/146] Get hourglass improved --- templates/Apps/DelveDiceHUD/style.css | 27 +++++++++++++++++++- templates/Apps/DelveDiceHUD/tour/current.hbs | 13 ++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/templates/Apps/DelveDiceHUD/style.css b/templates/Apps/DelveDiceHUD/style.css index 0e4a226..4b19a11 100644 --- a/templates/Apps/DelveDiceHUD/style.css +++ b/templates/Apps/DelveDiceHUD/style.css @@ -1,6 +1,6 @@ #ripcrypt-delve-dice { display: grid; - grid-template-columns: max-content 1fr 1fr 1fr max-content; + grid-template-columns: max-content 1fr 0.6fr 1fr max-content; gap: 8px; padding: 4px 1.5rem; background: var(--DelveDice-background); @@ -51,4 +51,29 @@ transform: rotate(-90deg); /* North by default */ } } + + #the-hourglass { + display: flex; + flex-direction: row; + position: relative; + + .hourglass-container { + position: absolute; + width: 34px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 4px 0; + background: var(--accent-1); + border-radius: 8px; + + rc-svg { + inset: 4px; + } + } + + .hud-text { + margin-left: 38px; + } + } } diff --git a/templates/Apps/DelveDiceHUD/tour/current.hbs b/templates/Apps/DelveDiceHUD/tour/current.hbs index 12c795e..991b6f4 100644 --- a/templates/Apps/DelveDiceHUD/tour/current.hbs +++ b/templates/Apps/DelveDiceHUD/tour/current.hbs @@ -1,3 +1,12 @@ -
- The Hourglass +
+
+ +
+
+ The Hourglass +
From c9ed4142e6994089f582e2176549c349f3cb73ad Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 1 Mar 2025 00:34:25 -0700 Subject: [PATCH 010/146] Finalize the general layout of the HUD --- templates/Apps/DelveDiceHUD/difficulty.hbs | 15 ++++++++-- templates/Apps/DelveDiceHUD/style.css | 29 +++++++++++++------- templates/Apps/DelveDiceHUD/tour/current.hbs | 11 +++++--- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/templates/Apps/DelveDiceHUD/difficulty.hbs b/templates/Apps/DelveDiceHUD/difficulty.hbs index 29b591f..d65b06b 100644 --- a/templates/Apps/DelveDiceHUD/difficulty.hbs +++ b/templates/Apps/DelveDiceHUD/difficulty.hbs @@ -1,3 +1,14 @@ -
- Difficulty: {{dc}} +
+
+ 8 + +
diff --git a/templates/Apps/DelveDiceHUD/style.css b/templates/Apps/DelveDiceHUD/style.css index 4b19a11..55128c2 100644 --- a/templates/Apps/DelveDiceHUD/style.css +++ b/templates/Apps/DelveDiceHUD/style.css @@ -1,6 +1,6 @@ #ripcrypt-delve-dice { display: grid; - grid-template-columns: max-content 1fr 0.6fr 1fr max-content; + grid-template-columns: max-content 2rem 90px 2rem max-content; gap: 8px; padding: 4px 1.5rem; background: var(--DelveDice-background); @@ -52,28 +52,37 @@ } } - #the-hourglass { + #the-hourglass, + #delve-difficulty { + width: 100%; + height: 100%; display: flex; flex-direction: row; position: relative; - .hourglass-container { + .icon-container { position: absolute; width: 34px; - display: flex; - flex-direction: column; - justify-content: center; + display: grid; padding: 4px 0; background: var(--accent-1); border-radius: 8px; + > * { + grid-row: 1 / -1; + grid-column: 1 / -1; + } + + span { + font-size: 1.25rem; + z-index: 2; + align-self: center; + justify-self: center; + } + rc-svg { inset: 4px; } } - - .hud-text { - margin-left: 38px; - } } } diff --git a/templates/Apps/DelveDiceHUD/tour/current.hbs b/templates/Apps/DelveDiceHUD/tour/current.hbs index 991b6f4..9e3d423 100644 --- a/templates/Apps/DelveDiceHUD/tour/current.hbs +++ b/templates/Apps/DelveDiceHUD/tour/current.hbs @@ -1,12 +1,15 @@
-
+
+ + 8 +
-
- The Hourglass -
From 507913139f915af34b0421f5575676b3e2b7601c Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 1 Mar 2025 00:44:49 -0700 Subject: [PATCH 011/146] Begin working on HUD interactivity --- module/Apps/DelveDiceHUD.mjs | 14 +++++++++- templates/Apps/DelveDiceHUD/fateCompass.hbs | 8 +++--- templates/Apps/DelveDiceHUD/tour/next.hbs | 27 ++++++++++++++----- templates/Apps/DelveDiceHUD/tour/previous.hbs | 27 ++++++++++++++----- templates/css/elements/button.css | 8 ++++++ 5 files changed, 65 insertions(+), 19 deletions(-) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index 0122aea..a487480 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -83,6 +83,10 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { await this._prepareDifficultyContext(ctx); break; }; + case `fateCompass`: { + await this._prepareFateCompassContext(ctx); + break; + }; }; Logger.log(`${partId} Context`, ctx); @@ -95,12 +99,20 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { async _prepareDifficultyContext(ctx) { ctx.dc = game.settings.get(`ripcrypt`, `dc`); + }; + + async _prepareFateCompassContext(ctx) { + ctx.direction = game.settings.get(`ripcrypt`, `currentFate`); } // #endregion // #region Actions static async #tourDelta() { - ui.notifications.info(`Button Clicked!`, { console: false }); + ui.notifications.info(`Delve Tour Changed`, { console: false }); + }; + + static async #setFate() { + ui.notifications.info(`Fate Set!`, { console: false }); }; // #endregion }; diff --git a/templates/Apps/DelveDiceHUD/fateCompass.hbs b/templates/Apps/DelveDiceHUD/fateCompass.hbs index 19ea83c..75c0ed5 100644 --- a/templates/Apps/DelveDiceHUD/fateCompass.hbs +++ b/templates/Apps/DelveDiceHUD/fateCompass.hbs @@ -1,16 +1,16 @@
- N - W + + - E - S + +
diff --git a/templates/Apps/DelveDiceHUD/tour/next.hbs b/templates/Apps/DelveDiceHUD/tour/next.hbs index 1e85007..28573c3 100644 --- a/templates/Apps/DelveDiceHUD/tour/next.hbs +++ b/templates/Apps/DelveDiceHUD/tour/next.hbs @@ -1,7 +1,20 @@ - +
+ {{!-- This is here to prevent height collapsing --}} + ​ + + {{#if meta.editable}} + + {{/if}} +
+ diff --git a/templates/Apps/DelveDiceHUD/tour/previous.hbs b/templates/Apps/DelveDiceHUD/tour/previous.hbs index d498a99..ac420ef 100644 --- a/templates/Apps/DelveDiceHUD/tour/previous.hbs +++ b/templates/Apps/DelveDiceHUD/tour/previous.hbs @@ -1,7 +1,20 @@ - +
+ {{!-- This is here to prevent height collapsing --}} + ​ + + {{#if meta.editable}} + + {{/if}} +
+ diff --git a/templates/css/elements/button.css b/templates/css/elements/button.css index c9e0bab..0493fe8 100644 --- a/templates/css/elements/button.css +++ b/templates/css/elements/button.css @@ -3,6 +3,8 @@ outline: none; border: none; padding: 2px 4px; + font-family: inherit; + font-size: inherit; background: var(--button-background); color: var(--button-text); @@ -23,4 +25,10 @@ width: 20px; height: 20px; } + + &.transparent { + background: inherit; + color: inherit; + padding: 0; + } } From 7c8d6a7208ef0890725d84ecdb07054cbfe612c4 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 1 Mar 2025 19:20:21 -0700 Subject: [PATCH 012/146] Make the distanceBetweenFates more situation-complete --- module/utils/distanceBetweenFates.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/module/utils/distanceBetweenFates.mjs b/module/utils/distanceBetweenFates.mjs index 4eaaddd..8c57240 100644 --- a/module/utils/distanceBetweenFates.mjs +++ b/module/utils/distanceBetweenFates.mjs @@ -14,12 +14,18 @@ export function distanceBetweenFates(start, end) { return undefined; }; - if (isOppositeFates(start, end)) { + if (start === end) { + return 0; + }; + + if (isOppositeFates(start, end) || isOppositeFates(end, start)) { return 2; }; let isForward = start === FatePath.SOUTH && end === FatePath.WEST; isForward ||= start === FatePath.NORTH && end === FatePath.EAST; + isForward ||= start === FatePath.WEST && end === FatePath.NORTH; + isForward ||= start === FatePath.EAST && end === FatePath.SOUTH; if (isForward) { return 1; }; From 76399621300f4dd40325818b1da78f239034fecb Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 1 Mar 2025 19:22:02 -0700 Subject: [PATCH 013/146] Add animation and editing support for the fate compass --- module/Apps/DelveDiceHUD.mjs | 82 ++++++++++++++++----- templates/Apps/DelveDiceHUD/fateCompass.hbs | 56 +++++++++++++- 2 files changed, 115 insertions(+), 23 deletions(-) diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index a487480..4e0d358 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -1,7 +1,17 @@ +import { distanceBetweenFates } from "../utils/distanceBetweenFates.mjs"; import { filePath } from "../consts.mjs"; +import { gameTerms } from "../gameTerms.mjs"; import { Logger } from "../utils/Logger.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; +const { FatePath } = gameTerms; + +const CompassRotations = { + [FatePath.NORTH]: -90, + [FatePath.EAST]: 0, + [FatePath.SOUTH]: 90, + [FatePath.WEST]: 180, +}; const conditions = [ { label: `RipCrypt.common.difficulties.easy`, value: 4 }, @@ -17,7 +27,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { tag: `aside`, classes: [ `ripcrypt`, - `ripcrypt--DelveDiceHUD` + `ripcrypt--DelveDiceHUD`, ], window: { frame: false, @@ -25,6 +35,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { }, actions: { tourDelta: this.#tourDelta, + setFate: this.#setFate, }, }; @@ -38,7 +49,7 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { fateCompass: { template: filePath(`templates/Apps/DelveDiceHUD/fateCompass.hbs`), }, - currentTour: { + sandsOfFate: { template: filePath(`templates/Apps/DelveDiceHUD/tour/current.hbs`), }, nextTour: { @@ -47,6 +58,24 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { }; // #endregion + // #region Instance Data + /** + * The current number of degrees the compass pointer should be rotated, this + * is not stored in the DB since we only care about the initial rotation on + * reload, which is derived from the current fate. + * @type {Number} + */ + _rotation; + + constructor(...args) { + super(...args); + this._sandsOfFate = game.settings.get(`ripcrypt`, `sandsOfFate`); + this._currentFate = game.settings.get(`ripcrypt`, `currentFate`); + this._rotation = CompassRotations[this._currentFate]; + this._difficulty = game.settings.get(`ripcrypt`, `dc`); + }; + // #endregion + // #region Lifecycle /** * Injects the element into the Foundry UI in the top middle @@ -75,16 +104,17 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { ctx.meta.editable = game.user.isGM; switch (partId) { - case `currentTour`: { - await this._prepareTourContext(ctx); + case `sandsOfFate`: { + ctx.sandsOfFate = this._sandsOfFate; break; }; case `difficulty`: { - await this._prepareDifficultyContext(ctx); + ctx.dc = this._difficulty; break; }; case `fateCompass`: { - await this._prepareFateCompassContext(ctx); + ctx.fate = this._currentFate; + ctx.rotation = `${this._rotation}deg`; break; }; }; @@ -93,26 +123,40 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { return ctx; }; - async _prepareTourContext(ctx) { - ctx.tour = game.settings.get(`ripcrypt`, `sandsOfFate`); + async animate({ parts = [] } = {}) { + if (parts.includes(`fateCompass`)) { + this.#animateCompassTo(); + }; + + if (parts.includes(`sandsOfFate`)) {}; }; - async _prepareDifficultyContext(ctx) { - ctx.dc = game.settings.get(`ripcrypt`, `dc`); - }; + #animateCompassTo(newFate) { + /** @type {HTMLElement|undefined} */ + const pointer = this.element.querySelector(`.compass-pointer`); + if (!pointer) { return }; - async _prepareFateCompassContext(ctx) { - ctx.direction = game.settings.get(`ripcrypt`, `currentFate`); - } + let distance = distanceBetweenFates(this._currentFate, newFate); + Logger.table({ newFate, fate: this._currentFate, distance, _rotation: this._rotation }); + if (distance === 3) { distance = -1 }; + + this._rotation += distance * 90; + + pointer.style.setProperty(`transform`, `rotate(${this._rotation}deg)`); + }; // #endregion // #region Actions - static async #tourDelta() { - ui.notifications.info(`Delve Tour Changed`, { console: false }); - }; + static async #tourDelta() {}; - static async #setFate() { - ui.notifications.info(`Fate Set!`, { console: false }); + /** @this {DelveDiceHUD} */ + static async #setFate(_event, element) { + const fate = element.dataset.toFate; + this.#animateCompassTo(fate); + + // must be done after animate, otherwise it won't change the rotation + this._currentFate = fate; + game.settings.set(`ripcrypt`, `currentFate`, fate); }; // #endregion }; diff --git a/templates/Apps/DelveDiceHUD/fateCompass.hbs b/templates/Apps/DelveDiceHUD/fateCompass.hbs index 75c0ed5..e4b74c0 100644 --- a/templates/Apps/DelveDiceHUD/fateCompass.hbs +++ b/templates/Apps/DelveDiceHUD/fateCompass.hbs @@ -1,16 +1,64 @@
- - - - + {{#if meta.editable}} + + + + + {{else}} + N + W + E + S + {{/if}}
From 110823a26b876252302edf62d6aa9ef79aff6522 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 1 Mar 2025 23:40:39 -0700 Subject: [PATCH 014/146] Get the delve tour incrementer changes working and affecting fate as well --- langs/en-ca.json | 17 +++++ module/Apps/DelveDiceHUD.mjs | 76 +++++++++++++++++-- module/api.mjs | 4 + module/settings/metaSettings.mjs | 19 ++++- module/settings/worldSettings.mjs | 43 ++++++++++- .../{distanceBetweenFates.mjs => fates.mjs} | 18 +++++ templates/Apps/DelveDiceHUD/tour/current.hbs | 7 +- templates/Apps/DelveDiceHUD/tour/next.hbs | 2 + templates/Apps/DelveDiceHUD/tour/previous.hbs | 2 + 9 files changed, 174 insertions(+), 14 deletions(-) rename module/utils/{distanceBetweenFates.mjs => fates.mjs} (69%) diff --git a/langs/en-ca.json b/langs/en-ca.json index a91a488..184e559 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -118,6 +118,20 @@ "condensedRange": { "name": "Condense Weapon Range Input", "hint": "With this enabled, the weapon range will be displayed as \"X / Y\" when editing a weapon. While disabled it will be as displayed as two different rows, one for Short Range and one for Long Range" + }, + "sandsOfFateInitial": { + "name": "Sands of Fate Initial", + "hint": "What value should The Hourglass reset to when a Cryptic Event occurs" + }, + "onCrypticEvent": { + "name": "Cryptic Event Alert", + "hint": "What happens when a cryptic event occurs by clicking the \"Next Delve Tour\" button in the HUD", + "options": { + "notif": "Notification", + "pause": "Pause Game", + "both": "Notification and Pause Game", + "nothing": "Do Nothing" + } } }, "Apps": { @@ -150,6 +164,9 @@ }, "warn": { "cannot-go-negative": "\"{name}\" is unable to be a negative number." + }, + "info": { + "cryptic-event-alert": "A Cryptic Event Has Occured!" } }, "tooltips": { diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index 4e0d358..960665e 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -1,6 +1,7 @@ -import { distanceBetweenFates } from "../utils/distanceBetweenFates.mjs"; +import { distanceBetweenFates, nextFate, previousFate } from "../utils/fates.mjs"; import { filePath } from "../consts.mjs"; import { gameTerms } from "../gameTerms.mjs"; +import { localizer } from "../utils/Localizer.mjs"; import { Logger } from "../utils/Logger.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -128,35 +129,96 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { this.#animateCompassTo(); }; - if (parts.includes(`sandsOfFate`)) {}; + if (parts.includes(`sandsOfFate`)) { + this.#animateSandsTo(); + }; }; #animateCompassTo(newFate) { + if (newFate === this._currentFate) { return }; + /** @type {HTMLElement|undefined} */ const pointer = this.element.querySelector(`.compass-pointer`); if (!pointer) { return }; + newFate ??= game.settings.get(`ripcrypt`, `currentFate`); + let distance = distanceBetweenFates(this._currentFate, newFate); - Logger.table({ newFate, fate: this._currentFate, distance, _rotation: this._rotation }); if (distance === 3) { distance = -1 }; this._rotation += distance * 90; pointer.style.setProperty(`transform`, `rotate(${this._rotation}deg)`); + this._currentFate = newFate; + }; + + #animateSandsTo(newSands) { + /** @type {HTMLElement|undefined} */ + const sands = this.element.querySelector(`.sands-value`); + if (!sands) { return }; + + newSands ??= game.settings.get(`ripcrypt`, `sandsOfFate`); + + sands.innerHTML = newSands; + this._sandsOfFate = newSands; }; // #endregion // #region Actions - static async #tourDelta() {}; + /** @this {DelveDiceHUD} */ + static async #tourDelta(_event, element) { + const delta = parseInt(element.dataset.delta); + const initial = game.settings.get(`ripcrypt`, `sandsOfFateInitial`); + let newSands = this._sandsOfFate + delta; + + if (newSands > initial) { + Logger.info(`Cannot go to a previous Delve Tour above the initial value`); + return; + }; + + if (newSands === 0) { + newSands = initial; + await this.alertCrypticEvent(); + }; + + switch (Math.sign(delta)) { + case -1: { + game.settings.set(`ripcrypt`, `currentFate`, nextFate(this._currentFate)); + break; + } + case 1: { + game.settings.set(`ripcrypt`, `currentFate`, previousFate(this._currentFate)); + break; + } + }; + + this.#animateSandsTo(newSands); + game.settings.set(`ripcrypt`, `sandsOfFate`, newSands); + }; /** @this {DelveDiceHUD} */ static async #setFate(_event, element) { const fate = element.dataset.toFate; this.#animateCompassTo(fate); - - // must be done after animate, otherwise it won't change the rotation - this._currentFate = fate; game.settings.set(`ripcrypt`, `currentFate`, fate); }; // #endregion + + // #region Public API + async alertCrypticEvent() { + const alertType = game.settings.get(`ripcrypt`, `onCrypticEvent`); + if (alertType === `nothing`) { return }; + + if ([`both`, `notif`].includes(alertType)) { + ui.notifications.info( + localizer(`RipCrypt.notifs.info.cryptic-event-alert`), + { console: false }, + ); + }; + + if ([`both`, `pause`].includes(alertType) && game.user.isGM) { + game.togglePause(true, { broadcast: true }); + }; + }; + // #endregion }; diff --git a/module/api.mjs b/module/api.mjs index 7b04ea8..d2e50ac 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -6,6 +6,7 @@ import { HeroSummaryCardV1 } from "./Apps/ActorSheets/HeroSummaryCardV1.mjs"; import { RichEditor } from "./Apps/RichEditor.mjs"; // Util imports +import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; const { deepFreeze } = foundry.utils; @@ -24,6 +25,9 @@ Object.defineProperty( }, utils: { documentSorter, + distanceBetweenFates, + nextFate, + previousFate, }, }), writable: false, diff --git a/module/settings/metaSettings.mjs b/module/settings/metaSettings.mjs index 77bc3dc..66fb669 100644 --- a/module/settings/metaSettings.mjs +++ b/module/settings/metaSettings.mjs @@ -1,3 +1,8 @@ +import { gameTerms } from "../gameTerms.mjs"; + +const { StringField } = foundry.data.fields; +const { FatePath } = gameTerms; + export function registerMetaSettings() { game.settings.register(`ripcrypt`, `dc`, { scope: `world`, @@ -15,15 +20,23 @@ export function registerMetaSettings() { initial: 8, config: false, requiresReload: false, - onChange: async () => {}, + onChange: async () => { + ui.delveDice.animate({ parts: [`sandsOfFate`] }); + }, }); game.settings.register(`ripcrypt`, `currentFate`, { scope: `world`, - type: String, + type: new StringField({ + blank: false, + nullable: false, + initial: FatePath.NORTH, + }), config: false, requiresReload: false, - onChange: async () => {}, + onChange: async () => { + ui.delveDice.animate({ parts: [`fateCompass`] }); + }, }); game.settings.register(`ripcrypt`, `whoFirst`, { diff --git a/module/settings/worldSettings.mjs b/module/settings/worldSettings.mjs index d7c426a..d193a32 100644 --- a/module/settings/worldSettings.mjs +++ b/module/settings/worldSettings.mjs @@ -1,10 +1,51 @@ +const { NumberField, StringField } = foundry.data.fields; + export function registerWorldSettings() { game.settings.register(`ripcrypt`, `showDelveTour`, { name: `Delve Tour Popup`, scope: `world`, type: Boolean, - config: true, + config: false, default: true, requiresReload: false, }); + + game.settings.register(`ripcrypt`, `sandsOfFateInitial`, { + name: `RipCrypt.setting.sandsOfFateInitial.name`, + hint: `RipCrypt.setting.sandsOfFateInitial.hint`, + scope: `world`, + config: true, + requiresReload: false, + type: new NumberField({ + required: true, + min: 1, + step: 1, + max: 10, + initial: 8, + }), + onChange: async (newInitialSands) => { + const currentSands = game.settings.get(`ripcrypt`, `sandsOfFate`); + if (newInitialSands <= currentSands) { + game.settings.set(`ripcrypt`, `sandsOfFate`, newInitialSands); + }; + }, + }); + + game.settings.register(`ripcrypt`, `onCrypticEvent`, { + name: `RipCrypt.setting.onCrypticEvent.name`, + hint: `RipCrypt.setting.onCrypticEvent.hint`, + scope: `world`, + config: true, + requiresReload: false, + type: new StringField({ + required: true, + initial: `notif`, + choices: { + "notif": `RipCrypt.setting.onCrypticEvent.options.notif`, + "pause": `RipCrypt.setting.onCrypticEvent.options.pause`, + "both": `RipCrypt.setting.onCrypticEvent.options.both`, + "nothing": `RipCrypt.setting.onCrypticEvent.options.nothing`, + }, + }), + }); }; diff --git a/module/utils/distanceBetweenFates.mjs b/module/utils/fates.mjs similarity index 69% rename from module/utils/distanceBetweenFates.mjs rename to module/utils/fates.mjs index 8c57240..d1fb6dd 100644 --- a/module/utils/distanceBetweenFates.mjs +++ b/module/utils/fates.mjs @@ -31,3 +31,21 @@ export function distanceBetweenFates(start, end) { }; return 3; }; + +const fateOrder = [ + FatePath.WEST, // to make the .find not integer overflow + FatePath.NORTH, + FatePath.EAST, + FatePath.SOUTH, + FatePath.WEST, +]; + +export function nextFate(fate) { + const fateIndex = fateOrder.findIndex(f => f === fate); + return fateOrder[fateIndex + 1]; +}; + +export function previousFate(fate) { + const fateIndex = fateOrder.lastIndexOf(fate); + return fateOrder[fateIndex - 1]; +}; diff --git a/templates/Apps/DelveDiceHUD/tour/current.hbs b/templates/Apps/DelveDiceHUD/tour/current.hbs index 9e3d423..18b98d9 100644 --- a/templates/Apps/DelveDiceHUD/tour/current.hbs +++ b/templates/Apps/DelveDiceHUD/tour/current.hbs @@ -1,12 +1,13 @@
- - 8 + + {{sandsOfFate}}
From 67753ce3e7bad8c140e5ca8c9380fcd779852bdd Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 6 Mar 2025 18:47:41 -0700 Subject: [PATCH 025/146] Add the Flect craft list (closes #21) --- templates/Apps/HeroCraftCardV1/content.hbs | 25 ++++++++++++++++++++++ templates/Apps/HeroCraftCardV1/style.css | 1 + 2 files changed, 26 insertions(+) diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index 44cc1d4..cc9fa23 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -23,4 +23,29 @@ {{/if}} {{/each}} + +
+ Flect + Details +
+
    + {{#each craft.flect as | craft |}} + {{#if craft}} +
  1. + {{ craft.name }} + {{#if craft.use}} + + {{/if}} +
  2. + {{else}} +
  3. + {{/if}} + {{/each}} +
diff --git a/templates/Apps/HeroCraftCardV1/style.css b/templates/Apps/HeroCraftCardV1/style.css index 2b81e63..a0dd159 100644 --- a/templates/Apps/HeroCraftCardV1/style.css +++ b/templates/Apps/HeroCraftCardV1/style.css @@ -44,6 +44,7 @@ } [data-aspect="focus"] { --row: 6; --col: 1; } + [data-aspect="flect"] { --row: 6; --col: 2; } [data-aspect] { &.aspect-header { From 5876d5fe980e00a094bb859225492d17009136ea Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 19:47:18 -0700 Subject: [PATCH 026/146] Add system summary --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index 9924bfe..68d0f25 100644 --- a/system.json +++ b/system.json @@ -1,7 +1,7 @@ { "id": "ripcrypt", "title": "RipCrypt", - "description": "", + "description": "A dungeon sprint RPG. Faster than an arrow to the eye. Smoother than a clean blade. Compact with consequences.", "version": "0.0.1", "compatibility": { "minimum": 13, From bd3c8d9accadfb2feecdda92399a6e2c994ed79f Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 19:48:08 -0700 Subject: [PATCH 027/146] Remove unused CSS import --- templates/Apps/apps.css | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/Apps/apps.css b/templates/Apps/apps.css index 1a261a4..2ec33cc 100644 --- a/templates/Apps/apps.css +++ b/templates/Apps/apps.css @@ -1,6 +1,5 @@ @import url("./AllItemSheetV1/style.css"); @import url("./CombinedHeroSheet/style.css"); -@import url("./CryptApp/style.css"); @import url("./DelveDiceHUD/style.css"); @import url("./DicePool/style.css"); @import url("./HeroCraftCardV1/style.css"); From f1d9fe187c37d598dc9ffac8a104597b152dbf1c Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 19:50:45 -0700 Subject: [PATCH 028/146] Add fract to the craft card (closes #22) --- templates/Apps/HeroCraftCardV1/content.hbs | 25 ++++++++++++++++++++++ templates/Apps/HeroCraftCardV1/style.css | 1 + 2 files changed, 26 insertions(+) diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index cc9fa23..8be66ce 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -48,4 +48,29 @@ {{/if}} {{/each}} + +
+ Fract + Details +
+
    + {{#each craft.fract as | craft |}} + {{#if craft}} +
  1. + {{ craft.name }} + {{#if craft.use}} + + {{/if}} +
  2. + {{else}} +
  3. + {{/if}} + {{/each}} +
diff --git a/templates/Apps/HeroCraftCardV1/style.css b/templates/Apps/HeroCraftCardV1/style.css index a0dd159..297844b 100644 --- a/templates/Apps/HeroCraftCardV1/style.css +++ b/templates/Apps/HeroCraftCardV1/style.css @@ -45,6 +45,7 @@ [data-aspect="focus"] { --row: 6; --col: 1; } [data-aspect="flect"] { --row: 6; --col: 2; } + [data-aspect="fract"] { --row: 11; --col: 1; } [data-aspect] { &.aspect-header { From a830adbd2da2dcd54a138b7b86a2ac608ec07d47 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 22:30:12 -0700 Subject: [PATCH 029/146] Get most of the Aura display implemented added to the HTML --- assets/caster-silhouette.v1.svg | 2 +- templates/Apps/HeroCraftCardV1/content.hbs | 22 +++++++++++ templates/Apps/HeroCraftCardV1/style.css | 44 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/assets/caster-silhouette.v1.svg b/assets/caster-silhouette.v1.svg index ed60c3e..9b53fcc 100644 --- a/assets/caster-silhouette.v1.svg +++ b/assets/caster-silhouette.v1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index 8be66ce..c9254da 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -1,4 +1,26 @@
+
+ Glimcraft +
+
+
10
+
8
+
6
+
4
+ +
+
+ Aura + 4 + 6 +
+
+
+
Focus Details diff --git a/templates/Apps/HeroCraftCardV1/style.css b/templates/Apps/HeroCraftCardV1/style.css index 297844b..b30cd9f 100644 --- a/templates/Apps/HeroCraftCardV1/style.css +++ b/templates/Apps/HeroCraftCardV1/style.css @@ -29,6 +29,49 @@ font-weight: bold; } + .aura-container { + grid-column: 1 / -1; + grid-row: 2 / span 4; + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-template-rows: minmax(0, 1fr); + position: relative; + } + + .circle-fragment, .full-circle { + display: flex; + justify-content: center; + align-items: center; + } + + .circle-fragment { + border-top-left-radius: 24% 100%; + border-bottom-left-radius: 25% 100%; + border-left: 2px dashed var(--accent-3); + margin-right: -5%; + } + + .full-circle { + border: 2px dashed var(--accent-3); + flex-grow: 0; + border-radius: 999px; + width: 80%; + aspect-ratio: 1; + align-self: center; + justify-self: center; + grid-row: 1; + grid-column: 4; + } + + .caster-silhouette { + grid-column: 4 / span 4; + grid-row: 1; + position: absolute; + left: 2rem; + width: 70%; + bottom: -10px; + } + .craft-list { display: grid; grid-template-rows: subgrid; @@ -49,6 +92,7 @@ [data-aspect] { &.aspect-header { + z-index: 1; grid-row: var(--row); grid-column: var(--col); } From 89b51a01e6d09ace61fc48e3af1a539611d7d3a3 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 8 Mar 2025 22:39:07 -0700 Subject: [PATCH 030/146] Get the aura display finished for the Craft Card (closes #23) --- templates/Apps/HeroCraftCardV1/content.hbs | 9 ++++-- templates/Apps/HeroCraftCardV1/style.css | 33 ++++++++++++++++++++++ templates/css/elements/span.css | 7 +++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index c9254da..a9348d6 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -10,13 +10,16 @@
Aura - 4 - 6 +
+ + + +
diff --git a/templates/Apps/HeroCraftCardV1/style.css b/templates/Apps/HeroCraftCardV1/style.css index b30cd9f..cff3266 100644 --- a/templates/Apps/HeroCraftCardV1/style.css +++ b/templates/Apps/HeroCraftCardV1/style.css @@ -72,6 +72,39 @@ bottom: -10px; } + .aura-values { + grid-row: 1; + grid-column: -3 / -1; + display: flex; + justify-content: center; + align-items: center; + z-index: 3; + + .dual-pill { + border-radius: 999px; + background: var(--accent-1); + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; + padding-left: 8px; + margin-left: 1rem; + margin-bottom: 1.2rem; + } + + .values { + border-radius: 999px; + margin: 2px; + background: var(--base-background); + color: var(--base-text); + padding: 0.125rem 0.5rem; + display: flex; + flex-direction: row; + gap: 0.5rem; + --slash-color: var(--accent-1); + } + } + .craft-list { display: grid; grid-template-rows: subgrid; diff --git a/templates/css/elements/span.css b/templates/css/elements/span.css index b4a2cef..85a099b 100644 --- a/templates/css/elements/span.css +++ b/templates/css/elements/span.css @@ -10,6 +10,13 @@ overflow: hidden; } + &.slash { + width: 2px; + background: var(--slash-color, currentColor); + border-radius: 999px; + transform: rotate(var(--slash-rotation, 15deg)); + } + /* Makes it so that spans are never less than the font size */ &:empty::before { content: "\200b"; From 4f35db01b6e7cd5a88cc5a45be93c9be20bba33c Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 9 Mar 2025 00:16:21 -0700 Subject: [PATCH 031/146] Add the derived data for the aura ranges --- module/Apps/ActorSheets/HeroCraftCardV1.mjs | 35 +++++++++++++++++++++ module/api.mjs | 2 ++ module/data/Actor/Hero.mjs | 8 +++++ module/utils/rank.mjs | 6 ++++ templates/Apps/HeroCraftCardV1/content.hbs | 6 ++-- 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 module/utils/rank.mjs diff --git a/module/Apps/ActorSheets/HeroCraftCardV1.mjs b/module/Apps/ActorSheets/HeroCraftCardV1.mjs index 7d78029..e931f17 100644 --- a/module/Apps/ActorSheets/HeroCraftCardV1.mjs +++ b/module/Apps/ActorSheets/HeroCraftCardV1.mjs @@ -8,6 +8,7 @@ import { Logger } from "../../utils/Logger.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; const { ContextMenu } = foundry.applications.ui; +const { deepClone } = foundry.utils; export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin(ActorSheetV2)) { @@ -79,12 +80,46 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; + ctx = await HeroCraftCardV1.prepareAura(ctx); ctx = await HeroCraftCardV1.prepareCraft(ctx); Logger.debug(`Context:`, ctx); return ctx; }; + static async prepareAura(ctx) { + const { normal, heavy } = ctx.aura = deepClone(ctx.actor.system.aura); + + ctx.auraClasses = {}; + if (heavy >= 4) { + ctx.auraClasses.four = `heavy`; + } + if (heavy >= 6) { + ctx.auraClasses.six = `heavy`; + } + if (heavy >= 8) { + ctx.auraClasses.eight = `heavy`; + } + if (heavy >= 10) { + ctx.auraClasses.ten = `heavy`; + } + + if (normal >= 4) { + ctx.auraClasses.four = `normal`; + } + if (normal >= 6) { + ctx.auraClasses.six = `normal`; + } + if (normal >= 8) { + ctx.auraClasses.eight = `normal`; + } + if (normal >= 10) { + ctx.auraClasses.ten = `normal`; + } + + return ctx; + }; + static async prepareCraft(ctx) { ctx.craft = {}; const aspects = Object.values(gameTerms.Aspects); diff --git a/module/api.mjs b/module/api.mjs index d2e50ac..a56f7b1 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -8,6 +8,7 @@ import { RichEditor } from "./Apps/RichEditor.mjs"; // Util imports import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; +import { rankToInteger } from "./utils/rank.mjs"; const { deepFreeze } = foundry.utils; @@ -28,6 +29,7 @@ Object.defineProperty( distanceBetweenFates, nextFate, previousFate, + rankToInteger, }, }), writable: false, diff --git a/module/data/Actor/Hero.mjs b/module/data/Actor/Hero.mjs index 2f0dc15..6f589d6 100644 --- a/module/data/Actor/Hero.mjs +++ b/module/data/Actor/Hero.mjs @@ -1,5 +1,6 @@ import { derivedMaximumBar } from "../helpers.mjs"; import { gameTerms } from "../../gameTerms.mjs"; +import { rankToInteger } from "../../utils/rank.mjs"; import { sumReduce } from "../../utils/sumReduce.mjs"; const { fields } = foundry.data; @@ -122,6 +123,13 @@ export class HeroData extends foundry.abstract.TypeDataModel { prepareBaseData() { super.prepareBaseData(); + // Calculate the person's base Crafting aura + const rank = rankToInteger(this.level.rank); + this.aura = { + normal: ( rank + 1 ) * 2, + heavy: ( rank + 2 ) * 2, + }; + this.guts.max = 0; // The limitations imposed on things like inventory spaces and equipped diff --git a/module/utils/rank.mjs b/module/utils/rank.mjs new file mode 100644 index 0000000..806703a --- /dev/null +++ b/module/utils/rank.mjs @@ -0,0 +1,6 @@ +import { gameTerms } from "../gameTerms.mjs"; + +export function rankToInteger(rankName) { + return Object.values(gameTerms.Rank) + .findIndex(r => r === rankName) + 1; +}; diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index a9348d6..29bba66 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -14,11 +14,11 @@ >
- Aura +
- + {{aura.normal}} - + {{aura.heavy}}
From f46bd6b5d3da875c2111ee8f545a90b91a66b546 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Mon, 10 Mar 2025 21:52:49 -0600 Subject: [PATCH 032/146] Localize the craft card (closes #35) --- langs/en-ca.json | 9 ++++++++- templates/Apps/HeroCraftCardV1/content.hbs | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/langs/en-ca.json b/langs/en-ca.json index 8199113..c40e853 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -52,6 +52,7 @@ "fract": "Fract", "focus": "Focus" }, + "aura": "Aura", "cost": "Cost", "currency": { "gold": "Gold", @@ -61,6 +62,7 @@ "damage": "Damage", "delete": "Delete", "description": "Description", + "details": "Details", "difficulties": { "easy": "Easy", "normal": "Normal", @@ -76,6 +78,7 @@ "equipped": "Equipped", "fate": "Fate", "gear": "Gear", + "glimcraft": "Glimcraft", "glory": "Glory", "guts": "Guts", "location": "Location", @@ -188,7 +191,11 @@ "set-fate-to": "Set Fate to {ordinal}", "current-tour": "Current Delve Tour", "next-tour": "Next Delve Tour", - "prev-tour": "Previous Delve Tour" + "prev-tour": "Previous Delve Tour", + "auras": { + "normal": "The distance of your aura normally", + "heavy": "The distance of your aura when using Heavycraft" + } } } } diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs index 29bba66..e30ee69 100644 --- a/templates/Apps/HeroCraftCardV1/content.hbs +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -1,6 +1,6 @@
- Glimcraft + {{rc-i18n "RipCrypt.common.glimcraft"}}
10
@@ -14,19 +14,19 @@ >
- +
- {{aura.normal}} + {{aura.normal}} - {{aura.heavy}} + {{aura.heavy}}
- Focus - Details + {{rc-i18n "RipCrypt.common.aspectNames.focus"}} + {{rc-i18n "RipCrypt.common.details"}}
    {{#each craft.focus as | craft |}} @@ -50,8 +50,8 @@
- Flect - Details + {{rc-i18n "RipCrypt.common.aspectNames.flect"}} + {{rc-i18n "RipCrypt.common.details"}}
    {{#each craft.flect as | craft |}} @@ -75,8 +75,8 @@
- Fract - Details + {{rc-i18n "RipCrypt.common.aspectNames.fract"}} + {{rc-i18n "RipCrypt.common.details"}}
    {{#each craft.fract as | craft |}} From fd2899395286e8b20a1ffc4e68e5e4902774fec2 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Mon, 10 Mar 2025 22:04:36 -0600 Subject: [PATCH 033/146] Clean-up data preparation --- module/Apps/ActorSheets/HeroCraftCardV1.mjs | 30 +-------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/module/Apps/ActorSheets/HeroCraftCardV1.mjs b/module/Apps/ActorSheets/HeroCraftCardV1.mjs index e931f17..66d78d4 100644 --- a/module/Apps/ActorSheets/HeroCraftCardV1.mjs +++ b/module/Apps/ActorSheets/HeroCraftCardV1.mjs @@ -88,35 +88,7 @@ export class HeroCraftCardV1 extends GenericAppMixin(HandlebarsApplicationMixin( }; static async prepareAura(ctx) { - const { normal, heavy } = ctx.aura = deepClone(ctx.actor.system.aura); - - ctx.auraClasses = {}; - if (heavy >= 4) { - ctx.auraClasses.four = `heavy`; - } - if (heavy >= 6) { - ctx.auraClasses.six = `heavy`; - } - if (heavy >= 8) { - ctx.auraClasses.eight = `heavy`; - } - if (heavy >= 10) { - ctx.auraClasses.ten = `heavy`; - } - - if (normal >= 4) { - ctx.auraClasses.four = `normal`; - } - if (normal >= 6) { - ctx.auraClasses.six = `normal`; - } - if (normal >= 8) { - ctx.auraClasses.eight = `normal`; - } - if (normal >= 10) { - ctx.auraClasses.ten = `normal`; - } - + ctx.aura = deepClone(ctx.actor.system.aura); return ctx; }; From 96ef2ba56f8eed9c45dd8ed4a4bc37c888710f23 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 12 Mar 2025 22:40:19 -0600 Subject: [PATCH 034/146] Add JSdoc for the API --- module/utils/rank.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/utils/rank.mjs b/module/utils/rank.mjs index 806703a..81f6b59 100644 --- a/module/utils/rank.mjs +++ b/module/utils/rank.mjs @@ -1,5 +1,12 @@ import { gameTerms } from "../gameTerms.mjs"; +/** + * Converts a rank's name into an integer form for use in mathematical calculations + * that rely on rank. + * + * @param {Novice|Adept|Expert|Master} rankName The rank to convert into an integer + * @returns An integer between 1 and 4 + */ export function rankToInteger(rankName) { return Object.values(gameTerms.Rank) .findIndex(r => r === rankName) + 1; From af5cf4acd5b61eee53ead4060e5b4a2480bc1e3e Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 13 Mar 2025 00:19:03 -0600 Subject: [PATCH 035/146] Begin working on laying the groundwork for the Ammo Tracker / popover --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 6 +++ module/Apps/popovers/AmmoTracker.mjs | 45 +++++++++++++++++++ .../Apps/popovers/AmmoTracker/content.hbs | 3 ++ 3 files changed, 54 insertions(+) create mode 100644 module/Apps/popovers/AmmoTracker.mjs create mode 100644 templates/Apps/popovers/AmmoTracker/content.hbs diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index b0e83fb..b9992b5 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -43,6 +43,12 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); + + const ammo = this.element.querySelector(`.ammo`); + + ammo.addEventListener(`mouseenter`, () => {console.log(`mouseenter-ing`)}); + + ammo.addEventListener(`contextmenu`, () => {console.log(`right-clicking`)}); }; static async _onRender(_context, options) { diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs new file mode 100644 index 0000000..b216185 --- /dev/null +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -0,0 +1,45 @@ +import { filePath } from "../../consts.mjs"; +import { GenericAppMixin } from "../GenericApp.mjs"; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) { + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt--AmmoTracker`, + ], + window: { + frame: false, + positioned: true, + resizable: false, + minimizable: false, + }, + position: { + width: 100, + height: 30, + }, + actions: {}, + }; + + static PARTS = { + main: { + template: filePath(`templates/Apps/popovers/AmmoTracker/content.hbs`), + }, + }; + // #endregion + + // #region Instance Data + // #endregion + + // #region Lifecycle + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + const ammoContainer = this.element.querySelector(`.ammo`); + console.dir(ammoContainer); + }; + // #endregion + + // #region Actions + // #endregion +}; diff --git a/templates/Apps/popovers/AmmoTracker/content.hbs b/templates/Apps/popovers/AmmoTracker/content.hbs new file mode 100644 index 0000000..80f0024 --- /dev/null +++ b/templates/Apps/popovers/AmmoTracker/content.hbs @@ -0,0 +1,3 @@ +
    + Hello +
    \ No newline at end of file From 8e8202f8a6508404b71ad0bf2cc344a981481ef2 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 13 Mar 2025 23:36:25 -0600 Subject: [PATCH 036/146] Getting the popover Application working on the most superficial level, and creating a generic popover mixin --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 63 +++++++- module/Apps/popovers/AmmoTracker.mjs | 158 ++++++++++++++++++- module/Apps/popovers/GenericPopoverMixin.mjs | 4 + module/api.mjs | 2 + module/consts.mjs | 11 ++ templates/Apps/HeroSkillsCardV1/content.hbs | 8 +- templates/Apps/HeroSkillsCardV1/style.css | 10 ++ 7 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 module/Apps/popovers/GenericPopoverMixin.mjs diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index b9992b5..78448a1 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -1,9 +1,10 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; -import { documentSorter, filePath } from "../../consts.mjs"; +import { documentSorter, filePath, getTooltipDelay } from "../../consts.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; +import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; @@ -39,16 +40,19 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; // #endregion + // #region Instance Data + /** @type {number | undefined} */ + #ammoTrackerHoverTimeout = null; + + /** @type {AmmoTracker | null} */ + #ammoTracker = null; + // #endregion + // #region Lifecycle async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); - - const ammo = this.element.querySelector(`.ammo`); - - ammo.addEventListener(`mouseenter`, () => {console.log(`mouseenter-ing`)}); - - ammo.addEventListener(`contextmenu`, () => {console.log(`right-clicking`)}); + await this.#createAmmoTrackerEvents(); }; static async _onRender(_context, options) { @@ -81,6 +85,13 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; + async #createAmmoTrackerEvents() { + const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); + ammoInfoIcon.addEventListener(`pointerenter`, this.#ammoInfoPointerEnter.bind(this)); + ammoInfoIcon.addEventListener(`pointerout`, this.#ammoInfoPointerOut.bind(this)); + ammoInfoIcon.addEventListener(`click`, this.#ammoInfoClick.bind(this)); + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; @@ -177,6 +188,44 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; // #endregion + // #region Event Listeners + /** + * @param {PointerEvent} event + */ + async #ammoInfoPointerEnter(event) { + console.log(event.x, event.y); + const { x, y } = event; + + this.#ammoTrackerHoverTimeout = setTimeout( + () => { + this.#ammoTrackerHoverTimeout = null; + const tracker = new AmmoTracker({ + popover: { + framed: false, + x, y, + }, + }); + tracker.render({ force: true }); + this.#ammoTracker = tracker; + }, + getTooltipDelay(), + ); + }; + + async #ammoInfoPointerOut() { + if (this.#ammoTracker) { + // this.#ammoTracker.close(); + }; + + if (this.#ammoTrackerHoverTimeout !== null) { + clearTimeout(this.#ammoTrackerHoverTimeout); + this.#ammoTrackerHoverTimeout = null; + }; + }; + + async #ammoInfoClick() {}; + // #endregion + // #region Actions // #endregion }; diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index b216185..b6d59db 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -1,9 +1,9 @@ import { filePath } from "../../consts.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) { +export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) { // #region Options static DEFAULT_OPTIONS = { classes: [ @@ -30,13 +30,163 @@ export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(Appl // #endregion // #region Instance Data + popover = {}; + constructor({ popover, ...options}) { + + // For when the caller doesn't provide anything, we want this to behave + // like a normal Application. + popover.framed ??= true; + popover.locked ??= true; + + if (popover.framed) { + options.window.frame = true; + options.window.minimizable = true; + } + + super(options); + + this.popover = popover; + }; // #endregion // #region Lifecycle + _insertElement(element) { + // console.log(this.popover); + const existing = document.getElementById(element.id); + if (existing) { + existing.replaceWith(element); + return; + }; + // const pos = element.getBoundingClientRect(); + + // const horizontalOffset = Math.floor(pos.width / 2); + // console.log({ x: this.popover.x, y: this.popover.y, height: pos.height, xOffset: horizontalOffset }); + + element.style.position = `absolute`; + element.style.color = `black`; + element.style.background = `greenyellow`; + element.style[`z-index`] = 10000; + // element.style.left = `${this.popover.x - horizontalOffset}px`; + // element.style.top = `${this.popover.y - pos.height}px`; + // this.position = { + // left: this.popover.x - horizontalOffset, + // top: this.popover.y - pos.height, + // }; + + // standard addition + document.body.append(element); + }; + + // _updatePosition(position) { + // const pos = super._updatePosition(position); + // if (this.popover.framed) { return pos }; + + // delete pos.left; + // delete pos.top; + + // const el = this.element; + // let bounds; + // let width, height; + + // // Implicit height + // if ( true ) { + // Object.assign(el.style, {width: `${width}px`, height: ""}); + // bounds = el.getBoundingClientRect(); + // height = bounds.height; + // } + + // // Implicit width + // if ( true ) { + // Object.assign(el.style, {height: `${height}px`, width: ""}); + // bounds = el.getBoundingClientRect(); + // width = bounds.width; + // } + + // // const { width, height } = this.element.getBoundingClientRect(); + // const horizontalOffset = Math.floor(width / 2); + // pos.left = this.popover.x - horizontalOffset; + // pos.top = this.popover.y - height; + + // console.log({ x: this.popover.x, y: this.popover.y, height, xOffset: horizontalOffset, width }); + + // return pos; + // } + + /** + * @override + * Custom implementation in order to make it show up approximately where I + * want it to when being created. + */ + _updatePosition(position) { + if ( !this.element ) { return position }; + const el = this.element; + let {width, height, left, top, scale} = position; + scale ??= 1.0; + const computedStyle = getComputedStyle(el); + let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0; + let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity; + let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0; + let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity; + let bounds = el.getBoundingClientRect(); + const {clientWidth, clientHeight} = document.documentElement; + + // Explicit width + const autoWidth = width === `auto`; + if ( !autoWidth ) { + const targetWidth = Number(width || bounds.width); + minWidth = parseInt(minWidth) || 0; + maxWidth = parseInt(maxWidth) || (clientWidth / scale); + width = Math.clamp(targetWidth, minWidth, maxWidth); + } + + // Explicit height + const autoHeight = height === `auto`; + if ( !autoHeight ) { + const targetHeight = Number(height || bounds.height); + minHeight = parseInt(minHeight) || 0; + maxHeight = parseInt(maxHeight) || (clientHeight / scale); + height = Math.clamp(targetHeight, minHeight, maxHeight); + } + + // Implicit height + if ( autoHeight ) { + Object.assign(el.style, {width: `${width}px`, height: ``}); + bounds = el.getBoundingClientRect(); + height = bounds.height; + } + + // Implicit width + if ( autoWidth ) { + Object.assign(el.style, {height: `${height}px`, width: ``}); + bounds = el.getBoundingClientRect(); + width = bounds.width; + } + + // Left Offset + const scaledWidth = width * scale; + const targetLeft = left ?? (this.popover.x - Math.floor( scaledWidth / 2 )); + const maxLeft = Math.max(clientWidth - scaledWidth, 0); + left = Math.clamp(targetLeft, 0, maxLeft); + + // Top Offset + const scaledHeight = height * scale; + const targetTop = top ?? (this.popover.y - scaledHeight); + const maxTop = Math.max(clientHeight - scaledHeight, 0); + top = Math.clamp(targetTop, 0, maxTop); + + // Scale + scale ??= 1.0; + return { + width: autoWidth ? `auto` : width, + height: autoHeight ? `auto` : height, + left, + top, + scale, + }; + } + async _onFirstRender(context, options) { await super._onFirstRender(context, options); - const ammoContainer = this.element.querySelector(`.ammo`); - console.dir(ammoContainer); }; // #endregion diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs new file mode 100644 index 0000000..89b7bfd --- /dev/null +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -0,0 +1,4 @@ +export function GenericPopoverMixin(HandlebarsApp) { + class GenericRipCryptPopover extends HandlebarsApp {}; + return GenericRipCryptPopover; +}; diff --git a/module/api.mjs b/module/api.mjs index a56f7b1..ee40143 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -9,6 +9,7 @@ import { RichEditor } from "./Apps/RichEditor.mjs"; import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; import { rankToInteger } from "./utils/rank.mjs"; +import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs"; const { deepFreeze } = foundry.utils; @@ -18,6 +19,7 @@ Object.defineProperty( { value: deepFreeze({ Apps: { + AmmoTracker, DicePool, CombinedHeroSheet, HeroSummaryCardV1, diff --git a/module/consts.mjs b/module/consts.mjs index 84d8740..242d332 100644 --- a/module/consts.mjs +++ b/module/consts.mjs @@ -54,3 +54,14 @@ export function documentSorter(a, b) { }; return Math.sign(a.name.localeCompare(b.name)); }; + +// MARK: getTooltipDelay +/** + * Retrieves the configured minimum delay between the user hovering an element + * and a tooltip showing up. Used for the pseudo-tooltip Applications that I use. + * + * @returns The number of milliseconds for the timeout + */ +export function getTooltipDelay() { + return 1000; // game.tooltip.constructor.TOOLTIP_ACTIVATION_MS; +}; diff --git a/templates/Apps/HeroSkillsCardV1/content.hbs b/templates/Apps/HeroSkillsCardV1/content.hbs index eee8d4a..776f673 100644 --- a/templates/Apps/HeroSkillsCardV1/content.hbs +++ b/templates/Apps/HeroSkillsCardV1/content.hbs @@ -104,7 +104,13 @@ {{/each}}
-
+
+
{{ rc-i18n "RipCrypt.common.ammo"}}
diff --git a/templates/Apps/HeroSkillsCardV1/style.css b/templates/Apps/HeroSkillsCardV1/style.css index 9c092ca..c19aaff 100644 --- a/templates/Apps/HeroSkillsCardV1/style.css +++ b/templates/Apps/HeroSkillsCardV1/style.css @@ -116,6 +116,16 @@ --input-background: var(--base-background); --input-text: var(--base-text); + &.with-icon { + grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr); + gap: 4px; + padding: 2px 0 2px 4px; + .label { + padding: 0; + } + } + + .input { margin: 2px; border-radius: 999px; From 77abcb11a98e545f3cf08f0626f1854f627746a8 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 14 Mar 2025 16:33:58 -0600 Subject: [PATCH 037/146] Get the non-framed popover working initially --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 7 +- module/Apps/popovers/AmmoTracker.mjs | 162 +------------------ module/Apps/popovers/GenericPopoverMixin.mjs | 121 +++++++++++++- templates/css/common.css | 2 + templates/css/popover.css | 5 + 5 files changed, 132 insertions(+), 165 deletions(-) create mode 100644 templates/css/popover.css diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 78448a1..6921b54 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -193,8 +193,9 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin * @param {PointerEvent} event */ async #ammoInfoPointerEnter(event) { - console.log(event.x, event.y); - const { x, y } = event; + const pos = event.target.getBoundingClientRect(); + const x = pos.x + Math.floor(pos.width / 2); + const y = pos.y; this.#ammoTrackerHoverTimeout = setTimeout( () => { @@ -214,7 +215,7 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin async #ammoInfoPointerOut() { if (this.#ammoTracker) { - // this.#ammoTracker.close(); + this.#ammoTracker.close(); }; if (this.#ammoTrackerHoverTimeout !== null) { diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index b6d59db..aeb96a9 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -7,14 +7,9 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( // #region Options static DEFAULT_OPTIONS = { classes: [ + `ripcrypt`, `ripcrypt--AmmoTracker`, ], - window: { - frame: false, - positioned: true, - resizable: false, - minimizable: false, - }, position: { width: 100, height: 30, @@ -30,164 +25,9 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( // #endregion // #region Instance Data - popover = {}; - constructor({ popover, ...options}) { - - // For when the caller doesn't provide anything, we want this to behave - // like a normal Application. - popover.framed ??= true; - popover.locked ??= true; - - if (popover.framed) { - options.window.frame = true; - options.window.minimizable = true; - } - - super(options); - - this.popover = popover; - }; // #endregion // #region Lifecycle - _insertElement(element) { - // console.log(this.popover); - const existing = document.getElementById(element.id); - if (existing) { - existing.replaceWith(element); - return; - }; - // const pos = element.getBoundingClientRect(); - - // const horizontalOffset = Math.floor(pos.width / 2); - // console.log({ x: this.popover.x, y: this.popover.y, height: pos.height, xOffset: horizontalOffset }); - - element.style.position = `absolute`; - element.style.color = `black`; - element.style.background = `greenyellow`; - element.style[`z-index`] = 10000; - // element.style.left = `${this.popover.x - horizontalOffset}px`; - // element.style.top = `${this.popover.y - pos.height}px`; - // this.position = { - // left: this.popover.x - horizontalOffset, - // top: this.popover.y - pos.height, - // }; - - // standard addition - document.body.append(element); - }; - - // _updatePosition(position) { - // const pos = super._updatePosition(position); - // if (this.popover.framed) { return pos }; - - // delete pos.left; - // delete pos.top; - - // const el = this.element; - // let bounds; - // let width, height; - - // // Implicit height - // if ( true ) { - // Object.assign(el.style, {width: `${width}px`, height: ""}); - // bounds = el.getBoundingClientRect(); - // height = bounds.height; - // } - - // // Implicit width - // if ( true ) { - // Object.assign(el.style, {height: `${height}px`, width: ""}); - // bounds = el.getBoundingClientRect(); - // width = bounds.width; - // } - - // // const { width, height } = this.element.getBoundingClientRect(); - // const horizontalOffset = Math.floor(width / 2); - // pos.left = this.popover.x - horizontalOffset; - // pos.top = this.popover.y - height; - - // console.log({ x: this.popover.x, y: this.popover.y, height, xOffset: horizontalOffset, width }); - - // return pos; - // } - - /** - * @override - * Custom implementation in order to make it show up approximately where I - * want it to when being created. - */ - _updatePosition(position) { - if ( !this.element ) { return position }; - const el = this.element; - let {width, height, left, top, scale} = position; - scale ??= 1.0; - const computedStyle = getComputedStyle(el); - let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0; - let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity; - let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0; - let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity; - let bounds = el.getBoundingClientRect(); - const {clientWidth, clientHeight} = document.documentElement; - - // Explicit width - const autoWidth = width === `auto`; - if ( !autoWidth ) { - const targetWidth = Number(width || bounds.width); - minWidth = parseInt(minWidth) || 0; - maxWidth = parseInt(maxWidth) || (clientWidth / scale); - width = Math.clamp(targetWidth, minWidth, maxWidth); - } - - // Explicit height - const autoHeight = height === `auto`; - if ( !autoHeight ) { - const targetHeight = Number(height || bounds.height); - minHeight = parseInt(minHeight) || 0; - maxHeight = parseInt(maxHeight) || (clientHeight / scale); - height = Math.clamp(targetHeight, minHeight, maxHeight); - } - - // Implicit height - if ( autoHeight ) { - Object.assign(el.style, {width: `${width}px`, height: ``}); - bounds = el.getBoundingClientRect(); - height = bounds.height; - } - - // Implicit width - if ( autoWidth ) { - Object.assign(el.style, {height: `${height}px`, width: ``}); - bounds = el.getBoundingClientRect(); - width = bounds.width; - } - - // Left Offset - const scaledWidth = width * scale; - const targetLeft = left ?? (this.popover.x - Math.floor( scaledWidth / 2 )); - const maxLeft = Math.max(clientWidth - scaledWidth, 0); - left = Math.clamp(targetLeft, 0, maxLeft); - - // Top Offset - const scaledHeight = height * scale; - const targetTop = top ?? (this.popover.y - scaledHeight); - const maxTop = Math.max(clientHeight - scaledHeight, 0); - top = Math.clamp(targetTop, 0, maxTop); - - // Scale - scale ??= 1.0; - return { - width: autoWidth ? `auto` : width, - height: autoHeight ? `auto` : height, - left, - top, - scale, - }; - } - - async _onFirstRender(context, options) { - await super._onFirstRender(context, options); - }; // #endregion // #region Actions diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index 89b7bfd..dfe2518 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -1,4 +1,123 @@ +const { ApplicationV2 } = foundry.applications.api; + export function GenericPopoverMixin(HandlebarsApp) { - class GenericRipCryptPopover extends HandlebarsApp {}; + class GenericRipCryptPopover extends HandlebarsApp { + static DEFAULT_OPTIONS = { + classes: [ + `popover`, + ], + window: { + frame: false, + positioned: true, + resizable: false, + minimizable: false, + }, + actions: {}, + }; + + popover = {}; + constructor({ popover, ...options}) { + + // For when the caller doesn't provide anything, we want this to behave + // like a normal Application instance. + popover.framed ??= true; + popover.locked ??= true; + + if (popover.framed) { + options.window.frame = true; + options.window.minimizable = true; + }; + + super(options); + + this.popover = popover; + }; + + async close(options = {}) { + if (!this.popover.framed) { + options.animate ??= false; + }; + return super.close(options); + }; + + /** + * @override + * Custom implementation in order to make it show up approximately where I + * want it to when being created. + * + * Most of this implementation is identical to the ApplicationV2 + * implementation, the biggest difference is how targetLeft and targetRight + * are calculated. + */ + _updatePosition(position) { + if (!this.element) { return position }; + if (this.popover.framed) { return position } + + const el = this.element; + let {width, height, left, top, scale} = position; + scale ??= 1.0; + const computedStyle = getComputedStyle(el); + let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0; + let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity; + let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0; + let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity; + let bounds = el.getBoundingClientRect(); + const {clientWidth, clientHeight} = document.documentElement; + + // Explicit width + const autoWidth = width === `auto`; + if ( !autoWidth ) { + const targetWidth = Number(width || bounds.width); + minWidth = parseInt(minWidth) || 0; + maxWidth = parseInt(maxWidth) || (clientWidth / scale); + width = Math.clamp(targetWidth, minWidth, maxWidth); + } + + // Explicit height + const autoHeight = height === `auto`; + if ( !autoHeight ) { + const targetHeight = Number(height || bounds.height); + minHeight = parseInt(minHeight) || 0; + maxHeight = parseInt(maxHeight) || (clientHeight / scale); + height = Math.clamp(targetHeight, minHeight, maxHeight); + } + + // Implicit height + if ( autoHeight ) { + Object.assign(el.style, {width: `${width}px`, height: ``}); + bounds = el.getBoundingClientRect(); + height = bounds.height; + } + + // Implicit width + if ( autoWidth ) { + Object.assign(el.style, {height: `${height}px`, width: ``}); + bounds = el.getBoundingClientRect(); + width = bounds.width; + } + + // Left Offset + const scaledWidth = width * scale; + const targetLeft = left ?? (this.popover.x - Math.floor( scaledWidth / 2 )); + const maxLeft = Math.max(clientWidth - scaledWidth, 0); + left = Math.clamp(targetLeft, 0, maxLeft); + + // Top Offset + const scaledHeight = height * scale; + const targetTop = top ?? (this.popover.y - scaledHeight); + const maxTop = Math.max(clientHeight - scaledHeight, 0); + top = Math.clamp(targetTop, 0, maxTop); + + // Scale + scale ??= 1.0; + return { + width: autoWidth ? `auto` : width, + height: autoHeight ? `auto` : height, + left, + top, + scale, + }; + }; + }; return GenericRipCryptPopover; }; diff --git a/templates/css/common.css b/templates/css/common.css index cb8c9f0..8d025bb 100644 --- a/templates/css/common.css +++ b/templates/css/common.css @@ -2,6 +2,8 @@ @import url("./vars.css"); +@import url("./popover.css"); + @import url("./elements/button.css"); @import url("./elements/input.css"); @import url("./elements/lists.css"); diff --git a/templates/css/popover.css b/templates/css/popover.css new file mode 100644 index 0000000..1fbb7c9 --- /dev/null +++ b/templates/css/popover.css @@ -0,0 +1,5 @@ +.ripcrypt.popover { + position: absolute; + z-index: var(--z-index-tooltip); + transform-origin: top left; +} From 4b75526708fd10081c12c4d5572c2df3b3199a44 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 14 Mar 2025 16:34:13 -0600 Subject: [PATCH 038/146] Await render call --- module/Apps/GenericApp.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 5f3a75b..17cfc0c 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -38,7 +38,7 @@ export function GenericAppMixin(HandlebarsApp) { * top after being re-rendered as normal */ async render(options = {}, _options = {}) { - super.render(options, _options); + await super.render(options, _options); const instance = foundry.applications.instances.get(this.id); if (instance !== undefined && options.orBringToFront) { instance.bringToFront(); From e594b6beb033017bcd030d6c08e73b598e0a8086 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 14 Mar 2025 22:52:40 -0600 Subject: [PATCH 039/146] Get the reusable foundations of custom popovers finished. --- eslint.config.mjs | 1 + module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 72 +++------- module/Apps/popovers/AmmoTracker.mjs | 9 +- module/Apps/popovers/GenericPopoverMixin.mjs | 43 +++++- module/utils/PopoverEventManager.mjs | 134 ++++++++++++++++++ templates/Apps/apps.css | 2 + templates/Apps/popovers/AmmoTracker/style.css | 4 + templates/css/popover.css | 17 ++- 8 files changed, 219 insertions(+), 63 deletions(-) create mode 100644 module/utils/PopoverEventManager.mjs create mode 100644 templates/Apps/popovers/AmmoTracker/style.css diff --git a/eslint.config.mjs b/eslint.config.mjs index aabd56c..70ae128 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,6 +36,7 @@ export default [ Combatant: `readonly`, canvas: `readonly`, Token: `readonly`, + Tour: `readonly`, }, }, }, diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 6921b54..9cd0016 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -1,10 +1,11 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; -import { documentSorter, filePath, getTooltipDelay } from "../../consts.mjs"; +import { documentSorter, filePath } from "../../consts.mjs"; +import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; -import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; +import { PopoverEventManager } from "../../utils/PopoverEventManager.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; @@ -40,19 +41,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; // #endregion - // #region Instance Data - /** @type {number | undefined} */ - #ammoTrackerHoverTimeout = null; - - /** @type {AmmoTracker | null} */ - #ammoTracker = null; - // #endregion - // #region Lifecycle + async _onFirstRender(context, options) { + await super._onFirstRender(context, options); + HeroSkillsCardV1._createPopoverListeners.bind(this)(); + }; + async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); - await this.#createAmmoTrackerEvents(); }; static async _onRender(_context, options) { @@ -85,11 +82,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; - async #createAmmoTrackerEvents() { + /** @type {Map} */ + #popoverManagers = new Map(); + /** @this {HeroSkillsCardV1} */ + static async _createPopoverListeners() { const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); - ammoInfoIcon.addEventListener(`pointerenter`, this.#ammoInfoPointerEnter.bind(this)); - ammoInfoIcon.addEventListener(`pointerout`, this.#ammoInfoPointerOut.bind(this)); - ammoInfoIcon.addEventListener(`click`, this.#ammoInfoClick.bind(this)); + this.#popoverManagers.set( + `.ammo-info-icon`, + new PopoverEventManager(ammoInfoIcon, AmmoTracker, { lockable: true }), + ); }; async _preparePartContext(partId, ctx, opts) { @@ -186,45 +187,14 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin } return ctx; }; - // #endregion - // #region Event Listeners - /** - * @param {PointerEvent} event - */ - async #ammoInfoPointerEnter(event) { - const pos = event.target.getBoundingClientRect(); - const x = pos.x + Math.floor(pos.width / 2); - const y = pos.y; - - this.#ammoTrackerHoverTimeout = setTimeout( - () => { - this.#ammoTrackerHoverTimeout = null; - const tracker = new AmmoTracker({ - popover: { - framed: false, - x, y, - }, - }); - tracker.render({ force: true }); - this.#ammoTracker = tracker; - }, - getTooltipDelay(), - ); - }; - - async #ammoInfoPointerOut() { - if (this.#ammoTracker) { - this.#ammoTracker.close(); - }; - - if (this.#ammoTrackerHoverTimeout !== null) { - clearTimeout(this.#ammoTrackerHoverTimeout); - this.#ammoTrackerHoverTimeout = null; + _tearDown(options) { + for (const manager of this.#popoverManagers.values()) { + manager.destroy(); }; + this.#popoverManagers.clear(); + super._tearDown(options); }; - - async #ammoInfoClick() {}; // #endregion // #region Actions diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index aeb96a9..a86e569 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -8,11 +8,12 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( static DEFAULT_OPTIONS = { classes: [ `ripcrypt`, - `ripcrypt--AmmoTracker`, ], - position: { - width: 100, - height: 30, + window: { + title: `Ammo Tracker`, + contentClasses: [ + `ripcrypt--AmmoTracker`, + ], }, actions: {}, }; diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index dfe2518..81e5721 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -1,8 +1,14 @@ const { ApplicationV2 } = foundry.applications.api; +/** + * This mixin provides the ability to designate an Application as a "popover", + * which means that it will spawn near the x/y coordinates provided it won't + * overflow the bounds of the screen. + */ export function GenericPopoverMixin(HandlebarsApp) { class GenericRipCryptPopover extends HandlebarsApp { static DEFAULT_OPTIONS = { + id: `popover-{id}`, classes: [ `popover`, ], @@ -21,21 +27,48 @@ export function GenericPopoverMixin(HandlebarsApp) { // For when the caller doesn't provide anything, we want this to behave // like a normal Application instance. popover.framed ??= true; - popover.locked ??= true; + popover.locked ??= false; + if (popover.framed) { + options.window ??= {}; options.window.frame = true; options.window.minimizable = true; - }; + } + + options.classes ??= []; + options.classes.push(popover.framed ? `framed` : `frameless`); super(options); - this.popover = popover; }; + toggleLock() { + this.popover.locked = !this.popover.locked; + this.classList.toggle(`locked`, this.popover.locked); + }; + + /** + * This render utility is intended in order to make the popovers able to be + * used in both framed and frameless mode, making sure that the content classes + * from the framed mode get shunted onto the frameless Application's root + * element. + */ + async _onFirstRender(...args) { + await super._onFirstRender(...args); + + const hasContentClasses = this.options?.window?.contentClasses?.length > 0; + if (!this.popover.framed && hasContentClasses) { + this.classList.add(...this.options.window.contentClasses); + }; + }; + async close(options = {}) { + // prevent locked popovers from being closed + if (this.popover.locked && !options.force) { return }; + if (!this.popover.framed) { - options.animate ??= false; + options.animate = false; }; return super.close(options); }; @@ -51,7 +84,7 @@ export function GenericPopoverMixin(HandlebarsApp) { */ _updatePosition(position) { if (!this.element) { return position }; - if (this.popover.framed) { return position } + if (this.popover.framed) { return super._updatePosition(position) }; const el = this.element; let {width, height, left, top, scale} = position; diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs new file mode 100644 index 0000000..777fbbd --- /dev/null +++ b/module/utils/PopoverEventManager.mjs @@ -0,0 +1,134 @@ +import { getTooltipDelay } from "../consts.mjs"; +import { Logger } from "./Logger.mjs"; + +export class PopoverEventManager { + #options; + + /** + * @param {HTMLElement} element The element to attach the listeners to. + * @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app + */ + constructor(element, popoverClass, options = {}) { + options.locked ??= false; + options.lockable ??= true; + + this.#options = options; + this.#element = element; + this.#class = popoverClass; + + element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this)); + element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this)); + element.addEventListener(`click`, this.#clickHandler.bind(this)); + + if (options.lockable) { + element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this)); + }; + }; + + destroy() { + this.close(); + this.#element.removeEventListener(`pointerenter`, this.#pointerEnterHandler); + this.#element.removeEventListener(`pointerout`, this.#pointerOutHandler); + this.#element.removeEventListener(`click`, this.#clickHandler); + if (this.#options.lockable) { + this.#element.removeEventListener(`pointerup`, this.#pointerUpHandler); + }; + this.stopOpen(); + this.stopClose(); + }; + + close() { + this.#frameless?.close({ force: true }); + this.#framed?.close({ force: true }); + }; + + stopOpen() { + if (this.#openTimeout != null) { + clearTimeout(this.#openTimeout); + this.#openTimeout = null; + }; + }; + + stopClose() { + if (this.#closeTimeout != null) { + clearTimeout(this.#closeTimeout); + this.#closeTimeout = null; + } + }; + + #element; + #class; + #openTimeout = null; + #closeTimeout = null; + + #frameless; + #framed; + + #clickHandler() { + // Cleanup for the frameless lifecycle + this.stopOpen(); + this.#frameless?.close({ force: true }); + + if (!this.#framed) { + const app = new this.#class({ popover: { ...this.#options, framed: true } }); + this.#framed = app; + } + this.#framed.render({ force: true }); + }; + + #pointerEnterHandler(event) { + this.stopClose(); + + const pos = event.target.getBoundingClientRect(); + const x = pos.x + Math.floor(pos.width / 2); + const y = pos.y; + + this.#openTimeout = setTimeout( + () => { + this.#openTimeout = null; + + // When we have the framed version rendered, we might as well just focus + // it instead of rendering a new application + if (this.#framed?.rendered) { + this.#framed.bringToFront(); + return; + }; + + if (this.#frameless?.rendered) { + const { width, height } = this.#frameless.position; + this.#frameless.render({ position: { left: x - Math.floor(width / 2), top: y - height }}); + return; + } + + this.#frameless = new this.#class({ + popover: { + ...this.#options, + framed: false, + x, y, + }, + }); + this.#frameless.render({ force: true }); + }, + getTooltipDelay(), + ); + }; + + #pointerOutHandler() { + this.stopOpen(); + + this.#closeTimeout = setTimeout( + () => { + this.#closeTimeout = null; + this.#frameless?.close(); + }, + getTooltipDelay(), + ); + }; + + #pointerUpHandler(event) { + Logger.debug(event); + if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return }; + event.preventDefault(); + this.#frameless.toggleLock(); + }; +}; diff --git a/templates/Apps/apps.css b/templates/Apps/apps.css index 2ec33cc..728dfb9 100644 --- a/templates/Apps/apps.css +++ b/templates/Apps/apps.css @@ -6,3 +6,5 @@ @import url("./HeroSummaryCardV1/style.css"); @import url("./HeroSkillsCardV1/style.css"); @import url("./RichEditor/style.css"); + +@import url("./popovers/AmmoTracker/style.css"); diff --git a/templates/Apps/popovers/AmmoTracker/style.css b/templates/Apps/popovers/AmmoTracker/style.css new file mode 100644 index 0000000..63e4d92 --- /dev/null +++ b/templates/Apps/popovers/AmmoTracker/style.css @@ -0,0 +1,4 @@ +.ripcrypt--AmmoTracker { + color: var(--base-text); + background: var(--base-background); +} diff --git a/templates/css/popover.css b/templates/css/popover.css index 1fbb7c9..05ab2dd 100644 --- a/templates/css/popover.css +++ b/templates/css/popover.css @@ -1,5 +1,16 @@ .ripcrypt.popover { - position: absolute; - z-index: var(--z-index-tooltip); - transform-origin: top left; + border-width: 2px; + border-style: solid; + border-color: transparent; + border-radius: 4px; + + &.frameless { + position: absolute; + z-index: var(--z-index-tooltip); + transform-origin: top left; + } + + &.locked { + border-color: var(--accent-3); + } } From 9ea2eebdd9a80ebcb21a30d19487f9d97d8c4960 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 14 Mar 2025 23:55:12 -0600 Subject: [PATCH 040/146] Make sure the moving works when the width/height are auto --- module/utils/PopoverEventManager.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index 777fbbd..b4b9751 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -94,9 +94,13 @@ export class PopoverEventManager { return; }; + // When the frameless is already rendered, we should just move it to the + // new location instead of spawning a new one if (this.#frameless?.rendered) { - const { width, height } = this.#frameless.position; - this.#frameless.render({ position: { left: x - Math.floor(width / 2), top: y - height }}); + const { width, height } = this.#frameless.element.getBoundingClientRect(); + const top = y - height; + const left = x - Math.floor(width / 2); + this.#frameless.setPosition({ left, top }); return; } @@ -126,7 +130,6 @@ export class PopoverEventManager { }; #pointerUpHandler(event) { - Logger.debug(event); if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return }; event.preventDefault(); this.#frameless.toggleLock(); From 88a47ba02bbd3f6b9c2a9ff5d27ec4de78e7c869 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 14 Mar 2025 23:55:48 -0600 Subject: [PATCH 041/146] Tweak default popover styles --- templates/css/popover.css | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/templates/css/popover.css b/templates/css/popover.css index 05ab2dd..0d898b2 100644 --- a/templates/css/popover.css +++ b/templates/css/popover.css @@ -1,16 +1,17 @@ .ripcrypt.popover { - border-width: 2px; - border-style: solid; - border-color: transparent; - border-radius: 4px; + box-sizing: border-box; &.frameless { + border-width: 2px; + border-style: solid; + border-color: transparent; + border-radius: 4px; position: absolute; z-index: var(--z-index-tooltip); transform-origin: top left; - } - &.locked { - border-color: var(--accent-3); + &.locked { + border-color: var(--accent-3); + } } } From e8fdf6e9522d315f335eddd7c9298da21c7c44f0 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 15 Mar 2025 14:43:08 -0600 Subject: [PATCH 042/146] Enable generic styling in frameless popovers --- templates/css/elements/button.css | 1 + templates/css/elements/input.css | 1 + templates/css/elements/lists.css | 1 + templates/css/elements/p.css | 1 + templates/css/elements/pill-bar.css | 1 + templates/css/elements/select.css | 1 + templates/css/elements/span.css | 1 + templates/css/elements/table.css | 1 + 8 files changed, 8 insertions(+) diff --git a/templates/css/elements/button.css b/templates/css/elements/button.css index 6eee9d8..14fd464 100644 --- a/templates/css/elements/button.css +++ b/templates/css/elements/button.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless button, .ripcrypt.hud button, .ripcrypt > .window-content button { all: revert; diff --git a/templates/css/elements/input.css b/templates/css/elements/input.css index 3121505..bdf6c7f 100644 --- a/templates/css/elements/input.css +++ b/templates/css/elements/input.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless, .ripcrypt.hud, .ripcrypt > .window-content { input, .input { diff --git a/templates/css/elements/lists.css b/templates/css/elements/lists.css index b55f34f..74ac6f3 100644 --- a/templates/css/elements/lists.css +++ b/templates/css/elements/lists.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless, .ripcrypt.hud, .ripcrypt > .window-content { ol { diff --git a/templates/css/elements/p.css b/templates/css/elements/p.css index fcf243a..91c29a1 100644 --- a/templates/css/elements/p.css +++ b/templates/css/elements/p.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless p, .ripcrypt.hud p, .ripcrypt > .window-content p { &.warning { diff --git a/templates/css/elements/pill-bar.css b/templates/css/elements/pill-bar.css index 425499c..ec85807 100644 --- a/templates/css/elements/pill-bar.css +++ b/templates/css/elements/pill-bar.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless .pill-bar, .ripcrypt.hud .pill-bar, .ripcrypt > .window-content .pill-bar { display: flex; diff --git a/templates/css/elements/select.css b/templates/css/elements/select.css index 0940786..be51dbb 100644 --- a/templates/css/elements/select.css +++ b/templates/css/elements/select.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless select, .ripcrypt.hud select, .ripcrypt > .window-content select { all: revert; diff --git a/templates/css/elements/span.css b/templates/css/elements/span.css index 85a099b..ef8bfa4 100644 --- a/templates/css/elements/span.css +++ b/templates/css/elements/span.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless span, .ripcrypt.hud span, .ripcrypt > .window-content span { &.small { diff --git a/templates/css/elements/table.css b/templates/css/elements/table.css index 44abd0b..e06668c 100644 --- a/templates/css/elements/table.css +++ b/templates/css/elements/table.css @@ -1,3 +1,4 @@ +.ripcrypt.popover.frameless table, .ripcrypt.hud table, .ripcrypt > .window-content table { all: revert; From 69bf712eca8a221488615ffae353faa9b566a87f Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 15 Mar 2025 17:05:14 -0600 Subject: [PATCH 043/146] Have the PopoverManager call a hook to get additional options for the popover Application --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 14 ++++++++++++-- module/utils/PopoverEventManager.mjs | 17 +++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 9cd0016..36e8ed3 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -44,12 +44,12 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin // #region Lifecycle async _onFirstRender(context, options) { await super._onFirstRender(context, options); - HeroSkillsCardV1._createPopoverListeners.bind(this)(); }; async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); + HeroSkillsCardV1._createPopoverListeners.bind(this)(); }; static async _onRender(_context, options) { @@ -84,13 +84,20 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin /** @type {Map} */ #popoverManagers = new Map(); + /** @type {Map} */ + #hookIDs = new Map(); /** @this {HeroSkillsCardV1} */ static async _createPopoverListeners() { const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); + this.#popoverManagers.set( `.ammo-info-icon`, - new PopoverEventManager(ammoInfoIcon, AmmoTracker, { lockable: true }), + new PopoverEventManager(ammoInfoIcon, AmmoTracker), ); + + this.#hookIDs.set(Hooks.on(`get${AmmoTracker.name}Options`, (opts) => { + opts.ammo = this.actor.itemTypes.ammo; + }), `get${AmmoTracker.name}Options`); }; async _preparePartContext(partId, ctx, opts) { @@ -193,6 +200,9 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin manager.destroy(); }; this.#popoverManagers.clear(); + for (const [id, hook] of this.#hookIDs.entries()) { + Hooks.off(hook, id); + } super._tearDown(options); }; // #endregion diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index b4b9751..4bb8cf1 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -1,5 +1,4 @@ import { getTooltipDelay } from "../consts.mjs"; -import { Logger } from "./Logger.mjs"; export class PopoverEventManager { #options; @@ -64,16 +63,22 @@ export class PopoverEventManager { #frameless; #framed; + #construct(options) { + const valid = Hooks.call(`get${this.#class.name}Options`, options); + if (!valid) { return }; + return new this.#class(options); + }; + #clickHandler() { // Cleanup for the frameless lifecycle this.stopOpen(); + this.stopClose(); this.#frameless?.close({ force: true }); if (!this.#framed) { - const app = new this.#class({ popover: { ...this.#options, framed: true } }); - this.#framed = app; + this.#framed = this.#construct({ popover: { ...this.#options, framed: true } }); } - this.#framed.render({ force: true }); + this.#framed?.render({ force: true }); }; #pointerEnterHandler(event) { @@ -104,14 +109,14 @@ export class PopoverEventManager { return; } - this.#frameless = new this.#class({ + this.#frameless = this.#construct({ popover: { ...this.#options, framed: false, x, y, }, }); - this.#frameless.render({ force: true }); + this.#frameless?.render({ force: true }); }, getTooltipDelay(), ); From 96e4d09e7bda2a6f82e56e46950288599cddbfc1 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 15 Mar 2025 18:35:24 -0600 Subject: [PATCH 044/146] Update the popover management to work with origin rerenders, and rerendering the popovers directly. --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 29 +++--------- module/Apps/GenericApp.mjs | 30 +++++++++++++ module/Apps/popovers/GenericPopoverMixin.mjs | 17 +++++++- module/utils/PopoverEventManager.mjs | 46 ++++++++++++++++++-- 4 files changed, 93 insertions(+), 29 deletions(-) diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 36e8ed3..ccffbef 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -82,22 +82,16 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; - /** @type {Map} */ - #popoverManagers = new Map(); - /** @type {Map} */ - #hookIDs = new Map(); /** @this {HeroSkillsCardV1} */ static async _createPopoverListeners() { const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); + const idPrefix = this.actor.uuid; - this.#popoverManagers.set( - `.ammo-info-icon`, - new PopoverEventManager(ammoInfoIcon, AmmoTracker), - ); - - this.#hookIDs.set(Hooks.on(`get${AmmoTracker.name}Options`, (opts) => { - opts.ammo = this.actor.itemTypes.ammo; - }), `get${AmmoTracker.name}Options`); + const manager = new PopoverEventManager(`${idPrefix}.ammo-info-icon`, ammoInfoIcon, AmmoTracker); + this._popoverManagers.set(`.ammo-info-icon`, manager); + this._hookIDs.set(Hooks.on(`prepare${manager.id}Context`, (ctx) => { + ctx.ammos = this.actor.itemTypes.ammo; + }), `prepare${manager.id}Context`); }; async _preparePartContext(partId, ctx, opts) { @@ -194,17 +188,6 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin } return ctx; }; - - _tearDown(options) { - for (const manager of this.#popoverManagers.values()) { - manager.destroy(); - }; - this.#popoverManagers.clear(); - for (const [id, hook] of this.#hookIDs.entries()) { - Hooks.off(hook, id); - } - super._tearDown(options); - }; // #endregion // #region Actions diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 17cfc0c..7c1077e 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -31,6 +31,13 @@ export function GenericAppMixin(HandlebarsApp) { }; // #endregion + // #region Instance Data + /** @type {Map} */ + _popoverManagers = new Map(); + /** @type {Map} */ + _hookIDs = new Map(); + // #endregion + // #region Lifecycle /** * @override @@ -45,6 +52,13 @@ export function GenericAppMixin(HandlebarsApp) { }; }; + async _onRender() { + await super._onRender(); + for (const manager of this._popoverManagers.values()) { + manager.render(); + }; + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); delete ctx.document; @@ -60,6 +74,22 @@ export function GenericAppMixin(HandlebarsApp) { return ctx; }; + + _tearDown(options) { + // Clear all popovers associated with the app + for (const manager of this._popoverManagers.values()) { + manager.destroy(); + }; + this._popoverManagers.clear(); + + // Remove any hooks added for this app + for (const [id, hook] of this._hookIDs.entries()) { + Hooks.off(hook, id); + }; + this._hookIDs.clear(); + + super._tearDown(options); + }; // #endregion // #region Actions diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index 81e5721..017c2b0 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -3,7 +3,10 @@ const { ApplicationV2 } = foundry.applications.api; /** * This mixin provides the ability to designate an Application as a "popover", * which means that it will spawn near the x/y coordinates provided it won't - * overflow the bounds of the screen. + * overflow the bounds of the screen. This also implements a _preparePartContext + * in order to allow the parent application passing new data into the popover + * whenever it rerenders; how the popover handles this data is up to the + * specific implementation. */ export function GenericPopoverMixin(HandlebarsApp) { class GenericRipCryptPopover extends HandlebarsApp { @@ -29,7 +32,6 @@ export function GenericPopoverMixin(HandlebarsApp) { popover.framed ??= true; popover.locked ??= false; - if (popover.framed) { options.window ??= {}; options.window.frame = true; @@ -151,6 +153,17 @@ export function GenericPopoverMixin(HandlebarsApp) { scale, }; }; + + /** + * This is here in order allow things that are not this Application + * to provide / augment the context data for the lifecycle of the app. + */ + async _prepareContext(_partId, _context, options) { + const context = {}; + Hooks.callAll(`prepare${this.constructor.name}Context`, context, options); + Hooks.callAll(`prepare${this.popover.managerId}Context`, context, options); + return context; + }; }; return GenericRipCryptPopover; }; diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index 4bb8cf1..6c0b0c8 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -1,13 +1,28 @@ import { getTooltipDelay } from "../consts.mjs"; +import { Logger } from "./Logger.mjs"; export class PopoverEventManager { #options; + id; + + /** @type {Map} */ + static #existing = new Map(); /** * @param {HTMLElement} element The element to attach the listeners to. * @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app */ - constructor(element, popoverClass, options = {}) { + constructor(id, element, popoverClass, options = {}) { + id = `${id}-${popoverClass.name}`; + this.id = id; + + if (PopoverEventManager.#existing.has(id)) { + const manager = PopoverEventManager.#existing.get(id); + manager.#addListeners(element); + return manager; + }; + + options.managerId = id; options.locked ??= false; options.lockable ??= true; @@ -15,11 +30,19 @@ export class PopoverEventManager { this.#element = element; this.#class = popoverClass; + this.#addListeners(element); + PopoverEventManager.#existing.set(id, this); + }; + + /** + * @param {HTMLElement} element + */ + #addListeners(element) { element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this)); element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this)); element.addEventListener(`click`, this.#clickHandler.bind(this)); - if (options.lockable) { + if (this.#options.lockable) { element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this)); }; }; @@ -55,6 +78,19 @@ export class PopoverEventManager { } }; + get rendered() { + return Boolean(this.#frameless?.rendered || this.#framed?.rendered); + }; + + render(options) { + if (this.#framed?.rendered) { + this.#framed.render(options); + }; + if (this.#frameless?.rendered) { + this.#frameless.render(options); + }; + }; + #element; #class; #openTimeout = null; @@ -64,12 +100,14 @@ export class PopoverEventManager { #framed; #construct(options) { - const valid = Hooks.call(`get${this.#class.name}Options`, options); - if (!valid) { return }; + options.popover ??= {}; + options.popover.managerId = this.id; + return new this.#class(options); }; #clickHandler() { + Logger.debug(`click event handler`); // Cleanup for the frameless lifecycle this.stopOpen(); this.stopClose(); From 3ae7e9489a248f8440c5e027dc0f6b222a5841cd Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 15 Mar 2025 18:36:04 -0600 Subject: [PATCH 045/146] Add ammo to the gear types, and get more design stuff for the AmmoTracker --- langs/en-ca.json | 3 +- module/Apps/popovers/AmmoTracker.mjs | 10 ++++- module/gameTerms.mjs | 1 + .../Apps/popovers/AmmoTracker/ammoList.hbs | 23 +++++++++++ .../Apps/popovers/AmmoTracker/content.hbs | 3 -- templates/Apps/popovers/AmmoTracker/style.css | 40 +++++++++++++++++-- templates/css/elements/button.css | 3 +- templates/css/elements/lists.css | 3 ++ templates/css/themes/dark.css | 10 +++++ 9 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 templates/Apps/popovers/AmmoTracker/ammoList.hbs delete mode 100644 templates/Apps/popovers/AmmoTracker/content.hbs diff --git a/langs/en-ca.json b/langs/en-ca.json index c40e853..badf3f2 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -172,7 +172,8 @@ "numberOfDice": "# of Dice", "rollTarget": "Target", "difficulty": "(DC: {dc})", - "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost." + "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost.", + "no-ammo": "You don't have any ammo!" }, "notifs": { "error": { diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index a86e569..e1203c0 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -19,8 +19,8 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( }; static PARTS = { - main: { - template: filePath(`templates/Apps/popovers/AmmoTracker/content.hbs`), + ammoList: { + template: filePath(`templates/Apps/popovers/AmmoTracker/ammoList.hbs`), }, }; // #endregion @@ -29,6 +29,12 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( // #endregion // #region Lifecycle + async _preparePartContext(partId, data) { + const ctx = { partId }; + ctx.canPin = false; + ctx.ammos = data.ammos; + return ctx; + }; // #endregion // #region Actions diff --git a/module/gameTerms.mjs b/module/gameTerms.mjs index 5bcbe71..86507ad 100644 --- a/module/gameTerms.mjs +++ b/module/gameTerms.mjs @@ -37,6 +37,7 @@ export const gameTerms = Object.preventExtensions({ }), /** The types of items that contribute to the gear limit */ gearItemTypes: new Set([ + `ammo`, `armour`, `weapon`, `shield`, diff --git a/templates/Apps/popovers/AmmoTracker/ammoList.hbs b/templates/Apps/popovers/AmmoTracker/ammoList.hbs new file mode 100644 index 0000000..87118d7 --- /dev/null +++ b/templates/Apps/popovers/AmmoTracker/ammoList.hbs @@ -0,0 +1,23 @@ +
+ {{#if ammos}} +
    + {{#each ammos as | ammo |}} +
    + {{ ammo.name }} + {{ ammo.system.quantity }} + +
    + {{/each}} +
+ {{else}} + + {{ rc-i18n "RipCrypt.Apps.no-ammo" }} + + {{/if}} +
\ No newline at end of file diff --git a/templates/Apps/popovers/AmmoTracker/content.hbs b/templates/Apps/popovers/AmmoTracker/content.hbs deleted file mode 100644 index 80f0024..0000000 --- a/templates/Apps/popovers/AmmoTracker/content.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
- Hello -
\ No newline at end of file diff --git a/templates/Apps/popovers/AmmoTracker/style.css b/templates/Apps/popovers/AmmoTracker/style.css index 63e4d92..1f1cf1d 100644 --- a/templates/Apps/popovers/AmmoTracker/style.css +++ b/templates/Apps/popovers/AmmoTracker/style.css @@ -1,4 +1,38 @@ -.ripcrypt--AmmoTracker { - color: var(--base-text); - background: var(--base-background); +.ripcrypt--AmmoTracker.ripcrypt--AmmoTracker { + color: var(--popover-text); + background: var(--popover-background); + padding: 4px 8px; + + --button-text: var(--header-text); + --button-background: var(--header-background); + + button { + border-radius: 4px; + } + + ul { + &:nth-child(even) { + color: var(--popover-alt-row-text); + background: var(--popover-alt-row-background); + --button-text: unset; + --button-background: unset; + } + } + + .ammo { + display: grid; + grid-template-columns: 150px 30px min-content; + grid-template-rows: min-content; + align-items: center; + justify-items: center; + /* display: flex; + flex-direction: row; + align-items: center; */ + gap: 8px; + + .name { + flex-grow: 1; + justify-self: left; + } + } } diff --git a/templates/css/elements/button.css b/templates/css/elements/button.css index 14fd464..e912a42 100644 --- a/templates/css/elements/button.css +++ b/templates/css/elements/button.css @@ -1,5 +1,4 @@ -.ripcrypt.popover.frameless button, -.ripcrypt.hud button, +.ripcrypt:where(.popover.frameless, .hud) button, .ripcrypt > .window-content button { all: revert; outline: none; diff --git a/templates/css/elements/lists.css b/templates/css/elements/lists.css index 74ac6f3..24aa561 100644 --- a/templates/css/elements/lists.css +++ b/templates/css/elements/lists.css @@ -36,6 +36,9 @@ } ul { + margin: 0; + padding: 0; + > li { margin: 0; } diff --git a/templates/css/themes/dark.css b/templates/css/themes/dark.css index aa4a8a4..b454025 100644 --- a/templates/css/themes/dark.css +++ b/templates/css/themes/dark.css @@ -27,6 +27,16 @@ --col-gap: 2px; --row-gap: 0px; + /* Popover Variables */ + --popover-text: var(--base-text); + --popover-background: var(--base-background); + + --popover-alt-row-text: var(--alt-row-text); + --popover-alt-row-background: var(--alt-row-background); + + --popover-header-text: var(--header-text); + --popover-header-background: var(--header-background); + /* Additional Variables */ --string-tags-border: inherit; --string-tags-tag-background: inherit; From 032f2564c140f7368c691459fc02a23c5a8ae3e0 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 16 Mar 2025 10:53:57 -0600 Subject: [PATCH 046/146] Make the stopEvent methods private --- module/utils/PopoverEventManager.mjs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index 6c0b0c8..ff1fd74 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -55,8 +55,8 @@ export class PopoverEventManager { if (this.#options.lockable) { this.#element.removeEventListener(`pointerup`, this.#pointerUpHandler); }; - this.stopOpen(); - this.stopClose(); + this.#stopOpen(); + this.#stopClose(); }; close() { @@ -64,14 +64,14 @@ export class PopoverEventManager { this.#framed?.close({ force: true }); }; - stopOpen() { + #stopOpen() { if (this.#openTimeout != null) { clearTimeout(this.#openTimeout); this.#openTimeout = null; }; }; - stopClose() { + #stopClose() { if (this.#closeTimeout != null) { clearTimeout(this.#closeTimeout); this.#closeTimeout = null; @@ -109,8 +109,8 @@ export class PopoverEventManager { #clickHandler() { Logger.debug(`click event handler`); // Cleanup for the frameless lifecycle - this.stopOpen(); - this.stopClose(); + this.#stopOpen(); + this.#stopClose(); this.#frameless?.close({ force: true }); if (!this.#framed) { @@ -120,7 +120,7 @@ export class PopoverEventManager { }; #pointerEnterHandler(event) { - this.stopClose(); + this.#stopClose(); const pos = event.target.getBoundingClientRect(); const x = pos.x + Math.floor(pos.width / 2); @@ -161,7 +161,7 @@ export class PopoverEventManager { }; #pointerOutHandler() { - this.stopOpen(); + this.#stopOpen(); this.#closeTimeout = setTimeout( () => { From 3437dadb9b5c099d9bc5f625741f206392573fd1 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 16 Mar 2025 10:57:53 -0600 Subject: [PATCH 047/146] Reduce the z-index of the popovers a little bit --- templates/css/popover.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/css/popover.css b/templates/css/popover.css index 0d898b2..73cfe4a 100644 --- a/templates/css/popover.css +++ b/templates/css/popover.css @@ -7,7 +7,7 @@ border-color: transparent; border-radius: 4px; position: absolute; - z-index: var(--z-index-tooltip); + z-index: calc(var(--z-index-tooltip) - 5); transform-origin: top left; &.locked { From 7d39c487dc21a2cc514aed5ca9053ee4d6e90d5f Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Mar 2025 14:34:20 -0600 Subject: [PATCH 048/146] Prevent deprecation warnings as of v13.338 --- eslint.config.mjs | 2 -- module/hooks/init.mjs | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 70ae128..a0d500d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,9 +22,7 @@ export default [ Hooks: `readonly`, ui: `readonly`, Actor: `readonly`, - Actors: `readonly`, Item: `readonly`, - Items: `readonly`, foundry: `readonly`, ChatMessage: `readonly`, ActiveEffect: `readonly`, diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 48c3171..d829eb4 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -35,6 +35,9 @@ import { registerMetaSettings } from "../settings/metaSettings.mjs"; 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`); @@ -70,10 +73,8 @@ Hooks.once(`init`, () => { // #region Sheets // Unregister core sheets - /* eslint-disable no-undef */ Items.unregisterSheet(`core`, ItemSheet); Actors.unregisterSheet(`core`, ActorSheet); - /* eslint-enabled no-undef */ // #region Actors Actors.registerSheet(game.system.id, CombinedHeroSheet, { From c495f45015250549551c8c6dea9a4f4bf6d96b10 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 22 Mar 2025 21:20:02 -0600 Subject: [PATCH 049/146] Get the base favourite mechanism working so the items are visible on the skills card --- assets/_credit.txt | 2 + assets/icons/star-empty.svg | 1 + assets/icons/star.svg | 1 + langs/en-ca.json | 9 ++- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 18 +++++- module/Apps/GenericApp.mjs | 5 ++ module/Apps/popovers/AmmoTracker.mjs | 57 ++++++++++++++++++- module/Apps/popovers/GenericPopoverMixin.mjs | 2 +- templates/Apps/HeroSkillsCardV1/content.hbs | 18 ++++++ templates/Apps/HeroSkillsCardV1/style.css | 8 ++- .../Apps/popovers/AmmoTracker/ammoList.hbs | 45 ++++++++++----- templates/Apps/popovers/AmmoTracker/style.css | 22 +++++-- templates/css/common.css | 1 + templates/css/elements/button.css | 3 + 14 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 assets/icons/star-empty.svg create mode 100644 assets/icons/star.svg diff --git a/assets/_credit.txt b/assets/_credit.txt index 43cf3fb..bac6dc2 100644 --- a/assets/_credit.txt +++ b/assets/_credit.txt @@ -1,6 +1,8 @@ Oliver Akins: - geist-silhouette.v2.svg : All rights reserved. - caster-silhouette.v1.svg : All rights reserved. + - icons/star-empty.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole + - icons/star.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole Kýnan Antos (Gritsilk Games): - hero-silhouette.svg : Licensed to Distribute and Modify within the bounds of the "Foundry-RipCrypt" system. diff --git a/assets/icons/star-empty.svg b/assets/icons/star-empty.svg new file mode 100644 index 0000000..8760cc9 --- /dev/null +++ b/assets/icons/star-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000..829431b --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/langs/en-ca.json b/langs/en-ca.json index badf3f2..de9e051 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -173,12 +173,17 @@ "rollTarget": "Target", "difficulty": "(DC: {dc})", "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost.", - "no-ammo": "You don't have any ammo!" + "AmmoTracker": { + "no-ammo": "You don't have any ammo!", + "pin-button": "Pin {name} to your character sheet", + "pin-button-tooltip": "Pin {name}" + } }, "notifs": { "error": { "cannot-equip": "Cannot equip the {itemType}, see console for more details.", - "invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action." + "invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action.", + "at-favourite-limit": "Cannot favourite more than three items, unfavourite one to make space." }, "warn": { "cannot-go-negative": "\"{name}\" is unable to be a negative number." diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index ccffbef..be8fb26 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -139,7 +139,23 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; static async prepareAmmo(ctx) { - ctx.ammo = 0; + let total = 0; + ctx.favouriteAmmo = []; + + for (const ammo of ctx.actor.itemTypes.ammo) { + total += ammo.system.quantity; + + if (ctx.favouriteAmmo.length < 3 && ammo.getFlag(game.system.id, `favourited`)) { + ctx.favouriteAmmo.push({ + uuid: ammo.uuid, + name: ammo.name, + quantity: ammo.system.quantity, + }); + }; + }; + ctx.favouriteAmmo.length = 3; // assert array length + + ctx.ammo = total; return ctx; }; diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 7c1077e..2e9c1c2 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -52,6 +52,11 @@ export function GenericAppMixin(HandlebarsApp) { }; }; + /** + * @override + * This override makes it so that if the application has any framable popovers + * within it that are currently open, they will rerender as well. + */ async _onRender() { await super._onRender(); for (const manager of this._popoverManagers.values()) { diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index e1203c0..8c4c798 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -1,5 +1,7 @@ import { filePath } from "../../consts.mjs"; import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs"; +import { localizer } from "../../utils/Localizer.mjs"; +import { Logger } from "../../utils/Logger.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -15,7 +17,10 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( `ripcrypt--AmmoTracker`, ], }, - actions: {}, + actions: { + favourite: this.#favourite, + unfavourite: this.#unfavourite, + }, }; static PARTS = { @@ -26,17 +31,63 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( // #endregion // #region Instance Data + _favouriteCount = 0; // #endregion // #region Lifecycle async _preparePartContext(partId, data) { const ctx = { partId }; - ctx.canPin = false; - ctx.ammos = data.ammos; + + let favouriteCount = 0; + ctx.ammos = data.ammos.map(ammo => { + const favourite = ammo.getFlag(game.system.id, `favourited`) ?? false; + if (favourite) { favouriteCount++ }; + + return { + ammo, + favourite, + }; + }); + + this._favouriteCount = favouriteCount; + ctx.atFavouriteLimit = favouriteCount >= 3; return ctx; }; // #endregion // #region Actions + static async #favourite(_, el) { + const targetEl = el.closest(`[data-item-id]`); + if (!targetEl) { + Logger.warn(`Cannot find a parent element with data-item-id`); + return; + }; + + // get count of favourites + if (this._favouriteCount > 3) { + ui.notifications.error(localizer(`RipCrypt.notifs.error.at-favourite-limit`)); + return; + }; + + const data = targetEl.dataset; + const item = await fromUuid(data.itemId); + if (!item) { return }; + + item.setFlag(game.system.id, `favourited`, true); + }; + + static async #unfavourite(_, el) { + const targetEl = el.closest(`[data-item-id]`); + if (!targetEl) { + Logger.warn(`Cannot find a parent element with data-item-id`); + return; + }; + + const data = targetEl.dataset; + const item = await fromUuid(data.itemId); + if (!item) { return }; + + item.unsetFlag(game.system.id, `favourited`); + }; // #endregion }; diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index 017c2b0..a64b067 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -81,7 +81,7 @@ export function GenericPopoverMixin(HandlebarsApp) { * want it to when being created. * * Most of this implementation is identical to the ApplicationV2 - * implementation, the biggest difference is how targetLeft and targetRight + * implementation, the biggest difference is how targetLeft and targetTop * are calculated. */ _updatePosition(position) { diff --git a/templates/Apps/HeroSkillsCardV1/content.hbs b/templates/Apps/HeroSkillsCardV1/content.hbs index 776f673..5259737 100644 --- a/templates/Apps/HeroSkillsCardV1/content.hbs +++ b/templates/Apps/HeroSkillsCardV1/content.hbs @@ -118,6 +118,24 @@ {{ ammo }}
+ {{#each favouriteAmmo as | data |}} + {{#if data}} +
+ + +
+ {{else}} + {{/if}} + {{/each}} {{!-- * Currencies --}}
diff --git a/templates/Apps/HeroSkillsCardV1/style.css b/templates/Apps/HeroSkillsCardV1/style.css index c19aaff..bf35321 100644 --- a/templates/Apps/HeroSkillsCardV1/style.css +++ b/templates/Apps/HeroSkillsCardV1/style.css @@ -8,6 +8,7 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: repeat(13, minmax(0, 1fr)); column-gap: var(--col-gap); + row-gap: var(--row-gap); background: var(--base-background); color: var(--base-text); @@ -125,8 +126,13 @@ } } + label, .label { + white-space: nowrap; + text-overflow: ellipsis; + } - .input { + + input, .input { margin: 2px; border-radius: 999px; text-align: center; diff --git a/templates/Apps/popovers/AmmoTracker/ammoList.hbs b/templates/Apps/popovers/AmmoTracker/ammoList.hbs index 87118d7..a7dc8b5 100644 --- a/templates/Apps/popovers/AmmoTracker/ammoList.hbs +++ b/templates/Apps/popovers/AmmoTracker/ammoList.hbs @@ -1,18 +1,37 @@
{{#if ammos}}
    - {{#each ammos as | ammo |}} -
    - {{ ammo.name }} - {{ ammo.system.quantity }} - -
    + {{#each ammos as | data |}} +
  • + {{ data.ammo.name }} + {{ data.ammo.system.quantity }} + {{#if data.favourite}} + + {{else}} + + {{/if}} +
  • {{/each}}
{{else}} @@ -20,4 +39,4 @@ {{ rc-i18n "RipCrypt.Apps.no-ammo" }} {{/if}} -
\ No newline at end of file +
diff --git a/templates/Apps/popovers/AmmoTracker/style.css b/templates/Apps/popovers/AmmoTracker/style.css index 1f1cf1d..d81ef2d 100644 --- a/templates/Apps/popovers/AmmoTracker/style.css +++ b/templates/Apps/popovers/AmmoTracker/style.css @@ -1,8 +1,9 @@ .ripcrypt--AmmoTracker.ripcrypt--AmmoTracker { color: var(--popover-text); background: var(--popover-background); - padding: 4px 8px; + padding: 4px; + --row-gap: 4px; --button-text: var(--header-text); --button-background: var(--header-background); @@ -11,11 +12,20 @@ } ul { - &:nth-child(even) { - color: var(--popover-alt-row-text); - background: var(--popover-alt-row-background); - --button-text: unset; - --button-background: unset; + display: flex; + flex-direction: column; + row-gap: var(--row-gap); + + > li { + padding: 4px 8px; + border-radius: 999px; + + &:nth-child(even) { + color: var(--popover-alt-row-text); + background: var(--popover-alt-row-background); + --button-text: unset; + --button-background: unset; + } } } diff --git a/templates/css/common.css b/templates/css/common.css index 8d025bb..1b2758e 100644 --- a/templates/css/common.css +++ b/templates/css/common.css @@ -29,6 +29,7 @@ /* height: 270px; */ width: 680px; --col-gap: 2px; + --row-gap: 4px; } label, input, select { diff --git a/templates/css/elements/button.css b/templates/css/elements/button.css index e912a42..62431e7 100644 --- a/templates/css/elements/button.css +++ b/templates/css/elements/button.css @@ -1,6 +1,9 @@ .ripcrypt:where(.popover.frameless, .hud) button, .ripcrypt > .window-content button { all: revert; + display: flex; + justify-content: center; + align-items: center; outline: none; border: none; padding: 2px 4px; From a7e0fe899a8683a6218a148e3b9c1d41e3065973 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 00:43:44 -0600 Subject: [PATCH 050/146] Display all of the pinned ammo on the sheet and tweak the list header style. --- langs/en-ca.json | 7 +++++-- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 11 ++++++----- templates/Apps/HeroSkillsCardV1/content.hbs | 17 ++++++++++------- templates/Apps/HeroSkillsCardV1/style.css | 14 ++++++-------- .../Apps/popovers/AmmoTracker/ammoList.hbs | 8 +++++--- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/langs/en-ca.json b/langs/en-ca.json index de9e051..1d2ad3e 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -173,10 +173,13 @@ "rollTarget": "Target", "difficulty": "(DC: {dc})", "RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost.", + "starred-ammo-placeholder": "Starred Ammo Slot", "AmmoTracker": { "no-ammo": "You don't have any ammo!", - "pin-button": "Pin {name} to your character sheet", - "pin-button-tooltip": "Pin {name}" + "star-button": "Add {name} as a starred ammo", + "star-button-tooltip": "Add Star", + "unstar-button": "Remove {name} as a starred ammo", + "unstar-button-tooltip": "Remove Star" } }, "notifs": { diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index be8fb26..7b1301a 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -140,20 +140,21 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin static async prepareAmmo(ctx) { let total = 0; - ctx.favouriteAmmo = []; + let favouriteCount = 0; + ctx.favouriteAmmo = new Array(3).fill(null); for (const ammo of ctx.actor.itemTypes.ammo) { total += ammo.system.quantity; - if (ctx.favouriteAmmo.length < 3 && ammo.getFlag(game.system.id, `favourited`)) { - ctx.favouriteAmmo.push({ + if (favouriteCount < 3 && ammo.getFlag(game.system.id, `favourited`)) { + ctx.favouriteAmmo[favouriteCount] = { uuid: ammo.uuid, name: ammo.name, quantity: ammo.system.quantity, - }); + }; + favouriteCount++; }; }; - ctx.favouriteAmmo.length = 3; // assert array length ctx.ammo = total; return ctx; diff --git a/templates/Apps/HeroSkillsCardV1/content.hbs b/templates/Apps/HeroSkillsCardV1/content.hbs index 5259737..6f8c11b 100644 --- a/templates/Apps/HeroSkillsCardV1/content.hbs +++ b/templates/Apps/HeroSkillsCardV1/content.hbs @@ -104,7 +104,7 @@ {{/each}} -
+
-
{{else}} +
+ {{ rc-i18n "RipCrypt.Apps.starred-ammo-placeholder" }} +
{{/if}} {{/each}} {{!-- * Currencies --}}
-
+
@@ -150,7 +153,7 @@ value="0" >
-
+
@@ -161,7 +164,7 @@ value="0" >
-
+
diff --git a/templates/Apps/HeroSkillsCardV1/style.css b/templates/Apps/HeroSkillsCardV1/style.css index bf35321..8c654b0 100644 --- a/templates/Apps/HeroSkillsCardV1/style.css +++ b/templates/Apps/HeroSkillsCardV1/style.css @@ -36,6 +36,7 @@ display: flex; justify-content: space-between; align-items: center; + border-radius: 999px; } .skill-list { display: grid; @@ -107,33 +108,30 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); } - .half-pill { + .pill { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); align-items: center; background: var(--section-header-background); - border-radius: 0 999px 999px 0; + border-radius: 999px; color: var(--section-header-text); + padding: 2px 0 2px 4px; --input-background: var(--base-background); --input-text: var(--base-text); &.with-icon { grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr); gap: 4px; - padding: 2px 0 2px 4px; - .label { - padding: 0; - } } label, .label { + padding: 0; white-space: nowrap; text-overflow: ellipsis; } - input, .input { - margin: 2px; + margin: 0 2px 0 0; border-radius: 999px; text-align: center; } diff --git a/templates/Apps/popovers/AmmoTracker/ammoList.hbs b/templates/Apps/popovers/AmmoTracker/ammoList.hbs index a7dc8b5..67d43b2 100644 --- a/templates/Apps/popovers/AmmoTracker/ammoList.hbs +++ b/templates/Apps/popovers/AmmoTracker/ammoList.hbs @@ -1,4 +1,4 @@ -
+
{{#if ammos}}
    {{#each ammos as | data |}} @@ -10,7 +10,8 @@ type="button" class="icon" data-action="unfavourite" - aria-label="Unpin ammo" + aria-label="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.unstar-button" name=data.ammo.name }}" + data-tooltip="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.unstar-button-tooltip" name=data.ammo.name }}" > Date: Sat, 5 Apr 2025 15:29:49 -0600 Subject: [PATCH 051/146] Localize the app title --- langs/en-ca.json | 3 +++ module/Apps/popovers/AmmoTracker.mjs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/langs/en-ca.json b/langs/en-ca.json index 1d2ad3e..1fef505 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -21,6 +21,9 @@ "HeroCraftCardV1": "Hero Craft Card", "HeroSkillsCardV1": "Hero Skill Card" }, + "app-titles": { + "AmmoTracker": "Ammo Tracker" + }, "common": { "abilities": { "grit": "Grit", diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index 8c4c798..6e0e399 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -12,7 +12,7 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( `ripcrypt`, ], window: { - title: `Ammo Tracker`, + title: `RipCrypt.app-titles.AmmoTracker`, contentClasses: [ `ripcrypt--AmmoTracker`, ], From 7ae5d1b814359bb42b7a996a3ddd81c6d2c0d2de Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 15:30:00 -0600 Subject: [PATCH 052/146] Get rid of extraneous function override --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 7b1301a..cccc359 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -42,10 +42,6 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin // #endregion // #region Lifecycle - async _onFirstRender(context, options) { - await super._onFirstRender(context, options); - }; - async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); From 95443d3709b961c74ccfbfb0bf4c91a96383b929 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 15:30:26 -0600 Subject: [PATCH 053/146] Pull the tooltip delay from the Foundry tooltip class --- module/consts.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/consts.mjs b/module/consts.mjs index 242d332..405707e 100644 --- a/module/consts.mjs +++ b/module/consts.mjs @@ -63,5 +63,5 @@ export function documentSorter(a, b) { * @returns The number of milliseconds for the timeout */ export function getTooltipDelay() { - return 1000; // game.tooltip.constructor.TOOLTIP_ACTIVATION_MS; + return game.tooltip.constructor.TOOLTIP_ACTIVATION_MS; }; From bfddf855a48b25424c22537da424abe95338132c Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 15:31:28 -0600 Subject: [PATCH 054/146] Move the magic string into an enum --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 3 ++- module/Apps/popovers/AmmoTracker.mjs | 7 ++++--- module/api.mjs | 6 +++++- module/flags/item.mjs | 4 ++++ 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 module/flags/item.mjs diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index cccc359..76f0e9d 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -3,6 +3,7 @@ import { documentSorter, filePath } from "../../consts.mjs"; import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; import { gameTerms } from "../../gameTerms.mjs"; import { GenericAppMixin } from "../GenericApp.mjs"; +import { ItemFlags } from "../../flags/item.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; import { PopoverEventManager } from "../../utils/PopoverEventManager.mjs"; @@ -142,7 +143,7 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin for (const ammo of ctx.actor.itemTypes.ammo) { total += ammo.system.quantity; - if (favouriteCount < 3 && ammo.getFlag(game.system.id, `favourited`)) { + if (favouriteCount < 3 && ammo.getFlag(game.system.id, ItemFlags.FAVOURITE)) { ctx.favouriteAmmo[favouriteCount] = { uuid: ammo.uuid, name: ammo.name, diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index 6e0e399..c91e3a4 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -1,5 +1,6 @@ import { filePath } from "../../consts.mjs"; import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs"; +import { ItemFlags } from "../../flags/item.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; @@ -40,7 +41,7 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( let favouriteCount = 0; ctx.ammos = data.ammos.map(ammo => { - const favourite = ammo.getFlag(game.system.id, `favourited`) ?? false; + const favourite = ammo.getFlag(game.system.id, ItemFlags.FAVOURITE) ?? false; if (favourite) { favouriteCount++ }; return { @@ -73,7 +74,7 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( const item = await fromUuid(data.itemId); if (!item) { return }; - item.setFlag(game.system.id, `favourited`, true); + item.setFlag(game.system.id, ItemFlags.FAVOURITE, true); }; static async #unfavourite(_, el) { @@ -87,7 +88,7 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( const item = await fromUuid(data.itemId); if (!item) { return }; - item.unsetFlag(game.system.id, `favourited`); + item.unsetFlag(game.system.id, ItemFlags.FAVOURITE); }; // #endregion }; diff --git a/module/api.mjs b/module/api.mjs index ee40143..389ca3e 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -1,4 +1,5 @@ // App imports +import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs"; import { CombinedHeroSheet } from "./Apps/ActorSheets/CombinedHeroSheet.mjs"; import { DicePool } from "./Apps/DicePool.mjs"; import { HeroSkillsCardV1 } from "./Apps/ActorSheets/HeroSkillsCardV1.mjs"; @@ -9,7 +10,9 @@ import { RichEditor } from "./Apps/RichEditor.mjs"; import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; import { rankToInteger } from "./utils/rank.mjs"; -import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs"; + +// Misc Imports +import { ItemFlags } from "./flags/item.mjs"; const { deepFreeze } = foundry.utils; @@ -33,6 +36,7 @@ Object.defineProperty( previousFate, rankToInteger, }, + ItemFlags, }), writable: false, }, diff --git a/module/flags/item.mjs b/module/flags/item.mjs new file mode 100644 index 0000000..de53d3b --- /dev/null +++ b/module/flags/item.mjs @@ -0,0 +1,4 @@ +export const ItemFlags = Object.freeze({ + /** The boolean value to indicate if an item is considered favourited/starred or not */ + FAVOURITE: `favourited`, +}); From 228cc21de7216b97394c97bb238691c0f5d09041 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 15:32:31 -0600 Subject: [PATCH 055/146] Make the ID publicly readonly, privately writable --- module/utils/PopoverEventManager.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index ff1fd74..07e4793 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -3,7 +3,11 @@ import { Logger } from "./Logger.mjs"; export class PopoverEventManager { #options; - id; + #id; + + get id() { + return this.#id; + }; /** @type {Map} */ static #existing = new Map(); @@ -14,7 +18,7 @@ export class PopoverEventManager { */ constructor(id, element, popoverClass, options = {}) { id = `${id}-${popoverClass.name}`; - this.id = id; + this.#id = id; if (PopoverEventManager.#existing.has(id)) { const manager = PopoverEventManager.#existing.get(id); @@ -101,7 +105,7 @@ export class PopoverEventManager { #construct(options) { options.popover ??= {}; - options.popover.managerId = this.id; + options.popover.managerId = this.#id; return new this.#class(options); }; From 8de50185c13a632d8425b75ef95e2c6dd940deee Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 15:32:55 -0600 Subject: [PATCH 056/146] Remove unused CSS --- templates/Apps/popovers/AmmoTracker/style.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/Apps/popovers/AmmoTracker/style.css b/templates/Apps/popovers/AmmoTracker/style.css index d81ef2d..d902772 100644 --- a/templates/Apps/popovers/AmmoTracker/style.css +++ b/templates/Apps/popovers/AmmoTracker/style.css @@ -35,9 +35,6 @@ grid-template-rows: min-content; align-items: center; justify-items: center; - /* display: flex; - flex-direction: row; - align-items: center; */ gap: 8px; .name { From 26134b03908606dab15a1cc0112911cdb245352a Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sat, 5 Apr 2025 23:33:20 -0600 Subject: [PATCH 057/146] Correct the height of the HUD element to prevent collapse only when not a GM --- templates/Apps/DelveDiceHUD/tour/next.hbs | 6 +++--- templates/Apps/DelveDiceHUD/tour/previous.hbs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/Apps/DelveDiceHUD/tour/next.hbs b/templates/Apps/DelveDiceHUD/tour/next.hbs index d81c879..5e102f0 100644 --- a/templates/Apps/DelveDiceHUD/tour/next.hbs +++ b/templates/Apps/DelveDiceHUD/tour/next.hbs @@ -1,7 +1,4 @@
    - {{!-- This is here to prevent height collapsing --}} - ​ - {{#if meta.editable}} + {{else}} + {{!-- This is here to prevent height collapsing --}} + ​ {{/if}}
    diff --git a/templates/Apps/DelveDiceHUD/tour/previous.hbs b/templates/Apps/DelveDiceHUD/tour/previous.hbs index 1120e8f..b8c59b7 100644 --- a/templates/Apps/DelveDiceHUD/tour/previous.hbs +++ b/templates/Apps/DelveDiceHUD/tour/previous.hbs @@ -1,7 +1,4 @@
    - {{!-- This is here to prevent height collapsing --}} - ​ - {{#if meta.editable}} + {{else}} + {{!-- This is here to prevent height collapsing --}} + ​ {{/if}}
    From f1487bd9b8a9d06e5f8f54b2049d3ab0c4ae570e Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 21:08:35 -0600 Subject: [PATCH 058/146] Correctly forward the parameters to the super method --- module/Apps/GenericApp.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 2e9c1c2..413569d 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -57,8 +57,8 @@ export function GenericAppMixin(HandlebarsApp) { * This override makes it so that if the application has any framable popovers * within it that are currently open, they will rerender as well. */ - async _onRender() { - await super._onRender(); + async _onRender(...args) { + await super._onRender(...args); for (const manager of this._popoverManagers.values()) { manager.render(); }; From 4e89763901e8c1cd9757ba044ec0d57cae851c82 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 21:08:52 -0600 Subject: [PATCH 059/146] Add localization string override for GM -> Keeper --- langs/en-ca.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/langs/en-ca.json b/langs/en-ca.json index 1fef505..8eab18e 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -209,5 +209,8 @@ "heavy": "The distance of your aura when using Heavycraft" } } + }, + "USER": { + "GM": "Keeper" } } From 01f9fba5934688761c64f83ae0f269a6aeb1578c Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 21:42:32 -0600 Subject: [PATCH 060/146] Add the ability to update the ammo quantity from the starred shortcuts --- module/Apps/GenericApp.mjs | 44 ++++++++++++++++++--- templates/Apps/HeroSkillsCardV1/content.hbs | 6 ++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 413569d..c131baa 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -52,16 +52,34 @@ export function GenericAppMixin(HandlebarsApp) { }; }; - /** - * @override - * This override makes it so that if the application has any framable popovers - * within it that are currently open, they will rerender as well. - */ + /** @override */ async _onRender(...args) { await super._onRender(...args); + + /* + Rendering each of the popover managers associated with this app allows us + to have them be dynamic and update when their parent application is rerendered, + this could eventually be something we can move into the Document's apps + collection so Foundry auto-rerenders it, but because it isn't actually + associated with the Document (as it's dependendant on the Application), I + decided that it would be best to do my own handling for it. + */ for (const manager of this._popoverManagers.values()) { manager.render(); }; + + /* + Foreign update listeners so that we can easily update items that may not + be this document itself, but are useful to be able to be edited from this + sheet. Primarily useful for editing the Actors' Item collection, or an Items' + ActiveEffect collection. + */ + this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { + const events = el.dataset.foreignUpdateOn.split(`,`); + for (const event of events) { + el.addEventListener(event, this.updateEmbedded); + }; + }); }; async _preparePartContext(partId, ctx, opts) { @@ -132,6 +150,22 @@ export function GenericAppMixin(HandlebarsApp) { }); app.render({ force: true }); }; + + /** + * @param {Event} event + */ + async updateForeign(event) { + const target = event.currentTarget; + const data = target.dataset; + const document = await fromUuid(data.foreignUuid); + + let value = target.value; + switch (target.type) { + case `checkbox`: value = target.checked; break; + }; + + await document?.update({ [data.foreignName]: value }); + }; // #endregion }; return GenericRipCryptApp; diff --git a/templates/Apps/HeroSkillsCardV1/content.hbs b/templates/Apps/HeroSkillsCardV1/content.hbs index 6f8c11b..a4a7a56 100644 --- a/templates/Apps/HeroSkillsCardV1/content.hbs +++ b/templates/Apps/HeroSkillsCardV1/content.hbs @@ -129,8 +129,12 @@
{{else}} From 55cff3c048c1b606df1c2b502dffa9531c491207 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 21:43:30 -0600 Subject: [PATCH 061/146] Set verified version Foundry v13.339 --- system.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system.json b/system.json index 68d0f25..06aecb9 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "0.0.1", "compatibility": { "minimum": 13, - "verified": 13, + "verified": 13.339, "maximum": 13 }, "authors": [ From 053ab05dcba53ff83f00cf0fbd3c2a094dcfc5a4 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 21:51:49 -0600 Subject: [PATCH 062/146] Pull the foreign document updating into a utility method and add it to the GenericPopoverMixin --- module/Apps/GenericApp.mjs | 20 ++----------------- module/Apps/popovers/GenericPopoverMixin.mjs | 19 ++++++++++++++++++ module/Apps/utils.mjs | 21 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index c131baa..1fe5edf 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -1,4 +1,4 @@ -import { createItemFromElement, deleteItemFromElement, editItemFromElement } from "./utils.mjs"; +import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "./utils.mjs"; import { DicePool } from "./DicePool.mjs"; import { RichEditor } from "./RichEditor.mjs"; import { toBoolean } from "../consts.mjs"; @@ -77,7 +77,7 @@ export function GenericAppMixin(HandlebarsApp) { this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { const events = el.dataset.foreignUpdateOn.split(`,`); for (const event of events) { - el.addEventListener(event, this.updateEmbedded); + el.addEventListener(event, updateForeignDocumentFromEvent); }; }); }; @@ -150,22 +150,6 @@ export function GenericAppMixin(HandlebarsApp) { }); app.render({ force: true }); }; - - /** - * @param {Event} event - */ - async updateForeign(event) { - const target = event.currentTarget; - const data = target.dataset; - const document = await fromUuid(data.foreignUuid); - - let value = target.value; - switch (target.type) { - case `checkbox`: value = target.checked; break; - }; - - await document?.update({ [data.foreignName]: value }); - }; // #endregion }; return GenericRipCryptApp; diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index a64b067..271bb9a 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -1,3 +1,5 @@ +import { updateForeignDocumentFromEvent } from "../utils.mjs"; + const { ApplicationV2 } = foundry.applications.api; /** @@ -65,6 +67,23 @@ export function GenericPopoverMixin(HandlebarsApp) { }; }; + async _onRender(...args) { + await super._onRender(...args); + + /* + Foreign update listeners so that we can easily update items that may not + be this document itself, but are useful to be able to be edited from this + sheet. Primarily useful for editing the Actors' Item collection, or an Items' + ActiveEffect collection. + */ + this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { + const events = el.dataset.foreignUpdateOn.split(`,`); + for (const event of events) { + el.addEventListener(event, updateForeignDocumentFromEvent); + }; + }); + }; + async close(options = {}) { // prevent locked popovers from being closed if (this.popover.locked && !options.force) { return }; diff --git a/module/Apps/utils.mjs b/module/Apps/utils.mjs index 1baee18..7c55ec4 100644 --- a/module/Apps/utils.mjs +++ b/module/Apps/utils.mjs @@ -42,3 +42,24 @@ export async function deleteItemFromElement(target) { const item = await fromUuid(itemId); item.delete(); }; + +/** + * Updates a document using the UUID, expects there to be the following + * dataset attributes: + * - "data-foreign-uuid" : The UUID of the document to update + * - "data-foreign-name" : The dot-separated path of the value to update + * + * @param {Event} event + */ +export async function updateForeignDocumentFromEvent(event) { + const target = event.currentTarget; + const data = target.dataset; + const document = await fromUuid(data.foreignUuid); + + let value = target.value; + switch (target.type) { + case `checkbox`: value = target.checked; break; + }; + + await document?.update({ [data.foreignName]: value }); +}; From 05a3db98c8816e392cc771e8d629beff7e5b9978 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 9 Apr 2025 22:13:52 -0600 Subject: [PATCH 063/146] Get the AmmoTracker's in-popover editing working using the foreign document updating --- module/Apps/popovers/AmmoTracker.mjs | 5 ++++- templates/Apps/popovers/AmmoTracker/ammoList.hbs | 11 ++++++++++- templates/Apps/popovers/AmmoTracker/style.css | 16 +++++++++------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index c91e3a4..6171c75 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -37,7 +37,10 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin( // #region Lifecycle async _preparePartContext(partId, data) { - const ctx = { partId }; + const ctx = { + meta: { idp: this.id }, + partId, + }; let favouriteCount = 0; ctx.ammos = data.ammos.map(ammo => { diff --git a/templates/Apps/popovers/AmmoTracker/ammoList.hbs b/templates/Apps/popovers/AmmoTracker/ammoList.hbs index 67d43b2..a9ed916 100644 --- a/templates/Apps/popovers/AmmoTracker/ammoList.hbs +++ b/templates/Apps/popovers/AmmoTracker/ammoList.hbs @@ -1,10 +1,19 @@
+ {{log @root}} {{#if ammos}}
    {{#each ammos as | data |}}
  • {{ data.ammo.name }} - {{ data.ammo.system.quantity }} + {{#if data.favourite}}