add improvements system and night market

This commit is contained in:
Joey Hines 2025-11-15 14:24:34 -07:00
parent 9f48d91b32
commit 2b5182a876
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
10 changed files with 346 additions and 44 deletions

39
Cargo.lock generated
View File

@ -1093,7 +1093,7 @@ dependencies = [
[[package]] [[package]]
name = "fren" name = "fren"
version = "2.4.1" version = "2.5.0"
dependencies = [ dependencies = [
"axum 0.8.1", "axum 0.8.1",
"base64 0.22.1", "base64 0.22.1",
@ -1118,9 +1118,11 @@ dependencies = [
"sha3", "sha3",
"songbird", "songbird",
"structopt", "structopt",
"strum 0.27.2",
"symphonia", "symphonia",
"tera", "tera",
"thiserror 2.0.12", "thiserror 2.0.12",
"thousands",
"tokio", "tokio",
"tonic", "tonic",
"tracing-core", "tracing-core",
@ -1464,7 +1466,7 @@ dependencies = [
"hex", "hex",
"shorthand", "shorthand",
"stable-vec", "stable-vec",
"strum", "strum 0.17.1",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@ -1998,7 +2000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@ -3881,7 +3883,16 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c" checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.17.1",
]
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.2",
] ]
[[package]] [[package]]
@ -3896,6 +3907,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -4228,6 +4251,12 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "thousands"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.8" version = "1.1.8"
@ -5147,7 +5176,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fren" name = "fren"
version = "2.4.1" version = "2.5.0"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -33,6 +33,8 @@ log = "0.4.26"
cta-api = { version = "0.5.0", registry = "ahines"} cta-api = { version = "0.5.0", registry = "ahines"}
thiserror = "2.0.12" thiserror = "2.0.12"
tracing-core = "0.1.33" tracing-core = "0.1.33"
strum = { version = "0.27.2", features = ["derive"] }
thousands = "0.2.0"
[dependencies.tokio] [dependencies.tokio]
version = "1.35.1" version = "1.35.1"

View File

@ -0,0 +1,73 @@
use crate::discord::Context;
use crate::error::Error;
use crate::models::improvements::{ImprovementType, Improvements};
use crate::user::User;
use poise::serenity_prelude::MessageBuilder;
use strum::IntoEnumIterator;
use thousands::Separable;
/// List out all the improvements available for the bot
#[poise::command(prefix_command, guild_only, category = "Improvements")]
pub async fn improvements(ctx: Context<'_>) -> Result<(), Error> {
let mut msg_builder = MessageBuilder::new();
msg_builder.push_line("# Fren Bot Improvements");
msg_builder.push_line("Anything can be improved with enough Fren Coins!");
msg_builder.push_line("");
for improvement_type in ImprovementType::iter() {
msg_builder.push("* ");
if let Some(improvement) = Improvements::get_improvement(&ctx.data().db, improvement_type)?
{
if improvement_type.has_levels() {
msg_builder.push(format!(" (✅ Level={}) ", improvement.level));
} else {
msg_builder.push(" (✅) ");
}
} else {
msg_builder.push(format!(
"({} FC) ",
improvement_type.price().separate_with_commas()
));
}
msg_builder.push_line(format!(
"**{}**: {}",
improvement_type.name(),
improvement_type.description()
));
}
ctx.reply(msg_builder.build()).await?;
Ok(())
}
/// Buy an improvement for the bot!
#[poise::command(prefix_command, guild_only, category = "Improvements")]
pub async fn buy_improvement(
ctx: Context<'_>,
#[description = "Improvement to buy"]
#[rest]
improvement_type: ImprovementType,
) -> Result<(), Error> {
if Improvements::get_improvement(&ctx.data().db, improvement_type)?.is_some()
&& !improvement_type.has_levels()
{
ctx.reply("Sorry, that improvement has already been purchased.")
.await?;
return Ok(());
}
User::try_take_funds(
&ctx.data().db,
ctx.author().id,
improvement_type.price() as u32,
)?;
Improvements::increment_improvement(&ctx.data().db, improvement_type)?;
ctx.reply(improvement_type.post_buy_description()).await?;
Ok(())
}

View File

@ -7,6 +7,7 @@ mod color;
mod emoji_race; mod emoji_race;
mod fren_coin; mod fren_coin;
mod image; mod image;
pub mod improvements;
mod joke; mod joke;
mod little_fren; mod little_fren;
mod movie; mod movie;
@ -356,6 +357,8 @@ pub async fn run_bot(global_data: GlobalData) {
image::green_screen(), image::green_screen(),
image::overlay(), image::overlay(),
image::edit_img(), image::edit_img(),
improvements::improvements(),
improvements::buy_improvement(),
movie::add_movie(), movie::add_movie(),
movie::list_movies(), movie::list_movies(),
movie::rate_movie(), movie::rate_movie(),

View File

@ -1,6 +1,7 @@
use crate::discord::voices::VoiceError; use crate::discord::voices::VoiceError;
use crate::image_manipulation::ModifyImageArgError; use crate::image_manipulation::ModifyImageArgError;
use crate::models::gogurt_reserves::GogurtError; use crate::models::gogurt_reserves::GogurtError;
use crate::models::improvements::ImprovementError;
use crate::user; use crate::user;
use magick_rust::MagickError; use magick_rust::MagickError;
use serde::ser::StdError; use serde::ser::StdError;
@ -28,6 +29,7 @@ pub enum Error {
PipelineArgumentError(ModifyImageArgError), PipelineArgumentError(ModifyImageArgError),
NoRandomFound, NoRandomFound,
GogurtError(GogurtError), GogurtError(GogurtError),
ImprovementError(ImprovementError),
} }
impl StdError for Error {} impl StdError for Error {}
@ -104,6 +106,12 @@ impl From<GogurtError> for Error {
} }
} }
impl From<ImprovementError> for Error {
fn from(value: ImprovementError) -> Self {
Self::ImprovementError(value)
}
}
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -125,6 +133,7 @@ impl Display for Error {
Error::PipelineArgumentError(err) => write!(f, "{err}"), Error::PipelineArgumentError(err) => write!(f, "{err}"),
Error::NoRandomFound => write!(f, "No random found"), Error::NoRandomFound => write!(f, "No random found"),
Error::GogurtError(err) => write!(f, "{err}"), Error::GogurtError(err) => write!(f, "{err}"),
Error::ImprovementError(err) => write!(f, "{err}"),
} }
} }
} }

