From d076318e4adfc152a5cd424e019d9a01278c9d8d Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Fri, 7 Jul 2023 19:47:03 -0600 Subject: [PATCH] Added buddy commands and horny license + clippy + fmt --- src/config.rs | 1 + src/discord/admin.rs | 36 ++++- src/discord/little_fren.rs | 208 +++++++++++++++++++++++++++++ src/discord/mod.rs | 9 ++ src/discord/shop.rs | 3 + src/inventory/mod.rs | 5 + src/main.rs | 1 + src/models/lil_fren.rs | 267 +++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 1 + 9 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 src/discord/little_fren.rs create mode 100644 src/models/lil_fren.rs diff --git a/src/config.rs b/src/config.rs index 79bffb1..3bd185f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,7 @@ pub struct BotConfig { pub guild_id: GuildId, pub api_addr: SocketAddr, pub announcement_channel: ChannelId, + pub toys: Vec, } impl BotConfig { diff --git a/src/discord/admin.rs b/src/discord/admin.rs index 009d1d5..efd7cf6 100644 --- a/src/discord/admin.rs +++ b/src/discord/admin.rs @@ -1,5 +1,6 @@ use crate::config::BotConfig; use crate::models::api_key::Apikey; +use crate::models::lil_fren::{AliveState, LilFren}; use crate::{command, group, GlobalData}; use json::JsonValue; use serenity::client::Context; @@ -9,7 +10,7 @@ use serenity::model::prelude::UserId; use std::borrow::Cow; #[group] -#[commands(reload, dump_db, load_db, add_key)] +#[commands(reload, dump_db, load_db, add_key, debug_buddy)] pub struct ADMIN; pub fn is_admin(user_id: &UserId, cfg: &BotConfig) -> bool { @@ -127,3 +128,36 @@ async fn add_key(ctx: &Context, msg: &Message, args: Args) -> CommandResult { Ok(()) } + +#[command] +#[description("Check little buddy stats")] +async fn debug_buddy(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + let lil_fren = LilFren::get_lil_fren(&global_data.db)?; + + if let Some(lil_fren) = lil_fren { + if lil_fren.is_alive() == AliveState::Alive { + msg.reply( + &ctx.http, + format!( + "Hunger: {} Thirst: {} Entertainment: {} Smarts: {} Metabolism: {} State: {:?}", + lil_fren.hunger, + lil_fren.thirst, + lil_fren.entertainment, + lil_fren.smarts, + lil_fren.metabolism, + lil_fren.state + ), + ) + .await?; + } else { + msg.reply(&ctx.http, "Lil buddy is dead!").await?; + } + } else { + msg.reply(&ctx.http, "Sorry no little buddy found!").await?; + } + + Ok(()) +} diff --git a/src/discord/little_fren.rs b/src/discord/little_fren.rs new file mode 100644 index 0000000..be870a6 --- /dev/null +++ b/src/discord/little_fren.rs @@ -0,0 +1,208 @@ +use crate::config::GlobalData; +use crate::models::lil_fren; +use crate::models::lil_fren::{AliveState, LilFren, LilFrenState}; +use crate::{command, group}; +use rand::prelude::SliceRandom; +use rand::{thread_rng, Rng}; +use serenity::client::Context; +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::channel::Message; +use serenity::model::misc::EmojiIdentifier; +use serenity::utils::MessageBuilder; + +#[group] +#[commands(adopt, checkup, feed, give_water, play)] +pub struct Buddy; + +#[command] +#[description("Adopt a new lil buddy")] +#[only_in(guilds)] +async fn adopt(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + if let Ok(emoji) = args.parse::() { + if let Some(lil_fren) = LilFren::get_lil_fren(&global_data.db)? { + if lil_fren.is_alive() == AliveState::Alive { + msg.reply( + &ctx.http, + "Your buddy is still alive, please take care of him :)", + ) + .await?; + return Ok(()); + } + } + + LilFren::create_new_lil_fren(&global_data.db, emoji.id)?; + let guild = msg.guild_id.unwrap(); + let emoji = guild.emoji(&ctx.http, emoji.id).await?; + + let mut msg_builder = MessageBuilder::new(); + + msg_builder.push("Congrats on adopting "); + msg_builder.emoji(&emoji); + msg_builder.push("!"); + + msg.reply(&ctx.http, msg_builder.build()).await?; + } else { + msg.reply(&ctx.http, "Sorry whatever that is not adoptable.") + .await?; + } + + Ok(()) +} + +#[command] +#[only_in(guilds)] +#[description("Checkup on your lil buddy")] +async fn checkup(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + let lil_fren = LilFren::get_lil_fren(&global_data.db)?; + + if let Some(lil_fren) = lil_fren { + let guild = msg.guild_id.unwrap(); + let alive_state = lil_fren.is_alive(); + + if alive_state == AliveState::Alive { + let state_msg = match lil_fren.state { + LilFrenState::Standing => "Little buddy is standing.", + LilFrenState::TaxFraud => "Your buddy is comiting tax fraud", + LilFrenState::Sick => "Oh no! Buddy is sick!", + LilFrenState::Dancing => "Buddy got some moves!", + LilFrenState::Sleep => "Shhh, buddy is asleep", + }; + + msg.reply(&ctx.http, state_msg).await?; + msg.reply(&ctx.http, lil_fren.draw(ctx, &guild).await) + .await?; + } else { + let resp = match alive_state { + AliveState::Alive => "", + AliveState::DiedOfBoredom => "Your little buddy died of boredom RIP", + AliveState::DiedOfHunger => "Your little buddy died of hunger RIP", + AliveState::DiedOfThirst => "Your little buddy died of thirst", + }; + + msg.reply(&ctx.http, resp).await?; + } + } else { + msg.reply( + &ctx.http, + "Sorry you have no little buddy right now. Please adopt!", + ) + .await?; + } + + Ok(()) +} + +#[command] +#[description("Feed lil buddy")] +async fn feed(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + if let Ok(food_emoji) = args.parse::() { + let lil_fren = LilFren::get_lil_fren(&global_data.db)?; + + if let Some(mut lil_fren) = lil_fren { + if lil_fren.is_alive() == AliveState::Alive { + let feed: f32 = thread_rng().gen_range(-0.1..=1.0); + lil_fren.hunger = (lil_fren.hunger + feed).clamp(-1.0, 1.0); + let guild = msg.guild_id.unwrap(); + + let lil_fren_emoji = guild.emoji(&ctx.http, lil_fren.emoji).await?; + + if feed > 0.0 { + msg.reply(&ctx.http, "Lil buddy seemed to like that!") + .await?; + } else { + msg.reply(&ctx.http, "Lil buddy DID not like that!").await?; + } + + msg.reply(&ctx.http, lil_fren::draw_feed(&lil_fren_emoji, &food_emoji)) + .await?; + + global_data.db.insert(lil_fren)?; + } else { + msg.reply(&ctx.http, "You may want to check on your buddy...") + .await?; + } + } else { + msg.reply(&ctx.http, "Sorry no little buddy found!").await?; + } + } else { + msg.reply(&ctx.http, "Please provide something to feed lil buddy") + .await?; + } + + Ok(()) +} + +#[command] +#[description("Give lil buddy some water")] +async fn give_water(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + let lil_fren = LilFren::get_lil_fren(&global_data.db)?; + + if let Some(mut lil_fren) = lil_fren { + if lil_fren.is_alive() == AliveState::Alive { + lil_fren.thirst = (lil_fren.thirst + 0.5).clamp(-1.0, 1.0); + let guild = msg.guild_id.unwrap(); + + let lil_fren_emoji = guild.emoji(&ctx.http, lil_fren.emoji).await?; + + msg.reply(&ctx.http, "Lil buddy be drinking!").await?; + global_data.db.insert(lil_fren)?; + + msg.reply( + &ctx.http, + lil_fren::draw_feed(&lil_fren_emoji, ":cup_with_straw:"), + ) + .await?; + } else { + msg.reply(&ctx.http, "You may want to check on your buddy...") + .await?; + } + } else { + msg.reply(&ctx.http, "Sorry no little buddy found!").await?; + } + + Ok(()) +} + +#[command] +#[description("Play with buddy")] +async fn play(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + let lil_fren = LilFren::get_lil_fren(&global_data.db)?; + + if let Some(mut lil_fren) = lil_fren { + if lil_fren.is_alive() == AliveState::Alive { + lil_fren.entertainment = + (lil_fren.entertainment + thread_rng().gen_range(-0.10..1.0)).clamp(-1.0, 1.0); + let guild = msg.guild_id.unwrap(); + + let lil_fren_emoji = guild.emoji(&ctx.http, lil_fren.emoji).await?; + + global_data.db.insert(lil_fren)?; + + let toy = global_data.cfg.toys.choose(&mut thread_rng()).unwrap(); + msg.reply(&ctx.http, lil_fren::draw_feed(&lil_fren_emoji, toy)) + .await?; + } else { + msg.reply(&ctx.http, "You may want to check on your buddy...") + .await?; + } + } else { + msg.reply(&ctx.http, "Sorry no little buddy found!").await?; + } + + Ok(()) +} diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 1b93dd0..bc50b18 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -6,6 +6,7 @@ pub mod color; pub mod emoji_race; pub mod fren_coin; pub mod joke; +pub mod little_fren; pub mod motivate; pub mod shop; pub mod story; @@ -17,6 +18,7 @@ use crate::discord::joke::random; use crate::discord::shop::restock_shop; use crate::models::birthday::BirthdayEntry; use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType}; +use crate::models::lil_fren::lil_fren_task; use crate::{help, hook, GlobalData}; use chrono::{Days, TimeZone, Timelike, Utc}; use rand::prelude::IteratorRandom; @@ -42,6 +44,13 @@ static ERROR_MSG: &str = #[async_trait] impl EventHandler for Handler { async fn cache_ready(&self, ctx: Context, _guilds: Vec) { + let ctx1 = ctx.clone(); + tokio::spawn(async move { + loop { + lil_fren_task(&ctx1).await; + } + }); + tokio::spawn(async move { let mut next_check = chrono_tz::America::Chicago .from_utc_datetime(&Utc::now().naive_utc()) diff --git a/src/discord/shop.rs b/src/discord/shop.rs index f0bc6c6..0f1e589 100644 --- a/src/discord/shop.rs +++ b/src/discord/shop.rs @@ -214,6 +214,9 @@ async fn use_item(ctx: &Context, msg: &Message, args: Args) -> CommandResult { .await?; } } + ItemType::LicenseToBeHorny => { + msg.reply(&ctx.http, "https://media.discordapp.net/attachments/840015650286075945/1127022083919069184/Img_2022_10_21_05_08_12.jpg").await?; + } } Ok(()) diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 452c80b..55ac648 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -32,6 +32,7 @@ pub enum ItemType { TheConceptOfLove, GoodFortune, Nft, + LicenseToBeHorny, } #[derive(Debug, Clone, Hash, Eq, PartialEq, Copy)] @@ -56,6 +57,8 @@ impl FromStr for ItemType { Ok(ItemType::GoodFortune) } else if item.starts_with("nft") { Ok(ItemType::Nft) + } else if item.starts_with("licensetobehorny") { + Ok(ItemType::LicenseToBeHorny) } else { Err(InventoryError::UnkownItem) } @@ -69,6 +72,7 @@ impl Display for ItemType { ItemType::TheConceptOfLove => "The Concept of Love".to_string(), ItemType::GoodFortune => "Good Fortune".to_string(), ItemType::Nft => "NFT".to_string(), + ItemType::LicenseToBeHorny => "License to be Horny".to_string(), }; write!(f, "{}", name) @@ -88,6 +92,7 @@ impl InventorySlot { ItemType::TheConceptOfLove => 300, ItemType::GoodFortune => 75, ItemType::Nft => 100, + ItemType::LicenseToBeHorny => 100, } } diff --git a/src/main.rs b/src/main.rs index 2f15d24..4c03c70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ async fn main() { .group(&discord::voices::VOICES_GROUP) .group(&discord::shop::SHOP_GROUP) .group(&discord::birthday::BIRTHDAY_GROUP) + .group(&discord::little_fren::BUDDY_GROUP) .unrecognised_command(unrecognised_command_hook) .bucket("bad_apple", |b| b.delay(60 * 10)) .await diff --git a/src/models/lil_fren.rs b/src/models/lil_fren.rs new file mode 100644 index 0000000..68a5f78 --- /dev/null +++ b/src/models/lil_fren.rs @@ -0,0 +1,267 @@ +use crate::config::GlobalData; +use crate::error::Error; +use j_db::database::Database; +use j_db::model::JdbModel; +use rand::distributions::Standard; +use rand::prelude::Distribution; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use serenity::model::guild::Emoji; +use serenity::model::id::{EmojiId, GuildId}; +use serenity::prelude::Context; +use serenity::utils::MessageBuilder; +use std::time::Duration; + +pub fn draw_standing(emoji: &Emoji) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push(":blue_square::blue_square:"); + msg.emoji(emoji); + msg.push_line(":blue_square::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_dancing(emoji: &Emoji) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::notes::blue_square:"); + msg.push_line(":blue_square::notes::blue_square::notes::blue_square:"); + msg.push(":blue_square::blue_square:"); + msg.emoji(emoji); + msg.push_line(":blue_square::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_tax_fraud(emoji: &Emoji) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push(":blue_square::page_with_curl:"); + msg.emoji(emoji); + msg.push_line(":blue_square::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_sick(emoji: &Emoji) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push(":blue_square::bucket:"); + msg.emoji(emoji); + msg.push_line(":roll_of_paper::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_sleep(emoji: &Emoji) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push(":blue_square::blue_square:"); + msg.emoji(emoji); + msg.push_line(":blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::bed::blue_square::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_dead() -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::headstone::blue_square::blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +pub fn draw_feed(emoji: &Emoji, food: &str) -> String { + let mut msg = MessageBuilder::new(); + + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push_line(":blue_square::blue_square::blue_square::blue_square::blue_square:"); + msg.push(":blue_square::blue_square:"); + msg.emoji(emoji); + msg.push(food); + msg.push_line(":blue_square:"); + msg.push_line(":green_square::green_square::green_square::green_square::green_square:"); + + msg.build() +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialOrd, PartialEq)] +pub enum LilFrenState { + Standing, + TaxFraud, + Sick, + Dancing, + Sleep, +} + +#[derive(Debug, Clone, PartialOrd, PartialEq)] +pub enum AliveState { + Alive, + DiedOfBoredom, + DiedOfHunger, + DiedOfThirst, +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> LilFrenState { + match rng.gen_range(0..100) { + 0..=30 => LilFrenState::Standing, + 31..=40 => LilFrenState::TaxFraud, + 41..=50 => LilFrenState::Sick, + 51..=70 => LilFrenState::Dancing, + _ => LilFrenState::Sleep, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct LilFren { + id: Option, + pub emoji: EmojiId, + pub hunger: f32, + pub thirst: f32, + pub entertainment: f32, + + pub state: LilFrenState, + pub smarts: f32, + pub metabolism: f32, +} + +impl JdbModel for LilFren { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "LilFren".to_string() + } +} + +impl LilFren { + pub fn new(emoji: EmojiId) -> Self { + Self { + id: None, + emoji, + hunger: 1.0, + thirst: 1.0, + entertainment: 1.0, + state: LilFrenState::Standing, + smarts: thread_rng().gen_range(0.1..1.0), + metabolism: thread_rng().gen_range(0.1..1.0), + } + } + + pub fn create_new_lil_fren(db: &Database, emoji: EmojiId) -> Result { + let lil_fren: Option = Self::get_lil_fren(db)?; + + if let Some(lil_fren) = lil_fren { + db.remove::(lil_fren.id().unwrap())?; + } + + let lil_fren = db.insert(Self::new(emoji))?; + + Ok(lil_fren) + } + + pub fn get_lil_fren(db: &Database) -> Result, Error> { + Ok(db.filter(|_, _fren: &Self| true)?.next()) + } + + pub fn update_fren(&mut self, db: &Database) -> Result<(), Error> { + if let Some(mut lil_fren) = Self::get_lil_fren(db)? { + let (hunger_diff, thirst_diff, entertainment_diff) = match lil_fren.state { + LilFrenState::Standing => (-0.01, -0.02, -0.01), + LilFrenState::TaxFraud => (-0.01, -0.02, 0.01), + LilFrenState::Sick => (-0.1, -0.15, -0.01), + LilFrenState::Dancing => (-0.05, -0.8, 0.05), + LilFrenState::Sleep => (-0.005, -0.01, 0.00), + }; + + lil_fren.hunger -= hunger_diff * self.metabolism; + lil_fren.thirst -= thirst_diff * self.metabolism; + lil_fren.entertainment -= entertainment_diff * self.smarts; + + if thread_rng().gen_bool(0.75) { + println!("fren is now {:?}", self.state); + lil_fren.state = thread_rng().gen(); + } + + db.insert(lil_fren)?; + } + + Ok(()) + } + + pub fn is_alive(&self) -> AliveState { + if self.thirst < 0.0 { + AliveState::DiedOfThirst + } else if self.entertainment < 0.0 { + AliveState::DiedOfBoredom + } else if self.hunger < 0.0 { + AliveState::DiedOfHunger + } else { + AliveState::Alive + } + } + + fn draw_activity(&self, emoji: &Emoji) -> String { + match self.state { + LilFrenState::Standing => draw_standing(emoji), + LilFrenState::TaxFraud => draw_tax_fraud(emoji), + LilFrenState::Sick => draw_sick(emoji), + LilFrenState::Dancing => draw_dancing(emoji), + LilFrenState::Sleep => draw_sleep(emoji), + } + } + + pub async fn draw(&self, ctx: &Context, guild: &GuildId) -> String { + let emoji = guild.emoji(&ctx.http, self.emoji).await.unwrap(); + let alive_state = self.is_alive(); + + if let AliveState::Alive = alive_state { + self.draw_activity(&emoji) + } else { + draw_dead() + } + } +} + +pub async fn lil_fren_task(ctx: &Context) { + tokio::time::sleep(Duration::from_secs(60)).await; + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + + let fren = LilFren::get_lil_fren(&global_data.db).unwrap(); + + if let Some(mut fren) = fren { + fren.update_fren(&global_data.db).unwrap(); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 69c0871..0503380 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod api_key; pub mod birthday; pub mod insult_compliment; +pub mod lil_fren; pub mod motivation; pub mod random;