add items

This commit is contained in:
2024-12-10 02:34:02 -05:00
parent d62d1feb09
commit 1db3e8b79a
7 changed files with 364 additions and 9 deletions

View File

@@ -4,10 +4,79 @@ pub mod give;
pub mod wager;
pub mod daily;
pub mod leaderboard;
pub mod shop;
use crate::common::Error;
use poise::serenity_prelude::UserId;
use crate::{inventory::{self, Inventory}, common::{Context, Error}};
use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId};
use sqlx::{Row, PgExecutor};
use std::collections::HashMap;
#[derive(Clone)]
pub enum Effect {
Multiplier(f64),
Chance(f64),
}
#[derive(Clone)]
pub struct Item {
pub name: &'static str,
pub desc: &'static str,
pub effects: &'static [Effect],
pub id: u64,
}
impl Item {
pub fn inv_item(self) -> inventory::Item {
inventory::Item {
id: 0,
name: self.name.to_string(),
game: ID as i64,
item: self.id as i64,
data: serde_json::json!({}),
}
}
}
const ID: u64 = 440;
mod items {
use super::{Item, Effect};
pub const DIRT: Item = Item {
name: "Pile of Dirt",
desc: "Returns a 1.01x multiplier on all earnings",
effects: &[Effect::Multiplier(1.01)],
id: id::DIRT,
};
pub const SAND: Item = Item {
name: "Pile of Sand",
desc: "Increase your odds of winning by 1%",
effects: &[Effect::Chance(0.51)],
id: id::SAND,
};
mod id {
pub const DIRT: u64 = 1;
pub const SAND: u64 = 2;
}
pub fn get_item_by_id(id: u64) -> Option<&'static Item> {
match id {
id::DIRT => Some(&DIRT),
id::SAND => Some(&SAND),
_ => None
}
}
pub fn get_item_by_name(name: &str) -> Option<&'static Item> {
match name {
"Pile of Dirt" => Some(&DIRT),
"Pile of Sand" => Some(&SAND),
_ => None
}
}
}
pub async fn get_balance<'a, E>(id: UserId, db: E) -> Result<i32, Error>
where
@@ -37,3 +106,27 @@ where
Ok(())
}
async fn autocomplete_inventory<'a>(
ctx: Context<'a>,
partial: &'a str,
) -> impl Iterator<Item = serenity::AutocompleteChoice> + use<'a> {
let db = &ctx.data().database;
let inventory = Inventory::new(ctx.author().id, Some(ID))
.items(db).await
.fold(HashMap::<i64, usize>::new(), |mut acc, item| async {
let item = item.unwrap();
let x = acc.get(&item.item);
acc.insert(item.item, x.map(|x| x + 1).unwrap_or(1));
acc
}).await;
inventory.into_iter()
.map(|(id, count)| (items::get_item_by_id(id as u64).unwrap(), count))
.filter(move |(item, _)| item.name.contains(partial))
.map(|(item, count)| serenity::AutocompleteChoice::new(
format!("{} - {} ({count}x)", item.desc, item.name),
item.name
))
}

View File