View File

@ -1,4 +1,5 @@
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::discord::get_role;
use crate::error::Error; use crate::error::Error;
use crate::inventory::{ItemData, ItemType}; use crate::inventory::{ItemData, ItemType};
use crate::user::{User, UserRole}; use crate::user::{User, UserRole};
@ -12,7 +13,6 @@ use rand::{Rng, rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use crate::discord::get_role;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -245,10 +245,17 @@ impl Listener {
} }
Action::Ghoulify { hours } => { Action::Ghoulify { hours } => {
// slight hack to prevent ghoul spamming // slight hack to prevent ghoul spamming
let ghoul_role = get_role(&ctx.http, data.cfg.guild_id, &UserRole::Ghoul.to_string()).await?; let ghoul_role =
if let Ok(user) = data.cfg.guild_id.member(&ctx.http(), trigger_event.triggerer).await get_role(&ctx.http, data.cfg.guild_id, &UserRole::Ghoul.to_string())
&& let Some(ghoul_role) = ghoul_role .await?;
&& user.roles.contains(&ghoul_role) { if let Ok(user) = data
.cfg
.guild_id
.member(&ctx.http(), trigger_event.triggerer)
.await
&& let Some(ghoul_role) = ghoul_role
&& user.roles.contains(&ghoul_role)
{
break; break;
} }

View File

@ -1,6 +1,7 @@
use crate::error::Error; use crate::error::Error;
use crate::models::improvements::{ImprovementType, Improvements};
use crate::user::User; use crate::user::User;
use chrono::{DateTime, NaiveTime, TimeZone, Utc}; use chrono::{DateTime, Days, NaiveTime, TimeZone, Utc};
use j_db::database::Database; use j_db::database::Database;
use log::info; use log::info;
use poise::serenity_prelude::UserId; use poise::serenity_prelude::UserId;
@ -13,13 +14,13 @@ const OPENING_HOUR: u32 = 7;
const UPDATE_HOUR: u32 = 12; const UPDATE_HOUR: u32 = 12;
const CLOSING_HOUR: u32 = 18; const CLOSING_HOUR: u32 = 18;
const NIGHT_MARKET_CLOSE: u32 = 23;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum GogurtError { pub enum GogurtError {
#[error("Wow, you really don't have enough gogurt. What are you 12?? Just got buy some poor.")] #[error("Wow, you really don't have enough gogurt. What are you 12?? Just got buy some poor.")]
NotEnoughGogurt, NotEnoughGogurt,
#[error( #[error("Sorry, gogurt can only be bought and sold when the market is open!")]
"Sorry, gogurt can only be bought and sold between the hours of 7AM to 6PM Naperville Time."
)]
OutsideOfGogurtTradingHours, OutsideOfGogurtTradingHours,
} }
@ -52,27 +53,79 @@ impl GogurtReserves {
.unwrap_or(Self::default())) .unwrap_or(Self::default()))
} }
fn market_time(time: u32) -> NaiveTime {
NaiveTime::from_hms_opt(time, 0, 0).unwrap()
}
pub fn market_opening_time() -> NaiveTime { pub fn market_opening_time() -> NaiveTime {
NaiveTime::from_hms_opt(OPENING_HOUR, 0, 0).unwrap() Self::market_time(OPENING_HOUR)
} }
pub fn market_update_time() -> NaiveTime { pub fn market_update_time() -> NaiveTime {
NaiveTime::from_hms_opt(UPDATE_HOUR, 0, 0).unwrap() Self::market_time(UPDATE_HOUR)
} }
pub fn market_closing_time() -> NaiveTime { pub fn normal_market_closing_time() -> NaiveTime {
NaiveTime::from_hms_opt(CLOSING_HOUR, 0, 0).unwrap() Self::market_time(CLOSING_HOUR)
} }
pub fn check_if_in_trading_hours(time: &DateTime<Utc>) -> bool { pub fn night_market_closing_time() -> NaiveTime {
Self::market_time(NIGHT_MARKET_CLOSE)
}
pub fn market_closing_time(db: &Database) -> Result<NaiveTime, Error> {
let closing_time = if Self::has_night_market(db)? {
Self::night_market_closing_time()
} else {
Self::normal_market_closing_time()
};
Ok(closing_time)
}
pub fn has_night_market(db: &Database) -> Result<bool, Error> {
Ok(Improvements::get_improvement(db, ImprovementType::GogurtNightMarket)?.is_some())
}
pub fn get_next_market_update_time(db: &Database) -> Result<DateTime<Utc>, Error> {
let chicago_time = chrono_tz::America::Chicago.from_utc_datetime(&Utc::now().naive_utc());
let mut update_times = vec![Self::market_opening_time(), Self::market_update_time()];
if Self::has_night_market(db)? {
update_times.push(Self::normal_market_closing_time())
}
let last_update_of_the_day = *update_times.last().unwrap();
let next_check = if chicago_time.time() > last_update_of_the_day {
chicago_time
.with_time(update_times[0])
.unwrap()
.checked_add_days(Days::new(1))
.unwrap()
} else {
let time = update_times
.into_iter()
.find(|t| &chicago_time.time() < t)
.unwrap();
chicago_time.with_time(time).unwrap()
};
let next_check = next_check.with_timezone(&Utc);
Ok(next_check)
}
pub fn check_if_in_trading_hours(db: &Database, time: &DateTime<Utc>) -> Result<bool, Error> {
let chicago_time = chrono_tz::America::Chicago.from_utc_datetime(&time.naive_utc()); let chicago_time = chrono_tz::America::Chicago.from_utc_datetime(&time.naive_utc());
chicago_time.time() >= Self::market_opening_time() Ok(chicago_time.time() >= Self::market_opening_time()
&& chicago_time.time() < Self::market_closing_time() && chicago_time.time() < Self::market_closing_time(db)?)
} }
pub fn add_contribution(db: &Database, user: UserId, fc_amount: u64) -> Result<f64, Error> { pub fn add_contribution(db: &Database, user: UserId, fc_amount: u64) -> Result<f64, Error> {
if !Self::check_if_in_trading_hours(&Utc::now()) { if !Self::check_if_in_trading_hours(db, &Utc::now())? {
return Err(GogurtError::OutsideOfGogurtTradingHours.into()); return Err(GogurtError::OutsideOfGogurtTradingHours.into());
} }
@ -97,7 +150,7 @@ impl GogurtReserves {
user: UserId, user: UserId,
pounds_of_gogurt: f64, pounds_of_gogurt: f64,
) -> Result<u64, Error> { ) -> Result<u64, Error> {
if !Self::check_if_in_trading_hours(&Utc::now()) { if !Self::check_if_in_trading_hours(db, &Utc::now())? {
return Err(GogurtError::OutsideOfGogurtTradingHours.into()); return Err(GogurtError::OutsideOfGogurtTradingHours.into());
} }

140
src/models/improvements.rs Normal file
View File

@ -0,0 +1,140 @@
use crate::error::Error;
use j_db::database::Database;
use j_db::model::JdbModel;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::str::FromStr;
use strum::EnumIter;
#[derive(Debug)]
pub enum ImprovementError {
UnknownImprovement,
}
impl Display for ImprovementError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImprovementError::UnknownImprovement => write!(
f,
"I don't know what that improvement is, sounds lame and cringe!"
),
}
}
}
impl std::error::Error for ImprovementError {}
#[derive(Debug, Deserialize, Serialize, Clone, Hash, Eq, PartialEq, Copy, EnumIter)]
pub enum ImprovementType {
GogurtNightMarket,
}
impl ImprovementType {
pub fn description(&self) -> String {
match self {
ImprovementType::GogurtNightMarket => {
"Keep the Gogurt Market open until 11 PM NST".to_string()
}
}
}
pub fn price(&self) -> u64 {
match self {
ImprovementType::GogurtNightMarket => 200_000,
}
}
pub fn has_levels(&self) -> bool {
false
}
pub fn name(&self) -> String {
match self {
ImprovementType::GogurtNightMarket => "Gogurt Night Market".to_string(),
}
}
pub fn post_buy_description(&self) -> String {
match self {
ImprovementType::GogurtNightMarket => "Congrats on investing in the night market. The first day of night trading will start tomorrow!".to_string()
}
}
}
impl FromStr for ImprovementType {
type Err = ImprovementError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let name = s.to_lowercase();
match name.as_str() {
"gogurt night market" => Ok(Self::GogurtNightMarket),
_ => Err(ImprovementError::UnknownImprovement),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
pub struct Improvement {
pub level: u32,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Improvements {
id: Option<u64>,
pub improvements: HashMap<ImprovementType, Improvement>,
}
impl Improvements {
pub fn get_improvement_config(db: &Database) -> Result<Self, Error> {
Ok(db.filter(|_, _: &Self| true)?.next().unwrap_or(Self {
id: None,
improvements: HashMap::new(),
}))
}
pub fn get_improvement(
db: &Database,
improvement_type: ImprovementType,
) -> Result<Option<Improvement>, Error> {
let improvements = Self::get_improvement_config(db)?;
Ok(improvements.improvements.get(&improvement_type).cloned())
}
pub fn increment_improvement(
db: &Database,
improvement_type: ImprovementType,
) -> Result<u32, Error> {
let mut improvements = Self::get_improvement_config(db)?;
let entry = improvements
.improvements
.entry(improvement_type)
.or_insert(Improvement { level: 1 })
.clone();
db.insert(improvements)?;
Ok(entry.level)
}
}
impl JdbModel for Improvements {
fn id(&self) -> Option<u64> {
self.id
}
fn set_id(&mut self, id: u64) {
self.id = Some(id)
}
fn tree() -> String {
"Improvements".to_string()
}
fn check_unique(&self, _: &Self) -> bool {
false
}
}

View File

@ -1,6 +1,7 @@
pub mod api_key; pub mod api_key;
pub mod birthday; pub mod birthday;
pub mod gogurt_reserves; pub mod gogurt_reserves;
pub mod improvements;
pub mod insult_compliment; pub mod insult_compliment;
pub mod lil_fren; pub mod lil_fren;
pub mod managed_roles; pub mod managed_roles;

View File

@ -201,26 +201,11 @@ impl Task {
} }
TaskType::UpdateGogurtRate => { TaskType::UpdateGogurtRate => {
GogurtReserves::update_rate(&data.db)?; GogurtReserves::update_rate(&data.db)?;
Task::add_task(
let chicago_time = &data.db,
chrono_tz::America::Chicago.from_utc_datetime(&Utc::now().naive_utc()); TaskType::UpdateGogurtRate,
GogurtReserves::get_next_market_update_time(&data.db)?,
let market_open_time = GogurtReserves::market_opening_time(); )?;
let midday_update_time = GogurtReserves::market_update_time();
let next_check = if chicago_time.time() >= midday_update_time {
chicago_time
.with_time(market_open_time)
.unwrap()
.checked_add_days(Days::new(1))
.unwrap()
} else {
chicago_time.with_time(midday_update_time).unwrap()
};
let next_check = next_check.with_timezone(&Utc);
Task::add_task(&data.db, TaskType::UpdateGogurtRate, next_check)?;
} }
} }