Refactored item handling, expanded listener actions

This commit is contained in:
Joey Hines 2025-08-02 16:58:04 -06:00
parent 6aa8a44b3e
commit 0e588d9d62
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
6 changed files with 428 additions and 221 deletions

View File

@ -392,3 +392,16 @@ pub async fn remove_random_result(
Ok(()) Ok(())
} }
/// Give yourself money
#[poise::command(prefix_command, category = "Admin", check = "is_admin")]
pub async fn op_give_money(
ctx: Context<'_>,
#[description = "Amount of money to give to a user"] amount: u32,
) -> Result<(), Error> {
User::give_funds(&ctx.data().db, ctx.author().id, amount)?;
ctx.reply("Money machine go brrrrrrrrrrrrrrrrrr").await?;
Ok(())
}

View File

@ -324,6 +324,7 @@ pub async fn run_bot(global_data: GlobalData) {
admin::remove_movie(), admin::remove_movie(),
admin::reset_random_score(), admin::reset_random_score(),
admin::remove_random_result(), admin::remove_random_result(),
admin::op_give_money(),
album::add_image(), album::add_image(),
album::list_albums(), album::list_albums(),
birthday::add_birthday(), birthday::add_birthday(),

View File

@ -1,17 +1,13 @@
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::discord::{Context, get_role}; use crate::discord::Context;
use crate::error::Error; use crate::error::Error;
use crate::image_manipulation::create_motivation_image; use crate::image_manipulation::create_motivation_image;
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value}; use crate::inventory::{InventoryError, ItemData, ItemType, Operation, Target};
use crate::models::task::{Task, TaskType};
use crate::user::{User, UserError}; use crate::user::{User, UserError};
use poise::serenity_prelude::all::{ use poise::ChoiceParameter;
CreateAttachment, CreateMessage, EditRole, GuildId, Member, parse_user_mention, use poise::serenity_prelude::UserId;
}; use poise::serenity_prelude::all::parse_user_mention;
use poise::serenity_prelude::model::Colour;
use poise::serenity_prelude::prelude::Mentionable;
use poise::serenity_prelude::utils::MessageBuilder; use poise::serenity_prelude::utils::MessageBuilder;
use rand::prelude::IndexedRandom;
use rand::{Rng, rng}; use rand::{Rng, rng};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher; use std::hash::Hasher;
@ -95,62 +91,6 @@ pub async fn buy(
Ok(()) Ok(())
} }
async fn blockable_item(
ctx: Context<'_>,
guild_id: GuildId,
msg_content: &str,
role_name: &str,
weapon_name: &str,
block_item: ItemType,
) -> Result<(bool, Member), Error> {
let split = msg_content.split(weapon_name);
let target = parse_user_mention(split.last().unwrap()).unwrap();
let target_member = match guild_id.member(ctx, target).await {
Ok(member) => member,
Err(_) => {
return Err(Error::CommandError(
"I have no clue who that is tbh".to_string(),
));
}
};
if target_member.user.id == ctx.cache().current_user().id {
return Err(Error::CommandError(
"You can not harm me in a way that matters.".to_string(),
));
}
if User::try_use_item(&ctx.data().db, target, block_item, true).is_ok() {
Ok((false, target_member))
} else {
let role = if let Some(role) = get_role(ctx.http(), guild_id, role_name).await? {
role
} else {
guild_id
.create_role(
&ctx,
EditRole::new()
.name(role_name)
.colour(Colour::from_rgb(1, 1, 1)),
)
.await?
.id
};
target_member.add_role(&ctx, role).await?;
Task::add_task(
&ctx.data().db,
TaskType::RemoveRole {
role_id: role.get(),
user_id: target_member.user.id.get(),
},
chrono::Utc::now() + chrono::Duration::seconds(ctx.data().cfg.effect_role_duration),
)?;
Ok((true, target_member))
}
}
/// Use an item and hope it doesn't use you /// Use an item and hope it doesn't use you
#[poise::command(prefix_command, category = "Shop", guild_only, aliases("use"))] #[poise::command(prefix_command, category = "Shop", guild_only, aliases("use"))]
pub async fn use_item( pub async fn use_item(
@ -173,6 +113,28 @@ pub async fn use_item(
} }
}; };
let target_users: Vec<UserId> = match item.target() {
Target::Myself => {
vec![ctx.author().id]
}
Target::User => {
let item_name = format!("{} ", item.name().to_ascii_lowercase());
let split = msg_content.split(&item_name);
let target = parse_user_mention(split.last().unwrap()).unwrap();
let target_member = match ctx.guild_id().unwrap().member(ctx, target).await {
Ok(member) => member,
Err(_) => {
return Err(Error::CommandError(
"I have no clue who that is tbh".to_string(),
));
}
};
vec![target_member.user.id]
}
Target::Everyone => ctx.guild().unwrap().members.keys().copied().collect(),
};
let item_data = match User::try_use_item(&ctx.data().db, ctx.author().id, item, false) { let item_data = match User::try_use_item(&ctx.data().db, ctx.author().id, item, false) {
Ok(i) => i, Ok(i) => i,
Err(err) => { Err(err) => {
@ -185,141 +147,16 @@ pub async fn use_item(
} }
}; };
match item { for (ndx, target_user) in target_users.iter().enumerate() {
ItemType::CancelInsurance => { let outcome =
ctx.reply("You are immune to the next (1) cancelings.") User::use_item_on_user(ctx, *target_user, ctx.guild_id().unwrap(), item, &item_data)
.await?; .await?;
}
ItemType::TheConceptOfLove => {
ctx.reply("I have DMed you the concept of love.").await?;
ctx.author() if let Some(outcome) = outcome {
.id if ndx + 1 == target_users.len() {
.create_dm_channel(ctx) ctx.reply(outcome).await?;
.await?
.say(
ctx,
"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 rng()).unwrap();
ctx.reply(fortune.to_string()).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(_) => {
ctx.reply("Sorry this was a pump and dump").await?;
return Ok(());
}
};
let value = nft_value(&path);
ctx.channel_id()
.send_message(
ctx,
CreateMessage::new()
.content(format!(
"Your NFT my good friend. It's worth **{value} FC**!"
))
.add_file(
CreateAttachment::file(&file, "nft.jpg".to_string())
.await
.unwrap(),
),
)
.await?;
} else {
ctx.reply("Fren NFTs were never my creation, I merely promoted the brand")
.await?;
} }
} }
ItemType::LicenseToBeHorny => {
ctx.reply("https://media.discordapp.net/attachments/840015650286075945/1127022083919069184/Img_2022_10_21_05_08_12.jpg").await?;
}
ItemType::KillGun => {
let (outcome, target) = match blockable_item(
ctx,
ctx.guild_id().unwrap(),
&msg_content,
"Dead",
"kill gun ",
ItemType::Helmet,
)
.await
{
Ok(ret) => ret,
Err(err) => {
ctx.reply(err.to_string()).await?;
return Ok(());
}
};
if outcome {
ctx.reply(format!("You draw your trusty kill gun and shoot one kill bullet. It hits it mark between {}'s eyes, killing them instantly. They are now dead.", target.mention())).await?;
} else {
ctx.reply(format!("The kill bullet shoots at kill velocity toward {}! They smirk, and simply pull out their Helmet and put it on, the bullet bounces off and falls to the floor. The crowd gasps (like they do in my animes). \"No death today pal\"", target.mention())).await?;
}
}
ItemType::CancelRay => {
let (outcome, target) = match blockable_item(
ctx,
ctx.guild_id().unwrap(),
&msg_content,
"Cancelled",
"cancel ray ",
ItemType::CancelInsurance,
)
.await
{
Ok(ret) => ret,
Err(err) => {
ctx.reply(err.to_string()).await?;
return Ok(());
}
};
if outcome {
ctx.reply( format!("You shoot the cancel ray at {}. As the ray impacts them, you can hear their phone buzz. They have been cancelled, you hear the liberal media in the distance.", target.mention())).await?;
} else {
ctx.reply(format!("The ray nearly hits {}, but they are surrounded in a a shimmering blue energy shield. \"The liberal media won't strike this time!\"", target.mention())).await?;
}
}
ItemType::Helmet => {
ctx.reply("Your trusty helmet regards you helmetly").await?;
}
ItemType::EMP => {
let mut users: Vec<User> = ctx.data().db.filter(|_, _user: &User| true)?.collect();
for user in &mut users {
for item in &mut user.inventory.inventory {
if item.item_type == ItemType::KillGun
|| item.item_type == ItemType::CancelRay
|| item.item_type == ItemType::CancelInsurance
|| item.item_type == ItemType::Helmet
{
item.quantity = 0;
}
}
ctx.data().db.insert(user.clone())?;
}
ctx.reply("All technology is null and void, have fun suckers!")
.await?;
}
} }
Ok(()) Ok(())

