trackmania.io
Version:
Node.js inplementation of Trackmania Live services (trackmania.io)
1,303 lines (1,160 loc) • 30.4 kB
JavaScript
const Client = require('../client/Client'); // eslint-disable-line no-unused-vars
const TMMap = require('../structures/TMMap'); // eslint-disable-line no-unused-vars
const PlayerEchelonData = require('../data/PlayerEchelons.json');
const {MMTypes, MatchmakingGroup} = require('../util/Constants'); // eslint-disable-line no-unused-vars
/**
* Represents a player in Trackmania.
*/
class Player {
constructor(client, data){
/**
* The client object of the player
* @type {Client}
*/
this.client = client;
/**
* The data of the player
* @type {Object}
* @private
*/
this._data = data;
}
/**
* Constructs an array of the zone of the player
* @returns {Array<Object>}
* @private
*/
_constructZoneArray(array, obj){
// Copy the object to a new object for displaing
const obj2 = Object.assign({}, obj);
delete obj2.parent;
// Add ranking to this object
obj2.ranking = this._data.trophies.zonepositions[array.length];
array.push(obj2);
Object.entries(obj).forEach(entry => {
const [key, value] = entry;
if (key == 'parent' && value != null) this._constructZoneArray(array, value);
});
return array;
}
/**
* The account ID of the player
* @type {string}
*/
get id(){
return this._data.accountid;
}
/**
* The login of the player
* @type {string}
*/
get login(){
return this.client.players.toLogin(this.id);
}
/**
* The display name of the player
* @type {string}
*/
get name(){
return this._data.displayname;
}
/**
* The date of the player's first login
* @type {Date}
* @readonly
*/
get firstLogin(){
return new Date(this._data.timestamp);
}
/**
* The club tag of the player (non-formatted)
* @type {string}
*/
get clubTag(){
return this._data.clubtag;
}
/**
* The last change of the player's club tag
* @type {Date}
* @readonly
*/
get lastClubTagChange(){
return new Date(this._data.clubtagtimestamp);
}
/**
* The player's zone data with the ranking of the player in the zone
* @type {Array<PlayerZone>}
* @example
* // Generate a string of the player's zone data
* const string = player.zone.map(z=>z.name).join(', ');
*/
get zone(){
const zonesArray = this._constructZoneArray([], this._data.trophies.zone);
return zonesArray.map(z=>new PlayerZone(this, z));
}
/**
* The player's trophy data
* @type {PlayerTrophies}
*/
get trophies(){
if (!this._PlayerTrophies){
/**
* The player's trophy data
* @type {PlayerTrophies}
* @private
*/
this._PlayerTrophies = new PlayerTrophies(this, this._data.trophies);
}
return this._PlayerTrophies;
}
/**
* The player's meta data
* @type {PlayerMeta}
*/
get meta(){
if (!this._PlayerMeta){
/**
* The player's meta data
* @type {PlayerMeta}
* @private
*/
this._PlayerMeta = new PlayerMeta(this);
}
return this._PlayerMeta;
}
/**
* The player's COTD Data
* @param {number} [page=0] The page number.
* @returns {Promise<PlayerCOTD>}
*/
async cotd(page = 0){
const player = this.client.options.api.paths.tmio.tabs.player,
cotd = this.client.options.api.paths.tmio.tabs.cotd,
ReqUtil = require('../util/ReqUtil');
const res = await this.client._apiReq(`${new ReqUtil(this.client).tmioAPIURL}/${player}/${this.id}/${cotd}/${page}`);
if (!res) throw "No response from the API";
return new PlayerCOTD(this, res);
}
/**
* The player's matchmaking data
* @param {MatchmakingGroup} [type="3v3"] The type of matchmaking data to return
* @returns {PlayerMatchmaking}
*/
matchmaking(type = '3v3'){
if (!this._PlayerMatchmaking || this._PlayerMatchmaking.type !== type){
/**
* The player's matchmaking data
* @type {PlayerMatchmaking}
* @private
*/
this._PlayerMatchmaking = new PlayerMatchmaking(this, type);
}
return this._PlayerMatchmaking;
}
}
/**
* Represents a zone in a player
*/
class PlayerZone {
constructor(player, zone){
/**
* The player instance
* @type {Player}
*/
this.player = player;
/**
* The client that instancied the Player
* @type {Client}
*/
this.client = this.player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = zone;
}
/**
* The name of the zone
* @type {string}
*/
get name(){
return this._data.name;
}
/**
* The flag of the zone
* @type {string}
*/
get flag(){
return this._data.flag;
}
/**
* The ranking of the player in the zone
* @type {number}
*/
get ranking(){
return this._data.ranking;
}
}
/**
* Represents the trophies of a player
*/
class PlayerTrophies {
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The points of the player
* @type {number}
*/
get points(){
return this._data.points;
}
/**
* The last time the player got a trophy
* @type {Date}
* @readonly
*/
get lastChange(){
return new Date(this._data.timestamp);
}
/**
* The echelon level of the player
* @type {PlayerEchelon}
*/
get echelon(){
if (!this._PlayerEchelon || this._PlayerEchelon.number !== this._data.echelon){
/**
* The player's echelon data
* @type {PlayerEchelon}
* @private
*/
this._PlayerEchelon = new PlayerEchelon(this.player, this._data);
}
return this._PlayerEchelon;
}
/**
* The number of trophies the player has
* @param {number} [number=1] The trophy number, from 1 (bronze 1) to 9 (gold 3)
* @returns {number}
* @example
* // Get number of trophy 5 (aka silver 2 trophy)
* player.trophies.trophy(5);
*/
trophy(number = 1){
if (number < 1 || number > 9){
throw "Invalid trophy number";
}
return this._data.counts[number - 1];
}
/**
* The number of trophies the player has
* @type {Array<number>}
*/
get trophies(){
return this._data.counts;
}
/**
* The last 25 trophies gains of the player
* @param {number} [page=0] The page number.
* @type {Array<PlayerTrophyHistory>}
*/
async history(page = 0){
const player = this.client.options.api.paths.tmio.tabs.player,
trophies = this.client.options.api.paths.tmio.tabs.trophies,
ReqUtil = require('../util/ReqUtil');
const res = await this.client._apiReq(`${new ReqUtil(this.client).tmioAPIURL}/${player}/${this.player.id}/${trophies}/${page}`);
if (!res) throw "No response from the API";
let arr = [];
for (let i = 0; i < res.gains.length; i++){
arr.push(new PlayerTrophyHistory(this.player, res.gains[i]));
}
return arr;
}
}
/**
* Represents the history of a player's trophies
*/
class PlayerTrophyHistory {
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The number of trophies the player has
* @param {number} [number=1] The trophy number, from 1 (bronze 1) to 9 (gold 3)
* @returns {number}
* @example
* // Get number of trophy 5 (aka silver 2 trophy) on the latest gain
* player.trophies.history[0].trophy(5);
*/
trophy(number = 1){
if (number < 1 || number > 9){
throw "Invalid trophy number";
}
return this._data.counts[number - 1];
}
/**
* The number of trophies the player has
* @type {Array<number>}
*/
get trophies(){
return this._data.counts;
}
/**
* The date of the gain
* @type {Date}
*/
get date(){
return new Date(this._data.timestamp);
}
/**
* The rank of the player
* @type {number}
*/
get rank(){
return this._data.details.rank;
}
/**
* The types of the achievement
* @type {PlayerTrophyAchievementType}
*/
get type(){
let achievement = this._data.achievement;
if (this._data.details) achievement.details = this._data.details;
if (!this._achievement || this.achievement.id != achievement.trophyAchievementId){
/**
* The achievement type object
* @type {PlayerTrophyAchievementType}
* @private
*/
this._achievement = new PlayerTrophyAchievementType(this.player, achievement);
}
return this._achievement;
}
/**
* The map where the achievement was earned (if any)
* @returns {Promise<TMMap>|null}
*/
async map(){
if (this._data.map){
return await this.client.maps.get(this._data.map.mapId);
} else return null;
}
}
/**
* Represents the type of an achievement
*/
class PlayerTrophyAchievementType{
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = this.player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* Gets the type of the achievement
* @type {string}
*/
get type(){
return this._data.trophyAchievementType;
}
/**
* Gets the ID of the achievement
* @type {string}
*/
get id(){
return this._data.trophyAchievementId;
}
/**
* Gets the solo ranking achievement type (if the type is SoloRanking)
* @type {?string}
*/
get soloRankingType(){
if (this.type == "SoloRanking") return this._data.trophySoloRankingAchievementType;
else return null;
}
/**
* Gets the solo ranking season ID (if the type is SoloRanking)
* @type {?string}
*/
get soloRankingSeasonId(){
if (this.type == "SoloRanking") return this._data.seasonId;
else return null;
}
/**
* Gets the competition id (if the type is CompetitionRanking)
* @type {?string}
*/
get competitionId(){
if (this.type == "CompetitionRanking") return this._data.competitionId;
else return null;
}
/**
* Gets the competition name (if the type is CompetitionRanking)
* @type {?string}
*/
get competitionName(){
if (this.type == "CompetitionRanking") return this._data.competitionName;
else return null;
}
/**
* Gets the competition stage (if the type is CompetitionRanking)
* @type {?string}
*/
get competitionStage(){
if (this.type == "CompetitionRanking") return this._data.competitionStage;
else return null;
}
/**
* Gets the competition stage step (if the type is CompetitionRanking)
* @type {?string}
*/
get competitionStageStep(){
if (this.type == "CompetitionRanking") return this._data.competitionStageStep;
else return null;
}
/**
* Gets the competition type (if the type is CompetitionRanking)
* @type {?string}
*/
get competitionType(){
if (this.type == "CompetitionRanking") return this._data.competitionType;
else return null;
}
/**
* Gets the Solo Medal type (if the type is SoloMedal)
* @type {?string}
*/
get soloMedalType(){
if (this.type == "SoloMedal") return this._data.trophySoloMedalAchievementType;
else return null;
}
/**
* Gets the solo medal level (if the type is SoloMedal)
* @type {?number}
*/
get soloMedalLevel(){
if (this.type == "SoloMedal") return this._data.detals.level;
else return null;
}
/**
* Gets the server ID of the Live Match (if the type is LiveMatch)
* @type {?string}
*/
get liveMatchServerId(){
if (this.type == "LiveMatch") return this._data.serverId;
else return null;
}
/**
* Gets the game mode of the Live Match (if the type is LiveMatch)
* @type {?string}
*/
get liveMatchGameMode(){
if (this.type == "LiveMatch") return this._data.gameMode;
else return null;
}
/**
* Gets the duration of the Live Match in seconds (if the type is LiveMatch)
* @type {?number}
*/
get liveMatchDuration(){
if (this.type == "LiveMatch") return this._data.duration;
else return null;
}
/**
* Gets the rank of the Live Match (if the type is LiveMatch)
* @type {?number}
*/
get liveMatchRank(){
if (this.type == "LiveMatch") return this._data.details.rank;
else return null;
}
/**
* Gets the trophy rank of the Live Match (if the type is LiveMatch)
* @type {?number}
*/
get liveMatchTrophyRank(){
if (this.type == "LiveMatch") return this._data.details.trophyRanking;
else return null;
}
}
/**
* Represents a player's echelon
*/
class PlayerEchelon {
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object of the player
* @type {Client}
*/
this.client = player.client;
/**
* The echelon number
* @type {number}
*/
this.number = data.echelon;
}
/**
* The name of the echelon
* @type {string}
*/
get name(){
return PlayerEchelonData[this.number].name;
}
/**
* The image URL of the echelon
* @type {string}
*/
get image(){
return PlayerEchelonData[this.number].img;
}
}
/**
* Represents a player's metadata
*/
class PlayerMeta {
constructor(player){
/**
* The player object
* @type {Player}
*/
this.player = player;
}
/**
* The vanity name of the player, if the player has one, otherwise null
* @type {string}
*/
get vanity(){
if (this.player._data.meta && (this.player._data.meta.vanity && this.player._data.vanity != "")) return this.player._data.meta.vanity;
else return null;
}
/**
* The youtube link of the player, if the player has one, otherwise null
* @type {string}
*/
get youtube(){
if (this.player._data.meta && (this.player._data.meta.youtube && this.player._data.youtube != "")) return 'https://www.youtube.com/channel/' + this.player._data.meta.youtube;
else return null;
}
/**
* The twitter link of the player, if the player has one, otherwise null
* @type {string}
*/
get twitter(){
if (this.player._data.meta && (this.player._data.meta.twitter && this.player._data.twitter != "")) return 'https://twitter.com/' + this.player._data.meta.twitter;
else return null;
}
/**
* The twitch channel link of the player, if the player has one, otherwise null
* @type {string}
*/
get twitch(){
if (this.player._data.meta && (this.player._data.meta.twitch && this.player._data.twitch != "")) return 'https://www.twitch.tv/' + this.player._data.meta.twitch;
else return null;
}
/**
* The mastodon profile link of the player, if the player has one, otherwise null
* @type {string}
*/
get mastodon(){
if (this.player._data.meta && (this.player._data.meta.mastodon && this.player._data.mastodon != "")) {
let mastodonDomain = this.player._data.meta.mastodon.substring(this.player._data.meta.mastodon.indexOf('@')+1),
mastodonHandle = this.player._data.meta.mastodon.substring(0, this.player._data.meta.mastodon.indexOf('@'));
return "https://"+mastodonDomain+"/@"+mastodonHandle;
}
else return null;
}
/**
* The display URL of the player
* @type {string}
*/
get displayURL(){
const tmio = this.player.client.options.api.paths.tmio,
player = `${tmio.protocol}://${tmio.host}/#/${tmio.tabs.player}/`;
if (this.vanity != null) {
return player + this.vanity;
} else {
return player + this.player.id;
}
}
/**
* Whether the player is in the TMGL group
* @type {boolean}
*/
get inTMGL(){
if(this.player._data.meta && this.player._data.meta.tmgl) return true;
else return false;
}
/**
* Whether the player is in the Nadeo company
* @type {boolean}
*/
get inNadeo(){
if(this.player._data.meta && this.player._data.meta.nadeo) return true;
else return false;
}
/**
* Whether the player is in the Openplanet & Trackmania.io team
* @type {boolean}
*/
get inTMIOTeam(){
if(this.player._data.meta && this.player._data.meta.team) return true;
else return false;
}
/**
* Whether the player is a Openplanet & Trackmania.io sponsor
* @type {boolean}
*/
get isSponsor(){
if(this.player._data.meta && this.player._data.meta.sponsor) return true;
else return false;
}
/**
* If the player is a sponsor, this returns the sponsor's level
* @type {?number}
*/
get sponsorLevel(){
if (this.isSponsor) return this.player._data.meta.sponsorlevel;
else return null;
}
}
/**
* Represents a player's stats in matchmaking
*/
class PlayerMatchmaking {
constructor(player, type){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = this.player.client;
/**
* The raw data of the player's matchmaking data based on the type
* @type {Object}
* @private
*/
this._data = typeof type == 'string' ? this.player._data.matchmaking.find(m=>m.info.typename == type) : this.player._data.matchmaking.find(m=>m.info.typeid == type);
// throw error if no matchmaking data found
if (!this._data){
throw "No matchmaking data found";
}
}
/**
* The type name of the matchmaking
* @type {string}
*/
get type(){
return this._data.info.typename;
}
/**
* The type ID of the matchmaking
* @type {number}
*/
get typeId(){
return this._data.info.typeid;
}
/**
* The rank of the player on this matchmaking
* @type {number}
*/
get rank(){
return this._data.info.rank;
}
/**
* The total number of players in this matchmaking
* @type {number}
*/
get totalPlayers(){
return this._data.total;
}
/**
* The MMR rank of the player on this matchmaking (score)
* @type {number}
*/
get score(){
return this._data.info.score;
}
/**
* The progression of the player on this matchmaking (can be number of wins for Royal, or score for 3v3)
* @type {number}
*/
get progression(){
return this._data.info.progression;
}
/**
* The division of the player on this matchmaking
* @type {MatchmakingDivision}
*/
get division(){
if (!this._MatchmakingDivision || this._MatchmakingDivision.division !== this._data.info.division.position){
const MatchmakingDivision = require('./MatchmakingDivision');
/**
* The division of the player on this matchmaking
* @type {MatchmakingDivision}
* @private
*/
this._MatchmakingDivision = new MatchmakingDivision(this.client, this.typeId, this._data.info.division);
}
return this._MatchmakingDivision;
}
/**
* The history of recent matches on this matchmaking
* @param {number} [page=0] The page number to get
* @type {Promise<Array<PlayerMatchmakingMatchResult>>}
*/
async history(page = 0){
const player = this.client.options.api.paths.tmio.tabs.player,
matches = this.client.options.api.paths.tmio.tabs.matches,
ReqUtil = require('../util/ReqUtil');
const res = await this.client._apiReq(`${new ReqUtil(this.client).tmioAPIURL}/${player}/${this.player.id}/${matches}/${this.typeId}/${page}`);
if (!res) throw "No matchmaking history found";
let arr = [];
for (let i = 0; i < res.matches.length; i++){
arr.push(new PlayerMatchmakingMatchResult(this.player, res.matches[i]));
}
return arr;
}
}
/**
* Represents a player's matchmaking match result
*/
class PlayerMatchmakingMatchResult {
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = this.player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The player has win the match
* @type {boolean}
*/
get win(){
return this._data.win;
}
/**
* The player has leaved the match
* @type {boolean}
*/
get leave(){
return this._data.leave;
}
/**
* The player is the most valuable player in the match
* @type {boolean}
*/
get mvp(){
return this._data.mvp;
}
/**
* The match LiveID
* @type {string}
*/
get liveId(){
return this._data.lid;
}
/**
* The start date of the match
* @type {Date}
*/
get startDate(){
return new Date(this._data.starttime);
}
/**
* The score of the player after this match
* @type {number}
*/
get afterScore(){
return this._data.afterscore;
}
}
/**
* Represents a player's COTD object
*/
class PlayerCOTD{
constructor(player, data){
/**
* The Player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The number of COTDs played
* @type {number}
*/
get count(){
return this._data.total;
}
/**
* The Player COTD stats
* @type {PlayerCOTDStats}
*/
get stats(){
if (!this._stats){
/**
* The Player COTD stats
* @type {PlayerCOTDStats}
* @private
*/
this._stats = new PlayerCOTDStats(this.player, this._data.stats);
}
return this._stats;
}
/**
* Get the 25 recents COTD results
* @type {Array<PlayerCOTDResult>}
*/
get recentResults(){
const arr = [];
this._data.cotds.forEach(cotd=>{
arr.push(new PlayerCOTDResult(this.player, cotd));
});
return arr;
}
}
/**
* Represents a player's COTD result
*/
class PlayerCOTDResult{
constructor(player, data){
/**
* The Player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The ID of the COTD
* @type {number}
*/
get id(){
return this._data.id;
}
/**
* The date of the COTD
* @type {Date}
* @readonly
*/
get date(){
return new Date(this._data.timestamp);
}
/**
* The name of the COTD
* @type {string}
*/
get name(){
return this._data.name;
}
/**
* The division of the COTD
* @type {number}
*/
get division(){
return this._data.div;
}
/**
* The overall rank on the COTD
* @type {number}
*/
get rank(){
return this._data.rank;
}
/**
* The division rank on the COTD
* @type {number}
*/
get divisionRank(){
return this._data.divrank;
}
/**
* The score of the COTD
* @type {number}
*/
get score(){
return this._data.score;
}
/**
* The total number of players of the COTD
* @type {number}
*/
get totalPlayers(){
return this._data.total;
}
}
/**
* Represents a player's COTD stats
*/
class PlayerCOTDStats{
constructor(player, data){
/**
* The player object
* @type {Player}
*/
this.player = player;
/**
* The client object
* @type {Client}
*/
this.client = player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The best stats in the primary COTD
* @type {PlayerCOTDStatsBest}
*/
get bestPrimary(){
if (!this._bestprimary){
/**
* The best stats in the primary COTD
* @type {PlayerCOTDStatsBest}
* @private
*/
this._bestprimary = new PlayerCOTDStatsBest(this, this._data.bestprimary);
}
return this._bestprimary;
}
/**
* The best stats in all COTDs (including reruns)
* @type {PlayerCOTDStatsBest}
*/
get bestOverall(){
if (!this._bestoverall){
/**
* The best stats in all COTDs (including reruns)
* @type {PlayerCOTDStatsBest}
* @private
*/
this._bestoverall = new PlayerCOTDStatsBest(this, this._data.bestoverall);
}
return this._bestoverall;
}
/**
* The total COTD wins in division 1
* @type {number}
*/
get totalWins(){
return this._data.totalwins;
}
/**
* The total COTD wins in any divison
* @type {number}
*/
get totalDivWins(){
return this._data.totaldivwins;
}
/**
* Average rank, float between 0 and 1
* @type {number}
*/
get averageRank(){
return this._data.avgrank;
}
/**
* Average div rank (in any division), float between 0 and 1
* @type {number}
*/
get averageDivRank(){
return this._data.avgdivrank;
}
/**
* Average division
* @type {number}
*/
get averageDiv(){
return this._data.avgdiv;
}
/**
* The win streak in division 1
* @type {number}
*/
get winStreak(){
return this._data.winstreak;
}
/**
* The win streak in any division
* @type {number}
*/
get divWinStreak(){
return this._data.divwinstreak;
}
}
/**
* Represents a player's COTD stats best stats
*/
class PlayerCOTDStatsBest{
constructor(PlayerCOTDStats, data){
/**
* The PlayerCOTDStats object
* @type {PlayerCOTDStats}
*/
this.stats = PlayerCOTDStats;
/**
* The player object
* @type {Player}
*/
this.player = PlayerCOTDStats.player;
/**
* The client object
* @type {Client}
*/
this.client = this.player.client;
/**
* The data
* @type {Object}
* @private
*/
this._data = data;
}
/**
* The best rank
* @type {number}
*/
get rank(){
return this._data.bestrank;
}
/**
* The best rank date
* @type {Date}
* @readonly
*/
get rankDate(){
return new Date(this._data.bestranktime);
}
/**
* The best div rank
* @type {number}
*/
get divRank(){
return this._data.bestrankdivrank;
}
/**
* The best division
* @type {number}
*/
get division(){
return this._data.bestdiv;
}
/**
* The best divison date
* @type {Date}
* @readonly
*/
get divisionDate(){
return new Date(this._data.bestdivtime);
}
/**
* The best rank in a division
* @type {number}
*/
get rankInDivision(){
return this._data.bestrankindiv;
}
/**
* The best rank in a division date
* @type {Date}
* @readonly
*/
get rankInDivisionDate(){
return new Date(this._data.bestrankindivtime);
}
/**
* The division who got the best rank in a division
* @type {number}
*/
get divisionOfRankInDivision(){
return this._data.bestrankindivdiv;
}
}
module.exports = Player;