@@ -0,0 +1,81 @@
use crate::common::{Context, Error};
use crate::inventory::Inventory;
use super::Item;
use once_cell::sync::Lazy;
use poise::serenity_prelude as serenity;
use std::collections::HashMap;
static ITEMS: Lazy<HashMap<&'static str, (i32, &Item)>> = Lazy::new(|| {
HashMap::from([
("Pile of Dirt", (10, &super::items::DIRT)),
("Pile of Sand", (10, &super::items::SAND)),
])
});
async fn autocomplete_shop<'a>(
ctx: Context<'_>,
partial: &'a str,
) -> impl Iterator<Item = serenity::AutocompleteChoice> + use<'a> {
let db = &ctx.data().database;
let balance = super::get_balance(ctx.author().id, db).await;
ITEMS.values()
.filter(move |(_, item)| item.name.contains(partial))
.map(move |(cost, item)| {
let balance = balance.as_ref().unwrap_or(cost);
serenity::AutocompleteChoice::new(
if cost > balance {
format!("{} ({cost} tokens) - {} - Can't Afford", item.name, item.desc)
} else {
format!("{} ({cost} tokens) - {}", item.name, item.desc)
},
item.name
)
})
}
#[poise::command(slash_command, prefix_command)]
pub async fn buy(ctx: Context<'_>,
#[autocomplete = "autocomplete_shop"]
item: String,
#[min = 1]
count: Option<i32>) -> Result<(), Error>
{
let count = count.unwrap_or(1);
if count < 1 {
ctx.reply("Ok, did you REALLY expect me to fall for that for a third time? You've gotta find a new trick.").await?;
return Ok(());
}
let mut tx = ctx.data().database.begin().await?;
if let Some((price, &ref item)) = ITEMS.get(item.as_str()) {
let author = ctx.author();
let balance = super::get_balance(author.id, &mut *tx).await?;
let total = *price * count;
if total > balance {
ctx.reply(format!("You could not afford the items ({count}x **{}** cost(s) **{total}** tokens)", item.name)).await?;
return Ok(())
}
let inventory = Inventory::new(author.id, Some(super::ID));
for _ in 0..count {
inventory.give_item(&mut *tx, item.clone().inv_item()).await?;
}
let balance = super::get_balance(author.id, &mut *tx).await?;
super::change_balance(author.id, balance - total, &mut *tx).await?;
ctx.reply(format!("You have purchased {count}x {}.", item.name)).await?;
tx.commit().await?;
} else {
ctx.reply(format!("The item {item} is not available in this shop.")).await?;
}
Ok(())
}

View File

@@ -1,10 +1,14 @@
use crate::common::{Context, Error};
use crate::{common::{Context, Error}, inventory::Inventory};
use super::Effect;
use rand::Rng;
/// Put forward an amount of tokens to either lose or earn
#[poise::command(slash_command, prefix_command)]
pub async fn wager(
ctx: Context<'_>,
amount: i32) -> Result<(), Error>
amount: i32,
#[autocomplete = "super::autocomplete_inventory"]
item: Option<String>) -> Result<(), Error>
{
// #[min = 1] does not appear to work with prefix commands
if amount < 1 {
@@ -16,16 +20,50 @@ pub async fn wager(
let mut wealth = super::get_balance(ctx.author().id, &mut *tx).await?;
let item = if let Some(item) = item {
let inventory = Inventory::new(ctx.author().id, Some(super::ID));
match super::items::get_item_by_name(&item) {
Some(item) => {
if let Some(item) = inventory.get_item_of_type(&mut *tx, item.id).await? {
inventory.remove_item(&mut *tx, item.id).await?;
} else {
ctx.reply(format!("You do not have a(n) {} to use.", item.name)).await?;
return Ok(());
}
Some(item)
}
None => {
ctx.reply("item {item} does not exist.").await?;
return Ok(());
}
}
} else {
None
};
if wealth < amount {
ctx.reply(format!("You do not have enough tokens (**{}**) to wager this amount.",
wealth)).await?;
return Ok(());
}
if rand::random() {
wealth += amount;
let multiplier = item.clone().map(|item| item.effects.iter().fold(1.0, |acc, effect| match effect {
Effect::Multiplier(c) => *c,
_ => acc,
})).unwrap_or(1.0);
let chance = item.map(|item| item.effects.iter().fold(0.5, |acc, effect| match effect {
Effect::Chance(c) => *c,
_ => acc,
})).unwrap_or(0.5);
if rand::thread_rng().gen_bool(chance) {
let win = (amount as f64 * multiplier) as i32;
wealth += win;
ctx.reply(format!("You just gained **{}** token(s)! You now have **{}**.",
amount, wealth)).await?;
win, wealth)).await?;
} else {
wealth -= amount;
ctx.reply(format!("You've lost **{}** token(s), you now have **{}**.",

View File

@@ -1,4 +1,3 @@
use crate::{Data, Error};
use poise::Command;
mod ping;
@@ -8,6 +7,8 @@ mod gambling;
mod eval;
mod self_roles;
use crate::common::{Data, Error};
pub fn commands() -> Vec<Command<Data, Error>> {
vec![
ping::ping(),
@@ -18,6 +19,7 @@ pub fn commands() -> Vec<Command<Data, Error>> {
gambling::wager::wager(),
gambling::daily::daily(),
gambling::leaderboard::leaderboard(),
gambling::shop::buy(),
eval::eval(),
self_roles::role(),
]