From 1367e427edb77ddd2601ffa1d60b93f2222fc0ea Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Mon, 4 Aug 2025 17:56:46 -0600 Subject: [PATCH] Added gogurt reserve --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/discord/mod.rs | 5 ++ src/discord/stonks.rs | 106 +++++++++++++++++++++++ src/error.rs | 5 ++ src/models/gogurt_reserves.rs | 153 ++++++++++++++++++++++++++++++++++ src/models/mod.rs | 1 + src/models/task.rs | 13 +++ 8 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 src/discord/stonks.rs create mode 100644 src/models/gogurt_reserves.rs diff --git a/Cargo.lock b/Cargo.lock index 496889d..8daa267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,7 +1093,7 @@ dependencies = [ [[package]] name = "fren" -version = "2.1.1" +version = "2.2.0" dependencies = [ "axum 0.8.1", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index de73b3c..9bba96b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fren" -version = "2.1.1" +version = "2.2.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 1840230..be646e3 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -11,6 +11,7 @@ mod little_fren; mod movie; mod role; pub(crate) mod shop; +mod stonks; mod transit; pub(crate) mod voices; @@ -379,6 +380,10 @@ pub async fn run_bot(global_data: GlobalData) { shop::land_mine(), shop::phrase_canceler(), shop::nuke(), + stonks::contribute_to_gogurt_reserve(), + stonks::sell_from_gogurt_reserve(), + stonks::gogurt_reserve_stats(), + stonks::gogurt_reserve_contribution(), transit::cta_bets(), voices::list_voices(), voices::list_words(), diff --git a/src/discord/stonks.rs b/src/discord/stonks.rs new file mode 100644 index 0000000..d745ddb --- /dev/null +++ b/src/discord/stonks.rs @@ -0,0 +1,106 @@ +use crate::discord::Context; +use crate::error::Error; +use crate::models::gogurt_reserves::GogurtReserves; +use poise::serenity_prelude::{Mentionable, MessageBuilder}; + +/// Contribute to the Gogurt Reserve +#[poise::command(prefix_command, category = "Stonks", aliases("cgr"))] +pub async fn contribute_to_gogurt_reserve( + ctx: Context<'_>, + #[description = "amount of FCs to spend"] contribute_amount: u64, +) -> Result<(), Error> { + let gogurt_bought = + GogurtReserves::add_contribution(&ctx.data().db, ctx.author().id, contribute_amount)?; + + ctx.reply(format!("You have purchased {gogurt_bought}lbs of Gogurt")) + .await?; + + Ok(()) +} + +/// Sell gogurt from the Gogurt Reserve +#[poise::command(prefix_command, category = "Stonks", aliases("sgr"))] +pub async fn sell_from_gogurt_reserve( + ctx: Context<'_>, + #[description = "number of pounds of gogurt to sell"] gogurt_to_sell: f64, +) -> Result<(), Error> { + let fc_coin_made = + GogurtReserves::take_contribution(&ctx.data().db, ctx.author().id, gogurt_to_sell)?; + + ctx.reply(format!( + "You have sold {gogurt_to_sell:.3}lbs of Gogurt for a profit of {fc_coin_made} FC" + )) + .await?; + + Ok(()) +} + +/// Get stats on the reserve +#[poise::command(prefix_command, category = "Stonks", aliases("grs"))] +pub async fn gogurt_reserve_stats(ctx: Context<'_>) -> Result<(), Error> { + let gogurt_reserves = GogurtReserves::get_gogurt_reserve(&ctx.data().db)?; + let contributors = gogurt_reserves.get_contributors(); + let total_amount = gogurt_reserves.get_total_amount(); + let total_worth = gogurt_reserves.get_total_worth(); + let going_rate = gogurt_reserves.gogurt_rate_per_pound; + + let mut msg_builder = MessageBuilder::new(); + + msg_builder.push_line("# Fren Gogurt Reserves LLC Report"); + msg_builder + .push_italic_line("Where your portable yogurt dreams are our portable yogurt solutions!"); + msg_builder.push_line(""); + msg_builder.push_line(format!( + "The reserve currently contains **{total_amount:.3} lb** of Gogurt brand Gogurt" + )); + msg_builder.push_line(format!( + "The worth of this Gogurt total is **{total_worth} FC **" + )); + msg_builder.push_line(""); + msg_builder.push_line(format!( + "The going rate of Gogurt on the free market is **{going_rate:.3} FC/lb**" + )); + msg_builder.push_line(""); + msg_builder.push_line("## Top Contributors"); + + let top_contributors_cnt = 5.clamp(0, contributors.len()); + + for (ndx, (contributor, amount)) in contributors[0..top_contributors_cnt].iter().enumerate() { + let mention = contributor.mention(); + let worth = gogurt_reserves.convert_pounds_to_fc(*amount); + let ndx = ndx + 1; + msg_builder.push_line(format!( + "{ndx}. {mention} with {amount:.3} lb worth {worth} FC" + )); + } + + ctx.reply(msg_builder.build()).await?; + + Ok(()) +} + +/// Get info on your reserve contribution +#[poise::command(prefix_command, category = "Stonks", aliases("grc"))] +pub async fn gogurt_reserve_contribution( + ctx: Context<'_>, + #[description = "Optional user to get info on"] user: Option, +) -> Result<(), Error> { + let gogurt_reserves = GogurtReserves::get_gogurt_reserve(&ctx.data().db)?; + let user_id = user.map(|u| u.id).unwrap_or(ctx.author().id); + let contribution = gogurt_reserves.reserve_contributors.get(&user_id); + + let mention = user_id.mention(); + if let Some(contribution) = contribution { + let worth = gogurt_reserves.convert_pounds_to_fc(*contribution); + let rate = gogurt_reserves.gogurt_rate_per_pound; + + ctx.reply(format!("{mention}'s contribution is **{contribution:.3} lb** of Gogurt Brand Gogurt worth **{worth} FC** at the current rate of **{rate:.3} FC/lb**")).await?; + } else { + ctx.reply(format!( + "{mention} has not contributed to our fine reserve yet!" + )) + .await?; + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index 70606f8..b41f72c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,7 @@ pub enum Error { NoImageFound, PipelineArgumentError(ModifyImageArgError), NoRandomFound, + NotEnoughGogurt, } impl StdError for Error {} @@ -116,6 +117,10 @@ impl Display for Error { Error::NoImageFound => write!(f, "Image not found"), Error::PipelineArgumentError(err) => write!(f, "{err}"), Error::NoRandomFound => write!(f, "No random found"), + Error::NotEnoughGogurt => write!( + f, + "Wow, you really don't have enough gogurt. What are you 12?? Just got buy some poor." + ), } } } diff --git a/src/models/gogurt_reserves.rs b/src/models/gogurt_reserves.rs new file mode 100644 index 0000000..1623651 --- /dev/null +++ b/src/models/gogurt_reserves.rs @@ -0,0 +1,153 @@ +use crate::error::Error; +use crate::user::User; +use chrono::Utc; +use j_db::database::Database; +use log::info; +use poise::serenity_prelude::UserId; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::f32::consts::PI; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct GogurtReserves { + id: Option, + pub reserve_contributors: HashMap, + pub gogurt_rate_per_pound: f64, +} + +impl j_db::model::JdbModel for GogurtReserves { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "GogurtReserves".to_string() + } +} + +impl GogurtReserves { + pub fn get_gogurt_reserve(db: &Database) -> Result { + Ok(db + .filter(|_, _: &Self| true)? + .next() + .unwrap_or(Self::default())) + } + + pub fn add_contribution(db: &Database, user: UserId, fc_amount: u64) -> Result { + let mut gogurt_reserve = Self::get_gogurt_reserve(db)?; + let gogurt_amount = gogurt_reserve.convert_fc_to_pounds(fc_amount); + + let contribution = gogurt_reserve + .reserve_contributors + .entry(user) + .or_insert_with(|| 0.0); + + User::try_take_funds(db, user, fc_amount as u32)?; + *contribution += gogurt_amount; + + db.insert(gogurt_reserve)?; + + Ok(gogurt_amount) + } + + pub fn take_contribution( + db: &Database, + user: UserId, + pounds_of_gogurt: f64, + ) -> Result { + let mut gogurt_reserve = Self::get_gogurt_reserve(db)?; + let contribution = gogurt_reserve + .reserve_contributors + .entry(user) + .or_insert_with(|| 0.0); + + if pounds_of_gogurt.is_sign_negative() { + return Err(Error::CommandError( + "You can't take negative gogurt, stop trying".to_string(), + )); + } + + if pounds_of_gogurt > *contribution { + Err(Error::NotEnoughGogurt) + } else { + *contribution -= pounds_of_gogurt; + let fc_profit = gogurt_reserve.convert_pounds_to_fc(pounds_of_gogurt); + + db.insert(gogurt_reserve)?; + + User::give_funds(db, user, fc_profit as u32)?; + + Ok(fc_profit) + } + } + + pub fn get_contributors(&self) -> Vec<(UserId, f64)> { + let mut contributors: Vec<(UserId, f64)> = + self.reserve_contributors.clone().into_iter().collect(); + + contributors.sort_by(|(_, a), (_, b)| b.total_cmp(a)); + + contributors + } + + pub fn get_total_amount(&self) -> f64 { + self.reserve_contributors.values().sum() + } + + pub fn get_total_worth(&self) -> u64 { + (self.get_total_amount() * self.gogurt_rate_per_pound) as u64 + } + + pub fn convert_pounds_to_fc(&self, pounds: f64) -> u64 { + (pounds * self.gogurt_rate_per_pound) as u64 + } + + pub fn convert_fc_to_pounds(&self, fc: u64) -> f64 { + fc as f64 / self.gogurt_rate_per_pound + } + + pub fn market_function(time: i64) -> f64 { + let x = time as f64; + (x.sin() + (PI as f64 * x).cos() + (PI as f64 * x).sin() + x.cos() + - (x * 4.0).sin() + - (x * 3.0).cos()) + .clamp(0.0, 6.0) + } + + pub fn calculate_new_rate(time: i64) -> f64 { + Self::market_function(time) + } + + pub fn update_rate(db: &Database) -> Result<(), Error> { + let mut reserves = Self::get_gogurt_reserve(db)?; + + let time = Utc::now().timestamp(); + + let new_rate = Self::calculate_new_rate(time); + + info!("Updating rate to '{new_rate}'"); + reserves.gogurt_rate_per_pound = new_rate; + + db.insert(reserves)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::models::gogurt_reserves::GogurtReserves; + + #[test] + fn test_avg_market_function() { + let sum: f64 = (0..100000).map(GogurtReserves::market_function).sum(); + let avg = sum / 100000.0; + + println!("Average = {avg}"); + assert!(avg < 1.0 && avg > 0.0) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 284a94c..d0ac263 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod api_key; pub mod birthday; +pub mod gogurt_reserves; pub mod insult_compliment; pub mod lil_fren; pub mod managed_roles; diff --git a/src/models/task.rs b/src/models/task.rs index 4198ab1..0b7c794 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -2,6 +2,7 @@ use crate::config::GlobalData; use crate::discord::shop::restock_shop; use crate::error::Error; use crate::models::birthday::BirthdayEntry; +use crate::models::gogurt_reserves::GogurtReserves; use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType}; use crate::models::lil_fren::lil_fren_task; use chrono::{Days, Duration, TimeDelta, TimeZone, Timelike, Utc}; @@ -20,6 +21,7 @@ pub enum TaskType { HandleReload, RestockShop, UpdateLilBuddy, + UpdateGogurtRate, } impl TaskType { @@ -30,6 +32,7 @@ impl TaskType { | TaskType::HandleReload | TaskType::RestockShop | TaskType::UpdateLilBuddy + | TaskType::UpdateGogurtRate ) } } @@ -90,6 +93,7 @@ impl Task { Task::add_task(&data.db, TaskType::CheckBirthdays, Utc::now())?; Task::add_task(&data.db, TaskType::RestockShop, Utc::now())?; Task::add_task(&data.db, TaskType::UpdateLilBuddy, Utc::now())?; + Task::add_task(&data.db, TaskType::UpdateGogurtRate, Utc::now())?; Task::add_task( &data.db, TaskType::HandleReload, @@ -195,6 +199,15 @@ impl Task { Utc::now() + Duration::minutes(5), )?; } + TaskType::UpdateGogurtRate => { + GogurtReserves::update_rate(&data.db)?; + + Task::add_task( + &data.db, + TaskType::UpdateGogurtRate, + Utc::now() + Duration::minutes(20), + )?; + } } let _ = data.db.remove::(task.id().unwrap()).is_ok();