Skip to content

Commit

Permalink
Merge branch 'release/0.10.15' into main
Browse files Browse the repository at this point in the history
caewok committed Nov 2, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 35d21b5 + b7dff22 commit 87956db
Showing 7 changed files with 185 additions and 100 deletions.
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 0.10.15
Fix for multiple token dragging resulting in token moving through a wall. Closes #224.
Correct error with Pathfinding when Wall Height 6.1.0 is active. Pathfinding will now assume vaulting is enabled if Wall Height is present. Closes #223. Thanks @drcormier!
Add speed method to handle dnd5e group actor speed (air, land). Closes #221.
Correct ruler measurement in gridless scenes after waypoint is placed. Closes #220.
Fix display of other user's ruler distances. Closes #219.

# 0.10.14
Fix for NaN in the move penalty calculation when moving over regions.

44 changes: 42 additions & 2 deletions scripts/Ruler.js
Original file line number Diff line number Diff line change
@@ -297,7 +297,7 @@ function _getMeasurementSegments(wrapped) {
this.segments ??= [];
for ( const s of this.segments ) {
if ( !s.label ) continue; // Not every segment has a label.
s.label = this.labels.children[labelIndex++];
s.label = this.labels.children[labelIndex++] ?? this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
return this.segments;
}
@@ -596,7 +596,7 @@ async function _animateMovement(wrapped, token) {
const promises = [wrapped(token)];
for ( const controlledToken of canvas.tokens.controlled ) {
if ( controlledToken === token ) continue;
if ( !(this.user.isGM || this._canMove(controlledToken)) ) {
if ( !(this.user.isGM || testMovement.call(this, controlledToken)) ) {
ui.notifications.error(`${game.i18n.localize("RULER.MovementNotAllowed")} for ${controlledToken.name}`);
continue;
}
@@ -610,6 +610,23 @@ async function _animateMovement(wrapped, token) {
return Promise.allSettled(promises);
}

/**
* Helper to catch movement errors. Must use "call" or "bind" to bind it to the Ruler instance.
*/
function testMovement(token) {
let error;
try {
if ( !this._canMove(token) ) error = "RULER.MovementNotAllowed";
} catch(err) {
error = err.message;
}
if ( error ) {
ui.notifications.error(error, {localize: true});
return false;
}
return true;
}

/**
* Wrap Ruler.prototype._getMeasurementHistory
* Store the history temporarily in the token.
@@ -656,6 +673,29 @@ function _createMeasurementHistory(wrapped) {
*/
function _canMove(wrapper, token) {
if ( this.user.isGM ) return true;
if ( !wrapper(token) ) return false;

// Adjust each segment for the difference from the original token position.
// Important when dragging multiple tokens.
// See Ruler#_animateMovement
const origin = this.segments[this.history.length].ray.A;
const dx = token.document.x - origin.x;
const dy = token.document.y - origin.y;
let {x, y} = token.document._source;
for ( const segment of this.segments ) {
if ( segment.history || (segment.ray.distance === 0) ) continue;
const r = segment.ray;
const adjustedDestination = {x: Math.round(r.B.x + dx), y: Math.round(r.B.y + dy)};
const a = token.getCenterPoint({x, y});
const b = token.getCenterPoint(adjustedDestination);
if ( token.checkCollision(b, {origin: a, type: "move", mode: "any"}) ) {
throw new Error("RULER.MovementCollision");
return false;
}
x = adjustedDestination.x;
y = adjustedDestination.y;
}

return wrapper(token);
}

10 changes: 5 additions & 5 deletions scripts/const.js
Original file line number Diff line number Diff line change
@@ -25,21 +25,21 @@ export const FLAGS = {

// Track certain modules that complement features of this module.
export const OTHER_MODULES = {
TERRAIN_MAPPER: { KEY: "terrainmapper" },
TERRAIN_MAPPER: { KEY: "terrainmapper" },
LEVELS: { KEY: "levels" },
WALL_HEIGHT: { KEY: "wall-height", FLAGS: { VAULTING: "blockSightMovement" } }
}
WALL_HEIGHT: { KEY: "wall-height" }
};

// Hook init b/c game.modules is not initialized at start.
Hooks.once("init", function() {
for ( const obj of Object.values(OTHER_MODULES) ) obj.ACTIVE = game.modules.get(obj.KEY)?.active
for ( const obj of Object.values(OTHER_MODULES) ) obj.ACTIVE = game.modules.get(obj.KEY)?.active;
});

// API not necessarily available until ready hook. (Likely added at init.)
Hooks.once("ready", function() {
const tm = OTHER_MODULES.TERRAIN_MAPPER;
if ( tm.ACTIVE ) tm.API = game.modules.get(tm.KEY).api;
})
});


export const MOVEMENT_TYPES = {
12 changes: 7 additions & 5 deletions scripts/measurement/Grid.js
Original file line number Diff line number Diff line change
@@ -27,12 +27,13 @@ PATCHES_HexagonalGrid.BASIC = {};
* Wrap GridlessGrid#getDirectPath
* Returns the sequence of grid offsets of a shortest, direct path passing through the given waypoints.
* @param {RegionMovementWaypoint3d|GridCoordinates3d[]} waypoints The waypoints the path must pass through
* @returns {GridOffset[]} The sequence of grid offsets of a shortest, direct path
* @returns {GridCoordinates|GridCoordinates3d[]} The sequence of grid offsets of a shortest, direct path
* @abstract
*/
function getDirectPathGridless(wrapped, waypoints) {
const GridCoordinates = CONFIG.GeometryLib.GridCoordinates;
const offsets2d = wrapped(waypoints);
if ( !(waypoints[0] instanceof CONFIG.GeometryLib.threeD.Point3d) ) return offsets2d;
if ( !(waypoints[0] instanceof CONFIG.GeometryLib.threeD.Point3d) ) return offsets2d.map(o => GridCoordinates.fromOffset(o));

// 1-to-1 relationship between the waypoints and the offsets2d for gridless.
const GridCoordinates3d = CONFIG.GeometryLib.threeD.GridCoordinates3d;
@@ -48,13 +49,14 @@ function getDirectPathGridless(wrapped, waypoints) {
* Wrap HexagonalGrid#getDirectPath and SquareGrid#getDirectPath
* Returns the sequence of grid offsets of a shortest, direct path passing through the given waypoints.
* @param {Point3d[]} waypoints The waypoints the path must pass through
* @returns {GridOffset[]} The sequence of grid offsets of a shortest, direct path
* @returns {GridCoordinates|GridCoordinates3d[]} The sequence of grid offsets of a shortest, direct path
* @abstract
*/
function getDirectPathGridded(wrapped, waypoints) {
const { HexGridCoordinates3d, GridCoordinates3d } = CONFIG.GeometryLib.threeD;
const { HexGridCoordinates3d, GridCoordinates3d, Point3d } = CONFIG.GeometryLib.threeD;
const GridCoordinates = CONFIG.GeometryLib.GridCoordinates;
if ( !(waypoints[0] instanceof Point3d) ) return wrapped(waypoints).map(o => GridCoordinates.fromObject(o));

if ( !(waypoints[0] instanceof CONFIG.GeometryLib.threeD.Point3d) ) return wrapped(waypoints);
let prevWaypoint = GridCoordinates3d.fromObject(waypoints[0]);
const path3d = [];
const path3dFn = canvas.grid.isHexagonal ? HexGridCoordinates3d._directPathHex : GridCoordinates3d._directPathSquare;
9 changes: 6 additions & 3 deletions scripts/measurement/MovePenalty.js
Original file line number Diff line number Diff line change
@@ -283,7 +283,7 @@ export class MovePenalty {
if ( this.#penaltyCache.has(key) ) {
const res = this.#penaltyCache.get(key);
log(`Using key ${key}: ${res}`);
console.groupEnd("movementCostForSegment");
if ( CONFIG[MODULE_ID].debug ) console.groupEnd("movementCostForSegment");
return res;
}

@@ -294,7 +294,10 @@ export class MovePenalty {
const isOneStep = Math.abs(endCoords.i - startCoords.i) < 2
&& Math.abs(endCoords.j - startCoords.j) < 2
&& Math.abs(endCoords.k - startCoords.k) < 2;
if ( isOneStep ) return this.movementCostForGridSpace(endCoords, costFreeDistance);
if ( isOneStep ) {
if ( CONFIG[MODULE_ID].debug ) console.groupEnd("movementCostForSegment");
return this.movementCostForGridSpace(endCoords, costFreeDistance);
}

// Unlikely scenario where endCoords are more than 1 step away from startCoords.
let totalCost = 0;
@@ -316,7 +319,7 @@ export class MovePenalty {
this.#penaltyCache.set(key, res);
const t1 = performance.now();
log(`Found cost ${res} in ${Math.round(t1 - t0)} ms`);
console.groupEnd("movementCostForSegment");
if ( CONFIG[MODULE_ID].debug ) console.groupEnd("movementCostForSegment");
return res;
}

4 changes: 2 additions & 2 deletions scripts/pathfinding/WallTracer.js
Original file line number Diff line number Diff line change
@@ -383,7 +383,7 @@ export class WallTracerEdge extends GraphEdge {

// If Wall Height vaulting is enabled, walls less than token vision height do not block.
const wh = OTHER_MODULES.WALL_HEIGHT;
if ( wh.ACTIVE && game.settings.get(wh.KEY, wh.FLAGS.VAULTING) && moveToken.visionZ >= wall.topZ ) return false;
if ( wh.ACTIVE && moveToken.visionZ >= wall.topZ ) return false;
return true;
}

@@ -408,7 +408,7 @@ export class WallTracerEdge extends GraphEdge {
// Don't block dead tokens (HP <= 0).
const { tokenHPAttribute, pathfindingIgnoreStatuses } = CONFIG[MODULE_ID];
let tokenHP = Number(foundry.utils.getProperty(token, tokenHPAttribute));

//DemonLord using damage system
if ( game.system.id === 'demonlord')
{
199 changes: 116 additions & 83 deletions scripts/system_attributes.js
Original file line number Diff line number Diff line change
@@ -71,73 +71,80 @@ Hooks.once("init", function() {
* @returns {string}
*/
export function defaultHPAttribute() {
/* eslint-disable no-multi-spaces */
switch ( game.system.id ) {
case "dnd5e": return "actor.system.attributes.hp.value";
case "dragonbane": return "actor.system.hitpoints.value";
case "twodsix": return "actor.system.hits.value";
case "ars": return "actor.system.attributes.hp.value";
case "a5e": return "actor.system.attributes.hp.value";
case "TheWitcherTRPG": return "actor.system.derivedStats.hp.value";
case "gurps": return "actor.system.HP.value";
default: return "actor.system.attributes.hp.value";
case "dnd5e": return "actor.system.attributes.hp.value";
case "dragonbane": return "actor.system.hitpoints.value";
case "twodsix": return "actor.system.hits.value";
case "ars": return "actor.system.attributes.hp.value";
case "a5e": return "actor.system.attributes.hp.value";
case "TheWitcherTRPG": return "actor.system.derivedStats.hp.value";
case "gurps": return "actor.system.HP.value";
default: return "actor.system.attributes.hp.value";
}
/* eslint-enable no-multi-spaces */
}

/**
* Location of the walk attribute for a given system's actor.
* @returns {string}
*/
export function defaultWalkAttribute() {
/* eslint-disable no-multi-spaces */
switch ( game.system.id ) {
case "a5e": return "actor.system.attributes.movement.walk.distance";
case "ars": return "actor.movement";
case "CoC7": return "actor.system.attribs.mov.value";
case "dcc": return "actor.system.attributes.speed.value";
case "sfrpg": return "actor.system.attributes.speed.value";
case "dnd4e": return "actor.system.movement.walk.value";
case "dnd5e": return "actor.system.attributes.movement.walk";
case "lancer": return "actor.system.speed";
case "gurps": return "actor.system.currentmove";
case "a5e": return "actor.system.attributes.movement.walk.distance";
case "ars": return "actor.movement";
case "CoC7": return "actor.system.attribs.mov.value";
case "dcc": return "actor.system.attributes.speed.value";
case "sfrpg": return "actor.system.attributes.speed.value";
case "dnd4e": return "actor.system.movement.walk.value";
case "dnd5e": return "actor.system.attributes.movement.walk";
case "lancer": return "actor.system.speed";
case "gurps": return "actor.system.currentmove";
case "pf1":
case "D35E": return "actor.system.attributes.speed.land.total";
case "shadowrun5e": return "actor.system.movement.walk.value";
case "swade": return "actor.system.stats.speed.adjusted";
case "ds4": return "actor.system.combatValues.movement.total";
case "splittermond": return "actor.derivedValues.speed.value";
case "wfrp4e": return "actor.system.details.move.walk";
case "crucible": return "actor.system.movement.stride";
case "dragonbane": return "actor.system.movement.value";
case "twodsix": return "actor.system.movement.walk";
case "D35E": return "actor.system.attributes.speed.land.total";
case "shadowrun5e": return "actor.system.movement.walk.value";
case "swade": return "actor.system.stats.speed.adjusted";
case "ds4": return "actor.system.combatValues.movement.total";
case "splittermond": return "actor.derivedValues.speed.value";
case "wfrp4e": return "actor.system.details.move.walk";
case "crucible": return "actor.system.movement.stride";
case "dragonbane": return "actor.system.movement.value";
case "twodsix": return "actor.system.movement.walk";
case "worldofdarkness": return "actor.system.movement.walk";
case "TheWitcherTRPG": return "actor.system.stats.spd.current";
case "demonlord": return "actor.system.characteristics.speed";
default: return "";
case "TheWitcherTRPG": return "actor.system.stats.spd.current";
case "demonlord": return "actor.system.characteristics.speed";
default: return "";
}
/* eslint-enable no-multi-spaces */
}

/**
* Location of the flying attribute for a given system's actor.
* @returns {string}
*/
export function defaultFlyAttribute() {
/* eslint-disable no-multi-spaces */
switch ( game.system.id ) {
case "a5e": return "actor.system.attributes.movement.fly.distance";
case "sfrpg": return "actor.system.attributes.flying.value";
case "dnd5e": return "actor.system.attributes.movement.fly";
case "a5e": return "actor.system.attributes.movement.fly.distance";
case "sfrpg": return "actor.system.attributes.flying.value";
case "dnd5e": return "actor.system.attributes.movement.fly";
case "pf1":
case "D35E": return "actor.system.attributes.speed.fly.total";
case "twodsix": return "actor.system.movement.fly";
case "D35E": return "actor.system.attributes.speed.fly.total";
case "twodsix": return "actor.system.movement.fly";
case "worldofdarkness": return "actor.system.movement.fly";
case "gurps": return "actor.system.currentflight";
default: return "";
case "gurps": return "actor.system.currentflight";
default: return "";
}
/* eslint-enable no-multi-spaces */
}

/**
* Location of the burrow attribute for a given system's actor.
* @returns {string}
*/
export function defaultBurrowAttribute() {
/* eslint-disable no-multi-spaces */
switch ( game.system.id ) {
case "a5e": return "actor.system.attributes.movement.burrow.distance";
case "sfrpg": return "actor.system.attributes.burrowing.value";
@@ -147,13 +154,15 @@ export function defaultBurrowAttribute() {
case "twodsix": return "actor.system.movement.burrow";
default: return "";
}
/* eslint-enable no-multi-spaces */
}

/**
* How much faster is dashing than walking for a given system?
* @returns {number}
*/
export function defaultDashMultiplier() {
/* eslint-disable no-multi-spaces */
switch ( game.system.id ) {
case "dcc":
case "dnd4e":
@@ -167,18 +176,18 @@ export function defaultDashMultiplier() {
case "twodsix":
case "a5e":
case "demonlord":
case "ds4": return 2;

case "CoC7": return 5;
case "splittermond": return 3;
case "wfrp4e": return 2;
case "gurps": return 1.2;
case "ds4": return 2;
case "CoC7": return 5;
case "splittermond": return 3;
case "wfrp4e": return 2;
case "gurps": return 1.2;

case "crucible":
case "swade": return 0;
case "TheWitcherTRPG": return 3;
default: return 0;
case "swade": return 0;
case "TheWitcherTRPG": return 3;
default: return 0;
}
/* eslint-enable no-multi-spaces */
}

// ----- Specialized move categories by system ----- //
@@ -191,12 +200,12 @@ function a5eSpeedCategories() {
name: "Bonus Dash",
color: Color.from(0xf77926),
multiplier: 4
}
};
SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, BonusDashCategory, MaximumSpeedCategory];
}

/**
* sfrpg
* SFRPG
*/
function sfrpgSpeedCategories() {
WalkSpeedCategory.name = "sfrpg.speeds.walk";
@@ -205,44 +214,44 @@ function sfrpgSpeedCategories() {
name: "sfrpg.speeds.run",
color: Color.from(0xff8000),
multiplier: 4
}
};
SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, RunSpeedCategory, MaximumSpeedCategory];
}

/**
* pf2e
* PF2E
* See https://github.com/7H3LaughingMan/pf2e-elevation-ruler/blob/main/scripts/module.js
*/
function pf2eSpeedCategories() {
const SingleAction = {
name: "Single Action",
color: Color.from("#3222C7"),
multiplier: 1
}
name: "Single Action",
color: Color.from("#3222C7"),
multiplier: 1
};

const DoubleAction = {
name: "Double Action",
color: Color.from("#FFEC07"),
multiplier: 2
}
name: "Double Action",
color: Color.from("#FFEC07"),
multiplier: 2
};

const TripleAction = {
name: "Triple Action",
color: Color.from("#C033E0"),
multiplier: 3
}
name: "Triple Action",
color: Color.from("#C033E0"),
multiplier: 3
};

const QuadrupleAction = {
name: "Quadruple Action",
color: Color.from("#1BCAD8"),
multiplier: 4
}
name: "Quadruple Action",
color: Color.from("#1BCAD8"),
multiplier: 4
};

const Unreachable = {
name: "Unreachable",
color: Color.from("#FF0000"),
multiplier: Number.POSITIVE_INFINITY
}
name: "Unreachable",
color: Color.from("#FF0000"),
multiplier: Number.POSITIVE_INFINITY
};

SPEED.CATEGORIES = [SingleAction, DoubleAction, TripleAction, QuadrupleAction, Unreachable];
}
@@ -256,6 +265,29 @@ const SPECIALIZED_SPEED_CATEGORIES = {

// ----- Specialized token speed by system ----- //

/**
* dnd5e
* Handles group tokens.
* @param {Token} token Token whose speed is required
* @param {MOVEMENT_TYPES} [movementType] Type of movement; if omitted automatically determined
* @returns {number|null} Distance, in grid units. Null if no speed provided for that category.
* (Null will disable speed highlighting.)
*/
function dnd5eTokenSpeed(token, movementType) {
movementType ??= token.movementType;
let speed = null;
switch ( token.actor?.type ) {
case "group": {
if ( movementType === MOVEMENT_TYPES.WALK ) speed = foundry.utils.getProperty(token, "actor.system.attributes.movement.land");
else if ( movementType === MOVEMENT_TYPES.FLY ) speed = foundry.utils.getProperty(token, "actor.system.attributes.movement.air");
break;
}
default: speed = foundry.utils.getProperty(token, SPEED.ATTRIBUTES[keyForValue(MOVEMENT_TYPES, movementType)]);
}
if ( speed == null ) return null;
return Number(speed);
}

/**
* sfrpg
* Given a token, retrieve its base speed.
@@ -276,7 +308,7 @@ function sfrpgTokenSpeed(token, movementType) {
}

/**
* pf2e
* PF2E
* See https://github.com/7H3LaughingMan/pf2e-elevation-ruler/blob/main/scripts/module.js
* Finds walk, fly, burrow values.
* @param {Token} token Token whose speed is required
@@ -291,21 +323,22 @@ function pf2eTokenSpeed(token, movementType) {
switch (movementType) {
case MOVEMENT_TYPES.WALK: speed = tokenSpeed.total; break;
case MOVEMENT_TYPES.FLY: {
const flySpeed = tokenSpeed.otherSpeeds.find(x => x.type == "fly");
const flySpeed = tokenSpeed.otherSpeeds.find(x => x.type === "fly");
if ( typeof flySpeed !== "undefined" ) speed = flySpeed.total;
break;
}
case MOVEMENT_TYPES.BURROW: {
const burrowSpeed = tokenSpeed.otherSpeeds.find(x => x.type == "burrow");
const burrowSpeed = tokenSpeed.otherSpeeds.find(x => x.type === "burrow");
if ( typeof burrowSpeed !== "undefined" ) speed = burrowSpeed.total;
break;
}
};
}
if (speed === null) return null;
return Number(speed);
}

const SPECIALIZED_TOKEN_SPEED = {
dnd5e: dnd5eTokenSpeed,
sfrpg: sfrpgTokenSpeed,
pf2e: pf2eTokenSpeed
};
@@ -410,7 +443,7 @@ function getActionCount(token) {
// Check to see if there is an encounter, if that encounter is active, and if the token is in that encounter
if ( game.combat == null
|| !game.combat.active
|| (game.combat.turns.find(x => x.tokenId == token.id) == null) ) return maxActions;
|| (game.combat.turns.find(x => x.tokenId == token.id) == null) ) return maxActions; // eslint-disable-line eqeqeq

// Check to see if the actor is stunned or slowed, and if so the value
const stunned = actor.getCondition("stunned")?.value ?? 0;
@@ -420,15 +453,15 @@ function getActionCount(token) {
// Check to see if PF2e Workbench is active and if Auto Reduce Stunned is enabled
let reduction = 0;
if ( game.modules.get("xdy-pf2e-workbench")?.active && game.settings.get("xdy-pf2e-workbench", "autoReduceStunned") ) {
const stunReduction = actor.getFlag("xdy-pf2e-workbench", "stunReduction");

// Make sure we actually got something and the combat matches.
if ( stunReduction && stunReduction.combat == game.combat.id ) {
// We are going to check to see if the combatant's last round matches the stun reduction round
// Note - A combatant's last round is updated at the start of their turn
const combatant = game.combat.turns.find(x => x.tokenId == token.id);
if ( combatant && combatant.roundOfLastTurn == stunReduction.round ) reduction = stunReduction.reducedBy;
}
const stunReduction = actor.getFlag("xdy-pf2e-workbench", "stunReduction");

// Make sure we actually got something and the combat matches.
if ( stunReduction && stunReduction.combat === game.combat.id ) {
// We are going to check to see if the combatant's last round matches the stun reduction round
// Note - A combatant's last round is updated at the start of their turn
const combatant = game.combat.turns.find(x => x.tokenId === token.id);
if ( combatant && combatant.roundOfLastTurn === stunReduction.round ) reduction = stunReduction.reducedBy;
}
}

// Return the token's maximum number of actions minus the greater of their stunned, slowed, or stun reduction.

0 comments on commit 87956db

Please sign in to comment.