diff --git a/lootalot_db/src/lib.rs b/lootalot_db/src/lib.rs index f11d199..4439256 100644 --- a/lootalot_db/src/lib.rs +++ b/lootalot_db/src/lib.rs @@ -47,16 +47,23 @@ impl ActionStatus<()> { response: (), } } - fn nop() -> ActionStatus<()> { +} + + +impl ActionStatus { + fn nop() -> ActionStatus { Self { executed: false, - response: (), + response: Default::default(), } } } - /// A wrapper providing an API over the database -/// It offers a convenient way to deal with connection +/// It offers a convenient way to deal with connection. +/// +/// # Note +/// All methods consumes the DbApi, so that only one action +/// can be performed using a single instance. /// /// # Todo list /// ```text @@ -70,8 +77,8 @@ impl ActionStatus<()> { /// v .loot() -> List of items owned (Vec) /// v .claim(loot_id) -> Success status (bool) /// v .unclaim(loot_id) -> Success status (bool) -/// x .sell(loot_id) -> Success status (bool, earned) -/// x .buy(item_desc) -> Success status (bool, cost) +/// v .sell(loot_id) -> Success status (bool, earned) +/// v .buy(item_desc) -> Success status (bool, cost) /// v .update_wealth(value_in_gold) -> Success status (bool, new_wealth) /// v .as_admin() /// x .add_loot(identifier, [items_desc]) -> Success status @@ -153,6 +160,48 @@ 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 + pub fn buy<'a>(self, name: &'a str, price: i32) -> ActionResult> { + let new_item = models::item::NewLoot::to_player(self.id, (name, price)); + diesel::insert_into(schema::looted::table) + .values(&new_item) + .execute(self.conn) + .and_then(|r| match r { + 1 => self.update_wealth(-(price as f32)), + _ => Ok(ActionStatus::nop()) + }) + } + /// Sell an item from this player chest + pub fn sell(self, loot_id: i32, _price_mod: Option) -> ActionResult> { + // Check that the item belongs to player + let exists_and_owned: bool = diesel::select(models::Loot::owns(self.id, loot_id)) + .get_result(self.conn)?; + if !exists_and_owned { + Ok(ActionStatus { + executed: false, + response: None }) + } else { + use schema::looted::dsl::*; + let loot_value = looted.find(loot_id) + .select(base_price) + .first::(self.conn)?; + let sell_value = (loot_value / 2) as f32; + diesel::delete(looted.find(loot_id)) + .execute(self.conn) + .and_then(|r| match r { + // On deletion, update this player wealth + 1 => self.update_wealth(sell_value), + _ => Ok(ActionStatus { + executed: false, + response: None, + }), + }) + } + + } + /// Adds the value in gold to the player's wealth. /// /// Value can be negative to substract wealth. @@ -222,6 +271,17 @@ impl<'q> AsAdmin<'q> { .map(ActionStatus::was_updated) } + /// Adds a list of items to the group loot + pub fn add_loot<'a>(self, items: Vec<(&'a str, i32)>) -> ActionResult<()> { + for item_desc in items.into_iter() { + let new_item = models::item::NewLoot::to_group(item_desc); + diesel::insert_into(schema::looted::table) + .values(&new_item) + .execute(self.0)?; + } + Ok(ActionStatus::ok()) + } + /// Resolve all pending claims and dispatch claimed items. /// /// When a player gets an item, it's debt is increased by this item sell value @@ -325,6 +385,37 @@ mod tests { fn test_player_unclaim_item() { } + + /// All-in-one checks one a simple buy/sell procedure + /// + /// Checks that player's chest and wealth are updated. + /// Checks that items are sold at half their value. + #[test] + fn test_buy_sell_simple() { + let conn = test_connection(); + DbApi::with_conn(&conn).as_admin().add_player("Player".to_string(), 1000.0).unwrap(); + // Buy an item + let bought = DbApi::with_conn(&conn).as_player(1).buy("Sword", 800).unwrap(); + assert_eq!(bought.executed, true); + assert_eq!(bought.response, Some((0,0,0,-8))); + let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); + assert_eq!(chest.len(), 1); + let loot = chest.get(0).unwrap(); + assert_eq!(loot.name, "Sword"); + assert_eq!(loot.base_price, 800); + let players = DbApi::with_conn(&conn).fetch_players().unwrap(); + let player = players.get(1).unwrap(); + assert_eq!(player.pp, 2); + // Sell back + let sold = DbApi::with_conn(&conn).as_player(1).sell(loot.id, None).unwrap(); + assert_eq!(sold.executed, true); + assert_eq!(sold.response, Some((0,0,0,4))); + let chest = DbApi::with_conn(&conn).as_player(1).loot().unwrap(); + assert_eq!(chest.len(), 0); + let players = DbApi::with_conn(&conn).fetch_players().unwrap(); + let player = players.get(1).unwrap(); + assert_eq!(player.pp, 6); + } } diff --git a/lootalot_db/src/models/item.rs b/lootalot_db/src/models/item.rs index 9b53b6f..e8ad50c 100644 --- a/lootalot_db/src/models/item.rs +++ b/lootalot_db/src/models/item.rs @@ -14,9 +14,9 @@ type OwnedBy = Select; /// Or maybe this is a little too confusing ?? #[derive(Debug, Queryable, Serialize)] pub struct Item { - id: i32, - name: String, - base_price: i32, + pub id: i32, + pub name: String, + pub base_price: i32, } impl Item { @@ -35,7 +35,7 @@ type OwnedLoot = Filter; pub(crate) struct Loot { id: i32, name: String, - base_value: i32, + base_price: i32, owner: i32, } @@ -45,13 +45,17 @@ impl Loot { looted::table.filter(looted::owner_id.eq(id)) } + pub(crate) fn owns(player: i32, item: i32) -> Exists> { + exists(Loot::owned_by(player).find(item)) + } + pub(crate) fn exists(id: i32) -> Exists> { exists(looted::table.find(id)) } } /// Description of an item : (name, value in gold) -type ItemDesc<'a> = (&'a str, i32); +pub type ItemDesc<'a> = (&'a str, i32); /// An item being looted or bought. ///