Skip to content

Commit

Permalink
Merge pull request #67 from kaelad02/grantsCriticalRange
Browse files Browse the repository at this point in the history
Support the grants.critical.range flag
  • Loading branch information
kaelad02 authored Apr 6, 2024
2 parents 0d82189 + ca3b942 commit ce17f30
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 9 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# upcoming

- feature: Support the system's new concentration rolls with sources and messages (system already handles advantage/disadvantage)
- feature: Support the system's new concentration rolls with sources and messages with `flags.adv-reminder.message.ability.concentration` (system already handles advantage/disadvantage)
- feature: Support `flags.midi-qol.grants.critical.range` for conditions like Paralyzed that turn hits into crits if the attacker is adjacent. Does not currently work with Ready Set Roll, will just be ignored.

# 3.4.1

Expand Down
23 changes: 21 additions & 2 deletions src/reminders.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,28 @@ export class DeathSaveReminder extends AbilityBaseReminder {
}

export class CriticalReminder extends BaseReminder {
constructor(actor, targetActor, item) {
constructor(actor, targetActor, item, distanceFn) {
super(actor);

/** @type {object} */
this.targetFlags = this._getFlags(targetActor);
/** @type {string} */
this.actionType = item.system.actionType;

// get the Range directly from the actor's flags
if (targetActor) {
const grantsCriticalRange =
getProperty(targetActor, "flags.midi-qol.grants.critical.range") || -Infinity;
this._adjustRange(distanceFn, grantsCriticalRange);
}
}

_adjustRange(distanceFn, grantsCriticalRange) {
// adjust the Range flag to look like a boolean like the rest
if ("grants.critical.range" in this.targetFlags) {
const distance = distanceFn();
this.targetFlags["grants.critical.range"] = distance <= grantsCriticalRange;
}
}

updateOptions(options, critProp = "critical") {
Expand All @@ -269,7 +284,11 @@ export class CriticalReminder extends BaseReminder {
// build the active effect keys applicable for this roll
const critKeys = ["critical.all", `critical.${this.actionType}`];
const normalKeys = ["noCritical.all", `noCritical.${this.actionType}`];
const grantsCritKeys = ["grants.critical.all", `grants.critical.${this.actionType}`];
const grantsCritKeys = [
"grants.critical.all",
`grants.critical.${this.actionType}`,
"grants.critical.range",
];
const grantsNormalKeys = ["fail.critical.all", `fail.critical.${this.actionType}`];

// find matching keys and update options
Expand Down
7 changes: 4 additions & 3 deletions src/rollers/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
SkillSource,
} from "../sources.js";
import { showSources } from "../settings.js";
import { debug, getTarget } from "../util.js";
import { debug, getDistanceToTargetFn, getTarget } from "../util.js";

/**
* Setup the dnd5e.preRoll hooks for use with the core roller.
Expand Down Expand Up @@ -146,10 +146,11 @@ export default class CoreRollerHooks {

if (this.isFastForwarding(config)) return;
const target = getTarget();
const distanceFn = getDistanceToTargetFn(config.messageData.speaker);

new DamageMessage(item.actor, target, item).addMessage(config);
if (showSources) new CriticalSource(item.actor, target, item).updateOptions(config);
new CriticalReminder(item.actor, target, item).updateOptions(config);
if (showSources) new CriticalSource(item.actor, target, item, distanceFn).updateOptions(config);
new CriticalReminder(item.actor, target, item, distanceFn).updateOptions(config);
}

/**
Expand Down
9 changes: 8 additions & 1 deletion src/rollers/midi.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,15 @@ export default class MidiRollerHooks extends CoreRollerHooks {

if (this.isFastForwarding(config)) return;
const target = getTarget();
// use distance from Midi's Workflow
const distanceFn = () => {
const workflow = MidiQOL.Workflow.getWorkflow(item.uuid);
if (!workflow) return Infinity;
const firstTarget = workflow.hitTargets.values().next().value;
return MidiQOL.computeDistance(firstTarget, workflow.token, false);
}

new DamageMessage(item.actor, target, item).addMessage(config);
if (showSources) new CriticalSource(item.actor, target, item).updateOptions(config);
if (showSources) new CriticalSource(item.actor, target, item, distanceFn).updateOptions(config);
}
}
8 changes: 6 additions & 2 deletions src/rollers/rsr.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import { showSources } from "../settings.js";
import { debug, getTarget } from "../util.js";
import CoreRollerHooks from "./core.js";

// disable the grants.critical.range flag since RSR can't have it's critical flag changed anyways,
// only set by the attack roll
const distanceFn = () => Infinity;

/**
* Setup the dnd5e.preRoll hooks for use with Ready Set Roll.
*/
Expand Down Expand Up @@ -137,7 +141,7 @@ export default class ReadySetRollHooks extends CoreRollerHooks {

if (this._doMessages(config)) {
new DamageMessage(item.actor, target, item).addMessage(config);
if (showSources) new CriticalSource(item.actor, target, item).updateOptions(config);
if (showSources) new CriticalSource(item.actor, target, item, distanceFn).updateOptions(config);
}
// don't use CriticalReminder here, it's done in another hook
}
Expand All @@ -147,7 +151,7 @@ export default class ReadySetRollHooks extends CoreRollerHooks {

// check for critical hits but set the "isCrit" property instead of the default "critical"
const target = getTarget();
new CriticalReminder(item.actor, target, item).updateOptions(config, "isCrit");
new CriticalReminder(item.actor, target, item, distanceFn).updateOptions(config, "isCrit");
}

_doMessages({ fastForward = false }) {
Expand Down
8 changes: 8 additions & 0 deletions src/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ export class SkillSource extends SourceMixin(SkillReminder) {}
export class DeathSaveSource extends SourceMixin(DeathSaveReminder) {}

export class CriticalSource extends SourceMixin(CriticalReminder) {
_adjustRange(distanceFn, grantsCriticalRange) {
// check if the range applies, remove flag if not
if ("grants.critical.range" in this.targetFlags) {
const distance = distanceFn();
if (distance > grantsCriticalRange) delete this.targetFlags["grants.critical.range"];
}
}

_accumulator() {
const criticalLabels = [];
const normalLabels = [];
Expand Down
41 changes: 41 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,44 @@ export function getTarget() {
export function isEmpty(obj) {
return !Object.keys(obj).length;
}

export function getDistanceToTargetFn(speaker) {
return () => {
const controlledTokenDoc = game.scenes.get(speaker.scene).tokens.get(speaker.token);
const targetTokenDoc = game.user.targets.first()?.document;
if (!controlledTokenDoc || !targetTokenDoc) return Infinity;

// make rays from each controlled grid space to targeted grid space
const controlledSpaces = _getAllTokenGridSpaces(controlledTokenDoc);
const targetSpaces = _getAllTokenGridSpaces(targetTokenDoc);
const rays = controlledSpaces.flatMap((c) => targetSpaces.map((t) => ({ ray: new Ray(c, t) })));

// measure the horizontal distance: shortest distance between the two tokens' squares
const dist = canvas.scene.grid.distance;
const distances = canvas.grid
.measureDistances(rays, { gridSpaces: true })
.map((d) => Math.round(d / dist) * dist);
const horizDistance = Math.min(distances);

// compute vertical distance: diff in elevation
const verticalDistance = Math.abs(controlledTokenDoc.elevation - targetTokenDoc.elevation);
return Math.max(horizDistance, verticalDistance);
};
}

function _getAllTokenGridSpaces({ width, height, x, y }) {
// if the token is in a single space, just return that
if (width <= 1 && height <= 1) return [{ x, y }];
// create a position for each grid square it takes up
const grid = canvas.grid.size;
const centers = [];
for (let w = 0; w < width; w++) {
for (let h = 0; h < height; h++) {
centers.push({
x: x + w * grid,
y: y + h * grid,
});
}
}
return centers;
}

0 comments on commit ce17f30

Please sign in to comment.