Added gogurt reserve

This commit is contained in:
Joey Hines 2025-08-04 17:56:46 -06:00
parent 3ec319b93d
commit 1367e427ed
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
8 changed files with 285 additions and 2 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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(),

106
src/discord/stonks.rs Normal file
View File

@ -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<poise::serenity_prelude::User>,
) -> 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(())
}

View File

@ -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."
),
}
}
}

View File

@ -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<u64>,
pub reserve_contributors: HashMap<UserId, f64>,
pub gogurt_rate_per_pound: f64,
}
impl j_db::model::JdbModel for GogurtReserves {
fn id(&self) -> Option<u64> {
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<Self, Error> {
Ok(db
.filter(|_, _: &Self| true)?
.next()
.unwrap_or(Self::default()))
}
pub fn add_contribution(db: &Database, user: UserId, fc_amount: u64) -> Result<f64, Error> {
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<u64, Error> {
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)
}
}

View File

@ -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;

View File

@ -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>(task.id().unwrap()).is_ok();