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

View File

@ -1,5 +1,6 @@
use crate::discord::{Context, get_role};
use crate::error::Error;
use crate::event_listener::Listener;
use crate::inventory::ItemType;
use crate::models::api_key::Apikey;
use crate::models::lil_fren::{
@ -405,3 +406,48 @@ pub async fn op_give_money(
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)? {
info!(
"{} matched phrase '{}' for social credit checking",
@ -141,7 +139,7 @@ async fn handle_message(
new_message.author.id,
TriggerType::OnMessage {
channel_id: Some(new_message.channel_id),
content: new_message.content.clone(),
content: Some(new_message.content.clone()),
},
new_message.id,
new_message.channel_id,
@ -325,6 +323,8 @@ pub async fn run_bot(global_data: GlobalData) {
admin::reset_random_score(),
admin::remove_random_result(),
admin::op_give_money(),
admin::list_listeners(),
admin::remove_listener(),
album::add_image(),
album::list_albums(),
birthday::add_birthday(),
@ -372,7 +372,16 @@ pub async fn run_bot(global_data: GlobalData) {
shop::item_help(),
shop::sell_item(),
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(),
voices::list_voices(),
voices::list_words(),

View File

@ -1,13 +1,12 @@
use crate::config::GlobalData;
use crate::discord::Context;
use crate::error::Error;
use crate::event_listener::{Action, Expiration, Listener, TriggerType};
use crate::image_manipulation::create_motivation_image;
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, Target};
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::{Channel, CreateMessage, UserId};
use rand::{Rng, rng};
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
@ -91,48 +90,109 @@ pub async fn buy(
Ok(())
}
/// Use an item and hope it doesn't use you
#[poise::command(prefix_command, category = "Shop", guild_only, aliases("use"))]
pub async fn use_item(
#[poise::command(prefix_command, category = "Shop", guild_only)]
pub async fn cancel_insurance(ctx: Context<'_>) -> Result<(), Error> {
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<'_>,
#[description = "item to use"]
#[rest]
item: String,
#[description = "target to kill"] target: poise::serenity_prelude::User,
) -> Result<(), Error> {
if item.is_empty() {
ctx.reply("You need to select one item to use.").await?;
return Ok(());
use_item(ctx, ItemType::KillGun, Target::User(target.id)).await?;
Ok(())
}
let msg_content = item.to_lowercase();
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(());
#[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(())
}
};
let target_users: Vec<UserId> = match item.target() {
#[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?;
}
Ok(())
}
pub async fn use_item(ctx: Context<'_>, item: ItemType, target: Target) -> Result<bool, Error> {
let target_users: Vec<UserId> = match 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::User(user_id) => {
vec![user_id]
}
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) {
@ -143,7 +203,7 @@ pub async fn use_item(
ctx.reply("Looks like you don't have enough of that item to use it")
.await?;
}
return Ok(());
return Ok(false);
}
};
@ -159,7 +219,7 @@ pub async fn use_item(
}
}
Ok(())
Ok(true)
}
/// Sell an item for profit

View File

@ -18,7 +18,7 @@ use std::sync::Arc;
pub enum TriggerType {
OnMessage {
channel_id: Option<ChannelId>,
content: String,
content: Option<String>,
},
UseItem {
item: ItemType,
@ -43,10 +43,17 @@ impl TriggerType {
} else {
true
};
event_msg
.to_ascii_lowercase()
.contains(&trigger.to_ascii_lowercase())
&& match_channel
let match_msg = if let Some(trigger) = trigger {
let phrase = trigger.to_ascii_lowercase();
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 }) => {
trigger == event_item
@ -113,8 +120,8 @@ pub enum Action {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Expiration {
NumberOfTriggers(u16),
Time(chrono::DateTime<Utc>),
NumberOfTriggers { triggers: u16 },
Time { time: chrono::DateTime<Utc> },
Never,
}
@ -126,6 +133,7 @@ pub struct Listener {
pub trigger_chance: f64,
pub expiration: Expiration,
pub system_listener: bool,
pub trigger_count: u64,
}
impl Listener {
@ -143,6 +151,7 @@ impl Listener {
trigger_chance,
expiration,
system_listener,
trigger_count: 0,
}
}
@ -151,7 +160,7 @@ impl Listener {
}
pub async fn handle_action(
&self,
&mut self,
ctx: &Context,
data: &Arc<GlobalData>,
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(())
}
pub fn add_event(db: &Database, new_event: Self) -> Result<Self, Error> {
debug!("Adding listener: {new_event:?}");
Ok(db.insert(new_event)?)
pub fn add_listener(db: &Database, new_listener: Self) -> Result<Self, Error> {
debug!("Adding listener: {new_listener:?}");
Ok(db.insert(new_listener)?)
}
pub async fn process_trigger(
@ -246,11 +268,18 @@ impl Listener {
data: &Arc<GlobalData>,
trigger_event: TriggerEvent,
) -> 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)
})?;
for listener in triggered_listeners {
for mut listener in &mut triggered_listeners {
match listener.handle_action(ctx, data, &trigger_event).await {
Ok(_) => {
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(())
}
}
@ -275,6 +306,6 @@ impl j_db::model::JdbModel for Listener {
}
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 rand::prelude::IndexedRandom;
use rand::rng;
@ -62,13 +63,16 @@ pub enum ItemType {
Helmet,
#[name = "EMP"]
EMP,
#[name = "EMP"]
LandMine,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Target {
Myself,
User,
User(UserId),
Everyone,
Other,
}
impl ItemType {
@ -85,6 +89,7 @@ impl ItemType {
ItemType::CancelRay => "Used to cancel people. `!use cancel ray @Austin`".to_string(),
ItemType::Helmet => "Automatically used to block being killed".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 => {
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(),
}
}
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)]
@ -197,6 +191,8 @@ impl FromStr for ItemType {
Ok(ItemType::CancelRay)
} else if item.starts_with("emp") {
Ok(ItemType::EMP)
} else if item.starts_with("landmine") {
Ok(ItemType::LandMine)
} else {
Err(InventoryError::UnkownItem)
}
@ -215,6 +211,7 @@ impl Display for ItemType {
ItemType::Helmet => "Helmet".to_string(),
ItemType::CancelRay => "Cancel Ray".to_string(),
ItemType::EMP => "EMP".to_string(),
ItemType::LandMine => "Land Mine".to_string(),
};
write!(f, "{name}")
@ -250,6 +247,7 @@ impl InventorySlot {
ItemType::Helmet => 10_000,
ItemType::CancelRay => 2_000,
ItemType::EMP => 50_000,
ItemType::LandMine => 3_000,
}
}