kgar / foundry-vtt-tidy-5e-sheets

D&D 5e sheet layouts for Foundry VTT, focused on a clean UI, user ergonomics, and extensibility.
https://kgar.github.io/foundry-vtt-tidy-5e-sheets/
MIT License
42 stars 14 forks source link

Rest Recovery compatibility #150

Closed kgar closed 5 months ago

kgar commented 11 months ago

Work with the module author to establish compatibility between Tidy 5e Sheets and Rest Recovery.

Known areas where compatibility is needed:

https://github.com/roth-michael/FoundryVTT-RestRecovery

roth-michael commented 5 months ago

First, for the Actor sheet, on the renderActorSheet5e hook:

function patch_actorSheet(app, html, data) {

  let actor = game.actors.get(data.actor._id);

  if(game.modules.get("tidy5e-sheet")?.active && lib.getSetting(CONSTANTS.SETTINGS.ONE_DND_EXHAUSTION)){

    if(!styleTag) {
      styleTag = document.createElement("style");
      document.head.appendChild(styleTag);
      styleTag.type = "text/css";
      styleTag.appendChild(document.createTextNode(`
    .tidy5e.sheet.actor .exhaustion-container:hover .exhaustion-wrap {
      width: 254px;
    }`));
    }

    const exhaustionElem = [
      $(`<li data-elvl="7">7</li>`),
      $(`<li data-elvl="8">8</li>`),
      $(`<li data-elvl="9">9</li>`),
      $(`<li data-elvl="10">10</li>`)
    ]

    exhaustionElem.forEach(elem => {
      elem.on("click", async function(event) {
        event.preventDefault();
        let target = event.currentTarget;
        let value = Number(target.dataset.elvl);
        await actor.update({ "system.attributes.exhaustion": value });
      })
    })

    html.find(".exhaust-level").append(exhaustionElem)

  }

  let border = true;
  let targetElem = html.find('.center-pane .attributes')[0];
  if (!targetElem) {
    border = false;
    targetElem = html.find('.center-pane .resources')[0];
    if (!targetElem) return;
  }
  const elem = $(`<div class="form-group" style="${border ? "border-bottom: 2px groove #eeede0; padding-bottom: 0.25rem;" : "padding-top: 0.25rem;"} flex:0;"  title="Module: Rest Recovery for 5e">
        <label style="flex: none; line-height: 20px; font-weight: bold; margin: 0 10px 0 0;">${game.i18n.localize("REST-RECOVERY.Dialogs.Resources.Configure")}</label>
        <a class="config-button" title="${game.i18n.localize("REST-RECOVERY.Dialogs.Resources.Configure")}" style="flex:1;">
            <i class="fas fa-cog" style="float: right; margin-right: 3px; text-align: right; color: #999;"></i>
        </a>
    </div>`);
  elem.insertAfter(targetElem);
  elem.find('.config-button').on('click', function () {
    ResourceConfig.show({ actor });
  });
}

Looks like Wasp did try at least some compatibility at some point a while back. Let me know if you need ResourceConfig defined.

Now for the Item sheet (renderItemSheet5e hook), there's two:

