copper/src/commands/eco/fish.rs

416 lines
12 KiB
Rust

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<Pool<Postgres>>,
}
impl FishCommand {
pub fn new(pool: Arc<Pool<Postgres>>) -> 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<String> = Vec::new();
let mut new_items: Vec<String> = 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<chrono::DateTime<Utc>>>(
"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::<Vec<_>>();
let uncommon_fish = FISH_TYPES
.iter()
.filter(|f| matches!(f.rarity, FishRarity::Uncommon))
.collect::<Vec<_>>();
let rare_fish = FISH_TYPES
.iter()
.filter(|f| matches!(f.rarity, FishRarity::Rare))
.collect::<Vec<_>>();
let super_rare_fish = FISH_TYPES
.iter()
.filter(|f| matches!(f.rarity, FishRarity::SuperRare))
.collect::<Vec<_>>();
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;
});
}
}