diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index 3335104..76d1ccf 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -121,51 +121,79 @@ impl<'q> AsPlayer<'q> { pub fn loot(self) -> QueryResult> { Ok(models::Item::owned_by(self.id).load(self.conn)?) } - /// Buy an item and add it to this player chest - /// TODO: Items should be picked from a custom list + /// Buy a batch of items and add them to this player chest + /// + /// Items can only be bought from inventory. Hence, the use + /// of the entity's id in 'items' table. + /// + /// # Params + /// List of (Item's id in inventory, Option) /// /// # Returns /// Result containing the difference in coins after operation - pub fn buy<'a>(self, name: &'a str, price: i32) -> ActionResult<(i32, i32, i32, i32)> { - self.conn.transaction(|| { - let new_item = models::item::NewLoot::to_player(self.id, (name, price)); - let _item_added = diesel::insert_into(schema::looted::table) - .values(&new_item) - .execute(self.conn) - .map(|rows_updated| match rows_updated { - 1 => (), - _ => panic!("RuntimeError: Buy made no changes at all"), - })?; - self.update_wealth(-(price as f32)) - }) + pub fn buy<'a>(self, params: &Vec<(i32, Option)>) -> ActionResult<(i32, i32, i32, i32)> { + let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); + for (item_id, price_mod) in params.into_iter() { + let res = self.conn.transaction(|| { + let item = schema::items::table.find(item_id).first::(self.conn)?; + let new_item = models::item::NewLoot::to_player(self.id, (&item.name, item.base_price)); + let _item_added = diesel::insert_into(schema::looted::table) + .values(&new_item) + .execute(self.conn) + .map(|rows_updated| match rows_updated { + 1 => (), + _ => panic!("RuntimeError: Buy made no changes at all"), + })?; + let sell_price = match price_mod { + Some(modifier) => item.base_price as f32 * modifier, + None => item.base_price as f32 + }; + DbApi::with_conn(self.conn).as_player(self.id).update_wealth(-sell_price) + }); + if let Ok(diff) = res { all_results.push(diff); } + } + Ok(all_results.into_iter().fold((0,0,0,0), |sum, diff| { + (sum.0 + diff.0, sum.1 + diff.1, sum.2 + diff.2, sum.3 + diff.3) + })) } - /// Sell an item from this player chest + /// Sell a set of items from this player chest /// /// # Returns /// Result containing the difference in coins after operation pub fn sell( self, - loot_id: i32, - price_mod: Option, + params: &Vec<(i32, Option)>, ) -> ActionResult<(i32, i32, i32, i32)> { - self.conn.transaction(|| { - use schema::looted::dsl::*; - let loot = looted - .find(loot_id) - .first::(self.conn)?; - if loot.owner != self.id { - // If the item does not belong to player, - // it can't be what we're looking for - return Err(diesel::result::Error::NotFound); + let mut all_results: Vec<(i32, i32, i32, i32)> = Vec::with_capacity(params.len()); + for (loot_id, price_mod) in params.into_iter() { + let res = self.conn.transaction(|| { + use schema::looted::dsl::*; + let loot = looted + .find(loot_id) + .first::(self.conn)?; + if loot.owner != self.id { + // If the item does not belong to player, + // it can't be what we're looking for + return Err(diesel::result::Error::NotFound); + } + let mut sell_value = loot.base_price as f32 / 2.0; + if let Some(modifier) = price_mod { + sell_value *= modifier; + } + let _deleted = diesel::delete(looted.find(loot_id)) + .execute(self.conn)?; + DbApi::with_conn(self.conn).as_player(self.id).update_wealth(sell_value) + }); + if let Ok(diff) = res { + all_results.push(diff) + } else { + // TODO: need to find a better way to deal with errors + return Err(diesel::result::Error::NotFound) } - let mut sell_value = (loot.base_price / 2) as f32; - if let Some(modifier) = price_mod { - sell_value *= modifier; - } - let _deleted = diesel::delete(looted.find(loot_id)) - .execute(self.conn)?; - self.update_wealth(sell_value) - }) + } + Ok(all_results.into_iter().fold((0,0,0,0), |sum, diff| { + (sum.0 + diff.0, sum.1 + diff.1, sum.2 + diff.2, sum.3 + diff.3) + })) } @@ -243,6 +271,13 @@ impl<'q> AsAdmin<'q> { } /// Adds a list of items to the group loot + /// + /// This offers complete control other created items, so that unique + /// items can be easily added. A user interface shall deal with + /// filling theses values for known items in inventory. + /// + /// # Params + /// List of (name, base_price) values for the new items pub fn add_loot(self, items: Vec<(&str, i32)>) -> ActionResult<()> { for item_desc in items.into_iter() { let new_item = models::item::NewLoot::to_group(item_desc); @@ -457,6 +492,14 @@ mod tests { #[test] fn as_player_simple_buy_sell() { let conn = test_connection(); + // Adds a sword into inventory + { + use schema::items::dsl::*; + diesel::insert_into(items) + .values((name.eq("Sword"), base_price.eq(800))) + .execute(&conn) + .expect("Could not set up items table"); + } DbApi::with_conn(&conn) .as_admin() .add_player("Player", 1000.0) @@ -464,7 +507,7 @@ mod tests { // Buy an item let bought = DbApi::with_conn(&conn) .as_player(1) - .buy("Sword", 800); + .buy(&vec![(1, None)]); assert_eq!(bought.ok(), Some((0, 0, 0, -8))); // Returns diff of player wealth ? let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); assert_eq!(chest.len(), 1); @@ -475,10 +518,10 @@ mod tests { let player = players.get(1).unwrap(); assert_eq!(player.pp, 2); // A player cannot sell loot from an other's chest - let result = DbApi::with_conn(&conn).as_player(0).sell(loot.id, None); + let result = DbApi::with_conn(&conn).as_player(0).sell(&vec![(loot.id, None)]); assert_eq!(result.is_ok(), false); // Sell back - let sold = DbApi::with_conn(&conn).as_player(1).sell(loot.id, None); + let sold = DbApi::with_conn(&conn).as_player(1).sell(&vec![(loot.id, None)]); assert_eq!(sold.ok(), Some((0, 0, 0, 4))); let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); assert_eq!(chest.len(), 0); diff --git a/lootalot_front/src/AppStorage.js b/lootalot_front/src/AppStorage.js index 5848c43..a20bf88 100644 --- a/lootalot_front/src/AppStorage.js +++ b/lootalot_front/src/AppStorage.js @@ -44,7 +44,16 @@ export const Api = { updateWealth (player_id, value_in_gp) { const payload = { player_id, value_in_gp: Number(value_in_gp) }; return this.__doFetch("players/update-wealth", 'PUT', payload); - } + }, + buyItems (player_id, items) { + const payload = { player_id, items }; + return this.__doFetch("players/buy", 'POST', payload); + }, + sellItems (player_id, items) { + const payload = { player_id, items }; + return this.__doFetch("players/sell", 'POST', payload); + + }, }; @@ -117,15 +126,15 @@ export const AppStorage = { }, updatePlayerWealth (goldValue) { return Api.updateWealth(this.state.player_id, goldValue) - .then(response => { - // Update player wealth - var diff = response; + .then(diff => this.__updatePlayerWealth(diff)); + }, + // TODO: Weird private name denotes a conflict + __updatePlayerWealth (diff) { if (this.debug) console.log('updatePlayerWealth', diff) this.state.player_list[this.state.player_id].cp += diff[0]; this.state.player_list[this.state.player_id].sp += diff[1]; this.state.player_list[this.state.player_id].gp += diff[2]; this.state.player_list[this.state.player_id].pp += diff[3]; - }); }, // Put a claim on an item from group chest. putRequest (itemId) { @@ -136,6 +145,22 @@ export const AppStorage = { this.state.player_claims[playerId].push(itemId); }); }, + buyItems (items) { + return Api.buyItems(this.state.player_id, items) + .then(diff => this.__updatePlayerWealth(diff)) + .then(() => { + // Add items to the player loot + console.log(items); + }); + }, + sellItems (items) { + return Api.sellItems(this.state.player_id, items) + .then(diff => this.__updatePlayerWealth(diff)) + .then(() => { + // Remove items from player chest + console.log(items); + }); + }, // Withdraws a claim. cancelRequest(itemId) { const playerId = this.state.player_id diff --git a/lootalot_front/src/components/Chest.vue b/lootalot_front/src/components/Chest.vue index b20bb66..22cf801 100644 --- a/lootalot_front/src/components/Chest.vue +++ b/lootalot_front/src/components/Chest.vue @@ -89,8 +89,12 @@ methods: { buySelectedItems () { const items = this.items.filter(i => this.selected_items.includes(i.id)); - this.$emit("buy", items); - this.selected_items.length = 0; + var payload = []; + items.forEach(item => { + payload.push([item.id, null]); + }); + this.$emit("buy", payload); + this.selected_items = []; }, sellSelectedItems () { if (!this.is_selling) { @@ -99,7 +103,11 @@ this.is_selling = false; if (this.selected_items.length > 0) { const items = this.items.filter(i => this.selected_items.includes(i.id)); - this.$emit("sell", items); + var payload = []; + items.forEach(item => { + payload.push([item.id, null]); + }); + this.$emit("sell", payload); this.selected_items = []; } } diff --git a/lootalot_front/src/components/PlayerView.js b/lootalot_front/src/components/PlayerView.js index e36fdf8..d512050 100644 --- a/lootalot_front/src/components/PlayerView.js +++ b/lootalot_front/src/components/PlayerView.js @@ -22,10 +22,12 @@ export default { }, buyItems(items) { - this.notifications.push(`Would buy ${items.length} items`); + AppStorage.buyItems(items) + .then(_ => this.notifications.push(`Bought ${items.length} items`)) }, sellItems (items) { - this.notifications.push(`Would sell ${items.length} items`); + AppStorage.sellItems(items) + .then(_ => this.notifications.push(`Sold ${items.length} items`)) }, parseLoot (items) { this.loot = []; diff --git a/src/server.rs b/src/server.rs index 9787b76..711e84a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,6 +67,12 @@ struct NewPlayer { wealth: f32, } +#[derive(Serialize, Deserialize, Debug)] +struct LootUpdate { + player_id: i32, + items: Vec<(i32, Option)>, +} + pub(crate) fn serve() -> std::io::Result<()> { let www_root: String = env::var("WWW_ROOT").expect("WWW_ROOT must be set"); dbg!(&www_root); @@ -109,6 +115,24 @@ pub(crate) fn serve() -> std::io::Result<()> { .update_wealth(data.value_in_gp)) }) ) + .route( + "/buy", + web::post().to_async(move |pool: AppPool, data: web::Json| { + db_call(pool, move |api| api + .as_player(data.player_id) + .buy(&data.items) + ) + }) + ) + .route( + "/sell", + web::post().to_async(move |pool: AppPool, data: web::Json| { + db_call(pool, move |api| api + .as_player(data.player_id) + .sell(&data.items) + ) + }) + ) ) .service( web::resource("/claims")