use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; use crate::commands::Command; use crate::commands::calculate_fish_value; use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; use chrono::Duration as ChronoDuration; use chrono::Utc; use std::sync::Arc; use async_trait::async_trait; use rand::Rng; use rand::prelude::IndexedRandom; use sqlx::Pool; use sqlx::Postgres; #[derive(Clone, Copy, Debug)] pub enum FishRarity { Common, Uncommon, Rare, SuperRare, } pub struct FishType { pub name: &'static str, pub color_hex: &'static str, pub price: u32, pub rarity: FishRarity, } pub const FISH_TYPES: [FishType; 20] = [ FishType { name: "Salmon", color_hex: "#FA8072", price: 50, rarity: FishRarity::Common, }, FishType { name: "Trout", color_hex: "#A2B5CD", price: 40, rarity: FishRarity::Common, }, FishType { name: "Bass", color_hex: "#6B8E23", price: 45, rarity: FishRarity::Common, }, FishType { name: "Catfish", color_hex: "#B0C4DE", price: 60, rarity: FishRarity::Uncommon, }, FishType { name: "Carp", color_hex: "#C2B280", price: 35, rarity: FishRarity::Common, }, FishType { name: "Pike", color_hex: "#556B2F", price: 70, rarity: FishRarity::Uncommon, }, FishType { name: "Perch", color_hex: "#FFD700", price: 30, rarity: FishRarity::Common, }, FishType { name: "Sturgeon", color_hex: "#708090", price: 200, rarity: FishRarity::Rare, }, FishType { name: "Bluegill", color_hex: "#4682B4", price: 25, rarity: FishRarity::Common, }, FishType { name: "Crappie", color_hex: "#D3D3D3", price: 20, rarity: FishRarity::Common, }, FishType { name: "Swordfish", color_hex: "#191970", price: 500, rarity: FishRarity::SuperRare, }, FishType { name: "Marlin", color_hex: "#4169E1", price: 400, rarity: FishRarity::Rare, }, FishType { name: "Tuna", color_hex: "#4682B4", price: 350, rarity: FishRarity::Rare, }, FishType { name: "Mahi Mahi", color_hex: "#00CED1", price: 150, rarity: FishRarity::Uncommon, }, FishType { name: "Halibut", color_hex: "#F0E68C", price: 120, rarity: FishRarity::Uncommon, }, FishType { name: "Eel", color_hex: "#2F4F4F", price: 80, rarity: FishRarity::Uncommon, }, FishType { name: "Opah", color_hex: "#FF4500", price: 600, rarity: FishRarity::SuperRare, }, FishType { name: "Angelfish", color_hex: "#FFB6C1", price: 250, rarity: FishRarity::Rare, }, FishType { name: "Clownfish", color_hex: "#FFA500", price: 180, rarity: FishRarity::Uncommon, }, FishType { name: "Snapper", color_hex: "#FF6347", price: 90, rarity: FishRarity::Uncommon, }, ]; pub struct FishCommand { pool: Arc>, } impl FishCommand { pub fn new(pool: Arc>) -> Self { Self { pool } } } #[async_trait] impl Command for FishCommand { fn name(&self) -> &'static str { "fish" } fn category(&self) -> &'static str { "eco" } fn description(&self) -> &'static str { "Do some fishing. Try to catch one of many fish types! Rarities and prices included." } fn aliases(&self) -> &[&'static str] { &[] } fn argument_spec(&self) -> &'static [ArgumentSpec] { &[ArgumentSpec { name: "action", arg_type: ArgumentType::String, required: false, default: None, children: &[], }] } async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} async fn execute( &mut self, client: Client, player: Player, args: ParsedArguments, mut user: User, ) { let action = match args.get("action") { Some(ParsedArgument::String(s)) => s.as_str(), _ => "", }; if action == "sell" { let mut total_value: i32 = 0; let mut fish_count: i32 = 0; let mut sold_fish: Vec = Vec::new(); let mut new_items: Vec = Vec::new(); for item in user.items.iter() { if item.starts_with("fish-") { let parts: Vec<&str> = item.split('-').collect(); if parts.len() >= 3 { let fish_name = parts[1].replace('_', " "); let weight_str = parts[2]; let weight: f64 = weight_str.parse().unwrap_or(0.0); if let Some(fish_type) = FISH_TYPES .iter() .find(|f| f.name.to_lowercase() == fish_name.to_lowercase()) { let value = calculate_fish_value(weight, fish_type.price); total_value += value; fish_count += 1; sold_fish.push(format!( "{} ({:.2} kg, {} coins)", fish_type.name, weight, value )); } } } else { new_items.push(item.clone()); } } if fish_count == 0 { client .message("You have no fish to sell.".to_string()) .await; } else { user.items = new_items.into(); user.balance += total_value; let _ = sqlx::query("UPDATE users SET items = $1, balance = $2 WHERE _id = $3") .bind(&user.items) .bind(user.balance) .bind(&user._id) .execute(self.pool.as_ref()) .await; client .message(format!( "You sold {} fish for a total of {} coins! {}", fish_count, total_value, sold_fish.join(", ") )) .await; } return; } let cooldown_query_opt = sqlx::query_scalar::<_, Option>>( "SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2", ) .bind(&user._id) .bind("fish") .fetch_one(self.pool.as_ref()) .await; let now = Utc::now(); let cooldown_exists = cooldown_query_opt.is_ok(); if cooldown_exists && let Some(until) = cooldown_query_opt.unwrap() && until > now { let secs_left = (until - now).num_seconds(); client .message(format!( "⏳ You are tired from fishing! Please wait {} seconds before fishing again.", secs_left )) .await; return; } let mut rod_bonus = 0.0; let mut bait_bonus = 0.0; let mut bait_used = None; fn count_item(items: &[String], id: &str) -> usize { items.iter().filter(|i| i.starts_with(id)).count() } if user.items.iter().any(|i| i == "carbon_rod") { rod_bonus = 0.15; } else if user.items.iter().any(|i| i == "fiberglass_rod") { rod_bonus = 0.07; } else if user.items.iter().any(|i| i == "wooden_rod") { rod_bonus = 0.0; } if count_item(&user.items, "large_bait") > 0 { bait_bonus = 0.10; bait_used = Some("large_bait"); } else if count_item(&user.items, "small_bait") > 0 { bait_bonus = 0.04; bait_used = Some("small_bait"); } if let Some(bait_id) = bait_used && let Some(pos) = user.items.iter().position(|i| i.starts_with(bait_id)) { user.items.remove(pos); client .message(format!("You used one {}.", bait_id.replace('_', " "))) .await; } let common_fish = FISH_TYPES .iter() .filter(|f| matches!(f.rarity, FishRarity::Common)) .collect::>(); let uncommon_fish = FISH_TYPES .iter() .filter(|f| matches!(f.rarity, FishRarity::Uncommon)) .collect::>(); let rare_fish = FISH_TYPES .iter() .filter(|f| matches!(f.rarity, FishRarity::Rare)) .collect::>(); let super_rare_fish = FISH_TYPES .iter() .filter(|f| matches!(f.rarity, FishRarity::SuperRare)) .collect::>(); let mut common_chance = 60.0; let mut uncommon_chance = 25.0; let mut rare_chance = 13.0; let mut super_rare_chance = 2.0; let rare_bonus = rod_bonus + bait_bonus; rare_chance += rare_bonus * 100.0 * 0.5; super_rare_chance += rare_bonus * 100.0 * 0.5; let total = common_chance + uncommon_chance + rare_chance + super_rare_chance; common_chance *= 100.0 / total; uncommon_chance *= 100.0 / total; rare_chance *= 100.0 / total; let roll = rand::rng().random_range(0.0..100.0); let fish = if roll < common_chance { common_fish.choose(&mut rand::rng()).unwrap() } else if roll < common_chance + uncommon_chance { uncommon_fish.choose(&mut rand::rng()).unwrap() } else if roll < common_chance + uncommon_chance + rare_chance { rare_fish.choose(&mut rand::rng()).unwrap() } else { super_rare_fish.choose(&mut rand::rng()).unwrap() }; let timeout_secs = rand::rng().random_range(24..=48); let cooldown_until = now + ChronoDuration::seconds(timeout_secs); if cooldown_exists { let _ = sqlx::query("UPDATE cooldowns SET untill = $1 WHERE _id = $2 AND type = $3") .bind(cooldown_until) .bind(&user._id) .bind("fish") .execute(self.pool.as_ref()) .await; } else { let _ = sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)") .bind(&user._id) .bind("fish") .bind(cooldown_until) .execute(self.pool.as_ref()) .await; } let base_weight = rand::rng().random_range(0.1..=50.0); let weight = base_weight * (1.0 + rod_bonus + bait_bonus); let fish_id = format!( "fish-{}-{:.2}", fish.name.to_lowercase().replace(' ', "_"), weight ); user.items.push(fish_id.clone()); let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2") .bind(&user.items) .bind(&user._id) .execute(self.pool.as_ref()) .await; let value = calculate_fish_value(weight, fish.price); client .message(format!( "🎣 You caught a {}! Color: {} Weight: {:.2} kg", fish.name, fish.color_hex, weight )) .await; client .message(format!("Rarity: {:?} Value: {} coins", fish.rarity, value)) .await; let client_clone = client.clone(); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(timeout_secs as u64)).await; client_clone .message(format!("🎣 @{} can fish again now!", player._id)) .await; }); } }