Add land mine

This commit is contained in:
Joey Hines 2025-08-02 20:39:44 -06:00
parent 0e588d9d62
commit 1669a9cf8f
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
6 changed files with 233 additions and 72 deletions

View File

@ -83,6 +83,7 @@ pub struct GlobalData {
pub picox: AlbumManager, pub picox: AlbumManager,
pub cta: Mutex<CTAClient>, pub cta: Mutex<CTAClient>,
pub speak_lock: Mutex<()>, pub speak_lock: Mutex<()>,
pub listener_lock: Mutex<()>,
} }
impl GlobalData { impl GlobalData {
@ -93,12 +94,12 @@ impl GlobalData {
}); });
info!("Adding system listeners..."); info!("Adding system listeners...");
Listener::add_event( Listener::add_listener(
db, db,
Listener::new( Listener::new(
TriggerType::OnMessage { TriggerType::OnMessage {
channel_id: None, channel_id: None,
content: "bad bot".to_string(), content: Some("bad bot".to_string()),
}, },
vec![Action::React { vec![Action::React {
emoji: "😭".to_string(), emoji: "😭".to_string(),
@ -108,6 +109,21 @@ impl GlobalData {
true, true,
), ),
)?; )?;
Listener::add_listener(
db,
Listener::new(
TriggerType::OnMessage {
channel_id: None,
content: None,
},
vec![Action::UpdateFrenCoins {
fren_coin_diff: 100,
}],
0.05,
Expiration::Never,
true,
),
)?;
Ok(()) Ok(())
} }
@ -136,6 +152,7 @@ impl GlobalData {
picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token), picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token),
cta: Mutex::new(CTAClient::new(cfg.cta_key)), cta: Mutex::new(CTAClient::new(cfg.cta_key)),
speak_lock: Default::default(), speak_lock: Default::default(),
listener_lock: Default::default(),
}) })
} }
} }

View File

