This commit is contained in:
Brandon 2023-05-21 16:27:56 +01:00
commit f5b674b160
31 changed files with 1778 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
yarn.lock

31
disabled/giveloss.js Normal file
View File

@ -0,0 +1,31 @@
import { giveLoss } from "../ranked/profile.js"
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
export default class GiveLoss extends Command {
constructor (client) {
super(client, {
name: "giveloss",
description: "Gives a player a loss.",
options: [
{
name: "player",
description: "The player that will recieve the loss.",
required: true,
type: "USER"
}
]
});
}
run (interaction) {
const target = interaction.options.get("player").value
giveLoss(target)
.then(() => {
interaction.editReply({ embeds: [MessageHelper.success("A loss has been added.")] })
})
.catch(err => {
interaction.editReply({ embeds: [MessageHelper.error(err)] })
})
}
}

33
disabled/givewin.js Normal file
View File

@ -0,0 +1,33 @@
import { giveWin } from "../ranked/profile.js"
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import RankedProfile from "../model/RankedProfile.js";
import { updateRank } from "../ranked/profile.js";
export default class GiveWin extends Command {
constructor (client) {
super(client, {
name: "givewin",
description: "Gives a player a win.",
options: [
{
name: "player",
description: "The player that will recieve the win.",
required: true,
type: "USER"
}
]
});
}
run (interaction) {
const target = interaction.options.get("player").value
giveWin(target)
.then(() => {
interaction.editReply({ embeds: [MessageHelper.success("A win has been added")] })
})
.catch(err => {
interaction.editReply({ embeds: [MessageHelper.error(err)] })
})
}
}

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "ranked-bot",
"version": "1.0.0",
"description": "A bot that manages ranked matches, ...",
"type": "module",
"main": "./src/index.js",
"author": "Beanes",
"dependencies": {
"discord.js": "^13.6.0",
"mongoose": "^6.2.10",
"node-fetch": "^3.2.3",
"redis": "^4.0.6"
}
}

89
src/base/Client.js Normal file
View File

@ -0,0 +1,89 @@
import { Client, Collection } from "discord.js";
import { resolve, join } from "path"
import { readdir } from "fs/promises";
import redis from 'redis';
import mongoose from "mongoose";
import config from "./Config.js"
import { onRedisMessage, startPing, startQueueCheck, cleanOldGames, startDodgeCheck } from "../ranked/game.js"
class CustomClient extends Client {
constructor() {
super({
intents: ["GUILDS", "GUILD_MESSAGES", "GUILD_VOICE_STATES", "GUILD_MEMBERS"]
});
this.events = new Collection();
this.commands = new Collection();
this.setupRedis(config.redis);
this.setupDatabase(config.mongo)
this.on("ready", () => {
this.loadCommands("./src/commands");
this.loadEvents("./src/events");
startQueueCheck(this);
startPing(this);
cleanOldGames(this);
startDodgeCheck(this);
console.log(`Client started. Node version: ${process.version}. Platform: ${process.platform}. PID ID: ${process.pid}. User ID: ${this.user.id}`);
}).on("error", console.error).on("warn", console.warn);
}
login(token) {
super.login(token);
}
async setupRedis(url) {
this.redis = redis.createClient({ url });
this.redis.on('error', (err) => console.log('Redis Client Error', err));
await this.redis.connect();
const subscriber = this.redis.duplicate();
await subscriber.connect()
await subscriber.pSubscribe('rankedhcf:*', (message, channel) => {
onRedisMessage(this, channel, message)
});
}
async setupDatabase(url) {
await mongoose.connect(url, {
useNewUrlParser: true,
})
}
async loadCommands(path) {
const guild = this.guilds.cache.get(config.guild)
readdir(path)
.then(files => {
files.forEach(async file => {
const load = resolve(join(path, file));
const CMD = await import("file://" + load);
const command = new CMD.default(this); // eslint-disable-line
console.log("Loaded command: " + command.constructor.name.toLowerCase())
this.commands.set(command.constructor.name.toLowerCase(), command);
guild.commands.create(command.data)
});
})
.catch(err => {
console.log(err);
});
}
loadEvents(path) {
readdir(path)
.then(files => {
files.forEach(async file => {
const load = resolve(join(path, file));
const EVT = await import("file://" + load);
const event = new EVT.default(this); // eslint-disable-line
console.log("Loaded event: " + event.constructor.name.toLowerCase())
this.events.set(event.constructor.name.toLowerCase(), event);
super.on(file.split(".")[0], (...args) => event.run(...args));
});
})
.catch(err => {
console.log(err);
});
}
}
export default CustomClient;

6
src/base/Command.js Normal file
View File

@ -0,0 +1,6 @@
export default class Command {
constructor(client, data) {
this.client = client;
this.data = data;
}
}

86
src/base/Config.js Normal file
View File

@ -0,0 +1,86 @@
export default {
"token": "NzE5ODc1NDk2NzQxMDQ0MzE0.Xt9yTQ.AomxJyZiGypwpzq7D-qeOeWiq50",
"guild": "960192473928458250",
"redis": "redis://:penis69@51.222.244.184:6379",
"mongo": "mongodb://51.222.244.184:27017",
"category": "960192475467759688",
"queue": ["964998749749379082", "973927606678343731"],
"feed": "968171596432936960",
"dodgefeed": "969657767700865104",
"roles": {
"registered": "969334690626543687"
},
"maps": [
{ "id": "NetherRoad2", "name": "Nether Road 2" },
{ "id": "NetherRoad4", "name": "Nether Road 4" },
{ "id": "CitadelAlphaMap1", "name": "Alpha Map 1" },
{ "id": "CitadelSand", "name": "Citadel Sand" },
{ "id": "JapCity", "name": "Jap City" }
],
"ranks": [
{
"name": "Bronze",
"role": "969659135220146206",
"min": 0,
"max": 100,
"gain": 30,
"loss": 10
},
{
"name": "Iron",
"role": "969658996141219900",
"min": 101,
"max": 200,
"gain": 25,
"loss": 10
},
{
"name": "Gold",
"role": "960260010003300372",
"min": 201,
"max": 300,
"gain": 20,
"loss": 15
},
{
"name": "Platinum",
"role": "961557649781059654",
"min": 301,
"max": 400,
"gain": 15,
"loss": 15
},
{
"name": "Diamond",
"role": "960260055737987172",
"min": 401,
"max": 600,
"gain": 15,
"loss": 20
},
{
"name": "Master",
"role": "961557597490663435",
"min": 601,
"max": 800,
"gain": 10,
"loss": 25
},
{
"name": "G-Master",
"role": "961562776810168320",
"min": 801,
"max": 1000,
"gain": 10,
"loss": 30
},
{
"name": "Challenger",
"role": "960258699291664414",
"min": 1001,
"max": 99999999,
"gain": 10,
"loss": 40
}
]
}

