diff --git a/Cargo.toml b/Cargo.toml index 4773b71..aa81d09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,8 @@ itertools = "*" shuttle-secrets = "*" shuttle-serenity = "*" shuttle-runtime = "*" -shuttle-shared-db = { version = "*", features = ["postgres"] } \ No newline at end of file +shuttle-shared-db = { version = "*", features = ["postgres"] } + +rusttype = "*" +image = "*" +imageproc = "*" \ No newline at end of file diff --git a/assets/fonts/big-noodle-titling.ttf b/assets/fonts/big-noodle-titling.ttf new file mode 100644 index 0000000..5577c00 Binary files /dev/null and b/assets/fonts/big-noodle-titling.ttf differ diff --git a/assets/fonts/big-noodle-too-oblique.ttf b/assets/fonts/big-noodle-too-oblique.ttf new file mode 100644 index 0000000..6ae4f06 Binary files /dev/null and b/assets/fonts/big-noodle-too-oblique.ttf differ diff --git a/assets/images/teams.png b/assets/images/teams.png new file mode 100644 index 0000000..4d68b2e Binary files /dev/null and b/assets/images/teams.png differ diff --git a/src/algorithm/glicko2/mod.rs b/src/algorithm/glicko2.rs similarity index 100% rename from src/algorithm/glicko2/mod.rs rename to src/algorithm/glicko2.rs diff --git a/src/bot/commands/lobby.rs b/src/bot/commands/lobby.rs index 35a5733..197537e 100644 --- a/src/bot/commands/lobby.rs +++ b/src/bot/commands/lobby.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use sea_orm::{EntityTrait, QueryFilter, ColumnTrait}; use serenity::async_trait; use serenity::builder::{CreateApplicationCommand, CreateEmbed}; use serenity::client::Context; @@ -13,15 +14,19 @@ use serenity::model::application::interaction::{ use serenity::model::channel::{ChannelType, PermissionOverwrite, PermissionOverwriteType}; use serenity::model::id::{ChannelId, RoleId, UserId}; use serenity::model::Permissions; +use serenity::model::prelude::{AttachmentType, Message, GuildId}; use serenity::utils::Colour; use sqlx::types::chrono::Utc; +use std::borrow::Cow; use std::time::Duration; use crate::bot::commands::MixerCommand; use crate::database::models::lobby::Model; +use crate::database::models::player; use crate::database::models::role::Role; use crate::database::queries::prelude::*; use crate::database::DatabaseContainer; +use crate::image_manipulation::ImageGeneratorContainer; use crate::mixer::mixer; use crate::mixer::player::Player; use crate::mixer::team::Team; @@ -247,7 +252,10 @@ impl LobbyCommand { let members = main_channel.members(ctx).await?; let users = members.iter().map(|m| m.user.id).collect::>(); - let players = PlayerQuery::players_by_user_ids(db.connection(), users).await; + let players = player::Entity::find() + .filter(player::Column::Id.is_in(1..11)) + .all(db.connection()).await.ok(); + // let players = PlayerQuery::players_by_user_ids(db.connection(), users).await; let players = match players { Some(p) => p, @@ -336,69 +344,85 @@ impl LobbyCommand { 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| { - components.create_action_row(|row| { - row.create_button(|button| { - button - .custom_id("cancel") - .label("Cancel") - .style(ButtonStyle::Danger) - }); - row.create_button(|button| { - button - .custom_id("swap") - .label("Swap") - .disabled(true) - .style(ButtonStyle::Primary) - }); - row.create_button(|button| { - button - .custom_id("start") - .label("Start") - .style(ButtonStyle::Success) - }) + let image_data = { + let data = ctx.data.read().await; + let image_gen = data.get::().unwrap(); + + let player_names = team1_names.into_iter().chain(team2_names.into_iter()).collect_vec(); + + let team1_rank = team1.average_rating(&players); + let team2_rank = team2.average_rating(&players); + + image_gen.draw_teams(player_names, [team1_rank.value as i32, team2_rank.value as i32]) + }; + + let attachment = AttachmentType::Bytes { + data: Cow::Owned(image_data), + filename: "teams.png".to_string(), + }; + + let msg = interaction.channel_id.send_message(ctx, |message| { + message + .add_file(attachment) + .components(|components| { + components.create_action_row(|row| { + row.create_button(|button| { + button + .custom_id("cancel") + .label("Cancel") + .style(ButtonStyle::Danger) + }); + row.create_button(|button| { + button + .custom_id("swap") + .label("Swap") + .disabled(true) + .style(ButtonStyle::Primary) + }); + row.create_button(|button| { + button + .custom_id("start") + .label("Start") + .style(ButtonStyle::Success) }) }) - .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, - ), - ("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)) - }) - }) - .await - .unwrap(); + }) + }).await?; - let msg = interaction.get_interaction_response(ctx).await.unwrap(); + interaction.delete_original_interaction_response(ctx).await?; + + // 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") + // .style(ButtonStyle::Danger) + // }); + // row.create_button(|button| { + // button + // .custom_id("swap") + // .label("Swap") + // .disabled(true) + // .style(ButtonStyle::Primary) + // }); + // row.create_button(|button| { + // button + // .custom_id("start") + // .label("Start") + // .style(ButtonStyle::Success) + // }) + // }) + // }) + // }) + // .await + // .unwrap(); + + // let msg = interaction.get_interaction_response(ctx).await.unwrap(); let collector = msg .await_component_interactions(ctx) .timeout(Duration::from_secs(2 * 60)) @@ -412,23 +436,21 @@ impl LobbyCommand { 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) + self.process_valid_teams_start(ctx, lobby, &team1, &team2, players, interaction.user.id, msg) .await? } "cancel" => { - self.process_valid_teams_cancel(ctx, interaction, &team1, &team2) + self.process_valid_teams_cancel(ctx, &team1, &team2, msg) .await? } "swap" => { - self.process_valid_teams_swap(ctx, interaction, &team1, &team2) + self.process_valid_teams_swap(ctx, &team1, &team2, msg) .await? } _ => {} } } else { - interaction - .delete_original_interaction_response(ctx) - .await?; + msg.delete(ctx).await?; } Ok(()) @@ -437,11 +459,12 @@ impl LobbyCommand { async fn process_valid_teams_start( &self, ctx: &Context, - interaction: &MessageComponentInteraction, lobby: Model, team1: &Team, team2: &Team, players: Vec, + author: UserId, + mut message: Message ) -> serenity::Result<()> { let main_channel = ChannelId::from(lobby.main_voice_id as u64) .to_channel(ctx) @@ -480,51 +503,41 @@ impl LobbyCommand { } } - let embed = interaction.message.embeds.get(0).unwrap(); - 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)) - .set_embeds(vec![CreateEmbed::from(embed.clone())]) - .components(|components| { - components - .create_action_row(|row| { - row.create_button(|button| { - button - .custom_id("win_team1") - .label("Team 1 win") - .style(ButtonStyle::Success) - }) - .create_button(|button| { - button - .custom_id("draw") - .label("Draw") - .style(ButtonStyle::Secondary) - }) - .create_button(|button| { - button - .custom_id("win_team2") - .label("Team 2 win") - .style(ButtonStyle::Success) - }) - }) - .create_action_row(|row| { - row.create_button(|button| { - button - .custom_id("cancel") - .label("Cancel game") - .style(ButtonStyle::Danger) - }) - }) + message.edit(ctx, |message| { + message + .content(format!("<@{}>", author.0)) + .components(|components| { + components + .create_action_row(|row| { + row.create_button(|button| { + button + .custom_id("win_team1") + .label("Team 1 win") + .style(ButtonStyle::Success) + }) + .create_button(|button| { + button + .custom_id("draw") + .label("Draw") + .style(ButtonStyle::Secondary) + }) + .create_button(|button| { + button + .custom_id("win_team2") + .label("Team 2 win") + .style(ButtonStyle::Success) + }) + }) + .create_action_row(|row| { + row.create_button(|button| { + button + .custom_id("cancel") + .label("Cancel game") + .style(ButtonStyle::Danger) + }) }) }) - .await?; + }).await?; // let msg = interaction.edit_original_interaction_response(ctx, |message| { // message.components(|components| { @@ -552,12 +565,12 @@ impl LobbyCommand { // }) // }).await?; - let collector = msg + let collector = message .await_component_interactions(ctx) .timeout(Duration::from_secs(30 * 60)) - .guild_id(interaction.guild_id.unwrap()) - .channel_id(interaction.channel_id) - .author_id(interaction.user.id) + .guild_id(GuildId::from(lobby.guild_id as u64)) + .channel_id(message.channel_id) + .author_id(author) .collect_limit(1) .build(); @@ -569,7 +582,7 @@ impl LobbyCommand { "draw" => score = 0.5, "win_team2" => score = 0.0, "cancel" => { - return msg.delete(ctx).await; + return message.delete(ctx).await; // return interaction.delete_original_interaction_response(ctx).await; } _ => {} @@ -646,25 +659,25 @@ impl LobbyCommand { drop(data); } - msg.delete(ctx).await + message.delete(ctx).await } async fn process_valid_teams_cancel( &self, ctx: &Context, - interaction: &MessageComponentInteraction, team1: &Team, team2: &Team, + message: Message ) -> serenity::Result<()> { - interaction.delete_original_interaction_response(ctx).await + message.delete(ctx).await } async fn process_valid_teams_swap( &self, ctx: &Context, - interaction: &MessageComponentInteraction, team1: &Team, team2: &Team, + message: Message ) -> serenity::Result<()> { Ok(()) } diff --git a/src/image_manipulation.rs b/src/image_manipulation.rs new file mode 100644 index 0000000..819882a --- /dev/null +++ b/src/image_manipulation.rs @@ -0,0 +1,73 @@ +use std::{sync::Arc, io::Read}; +use image::EncodableLayout; +use imageproc::drawing::text_size; +use itertools::Itertools; +use rusttype::{Font, Scale}; +use serenity::prelude::TypeMapKey; + +pub struct ImageGenerator<'a> { + pub player_font: Font<'a>, + pub text_font: Font<'a>, + pub teams_image: image::ImageBuffer, Vec> +} + +impl<'a> ImageGenerator<'a> { + pub fn draw_teams(&self, player_names: Vec, teams_rating: [i32; 2]) -> Vec { + let mut image = self.teams_image.clone(); + + let player_text_scale = Scale::uniform(60.0); + for i in 0..2 { + for j in 0..5 { + let player_name = match player_names.get(i * 5 + j) { + Some(name) => name, + None => "Unknown" + }; + + let size = text_size(player_text_scale, &self.player_font, player_name); + let scale = if size.0 > 340 { + Scale::uniform(340.0 / size.0 as f32 * player_text_scale.x) + } else { + player_text_scale + }; + let size = text_size(scale, &self.player_font, player_name); + + let x: i32 = 83 + 540 * i as i32 - 2; + let y: i32 = 182 + 70 * j as i32 - size.1 + ((size.1 as f32 * 1.0 / 5.0) / 10.0) as i32; + + imageproc::drawing::draw_text_mut( + &mut image, + image::Rgb([255, 255, 255]), + x, + y, + scale, + &self.player_font, + player_name + ); + } + } + + let rating_text_scale = Scale::uniform(86.5); + for i in 0..2 { + let rating = teams_rating[i].to_string(); + + let size = text_size(rating_text_scale, &self.player_font, &rating); + imageproc::drawing::draw_text_mut( + &mut image, + image::Rgb([255, 255, 255]), + 365 - size.0 / 2 + 540 * i as i32, + 100 - size.1, + rating_text_scale, + &self.text_font, + &rating, + ); + } + + image.as_bytes().iter().cloned().collect_vec() + } +} + +pub struct ImageGeneratorContainer; + +impl TypeMapKey for ImageGeneratorContainer { + type Value = Arc>; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4f53de2..0231c86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,11 @@ mod algorithm; mod bot; mod database; mod mixer; +mod image_manipulation; use bot::commands::creator::CreatorCommand; +use image_manipulation::{ImageGenerator, ImageGeneratorContainer}; +use rusttype::Font; use serenity::model::prelude::UserId; use serenity::prelude::{GatewayIntents, TypeMapKey}; use serenity::Client; @@ -74,6 +77,13 @@ async fn serenity( .unwrap(), ); data.insert::(Arc::new(creator)); + + let image_generator = ImageGenerator { + player_font: Font::try_from_bytes(include_bytes!("../assets/fonts/big-noodle-too-oblique.ttf")).unwrap(), + text_font: Font::try_from_bytes(include_bytes!("../assets/fonts/big-noodle-titling.ttf")).unwrap(), + teams_image: image::load_from_memory(include_bytes!("../assets/images/teams.png")).unwrap().to_rgb8() + }; + data.insert::(Arc::new(image_generator)); } let shard_manager = client.shard_manager.clone();