416 lines
12 KiB
Rust
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;
|
|
});
|
|
}
|
|
}
|