diff --git a/.gitignore b/.gitignore index e020a9d..f9ec699 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /.idea config.toml stories/ -voices/ \ No newline at end of file +voices/ +nft/ \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 79f0dc4..9ca787e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,8 @@ use crate::error::Error::NoAlbumFound; use crate::imgur; use crate::imgur::Image; use crate::insult_compliment::InsultComplimentTemplate; -use crate::wallet::WalletManager; +use crate::inventory::InventoryManager; +use crate::user::UserManager; use config::{Config, File}; use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; @@ -48,6 +49,11 @@ pub struct BotConfig { pub imgur_client_id: String, pub story_path: PathBuf, pub voice_path: PathBuf, + pub nft_path: PathBuf, + + pub user_manager: UserManager, + + pub bot_inventory: InventoryManager, #[serde(default)] pub albums: Vec, @@ -55,8 +61,6 @@ pub struct BotConfig { #[serde(default)] pub randoms: Vec, - pub wallet_manager: WalletManager, - pub motivation: MotivationConfig, pub insults: Vec, diff --git a/src/discord/emoji_race.rs b/src/discord/emoji_race.rs index 401df31..f0ce2d1 100644 --- a/src/discord/emoji_race.rs +++ b/src/discord/emoji_race.rs @@ -1,5 +1,5 @@ use crate::config::{Channel, GlobalData}; -use crate::wallet::WalletError; +use crate::user::UserError; use crate::{command, group}; use rand::seq::IteratorRandom; use rand::{thread_rng, Rng}; @@ -25,7 +25,7 @@ pub struct EmojiRace; #[derive(Clone, Debug)] pub enum RaceError { RacerNotFound, - BetFundError(WalletError), + BetFundError(UserError), UserHasBet, } @@ -39,8 +39,8 @@ impl Display for RaceError { } } -impl From for RaceError { - fn from(value: WalletError) -> Self { +impl From for RaceError { + fn from(value: UserError) -> Self { Self::BetFundError(value) } } @@ -135,7 +135,7 @@ async fn add_bet( global .cfg - .wallet_manager + .user_manager .try_take_funds(bet.author, bet.amount)?; if !racers.iter().any(|r| r.emoji.id == bet.emoji) { @@ -298,9 +298,8 @@ async fn race(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { global_data .cfg - .wallet_manager - .get_user_wallet(winner) - .coin_count += payout; + .user_manager + .give_funds(winner, payout as u32); } } } diff --git a/src/discord/fren_coin.rs b/src/discord/fren_coin.rs index f806f33..8d0654c 100644 --- a/src/discord/fren_coin.rs +++ b/src/discord/fren_coin.rs @@ -1,6 +1,6 @@ use crate::config::GlobalData; use crate::error::Error; -use crate::wallet::WalletError; +use crate::user::UserError; use crate::{command, group}; use rand::{thread_rng, Rng}; use serenity::client::Context; @@ -23,7 +23,7 @@ async fn balance(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let user = args.parse::().unwrap_or(msg.author.id); - let wallet = global_data.cfg.wallet_manager.get_user_wallet(user); + let wallet = global_data.cfg.user_manager.get_user(user); msg.reply( &ctx.http, @@ -74,10 +74,10 @@ async fn gift(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { if let Err(e) = global_data .cfg - .wallet_manager + .user_manager .transfer_funds(msg.author.id, target, amount) { - if let WalletError::NotEnoughFunds = e { + if let UserError::NotEnoughFunds = e { msg.reply( &ctx.http, "Sorry pal, I can't give credit. Come back when you're a bit mmmm richer.", @@ -108,7 +108,7 @@ pub async fn give_coin( ctx: &Context, user: UserId, percent: f64, - numer_of_coins: i64, + number_of_coins: i64, ) -> Result { let should_get_coin = { let mut thread_rng = thread_rng(); @@ -120,10 +120,10 @@ pub async fn give_coin( let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); - { - let wallet = global_data.cfg.wallet_manager.get_user_wallet(user); - wallet.coin_count += numer_of_coins; - } + global_data + .cfg + .user_manager + .give_funds(user, number_of_coins as u32); global_data .cfg diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 8ae835e..a296922 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -6,11 +6,13 @@ pub mod emoji_race; pub mod fren_coin; pub mod joke; pub mod motivate; +pub mod shop; pub mod story; pub mod voices; use crate::discord::fren_coin::give_coin; use crate::discord::joke::random; +use crate::discord::shop::restock_shop; use crate::{help, hook, GlobalData}; use rand::prelude::IteratorRandom; use rand::thread_rng; @@ -29,22 +31,28 @@ use std::time::Duration; pub struct Handler; static ERROR_MSG: &str = - "OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The admins at our - headquarters are working VEWY HAWD to fix this!"; + "OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The admins at our headquarters are working VEWY HAWD to fix this!"; #[async_trait] impl EventHandler for Handler { async fn cache_ready(&self, ctx: Context, _guilds: Vec) { tokio::spawn(async move { loop { - tokio::time::sleep(Duration::from_secs(60 * 60)).await; { - println!("Reloading config..."); - let mut data = ctx.data.write().await; - let global_data = data.get_mut::().unwrap(); + { + println!("Reloading config..."); + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); - global_data.reload().await.unwrap(); + global_data.reload().await.unwrap(); + } } + + { + println!("Restocking shop..."); + restock_shop(&ctx).await.unwrap(); + } + tokio::time::sleep(Duration::from_secs(60 * 60)).await; } }); } diff --git a/src/discord/motivate.rs b/src/discord/motivate.rs index 842ecbe..7820124 100644 --- a/src/discord/motivate.rs +++ b/src/discord/motivate.rs @@ -4,7 +4,7 @@ use magick_rust::{DrawingWand, MagickWand, PixelWand}; use rand::prelude::IteratorRandom; use rand::thread_rng; use serenity::client::Context; -use serenity::framework::standard::{Args, CommandResult}; +use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::model::channel::{AttachmentType, Message}; use std::borrow::Cow; @@ -12,13 +12,10 @@ use std::borrow::Cow; #[commands(motivation)] pub struct Motivate; -#[command] -#[only_in(guilds)] -#[description("Let's give you motivation")] -async fn motivation(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let data = ctx.data.read().await; - let global_data = data.get::().unwrap(); - +pub async fn create_image( + global_data: &GlobalData, + border_color: &str, +) -> Result, CommandError> { let album = &global_data .cfg .motivation @@ -49,45 +46,54 @@ async fn motivation(ctx: &Context, msg: &Message, _args: Args) -> CommandResult .iter() .choose(&mut thread_rng()) .unwrap(); + let motivation = format!("{} {}", action, goal); - let image = { - let mut wand = MagickWand::new(); - let mut border_wand = PixelWand::new(); - wand.read_image_blob(motivation_image_blob)?; + let mut wand = MagickWand::new(); + let mut border_wand = PixelWand::new(); + wand.read_image_blob(motivation_image_blob)?; - border_wand.set_color("white")?; - let width = wand.get_image_width(); - let border = width / 100; - wand.border_image( - &border_wand, - border, - border, - magick_rust::bindings::CompositeOperator_OverCompositeOp, - )?; + border_wand.set_color(border_color)?; + let width = wand.get_image_width(); + let border = width / 100; + wand.border_image( + &border_wand, + border, + border, + magick_rust::bindings::CompositeOperator_OverCompositeOp, + )?; - border_wand.set_color("black")?; - let width = wand.get_image_width(); - let border = width * 20 / 100; - wand.border_image( - &border_wand, - border, - border, - magick_rust::bindings::CompositeOperator_OverCompositeOp, - )?; + border_wand.set_color("black")?; + let width = wand.get_image_width(); + let border = width * 20 / 100; + wand.border_image( + &border_wand, + border, + border, + magick_rust::bindings::CompositeOperator_OverCompositeOp, + )?; - let text_pos_x = wand.get_image_width() as f64 / 2.0; - let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05); + let text_pos_x = wand.get_image_width() as f64 / 2.0; + let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05); - let mut text_wand = DrawingWand::new(); - let mut text_color_wand = PixelWand::new(); - text_color_wand.set_color("white")?; - text_wand.set_fill_color(&text_color_wand); - text_wand.set_font_size(0.07 * (wand.get_image_width() as f64)); - text_wand.set_text_alignment(magick_rust::bindings::AlignType_CenterAlign); - wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &motivation)?; - wand.write_image_blob("png")? - }; + let mut text_wand = DrawingWand::new(); + let mut text_color_wand = PixelWand::new(); + text_color_wand.set_color("white")?; + text_wand.set_fill_color(&text_color_wand); + text_wand.set_font_size(0.07 * (wand.get_image_width() as f64)); + text_wand.set_text_alignment(magick_rust::bindings::AlignType_CenterAlign); + wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &motivation)?; + Ok(wand.write_image_blob("png")?) +} + +#[command] +#[only_in(guilds)] +#[description("Let's give you motivation")] +async fn motivation(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + + let image = create_image(global_data, "white").await?; msg.channel_id .send_message(&ctx.http, |m| { diff --git a/src/discord/shop.rs b/src/discord/shop.rs new file mode 100644 index 0000000..e6d5bc1 --- /dev/null +++ b/src/discord/shop.rs @@ -0,0 +1,295 @@ +use crate::discord::motivate::create_image; +use crate::inventory::{InventoryError, ItemData, ItemType}; +use crate::user::UserError; +use crate::{command, group, GlobalData}; +use rand::prelude::SliceRandom; +use rand::{thread_rng, Rng}; +use serenity::client::Context; +use serenity::framework::standard::{Args, CommandError, CommandResult}; +use serenity::model::channel::{AttachmentType, Message}; +use serenity::utils::MessageBuilder; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use tokio::io::AsyncWriteExt; + +#[group] +#[commands(shop, buy, inventory, use_item)] +pub struct Shop; + +#[command] +#[description("Fren has wares if you have coin")] +async fn shop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + if global_data.cfg.bot_inventory.inventory.is_empty() { + msg.reply(&ctx.http, "Sorry shop is closed until we get more wares.") + .await?; + } else { + let mut inv_msg = MessageBuilder::new(); + + inv_msg.push_bold_line("Fren has wares if you have coin:"); + + inv_msg.push_safe(global_data.cfg.bot_inventory.list_items(true)); + + msg.reply(&ctx.http, inv_msg.build()).await?; + } + + Ok(()) +} + +#[command] +#[description("Open your inventory, is specifically can not be rebound to the B key")] +async fn inventory(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + let mut inv_msg = MessageBuilder::new(); + + let user = global_data.cfg.user_manager.get_user(msg.author.id); + + if user.inventory.inventory.is_empty() { + msg.reply(&ctx, "Sorry your inventory is empty.").await?; + } else { + inv_msg.push_bold_line("Your inventory: "); + + inv_msg.push_safe(user.inventory.list_items(false)); + + msg.reply(&ctx.http, inv_msg.build()).await?; + } + + Ok(()) +} + +#[command] +#[description("Buying something?")] +async fn buy(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + if args.is_empty() { + msg.reply( + &ctx.http, + "You gonna buy something or do you just like to stare?", + ) + .await?; + return Ok(()); + } + + let item = match args.rest().parse::() { + Ok(i) => i, + Err(_) => { + msg.reply(&ctx.http, "I don't know what the heck that is tbh.") + .await?; + return Ok(()); + } + }; + + if let Some(item_slot) = global_data.cfg.bot_inventory.get_item(item) { + if let Err(err) = global_data + .cfg + .user_manager + .try_take_funds(msg.author.id, item_slot.value() as u32) + { + msg.reply( + &ctx.http, + format!( + "I see why you don't get to the cloud district very often: {}", + err + ), + ) + .await?; + return Ok(()); + } + } else { + msg.reply(&ctx.http, "I don't have that in stock you goob") + .await?; + return Ok(()); + } + + let item_data = match global_data.cfg.bot_inventory.try_take_item(item, 1) { + Ok(i) => i, + Err(err) => { + msg.reply(&ctx.http, format!("I can't just sell you that: {}", err)) + .await?; + return Ok(()); + } + }; + + global_data + .cfg + .user_manager + .give_item(msg.author.id, item, 1, item_data); + + msg.reply(&ctx, format!("Congrats, you now own a '{}'", item)) + .await?; + + Ok(()) +} + +#[command] +#[only_in(guilds)] +#[aliases("use")] +#[description("Buying something?")] +async fn use_item(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + if args.is_empty() { + msg.reply(&ctx.http, "You need to select one item to use.") + .await?; + return Ok(()); + } + + let item = match args.rest().parse::() { + Ok(i) => i, + Err(_) => { + msg.reply(&ctx.http, "I don't know what the heck that is tbh.") + .await?; + return Ok(()); + } + }; + + let item_data = match global_data + .cfg + .user_manager + .try_use_item(msg.author.id, item) + { + Ok(i) => i, + Err(err) => { + if let UserError::InventoryError(InventoryError::NotEnoughItems) = err { + msg.reply( + &ctx.http, + "Looks like you don't have enough of that item to use it", + ) + .await?; + } + return Ok(()); + } + }; + + match item { + ItemType::CancelInsurance => { + msg.reply(&ctx.http, "You are immune to the next (1) cancelings.") + .await?; + } + ItemType::TheConceptOfLove => { + msg.reply(&ctx.http, "I have DMed you the concept of love.") + .await?; + + msg.author + .id + .create_dm_channel(&ctx.http) + .await? + .say( + &ctx.http, + "DO NOT SHARE\nhttps://www.youtube.com/watch?v=HNy_retSME0", + ) + .await?; + } + ItemType::GoodFortune => { + let good_fortunes = [ + "Yes.", + "OF COURSE.", + "Carolyn, I'm sorry. So yes :)", + "That sounds great!", + "Yes, I am happy for you!", + "||YES||", + ]; + let fortune = good_fortunes.choose(&mut thread_rng()).unwrap(); + + msg.reply(&ctx.http, fortune).await?; + } + ItemType::Nft => { + if let Some(ItemData::Nft(path)) = item_data { + let file: tokio::fs::File = match tokio::fs::File::open(path).await { + Ok(f) => f, + Err(_) => { + msg.reply(&ctx.http, "Sorry this was a pump and dump") + .await?; + return Ok(()); + } + }; + msg.channel_id + .send_message(&ctx.http, |m| { + m.content("Your NFT my good friend:") + .add_file(AttachmentType::File { + file: &file, + filename: "nft.png".to_string(), + }) + }) + .await?; + } else { + msg.reply( + &ctx.http, + "Fren NFTs were never my creation, I merely promoted the brand", + ) + .await?; + } + } + } + + Ok(()) +} + +pub async fn restock_shop(ctx: &Context) -> Result<(), CommandError> { + let mut data = ctx.data.write().await; + let global_data = data.get_mut::().unwrap(); + + global_data.cfg.bot_inventory.inventory.clear(); + + global_data.cfg.bot_inventory.give_item( + ItemType::TheConceptOfLove, + thread_rng().gen_range(0..5), + None, + ); + global_data.cfg.bot_inventory.give_item( + ItemType::GoodFortune, + thread_rng().gen_range(0..10), + None, + ); + global_data.cfg.bot_inventory.give_item( + ItemType::CancelInsurance, + thread_rng().gen_range(0..10), + None, + ); + + loop { + let mut dir = tokio::fs::read_dir(&global_data.cfg.nft_path).await?; + + let mut count = 0; + while let Ok(Some(_)) = dir.next_entry().await { + count += 1; + } + + if count > 64 { + tokio::fs::remove_dir_all(&global_data.cfg.nft_path).await?; + tokio::fs::create_dir(&global_data.cfg.nft_path).await? + } + + let nft = create_image(global_data, "gold").await.unwrap(); + let mut hasher = DefaultHasher::new(); + hasher.write(&nft); + let nft_hash = hasher.finish(); + + let path = global_data.cfg.nft_path.join(format!("{}.png", nft_hash)); + + if path.exists() { + continue; + } + + let mut file = tokio::fs::File::create(path.clone()).await?; + file.write_all(&nft).await?; + + global_data.cfg.bot_inventory.give_item( + ItemType::Nft, + 1, + Some(ItemData::Nft(path.to_str().unwrap().to_string())), + ); + break; + } + + global_data.cfg.save(&global_data.args.cfg_path).await?; + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index a6b61bb..b372e55 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use crate::imgur::ImgurError; +use crate::user; use serde::ser::StdError; use std::fmt::{Display, Formatter}; @@ -11,7 +12,7 @@ pub enum Error { SerenityError(serenity::Error), TeraError(tera::Error), NoAlbumFound, - FrenCoin(crate::wallet::WalletError), + UserError(user::UserError), } impl StdError for Error {} @@ -48,7 +49,7 @@ impl Display for Error { Error::SerenityError(e) => write!(f, "Discord error: {}", e), Error::TeraError(e) => write!(f, "Tera error: {}", e), Error::NoAlbumFound => write!(f, "No album found"), - Error::FrenCoin(e) => write!(f, "Fren coin error: {}", e), + Error::UserError(e) => write!(f, "User error: {}", e), } } } diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs new file mode 100644 index 0000000..137140f --- /dev/null +++ b/src/inventory/mod.rs @@ -0,0 +1,192 @@ +use serde::{Deserialize, Serialize}; +use serenity::utils::MessageBuilder; +use std::collections::hash_map::DefaultHasher; +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub enum InventoryError { + NotEnoughItems, + UnkownItem, +} + +impl Display for InventoryError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + InventoryError::NotEnoughItems => write!(f, "Not enough items"), + InventoryError::UnkownItem => write!(f, "Not sure that that is, maybe check Walmart?"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(tag = "type", content = "data")] +pub enum ItemData { + Nft(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Copy)] +pub enum ItemType { + CancelInsurance, + TheConceptOfLove, + GoodFortune, + Nft, +} + +impl FromStr for ItemType { + type Err = InventoryError; + + fn from_str(s: &str) -> Result { + let item = s.to_lowercase().replace(' ', ""); + + if item.starts_with("cancelinsurance") { + Ok(ItemType::CancelInsurance) + } else if item.starts_with("theconceptoflove") { + Ok(ItemType::TheConceptOfLove) + } else if item.starts_with("goodfortune") { + Ok(ItemType::GoodFortune) + } else if item.starts_with("nft") { + Ok(ItemType::Nft) + } else { + Err(InventoryError::UnkownItem) + } + } +} + +impl Display for ItemType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let name = match self { + ItemType::CancelInsurance => "Cancel Insurance".to_string(), + ItemType::TheConceptOfLove => "The Concept of Love".to_string(), + ItemType::GoodFortune => "Good Fortune".to_string(), + ItemType::Nft => "NFT".to_string(), + }; + + write!(f, "{}", name) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InventorySlot { + pub quantity: i64, + pub item_type: ItemType, + pub item_data: Option, +} +impl InventorySlot { + pub fn value(&self) -> i64 { + match self.item_type { + ItemType::CancelInsurance => 50, + ItemType::TheConceptOfLove => 300, + ItemType::GoodFortune => 75, + ItemType::Nft => 100, + } + } + + pub fn sell_value(&self) -> i64 { + match self.item_type { + ItemType::Nft => { + if let Some(ItemData::Nft(s)) = &self.item_data { + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + + let hash = hasher.finish(); + + let per_value = (hash as f64) / (u64::MAX as f64); + + ((2000.0 * per_value) as i64) - 1000 + } else { + 0 + } + } + _ => self.value() / 2, + } + } + + pub fn use_cost(&self) -> i64 { + match self.item_type { + ItemType::Nft => 0, + _ => 1, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct InventoryManager { + pub inventory: Vec, +} + +impl InventoryManager { + pub fn get_item(&mut self, item_type: ItemType) -> Option<&mut InventorySlot> { + if let Some(inv_ndx) = self.inventory.iter().position(|i| i.item_type == item_type) { + Some(&mut self.inventory[inv_ndx]) + } else { + None + } + } + + pub fn give_item(&mut self, item_type: ItemType, quantity: i64, item_data: Option) { + match self.get_item(item_type) { + None => self.inventory.push(InventorySlot { + quantity, + item_type, + item_data, + }), + Some(slot) => { + slot.item_data = item_data; + slot.quantity += quantity; + } + } + } + + pub fn try_take_item( + &mut self, + item: ItemType, + quantity: i64, + ) -> Result, InventoryError> { + let slot = self.get_item(item); + + if let Some(slot) = slot { + if slot.quantity < quantity { + return Err(InventoryError::NotEnoughItems); + } + slot.quantity -= quantity; + + Ok(slot.item_data.clone()) + } else { + Err(InventoryError::NotEnoughItems) + } + } + + pub fn list_items(&self, buy_value: bool) -> String { + let mut msg_builder = MessageBuilder::new(); + + for item_slot in &self.inventory { + if item_slot.quantity != 0 { + let value = if buy_value { + item_slot.value() + } else { + item_slot.sell_value() + }; + msg_builder.push_line(format!( + "* {} [{} coins] (x{})", + item_slot.item_type, value, item_slot.quantity + )); + } + } + + msg_builder.build() + } + + pub fn try_use_item(&mut self, item: ItemType) -> Result, InventoryError> { + let item_slot = self.get_item(item).ok_or(InventoryError::NotEnoughItems)?; + + if item_slot.quantity < item_slot.use_cost() { + return Err(InventoryError::NotEnoughItems); + } + + item_slot.quantity -= item_slot.use_cost(); + + Ok(item_slot.item_data.clone()) + } +} diff --git a/src/main.rs b/src/main.rs index b4eaf9c..8130caf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,8 @@ mod discord; mod error; mod imgur; mod insult_compliment; -mod wallet; +mod inventory; +mod user; use crate::config::{Args, BotConfig, Channel, GlobalData}; use crate::discord::emoji_race::RaceMessage; @@ -56,6 +57,7 @@ async fn main() { .group(&discord::emoji_race::EMOJIRACE_GROUP) .group(&discord::motivate::MOTIVATE_GROUP) .group(&discord::voices::VOICES_GROUP) + .group(&discord::shop::SHOP_GROUP) .unrecognised_command(unrecognised_command_hook) .bucket("bad_apple", |b| b.delay(60 * 10)) .await diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..73a1863 --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,121 @@ +use crate::inventory::{InventoryError, InventoryManager, InventorySlot, ItemData, ItemType}; +use serde::{Deserialize, Serialize}; +use serenity::model::id::UserId; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum UserError { + NotEnoughFunds, + InvalidTarget, + InventoryError(InventoryError), +} + +impl Display for UserError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UserError::NotEnoughFunds => write!(f, "Not enough funds"), + UserError::InvalidTarget => write!(f, "Invalid target"), + UserError::InventoryError(e) => write!(f, "Inventory error: {}", e), + } + } +} + +impl From for UserError { + fn from(e: InventoryError) -> Self { + Self::InventoryError(e) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct User { + pub user_id: UserId, + pub coin_count: i64, + #[serde(default)] + pub inventory: InventoryManager, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct UserManager { + #[serde(default)] + users: Vec, +} + +#[allow(dead_code)] +impl UserManager { + pub fn get_user(&mut self, discord_id: UserId) -> &mut User { + if let Some(user_ndx) = self.users.iter().position(|u| u.user_id == discord_id) { + &mut self.users[user_ndx] + } else { + self.users.push(User { + user_id: discord_id, + coin_count: 100, + inventory: Default::default(), + }); + self.users.last_mut().unwrap() + } + } + + pub fn transfer_funds( + &mut self, + src: UserId, + dest: UserId, + amount: u32, + ) -> Result<(), UserError> { + self.try_take_funds(src, amount)?; + + self.give_funds(dest, amount); + Ok(()) + } + + pub fn give_funds(&mut self, discord_id: UserId, amount: u32) { + let mut wallet = self.get_user(discord_id); + wallet.coin_count += amount as i64; + } + + pub fn try_take_funds(&mut self, discord_id: UserId, amount: u32) -> Result<(), UserError> { + let mut wallet = self.get_user(discord_id); + if wallet.coin_count < amount as i64 { + Err(UserError::NotEnoughFunds) + } else { + wallet.coin_count -= amount as i64; + Ok(()) + } + } + + pub fn get_item(&mut self, discord_id: UserId, item: ItemType) -> Option<&mut InventorySlot> { + self.get_user(discord_id).inventory.get_item(item) + } + + pub fn give_item( + &mut self, + discord_id: UserId, + item: ItemType, + quantity: i64, + item_data: Option, + ) { + self.get_user(discord_id) + .inventory + .give_item(item, quantity, item_data); + } + + pub fn try_take_item( + &mut self, + discord_id: UserId, + item: ItemType, + quantity: i64, + ) -> Result, UserError> { + Ok(self + .get_user(discord_id) + .inventory + .try_take_item(item, quantity)?) + } + + pub fn try_use_item( + &mut self, + discord_id: UserId, + item: ItemType, + ) -> Result, UserError> { + Ok(self.get_user(discord_id).inventory.try_use_item(item)?) + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs deleted file mode 100644 index 413ee04..0000000 --- a/src/wallet/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serenity::model::id::UserId; -use std::fmt::{Display, Formatter}; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum WalletError { - NotEnoughFunds, - InvalidTarget, -} - -impl Display for WalletError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - WalletError::NotEnoughFunds => write!(f, "Not enough funds"), - WalletError::InvalidTarget => write!(f, "Invalid target"), - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Wallet { - pub owner: UserId, - pub coin_count: i64, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Default)] -pub struct WalletManager { - #[serde(default)] - wallets: Vec, -} - -impl WalletManager { - pub fn get_user_wallet(&mut self, discord_id: UserId) -> &mut Wallet { - if let Some(user_ndx) = self.wallets.iter().position(|u| u.owner == discord_id) { - &mut self.wallets[user_ndx] - } else { - self.wallets.push(Wallet { - owner: discord_id, - coin_count: 100, - }); - self.wallets.last_mut().unwrap() - } - } - - pub fn transfer_funds( - &mut self, - src: UserId, - dest: UserId, - amount: u32, - ) -> Result<(), WalletError> { - self.try_take_funds(src, amount)?; - - self.give_funds(dest, amount); - Ok(()) - } - - pub fn give_funds(&mut self, discord_id: UserId, amount: u32) { - let mut wallet = self.get_user_wallet(discord_id); - wallet.coin_count += amount as i64; - } - - pub fn try_take_funds(&mut self, discord_id: UserId, amount: u32) -> Result<(), WalletError> { - let mut wallet = self.get_user_wallet(discord_id); - if wallet.coin_count < amount as i64 { - Err(WalletError::NotEnoughFunds) - } else { - wallet.coin_count -= amount as i64; - Ok(()) - } - } -}