update roll helpers for swade 5.1.0

This commit is contained in:
Mike Bloy 2025-12-07 23:09:36 -06:00
parent 694a09d603
commit 56233e7c10
3 changed files with 142 additions and 161 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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);