add items
This commit is contained in:
@@ -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"
|
||||||
@@ -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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
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
|
/// 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 **{}**.",
|
||||||
|
|||||||
@@ -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
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;
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user