diff --git a/.github/readme/token-action-hud.gif b/.github/readme/token-action-hud.gif
new file mode 100644
index 0000000..71dba22
Binary files /dev/null and b/.github/readme/token-action-hud.gif differ
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..5c304e6
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,45 @@
+name: Release Creation
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ # get part of the tag after the `v`
+ - name: Extract tag version number
+ id: get_version
+ uses: battila7/get-version-action@v2
+
+ # Substitute the Manifest and Download URLs in the module.json
+ - name: Substitute Manifest and Download Links For Versioned Ones
+ id: sub_manifest_link_version
+ uses: microsoft/variable-substitution@v1
+ with:
+ files: 'module.json'
+ env:
+ version: ${{steps.get_version.outputs.version-without-v}}
+ url: https://github.com/${{github.repository}}
+ manifest: https://github.com/${{github.repository}}/releases/latest/download/module.json
+ download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/module.zip
+
+ # Create a zip file with all files required by the module to add to the release
+ - run: zip -r ./module.zip module.json readme.md LICENSE languages/ scripts/
+
+ # Create a release for this specific version
+ - name: Update Release with Files
+ id: create_version_release
+ uses: ncipollo/release-action@v1
+ with:
+ allowUpdates: true # Set this to false if you want to prevent updating existing releases
+ name: ${{ github.event.release.name }}
+ draft: ${{ github.event.release.unpublished }}
+ prerelease: ${{ github.event.release.prerelease }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ artifacts: './module.json, ./module.zip'
+ tag: ${{ github.event.release.tag_name }}
+ body: ${{ github.event.release.body }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ac2754
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/*
+.eslintrc.js
+.eslintrc.json
+package-lock.json
+package.json
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..6c2ff60
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "githubPullRequests.ignoredPullRequestBranches": [
+ "master"
+ ]
+}
\ No newline at end of file
diff --git a/languages/br.json b/languages/br.json
new file mode 100644
index 0000000..d3f4de2
--- /dev/null
+++ b/languages/br.json
@@ -0,0 +1,57 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Atributos",
+ "bonusActions": "Ações de bônus",
+ "checks": "Testes",
+ "conditions": "Condições",
+ "crewActions": "Ações da tripulação",
+ "lair": "Covil",
+ "lairActions": "Ações de covil",
+ "legendary": "Lendária",
+ "legendaryActions": "Ações Lendárias",
+ "otherActions": "Outras ações",
+ "rests": "Descansos",
+ "saves": "Salvaguarda",
+ "skills": "Pericias",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "Habilidades e atributos usarão uma abreviação de três caracteres.",
+ "name": "Abrevie nomes de habilidades e atributos"
+ },
+ "displaySpellInfo": {
+ "hint": "As informações do componente de magia, a concentração e o estatus ritual serão anotados ao lado do nome do feitiço.",
+ "name": "Exibir informação de magias"
+ },
+ "itemMacroReplace": {
+ "hint": "Select which of the items will be shown in the inventory.",
+ "name": "Item-Macro: item macro, original item, or both"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Mostrar talentos passivos",
+ "name": "Mostrar talentos passivos"
+ },
+ "showSlowActions": {
+ "hint": "Mostrar ações que levam mais de uma rodada",
+ "name": "Mostrar ações lentas"
+ },
+ "showUnchargedItems": {
+ "hint": "If enabled, items are shown in the HUD even if they have no remaining charges.",
+ "name": "Show empty items (includes items and spells)"
+ },
+ "showUnequippedItems": {
+ "hint": "Todos os itens dos NPCs serão exibidos, não apenas itens equipados.",
+ "name": "Exibir todos os itens de NPCs"
+ },
+ "showUnpreparedSpells": {
+ "hint": "Se desativado, magias como cantrips, magias inatas, pactos e à vontade precisam ser 'preparadas' através dos detalhes da magia que serão mostrados no HUD. ",
+ "name": "Exibe todas as magias não preparadas"
+ },
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/cn.json b/languages/cn.json
new file mode 100644
index 0000000..912662d
--- /dev/null
+++ b/languages/cn.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "属性",
+ "bonusActions": "獎勵行動",
+ "checks": "检定",
+ "conditions": "状态",
+ "crewActions": "船員行動",
+ "lair": "巢穴",
+ "lairActions": "巢穴行動",
+ "legendary": "传奇",
+ "legendaryActions": "傳奇動作",
+ "otherActions": "其他行為",
+ "rests": "休息",
+ "skills": "技能",
+ "settings": {
+ "abbreviateSkills": {
+ "name": "缩写技能和属性名",
+ "hint": "启用后,技能和属性都会用其三字母缩写。"
+ },
+ "displaySpellInfo": {
+ "name": "显示法术信息",
+ "hint": "启用后,法术构材信息、专注、仪式状态会在法术名旁标记出来。"
+ },
+ "itemMacroReplace": {
+ "name": "物品宏:覆盖原有物品",
+ "hint": "启用后,有宏的物品的物品宏将会覆盖原本的物品,否则两个都会显示。"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "name": "顯示被動功能",
+ "hint": "顯示被動功能"
+ },
+ "showSlowActions": {
+ "name": "顯示緩慢的動作",
+ "hint": "顯示超過一輪的動作"
+ },
+ "showUnchargedItems": {
+ "name": "显示空物品(包含物品和法术)",
+ "hint": "启用后,HUD上会显示没有剩余充能的物品。"
+ },
+ "showUnequippedItems": {
+ "name": "显示所有NPC物品",
+ "hint": "启用后,NPC的所有物品都会显示,而不仅仅是已装备的物品。"
+ },
+ "showUnpreparedSpells": {
+ "name": "显示所有未准备法术",
+ "hint": "禁用后,戏法、天生、契约和随意使用的法术需要在法术详情页设置为‘准备’状态才能显示在HUD上。"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/de.json b/languages/de.json
new file mode 100644
index 0000000..fd1b99e
--- /dev/null
+++ b/languages/de.json
@@ -0,0 +1,57 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Abilities",
+ "bonusActions": "Bonus Actions",
+ "checks": "Checks",
+ "conditions": "Conditions",
+ "crewActions": "Crew Actions",
+ "lair": "Lair",
+ "lairActions": "Lair Actions",
+ "legendary": "Legendär",
+ "legendaryActions": "Legendary Actions",
+ "otherActions": "Other Actions",
+ "rests": "Rests",
+ "skills": "Skills",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "If enabled, skills and abilities will use a three-character abbreviation.",
+ "name": "Abbreviate Skill and Ability Names"
+ },
+ "displaySpellInfo": {
+ "hint": "If enabled, spell component information, concentration, and ritual status will be noted next to the spell name.",
+ "name": "Display spell information"
+ },
+ "itemMacroReplace": {
+ "hint": "Select which of the items will be shown in the inventory.",
+ "name": "Item-Macro: item macro, original item, or both"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Show passive feats",
+ "name": "Show passive feats"
+ },
+ "showSlowActions": {
+ "hint": "Aktionen anzeigen, die länger als eine Runde dauern",
+ "name": "Langsame Aktionen anzeigen"
+ },
+ "showUnchargedItems": {
+ "hint": "If enabled, items are shown in the HUD even if they have no remaining charges.",
+ "name": "Show empty items (includes items and spells)"
+ },
+ "showUnequippedItems": {
+ "hint": "If enabled, all items are shown for NPCs, not just equipped items.",
+ "name": "Show all NPC items"
+ },
+ "showUnpreparedSpells": {
+ "hint": "If disabled, spells such as cantrips, innate, pact, and at-will spells need to be 'prepared' via the spell details to be shown on the HUD.",
+ "name": "Show all non-preparable spells"
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/en.json b/languages/en.json
new file mode 100644
index 0000000..9cfaee9
--- /dev/null
+++ b/languages/en.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Abilities",
+ "bonusActions": "Bonus Actions",
+ "checks": "Checks",
+ "conditions": "Conditions",
+ "crewActions": "Crew Actions",
+ "lair": "Lair",
+ "lairActions": "Lair Actions",
+ "legendary": "Legendary",
+ "legendaryActions": "Legendary Actions",
+ "otherActions": "Other Actions",
+ "rests": "Rests",
+ "skills": "Skills",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "Display skill and ability names as a three-character abbreviation",
+ "name": "Abbreviate Skill and Ability Names"
+ },
+ "displaySpellInfo": {
+ "hint": "Display spell component information, concentration, and ritual status next to the spell's name",
+ "name": "Display Spell Information"
+ },
+ "itemMacroReplace": {
+ "hint": "Select which of the items will be shown in the inventory",
+ "name": "Item-Macro: item macro, original item, or both"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "Show items without an activation cost",
+ "name": "Show Items without Activation Costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Show passive features",
+ "name": "Show Passive Features"
+ },
+ "showSlowActions": {
+ "hint": "Show actions that take longer than one round",
+ "name": "Show Slow Actions"
+ },
+ "showUnchargedItems": {
+ "hint": "Show items with no charges remaining",
+ "name": "Show Uncharged Items"
+ },
+ "showUnequippedItems": {
+ "hint": "Show unequipped items",
+ "name": "Show Unequipped Items"
+ },
+ "showUnpreparedSpells": {
+ "hint": "Show unprepared spells",
+ "name": "Show Unprepared Spells"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/es.json b/languages/es.json
new file mode 100644
index 0000000..7063cb3
--- /dev/null
+++ b/languages/es.json
@@ -0,0 +1,57 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Características",
+ "bonusActions": "Acciones de bonificación",
+ "checks": "Pruebas",
+ "conditions": "Estados",
+ "crewActions": "Acciones de la tripulación",
+ "inventory": "Inventario",
+ "lair": "Guarida",
+ "lairActions": "Acciones de guarida",
+ "legendary": "Legendario",
+ "legendaryActions": "Acciones legendarias",
+ "otherActions": "Otras acciones",
+ "rests": "Descansos",
+ "skills": "Habilidades",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "Si se activa, las habilidades y características utilizarán una abreviatura de tres letras.",
+ "name": "Nombres de habilidades y características abreviados"
+ },
+ "displaySpellInfo": {
+ "hint": "Si se activa, la información de los componentes del conjuro, la concentración y el estado del ritual se indicarán junto al nombre del conjuro.",
+ "name": "Mostrar información del conjuro"
+ },
+ "itemMacroReplace": {
+ "hint": "Selecciona qué elementos se mostrarán en el inventario.",
+ "name": "Objeto-Macro: macro del objeto, objeto original, o ambos"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Mostrar rasgos pasivos",
+ "name": "Mostrar rasgos pasivos"
+ },
+ "showSlowActions": {
+ "hint": "Mostrar acciones que toman más de una ronda",
+ "name": "Mostrar acciones lentas"
+ },
+ "showUnchargedItems": {
+ "hint": "Si se activa, los elementos se muestran en el HUD incluso si no tienen cargas restantes.",
+ "name": "Mostrar elementos sin carga (incluye objetos y conjuros)"
+ },
+ "showUnequippedItems": {
+ "hint": "Si se activa, se muestran todos los objetos de los PNJ, no sólo los equipados.",
+ "name": "Mostrar todos los objetos de los PNJ"
+ },
+ "showUnpreparedSpells": {
+ "hint": "Si se desactiva, los conjuros como los trucos, innatos, del pactos y los conjuros a voluntad, se deben 'preparar' a través de los detalles del conjuro para que se muestren en el HUD.",
+ "name": "Mostrar todos los conjuros no preparables"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/fr.json b/languages/fr.json
new file mode 100644
index 0000000..1b3a227
--- /dev/null
+++ b/languages/fr.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Abilités",
+ "bonusActions": "Actions bonus",
+ "checks": "Tests",
+ "conditions": "Conditions",
+ "crewActions": "Actions de l'équipage",
+ "lair": "Antre",
+ "lairActions": "Actions de antre",
+ "legendary": "Légendaire",
+ "legendaryActions": "Actions légendaires",
+ "otherActions": "D'autres actions",
+ "rests": "Repos",
+ "skills": "Compétences",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "Si activée les caractéristiques et les compétences utiliserons une abréviation de 3 caractères.",
+ "name": "Abréger les noms des caractéristiques et des compétences"
+ },
+ "displaySpellInfo": {
+ "hint": "Si activée, les informations sur les composantes de sort, la concentration et les rituels seront affichées à côté du nom du sort.",
+ "name": "Afficher les informations des sorts"
+ },
+ "itemMacroReplace": {
+ "hint": "Sélectionne quels objets seront montrés dans l'inventaire.",
+ "name": "Item-Macro: macro d'objet, objet original, ou les deux"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Afficher les dons passifs",
+ "name": "Afficher les dons passifs"
+ },
+ "showSlowActions": {
+ "hint": "Afficher les actions qui prennent plus d'un tour",
+ "name": "Afficher les actions lentes"
+ },
+ "showUnchargedItems": {
+ "hint": "Si activée, les objets seront affichés dans le HUD même s'ils n'ont plus de charges.",
+ "name": "Afficher les objets vides (y compris les objets et les sorts)"
+ },
+ "showUnequippedItems": {
+ "hint": "Si activée, tous les objets seront affichés pour les PNJ, et pas seulement les objets équipés.",
+ "name": "Afficher tous les objets des PNJs"
+ },
+ "showUnpreparedSpells": {
+ "hint": "Si désactivée, les sorts tels que les tours de magie, innés, de pacte et à volonté doivent être préparés dans le détail du sort pour être affiché dans le HUD.",
+ "name": "Afficher tous les sorts non préparés"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/it.json b/languages/it.json
new file mode 100644
index 0000000..635a60d
--- /dev/null
+++ b/languages/it.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Capacità",
+ "bonusActions": "Azioni bonus",
+ "checks": "Caratteristiche",
+ "conditions": "Condizioni",
+ "crewActions": "Azioni dell'equipaggio",
+ "lair": "Tana",
+ "lairActions": "Azioni di Tana",
+ "legendary": "Leggendarie",
+ "legendaryActions": "Azioni Leggendarie",
+ "otherActions": "Altre azioni",
+ "rests": "Riposi",
+ "skills": "Abilità",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "If enabled, skills and abilities will use a three-character abbreviation.",
+ "name": "Abbreviate Skill and Ability Names"
+ },
+ "displaySpellInfo": {
+ "hint": "If enabled, spell component information, concentration, and ritual status will be noted next to the spell name.",
+ "name": "Display spell information"
+ },
+ "itemMacroReplace": {
+ "hint": "Select which of the items will be shown in the inventory.",
+ "name": "Item-Macro: item macro, original item, or both"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "If enabled, Items that have no activation information will be added to the Inventory list",
+ "name": "Show Items without Activation costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Mostra privilegi passivi",
+ "name": "Mostra privilegi passivi"
+ },
+ "showSlowActions": {
+ "hint": "Mostra le azioni che richiedono più di un round",
+ "name": "Mostra azioni lente"
+ },
+ "showUnchargedItems": {
+ "hint": "If enabled, items are shown in the HUD even if they have no remaining charges.",
+ "name": "Show empty items (includes items and spells)"
+ },
+ "showUnequippedItems": {
+ "hint": "If enabled, all items are shown for NPCs, not just equipped items.",
+ "name": "Show all NPC items"
+ },
+ "showUnpreparedSpells": {
+ "hint": "If disabled, spells such as cantrips, innate, pact, and at-will spells need to be 'prepared' via the spell details to be shown on the HUD.",
+ "name": "Show all non-preparable spells"
+ }
+ }
+ }
+ }
+}
diff --git a/languages/ja.json b/languages/ja.json
new file mode 100644
index 0000000..a89d003
--- /dev/null
+++ b/languages/ja.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "能力値",
+ "bonusActions": "ボーナス・アクション",
+ "checks": "判定",
+ "conditions": "状態",
+ "crewActions": "乗組員の行動",
+ "lair": "住処",
+ "lairActions": "住処アクション",
+ "legendary": "伝説",
+ "legendaryActions": "伝説的アクション",
+ "otherActions": "その他のアクション",
+ "rests": "休憩",
+ "skills": "技能",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "技能や能力名をすべて3文字に省略します",
+ "name": "特技や能力名を省略する"
+ },
+ "displaySpellInfo": {
+ "hint": "呪文の構成要素、集中、儀式などの追加の情報が名前の隣に表示されます。",
+ "name": "呪文の情報を表示する。"
+ },
+ "itemMacroReplace": {
+ "hint": "アイテムマクロを含むマクロはオリジナルを上書きします。それ以外では両方が表示されます。",
+ "name": "アイテムマクロ:オリジナルを上書き"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "有効にすると、有効化の情報がないアイテムを所持品リストに追加します。",
+ "name": "有効化コストのないアイテムの表示"
+ },
+ "showPassiveFeats": {
+ "hint": "パッシブ特技を表示",
+ "name": "パッシブ特技を表示"
+ },
+ "showSlowActions": {
+ "hint": "1ラウンド以上かかるアクションを表示",
+ "name": "遅いアクションを表示"
+ },
+ "showUnchargedItems": {
+ "hint": "ホットバーにチャージや使用回数が空のものも表示します。",
+ "name": "空のアイテムを表示する(呪文含む)"
+ },
+ "showUnequippedItems": {
+ "hint": "NPCの装備済みのアイテムだけではなく、他の所持しているだけのアイテムも表示します。",
+ "name": "NPCのすべてのアイテムを表示する"
+ },
+ "showUnpreparedSpells": {
+ "hint": "これがオフになっていると初級呪文、生得、契約等の呪文はすべて手動で「準備状態」にしないと表示されなくなります。",
+ "name": "すべての非準備呪文も表示する"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/ko.json b/languages/ko.json
new file mode 100644
index 0000000..0decb58
--- /dev/null
+++ b/languages/ko.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "능력치",
+ "bonusActions": "보너스 액션",
+ "checks": "판정",
+ "conditions": "상태",
+ "crewActions": "승무원 조치",
+ "lair": "소굴",
+ "lairActions": "소굴 액션",
+ "legendary": "전설적",
+ "legendaryActions": "전설적 액션",
+ "otherActions": "다른 행동",
+ "rests": "휴식",
+ "skills": "기술",
+ "settings": {
+ "abbreviateSkills": {
+ "name": "기술 및 능력치 이름을 줄임",
+ "hint": "활성화할 경우 기술 및 능력치는 3 글자 약어로 표시된다."
+ },
+ "displaySpellInfo": {
+ "name": "주문 정보 표시",
+ "hint": "활성화할 경우 주문 구성요소 정보, 집중, 의식 상태가 주문 이름 옆에 표시된다."
+ },
+ "itemMacroReplace": {
+ "name": "아이템 매크로: 원항목 덮어쓰기",
+ "hint": "활성화될 경우 아이템 매크로가 있는 아이템은 원래 아이템을 덮어쓰게 되며 그렇지 않을 경우 두 옵션이 모두 표시된다."
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "Show items without an activation cost",
+ "name": "Show Items without Activation Costs"
+ },
+ "showPassiveFeats": {
+ "name": "패시브 투수 표시",
+ "hint": "패시브 투수 표시"
+ },
+ "showSlowActions": {
+ "name": "느린 동작 표시",
+ "hint": "한 라운드보다 오래 걸리는 작업 표시"
+ },
+ "showUnchargedItems": {
+ "name": "빈 항목 표시 (아이템과 주문 포함)",
+ "hint": "활성화할 경우 남은 충전량이 없더라도 HUD에 항목이 표시된다."
+ },
+ "showUnequippedItems": {
+ "name": "모든 NPC 아이템 표시",
+ "hint": "활성화할 경우 NPC가 장비한 아이템 뿐만이 아닌, 모든 아이템이 표시된다."
+ },
+ "showUnpreparedSpells": {
+ "name": "모든 준비 불가능한 주문들 표시",
+ "hint": "비활성화할 경우 캔트립(소마법), 선천적, 계약, 의지(At-will) 주문들은 주문 상세 정보에서 '준비'하여야 HUD에 표시된다."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/languages/pl.json b/languages/pl.json
new file mode 100644
index 0000000..ed2a7ab
--- /dev/null
+++ b/languages/pl.json
@@ -0,0 +1,56 @@
+{
+ "tokenActionHud": {
+ "dnd5e": {
+ "abilities": "Zdolności",
+ "bonusActions": "Akcje bonusowe",
+ "checks": "Sprawdzenie",
+ "conditions": "Stany",
+ "crewActions": "Akcje załogi",
+ "lair": "Matecznik",
+ "lairActions": "Matecznik akcje",
+ "legendary": "Legendarny",
+ "legendaryActions": "Legendarny akcje",
+ "otherActions": "Inne akcje",
+ "rests": "Odpoczynki",
+ "skills": "Umiejętności",
+ "settings": {
+ "abbreviateSkills": {
+ "hint": "If enabled, skills and abilities will use a three-character abbreviation.",
+ "name": "Abbreviate Skill and Ability Names"
+ },
+ "displaySpellInfo": {
+ "hint": "If enabled, spell component information, concentration, and ritual status will be noted next to the spell name.",
+ "name": "Display spell information"
+ },
+ "itemMacroReplace": {
+ "hint": "If enabled, items with an item macro will overwrite their original, otherwise both options will be shown.",
+ "name": "Item-Macro: overwrite original item"
+ },
+ "showItemsWithoutActivationCosts": {
+ "hint": "Show items without an activation cost",
+ "name": "Show Items without Activation Costs"
+ },
+ "showPassiveFeats": {
+ "hint": "Pokaż funkcje pasywne",
+ "name": "Pokaż funkcje pasywne"
+ },
+ "showSlowActions": {
+ "hint": "Pokaż działania, które trwają dłużej niż jedną rundę",
+ "name": "Pokaż powolne działania"
+ },
+ "showUnchargedItems": {
+ "hint": "If enabled, items are shown in the HUD even if they have no remaining charges.",
+ "name": "Show empty items (includes items and spells)"
+ },
+ "showUnequippedItems": {
+ "hint": "If enabled, all items are shown for NPCs, not just equipped items.",
+ "name": "Show all NPC items"
+ },
+ "showUnpreparedSpells": {
+ "hint": "If disabled, spells such as cantrips, innate, pact, and at-will spells need to be 'prepared' via the spell details to be shown on the HUD.",
+ "name": "Show all non-preparable spells"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/module.json b/module.json
new file mode 100644
index 0000000..65d633a
--- /dev/null
+++ b/module.json
@@ -0,0 +1,110 @@
+{
+ "id": "token-action-hud-dnd5e",
+ "title": "Token Action HUD D&D 5e",
+ "description": "Token Action HUD is a repositionable HUD of actions for a selected token",
+ "authors": [
+ {
+ "name": "Larkinabout",
+ "url": "https://github.com/Larkinabout"
+ },
+ {
+ "name": "Drental"
+ },
+ {
+ "name": "^ and stick"
+ }
+ ],
+ "url": "https://github.com/Larkinabout/fvtt-token-action-hud-dnd5e",
+ "flags": {},
+ "version": "0.1.0",
+ "compatibility": {
+ "minimum": "10",
+ "verified": "10.291"
+ },
+ "scripts": [
+ ],
+ "styles": [
+ ],
+ "languages": [
+ {
+ "lang": "en",
+ "name": "English",
+ "path": "languages/en.json"
+ },
+ {
+ "lang": "ko",
+ "name": "Korean",
+ "path": "languages/ko.json"
+ },
+ {
+ "lang": "pt-BR",
+ "name": "Português (Brasil)",
+ "path": "languages/br.json"
+ },
+ {
+ "lang": "es",
+ "name": "Spanish",
+ "path": "languages/es.json"
+ },
+ {
+ "lang": "fr",
+ "name": "French",
+ "path": "languages/fr.json"
+ },
+ {
+ "lang": "pl",
+ "name": "Polish",
+ "path": "languages/pl.json"
+ },
+ {
+ "lang": "ja",
+ "name": "日本語",
+ "path": "languages/ja.json"
+ },
+ {
+ "lang": "cn",
+ "name": "中文",
+ "path": "languages/cn.json"
+ },
+ {
+ "lang": "it",
+ "name": "italiano",
+ "path": "languages/it.json"
+ },
+ {
+ "lang": "de",
+ "name": "Deutsch",
+ "path": "languages/de.json"
+ }
+ ],
+ "packs": [],
+ "relationships": {
+ "systems": [
+ {
+ "id": "dnd5e",
+ "type": "system"
+ }
+ ],
+ "requires": [
+ {
+ "id": "token-action-hud-core",
+ "compatibility": [
+ {
+ "minimum": "0.1.0",
+ "maximum": "0.1.0",
+ "verified": "0.1.0"
+ }
+ ]
+ }
+ ],
+ "optional": [],
+ "flags": {
+ "optional": []
+ }
+ },
+ "socket": false,
+ "manifest": "https://github.com/Larkinabout/fvtt-token-action-hud-dnd5e/releases/download/0.1.0/module.zip",
+ "protected": false,
+ "coreTranslation": false,
+ "library": false
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..9190a35
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,48 @@
+# Token Action HUD D&D 5e
+
+Token Action HUD is a repositionable HUD of actions for a selected token.
+
+![Token Action HUD](.github/readme/token-action-hud.gif)
+
+# Features
+- Make rolls directly from the HUD instead of opening your character sheet.
+- Use items from the HUD or right-click an item to opens its sheet.
+- Move the HUD and choose to expand the menus up or down.
+- Unlock the HUD to customise categories and subcategories per user, and actions per actor.
+- Add your own macros and Journal Entry and Roll Table compendiums.
+
+# Installation
+
+## Method 1
+1. On Foundry VTT's **Configuration and Setup** screen, go to **Add-on Modules**
+2. Click **Install Module**
+3. Search for **Token Action HUD D&D 5e**
+4. Click **Install** next to the module listing
+
+## Method 2
+1. On Foundry VTT's **Configuration and Setup** screen, go to **Add-on Modules**
+2. Click **Install Module**
+3. In the Manifest URL field, paste: `https://github.com/Larkinabout/fvtt-token-action-hud-dnd5e/releases/latest/download/module.json`
+4. Click **Install** next to the pasted Manifest URL
+
+## Required Modules
+
+**IMPORTANT** - Token Action HUD D&D 5e requires the [Token Action HUD D&D Core](https://foundryvtt.com/packages/token-action-hud-core) module to be installed.
+
+## Recommended Modules
+Token Action HUD uses either the [Color Picker](https://foundryvtt.com/packages/color-picker), [libThemer](https://foundryvtt.com/packages/lib-themer) or [VTTColorSettings](https://foundryvtt.com/packages/colorsettings) library modules for its color picker settings. Only one is required.
+
+# Support
+
+For questions, feature requests or bug reports, please open an issue [here](https://github.com/Larkinabout/fvtt-token-action-hud-core/issues).
+
+Pull requests are welcome. Please include a reason for the request or create an issue before starting one.
+
+# Acknowledgements
+
+First and foremost, thank you to the Community Helpers on Foundry's Discord who provide tireless support for people seeking help with the HUD.
+Enormous thanks also goes to the following people for their help in getting the HUD to its current state: Drental, Kekilla, Rainer, Xacus, Giddy, and anyone who has provided advice to any and all of my problems on Discord, as well as all the other developers who make FoundryVTT a joy to use.
+
+# License
+
+This Foundry VTT module is licensed under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/) and this work is licensed under [Foundry Virtual Tabletop EULA - Limited License Agreement for module development](https://foundryvtt.com/article/license/).
diff --git a/scripts/action-handler.js b/scripts/action-handler.js
new file mode 100644
index 0000000..7c31a7d
--- /dev/null
+++ b/scripts/action-handler.js
@@ -0,0 +1,1295 @@
+// System Module Imports
+import { getSetting } from './utils.js'
+
+// Core Module Imports
+import { CoreActionHandler, Logger } from './config.js'
+
+export default class ActionHandler extends CoreActionHandler {
+ /**
+ * Build System Actions
+ * @override
+ * @param {object} actionList
+ * @param {object} character
+ * @param {array} subcategoryIds
+ * @returns {object}
+ */
+ async buildSystemActions (actionList, character, subcategoryIds) {
+ const actor = character?.actor
+
+ if (actor?.type === 'character' || actor?.type === 'npc') {
+ return this._buildCharacterActions(actionList, character, subcategoryIds)
+ }
+ if (actor?.type === 'vehicle') {
+ return this._buildVehicleActions(actionList, character, subcategoryIds)
+ }
+ if (!actor) {
+ return this._buildMultipleTokenActions(actionList, subcategoryIds)
+ }
+
+ return actionList
+ }
+
+ /**
+ * Build Character Actions
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ * @param {array} subcategoryIds
+ * @returns {object}
+ */
+ async _buildCharacterActions (actionList, character, subcategoryIds) {
+ const inventorySubcategoryIds = subcategoryIds.filter((subcategoryId) =>
+ subcategoryId === 'equipped' ||
+ subcategoryId === 'consumables' ||
+ subcategoryId === 'containers' ||
+ subcategoryId === 'equipment' ||
+ subcategoryId === 'loot' ||
+ subcategoryId === 'tools' ||
+ subcategoryId === 'weapons' ||
+ subcategoryId === 'unequipped'
+ )
+
+ const actionSubcategoryIds = subcategoryIds.filter((subcategoryId) =>
+ subcategoryId === 'actions' ||
+ subcategoryId === 'bonus-actions' ||
+ subcategoryId === 'crew-actions' ||
+ subcategoryId === 'lair-actions' ||
+ subcategoryId === 'legendary-actions' ||
+ subcategoryId === 'reactions' ||
+ subcategoryId === 'other-actions'
+ )
+
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'abilities')) {
+ this._buildAbilities(actionList, character, 'ability', 'abilities')
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'checks')) {
+ this._buildAbilities(actionList, character, 'abilityCheck', 'checks')
+ }
+ if (actionSubcategoryIds) {
+ this._buildActions(actionList, character, actionSubcategoryIds)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'combat')) {
+ this._buildCombat(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'conditions')) {
+ this._buildConditions(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'features')) {
+ this._buildFeatures(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'effects')) {
+ this._buildEffects(actionList, character)
+ }
+ if (inventorySubcategoryIds) {
+ this._buildInventory(actionList, character, inventorySubcategoryIds)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'rests')) {
+ this._buildRests(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'saves')) {
+ this._buildAbilities(actionList, character, 'abilitySave', 'saves')
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'skills')) {
+ this._buildSkills(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'spells')) {
+ this._buildSpells(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'utility')) {
+ this._buildUtility(actionList, character)
+ }
+
+ return actionList
+ }
+
+ /**
+ * Build Vehicle Actions
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ * @param {array} subcategoryIds
+ * @returns {object}
+ */
+ async _buildVehicleActions (actionList, character, subcategoryIds) {
+ const inventorySubcategoryIds = subcategoryIds.filter((subcategoryId) =>
+ subcategoryId === 'consumables' ||
+ subcategoryId === 'equipment' ||
+ subcategoryId === 'tools' ||
+ subcategoryId === 'weapons'
+ )
+
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'abilities')) {
+ this._buildAbilities(actionList, character, 'ability', 'abilities')
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'checks')) {
+ this._buildAbilities(actionList, character, 'abilityCheck', 'checks')
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'combat')) {
+ this._buildCombat(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'conditions')) {
+ this._buildConditions(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'effects')) {
+ this._buildEffects(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'features')) {
+ this._buildFeatures(actionList, character)
+ }
+ if (inventorySubcategoryIds) {
+ this._buildInventory(actionList, character, inventorySubcategoryIds)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'saves')) {
+ this._buildAbilities(actionList, character, 'abilitySave', 'saves')
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'utility')) {
+ this._buildUtility(actionList, character)
+ }
+
+ return actionList
+ }
+
+ /**
+ * Build Multiple Token Actions
+ * @private
+ * @param {object} actionList
+ * @param {array} subcategoryIds
+ * @returns {object}
+ */
+ async _buildMultipleTokenActions (actionList, subcategoryIds) {
+ const character = { actor: { id: 'multi' }, token: { id: 'multi' } }
+
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'abilities')) {
+ this._buildAbilities(
+ actionList,
+ character,
+ 'ability',
+ 'abilities'
+ )
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'combat')) {
+ this._buildCombat(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'conditions')) {
+ this._buildConditions(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'checks')) {
+ this._buildAbilities(
+ actionList,
+ character,
+ 'abilityCheck',
+ 'checks'
+ )
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'rests')) {
+ this._buildRests(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'saves')) {
+ this._buildAbilities(
+ actionList,
+ character,
+ 'abilitySave',
+ 'saves'
+ )
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'skills')) {
+ this._buildSkills(actionList, character)
+ }
+ if (subcategoryIds.some((subcategoryId) => subcategoryId === 'utility')) {
+ this._buildUtility(actionList, character)
+ }
+
+ return actionList
+ }
+
+ /**
+ * Build Abilities
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ * @param {string} actionType
+ * @param {string} subcategoryId
+ */
+ _buildAbilities (actionList, character, actionType, subcategoryId) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character.token?.id
+ const abbr = getSetting('abbreviateSkills')
+
+ const abilities = (actorId === 'multi') ? game.dnd5e.config.abilities : actor.system.abilities
+
+ if (abilities.length === 0) return
+
+ // Get actions
+ const actions = Object.entries(abilities)
+ .filter((ability) => abilities[ability[0]].value !== 0)
+ .map((ability) => {
+ const id = ability[0]
+ const abbreviatedName = id.charAt(0).toUpperCase() + id.slice(1)
+ const name = abbr ? abbreviatedName : game.dnd5e.config.abilities[id]
+ const encodedValue = [actionType, actorId, tokenId, id].join(
+ this.delimiter
+ )
+ let icon
+ if (subcategoryId === 'checks') {
+ icon = ''
+ } else {
+ icon = this._getProficiencyIcon(abilities[id].proficient)
+ }
+ return {
+ id,
+ name,
+ encodedValue,
+ icon,
+ selected: true
+ }
+ })
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Build Actions
+ * @param {object} actionList The action list
+ * @param {object} character The actor and/or token
+ * @param {array} actionSubcategoryIds The action subcategory IDs
+ */
+ _buildActions (actionList, character, actionSubcategoryIds) {
+ const actor = character?.actor
+ if (!actor) return
+
+ let items = actor.items
+
+ if (items.length === 0) return
+
+ // Discard slow items
+ items = this._discardSlowItems(items)
+
+ actionSubcategoryIds.forEach(actionSubcategoryId => {
+ let activationType = null
+ switch (actionSubcategoryId) {
+ case 'actions':
+ activationType = 'action'
+ break
+ case 'bonus-actions':
+ activationType = 'bonus'
+ break
+ case 'crew-actions':
+ activationType = 'crew'
+ break
+ case 'lair-actions':
+ activationType = 'lair'
+ break
+ case 'legendary-actions':
+ activationType = 'legendary'
+ break
+ case 'reactions':
+ activationType = 'reaction'
+ break
+ case 'other-actions':
+ activationType = ''
+ break
+ }
+
+ const itemsByActivationType = items.filter(item => item.system?.activation?.type === activationType)
+
+ this._buildItems(actionList, character, itemsByActivationType, actionSubcategoryId)
+ })
+ }
+
+ /**
+ * Build Combat
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildCombat (actionList, character) {
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actionType = 'utility'
+ const subcategoryId = 'combat'
+
+ const combatTypes = {
+ initiative: { id: 'initiative', name: this.i18n('tokenActionHud.rollInitiative') },
+ endTurn: { id: 'endTurn', name: this.i18n('tokenActionHud.endTurn') }
+ }
+
+ // Delete endTurn for multiple tokens
+ if (game.combat?.current?.tokenId !== tokenId) delete combatTypes.endTurn
+
+ // Get actions
+ const actions = Object.entries(combatTypes).map((combatType) => {
+ const id = combatType[1].id
+ const name = combatType[1].name
+ const encodedValue = [actionType, actorId, tokenId, id].join(this.delimiter)
+ let info1 = ''
+ let cssClass = ''
+ if (combatType[0] === 'initiative' && game.combat) {
+ const tokenIds = canvas.tokens.controlled.map((token) => token.id)
+ // const combatants = tokenIds.map((id) =>
+ // game.combat.combatants.find((combatant) => combatant.tokenId === id)
+ // ).filter((combatant) => combatant)
+ const combatants = game.combat.combatants.filter((combatant) => tokenIds.includes(combatant.tokenId))
+
+ // Get initiative for single token
+ if (combatants.length === 1) {
+ const currentInitiative = combatants[0].initiative
+ info1 = currentInitiative
+ }
+
+ const active = combatants.length > 0 && (combatants.every((combatant) => combatant?.initiative)) ? ' active' : ''
+ cssClass = `toggle${active}`
+ }
+ return {
+ id,
+ name,
+ encodedValue,
+ info1,
+ cssClass,
+ selected: true
+ }
+ })
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Build Conditions
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildConditions (actionList, character) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actors = (actorId === 'multi') ? this._getActors() : [actor]
+ const actionType = 'condition'
+ const subcategoryId = 'conditions'
+
+ const conditions = CONFIG.statusEffects.filter((condition) => condition.id !== '')
+
+ if (conditions.length === 0) return
+
+ const actions = conditions.map((condition) => {
+ const id = condition.id
+ const name = this.i18n(condition.label)
+ const encodedValue = [actionType, actorId, tokenId, id].join(
+ this.delimiter
+ )
+ const active = actors.every((actor) => {
+ const effects = actor.effects
+ return effects
+ .map((effect) => effect.flags?.core?.statusId)
+ .some((statusId) => statusId === id)
+ })
+ ? ' active'
+ : ''
+ const cssClass = `toggle${active}`
+ const img = condition.icon
+ return {
+ id,
+ name,
+ encodedValue,
+ img,
+ cssClass,
+ selected: true
+ }
+ })
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Build Effects
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildEffects (actionList, character) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actionType = 'effect'
+ const parentSubcategoryId = 'effects'
+ const subcategoryList = []
+
+ const effectTypes = {
+ temporary: { label: this.i18n('tokenActionHud.temporary'), effects: [] },
+ passive: { label: this.i18n('tokenActionHud.passive'), effects: [] }
+ }
+
+ // Group effects by type
+ const effects = actor.effects
+
+ if (effects.length === 0) return
+
+ effects.forEach((effect) => {
+ if (effect.isTemporary) {
+ effectTypes.temporary.effects.push(effect)
+ } else {
+ effectTypes.passive.effects.push(effect)
+ }
+ })
+
+ Object.entries(effectTypes).forEach((effectType) => {
+ const subcategoryId = `effects_${effectType[0]}`
+ const subcategoryName = effectType[1].label
+ const subcategory = this.initializeEmptySubcategory(subcategoryId, parentSubcategoryId, subcategoryName, 'system')
+ const actions = effectType[1].effects.map((effect) => {
+ const id = effect.id
+ const name = effect.label
+ const encodedValue = [actionType, actorId, tokenId, id].join(this.delimiter)
+ const active = (!effect.disabled) ? ' active' : ''
+ const cssClass = `toggle${active}`
+ const img = effect.icon
+ return {
+ id,
+ name,
+ encodedValue,
+ cssClass,
+ img,
+ selected: true
+ }
+ })
+
+ this.addToSubcategoryList(subcategoryList, subcategoryId, subcategory, actions)
+ })
+
+ this.addSubcategoriesToActionList(actionList, subcategoryList, parentSubcategoryId)
+ }
+
+ /**
+ * Build Features
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildFeatures (actionList, character) {
+ const actor = character?.actor
+ const actionType = 'feature'
+ const parentSubcategoryId = 'features'
+ const subcategoryList = []
+
+ const featureTypes = {
+ active: { label: this.i18n('tokenActionHud.active'), items: [] },
+ passive: { label: this.i18n('tokenActionHud.passive'), items: [] },
+ lair: { label: this.i18n('tokenActionHud.dnd5e.lair'), items: [] },
+ legendary: { label: this.i18n('tokenActionHud.dnd5e.legendary'), items: [] },
+ actions: { label: this.i18n('tokenActionHud.actions'), items: [] },
+ features: { label: this.i18n('tokenActionHud.features'), items: [] },
+ reactions: { label: this.i18n('tokenActionHud.reactions'), items: [] }
+ }
+
+ // Get feat items
+ let feats = actor.items.filter((item) => item.type === 'feat')
+
+ if (feats.length === 0) return
+
+ // Discard slow items
+ feats = this._discardSlowItems(feats)
+
+ // Sort items
+ feats = this.sortItems(feats)
+
+ // Group feats by type
+ feats.forEach((item) => {
+ const activationType = item.system.activation.type
+ if (actor.type === 'vehicle') {
+ if (
+ activationType &&
+ activationType !== 'none' &&
+ activationType !== 'reaction'
+ ) {
+ featureTypes.actions.items.push(item)
+ }
+ if (!activationType || activationType === 'none') {
+ featureTypes.features.items.push(item)
+ }
+ if (activationType === 'reaction') {
+ featureTypes.reactions.items.push(item)
+ }
+ }
+ if (actor.type === 'character' || actor.type === 'npc') {
+ if (
+ activationType && activationType !== '' &&
+ activationType !== 'lair' &&
+ activationType !== 'legendary'
+ ) {
+ featureTypes.active.items.push(item)
+ }
+ if (!activationType || activationType === '') {
+ featureTypes.passive.items.push(item)
+ }
+ if (activationType === 'lair') {
+ featureTypes.lair.items.push(item)
+ }
+ if (activationType === 'legendary') {
+ featureTypes.legendary.items.push(item)
+ }
+ }
+ })
+
+ // Delete unneeded feature types
+ if (actor.type === 'vehicle') {
+ delete featureTypes.active
+ delete featureTypes.passive
+ delete featureTypes.lair
+ delete featureTypes.legendary
+ }
+ if (actor.type === 'character' || actor.type === 'npc') {
+ if (!getSetting('showPassiveFeats')) { delete featureTypes.passive }
+ delete featureTypes.actions
+ delete featureTypes.features
+ delete featureTypes.reactions
+ }
+
+ // Loop feature types
+ Object.entries(featureTypes).forEach((featureType) => {
+ const subcategoryId = `features_${featureType[0]}`
+ const subcategoryName = featureType[1].label
+ const subcategory = this.initializeEmptySubcategory(
+ subcategoryId,
+ parentSubcategoryId,
+ subcategoryName,
+ 'system'
+ )
+
+ // Get items
+ const items = featureType[1].items
+
+ // Get actions
+ const actions = items.map((item) => this._getAction(actionType, character, item))
+
+ this.addToSubcategoryList(subcategoryList, subcategoryId, subcategory, actions)
+ })
+
+ this.addSubcategoriesToActionList(actionList, subcategoryList, parentSubcategoryId)
+ }
+
+ /**
+ * Build Inventory
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ * @param {array} inventorySubcategoryIds
+ */
+ _buildInventory (actionList, character, inventorySubcategoryIds) {
+ const actor = character?.actor
+
+ const validItems = this._discardSlowItems(
+ actor.items.filter((item) => item.system.quantity > 0)
+ )
+
+ if (validItems.length === 0) return
+
+ const sortedItems = this.sortItems(validItems)
+
+ // Equipped
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'equipped')) {
+ const equipped = this._getActiveEquipment(
+ sortedItems.filter((item) => item.system.equipped)
+ )
+ this._buildItems(actionList, character, equipped, 'equipped')
+ }
+
+ // Unequipped
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'unequipped')) {
+ const unequipped = this._getActiveEquipment(
+ sortedItems.filter((item) => !item.system.equipped)
+ )
+ this._buildItems(actionList, character, unequipped, 'unequipped')
+ }
+
+ // Consumables
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'consumables')) {
+ const allConsumables = this._getActiveEquipment(
+ sortedItems.filter((item) => item.type === 'consumable')
+ )
+ const consumables = this._discardExpendedItems(allConsumables)
+
+ this._buildItems(actionList, character, consumables, 'consumables')
+ }
+
+ // Equipped Inventory
+ let equippedItems
+ if (getSetting('showUnequippedItems')) {
+ equippedItems = sortedItems.filter((item) =>
+ item.type !== 'consumable' &&
+ item.type !== 'spell' &&
+ item.type !== 'feat'
+ )
+ } else {
+ equippedItems = sortedItems.filter((item) => item.type !== 'consumable' && item.system.equipped)
+ }
+ const activeEquipped = this._getActiveEquipment(equippedItems)
+
+ // Containers
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'containers')) {
+ const containers = activeEquipped.filter((item) => item.type === 'backpack')
+ this._buildItems(actionList, character, containers, 'containers')
+ }
+
+ // Equipment
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'equipment')) {
+ const equipment = activeEquipped.filter((item) => item.type === 'equipment')
+ this._buildItems(actionList, character, equipment, 'equipment')
+ }
+
+ // Loot
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'loot')) {
+ const loot = activeEquipped.filter((item) => item.type === 'loot')
+ this._buildItems(actionList, character, loot, 'loot')
+ }
+
+ // Tools
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'tools')) {
+ const tools = validItems.filter((item) => item.type === 'tool')
+ this._buildItems(actionList, character, tools, 'tools')
+ }
+
+ // Weapons
+ if (inventorySubcategoryIds.some((subcategoryId) => subcategoryId === 'weapons')) {
+ const weapons = activeEquipped.filter((item) => item.type === 'weapon')
+ this._buildItems(actionList, character, weapons, 'weapons')
+ }
+ }
+
+ /**
+ * Build Items
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ * @param {object} items
+ * @param {string} subcategoryId
+ */
+ _buildItems (actionList, character, items, subcategoryId) {
+ if (items.length === 0) return
+
+ const actionType = 'item'
+ const actions = items.map((item) =>
+ this._getAction(actionType, character, item)
+ )
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Get Active Equipment
+ * @private
+ * @param {object} equipment
+ * @returns {object}
+ */
+ _getActiveEquipment (equipment) {
+ let activeEquipment = []
+ if (!getSetting('showItemsWithoutActivationCosts')) {
+ const activationTypes = Object.keys(game.dnd5e.config.abilityActivationTypes)
+ .filter((at) => at !== 'none')
+
+ activeEquipment = equipment.filter((e) => {
+ const activation = e.system.activation
+ if (!activation) return false
+
+ return activationTypes.includes(e.system.activation.type)
+ })
+ } else {
+ activeEquipment = equipment
+ }
+
+ return activeEquipment
+ }
+
+ /**
+ * Get Action
+ * @private
+ * @param {string} actionType
+ * @param {object} character
+ * @param {object} entity
+ * @returns {object}
+ */
+ _getAction (actionType, character, entity) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const id = entity.id ?? entity._id
+ let name = entity.name
+ if (
+ entity?.system?.recharge &&
+ !entity?.system?.recharge?.charged &&
+ entity?.system?.recharge?.value
+ ) {
+ name += ` (${this.i18n('tokenActionHud.recharge')})`
+ }
+ const encodedValue = [actionType, actorId, tokenId, id].join(
+ this.delimiter
+ )
+ const img = this.getImage(entity)
+ const icon = this._getIcon(entity?.system?.activation?.type)
+ const itemInfo = this._getItemInfo(actor, entity)
+ const info1 = itemInfo.info1
+ const info2 = itemInfo.info2
+ const info3 = itemInfo.info3
+ return {
+ id,
+ name,
+ encodedValue,
+ img,
+ icon,
+ info1,
+ info2,
+ info3,
+ selected: true
+ }
+ }
+
+ /**
+ * Get Item Info
+ * @private
+ * @param {object} actor
+ * @param {object} item
+ * @returns {object}
+ */
+ _getItemInfo (actor, item) {
+ return {
+ info1: this._getQuantityData(item) ?? '',
+ info2: this._getUsesData(item) ?? '',
+ info3: this._getConsumeData(item, actor) ?? ''
+ }
+ }
+
+ /**
+ * Build Rests
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildRests (actionList, character) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actors = (actorId === 'multi') ? this._getActors() : [actor]
+ if (!actors.every((actor) => actor.type === 'character')) return
+ const actionType = 'utility'
+ const subcategoryId = 'rests'
+
+ const restTypes = {
+ shortRest: { name: this.i18n('tokenActionHud.shortRest') },
+ longRest: { name: this.i18n('tokenActionHud.longRest') }
+ }
+
+ const actions = Object.entries(restTypes)
+ .map((restType) => {
+ const id = restType[0]
+ const name = restType[1].name
+ const encodedValue = [actionType, actorId, tokenId, id].join(this.delimiter)
+ return {
+ id,
+ name,
+ encodedValue,
+ selected: true
+ }
+ })
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Build Skills
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildSkills (actionList, character) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actionType = 'skill'
+ const abbr = getSetting('abbreviateSkills')
+
+ // Get skills
+ const skills = (actorId === 'multi') ? game.dnd5e.config.skills : actor.system.skills
+
+ if (skills.length === 0) return
+
+ // Get actions
+ const actions = Object.entries(skills)
+ .map((skill) => {
+ try {
+ const id = skill[0]
+ const abbreviatedName = id.charAt(0).toUpperCase() + id.slice(1)
+ const name = abbr ? abbreviatedName : game.dnd5e.config.skills[id].label
+ const encodedValue = [actionType, actorId, tokenId, id].join(
+ this.delimiter
+ )
+ const icon = this._getProficiencyIcon(skills[id].value)
+ return {
+ id,
+ name,
+ encodedValue,
+ icon
+ }
+ } catch (error) {
+ Logger.error(skill)
+ return null
+ }
+ })
+ .filter((skill) => !!skill)
+
+ this.addActionsToActionList(actionList, actions, 'skills')
+ }
+
+ /**
+ * Build Spells
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildSpells (actionList, character) {
+ const actor = character?.actor
+ const actionType = 'spell'
+ const parentSubcategoryId = 'spells'
+
+ // Get spell items
+ let spells = actor.items.filter((item) => item.type === 'spell')
+
+ if (spells.length === 0) return
+
+ // Discard slow spells
+ spells = this._discardSlowItems(spells)
+
+ // Discard expended spells
+ spells = this._discardExpendedItems(spells)
+
+ // Discard non-preprared spells
+ spells = this._discardNonPreparedSpells(actor, spells)
+
+ // Sport spells by level
+ spells = this._sortSpellsByLevel(spells)
+
+ // Reverse sort spells by level
+ const spellSlotInfo = Object.entries(actor.system.spells).sort((a, b) => {
+ return b[0].toUpperCase().localeCompare(a[0].toUpperCase(), undefined, {
+ sensitivity: 'base'
+ })
+ })
+
+ // Go through spells and if higher available slots exist, mark spell slots available at lower levels.
+ const pactInfo = spellSlotInfo.find((spell) => spell[0] === 'pact')
+
+ let slotsAvailable = false
+ spellSlotInfo
+ .filter((spell) => spell[0] !== 'maxLevel')
+ .forEach((spell) => {
+ if (spell[0].startsWith('spell')) {
+ if (!slotsAvailable && spell[1].max > 0 && spell[1].value > 0) {
+ slotsAvailable = true
+ }
+
+ if (!slotsAvailable && spell[0] === 'spell' + pactInfo[1]?.level) {
+ if (pactInfo[1].max > 0 && pactInfo[1].value > 0) { slotsAvailable = true }
+ }
+
+ spell[1].slotsAvailable = slotsAvailable
+ } else {
+ if (!spell[1]) spell[1] = {}
+
+ spell[1].slotsAvailable = !spell[1].max || spell[1].value > 0
+ }
+ })
+
+ const pactIndex = spellSlotInfo.findIndex((spell) => spell[0] === 'pact')
+
+ if (!spellSlotInfo[pactIndex][1].slotsAvailable) {
+ const pactSpellEquivalent = spellSlotInfo.findIndex(
+ (spell) => spell[0] === 'spell' + pactInfo[1].level
+ )
+ spellSlotInfo[pactIndex][1].slotsAvailable =
+ spellSlotInfo[pactSpellEquivalent][1].slotsAvailable
+ }
+
+ // Get preparation modes/levels
+ const spellLevelIds = [
+ ...new Set(
+ spells.map((spell) => {
+ const prepId = spell.system.preparation.mode
+ const levelId = spell.system.level
+ const isPrep = !!(prepId === 'pact' || prepId === 'atwill' || prepId === 'innate')
+ if (isPrep) {
+ return prepId
+ } else {
+ return levelId
+ }
+ })
+ )
+ ]
+
+ // Get spell levels
+ const spellLevels = spellLevelIds.map((spellLevel) => {
+ const isPrep = !!(
+ spellLevel === 'pact' ||
+ spellLevel === 'atwill' ||
+ spellLevel === 'innate'
+ )
+ const id = isPrep ? spellLevel : `spell${spellLevel}`
+ const name = isPrep
+ ? game.dnd5e.config.spellPreparationModes[spellLevel]
+ : spellLevel === 0
+ ? this.i18n('tokenActionHud.cantrips')
+ : `${this.i18n('tokenActionHud.level')} ${spellLevel}`
+ return [id, name]
+ })
+
+ const subcategoryList = []
+
+ for (const spellLevel of spellLevels) {
+ const spellLevelId = `spells_${spellLevel[0]}`
+ const spellLevelName = spellLevel[1]
+ const isPrep = !!(
+ spellLevelId === 'pact' ||
+ spellLevelId === 'atwill' ||
+ spellLevelId === 'innate'
+ )
+ const levelInfo = spellSlotInfo.find((level) => level[0] === spellLevel[0])?.[1]
+ const slots = levelInfo?.value
+ const max = levelInfo?.max
+ const slotsAvailable = levelInfo?.slotsAvailable
+ const ignoreSlotsAvailable = getSetting('showUnchargedItems')
+
+ if (max && !(slotsAvailable || ignoreSlotsAvailable)) continue
+
+ const subcategory = this.initializeEmptySubcategory(
+ spellLevelId,
+ parentSubcategoryId,
+ spellLevelName,
+ 'system'
+ )
+ if (max > 0) subcategory.info1 = `${slots}/${max}`
+
+ // Get actions
+ const actions = []
+ for (const spell of spells) {
+ const spellSpellLevelId = isPrep
+ ? `spells_${spell.system.preparation.mode}`
+ : `spells_spell${spell.system.level}`
+
+ if (spellSpellLevelId === spellLevelId) {
+ const action = this._getAction(actionType, character, spell)
+ if (getSetting('displaySpellInfo')) {
+ this._addSpellInfo(spell, action)
+ }
+ actions.push(action)
+ }
+ }
+
+ this.addToSubcategoryList(
+ subcategoryList,
+ spellLevelId,
+ subcategory,
+ actions
+ )
+ }
+
+ this.addSubcategoriesToActionList(actionList, subcategoryList, 'spells')
+ }
+
+ /**
+ * Sort Spells by Level
+ * @private
+ * @param {object} spells
+ * @returns {object}
+ */
+ _sortSpellsByLevel (spells) {
+ const result = Object.values(spells)
+ result.sort((a, b) => {
+ if (a.system.level === b.system.level) {
+ return a.name
+ .toUpperCase()
+ .localeCompare(b.name.toUpperCase(), undefined, {
+ sensitivity: 'base'
+ })
+ }
+ return a.system.level - b.system.level
+ })
+
+ return result
+ }
+
+ /**
+ * Add Spell Info
+ * @param {object} spell
+ * @param {object} action
+ */
+ _addSpellInfo (spell, action) {
+ const components = spell.system.components
+
+ action.info1 = ''
+ action.info2 = ''
+ action.info3 = ''
+
+ if (components?.vocal) {
+ action.info1 += this.i18n('DND5E.ComponentVerbal').charAt(0).toUpperCase()
+ }
+
+ if (components?.somatic) {
+ action.info1 += this.i18n('DND5E.ComponentSomatic').charAt(0).toUpperCase()
+ }
+
+ if (components?.material) {
+ action.info1 += this.i18n('DND5E.ComponentMaterial').charAt(0).toUpperCase()
+ }
+
+ if (components?.concentration) {
+ action.info2 += this.i18n('DND5E.Concentration').charAt(0).toUpperCase()
+ }
+
+ if (components?.ritual) {
+ action.info3 += this.i18n('DND5E.Ritual').charAt(0).toUpperCase()
+ }
+ }
+
+ /**
+ * Build Utility
+ * @private
+ * @param {object} actionList
+ * @param {object} character
+ */
+ _buildUtility (actionList, character) {
+ const actor = character?.actor
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ const actors = (actorId === 'multi') ? this._getActors() : [actor]
+ if (!actors.every((actor) => actor.type === 'character')) return
+ const actionType = 'utility'
+ const subcategoryId = 'utility'
+
+ const utilityTypes = {
+ deathSave: { name: this.i18n('DND5E.DeathSave') },
+ inspiration: { name: this.i18n('tokenActionHud.inspiration') }
+ }
+
+ // Delete 'deathSave' for multiple tokens
+ if (actorId === 'multi' || actor.system.attributes.hp.value > 0) delete utilityTypes.deathSave
+
+ // Get actions
+ const actions = Object.entries(utilityTypes)
+ .map((utilityType) => {
+ const id = utilityType[0]
+ const name = utilityType[1].name
+ const encodedValue = [actionType, actorId, tokenId, id].join(this.delimiter)
+ let cssClass = ''
+ if (utilityType[0] === 'inspiration') {
+ const active = actors.every((actor) => actor.system.attributes?.inspiration)
+ ? ' active'
+ : ''
+ cssClass = `toggle${active}`
+ }
+ return {
+ id,
+ name,
+ encodedValue,
+ cssClass,
+ selected: true
+ }
+ })
+
+ this.addActionsToActionList(actionList, actions, subcategoryId)
+ }
+
+ /**
+ * Get Actors
+ * @private
+ * @returns {object}
+ */
+ _getActors () {
+ const allowedTypes = ['character', 'npc']
+ const actors = canvas.tokens.controlled.map((token) => token.actor)
+ if (actors.every((actor) => allowedTypes.includes(actor.type))) {
+ return actors
+ }
+ }
+
+ /**
+ * Get Quantity
+ * @private
+ * @param {object} item
+ * @returns {string}
+ */
+ _getQuantityData (item) {
+ const quantity = item?.system?.quantity
+ return (quantity > 1) ? quantity : ''
+ }
+
+ /**
+ * Get Uses
+ * @private
+ * @param {object} item
+ * @returns {string}
+ */
+ _getUsesData (item) {
+ let result = ''
+
+ const uses = item?.system?.uses
+ if (!uses) return result
+
+ result = uses.value === 0 && uses.max ? '0' : uses.value
+
+ if (uses.max > 0) {
+ result += `/${uses.max}`
+ }
+
+ return result
+ }
+
+ /**
+ * Get Consume
+ * @private
+ * @param {object} item
+ * @param {object} actor
+ * @returns {string}
+ */
+ _getConsumeData (item, actor) {
+ let result = ''
+
+ const consumeType = item?.system?.consume?.type
+ if (consumeType && consumeType !== '') {
+ const consumeId = item.system.consume.target
+ const parentId = consumeId.substr(0, consumeId.lastIndexOf('.'))
+ if (consumeType === 'attribute') {
+ const target = getProperty(actor, `system.${parentId}`)
+
+ if (target) {
+ result = target.value ?? 0
+ if (target.max) result += `/${target.max}`
+ }
+ }
+
+ if (consumeType === 'charges') {
+ const consumeId = item.system.consume.target
+ const target = actor.items.get(consumeId)
+ const uses = target?.system.uses
+ if (uses?.value) {
+ result = uses.value
+ if (uses.max) result += `/${uses.max}`
+ }
+ }
+
+ if (!(consumeType === 'attribute' || consumeType === 'charges')) {
+ const consumeId = item.system.consume.target
+ const target = actor.items.get(consumeId)
+ const quantity = target?.system.quantity
+ if (quantity) {
+ result = quantity
+ }
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * Discard Slow Items
+ * @private
+ * @param {object} items
+ * @returns {object}
+ */
+ _discardSlowItems (items) {
+ let result
+
+ if (!getSetting('showSlowActions')) {
+ result = items.filter((item) => {
+ return (
+ !item.system.activation ||
+ !(
+ item.system.activation.type === 'minute' ||
+ item.system.activation.type === 'hour' ||
+ item.system.activation.type === 'day'
+ )
+ )
+ })
+ }
+
+ return result || items
+ }
+
+ /**
+ * Discard Non-Prepared Spells
+ * @private
+ * @param {object} spells
+ * @returns {object}
+ */
+ _discardNonPreparedSpells (actor, spells) {
+ if (actor.type !== 'character' && getSetting('showUnequippedItems')) return
+
+ const nonpreparableSpells = Object.keys(game.dnd5e.config.spellPreparationModes)
+ .filter((p) => p !== 'prepared')
+
+ let result = spells
+ if (getSetting('showUnpreparedSpells')) {
+ result = spells.filter((spell) => {
+ return (
+ spell.system.preparation.prepared ||
+ nonpreparableSpells.includes(spell.system.preparation.mode) ||
+ spell.system.level === 0
+ )
+ })
+ } else {
+ result = spells.filter((spell) => spell.system.preparation.prepared)
+ }
+
+ return result
+ }
+
+ /**
+ * Discard Expended Items
+ * @private
+ * @param {object} items
+ * @returns {object}
+ */
+ _discardExpendedItems (items) {
+ if (getSetting('showUnchargedItems')) return items
+
+ return items.filter((item) => {
+ const uses = item.system.uses
+ // Assume something with no uses is unlimited in its use
+ if (!uses) return true
+
+ // If it has a max but value is 0, don't return
+ if (uses.max > 0 && !uses.value) return false
+
+ return true
+ })
+ }
+
+ /**
+ * Get Proficiency Icon
+ * @param {string} level
+ * @returns {string}
+ */
+ _getProficiencyIcon (level) {
+ const icons = {
+ 0: '',
+ 0.5: '',
+ 1: '',
+ 2: ''
+ }
+ return icons[level]
+ }
+
+ /**
+ * Get Icon
+ * @param {object} action
+ * @returns {string}
+ */
+ _getIcon (action) {
+ const img = {
+ bonus: '',
+ crew: '',
+ day: '',
+ hour: '',
+ lair: '',
+ minute: '',
+ legendary: '',
+ reaction: '',
+ special: ''
+ }
+ return img[action]
+ }
+}
diff --git a/scripts/config.js b/scripts/config.js
new file mode 100644
index 0000000..0e0ea1e
--- /dev/null
+++ b/scripts/config.js
@@ -0,0 +1,21 @@
+import { importClass } from './utils.js'
+
+export const coreModuleVersion = '0.1.0'
+const coreModulePath = '../../token-action-hud-core/'
+const coreActionHandlerFile = `${coreModulePath}scripts/action-handlers/action-handler.js`
+const coreActionListExtenderFile = `${coreModulePath}scripts/action-handlers/action-list-extender.js`
+const coreCategoryManagerFile = `${coreModulePath}scripts/category-manager.js`
+const corePreRollHandlerFile = `${coreModulePath}scripts/roll-handlers/pre-roll-handler.js`
+const coreRollHandlerFile = `${coreModulePath}scripts/roll-handlers/roll-handler.js`
+const coreSystemManagerFile = `${coreModulePath}scripts/system-manager.js`
+const loggerFile = `${coreModulePath}scripts/logger.js`
+const coreUtilsFile = `${coreModulePath}scripts/utilities/utils.js`
+
+export const CoreActionHandler = await importClass(coreActionHandlerFile)
+export const CoreActionListExtender = await importClass(coreActionListExtenderFile)
+export const CoreCategoryManager = await importClass(coreCategoryManagerFile)
+export const CorePreRollHandler = await importClass(corePreRollHandlerFile)
+export const CoreRollHandler = await importClass(coreRollHandlerFile)
+export const CoreSystemManager = await importClass(coreSystemManagerFile)
+export const Logger = await importClass(loggerFile)
+export const CoreUtils = await import(coreUtilsFile)
diff --git a/scripts/item-macro-options.js b/scripts/item-macro-options.js
new file mode 100644
index 0000000..b985a78
--- /dev/null
+++ b/scripts/item-macro-options.js
@@ -0,0 +1,5 @@
+export const ItemMacroOptions = {
+ SHOW_BOTH: 'tokenActionHud.settings.itemMacroReplace.showBoth',
+ SHOW_ITEM_MACRO: 'tokenActionHud.settings.itemMacroReplace.showItemMacro',
+ SHOW_ORIGINAL_ITEM: 'tokenActionHud.settings.itemMacroReplace.showOriginal'
+}
diff --git a/scripts/magic-items-extender.js b/scripts/magic-items-extender.js
new file mode 100644
index 0000000..57eb452
--- /dev/null
+++ b/scripts/magic-items-extender.js
@@ -0,0 +1,92 @@
+import { CoreActionListExtender } from './config.js'
+
+export default class MagicItemActionListExtender extends CoreActionListExtender {
+ /**
+ * Extend the action list
+ * @param {object} actionList The action list
+ * @param {object} character The actor and/or token
+ */
+ extendActionList (actionList, character) {
+ const actorId = character?.actor?.id
+ const tokenId = character?.token?.id
+ if (!actorId) return
+
+ const actor = MagicItems.actor(actorId)
+
+ if (!actor) return
+
+ const magicItems = actor.items ?? []
+
+ if (magicItems.length === 0) return
+
+ const parentSubcategoryId = 'magic-items'
+ const subcategoryList = []
+
+ magicItems.forEach((magicItem) => {
+ if (magicItem.attuned && !this._isItemAttuned(magicItem)) return
+ if (magicItem.equipped && !this._isItemEquipped(magicItem)) return
+
+ const subcategoryId = `magic-items_${magicItem.id}`
+ const subcategoryName = magicItem.name
+ const subcategory = this.initializeEmptySubcategory(subcategoryId, parentSubcategoryId, subcategoryName, 'system')
+ subcategory.info1 = `${magicItem.uses}/${magicItem.charges}`
+
+ const actions = magicItem.ownedEntries.map((entry) => {
+ const effect = entry.item
+ const id = effect.id
+ const name = effect.name
+ const encodedValue = [
+ 'magicItem',
+ actorId,
+ tokenId,
+ `${magicItem.id}>${id}`
+ ].join('|')
+ const img = this.getImage(effect)
+ const info1 = effect.consumption
+ let info2 = ''
+ if (effect.baseLevel) {
+ info2 = `${this.i18n('tokenActionHud.levelAbbreviation')} ${effect.baseLevel}`
+ }
+ return {
+ id,
+ name,
+ encodedValue,
+ img,
+ info1,
+ info2
+ }
+ })
+
+ this.addToSubcategoryList(subcategoryList, subcategoryId, subcategory, actions)
+ })
+
+ this.addSubcategoriesToActionList(actionList, subcategoryList, parentSubcategoryId)
+ }
+
+ /**
+ * Whether the magic item is equipped or not
+ * @param {object} magicItem The item
+ * @returns {boolean}
+ */
+ _isItemEquipped (magicItem) {
+ return magicItem.item.system.equipped
+ }
+
+ /**
+ * Whether the magic items is attuned or not
+ * @param {object} magicItem The item
+ * @returns {boolean}
+ */
+ _isItemAttuned (magicItem) {
+ const itemData = magicItem.item.system
+
+ if (itemData.attunement) {
+ const attuned = CONFIG.DND5E.attunementTypes?.ATTUNED ?? 2
+ return itemData.attunement === attuned
+ }
+
+ if (itemData.attuned) return itemData.attuned
+
+ return false
+ }
+}
diff --git a/scripts/roll-handler-obsidian.js b/scripts/roll-handler-obsidian.js
new file mode 100644
index 0000000..b72b690
--- /dev/null
+++ b/scripts/roll-handler-obsidian.js
@@ -0,0 +1,55 @@
+import { RollHandler } from './roll-handler.js'
+
+export class RollHandlerObsidian extends RollHandler {
+ /**
+ * Roll Ability Test
+ * @override
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollAbilityTest (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ OBSIDIAN.Items.roll(actor, { roll: 'abl', abl: actionId })
+ }
+
+ /**
+ * Roll Ability Save
+ * @override
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollAbilitySave (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ OBSIDIAN.Items.roll(actor, { roll: 'save', save: actionId })
+ }
+
+ /**
+ * Roll Skill
+ * @override
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollSkill (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ OBSIDIAN.Items.roll(actor, { roll: 'skl', skl: actionId })
+ }
+
+ /**
+ * Use Item
+ * @override
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _useItem (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ OBSIDIAN.Items.roll(actor, { roll: 'item', id: actionId })
+ }
+}
diff --git a/scripts/roll-handler.js b/scripts/roll-handler.js
new file mode 100644
index 0000000..49e04f6
--- /dev/null
+++ b/scripts/roll-handler.js
@@ -0,0 +1,319 @@
+// Core Module Imports
+import { CoreRollHandler } from './config.js'
+
+export class RollHandler extends CoreRollHandler {
+ /**
+ * Handle Action Event
+ * @override
+ * @param {object} event
+ * @param {string} encodedValue
+ */
+ async doHandleActionEvent (event, encodedValue) {
+ const payload = encodedValue.split('|')
+
+ if (payload.length !== 4) {
+ super.throwInvalidValueErr()
+ }
+
+ const actionType = payload[0]
+ const actorId = payload[1]
+ const tokenId = payload[2]
+ const actionId = payload[3]
+
+ if (tokenId === 'multi' && actionId !== 'toggleCombat') {
+ for (const token of canvas.tokens.controlled) {
+ const tokenActorId = token.actor?.id
+ const tokenTokenId = token.id
+ await this._handleMacros(
+ event,
+ actionType,
+ tokenActorId,
+ tokenTokenId,
+ actionId
+ )
+ }
+ } else {
+ await this._handleMacros(event, actionType, actorId, tokenId, actionId)
+ }
+ }
+
+ /**
+ * Handle Macros
+ * @private
+ * @param {object} event
+ * @param {string} actionType
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ async _handleMacros (event, actionType, actorId, tokenId, actionId) {
+ switch (actionType) {
+ case 'ability':
+ this._rollAbility(event, actorId, tokenId, actionId)
+ break
+ case 'abilityCheck':
+ this._rollAbilityTest(event, actorId, tokenId, actionId)
+ break
+ case 'abilitySave':
+ this._rollAbilitySave(event, actorId, tokenId, actionId)
+ break
+ case 'condition':
+ if (!tokenId) return
+ await this._toggleCondition(event, tokenId, actionId)
+ break
+ case 'effect':
+ await this._toggleEffect(event, actorId, tokenId, actionId)
+ break
+ case 'feature':
+ case 'item':
+ case 'spell':
+ case 'weapon':
+ if (this.isRenderItem()) this.doRenderItem(actorId, tokenId, actionId)
+ else this._useItem(event, actorId, tokenId, actionId)
+ break
+ case 'magicItem':
+ this._rollMagicItem(event, actorId, tokenId, actionId)
+ break
+ case 'skill':
+ this._rollSkill(event, actorId, tokenId, actionId)
+ break
+ case 'utility':
+ await this._performUtilityMacro(event, actorId, tokenId, actionId)
+ break
+ default:
+ break
+ }
+ }
+
+ /**
+ * Roll Ability
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollAbility (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ actor.rollAbility(actionId, { event })
+ }
+
+ /**
+ * Roll Ability Save
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollAbilitySave (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ actor.rollAbilitySave(actionId, { event })
+ }
+
+ /**
+ * Roll Ability Test
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollAbilityTest (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ actor.rollAbilityTest(actionId, { event })
+ }
+
+ /**
+ * Roll Magic Item
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollMagicItem (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ const actionParts = actionId.split('>')
+
+ const itemId = actionParts[0]
+ const magicEffectId = actionParts[1]
+
+ const magicItemActor = MagicItems.actor(actor.id)
+
+ magicItemActor.roll(itemId, magicEffectId)
+
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ }
+
+ /**
+ * Roll Skill
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ _rollSkill (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ actor.rollSkill(actionId, { event })
+ }
+
+ /**
+ * Use Item
+ * @private
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ * @returns {object}
+ */
+ _useItem (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ const item = super.getItem(actor, actionId)
+
+ if (this._needsRecharge(item)) {
+ item.rollRecharge()
+ return
+ }
+
+ return item.use({ event })
+ }
+
+ /**
+ * Needs Recharge
+ * @private
+ * @param {object} item
+ * @returns {boolean}
+ */
+ _needsRecharge (item) {
+ return (
+ item.system.recharge &&
+ !item.system.recharge.charged &&
+ item.system.recharge.value
+ )
+ }
+
+ /**
+ * Perform Utility Macro
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ async _performUtilityMacro (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ const token = super.getToken(tokenId)
+
+ switch (actionId) {
+ case 'deathSave':
+ actor.rollDeathSave({ event })
+ break
+ case 'endTurn':
+ if (!token) break
+ if (game.combat?.current?.tokenId === tokenId) {
+ await game.combat?.nextTurn()
+ }
+ break
+ case 'initiative':
+ await this._rollInitiative(actorId)
+ break
+ case 'inspiration': {
+ const update = !actor.system.attributes.inspiration
+ actor.update({ 'data.attributes.inspiration': update })
+ break
+ }
+ case 'longRest':
+ actor.longRest()
+ break
+ case 'shortRest':
+ actor.shortRest()
+ break
+ case 'toggleCombat':
+ if (canvas.tokens.controlled.length === 0) break
+ await canvas.tokens.controlled[0].toggleCombat()
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ break
+ case 'toggleVisibility':
+ if (!token) break
+ token.toggleVisibility()
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ break
+ }
+ }
+
+ /**
+ * Roll Initiative
+ * @private
+ * @param {string} actorId
+ * @param {string} tokenId
+ */
+ async _rollInitiative (actorId, tokenId) {
+ const actor = super.getActor(actorId, tokenId)
+
+ await actor.rollInitiative({ createCombatants: true })
+
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ }
+
+ /**
+ * Toggle Condition
+ * @private
+ * @param {object} event
+ * @param {string} tokenId
+ * @param {string} actionId
+ * @param {object} effect
+ */
+ async _toggleCondition (event, tokenId, actionId, effect = null) {
+ const token = super.getToken(tokenId)
+ const isRightClick = this.isRightClick(event)
+ if (game.dfreds && effect?.flags?.isConvenient) {
+ const effectLabel = effect.label
+ game.dfreds.effectInterface._toggleEffect(effectLabel)
+ } else {
+ const condition = this._getCondition(actionId)
+ if (!condition) return
+
+ isRightClick
+ ? await token.toggleEffect(condition, { overlay: true })
+ : await token.toggleEffect(condition)
+ }
+
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ }
+
+ /**
+ * Get Condition
+ * @private
+ * @param {string} actionId
+ * @returns {object}
+ */
+ _getCondition (actionId) {
+ return CONFIG.statusEffects.find((effect) => effect.id === actionId)
+ }
+
+ /**
+ * Toggle Effect
+ * @param {object} event
+ * @param {string} actorId
+ * @param {string} tokenId
+ * @param {string} actionId
+ */
+ async _toggleEffect (event, actorId, tokenId, actionId) {
+ const actor = super.getActor(actorId, tokenId)
+ const effects = 'find' in actor.effects.entries ? actor.effects.entries : actor.effects
+ const effect = effects.find((e) => e.id === actionId)
+
+ if (!effect) return
+
+ const statusId = effect.flags.core?.statusId
+ if (tokenId && statusId) {
+ await this._toggleCondition(event, tokenId, statusId, effect)
+ return
+ }
+
+ await effect.update({ disabled: !effect.disabled })
+
+ Hooks.callAll('forceUpdateTokenActionHUD')
+ }
+}
diff --git a/scripts/settings.js b/scripts/settings.js
new file mode 100644
index 0000000..b3ef300
--- /dev/null
+++ b/scripts/settings.js
@@ -0,0 +1,156 @@
+import { ItemMacroOptions } from './item-macro-options.js'
+
+export function register (updateFunc) {
+ const appName = 'token-action-hud-dnd5e'
+
+ if (game.modules.get('itemacro')?.active) {
+ game.settings.register(appName, 'itemMacroReplace', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.itemMacroReplace.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.itemMacroReplace.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: String,
+ choices: {
+ showBoth: game.i18n.localize(ItemMacroOptions.SHOW_BOTH),
+ showItemMacro: game.i18n.localize(ItemMacroOptions.SHOW_ITEM_MACRO),
+ showOriginal: game.i18n.localize(ItemMacroOptions.SHOW_ORIGINAL_ITEM)
+ },
+ default: 'showBoth',
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+ }
+
+ game.settings.register(appName, 'abbreviateSkills', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.abbreviateSkills.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.abbreviateSkills.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: false,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showSlowActions', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showSlowActions.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showSlowActions.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: true,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showPassiveFeats', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showPassiveFeats.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showPassiveFeats.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: true,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'displaySpellInfo', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.displaySpellInfo.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.displaySpellInfo.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: true,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showUnchargedItems', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnchargedItems.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnchargedItems.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: false,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showUnequippedItems', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnequippedItems.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnequippedItems.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: true,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showUnpreparedSpells', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnpreparedSpells.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showUnpreparedSpells.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: true,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+
+ game.settings.register(appName, 'showItemsWithoutActivationCosts', {
+ name: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showItemsWithoutActivationCosts.name'
+ ),
+ hint: game.i18n.localize(
+ 'tokenActionHud.dnd5e.settings.showItemsWithoutActivationCosts.hint'
+ ),
+ scope: 'client',
+ config: true,
+ type: Boolean,
+ default: false,
+ onChange: (value) => {
+ updateFunc(value)
+ }
+ })
+}
diff --git a/scripts/system-manager.js b/scripts/system-manager.js
new file mode 100644
index 0000000..4288636
--- /dev/null
+++ b/scripts/system-manager.js
@@ -0,0 +1,226 @@
+// System Module Imports
+import ActionHandler from './action-handler.js'
+import MagicItemActionListExtender from './magic-items-extender.js'
+import { RollHandler as Core } from './roll-handler.js'
+import { RollHandlerObsidian as Obsidian5e } from './roll-handler-obsidian.js'
+import * as systemSettings from './settings.js'
+
+// Core Module Imports
+import { CoreSystemManager, CoreCategoryManager } from './config.js'
+
+export default class SystemManager extends CoreSystemManager {
+ /** @override */
+ doGetCategoryManager (user) {
+ const categoryManager = new CoreCategoryManager(user)
+ return categoryManager
+ }
+
+ /** @override */
+ doGetActionHandler (character, categoryManager) {
+ const actionHandler = new ActionHandler(character, categoryManager)
+
+ if (CoreSystemManager.isModuleActive('magicitems')) { actionHandler.addFurtherActionHandler(new MagicItemActionListExtender()) }
+
+ return actionHandler
+ }
+
+ /** @override */
+ getAvailableRollHandlers () {
+ let coreTitle = 'Core D&D5e'
+
+ if (CoreSystemManager.isModuleActive('midi-qol')) { coreTitle += ` [supports ${CoreSystemManager.getModuleTitle('midi-qol')}]` }
+
+ const choices = { core: coreTitle }
+ CoreSystemManager.addHandler(choices, 'obsidian')
+
+ return choices
+ }
+
+ /** @override */
+ doGetRollHandler (handlerId) {
+ let rollHandler
+ switch (handlerId) {
+ case 'obsidian':
+ rollHandler = new Obsidian5e()
+ break
+ case 'core':
+ default:
+ rollHandler = new Core()
+ break
+ }
+
+ return rollHandler
+ }
+
+ /** @override */
+ doRegisterSettings (updateFunc) {
+ systemSettings.register(updateFunc)
+ }
+
+ /** @override */
+ async doRegisterDefaultFlags () {
+ const defaults = {
+ default: {
+ categories: {
+ inventory: {
+ id: 'inventory',
+ title: this.i18n('DND5E.Inventory'),
+ subcategories: {
+ inventory_weapons: {
+ id: 'weapons',
+ title: this.i18n('DND5E.ItemTypeWeaponPl'),
+ type: 'system'
+ },
+ inventory_equipment: {
+ id: 'equipment',
+ title: this.i18n('DND5E.ItemTypeEquipmentPl'),
+ type: 'system'
+ },
+ inventory_consumables: {
+ id: 'consumables',
+ title: this.i18n('DND5E.ItemTypeConsumablePl'),
+ type: 'system'
+ },
+ inventory_tools: {
+ id: 'tools',
+ title: this.i18n('DND5E.ItemTypeToolPl'),
+ type: 'system'
+ },
+ inventory_containers: {
+ id: 'containers',
+ title: this.i18n('DND5E.ItemTypeContainerPl'),
+ type: 'system'
+ },
+ inventory_loot: {
+ id: 'loot',
+ title: this.i18n('DND5E.ItemTypeLootPl'),
+ type: 'system'
+ }
+ }
+ },
+ features: {
+ id: 'features',
+ title: this.i18n('DND5E.Features'),
+ subcategories: {
+ features_features: {
+ id: 'features',
+ title: this.i18n('DND5E.Features'),
+ type: 'system'
+ }
+ }
+ },
+ spells: {
+ id: 'spells',
+ title: this.i18n('DND5E.ItemTypeSpellPl'),
+ subcategories: {
+ spells_spells: {
+ id: 'spells',
+ title: this.i18n('DND5E.ItemTypeSpellPl'),
+ type: 'system'
+ }
+ }
+ },
+ attributes: {
+ id: 'attributes',
+ title: this.i18n('DND5E.Attributes'),
+ subcategories: {
+ attributes_abilities: {
+ id: 'abilities',
+ title: this.i18n('tokenActionHud.dnd5e.abilities'),
+ type: 'system'
+ },
+ attributes_skills: {
+ id: 'skills',
+ title: this.i18n('tokenActionHud.dnd5e.skills'),
+ type: 'system'
+ }
+ }
+ },
+ effects: {
+ id: 'effects',
+ title: this.i18n('DND5E.Effects'),
+ subcategories: {
+ effects_effects: {
+ id: 'effects',
+ title: this.i18n('DND5E.Effects'),
+ type: 'system'
+ }
+ }
+ },
+ conditions: {
+ id: 'conditions',
+ title: this.i18n('tokenActionHud.dnd5e.conditions'),
+ subcategories: {
+ conditions_conditions: {
+ id: 'conditions',
+ title: this.i18n('tokenActionHud.dnd5e.conditions'),
+ type: 'system'
+ }
+ }
+ },
+ utility: {
+ id: 'utility',
+ title: this.i18n('tokenActionHud.utility'),
+ subcategories: {
+ utility_combat: {
+ id: 'combat',
+ title: this.i18n('tokenActionHud.combat'),
+ type: 'system'
+ },
+ utility_token: {
+ id: 'token',
+ title: this.i18n('tokenActionHud.token'),
+ type: 'system'
+ },
+ utility_rests: {
+ id: 'rests',
+ title: this.i18n('tokenActionHud.dnd5e.rests'),
+ type: 'system'
+ },
+ utility_utility: {
+ id: 'utility',
+ title: this.i18n('tokenActionHud.utility'),
+ type: 'system'
+ }
+ }
+ }
+ },
+ subcategories: [
+ { id: 'abilities', title: this.i18n('tokenActionHud.dnd5e.abilities'), type: 'system' },
+ { id: 'actions', title: this.i18n('DND5E.ActionPl'), type: 'system' },
+ { id: 'bonus-actions', title: this.i18n('tokenActionHud.dnd5e.bonusActions'), type: 'system' },
+ { id: 'checks', title: this.i18n('tokenActionHud.dnd5e.checks'), type: 'system' },
+ { id: 'combat', title: this.i18n('tokenActionHud.combat'), type: 'system' },
+ { id: 'conditions', title: this.i18n('tokenActionHud.dnd5e.conditions'), type: 'system' },
+ { id: 'consumables', title: this.i18n('DND5E.ItemTypeConsumablePl'), type: 'system' },
+ { id: 'containers', title: this.i18n('DND5E.ItemTypeContainerPl'), type: 'system' },
+ { id: 'crew-actions', title: this.i18n('tokenActionHud.dnd5e.crewActions'), type: 'system' },
+ { id: 'effects', title: this.i18n('DND5E.Effects'), vtype: 'system' },
+ { id: 'equipment', title: this.i18n('DND5E.ItemTypeEquipmentPl'), type: 'system' },
+ { id: 'equipped', title: this.i18n('DND5E.Equipped'), type: 'system' },
+ { id: 'features', title: this.i18n('DND5E.Features'), type: 'system' },
+ { id: 'lair-actions', title: this.i18n('tokenActionHud.dnd5e.lairActions'), type: 'system' },
+ { id: 'legendary-actions', title: this.i18n('tokenActionHud.dnd5e.legendaryActions'), type: 'system' },
+ { id: 'loot', title: this.i18n('DND5E.ItemTypeLootPl'), type: 'system' },
+ { id: 'other-actions', title: this.i18n('tokenActionHud.dnd5e.otherActions'), type: 'system' },
+ { id: 'reactions', title: this.i18n('DND5E.ReactionPl'), type: 'system' },
+ { id: 'rests', title: this.i18n('tokenActionHud.dnd5e.rests'), type: 'system' },
+ { id: 'saves', title: this.i18n('DND5E.ClassSaves'), type: 'system' },
+ { id: 'skills', title: this.i18n('tokenActionHud.dnd5e.skills'), type: 'system' },
+ { id: 'spells', title: this.i18n('DND5E.ItemTypeSpellPl'), type: 'system' },
+ { id: 'token', title: this.i18n('tokenActionHud.token'), type: 'system' },
+ { id: 'tools', title: this.i18n('DND5E.ItemTypeToolPl'), type: 'system' },
+ { id: 'weapons', title: this.i18n('DND5E.ItemTypeWeaponPl'), type: 'system' },
+ { id: 'unequipped', title: this.i18n('DND5E.Unequipped'), type: 'system' },
+ { id: 'utility', title: this.i18n('tokenActionHud.utility'), type: 'system' }
+ ]
+ }
+ }
+ // If the 'Magic Items' module is active, then add a subcategory for it
+ if (game.modules.get('magicitems').active) {
+ defaults.default.subcategories.push({ id: 'magic-items', title: this.i18n('tokenActionHud.magicItems'), type: 'system' })
+ defaults.default.subcategories.sort((a, b) => a.id.localeCompare(b.id))
+ }
+ await game.user.update({ flags: { 'token-action-hud-core': defaults } })
+ }
+}
diff --git a/scripts/utils.js b/scripts/utils.js
new file mode 100644
index 0000000..b56d92e
--- /dev/null
+++ b/scripts/utils.js
@@ -0,0 +1,43 @@
+import { Logger } from './config.js'
+
+const namespace = 'token-action-hud-dnd5e'
+
+/**
+ * Get setting value
+ * @param {string} key The key
+ * @param {string=null} defaultValue The default value
+ * @returns The setting value
+ */
+export function getSetting (key, defaultValue = null) {
+ let value = defaultValue ?? null
+ if (game.settings.settings.get(`${namespace}.${key}`)) {
+ value = game.settings.get(namespace, key)
+ } else {
+ Logger.debug(`Setting '${key}' not found`)
+ }
+ return value
+}
+
+/**
+ * Set setting value
+ * @param {string} key The key
+ * @param {string} value The value
+ */
+export function setSetting (key, value) {
+ if (game.settings.settings.get(`${namespace}.${key}`)) {
+ value = game.settings.set(namespace, key, value)
+ Logger.debug(`Setting '${key}' set to '${value}'`)
+ } else {
+ Logger.debug(`Setting '${key}' not found`)
+ }
+}
+
+/**
+ * Import a default class
+ * @param {string} path The path of the file
+ */
+export async function importClass (path) {
+ return await import(path).then(module => {
+ return module.default
+ })
+}