42
src/base/MessageHelper.js Normal file
View File

@ -0,0 +1,42 @@
import { MessageEmbed } from "discord.js";
const footer = '⚔️ Ranked Season 1 ⚔️'
function blank() {
return new MessageEmbed(this.message);
}
/**
* @returns {MessageEmbed}
*/
function error(message, title = "Error") {
return new MessageEmbed(this.message)
.setTitle(`» ${title}`)
.setDescription(message)
.setColor(15217152)
.setFooter({ text: footer });
}
/**
* @returns {MessageEmbed}
*/
function info(message, title) {
return new MessageEmbed(this.message)
.setTitle(title ? `» ${title}` : "")
.setDescription(message)
.setColor("#272727")
.setFooter({ text: footer });
}
/**
* @returns {MessageEmbed}
*/
function success(message, title) {
return new MessageEmbed(this.message)
.setTitle(title ? `» ${title}` : "")
.setDescription(message)
.setColor(572788)
.setFooter({ text: footer });
}
export default { blank, error, info, success }

20
src/commands/close.js Normal file
View File

@ -0,0 +1,20 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
export default class Close extends Command {
constructor (client) {
super(client, {
name: "close",
description: "Close the ticket channel.",
options: []
});
}
async run (interaction) {
if (interaction.channel.parent.name.toLowerCase() === "tickets") {
interaction.channel.delete()
} else {
interaction.reply({ embeds: [MessageHelper.error("You can only run this in ticket channels!")] });
}
}
}

48
src/commands/embed.js Normal file
View File

@ -0,0 +1,48 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
export default class Embed extends Command {
constructor(client) {
super(client, {
name: "embed",
description: "Sends a message as an embed.",
options: [
{
name: "channel",
description: "The channel to send the message to.",
required: true,
type: "CHANNEL"
},
{
name: "message",
description: "The text that will be in the embed.",
required: true,
type: "STRING"
}
]
});
}
run(interaction) {
const channel = interaction.options.get("channel").channel;
if (channel.type !== "GUILD_TEXT" && channel.type !== "GUILD_NEWS") {
return interaction.reply({ embeds: [MessageHelper.error("That is not a channel!")] });
}
const msg = interaction.options.get("message").value;
channel.send({
embeds: [
MessageHelper.blank()
.setAuthor({
name: interaction.user.username,
iconURL: interaction.user.avatarURL()
})
.setColor("#272727")
.setDescription(msg)
]
}).then(() => {
interaction.editReply({ embeds: [MessageHelper.success("The message has been sent!")], ephemeral: true })
});
}
}

53
src/commands/game.js Normal file
View File

@ -0,0 +1,53 @@
import Command from "../base/Command.js";
import { states } from "../ranked/game.js"
import RankedGame from "../model/RankedGame.js";
import MessageHelper from "../base/MessageHelper.js";
export default class Game extends Command {
constructor(client) {
super(client, {
name: "game",
description: "Get information of a game",
options: [
{
name: "game",
description: "The id of the game you wish to get information from.",
required: true,
type: "STRING"
}
]
});
}
async run(interaction) {
const _id = interaction.options.get("game").value
const game = await RankedGame.findOne({ _id })
if (!game) {
return interaction.editReply({ embeds: [MessageHelper.error("That game was not found!")] })
}
const embed = MessageHelper.success("Invalid Data", `Game Info [${game._id}]`);
switch (game.state) {
case states.MOVING:
case states.PICKING:
case states.VOIDED:
case states.PREGAME:
case states.STARTED:
case states.VOTING:
embed.setDescription(`**State:** ${game.state}
**Team One**:
Captain: <@${game.team1.captain}>
${game.team1.players.filter(player => player != game.team1.captain).map(player => `<@${player}>`).join("\n")}
**Team Two:**
Captain: <@${game.team2.captain}>
${game.team2.players.filter(player => player != game.team2.captain).map(player => `<@${player}>`).join("\n")}`);
break;
case states.ENDED:
embed.setDescription(`**State:** ${game.state}
**Elo Changes**:
${Object.entries(game.eloChanges).map(eloChange => `<@${eloChange[0]}>: **${eloChange[1] > 0 ? "+" : ""}${eloChange[1]}**`).join("\n")}`);
break;
}
interaction.editReply({ embeds: [embed] })
}
}

23
src/commands/games.js Normal file
View File

@ -0,0 +1,23 @@
import Command from "../base/Command.js";
import RankedGame from "../model/RankedGame.js"
import { states } from "../ranked/game.js";
import config from "../base/Config.js"
import MessageHelper from "../base/MessageHelper.js";
export default class Games extends Command {
constructor(client) {
super(client, {
name: "games",
description: "All games that are not ended or voided."
});
}
async run(interaction) {
const games = await RankedGame.find({ "state": { "$nin": [states.ENDED, states.VOIDED] } })
interaction.editReply({
embeds: [MessageHelper.success(`${games.map(game =>
`**Game:** ${game._id} **Map:** ${game.map} **State:** ${game.state} **Captain 1:** <@${game.team1.captain}> **Captain 2:** <@${game.team2.captain}>\n`
).join("\n")}`, "Games")]
})
}
}

View File

@ -0,0 +1,31 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import { getLeaderboard } from "../ranked/profile.js";
export default class Leaderboard extends Command {
constructor (client) {
super(client, {
name: "leaderboard",
description: "Check the current leaderboard."
});
}
async run (interaction) {
const leaderboard = await getLeaderboard()
const lines = [];
const first = leaderboard.shift();
lines.push(`🥇 <@${first.id}>`)
const second = leaderboard.shift();
if (second) lines.push(`🥈 <@${second.id}>`)
const third = leaderboard.shift();
if (third) lines.push(`🥉 <@${third.id}>`)
let i = 4;
while (leaderboard.length > 0) {
const other = leaderboard.shift()
lines.push(`**${i}.** <@${other.id}>`)
i++;
}
interaction.editReply({ embeds: [MessageHelper.success(lines.join("\n"), "Leaderboard")] })
}
}

18
src/commands/ranks.js Normal file
View File

@ -0,0 +1,18 @@
import Command from "../base/Command.js";
import config from "../base/Config.js"
import MessageHelper from "../base/MessageHelper.js";
export default class Ranks extends Command {
constructor (client) {
super(client, {
name: "ranks",
description: "View all the ranks with the amount of elo required"
});
}
async run (interaction) {
interaction.editReply({ embeds: [MessageHelper.success(`${config.ranks.map(r =>
`${r.max == 99999999 ? `[${r.min - 1}+]` : `[${r.min > 0 ? r.min - 1 : r.min}-${r.max}]`} <@&${r.role}> **Win:** +${r.gain} **Loss:** -${r.loss}`
).join("\n")}`, "All Ranks")] })
}
}

