diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e61ecf1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.detectIndentation": false +} \ No newline at end of file diff --git a/README.md b/README.md index cacb6bc..08fba84 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # Token Action HUD TheWitcherTRPG +Token Action HUD is a repositionable HUD of actions for a selected token. + + + +# Features +- Make rolls directly from the HUD instead of opening your character sheet. +- Use items from the HUD or right-click an item to open 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. In the Manifest URL field, paste: `https://github.com/ortegamarcel/fvtt-token-action-hud-TheWitcherTRPG/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. diff --git a/languages/de.json b/languages/de.json index de807ce..b348feb 100644 --- a/languages/de.json +++ b/languages/de.json @@ -1,5 +1,7 @@ { "TAH_WITCHER": { - "combat": "Kampf" + "combat": "Kampf", + "weapons": "Waffen", + "magic": "Magie" } } \ No newline at end of file diff --git a/languages/en.json b/languages/en.json index a1fdfe0..b0a6fbf 100644 --- a/languages/en.json +++ b/languages/en.json @@ -1,5 +1,7 @@ { - "TAH_WITCHER": { - "combat": "Combat" - } + "TAH_WITCHER": { + "combat": "Combat", + "weapons": "Weapons", + "magic": "Magic" + } } \ No newline at end of file diff --git a/module.json b/module.json index 91d490f..1418878 100644 --- a/module.json +++ b/module.json @@ -10,7 +10,7 @@ ], "version": "AUTOMATICALLY REPLACED BY GITHUB WORKFLOW ACTION", "esmodules": [ - "scripts/module.js" + "scripts/init.js" ], "relationships": { "systems": [{ diff --git a/scripts/action-handler.js b/scripts/action-handler.js new file mode 100644 index 0000000..2c3bf42 --- /dev/null +++ b/scripts/action-handler.js @@ -0,0 +1,100 @@ +import { ACTION_TYPE, GROUP } from "./constants.js"; +import { Utils } from "./utils.js"; + +export let ActionHandler = null; + +Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { + ActionHandler = class ActionHandler extends coreModule.api.ActionHandler { + + /** @override */ + async buildSystemActions(subcategoryIds) { + // We don't support MULTIPLE tokens being selected at the same time. + //this.actors = (!this.actor) ? this._getActors() : [this.actor] + //this.tokens = (!this.token) ? this._getTokens() : [this.token] + //this.actorType = this.actor?.type + + const token = this.token; + const actor = this.actor; + if (!token || !actor) { + return; + } + + this._getCombat(actor, token.id, { id: GROUP.weapons.id, type: 'system' }); + + + //if (settings.get("showHudTitle")) result.hudTitle = token.name; + } + + _getCombat(actor, tokenId, parent) { + // just one long list of actions for the combat category + const actions = actor.items + .filter(item => item.type === 'weapon' && item.system.equiped) + .map(item => ({ + id: item.id, + name: item.name, + encodedValue: [ACTION_TYPE.attack, actor.id, tokenId, item.id].join(this.delimiter), + img: Utils.getImage(item) + })); + console.log('actions :>> ', actions); + this.addActions(actions, parent); + } + + // createList(parent, actor, tokenId, itemtype, checksort, sorting, label, selectedfunc=undefined) { + // // create one sublist + // const actions = actor.items.filter( item => item.type === itemtype && + // (!checksort || item.system.settings.general.sorting === sorting) && + // (!actor.system.settings.general.hideArchive || !item.system.archived)) + // .map(item => { + // return { + // id: item.id, + // name: item.name, + // encodedValue: [itemtype, actor.id, tokenId, item.id].join(this.delimiter), + // cssClass: item.system.archived ? 'disabled' : selectedfunc ? (selectedfunc(item) ? 'toggle active' : 'toggle') : '', + // img: Utils.getImage(item) + // } + // }) + // if (actions.length) { + // const subcat = { id: sorting, name: Utils.i18n(label), type: 'system-derived'}; + // this.addGroup(subcat, parent); + // this.addActions(actions, subcat); + // } + // } + + // _getSkills(actor, tokenId, parent) { + // // up to four groups of skills + // const table = { + // Skill: actor.system.settings.skills.labelCategory1 || 'CYPHERSYSTEM.Skills', + // SkillTwo: actor.system.settings.skills.labelCategory2 || 'CYPHERSYSTEM.SkillCategoryTwo', + // SkillThree: actor.system.settings.skills.labelCategory3 || 'CYPHERSYSTEM.SkillCategoryThree', + // SkillFour: actor.system.settings.skills.labelCategory4 || 'CYPHERSYSTEM.SkillCategoryFour', + // } + // for (const [ sorting, label ] of Object.entries(table)) { + // this.createList(parent, actor, tokenId, ACTION_SKILL, true, sorting, label) + // } + // } + + // _getAbilities(actor, tokenId, parent) { + // // up to four groups of abilities + // const table = { + // Ability: actor.system.settings.abilities.labelCategory1 || 'CYPHERSYSTEM.Abilities', + // AbilityTwo: actor.system.settings.abilities.labelCategory2 || 'CYPHERSYSTEM.AbilityCategoryTwo', + // AbilityThree: actor.system.settings.abilities.labelCategory3 || 'CYPHERSYSTEM.AbilityCategoryThree', + // AbilityFour: actor.system.settings.abilities.labelCategory4 || 'CYPHERSYSTEM.AbilityCategoryFour', + // Spell: 'CYPHERSYSTEM.Spells' + // } + // for (const [ sorting, label ] of Object.entries(table)) { + // this.createList(parent, actor, tokenId, ACTION_ABILITY, true, sorting, label); + // } + // } + + // _getTags(actor, tokenId, parent) { + // // current recursion is from actor.getFlag("cyphersystem", "recursion"), but the stored string is @ + // const recursion = actor.getFlag("cyphersystem", "recursion")?.slice(1); // strip leading '@' + // const recursionname = actor.items.find(item => item.name.toLowerCase() === recursion)?.name; + // this.createList(parent, actor, tokenId, ACTION_RECURSION, false, 'recursion', 'CYPHERSYSTEM.Recursions', + // (item) => item.name == recursionname ); + // this.createList(parent, actor, tokenId, ACTION_TAG, false, 'tag', 'CYPHERSYSTEM.Tags', + // (item) => item.system.active ); + // } + } +}); \ No newline at end of file diff --git a/scripts/constants.js b/scripts/constants.js new file mode 100644 index 0000000..7c36a98 --- /dev/null +++ b/scripts/constants.js @@ -0,0 +1,29 @@ +/** + * Module-based constants + */ +export const MODULE = { + ID: 'fvtt-token-action-hud-TheWitcherTRPG' +}; + +/** + * Core module + */ +export const CORE_MODULE = { + ID: 'token-action-hud-core' +}; + +/** + * Core module version required by the system module + */ +export const REQUIRED_CORE_MODULE_VERSION = '1.4'; + +/** + * Action type + */ +export const ACTION_TYPE = { + attack: 'attack' +}; + +export const GROUP = { + weapons: { id: 'weapons', name: 'TAH_WITCHER.weapons', type: 'system' } +}; \ No newline at end of file diff --git a/scripts/defaults.js b/scripts/defaults.js new file mode 100644 index 0000000..06896db --- /dev/null +++ b/scripts/defaults.js @@ -0,0 +1,42 @@ +import { GROUP } from './constants.js' + +/** + * Default categories and groups + */ +export let DEFAULTS = null; + +Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { + const groups = GROUP + Object.values(groups).forEach(group => { + group.name = coreModule.api.Utils.i18n(group.name) + group.listName = `Group: ${coreModule.api.Utils.i18n(group.name)}` + }) + const groupsArray = Object.values(groups) + DEFAULTS = { + layout: [ + { + nestId: 'combat', + id: 'combat', + name: coreModule.api.Utils.i18n('TAH_WITCHER.combat'), + groups: [ + { ...groups.weapons, nestId: 'combat_weapons' } + ] + }, + { + nestId: 'skills', + id: 'skills', + name: coreModule.api.Utils.i18n('WITCHER.Monster.SkillTab'), + groups: [ + ] + }, + { + nestId: 'magic', + id: 'magic', + name: coreModule.api.Utils.i18n('TAH_WITCHER.magic'), + groups: [ + ] + } + ], + groups: groupsArray + } +}); \ No newline at end of file diff --git a/scripts/init.js b/scripts/init.js new file mode 100644 index 0000000..39d6866 --- /dev/null +++ b/scripts/init.js @@ -0,0 +1,11 @@ +import { SystemManager } from './system-manager.js' +import { MODULE, REQUIRED_CORE_MODULE_VERSION } from './constants.js' + +Hooks.on('tokenActionHudCoreApiReady', async () => { + const module = game.modules.get(MODULE.ID) + module.api = { + requiredCoreModuleVersion: REQUIRED_CORE_MODULE_VERSION, + SystemManager + } + Hooks.call('tokenActionHudSystemReady', module) +}); \ No newline at end of file diff --git a/scripts/module.js b/scripts/module.js deleted file mode 100644 index f519cec..0000000 --- a/scripts/module.js +++ /dev/null @@ -1,210 +0,0 @@ -// FOR LIVE -import { ActionHandler, DELIMITER, RollHandler, SystemManager, Utils } from '../../token-action-hud-core/scripts/token-action-hud-core.min.js' - -// For DEBUGGING -/* -import { ActionHandler } from '../../token-action-hud-core/scripts/action-handlers/action-handler.js' -import { RollHandler } from '../../token-action-hud-core/scripts/roll-handlers/roll-handler.js' -import { SystemManager } from '../../token-action-hud-core/scripts/system-manager.js' -import { Utils } from '../../token-action-hud-core/scripts/utilities/utils.js' -*/ - -const COMBAT_ID = 'combat'; -const WEAPONS_ID = 'weapons'; - -const ACTION_ATTACK = 'attack'; - -/* ACTIONS */ - -class MyActionHandler extends ActionHandler { - - /** @override */ - async buildSystemActions(subcategoryIds) { - // We don't support MULTIPLE tokens being selected at the same time. - //this.actors = (!this.actor) ? this._getActors() : [this.actor] - //this.tokens = (!this.token) ? this._getTokens() : [this.token] - //this.actorType = this.actor?.type - - const token = this.token; - const actor = this.actor; - if (!token || !actor) { - return; - } - - this._getCombat(actor, token.id, { id: COMBAT_ID, type: 'system' }); - - - //if (settings.get("showHudTitle")) result.hudTitle = token.name; - } - - _getCombat(actor, tokenId, parent) { - // just one long list of actions for the combat category - const actions = actor.items - .filter(item => item.type === 'weapon' && item.system.equiped) - .map(item => ({ - id: item.id, - name: item.name, - encodedValue: [ACTION_ATTACK, actor.id, tokenId, item.id].join(DELIMITER), - img: Utils.getImage(item) - })); - this.addActions(actions, parent); - } - - // createList(parent, actor, tokenId, itemtype, checksort, sorting, label, selectedfunc=undefined) { - // // create one sublist - // const actions = actor.items.filter( item => item.type === itemtype && - // (!checksort || item.system.settings.general.sorting === sorting) && - // (!actor.system.settings.general.hideArchive || !item.system.archived)) - // .map(item => { - // return { - // id: item.id, - // name: item.name, - // encodedValue: [itemtype, actor.id, tokenId, item.id].join(this.delimiter), - // cssClass: item.system.archived ? 'disabled' : selectedfunc ? (selectedfunc(item) ? 'toggle active' : 'toggle') : '', - // img: Utils.getImage(item) - // } - // }) - // if (actions.length) { - // const subcat = { id: sorting, name: Utils.i18n(label), type: 'system-derived'}; - // this.addGroup(subcat, parent); - // this.addActions(actions, subcat); - // } - // } - - // _getSkills(actor, tokenId, parent) { - // // up to four groups of skills - // const table = { - // Skill: actor.system.settings.skills.labelCategory1 || 'CYPHERSYSTEM.Skills', - // SkillTwo: actor.system.settings.skills.labelCategory2 || 'CYPHERSYSTEM.SkillCategoryTwo', - // SkillThree: actor.system.settings.skills.labelCategory3 || 'CYPHERSYSTEM.SkillCategoryThree', - // SkillFour: actor.system.settings.skills.labelCategory4 || 'CYPHERSYSTEM.SkillCategoryFour', - // } - // for (const [ sorting, label ] of Object.entries(table)) { - // this.createList(parent, actor, tokenId, ACTION_SKILL, true, sorting, label) - // } - // } - - // _getAbilities(actor, tokenId, parent) { - // // up to four groups of abilities - // const table = { - // Ability: actor.system.settings.abilities.labelCategory1 || 'CYPHERSYSTEM.Abilities', - // AbilityTwo: actor.system.settings.abilities.labelCategory2 || 'CYPHERSYSTEM.AbilityCategoryTwo', - // AbilityThree: actor.system.settings.abilities.labelCategory3 || 'CYPHERSYSTEM.AbilityCategoryThree', - // AbilityFour: actor.system.settings.abilities.labelCategory4 || 'CYPHERSYSTEM.AbilityCategoryFour', - // Spell: 'CYPHERSYSTEM.Spells' - // } - // for (const [ sorting, label ] of Object.entries(table)) { - // this.createList(parent, actor, tokenId, ACTION_ABILITY, true, sorting, label); - // } - // } - - // _getTags(actor, tokenId, parent) { - // // current recursion is from actor.getFlag("cyphersystem", "recursion"), but the stored string is @ - // const recursion = actor.getFlag("cyphersystem", "recursion")?.slice(1); // strip leading '@' - // const recursionname = actor.items.find(item => item.name.toLowerCase() === recursion)?.name; - // this.createList(parent, actor, tokenId, ACTION_RECURSION, false, 'recursion', 'CYPHERSYSTEM.Recursions', - // (item) => item.name == recursionname ); - // this.createList(parent, actor, tokenId, ACTION_TAG, false, 'tag', 'CYPHERSYSTEM.Tags', - // (item) => item.system.active ); - // } -} - - -/* ROLL HANDLER */ - -class MyRollHandler extends RollHandler { - - doHandleActionEvent(event, encodedValue) { - let payload = encodedValue.split(DELIMITER); - - if (payload.length != 4) { - super.throwInvalidValueErr(); - } - - const action = payload[0]; - const actorId = payload[1]; - const tokenId = payload[2]; - const value = payload[3]; - - const actor = Utils.getActor(actorId, tokenId); - - switch (action) { - case ACTION_ATTACK: - actor.sheet._onItemRoll.call(actor.sheet, null, value); - break; - default: - console.warn(`token-action-hud-TheWitcherTRPG: Unknown action "${action}"`); - break; - } - - // Ensure the HUD reflects the new conditions - // Hooks.callAll('forceUpdateTokenActionHud'); - } -} - -// Core Module Imports - -export class MySystemManager extends SystemManager { - /** @override */ - doGetActionHandler(categoryManager) { - return new MyActionHandler(categoryManager); - } - - /** @override */ - getAvailableRollHandlers() { - const choices = { core: "Core The Witcher TRPG" }; - return choices; - } - - /** @override */ - doGetRollHandler(handlerId) { - return new MyRollHandler(); - } - - /** @override */ - /*doRegisterSettings(updateFunc) { - systemSettings.register(updateFunc) - }*/ - - async doRegisterDefaultFlags() { - const COMBAT_NAME = 'Kampf'; // game.i18n.localize('TAH_WITCHER.Combat'); - const WEAPONS_NAME = 'Waffen'; - - const DEFAULTS = { - layout: [ - { - nestId: COMBAT_ID, - id: COMBAT_ID, - name: COMBAT_NAME, - type: 'system', - groups: [ - { - nestId: 'combat_weapons', - id: WEAPONS_NAME, - name: WEAPONS_NAME, - type: 'system' - } - ] - } - ], - groups: [ - { id: COMBAT_ID, name: COMBAT_NAME, type: 'system' } - ] - }; - - // HUD CORE v1.2 wants us to return the DEFAULTS - return DEFAULTS; - } -} - -/* STARTING POINT */ - -Hooks.once('tokenActionHudCoreApiReady', async () => { - const module = game.modules.get('fvtt-token-action-hud-TheWitcherTRPG'); - module.api = { - requiredCoreModuleVersion: '1.4', - SystemManager: MySystemManager - }; - console.log('test'); - Hooks.call('tokenActionHudSystemReady', module); -}); \ No newline at end of file diff --git a/scripts/roll-handler.js b/scripts/roll-handler.js new file mode 100644 index 0000000..fa84ec2 --- /dev/null +++ b/scripts/roll-handler.js @@ -0,0 +1,35 @@ +import { ACTION_TYPE } from "./constants.js"; +import { Utils } from "./utils.js"; + +export let RollHandler = null + +Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { + RollHandler = class RollHandler extends coreModule.api.RollHandler { + doHandleActionEvent(event, encodedValue) { + let payload = encodedValue.split(this.delimiter); + + if (payload.length != 4) { + super.throwInvalidValueErr(); + } + + const action = payload[0]; + const actorId = payload[1]; + const tokenId = payload[2]; + const value = payload[3]; + + const actor = Utils.getActor(actorId, tokenId); + + switch (action) { + case ACTION_TYPE.attack: + actor.sheet._onItemRoll.call(actor.sheet, null, value); + break; + default: + console.warn(`token-action-hud-TheWitcherTRPG: Unknown action "${action}"`); + break; + } + + // Ensure the HUD reflects the new conditions + // Hooks.callAll('forceUpdateTokenActionHud'); + } + } +}); diff --git a/scripts/system-manager.js b/scripts/system-manager.js new file mode 100644 index 0000000..21a1fe5 --- /dev/null +++ b/scripts/system-manager.js @@ -0,0 +1,42 @@ +import { ActionHandler } from "./action-handler.js"; +import { RollHandler } from "./roll-handler.js"; +import { DEFAULTS } from "./defaults.js"; + +export let SystemManager = null + +Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { + SystemManager = class SystemManager extends coreModule.api.SystemManager { + /** @override */ + doGetCategoryManager() { + return new coreModule.api.CategoryManager() + } + + /** @override */ + doGetActionHandler(categoryManager) { + return new ActionHandler(categoryManager); + } + + /** @override */ + getAvailableRollHandlers() { + const choices = { core: "Core The Witcher TRPG" }; + // coreModule.api.SystemManager.addHandler(choices, 'TheWitcherTRPG') + return choices; + } + + /** @override */ + doGetRollHandler(handlerId) { + return new RollHandler(); + } + + /** @override */ + /*doRegisterSettings(updateFunc) { + systemSettings.register(updateFunc) + }*/ + + /** @override */ + async doRegisterDefaultFlags() { + const defaults = DEFAULTS; + return defaults; + } + } +}); \ No newline at end of file diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..cf9be6f --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,37 @@ +import { MODULE } from './constants.js' + +export let Utils = null + +Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { + Utils = class Utils extends coreModule.api.Utils { + /** + * Get setting value + * @param {string} key The key + * @param {string=null} defaultValue The default value + * @returns The setting value + */ + static getSetting (key, defaultValue = null) { + let value = defaultValue ?? null + try { + value = game.settings.get(MODULE.ID, key) + } catch { + coreModule.api.Logger.debug(`Setting '${key}' not found`) + } + return value + } + + /** + * Set setting value + * @param {string} key The key + * @param {string} value The value + */ + static async setSetting(key, value) { + try { + value = await game.settings.set(MODULE.ID, key, value) + coreModule.api.Logger.debug(`Setting '${key}' set to '${value}'`) + } catch { + coreModule.api.Logger.debug(`Setting '${key}' not found`) + } + } + } +}) \ No newline at end of file