From 690eff8e460150c26cc4be89a26936c9abae4adb Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 2 Mar 2025 21:19:12 -0700 Subject: [PATCH 001/128] Add the most basic form of a difficulty delta region behaviour --- langs/en-ca.json | 13 +++++++ .../data/region-behaviors/DifficultyDelta.mjs | 36 +++++++++++++++++++ module/hooks/init.mjs | 2 ++ system.json | 3 ++ 4 files changed, 54 insertions(+) create mode 100644 module/data/region-behaviors/DifficultyDelta.mjs diff --git a/langs/en-ca.json b/langs/en-ca.json index a91a488..9d24b68 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -11,6 +11,9 @@ "shield": "Shield", "skill": "Skill", "weapon": "Weapon" + }, + "RegionBehavior": { + "difficultyDelta": "Condition Delta" } }, "RipCrypt": { @@ -154,6 +157,16 @@ }, "tooltips": { "shield-bonus": "Shield Bonus: {value}" + }, + "region": { + "difficultyDelta": { + "FIELDS": { + "delta": { + "label": "Delta", + "hint": "How much should actors in this area have their difficulty changed from the global value." + } + } + } } } } diff --git a/module/data/region-behaviors/DifficultyDelta.mjs b/module/data/region-behaviors/DifficultyDelta.mjs new file mode 100644 index 0000000..5558c9d --- /dev/null +++ b/module/data/region-behaviors/DifficultyDelta.mjs @@ -0,0 +1,36 @@ +import { Logger } from "../../utils/Logger.mjs"; + +const { fields } = foundry.data; +const { RegionBehaviorType } = foundry.data.regionBehaviors; + +export class DifficultyDeltaBehaviorData extends RegionBehaviorType { + static LOCALIZATION_PREFIXES = [`RipCrypt.region.difficultyDelta`]; + + static defineSchema() { + return { + delta: new fields.NumberField({ + required: true, + initial: 1, + }), + }; + }; + + static events = { + [CONST.REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter, + [CONST.REGION_EVENTS.TOKEN_EXIT]: this.#onTokenExit, + }; + + static async #onTokenEnter(event) { + Logger.debug(`token enter`, event, this); + const actor = event.data.token.actor; + if (!actor) { return }; + actor.setFlag(`ripcrypt`, `dcDelta`, this.delta); + }; + + static async #onTokenExit(event) { + Logger.debug(`token exit`, event, this); + const actor = event.data.token.actor; + if (!actor) { return }; + actor.unsetFlag(`ripcrypt`, `dcDelta`); + }; +}; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 812c6e2..f295ef3 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -33,6 +33,7 @@ import { registerDevSettings } from "../settings/devSettings.mjs"; import { registerMetaSettings } from "../settings/metaSettings.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; +import { DifficultyDeltaBehaviorData } from "../data/region-behaviors/DifficultyDelta.mjs"; Hooks.once(`init`, () => { Logger.log(`Initializing`); @@ -56,6 +57,7 @@ Hooks.once(`init`, () => { CONFIG.Item.dataModels.shield = ShieldData; CONFIG.Item.dataModels.skill = SkillData; CONFIG.Item.dataModels.weapon = WeaponData; + CONFIG.RegionBehavior.dataModels.difficultyDelta = DifficultyDeltaBehaviorData; // #endregion // #region Class Changes diff --git a/system.json b/system.json index 9924bfe..0adbd73 100644 --- a/system.json +++ b/system.json @@ -51,6 +51,9 @@ "shield": {}, "skill": {}, "weapon": {} + }, + "RegionBehavior": { + "difficultyDelta": {} } } } \ No newline at end of file From f74c22c510e446f93de1eb4a510172b37fbf487e Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 2 Mar 2025 21:21:44 -0700 Subject: [PATCH 002/128] Fix import order --- module/hooks/init.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index f295ef3..70f81ff 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -10,6 +10,7 @@ import { RipCryptCombatTracker } from "../Apps/sidebar/CombatTracker.mjs"; import { AmmoData } from "../data/Item/Ammo.mjs"; import { ArmourData } from "../data/Item/Armour.mjs"; import { CraftData } from "../data/Item/Craft.mjs"; +import { DifficultyDeltaBehaviorData } from "../data/region-behaviors/DifficultyDelta.mjs"; import { GoodData } from "../data/Item/Good.mjs"; import { HeroData } from "../data/Actor/Hero.mjs"; import { ShieldData } from "../data/Item/Shield.mjs"; @@ -33,7 +34,6 @@ import { registerDevSettings } from "../settings/devSettings.mjs"; import { registerMetaSettings } from "../settings/metaSettings.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; -import { DifficultyDeltaBehaviorData } from "../data/region-behaviors/DifficultyDelta.mjs"; Hooks.once(`init`, () => { Logger.log(`Initializing`); From 17cd5532f47668b4b046cdd01b1862fd351f3899 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 5 Mar 2025 23:05:59 -0700 Subject: [PATCH 003/128] Begin work on the Hero Craft card itself --- langs/en-ca.json | 1 + module/hooks/init.mjs | 6 +++ templates/Apps/HeroCraftCardV1/content.hbs | 26 ++++++++++ templates/Apps/HeroCraftCardV1/style.css | 58 ++++++++++++++++++++++ templates/Apps/apps.css | 1 + templates/css/common.css | 4 +- templates/css/elements/lists.css | 1 + 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 templates/Apps/HeroCraftCardV1/content.hbs create mode 100644 templates/Apps/HeroCraftCardV1/style.css diff --git a/langs/en-ca.json b/langs/en-ca.json index 65bb80d..e2eca5c 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -18,6 +18,7 @@ "AllItemsSheetV1": "RipCrypt Item Sheet", "CombinedHeroSheet": "Hero Sheet", "HeroSummaryCardV1": "Hero Stat Card", + "HeroCraftCardV1": "Hero Craft Card", "HeroSkillsCardV1": "Hero Skill Card" }, "common": { diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 547ea3e..48c3171 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -2,6 +2,7 @@ import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; +import { HeroCraftCardV1 } from "../Apps/ActorSheets/HeroCraftCardV1.mjs"; import { HeroSkillsCardV1 } from "../Apps/ActorSheets/HeroSkillsCardV1.mjs"; import { HeroSummaryCardV1 } from "../Apps/ActorSheets/HeroSummaryCardV1.mjs"; import { RipCryptCombatTracker } from "../Apps/sidebar/CombatTracker.mjs"; @@ -91,6 +92,11 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.HeroSkillsCardV1`, themes: HeroSkillsCardV1.themes, }); + Actors.registerSheet(game.system.id, HeroCraftCardV1, { + types: [`hero`], + label: `RipCrypt.sheet-names.HeroCraftCardV1`, + themes: HeroCraftCardV1.themes, + }); // #endregion // #region Items diff --git a/templates/Apps/HeroCraftCardV1/content.hbs b/templates/Apps/HeroCraftCardV1/content.hbs new file mode 100644 index 0000000..44cc1d4 --- /dev/null +++ b/templates/Apps/HeroCraftCardV1/content.hbs @@ -0,0 +1,26 @@ +
+
+ Focus + Details +
+
    + {{#each craft.focus 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 new file mode 100644 index 0000000..2b81e63 --- /dev/null +++ b/templates/Apps/HeroCraftCardV1/style.css @@ -0,0 +1,58 @@ +.ripcrypt .HeroCraftCardV1 { + --col-gap: 8px; + + display: grid; + column-gap: var(--col-gap); + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(15, minmax(0, 1fr)); + + background: var(--base-background); + color: var(--base-text); + + .col-header { + display: flex; + flex-direction: row; + align-items: center; + background: var(--section-header-background); + color: var(--section-header-text); + padding: 2px 4px; + border-radius: 999px; + } + + label, .label { + box-sizing: border-box; + padding: 2px 4px; + text-transform: uppercase; + font-size: var(--font-size-14); + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + } + + .craft-list { + display: grid; + grid-template-rows: subgrid; + + > :nth-child(even) { + background: var(--alt-row-background); + color: var(--alt-row-text); + } + } + + span.name { + flex-grow: 1; + } + + [data-aspect="focus"] { --row: 6; --col: 1; } + + [data-aspect] { + &.aspect-header { + grid-row: var(--row); + grid-column: var(--col); + } + &.craft-list { + grid-row: calc(var(--row) + 1) / span 4; + grid-column: var(--col); + } + } +} diff --git a/templates/Apps/apps.css b/templates/Apps/apps.css index 9f6a4f6..1a261a4 100644 --- a/templates/Apps/apps.css +++ b/templates/Apps/apps.css @@ -3,6 +3,7 @@ @import url("./CryptApp/style.css"); @import url("./DelveDiceHUD/style.css"); @import url("./DicePool/style.css"); +@import url("./HeroCraftCardV1/style.css"); @import url("./HeroSummaryCardV1/style.css"); @import url("./HeroSkillsCardV1/style.css"); @import url("./RichEditor/style.css"); diff --git a/templates/css/common.css b/templates/css/common.css index f11d646..cb8c9f0 100644 --- a/templates/css/common.css +++ b/templates/css/common.css @@ -21,7 +21,9 @@ } .HeroSummaryCardV1, - .HeroSkillsCardV1 { + .HeroSkillsCardV1, + .HeroCraftCardV1 { + padding: 8px; /* height: 270px; */ width: 680px; --col-gap: 2px; diff --git a/templates/css/elements/lists.css b/templates/css/elements/lists.css index 2940b37..b55f34f 100644 --- a/templates/css/elements/lists.css +++ b/templates/css/elements/lists.css @@ -15,6 +15,7 @@ counter-increment: list-index 1; position: relative; padding: 0 4px; + border-radius: 999px; } &.num-before > li::before, From b6f3539a9584e2ae497d2a5a37be4159cbc8f3d9 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Wed, 5 Mar 2025 23:06:28 -0700 Subject: [PATCH 004/128] Replace the Move - Run label with just Move --- langs/en-ca.json | 1 - templates/Apps/HeroSummaryCardV1/content.hbs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/langs/en-ca.json b/langs/en-ca.json index e2eca5c..8199113 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -150,7 +150,6 @@ } }, "Apps": { - "move-run": "@RipCrypt.common.move • @RipCrypt.common.run", "traits-range": "@RipCrypt.common.traits • @RipCrypt.common.range", "grit-skills": "@RipCrypt.common.abilities.grit Skills", "gait-skills": "@RipCrypt.common.abilities.gait Skills", diff --git a/templates/Apps/HeroSummaryCardV1/content.hbs b/templates/Apps/HeroSummaryCardV1/content.hbs index 23b84c8..beefb7d 100644 --- a/templates/Apps/HeroSummaryCardV1/content.hbs +++ b/templates/Apps/HeroSummaryCardV1/content.hbs @@ -351,7 +351,7 @@ {{speed.run}} From 67753ce3e7bad8c140e5ca8c9380fcd779852bdd Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 6 Mar 2025 18:47:41 -0700 Subject: [PATCH 005/128] 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 9336e3465c639c3023429c7ff76871617d9ef3e7 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 6 Mar 2025 21:22:14 -0700 Subject: [PATCH 006/128] Get the deltas stacking properly --- .../data/region-behaviors/DifficultyDelta.mjs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/module/data/region-behaviors/DifficultyDelta.mjs b/module/data/region-behaviors/DifficultyDelta.mjs index 5558c9d..3a6691f 100644 --- a/module/data/region-behaviors/DifficultyDelta.mjs +++ b/module/data/region-behaviors/DifficultyDelta.mjs @@ -23,14 +23,49 @@ export class DifficultyDeltaBehaviorData extends RegionBehaviorType { static async #onTokenEnter(event) { Logger.debug(`token enter`, event, this); const actor = event.data.token.actor; + // const token = event.data.token.object; + // Logger.debug(token.center, token.h) if (!actor) { return }; - actor.setFlag(`ripcrypt`, `dcDelta`, this.delta); + + let delta = actor.getFlag(game.system.id, `dcDelta`) ?? 0; + delta += this.delta; + actor.setFlag(game.system.id, `dcDelta`, delta); + ui.notifications.info(`Updated delta to: ${delta}`); + Logger.debug(`Updated delta to:`, delta); + // await canvas.interface.createScrollingText( + // token.center, + // delta, + // { + // distance: 2 * token.h, + // fontSize: 40, + // fill: `#aa0000`, + // } + // ); }; static async #onTokenExit(event) { Logger.debug(`token exit`, event, this); const actor = event.data.token.actor; + // const token = event.data.token.object; if (!actor) { return }; - actor.unsetFlag(`ripcrypt`, `dcDelta`); + + let delta = actor.getFlag(game.system.id, `dcDelta`) ?? 0; + delta -= this.delta; + if (delta === 0) { + actor.unsetFlag(game.system.id, `dcDelta`); + } else { + actor.setFlag(game.system.id, `dcDelta`, delta); + }; + ui.notifications.info(`Updated delta to: ${delta}`); + Logger.debug(`Updated delta to:`, delta); + // await canvas.interface.createScrollingText( + // token.center, + // delta, + // { + // distance: 2 * token.h, + // fontSize: 40, + // fill: `#00aa00`, + // } + // ); }; }; From 245dc1f7e15d1d05e9b5cf99e14818a4b2ea334a Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 19:42:07 -0700 Subject: [PATCH 007/128] Rename folder --- module/data/{region-behaviors => Behavior}/DifficultyDelta.mjs | 0 module/hooks/init.mjs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename module/data/{region-behaviors => Behavior}/DifficultyDelta.mjs (100%) diff --git a/module/data/region-behaviors/DifficultyDelta.mjs b/module/data/Behavior/DifficultyDelta.mjs similarity index 100% rename from module/data/region-behaviors/DifficultyDelta.mjs rename to module/data/Behavior/DifficultyDelta.mjs diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index b7dda31..41b6476 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -10,7 +10,7 @@ import { RipCryptCombatTracker } from "../Apps/sidebar/CombatTracker.mjs"; import { AmmoData } from "../data/Item/Ammo.mjs"; import { ArmourData } from "../data/Item/Armour.mjs"; import { CraftData } from "../data/Item/Craft.mjs"; -import { DifficultyDeltaBehaviorData } from "../data/region-behaviors/DifficultyDelta.mjs"; +import { DifficultyDeltaBehaviorData } from "../data/Behavior/DifficultyDelta.mjs"; import { GoodData } from "../data/Item/Good.mjs"; import { HeroData } from "../data/Actor/Hero.mjs"; import { ShieldData } from "../data/Item/Shield.mjs"; From 5876d5fe980e00a094bb859225492d17009136ea Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 7 Mar 2025 19:47:18 -0700 Subject: [PATCH 008/128] 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 009/128] 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 010/128] 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 011/128] 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 012/128] 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 013/128] 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 014/128] 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 015/128] 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 016/128] 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 017/128] 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 018/128] 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 019/128] 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 020/128] 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 021/128] 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 022/128] 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 023/128] 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 024/128] 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 025/128] 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 026/128] 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 027/128] 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 028/128] 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 029/128] 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 030/128] 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 031/128] 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 032/128] 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 033/128] 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 034/128] 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 035/128] 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 036/128] 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 037/128] 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 038/128] 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 039/128] 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 040/128] 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 041/128] 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 042/128] 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 043/128] 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 044/128] 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 045/128] 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}}