Skip to content

Commit

Permalink
fast merge residents when creating nations
Browse files Browse the repository at this point in the history
  • Loading branch information
Owen3H committed Jun 23, 2024
1 parent 977b786 commit 07c68c0
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 59 deletions.
21 changes: 21 additions & 0 deletions src/api/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { StrictPoint2D, Point2D } from "types"
import { hypot, safeParseInt } from "utils/functions.js"

type TownOrNation = Partial<Point2D & {
capital: Point2D
}>

export const getNearest = async<T extends TownOrNation>(
location: StrictPoint2D, radius: StrictPoint2D,
arr?: T[], fallback?: () => Promise<T[]>
) => {
if (!arr) {
arr = await fallback()
if (!arr) return null
}

return arr.filter(t =>
hypot(safeParseInt(t.x ?? t.capital.x), [location.x, radius.x]) &&
hypot(safeParseInt(t.z ?? t.capital.z), [location.z, radius.z])
)
}
45 changes: 23 additions & 22 deletions src/api/dynmap/GPS.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as fn from 'utils/functions.js'
import type Dynmap from './Dynmap.js'

import {
Expand All @@ -7,6 +6,7 @@ import {
} from 'types'

import Mitt from '../../helpers/EventEmitter.js'
import { manhattan, safeParseInt, strictFalsy } from 'utils/functions.js'

class GPS extends Mitt {
#map: Dynmap
Expand Down Expand Up @@ -48,10 +48,10 @@ class GPS extends Mitt {

readonly track = async(playerName: string, interval = 3000, route = Routes.FASTEST) => {
setInterval(async () => {
const player = await this.map.Players.get(playerName).catch(e => {
const player: Player = await this.map.Players.get(playerName).catch(e => {
this.emit('error', { err: "FETCH_ERROR", msg: e.message })
return null
}) as Player
})

if (!player) return
if (!this.playerIsOnline(player)) return
Expand All @@ -78,8 +78,8 @@ class GPS extends Mitt {
}
else {
this.lastLoc = {
x: fn.safeParseInt(player.x),
z: fn.safeParseInt(player.z)
x: safeParseInt(player.x),
z: safeParseInt(player.z)
}

try {
Expand All @@ -102,7 +102,7 @@ class GPS extends Mitt {
readonly fastestRoute = (loc: Location) => this.findRoute(loc, Routes.FASTEST)

readonly findRoute = async(loc: Location, options: Route) => {
if (fn.strictFalsy(loc.x) || fn.strictFalsy(loc.z)) {
if (strictFalsy(loc.x) || strictFalsy(loc.z)) {
const obj = JSON.stringify(loc)
throw new Error(`Cannot calculate route! One or more inputs are invalid:\n${obj}`)
}
Expand Down Expand Up @@ -134,11 +134,10 @@ class GPS extends Mitt {
}

// Use reduce to find the minimum distance and corresponding nation
const { distance, nation } = filtered.reduce((acc: any, nation: Nation) => {
const capital = nation.capital
const dist = fn.manhattan(
fn.safeParseInt(capital.x), fn.safeParseInt(capital.z),
fn.safeParseInt(loc.x), fn.safeParseInt(loc.z)
const { distance, nation } = filtered.reduce((acc: RouteInfo, nation: Nation) => {
const dist = manhattan(
safeParseInt(nation.capital.x), safeParseInt(nation.capital.z),
safeParseInt(loc.x), safeParseInt(loc.z)
)

// Update acc if this nation is closer
Expand All @@ -147,7 +146,7 @@ class GPS extends Mitt {
distance: Math.round(dist),
nation: {
name: nation.name,
capital: capital
capital: nation.capital
}
}
}, { distance: null, nation: null })
Expand All @@ -156,23 +155,25 @@ class GPS extends Mitt {
return { nation, distance, direction } as RouteInfo
}

/**
* Determines the direction to the destination from the origin.
*
* Only one of the main four directions (N, S, W, E) can be returned, no intermediates.
* @param origin The location where something is currently at.
* @param destination The location we wish to arrive at.
*/
static cardinalDirection(origin: Location, destination: Location) {
// Calculate the differences in x and z coordinates
const deltaX = fn.safeParseInt(origin.x) - fn.safeParseInt(destination.x)
const deltaZ = fn.safeParseInt(origin.z) - fn.safeParseInt(destination.z)
const deltaX = safeParseInt(origin.x) - safeParseInt(destination.x)
const deltaZ = safeParseInt(origin.z) - safeParseInt(destination.z)

// Calculates radians with atan2, then converted to degrees.
const angle = Math.atan2(deltaZ, deltaX) * 180 / Math.PI

// Determine the cardinal direction
if (angle >= -45 && angle < 45)
return "east"

if (angle >= 45 && angle < 135)
return "north"

if (angle >= 135 || angle < -135)
return "west"
if (angle >= -45 && angle < 45) return "east"
if (angle >= 45 && angle < 135) return "north"
if (angle >= 135 || angle < -135) return "west"

return "south"
}
Expand Down
38 changes: 18 additions & 20 deletions src/api/dynmap/Nations.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import type {
Nation, Town
Nation, StrictPoint2D, Town
} from 'types'

import type { Dynmap } from "./Dynmap.js"
import type { EntityApi } from 'helpers/EntityApi.js'

import * as fn from 'utils/functions.js'
import {
FetchError,
type NotFoundError
} from "utils/errors.js"

import {
getNearest
} from '../common.js'

import {
sqr, getExisting,
fastMergeUnique
} from 'utils/functions.js'

//import OfficialAPI from '../OAPI.js'

class Nations implements EntityApi<Nation | NotFoundError> {
Expand All @@ -31,7 +39,7 @@ class Nations implements EntityApi<Nation | NotFoundError> {
const nations = await this.all()
if (!nations) throw new FetchError('Error fetching nations! Please try again.')

const existing = fn.getExisting(nations, nationList, 'name')
const existing = getExisting(nations, nationList, 'name')
return existing.length > 1 ? Promise.all(existing): Promise.resolve(existing[0])
}

Expand Down Expand Up @@ -66,7 +74,9 @@ class Nations implements EntityApi<Nation | NotFoundError> {
}

//#region Add extra stuff
raw[nationName].residents = fn.removeDuplicates(raw[nationName].residents.concat(town.residents))
const resNames = raw[nationName].residents

raw[nationName].residents = fastMergeUnique(resNames, town.residents)
raw[nationName].area += town.area

// Current town is in existing nation
Expand All @@ -86,23 +96,11 @@ class Nations implements EntityApi<Nation | NotFoundError> {
//#endregion
}

return nations as Nation[]
return nations
}

readonly nearby = async (
xInput: number, zInput: number,
xRadius: number, zRadius: number,
nations?: Nation[]
) => {
if (!nations) {
nations = await this.all()
if (!nations) return null
}

return nations.filter(n =>
fn.hypot(fn.safeParseInt(n.capital.x), [xInput, xRadius]) &&
fn.hypot(fn.safeParseInt(n.capital.z), [zInput, zRadius]))
}
readonly nearby = async (location: StrictPoint2D, radius: StrictPoint2D, nations?: Nation[]) =>
getNearest<Nation>(location, radius, nations, this.all)

readonly joinable = async (townName: string, nationless = true) => {
let town: Town = null
Expand All @@ -116,7 +114,7 @@ class Nations implements EntityApi<Nation | NotFoundError> {
if (!nations) throw new FetchError('Error fetching nations! Please try again.')

return nations.filter(n => {
const joinable = fn.sqr(n.capital, town, this.map.inviteRange)
const joinable = sqr(n.capital, town, this.map.inviteRange)
return nationless ? joinable && town.nation == "No Nation" : joinable
}) as Nation[]
}
Expand Down
19 changes: 14 additions & 5 deletions src/api/squaremap/Nations.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { FetchError, type NotFoundError } from "utils/errors.js"
import type { EntityApi } from "helpers/EntityApi.js"
import type { Nation, SquaremapTown } from "types"
import type { Nation, SquaremapTown, StrictPoint2D } from "types"

import type Squaremap from "./Squaremap.js"
import { getExisting, removeDuplicates } from "utils/functions.js"
import { getExisting, fastMergeUnique } from "utils/functions.js"

import {
getNearest
} from "../common.js"

class Nations implements EntityApi<Nation | NotFoundError> {
#map: Squaremap
Expand Down Expand Up @@ -50,9 +54,11 @@ class Nations implements EntityApi<Nation | NotFoundError> {

nations.push(raw[nationName])
}

//#region Add extra stuff
raw[nationName].residents = removeDuplicates(raw[nationName].residents.concat(town.residents))
const resNames = raw[nationName].residents

raw[nationName].residents = fastMergeUnique(resNames, town.residents)
raw[nationName].area += town.area

// Current town is in existing nation
Expand All @@ -72,8 +78,11 @@ class Nations implements EntityApi<Nation | NotFoundError> {
//#endregion
}

return nations as Nation[]
return nations
}

readonly nearby = async (location: StrictPoint2D, radius: StrictPoint2D, nations?: Nation[]) =>
getNearest<Nation>(location, radius, nations, this.all)
}

export {
Expand Down
12 changes: 3 additions & 9 deletions src/api/squaremap/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface TownCoords {
townZ: number[]
}

const parseTowns = async(res: SquaremapMarkerset, removeAccents = false) => {
export const parseTowns = async(res: SquaremapMarkerset, removeAccents = false) => {
if (res.id == "chunky") throw new Error("Error parsing towns: Chunky markerset detected, pass a towny markerset instead.")
if (!res?.markers) throw new ReferenceError('Error parsing towns: Missing or invalid markers!')

Expand Down Expand Up @@ -110,7 +110,7 @@ const parseTowns = async(res: SquaremapMarkerset, removeAccents = false) => {

// }

const parseResidents = (towns: SquaremapTown[]) => towns.reduce((acc: Resident[], town: SquaremapTown) => [
export const parseResidents = (towns: SquaremapTown[]) => towns.reduce((acc: Resident[], town: SquaremapTown) => [
...acc,
...town.residents.map(res => {
const r: Resident = {
Expand All @@ -124,12 +124,6 @@ const parseResidents = (towns: SquaremapTown[]) => towns.reduce((acc: Resident[]
})
], [])

const parsePlayers = async(res: SquaremapRawPlayer[]) => {
export const parsePlayers = async(res: SquaremapRawPlayer[]) => {

}

export {
parseTowns,
parsePlayers,
parseResidents
}
5 changes: 5 additions & 0 deletions src/types/gps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export type Point2D = {
z: number | string
}

export type StrictPoint2D = {
x: number
z: number
}

export type RouteInfo = {
distance: number
direction: "north" | "east" | "south" | "west"
Expand Down
14 changes: 12 additions & 2 deletions src/utils/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@ import type {

import { NotFound } from './errors.js'

export const removeDuplicates = <T>(arr: T[]) => [...new Set(arr)]
// Thoroughly tested, faster than both spread and concat w/ high No. of items.
export const fastMerge = <T>(original: T[], args: T[]) => {
original.push.apply(args)
return original
}

// Fast merge, but convert to set and back to ensure duplicates are removed.
export const fastMergeUnique = <T>(original: T[], args: T[]) => {
fastMerge(original, args)
return [...new Set(original)]
}

export const stripInvalidChars = (str: string) => {
export const stripInvalidChars = (str: string) => {
return str.replace(/((&#34)|(&\w[a-z0-9].|&[0-9kmnola-z]));/g, "")
.replace(/&quot;|&#039;/g, '"')
}
Expand Down
21 changes: 21 additions & 0 deletions tests/squaremap/nations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect, expectTypeOf, assertType } from 'vitest'

import type { Nation } from '../../src/types'

describe('[Squaremap/Aurora] Nations', () => {
it('can get all nations', async () => {
const nations = await globalThis.Aurora.Nations.all()
assertType<Nation[]>(nations)
})

it('can get single nation', async () => {
const nation = await globalThis.Aurora.Nations.get('madagascar')

expect(nation).toBeDefined()
expectTypeOf(nation).not.toEqualTypeOf<Error>()
assertType<Nation | Nation[]>(nation)

expect(nation.name).toBe('Madagascar')
console.log(nation)
})
})
2 changes: 1 addition & 1 deletion tests/squaremap/residents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ describe('[Squaremap/Aurora] Residents', () => {
expect(res.rank).toBe("Mayor")
expect(res.town).toBe("Krn")

console.log(res)
//console.log(res)
})
})

0 comments on commit 07c68c0

Please sign in to comment.