diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b09152..81f33c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.2.2] + +### Changed + +- Minimum SWADE version is now 5.2.0 +- Reworked Gang Up to still handle Formation Fighter along with the new system Gang Up improvements +- BREAKING: Pack Tactics needs an effect added setting `system.stats.gangUpDamage` to `true` + +### Fixed + +- Gang up calculation was double-counting the attacker if the attacker had formation fighter. + ## [4.2.1] ### Added diff --git a/src/module.json b/src/module.json index 5540c32..963c05d 100644 --- a/src/module.json +++ b/src/module.json @@ -9,7 +9,7 @@ } ], "url": "https://git.bloy.org/foundryvtt/swade-mb-helpers", - "version": "4.2.1", + "version": "4.2.2", "compatibility": { "minimum": "13", "verified": "13" @@ -111,8 +111,8 @@ "type": "system", "manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json", "compatibility": { - "minimum": "5.1.0", - "verified": "5.1.0" + "minimum": "5.2.0", + "verified": "5.2.0" } } ], diff --git a/src/module/rollHelpers.js b/src/module/rollHelpers.js index c65b639..ee4b0f3 100644 --- a/src/module/rollHelpers.js +++ b/src/module/rollHelpers.js @@ -21,9 +21,9 @@ function getEdgeToEdgeDistance(tokenA, tokenB) { return distance - combinedRadii * conversionFactor; } -function calcGangupBonus(sourceToken, targetToken) { - const targetActor = targetToken.actor; +function getGangupModifiers(sourceToken, targetToken, label = null) { const sourceActor = sourceToken.actor; + const targetActor = targetToken.actor; const scene = targetToken.parent; const ignoreStatuses = ['defeated', 'incapacitated', 'stunned']; const attackerAllies = scene.tokens.filter((t) => { @@ -34,9 +34,6 @@ function calcGangupBonus(sourceToken, targetToken) { let formationFighterBonus = attackerAllies.filter((t) => t.actor.getSingleItemBySwid('formation-fighter', 'edge'), ).length; - if (sourceActor.getSingleItemBySwid('formation-fighter', 'edge')) { - formationFighterBonus++; - } let numAttackerAllies = attackerAllies.length; const numDefenderAllies = scene.tokens.filter((t) => { if (t.disposition !== targetToken.disposition) return false; @@ -44,16 +41,49 @@ function calcGangupBonus(sourceToken, targetToken) { if (getEdgeToEdgeDistance(targetToken, t) >= 1) return false; return getEdgeToEdgeDistance(sourceToken, t) < 1; }).length; - if (numAttackerAllies > 0) { + if (numAttackerAllies > 1) { numAttackerAllies += formationFighterBonus; } let gangUpBonus = Math.min(4, numAttackerAllies - numDefenderAllies); - if (targetActor?.getSingleItemBySwid('improved-block', 'edge')) { - gangUpBonus -= 2; - } else if (targetActor?.getSingleItemBySwid('block', 'edge')) { - gangUpBonus -= 1; + const attackerGlobalMods = foundry.utils.getProperty(sourceActor, 'system.stats.globalMods'); + if (attackerGlobalMods?.gangUp && Array.isArray(attackerGlobalMods.gangUp)) { + attackerGlobalMods.gangUp.forEach((m) => (gangUpBonus += Number(m.value))); } - return gangUpBonus; + let targetGangUpMod = 0; + const targetGangUpLabels = []; + const targetGlobalMods = targetActor ? foundry.utils.getProperty(targetActor, 'system.stats.globalMods') : {}; + if (targetGlobalMods?.gangUp && Array.isArray(targetGlobalMods.gangUp)) { + for (const m of targetGlobalMods.gangUp) { + targetGangUpMod += Number(m.value); + targetGangUpLabels.push(m.label); + } + } + const improvedBlock = targetActor?.getSingleItemBySwid('improved-block', 'edge'); + if (improvedBlock) { + targetGangUpMod -= 2; + targetGangUpLabels.push(improvedBlock.name); + } else { + const block = targetActor?.getSingleItemBySwid('block', 'edge'); + if (block) { + targetGangUpMod -= 1; + targetGangUpLabels.push(block.name); + } + } + const mods = []; + if (gangUpBonus > 0) { + mods.push({ + label: label || game.i18n.localize('SWADE.GangUp'), + value: gangUpBonus, + }); + if (targetGangUpMod !== 0) { + const value = Math.max(targetGangUpMod, -gangUpBonus); + mods.push({ + label: targetGangUpLabels.join(' + ') || 'SWADE.GlobalMod.TargetGangup', + value, + }); + } + } + return mods; } export async function preAttackRollModifiers( @@ -121,16 +151,21 @@ export async function preAttackRollModifiers( // Gang Up correction if (isMeleeAttack) { - const gangUpBonus = calcGangupBonus(sourceToken, targetToken); - const gangUpModIndex = additionalMods.findIndex((m) => m.label === 'SWADE.GangUp'); - if (gangUpModIndex > -1) { - additionalMods.splice(gangUpModIndex, 1); - } - if (gangUpBonus && gangUpBonus > 0) { - additionalMods.push({ - label: 'SWADE.GangUp', - value: gangUpBonus, - }); + const gangUpMods = getGangupModifiers(sourceToken, targetToken); + const titles = gangUpMods.map((mod) => mod.label); + titles.push('SWADE.GangUp'); + titles.push('SWADE.GlobalMod.TargetGangUp'); + removeDuplicateMods(additionalMods, titles); + additionalMods.push(...gangUpMods); + } +} + +function removeDuplicateMods(additionalMods, replacementModTitles) { + const translatedTitles = replacementModTitles.map((t) => game.i18n.localize(t)); + for (const title of [...replacementModTitles, ...translatedTitles]) { + const modIndex = additionalMods.findIndex((m) => m.label === title); + if (modIndex > -1) { + additionalMods.splice(modIndex, 1); } } } @@ -172,11 +207,26 @@ export async function preDamageRollModifiers(actor, item, roll, modifiers, optio }), ); } - if (token.actor.getSingleItemBySwid('pack-tactics', 'ability')) { - const gangupBonus = calcGangupBonus(token, target); - if (gangupBonus > 0) { - modifiers.push({ label: 'Gang Up (Pack Tactics)', value: gangupBonus, ignore: false }); + if ( + item.isMeleeWeapon && + 'stats' in token.actor.system && + token.actor.system.stats.gangUpDamage && + target && + token + ) { + const effect = token.actor.effects.find( + (e) => !e.disabled && e.changes.some((c) => c.key === 'system.stats.gangUpDamage'), + ); + const gangUpMods = getGangupModifiers(token, target, effect?.name); + const titles = gangUpMods.map((mod) => mod.label); + titles.push('SWADE.GangUp'); + titles.push('SWADE.GlobalMod.TargetGangUp'); + removeDuplicateMods(modifiers, titles); + const gangUpModIndex = modifiers.findIndex((m) => m.label === 'SWADE.GangUp'); + if (gangUpModIndex > -1) { + modifiers.splice(gangUpModIndex, 1); } + modifiers.push(...gangUpMods); } } } diff --git a/src/packsrc/module-docs/Setting_Adjustments_YSuk1v59tLaL9XUK.json b/src/packsrc/module-docs/Setting_Adjustments_YSuk1v59tLaL9XUK.json index e5a8ec0..53bfbad 100644 --- a/src/packsrc/module-docs/Setting_Adjustments_YSuk1v59tLaL9XUK.json +++ b/src/packsrc/module-docs/Setting_Adjustments_YSuk1v59tLaL9XUK.json @@ -52,7 +52,7 @@ "image": {}, "text": { "format": 1, - "content": "
SWADE Trait and Damage Rolls can now take into account common modifiers based on the target, if there is exactly one attacker and one target. Most will show up on all trait rolls if the target conditions are right. Gang up bonuses will only show up on Fighting rolls.
Any of the proposed modifiers may be ignored by checking the Ignore checkbox. Some modifiers are pre-ignored and must be unchecked to take effect.
The following target conditions are checked for before trait rolls:
Vulnerable
Deflection (as applied by the Deflection power effect from this module)
Glow/Shroud (as applied by the power effect macros from this module)
Arcane Protection (as applied by the Arcane Protection power effect from this module)
Arcane Resistance
Scale Modifiers
Gang Up, taking into account Block and Formation Fighter
Range modifiers
The following target conditions are checked for before damage rolls:
Arcane Protection (as applied by the Arcane Protection power effect from this module)
Arcane Resistance
Special Abilities with 'weakness' in the swid (ignored by default, +4 damage)
Special Abilities with 'resistance' in the swid (ignored by default, -4 damage)
Gang Up bonus if the attacker has Pack Tactics
SWADE Trait and Damage Rolls can now take into account common modifiers based on the target, if there is exactly one attacker and one target. Most will show up on all trait rolls if the target conditions are right. Gang up bonuses will only show up on Fighting rolls.
Any of the proposed modifiers may be ignored by checking the Ignore checkbox. Some modifiers are pre-ignored and must be unchecked to take effect.
The following target conditions are checked for before trait rolls:
Vulnerable - Removed, handled by the system
Deflection (as applied by the Deflection power effect from this module)
Glow/Shroud (as applied by the power effect macros from this module)
Arcane Protection (as applied by the Arcane Protection power effect from this module)
Arcane Resistance
Scale Modifiers, corrected from the system values by taking Swat into account
Correction of system Gang Up modifiers to include Formation Fighter
Range modifiers - Removed, handled by the system.
The following target conditions are checked for before damage rolls:
Arcane Protection (as applied by the Arcane Protection power effect from this module)
Arcane Resistance
Special Abilities with 'weakness' in the swid (ignored by default, +4 damage)
Special Abilities with 'resistance' in the swid (ignored by default, -4 damage)
Gang Up modifiers corrected to include Formation Fighter