View File

@ -1,10 +1,13 @@
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::error::Error; use crate::error::Error;
use crate::inventory::{ItemData, ItemType}; use crate::inventory::{ItemData, ItemType};
use chrono::Utc; use crate::user::{User, UserRole};
use chrono::{Duration, Utc};
use j_db::database::Database; use j_db::database::Database;
use log::{debug, error}; use log::{debug, error};
use poise::serenity_prelude::{ArgumentConvert, ChannelId, Context, Emoji, MessageId, UserId}; use poise::serenity_prelude::{
ArgumentConvert, CacheHttp, ChannelId, Context, Emoji, MessageId, UserId,
};
use rand::{Rng, rng}; use rand::{Rng, rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@ -80,14 +83,31 @@ impl TriggerEvent {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Action { pub enum Action {
Kill { hours: u16 }, Kill {
Cancel { hours: u16 }, hours: u16,
UpdateSocialCredit { score_diff: i64 }, },
UpdateFrenCoins { fren_coin_diff: i64 }, Cancel {
GiveItem { item: ItemType, data: ItemData }, hours: u16,
TakeItem { item: ItemType }, },
Speak { msg: String }, UpdateSocialCredit {
React { emoji: String }, score_diff: i64,
},
UpdateFrenCoins {
fren_coin_diff: i64,
},
GiveItem {
item: ItemType,
item_data: Option<ItemData>,
},
TakeItem {
item: ItemType,
},
Speak {
msg: String,
},
React {
emoji: String,
},
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -137,21 +157,78 @@ impl Listener {
trigger_event: &TriggerEvent, trigger_event: &TriggerEvent,
) -> Result<(), Error> { ) -> Result<(), Error> {
for action in &self.actions { for action in &self.actions {
if let Action::React { emoji } = action { match action {
let msg = ctx Action::React { emoji } => {
.http let msg = ctx
.get_message(trigger_event.channel_id, trigger_event.message_id) .http
.get_message(trigger_event.channel_id, trigger_event.message_id)
.await?;
if emoji.chars().count() == 1 {
msg.react(&ctx.http, emoji.chars().next().unwrap()).await?;
} else {
let emoji =
Emoji::convert(&ctx.http, None, Some(msg.channel_id), emoji.as_str())
.await
.unwrap();
msg.react(&ctx.http, emoji).await?;
}
}
Action::Kill { hours } => {
User::add_role(
ctx.http(),
&data.db,
trigger_event.triggerer,
data.cfg.guild_id,
UserRole::Dead,
Duration::hours(*hours as i64),
)
.await?; .await?;
}
if emoji.chars().count() == 1 { Action::Cancel { hours } => {
msg.react(&ctx.http, emoji.chars().next().unwrap()).await?; User::add_role(
} else { ctx.http(),
let emoji = &data.db,
Emoji::convert(&ctx.http, None, Some(msg.channel_id), emoji.as_str()) trigger_event.triggerer,
.await data.cfg.guild_id,
.unwrap(); UserRole::Cancelled,
Duration::hours(*hours as i64),
msg.react(&ctx.http, emoji).await?; )
.await?;
}
Action::UpdateSocialCredit { score_diff } => {
User::update_social_credit(&data.db, trigger_event.triggerer, *score_diff)?;
}
Action::UpdateFrenCoins { fren_coin_diff } => {
if *fren_coin_diff >= 0 {
User::give_funds(
&data.db,
trigger_event.triggerer,
*fren_coin_diff as u32,
)?;
} else {
User::take_funds(
&data.db,
trigger_event.triggerer,
*fren_coin_diff as u32,
)?;
}
}
Action::GiveItem { item, item_data } => {
User::give_item(
&data.db,
trigger_event.triggerer,
*item,
1,
item_data.clone(),
)?;
}
Action::TakeItem { item } => {
User::try_take_item(&data.db, trigger_event.triggerer, *item, 1)?;
}
Action::Speak { msg } => {
trigger_event.channel_id.say(ctx.http(), msg).await?;
} }
} }
} }

View File

@ -1,4 +1,6 @@
use poise::serenity_prelude::utils::MessageBuilder; use poise::serenity_prelude::utils::MessageBuilder;
use rand::prelude::IndexedRandom;
use rand::rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
@ -62,6 +64,13 @@ pub enum ItemType {
EMP, EMP,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Target {
Myself,
User,
Everyone,
}
impl ItemType { impl ItemType {
pub fn description(&self) -> String { pub fn description(&self) -> String {
match self { match self {
@ -78,6 +87,82 @@ impl ItemType {
ItemType::EMP => "Disables weapons and defenses".to_string(), ItemType::EMP => "Disables weapons and defenses".to_string(),
} }
} }
pub fn use_item_message(&self, target: &str, _item_data: &Option<ItemData>) -> Option<String> {
match self {
ItemType::CancelInsurance => {
Some("You are immune to the next (1) cancelings.".to_string())
}
ItemType::TheConceptOfLove => {
Some("I have DMed you the concept of love.".to_string())
}
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 rng()).unwrap();
Some(fortune.to_string())
}
ItemType::Nft => {
None
}
ItemType::LicenseToBeHorny => {
Some("https://media.discordapp.net/attachments/840015650286075945/1127022083919069184/Img_2022_10_21_05_08_12.jpg".to_string())
}
ItemType::KillGun => {
Some(format!("You draw your trusty kill gun and shoot one kill bullet. It hits it mark between {target}'s eyes, killing them instantly. They are now dead."))
}
ItemType::CancelRay => {
Some(format!("You shoot the cancel ray at {target}. As the ray impacts them, you can hear their phone buzz. They have been cancelled, you hear the liberal media in the distance."))
}
ItemType::Helmet => {
Some("Your trusty helmet regards you helmetly".to_string())
}
ItemType::EMP => {
Some("Humanity has forgotten a time before electricity. A time before all our weapons, toys, gadgets. We no longer have to try and remember that time. That time has returned".to_string())
}
}
}
pub fn block_message(&self, target: &str) -> String {
match self {
ItemType::CancelInsurance => format!(
"The cancel beam nearly hits {target}, but they are surrounded by a shimmering blue energy shield. \"The liberal media won't strike this time!\""
),
ItemType::KillGun => format!(
"The kill bullet shoots at kill velocity toward {target}! They smirk, and simply pull out their Helmet and put it on, the bullet bounces off and falls to the floor. The crowd gasps (like they do in my animes). \"No death today pal\""
),
_ => "How did you block that, wtf bro. Hacks! ".to_string(),
}
}
pub fn blocked_by(&self) -> Vec<ItemType> {
match self {
ItemType::KillGun => vec![ItemType::Helmet],
ItemType::CancelRay => vec![ItemType::CancelInsurance],
_ => Vec::new(),
}
}
pub fn target(&self) -> Target {
match self {
ItemType::CancelInsurance => Target::Myself,
ItemType::TheConceptOfLove => Target::Myself,
ItemType::GoodFortune => Target::Myself,
ItemType::Nft => Target::Myself,
ItemType::LicenseToBeHorny => Target::Myself,
ItemType::KillGun => Target::User,
ItemType::CancelRay => Target::User,
ItemType::Helmet => Target::Myself,
ItemType::EMP => Target::Everyone,
}
}
} }
#[derive(Debug, Clone, Hash, Eq, PartialEq, Copy)] #[derive(Debug, Clone, Hash, Eq, PartialEq, Copy)]
@ -293,4 +378,12 @@ impl InventoryManager {
Ok(item_slot.item_data.clone()) Ok(item_slot.item_data.clone())
} }
pub fn has_item(&self, item: ItemType) -> bool {
if let Some(slot) = self.get_item(item) {
slot.quantity > 0
} else {
false
}
}
} }

View File

@ -1,11 +1,47 @@
use crate::discord::{Context, get_role};
use crate::error::Error; use crate::error::Error;
use crate::inventory::{InventoryError, InventoryManager, ItemData, ItemType, Operation}; use crate::inventory::{
InventoryError, InventoryManager, ItemData, ItemType, Operation, nft_value,
};
use crate::models::task::{Task, TaskType};
use chrono::Duration;
use j_db::database::Database; use j_db::database::Database;
use j_db::model::JdbModel; use j_db::model::JdbModel;
use poise::serenity_prelude::model::id::UserId; use poise::serenity_prelude::model::id::UserId;
use poise::serenity_prelude::{
Color, CreateAttachment, CreateMessage, EditRole, GuildId, Http, Mentionable,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
#[derive(Debug, Copy, Clone)]
pub enum UserRole {
Cancelled,
Dead,
Ghoul,
}
impl UserRole {
pub fn color(&self) -> Color {
match self {
UserRole::Cancelled => Color::from_rgb(255, 0, 0),
UserRole::Dead => Color::from_rgb(0, 0, 0),
UserRole::Ghoul => Color::from_rgb(196, 244, 19),
}
}
}
impl Display for UserRole {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str = match self {
UserRole::Cancelled => "Cancelled".to_string(),
UserRole::Dead => "Dead".to_string(),
UserRole::Ghoul => "Ghoul".to_string(),
};
write!(f, "{str}")
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum UserError { pub enum UserError {
@ -212,6 +248,156 @@ impl User {
db.insert::<User>(user)?; db.insert::<User>(user)?;
Ok(()) Ok(())
} }
pub async fn add_role(
http: &Http,
db: &Database,
user: UserId,
guild_id: GuildId,
role: UserRole,
duration: Duration,
) -> Result<(), Error> {
let role = if let Some(role) = get_role(http, guild_id, &role.to_string()).await? {
role
} else {
guild_id
.create_role(
http,
EditRole::new().name(role.to_string()).colour(role.color()),
)
.await?
.id
};
let target_member = match guild_id.member(http, user).await {
Ok(member) => member,
Err(_) => {
return Err(Error::CommandError(
"I have no clue who that is tbh".to_string(),
));
}
};
target_member.add_role(http, role).await?;
Task::add_task(
db,
TaskType::RemoveRole {
role_id: role.get(),
user_id: target_member.user.id.get(),
},
chrono::Utc::now() + duration,
)?;
Ok(())
}
pub async fn use_item_on_user(
ctx: Context<'_>,
user_id: UserId,
guild_id: GuildId,
item: ItemType,
item_data: &Option<ItemData>,
) -> Result<Option<String>, Error> {
let mut user = Self::get_user(&ctx.data().db, user_id)?;
if user_id == ctx.cache().current_user().id {
return Ok(Some("can you feel your heart burning? can you feel the struggle within? the fear within me is beyond anything your soul can make. you cannot kill me in a way that matters".to_string()));
}
let block_by_list = item.blocked_by();
let block_item = block_by_list
.iter()
.find(|item| user.inventory.has_item(**item));
if let Some(block_item) = block_item {
User::try_take_item(&ctx.data().db, user_id, *block_item, 1)?;
Ok(Some(
block_item.block_message(&user_id.mention().to_string()),
))
} else {
match item {
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(_) => {
return Ok(Some("Sorry this was a pump and dump".to_string()));
}
};
let value = nft_value(path);
ctx.channel_id()
.send_message(
ctx,
CreateMessage::new()
.content(format!(
"Your NFT my good friend. It's worth **{value} FC**!"
))
.add_file(
CreateAttachment::file(&file, "nft.jpg".to_string())
.await
.unwrap(),
),
)
.await?;
} else {
return Ok(Some("Sorry this was a pump and dump".to_string()));
}
}
ItemType::KillGun => {
Self::add_role(
ctx.http(),
&ctx.data().db,
user_id,
guild_id,
UserRole::Dead,
Duration::seconds(ctx.data().cfg.effect_role_duration),
)
.await?;
}
ItemType::CancelRay => {
Self::add_role(
ctx.http(),
&ctx.data().db,
user_id,
guild_id,
UserRole::Cancelled,
Duration::seconds(ctx.data().cfg.effect_role_duration),
)
.await?;
}
ItemType::EMP => {
for item in &mut user.inventory.inventory {
if item.item_type == ItemType::KillGun
|| item.item_type == ItemType::CancelRay
|| item.item_type == ItemType::CancelInsurance
|| item.item_type == ItemType::Helmet
|| item.item_type == ItemType::LicenseToBeHorny
{
item.quantity = 0;
}
}
ctx.data().db.insert(user)?;
}
ItemType::TheConceptOfLove => {
user_id
.create_dm_channel(ctx)
.await?
.say(
ctx,
"DO NOT SHARE\nhttps://www.youtube.com/watch?v=HNy_retSME0",
)
.await?;
}
_ => {}
}
Ok(item.use_item_message(&user_id.mention().to_string(), item_data))
}
}
} }
impl JdbModel for User { impl JdbModel for User {