Skip to content

Commit

Permalink
Merge branch 'release/0.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
caewok committed May 20, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents b24122b + 8d1c30a commit 4b5ef4d
Showing 19 changed files with 843 additions and 143 deletions.
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.3.0
Added settings to autotarget tokens from templates.
- Option to add a toggle to template controls.
- Target tokens by whether their center-point falls within the template.
- Target tokens by whether a percentage of their area overlaps the template
Localization improvements for settings.

## 0.2.5
Improve compatibility with Pathfinder 2e. Fixes [issue #4](https://github.com/caewok/fvtt-walled-templates/issues/4)

19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -4,10 +4,12 @@

# Walled Templates

This module lets you toggle measured templates so that they can be blocked by walls. For example, if in your game system, walls block *fireball*, you can use this module to determine the extent of the fireball given one or more walls.
This module lets you toggle measured templates so that they can be blocked by walls. For example, if in your game system, walls block *fireball*, you can use this module to determine the extent of the fireball given one or more walls.

This could be used in combination with other modules or macros that automatically target tokens based on measured templates, such as [midi-qol](https://foundryvtt.com/packages/midi-qol/) or [DragonFlagon Quality of Life](https://foundryvtt.com/packages/df-qol).

As of v0.3.0, you also have the option to enable autotargeting of tokens in templates.

# Installation

Add this [Manifest URL](https://github.com/caewok/fvtt-walled-templates/releases/latest/download/module.json) in Foundry to install.
@@ -16,10 +18,10 @@ Add this [Manifest URL](https://github.com/caewok/fvtt-walled-templates/releases
- [libWrapper](https://github.com/ruipin/fvtt-lib-wrapper)

## Known conflicts
None.
- [DF Template Enhancements](https://foundryvtt.com/packages/df-templates) (See [issue #5](https://github.com/caewok/fvtt-walled-templates/issues/5))

# Usage
When you add a template to the canvas, double click the template drag handle to open the template configuration. Select "Blocked by Walls" to enable for the given template.
When you add a template to the canvas, double click the template drag handle to open the template configuration. Select "Blocked by Walls" to enable for the given template.

<img src="https://raw.githubusercontent.com/caewok/fvtt-walled-templates/feature/screenshots/screenshots/template_config.jpg" width="400" alt="Screenshot of template configuration for Walled Templates: 'Blocked by Walls' selected">

@@ -32,7 +34,7 @@ To make "Blocked by Walls" the default for all templates, select "Default to Wal
For the dnd5e system, this module adds a checkbox to spell detail templates that overrides the world default, so you can indicate on a per-spell basis whether walls should block.

## Macros and advanced usage
This module adds a flag to template objects, `flags.walledtemplates.enabled: true` or `flags.walledtemplates.enabled: false`, to indicate if walls should block a given template. Templates without the flag will use the world default.
This module adds a flag to template objects, `flags.walledtemplates.enabled: true` or `flags.walledtemplates.enabled: false`, to indicate if walls should block a given template. Templates without the flag will use the world default.

## Circle

@@ -57,3 +59,12 @@ And here is a comparable circle template on a gridded scene:
## Ray

<img src="https://raw.githubusercontent.com/caewok/fvtt-walled-templates/feature/screenshots/screenshots/ray_gridless.jpg" width="400" alt="Gridless ray template screenshot">

# Autotargeting

Walled Templates v0.3.0 adds settings to autotarget tokens touched by the template. These settings work with or without enabling "Blocked by Walls". Options include:
1. Disable autotargeting completely.
2. Add a toggle switch to the template controls to enable/disable autotargeting.
3. Enable autotargeting everywhere.

Several settings specified at the "world" level allow you to specify rules for autotargeting. The default targets tokens if their centerpoint is under the template. Alternatively, you can specify a percentage of the token area that must be covered by the template to be considered a target. Gridless or squares use the rectangular (generally, square) hit area for the token area and overlap. Hex grids use the hexagon hit area for the token area and overlap.
22 changes: 21 additions & 1 deletion languages/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
{
"walledtemplates.MeasuredTemplateConfiguration.LegendTitle": "Walled Templates",
"walledtemplates.MeasuredTemplateConfiguration.enabled.Name": "Blocked by Walls",
"walledtemplates.dnd5eSpellTemplateConfiguration.enabled.Name": "Blocked by Walls"
"walledtemplates.dnd5eSpellTemplateConfiguration.enabled.Name": "Blocked by Walls",

"walledtemplates.settings.default-to-walled.Name": "Default to Walled Measured Templates",
"walledtemplates.settings.default-to-walled.Hint": "If set, newly-created templates will default to being walled. Each template configuration has a toggle to turn walled on/off.",

"walledtemplates.settings.autotarget-menu.Name": "Enable autotargeting",
"walledtemplates.settings.autotarget-menu.Hint": "When toggle option is selected, a bullseye control button in the template control bar will allow you to switch between targeting or not targeting.",
"walledtemplates.settings.autotarget-menu.Choice.No": "Do not autotarget",
"walledtemplates.settings.autotarget-menu.Choice.Toggle_Off": "Display toggle button; default off",
"walledtemplates.settings.autotarget-menu.Choice.Toggle_On": "Display toggle button; default on",
"walledtemplates.settings.autotarget-menu.Choice.Yes": "Always autotarget",

"walledtemplates.settings.autotarget-method.Name": "Autotargeting method",
"walledtemplates.settings.autotarget-method.Hint": "Method to auto-target tokens with templates",
"walledtemplates.settings.autotarget-method.Method.Center": "Token center inside template",
"walledtemplates.settings.autotarget-method.Method.Overlap": "Token area overlaps template",

"walledtemplates.settings.autotarget-area.Name": "Autotargeting area",
"walledtemplates.settings.autotarget-area.Hint": "For overlap autotarget method only. The percent of the token area that must overlap the template to count as a target. 0 means any overlap counts.",

"walledtemplates.controls.autotarget.Title": "Autotarget tokens with template"
}
21 changes: 21 additions & 0 deletions scripts/ClockwiseSweep/IntersectionsBrute.js
Original file line number Diff line number Diff line change
@@ -64,3 +64,24 @@ export function findIntersectionsBruteRedBlack(red, black, reportFn = (_s1, _s2)
}
}
}

/**
* Determine if at least one segment from black intersects one segment from red.
* @param {Segments[]} red Array of objects that contain points A.x, A.y, B.x, B.y.
* @param {Segments[]} black Array of objects that contain points A.x, A.y, B.x, B.y.
* @return {Boolean}
*/
export function hasIntersectionBruteRedBlack(red, black) {
const ln1 = red.length;
const ln2 = black.length;
if (!ln1 || !ln2) { return; }

for (let i = 0; i < ln1; i += 1) {
const si = red[i];
for (let j = 0; j < ln2; j += 1) {
const sj = black[j];
if ( foundry.utils.lineSegmentIntersects(si.A, si.B, sj.A, sj.B) ) return true;
}
}
return false;
}
111 changes: 18 additions & 93 deletions scripts/ClockwiseSweep/IntersectionsSort.js
Original file line number Diff line number Diff line change
@@ -33,67 +33,22 @@ identifying the endpoints.
* segment objects that intersect.
*/
export function findIntersectionsSortSingle(segments, reportFn = (_s1, _s2) => {}) {
segments.sort((a, b) => compareXYInt(a.nw, b.nw));
const ln = segments.length;
if (!ln) { return; }

// In a single pass through the array, build an array of endpoint objects.
// Each object contains an endpoint, a link to the underlying segment, and a boolean
// Indicator for whether it is the nw or se endpoint.
// Sort the new array by the x values, breaking ties by sorting the se point first.
// (it is fine if two segments are otherwise equivalent in the sort)

const endpoints = [];
for (let i = 0; i < ln; i += 1) {
const s = segments[i];
endpoints.push({e: s.nw, s, se: -1},
{e: s.se, s, se: 1}); // eslint-disable-line indent
}
endpoints.sort(sortEndpoints);

const ln2 = endpoints.length;
for (let i = 0; i < ln2; i += 1) {
const endpoint1 = endpoints[i];
if (~endpoint1.se) continue; // Avoid duplicating the check

// Starting j is always i + 1 b/c any segment with an se endpoint after si
// would be after si or already processed b/c its ne endpoint was before.
const start_j = i + 1;
const si = endpoint1.s;
for (let j = start_j; j < ln2; j += 1) {
const endpoint2 = endpoints[j];

if (endpoint2.e.x > si.se.x) break; // Segments past here are entirely right of si
if (~endpoint2.se) continue;

const sj = endpoint2.s;
for ( let i = 0; i < ln; i++ ) {
const si = segments[i];
for ( let j = i + 1; j < ln; j++ ) {
const sj = segments[j];
if ( sj.nw.x > si.se.x ) break; // The sj segments are all entirely to the right of si
foundry.utils.lineSegmentIntersects(si.A, si.B, sj.A, sj.B) && reportFn(si, sj); // eslint-disable-line no-unused-expressions
}
}
}

/**
* Comparison function for SortSingle
* Sort each endpoint object by the endpoint x coordinate then sort se first.
* @param {Object} e1 Endpoint object containing:
* e (endpoint), s (segment), and se (boolean)
* @param {Object} e2 Endpoint object containing:
* e (endpoint), s (segment), and se (boolean)
* @return {Number} Number indicating whether to sort e1 before e2 or vice-versa.
* > 0: sort e2 before e1
* < 0: sort e1 before e2
*/
function sortEndpoints(e1, e2) {
return e1.e.x - e2.e.x
|| e2.se - e1.se;

// If e1.se then we want e1 first or they are equal. So return -
// If e2.se then we want e2 first or they are equal. So return +
// e2.se - e1.se
// e1.se: -1, e2.se: 1. 1 - - 1 = 2; e2 first
// e1.se: 1, e2.se: -1. -1 - 1 = -2:; e1 first
function compareXYInt(a, b) {
return (a.x - b.x) || (a.y - b.y);
}


/**
* Identify intersections between two arrays of segments.
* Segments within a single array are not checked for intersections.
@@ -110,50 +65,20 @@ function sortEndpoints(e1, e2) {
* segment objects that intersect.
*/
export function findIntersectionsSortRedBlack(red, black, reportFn = (_s1, _s2) => {}) {
const ln_red = red.length;
const ln_black = black.length;
if (!ln_red || !ln_black) return;

// Build an array of endpoint objects like with SortSingle.
// But mark red/black segments so same color segments can be skipped.
// By convention, red is true; black is false.
// (slightly faster b/c the inner loop gets hit more and so inner tests for true?)
// Alternatively, could sort by red/black and then break a bit earlier in
// the inner loop. But the check for color => continue is pretty quick and simpler.
const endpoints = [];
for (let i = 0; i < ln_red; i += 1) {
const s = red[i];
endpoints.push({e: s.nw, s, se: -1, red: true},
{e: s.se, s, se: 1, red: true}); // eslint-disable-line indent, no-multi-spaces
}
for (let i = 0; i < ln_black; i += 1) {
const s = black[i];
endpoints.push({e: s.nw, s, se: -1, red: false},
{e: s.se, s, se: 1, red: false}); // eslint-disable-line indent, no-multi-spaces
}

endpoints.sort(sortEndpoints);
const ln_endpoints = endpoints.length;
for (let i = 0; i < ln_endpoints; i += 1) {
const endpoint1 = endpoints[i];
if (~endpoint1.se) continue; // Avoid duplicating the check

// Starting j is always i + 1 b/c any segment with an se endpoint after si
// Would be after si or already processed b/c its ne endpoint was before.
const start_j = i + 1;
const si = endpoint1.s;
for (let j = start_j; j < ln_endpoints; j += 1) {
const endpoint2 = endpoints[j];

if (endpoint2.e.x > si.se.x) break; // Segments past here are entirely right of si
if (!(endpoint1.red ^ endpoint2.red)) continue; // Only want segments of different color
if (~endpoint2.se) continue;

const sj = endpoint2.s;
black.sort((a, b) => compareXYInt(a.nw, b.nw));
const red_ln = red.length;
const black_ln = black.length;
for ( let i = 0; i < red_ln; i++ ) {
const si = red[i];
for ( let j = 0; j < black_ln; j++ ) {
const sj = black[j];
if ( sj.nw.x > si.se.x ) break; // The sj segments are all entirely to the right of si
if ( sj.se.x < si.nw.x ) continue; // This segment is entirely to the left of si
foundry.utils.lineSegmentIntersects(si.A, si.B, sj.A, sj.B) && reportFn(si, sj); // eslint-disable-line no-unused-expressions
}
}
}

// Testing:
// reportFn = (_s1, _s2) => { console.log(`${_s1.id} x ${_s2.id}`) }

1 change: 0 additions & 1 deletion scripts/ClockwiseSweep/LightMaskClockwisePolygonSweep.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ NormalizedRectangle,
CollisionResult,
PIXI,
CONFIG,
ClipperLib,
PolygonVertex
*/

2 changes: 1 addition & 1 deletion scripts/ClockwiseSweep/PIXICircle.js
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ export function registerPIXICircleMethods() {
});

// For equivalence with a PIXI.Polygon
if(!PIXI.Circle.prototype.hasOwnProperty("isClosed")) {
if ( !Object.hasOwn(PIXI.Circle.prototype, "isClosed") ) {
Object.defineProperty(PIXI.Circle.prototype, "isClosed", {
get: () => true
});
Loading

0 comments on commit 4b5ef4d

Please sign in to comment.