49
src/commands/register.js Normal file
View File

@ -0,0 +1,49 @@
import Command from "../base/Command.js";
import Sync from "../model/Sync.js"
import { createProfile, updateRank } from "../ranked/profile.js";
import config from "../base/Config.js"
import MessageHelper from "../base/MessageHelper.js";
export default class Register extends Command {
constructor (client) {
super(client, {
name: "register",
description: "This command links your discord account with your minecraft account.",
options: [
{
name: "code",
description: "Type here the code you recieved on the server",
required: true,
type: "INTEGER"
}
]
});
}
async run (interaction) {
const alreadyLinked = await Sync.findOne({
discord: interaction.user.id
})
if (alreadyLinked) {
return interaction.editReply({ embeds: [MessageHelper.error("You are already registered!")] })
}
const code = interaction.options.get("code").value;
const minecraftUUID = await this.client.redis.get("rankedhcf.sync.code." + code)
if (minecraftUUID) {
this.client.redis.del("rankedhcf.sync.code." + code)
this.client.redis.del("rankedhcf.sync.player." + minecraftUUID)
const link = new Sync({ minecraft: minecraftUUID, discord: interaction.user.id });
await link.save()
interaction.member.roles.add(await interaction.guild.roles.fetch(config.roles.registered))
await createProfile(interaction.user.id);
setTimeout(() => { updateRank(interaction.guild, interaction.user.id) }, 3000)
interaction.editReply({ embeds: [MessageHelper.success("Welcome to Elevate Ranked!\nYour discord account is now synchronized with your minecraft account.\nFeel free to join the queue channel.").setThumbnail("https://crafthead.net/avatar/" + minecraftUUID)]})
} else {
interaction.editReply({ embeds: [MessageHelper.error("Invalid code! **NOTE:** On the minecraft server its /discord not /register! You use /register for the website")] })
}
}
}

37
src/commands/reset.js Normal file
View File

@ -0,0 +1,37 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import { getProfile, updateRank } from "../ranked/profile.js";
export default class Reset extends Command {
constructor (client) {
super(client, {
name: "reset",
description: "Resets a player stats.",
options: [
{
name: "player",
description: "The player to reset.",
required: true,
type: "USER"
}
]
});
}
async run (interaction) {
const target = interaction.options.get("player")?.value
const profile = await getProfile(target)
if (profile == null) {
interaction.editReply({ embeds: [MessageHelper.error(`There is no profile associated with <@${target}>`)] })
return;
}
profile.elo = 0;
profile.wins = 0;
profile.losses = 0;
await profile.save();
updateRank(interaction.guild, target)
interaction.editReply({ embeds: [MessageHelper.success(`User's elo has been rese`, `Stats Reset`)] })
}
}

View File

@ -0,0 +1,44 @@
import Command from "../base/Command.js";
import { MessageActionRow, MessageButton } from "discord.js";
import MessageHelper from "../base/MessageHelper.js";
export default class SendTicketMSG extends Command {
constructor (client) {
super(client, {
name: "sendticketmsg",
description: "Sends the ticket message to a channel.",
options: [
{
name: "channel",
description: "The channel to send the message to.",
required: true,
type: "CHANNEL"
}
]
});
}
run (interaction) {
const channel = interaction.options.data[0].channel;
if (channel.type !== "GUILD_TEXT") {
const embed = this.helper.error("That is not a channel!");
return interaction.editReply({ embeds: [embed] });
}
const row = new MessageActionRow()
.addComponents(
new MessageButton()
.setCustomId("ticket")
.setLabel("Create a ticket")
.setStyle("PRIMARY")
.setEmoji("🎫")
);
const ticketMsg = MessageHelper.info("If you require any assistance please click the button below.", "Ticket Support");
channel.send({ embeds: [ticketMsg], components: [row] });
const embed = MessageHelper.success("Message sent!", "Success");
interaction.editReply({ embeds: [embed] });
}
}

44
src/commands/stats.js Normal file
View File

@ -0,0 +1,44 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import RankedProfile from "../model/RankedProfile.js";
import { getProfile, updateRank } from "../ranked/profile.js";
export default class Stats extends Command {
constructor (client) {
super(client, {
name: "stats",
description: "Check your current stats.",
options: [
{
name: "player",
description: "If you wish to view someone else their stats type their name here.",
required: false,
type: "USER"
}
]
});
}
async run (interaction) {
const target = interaction.options.get("player")?.value || interaction.user.id;
const profile = await getProfile(target)
if (profile == null) {
interaction.editReply({ embeds: [MessageHelper.error(`There is no profile associated with <@${target}>`)] })
return;
}
if (profile.elo < 0) {
console.log("Invalid elo found in profile", profile)
profile.elo = 0;
profile.save()
}
let wlr = Math.round(profile.wins / profile.losses * 100) / 100
if (isNaN(wlr)) {
wlr = 0;
}
updateRank(interaction.guild, target)
// updateRank(interaction.guild, interaction.user.id)
interaction.editReply({ embeds: [MessageHelper.success(`**User:** <@${target}>\n**Elo:** ${profile.elo}\n**Wins:** ${profile.wins}\n**Losses:** ${profile.losses}\n**WLR:** ${wlr}`, `Stats`)] })
}
}

41
src/commands/unsyncdc.js Normal file
View File

@ -0,0 +1,41 @@
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import Sync from "../model/Sync.js";
import { getProfile, updateRank } from "../ranked/profile.js";
export default class UnSyncDC extends Command {
constructor (client) {
super(client, {
name: "unsyncdc",
description: "Unsync a discord account.",
options: [
{
name: "member",
description: "The member that will be unsynced. Profile will be reset.",
required: true,
type: "USER"
}
]
});
}
async run (interaction) {
const target = interaction.options.get("member").value
const sync = await Sync.findOne({ discord: target });
const profile = await getProfile(target)
if (sync == null) {
interaction.editReply({ embeds: [MessageHelper.error(`There is no sync associated with <@${target}>`)] })
return;
}
if (profile) {
await profile.remove();
}
await sync.remove();
updateRank(interaction.guild, target)
interaction.editReply({ embeds: [MessageHelper.error(`<@${target}> is no longer synchronized!`, "Unsynchronized").setThumbnail("https://crafthead.net/avatar/" + sync.minecraft)]})
}
}

54
src/commands/unsyncmc.js Normal file
View File

