Complete Examples

Example 1: Full Custom System Adapter

// my-system-adapter.js

class MySystemAdapter {
  constructor() {
    this.systemId = "my-custom-system";
  }

  getStats(actor, configAttributes) {
    if (!configAttributes?.length) return [];

    return configAttributes.map((attr) => {
      const raw = foundry.utils.getProperty(actor, attr.path);
      let value = 0;
      let max = 0;

      if (typeof raw === "object" && raw !== null) {
        value = raw.value ?? 0;
        max = raw.max ?? 0;
      } else if (typeof raw === "number") {
        value = raw;
      }

      return {
        path: attr.path,
        label: attr.label,
        color: attr.color,
        value,
        max,
        percent: max > 0 ? Math.clamp((value / max) * 100, 0, 100) : 0,
        style: attr.style || "bar",
        subtype: "resource",
        x: attr.x || 0,
        y: attr.y || 0,
      };
    });
  }

  getConditions(actor) {
    return (actor.temporaryEffects || [])
      .filter((e) => e.icon)
      .map((e) => ({
        id: e.id || e.name,
        src: e.icon,
        name: e.name || "Unknown",
        value: e.value ?? null,
      }));
  }

  async removeCondition(actor, conditionId) {
    const effect = actor.effects.find(
      (e) => e.id === conditionId || e.name === conditionId
    );
    if (effect) {
      await effect.delete();
      ui.notifications.info(`Removed ${effect.name} from ${actor.name}`);
    }
  }

  getActionCategories(actor) {
    return [
      { id: "combat", systemId: "combat", label: "Combat", icon: "fa-solid fa-swords", type: "submenu" },
      { id: "magic", systemId: "magic", label: "Magic", icon: "fa-solid fa-hat-wizard", type: "submenu" },
      { id: "items", systemId: "items", label: "Items", icon: "fa-solid fa-backpack", type: "submenu" },
      { id: "sheet", label: "Sheet", icon: "fa-solid fa-id-card", type: "sheet" },
    ];
  }

  async getSubMenuData(actor, categoryId) {
    const index = parseInt(categoryId.split("-").pop());
    const categories = this.getActionCategories(actor);
    const category = categories[index];

    if (!category?.systemId) {
      return { title: "", items: [] };
    }

    switch (category.systemId) {
      case "combat":
        return this._getCombatItems(actor);
      case "magic":
        return this._getMagicItems(actor);
      case "items":
        return this._getInventoryItems(actor);
      default:
        return { title: category.label, items: [] };
    }
  }

  _getCombatItems(actor) {
    const weapons = actor.items.filter((i) => i.type === "weapon" && i.system.equipped);
    const abilities = actor.items.filter((i) => i.type === "ability" && i.system.actionType === "combat");

    return {
      title: "Combat",
      hasTabs: true,
      items: {
        weapons: weapons.map((w) => ({
          id: w.id,
          name: w.name,
          img: w.img,
          description: `${w.system.damage} damage`,
        })),
        abilities: abilities.map((a) => ({
          id: a.id,
          name: a.name,
          img: a.img,
          cost: a.system.cost || "Free",
        })),
      },
      tabLabels: {
        weapons: "Weapons",
        abilities: "Abilities",
      },
    };
  }

  _getMagicItems(actor) {
    const spells = actor.items.filter((i) => i.type === "spell");
    const grouped = {};
    const tabLabels = {};

    spells.forEach((spell) => {
      const level = spell.system.level || 0;
      const key = `level-${level}`;

      if (!grouped[key]) {
        grouped[key] = [];
        tabLabels[key] = level === 0 ? "Cantrips" : `Level ${level}`;
      }

      grouped[key].push({
        id: spell.id,
        name: spell.name,
        img: spell.img,
        cost: spell.system.castingTime || "1 Action",
        uses: spell.system.uses,
      });
    });

    return {
      title: "Magic",
      hasTabs: true,
      items: grouped,
      tabLabels,
    };
  }

  _getInventoryItems(actor) {
    const consumables = actor.items.filter((i) => i.type === "consumable");
    const equipment = actor.items.filter((i) => i.type === "equipment");

    return {
      title: "Items",
      hasTabs: true,
      items: {
        consumables: consumables.map((c) => ({
          id: c.id,
          name: c.name,
          img: c.img,
          uses: c.system.quantity ? { value: c.system.quantity, max: c.system.quantity } : null,
        })),
        equipment: equipment.map((e) => ({
          id: e.id,
          name: e.name,
          img: e.img,
          description: e.system.equipped ? "Equipped" : "",
        })),
      },
      tabLabels: {
        consumables: "Consumables",
        equipment: "Equipment",
      },
    };
  }

  async useItem(actor, itemId) {
    if (itemId.startsWith("macro-")) {
      const macro = game.macros.get(itemId.replace("macro-", ""));
      return macro?.execute({ actor });
    }

    const item = actor.items.get(itemId);
    if (!item) {
      ui.notifications.warn(`Item not found: ${itemId}`);
      return;
    }

    if (typeof item.use === "function") return item.use();
    if (typeof item.roll === "function") return item.roll();
    return item.sheet.render(true);
  }

