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",
});
});