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

@@ -14,5 +14,5 @@ regex = "1.11.1"
sqlx = { version = "0.8", features = [ "runtime-tokio-native-tls", "postgres" ] } sqlx = { version = "0.8", features = [ "runtime-tokio-native-tls", "postgres" ] }
hex_color = "3" hex_color = "3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.133" serde_json = { version = "1.0", features = ["raw_value"] }
once_cell = "1.20.2" once_cell = "1.20.2"

View File

@@ -4,10 +4,79 @@ pub mod give;
pub mod wager; pub mod wager;
pub mod daily; pub mod daily;
pub mod leaderboard; pub mod leaderboard;
pub mod shop;
use crate::common::Error; use crate::{inventory::{self, Inventory}, common::{Context, Error}};
use poise::serenity_prelude::UserId; use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId};
use sqlx::{Row, PgExecutor}; 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> pub async fn get_balance<'a, E>(id: UserId, db: E) -> Result<i32, Error>
where where
@@ -37,3 +106,27 @@ where
Ok(()) 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 /// Put forward an amount of tokens to either lose or earn
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn wager( pub async fn wager(
ctx: Context<'_>, 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 // #[min = 1] does not appear to work with prefix commands
if amount < 1 { if amount < 1 {
@@ -16,16 +20,50 @@ pub async fn wager(
let mut wealth = super::get_balance(ctx.author().id, &mut *tx).await?; 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 { if wealth < amount {
ctx.reply(format!("You do not have enough tokens (**{}**) to wager this amount.", ctx.reply(format!("You do not have enough tokens (**{}**) to wager this amount.",
wealth)).await?; wealth)).await?;
return Ok(()); return Ok(());
} }
if rand::random() { let multiplier = item.clone().map(|item| item.effects.iter().fold(1.0, |acc, effect| match effect {
wealth += amount; 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 **{}**.", ctx.reply(format!("You just gained **{}** token(s)! You now have **{}**.",
amount, wealth)).await?; win, wealth)).await?;
} else { } else {
wealth -= amount; wealth -= amount;
ctx.reply(format!("You've lost **{}** token(s), you now have **{}**.", ctx.reply(format!("You've lost **{}** token(s), you now have **{}**.",

View File

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

118
src/inventory.rs Normal file
View File

@@ -0,0 +1,118 @@
use crate::common::Error;
use poise::serenity_prelude::{futures::Stream, UserId};
use sqlx::PgExecutor;
#[derive(Clone, sqlx::FromRow, Debug, PartialEq, Eq)]
pub struct Item {
pub id: i64,
pub name: String,
pub game: i64,
pub item: i64,
pub data: sqlx::types::JsonValue,
}
pub struct Inventory {
user: UserId,
game: Option<u64>,
}
impl Inventory {
pub fn new(user: UserId, game: Option<u64>) -> Self {
Self {
user,
game,
}
}
pub async fn give_item<'a, E>(&self, db: E, item: Item) -> Result<(), Error>
where
E: PgExecutor<'a>,
{
sqlx::query(
r#"
INSERT INTO items (owner, game, item, data, name)
VALUES ($1, $2, $3, $4, $5)
"#
)
.bind(self.user.get() as i64)
.bind(self.game.unwrap() as i64)
.bind(item.item)
.bind(item.data)
.bind(item.name)
.execute(db).await?;
Ok(())
}
pub async fn get_item_of_type<'a, E>(&self, db: E, item: u64) -> Result<Option<Item>, Error>
where
E: PgExecutor<'a>,
{
let x = sqlx::query_as(
r#"
SELECT id, name, game, item, data FROM items
where item = $1
"#
).bind(item as i64).fetch_one(db).await.ok();
Ok(x)
}
pub async fn get_item_with_name<'a, E>(&self, db: E, name: &str) -> Result<Option<Item>, Error>
where
E: PgExecutor<'a>,
{
let x = sqlx::query_as(
r#"
SELECT id, name, game, item, data FROM items
where name = $1
"#
).bind(name).fetch_one(db).await.ok();
Ok(x)
}
pub async fn remove_item<'a, E>(&self, db: E, item: i64) -> Result<(), Error>
where
E: PgExecutor<'a>,
{
sqlx::query(
r#"
DELETE FROM items
WHERE id = $1
"#
).bind(item).execute(db).await?;
Ok(())
}
pub async fn items<'a, E>(&self, db: E) -> impl Stream<Item = Result<Item, sqlx::Error>> + use<'a, E>
where
E: PgExecutor<'a> + 'a,
{
match self.game {
Some(game) =>
sqlx::query_as(
r#"
SELECT id, name, game, item, data FROM items
WHERE owner = $1 AND game = $2
"#
)
.bind(self.user.get() as i64)
.bind(game as i64)
.fetch(db),
None =>
sqlx::query_as(
r#"
SELECT id, name, game, item, data FROM items
WHERE owner = $1
"#
)
.bind(self.user.get() as i64)
.fetch(db)
}
}
}

View File

@@ -1,6 +1,7 @@
mod commands; mod commands;
pub mod common; pub mod common;
pub mod inventory;
use crate::common::{Context, Error, Data}; use crate::common::{Context, Error, Data};
use std::collections::HashMap; use std::collections::HashMap;
@@ -89,6 +90,28 @@ async fn main() -> Result<(), Error> {
"#, "#,
).execute(&database).await?; ).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
name CHAR[255]
)
"#
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS items (
id BIGSERIAL PRIMARY KEY,
owner BIGINT NOT NULL,
game BIGINT NOT NULL,
item BIGINT NOT NULL,
data JSON NOT NULL,
name TEXT
)
"#
).execute(&database).await?;
println!("Bot is ready!"); println!("Bot is ready!");
Ok(Data { Ok(Data {