function patch_itemConsumableInputs(app, html, item) {

  const customConsumable = foundry.utils.getProperty(item, CONSTANTS.FLAGS.CONSUMABLE) ?? {};
  const uses = Number(foundry.utils.getProperty(item, "system.uses.max"));
  const per = foundry.utils.getProperty(item, "system.uses.per");
  const validUses = uses && uses > 0 && per;

  let targetElem = html.find('.form-header')?.[1];
  if (!targetElem) return;
  $(`
        <div class="form-header">${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.Title")}</div>
        <div class="form-group">
            <div class="form-fields" style="margin-right:0.5rem;">
                <label class="checkbox" style="font-size:13px;">
                    <input type="checkbox" name="${CONSTANTS.FLAGS.CONSUMABLE_ENABLED}" ${customConsumable.enabled ? "checked" : ""}> ${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.IsConsumable")}
                </label>
            </div>
            <div class="form-fields" style="margin-right:0.5rem;">
                <label style="flex:0 1 auto;">${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.Type")}</label>
                <select name="${CONSTANTS.FLAGS.CONSUMABLE_TYPE}" ${!customConsumable.enabled ? "disabled" : ""}>
                    <option ${customConsumable.type === CONSTANTS.FLAGS.CONSUMABLE_TYPE_FOOD ? "selected" : ""} value="${CONSTANTS.FLAGS.CONSUMABLE_TYPE_FOOD}">${game.i18n.localize("REST-RECOVERY.Misc.Food")}</option>
                    <option ${customConsumable.type === CONSTANTS.FLAGS.CONSUMABLE_TYPE_WATER ? "selected" : ""} value="${CONSTANTS.FLAGS.CONSUMABLE_TYPE_WATER}">${game.i18n.localize("REST-RECOVERY.Misc.Water")}</option>
                    <option ${customConsumable.type === CONSTANTS.FLAGS.CONSUMABLE_TYPE_BOTH ? "selected" : ""} value="${CONSTANTS.FLAGS.CONSUMABLE_TYPE_BOTH}">${game.i18n.localize("REST-RECOVERY.Misc.Both")}</option>
                </select>
            </div>
        </div>

        <div class="form-group">
            <div class="form-fields" style="margin-right:0.5rem;">
                <label class="checkbox" style="font-size:13px;">
                    <input type="checkbox" name="${CONSTANTS.FLAGS.CONSUMABLE_DAY_WORTH}" ${customConsumable.dayWorth ? "checked" : ""}> ${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.DayWorth")}
                </label>
            </div>
        </div>

        <small style="display:${customConsumable.enabled && !validUses ? "block" : "none"}; margin: 0.5rem 0;">
            <i class="fas fa-info-circle" style="color:rgb(217, 49, 49);"></i> ${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.ChargesDescription")}
        </small>
    `).insertBefore(targetElem);

}

and

function patch_itemCustomRecovery(app, html, item) {

  const customRecovery = foundry.utils.getProperty(item, `${CONSTANTS.FLAGS.RECOVERY_ENABLED}`) ?? false;
  const customFormula = foundry.utils.getProperty(item, `${CONSTANTS.FLAGS.RECOVERY_FORMULA}`) ?? "";
  let targetElem = html.find('.uses-per')?.[0];
  if (!targetElem) return;
  $(`<div class="form-group" title="Module: Rest Recovery for 5e">
        <label>${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.UsesCustomRecovery")} <i class="fas fa-info-circle"></i></label>
        <div class="form-fields">
            <label class="checkbox">
                <input type="checkbox" name="${CONSTANTS.FLAGS.RECOVERY_ENABLED}" ${customRecovery ? "checked" : ""}>
                ${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.Enabled")}
            </label>
            <span style="flex: 0 0 auto; margin: 0 0.25rem;">|</span>
            <span class="sep" style="flex: 0 0 auto; margin-right: 0.25rem;">${game.i18n.localize("REST-RECOVERY.Dialogs.ItemOverrides.Formula")}</span>
            <input type="text" name="${CONSTANTS.FLAGS.RECOVERY_FORMULA}" ${!customRecovery ? "disabled" : ""} value="${customRecovery ? customFormula : ""}">
        </div>
    </div>`).insertAfter(targetElem);

}
kgar commented 5 months ago

This part where exhaustion levels are added to the tracker is now basically handled by Tidy via settings:

    const exhaustionElem = [
      $(`<li data-elvl="7">7</li>`),
      $(`<li data-elvl="8">8</li>`),
      $(`<li data-elvl="9">9</li>`),
      $(`<li data-elvl="10">10</li>`)
    ]

    exhaustionElem.forEach(elem => {
      elem.on("click", async function(event) {
        event.preventDefault();
        let target = event.currentTarget;
        let value = Number(target.dataset.elvl);
        await actor.update({ "system.attributes.exhaustion": value });
      })
    })

    html.find(".exhaust-level").append(exhaustionElem)

With that said, I do have an API that allows for phoning in and telling Tidy how to configure its rest settings: https://kgar.github.io/foundry-vtt-tidy-5e-sheets/classes/ExhaustionApi.html#useSpecificLevelExhaustion

As far as how to leverage it, I recommend invoking the API when saving settings if you want to automatically set Tidy's exhaustion level count and hint text for each level, because calling this API function is actually changing a Tidy world setting. The API is available after Foundry's ready event >> Tidy's ready event, in game.modules.get('tidy5e-sheet')?.api:

await game.modules.get('tidy5e-sheet')?.api.config.exhaustion.useSpecificLevelExhaustion({
    totalLevels: 10
});

If you want to specifically set the hint text, you can. Here's the example in the docs:

