Shattered Codex

Wiki

Visual FX

Item Rarity Colors Rarity Effect Picker

Select item-pile tokens on the canvas, choose a rarity profile, and apply Token Magic FX based on your SC Item Rarity Colors settings.

javascriptUpdated March 2026

Overview

Use this macro when item tokens are already on the canvas and you want to apply rarity-based visual treatment quickly.

It reads the rarity configuration from SC Item Rarity Colors, builds a picker from the available rarities, and then applies Token Magic FX filters to the selected tokens.

This is a manual picker workflow: the macro does not inspect an item pile and auto-detect its rarity. You select the rarity profile to apply.

Rarity Effect Picker macro dialog with rarity selector and color preview

How to Use

  1. Open the macro file, copy the full script, and paste it into a new Foundry VTT script macro.
  2. Make sure the required modules are installed and active in the world.
  3. Drop or prepare the item-pile tokens you want to style on the canvas, then select at least one token.
  4. Run the macro, choose the rarity entry from the picker, and click Apply.
  5. Use Remove in the same dialog whenever you want to clear all rarity filters from the selected tokens.

Notes

  • The macro stops early if no token is selected.
  • The macro stops early if Token Magic FX is not available.
  • The macro stops early if SC Item Rarity Colors is not active.
  • Rarity entries come from the SC Item Rarity Colors rarity list first, then fall back to D&D 5e system rarity names when needed.
  • A rarity with one configured color applies a Glow filter.
  • A rarity with two configured colors applies a Bevel filter.
  • A rarity with the glow option enabled applies a layered Glow FX stack using fog, ray, and animated glow filters.
  • Use this with Item Piles when those selected canvas tokens represent dropped items or loot containers that you want to emphasize visually.

Code

javascript
const MODULE_ID = "sc-item-rarity-colors";
const FILTER_IDS = {
  glow: "scRarityGlow",
  bevel: "scRarityBevel",
  fog: "scRarityFog",
  ray: "scRarityRay",
  glowFx: "scRarityGlowFx"
};


if (!canvas?.tokens?.controlled?.length) return ui.notifications.warn("Select at least one token.");
if (!globalThis.TokenMagic) return ui.notifications.error("TokenMagic not found.");
if (!game.modules.get(MODULE_ID)?.active) return ui.notifications.error("SC Item Rarity Colors is not active.");


const controlledTokens = canvas.tokens.controlled;


const esc = (s) =>
  String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));


const hasSetting = (k) => game.settings.settings.has(`${MODULE_ID}.${k}`);
const getSetting = (k, fallback = null) => (hasSetting(k) ? game.settings.get(MODULE_ID, k) : fallback);
const normalizeKey = (v) => String(v ?? "").trim().toLowerCase();


