From 56233e7c10eba4fe09b4fb2b952eccf1561978c6 Mon Sep 17 00:00:00 2001 From: Mike Bloy Date: Sun, 7 Dec 2025 23:09:36 -0600 Subject: [PATCH] update roll helpers for swade 5.1.0 --- CHANGELOG.md | 20 +++ src/module/rollHelpers.js | 280 ++++++++++++++------------------- src/module/swade-mb-helpers.js | 3 +- 3 files changed, 142 insertions(+), 161 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02eed59..da580b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ 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.1.0] + +### Added + +- added suppression of system gang up calculation +- added correction for gang up for formation fighter +- added swat correction for scale penalty + +### Changed + +- updated roll modifiers for SWADE version 5.1.0 +- updated pack tactics gang up to use same calculation as system + +### Removed + +- removed roll modifiers handled by the system: + - range check + - vulnerable target + - dodge check + ## [4.0.0] ### Added diff --git a/src/module/rollHelpers.js b/src/module/rollHelpers.js index f6916bb..65c524c 100644 --- a/src/module/rollHelpers.js +++ b/src/module/rollHelpers.js @@ -1,5 +1,9 @@ import { log } from './globals.js'; +function targetHasEffect(target, effectName) { + return target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes(effectName)).length > 0; +} + function hasDeflection(target, type) { return ( target.actor.effects.filter( @@ -8,79 +12,141 @@ function hasDeflection(target, type) { ); } -export async function preTraitRollModifiers(actor, trait, roll, modifiers, options) { - const targets = Array.from(game.user.targets); - const token = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0] : null; - if (targets.some((target) => target.actor.system.status.isVulnerable)) { - modifiers.push({ label: 'Target is Vulnerable', value: '+2', ignore: false }); +function getEdgeToEdgeDistance(tokenA, tokenB) { + const scene = tokenA.parent; + if (!scene) return 0; + const conversionFactor = scene.grid.distance / scene.grid.size; + const combinedRadii = (tokenA?.object?.externalRadius ?? 0) + (tokenB?.object?.externalRadius ?? 0); + const distance = scene.grid.measurePath([tokenA.getCenterPoint(), tokenB.getCenterPoint()]).distance; + return distance - combinedRadii * conversionFactor; +} + +function calcGangupBonus(sourceToken, targetToken) { + const targetActor = targetToken.actor; + const sourceActor = sourceToken.actor; + const scene = targetToken.parent; + const ignoreStatuses = ['defeated', 'incapacitated', 'stunned']; + const attackerAllies = scene.tokens.filter((t) => { + if (t.disposition !== sourceToken.disposition) return false; + if (ignoreStatuses.some((status) => t.hasStatusEffect(status))) return false; + return getEdgeToEdgeDistance(targetToken, t) < 1; + }); + let formationFighterBonus = attackerAllies.filter((t) => + t.actor.getSingleItemBySwid('formation-fighter', 'edge'), + ).length; + if (sourceActor.getSingleItemBySwid('formation-fighter', 'edge')) { + formationFighterBonus++; } - if (targets.some((target) => hasDeflection(target, 'melee'))) { - const ignore = trait?.type !== 'skill' || trait?.system?.swid !== 'fighting'; - modifiers.push({ label: 'Target has Melee Deflection', value: '-2', ignore: ignore }); + let numAttackerAllies = attackerAllies.length; + const numDefenderAllies = scene.tokens.filter((t) => { + if (t.disposition !== targetToken.disposition) return false; + if (ignoreStatuses.some((status) => t.hasStatusEffect(status))) return false; + if (getEdgeToEdgeDistance(targetToken, t) >= 1) return false; + return getEdgeToEdgeDistance(sourceToken, t) < 1; + }).length; + if (numAttackerAllies > 0) { + numAttackerAllies += formationFighterBonus; } - if (targets.some((target) => hasDeflection(target, 'range'))) { - const ignore = - trait?.type !== 'skill' || !(trait?.system?.swid === 'shooting' || trait?.system?.swid === 'athletics'); - modifiers.push({ label: 'Target has Ranged Deflection', value: '-2', ignore: ignore }); + let gangUpBonus = Math.min(4, numAttackerAllies - numDefenderAllies); + if (targetActor?.getSingleItemBySwid('improved-block', 'edge')) { + gangUpBonus -= 2; + } else if (targetActor?.getSingleItemBySwid('block', 'edge')) { + gangUpBonus -= 1; } - if (targets.some((target) => hasDeflection(target, 'all'))) { - const ignore = - trait?.type !== 'skill' || - !( - trait?.system?.swid === 'fighting' || - trait?.system?.swid === 'shooting' || - trait?.system?.swid === 'athletics' - ); - modifiers.push({ label: 'Target has Deflection', value: '-2', ignore: ignore }); + return gangUpBonus; +} + +export async function preAttackRollModifiers( + sourceToken, + targetToken, + skill, + item, + isRangedAttack, + isMeleeAttack, + additionalMods, + bestNonStackingMods, +) { + log('preAttackRollModifiers'); + log('sourceToken', sourceToken); + log('targetToken', targetToken); + log('skill', skill); + log('item', item); + log('additionalMods', additionalMods); + log('bestNonStackingMods', bestNonStackingMods); + log('isRangedAttack', isRangedAttack, 'isMeleeAttack', isMeleeAttack); + + // Deflection + if (isMeleeAttack && hasDeflection(targetToken, 'melee')) { + additionalMods.push({ label: 'Target has melee deflection', value: -2 }); } - if ( - targets.some( - (target) => target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes('glow')).length > 0, - ) - ) { - modifiers.push({ + if (isMeleeAttack && hasDeflection(targetToken, 'range')) { + additionalMods.push({ label: 'Target has ranged deflection', value: -2 }); + } + if (hasDeflection(targetToken, 'all')) { + additionalMods.push({ label: 'Target has deflection', value: -2 }); + } + + // Glow/Shroud + if (targetHasEffect(targetToken, 'glow')) { + additionalMods.push({ label: 'Glowing target (negate 1 point of illumination penalty)', - value: '+1', + value: 1, ignore: true, }); } - if ( - targets.some( - (target) => target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes('shroud')).length > 0, - ) - ) { - modifiers.push({ + if (targetHasEffect(targetToken, 'shroud')) { + additionalMods.push({ label: 'Shrouded target', value: '-1', ignore: false, }); } + + // Swat correction + const scaleMod = additionalMods.find((m) => m.label === 'SWADE.ScaleDifference'); + if (scaleMod && scaleMod.value < 0 && sourceToken.actor.getSingleItemBySwid('swat', 'ability')) { + const swatMod = Math.min(4, scaleMod.value * -1); + if (swatMod > 0) { + additionalMods.push({ + label: 'Using Swat attack', + value: swatMod, + ignore: true, + }); + } + } + + // 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, + }); + } + } +} + +// eslint-disable-next-line no-unused-vars +export async function preTraitRollModifiers(actor, trait, roll, modifiers, options) { + const targets = Array.from(game.user.targets); + const token = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0]?.document : null; if (targets.length === 1 && token) { const target = targets[0]; _addArcaneModifiers(target, modifiers); - _addRangeModifiers(token, target, options, modifiers); - const scaleMod = calcScaleMod(token, target); - if (scaleMod !== 0) { - modifiers.push({ label: 'Scale', value: scaleMod, ignore: false }); - } - if (target.actor.items.find((e) => e.type === 'edge' && e.system.swid === 'dodge')) { - modifiers.push({ label: 'Dodge', value: -2, ignore: true }); - } - if (trait?.type === 'skill' && trait?.system?.swid === 'fighting') { - const gangUpBonus = calcGangup(token, target); - if (gangUpBonus > 0) { - modifiers.push({ label: 'Gang Up', value: gangUpBonus, ignore: false }); - } - } } } // eslint-disable-next-line no-unused-vars export async function preDamageRollModifiers(actor, item, roll, modifiers, options) { const targets = Array.from(game.user.targets); - const token = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0] : null; + const token = game.canvas.tokens.controlled.length > 0 ? game.canvas.tokens.controlled[0].document : null; if (targets.length === 1 && token) { - const target = targets[0]; + const target = targets[0].scene.tokens.get(targets[0].id); _addArcaneModifiers(target, modifiers); const weaknesses = target.actor.items.filter( (i) => i.type === 'ability' && i.system.swid.toLowerCase().includes('environmental-weakness'), @@ -102,8 +168,8 @@ export async function preDamageRollModifiers(actor, item, roll, modifiers, optio }), ); } - if (_findItem(token.actor, 'ability', 'pack-tactics')) { - const gangupBonus = calcGangup(token, target); + 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 }); } @@ -111,38 +177,10 @@ export async function preDamageRollModifiers(actor, item, roll, modifiers, optio } } -export async function getPowerModifiers(token) { - const modifiers = []; - _addArcaneModifiers(token, modifiers); - for (const modifier of modifiers) { - modifier.value *= -1; - } - return modifiers; -} - -function _addRangeModifiers(token, target, options, modifiers) { - if (options?.item?.type !== 'weapon' || !options?.item?.system?.range.includes('/')) { - return; - } - const ranges = options.item.system.range.split('/').map((x) => parseInt(x)); - const distance = getDistance(token, target); - const rollmods = CONFIG.SWADE.prototypeRollGroups.find((g) => g.name === 'Range').modifiers; - log('ITEM RANGES:', ranges); - if (distance <= ranges[0]) { - // nothing here - } else if (ranges.length >= 2 && distance <= ranges[1]) { - modifiers.push(rollmods[0]); - } else if (ranges.length >= 3 && distance <= ranges[2]) { - modifiers.push(rollmods[1]); - } else { - modifiers.push(rollmods[2]); // extreme range - } -} - function _addArcaneModifiers(target, modifiers) { - if (_findItem(target.actor, 'edge', 'improved-arcane-resistance')) { + if (target.actor.getSingleItemBySwid('improved-arcane-resistance', 'edge')) { modifiers.push({ label: 'Arcane Resistance', value: -4, ignore: true }); - } else if (_findItem(target.actor, 'edge', 'arcane-resistance')) { + } else if (target.actor.getSingleItemBySwid('arcane-resistance', 'edge')) { modifiers.push({ label: 'Arcane Resistance', value: -2, ignore: true }); } const effect = target.actor.effects.find((e) => !e.disabled && e.name.toLowerCase().includes('arcane protection')); @@ -152,81 +190,3 @@ function _addArcaneModifiers(target, modifiers) { modifiers.push({ label: 'Target Arcane Protection', value: effectMod, ignore: true }); } } - -function getScaleDistanceMod(token) { - const scale = token.actor.system.stats.scale; - return scale > 0 ? scale / 2 : 0; -} - -function getDistance(origin, target) { - const ray = new Ray(origin, target); - const originScale = getScaleDistanceMod(origin); - const targetScale = getScaleDistanceMod(target); - const flatDistance = game.canvas.grid.measureDistances([{ ray }], { gridSpaces: true })[0]; - const elevation = Math.abs(origin.document.elevation - target.document.elevation); - const distance = Math.sqrt(elevation * elevation + flatDistance * flatDistance); - return distance - (originScale + targetScale); -} - -function withinRange(origin, target, range) { - const distance = getDistance(origin, target); - return range >= distance; -} - -function _findItem(actor, type, swid) { - return actor.items.find((i) => i.type === type && i.system.swid === swid); -} - -function calcScaleMod(attacker, target) { - const attackerScale = attacker.actor.system.stats.scale; - const targetScale = target.actor.system.stats.scale; - const attackerHasSwat = !!_findItem(attacker.actor, 'ability', 'swat'); - let modifier = targetScale - attackerScale; - if (attackerHasSwat && modifier < 0) { - modifier = Math.min(modifier + 4, 0); - } - return modifier; -} - -function calcGangup(attacker, target, debug) { - debug = typeof debug === 'undefined' ? false : debug; - const range = 1.2; - let modifier = 0; - if (_findItem(target.actor, 'edge', 'improved-block')) { - modifier = -2; - } else if (_findItem(target.actor, 'edge', 'block')) { - modifier = -1; - } - const attackerHasFormationFighter = !!_findItem(attacker.actor, 'edge', 'formation-fighter'); - - const withinRangeOfToken = game.canvas.tokens.placeables.filter( - (t) => - t.id !== attacker.id && - t.id !== target.id && - t.actor.system.status.isStunned === false && - t.visible && - withinRange(target, t, range) && - !t.actor.effects.find((c) => c.name === 'Incapacitated' || c.name === 'Defeated'), - ); - const attackerAllies = withinRangeOfToken.filter((t) => t.document.disposition === attacker.document.disposition); - const targetAllies = withinRangeOfToken.filter( - (t) => t.document.disposition === target.document.disposition && withinRange(attacker, t, range), - ); - const attackersWithFormationFighter = attackerAllies.filter((t) => !!_findItem(t.actor, 'edge', 'formation-fighter')); - const attackerCount = attackerAllies.length; - const attackerFormationBonus = - (attackerCount > 0 && attackerHasFormationFighter ? 1 : 0) + attackersWithFormationFighter.length; - const defenderCount = targetAllies.length; - const gangUp = Math.max(0, Math.min(4, attackerCount + attackerFormationBonus - defenderCount + modifier)); - if (debug) { - log('GANG UP | Attacker:', attacker); - log('GANG UP | Target:', target); - log('GANG UP | Others within range:', withinRangeOfToken); - log('GANG UP | Attacker Allies:', attackerCount); - log('GANG UP | Attacker Formation Bonus:', attackerFormationBonus); - log('GANG UP | Effective Defender Allies:', defenderCount); - log('GANG UP | Target Block Modifier:', modifier); - log('GANG UP | Total Bonus:', gangUp); - } - return gangUp; -} diff --git a/src/module/swade-mb-helpers.js b/src/module/swade-mb-helpers.js index a88718b..b25070f 100644 --- a/src/module/swade-mb-helpers.js +++ b/src/module/swade-mb-helpers.js @@ -14,7 +14,7 @@ import { SwadeVAEbuttons, updateOwnedToken, } from './helpers.js'; -import { preDamageRollModifiers, preTraitRollModifiers } from './rollHelpers.js'; +import { preAttackRollModifiers, preDamageRollModifiers, preTraitRollModifiers } from './rollHelpers.js'; import { log, moduleHelpers } from './globals.js'; import { powerEffectManagementHook, visualActiveEffectPowerButtons } from './powers/powers.js'; import { embeddedHelperHook } from './powers/basePowers.js'; @@ -53,6 +53,7 @@ function _checkModule(name) { Hooks.on('preCreateItem', embeddedHelperHook); Hooks.on('swadePreRollAttribute', preTraitRollModifiers); Hooks.on('swadePreRollSkill', preTraitRollModifiers); +Hooks.on('swadeCalculateDefaultAttackMods', preAttackRollModifiers); Hooks.on('swadeRollDamage', preDamageRollModifiers); Hooks.on('deleteActiveEffect', powerEffectManagementHook); Hooks.on('deleteToken', shapeChangeTokenDeleteHandler);