ive gone insane
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
.idea
|
||||
mixer_discord_bot.iml
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Rust
|
||||
Cargo.lock
|
||||
Secrets.toml
|
||||
|
||||
@@ -5,6 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
tracing = "*"
|
||||
serenity = {version = "*", default-features = false, features = ["rustls_backend", "client", "gateway", "model", "cache", "collector", "utils"] }
|
||||
sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] }
|
||||
sea-orm = { version = "*", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
|
||||
|
||||
44
schema.sql
44
schema.sql
@@ -10,8 +10,6 @@
|
||||
-- volatility REAL
|
||||
-- );
|
||||
|
||||
-- CREATE TYPE role AS ENUM ('tank', 'dps', 'support');
|
||||
|
||||
-- CREATE OR REPLACE FUNCTION rating_not_null(r rating) RETURNS BOOLEAN AS $$
|
||||
-- BEGIN
|
||||
-- RETURN (r.rating IS NOT NULL) AND (r.rd IS NOT NULL) AND (r.volatility IS NOT NULL);
|
||||
@@ -40,11 +38,19 @@
|
||||
-- )
|
||||
-- );
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role') THEN
|
||||
CREATE TYPE role AS ENUM ('tank', 'dps', 'support');
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id SERIAL PRIMARY KEY,
|
||||
discord_id BIGINT NOT NULL,
|
||||
bn_name TEXT,
|
||||
bn_tag TEXT,
|
||||
last_played TIMESTAMP,
|
||||
tank_rating REAL NOT NULL DEFAULT 2500.0,
|
||||
tank_rd REAL NOT NULL DEFAULT 300,
|
||||
tank_volatility REAL NOT NULL DEFAULT 0.06,
|
||||
@@ -55,12 +61,20 @@ CREATE TABLE IF NOT EXISTS players (
|
||||
support_rd REAL NOT NULL DEFAULT 300,
|
||||
support_volatility REAL NOT NULL DEFAULT 0.06,
|
||||
flex BOOLEAN NOT NULL DEFAULT true,
|
||||
primary_role INTEGER NOT NULL DEFAULT -1,
|
||||
secondary_role INTEGER NOT NULL DEFAULT -1,
|
||||
tertiary_role INTEGER NOT NULL DEFAULT -1,
|
||||
primary_role role,
|
||||
secondary_role role,
|
||||
tertiary_role role,
|
||||
UNIQUE (discord_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS guilds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
guild_id BIGINT NOT NULL,
|
||||
verified BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
UNIQUE (guild_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lobbies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
guild_id BIGINT NOT NULL,
|
||||
@@ -69,27 +83,9 @@ CREATE TABLE IF NOT EXISTS lobbies (
|
||||
blue_team_voice_id BIGINT NOT NULL
|
||||
);
|
||||
|
||||
-- CREATE TABLE IF NOT EXISTS guilds (
|
||||
-- id SERIAL PRIMARY KEY,
|
||||
-- guild_id BIGINT NOT NULL,
|
||||
-- separate_roles BOOLEAN NOT NULL DEFAULT false,
|
||||
-- grandmaster_name TEXT,
|
||||
-- master_name TEXT,
|
||||
-- diamond_name TEXT,
|
||||
-- platinum_name TEXT,
|
||||
-- gold_name TEXT,
|
||||
-- silver_name TEXT,
|
||||
-- bronze_name TEXT,
|
||||
--
|
||||
-- tank_name TEXT,
|
||||
-- dps_name TEXT,
|
||||
-- support_name TEXT,
|
||||
--
|
||||
-- UNIQUE (guild_id)
|
||||
-- );
|
||||
|
||||
CREATE INDEX IF NOT EXISTS players_discord_id_idx ON players (discord_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_guild_id_idx ON lobbies (guild_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_main_voice_id_idx ON lobbies (main_voice_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_red_team_voice_id_idx ON lobbies (red_team_voice_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_blue_team_voice_id_idx ON lobbies (blue_team_voice_id);
|
||||
CREATE INDEX IF NOT EXISTS guilds_guild_id_idx ON guilds (guild_id);
|
||||
122
src/bot/commands/creator.rs
Normal file
122
src/bot/commands/creator.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
};
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use serenity::model::prelude::interaction::application_command::CommandDataOption;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::queries::prelude::*;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::CreatorContainer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CreatorCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for CreatorCommand {
|
||||
fn name(&self) -> String {
|
||||
"creator".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command
|
||||
.name(self.name())
|
||||
.description("Hello world!")
|
||||
.create_option(|option| {
|
||||
option
|
||||
.name("verify")
|
||||
.description("Verify or unverify the server")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|sub_option| {
|
||||
sub_option
|
||||
.name("verify")
|
||||
.description("Whether to verify or unverify the server")
|
||||
.kind(CommandOptionType::Boolean)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let has_permission = {
|
||||
let data = ctx.data.read().await;
|
||||
let creator = data.get::<CreatorContainer>().unwrap().clone();
|
||||
interaction.user.id == *creator
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
interaction
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content("You are not the creator of this bot!")
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = interaction.data.options.get(0).unwrap().clone();
|
||||
match data.name.as_str() {
|
||||
"verify" => self.process_verify(ctx, interaction.clone(), data).await?,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CreatorCommand {
|
||||
async fn process_verify(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
data: CommandDataOption,
|
||||
) -> serenity::Result<()> {
|
||||
let verify = data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
let guild_id = interaction.guild_id.unwrap();
|
||||
|
||||
{
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
// GuildQuery::create_if_not_exists(db.get_connection(), guild_id).await;
|
||||
GuildQuery::set_verified(db.connection(), guild_id, verify).await;
|
||||
}
|
||||
|
||||
interaction
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content(format!("Updated guild verification to {verify}"))
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,31 @@
|
||||
use std::time::Duration;
|
||||
use itertools::Itertools;
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::{CreateApplicationCommand, CreateEmbed};
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction,
|
||||
InteractionResponseType
|
||||
};
|
||||
use serenity::async_trait;
|
||||
use serenity::futures::future::join_all;
|
||||
use serenity::futures::StreamExt;
|
||||
use serenity::model::application::command::CommandOptionType;
|
||||
use serenity::model::application::component::ButtonStyle;
|
||||
use serenity::model::application::interaction::message_component::MessageComponentInteraction;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
};
|
||||
use serenity::model::channel::{ChannelType, PermissionOverwrite, PermissionOverwriteType};
|
||||
use serenity::model::id::{ChannelId, RoleId, UserId};
|
||||
use serenity::model::Permissions;
|
||||
use serenity::utils::Colour;
|
||||
use sqlx::types::chrono::Utc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::database::models::lobby::Model;
|
||||
use crate::database::models::role::Role;
|
||||
use crate::database::queries::prelude::*;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::mixer;
|
||||
use crate::mixer::player::Player;
|
||||
use crate::mixer::role::Role;
|
||||
use crate::mixer::team::Team;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LobbyCommand;
|
||||
|
||||
@@ -35,14 +36,18 @@ impl MixerCommand for LobbyCommand {
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Create or start a lobby")
|
||||
command
|
||||
.name(self.name())
|
||||
.description("Create or start a lobby")
|
||||
.create_option(|option| {
|
||||
option.name("create")
|
||||
option
|
||||
.name("create")
|
||||
.description("Create a lobby")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
})
|
||||
.create_option(|option| {
|
||||
option.name("start")
|
||||
option
|
||||
.name("start")
|
||||
.description("Start a lobby")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
})
|
||||
@@ -50,17 +55,25 @@ impl MixerCommand for LobbyCommand {
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"create" => self.create_lobby(ctx, interaction).await,
|
||||
"start" => self.start_lobby(ctx, interaction).await,
|
||||
_ => Ok(())
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LobbyCommand {
|
||||
async fn create_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
async fn create_lobby(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let guild_id = interaction.guild_id.unwrap();
|
||||
let guild = guild_id.to_partial_guild(ctx).await?;
|
||||
let member = guild.member(ctx, interaction.user.id).await?;
|
||||
@@ -76,51 +89,83 @@ impl LobbyCommand {
|
||||
}
|
||||
|
||||
if !has_permission {
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("You don't have permission to create a lobby!")
|
||||
})
|
||||
}).await?;
|
||||
return Ok(())
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
let main_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||
c.name("Mix Lobby").kind(ChannelType::Voice)
|
||||
}).await?;
|
||||
let main_voice = interaction
|
||||
.guild_id
|
||||
.unwrap()
|
||||
.create_channel(ctx, |c| c.name("Mix Lobby").kind(ChannelType::Voice))
|
||||
.await?;
|
||||
|
||||
let permissions = vec![
|
||||
PermissionOverwrite {
|
||||
let permissions = vec![PermissionOverwrite {
|
||||
allow: Permissions::empty(),
|
||||
deny: Permissions::VIEW_CHANNEL,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(interaction.guild_id.unwrap().0))
|
||||
}
|
||||
];
|
||||
let red_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||
c.name("Red").kind(ChannelType::Voice).user_limit(5).permissions(permissions.clone())
|
||||
}).await?;
|
||||
let blue_voice = interaction.guild_id.unwrap().create_channel(ctx, |c| {
|
||||
c.name("Blue").kind(ChannelType::Voice).user_limit(5).permissions(permissions)
|
||||
}).await?;
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(interaction.guild_id.unwrap().0)),
|
||||
}];
|
||||
let red_voice = interaction
|
||||
.guild_id
|
||||
.unwrap()
|
||||
.create_channel(ctx, |c| {
|
||||
c.name("Red")
|
||||
.kind(ChannelType::Voice)
|
||||
.user_limit(5)
|
||||
.permissions(permissions.clone())
|
||||
})
|
||||
.await?;
|
||||
let blue_voice = interaction
|
||||
.guild_id
|
||||
.unwrap()
|
||||
.create_channel(ctx, |c| {
|
||||
c.name("Blue")
|
||||
.kind(ChannelType::Voice)
|
||||
.user_limit(5)
|
||||
.permissions(permissions)
|
||||
})
|
||||
.await?;
|
||||
|
||||
LobbyQuery::create(
|
||||
db.connection(),
|
||||
interaction.guild_id.unwrap(),
|
||||
main_voice.id,
|
||||
red_voice.id,
|
||||
blue_voice.id,
|
||||
)
|
||||
.await;
|
||||
|
||||
db.insert_guild_lobby(interaction.guild_id.unwrap(), main_voice.id, red_voice.id, blue_voice.id).await;
|
||||
drop(db);
|
||||
drop(data);
|
||||
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("Successfully created a new mix lobby!")
|
||||
})
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
async fn start_lobby(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
@@ -136,7 +181,11 @@ impl LobbyCommand {
|
||||
continue;
|
||||
}
|
||||
let members = channel.members(ctx).await?;
|
||||
if members.iter().any(|m| m.user.id == member.user.id) && db.get_lobby_by_channel(guild_id, id).await.is_some() {
|
||||
if members.iter().any(|m| m.user.id == member.user.id)
|
||||
&& LobbyQuery::lobby_by_channel_id(db.connection(), guild_id, id)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
is_in_lobby = true;
|
||||
channel_id = Some(id);
|
||||
break;
|
||||
@@ -144,25 +193,49 @@ impl LobbyCommand {
|
||||
}
|
||||
|
||||
if !is_in_lobby {
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("You are not in the mix lobby!").ephemeral(true)
|
||||
message
|
||||
.content("You are not in the mix lobby!")
|
||||
.ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lobby = db.get_lobby_by_channel(guild_id, channel_id.unwrap()).await.unwrap();
|
||||
let lobby = LobbyQuery::lobby_by_channel_id(db.connection(), guild_id, channel_id.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
for member in red_channel.members(ctx).await? {
|
||||
member.move_to_voice_channel(ctx, main_channel.id).await?;
|
||||
@@ -171,90 +244,163 @@ impl LobbyCommand {
|
||||
member.move_to_voice_channel(ctx, main_channel.id).await?;
|
||||
}
|
||||
|
||||
// TODO: uncomment this
|
||||
let members = main_channel.members(ctx).await?;
|
||||
let users = members.iter().map(|m| m.user.id).collect::<Vec<UserId>>();
|
||||
// let users = (0..11).map(|id| UserId::from(id)).chain(users.into_iter()).collect::<Vec<UserId>>();
|
||||
let players = db.get_players(users).await;
|
||||
let players = players.into_iter().map(|p| Player::new(p)).collect::<Vec<Player>>();
|
||||
let slots = vec![Role::Tank, Role::Dps, Role::Dps, Role::Support, Role::Support];
|
||||
|
||||
interaction.edit_original_interaction_response(ctx, |response| {
|
||||
response.content("Mixing teams...")
|
||||
}).await?;
|
||||
let players = PlayerQuery::players_by_user_ids(db.connection(), users).await;
|
||||
|
||||
let players = match players {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
interaction
|
||||
.edit_original_interaction_response(ctx, |response| {
|
||||
response.content(format!("Failed to get players"))
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let players = players
|
||||
.into_iter()
|
||||
.map(|p| Player::new(p))
|
||||
.collect::<Vec<Player>>();
|
||||
let slots = vec![
|
||||
Role::Tank,
|
||||
Role::DPS,
|
||||
Role::DPS,
|
||||
Role::Support,
|
||||
Role::Support,
|
||||
];
|
||||
|
||||
interaction
|
||||
.edit_original_interaction_response(ctx, |response| response.content("Mixing teams..."))
|
||||
.await?;
|
||||
|
||||
if let Some(teams) = mixer::mix_players(&players, slots) {
|
||||
// let interaction = interaction.clone();
|
||||
self.process_valid_teams(ctx, interaction, lobby, teams, players).await?;
|
||||
}
|
||||
else {
|
||||
interaction.edit_original_interaction_response(ctx, |response| {
|
||||
self.process_valid_teams(ctx, interaction, lobby, teams, players)
|
||||
.await?;
|
||||
} else {
|
||||
interaction
|
||||
.edit_original_interaction_response(ctx, |response| {
|
||||
response.content("Fair teams' composition could not be found!")
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_valid_teams(&self, ctx: &Context, interaction: ApplicationCommandInteraction, lobby: Model, teams: (Team, Team), players: Vec<Player>) -> serenity::Result<()> {
|
||||
async fn process_valid_teams(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
lobby: Model,
|
||||
teams: (Team, Team),
|
||||
players: Vec<Player>,
|
||||
) -> serenity::Result<()> {
|
||||
let (team1, team2) = teams.clone();
|
||||
|
||||
let team1_names = team1.players.iter()
|
||||
let team1_names = team1
|
||||
.players
|
||||
.iter()
|
||||
.sorted_by(|((a, _), _), ((b, _), _)| i32::from(*a).cmp(&i32::from(*b)))
|
||||
.map(|(_, i)| async {
|
||||
if let Some(user) = players[i.unwrap()].discord_id.to_user(ctx).await.ok() {
|
||||
user.name
|
||||
} else {
|
||||
players[i.unwrap()]
|
||||
.bn_name
|
||||
.clone()
|
||||
.unwrap_or("Unknown".to_string())
|
||||
}
|
||||
else {
|
||||
players[i.unwrap()].bn_name.clone()
|
||||
}
|
||||
}).collect_vec();
|
||||
let team2_names = team2.players.iter()
|
||||
})
|
||||
.collect_vec();
|
||||
let team2_names = team2
|
||||
.players
|
||||
.iter()
|
||||
.sorted_by(|((a, _), _), ((b, _), _)| i32::from(*a).cmp(&i32::from(*b)))
|
||||
.map(|(_, i)| async {
|
||||
if let Some(user) = players[i.unwrap()].discord_id.to_user(ctx).await.ok() {
|
||||
user.name
|
||||
} else {
|
||||
players[i.unwrap()]
|
||||
.bn_name
|
||||
.clone()
|
||||
.unwrap_or("Unknown".to_string())
|
||||
}
|
||||
else {
|
||||
players[i.unwrap()].bn_name.clone()
|
||||
}
|
||||
}).collect_vec();
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let team1_names = join_all(team1_names).await;
|
||||
let team2_names = join_all(team2_names).await;
|
||||
|
||||
interaction.edit_original_interaction_response(ctx, |response| {
|
||||
response.content("").components(|components| {
|
||||
interaction
|
||||
.edit_original_interaction_response(ctx, |response| {
|
||||
response
|
||||
.content("")
|
||||
.components(|components| {
|
||||
components.create_action_row(|row| {
|
||||
row.create_button(|button| {
|
||||
button.custom_id("cancel").label("Cancel")
|
||||
button
|
||||
.custom_id("cancel")
|
||||
.label("Cancel")
|
||||
.style(ButtonStyle::Danger)
|
||||
});
|
||||
row.create_button(|button| {
|
||||
button.custom_id("swap").label("Swap").disabled(true)
|
||||
button
|
||||
.custom_id("swap")
|
||||
.label("Swap")
|
||||
.disabled(true)
|
||||
.style(ButtonStyle::Primary)
|
||||
});
|
||||
row.create_button(|button| {
|
||||
button.custom_id("start").label("Start")
|
||||
button
|
||||
.custom_id("start")
|
||||
.label("Start")
|
||||
.style(ButtonStyle::Success)
|
||||
})
|
||||
})
|
||||
}).embed(|embed| {
|
||||
embed.title("Teams").fields(vec![
|
||||
})
|
||||
.embed(|embed| {
|
||||
embed
|
||||
.title("Teams")
|
||||
.fields(vec![
|
||||
("Team 1", "", false),
|
||||
("Tank", &team1_names[0], true),
|
||||
("Dps", &format!("{}\n{}", team1_names[1], team1_names[2]), true),
|
||||
("Support", &format!("{}\n{}", team1_names[3], team1_names[4]), true),
|
||||
(
|
||||
"Dps",
|
||||
&format!("{}\n{}", team1_names[1], team1_names[2]),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"Support",
|
||||
&format!("{}\n{}", team1_names[3], team1_names[4]),
|
||||
true,
|
||||
),
|
||||
("Team 2", "", false),
|
||||
("Tank", &team2_names[0], true),
|
||||
("Dps", &format!("{}\n{}", team2_names[1], team2_names[2]), true),
|
||||
("Support", &format!("{}\n{}", team2_names[3], team2_names[4]), true),
|
||||
]).colour(Colour::new(0xcfa22f))
|
||||
(
|
||||
"Dps",
|
||||
&format!("{}\n{}", team2_names[1], team2_names[2]),
|
||||
true,
|
||||
),
|
||||
(
|
||||
"Support",
|
||||
&format!("{}\n{}", team2_names[3], team2_names[4]),
|
||||
true,
|
||||
),
|
||||
])
|
||||
.colour(Colour::new(0xcfa22f))
|
||||
})
|
||||
}).await.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = interaction.get_interaction_response(ctx).await.unwrap();
|
||||
let collector = msg.await_component_interactions(ctx)
|
||||
let collector = msg
|
||||
.await_component_interactions(ctx)
|
||||
.timeout(Duration::from_secs(2 * 60))
|
||||
.guild_id(interaction.guild_id.unwrap())
|
||||
.channel_id(interaction.channel_id)
|
||||
@@ -265,67 +411,120 @@ impl LobbyCommand {
|
||||
let interactions = collector.collect::<Vec<_>>().await;
|
||||
if let Some(interaction) = interactions.first() {
|
||||
match interaction.data.custom_id.as_str() {
|
||||
"start" => self.process_valid_teams_start(ctx, interaction, lobby, &team1, &team2, players).await?,
|
||||
"cancel" => self.process_valid_teams_cancel(ctx, interaction, &team1, &team2).await?,
|
||||
"swap" => self.process_valid_teams_swap(ctx, interaction, &team1, &team2).await?,
|
||||
"start" => {
|
||||
self.process_valid_teams_start(ctx, interaction, lobby, &team1, &team2, players)
|
||||
.await?
|
||||
}
|
||||
"cancel" => {
|
||||
self.process_valid_teams_cancel(ctx, interaction, &team1, &team2)
|
||||
.await?
|
||||
}
|
||||
"swap" => {
|
||||
self.process_valid_teams_swap(ctx, interaction, &team1, &team2)
|
||||
.await?
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
else {
|
||||
interaction.delete_original_interaction_response(ctx).await?;
|
||||
} else {
|
||||
interaction
|
||||
.delete_original_interaction_response(ctx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_valid_teams_start(&self, ctx: &Context, interaction: &MessageComponentInteraction, lobby: Model, team1: &Team, team2: &Team, players: Vec<Player>) -> serenity::Result<()> {
|
||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx).await.unwrap().guild().unwrap();
|
||||
async fn process_valid_teams_start(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: &MessageComponentInteraction,
|
||||
lobby: Model,
|
||||
team1: &Team,
|
||||
team2: &Team,
|
||||
players: Vec<Player>,
|
||||
) -> serenity::Result<()> {
|
||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64)
|
||||
.to_channel(ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.guild()
|
||||
.unwrap();
|
||||
|
||||
for member in main_channel.members(ctx).await? {
|
||||
let index = players.iter().position(|p| p.discord_id == member.user.id);
|
||||
|
||||
if team1.players.iter().any(|(_, i)| *i == index && index.is_some()) {
|
||||
if team1
|
||||
.players
|
||||
.iter()
|
||||
.any(|(_, i)| *i == index && index.is_some())
|
||||
{
|
||||
member.move_to_voice_channel(ctx, red_channel.id).await?;
|
||||
}
|
||||
else if team2.players.iter().any(|(_, i)| *i == index && index.is_some()) {
|
||||
} else if team2
|
||||
.players
|
||||
.iter()
|
||||
.any(|(_, i)| *i == index && index.is_some())
|
||||
{
|
||||
member.move_to_voice_channel(ctx, blue_channel.id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let embed = interaction.message.embeds.get(0).unwrap();
|
||||
interaction.delete_original_interaction_response(ctx).await?;
|
||||
interaction
|
||||
.delete_original_interaction_response(ctx)
|
||||
.await?;
|
||||
|
||||
// create msg without interaction
|
||||
let msg = interaction.channel_id.send_message(ctx, |message| {
|
||||
message.content(format!("<@{}>", interaction.user.id.0))
|
||||
let msg = interaction
|
||||
.channel_id
|
||||
.send_message(ctx, |message| {
|
||||
message
|
||||
.content(format!("<@{}>", interaction.user.id.0))
|
||||
.set_embeds(vec![CreateEmbed::from(embed.clone())])
|
||||
.components(|components| {
|
||||
components.create_action_row(|row| {
|
||||
components
|
||||
.create_action_row(|row| {
|
||||
row.create_button(|button| {
|
||||
button.custom_id("win_team1")
|
||||
button
|
||||
.custom_id("win_team1")
|
||||
.label("Team 1 win")
|
||||
.style(ButtonStyle::Success)
|
||||
}).create_button(|button| {
|
||||
button.custom_id("draw")
|
||||
})
|
||||
.create_button(|button| {
|
||||
button
|
||||
.custom_id("draw")
|
||||
.label("Draw")
|
||||
.style(ButtonStyle::Secondary)
|
||||
}).create_button(|button| {
|
||||
button.custom_id("win_team2")
|
||||
})
|
||||
.create_button(|button| {
|
||||
button
|
||||
.custom_id("win_team2")
|
||||
.label("Team 2 win")
|
||||
.style(ButtonStyle::Success)
|
||||
})
|
||||
}).create_action_row(|row| {
|
||||
})
|
||||
.create_action_row(|row| {
|
||||
row.create_button(|button| {
|
||||
button.custom_id("cancel")
|
||||
button
|
||||
.custom_id("cancel")
|
||||
.label("Cancel game")
|
||||
.style(ButtonStyle::Danger)
|
||||
})
|
||||
})
|
||||
})
|
||||
}).await?;
|
||||
|
||||
})
|
||||
.await?;
|
||||
|
||||
// let msg = interaction.edit_original_interaction_response(ctx, |message| {
|
||||
// message.components(|components| {
|
||||
@@ -353,7 +552,8 @@ impl LobbyCommand {
|
||||
// })
|
||||
// }).await?;
|
||||
|
||||
let collector = msg.await_component_interactions(ctx)
|
||||
let collector = msg
|
||||
.await_component_interactions(ctx)
|
||||
.timeout(Duration::from_secs(30 * 60))
|
||||
.guild_id(interaction.guild_id.unwrap())
|
||||
.channel_id(interaction.channel_id)
|
||||
@@ -378,22 +578,30 @@ impl LobbyCommand {
|
||||
let team1_average_rating = team1.average_rating(&players);
|
||||
let team2_average_rating = team2.average_rating(&players);
|
||||
|
||||
let team1 = team1.players.clone().into_iter()
|
||||
.filter_map(|((role, _), player)|
|
||||
let team1 = team1
|
||||
.players
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|((role, _), player)| {
|
||||
if player.is_some() {
|
||||
Some((role, player.unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
).collect::<Vec<_>>();
|
||||
let team2 = team2.players.clone().into_iter()
|
||||
.filter_map(|((role, _), player)|
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let team2 = team2
|
||||
.players
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|((role, _), player)| {
|
||||
if player.is_some() {
|
||||
Some((role, player.unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
).collect::<Vec<_>>();
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
@@ -401,13 +609,37 @@ impl LobbyCommand {
|
||||
for (role, index) in team1 {
|
||||
let mut rating = players[index].ranks[&role];
|
||||
rating.update(&team2_average_rating, score);
|
||||
db.update_player_rank(players[index].discord_id, Some(role), rating).await;
|
||||
PlayerQuery::update_rating(
|
||||
db.connection(),
|
||||
players[index].discord_id,
|
||||
role,
|
||||
rating,
|
||||
)
|
||||
.await;
|
||||
PlayerQuery::update_last_played(
|
||||
db.connection(),
|
||||
players[index].discord_id,
|
||||
Utc::now().naive_utc(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
for (role, index) in team2 {
|
||||
let mut rating = players[index].ranks[&role];
|
||||
rating.update(&team1_average_rating, 1.0 - score);
|
||||
db.update_player_rank(players[index].discord_id, Some(role), rating).await;
|
||||
PlayerQuery::update_rating(
|
||||
db.connection(),
|
||||
players[index].discord_id,
|
||||
role,
|
||||
rating,
|
||||
)
|
||||
.await;
|
||||
PlayerQuery::update_last_played(
|
||||
db.connection(),
|
||||
players[index].discord_id,
|
||||
Utc::now().naive_utc(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
drop(db);
|
||||
@@ -417,12 +649,23 @@ impl LobbyCommand {
|
||||
msg.delete(ctx).await
|
||||
}
|
||||
|
||||
async fn process_valid_teams_cancel(&self, ctx: &Context, interaction: &MessageComponentInteraction, team1: &Team, team2: &Team) -> serenity::Result<()> {
|
||||
async fn process_valid_teams_cancel(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: &MessageComponentInteraction,
|
||||
team1: &Team,
|
||||
team2: &Team,
|
||||
) -> serenity::Result<()> {
|
||||
interaction.delete_original_interaction_response(ctx).await
|
||||
}
|
||||
|
||||
async fn process_valid_teams_swap(&self, ctx: &Context, interaction: &MessageComponentInteraction, team1: &Team, team2: &Team) -> serenity::Result<()> {
|
||||
|
||||
async fn process_valid_teams_swap(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: &MessageComponentInteraction,
|
||||
team1: &Team,
|
||||
team2: &Team,
|
||||
) -> serenity::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
pub mod ping;
|
||||
pub mod creator;
|
||||
pub mod lobby;
|
||||
pub mod rank;
|
||||
pub mod ping;
|
||||
pub mod preference;
|
||||
pub mod rating;
|
||||
pub mod settings;
|
||||
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||
use serenity::async_trait;
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait MixerCommand: Sync + Send {
|
||||
fn name(&self) -> String;
|
||||
fn create(&self, command: &mut CreateApplicationCommand);
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()>;
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction,
|
||||
InteractionResponseType
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
};
|
||||
use serenity::async_trait;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -17,24 +17,36 @@ impl MixerCommand for PingCommand {
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Hello world!");
|
||||
command
|
||||
.name(self.name())
|
||||
.description("Hello world!")
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let content = "Pong!";
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(content).ephemeral(true)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(content).ephemeral(true))
|
||||
})
|
||||
}).await?;
|
||||
.await?;
|
||||
|
||||
let follow1 = interaction.create_followup_message(ctx, |followup| {
|
||||
let follow1 = interaction
|
||||
.create_followup_message(ctx, |followup| {
|
||||
followup.content("followup1").ephemeral(true)
|
||||
}).await?;
|
||||
let follow2 = interaction.create_followup_message(ctx, |followup| {
|
||||
})
|
||||
.await?;
|
||||
let follow2 = interaction
|
||||
.create_followup_message(ctx, |followup| {
|
||||
followup.content("followup2").ephemeral(true)
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
interaction.delete_followup_message(ctx, follow1).await?;
|
||||
interaction.delete_followup_message(ctx, follow2).await?;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::str::FromStr;
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction,
|
||||
InteractionResponseType
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
};
|
||||
use serenity::async_trait;
|
||||
use serenity::model::Permissions;
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use serenity::model::prelude::interaction::application_command::CommandDataOptionValue::User;
|
||||
use serenity::model::Permissions;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::models::role::Role;
|
||||
use crate::database::queries::prelude::*;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PreferenceCommand;
|
||||
@@ -23,29 +23,43 @@ impl MixerCommand for PreferenceCommand {
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Hello world!")
|
||||
command
|
||||
.name(self.name())
|
||||
.description("Hello world!")
|
||||
.create_option(|option| {
|
||||
option.name("set").description("Set role preference for user")
|
||||
option
|
||||
.name("set")
|
||||
.description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommandGroup)
|
||||
.create_sub_option(|option| {
|
||||
option.name("flex").description("Set role preference for user")
|
||||
option
|
||||
.name("flex")
|
||||
.description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user").description("User to set preference for")
|
||||
option
|
||||
.name("user")
|
||||
.description("User to set preference for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("complex").description("Set role preference for user")
|
||||
option
|
||||
.name("complex")
|
||||
.description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user").description("User to set preference for")
|
||||
option
|
||||
.name("user")
|
||||
.description("User to set preference for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("first").description("First role preference")
|
||||
option
|
||||
.name("first")
|
||||
.description("First role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
@@ -54,7 +68,9 @@ impl MixerCommand for PreferenceCommand {
|
||||
.add_string_choice("None", "none")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("second").description("Second role preference")
|
||||
option
|
||||
.name("second")
|
||||
.description("Second role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
@@ -63,7 +79,9 @@ impl MixerCommand for PreferenceCommand {
|
||||
.add_string_choice("None", "none")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("third").description("Third role preference")
|
||||
option
|
||||
.name("third")
|
||||
.description("Third role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
@@ -77,16 +95,37 @@ impl MixerCommand for PreferenceCommand {
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let user = match interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.resolved
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
{
|
||||
User(user, _) => user,
|
||||
_ => {
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("User not found")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -96,27 +135,112 @@ impl MixerCommand for PreferenceCommand {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
match interaction.data.options.get(0).unwrap().options.get(0).unwrap().name.as_str() {
|
||||
match interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.name
|
||||
.as_str()
|
||||
{
|
||||
"flex" => {
|
||||
db.update_player_preference(user.id, true, None, None, None).await;
|
||||
},
|
||||
PlayerQuery::update_preference(
|
||||
db.connection(),
|
||||
user.id,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
"complex" => {
|
||||
let role1 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok();
|
||||
let role2 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok();
|
||||
let role3 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(3).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok();
|
||||
let role1 = Role::try_from(
|
||||
interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok();
|
||||
let role2 = Role::try_from(
|
||||
interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok();
|
||||
let role3 = Role::try_from(
|
||||
interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(3)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok();
|
||||
|
||||
db.update_player_preference(user.id, false, role1, role2, role3).await;
|
||||
},
|
||||
PlayerQuery::update_preference(
|
||||
db.connection(),
|
||||
user.id,
|
||||
false,
|
||||
role1,
|
||||
role2,
|
||||
role3,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Preference set for {}", user.name)).ephemeral(true)
|
||||
message
|
||||
.content(format!("Preference set for {}", user.name))
|
||||
.ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{application_command::ApplicationCommandInteraction, InteractionResponseType};
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::interaction::application_command::CommandDataOptionValue::User;
|
||||
use serenity::model::Permissions;
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::rating::Rating;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RankCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for RankCommand {
|
||||
fn name(&self) -> String {
|
||||
"rank".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Manage user ranks")
|
||||
.create_option(|option| {
|
||||
option.name("set")
|
||||
.description("Set a user's rank")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user")
|
||||
.description("The user to set the rank for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("role")
|
||||
.description("The role to set the rank for")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("tank", "tank")
|
||||
.add_string_choice("dps", "dps")
|
||||
.add_string_choice("support", "support")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("rank")
|
||||
.description("The rank to set")
|
||||
.kind(CommandOptionType::Integer)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"set" => {
|
||||
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
||||
User(user, _) => user,
|
||||
_ => {
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("User not found")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let role = Role::from_str(interaction.data.options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).ok();
|
||||
let rank = interaction.data.options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_u64().unwrap();
|
||||
|
||||
if rank < 1 || rank > 5000 {
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Rank must be between 1 and 5000")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
db.update_player_rank(user.id, role, Rating::new_no_sigma(
|
||||
rank as f32,
|
||||
300.0
|
||||
)).await;
|
||||
}
|
||||
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Setting rank for user {:?} to {:?} {:?}", user.id, role, rank))
|
||||
})
|
||||
}).await?;
|
||||
Ok(())
|
||||
},
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/bot/commands/rating.rs
Normal file
179
src/bot/commands/rating.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::CommandDataOptionValue::User;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
};
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use serenity::model::Permissions;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::models::role::Role;
|
||||
use crate::database::queries::prelude::*;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::rating::Rating;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RatingCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for RatingCommand {
|
||||
fn name(&self) -> String {
|
||||
"rating".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command
|
||||
.name(self.name())
|
||||
.description("")
|
||||
.create_option(|option| {
|
||||
option
|
||||
.name("set")
|
||||
.description("Set a user's rank")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option
|
||||
.name("user")
|
||||
.description("The user to set the rank for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option
|
||||
.name("role")
|
||||
.description("The role to set the rank for")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("tank", "tank")
|
||||
.add_string_choice("dps", "dps")
|
||||
.add_string_choice("support", "support")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option
|
||||
.name("rank")
|
||||
.description("The rank to set")
|
||||
.kind(CommandOptionType::Integer)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"set" => {
|
||||
let user = match interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.resolved
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
{
|
||||
User(user, _) => user,
|
||||
_ => {
|
||||
interaction
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("User not found")).ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let role = Role::try_from(
|
||||
interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
let rating = interaction
|
||||
.data
|
||||
.options
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.options
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.value
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
|
||||
if rating < 1 || rating > 5000 {
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content(format!("Rank must be between 1 and 5000"))
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
PlayerQuery::update_rating(
|
||||
db.connection(),
|
||||
user.id,
|
||||
role,
|
||||
Rating::new_no_sigma(rating as f32, 125.0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content(format!(
|
||||
"Setting rank for user <@{}> to {} {}",
|
||||
user.id,
|
||||
String::from(role),
|
||||
rating
|
||||
))
|
||||
.allowed_mentions(|mentions| mentions.empty_users())
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::collections::hash_map::RandomState;
|
||||
use std::collections::HashMap;
|
||||
use serenity::async_trait;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::{ApplicationCommandInteraction, CommandDataOption};
|
||||
use serenity::model::application::interaction::InteractionResponseType;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::command::CommandOptionType;
|
||||
use serenity::model::application::interaction::application_command::{
|
||||
ApplicationCommandInteraction, CommandDataOption,
|
||||
};
|
||||
use serenity::model::application::interaction::InteractionResponseType;
|
||||
use serenity::model::Permissions;
|
||||
use std::collections::hash_map::RandomState;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::mixer::role::Role;
|
||||
use crate::database::models::role::Role;
|
||||
|
||||
pub struct SettingsCommand;
|
||||
|
||||
@@ -19,47 +22,71 @@ impl MixerCommand for SettingsCommand {
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(&self.name()).description("Change your server settings")
|
||||
command
|
||||
.name(&self.name())
|
||||
.description("Change your server settings")
|
||||
.create_option(|option| {
|
||||
option.name("roles").kind(CommandOptionType::SubCommandGroup)
|
||||
option
|
||||
.name("roles")
|
||||
.kind(CommandOptionType::SubCommandGroup)
|
||||
.description("Change your role settings")
|
||||
.create_sub_option(|sub_option| {
|
||||
sub_option.name("automatic").kind(CommandOptionType::SubCommand)
|
||||
sub_option
|
||||
.name("automatic")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.description("Automatically assign roles based on your rank")
|
||||
})
|
||||
}).default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
})
|
||||
.default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
let data = interaction.data.options.get(0).unwrap().clone();
|
||||
match data.name.as_str() {
|
||||
"roles" => self.process_roles_subcommand(ctx, interaction.clone(), data).await?,
|
||||
"roles" => {
|
||||
self.process_roles_subcommand(ctx, interaction.clone(), data)
|
||||
.await?
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
interaction.create_interaction_response(ctx, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("Settings updated")
|
||||
interaction
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content("Settings updated"))
|
||||
})
|
||||
}).await?;
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsCommand {
|
||||
async fn process_roles_subcommand(&self, ctx: &Context, interaction: ApplicationCommandInteraction, data: CommandDataOption) -> serenity::Result<()> {
|
||||
async fn process_roles_subcommand(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
data: CommandDataOption,
|
||||
) -> serenity::Result<()> {
|
||||
match data.options.get(0).unwrap().name.as_str() {
|
||||
"automatic" => {
|
||||
let roles: HashMap<_, _, RandomState> = HashMap::from_iter([
|
||||
let roles: HashMap<_, _, RandomState> = HashMap::from_iter(
|
||||
[
|
||||
("support", Role::Support),
|
||||
("damage", Role::Dps),
|
||||
("dps", Role::Dps),
|
||||
("tank", Role::Tank)
|
||||
].into_iter());
|
||||
let ranks: HashMap<_, _, RandomState> = HashMap::from_iter([
|
||||
("damage", Role::DPS),
|
||||
("dps", Role::DPS),
|
||||
("tank", Role::Tank),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
let ranks: HashMap<_, _, RandomState> = HashMap::from_iter(
|
||||
[
|
||||
("bronze", 0),
|
||||
("silver", 1),
|
||||
("gold", 2),
|
||||
@@ -67,9 +94,16 @@ impl SettingsCommand {
|
||||
("diamond", 4),
|
||||
("master", 5),
|
||||
("grandmaster", 6),
|
||||
].into_iter());
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let guild = interaction.guild_id.unwrap().to_partial_guild(ctx).await.unwrap();
|
||||
let guild = interaction
|
||||
.guild_id
|
||||
.unwrap()
|
||||
.to_partial_guild(ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
// let guild_roles = guild.roles;
|
||||
let mut guild_roles = HashMap::new();
|
||||
for (id, guild_role) in guild.roles {
|
||||
@@ -84,8 +118,7 @@ impl SettingsCommand {
|
||||
}
|
||||
if !guild_roles.contains_key(&id) {
|
||||
guild_roles.insert(id, (role, rank));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let (_, old_rank) = guild_roles.get(&id).unwrap();
|
||||
if rank > *old_rank {
|
||||
guild_roles.insert(id, (role, rank));
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use serenity::builder::CreateApplicationCommands;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||
use serenity::model::prelude::interaction::InteractionResponseType;
|
||||
use std::collections::HashMap;
|
||||
use tracing::log::info;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
|
||||
pub struct MixerCommandHandler {
|
||||
@@ -10,9 +13,7 @@ pub struct MixerCommandHandler {
|
||||
|
||||
impl MixerCommandHandler {
|
||||
pub fn new(commands: HashMap<String, Box<dyn MixerCommand>>) -> Self {
|
||||
Self {
|
||||
commands
|
||||
}
|
||||
Self { commands }
|
||||
}
|
||||
|
||||
pub fn add_command<T: MixerCommand + 'static>(&mut self, command: T) {
|
||||
@@ -25,13 +26,38 @@ impl MixerCommandHandler {
|
||||
command.create(create_command);
|
||||
create_command
|
||||
});
|
||||
println!("Registered command \"{}\"", command.name())
|
||||
info!("Registered command \"{}\"", command.name())
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn handle_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
pub async fn handle_command(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: ApplicationCommandInteraction,
|
||||
) -> serenity::Result<()> {
|
||||
if let Some(command) = self.commands.get(&interaction.data.name) {
|
||||
return command.execute(&ctx, interaction).await
|
||||
info!(
|
||||
"User {} ({}) executed command \"{}\"",
|
||||
interaction.user.name,
|
||||
interaction.user.id,
|
||||
command.name()
|
||||
);
|
||||
return command.execute(&ctx, interaction).await;
|
||||
} else {
|
||||
info!(
|
||||
"User {} ({}) executed unknown command \"{}\"",
|
||||
interaction.user.name, interaction.user.id, interaction.data.name
|
||||
);
|
||||
interaction
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("This command does not exist!")
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use serenity::client::Context;
|
||||
use serenity::async_trait;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::message_component::MessageComponentInteraction;
|
||||
|
||||
#[async_trait]
|
||||
pub trait MixerInteraction: Sync + Send {
|
||||
fn custom_id(&self) -> String;
|
||||
// fn create(&self, command: &mut CreateApplicationCommand);
|
||||
async fn execute(&self, ctx: &Context, interaction: MessageComponentInteraction) -> serenity::Result<()>;
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
interaction: MessageComponentInteraction,
|
||||
) -> serenity::Result<()>;
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
pub mod commands;
|
||||
pub mod interactions;
|
||||
mod handlers;
|
||||
pub mod interactions;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serenity::async_trait;
|
||||
use serenity::client::{Context, EventHandler};
|
||||
use serenity::http::CacheHttp;
|
||||
use serenity::model::application::command::Command;
|
||||
use serenity::model::application::interaction::{Interaction, InteractionResponseType};
|
||||
use serenity::model::gateway::Ready;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::command::Command;
|
||||
use serenity::model::prelude::VoiceState;
|
||||
use std::collections::HashMap;
|
||||
use tracing::log::info;
|
||||
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::bot::handlers::command_handler::MixerCommandHandler;
|
||||
use crate::database::queries::prelude::*;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::CreatorContainer;
|
||||
|
||||
pub struct MixerBot {
|
||||
command_handler: MixerCommandHandler,
|
||||
@@ -22,7 +25,7 @@ pub struct MixerBot {
|
||||
impl MixerBot {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
command_handler: MixerCommandHandler::new(HashMap::new())
|
||||
command_handler: MixerCommandHandler::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +38,14 @@ impl MixerBot {
|
||||
#[async_trait]
|
||||
impl EventHandler for MixerBot {
|
||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
||||
println!("{} is connected!", data_about_bot.user.name);
|
||||
info!("{} is connected!", data_about_bot.user.name);
|
||||
|
||||
Command::set_global_application_commands(&ctx.http, |commands| {
|
||||
self.command_handler.create_all(commands);
|
||||
commands
|
||||
}).await.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn voice_state_update(&self, ctx: Context, _old: Option<VoiceState>, new: VoiceState) {
|
||||
@@ -49,16 +54,16 @@ impl EventHandler for MixerBot {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
if let Some(_) = db.get_lobby_by_channel(guild_id, channel_id).await {
|
||||
if let Some(_) =
|
||||
LobbyQuery::lobby_by_channel_id(db.connection(), guild_id, channel_id).await
|
||||
{
|
||||
if let Some(member) = new.member {
|
||||
if member.user.bot {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if db.get_player(new.user_id).await.is_none() {
|
||||
db.insert_player(new.user_id).await;
|
||||
}
|
||||
PlayerQuery::create_if_not_exists(db.connection(), new.user_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,12 +72,52 @@ impl EventHandler for MixerBot {
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
match interaction {
|
||||
Interaction::ApplicationCommand(command) => {
|
||||
self.command_handler.handle_command(&ctx, command).await.unwrap();
|
||||
let has_permission = {
|
||||
let data = ctx.data.read().await;
|
||||
let creator = data.get::<CreatorContainer>().unwrap().clone();
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
let guild = GuildQuery::create_if_not_exists(
|
||||
db.connection(),
|
||||
command.guild_id.unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let verified = match guild {
|
||||
Some(guild) => guild.verified,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
verified || command.user.id == *creator
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
command
|
||||
.create_interaction_response(ctx, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content("You do not have permission to use this bot!")
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
self.command_handler
|
||||
.handle_command(&ctx, command)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Interaction::MessageComponent(component) => {
|
||||
component.create_interaction_response(ctx.http(), |response| {
|
||||
component
|
||||
.create_interaction_response(ctx.http(), |response| {
|
||||
response.kind(InteractionResponseType::DeferredUpdateMessage)
|
||||
}).await.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,143 +1,25 @@
|
||||
pub mod models;
|
||||
pub mod queries;
|
||||
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, SqlxPostgresConnector};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use serenity::model::prelude::GuildId;
|
||||
use sea_orm::{DatabaseConnection, SqlxPostgresConnector};
|
||||
use serenity::prelude::TypeMapKey;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use crate::mixer::rating::Rating;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
pub struct MixerDatabase {
|
||||
connection: DatabaseConnection
|
||||
connection: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl MixerDatabase {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self {
|
||||
connection: SqlxPostgresConnector::from_sqlx_postgres_pool(pool)
|
||||
connection: SqlxPostgresConnector::from_sqlx_postgres_pool(pool),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_player(&self, id: UserId) {
|
||||
let player = models::player::ActiveModel {
|
||||
discord_id: Set(id.0 as i64),
|
||||
bn_name: Set("".to_string()),
|
||||
bn_tag: Set("".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
player.insert(&self.connection)
|
||||
.await
|
||||
.expect("Could not insert player into database");
|
||||
}
|
||||
|
||||
pub async fn update_player_rank(&self, id: UserId, role: Option<Role>, rating: Rating) {
|
||||
if self.get_player(id).await.is_none() {
|
||||
self.insert_player(id).await;
|
||||
}
|
||||
|
||||
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
||||
|
||||
match role {
|
||||
Some(Role::Tank) => {
|
||||
player.tank_rating = Set(rating.value);
|
||||
player.tank_rd = Set(rating.rd);
|
||||
player.tank_volatility = Set(rating.volatility);
|
||||
},
|
||||
Some(Role::Dps) => {
|
||||
player.dps_rating = Set(rating.value);
|
||||
player.dps_rd = Set(rating.rd);
|
||||
player.dps_volatility = Set(rating.volatility);
|
||||
},
|
||||
Some(Role::Support) => {
|
||||
player.support_rating = Set(rating.value);
|
||||
player.support_rd = Set(rating.rd);
|
||||
player.support_volatility = Set(rating.volatility);
|
||||
},
|
||||
None => return
|
||||
}
|
||||
|
||||
player.update(&self.connection)
|
||||
.await
|
||||
.expect("Could not update player rank in database");
|
||||
}
|
||||
|
||||
pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Option<Role>, secondary: Option<Role>, tertiary: Option<Role>) {
|
||||
if self.get_player(id).await.is_none() {
|
||||
self.insert_player(id).await;
|
||||
}
|
||||
|
||||
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
||||
|
||||
player.flex = Set(flex);
|
||||
player.primary_role = Set(Role::option_to_i32(primary));
|
||||
player.secondary_role = Set(Role::option_to_i32(secondary));
|
||||
player.tertiary_role = Set(Role::option_to_i32(tertiary));
|
||||
|
||||
player.update(&self.connection)
|
||||
.await
|
||||
.expect("Could not update player preference in database");
|
||||
}
|
||||
|
||||
pub async fn get_player(&self, id: UserId) -> Option<models::player::Model> {
|
||||
models::player::Entity::find()
|
||||
.filter(models::player::Column::DiscordId.eq(id.0))
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get player from database")
|
||||
}
|
||||
|
||||
pub async fn get_players(&self, ids: Vec<UserId>) -> Vec<models::player::Model> {
|
||||
models::player::Entity::find()
|
||||
.filter(models::player::Column::DiscordId.is_in(ids.iter().map(|id| id.0).collect::<Vec<u64>>()))
|
||||
.all(&self.connection)
|
||||
.await
|
||||
.expect("Could not get players from database")
|
||||
}
|
||||
|
||||
// pub async fn get_all_guild_lobbies(&self, guild_id: GuildId) -> Vec<models::lobby::Model> {
|
||||
// models::lobby::Entity::find()
|
||||
// .filter(models::lobby::Column::GuildId.eq(guild_id.0))
|
||||
// .all(&self.connection)
|
||||
// .await
|
||||
// .expect("Could not get lobbies from database")
|
||||
// }
|
||||
|
||||
// pub async fn get_guild_lobby(&self, lobby_id: i32) -> Option<models::lobby::Model> {
|
||||
// models::lobby::Entity::find_by_id(lobby_id)
|
||||
// .one(&self.connection)
|
||||
// .await
|
||||
// .expect("Could not get lobby from database")
|
||||
// }
|
||||
|
||||
pub async fn insert_guild_lobby(&self, guild_id: GuildId, main_voice_id: ChannelId, red_team_voice_id: ChannelId, blue_team_voice_id: ChannelId) {
|
||||
let lobby = models::lobby::ActiveModel {
|
||||
guild_id: Set(guild_id.0 as i64),
|
||||
main_voice_id: Set(main_voice_id.0 as i64),
|
||||
red_team_voice_id: Set(red_team_voice_id.0 as i64),
|
||||
blue_team_voice_id: Set(blue_team_voice_id.0 as i64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
lobby.insert(&self.connection)
|
||||
.await
|
||||
.expect("Could not insert lobby into database");
|
||||
}
|
||||
|
||||
pub async fn get_lobby_by_channel(&self, guild_id: GuildId, channel_id: ChannelId) -> Option<models::lobby::Model> {
|
||||
models::lobby::Entity::find()
|
||||
.filter(models::lobby::Column::GuildId.eq(guild_id.0).and(
|
||||
models::lobby::Column::MainVoiceId.eq(channel_id.0 as i64)
|
||||
.or(models::lobby::Column::RedTeamVoiceId.eq(channel_id.0 as i64))
|
||||
.or(models::lobby::Column::BlueTeamVoiceId.eq(channel_id.0 as i64))
|
||||
))
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get lobby from database")
|
||||
pub fn connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/database/models/guild.rs
Normal file
17
src/database/models/guild.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "guilds")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub guild_id: i64,
|
||||
#[sea_orm(default_value = false)]
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -3,7 +3,7 @@ use sea_orm::entity::prelude::*;
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "lobbies")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
#[sea_orm(primary_key, auto_increment)]
|
||||
pub id: i32,
|
||||
|
||||
pub guild_id: i64,
|
||||
@@ -13,7 +13,6 @@ pub struct Model {
|
||||
}
|
||||
|
||||
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
pub(super) mod prelude;
|
||||
|
||||
pub mod guild;
|
||||
pub mod lobby;
|
||||
pub mod player;
|
||||
pub mod role;
|
||||
|
||||
@@ -1,51 +1,49 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "players")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
#[sea_orm(primary_key, auto_increment)]
|
||||
pub id: i32,
|
||||
|
||||
#[sea_orm(unique)]
|
||||
pub discord_id: i64,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub bn_name: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub bn_tag: String,
|
||||
pub bn_name: Option<String>,
|
||||
pub bn_tag: Option<String>,
|
||||
|
||||
#[sea_orm(default_value = true)]
|
||||
pub flex: bool,
|
||||
pub last_played: Option<DateTime>,
|
||||
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub tank_rating: f32,
|
||||
#[sea_orm(default_value = 580.0)]
|
||||
#[sea_orm(default_value = 300.0)]
|
||||
pub tank_rd: f32,
|
||||
#[sea_orm(default_value = 0.06)]
|
||||
pub tank_volatility: f32,
|
||||
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub dps_rating: f32,
|
||||
#[sea_orm(default_value = 580.0)]
|
||||
#[sea_orm(default_value = 300.0)]
|
||||
pub dps_rd: f32,
|
||||
#[sea_orm(default_value = 0.06)]
|
||||
pub dps_volatility: f32,
|
||||
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub support_rating: f32,
|
||||
#[sea_orm(default_value = 580.0)]
|
||||
#[sea_orm(default_value = 300.0)]
|
||||
pub support_rd: f32,
|
||||
#[sea_orm(default_value = 0.06)]
|
||||
pub support_volatility: f32,
|
||||
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub primary_role: i32,
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub secondary_role: i32,
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub tertiary_role: i32,
|
||||
#[sea_orm(default_value = true)]
|
||||
pub flex: bool,
|
||||
pub primary_role: Option<Role>,
|
||||
pub secondary_role: Option<Role>,
|
||||
pub tertiary_role: Option<Role>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
3
src/database/models/prelude.rs
Normal file
3
src/database/models/prelude.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use super::guild::Entity as Guilds;
|
||||
pub use super::lobby::Entity as Lobbies;
|
||||
pub use super::player::Entity as Players;
|
||||
45
src/database/models/role.rs
Normal file
45
src/database/models/role.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use sea_orm::{DeriveActiveEnum, EnumIter};
|
||||
|
||||
#[derive(EnumIter, DeriveActiveEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "role")]
|
||||
pub enum Role {
|
||||
#[sea_orm(string_value = "tank")]
|
||||
Tank,
|
||||
#[sea_orm(string_value = "dps")]
|
||||
DPS,
|
||||
#[sea_orm(string_value = "support")]
|
||||
Support,
|
||||
}
|
||||
|
||||
impl From<Role> for String {
|
||||
fn from(role: Role) -> Self {
|
||||
match role {
|
||||
Role::Tank => "tank".to_string(),
|
||||
Role::DPS => "dps".to_string(),
|
||||
Role::Support => "support".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Role {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(role: &str) -> Result<Self, Self::Error> {
|
||||
match role {
|
||||
"tank" => Ok(Role::Tank),
|
||||
"dps" => Ok(Role::DPS),
|
||||
"support" => Ok(Role::Support),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for i32 {
|
||||
fn from(role: Role) -> Self {
|
||||
match role {
|
||||
Role::Tank => 0,
|
||||
Role::DPS => 1,
|
||||
Role::Support => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/database/queries/guild.rs
Normal file
60
src/database/queries/guild.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{DatabaseConnection, IntoActiveModel, Set};
|
||||
use serenity::model::prelude::GuildId;
|
||||
|
||||
use crate::database::models::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn create(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
) -> Option<guild::Model> {
|
||||
let guild = guild::ActiveModel {
|
||||
guild_id: Set(guild_id.0 as i64),
|
||||
verified: Set(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
guild::Entity::insert(guild).exec(connection).await.ok()?;
|
||||
|
||||
Self::guild_by_guild_id(connection, guild_id).await
|
||||
}
|
||||
|
||||
pub async fn create_if_not_exists(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
) -> Option<guild::Model> {
|
||||
if let Some(guild) = Self::guild_by_guild_id(connection, guild_id).await {
|
||||
Some(guild)
|
||||
} else {
|
||||
Self::create(connection, guild_id).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn guild_by_guild_id(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
) -> Option<guild::Model> {
|
||||
guild::Entity::find()
|
||||
.filter(guild::Column::GuildId.eq(guild_id.0 as i64))
|
||||
.one(connection)
|
||||
.await
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub async fn set_verified(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
verified: bool,
|
||||
) -> Option<guild::Model> {
|
||||
let mut guild = Query::guild_by_guild_id(connection, guild_id)
|
||||
.await?
|
||||
.into_active_model();
|
||||
|
||||
guild.verified = Set(verified);
|
||||
|
||||
guild::Entity::update(guild).exec(connection).await.ok()
|
||||
}
|
||||
}
|
||||
50
src/database/queries/lobby.rs
Normal file
50
src/database/queries/lobby.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serenity::model::prelude::*;
|
||||
|
||||
use crate::database::models::prelude::*;
|
||||
use crate::database::models::{self, lobby};
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn create(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
main_voice_id: ChannelId,
|
||||
red_team_voice_id: ChannelId,
|
||||
blue_team_voice_id: ChannelId,
|
||||
) -> Option<lobby::Model> {
|
||||
let lobby = models::lobby::ActiveModel {
|
||||
guild_id: Set(guild_id.0 as i64),
|
||||
main_voice_id: Set(main_voice_id.0 as i64),
|
||||
red_team_voice_id: Set(red_team_voice_id.0 as i64),
|
||||
blue_team_voice_id: Set(blue_team_voice_id.0 as i64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Lobbies::insert(lobby).exec(connection).await.ok()?;
|
||||
|
||||
Self::lobby_by_channel_id(connection, guild_id, main_voice_id).await
|
||||
}
|
||||
|
||||
pub async fn lobby_by_channel_id(
|
||||
connection: &DatabaseConnection,
|
||||
guild_id: GuildId,
|
||||
channel_id: ChannelId,
|
||||
) -> Option<lobby::Model> {
|
||||
Lobbies::find()
|
||||
.filter(
|
||||
lobby::Column::GuildId.eq(guild_id.0 as i64).and(
|
||||
lobby::Column::MainVoiceId
|
||||
.eq(channel_id.0 as i64)
|
||||
.or(lobby::Column::RedTeamVoiceId.eq(channel_id.0 as i64))
|
||||
.or(lobby::Column::BlueTeamVoiceId.eq(channel_id.0 as i64)),
|
||||
),
|
||||
)
|
||||
.one(connection)
|
||||
.await
|
||||
.ok()?
|
||||
}
|
||||
}
|
||||
4
src/database/queries/mod.rs
Normal file
4
src/database/queries/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod guild;
|
||||
pub mod lobby;
|
||||
pub mod player;
|
||||
pub mod prelude;
|
||||
124
src/database/queries/player.rs
Normal file
124
src/database/queries/player.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use itertools::Itertools;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{DatabaseConnection, IntoActiveModel, Set};
|
||||
use serenity::model::prelude::UserId;
|
||||
|
||||
use crate::database::models::player;
|
||||
use crate::database::models::role::Role;
|
||||
use crate::mixer::rating::Rating;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn create(connection: &DatabaseConnection, user_id: UserId) -> Option<player::Model> {
|
||||
let player = player::ActiveModel {
|
||||
discord_id: Set(user_id.0 as i64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
player::Entity::insert(player).exec(connection).await.ok()?;
|
||||
|
||||
Self::player_by_user_id(connection, user_id).await
|
||||
}
|
||||
|
||||
pub async fn create_if_not_exists(
|
||||
connection: &DatabaseConnection,
|
||||
user_id: UserId,
|
||||
) -> Option<player::Model> {
|
||||
if let Some(player) = Self::player_by_user_id(connection, user_id).await {
|
||||
Some(player)
|
||||
} else {
|
||||
Self::create(connection, user_id).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn player_by_user_id(
|
||||
connection: &DatabaseConnection,
|
||||
user_id: UserId,
|
||||
) -> Option<player::Model> {
|
||||
player::Entity::find()
|
||||
.filter(player::Column::DiscordId.eq(user_id.0 as i64))
|
||||
.one(connection)
|
||||
.await
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub async fn players_by_user_ids(
|
||||
connection: &DatabaseConnection,
|
||||
user_ids: Vec<UserId>,
|
||||
) -> Option<Vec<player::Model>> {
|
||||
player::Entity::find()
|
||||
.filter(
|
||||
player::Column::DiscordId
|
||||
.is_in(user_ids.iter().map(|id| id.0 as i64).collect_vec()),
|
||||
)
|
||||
.all(connection)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn update_rating(
|
||||
connection: &DatabaseConnection,
|
||||
user_id: UserId,
|
||||
role: Role,
|
||||
rating: Rating,
|
||||
) -> Option<player::Model> {
|
||||
let mut player = Self::player_by_user_id(connection, user_id)
|
||||
.await?
|
||||
.into_active_model();
|
||||
|
||||
match role {
|
||||
Role::Tank => {
|
||||
player.tank_rating = Set(rating.value);
|
||||
player.tank_rd = Set(rating.rd);
|
||||
player.tank_volatility = Set(rating.volatility);
|
||||
}
|
||||
Role::DPS => {
|
||||
player.dps_rating = Set(rating.value);
|
||||
player.dps_rd = Set(rating.rd);
|
||||
player.dps_volatility = Set(rating.volatility);
|
||||
}
|
||||
Role::Support => {
|
||||
player.support_rating = Set(rating.value);
|
||||
player.support_rd = Set(rating.rd);
|
||||
player.support_volatility = Set(rating.volatility);
|
||||
}
|
||||
}
|
||||
|
||||
player::Entity::update(player).exec(connection).await.ok()
|
||||
}
|
||||
|
||||
pub async fn update_preference(
|
||||
connection: &DatabaseConnection,
|
||||
user_id: UserId,
|
||||
flex: bool,
|
||||
primary: Option<Role>,
|
||||
secondary: Option<Role>,
|
||||
tertiary: Option<Role>,
|
||||
) -> Option<player::Model> {
|
||||
let mut player = Self::player_by_user_id(connection, user_id)
|
||||
.await?
|
||||
.into_active_model();
|
||||
|
||||
player.flex = Set(flex);
|
||||
player.primary_role = Set(primary);
|
||||
player.secondary_role = Set(secondary);
|
||||
player.tertiary_role = Set(tertiary);
|
||||
|
||||
player::Entity::update(player).exec(connection).await.ok()
|
||||
}
|
||||
|
||||
pub async fn update_last_played(
|
||||
connection: &DatabaseConnection,
|
||||
user_id: UserId,
|
||||
last_played: DateTime,
|
||||
) -> Option<player::Model> {
|
||||
let mut player = Self::player_by_user_id(connection, user_id)
|
||||
.await?
|
||||
.into_active_model();
|
||||
|
||||
player.last_played = Set(Some(last_played));
|
||||
|
||||
player::Entity::update(player).exec(connection).await.ok()
|
||||
}
|
||||
}
|
||||
3
src/database/queries/prelude.rs
Normal file
3
src/database/queries/prelude.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use super::guild::Query as GuildQuery;
|
||||
pub use super::lobby::Query as LobbyQuery;
|
||||
pub use super::player::Query as PlayerQuery;
|
||||
52
src/main.rs
52
src/main.rs
@@ -1,33 +1,47 @@
|
||||
mod bot;
|
||||
mod mixer;
|
||||
mod database;
|
||||
mod algorithm;
|
||||
mod bot;
|
||||
mod database;
|
||||
mod mixer;
|
||||
|
||||
use std::sync::Arc;
|
||||
use bot::commands::creator::CreatorCommand;
|
||||
use serenity::model::prelude::UserId;
|
||||
use serenity::prelude::{GatewayIntents, TypeMapKey};
|
||||
use serenity::Client;
|
||||
use serenity::prelude::GatewayIntents;
|
||||
use shuttle_runtime::Context;
|
||||
use shuttle_secrets::SecretStore;
|
||||
use sqlx::{Executor, PgPool};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::log::info;
|
||||
|
||||
use crate::bot::commands::lobby::LobbyCommand;
|
||||
use crate::bot::commands::ping::PingCommand;
|
||||
use crate::bot::commands::preference::PreferenceCommand;
|
||||
use crate::bot::commands::rank::RankCommand;
|
||||
use crate::bot::commands::rating::RatingCommand;
|
||||
use crate::bot::commands::settings::SettingsCommand;
|
||||
use crate::bot::MixerBot;
|
||||
use crate::database::{DatabaseContainer, MixerDatabase};
|
||||
|
||||
struct CreatorContainer;
|
||||
|
||||
impl TypeMapKey for CreatorContainer {
|
||||
type Value = Arc<UserId>;
|
||||
}
|
||||
|
||||
#[shuttle_runtime::main]
|
||||
async fn serenity(
|
||||
#[shuttle_shared_db::Postgres(
|
||||
local_uri = "postgres://postgres:{secrets.PASSWORD}@localhost:5432/postgres"
|
||||
)] pool: PgPool,
|
||||
#[shuttle_secrets::Secrets] secret_store: SecretStore
|
||||
)]
|
||||
pool: PgPool,
|
||||
#[shuttle_secrets::Secrets] secret_store: SecretStore,
|
||||
) -> shuttle_serenity::ShuttleSerenity {
|
||||
let token = secret_store.get("DISCORD_TOKEN").context("'DISCORD_TOKEN' was not found")?;
|
||||
let app_id = secret_store.get("DISCORD_APP_ID").context("'DISCORD_APP_ID' was not found")?;
|
||||
let token = secret_store
|
||||
.get("DISCORD_TOKEN")
|
||||
.context("'DISCORD_TOKEN' was not found")?;
|
||||
let app_id = secret_store
|
||||
.get("DISCORD_APP_ID")
|
||||
.context("'DISCORD_APP_ID' was not found")?;
|
||||
|
||||
pool.execute(include_str!("../schema.sql")).await.unwrap();
|
||||
|
||||
@@ -35,9 +49,10 @@ async fn serenity(
|
||||
|
||||
bot.add_command(PingCommand);
|
||||
bot.add_command(LobbyCommand);
|
||||
bot.add_command(RankCommand);
|
||||
bot.add_command(RatingCommand);
|
||||
bot.add_command(PreferenceCommand);
|
||||
bot.add_command(SettingsCommand);
|
||||
bot.add_command(CreatorCommand);
|
||||
|
||||
let client = Client::builder(&token, GatewayIntents::all())
|
||||
.event_handler(bot)
|
||||
@@ -50,13 +65,24 @@ async fn serenity(
|
||||
|
||||
let db = MixerDatabase::new(pool);
|
||||
data.insert::<DatabaseContainer>(Arc::new(RwLock::new(db)));
|
||||
|
||||
let creator = UserId::from(
|
||||
secret_store
|
||||
.get("CREATOR_ID")
|
||||
.context("'CREATOR_ID' was not found")?
|
||||
.parse::<u64>()
|
||||
.unwrap(),
|
||||
);
|
||||
data.insert::<CreatorContainer>(Arc::new(creator));
|
||||
}
|
||||
|
||||
let shard_manager = client.shard_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler");
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Could not register ctrl+c handler");
|
||||
|
||||
println!("Bot has been shutdown.");
|
||||
info!("Bot has been shutdown.");
|
||||
|
||||
shard_manager.lock().await.shutdown_all().await;
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
use itertools::Itertools;
|
||||
use crate::mixer::role::Role;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::database::models::role::Role;
|
||||
use crate::mixer::player::Player;
|
||||
use crate::mixer::team::Team;
|
||||
|
||||
|
||||
struct PlayerRoleEntry {
|
||||
pub index: usize,
|
||||
pub role: Role,
|
||||
@@ -15,42 +15,50 @@ impl Eq for PlayerRoleEntry {}
|
||||
|
||||
impl PartialEq for PlayerRoleEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index && self.role == other.role
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
fn get_role_entries(
|
||||
entries: Vec<PlayerRoleEntry>,
|
||||
) -> (
|
||||
Vec<PlayerRoleEntry>,
|
||||
Vec<PlayerRoleEntry>,
|
||||
Vec<PlayerRoleEntry>,
|
||||
) {
|
||||
let (tanks, rest) = entries
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|e| e.role == Role::Tank);
|
||||
let (dps, supports) = rest.into_iter().partition(|e| e.role == Role::DPS);
|
||||
|
||||
(tanks, dps, supports)
|
||||
}
|
||||
|
||||
fn get_combinations(entries: &[PlayerRoleEntry], count: usize) -> Vec<Vec<&PlayerRoleEntry>> {
|
||||
entries
|
||||
.into_iter()
|
||||
.combinations(count)
|
||||
.sorted_by(|a, b| {
|
||||
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
pub fn mix_players(players: &[Player], slots: Vec<Role>) -> Option<(Team, Team)> {
|
||||
let players = players.to_vec();
|
||||
|
||||
let entries = calculate_priorities(&players);
|
||||
let tanks = entries.iter().filter(|e| e.role == Role::Tank).collect_vec();
|
||||
let supports = entries.iter().filter(|e| e.role == Role::Support).collect_vec();
|
||||
let dps = entries.iter().filter(|e| e.role == Role::Dps).collect_vec();
|
||||
|
||||
let (tanks, dps, supports) = get_role_entries(entries);
|
||||
|
||||
let tank_count = slots.iter().filter(|r| **r == Role::Tank).count();
|
||||
let support_count = slots.iter().filter(|r| **r == Role::Support).count();
|
||||
let dps_count = slots.iter().filter(|r| **r == Role::Dps).count();
|
||||
let dps_count = slots.iter().filter(|r| **r == Role::DPS).count();
|
||||
|
||||
let tank_combos = tanks.iter().combinations(tank_count)
|
||||
.sorted_by(|a, b| {
|
||||
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||
}).collect_vec();
|
||||
let dps_combos = dps.iter().combinations(dps_count)
|
||||
.sorted_by(|a, b| {
|
||||
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||
}).collect_vec();
|
||||
let support_combos = supports.iter().combinations(support_count)
|
||||
.sorted_by(|a, b| {
|
||||
let a = a.iter().map(|e| e.priority).sum::<f32>();
|
||||
let b = b.iter().map(|e| e.priority).sum::<f32>();
|
||||
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||
}).collect_vec();
|
||||
let tank_combos = get_combinations(&tanks, tank_count);
|
||||
let dps_combos = get_combinations(&dps, dps_count);
|
||||
let support_combos = get_combinations(&supports, support_count);
|
||||
|
||||
let mut best_team1 = None;
|
||||
let mut best_team2 = None;
|
||||
@@ -61,76 +69,128 @@ pub fn mix_players(players: &[Player], slots: Vec<Role>) -> Option<(Team, Team)>
|
||||
// this is awful, but it works
|
||||
for tank1_combo in &tank_combos {
|
||||
for tank2_combo in &tank_combos {
|
||||
if tank1_combo.iter().any(|e| tank2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank1_combo
|
||||
.iter()
|
||||
.any(|e| tank2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for dps1_combo in &dps_combos {
|
||||
if tank1_combo.iter().any(|e| dps1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank1_combo
|
||||
.iter()
|
||||
.any(|e| dps1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if tank2_combo.iter().any(|e| dps1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank2_combo
|
||||
.iter()
|
||||
.any(|e| dps1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for dps2_combo in &dps_combos {
|
||||
if tank1_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank1_combo
|
||||
.iter()
|
||||
.any(|e| dps2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if tank2_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank2_combo
|
||||
.iter()
|
||||
.any(|e| dps2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dps1_combo.iter().any(|e| dps2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if dps1_combo
|
||||
.iter()
|
||||
.any(|e| dps2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for support1_combo in &support_combos {
|
||||
if tank1_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank1_combo
|
||||
.iter()
|
||||
.any(|e| support1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if tank2_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank2_combo
|
||||
.iter()
|
||||
.any(|e| support1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dps1_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if dps1_combo
|
||||
.iter()
|
||||
.any(|e| support1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dps2_combo.iter().any(|e| support1_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if dps2_combo
|
||||
.iter()
|
||||
.any(|e| support1_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for support2_combo in &support_combos {
|
||||
if tank1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank1_combo
|
||||
.iter()
|
||||
.any(|e| support2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if tank2_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if tank2_combo
|
||||
.iter()
|
||||
.any(|e| support2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dps1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if dps1_combo
|
||||
.iter()
|
||||
.any(|e| support2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dps2_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if dps2_combo
|
||||
.iter()
|
||||
.any(|e| support2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if support1_combo.iter().any(|e| support2_combo.iter().any(|e2| e.index == e2.index)) {
|
||||
if support1_combo
|
||||
.iter()
|
||||
.any(|e| support2_combo.iter().any(|e2| e.index == e2.index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut team1 = Team::new(slots.clone());
|
||||
let mut team2 = Team::new(slots.clone());
|
||||
|
||||
for entry in tank1_combo.iter().chain(dps1_combo.iter()).chain(support1_combo.iter()) {
|
||||
for entry in tank1_combo
|
||||
.iter()
|
||||
.chain(dps1_combo.iter())
|
||||
.chain(support1_combo.iter())
|
||||
{
|
||||
team1.add_player(entry.index, &entry.role);
|
||||
}
|
||||
|
||||
for entry in tank2_combo.iter().chain(dps2_combo.iter()).chain(support2_combo.iter()) {
|
||||
for entry in tank2_combo
|
||||
.iter()
|
||||
.chain(dps2_combo.iter())
|
||||
.chain(support2_combo.iter())
|
||||
{
|
||||
team2.add_player(entry.index, &entry.role);
|
||||
}
|
||||
|
||||
let diff_rating = (team1.full_rating(&players).value - team2.full_rating(&players).value).abs();
|
||||
let diff_rating_tank = (team1.full_rating_role(&Role::Tank, &players).value - team2.full_rating_role(&Role::Tank, &players).value).abs();
|
||||
let diff_rating_dps = (team1.full_rating_role(&Role::Dps, &players).value - team2.full_rating_role(&Role::Dps, &players).value).abs();
|
||||
let diff_rating_support = (team1.full_rating_role(&Role::Support, &players).value - team2.full_rating_role(&Role::Support, &players).value).abs();
|
||||
let diff = diff_rating + (diff_rating_tank + diff_rating_dps + diff_rating_support);
|
||||
let diff_rating = (team1.full_rating(&players).value
|
||||
- team2.full_rating(&players).value)
|
||||
.abs();
|
||||
let diff = diff_rating;
|
||||
|
||||
if diff + threshold < best_diff.unwrap_or(f32::MAX) {
|
||||
if diff < threshold {
|
||||
@@ -160,7 +220,11 @@ fn calculate_priorities(players: &[Player]) -> Vec<PlayerRoleEntry> {
|
||||
|
||||
for (i, player) in players.iter().enumerate() {
|
||||
for (role, priority) in player.base_priority() {
|
||||
priorities.push(PlayerRoleEntry { index: i, role, priority });
|
||||
priorities.push(PlayerRoleEntry {
|
||||
index: i,
|
||||
role,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod mixer;
|
||||
pub mod role;
|
||||
pub mod player;
|
||||
pub mod team;
|
||||
pub mod rating;
|
||||
pub mod team;
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Iterable;
|
||||
use serenity::model::id::UserId;
|
||||
use sqlx::types::chrono::Utc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::models::player::Model;
|
||||
use crate::database::models::role::Role;
|
||||
use crate::mixer::rating::Rating;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Player {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) discord_id: UserId,
|
||||
pub(crate) bn_name: String,
|
||||
pub(crate) bn_tag: String,
|
||||
pub id: i32,
|
||||
pub discord_id: UserId,
|
||||
pub bn_name: Option<String>,
|
||||
pub bn_tag: Option<String>,
|
||||
pub last_played: Option<DateTime>,
|
||||
|
||||
pub(crate) ranks: HashMap<Role, Rating>,
|
||||
pub(crate) flex: bool,
|
||||
pub(crate) priority_roles: Vec<Option<Role>>,
|
||||
pub ranks: HashMap<Role, Rating>,
|
||||
pub flex: bool,
|
||||
pub priority_roles: Vec<Option<Role>>,
|
||||
}
|
||||
|
||||
|
||||
impl Player {
|
||||
pub fn new(model: Model) -> Self {
|
||||
Self {
|
||||
@@ -24,55 +28,48 @@ impl Player {
|
||||
discord_id: UserId::from(model.discord_id as u64),
|
||||
bn_name: model.bn_name,
|
||||
bn_tag: model.bn_tag,
|
||||
last_played: model.last_played,
|
||||
|
||||
ranks: vec![
|
||||
(Role::Tank, Rating::new(model.tank_rating, model.tank_rd, model.tank_volatility)),
|
||||
(Role::Dps, Rating::new(model.dps_rating, model.dps_rd, model.dps_volatility)),
|
||||
(Role::Support, Rating::new(model.support_rating, model.support_rd, model.support_volatility))
|
||||
].into_iter().collect(),
|
||||
(
|
||||
Role::Tank,
|
||||
Rating::new(model.tank_rating, model.tank_rd, model.tank_volatility),
|
||||
),
|
||||
(
|
||||
Role::DPS,
|
||||
Rating::new(model.dps_rating, model.dps_rd, model.dps_volatility),
|
||||
),
|
||||
(
|
||||
Role::Support,
|
||||
Rating::new(
|
||||
model.support_rating,
|
||||
model.support_rd,
|
||||
model.support_volatility,
|
||||
),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
|
||||
flex: model.flex,
|
||||
priority_roles: vec![
|
||||
model.primary_role,
|
||||
model.secondary_role, model.tertiary_role
|
||||
].into_iter().map(|role| {
|
||||
match role {
|
||||
-1 => None,
|
||||
_ => Some(Role::from(role))
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_model(self) -> Model {
|
||||
Model {
|
||||
id: self.id,
|
||||
discord_id: self.discord_id.0 as i64,
|
||||
bn_name: self.bn_name,
|
||||
bn_tag: self.bn_tag,
|
||||
tank_rating: self.ranks.get(&Role::Tank).unwrap().value,
|
||||
tank_rd: self.ranks.get(&Role::Tank).unwrap().rd,
|
||||
tank_volatility: self.ranks.get(&Role::Tank).unwrap().volatility,
|
||||
dps_rating: self.ranks.get(&Role::Dps).unwrap().value,
|
||||
dps_rd: self.ranks.get(&Role::Dps).unwrap().rd,
|
||||
dps_volatility: self.ranks.get(&Role::Dps).unwrap().volatility,
|
||||
support_rating: self.ranks.get(&Role::Support).unwrap().value,
|
||||
support_rd: self.ranks.get(&Role::Support).unwrap().rd,
|
||||
support_volatility: self.ranks.get(&Role::Support).unwrap().volatility,
|
||||
flex: self.flex,
|
||||
primary_role: Role::option_to_i32(self.priority_roles.get(0).unwrap().clone()),
|
||||
secondary_role: Role::option_to_i32(self.priority_roles.get(1).unwrap().clone()),
|
||||
tertiary_role: Role::option_to_i32(self.priority_roles.get(2).unwrap().clone()),
|
||||
model.secondary_role,
|
||||
model.tertiary_role,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_priority(&self) -> HashMap<Role, f32> {
|
||||
let mut priorities = HashMap::new();
|
||||
let priority_points = 100.0;
|
||||
let time = self.last_played.unwrap_or(Utc::now().naive_utc());
|
||||
let time_since = (Utc::now().naive_utc() - time).num_minutes() as f32;
|
||||
let priority_points = 100.0 + (time_since / 5.0).powf(1.5);
|
||||
|
||||
if self.flex {
|
||||
let role_count = Role::iter().count();
|
||||
for role in Role::iter() {
|
||||
priorities.insert(role, 1.5 * (priority_points / (Role::iter().count()) as f32));
|
||||
priorities.insert(role, 1.5 * (priority_points / role_count as f32));
|
||||
}
|
||||
|
||||
return priorities;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::Sum;
|
||||
use std::ops::{Add, AddAssign, Div};
|
||||
|
||||
use crate::algorithm::glicko2;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -14,7 +15,9 @@ impl Eq for Rating {}
|
||||
|
||||
impl Ord for Rating {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.value.partial_cmp(&other.value).unwrap_or(Ordering::Equal)
|
||||
self.value
|
||||
.partial_cmp(&other.value)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +97,15 @@ impl Rating {
|
||||
}
|
||||
|
||||
pub fn update(&mut self, other: &Self, score: f32) {
|
||||
let (value, rd, sigma) = glicko2::update(self.value, self.rd, self.volatility, other.value, other.rd, 5.0/3.0, score);
|
||||
let (value, rd, sigma) = glicko2::update(
|
||||
self.value,
|
||||
self.rd,
|
||||
self.volatility,
|
||||
other.value,
|
||||
other.rd,
|
||||
5.0 / 3.0,
|
||||
score,
|
||||
);
|
||||
self.value = value;
|
||||
self.rd = rd;
|
||||
self.volatility = sigma;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Role {
|
||||
Tank,
|
||||
Dps,
|
||||
Support
|
||||
}
|
||||
|
||||
impl FromStr for Role {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"tank" => Ok(Role::Tank),
|
||||
"dps" => Ok(Role::Dps),
|
||||
"support" => Ok(Role::Support),
|
||||
_ => Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for i32 {
|
||||
fn from(role: Role) -> Self {
|
||||
match role {
|
||||
Role::Tank => 0,
|
||||
Role::Dps => 1,
|
||||
Role::Support => 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Role {
|
||||
fn from(i: i32) -> Self {
|
||||
match i {
|
||||
0 => Role::Tank,
|
||||
1 => Role::Dps,
|
||||
2 => Role::Support,
|
||||
_ => panic!("Invalid role number")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<i32> for Role {
|
||||
fn eq(&self, other: &i32) -> bool {
|
||||
match self {
|
||||
Role::Tank => *other == 0,
|
||||
Role::Dps => *other == 1,
|
||||
Role::Support => *other == 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn iter() -> impl Iterator<Item = Role> {
|
||||
vec![Role::Tank, Role::Dps, Role::Support].into_iter()
|
||||
}
|
||||
|
||||
pub fn option_to_i32(role: Option<Role>) -> i32 {
|
||||
match role {
|
||||
Some(role) => role as i32,
|
||||
None => -1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use sea_orm::Iterable;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::models::role::Role;
|
||||
use crate::mixer::player::Player;
|
||||
use crate::mixer::rating::Rating;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Team {
|
||||
@@ -11,7 +13,6 @@ pub struct Team {
|
||||
count_role: HashMap<Role, usize>,
|
||||
}
|
||||
|
||||
|
||||
impl Team {
|
||||
pub fn new(slots: Vec<Role>) -> Self {
|
||||
Self {
|
||||
@@ -50,13 +51,16 @@ impl Team {
|
||||
}
|
||||
|
||||
pub fn full_rating(&self, players: &[Player]) -> Rating {
|
||||
self.players.iter().map(|((role, _), index)| {
|
||||
self.players
|
||||
.iter()
|
||||
.map(|((role, _), index)| {
|
||||
if let Some(index) = index {
|
||||
players[*index].ranks.get(role).unwrap().clone()
|
||||
} else {
|
||||
Rating::zero()
|
||||
}
|
||||
}).sum()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn average_rating(&self, players: &[Player]) -> Rating {
|
||||
@@ -68,19 +72,23 @@ impl Team {
|
||||
}
|
||||
|
||||
pub fn full_rating_role(&self, role: &Role, players: &[Player]) -> Rating {
|
||||
self.players.iter().filter(|((r, _), _)| r == role).map(|((_, _), index)| {
|
||||
self.players
|
||||
.iter()
|
||||
.filter(|((r, _), _)| r == role)
|
||||
.map(|((_, _), index)| {
|
||||
if let Some(index) = index {
|
||||
players[*index].ranks.get(&role).unwrap().clone()
|
||||
} else {
|
||||
Rating::zero()
|
||||
}
|
||||
}).sum()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn average_rating_role(&self, role: &Role, players: &[Player]) -> Rating {
|
||||
let count = self.players.iter().filter(|((r, _), _)| r == role).count();
|
||||
if count == 0 {
|
||||
return Rating::zero()
|
||||
return Rating::zero();
|
||||
}
|
||||
|
||||
self.full_rating_role(role, players) / count as f32
|
||||
|
||||
Reference in New Issue
Block a user