const cleanHex = (v) => {
  const m = String(v ?? "").trim().match(/^#([0-9a-f]{6}|[0-9a-f]{8})$/i);
  return m ? `#${m[1].slice(0, 6).toUpperCase()}` : null;
};


const hexToInt = (h) => Number.parseInt(h.slice(1), 16);


function mixHex(a, b, t = 0.5) {
  const A = parseInt(a.slice(1), 16);
  const B = parseInt(b.slice(1), 16);
  const ar = (A >> 16) & 255, ag = (A >> 8) & 255, ab = A & 255;
  const br = (B >> 16) & 255, bg = (B >> 8) & 255, bb = B & 255;
  const rr = Math.round(ar + (br - ar) * t);
  const rg = Math.round(ag + (bg - ag) * t);
  const rb = Math.round(ab + (bb - ab) * t);
  return `#${((rr << 16) | (rg << 8) | rb).toString(16).padStart(6, "0").toUpperCase()}`;
}
const lighten = (h, amount = 0.35) => mixHex(h, "#FFFFFF", amount);
const darken = (h, amount = 0.45) => mixHex(h, "#000000", amount);


function getRarityEntries() {
  const out = new Map();


  // SC rarity list (includes custom rarities created in the module)
  const listEnabled = getSetting("rarity-list-enabled", true);
  if (listEnabled) {
    const cfg = getSetting("rarity-list-config", {});
    if (cfg && typeof cfg === "object") {
      for (const [rawKey, rawVal] of Object.entries(cfg)) {
        const key = normalizeKey(rawVal?.key ?? rawKey);
        if (!key || rawVal?.visible === false || out.has(key)) continue;
        out.set(key, { key, label: String(rawVal?.label ?? rawKey) });
      }
    }
  }


  // Fallback to system rarity names (if SC list is empty/missing entries)
  const sys = CONFIG?.DND5E?.itemRarity ?? {};
  if (sys && typeof sys === "object") {
    for (const [k, v] of Object.entries(sys)) {
      const key = normalizeKey(k);
      if (!key || out.has(key)) continue;
      out.set(key, { key, label: typeof v === "string" ? v : String(v?.label ?? k) });
    }
  }


  return [...out.values()];
}


function buildRarityData(entry) {
  const primary = cleanHex(getSetting(`${entry.key}-item-color`));
  const gradientOn = !!getSetting(`${entry.key}-gradient-option`, false);
  const secondary = gradientOn ? cleanHex(getSetting(`${entry.key}-secondary-item-color`)) : null;
  const glowOption = !!getSetting(`${entry.key}-glow-option`, false);


  const colors = [primary, secondary].filter(Boolean).filter((c, i, arr) => arr.indexOf(c) === i);
  return { ...entry, colors, glowOption };
}


async function clearAllRarityFilters() {
  for (const t of controlledTokens) {
    await TokenMagic.deleteFiltersOnSelected();
  }
}


async function applySingleGlow(color1) {
  const params = [{
    filterType: "glow",
    filterId: FILTER_IDS.glow,
    color: hexToInt(color1),
    animated: null
  }];
  for (const t of controlledTokens) await TokenMagic.addUpdateFilters(t, params);
}


async function applyBevel(color1, color2) {
  const params = [{
    filterType: "bevel",
    filterId: FILTER_IDS.bevel,
    rotation: 0,
    thickness: 5,
    lightColor: hexToInt(color1),
    lightAlpha: 0.8,
    shadowColor: hexToInt(color2),
    shadowAlpha: 0.5,
    animated: {
      rotation: {
        active: true,
        clockWise: true,
        loopDuration: 1600,
        animType: "syncRotation"
      }
    }
  }];
  for (const t of controlledTokens) await TokenMagic.addUpdateFilters(t, params);
}


async function applyGlowEffect(color1, color2 = null) {
  const rayColor = color2 || lighten(color1, 0.35);
  const glowStart = darken(color1, 0.55);
  const glowEnd = color2 || lighten(color1, 0.45);


  const params = [
    {
      filterType: "fog",
      filterId: FILTER_IDS.fog,
      color: hexToInt(color1),
      density: 0.20,
      time: 0,
      animated: {
        time: {
          active: true,
          speed: 1.2,
          animType: "move"
        }
      }
    },
    {
      filterType: "ray",
      filterId: FILTER_IDS.ray,
      time: 0,
      color: hexToInt(rayColor),
      alpha: 0.25,
      divisor: 32,
      anchorY: 0,
      animated: {
        time: {
          active: true,
          speed: 0.0005,
          animType: "move"
        }
      }
    },
    {
      filterType: "glow",
      filterId: FILTER_IDS.glowFx,
      distance: 10,
      outerStrength: 8,
      innerStrength: 0,
      color: hexToInt(glowStart),
      quality: 0.5,
      padding: 10,
      animated: {
        color: {
          active: true,
          loopDuration: 3000,
          animType: "colorOscillation",
          val1: hexToInt(glowStart),
          val2: hexToInt(glowEnd)
        }
      }
    }
  ];


  for (const t of controlledTokens) await TokenMagic.addUpdateFilters(t, params);
}


const rarities = getRarityEntries().map(buildRarityData);
if (!rarities.length) return ui.notifications.warn("No rarities found.");


const optionsHtml = rarities
  .map((r) => {
    const c1 = r.colors[0] || "";
    const c2 = r.colors[1] || "";
    return `<option value="${esc(r.key)}" data-c1="${esc(c1)}" data-c2="${esc(c2)}" data-glow="${r.glowOption ? "1" : "0"}">${esc(r.label)}</option>`;
  })
  .join("");


new Dialog({
  title: "Rarity Effect Picker",
  content: `
    <div style="display:flex; gap:10px; align-items:flex-end;">
      <div style="flex:1;">
        <label>Rarity</label>
        <select name="rarity" style="width:100%;">${optionsHtml}</select>
      </div>
      <div style="min-width:120px;">
        <label>Colors</label>
        <div data-role="preview" style="display:flex; gap:6px; min-height:28px; align-items:center;"></div>
      </div>
    </div>
    <p data-role="mode" style="opacity:.85; margin:.5rem 0 0 0;"></p>
  `,
  buttons: {
    apply: {
      label: "Apply",
      callback: async (html) => {
        const select = html[0].querySelector('select[name="rarity"]');
        const selected = rarities.find((r) => r.key === select?.value);
        if (!selected) return;


        const c1 = selected.colors[0] || null;
        const c2 = selected.colors[1] || null;
        if (!c1) return ui.notifications.warn(`"${selected.label}" has no configured color.`);


        await clearAllRarityFilters();


        if (selected.glowOption) {
          await applyGlowEffect(c1, c2);
          return ui.notifications.info(`Applied Glow FX: ${selected.label}`);
        }


        if (c2) {
          await applyBevel(c1, c2);
          return ui.notifications.info(`Applied Bevel: ${selected.label}`);
        }


        await applySingleGlow(c1);
        ui.notifications.info(`Applied Glow: ${selected.label}`);
      }
    },
    remove: {
      label: "Remove",
      callback: async () => {
        await clearAllRarityFilters();
        ui.notifications.info("Rarity effects removed.");
      }
    },
    cancel: { label: "Cancel" }
  },
  default: "apply",
  render: (html) => {
    const root = html[0];
    const select = root.querySelector('select[name="rarity"]');
    const preview = root.querySelector('[data-role="preview"]');
    const mode = root.querySelector('[data-role="mode"]');


    const paint = () => {
      const opt = select?.selectedOptions?.[0];
      if (!opt) return;


      const c1 = opt.dataset.c1 || null;
      const c2 = opt.dataset.c2 || null;
      const glow = opt.dataset.glow === "1";


      if (!c1) {
        preview.innerHTML = `<span style="opacity:.65;">No color</span>`;
      } else if (c2) {
        preview.innerHTML = `
          <span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${c1};display:inline-block;"></span>
          <span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${c2};display:inline-block;"></span>
        `;
      } else {
        preview.innerHTML = `<span style="width:26px;height:26px;border:1px solid #666;border-radius:4px;background:${c1};display:inline-block;"></span>`;
      }


      mode.textContent = glow
        ? "Mode: Glow FX (fog + ray + glow)"
        : c2
          ? "Mode: Bevel (2 colors)"
          : c1
            ? "Mode: Glow (1 color)"
            : "Mode: None";
    };


    select?.addEventListener("change", paint);
    paint();
  }
}).render(true);