diff --git a/scripts/api.js b/scripts/api.js index c61cd5b..d5e612d 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -135,9 +135,7 @@ export async function revertMinions(actors) { * @returns {boolean} */ export function isMinion(target) { - target = target?.actor ?? target; - const minionFeatureName = getSetting(CONSTANTS.SETTING_KEYS.MINION_FEATURE_NAME).toLowerCase(); - return target.items.some(item => item.name.toLowerCase() === minionFeatureName); + return lib.hasActorItemNamed(target, getSetting(CONSTANTS.SETTING_KEYS.MINION_FEATURE_NAME), true) } /** diff --git a/scripts/constants.js b/scripts/constants.js index a8644f8..1ecca43 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -20,6 +20,7 @@ const CONSTANTS = { GROUP_NUMBER: `${FLAG}.groupNumber`, DELETE_GROUP_NUMBER: `${FLAG}.-=groupNumber`, MIDI_GROUP_ATTACK: "flags.midiProperties.grpact", + MINION_FEATURE: `${FLAG}.minionfeature.` }, MODULES: { MIDI: false @@ -40,98 +41,108 @@ CONSTANTS["SETTING_KEYS"] = { ENABLE_MINION_FEATURE_AUTOMATION: "enableMinionFeatureAutomation" } -CONSTANTS["SETTINGS"] = { - [CONSTANTS.SETTING_KEYS.DEBUG]: { - name: "MINIONMANAGER.Settings.Debug.Title", - hint: "MINIONMANAGER.Settings.Debug.Hint", - scope: "client", - config: true, - default: false, - type: Boolean - }, +/** + * @returns {{ + * Object + * }} + * @constructor + */ +CONSTANTS["SETTINGS"] = () => { - [CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_DAMAGE]: { - name: "MINIONMANAGER.Settings.EnableOverkillDamage.Title", - hint: "MINIONMANAGER.Settings.EnableOverkillDamage.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + return { - [CONSTANTS.SETTING_KEYS.ENABLE_RANGED_OVERKILL]: { - name: "MINIONMANAGER.Settings.EnableRangedOverkill.Title", - hint: "MINIONMANAGER.Settings.EnableRangedOverkill.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.DEBUG]: { + name: "MINIONMANAGER.Settings.Debug.Title", + hint: "MINIONMANAGER.Settings.Debug.Hint", + scope: "client", + config: true, + default: false, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.ENABLE_SPELL_OVERKILL]: { - name: "MINIONMANAGER.Settings.EnableSpellOverkill.Title", - hint: "MINIONMANAGER.Settings.EnableSpellOverkill.Hint", - scope: "world", - config: true, - default: false, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_DAMAGE]: { + name: "MINIONMANAGER.Settings.EnableOverkillDamage.Title", + hint: "MINIONMANAGER.Settings.EnableOverkillDamage.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_MESSAGE]: { - name: "MINIONMANAGER.Settings.EnableOverkillMessage.Title", - hint: "MINIONMANAGER.Settings.EnableOverkillMessage.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.ENABLE_RANGED_OVERKILL]: { + name: "MINIONMANAGER.Settings.EnableRangedOverkill.Title", + hint: "MINIONMANAGER.Settings.EnableRangedOverkill.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS]: { - name: "MINIONMANAGER.Settings.EnableGroupAttacks.Title", - hint: "MINIONMANAGER.Settings.EnableGroupAttacks.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.ENABLE_SPELL_OVERKILL]: { + name: "MINIONMANAGER.Settings.EnableSpellOverkill.Title", + hint: "MINIONMANAGER.Settings.EnableSpellOverkill.Hint", + scope: "world", + config: true, + default: false, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS]: { - name: "MINIONMANAGER.Settings.EnableGroupAttackBonus.Title", - hint: "MINIONMANAGER.Settings.EnableGroupAttackBonus.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_MESSAGE]: { + name: "MINIONMANAGER.Settings.EnableOverkillMessage.Title", + hint: "MINIONMANAGER.Settings.EnableOverkillMessage.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.ENABLE_MINION_SUPER_SAVE]: { - name: "MINIONMANAGER.Settings.EnableMinionSuperSave.Title", - hint: "MINIONMANAGER.Settings.EnableMinionSuperSave.Hint", - scope: "world", - config: true, - default: true, - type: Boolean - }, + [CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS]: { + name: "MINIONMANAGER.Settings.EnableGroupAttacks.Title", + hint: "MINIONMANAGER.Settings.EnableGroupAttacks.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.MINION_FEATURE_NAME]: { - name: "MINIONMANAGER.Settings.MinionFeatureName.Title", - hint: "MINIONMANAGER.Settings.MinionFeatureName.Hint", - scope: "world", - config: true, - default: "Minion", - required: true, - type: String - }, + [CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACK_BONUS]: { + name: "MINIONMANAGER.Settings.EnableGroupAttackBonus.Title", + hint: "MINIONMANAGER.Settings.EnableGroupAttackBonus.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, - [CONSTANTS.SETTING_KEYS.MINION_FEATURE_DESCRIPTION]: { - name: "MINIONMANAGER.Settings.MinionFeatureDescription.Title", - hint: "MINIONMANAGER.Settings.MinionFeatureDescription.Hint", - scope: "world", - config: true, - default: "If the minion takes damage from an attack or as the result of a failed saving throw, their hit points are reduced to 0. If the minion takes damage from another effect, they die if the damage equals or exceeds their hit point maximum, otherwise they take no damage.", - required: true, - type: String - }, + [CONSTANTS.SETTING_KEYS.ENABLE_MINION_SUPER_SAVE]: { + name: "MINIONMANAGER.Settings.EnableMinionSuperSave.Title", + hint: "MINIONMANAGER.Settings.EnableMinionSuperSave.Hint", + scope: "world", + config: true, + default: true, + type: Boolean + }, + + [CONSTANTS.SETTING_KEYS.MINION_FEATURE_NAME]: { + name: "MINIONMANAGER.Settings.MinionFeatureName.Title", + hint: "MINIONMANAGER.Settings.MinionFeatureName.Hint", + scope: "world", + config: true, + default: "Minion", + required: true, + type: String + }, + + [CONSTANTS.SETTING_KEYS.MINION_FEATURE_DESCRIPTION]: { + name: "MINIONMANAGER.Settings.MinionFeatureDescription.Title", + hint: "MINIONMANAGER.Settings.MinionFeatureDescription.Hint", + scope: "world", + config: true, + default: "If the minion takes damage from an attack or as the result of a failed saving throw, their hit points are reduced to 0. If the minion takes damage from another effect, they die if the damage equals or exceeds their hit point maximum, otherwise they take no damage.", + required: true, + type: String + } + } } diff --git a/scripts/initiative.js b/scripts/initiative.js index 0d01681..a5c5df4 100644 --- a/scripts/initiative.js +++ b/scripts/initiative.js @@ -107,8 +107,24 @@ export function initializeInitiative() { let tokenBeingDeleted = false; Hooks.on("preDeleteToken", (doc) => { + const groupNumber = getProperty(doc, CONSTANTS.FLAGS.GROUP_NUMBER); + if (!groupNumber || !game.combats.viewed) return true; + const tokenCombatant = game.combats.viewed.combatants.find(combatant => getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === groupNumber); + if(!tokenCombatant) return true; tokenBeingDeleted = doc.id; - }) + }); + + Hooks.on("deleteToken", async (doc) => { + if(doc.id !== tokenBeingDeleted) return; + const groupNumber = getProperty(doc, CONSTANTS.FLAGS.GROUP_NUMBER); + const tokenCombatant = game.combats.viewed.combatants.find(combatant => getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === groupNumber); + const subCombatants = foundry.utils.deepClone(getProperty(tokenCombatant, CONSTANTS.FLAGS.COMBATANTS) ?? []) + subCombatants.splice(subCombatants.indexOf(doc.uuid), 1); + await tokenCombatant.update({ + [CONSTANTS.FLAGS.COMBATANTS]: subCombatants + }); + ui.combat.render(true); + }); Hooks.on("preDeleteCombatant", (combatant) => { if (tokenBeingDeleted !== combatant.tokenId) return true; diff --git a/scripts/interface.js b/scripts/interface.js index af6d38f..eb5e820 100644 --- a/scripts/interface.js +++ b/scripts/interface.js @@ -9,7 +9,8 @@ export function initializeInterface() { Hooks.on("renderCombatTracker", async (app) => { app.element.find(".combatant").each(function () { - const combatant = game.combats.viewed.combatants.get($(this).data("combatantId")); + const combatant = game.combats.viewed ? game.combats.viewed.combatants.get($(this).data("combatantId")) : null; + if (!combatant) return; const minionGroup = foundry.utils.deepClone(getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER)); if (!minionGroup) return; const tokenImageDiv = $("
"); @@ -45,7 +46,11 @@ export function initializeInterface() { const newGroupNumber = Number(index) + 1; - const colorBox = $(``); + const groupAlreadyExists = game.combats.viewed ? game.combats.viewed.combatants.some(combatant => { + return getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === newGroupNumber; + }) : false; + + const colorBox = $(``); colorBox.on("click", async () => { @@ -56,9 +61,9 @@ export function initializeInterface() { return !newTokens.includes(oldToken) && existingGroupNumber && tokenGroupNumber && existingGroupNumber === tokenGroupNumber; }); - const existingCombatantGroup = game.combats.viewed.combatants.find(combatant => { + const existingCombatantGroup = game.combats.viewed ? game.combats.viewed.combatants.find(combatant => { return existingGroupNumber && getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === existingGroupNumber; - }); + }) : false; if (existingCombatantGroup && !tokensKeptInOldGroup.length) { await existingCombatantGroup.delete() @@ -84,9 +89,13 @@ export function initializeInterface() { [CONSTANTS.FLAGS.GROUP_NUMBER]: newGroupNumber }))); + if (!game.combats.viewed) { + await Combat.create({ scene: canvas.scene.id }); + } + const existingCombatantInNewGroup = game.combats.viewed.combatants.find(combatant => { return getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === newGroupNumber; - }); + }) if (existingCombatantInNewGroup) { const existingUuids = foundry.utils.deepClone(getProperty(existingCombatantInNewGroup, CONSTANTS.FLAGS.COMBATANTS) ?? []); @@ -123,7 +132,8 @@ export function initializeInterface() { libWrapper.register(CONSTANTS.MODULE_NAME, 'CombatTracker.prototype.getData', async function (wrapped, ...args) { const data = await wrapped(...args); for (const turn of data.turns) { - const combatant = game.combats.viewed.combatants.get(turn.id); + const combatant = game.combats.viewed ? game.combats.viewed.combatants.get(turn.id) : false; + if (!combatant) continue; const subCombatants = foundry.utils.deepClone(getProperty(combatant, CONSTANTS.FLAGS.COMBATANTS) ?? []) if (!subCombatants.length) continue; const documents = subCombatants.map((uuid) => fromUuidSync(uuid)).filter(Boolean); diff --git a/scripts/lib.js b/scripts/lib.js index a5d4325..bd01fcc 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -49,3 +49,10 @@ export function patchItemDamageRollConfig(item){ return `${newFormula}${damageType ? `[${damageType}]` : ""}` }) } + +export function hasActorItemNamed(target, itemName, lowerCase=false){ + target = target?.actor ?? target; + return target && target.items.some(item => { + return (lowerCase ? item.name.toLowerCase() : item.name) === (lowerCase ? itemName.toLowerCase() : itemName); + }); +} diff --git a/scripts/module.js b/scripts/module.js index 9064b89..66d1f37 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -13,15 +13,11 @@ Hooks.on("init", () => { }); Hooks.once("ready", () => { - if (CONSTANTS.MODULES.MIDI) { - const flagName = CONSTANTS.FLAGS.MIDI_GROUP_ATTACK.split(".").pop(); - CONFIG.DND5E.midiProperties[flagName] = "Group Action"; - } game.modules.get(CONSTANTS.MODULE_NAME).api = API; }); function initializeSettings() { - for (const [key, setting] of Object.entries(CONSTANTS.SETTINGS)) { + for (const [key, setting] of Object.entries(CONSTANTS.SETTINGS())) { game.settings.register(CONSTANTS.MODULE_NAME, key, setting); } } diff --git a/scripts/plugins/midiqol.js b/scripts/plugins/midiqol.js index 31377a9..b5d5feb 100644 --- a/scripts/plugins/midiqol.js +++ b/scripts/plugins/midiqol.js @@ -118,7 +118,7 @@ export default { const closestTokens = new Set(canvas.tokens.placeables .filter(_token => { const withinRange = canvas.grid.measureDistance(workflow.token, _token) <= workflow.item.system.range.value + 2.5; - return hitTarget?.actor && _token?.actor && hitTarget?.actor?.name === _token?.actor?.name && withinRange; + return hitTarget?.actor && _token?.actor && hitTarget.document?.baseActor?.name === _token?.document?.baseActor?.name && withinRange; }) .sort((a, b) => canvas.grid.measureDistance(workflow.token, a) - canvas.grid.measureDistance(workflow.token, b))); @@ -162,7 +162,7 @@ export default { }); const userTargets = new Set([...game.user.targets] - .filter(_token => _token.name === hitTarget.name) + .filter(_token => _token.document.baseActor.name === hitTarget.document.baseActor.name) ); userTargets.delete(hitTarget); diff --git a/styles/module.css b/styles/module.css index 9f92926..108a4be 100644 --- a/styles/module.css +++ b/styles/module.css @@ -41,18 +41,16 @@ div.token-image > img.minion-group{ margin: 0; padding: 0; border-radius: 99px; - background-color: white; cursor: pointer; - position: relative; + max-width: initial !important; + opacity: 1; + background-color: white; + background-size: calc(100% + 2px) calc(100% + 2px) !important; + background-position: -1px -1px !important; + overflow: visible !important; } -.grouped-initiative > .minion-group > img { - opacity: 1 !important; - margin: 0 !important; - left: -1px; - top: -1px; - width: 22px; - height: 22px; - position: absolute; - max-width: initial !important; +.grouped-initiative > .minion-group.minion-group-used { + background-color: black; + box-shadow: inset 0px 0px 4px 1px rgba(255,255,255,1); }