@ -1,5 +1,6 @@
use crate::discord::{Context, get_role}; use crate::discord::{Context, get_role};
use crate::error::Error; use crate::error::Error;
use crate::event_listener::Listener;
use crate::inventory::ItemType; use crate::inventory::ItemType;
use crate::models::api_key::Apikey; use crate::models::api_key::Apikey;
use crate::models::lil_fren::{ use crate::models::lil_fren::{
@ -405,3 +406,48 @@ pub async fn op_give_money(
Ok(()) Ok(())
} }
/// List all the listeners
#[poise::command(prefix_command, category = "Admin", check = "is_admin")]
pub async fn list_listeners(ctx: Context<'_>) -> Result<(), Error> {
let mut msg_builder = MessageBuilder::new();
msg_builder.push_line("The following listeners are active:");
for listener in ctx.data().db.filter(|_, _listener: &Listener| true)? {
msg_builder.push_line(format!(
"* id={} `{:?}`:",
listener.id().unwrap(),
listener.trigger
));
msg_builder.push_line(format!("\t* Trigger Count: `{:?}`", listener.trigger_count));
msg_builder.push_line(format!(
"\t* Trigger Chance: `{:?}`",
listener.trigger_chance
));
msg_builder.push_line(format!(
"\t* System Listener: `{}`",
listener.system_listener
));
msg_builder.push_line(format!("\t* Actions: `{:?}`", listener.actions));
}
ctx.reply(msg_builder.build()).await?;
Ok(())
}
/// Remove listener
#[poise::command(prefix_command, category = "Admin", check = "is_admin")]
pub async fn remove_listener(
ctx: Context<'_>,
#[description = "ID of the listener to remove"] listener_id: u64,
) -> Result<(), Error> {
let listener = ctx.data().db.remove::<Listener>(listener_id)?;
ctx.reply(format!(
"Removed listener with id={}",
listener.id().unwrap()
))
.await?;
Ok(())
}

View File

@ -123,8 +123,6 @@ async fn handle_message(
} }
}; };
give_coin(&data.db, new_message.author.id, 0.05, 10).await?;
if let Some(phrase) = SocialCreditPhrase::check_if_match(&data.db, &new_message.content)? { if let Some(phrase) = SocialCreditPhrase::check_if_match(&data.db, &new_message.content)? {
info!( info!(
"{} matched phrase '{}' for social credit checking", "{} matched phrase '{}' for social credit checking",
@ -141,7 +139,7 @@ async fn handle_message(
new_message.author.id, new_message.author.id,
TriggerType::OnMessage { TriggerType::OnMessage {
channel_id: Some(new_message.channel_id), channel_id: Some(new_message.channel_id),
content: new_message.content.clone(), content: Some(new_message.content.clone()),
}, },
new_message.id, new_message.id,
new_message.channel_id, new_message.channel_id,
@ -325,6 +323,8 @@ pub async fn run_bot(global_data: GlobalData) {
admin::reset_random_score(), admin::reset_random_score(),
admin::remove_random_result(), admin::remove_random_result(),
admin::op_give_money(), admin::op_give_money(),
admin::list_listeners(),
admin::remove_listener(),
album::add_image(), album::add_image(),
album::list_albums(), album::list_albums(),
birthday::add_birthday(), birthday::add_birthday(),
@ -372,7 +372,16 @@ pub async fn run_bot(global_data: GlobalData) {
shop::item_help(), shop::item_help(),
shop::sell_item(), shop::sell_item(),
shop::shop(), shop::shop(),
shop::use_item(), shop::cancel_insurance(),
shop::the_concept_of_love(),
shop::good_fortune(),
shop::nft(),
shop::license_to_be_horny(),
shop::kill_gun(),
shop::cancel_ray(),
shop::helmet(),
shop::emp(),
shop::land_mine(),
transit::cta_bets(), transit::cta_bets(),
voices::list_voices(), voices::list_voices(),
voices::list_words(), voices::list_words(),

View File

@ -1,13 +1,12 @@
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::discord::Context; use crate::discord::Context;
use crate::error::Error; use crate::error::Error;
use crate::event_listener::{Action, Expiration, Listener, TriggerType};
use crate::image_manipulation::create_motivation_image; use crate::image_manipulation::create_motivation_image;
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, Target}; use crate::inventory::{InventoryError, ItemData, ItemType, Operation, Target};
use crate::user::{User, UserError}; use crate::user::{User, UserError};
use poise::ChoiceParameter;
use poise::serenity_prelude::UserId;
use poise::serenity_prelude::all::parse_user_mention;
use poise::serenity_prelude::utils::MessageBuilder; use poise::serenity_prelude::utils::MessageBuilder;
use poise::serenity_prelude::{Channel, CreateMessage, UserId};
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;
@ -91,48 +90,109 @@ pub async fn buy(
Ok(()) Ok(())
} }
/// Use an item and hope it doesn't use you #[poise::command(prefix_command, category = "Shop", guild_only)]
#[poise::command(prefix_command, category = "Shop", guild_only, aliases("use"))] pub async fn cancel_insurance(ctx: Context<'_>) -> Result<(), Error> {
pub async fn use_item( use_item(ctx, ItemType::CancelInsurance, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn the_concept_of_love(ctx: Context<'_>) -> Result<(), Error> {
use_item(ctx, ItemType::TheConceptOfLove, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn good_fortune(ctx: Context<'_>, #[rest] _msg: String) -> Result<(), Error> {
use_item(ctx, ItemType::TheConceptOfLove, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn nft(ctx: Context<'_>) -> Result<(), Error> {
use_item(ctx, ItemType::Nft, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn license_to_be_horny(ctx: Context<'_>) -> Result<(), Error> {
use_item(ctx, ItemType::LicenseToBeHorny, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn kill_gun(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "item to use"] #[description = "target to kill"] target: poise::serenity_prelude::User,
#[rest]
item: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
if item.is_empty() { use_item(ctx, ItemType::KillGun, Target::User(target.id)).await?;
ctx.reply("You need to select one item to use.").await?; Ok(())
return Ok(()); }
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn cancel_ray(
ctx: Context<'_>,
#[description = "target to cancel"] target: poise::serenity_prelude::User,
) -> Result<(), Error> {
use_item(ctx, ItemType::CancelRay, Target::User(target.id)).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn helmet(ctx: Context<'_>) -> Result<(), Error> {
use_item(ctx, ItemType::Helmet, Target::Myself).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn emp(ctx: Context<'_>) -> Result<(), Error> {
use_item(ctx, ItemType::EMP, Target::Everyone).await?;
Ok(())
}
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn land_mine(
ctx: Context<'_>,
#[description = "Channel to land mine"] channel: Channel,
) -> Result<(), Error> {
let used_item = use_item(ctx, ItemType::LandMine, Target::Other).await?;
if used_item {
Listener::add_listener(&ctx.data().db, {
Listener::new(
TriggerType::OnMessage {
channel_id: Some(channel.id()),
content: None,
},
vec![Action::Kill { hours: ctx.data().cfg.effect_role_duration as u16}, Action::Speak {msg: "*click* **BOOM**, sorry kid you've run out of time. That was a landmine. You're dead.".to_string()}],
0.95,
Expiration::NumberOfTriggers { triggers: 1 },
false
)
})?;
ctx.author()
.id
.direct_message(
ctx,
CreateMessage::default().content("Bomb has been planted"),
)
.await?;
} }
let msg_content = item.to_lowercase(); Ok(())
let item = match msg_content.parse::<ItemType>() { }
Ok(i) => i,
Err(_) => {
ctx.reply("I don't know what the heck that is tbh.").await?;
return Ok(());
}
};
let target_users: Vec<UserId> = match item.target() { pub async fn use_item(ctx: Context<'_>, item: ItemType, target: Target) -> Result<bool, Error> {
let target_users: Vec<UserId> = match target {
Target::Myself => { Target::Myself => {
vec![ctx.author().id] vec![ctx.author().id]
} }
Target::User => { Target::User(user_id) => {
let item_name = format!("{} ", item.name().to_ascii_lowercase()); vec![user_id]
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(), Target::Everyone => ctx.guild().unwrap().members.keys().copied().collect(),
Target::Other => vec![],
}; };
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) {
@ -143,7 +203,7 @@ pub async fn use_item(
ctx.reply("Looks like you don't have enough of that item to use it") ctx.reply("Looks like you don't have enough of that item to use it")
.await?; .await?;
} }
return Ok(()); return Ok(false);
} }
}; };
@ -159,7 +219,7 @@ pub async fn use_item(
} }
} }
Ok(()) Ok(true)
} }
/// Sell an item for profit /// Sell an item for profit

View File

@ -18,7 +18,7 @@ use std::sync::Arc;
pub enum TriggerType { pub enum TriggerType {
OnMessage { OnMessage {
channel_id: Option<ChannelId>, channel_id: Option<ChannelId>,
content: String, content: Option<String>,
}, },
UseItem { UseItem {
item: ItemType, item: ItemType,
@ -43,10 +43,17 @@ impl TriggerType {
} else { } else {
true true
}; };
event_msg
.to_ascii_lowercase() let match_msg = if let Some(trigger) = trigger {
.contains(&trigger.to_ascii_lowercase()) let phrase = trigger.to_ascii_lowercase();
&& match_channel let event_msg = event_msg.as_deref().unwrap().to_ascii_lowercase();
event_msg.contains(&phrase)
} else {
true
};
match_channel && match_msg
} }
(TriggerType::UseItem { item: trigger }, TriggerType::UseItem { item: event_item }) => { (TriggerType::UseItem { item: trigger }, TriggerType::UseItem { item: event_item }) => {
trigger == event_item trigger == event_item
@ -113,8 +120,8 @@ pub enum Action {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Expiration { pub enum Expiration {
NumberOfTriggers(u16), NumberOfTriggers { triggers: u16 },
Time(chrono::DateTime<Utc>), Time { time: chrono::DateTime<Utc> },
Never, Never,
} }
@ -126,6 +133,7 @@ pub struct Listener {
pub trigger_chance: f64, pub trigger_chance: f64,
pub expiration: Expiration, pub expiration: Expiration,
pub system_listener: bool, pub system_listener: bool,
pub trigger_count: u64,
} }
impl Listener { impl Listener {
@ -143,6 +151,7 @@ impl Listener {
trigger_chance, trigger_chance,
expiration, expiration,
system_listener, system_listener,
trigger_count: 0,
} }
} }
@ -151,7 +160,7 @@ impl Listener {
} }
pub async fn handle_action( pub async fn handle_action(
&self, &mut self,
ctx: &Context, ctx: &Context,
data: &Arc<GlobalData>, data: &Arc<GlobalData>,
trigger_event: &TriggerEvent, trigger_event: &TriggerEvent,
@ -233,12 +242,25 @@ impl Listener {
} }
} }
self.trigger_count += 1;
let expire = match self.expiration {
Expiration::NumberOfTriggers { triggers } => self.trigger_count >= triggers as u64,
Expiration::Time { time } => time > Utc::now(),
Expiration::Never => false,
};
if expire {
data.db.remove::<Listener>(self.id.unwrap())?;
} else {
data.db.insert(self.clone())?;
}
Ok(()) Ok(())
} }
pub fn add_event(db: &Database, new_event: Self) -> Result<Self, Error> { pub fn add_listener(db: &Database, new_listener: Self) -> Result<Self, Error> {
debug!("Adding listener: {new_event:?}"); debug!("Adding listener: {new_listener:?}");
Ok(db.insert(new_event)?) Ok(db.insert(new_listener)?)
} }
pub async fn process_trigger( pub async fn process_trigger(
@ -246,11 +268,18 @@ impl Listener {
data: &Arc<GlobalData>, data: &Arc<GlobalData>,
trigger_event: TriggerEvent, trigger_event: TriggerEvent,
) -> Result<(), Error> { ) -> Result<(), Error> {
let triggered_listeners = data.db.filter(|_, listener: &Listener| { // Ignore the bot for triggers
if trigger_event.triggerer == ctx.cache.current_user().id {
return Ok(());
}
let _ = data.listener_lock.lock().await;
let mut triggered_listeners = data.db.filter(|_, listener: &Listener| {
listener.check_if_triggers(&trigger_event.trigger_type) listener.check_if_triggers(&trigger_event.trigger_type)
})?; })?;
for listener in triggered_listeners { for mut listener in &mut triggered_listeners {
match listener.handle_action(ctx, data, &trigger_event).await { match listener.handle_action(ctx, data, &trigger_event).await {
Ok(_) => { Ok(_) => {
debug!("Processed event: {listener:?}"); debug!("Processed event: {listener:?}");
@ -261,6 +290,8 @@ impl Listener {
} }
} }
// Ensure the DB is synced to prevent re-triggering of events
data.db.db.flush_async().await.unwrap();
Ok(()) Ok(())
} }
} }
@ -275,6 +306,6 @@ impl j_db::model::JdbModel for Listener {
} }
fn tree() -> String { fn tree() -> String {
"Events".to_string() "Listener".to_string()
} }
} }

View File

@ -1,3 +1,4 @@
use poise::serenity_prelude::UserId;
use poise::serenity_prelude::utils::MessageBuilder; use poise::serenity_prelude::utils::MessageBuilder;
use rand::prelude::IndexedRandom; use rand::prelude::IndexedRandom;
use rand::rng; use rand::rng;
@ -62,13 +63,16 @@ pub enum ItemType {
Helmet, Helmet,
#[name = "EMP"] #[name = "EMP"]
EMP, EMP,
#[name = "EMP"]
LandMine,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Target { pub enum Target {
Myself, Myself,
User, User(UserId),
Everyone, Everyone,
Other,
} }
impl ItemType { impl ItemType {
@ -85,6 +89,7 @@ impl ItemType {
ItemType::CancelRay => "Used to cancel people. `!use cancel ray @Austin`".to_string(), ItemType::CancelRay => "Used to cancel people. `!use cancel ray @Austin`".to_string(),
ItemType::Helmet => "Automatically used to block being killed".to_string(), ItemType::Helmet => "Automatically used to block being killed".to_string(),
ItemType::EMP => "Disables weapons and defenses".to_string(), ItemType::EMP => "Disables weapons and defenses".to_string(),
ItemType::LandMine => "A land mine you can setup for someone to step on".to_string(),
} }
} }
@ -127,6 +132,9 @@ impl ItemType {
ItemType::EMP => { 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()) 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())
} }
ItemType::LandMine => {
None
}
} }
} }
@ -149,20 +157,6 @@ impl ItemType {
_ => Vec::new(), _ => 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)]
@ -197,6 +191,8 @@ impl FromStr for ItemType {
Ok(ItemType::CancelRay) Ok(ItemType::CancelRay)
} else if item.starts_with("emp") { } else if item.starts_with("emp") {
Ok(ItemType::EMP) Ok(ItemType::EMP)
} else if item.starts_with("landmine") {
Ok(ItemType::LandMine)
} else { } else {
Err(InventoryError::UnkownItem) Err(InventoryError::UnkownItem)
} }
@ -215,6 +211,7 @@ impl Display for ItemType {
ItemType::Helmet => "Helmet".to_string(), ItemType::Helmet => "Helmet".to_string(),
ItemType::CancelRay => "Cancel Ray".to_string(), ItemType::CancelRay => "Cancel Ray".to_string(),
ItemType::EMP => "EMP".to_string(), ItemType::EMP => "EMP".to_string(),
ItemType::LandMine => "Land Mine".to_string(),
}; };
write!(f, "{name}") write!(f, "{name}")
@ -250,6 +247,7 @@ impl InventorySlot {
ItemType::Helmet => 10_000, ItemType::Helmet => 10_000,
ItemType::CancelRay => 2_000, ItemType::CancelRay => 2_000,
ItemType::EMP => 50_000, ItemType::EMP => 50_000,
ItemType::LandMine => 3_000,
} }
} }