diff --git a/lootalot_db/migrations/2019-06-10-123922_create_items/down.sql b/lootalot_db/migrations/2019-06-10-123922_create_items/down.sql index d962a59..f7fc944 100644 --- a/lootalot_db/migrations/2019-06-10-123922_create_items/down.sql +++ b/lootalot_db/migrations/2019-06-10-123922_create_items/down.sql @@ -1,3 +1,3 @@ DROP TABLE items; DROP TABLE looted; - +DROP TABLE shop; diff --git a/lootalot_db/migrations/2019-06-10-123922_create_items/up.sql b/lootalot_db/migrations/2019-06-10-123922_create_items/up.sql index 4893561..3809eff 100644 --- a/lootalot_db/migrations/2019-06-10-123922_create_items/up.sql +++ b/lootalot_db/migrations/2019-06-10-123922_create_items/up.sql @@ -13,3 +13,10 @@ CREATE TABLE looted ( owner_id INTEGER NOT NULL, FOREIGN KEY (owner_id) REFERENCES players(id) ); + +-- The items that are available in shop +CREATE TABLE shop ( + id INTEGER PRIMARY KEY NOT NULL, + name VARCHAR NOT NULL, + base_price INTEGER NOT NULL +); diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index c2a6349..157cd77 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -19,7 +19,7 @@ mod schema; pub use models::{ claim::{Claim, Claims}, history::{Event, UpdateList}, - item::{Inventory, Item, LootManager}, + item::{Inventory, Shop, Item, LootManager}, player::{AsPlayer, Player, Players, Wealth}, }; @@ -140,8 +140,8 @@ pub fn buy_item_from_inventory( let item = Inventory(conn).find(item_id)?; let new_item = LootManager(conn, id).add_from(&item)?; let sell_price = match price_mod { - Some(modifier) => item.value() as f64 * modifier, - None => item.value() as f64, + Some(modifier) => item.value() * modifier, + None => item.value(), }; if let Update::Wealth(diff) = AsPlayer(conn, id).update_wealth(-sell_price)? { Ok((new_item, diff)) @@ -151,11 +151,31 @@ pub fn buy_item_from_inventory( }) } -/// Fetch all existing claims -pub fn fetch_claims(conn: &DbConnection) -> QueryResult> { - schema::claims::table.load::(conn) +pub fn buy_item_from_shop( + conn: &DbConnection, + id: i32, + item_id: i32, + price_mod: Option, +) -> QueryResult<(Update, Wealth)> { + conn.transaction(|| { + let shop = Shop(conn); + // Find item in inventory + let item = shop.get(item_id)?; + let new_item = LootManager(conn, id).add_from(&item)?; + let _deleted = shop.remove(item_id)?; + let sell_price = match price_mod { + Some(modifier) => item.value() * modifier, + None => item.value(), + }; + if let Update::Wealth(diff) = AsPlayer(conn, id).update_wealth(-sell_price)? { + Ok((new_item, diff)) + } else { + Err(diesel::result::Error::RollbackTransaction) + } + }) } + /// Resolve all pending claims and dispatch claimed items. /// /// When a player gets an item, it's debt is increased by this item sell value @@ -171,7 +191,7 @@ pub fn resolve_claims(conn: &DbConnection) -> QueryResult<()> { let winner = claims.get(0).expect("Claims should not be empty !"); let player_id = winner.player_id; winner.resolve_claim(conn)?; - models::player::AsPlayer(conn, player_id).update_debt(item.sell_value())?; + models::player::AsPlayer(conn, player_id).update_debt(item.sell_value() as i32)?; } Ok(()) }) diff --git a/lootalot_db/src/models/claim.rs b/lootalot_db/src/models/claim.rs index 6b0a10d..91cc0a4 100644 --- a/lootalot_db/src/models/claim.rs +++ b/lootalot_db/src/models/claim.rs @@ -41,6 +41,11 @@ impl<'q> Claims<'q> { claims::table.load(self.0) } + pub fn by_player(&self, id: i32) -> QueryResult> { + claims::table.filter(claims::dsl::player_id.eq(id)) + .load(self.0) + } + /// Finds a single claim by association of player and loot ids. pub fn find(&self, player_id: i32, loot_id: i32) -> QueryResult { claims::table diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index c0225dd..30dd6e2 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -2,7 +2,7 @@ use diesel::dsl::{exists, Eq, Filter, Find, Select}; use diesel::expression::exists::Exists; use diesel::prelude::*; -use crate::schema::{items, looted}; +use crate::schema::{items, looted, shop}; use crate::{DbConnection, QueryResult, Update, UpdateResult, Claims }; type ItemColumns = (looted::id, looted::name, looted::base_price); const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price); @@ -18,13 +18,13 @@ pub struct Item { impl Item { /// Returns this item value - pub fn value(&self) -> i32 { - self.base_price + pub fn value(&self) -> f64 { + self.base_price as f64 } /// Returns this item sell value - pub fn sell_value(&self) -> i32 { - self.base_price / 2 + pub fn sell_value(&self) -> f64 { + self.base_price as f64 / 2.0 } pub fn remove(self, conn: &DbConnection) -> UpdateResult { @@ -58,6 +58,47 @@ impl<'q> Inventory<'q> { } } +pub struct Shop<'q>(pub &'q DbConnection); + +impl<'q> Shop<'q> { + pub fn all(&self) -> QueryResult> { + shop::table.load(self.0) + } + + pub fn get(&self, id: i32) -> QueryResult { + shop::table.find(&id).first::(self.0) + } + + pub fn remove(&self, id: i32) -> QueryResult<()> { + diesel::delete( + shop::table.find(&id) + ).execute(self.0)?; + Ok(()) + } + + pub fn replace_list(&self, items: Vec) -> QueryResult<()> { + self.0.transaction( + || -> QueryResult<()> + { + // Remove all content + diesel::delete(shop::table).execute(self.0)?; + // Adds new list + for item in &items { + let new_item = NewItem { + name: &item.name, + base_price: item.base_price, + }; + diesel::insert_into(shop::table) + .values(&new_item) + .execute(self.0)?; + + } + Ok(()) + } + ) + } +} + type WithOwner = Eq; type OwnedLoot = Filter; @@ -169,3 +210,10 @@ struct NewLoot<'a> { base_price: i32, owner_id: i32, } + +#[derive(Insertable)] +#[table_name = "shop"] +struct NewItem<'a> { + name: &'a str, + base_price: i32, +} diff --git a/lootalot_db/src/schema.rs b/lootalot_db/src/schema.rs index 7fb9296..106ee28 100644 --- a/lootalot_db/src/schema.rs +++ b/lootalot_db/src/schema.rs @@ -54,6 +54,14 @@ table! { } } +table! { + shop (id) { + id -> Integer, + name -> Text, + base_price -> Integer, + } +} + joinable!(claims -> looted (loot_id)); joinable!(claims -> players (player_id)); joinable!(history -> players (player_id)); @@ -67,4 +75,5 @@ allow_tables_to_appear_in_same_query!( looted, notifications, players, + shop, ); diff --git a/src/api.rs b/src/api.rs index 30f7a00..866231d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,14 +13,12 @@ pub struct BuySellParams { global_mod: Option, } - #[derive(Serialize, Deserialize, Debug)] pub struct NewGroupLoot { source_name: String, pub items: ItemList, } - /// A generic response for all queries #[derive(Serialize, Debug, Default)] pub struct ApiResponse { @@ -70,10 +68,12 @@ pub enum ApiActions { // Application level FetchPlayers, FetchInventory, + FetchShopInventory, FetchClaims, CheckItemList(Vec), // Player level FetchPlayer(i32), + FetchPlayerClaims(i32), FetchNotifications(i32), FetchLoot(i32), UpdateWealth(i32, f64), @@ -86,6 +86,7 @@ pub enum ApiActions { // Group level AddLoot(NewGroupLoot), // Admin level + RefreshShopInventory(ItemList), //AddPlayer(String, f64), //AddInventoryItem(pub String, pub i32), //ResolveClaims, @@ -106,9 +107,7 @@ pub fn execute( let mut errors = String::new(); let items = db::Inventory(conn).all()?; for name in &names { - if let Some(item) = - items.iter().filter(|i| &i.name == name).take(1).next() - { + if let Some(item) = items.iter().filter(|i| &i.name == name).take(1).next() { found.push(item.clone()) } else { errors.push_str(&format!("{},\n", name)); @@ -130,14 +129,22 @@ pub fn execute( response.set_value(Value::ItemList(db::Inventory(conn).all()?)); None } + ApiActions::FetchShopInventory => { + response.set_value(Value::ItemList(db::Shop(conn).all()?)); + None + } ApiActions::FetchClaims => { - response.set_value(Value::ClaimList(db::fetch_claims(conn)?)); + response.set_value(Value::ClaimList(db::Claims(conn).all()?)); None } ApiActions::FetchPlayer(id) => { response.set_value(Value::Player(db::Players(conn).find(id)?)); None } + ApiActions::FetchPlayerClaims(id) => { + response.set_value(Value::ClaimList(db::Claims(conn).by_player(id)?)); + None + } ApiActions::FetchNotifications(id) => { response.set_value(Value::Notifications( db::AsPlayer(conn, id).notifications()?, @@ -155,27 +162,35 @@ pub fn execute( } ApiActions::BuyItems(id, params) => { // TODO: check that player has enough money ! - let mut gains: Vec = Vec::with_capacity(params.items.len()); - for (item_id, price_mod) in params.items.into_iter() { - if let Ok((item, diff)) = db::buy_item_from_inventory(conn, id, item_id, price_mod) - { - response.push_update(item); - gains.push(diff); - } else { - response.push_error(format!("Error adding {}", item_id)); + let has_enough_gold = true; + + if has_enough_gold { + let mut gains: Vec = Vec::with_capacity(params.items.len()); + for (item_id, price_mod) in params.items.into_iter() { + if let Ok((item, diff)) = + db::buy_item_from_shop(conn, id, item_id, price_mod) + { + response.push_update(item); + gains.push(diff); + } else { + response.push_error(format!("Error adding {}", item_id)); + } } + let added_items = gains.len(); + let total_amount = gains + .into_iter() + .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); + response.notify(format!( + "{} objets achetés pour {}po", + added_items, + total_amount.to_gp() + )); + response.push_update(Update::Wealth(total_amount)); + Some((id, "Achat d'objets")) + } else { + response.push_error("Vous n'avez pas assez d'argent !"); + None } - let added_items = gains.len(); - let total_amount = gains - .into_iter() - .fold(db::Wealth::from_gp(0.0), |acc, i| acc + i); - response.notify(format!( - "{} objets achetés pour {}po", - added_items, - total_amount.to_gp() - )); - response.push_update(Update::Wealth(total_amount)); - Some((id, "Achat d'objets")) } // Behavior differs if player is group or regular. // Group sells item like players then split the total amount among players. @@ -200,20 +215,15 @@ pub fn execute( let players = params .players .unwrap_or(db::Players(conn).all()?.into_iter().map(|p| p.id).collect()); - let shared = db::split_and_share(conn, total_amount.to_gp() as i32, &players)?; - let shared_amount = { - if let Update::Wealth(amount) = shared { - amount.to_gp() - } else { - panic!("cannot happen") - } + if let Update::Wealth(shared) = + db::split_and_share(conn, total_amount.to_gp() as i32, &players)? + { + response.notify(format!( + "Les objets ont été vendus, les joueurs ont reçu (au total) {} po", + shared.to_gp() + )); + response.push_update(Update::Wealth(db::Wealth::from_gp(total_amount.to_gp() - shared.to_gp()))); }; - response.notify(format!( - "Les objets ont été vendus, les joueurs ont reçu (au total) {} po", - shared_amount - )); - response.push_update(Update::Wealth(total_amount)); - response.push_update(shared); } _ => { response.notify(format!( @@ -238,8 +248,12 @@ pub fn execute( } ApiActions::ClaimItems(id, items) => { conn.transaction(|| -> Result, diesel::result::Error> { - let current_claims: HashSet = - db::Claims(conn).all()?.iter().filter(|c| c.player_id == id).map(|c| c.loot_id).collect(); + let current_claims: HashSet = db::Claims(conn) + .all()? + .iter() + .filter(|c| c.player_id == id) + .map(|c| c.loot_id) + .collect(); let new_claims: HashSet = items.into_iter().collect(); // Claims to delete for item in current_claims.difference(&new_claims) { @@ -284,6 +298,11 @@ pub fn execute( }; Some((0, "Nouveau loot")) } + // Admin actions + ApiActions::RefreshShopInventory(items) => { + db::Shop(conn).replace_list(items)?; + None + } }; // Store the event if it can be undone. diff --git a/src/server.rs b/src/server.rs index 60b5ea4..5a75d33 100644 --- a/src/server.rs +++ b/src/server.rs @@ -126,7 +126,9 @@ fn configure_api(config: &mut web::ServiceConfig) { ) .service( web::resource("/claims") - //.route(web::get().to_async(endpoints::player_claims)) + .route(web::get().to_async(|pool, player: PlayerId| { + db_call(pool, Q::FetchPlayerClaims(*player)) + })) .route(web::post().to_async( |pool, (player, data): (PlayerId, IdList)| { db_call(pool, Q::ClaimItems(*player, data.into_inner())) @@ -188,6 +190,15 @@ fn configure_api(config: &mut web::ServiceConfig) { "/claims", web::get().to_async(|pool| db_call(pool, Q::FetchClaims)), ) + .service( + web::resource("/shop") + .route(web::get().to_async(|pool| db_call(pool, Q::FetchShopInventory))) + .route( + web::post().to_async(|pool, items: web::Json| { + db_call(pool, Q::RefreshShopInventory(items.into_inner())) + }), + ), + ) .service( web::resource("/items") .route(