  getDefaultAttributes() {
    return [
      { path: "system.health", label: "HP", color: "#e61c34", style: "bar" },
      { path: "system.mana", label: "MP", color: "#3498db", style: "bar" },
    ];
  }

  getTrackableAttributes(actor) {
    const paths = [];

    const scan = (obj, prefix, depth = 0) => {
      if (depth > 4) return;

      for (const [key, value] of Object.entries(obj)) {
        if (typeof value === "object" && value !== null) {
          if ("value" in value && typeof value.value === "number") {
            paths.push({
              path: `${prefix}.${key}.value`,
              label: key.charAt(0).toUpperCase() + key.slice(1),
            });
          } else {
            scan(value, `${prefix}.${key}`, depth + 1);
          }
        }
      }
    };

    if (actor?.system) {
      scan(actor.system, "system");
    }

    return paths;
  }

  getDefaultStatusEffects() {
    return [
      {
        id: "dead",
        label: "Dead",
        filters: { grayscale: 100, brightness: 50 },
        tintColor: "#000000",
        tintAlpha: 0.5,
      },
    ];
  }
}

// Registration
Hooks.once("stylish-action-hud.apiReady", (api) => {
  api.registerSystemAdapter("my-custom-system", MySystemAdapter, {
    priority: 0,
    source: "my-system-module",
  });

  api.registerDefaultAttributes("my-custom-system", [
    { path: "system.health", label: "HP", color: "#e61c34", style: "bar" },
    { path: "system.mana", label: "MP", color: "#3498db", style: "bar" },
  ]);

  api.registerDefaultLayout("my-custom-system", [
    { systemId: "combat", label: "Combat", icon: "fa-solid fa-swords" },
    { systemId: "magic", label: "Magic", icon: "fa-solid fa-hat-wizard" },
    { systemId: "items", label: "Items", icon: "fa-solid fa-backpack" },
  ]);
});

Example 2: Adding Custom Actions via Hooks

// Add a "Quick Rest" button without creating a full adapter

Hooks.once("stylish-action-hud.apiReady", (api) => {
  // Add custom category
  api.registerActionMenuCategory({
    id: "quick-rest",
    label: "Quick Rest",
    icon: "fa-solid fa-bed",
    type: "system",  // Triggers executeAction
  }, {
    priority: -10,  // Appears at the end
    source: "my-rest-module",
    isCompatible: ({ actor }) => actor.type === "character",
  });
});

// Handle the system action
Hooks.on("stylish-action-hud.modifyActionMenuCategories", (categories, actor) => {
  // Find our category
  const restCategory = categories.find((c) => c.id === "quick-rest");
  if (restCategory) {
    // Add click handler reference
    restCategory.actionId = "quick-rest-action";
  }
});

// Execute when clicked (requires adapter support or hook)
// This would typically be handled by overriding executeAction in an adapter

Example 3: Modifying Existing Submenu Data

// Add custom items to existing D&D 5e spells menu

Hooks.on("stylish-action-hud.modifyActionMenuData", (data, actor, categoryId) => {
  // Only modify spell menus
  if (!data.title?.toLowerCase().includes("spell")) return;

  // Add a "Favorites" tab at the beginning
  const favorites = actor.items.filter(
    (i) => i.type === "spell" && i.getFlag("my-module", "favorite")
  );

  if (favorites.length > 0) {
    // Restructure items to add favorites first
    const newItems = {
      favorites: favorites.map((s) => ({
        id: s.id,
        name: s.name,
        img: s.img,
        isFavorite: true,
      })),
      ...data.items,
    };

    const newLabels = {
      favorites: "Favorites",
      ...data.tabLabels,
    };

    data.items = newItems;
    data.tabLabels = newLabels;
  }
});

Example 4: Dynamic Defaults Based on Installed Modules

Hooks.once("stylish-action-hud.apiReady", (api) => {
  // Register defaults that change based on installed modules
  api.registerDefaultLayout("dnd5e", (context) => {
    const layout = [
      { systemId: "attack", label: "Attacks", icon: "fa-solid fa-sword" },
      { systemId: "magic", label: "Spells", icon: "fa-solid fa-wand-sparkles" },
    ];

    // If Tidy5e is installed, add a features tab
    if (game.modules.get("tidy5e-sheet")?.active) {
      layout.push({
        systemId: "features",
        label: "Features",
        icon: "fa-solid fa-star",
      });
    }

    // If Item Piles is installed, add loot tab
    if (game.modules.get("item-piles")?.active) {
      layout.push({
        systemId: "loot",
        label: "Loot",
        icon: "fa-solid fa-coins",
      });
    }

    layout.push({
      systemId: "inventory",
      label: "Items",
      icon: "fa-solid fa-bag-shopping",
    });

    return layout;
  }, {
    priority: 5,  // Higher than default
    mode: "replace",
    source: "my-compatibility-module",
    isCompatible: () => game.system.id === "dnd5e",
  });
});