mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-09-08 17:25:38 -05:00
689 lines
22 KiB
TypeScript
689 lines
22 KiB
TypeScript
/**
|
|
* Many parts borrowed from POSHO-9000.
|
|
* Refactored at length by @author mia-pi-git
|
|
*/
|
|
import { FS, Utils, Net } from '../../lib';
|
|
import { prefixManager } from './username-prefixes';
|
|
|
|
const MINUTE = 60000;
|
|
const INTERVAL = MINUTE;
|
|
const FACTOR = 1.5;
|
|
|
|
interface TrackerConfig {
|
|
format: string;
|
|
prefix: string;
|
|
rating: number;
|
|
deadline?: string;
|
|
cutoff?: number;
|
|
users?: ID[];
|
|
showdiffs?: boolean;
|
|
}
|
|
|
|
interface Leaderboard {
|
|
current?: LeaderboardEntry[];
|
|
last?: LeaderboardEntry[];
|
|
lookup: Map<string, LeaderboardEntry>;
|
|
}
|
|
|
|
interface LeaderboardEntry {
|
|
name: string;
|
|
rank?: number;
|
|
elo: number;
|
|
gxe: number;
|
|
glicko: number;
|
|
glickodev: number;
|
|
}
|
|
|
|
export class LadderTracker {
|
|
readonly config: TrackerConfig;
|
|
|
|
format: ID;
|
|
prefix: ID;
|
|
private deadline?: Date;
|
|
rating: number;
|
|
users: Set<ID>;
|
|
|
|
private lastid?: string;
|
|
showdiffs?: boolean;
|
|
private started?: NodeJS.Timeout;
|
|
private final?: NodeJS.Timeout;
|
|
|
|
leaderboard: Leaderboard;
|
|
|
|
cooldown?: Date;
|
|
private changed?: boolean;
|
|
lines: { them: number, total: number };
|
|
room: Room;
|
|
|
|
constructor(room: Room, config: TrackerConfig) {
|
|
this.config = config;
|
|
this.room = room;
|
|
|
|
this.format = toID(config.format);
|
|
this.prefix = toID(config.prefix);
|
|
this.rating = config.rating || 0;
|
|
if (config.deadline) this.setDeadline(config.deadline, false);
|
|
|
|
this.users = new Set(config.users);
|
|
this.leaderboard = { lookup: new Map() };
|
|
this.showdiffs = config.showdiffs || false;
|
|
|
|
this.lines = { them: 0, total: 0 };
|
|
}
|
|
|
|
static get(room: Room | string) {
|
|
room = toID(room);
|
|
return trackers[room];
|
|
}
|
|
|
|
addHTML(msg: string, box = false) {
|
|
if (box) msg = `<div class="infobox">${msg}</div>`;
|
|
this.room.add(`|html|${msg}`).update();
|
|
}
|
|
|
|
setDeadline(argument: string, save = true) {
|
|
const date = new Date(argument);
|
|
if (!+date) throw new Chat.ErrorMessage(`Invalid date: ${argument}`);
|
|
|
|
this.deadline = date;
|
|
if (this.final) clearTimeout(this.final);
|
|
// We set the timer to fire slightly before the deadline and then
|
|
// repeatedly do process.nextTick checks for accuracy
|
|
this.final = setTimeout(() => {
|
|
this.stop();
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.captureFinalLeaderboard();
|
|
}, (+this.deadline - Date.now()) - 500);
|
|
|
|
if (save) LadderTracker.save();
|
|
}
|
|
|
|
async captureFinalLeaderboard() {
|
|
const now = new Date();
|
|
if (now < this.deadline!) {
|
|
process.nextTick(this.captureFinalLeaderboard.bind(this));
|
|
return;
|
|
}
|
|
const leaderboard = await this.getLeaderboard();
|
|
this.addHTML(this.styleLeaderboard(leaderboard, +now), true);
|
|
this.deadline = undefined;
|
|
}
|
|
|
|
handleBattleEnd(battle: Rooms.RoomBattle) {
|
|
const skipid = this.lastid;
|
|
const roomid = battle.roomid;
|
|
|
|
const [rating, rmsg] = this.getRating(battle);
|
|
if (!this.tracking(battle, rating) || (skipid && skipid >= roomid)) return;
|
|
|
|
const style = (p: string) => this.stylePlayer(p);
|
|
const msg = `Battle started between ${style(battle.p1.name)} and ${style(battle.p2.name)}`;
|
|
this.addHTML(`<a href="/${roomid}" class="ilink">${msg}. ${rmsg}</a></div>`, true);
|
|
if (!this.lastid || this.lastid < roomid) this.lastid = roomid;
|
|
}
|
|
formatTimeRemaining(ms: number, round?: boolean): string {
|
|
let s = ms / 1000;
|
|
let h = Math.floor(s / 3600);
|
|
let m = Math.floor((s - h * 3600) / 60);
|
|
s = s - h * 3600 - m * 60;
|
|
|
|
if (round) {
|
|
s = Math.round(s);
|
|
if (s === 60) {
|
|
s = 0;
|
|
m++;
|
|
}
|
|
if (m === 60) {
|
|
m = 0;
|
|
h++;
|
|
}
|
|
}
|
|
|
|
const time = [];
|
|
if (h > 0) time.push(`${h} hour${h === 1 ? '' : 's'}`);
|
|
if (m > 0) time.push(`${m} minute${m === 1 ? '' : 's'}`);
|
|
if (s > 0) time.push(`${s} second${s === 1 ? '' : 's'}`);
|
|
return time.join(' ');
|
|
}
|
|
|
|
getRating(battle: Rooms.RoomBattle): [number, string] {
|
|
const p1 = this.leaderboard.lookup.get(toID(battle.p1.name));
|
|
const p2 = this.leaderboard.lookup.get(toID(battle.p2.name));
|
|
if (p1 && p2) return this.averageRating(p1.elo, p2.elo);
|
|
const minElo = Math.floor(battle.rated);
|
|
if (p1 && p1.elo > minElo) return this.averageRating(p1.elo, minElo);
|
|
if (p2 && p2.elo > minElo) return this.averageRating(p2.elo, minElo);
|
|
return [minElo, `(min rating: ${minElo})`];
|
|
}
|
|
|
|
averageRating(a: number, b: number): [number, string] {
|
|
const rating = Math.round((a + b) / 2);
|
|
return [rating, `(avg rating: ${rating})`];
|
|
}
|
|
|
|
stylePlayer(player: string) {
|
|
return `<username>${player}</username>`;
|
|
}
|
|
|
|
tracking(battle: Rooms.RoomBattle, rating: number) {
|
|
const minElo = Math.floor(Number(battle.rated) || 0);
|
|
if (battle.format !== this.config.format) return false;
|
|
if (!minElo || minElo < 1000) {
|
|
return false;
|
|
}
|
|
const p1 = toID(battle.p1.name);
|
|
const p2 = toID(battle.p2.name);
|
|
|
|
// If we are tracking users and a player in the game is one of them, report the battle
|
|
if (this.users.size && (this.users.has(p1) || this.users.has(p2))) {
|
|
return true;
|
|
}
|
|
|
|
// If a player has an our prefix, report if the battle is above the required rating
|
|
if (p1.startsWith(this.prefix) || p2.startsWith(this.prefix)) {
|
|
return rating >= this.rating;
|
|
}
|
|
|
|
// Report if a cutoff has been set and both prefixed players are within a factor of the cutoff
|
|
if (this.config.cutoff && p1.startsWith(this.prefix) && p2.startsWith(this.prefix)) {
|
|
const a = this.leaderboard.lookup.get(p1);
|
|
const b = this.leaderboard.lookup.get(p2);
|
|
const rank = this.config.cutoff * FACTOR;
|
|
return a?.rank && a.rank <= rank && b?.rank && b.rank <= rank;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
leaderboardCooldown(now: Date) {
|
|
if (!this.cooldown) return true;
|
|
const wait = Math.floor((+now - +this.cooldown) / MINUTE);
|
|
const lines = this.changed ? this.lines.them : this.lines.total;
|
|
if (lines < 5 && wait < 3) return false;
|
|
const factor = this.changed ? 6 : 1;
|
|
return factor * (wait + lines) >= 60;
|
|
}
|
|
|
|
getDeadline(now: Date) {
|
|
if (!this.deadline) {
|
|
return ('No deadline has been set.');
|
|
} else {
|
|
return (`**Time Remaining:** ${this.formatTimeRemaining(+this.deadline - +now, true)}`);
|
|
}
|
|
}
|
|
|
|
tracked() {
|
|
if (!this.users.size) {
|
|
return ('Not currently tracking any users.');
|
|
} else {
|
|
const users = Array.from(this.users.values()).join(', ');
|
|
const plural = this.users.size === 1 ? 'user' : 'users';
|
|
return (`Currently tracking <b>${this.users.size}</b> ${plural}: ${users}`);
|
|
}
|
|
}
|
|
|
|
async getLeaderboard(display?: boolean) {
|
|
const url = `https://pokemonshowdown.com/ladder/${this.format}.json?prefix=${this.prefix}`;
|
|
const leaderboard: LeaderboardEntry[] = [];
|
|
try {
|
|
const response = await Net(url).get().then(JSON.parse);
|
|
this.leaderboard.lookup = new Map();
|
|
for (const data of response.toplist) {
|
|
// TODO: move the rounding until later
|
|
const entry: LeaderboardEntry = {
|
|
name: data.username,
|
|
elo: Math.round(data.elo),
|
|
gxe: data.gxe,
|
|
glicko: Math.round(data.rpr),
|
|
glickodev: Math.round(data.rprd),
|
|
};
|
|
this.leaderboard.lookup.set(data.userid, entry);
|
|
if (!data.userid.startsWith(this.prefix)) continue;
|
|
entry.rank = leaderboard.length + 1;
|
|
leaderboard.push(entry);
|
|
}
|
|
if (display) {
|
|
this.addHTML(this.styleLeaderboard(leaderboard), true);
|
|
this.leaderboard.last = leaderboard;
|
|
this.changed = false;
|
|
this.lines = { them: 0, total: 0 };
|
|
}
|
|
} catch (err: any) {
|
|
Monitor.crashlog(err, 'a ladder tracker request', this.config);
|
|
if (display) throw new Chat.ErrorMessage(`Unable to fetch the leaderboard for ${this.prefix}.`);
|
|
}
|
|
|
|
return leaderboard;
|
|
}
|
|
|
|
static getTracker(context: Chat.CommandContext) {
|
|
const room = context.requireRoom();
|
|
const t = this.get(room);
|
|
if (!t) throw new Chat.ErrorMessage(`There is no active ladder tracker in this room.`);
|
|
return t;
|
|
}
|
|
|
|
static save() {
|
|
FS('config/chat-plugins/laddertrackers.json').writeUpdate(() => {
|
|
const out: Record<string, TrackerConfig> = {};
|
|
for (const roomid in trackers) {
|
|
trackers[roomid].restoreConfig();
|
|
out[roomid] = trackers[roomid].config;
|
|
}
|
|
return JSON.stringify(out);
|
|
});
|
|
}
|
|
|
|
restoreConfig() {
|
|
this.config.users = [...this.users];
|
|
this.config.showdiffs = this.showdiffs;
|
|
this.config.rating = this.rating;
|
|
this.config.prefix = this.prefix;
|
|
this.config.deadline = this.deadline?.toString();
|
|
}
|
|
|
|
styleLeaderboard(leaderboard: LeaderboardEntry[], final?: number) {
|
|
const diffs = this.leaderboard.last && !final ?
|
|
this.getDiffs(this.leaderboard.last, leaderboard) :
|
|
new Map();
|
|
let buf = '<center>';
|
|
if (final) {
|
|
buf +=
|
|
`<h1 style="margin-bottom: 0.2em">Final Leaderboard - ${this.prefix}</h1>` +
|
|
`<div style="margin-bottom: 1em"><small><em>${final}</em></small></div>`;
|
|
}
|
|
buf +=
|
|
'<div class="ladder" style="max-height: 250px; overflow-y: auto"><table>' +
|
|
'<tr><th></th><th>Name</th><th><abbr title="Elo rating">Elo</abbr></th>' +
|
|
'<th><abbr title="user\'s percentage chance of winning a random battle (aka GLIXARE)">GXE</abbr></th>' +
|
|
'<th><abbr title="Glicko-1 rating system: rating±deviation (provisional if deviation>100)">Glicko-1</abbr></th></tr>';
|
|
for (const [i, p] of leaderboard.entries()) {
|
|
const id = toID(p.name);
|
|
const link = `https://www.smogon.com/forums/search/1/?q="${encodeURIComponent(p.name)}"`;
|
|
const diff = diffs.get(id);
|
|
let rank = `${i + 1}`;
|
|
if (diff) {
|
|
const symbol = diff[2] < diff[3] ?
|
|
'<span style="color: #F00">▼</span>' :
|
|
'<span style="color: #008000">▲</span>';
|
|
rank = `${symbol}${rank}`;
|
|
}
|
|
buf +=
|
|
`<tr><td style="text-align: right"><a href='${link}' class="subtle">${rank}</a></td>` +
|
|
Utils.html`<td><username class="username">${p.name}</username></td>` +
|
|
`<td><strong>${p.elo}</strong></td><td>${p.gxe.toFixed(1)}%</td>` +
|
|
`<td>${p.glicko} ± ${p.glickodev}</td></tr>`;
|
|
}
|
|
buf += '</table></div></center>';
|
|
return buf;
|
|
}
|
|
|
|
getDiffs(last: LeaderboardEntry[], current: LeaderboardEntry[], num?: number) {
|
|
const diffs = new Map<string, [string, number, number, number]>();
|
|
|
|
const lastN = num ? last.slice(0, num) : last;
|
|
for (const [i, player] of lastN.entries()) {
|
|
const id = toID(player.name);
|
|
const oldrank = i + 1;
|
|
let newrank = current.findIndex(e => toID(e.name) === id) + 1;
|
|
let elo: number;
|
|
if (!newrank) {
|
|
newrank = Infinity;
|
|
elo = 0;
|
|
} else {
|
|
elo = current[newrank - 1].elo;
|
|
}
|
|
if (oldrank !== newrank) diffs.set(id, [player.name, elo, oldrank, newrank]);
|
|
}
|
|
|
|
const currentN = num ? current.slice(0, num) : current;
|
|
for (const [i, player] of currentN.entries()) {
|
|
const id = toID(player.name);
|
|
const newrank = i + 1;
|
|
let oldrank = last.findIndex(e => toID(e.name) === id) + 1;
|
|
if (!oldrank) oldrank = Infinity;
|
|
if (oldrank !== newrank) diffs.set(id, [player.name, player.elo, oldrank, newrank]);
|
|
}
|
|
|
|
return diffs;
|
|
}
|
|
|
|
trackChanges(leaderboard: LeaderboardEntry[], display?: boolean) {
|
|
if (!this.leaderboard.current || !this.config.cutoff) return;
|
|
const n = this.config.cutoff;
|
|
const diffs = this.getDiffs(this.leaderboard.current, leaderboard, n * FACTOR);
|
|
if (!diffs.size) return;
|
|
|
|
const sorted = Array.from(diffs.values()).sort((a, b) => a[3] - b[3]);
|
|
const messages = [];
|
|
for (const [name, elo, oldrank, newrank] of sorted) {
|
|
if (!((oldrank > n && newrank <= n) || (oldrank <= n && newrank > n))) {
|
|
this.changed = true;
|
|
}
|
|
|
|
if (display) {
|
|
const symbol = oldrank < newrank ? '▼' : '▲';
|
|
const rank = newrank === Infinity ? '?' : newrank;
|
|
const rating = elo || '?';
|
|
const message = newrank > n ? `<i>${name} (${rating})</i>` : `${name} (${rating})`;
|
|
messages.push(`${symbol}<b>${rank}.</b> ${message}`);
|
|
}
|
|
}
|
|
|
|
if (display) this.addHTML(messages.join(' '));
|
|
}
|
|
|
|
setPrefix(prefix: string, save = true) {
|
|
const oldPrefix = this.prefix;
|
|
this.prefix = toID(prefix);
|
|
if (oldPrefix !== prefix) {
|
|
if (save) LadderTracker.save();
|
|
this.togglePrefix(oldPrefix);
|
|
}
|
|
}
|
|
|
|
togglePrefix(oldPrefix?: ID) {
|
|
if (!this.room.settings.isPrivate) {
|
|
try {
|
|
if (oldPrefix) prefixManager.removePrefix(oldPrefix, 'privacy');
|
|
} catch {} // suppress errorMessages in case it's the first start and it hasn't been made priv yet
|
|
try {
|
|
prefixManager.addPrefix(this.prefix, 'privacy');
|
|
} catch {} // same as above
|
|
}
|
|
}
|
|
|
|
start() {
|
|
if (this.started) return;
|
|
|
|
this.togglePrefix();
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
this.started = setInterval(async () => {
|
|
// Leaderboard
|
|
const leaderboard = await this.getLeaderboard();
|
|
if (!leaderboard.length) return;
|
|
if (this.leaderboard) this.trackChanges(leaderboard, this.showdiffs);
|
|
this.leaderboard.current = leaderboard;
|
|
}, INTERVAL);
|
|
}
|
|
|
|
stop() {
|
|
if (this.started) {
|
|
clearInterval(this.started);
|
|
this.started = undefined;
|
|
this.leaderboard.current = undefined;
|
|
this.leaderboard.last = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const trackers: Record<string, LadderTracker> = {};
|
|
try {
|
|
const data = JSON.parse(FS("config/chat-plugins/laddertrackers.json").readIfExistsSync() || "{}");
|
|
for (const roomid in data) {
|
|
const room = Rooms.search(roomid);
|
|
if (!room) {
|
|
Monitor.adminlog(`Missing room ${roomid} for ladder tracker.`);
|
|
continue;
|
|
}
|
|
trackers[roomid] = new LadderTracker(room, data[roomid]);
|
|
trackers[roomid].start();
|
|
}
|
|
} catch {}
|
|
|
|
export const commands: Chat.ChatCommands = {
|
|
ld: 'laddertrack',
|
|
laddertrack: {
|
|
override: '',
|
|
''(target, room, user, connection, cmd) {
|
|
room = this.requireRoom();
|
|
this.checkCan('declare', null, room);
|
|
if (!toID(target)) {
|
|
return this.parse(`/j view-laddertrack-${room.roomid}`);
|
|
}
|
|
if (trackers[room.roomid] && !cmd.includes('override')) {
|
|
return this.errorReply(
|
|
`There is already a tracker for this room. Use /laddertrack override if you want to set a new one.`
|
|
);
|
|
}
|
|
const config: Partial<TrackerConfig> = {};
|
|
/* cutoff?: number; */
|
|
const rating = Number(/rating=([0-9]+)/.exec(target)?.[1]);
|
|
if (!rating || rating < 1000) {
|
|
return this.errorReply("Invalid rating. Must be a number above 1000.");
|
|
}
|
|
config.rating = rating;
|
|
const rawCutoff = /cutoff=([0-9]+)/.exec(target)?.[1];
|
|
const cutoff = Number(rawCutoff);
|
|
if (rawCutoff && (!cutoff || cutoff < 1000)) {
|
|
return this.errorReply("Invalid cutoff. Must be a number above 1000.");
|
|
}
|
|
if (cutoff) {
|
|
config.cutoff = cutoff;
|
|
}
|
|
const deadline = /deadline=([^,]+)/i.exec(target)?.[1];
|
|
if (deadline) {
|
|
const date = new Date(deadline);
|
|
if (!+date) {
|
|
return this.errorReply("Invalid date.");
|
|
}
|
|
config.deadline = date.toString();
|
|
}
|
|
const format = toID(/format=([a-zA-Z0-9]+)/i.exec(target)?.[1]);
|
|
if (!format) {
|
|
return this.errorReply("You must specify a format.");
|
|
}
|
|
config.format = format;
|
|
const prefix = toID(/prefix=([a-zA-Z0-9]+)/i.exec(target)?.[1]);
|
|
if (!prefix) {
|
|
return this.errorReply("You must specify a prefix.");
|
|
}
|
|
config.prefix = prefix;
|
|
|
|
trackers[room.roomid] = new LadderTracker(room, config as TrackerConfig);
|
|
trackers[room.roomid].start();
|
|
LadderTracker.save();
|
|
|
|
this.sendReply("Ladder tracking started.");
|
|
this.modlog('LADDERTRACK', null, Object.entries(config).map(([k, v]) => `${k}=${v}`).join(', '));
|
|
},
|
|
top: 'leaderboard',
|
|
async leaderboard(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.runBroadcast();
|
|
await tracker.getLeaderboard(true);
|
|
},
|
|
prefix(target, room, user, sf) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
target = toID(target);
|
|
if (target) {
|
|
this.checkCan('mute', null, tracker.room);
|
|
tracker.setPrefix(target);
|
|
LadderTracker.save();
|
|
this.modlog(`TRACKER PREFIX`, null, target);
|
|
this.addModAction(`${user.name} updated the ladder tracker prefix to ${target}`);
|
|
} else {
|
|
this.runBroadcast();
|
|
}
|
|
this.sendReply(`Current prefix: ${tracker.prefix}`);
|
|
},
|
|
remaining: 'deadline',
|
|
deadline(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
if (!tracker) return;
|
|
if (target) {
|
|
this.checkCan('mute', null, tracker.room);
|
|
const date = new Date(target);
|
|
if (!+date) {
|
|
return this.errorReply("Invalid date.");
|
|
}
|
|
tracker.setDeadline(target);
|
|
LadderTracker.save();
|
|
} else {
|
|
this.runBroadcast();
|
|
}
|
|
this.sendReplyBox(tracker.getDeadline(new Date()));
|
|
},
|
|
elo: 'rating',
|
|
rating(target, room, user, sf, cmd) {
|
|
room = this.requireRoom();
|
|
const tracker = LadderTracker.get(room);
|
|
this.runBroadcast();
|
|
if (!tracker) return this.errorReply(`There is no active ladder tracker for this room.`);
|
|
const rating = Number(target);
|
|
if (target) {
|
|
if (!rating) return this.errorReply("Invalid rating.");
|
|
this.checkCan('mute', null, room);
|
|
if (rating < 1000) return this.errorReply("Invalid rating. Must be a number above 1000.");
|
|
tracker.rating = rating;
|
|
tracker.config.rating = rating;
|
|
LadderTracker.save();
|
|
this.addModAction(`${user.name} updated the ladder tracker rating to ${rating}.`);
|
|
this.modlog(`LADDERTRACK RATING`, null, target);
|
|
} else {
|
|
this.runBroadcast();
|
|
}
|
|
this.sendReplyBox(`<b>Rating:</b> ${tracker.rating}`);
|
|
},
|
|
cutoff(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
if (!tracker) return;
|
|
const rating = Number(target);
|
|
if (target) {
|
|
if (!rating) return this.errorReply("Invalid cutoff.");
|
|
this.checkCan('mute', null, tracker.room);
|
|
tracker.config.cutoff = rating;
|
|
LadderTracker.save();
|
|
this.modlog(`TRACKER CUTOFF`, null, `${rating}`);
|
|
}
|
|
this.sendReplyBox(`<b>Cutoff:</b> ${tracker.config.cutoff || 0}`);
|
|
},
|
|
add: 'watch',
|
|
track: 'watch',
|
|
follow: 'watch',
|
|
watch(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
for (const userid of target.split(',').map(toID)) {
|
|
if (userid) {
|
|
tracker.users.add(userid);
|
|
}
|
|
}
|
|
this.modlog(`LADDER ADDTRACKUSER`, null, target);
|
|
this.sendReplyBox(tracker.tracked());
|
|
LadderTracker.save();
|
|
},
|
|
untrack: 'unwatch',
|
|
unfollow: 'unwatch',
|
|
remove: 'unwatch',
|
|
unwatch(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
for (const userid of target.split(',').map(toID)) {
|
|
if (userid) {
|
|
tracker.users.delete(userid);
|
|
}
|
|
}
|
|
this.modlog(`LADDER UNTRACKUSER`, null, target);
|
|
this.sendReplyBox(tracker.tracked());
|
|
LadderTracker.save();
|
|
},
|
|
tracked: 'tracking',
|
|
watched: 'tracking',
|
|
followed: 'tracking',
|
|
watching: 'tracking',
|
|
tracking(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
this.runBroadcast();
|
|
this.sendReplyBox(tracker.tracked());
|
|
},
|
|
start(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
tracker.start();
|
|
LadderTracker.save();
|
|
},
|
|
pause(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
tracker.stop();
|
|
LadderTracker.save();
|
|
},
|
|
end: 'endtrack',
|
|
endtrack(target, room, user, nike, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
tracker.stop();
|
|
delete trackers[tracker.room.roomid];
|
|
LadderTracker.save();
|
|
this.modlog(`LADDER ENDTRACK`, null);
|
|
this.addModAction(`${user.name} ended the active ladder tracker.`);
|
|
},
|
|
hidediffs: 'showdiffs',
|
|
showdiffs(target, room, user, sf, cmd) {
|
|
const tracker = LadderTracker.getTracker(this);
|
|
this.checkCan('mute', null, tracker.room);
|
|
const setting = !cmd.includes('hide');
|
|
if (tracker.showdiffs === setting) {
|
|
return this.sendReply(`Differences are already ${setting ? 'shown' : 'hidden'}.`);
|
|
}
|
|
tracker.showdiffs = setting;
|
|
this.addModAction(`${user.name} set ladder tracker showdiffs ${setting ? 'on' : 'off'}.`);
|
|
LadderTracker.save();
|
|
},
|
|
},
|
|
laddertrackhelp() {
|
|
this.runBroadcast();
|
|
this.sendReplyBox([
|
|
`- /laddertrack OR /ld displays a page to start a tracker (requires # in the room to do so).`,
|
|
// ` (can also be used with key=value formatted args to start it directly - requires keys: rating, prefix, format, deadline, cutoff)`,
|
|
` - /laddertrack [leaderboard/top] - updates and displays the current leaderboard`,
|
|
` - /laddertrack deadline [optional date] - displays the current deadline, or sets the deadline to the given deadline if one is given (requires % to do so)`,
|
|
` - /laddertrack rating [optional rating] - displays the current rating, or sets the rating to the given rating if one is given (requires % to do so)`,
|
|
` - /laddertrack watch [optional list,of,users] - tracks the given user's battles (requires % to do so)`,
|
|
` - /laddertrack unwatch [list,of,users] - stops tracking the given user's battles (requires % to do so)`,
|
|
` - /laddertrack tracking - displays the current users being tracked`,
|
|
` - /laddertrack start - starts the tracker`,
|
|
` - /laddertrack pause - temporarily pauses the tracker`,
|
|
` - /laddertrack endtrack - ends the tracker entirely`,
|
|
` - /laddertrack showdiffs - shows or hides differences between the current leaderboard and the previous leaderboard`,
|
|
`All commands require % to be used to edit settings, and can be used by anyone to view settings.`,
|
|
].join('<br />'));
|
|
},
|
|
};
|
|
|
|
export const pages: Chat.PageTable = {
|
|
laddertrack(query, user) {
|
|
const room = this.requireRoom();
|
|
let buf = ``;
|
|
this.title = '[Ladder Tracker] Setup';
|
|
buf += `<div class="pad"><h2>Ladder tracker setup</h2><hr />`;
|
|
buf += `<div class="infobox">`;
|
|
let cmd = `/msgroom ${room},/laddertrack `;
|
|
const keys = ['rating', 'prefix', 'format', 'deadline', 'cutoff'];
|
|
const required = ['rating', 'prefix', 'format'];
|
|
cmd += keys.map(k => `${k}={${k}}`).join(', ');
|
|
buf += `<form data-submitsend="${cmd}">`;
|
|
for (const key of keys) {
|
|
buf += `<label>${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
if (required.includes(key)) {
|
|
buf += ` (required)`;
|
|
}
|
|
buf += `:</label>`;
|
|
buf += ` <input name="${key}" />`;
|
|
buf += `<br />`;
|
|
}
|
|
buf += `<button class="button notifying" type="submit">Create</button>`;
|
|
buf += `</form></div></div>`;
|
|
return buf;
|
|
},
|
|
};
|
|
|
|
export const handlers: Chat.Handlers = {
|
|
onBattleCreate(battle, players) {
|
|
for (const tracker of Object.values(trackers)) {
|
|
tracker.handleBattleEnd(battle);
|
|
}
|
|
},
|
|
};
|