System Adapter API

System adapters define how Stylish Action HUD interacts with different game systems. Each adapter translates system-specific data (attributes, items, spells) into a format the HUD can display.

BaseSystemAdapter Class

All custom adapters should extend or implement the interface defined by BaseSystemAdapter.

class BaseSystemAdapter {
  constructor() {
    this.systemId = "generic";
  }

  // Required Methods
  getStats(actor, configAttributes) { }
  getConditions(actor) { }
  getActionCategories(actor) { }
  getSubMenuData(actor, categoryId) { }

  // Optional Methods
  updateAttribute(actor, path, input) { }
  removeCondition(actor, conditionId) { }
  executeAction(actor, actionId) { }
  useItem(actor, itemId) { }
  getTrackableAttributes(actor) { }
  getDefaultAttributes() { }
  getDefaultStatusEffects() { }
}

Accessing BaseSystemAdapter (v2.5.8+)

Use api.BaseSystemAdapter when a companion module needs to extend Stylish Action HUD's generic adapter behavior. This keeps the companion module on the public API surface instead of importing scripts/systems/base.js, which is an internal module file.

The safest timing is the API ready hook:

Hooks.once("stylish-action-hud.apiReady", (api) => {
  const { BaseSystemAdapter } = api;

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

    async getSubMenuData(actor, categoryId) {
      if (String(categoryId).startsWith("custom-")) {
        return super.getSubMenuData(actor, categoryId);
      }

      return this._getSystemSubMenuData(actor, categoryId, { label: categoryId });
    }
  }

  api.registerSystemAdapter("my-system", MySystemAdapter, {
    priority: 1,
    source: "my-module",
    isCompatible: (context) => context.system.id === "my-system",
  });
});

If the adapter class lives in another file, export a factory that receives BaseSystemAdapter:

// adapter.js
export function createMySystemAdapter(BaseSystemAdapter) {
  return class MySystemAdapter extends BaseSystemAdapter {
    constructor() {
      super();
      this.systemId = "my-system";
    }
  };
}

// main.js
import { createMySystemAdapter } from "./adapter.js";

Hooks.once("stylish-action-hud.apiReady", (api) => {
  const MySystemAdapter = createMySystemAdapter(api.BaseSystemAdapter);
  api.registerSystemAdapter("my-system", MySystemAdapter, {
    source: "my-module",
  });
});

After stylish-action-hud.apiReady has fired, the same class is also available at game.modules.get("stylish-action-hud").api.BaseSystemAdapter and window.stylishActionHUD.BaseSystemAdapter. Prefer the hook for registration because it guarantees that the API object has been initialized.

Do not import modules/stylish-action-hud/scripts/systems/base.js from a companion module. That path is not part of the public API and can change between releases.

Creating a Custom Adapter

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

  /**
   * Get actor statistics for the Party HUD
   * @param {Actor} actor - The Foundry actor
   * @param {Array} configAttributes - Configured attributes [{path, label, color}, ...]
   * @returns {Array} - Processed attributes [{path, label, color, value, max, percent}, ...]
   */
  getStats(actor, configAttributes) {
    if (!configAttributes || configAttributes.length === 0) return [];

    return configAttributes.map((attr) => {
      const value = foundry.utils.getProperty(actor, `${attr.path}.value`) ?? 0;
      const max = foundry.utils.getProperty(actor, `${attr.path}.max`) ?? 0;
      const percent = Math.clamp((value / (max || 1)) * 100, 0, 100);

      return {
        path: attr.path,
        label: attr.label,
        color: attr.color,
        value: value,
        max: max,
        percent: percent,
        style: attr.style || "bar",
        subtype: "resource",
        x: attr.x || 0,
        y: attr.y || 0,
      };
    });
  }

  /**
   * Get active conditions/effects for display
   * @param {Actor} actor
   * @returns {Array} - [{id, src, name, value}, ...]
   */
  getConditions(actor) {
    const effects = actor.temporaryEffects || [];

    return effects
      .map((e) => ({
        id: e.id || e.flags?.core?.statusId || e.name || "unknown",
        src: e.icon,
        name: e.name || e.label || "Unknown",
        value: e.value ?? null,
      }))
      .filter((c) => c.src);
  }

  /**
   * Define action menu button categories
   * @param {Actor} actor
   * @returns {Array} - Action category definitions
   */
  getActionCategories(actor) {
    return [
      {
        id: "attacks",
        systemId: "attacks",
        label: "Attacks",
        icon: "fa-solid fa-sword",
        type: "submenu",
      },
      {
        id: "spells",
        systemId: "spells",
        label: "Spells",
        icon: "fa-solid fa-sparkles",
        type: "submenu",
      },
      {
        id: "inventory",
        systemId: "inventory",
        label: "Inventory",
        icon: "fa-solid fa-backpack",
        type: "submenu",
      },
      {
        id: "sheet",
        label: "Character Sheet",
        icon: "fa-solid fa-user",
        type: "sheet",
      },
    ];
  }

  /**
   * Get submenu data for a category
   * @param {Actor} actor
   * @param {String} categoryId
   * @returns {Object} - {title, items, hasTabs, tabLabels, ...}
   */
  async getSubMenuData(actor, categoryId) {
    // Parse category index
    const parts = categoryId.split("-");
    const index = parseInt(parts[parts.length - 1]);

    // Get category definition
    const categories = this.getActionCategories(actor);
    const category = categories[index];

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

    return await this._getSystemSubMenuData(actor, category.systemId, category);
  }

  /**
   * Internal: Load system-specific submenu data
   */
  async _getSystemSubMenuData(actor, systemId, menuData) {
    switch (systemId) {
      case "attacks":
        return this._getAttacks(actor, menuData);
      case "spells":
        return this._getSpells(actor, menuData);
      case "inventory":
        return this._getInventory(actor, menuData);
      default:
        return { title: menuData.label, items: [] };
    }
  }

  _getAttacks(actor, menuData) {
    const weapons = actor.items.filter((i) => i.type === "weapon");

    return {
      title: menuData.label,
      hasTabs: false,
      items: weapons.map((w) => ({
        id: w.id,
        name: w.name,
        img: w.img,
        description: w.system.description?.value || "",
      })),
    };
  }

  // ... implement other methods
}

Registering an Adapter

Hooks.once("stylish-action-hud.apiReady", (api) => {
  api.registerSystemAdapter("my-system", MySystemAdapter, {
    priority: 10,           // Higher priority = preferred (default: 0)
    source: "my-module",    // Identifier for debugging
    isCompatible: (context) => {
      // Optional: Check compatibility conditions
      return context.system.id === "my-system";
    },
  });
});

Registration Options

Option Type Default Description
priority number 0 Higher priority adapters are preferred
source string "unknown" Module identifier for debugging
isCompatible function null Function returning boolean for compatibility check

Priority System