UNPKG

ddnet

Version:

A typescript npm package for interacting with data from ddnet.org

367 lines 13 kB
import axios from 'axios'; import { _Schema_players_json } from '../../schemas/players/json.js'; import { _Schema_players_json2 } from '../../schemas/players/json2.js'; import { _Schema_players_query } from '../../schemas/players/query.js'; import { DDNetError, RankAvailableRegion, ServerRegion, Type, dePythonifyTime } from '../../util.js'; import { Map } from '../maps/Map.js'; import { CacheManager } from '../other/CacheManager.js'; import { Finish, RecentFinish } from '../other/Finish.js'; import { ActivityEntry } from './ActivityEntry.js'; import { Finishes } from './Finishes.js'; import { GlobalLeaderboard } from './GlobalLeaderboard.js'; import { Leaderboard } from './Leaderboard.js'; import { CompletedMapStats, UncompletedMapStats } from './MapStats.js'; import { Partner } from './Partner.js'; import { ServerStats } from './ServerStats.js'; import { Servers } from './Servers.js'; /** * Represents a DDNet player. * * @example * ```ts * const coolGuy = await Player.new('Sans3108'); * * console.log(coolGuy.favoriteServer); // "GER" * console.log(coolGuy.globalLeaderboard.completionist.points); // 2727 * ``` */ export class Player { //#region Declarations /** * Raw data for this player. */ #rawData; // Marked private with vanilla JS syntax for better logging. /** * Player responses cache. (8h default TTL) */ static cache = new CacheManager('player-cache', 8 * 60 * 60 * 1000); // 8h /** * Sets the TTL (Time-To-Live) for objects in cache. */ static setTTL = this.cache.setTTL; /** * Clears the {@link Player.cache}. */ static clearCache = this.cache.clearCache; /** * The name of this player. */ name; /** * The url of this player on ddnet.org */ url; /** * Global leaderboard stats for this player. */ globalLeaderboard; /** * Total amount of points earnable from first completions across all maps. */ totalCompletionistPoints; /** * The favorite server region of this player. */ favoriteServer; /** * First and recent finishes for this player. * * @remarks Does not include rank information for any finishes, values are set to `-1`. */ finishes; /** * Favorite partners of this player. */ favoritePartners; /** * Server stats leaderboards for this player. */ serverTypes; /** * Recorded player activity. */ activity; /** * Number of hours played in the past 365 days by this player. */ hoursPlayedPast365days; /** * All maps stats for this player. */ allMapStats; //#endregion /** * Create a new instance of {@link Player} from API data. * Not intended to be used, use {@link new Player.new} instead. */ constructor( /** * The raw data for this player. */ rawData) { this.populate(rawData); } /** * Fetch, parse and construct a new {@link Player} instance. */ static async new( /** * The name or ddnet.org url of this player. */ nameOrUrl, /** * Wether to bypass the player data cache. */ bypassCache = false) { const response = await this.makeRequest(nameOrUrl, bypassCache); if (response instanceof DDNetError) throw response; const parsed = this.parseObject(response.data); if (!parsed.success) throw parsed.error; return new this(parsed.data); } /** * Parse an object using the player {@link _Schema_players_json2 raw data zod schema}. */ static parseObject( /** * The object to be parsed. */ data) { const parsed = _Schema_players_json2.safeParse(data); if (parsed.success) return { success: true, data: parsed.data }; return { success: false, error: new DDNetError(parsed.error.message, parsed.error) }; } /** * Fetch the player data from the API. */ static async makeRequest( /** * The name or ddnet.org url of the player. */ nameOrUrl, /** * Wether to bypass the cache. */ force = false) { const url = nameOrUrl.startsWith('https://ddnet.org/players/') ? nameOrUrl : `https://ddnet.org/players/?json2=${encodeURIComponent(nameOrUrl)}`; if (!force) { if (await this.cache.has(url)) { const data = await this.cache.get(url); if (data) return { data, fromCache: true }; } } const response = await axios.get(url).catch((err) => new DDNetError(err.cause?.message, err)); if (response instanceof DDNetError) return response; const data = response.data; if (typeof data === 'string') return new DDNetError(`Invalid response!`, data); await this.cache.set(url, data); return { data, fromCache: false }; } /** * Populate the object with the raw player data. */ populate( /** * The raw player data. */ rawData) { this.#rawData = rawData; this.name = this.#rawData.player; this.url = `https://ddnet.org/players/${encodeURIComponent(this.name)}`; if (!this.#rawData.points.rank) throw new DDNetError('Player points assumption turned out to be null.'); this.globalLeaderboard = new GlobalLeaderboard({ completionist: { placement: this.#rawData.points.rank, points: this.#rawData.points.points }, completionistLastMonth: 'points' in this.#rawData.points_last_month ? { placement: this.#rawData.points_last_month.rank, points: this.#rawData.points_last_month.points } : null, completionistLastWeek: 'points' in this.#rawData.points_last_week ? { placement: this.#rawData.points_last_week.rank, points: this.#rawData.points_last_week.points } : null, rank: 'points' in this.#rawData.rank ? { placement: this.#rawData.rank.rank, points: this.#rawData.rank.points } : null, team: 'points' in this.#rawData.team_rank ? { placement: this.#rawData.team_rank.rank, points: this.#rawData.team_rank.points } : null }); this.totalCompletionistPoints = this.#rawData.points.total; this.favoriteServer = ServerRegion[this.#rawData.favorite_server.server] ?? ServerRegion.UNK; this.finishes = new Finishes({ first: new Finish({ mapName: this.#rawData.first_finish.map, players: [this.name], rank: { placement: -1, points: -1 }, region: RankAvailableRegion.UNK, timeSeconds: this.#rawData.first_finish.time, timestamp: dePythonifyTime(this.#rawData.first_finish.timestamp) }), recent: this.#rawData.last_finishes.map(f => new RecentFinish({ mapName: f.map, mapType: Type[Object.entries(Type).find(e => e[1] === f.type)?.[0]] ?? Type.unknown, players: [this.name], rank: { placement: -1, points: -1 }, region: RankAvailableRegion[f.country] ?? RankAvailableRegion.UNK, timeSeconds: f.time, timestamp: dePythonifyTime(f.timestamp) })) }); this.favoritePartners = this.#rawData.favorite_partners.map(p => new Partner({ name: p.name, finishCount: p.finishes })); const keys = Object.keys(Type).filter(k => k !== 'unknown'); const servers = keys.map(k => { const raw = this.#rawData.types[Type[k]]; const leaderboard = new Leaderboard({ completionist: 'points' in raw.points ? { placement: raw.points.rank, points: raw.points.points } : null, rank: 'points' in raw.rank ? { placement: raw.rank.rank, points: raw.rank.points } : null, team: 'points' in raw.team_rank ? { placement: raw.team_rank.rank, points: raw.team_rank.points } : null }); const maps = Object.keys(raw.maps).map(key => { const map = raw.maps[key]; if (map.finishes === 0) { return new UncompletedMapStats({ mapName: key, mapType: Type[k], pointsReward: map.points }); } else { // TS is dumb const casted = map; return new CompletedMapStats({ bestTimeSeconds: casted.time, finishCount: casted.finishes, firstFinishTimestamp: dePythonifyTime(casted.first_finish), mapName: key, mapType: Type[k], pointsReward: casted.points, rank: casted.rank, teamRank: casted.team_rank }); } }); const server = new ServerStats({ name: Type[k], leaderboard, maps, totalCompletionistPoints: raw.points.total }); return server; }); this.serverTypes = new Servers(servers); this.activity = this.#rawData.activity.map(a => new ActivityEntry({ date: a.date, hoursPlayed: a.hours_played })); this.hoursPlayedPast365days = this.#rawData.hours_played_past_365_days; this.allMapStats = []; for (const key in this.serverTypes) { const k = key; const maps = this.serverTypes[k].maps; this.allMapStats.push(...maps); } return this; } /** * Refresh the data for this player. */ async refresh() { const data = await Player.makeRequest(this.name, true); if (data instanceof DDNetError) throw new DDNetError(`Failed to refresh ${this}`, data); const parsed = Player.parseObject(data.data); if (!parsed.success) throw new DDNetError(`Failed to refresh ${this}`, parsed.error); return this.populate(parsed.data); } /** * Returns the name and url of this player in markdown format. */ toString() { return `[${this.name}](${this.url})`; } /** * Returns an array of objects containing the names of all finished maps and a function to turn them into proper {@link Map} objects. */ async getAllFinishedMapNames( /** * Wether to bypass the cache. */ force = false, /** * The region to pull ranks from in the `toMap` function from the returned value. Omit for global ranks. */ rankSource) { const data = await Player.makeRequest(`https://ddnet.org/players/?json=${encodeURIComponent(this.name)}`, force); if (data instanceof DDNetError) throw data; const parsed = _Schema_players_json.safeParse(data.data); if (!parsed.success) throw new DDNetError(`Failed to parse received data.`, parsed.error); return parsed.data.map(map => ({ name: map, toMap: async () => await Map.new(map, rankSource) })); } /** * Search for a player. */ static async search( /** * The value to search for. */ value, /** * Wether to bypass the cache. */ force = false) { const data = await Player.makeRequest(`https://ddnet.org/players/?query=${encodeURIComponent(value)}`, force); if (data instanceof DDNetError) throw data; const parsed = _Schema_players_query.safeParse(data.data); if (!parsed.success) throw new DDNetError(`Failed to parse received data.`, parsed.error); if (parsed.data.length === 0) return null; return parsed.data.map(player => ({ name: player.name, points: player.points, toPlayer: async () => await Player.new(player.name) })); } } //# sourceMappingURL=Player.js.map