@ -0,0 +1,54 @@
import fetch from "node-fetch";
import Command from "../base/Command.js";
import MessageHelper from "../base/MessageHelper.js";
import Sync from "../model/Sync.js";
import { getProfile, updateRank } from "../ranked/profile.js";
export default class UnSyncMC extends Command {
constructor(client) {
super(client, {
name: "unsyncmc",
description: "Unsync a minecraft account.",
options: [
{
name: "ign",
description: "The ign that will be unsynced.",
required: true,
type: "STRING"
}
]
});
}
async run(interaction) {
const ign = interaction.options.get("ign").value
fetch(`https://api.mojang.com/users/profiles/minecraft/${ign}`)
.then((res) => {
if (res.status != 200) {
interaction.editReply({ embeds: [MessageHelper.error(`There is no minecraft account with that ign`)] });
return;
}
return res.json()
})
.then(async json => {
if (json.error) return interaction.editReply({ embeds: [MessageHelper.error(`Mojang return an error`)] });
const id = json.id;
const sync = await Sync.findOne({ minecraft: id.substr(0, 8) + "-" + id.substr(8, 4) + "-" + id.substr(12, 4) + "-" + id.substr(16, 4) + "-" + id.substr(20) });
if (sync == null) {
return interaction.editReply({ embeds: [MessageHelper.error(`There is no sync associated with ${ign}`)] });
}
const target = sync.discord;
const profile = await getProfile(target)
if (profile) {
await profile.remove();
}
await sync.remove();
updateRank(interaction.guild, target)
interaction.editReply({ embeds: [MessageHelper.error(`<@${target}> is no longer synchronized!`, "Unsynchronized").setThumbnail("https://crafthead.net/avatar/" + sync.minecraft)] })
})
.catch((e) => {
console.log(e)
interaction.editReply({ embeds: [MessageHelper.error(`The code failed to unsync`)] })
})
}
}

53
src/commands/void.js Normal file
View File

@ -0,0 +1,53 @@
import { states, voidGame } from "../ranked/game.js"
import Command from "../base/Command.js";
import RankedGame from "../model/RankedGame.js";
import MessageHelper from "../base/MessageHelper.js";
export default class Void extends Command {
constructor (client) {
super(client, {
name: "void",
description: "Voids a game",
options: [
{
name: "game",
description: "The id of the game that will be voided.",
required: true,
type: "STRING"
},
{
name: "revertwin",
description: "If the win should be reverted",
required: true,
type: "BOOLEAN"
}
]
});
}
async run (interaction) {
const _id = interaction.options.get("game").value
const revertwin = interaction.options.get("revertwin").value
const game = await RankedGame.findOne({ _id })
if (!game) {
return interaction.editReply({ embeds: [MessageHelper.error("That game was not found!")] })
}
if (game.state == states.VOIDED) {
return interaction.editReply({ embeds: [MessageHelper.error("That game is already voided!")] })
}
if (game.state == states.ENDED) {
interaction.editReply({ embeds: [MessageHelper.success(`That game has been voided!\n\n**Elo Reverts:**\n${Object.entries(game.eloChanges).filter(eloChange => {
if (revertwin === false && eloChange[1] > 0) {
return false;
} else {
return true;
}
}).map(eloChange => `<@${eloChange[0]}>: ${-eloChange[1] > 0 ? "+" : ""}**${-eloChange[1]}**`).join("\n")}`, "Voided")] })
} else {
interaction.editReply({ embeds: [MessageHelper.success("That game has been voided!", "Voided")] })
}
voidGame(this.client, game, revertwin)
}
}

21
src/commands/voidgames.js Normal file
View File

@ -0,0 +1,21 @@
import Command from "../base/Command.js";
import RankedGame from "../model/RankedGame.js"
import { states, voidGame } from "../ranked/game.js";
import MessageHelper from "../base/MessageHelper.js";
export default class VoidGames extends Command {
constructor(client) {
super(client, {
name: "voidgames",
description: "All current games will be voided."
});
}
async run(interaction) {
const games = await RankedGame.find({ "state": { "$nin": [states.ENDED, states.VOIDED] } })
games.forEach(game => {
voidGame(this.client, game)
})
interaction.editReply({ embeds: [MessageHelper.success(`All current games are voided!`, "Games Voided")] })
}
}

View File

@ -0,0 +1,11 @@
import { updateRank } from "../ranked/profile.js";
export default class guildMemberAdd {
constructor(client) {
this.client = client;
}
run (member) {
updateRank(member.guild, member.user.id)
}
}

View File

@ -0,0 +1,36 @@
import MessageHelper from "../base/MessageHelper.js";
export default class interactionCreate {
constructor(client) {
this.client = client;
}
run(interaction) {
if (interaction.isCommand()) {
const command = this.client.commands.get(interaction.commandName);
if (command) {
interaction.deferReply()
.then(() => command.run(interaction));
}
};
if (interaction.customId === "ticket" && interaction.componentType === "BUTTON") {
if (interaction.guild.channels.cache.some(x => x.name.toLowerCase() === `ticket-${interaction.user.username.toLowerCase().replace(/[^a-z0-9]/g, "")}` === true)) {
interaction.reply({ embeds: [MessageHelper.error("You already have a ticket!")], ephemeral: true });
return;
}
interaction.guild.channels.create("ticket-" + interaction.user.username.toLowerCase().replace(/[^a-z0-9]/g, ""), {
parent: interaction.guild.channels.cache.find(c => c.name.toLowerCase() === "tickets" && c.type === "GUILD_CATEGORY"),
lockPermissions: true
}).then(async channel => {
await channel.permissionOverwrites.create(interaction.user, {
VIEW_CHANNEL: true,
SEND_MESSAGES: true
});
channel.send(`${interaction.guild.roles.cache.find(x => x.name === 'Support')}`).then(msg => msg.delete())
await channel.send({ embeds: [MessageHelper.success(`Hello ${interaction.user.toString()}, You have successfully created a ticket. Please specify what you're in need of assistance with, we will get back to you as soon as possible.\n\nThanks.`, "Ticket")] });
interaction.reply({ embeds: [MessageHelper.success(`Your ticket has been created ${channel.toString()}!`, "Ticket Created")], ephemeral: true });
});
}
}
}

View File

@ -0,0 +1,11 @@
export default class voiceStateUpdate {
constructor (client) {
this.client = client;
}
run (oldState, newState) {
if (oldState.channel?.id && oldState.channel?.id != newState.channel?.id) {
// checkDodge(this.client, oldState.guild, newState.id, oldState.channel?.id, newState.channel?.id)
}
}
}

15
src/index.js Normal file
View File

@ -0,0 +1,15 @@
import Client from "./base/Client.js";
import config from "./base/Config.js"
process
.on("uncaughtException", err => console.error(err.stack))
.on("unhandledRejection", err => console.error(err.stack));
console.log("Starting...");
const client = new Client({
config: "./config"
});
// Login with config token
client.login(config.token);

27
src/model/RankedGame.js Normal file
View File

