add items
This commit is contained in:
@@ -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
|
||||
))
|
||||
}
|
||||
|
||||
81
src/commands/gambling/shop.rs
Normal file
81
src/commands/gambling/shop.rs
Normal 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(())
|
||||
}
|
||||
@@ -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 **{}**.",
|
||||
|
||||
@@ -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(),
|
||||
]
|
||||
|
||||
118
src/inventory.rs
Normal file
118
src/inventory.rs
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
23
src/main.rs
23
src/main.rs
@@ -1,6 +1,7 @@
|
||||
mod commands;
|
||||
|
||||
pub mod common;
|
||||
pub mod inventory;
|
||||
use crate::common::{Context, Error, Data};
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -89,6 +90,28 @@ async fn main() -> Result<(), Error> {
|
||||
"#,
|
||||
).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!");
|
||||
|
||||
Ok(Data {
|
||||
|
||||
Reference in New Issue
Block a user