await game.modules.get('tidy5e-sheet')?.api.config.exhaustion.useSpecificLevelExhaustion({
    totalLevels: 3,
    hints: [
      'No exhaustion', // 👈 notice this is "Level 0"
      'You are kind of tired', // Level 1
      'You look unwell', // Level 2
      'Dead 💀', // Level 3
    ],
});
kgar commented 5 months ago

For the config button found here:

  let border = true;
  let targetElem = html.find('.center-pane .attributes')[0];
  if (!targetElem) {
    border = false;
    targetElem = html.find('.center-pane .resources')[0];
    if (!targetElem) return;
  }
  const elem = $(`<div class="form-group" style="${border ? "border-bottom: 2px groove #eeede0; padding-bottom: 0.25rem;" : "padding-top: 0.25rem;"} flex:0;"  title="Module: Rest Recovery for 5e">
        <label style="flex: none; line-height: 20px; font-weight: bold; margin: 0 10px 0 0;">${game.i18n.localize("REST-RECOVERY.Dialogs.Resources.Configure")}</label>
        <a class="config-button" title="${game.i18n.localize("REST-RECOVERY.Dialogs.Resources.Configure")}" style="flex:1;">
            <i class="fas fa-cog" style="float: right; margin-right: 3px; text-align: right; color: #999;"></i>
        </a>
    </div>`);
  elem.insertAfter(targetElem);
  elem.find('.config-button').on('click', function () {
    ResourceConfig.show({ actor });
  });

I have a specific API to help with this, thanks to Arbron's Summoning Module a while back: https://kgar.github.io/foundry-vtt-tidy-5e-sheets/classes/ActorTraitsApi.html#registerActorTrait

// Make sure to customize all this to your liking.
// Registering in this way on main/startup, it will be set up anytime the sheet renders, without the need to fuss with HTML.
// If/when I create new sheet layouts, this will continue to work because it's just data and not specific markup.
Hooks.once('tidy5e-sheet.ready', (api) => {
  api.config.actorTraits.registerActorTrait({
    title: "Configure My Module",
    iconClass: "fa-solid fa-spaghetti-monster-flying",
    enabled: (params) =>
      params.context.actor.type === "character",
    openConfiguration: ({app, data, element, event}) => {
      ResourceConfig.show({ actor: app.actor });
    },
    openConfigurationTooltip: "Click to configure my module",
  });
});

This is where these traits go:

image

kgar commented 5 months ago

For patch_itemConsumableInputs, this will be a tag team thing. For the short-to-medium term, I don't have a data-driven way to handle this, so I will be endeavoring to adjust my sheet styles so that the standard form-header / form-group classes from the default sheets look Tidyified on my sheets.

Because Tidy runs on svelte, my hooks are a bit different. When the item sheet has a full render, Tidy calls the renderItemSheet5e. However, when a render is force=false, or when an input changes, for example, Tidy does not call renderItemSheet5e. Tidy calls tidy5e-sheet.renderItemSheet on every change cycle. Also, because Svelte handles reactivity and doesn't completely throw out the old HTML when calling the Tidy hook, if you were to inject things without removing old things, you'd see things piling up in duplicate.

For this reason, I have a 2-step process:

  1. Tidy should not be included in renderItemSheet5e logic. Instead, it should be wired up via tidy5e-sheet.renderItemSheet.
  2. The topmost HTML to be injected needs this attribute and value: data-tidy-render-scheme="handlebars".

Example code to spark ideas on achieving this:

Hooks.on(`tidy5e-sheet.renderItemSheet`, (app, element, data, forced) => {
  const html = $(element);
  const markupToInject = `<div style="display: contents;" data-tidy-render-scheme="handlebars">${getConsumableInputsHtml(app, html, data.item)}</div>`;
  let targetElem = html.find('.form-header')?.[1];
  if (!targetElem) return;
  $(markupToInject).insertBefore(targetElem);  
});

This will ensure that your elements are enabling/disabling and show/hiding things appropriately between input changes.

For my part, I'll fix those green checkboxes. I've had it out for those for a long time 🔪

kgar commented 5 months ago

In patch_itemCustomRecovery, the same wire-up is needed as patch_itemConsumableInputs.

  1. Wire up in tidy5e-sheet.renderItemSheet
  2. data-tidy-render-scheme="handlebars" wrapped around or applied to the topmost HTML nodes to be injected.
kgar commented 5 months ago

Compatible!

Image