first commit
This commit is contained in:
commit
0cb536b42b
38 changed files with 9044 additions and 0 deletions
72
src/commands/eco/balance.rs
Normal file
72
src/commands/eco/balance.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use crate::User;
|
||||
use crate::client::Client;
|
||||
use crate::client::ClientEvent;
|
||||
use crate::client::Player;
|
||||
use crate::commands::Command;
|
||||
|
||||
use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sqlx::Pool;
|
||||
use sqlx::Postgres;
|
||||
|
||||
pub struct BalanceCommand {
|
||||
pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
impl BalanceCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Command for BalanceCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"balance"
|
||||
}
|
||||
fn aliases(&self) -> &[&'static str] {
|
||||
&["bal"]
|
||||
}
|
||||
|
||||
fn category(&self) -> &'static str {
|
||||
"eco"
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"View your balance."
|
||||
}
|
||||
fn argument_spec(&self) -> &'static [ArgumentSpec] {
|
||||
&[ArgumentSpec {
|
||||
name: "user_id",
|
||||
arg_type: ArgumentType::String,
|
||||
required: false,
|
||||
default: None,
|
||||
}]
|
||||
}
|
||||
async fn constructed(&mut self, _: Client) {}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let id = match args.get("user_id") {
|
||||
Some(ParsedArgument::String(s)) => s.as_str(),
|
||||
_ => player._id.as_str(),
|
||||
};
|
||||
|
||||
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match user {
|
||||
Some(user) => {
|
||||
client
|
||||
.message(format!("You have {} coins.", user.balance))
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
client.message("No user found.").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/commands/eco/cf.rs
Normal file
143
src/commands/eco/cf.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use crate::User;
|
||||
use crate::client::{Client, ClientEvent, Player};
|
||||
use crate::commands::Command;
|
||||
|
||||
use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments};
|
||||
use async_trait::async_trait;
|
||||
use rand::Rng;
|
||||
use sqlx::Pool;
|
||||
use sqlx::Postgres;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
pub struct CoinflipCommand {
|
||||
pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
impl CoinflipCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Command for CoinflipCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"coinflip"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &[&'static str] {
|
||||
&["cf"]
|
||||
}
|
||||
|
||||
fn category(&self) -> &'static str {
|
||||
"eco"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Flip a coin 50/50, optionally bet some coins."
|
||||
}
|
||||
|
||||
async fn constructed(&mut self, _: Client) {}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
|
||||
fn argument_spec(&self) -> &'static [ArgumentSpec] {
|
||||
&[ArgumentSpec {
|
||||
name: "bet_amount",
|
||||
arg_type: ArgumentType::Integer,
|
||||
required: false,
|
||||
default: None,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let bet_amount: Option<i32> = match args.get("bet_amount") {
|
||||
Some(ParsedArgument::Integer(i)) => Some(*i as i32),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let user_opt = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(&player._id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut user = match user_opt {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
client.message("No user found.").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let choice = if rand::rng().random_bool(0.5) {
|
||||
"Heads"
|
||||
} else {
|
||||
"Tails"
|
||||
};
|
||||
|
||||
if let Some(amount) = bet_amount {
|
||||
if user.balance < amount {
|
||||
client
|
||||
.message(format!(
|
||||
"You don't have enough coins to bet {} coins. You have {}.",
|
||||
amount, user.balance
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
client
|
||||
.message(format!(
|
||||
"Coinflipping for {} coins.. I choose {}.",
|
||||
amount, choice
|
||||
))
|
||||
.await;
|
||||
|
||||
sleep(Duration::from_millis(1750)).await;
|
||||
|
||||
let flip = if rand::rng().random_bool(0.5) {
|
||||
"Heads"
|
||||
} else {
|
||||
"Tails"
|
||||
};
|
||||
|
||||
if flip == choice {
|
||||
let return_coins = (amount as f64 * 1.75).round() as i32;
|
||||
user.balance += return_coins;
|
||||
let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2")
|
||||
.bind(user.balance)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
client
|
||||
.message(format!("{}! You got {} coins.", flip, return_coins))
|
||||
.await;
|
||||
} else {
|
||||
user.balance -= amount;
|
||||
let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2")
|
||||
.bind(user.balance)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
client
|
||||
.message(format!("{}! You lost {} coins.", flip, amount))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.message(format!("Flipping a coin.. I choose {}.", choice))
|
||||
.await;
|
||||
sleep(Duration::from_millis(1750)).await;
|
||||
|
||||
let flip = if rand::rng().random_bool(0.5) {
|
||||
"Heads"
|
||||
} else {
|
||||
"Tails"
|
||||
};
|
||||
let result = if flip == choice { "won" } else { "lost" };
|
||||
|
||||
client.message(format!("{}! I {}.", flip, result)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
704
src/commands/eco/farm.rs
Normal file
704
src/commands/eco/farm.rs
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
use crate::User;
|
||||
use crate::client::Client;
|
||||
use crate::client::ClientEvent;
|
||||
use crate::client::Player;
|
||||
use crate::commands::Command;
|
||||
|
||||
use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments};
|
||||
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;
|
||||
use sqlx::Row;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum FruitQuality {
|
||||
OneStar = 1,
|
||||
TwoStar = 2,
|
||||
ThreeStar = 3,
|
||||
FourStar = 4,
|
||||
FiveStar = 5,
|
||||
}
|
||||
|
||||
impl FruitQuality {
|
||||
pub fn stars(&self) -> u8 {
|
||||
*self as u8
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
FruitQuality::OneStar => "⭐",
|
||||
FruitQuality::TwoStar => "⭐⭐",
|
||||
FruitQuality::ThreeStar => "⭐⭐⭐",
|
||||
FruitQuality::FourStar => "⭐⭐⭐⭐",
|
||||
FruitQuality::FiveStar => "⭐⭐⭐⭐⭐",
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct FruitType {
|
||||
pub name: &'static str,
|
||||
pub color_hex: &'static str,
|
||||
pub price: u32,
|
||||
}
|
||||
|
||||
pub struct CropType {
|
||||
pub name: &'static str,
|
||||
pub color_hex: &'static str,
|
||||
pub price: u32,
|
||||
pub grow_time_minutes: u32,
|
||||
pub fruits: &'static [FruitType],
|
||||
pub base_fruit_count: u32,
|
||||
}
|
||||
|
||||
pub const FRUIT_TYPES: [FruitType; 10] = [
|
||||
FruitType {
|
||||
name: "Beetroot",
|
||||
color_hex: "#8B0000",
|
||||
price: 15,
|
||||
},
|
||||
FruitType {
|
||||
name: "Carrot",
|
||||
color_hex: "#FF8C00",
|
||||
price: 12,
|
||||
},
|
||||
FruitType {
|
||||
name: "Potato",
|
||||
color_hex: "#D2691E",
|
||||
price: 8,
|
||||
},
|
||||
FruitType {
|
||||
name: "Tomato",
|
||||
color_hex: "#FF6347",
|
||||
price: 20,
|
||||
},
|
||||
FruitType {
|
||||
name: "Lettuce",
|
||||
color_hex: "#90EE90",
|
||||
price: 18,
|
||||
},
|
||||
FruitType {
|
||||
name: "Cucumber",
|
||||
color_hex: "#32CD32",
|
||||
price: 16,
|
||||
},
|
||||
FruitType {
|
||||
name: "Apple",
|
||||
color_hex: "#FF0000",
|
||||
price: 25,
|
||||
},
|
||||
FruitType {
|
||||
name: "Pear",
|
||||
color_hex: "#FFFF00",
|
||||
price: 22,
|
||||
},
|
||||
FruitType {
|
||||
name: "Grape",
|
||||
color_hex: "#8B008B",
|
||||
price: 30,
|
||||
},
|
||||
FruitType {
|
||||
name: "Wheat",
|
||||
color_hex: "#F5DEB3",
|
||||
price: 10,
|
||||
},
|
||||
];
|
||||
|
||||
static ROOT_FRUITS: [FruitType; 3] = [
|
||||
FruitType {
|
||||
name: "Beetroot",
|
||||
color_hex: "#8B0000",
|
||||
price: 15,
|
||||
},
|
||||
FruitType {
|
||||
name: "Carrot",
|
||||
color_hex: "#FF8C00",
|
||||
price: 12,
|
||||
},
|
||||
FruitType {
|
||||
name: "Potato",
|
||||
color_hex: "#D2691E",
|
||||
price: 8,
|
||||
},
|
||||
];
|
||||
|
||||
static GARDEN_FRUITS: [FruitType; 3] = [
|
||||
FruitType {
|
||||
name: "Tomato",
|
||||
color_hex: "#FF6347",
|
||||
price: 20,
|
||||
},
|
||||
FruitType {
|
||||
name: "Lettuce",
|
||||
color_hex: "#90EE90",
|
||||
price: 18,
|
||||
},
|
||||
FruitType {
|
||||
name: "Cucumber",
|
||||
color_hex: "#32CD32",
|
||||
price: 16,
|
||||
},
|
||||
];
|
||||
|
||||
static TREE_FRUITS: [FruitType; 3] = [
|
||||
FruitType {
|
||||
name: "Apple",
|
||||
color_hex: "#FF0000",
|
||||
price: 25,
|
||||
},
|
||||
FruitType {
|
||||
name: "Pear",
|
||||
color_hex: "#FFFF00",
|
||||
price: 22,
|
||||
},
|
||||
FruitType {
|
||||
name: "Grape",
|
||||
color_hex: "#8B008B",
|
||||
price: 30,
|
||||
},
|
||||
];
|
||||
|
||||
static GRAIN_FRUITS: [FruitType; 1] = [FruitType {
|
||||
name: "Wheat",
|
||||
color_hex: "#F5DEB3",
|
||||
price: 10,
|
||||
}];
|
||||
|
||||
pub const CROP_TYPES: [CropType; 4] = [
|
||||
CropType {
|
||||
name: "Root Vegetables",
|
||||
color_hex: "#8B4513",
|
||||
price: 50,
|
||||
grow_time_minutes: 8,
|
||||
fruits: &ROOT_FRUITS,
|
||||
base_fruit_count: 3,
|
||||
},
|
||||
CropType {
|
||||
name: "Garden Vegetables",
|
||||
color_hex: "#228B22",
|
||||
price: 75,
|
||||
grow_time_minutes: 12,
|
||||
fruits: &GARDEN_FRUITS,
|
||||
base_fruit_count: 2,
|
||||
},
|
||||
CropType {
|
||||
name: "Fruit Trees",
|
||||
color_hex: "#FF69B4",
|
||||
price: 150,
|
||||
grow_time_minutes: 15,
|
||||
fruits: &TREE_FRUITS,
|
||||
base_fruit_count: 4,
|
||||
},
|
||||
CropType {
|
||||
name: "Grain Crops",
|
||||
color_hex: "#DAA520",
|
||||
price: 30,
|
||||
grow_time_minutes: 5,
|
||||
fruits: &GRAIN_FRUITS,
|
||||
base_fruit_count: 8,
|
||||
},
|
||||
];
|
||||
|
||||
pub struct FarmCommand {
|
||||
pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
impl FarmCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
fn get_hoe_bonus(&self, items: &[String]) -> (f64, f64) {
|
||||
if items.iter().any(|i| i == "diamond_hoe") {
|
||||
(0.25, 0.30)
|
||||
} else if items.iter().any(|i| i == "iron_hoe") {
|
||||
(0.15, 0.20)
|
||||
} else if items.iter().any(|i| i == "stone_hoe") {
|
||||
(0.08, 0.10)
|
||||
} else if items.iter().any(|i| i == "wooden_hoe") {
|
||||
(0.03, 0.05)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_fruit_quality(&self, quality_bonus: f64) -> FruitQuality {
|
||||
let base_roll = rand::rng().random_range(0.0..100.0);
|
||||
let adjusted_roll = base_roll + (quality_bonus * 100.0);
|
||||
|
||||
if adjusted_roll >= 95.0 {
|
||||
FruitQuality::FiveStar
|
||||
} else if adjusted_roll >= 80.0 {
|
||||
FruitQuality::FourStar
|
||||
} else if adjusted_roll >= 60.0 {
|
||||
FruitQuality::ThreeStar
|
||||
} else if adjusted_roll >= 35.0 {
|
||||
FruitQuality::TwoStar
|
||||
} else {
|
||||
FruitQuality::OneStar
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fruit_value(fruit_type: &FruitType, quality: u8, count: u32) -> i32 {
|
||||
let base_value = fruit_type.price * count;
|
||||
let quality_multiplier = quality as f32 * 0.5 + 0.5;
|
||||
(base_value as f32 * quality_multiplier) as i32
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Command for FarmCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"farm"
|
||||
}
|
||||
|
||||
fn category(&self) -> &'static str {
|
||||
"eco"
|
||||
}
|
||||
fn aliases(&self) -> &[&'static str] {
|
||||
&[]
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"Plant crops and harvest fruits! Use 'let farm sell' to sell your fruits, or 'let farm harvest' to collect ready crops."
|
||||
}
|
||||
fn argument_spec(&self) -> &'static [ArgumentSpec] {
|
||||
&[ArgumentSpec {
|
||||
name: "action",
|
||||
arg_type: ArgumentType::Enum(&["sell", "all", "harvest"]),
|
||||
required: false,
|
||||
default: None,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn constructed(&mut self, client: Client) {
|
||||
let notification_client = client.clone();
|
||||
let notification_pool = self.pool.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
let ready_crops = sqlx::query("SELECT DISTINCT _id, type FROM cooldowns WHERE type LIKE 'crop-%' AND untill <= $1")
|
||||
.bind(Utc::now())
|
||||
.fetch_all(notification_pool.as_ref())
|
||||
.await;
|
||||
|
||||
if let Ok(crops) = ready_crops {
|
||||
for crop in crops {
|
||||
let user_id: String = crop.get("_id");
|
||||
let crop_type_name: String = crop.get("type");
|
||||
if let Some(crop_name) = crop_type_name.strip_prefix("crop-")
|
||||
&& let Ok(Some(mut user)) =
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(&user_id)
|
||||
.fetch_optional(notification_pool.as_ref())
|
||||
.await
|
||||
&& let Some(crop_type) = CROP_TYPES
|
||||
.iter()
|
||||
.find(|c| c.name.to_lowercase().replace(' ', "_") == crop_name)
|
||||
{
|
||||
let (quality_bonus, count_bonus) = {
|
||||
if user.items.iter().any(|i| i == "diamond_hoe") {
|
||||
(0.25, 0.30)
|
||||
} else if user.items.iter().any(|i| i == "iron_hoe") {
|
||||
(0.15, 0.20)
|
||||
} else if user.items.iter().any(|i| i == "stone_hoe") {
|
||||
(0.08, 0.10)
|
||||
} else if user.items.iter().any(|i| i == "wooden_hoe") {
|
||||
(0.03, 0.05)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
};
|
||||
|
||||
let base_roll = rand::rng().random_range(0.0..100.0);
|
||||
let adjusted_roll = base_roll + (quality_bonus * 100.0);
|
||||
let quality = if adjusted_roll >= 95.0 {
|
||||
5
|
||||
} else if adjusted_roll >= 80.0 {
|
||||
4
|
||||
} else if adjusted_roll >= 60.0 {
|
||||
3
|
||||
} else if adjusted_roll >= 35.0 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let stars = match quality {
|
||||
1 => "⭐",
|
||||
2 => "⭐⭐",
|
||||
3 => "⭐⭐⭐",
|
||||
4 => "⭐⭐⭐⭐",
|
||||
5 => "⭐⭐⭐⭐⭐",
|
||||
_ => "⭐",
|
||||
};
|
||||
|
||||
let base_count = crop_type.base_fruit_count;
|
||||
let bonus_count = (base_count as f64 * count_bonus) as u32;
|
||||
let total_count =
|
||||
base_count + bonus_count + rand::rng().random_range(1..=5);
|
||||
|
||||
let fruit = crop_type.fruits.choose(&mut rand::rng()).unwrap();
|
||||
let fruit_id = format!(
|
||||
"fruit-{}-{}-{}",
|
||||
fruit.name.to_lowercase().replace(' ', "_"),
|
||||
quality,
|
||||
total_count
|
||||
);
|
||||
|
||||
user.items.push(fruit_id);
|
||||
|
||||
let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2")
|
||||
.bind(&user.items)
|
||||
.bind(&user._id)
|
||||
.execute(notification_pool.as_ref())
|
||||
.await;
|
||||
|
||||
let _ =
|
||||
sqlx::query("DELETE FROM cooldowns WHERE _id = $1 AND type = $2")
|
||||
.bind(&user_id)
|
||||
.bind(&crop_type_name)
|
||||
.execute(notification_pool.as_ref())
|
||||
.await;
|
||||
|
||||
notification_client
|
||||
.message(format!(
|
||||
"🌾 Hey, @{} your {} is ready! You got {} {} x{}",
|
||||
user_id, crop_type.name, stars, fruit.name, total_count
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(&player._id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let command = match args.get("action") {
|
||||
Some(ParsedArgument::Enum(s)) => s.as_str(),
|
||||
_ => "",
|
||||
};
|
||||
|
||||
if command.eq_ignore_ascii_case("all") {
|
||||
let mut bought_crops = Vec::new();
|
||||
let mut total_cost = 0;
|
||||
|
||||
for crop in &CROP_TYPES {
|
||||
if user.balance < crop.price as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
use chrono::Utc;
|
||||
let crop_id = format!("crop-{}", crop.name.to_lowercase().replace(' ', "_"));
|
||||
|
||||
let existing_crop = sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
||||
"SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2",
|
||||
)
|
||||
.bind(&user._id)
|
||||
.bind(&crop_id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if existing_crop.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
user.balance -= crop.price as i32;
|
||||
total_cost += crop.price;
|
||||
|
||||
use chrono::Duration as ChronoDuration;
|
||||
let ready_time =
|
||||
Utc::now() + ChronoDuration::minutes(crop.grow_time_minutes as i64);
|
||||
|
||||
let _ =
|
||||
sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)")
|
||||
.bind(&user._id)
|
||||
.bind(&crop_id)
|
||||
.bind(ready_time)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
bought_crops.push(format!(
|
||||
"{} ({} coins, {} min)",
|
||||
crop.name, crop.price, crop.grow_time_minutes
|
||||
));
|
||||
}
|
||||
|
||||
if bought_crops.is_empty() {
|
||||
client
|
||||
.message(
|
||||
"You couldn't buy any crops: not enough coins or crops already growing."
|
||||
.to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2")
|
||||
.bind(user.balance)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
client
|
||||
.message(format!(
|
||||
"✅ You bought {} crops for a total of {} coins: {}",
|
||||
bought_crops.len(),
|
||||
total_cost,
|
||||
bought_crops.join(", ")
|
||||
))
|
||||
.await;
|
||||
|
||||
return;
|
||||
}
|
||||
if command == "sell" {
|
||||
let mut total_value: i32 = 0;
|
||||
let mut fruit_count: i32 = 0;
|
||||
let mut sold_fruits: Vec<String> = Vec::new();
|
||||
|
||||
let mut new_items: Vec<String> = Vec::new();
|
||||
for item in user.items.iter() {
|
||||
if item.starts_with("fruit-") {
|
||||
let parts: Vec<&str> = item.split('-').collect();
|
||||
if parts.len() >= 4 {
|
||||
let fruit_name = parts[1].replace('_', " ");
|
||||
let quality_str = parts[2];
|
||||
let count_str = parts[3];
|
||||
let quality: u8 = quality_str.parse().unwrap_or(1);
|
||||
let count: u32 = count_str.parse().unwrap_or(1);
|
||||
|
||||
if let Some(fruit_type) = FRUIT_TYPES
|
||||
.iter()
|
||||
.find(|f| f.name.to_lowercase() == fruit_name.to_lowercase())
|
||||
{
|
||||
let value = FarmCommand::fruit_value(fruit_type, quality, count);
|
||||
total_value += value;
|
||||
fruit_count += count as i32;
|
||||
|
||||
let stars = match quality {
|
||||
1 => "⭐",
|
||||
2 => "⭐⭐",
|
||||
3 => "⭐⭐⭐",
|
||||
4 => "⭐⭐⭐⭐",
|
||||
5 => "⭐⭐⭐⭐⭐",
|
||||
_ => "⭐",
|
||||
};
|
||||
|
||||
sold_fruits.push(format!(
|
||||
"{} {} x{} ({} coins)",
|
||||
stars, fruit_type.name, count, value
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new_items.push(item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if fruit_count == 0 {
|
||||
client
|
||||
.message("You have no fruits 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 {} fruits for a total of {} coins. {}",
|
||||
fruit_count,
|
||||
total_value,
|
||||
sold_fruits.join(", ")
|
||||
))
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if command == "harvest" {
|
||||
let ready_crops = sqlx::query_scalar::<_, String>(
|
||||
"SELECT type FROM cooldowns WHERE _id = $1 AND type LIKE 'crop-%' AND untill <= $2",
|
||||
)
|
||||
.bind(&user._id)
|
||||
.bind(Utc::now())
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if ready_crops.is_empty() {
|
||||
client
|
||||
.message("You have no crops ready for harvest.".to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let (quality_bonus, count_bonus) = self.get_hoe_bonus(&user.items);
|
||||
let mut harvested_items: Vec<String> = Vec::new();
|
||||
|
||||
for crop_id in ready_crops {
|
||||
if let Some(crop_name) = crop_id.strip_prefix("crop-")
|
||||
&& let Some(crop_type) = CROP_TYPES
|
||||
.iter()
|
||||
.find(|c| c.name.to_lowercase().replace(' ', "_") == crop_name)
|
||||
{
|
||||
let quality = self.calculate_fruit_quality(quality_bonus);
|
||||
let base_count = crop_type.base_fruit_count;
|
||||
let bonus_count = (base_count as f64 * count_bonus) as u32;
|
||||
let total_count = base_count + bonus_count + rand::rng().random_range(0..=2);
|
||||
|
||||
let fruit = crop_type.fruits.choose(&mut rand::rng()).unwrap();
|
||||
let fruit_id = format!(
|
||||
"fruit-{}-{}-{}",
|
||||
fruit.name.to_lowercase().replace(' ', "_"),
|
||||
quality.stars(),
|
||||
total_count
|
||||
);
|
||||
|
||||
user.items.push(fruit_id.clone());
|
||||
harvested_items.push(format!(
|
||||
"{} {} x{}",
|
||||
quality.name(),
|
||||
fruit.name,
|
||||
total_count
|
||||
));
|
||||
|
||||
let _ = sqlx::query("DELETE FROM cooldowns WHERE _id = $1 AND type = $2")
|
||||
.bind(&user._id)
|
||||
.bind(&crop_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2")
|
||||
.bind(&user.items)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
client
|
||||
.message(format!(
|
||||
"Harvest complete! You got: {}",
|
||||
harvested_items.join(", ")
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let crop_name = command;
|
||||
let crop = CROP_TYPES
|
||||
.iter()
|
||||
.find(|c| c.name.eq_ignore_ascii_case(crop_name));
|
||||
|
||||
if let Some(crop) = crop {
|
||||
if user.balance < crop.price as i32 {
|
||||
client
|
||||
.message(format!(
|
||||
"You don't have enough coins to plant {}. You need {} coins, but you have {}.",
|
||||
crop.name, crop.price, user.balance
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let crop_id = format!("crop-{}", crop.name.to_lowercase().replace(' ', "_"));
|
||||
|
||||
let existing_crop = sqlx::query_scalar::<_, Option<chrono::DateTime<chrono::Utc>>>(
|
||||
"SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2",
|
||||
)
|
||||
.bind(&user._id)
|
||||
.bind(&crop_id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
if let Ok(Some(until)) = existing_crop {
|
||||
let now = Utc::now();
|
||||
if until > now {
|
||||
let minutes_left = (until - now).num_minutes();
|
||||
client
|
||||
.message(format!(
|
||||
"You already have {} growing! It will be ready in {} minutes.",
|
||||
crop.name, minutes_left
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
} else {
|
||||
client
|
||||
.message(format!(
|
||||
"{} has fully grown, Use `let farm harvest` to harvest it.",
|
||||
crop.name
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
user.balance -= crop.price as i32;
|
||||
|
||||
use chrono::Duration as ChronoDuration;
|
||||
let ready_time = Utc::now() + ChronoDuration::minutes(crop.grow_time_minutes as i64);
|
||||
|
||||
let _ = sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)")
|
||||
.bind(&user._id)
|
||||
.bind(&crop_id)
|
||||
.bind(ready_time)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2")
|
||||
.bind(user.balance)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await;
|
||||
|
||||
client
|
||||
.message(format!(
|
||||
"You planted {} for {} coins! It will be ready in {} minutes. Use 'let farm harvest' to collect when ready.",
|
||||
crop.name, crop.price, crop.grow_time_minutes
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
let available_crops = CROP_TYPES
|
||||
.iter()
|
||||
.map(|c| {
|
||||
format!(
|
||||
"{} ({} coins, {} min)",
|
||||
c.name, c.price, c.grow_time_minutes
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
client
|
||||
.message(format!(
|
||||
"Crop not found. Available crops: {}",
|
||||
available_crops
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
416
src/commands/eco/fish.rs
Normal file
416
src/commands/eco/fish.rs
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
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,
|
||||
}]
|
||||
}
|
||||
async fn constructed(&mut self, _: Client) {}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(&player._id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
153
src/commands/eco/inv.rs
Normal file
153
src/commands/eco/inv.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use crate::User;
|
||||
use crate::client::Client;
|
||||
use crate::client::ClientEvent;
|
||||
use crate::client::Player;
|
||||
use crate::commands::Command;
|
||||
use crate::commands::eco::fish::FISH_TYPES;
|
||||
|
||||
use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sqlx::Pool;
|
||||
use sqlx::Postgres;
|
||||
|
||||
pub struct InventoryCommand {
|
||||
pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
impl InventoryCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Command for InventoryCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"inventory"
|
||||
}
|
||||
fn category(&self) -> &'static str {
|
||||
"eco"
|
||||
}
|
||||
fn aliases(&self) -> &[&'static str] {
|
||||
&["inv"]
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"View your inventory."
|
||||
}
|
||||
fn argument_spec(&self) -> &'static [ArgumentSpec] {
|
||||
&[ArgumentSpec {
|
||||
name: "user_id",
|
||||
arg_type: ArgumentType::String,
|
||||
required: false,
|
||||
default: None,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn constructed(&mut self, _: Client) {}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let id = match args.get("user_id") {
|
||||
Some(ParsedArgument::String(s)) => s.as_str(),
|
||||
_ => player._id.as_str(),
|
||||
};
|
||||
|
||||
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(user) = user {
|
||||
if user.items.is_empty() {
|
||||
client.message("Your inventory is empty.".to_string()).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut fruits: Vec<(u8, String)> = Vec::new();
|
||||
let mut fish: Vec<String> = Vec::new();
|
||||
let mut others: HashMap<String, u32> = HashMap::new();
|
||||
|
||||
for item in user.items.iter() {
|
||||
if item.starts_with("fruit-") {
|
||||
let parts: Vec<&str> = item.split('-').collect();
|
||||
if parts.len() >= 4 {
|
||||
let mut fruit_name = parts[1].replace('_', " ");
|
||||
let quality: u8 = parts[2].parse().unwrap_or(1);
|
||||
let count: u32 = parts[3].parse().unwrap_or(1);
|
||||
|
||||
let stars = match quality {
|
||||
1 => "⭐",
|
||||
2 => "⭐⭐",
|
||||
3 => "⭐⭐⭐",
|
||||
4 => "⭐⭐⭐⭐",
|
||||
5 => "⭐⭐⭐⭐⭐",
|
||||
_ => "⭐",
|
||||
};
|
||||
fruits.push((
|
||||
quality,
|
||||
format!(
|
||||
"{} {}{fruit_name} x{}",
|
||||
stars,
|
||||
fruit_name.remove(0).to_uppercase(),
|
||||
count
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if item.starts_with("fish-") {
|
||||
let parts: Vec<&str> = item.split('-').collect();
|
||||
if parts.len() >= 3 {
|
||||
let fish_name = parts[1].replace('_', " ");
|
||||
let weight: f64 = parts[2].parse().unwrap_or(0.0);
|
||||
|
||||
if let Some(fish_type) = FISH_TYPES
|
||||
.iter()
|
||||
.find(|f| f.name.eq_ignore_ascii_case(&fish_name))
|
||||
{
|
||||
fish.push(format!("{} ({:.2} kg)", fish_type.name, weight));
|
||||
} else {
|
||||
fish.push(format!("{} ({:.2} kg)", fish_name, weight));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let display_name = match item.as_str() {
|
||||
"wooden_rod" => "Wooden Rod".to_string(),
|
||||
"fiberglass_rod" => "Fiberglass Rod".to_string(),
|
||||
"carbon_rod" => "Carbon Rod".to_string(),
|
||||
"small_bait" => "Small Bait".to_string(),
|
||||
"large_bait" => "Large Bait".to_string(),
|
||||
"wooden_hoe" => "Wooden Hoe".to_string(),
|
||||
"iron_hoe" => "Iron Hoe".to_string(),
|
||||
"golden_hoe" => "Golden Hoe".to_string(),
|
||||
"diamond_hoe" => "Diamond Hoe".to_string(),
|
||||
_ => item.replace('_', " "),
|
||||
};
|
||||
*others.entry(display_name).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fruits.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
let mut inventory_display = Vec::new();
|
||||
|
||||
inventory_display.extend(fruits.into_iter().map(|(_, s)| s));
|
||||
inventory_display.extend(fish);
|
||||
inventory_display.extend(others.into_iter().map(|(name, count)| {
|
||||
if count > 1 {
|
||||
format!("{} x{}", name, count)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}));
|
||||
|
||||
client
|
||||
.message(format!("Inventory: {}", inventory_display.join(", ")))
|
||||
.await;
|
||||
} else {
|
||||
client.message("No user found.".to_string()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/commands/eco/mod.rs
Normal file
15
src/commands/eco/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use crate::submods;
|
||||
|
||||
submods!(balance, fish, farm, shop, inv, cf);
|
||||
|
||||
pub fn calculate_fish_value(weight: f64, price: u32) -> i32 {
|
||||
let min_weight = 0.1;
|
||||
let max_weight = 50.0;
|
||||
let min_multiplier = 1.0;
|
||||
let max_multiplier = 2.5;
|
||||
|
||||
let normalized_weight = ((weight - min_weight) / (max_weight - min_weight)).clamp(0.0, 1.0);
|
||||
let multiplier = min_multiplier + (max_multiplier - min_multiplier) * normalized_weight;
|
||||
|
||||
(price as f64 * multiplier).round() as i32
|
||||
}
|
||||
209
src/commands/eco/shop.rs
Normal file
209
src/commands/eco/shop.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
use crate::User;
|
||||
use crate::client::Client;
|
||||
use crate::client::ClientEvent;
|
||||
use crate::client::Player;
|
||||
use crate::commands::Command;
|
||||
|
||||
use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sqlx::Pool;
|
||||
use sqlx::Postgres;
|
||||
|
||||
pub struct ShopItem {
|
||||
pub name: &'static str,
|
||||
pub price: i32,
|
||||
pub max: u8,
|
||||
pub category: &'static str,
|
||||
pub id: &'static str,
|
||||
}
|
||||
|
||||
pub const SHOP_ITEMS: [ShopItem; 9] = [
|
||||
ShopItem {
|
||||
name: "Wooden Rod",
|
||||
price: 100,
|
||||
category: "Fishing Rod",
|
||||
id: "wooden_rod",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Fiberglass Rod",
|
||||
price: 500,
|
||||
category: "Fishing Rod",
|
||||
id: "fiberglass_rod",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Carbon Rod",
|
||||
price: 2000,
|
||||
category: "Fishing Rod",
|
||||
id: "carbon_rod",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Small Bait",
|
||||
price: 20,
|
||||
category: "Bait",
|
||||
id: "small_bait",
|
||||
max: 100,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Large Bait",
|
||||
price: 80,
|
||||
category: "Bait",
|
||||
id: "large_bait",
|
||||
max: 100,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Wooden Hoe",
|
||||
price: 80,
|
||||
category: "Farming Tool",
|
||||
id: "wooden_hoe",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Stone Hoe",
|
||||
price: 200,
|
||||
category: "Farming Tool",
|
||||
id: "stone_hoe",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Iron Hoe",
|
||||
price: 800,
|
||||
category: "Farming Tool",
|
||||
id: "iron_hoe",
|
||||
max: 1,
|
||||
},
|
||||
ShopItem {
|
||||
name: "Diamond Hoe",
|
||||
price: 3000,
|
||||
category: "Farming Tool",
|
||||
id: "diamond_hoe",
|
||||
max: 1,
|
||||
},
|
||||
];
|
||||
|
||||
pub struct ShopCommand {
|
||||
pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
impl ShopCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Command for ShopCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"shop"
|
||||
}
|
||||
fn category(&self) -> &'static str {
|
||||
"eco"
|
||||
}
|
||||
fn aliases(&self) -> &[&'static str] {
|
||||
&[]
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"Do some shopping."
|
||||
}
|
||||
|
||||
async fn constructed(&mut self, _: Client) {}
|
||||
async fn event(&mut self, _: Client, _: ClientEvent) {}
|
||||
|
||||
fn argument_spec(&self) -> &'static [ArgumentSpec] {
|
||||
&[
|
||||
ArgumentSpec {
|
||||
name: "item_id",
|
||||
arg_type: ArgumentType::String,
|
||||
required: false,
|
||||
default: None,
|
||||
},
|
||||
ArgumentSpec {
|
||||
name: "quantity",
|
||||
arg_type: ArgumentType::Integer,
|
||||
required: false,
|
||||
default: Some("1"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) {
|
||||
let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1")
|
||||
.bind(&player._id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let item_name = match args.get("item_id") {
|
||||
Some(ParsedArgument::String(s)) => s.as_str(),
|
||||
_ => "empty",
|
||||
};
|
||||
|
||||
let quantity: u8 = match args.get("quantity") {
|
||||
Some(ParsedArgument::Integer(i)) => *i as u8,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
let item = SHOP_ITEMS
|
||||
.iter()
|
||||
.find(|i| i.id.eq_ignore_ascii_case(item_name));
|
||||
|
||||
if let Some(item) = item {
|
||||
let owned_count = user.items.iter().filter(|id| id == &item.id).count() as u8;
|
||||
if owned_count >= item.max {
|
||||
client
|
||||
.message(&format!(
|
||||
"You already own the maximum amount of {} ({}).",
|
||||
item.name, item.max
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let quantity = quantity.min(item.max - owned_count);
|
||||
|
||||
let total_price = item.price * quantity as i32;
|
||||
|
||||
if user.balance >= total_price {
|
||||
user.balance -= total_price;
|
||||
for _ in 0..quantity {
|
||||
user.items.push(item.id.to_string());
|
||||
}
|
||||
sqlx::query("UPDATE users SET balance = $1, items = $2 WHERE _id = $3")
|
||||
.bind(user.balance as i64)
|
||||
.bind(&user.items)
|
||||
.bind(&user._id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.message(&format!(
|
||||
"You purchased {}x {} ({}) for {} coins!",
|
||||
quantity, item.name, item.category, total_price
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
client.message(&format!(
|
||||
"You don't have enough coins to buy {}x {}. You need {} coins, but you have {}.",
|
||||
quantity, item.name, total_price, user.balance
|
||||
)).await;
|
||||
}
|
||||
} else {
|
||||
let available_items = SHOP_ITEMS
|
||||
.iter()
|
||||
.map(|i| format!("{} ({})", i.name, i.id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
client
|
||||
.message(&format!(
|
||||
"Item not found. Available items: {}",
|
||||
available_items
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue