import { log } from './globals.js'; 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 }); } if ( targets.some( (target) => target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes('deflection')).length > 0, ) ) { modifiers.push({ label: 'Target has Deflection', value: '-2', ignore: false }); } if ( targets.some( (target) => target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes('glow')).length > 0, ) ) { modifiers.push({ label: 'Glowing target (negate 1 point of illumination penalty)', value: '+1', ignore: true, }); } if ( targets.some( (target) => target.actor.effects.filter((e) => !e.disabled && e.name.toLowerCase().includes('shroud')).length > 0, ) ) { modifiers.push({ label: 'Shrouded target', value: '-1', ignore: false, }); } 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; if (targets.length === 1 && token) { const target = targets[0]; _addArcaneModifiers(target, modifiers); const weaknesses = target.actor.items.filter( (i) => i.type === 'ability' && i.system.swid.toLowerCase().includes('environmental-weakness'), ); if (weaknesses.length > 0) { modifiers.push( ...weaknesses.map((i) => { return { label: i.name, value: '+4', ignore: true }; }), ); } const resistances = target.actor.items.filter( (i) => i.type === 'ability' && i.system.swid.toLowerCase().includes('environmental-resistance'), ); if (resistances.length > 0) { modifiers.push( ...resistances.map((i) => { return { label: i.name, value: '-4', ignore: true }; }), ); } if (_findItem(token.actor, 'ability', 'pack-tactics')) { const gangupBonus = calcGangup(token, target); if (gangupBonus > 0) { modifiers.push({ label: 'Gang Up (Pack Tactics)', value: gangupBonus, ignore: false }); } } } } 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')) { modifiers.push({ label: 'Arcane Resistance', value: -4, ignore: true }); } else if (_findItem(target.actor, 'edge', 'arcane-resistance')) { modifiers.push({ label: 'Arcane Resistance', value: -2, ignore: true }); } const effect = target.actor.effects.find((e) => !e.disabled && e.name.toLowerCase().includes('arcane protection')); if (effect) { const effectName = effect.name.toLowerCase(); const effectMod = -2 + (effectName.includes('major') ? -2 : 0) + (effectName.includes('greater') ? -2 : 0); 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; }