diff --git a/resources/util.ts b/resources/util.ts index 6e95ef6c2c4..7072bb6f7b7 100644 --- a/resources/util.ts +++ b/resources/util.ts @@ -264,16 +264,16 @@ const output16Dir: DirectionOutput16[] = [ const outputCardinalDir: DirectionOutputCardinal[] = ['dirN', 'dirE', 'dirS', 'dirW']; const outputIntercardDir: DirectionOutputIntercard[] = ['dirNE', 'dirSE', 'dirSW', 'dirNW']; -const compareDirectionOutput = (a: DirectionOutput16, b: DirectionOutput16): number => { - const getIndex = (n: DirectionOutput16) => { - const index = output16Dir.indexOf(n); - // Values outside of output16Dir (i.e. 'unknown') sort last - if (index < 0) - return output16Dir.length; - return index; - }; +const getDirectionIndex = (n: DirectionOutput16) => { + const index = output16Dir.indexOf(n); + // Values outside of output16Dir (i.e. 'unknown') sort last + if (index < 0) + return output16Dir.length; + return index; +}; - return getIndex(a) - getIndex(b); +const compareDirectionOutput = (a: DirectionOutput16, b: DirectionOutput16): number => { + return getDirectionIndex(a) - getDirectionIndex(b); }; const outputStrings16Dir: OutputStrings = { @@ -383,6 +383,76 @@ const outputFromIntercardNum = (dirNum: number): DirectionOutputIntercard => { return outputIntercardDir[dirNum] ?? 'unknown'; }; +export type AnyDirection = + | DirectionOutputCardinal + | DirectionOutputIntercard + | DirectionOutput8 + | DirectionOutput16; + +// Example usage: +// const dirs: DirectionOutputCardinal[] = ['dirN', 'dirW']; +// dirs.sort(getSortDirectionsClockwiseFunction('dirE')); +// `dirs` should equal `['dirW', 'dirN']` +export const getSortDirectionsClockwiseFunction = ( + from?: AnyDirection, +): (left: AnyDirection, right: AnyDirection) => number => { + // Default to dirN + let offset = 0; + if (from !== undefined && from !== 'unknown') + offset = getDirectionIndex(from); + + const count = output16Dir.length; + + return (left: AnyDirection, right: AnyDirection) => { + if (left === 'unknown' || right === 'unknown') { + return left === right ? 0 : left === 'unknown' ? 1 : -1; + } + const rightIndex = (count + getDirectionIndex(right) - offset) % count; + const leftIndex = (count + getDirectionIndex(left) - offset) % count; + return leftIndex - rightIndex; + }; +}; + +type Point = { + x: number; + y: number; +}; + +const xyToHeading = (x: number, y: number, centerX: number, centerY: number) => { + x = x - centerX; + y = y - centerY; + return Math.atan2(x, y); +}; + +// Example usage: +// getSortPointsClockwiseFunction +// const points = [{ x: 101, y: 101 }, { x: 99, y: 99 }]; +// points.sort(getSortPointsClockwiseFunction({x: 100, y: 100}, {x: 99, y: 101})); +// `points` should now equal `[{ x: 99, y: 99 }, { x: 101, y: 101 }]` +export const getSortPointsClockwiseFunction = ( + center: T, + reference: number | T = Math.PI, // Default to north +): (left: T, right: T) => number => { + // Convert point to heading if needed + const offset = typeof reference === 'object' + ? xyToHeading(reference.x, reference.y, center.x, center.y) + : reference; + + const twoPI = Math.PI * 2; + + return (left: T, right: T) => { + // Get our base headings for the two points + const rightHeading = xyToHeading(right.x, right.y, center.x, center.y); + const leftHeading = xyToHeading(left.x, left.y, center.x, center.y); + + // Adjust by reference offset + const rightHeadingOffset = (twoPI + (offset - rightHeading)) % twoPI; + const leftHeadingOffset = (twoPI + (offset - leftHeading)) % twoPI; + + return leftHeadingOffset - rightHeadingOffset; + }; +}; + export const Directions = { output8Dir: output8Dir, output16Dir: output16Dir, diff --git a/test/unittests/util_test.ts b/test/unittests/util_test.ts index 4078335c5c7..816ca76b3aa 100644 --- a/test/unittests/util_test.ts +++ b/test/unittests/util_test.ts @@ -2,9 +2,12 @@ import { assert } from 'chai'; import Util, { allJobs, + AnyDirection, casterDpsJobs, craftingJobs, gatheringJobs, + getSortDirectionsClockwiseFunction, + getSortPointsClockwiseFunction, healerJobs, limitedJobs, meleeDpsJobs, @@ -93,4 +96,97 @@ describe('util tests', () => { assert(!Util.isLimitedJob(job)) ); }); + + it('sorts directions clockwise from a reference direction and undefined reference', () => { + const dirs: AnyDirection[] = [ + 'dirNE', + 'dirSW', + 'dirN', + 'dirNW', + 'dirS', + 'dirW', + 'dirSE', + 'dirE', + ]; + + let expected = ['dirNW', 'dirN', 'dirNE', 'dirE', 'dirSE', 'dirS', 'dirSW', 'dirW']; + let sorted = dirs.sort(getSortDirectionsClockwiseFunction('dirNW')); + assert.deepEqual(sorted, expected); + + expected = ['dirN', 'dirNE', 'dirE', 'dirSE', 'dirS', 'dirSW', 'dirW', 'dirNW']; + sorted = dirs.sort(getSortDirectionsClockwiseFunction()); + assert.deepEqual(sorted, expected); + }); + + it('sorts directions and unknowns, putting unknowns at the end', () => { + const dirs1: AnyDirection[] = [ + 'dirNE', + 'unknown', + 'dirN', + 'unknown', + 'dirNW', + 'dirS', + ]; + + const expected1 = ['dirN', 'dirNE', 'dirS', 'dirNW', 'unknown', 'unknown']; + assert.deepEqual(dirs1.sort(getSortDirectionsClockwiseFunction()), expected1); + + const dirs2: AnyDirection[] = ['dirNE', 'unknown', 'dirN', 'dirNW']; + const expected2 = ['dirNW', 'dirN', 'dirNE', 'unknown']; + assert.deepEqual( + dirs2.sort(getSortDirectionsClockwiseFunction('dirNW')), + expected2, + ); + }); + + it('sorts points clockwise from a reference point', () => { + const getPoints = () => [ + { id: 'NE', x: 1, y: -1 }, + { id: 'SW', x: -1, y: 1 }, + { id: 'N', x: 0, y: -1 }, + { id: 'NW', x: -1, y: -1 }, + { id: 'S', x: 0, y: 1 }, + { id: 'W', x: -1, y: 0 }, + { id: 'SE', x: 3, y: 3 }, + { id: 'E', x: 2, y: 0 }, + ]; + + const expected = ['NW', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W']; + + let [refX, refY] = [-1, -1]; // same as NW + let sorted = getPoints().sort( + getSortPointsClockwiseFunction({ x: 0, y: 0 }, { x: refX, y: refY }), + ); + assert.deepEqual(sorted.map((point) => point.id), expected); + + [refX, refY] = [-1.2, -0.8]; + sorted = getPoints().sort(getSortPointsClockwiseFunction({ x: 0, y: 0 }, { x: refX, y: refY })); + assert.deepEqual(sorted.map((point) => point.id), expected); + }); + + it('sorts points clockwise with numeric reference', () => { + const points = [ + { id: 'NE', x: 1, y: -1 }, + { id: 'NW', x: -1, y: -1 }, + { id: 'N', x: 0, y: -1 }, + ]; + + const sorted = points.sort( + getSortPointsClockwiseFunction({ x: 0, y: 0 }, 0), // reference is south + ); + + assert.deepEqual(sorted.map((point) => point.id), ['NW', 'N', 'NE']); + }); + + it('keeps points in input order when all angles are equal', () => { + const points = [ + { id: '1', x: 0, y: -3 }, + { id: '2', x: 0, y: -4 }, + { id: '3', x: 0, y: -1 }, + { id: '4', x: 0, y: -2 }, + ]; + + const sorted = points.sort(getSortPointsClockwiseFunction({ x: 0, y: 0 })); + assert.deepEqual(sorted.map((point) => point.id), ['1', '2', '3', '4']); + }); });