update roll helpers for swade 5.1.0
This commit is contained in:
parent
694a09d603
commit
56233e7c10
20
CHANGELOG.md
20
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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user