Nigger
This commit is contained in:
commit
f5b674b160
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
yarn.lock
|
31
disabled/giveloss.js
Normal file
31
disabled/giveloss.js
Normal 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
33
disabled/givewin.js
Normal 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
14
package.json
Normal 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
89
src/base/Client.js
Normal 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
6
src/base/Command.js
Normal 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
86
src/base/Config.js
Normal 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
42
src/base/MessageHelper.js
Normal 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
20
src/commands/close.js
Normal 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
48
src/commands/embed.js
Normal 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
53
src/commands/game.js
Normal 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
23
src/commands/games.js
Normal 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")]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
31
src/commands/leaderboard.js
Normal file
31
src/commands/leaderboard.js
Normal 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
18
src/commands/ranks.js
Normal 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
49
src/commands/register.js
Normal 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
37
src/commands/reset.js
Normal 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`)] })
|
||||||
|
}
|
||||||
|
}
|
44
src/commands/sendticketmsg.js
Normal file
44
src/commands/sendticketmsg.js
Normal 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
44
src/commands/stats.js
Normal 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
41
src/commands/unsyncdc.js
Normal 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
54
src/commands/unsyncmc.js
Normal 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
53
src/commands/void.js
Normal 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
21
src/commands/voidgames.js
Normal 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")] })
|
||||||
|
}
|
||||||
|
}
|
11
src/events/guildMemberAdd.js
Normal file
11
src/events/guildMemberAdd.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
36
src/events/interactionCreate.js
Normal file
36
src/events/interactionCreate.js
Normal 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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
src/events/voiceStateUpdate.js
Normal file
11
src/events/voiceStateUpdate.js
Normal 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
15
src/index.js
Normal 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
27
src/model/RankedGame.js
Normal 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')
|
13
src/model/RankedProfile.js
Normal file
13
src/model/RankedProfile.js
Normal 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
10
src/model/Sync.js
Normal 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
689
src/ranked/game.js
Normal 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
127
src/ranked/profile.js
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user