@ -0,0 +1,27 @@
import mongoose from 'mongoose';
const database = mongoose.connection.useDb('practice')
export default database.model('RankedGame', new mongoose.Schema({
_id: String,
players: [String],
team1: {
captain: String,
players: [String],
voice: String
},
team2: {
captain: String,
players: [String],
voice: String
},
winner: Number,
map: String,
match: String,
voice: String,
text: String,
state: String,
eloChanges: Object
}, {
versionKey: false
}), 'rankedgames')

View File

@ -0,0 +1,13 @@
import mongoose from 'mongoose';
const database = mongoose.connection.useDb('practice')
export default database.model('RankedProfile', new mongoose.Schema({
id: String,
wins: Number,
losses: Number,
elo: Number,
dodges: Number
}, {
versionKey: false
}), 'rankedprofiles')

10
src/model/Sync.js Normal file
View File

@ -0,0 +1,10 @@
import mongoose from 'mongoose';
const database = mongoose.connection.useDb('practice')
export default database.model('Sync', new mongoose.Schema({
minecraft: String,
discord: String
}, {
versionKey: false
}), 'syncs')

689
src/ranked/game.js Normal file
View File

@ -0,0 +1,689 @@
import RankedGame from "../model/RankedGame.js"
import MessageHelper from "../base/MessageHelper.js";
import { MessageActionRow, MessageSelectMenu, MessageButton } from "discord.js"
import { randomUUID } from "crypto"
import { getProfile, giveLoss, giveWin, updateRank } from "./profile.js"
import config from "../base/Config.js"
// States
const states = {
"MOVING": "MOVING",
"PICKING": "PICKING",
"VOTING": "VOTING",
"PREGAME": "PREGAME",
"STARTED": "STARTED",
"ENDED": "ENDED",
"VOIDED": "VOIDED"
}
// Constants
const PING_UPDATE_INTERVAL = 1;
const QUEUE_UPDATE_INTERVAL = 25;
const DODGE_CHECK_DELAY = QUEUE_UPDATE_INTERVAL / 2;
const TIME_MAP_VOTING = 30;
const TIME_PICK = 15;
const IDLE_PICK = 3;
const GAME_SIZE = 12;
const gameCollectors = {}
// Online values
let online = true;
let recieved = true;
// Ping to check online value
async function startPing(client) {
setInterval(async () => {
if (recieved) {
online = true;
recieved = false;
} else {
online = false;
}
client.user.setPresence({ activities: [{ name: online ? 'elevatemc.com' : 'offline' }], status: 'dnd' });
client.redis.publish("rankedhcf:ping")
}, PING_UPDATE_INTERVAL * 1000)
}
// Queue loop
async function startQueueCheck(client) {
const guild = client.guilds.cache.get(config.guild);
setInterval(async () => {
if (!online) return;
config.queue.forEach(async q => {
const queue = await guild.channels.fetch(q);
if (queue.members.size == GAME_SIZE) {
console.log("[QUEUE] Starting a new game")
const members = queue.members.clone()
const shuffled = members.sort(() => 0.5 - Math.random());
createGame(client, shuffled)
}
})
}, QUEUE_UPDATE_INTERVAL * 1000)
}
// Function to clean up old games
async function cleanOldGames(client) {
const games = await RankedGame.find({ state: { $nin: [states.ENDED, states.VOIDED] } })
if (games.length > 0) {
games.forEach(game => { voidGame(client, game) })
console.log(`Voided ${games.length} games to make sure there are no glitches.`)
}
}
async function startDodgeCheck(client) {
const guild = client.guilds.cache.get(config.guild);
setTimeout(() => {
setInterval(async () => {
const games = await RankedGame.find({ state: { $nin: [states.STARTED, states.ENDED, states.VOIDED, states.MOVING] } })
games.forEach(async game => {
const players = [...game.players];
let allIn = [];
const team1Voice = await guild.channels.fetch(game.team1.voice).catch(() => { });
if (team1Voice?.id) {
allIn = allIn.concat(team1Voice.members.map(m => m.user.id))
}
const team2Voice = await guild.channels.fetch(game.team2.voice).catch(() => { });
if (team2Voice?.id) {
allIn = allIn.concat(team2Voice.members.map(m => m.user.id))
}
const voice = await guild.channels.fetch(game.voice).catch(() => { });
if (voice?.id) {
allIn = allIn.concat(voice.members.map(m => m.user.id))
}
const absent = players.filter(pl => !allIn.includes(pl));
if (absent.length > 0) {
game.state = states.VOIDED;
dodge(client, game, guild, absent)
}
})
}, QUEUE_UPDATE_INTERVAL * 1000)
}, 1000 * DODGE_CHECK_DELAY)
}
// Check if the disconnect was a dodge
/* async function checkDodge(client, guild, id, oldChannel, newChannel) {
if (oldChannel == config.queue) return;
const games = gameCache.filter(x => x.state != states.ENDED && x.state != states.VOIDED && x.players.includes(id));
games.forEach(game => {
switch (game.state) {
case states.PICKING:
if (newChannel == game.team1.voice || newChannel == game.team2.voice) {
// Fine move -> from lobby to team voice
return;
} else {
dodge(client, game, guild, id)
}
break;
case states.PREGAME:
case states.VOTING:
dodge(client, game, guild, id)
break;
case states.STARTED:
break;
}
})
} */
// Activates when someone dodged
async function dodge(client, game, guild, ids) {
game.state = states.VOIDED;
await game.save()
const channel = await guild.channels.fetch(game.text).catch(() => { });
if (channel) {
channel.send({ embeds: [MessageHelper.error(`${ids.map(id => `<@${id}>`).join(" ")} has dodged this game.`, `Game Dodged [${game._id}]`)] })
}
const feed = await guild.channels.fetch(config.feed)
console.log(`[${game._id}] Game Dodged`)
feed.send({ embeds: [MessageHelper.error(`${ids.map(id => `<@${id}>`).join(" ")} has dodged this game.`, `Game Dodged [${game._id}]`)] })
const dodgefeed = await guild.channels.fetch(config.dodgefeed)
dodgefeed.send({ content: `${ids.map(id => `<@${id}>`).join(" ")} has dodged ${game._id} [auto timed out for ${ids.length > 1 ? "20" : "60"}m]` })
ids.forEach(async id => {
const member = await guild.members.fetch(id).catch(() => {});
if (member) {
if (ids.length == 1) {
member.timeout(60 * 60 * 1000, `Dodged ${game._id}`)
.then(() => {})
.catch(() => {});
} else {
member.timeout(20 * 60 * 1000, `Dodged ${game._id}`)
.then(() => {})
.catch(() => {});
}
} else {
dodgefeed.send({ content: `<@${id}> left the discord while playing ${game._id}` })
}
})
voidGame(client, game)
}
// Creates a new game
async function createGame(client, members) {
// Verify it they arent in a game yet
for (const member of members.toJSON()) {
const game = await RankedGame.findOne({ players: member.user.id, state: { "$nin": [states.ENDED, states.VOIDED, states.STARTED] } })
if (game) {
console.log("Failed to make a game because", member.nickname, "is already in a game")
return;
}
}
// Create game object
const game = new RankedGame({
players: members.map(x => x.id),
team1: {
captain: "",
players: []
},
team2: {
captain: "",
players: []
},
map: "",
match: "",
state: states.MOVING
})
// Generate id
const id = randomUUID().toString().substring(0, 7);
game._id = id;
await game.save();
// Create channels
const guild = await client.guilds.cache.get(config.guild);
const parent = await guild.channels.fetch(config.category);
const textChannel = await guild.channels.create("game-" + id, { parent })
const voiceChannel = await guild.channels.create(`[${id}] Game`, {
type: "GUILD_VOICE",
parent
})
game.text = textChannel.id;
game.voice = voiceChannel.id;
// Move everyone to the pregame voice channel
for (const member of members.toJSON()) {
voiceChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true,
CONNECT: true
});
textChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true
});
await move(member, voiceChannel)
}
const eloMap = {};
for (const m of members.toJSON()) {
const profile = await getProfile(m.user.id);
eloMap[m.user.id] = profile.elo;
}
members = members.sort((a, b) => {
return eloMap[b.user.id] - eloMap[a.user.id]
})
const random = Math.random() < 0.5;
if (random) {
// Random team 1 captain
const randomUser1 = members.firstKey();
game.team1.captain = randomUser1;
addToTeam(game, randomUser1, 1)
members.delete(randomUser1)
// Random team 2 captain
const randomUser2 = members.firstKey();
game.team2.captain = randomUser2;
addToTeam(game, randomUser2, 2)
members.delete(randomUser2)
} else {
// Random team 2 captain
const randomUser2 = members.firstKey();
game.team2.captain = randomUser2;
addToTeam(game, randomUser2, 2)
members.delete(randomUser2)
// Random team 1 captain
const randomUser1 = members.firstKey();
game.team1.captain = randomUser1;
addToTeam(game, randomUser1, 1)
members.delete(randomUser1)
}
console.log(`[${game._id}] Game Created`)
const feed = await guild.channels.fetch(config.feed);
feed.send({
embeds: [MessageHelper.info(`**Team One**:
Captain: <@${game.team1.captain}>
**Team Two:**
Captain: <@${game.team2.captain}>
**All Players:**
${game.players.map(x => `<@${x}>`).join("\n")}`, `Game Created [${game._id}]`)]
})
// Start picking round
game.state = states.PICKING
await game.save();
await doPicking(game, textChannel, members)
.then(async () => {
game.state = states.MOVING;
await game.save()
await movePlayersToTeamChannels(game, guild)
setTimeout(() => { voiceChannel.delete(); }, 1500)
game.state = states.VOTING
await game.save();
await doMapVote(game, textChannel)
.then(async () => {
game.state = states.PREGAME
sendPlayMessage(game, textChannel)
await game.save()
client.redis.publish("rankedhcf:loadgame", game._id)
})
.catch(err => {
if (err != "VOID") {
console.log(err)
}
})
})
.catch(err => {
if (err == "TIME") {
voidGame(client, game)
}
})
}
// Does the picking round
async function doPicking(game, channel, pickables) {
return new Promise(async (resolve, reject) => {
const generatedMessage = generatePickingMessage(game, pickables)
const pickingMessage = await channel.send(generatedMessage);
if (pickables.size < 1) {
return resolve();
}
const collector = pickingMessage.createMessageComponentCollector({ componentType: "SELECT_MENU", time: TIME_PICK * 60 * 1000, idle: IDLE_PICK * 60 * 1000 });
gameCollectors[game._id] = collector;
let turn = true;
collector.on('collect', async i => {
if (i.user.id != game.team1.captain && i.user.id != game.team2.captain) {
return i.reply({ embeds: [MessageHelper.error("You are not a captain!")], ephemeral: true });
}
if (turn && i.user.id == game.team1.captain) {
i.deferUpdate()
turn = !turn;
pick(game, pickables, i.values[0], 1, collector, turn)
await pickingMessage.edit(generatePickingMessage(game, pickables))
} else if (!turn && i.user.id == game.team2.captain) {
i.deferUpdate()
turn = !turn;
pick(game, pickables, i.values[0], 2, collector, turn)
await pickingMessage.edit(generatePickingMessage(game, pickables))
} else {
return i.reply({ embeds: [MessageHelper.error("It is not your turn!")], ephemeral: true });
}
if (pickables.size < 1) {
collector.stop()
resolve()
}
});
collector.on('end', (collected, reason) => {
if (reason == "VOID") {
pickingMessage.delete();
return reject("VOID");
}
if (reason == "time" || reason == "idle") {
pickingMessage.delete();
return reject("TIME");
}
})
})
}
function addToTeam(game, player, team) {
if (team === 1) {
game.team1.players.addToSet(player)
} else if (team === 2) {
game.team2.players.addToSet(player)
}
}
function pick(game, pickables, pick, team, collector, turn) {
addToTeam(game, pick, team)
pickables.delete(pick)
if (pickables.size === 1) {
const last = pickables.firstKey();
if (turn) {
addToTeam(game, last, 1);
pickables.delete(last);
} else {
addToTeam(game, last, 2);
pickables.delete(last);
}
collector.stop()
}
}
function shuffleAndMode(arr) {
return arr
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
.sort((a, b) =>
arr.filter(v => v === a).length - arr.filter(v => v === b).length)
.pop();
}
async function doMapVote(game, channel) {
return new Promise(async (resolve, reject) => {
const votes = {};
const generatedMessage = generateMapVoteMessage(game, votes)
const voteMessage = await channel.send(generatedMessage);
const collector = voteMessage.createMessageComponentCollector({ componentType: 'BUTTON', time: 1000 * TIME_MAP_VOTING });
gameCollectors[game._id] = collector;
collector.on('collect', async i => {
if (!game.players.includes(i.user.id)) {
return i.reply({ embeds: [MessageHelper.error("You are not part of this game!")], ephemeral: true });
}
i.deferUpdate()
votes[i.user.id] = i.customId;
voteMessage.edit(generateMapVoteMessage(game, votes))
});
collector.on('end', async (collected, reason) => {
if (reason == "VOID") {
voteMessage.delete();
return reject("VOID");
}
const values = Object.values(votes);
if (values.length > 0) {
game.map = shuffleAndMode(values);
} else {
game.map = config.maps[Math.floor(Math.random() * config.maps.length)].id;
}
voteMessage.edit({ embeds: [MessageHelper.success(`${config.maps.find(x => x.id === game.map).name} won the map vote.`, `Map Vote`)], components: [] })
resolve()
})
})
}
async function movePlayersToTeamChannels(game, guild) {
const parent = await guild.channels.fetch(config.category);
const team1VoiceChannel = await guild.channels.create(`[${game._id}] Team 1`, {
type: 'GUILD_VOICE',
parent
})
game.team1.voice = team1VoiceChannel.id;
await new Promise(resolve => { setTimeout(() => resolve(), 3000) })
const team2VoiceChannel = await guild.channels.create(`[${game._id}] Team 2`, {
type: 'GUILD_VOICE',
parent
})
game.team2.voice = team2VoiceChannel.id;
for (const id of game.team1.players) {
const member = await guild.members.fetch(id);
team1VoiceChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true,
CONNECT: true
});
team2VoiceChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true
});
await move(member, team1VoiceChannel)
}
for (const id of game.team2.players) {
const member = await guild.members.fetch(id);
team2VoiceChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true,
CONNECT: true
});
team1VoiceChannel.permissionOverwrites.create(member, {
VIEW_CHANNEL: true
});
await move(member, team2VoiceChannel)
}
}
async function move(member, channel) {
const voice = member.voice
if (voice?.channelId) {
await voice.setChannel(channel).catch(() => {})
}
}
async function deleteChannels(client, game) {
const guild = client.guilds.cache.get(config.guild);
const channel = await guild.channels.fetch(game.text).catch(() => { });
if (channel) channel.delete().catch(() => { });
const team1Voice = await guild.channels.fetch(game.team1.voice).catch(() => { });
if (team1Voice?.id) team1Voice.delete().catch(() => { });
const team2Voice = await guild.channels.fetch(game.team2.voice).catch(() => { });
if (team2Voice?.id) team2Voice.delete().catch(() => { });
const voice = await guild.channels.fetch(game.voice).catch(() => { });
if (voice?.id) voice.delete().catch(() => { });
}
async function voidGame(client, game, revertWin = true) {
client.redis.publish("rankedhcf:voidgame", game._id)
if (game.state === states.ENDED) {
Object.entries(game.eloChanges).forEach(async eloChange => {
const user = eloChange[0];
const elo = eloChange[1];
if (elo > 0 && revertWin === false) return
const profile = await getProfile(user);
if (profile) {
if (elo > 0) {
profile.wins -= 1;
} else {
profile.losses -= 1;
}
profile.elo -= elo;
if (profile < 0) {
profile.elo = 0;
}
await profile.save();
updateRank(client.guilds.cache.get(config.guild), eloChange[0])
}
})
}
game.state = states.VOIDED;
game.save();
const guild = client.guilds.cache.get(config.guild);
const channel = await guild.channels.fetch(game.text).catch(() => { });
if (channel) channel.send({ embeds: [MessageHelper.error(`This game has been voided. All channels will close in 10 seconds.`, `Game Voided [${game._id}]`)] });
const feed = await guild.channels.fetch(config.feed)
feed.send({
embeds: [MessageHelper.info(`
**Team One**:
Captain: <@${game.team1.captain}>
${game.team1.players.filter(x => x != game.team1.captain).map(x => `<@${x}>`).join("\n")}
**Team Two:**
Captain: <@${game.team2.captain}>
${game.team2.players.filter(x => x != game.team2.captain).map(x => `<@${x}>`).join("\n")}
If you recieved or lost any elo during this game it will be automatically reverted.
`, `Game Voided [${game._id}]`)]
})
gameCollectors[game._id]?.stop("VOID");
delete gameCollectors[game._id];
setTimeout(() => deleteChannels(client, game), 10 * 1000);
}
async function winGame(client, game, winnerTeam) {
const winners = winnerTeam === 1 ? game.team1.players : game.team2.players
const losers = winnerTeam === 1 ? game.team2.players : game.team1.players
const eloChanges = {}
for (const w of winners) {
const firstElo = (await getProfile(w)).elo;
await giveWin(w).catch(err => { console.log("Failed to give win", err) })
const secondElo = (await getProfile(w)).elo;
eloChanges[w] = secondElo - firstElo;
}
for (const l of losers) {
const firstElo = (await getProfile(l)).elo;
await giveLoss(l).catch(err => { console.log("Failed to give win", err) })
const secondElo = (await getProfile(l)).elo;
eloChanges[l] = secondElo - firstElo;
}
game.eloChanges = eloChanges;
game.state = states.ENDED;
game.save();
const guild = client.guilds.cache.get(config.guild);
game.players.forEach(id => { updateRank(guild, id) })
const channel = await guild.channels.fetch(game.text).catch(() => { });
if (channel) channel.send({
embeds: [MessageHelper.success(`**Elo Changes:**
${Object.entries(eloChanges).map(x => `<@${x[0]}>: **${x[1] > 0 ? "+" : ""}${x[1]}**`).join("\n")}
All the game channels will be deleted automatically in 30 seconds.`, `Game Ended`)]
});
const feed = await guild.channels.fetch(config.feed)
console.log(`[${game._id}] A game was won Winner Team: ${winnerTeam}`)
feed.send({
embeds: [MessageHelper.info(`**Elo Changes:**
${Object.entries(eloChanges).map(x => `<@${x[0]}>: **${x[1] > 0 ? "+" : ""}${x[1]}**`).join("\n")}
All the game channels will be deleted automatically in 30 seconds.`, `Game Ended [${game._id}]`)]
});
setTimeout(() => deleteChannels(client, game), 30 * 1000)
}
async function sendPlayMessage(game, channel) {
channel.send({
embeds: [MessageHelper.success(`1) Log on **elevatemc.com** or **cavepvp.org**
2) Connect to **practice**
3) Type **/ranked** in the chat to join your game
4) **Good luck!**`, `Game Started`)]
});
}
async function onRedisMessage(client, channel, message) {
switch (channel) {
case "rankedhcf:startgame": {
const data = message.split(":")
if (data.length < 2) {
return console.log("Invalid game start data: " + message)
}
const _id = data[0];
const match = data[1];
const game = await RankedGame.findOne({ _id })
if (game) {
if (game.state != states.PREGAME) return console.log("Invalid game start state: " + message + " state: " + game.state)
game.state = states.STARTED;
game.match = match;
game.save()
const feed = await client.guilds.cache.get(config.guild).channels.fetch(config.feed);
console.log(`[${game._id}] Game Started Match: ${match}`)
feed.send({
embeds: [MessageHelper.info(`This game has started. Match id: ${match}
${game.players.map(x => `<@${x}>`).join("\n")}`, `Game Started [${game._id}]`)]
})
} else {
console.log("Invalid game start id: " + message)
}
break;
}
case "rankedhcf:wingame": {
const data = message.split(":")
if (data.length < 2) {
return console.log("Invalid game win data: " + message)
}
const _id = data[0];
const winnerTeam = data[1];
const game = await RankedGame.findOne({ _id })
if (game) {
if (game.state == states.PREGAME) console.log(game)
if (game.state != states.STARTED && game.state != states.PREGAME) return console.log("Invalid game win state: " + message + " state: " + game.state)
winGame(client, game, parseInt(winnerTeam))
} else {
console.log("Invalid game win id: " + message)
}
break;
}
case "rankedhcf:pong": {
recieved = true;
break;
}
}
}
function generatePickingMessage(game, pickables) {
if (pickables.size > 0) {
const options = pickables.map(m => {
return {
label: m.nickname || m.user.username,
description: "",
value: m.user.id
}
});
const row = new MessageActionRow()
.addComponents(
new MessageSelectMenu()
.setCustomId('select')
.setPlaceholder('Select a player here')
.setMinValues(1)
.setMaxValues(1)
.addOptions(options)
)
const embed = MessageHelper.success(`**Team One**:
Captain: <@${game.team1.captain}>
${game.team1.players.filter(x => x != game.team1.captain).map(x => `<@${x}>`).join("\n")}
**Team Two:**
Captain: <@${game.team2.captain}>
${game.team2.players.filter(x => x != game.team2.captain).map(x => `<@${x}>`).join("\n")}`, `Player Picking`)
return { content: game.players.map(x => `<@${x}>`).join(" "), embeds: [embed], components: [row] }
} else {
const embed = MessageHelper.success(`**Team One**:
Captain: <@${game.team1.captain}>
${game.team1.players.filter(x => x != game.team1.captain).map(x => `<@${x}>`).join("\n")}
**Team Two:**
Captain: <@${game.team2.captain}>
${game.team2.players.filter(x => x != game.team2.captain).map(x => `<@${x}>`).join("\n")}`, `Final Team`)
return { content: game.players.map(x => `<@${x}>`).join(" "), embeds: [embed], components: [] }
}
}
function generateMapVoteMessage(game, votes) {
const buttons = config.maps.map(map =>
new MessageButton()
.setCustomId(map.id)
.setLabel(map.name)
.setStyle('PRIMARY')
)
const row = new MessageActionRow()
.addComponents(
...buttons
);
const embed = MessageHelper.success(`
${config.maps.map(map => `**${map.name}**\nVotes: ${Object.values(votes).filter(v => v === map.id).length}`).join("\n\n")}\n\nYou have ${TIME_MAP_VOTING} seconds to vote for the map you wish to play on.`
, `Map Vote`)
return { embeds: [embed], components: [row] }
}
export {
createGame, onRedisMessage, voidGame, states, startQueueCheck, startPing, cleanOldGames, startDodgeCheck
}

