earthmc
Version:
An unofficial EarthMC library providing handy methods and extensive info.
198 lines (162 loc) • 6.74 kB
text/typescript
import { Routes } from '../../types/index.js'
import type {
Route, RouteInfo,
Location, Nation, Player,
StrictPoint2D
} from '../../types/index.js'
import Emitter from '../../helpers/EventEmitter.js'
import { manhattan, safeParseInt, strictFalsy } from '../../utils/functions.js'
import type Dynmap from './Dynmap.js'
type GPSEvents = {
error: {
err: string
msg: string
}
underground: string | {
lastLocation: StrictPoint2D,
routeInfo: RouteInfo
}
locationUpdate: RouteInfo
}
class GPS extends Emitter<GPSEvents> {
#map: Dynmap
#emittedUnderground = false
#lastLoc: undefined | {
x: number
z: number
}
get map() { return this.#map }
get emittedUnderground() { return this.#emittedUnderground }
protected set emittedUnderground(val: boolean) {
this.#emittedUnderground = val
}
get lastLoc() { return this.#lastLoc }
protected set lastLoc(val: { x: number, z: number }) {
this.#lastLoc = val
}
static readonly Routes = Routes
constructor(map: Dynmap) {
super()
this.#map = map
}
playerIsOnline = (player: Player) => {
if (!player.online) {
this.emit('error', {
err: "INVALID_PLAYER",
msg: "Player is offline or does not exist!"
})
}
return player.online
}
readonly track = async(playerName: string, interval = 3000, route = Routes.FASTEST) => {
setInterval(async () => {
const player: Player = await this.map.Players.get(playerName).catch(e => {
this.emit('error', { err: "FETCH_ERROR", msg: e.message })
return null
})
if (!player) return
if (!this.playerIsOnline(player)) return
if (player.underground) {
if (!this.emittedUnderground) {
this.emittedUnderground = true
if (!this.lastLoc) {
this.emit("underground", "No last location. Waiting for this player to show.")
return
}
try {
const routeInfo = await this.findRoute(this.lastLoc, route)
this.emit('underground', {
lastLocation: this.lastLoc,
routeInfo: routeInfo
})
} catch(e: any) {
this.emit('error', { err: "INVALID_LAST_LOC", msg: e.message })
}
}
}
else {
this.lastLoc = {
x: safeParseInt(player.x),
z: safeParseInt(player.z)
}
try {
const routeInfo = await this.findRoute({
x: player.x,
z: player.z
}, route)
this.emit('locationUpdate', routeInfo)
} catch(e: any) {
this.emit('error', { err: "INVALID_LOC", msg: e.message })
}
}
}, interval)
return this
}
readonly safestRoute = (loc: Location) => this.findRoute(loc, Routes.SAFEST)
readonly fastestRoute = (loc: Location) => this.findRoute(loc, Routes.FASTEST)
readonly findRoute = async(loc: Location, options: Route) => {
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}`)
}
// Scan all nations for closest match.
// Computationally more expensive to include PVP disabled nations.
const [towns, nations] = await Promise.all([this.map.Towns.all(), this.map.Nations.all()])
const townsMap = new Map(towns.map(t => [t.name, t]))
const len = nations.length
const filtered = []
for (let i = 0; i < len; i++) {
const nation = nations[i]
const capitalName = nation.capital.name
const capital = townsMap.get(capitalName)
if (!capital) continue
// Filter out nations where either capital is not public
// or both avoidPvp and flags.pvp are true
const flags = capital.flags
const PVP = options.avoidPvp && flags.pvp
const PRIVATE = options.avoidPrivate && !flags.public
if (PVP || PRIVATE) continue
filtered.push(nation)
}
// Use reduce to find the minimum distance and corresponding nation
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
const closer = !acc.distance || dist < acc.distance
return !closer ? acc : {
distance: Math.round(dist),
nation: {
name: nation.name,
capital: nation.capital
}
}
}, { distance: null, nation: null })
const direction = GPS.cardinalDirection(nation.capital, loc)
return { nation, distance, direction }
}
/**
* 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 = 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"
return "south"
}
}
export {
GPS, GPS as default
}