127
src/ranked/profile.js Normal file
View File

@ -0,0 +1,127 @@
import RankedProfile from "../model/RankedProfile.js"
import Sync from "../model/Sync.js"
import config from "../base/Config.js"
import fetch from "node-fetch"
function createProfile(id) {
const profile = new RankedProfile({ id, wins: 0, losses: 0, elo: 0, dodges: 0 });
profile.save()
return profile
}
async function getProfile(id) {
const profile = await RankedProfile.findOne({ id })
return profile;
}
async function getLeaderboard() {
const leaderboard = await RankedProfile.find().sort({ elo: "desc" }).limit(10)
return leaderboard;
}
function getRank(elo) {
return config.ranks.find(r => elo >= r.min && elo <= r.max);
}
async function updateRank(guild, id) {
const profile = await getProfile(id)
const member = await guild.members.fetch(id).catch((err) => { console.log(err); console.log("[RANKUPDATE] Failed to fetch member", id )});
if (profile && member) {
let current = member.roles.cache.filter(r => config.ranks.map(x => x.role).includes(r.id)).map(r => r.id)
if (current.length > 1) {
current.forEach(r => member.roles.remove(r));
current = [];
}
if (!member.roles.cache.some(r => r.id == config.roles.registered)) {
const registeredRole = await guild.roles.fetch(config.roles.registered);
member.roles.add(registeredRole)
}
const rank = getRank(profile.elo);
if (!current.includes(rank.role)) {
current.forEach(r => member.roles.remove(r));
member.roles.add(await guild.roles.fetch(rank.role))
}
const sync = await Sync.findOne({ "discord": id })
if (!sync) return;
fetch(`https://api.mojang.com/user/profiles/${sync.minecraft}/names`)
.then(res => res.json())
.then(json => {
if (json.error) return console.log("[RANKUPDATE] Mojang failed", json.error);
if (id == "261885314933587969") {
profile.elo = 0;
profile.wins = 0;
profile.losses = 0;
profile.save();
return member.setNickname(`[-1] ${json[json.length - 1].name}`).catch((err) => { console.log(err) })
}
member.setNickname(`[${profile.elo}] ${json[json.length - 1].name}`).catch((err) => { console.log(err) })
})
.catch((err) => { console.log("[RANKUPDATE] fail", err) })
} else if (member) {
// Member exists but has no profile
let current = member.roles.cache.filter(r => config.ranks.map(x => x.role).includes(r.id)).map(r => r.id)
if (current.length > 0) {
current.forEach(r => member.roles.remove(r));
current = [];
}
const registeredRole = await guild.roles.fetch(config.roles.registered);
member.roles.remove(registeredRole)
member.setNickname(null)
}
}
async function giveWin(id) {
return new Promise(async (resolve, reject) => {
const profile = await getProfile(id)
if (profile) {
const rank = getRank(profile.elo);
if (rank) {
profile.elo += rank.gain;
profile.wins += 1;
profile.save()
.then(() => resolve())
.catch(() => reject("Failed to give a win"))
} else {
console.log("Invalid rank:", + rank)
console.log("Profile --->", profile)
reject("The rank was invalid")
}
} else {
reject("That person has no profile associated with their discord account")
}
})
}
async function giveLoss(id) {
return new Promise(async (resolve, reject) => {
const profile = await getProfile(id)
if (profile) {
const rank = getRank(profile.elo);
if (rank) {
profile.elo -= rank.loss;
if (profile.elo < 0) {
profile.elo = 0;
}
profile.losses += 1;
profile.save()
.then(() => resolve())
.catch(() => reject("Failed to give a loss"))
} else {
console.log("Invalid rank:", + rank)
console.log("Profile --->", profile)
reject("The rank was invalid")
}
} else {
reject("That person has no discord account associated with their discord account")
}
})
}
export {
getProfile, getLeaderboard, giveWin, giveLoss, createProfile, updateRank
}