20 Commits

Author SHA1 Message Date
08f29fc90e this looks this overdoing it... going back to master 2019-07-24 13:45:03 +02:00
4f60df88d7 moves player actions code inside UserAction trait impls 2019-07-20 15:49:51 +02:00
51a3d00d03 formats code 2019-07-20 15:00:28 +02:00
0636829fdd adds doc 2019-07-17 14:59:04 +02:00
143d9bb5aa adds tests 2019-07-17 14:26:27 +02:00
21369535e8 impls more DbApi actions, adds a test 2019-07-16 14:56:54 +02:00
441b7f5ad6 little fixes 2019-07-15 16:07:25 +02:00
4b55964786 adds comments and new tests 2019-07-15 15:55:13 +02:00
caabad3982 small refactoring 2019-07-15 15:34:51 +02:00
96f8e36c97 adds test 2019-07-15 15:25:49 +02:00
58f39ae5d0 code cleanup, starts testing 2019-07-15 14:59:49 +02:00
5d54d40bec learning how to test 2019-07-12 15:56:16 +02:00
caa8d3fad6 adds docs, tests unpack_gold_value 2019-07-11 15:35:32 +02:00
e08cd64743 enhance responses from API, integrates with frontend 2019-07-11 14:54:18 +02:00
bc65dfcd78 small changes 2019-07-05 15:45:53 +02:00
63c14b4a54 improves progressive rendering, work in progress... 2019-07-05 15:06:16 +02:00
b4692a7a4e fixes Vue reactivity problems, updates REST Api endpoints 2019-07-05 14:18:12 +02:00
5f0e6624e0 going on, reactivity is not properly working 2019-07-04 16:02:05 +02:00
503ebf7b6b updates frontend to use DB api 2019-07-04 14:53:50 +02:00
4e28eb4159 basic impl of players list and loot api in frontend 2019-07-03 16:07:56 +02:00
23 changed files with 3968 additions and 362 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
node_modules
fontawesome
package-lock.json
Cargo.lock
**/*.sqlite3

View File

@@ -6,9 +6,10 @@ edition = "2018"
[dependencies]
dotenv = "*"
diesel_migrations = "*"
serde = "*"
serde_derive = "*"
[dependencies.diesel]
version = "1.0"
version = "1.4"
features = ["sqlite", "r2d2"]

Binary file not shown.

View File

@@ -1,3 +1,8 @@
//! Loot-a-lot Database module
//!
//! # Description
//! This module wraps all needed database operations.
//! It exports a public API for integration with various clients (REST Api, CLI, ...)
extern crate dotenv;
#[macro_use]
extern crate diesel;
@@ -5,40 +10,74 @@ extern crate diesel;
extern crate serde_derive;
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use diesel::query_dsl::RunQueryDsl;
use diesel::r2d2::{self, ConnectionManager};
mod models;
mod transactions;
pub mod models;
mod schema;
use transactions::{DbTransaction};
/// The connection used
pub type DbConnection = SqliteConnection;
/// A pool of connections
pub type Pool = r2d2::Pool<ConnectionManager<DbConnection>>;
/// The result of a query on DB
pub type QueryResult<T> = Result<T, diesel::result::Error>;
pub type ActionResult = QueryResult<bool>;
pub type ActionResult<T> = QueryResult<ActionStatus<T>>;
/// Return status of an Action
#[derive(Serialize, Debug)]
pub struct ActionStatus<R: serde::Serialize> {
/// Has the action made changes ?
pub executed: bool,
/// Response payload
pub response: R,
}
impl ActionStatus<()> {
pub fn was_updated(updated_lines: usize) -> Self {
match updated_lines {
1 => Self::ok(),
_ => Self::nop(),
}
}
pub fn ok() -> ActionStatus<()> {
Self {
executed: true,
response: (),
}
}
}
impl<T: Default + serde::Serialize> ActionStatus<T> {
pub fn nop() -> ActionStatus<T> {
Self {
executed: false,
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
///
/// struct DbApi<'q>(&'q DbConnection);
/// ::new() -> DbApi<'q> (Db finds a connection by itself, usefull for cli)
/// ::with_conn(conn) -> DbApi<'q> (uses a user-defined connection)
/// v .fetch_players()
/// v .fetch_inventory()
/// v .fetch_claims()
/// v .as_player(player_id) -> AsPlayer<'q>
/// v .loot() -> List of items owned (Vec<Item>)
/// 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 .update_wealth(value_in_gold) -> Success status (bool, new_wealth)
/// ```text
/// v .as_player()
/// // Needs an action's history (one entry only should be enough)
/// x .undo_last_action() -> Success status
/// v .as_admin()
/// x .add_loot(identifier, [items_desc]) -> Success status
/// // When adding loot, an identifier should be used to build some kind of history
/// vx .add_loot(identifier, [items_desc]) -> Success status
/// x .sell_loot([players], [excluded_item_ids]) -> Success status (bool, player_share)
/// // Claims should be resolved after a certain delay
/// x .set_claims_timeout()
/// x .resolve_claims()
/// v .add_player(player_data)
/// ```
///
pub struct DbApi<'q>(&'q DbConnection);
@@ -47,43 +86,34 @@ impl<'q> DbApi<'q> {
///
/// # Usage
/// ```
/// let conn = DbConnection::establish();
/// use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// let conn = DbConnection::establish(":memory:").unwrap();
/// let api = DbApi::with_conn(&conn);
/// ```
pub fn with_conn(conn: &'q DbConnection) -> Self {
Self(conn)
}
/// Fetch the list of all players
///
/// This method consumes the DbApi object.
pub fn fetch_players(self) -> QueryResult<Vec<models::Player>> {
Ok(
schema::players::table
.load::<models::Player>(self.0)?
)
Ok(schema::players::table.load::<models::Player>(self.0)?)
}
/// Fetch the inventory of items
///
/// Consumes the DbApi instance
pub fn fetch_inventory(self) -> QueryResult<Vec<models::Item>> {
Ok(
schema::items::table
.load::<models::Item>(self.0)?
)
Ok(schema::items::table.load::<models::Item>(self.0)?)
}
/// Fetch all existing claims
pub fn fetch_claims(self) -> QueryResult<Vec<models::Claim>> {
Ok(
schema::claims::table
.load::<models::Claim>(self.0)?
)
Ok(schema::claims::table.load::<models::Claim>(self.0)?)
}
/// Wrapper for acting as a specific player
///
/// The DbApi is moved inside a new AsPlayer object.
///
/// # Usage
/// ```
/// # use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// # let conn = DbConnection::establish(":memory:").unwrap();
/// # let api = DbApi::with_conn(&conn);
/// let player_id: i32 = 1; // Id that references player in DB
/// let player = api.as_player(player_id);
/// ```
@@ -97,7 +127,7 @@ impl<'q> DbApi<'q> {
}
}
/// A wrapper for interactions of players with the database
/// A wrapper for interactions of players with the database.
/// Possible actions are exposed as methods
pub struct AsPlayer<'q> {
id: i32,
@@ -106,66 +136,162 @@ pub struct AsPlayer<'q> {
impl<'q> AsPlayer<'q> {
/// Fetch the content of a player's chest
///
/// # Usage
/// ```
/// # extern crate diesel_migrations;
/// # use lootalot_db::{DbConnection, DbApi};
/// # use diesel::connection::Connection;
/// # let conn = DbConnection::establish(":memory:").unwrap();
/// # diesel_migrations::run_pending_migrations(&conn).unwrap();
/// # let api = DbApi::with_conn(&conn);
/// // Get loot of player with id of 1
/// let loot = api.as_player(1).loot().unwrap();
/// assert_eq!(format!("{:?}", loot), "[]".to_string());
/// ```
pub fn loot(self) -> QueryResult<Vec<models::Item>> {
Ok(
models::Item::owned_by(self.id)
.load(self.conn)?
)
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
///
/// # Panics
///
/// This currently panics if player wealth fails to be updated, as this is
/// a serious error. TODO: handle deletion of bought item in case of wealth update failure.
pub fn buy<S: Into<String>>(self, name: S, price: i32) -> ActionResult<Option<(i32, i32, i32, i32)>> {
match transactions::player::Buy.execute(
self.conn,
transactions::player::AddLootParams {
player_id: self.id,
loot_name: name.into(),
loot_price: price,
},
) {
Ok(res) => Ok(ActionStatus { executed: true, response: Some(res.loot_cost) }),
Err(e) => { dbg!(&e); Ok(ActionStatus { executed: false, response: None}) },
}
}
/// Sell an item from this player chest
///
/// # Panics
///
/// This currently panics if player wealth fails to be updated, as this is
/// a serious error. TODO: handle restoring of sold item in case of wealth update failure.
pub fn sell(
self,
loot_id: i32,
_price_mod: Option<f32>,
) -> ActionResult<Option<(i32, i32, i32, i32)>> {
// 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 {
return Ok(ActionStatus::nop());
}
transactions::player::Sell.execute(
self.conn,
transactions::player::LootParams {
player_id: self.id,
loot_id,
},
)
.map(|res| ActionStatus { executed: true, response: Some(res.loot_cost) })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop()) })
}
/// Adds the value in gold to the player's wealth.
///
/// Value can be negative to substract wealth.
pub fn update_wealth(self, value_in_gp: f32) -> ActionResult {
use schema::players::dsl::*;
let current_wealth = players
.find(self.id)
.select((cp, sp, gp, pp))
.first::<models::WealthUpdate>(self.conn)?;
// TODO: improve this
// should be move inside a WealthUpdate method
let update = models::WealthUpdate::from_gp(
current_wealth.to_gp() + value_in_gp
);
diesel::update(players)
.filter(id.eq(self.id))
.set(&update)
.execute(self.conn)
// TODO: need to work out what this boolean REALLY means
.map(|r| match r { 1 => true, _ => false })
pub fn update_wealth(self, value_in_gp: f32) -> ActionResult<Option<(i32, i32, i32, i32)>> {
transactions::player::UpdateWealth.execute(
self.conn,
transactions::player::WealthParams {
player_id: self.id,
value_in_gp,
},
)
.map(|res| ActionStatus { executed: true, response: Some(res) })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
}
/// Put a claim on a specific item
pub fn claim(self, item: i32) -> ActionResult {
let request = models::NewClaim { player_id: self.id, loot_id: item };
diesel::insert_into(schema::claims::table)
.values(&request)
.execute(self.conn)
.map(|r| match r { 1 => true, _ => false })
pub fn claim(self, item: i32) -> ActionResult<()> {
let exists: bool =
diesel::select(models::Loot::exists(item)).get_result(self.conn)?;
if !exists {
return Ok(ActionStatus::nop());
};
transactions::player::PutClaim.execute(
self.conn,
transactions::player::LootParams {
player_id: self.id,
loot_id: item,
},
)
.map(|_| ActionStatus { executed: true, response: () })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
}
/// Withdraw claim
pub fn unclaim(self, item: i32) -> ActionResult {
use schema::claims::dsl::*;
diesel::delete(
claims
.filter(loot_id.eq(item))
.filter(player_id.eq(self.id)))
.execute(self.conn)
.map(|r| match r { 1 => true, _ => false })
pub fn unclaim(self, item: i32) -> ActionResult<()> {
transactions::player::WithdrawClaim.execute(
self.conn,
transactions::player::LootParams {
player_id: self.id,
loot_id: item,
},
)
.map(|_| ActionStatus { executed: true, response: () })
.or_else(|e| { dbg!(&e); Ok(ActionStatus::nop())})
}
}
/// Wrapper for interactions of admins with the DB.
pub struct AsAdmin<'q>(&'q DbConnection);
impl<'q> AsAdmin<'q> {
pub fn add_player(self, name: String, start_wealth: f32) -> ActionResult {
/// Adds a player to the database
///
/// Takes the player name and starting wealth (in gold value).
pub fn add_player(self, name: String, start_wealth: f32) -> ActionResult<()> {
diesel::insert_into(schema::players::table)
.values(&models::NewPlayer::create(&name, start_wealth))
.values(&models::player::NewPlayer::create(&name, start_wealth))
.execute(self.0)
.map(|r| match r { 1 => true, _ => false })
.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
pub fn resolve_claims(self) -> ActionResult<()> {
// Fetch all claims, grouped by items.
let loot = models::Loot::owned_by(0).load(self.0)?;
let claims = schema::claims::table
.load::<models::Claim>(self.0)?
.grouped_by(&loot);
// For each claimed item
let data = loot.into_iter().zip(claims).collect::<Vec<_>>();
dbg!(data);
// If mutiples claims -> find highest resolve, give to this player
// If only one claim -> give to claiming
Ok(ActionStatus::nop())
}
}
/// Sets up a connection pool and returns it.
/// Uses the DATABASE_URL environment variable (must be set)
pub fn create_pool() -> Pool {
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
dbg!(&connspec);
@@ -177,5 +303,187 @@ pub fn create_pool() -> Pool {
#[cfg(test)]
mod tests {
use super::*;
type TestConnection = DbConnection;
/// Return a connection to a fresh database (stored in memory)
fn test_connection() -> TestConnection {
let test_conn = DbConnection::establish(":memory:").unwrap();
diesel_migrations::run_pending_migrations(&test_conn).unwrap();
test_conn
}
/// When migrations are run, a special player with id 0 and name "Groupe"
/// must be created.
#[test]
fn test_group_is_autocreated() {
let conn = test_connection();
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
assert_eq!(players.len(), 1);
let group = players.get(0).unwrap();
assert_eq!(group.id, 0);
assert_eq!(group.name, "Groupe".to_string());
}
/// When a player updates wealth, a difference is returned by API.
/// Added to the previous amount of coins, it should equal the updated weath.
#[test]
fn test_player_updates_wealth() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("PlayerName".to_string(), 403.21)
.unwrap();
let diff = DbApi::with_conn(&conn)
.as_player(1)
.update_wealth(-401.21)
.unwrap()
.response
.unwrap();
// Check the returned diff
assert_eq!(diff, (-1, -2, -1, -4));
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
let player = players.get(1).unwrap();
// Check that we can add old value to return diff to get resulting value
assert_eq!(
(player.cp, player.sp, player.gp, player.pp),
(1 + diff.0, 2 + diff.1, 3 + diff.2, 4 + diff.3)
);
}
#[test]
fn test_admin_add_player() {
let conn = test_connection();
let result = DbApi::with_conn(&conn)
.as_admin()
.add_player("PlayerName".to_string(), 403.21)
.unwrap();
assert_eq!(result.executed, true);
let players = DbApi::with_conn(&conn).fetch_players().unwrap();
assert_eq!(players.len(), 2);
let new_player = players.get(1).unwrap();
assert_eq!(new_player.name, "PlayerName");
assert_eq!(
(new_player.cp, new_player.sp, new_player.gp, new_player.pp),
(1, 2, 3, 4)
);
}
#[test]
fn test_admin_resolve_claims() {
let conn = test_connection();
let claims = DbApi::with_conn(&conn).fetch_claims().unwrap();
assert_eq!(claims.len(), 0);
assert_eq!(true, false); // Failing as test is not complete
}
#[test]
fn test_player_claim_item() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("Player".to_string(), 0.0)
.unwrap();
DbApi::with_conn(&conn)
.as_admin()
.add_loot(vec![("Épée", 25)])
.unwrap();
// Claim an existing item
let result = DbApi::with_conn(&conn).as_player(1).claim(1).unwrap();
assert_eq!(result.executed, true);
let claims = DbApi::with_conn(&conn).fetch_claims().unwrap();
assert_eq!(claims.len(), 1);
let claim = claims.get(0).unwrap();
assert_eq!(claim.player_id, 1);
assert_eq!(claim.loot_id, 1);
// Claim an inexistant item
let result = DbApi::with_conn(&conn).as_player(1).claim(2).unwrap();
assert_eq!(result.executed, false);
}
#[test]
fn test_player_unclaim_item() {
let conn = test_connection();
DbApi::with_conn(&conn)
.as_admin()
.add_player("Player".to_string(), 0.0)
.unwrap();
DbApi::with_conn(&conn)
.as_admin()
.add_loot(vec![("Épée", 25)])
.unwrap();
// Claim an existing item
let result = DbApi::with_conn(&conn).as_player(1).claim(1).unwrap();
assert_eq!(result.executed, true);
let result = DbApi::with_conn(&conn).as_player(1).unclaim(1).unwrap();
assert_eq!(result.executed, true);
// Check that unclaimed items will not be unclaimed...
let result = DbApi::with_conn(&conn).as_player(1).unclaim(1).unwrap();
assert_eq!(result.executed, false);
let claims = DbApi::with_conn(&conn).fetch_claims().unwrap();
assert_eq!(claims.len(), 0);
}
/// 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); // Was updated ?
assert_eq!(bought.response, 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);
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);
}
#[test]
fn test_admin_add_loot() {
let conn = test_connection();
assert_eq!(
0,
DbApi::with_conn(&conn).as_player(0).loot().unwrap().len()
);
let loot_to_add = vec![("Cape d'invisibilité", 8000), ("Arc long", 25)];
let result = DbApi::with_conn(&conn)
.as_admin()
.add_loot(loot_to_add.clone())
.unwrap();
assert_eq!(result.executed, true);
let looted = DbApi::with_conn(&conn).as_player(0).loot().unwrap();
assert_eq!(looted.len(), 2);
// NB: Not a problem now, but this adds constraints of items being
// created in the same order.
for (added, to_add) in looted.into_iter().zip(loot_to_add) {
assert_eq!(added.name, to_add.0);
assert_eq!(added.base_price, to_add.1);
}
}
}

View File

@@ -1,17 +1,29 @@
use diesel::prelude::*;
use crate::models::item::Loot;
use crate::schema::claims;
#[derive(Queryable, Serialize, Debug)]
/// A Claim is a request by a single player on an item from group chest.
#[derive(Identifiable, Queryable, Associations, Serialize, Debug)]
#[belongs_to(Loot)]
pub struct Claim {
id: i32,
player_id: i32,
loot_id: i32,
resolve: i32,
/// DB Identifier
pub id: i32,
/// ID that references the player making this claim
pub player_id: i32,
/// ID that references the loot claimed
pub loot_id: i32,
/// WIP: How bad the player wants this item
pub resolve: i32,
}
#[derive(Insertable, Debug)]
#[table_name = "claims"]
pub struct NewClaim {
pub player_id: i32,
pub loot_id: i32,
pub(crate) struct NewClaim {
player_id: i32,
loot_id: i32,
}
impl NewClaim {
pub(crate) fn new(player_id: i32, loot_id: i32) -> Self {
Self { player_id, loot_id }
}
}

View File

@@ -1,12 +1,10 @@
use crate::schema::looted;
use diesel::dsl::{exists, Eq, Filter, Find, Select};
use diesel::expression::exists::Exists;
use diesel::prelude::*;
use diesel::dsl::{Eq, Filter, Select};
use crate::schema::{looted, items};
use crate::DbConnection;
type ItemColumns =
( looted::id, looted::name, looted::base_price, );
const ITEM_COLUMNS: ItemColumns =
( looted::id, looted::name, looted::base_price, );
type ItemColumns = (looted::id, looted::name, looted::base_price);
const ITEM_COLUMNS: ItemColumns = (looted::id, looted::name, looted::base_price);
type OwnedBy = Select<OwnedLoot, ItemColumns>;
/// Represents a unique item in inventory
@@ -16,16 +14,15 @@ type OwnedBy = Select<OwnedLoot, ItemColumns>;
/// 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 {
/// Public proxy for Loot::owned_by that selects only Item fields
pub fn owned_by(player: i32) -> OwnedBy {
Loot::owned_by(player)
.select(ITEM_COLUMNS)
Loot::owned_by(player).select(ITEM_COLUMNS)
}
}
@@ -33,37 +30,48 @@ type WithOwner = Eq<looted::owner_id, i32>;
type OwnedLoot = Filter<looted::table, WithOwner>;
/// Represents an item that has been looted
#[derive(Debug, Queryable, Serialize)]
struct Loot {
#[derive(Identifiable, Debug, Queryable, Serialize)]
#[table_name = "looted"]
pub(crate) struct Loot {
id: i32,
name: String,
base_value: i32,
base_price: i32,
owner: i32,
}
impl Loot {
/// A filter on Loot that is owned by given player
fn owned_by(id: i32) -> OwnedLoot {
looted::table
.filter(looted::owner_id.eq(id))
pub(crate) fn owned_by(id: i32) -> OwnedLoot {
looted::table.filter(looted::owner_id.eq(id))
}
pub(crate) fn owns(player: i32, item: i32) -> Exists<Find<OwnedLoot, i32>> {
exists(Loot::owned_by(player).find(item))
}
pub(crate) fn exists(id: i32) -> Exists<Find<looted::table, i32>> {
exists(looted::table.find(id))
}
}
type ItemDesc<'a> = (&'a str, i32);
/// Description of an item : (name, value in gold)
pub type ItemDesc<'a> = (&'a str, i32);
/// An item being looted or bought.
///
/// The owner is set to 0 in case of looting,
/// to the id of buying player otherwise.
#[derive(Insertable)]
#[table_name = "looted"]
struct NewLoot<'a> {
pub(crate) struct NewLoot<'a> {
name: &'a str,
base_price: i32,
owner_id: i32,
}
impl<'a> NewLoot<'a> {
fn to_group(desc: ItemDesc<'a>) -> Self {
/// A new loot going to the group (loot procedure)
pub(crate) fn to_group(desc: ItemDesc<'a>) -> Self {
Self {
name: desc.0,
base_price: desc.1,
@@ -71,7 +79,8 @@ impl<'a> NewLoot<'a> {
}
}
fn to_player(player: i32, desc: ItemDesc<'a>) -> Self {
/// A new loot going to a specific player (buy procedure)
pub(crate) fn to_player(player: i32, desc: ItemDesc<'a>) -> Self {
Self {
name: desc.0,
base_price: desc.1,

View File

@@ -1,7 +1,8 @@
mod item;
mod player;
mod claim;
pub(super) mod claim;
pub(super) mod item;
pub(super) mod player;
pub use claim::Claim;
pub use item::Item;
pub use player::{Player, NewPlayer, WealthUpdate};
pub use claim::{NewClaim, Claim};
pub(crate) use item::Loot;
pub use player::{Player, Wealth};

View File

@@ -1,20 +1,25 @@
use diesel::prelude::*;
use crate::schema::players;
use crate::DbConnection;
/// Representation of a player in database
#[derive(Debug, Queryable, Serialize)]
pub struct Player {
id: i32,
name: String,
debt: i32,
cp: i32,
sp: i32,
gp: i32,
pp: i32,
/// DB Identitier
pub id: i32,
/// Full name of the character
pub name: String,
/// Amount of gold coins owed to the group.
/// Taking a looted items will increase the debt by it's sell value
pub debt: i32,
/// Count of copper pieces
pub cp: i32,
/// Count of silver pieces
pub sp: i32,
/// Count of gold pieces
pub gp: i32,
/// Count of platinum pieces
pub pp: i32,
}
/// Unpack a floating value in gold pieces to integer
/// values of copper, silver, gold and platinum pieces
///
@@ -33,38 +38,55 @@ fn unpack_gold_value(gold: f32) -> (i32, i32, i32, i32) {
(cp, sp, gp, pp)
}
/// Represent an update on a player's wealth
/// State of a player's wealth
///
/// The values held here are the amount of pieces to add or
/// substract to player wealth.
/// Values are held as individual pieces counts.
/// Allows conversion from and to a floating amount of gold pieces.
#[derive(Queryable, AsChangeset, Debug)]
#[table_name = "players"]
pub struct WealthUpdate {
cp: i32,
sp: i32,
gp: i32,
pp: i32
pub struct Wealth {
pub cp: i32,
pub sp: i32,
pub gp: i32,
pub pp: i32,
}
impl WealthUpdate{
impl Wealth {
/// Unpack individual pieces counts from gold value
///
/// # Examples
/// ```
/// # use lootalot_db::models::Wealth;
/// let wealth = Wealth::from_gp(403.21);
/// assert_eq!(wealth.as_tuple(), (1, 2, 3, 4));
/// ```
pub fn from_gp(gp: f32) -> Self {
let (cp, sp, gp, pp) = unpack_gold_value(gp);
Self { cp, sp, gp, pp }
}
/// Convert total value to a floating value in gold pieces
///
/// # Examples
/// ```
/// # use lootalot_db::models::Wealth;
/// let wealth = Wealth{ pp: 4, gp: 3, sp: 2, cp: 1};
/// assert_eq!(wealth.to_gp(), 403.21);
/// ```
pub fn to_gp(&self) -> f32 {
let i = self.pp * 100 + self.gp;
let f = (self.sp * 10 + self.cp) as f32 / 100.0;
i as f32 + f
}
/// Pack the counts inside a tuple, from lower to higher coin value.
pub fn as_tuple(&self) -> (i32, i32, i32, i32) {
(self.cp, self.sp, self.gp, self.pp)
}
}
/// Representation of a new player record
#[derive(Insertable)]
#[table_name = "players"]
pub struct NewPlayer<'a> {
pub(crate) struct NewPlayer<'a> {
name: &'a str,
cp: i32,
sp: i32,
@@ -73,37 +95,37 @@ pub struct NewPlayer<'a> {
}
impl<'a> NewPlayer<'a> {
pub fn create(name: &'a str, wealth: f32) -> Self {
let wealth = WealthUpdate::from_gp(wealth);
pub(crate) fn create(name: &'a str, wealth_in_gp: f32) -> Self {
let (cp, sp, gp, pp) = Wealth::from_gp(wealth_in_gp).as_tuple();
Self {
name,
cp: wealth.cp,
sp: wealth.sp,
gp: wealth.gp,
pp: wealth.pp,
cp,
sp,
gp,
pp,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests;
#[test]
fn new_player_only_with_name() {
let n = NewPlayer::new("Féfi", None);
assert_eq!(n.name, "Féfi");
assert_eq!(n.cp, 0);
assert_eq!(n.sp, 0);
assert_eq!(n.gp, 0);
assert_eq!(n.pp, 0);
fn test_unpack_gold_values() {
use super::unpack_gold_value;
let test_values = [
(1.0, (0, 0, 1, 0)),
(1.23, (3, 2, 1, 0)),
(1.03, (3, 0, 1, 0)),
(100.23, (3, 2, 0, 1)),
(-100.23, (-3, -2, -0, -1)),
(10189.23, (3, 2, 89, 101)),
(-8090.20, (0, -2, -90, -80)),
];
for (tested, expected) in test_values.into_iter() {
assert_eq!(unpack_gold_value(*tested), *expected);
}
}
#[test]
fn new_player_with_wealth() {
let initial_wealth = (1, 2, 3, 4);
let n = NewPlayer::new("Féfi", Some(initial_wealth));
assert_eq!(initial_wealth, (n.cp, n.sp, n.gp, n.pp));
}
}

View File

@@ -40,9 +40,4 @@ joinable!(claims -> looted (loot_id));
joinable!(claims -> players (player_id));
joinable!(looted -> players (owner_id));
allow_tables_to_appear_in_same_query!(
claims,
items,
looted,
players,
);
allow_tables_to_appear_in_same_query!(claims, items, looted, players,);

View File

@@ -0,0 +1,295 @@
//! TODO:
//! Extract actions provided by API into their dedicated module.
//! Will allow more flexibilty to combinate them inside API methods.
//! Should make it easier to add a new feature : Reverting an action
//!
use crate::models;
use crate::schema;
use crate::DbConnection;
use diesel::prelude::*;
// TODO: revertable actions :
// - Buy
// - Sell
// - UpdateWealth
pub type TransactionResult<T> = Result<T, diesel::result::Error>;
pub trait DbTransaction {
type Params;
type Response: serde::Serialize;
fn execute<'q>(
self,
conn: &'q DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response>;
}
pub trait Revertable : DbTransaction {
fn revert<'q>(
self,
conn: &'q DbConnection,
player_id: i32,
params: <Self as DbTransaction>::Response,
) -> TransactionResult<<Self as DbTransaction>::Response>;
}
/// Return status of an Action
#[derive(Serialize, Debug)]
pub struct ActionStatus<R: serde::Serialize> {
/// Has the action made changes ?
pub executed: bool,
/// Response payload
pub response: R,
}
impl ActionStatus<()> {
pub fn was_updated(updated_lines: usize) -> Self {
match updated_lines {
1 => Self::ok(),
_ => Self::nop(),
}
}
pub fn ok() -> ActionStatus<()> {
Self {
executed: true,
response: (),
}
}
}
impl<T: Default + serde::Serialize> ActionStatus<T> {
pub fn nop() -> ActionStatus<T> {
Self {
executed: false,
response: Default::default(),
}
}
}
// Or a module ?
pub(crate) mod player {
use super::*;
pub struct AddLootParams {
pub player_id: i32,
pub loot_name: String,
pub loot_price: i32,
}
pub struct Buy;
enum LootTransactionKind {
Buy,
Sell,
}
#[derive(Serialize, Debug)]
pub struct LootTransaction {
player_id: i32,
loot_id: i32,
kind: LootTransactionKind,
pub loot_cost: (i32, i32, i32, i32),
}
impl DbTransaction for Buy {
type Params = AddLootParams;
type Response = LootTransaction;
fn execute<'q>(
self,
conn: &'q DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
let added_item = {
let new_item = models::item::NewLoot::to_player(
params.player_id,
(&params.loot_name, params.loot_price),
);
diesel::insert_into(schema::looted::table)
.values(&new_item)
.execute(conn)?
// TODO: return ID of inserted item
};
let updated_wealth = UpdateWealth.execute(
conn,
WealthParams {
player_id: params.player_id,
value_in_gp: -(params.loot_price as f32),
},
);
match (added_item, updated_wealth) {
(1, Ok(loot_cost)) => Ok(LootTransaction {
kind: LootTransactionKind::Buy,
player_id: params.player_id,
loot_id: 0, //TODO: find added item ID
loot_cost,
}),
// TODO: Handle other cases
_ => panic!()
}
}
}
impl Revertable for Buy {
fn revert<'q>(self, conn: &'q DbConnection, player_id: i32, params: <Self as DbTransaction>::Response)
-> TransactionResult<<Self as DbTransaction>::Response> {
unimplemented!()
}
}
pub struct LootParams {
pub player_id: i32,
pub loot_id: i32,
}
pub struct Sell;
impl DbTransaction for Sell {
type Params = LootParams;
type Response = LootTransaction;
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
use schema::looted::dsl::*;
let loot_value = looted
.find(params.loot_id)
.select(base_price)
.first::<i32>(conn)?;
let sell_value = (loot_value / 2) as f32;
diesel::delete(looted.find(params.loot_id))
.execute(conn)
.and_then(|r| match r {
// On deletion, update this player wealth
1 => Ok(UpdateWealth
.execute(
conn,
WealthParams {
player_id: params.player_id,
value_in_gp: sell_value as f32,
},
)
.unwrap()),
_ => Ok(ActionStatus {
executed: false,
response: None,
}),
})
}
}
pub struct PutClaim;
impl DbTransaction for PutClaim {
type Params = LootParams;
type Response = ();
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
let claim = models::claim::NewClaim::new(params.player_id, params.loot_id);
diesel::insert_into(schema::claims::table)
.values(&claim)
.execute(conn)
.and_then(|_| Ok(()))
}
}
pub struct WithdrawClaim;
impl DbTransaction for WithdrawClaim {
type Params = LootParams;
type Response = ();
fn execute<'q>(
self,
conn: &DbConnection,
params: Self::Params,
) -> TransactionResult<Self::Response> {
use schema::claims::dsl::*;
diesel::delete(
claims
.filter(loot_id.eq(params.loot_id))
.filter(player_id.eq(params.player_id)),
)
.execute(conn)
.and_then(|_| Ok(()))
}
}
pub struct WealthParams {
pub player_id: i32,
pub value_in_gp: f32,
}
pub struct UpdateWealth;
impl DbTransaction for UpdateWealth {
type Params = WealthParams;
type Response = (i32, i32, i32, i32);
fn execute<'q>(
self,
conn: &'q DbConnection,
params: WealthParams,
) -> TransactionResult<Self::Response> {
use schema::players::dsl::*;
let current_wealth = players
.find(params.player_id)
.select((cp, sp, gp, pp))
.first::<models::Wealth>(conn)?;
// TODO: improve thisdiesel dependant transaction
// should be move inside a WealthUpdate method
let updated_wealth =
models::Wealth::from_gp(current_wealth.to_gp() + params.value_in_gp);
// Difference in coins that is sent back
let (old, new) = (current_wealth.as_tuple(), updated_wealth.as_tuple());
let diff = (new.0 - old.0, new.1 - old.1, new.2 - old.2, new.3 - old.3);
diesel::update(players)
.filter(id.eq(params.player_id))
.set(&updated_wealth)
.execute(conn)
.and_then(|r| match r {
1 => Ok(diff),
_ => panic!("UpdateWealth made no changes !"),
})
}
}
impl Revertable for UpdateWealth {
fn revert<'q>(
self,
conn: &'q DbConnection,
player_id: i32,
params: <Self as DbTransaction>::Response,
) -> TransactionResult<<Self as DbTransaction>::Response> {
use schema::players::dsl::*;
let cur_wealth = players
.find(player_id)
.select((cp, sp, gp, pp))
.first::<models::Wealth>(conn)?;
let reverted_wealth = models::player::Wealth {
cp: cur_wealth.cp - params.0,
sp: cur_wealth.cp - params.1,
gp: cur_wealth.cp - params.2,
pp: cur_wealth.cp - params.3,
};
// Difference in coins that is sent back
let diff = ( -params.0, -params.1, -params.2, -params.3);
diesel::update(players)
.filter(id.eq(params.0))
.set(&reverted_wealth)
.execute(conn)
.and_then(|r| match r {
1 => Ok(diff),
_ => panic!("RevertableWealthUpdate made no changes"),
})
}
}
}
pub(crate) mod admin {
pub struct AddPlayer;
pub struct AddLoot;
pub struct SellLoot;
pub struct ResolveClaims;
}

View File

@@ -1,5 +1,11 @@
module.exports = {
presets: [
'@vue/app'
]
],
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"presets": [["env", { "targets": { "node": "current" } }]]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"css-watch": "npm run css-build -- --watch",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"test": "jest"
},
"main": "sass/scroll.scss",
"dependencies": {
@@ -19,10 +20,15 @@
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-eslint": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.8.0",
"babel-preset-env": "^1.7.0",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"jest": "^24.8.0",
"node-sass": "^4.12.0",
"vue-jest": "^3.0.4",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
@@ -47,5 +53,16 @@
"browserslist": [
"> 1%",
"last 2 versions"
]
],
"jest": {
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"transform": {
".*\\.(vue)$": "vue-jest",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
}
}
}

View File

@@ -16,7 +16,6 @@ $danger: $red;
$table-cell-border: 1px solid $dark-red;
$table-striped-row-even-background-color: $yellow-light;
$button-padding-horizontal: 1em;
@import "../node_modules/bulma/bulma.sass";

View File

@@ -3,7 +3,7 @@
<section id="content" class="columns is-desktop">
<Player></Player>
<div class="column">
<Chest :player="0"></Chest>
<Chest :player="0" v-if="state.initiated"></Chest>
</div>
</section>
</main>
@@ -32,11 +32,18 @@ function getCookie(cname) {
export default {
name: 'app',
data () {
return {
state: AppStorage.state,
};
},
components: {
Player,
Chest
},
beforeCreate () {
created () {
// Initiate with active player set to value found in cookie
// or as group by default.
const cookie = getCookie("player_id");
let playerId;
if (cookie == "") {
@@ -45,8 +52,7 @@ export default {
playerId = Number(cookie);
}
AppStorage.initStorage(playerId);
return;
}
},
}
</script>

View File

@@ -1,41 +1,102 @@
import Vue from 'vue'
const PLAYER_LIST = [
{id: 0, name: "Groupe", wealth: [0,0,0,0], debt: 0},
{id: 1, name: "Lomion", wealth: [0,0,0,0], debt: 0},
{id: 4, name: "Oilosse", wealth: [0,0,0,0], debt: 0},
{id: 3, name: "Fefi", wealth: [0,0,0,0], debt: 0},
];
const API_BASEURL = "http://localhost:8088/api/"
const API_ENDPOINT = function (tailString) {
return API_BASEURL + tailString;
}
const fetchPlayerList = function () {
fetch("http://localhost:8088/players")
const Api = {
fetchPlayerList () {
return fetch(API_ENDPOINT("players"))
.then(r => r.json())
.then(r => console.log(r));
.catch(e => console.error("Fetch error ", e));
},
fetchClaims () {
return fetch(API_ENDPOINT("claims"))
.then(r => r.json())
.catch(e => console.error("Fetch error ", e));
},
fetchLoot (playerId) {
return fetch(API_ENDPOINT(playerId + "/loot"))
.then(r => r.json())
.catch(e => console.error("Fetch error", e));
},
putClaim (playerId, itemId) {
return fetch(API_ENDPOINT(playerId + "/claim/" + itemId))
.then(r => r.json())
.catch(e => console.error("Fetch error", e));
},
unClaim (playerId, itemId) {
return fetch(API_ENDPOINT(playerId + "/unclaim/" + itemId))
.then(r => r.json())
.catch(e => console.error("Fetch error", e));
},
updateWealth (playerId, goldValue) {
return fetch(API_ENDPOINT(playerId + "/update-wealth/" + goldValue))
.then(r => r.json())
.catch(e => console.error("Fetch error", e));
}
};
const fetchRequests = function () {
};
export const AppStorage = {
debug: true,
state: {
player_id: 0,
player_list: [],
player_list: {},
player_loot: {},
player_claims: {},
initiated: false,
show_player_chest: false,
requests: {},
},
// Initiate the state
initStorage (playerId) {
if (this.debug) console.log('Initiate with player : ', playerId)
fetchPlayerList();
if (this.debug) console.log('Initiates with player : ', playerId)
this.state.player_id = playerId;
for (var idx in PLAYER_LIST) {
var player = PLAYER_LIST[idx];
this.state.player_list.push(player);
this.state.requests[player.id] = [];
// Fetch initial data
return Promise
.all([ Api.fetchPlayerList(), Api.fetchClaims(), ])
.then(data => {
const [players, claims] = data;
this.__initPlayerList(players);
this.__initClaimsStore(claims);
});
// TODO: when __initPlayerList won't use promises
//.then(_ => this.state.initiated = true);
},
__initClaimsStore(data) {
for (var idx in data) {
var claimDesc = data[idx];
this.state.player_claims[claimDesc.player_id].push(claimDesc.loot_id);
}
},
// Player actions
__initPlayerList(data) {
for (var idx in data) {
var playerDesc = data[idx];
const playerId = Number(playerDesc.id);
if (this.debug) console.log("Creates", playerId, playerDesc.name)
// Initiate data for a single Player.
Vue.set(this.state.player_list, playerId, playerDesc);
Vue.set(this.state.player_loot, playerId, []);
Vue.set(this.state.player_claims, playerId, []);
}
// Hack for now !!
// Fetch all players loot and wait to set initiated to true
var promises = [];
for (var idx in data) {
const playerId = data[idx].id;
var promise = Api.fetchLoot(playerId)
.then(data => data.forEach(
item => {
if (this.debug) console.log("add looted item", item, playerId)
this.state.player_loot[playerId].push(item)
}
));
promises.push(promise);
}
Promise.all(promises).then(_ => this.state.initiated = true);
},
// User actions
// Sets a new active player by id
setActivePlayer (newPlayerId) {
if (this.debug) console.log('setActivePlayer to ', newPlayerId)
@@ -47,22 +108,56 @@ export const AppStorage = {
if (this.debug) console.log('switchPlayerChestVisibility', !this.state.show_player_chest)
this.state.show_player_chest = !this.state.show_player_chest
},
// TODO
// get the content of a player Chest, retrieve form cache or fetched
// will replace hack that loads *all* chest...
getPlayerLoot (playerId) {
},
updatePlayerWealth (goldValue) {
return Api.updateWealth(this.state.player_id, goldValue)
.then(done => {
if (done.executed) {
// Update player wealth
var diff = done.response;
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];
}
return done.executed;
});
},
// Put a claim on an item from group chest.
putRequest (itemId) {
const playerId = this.state.player_id
if (this.debug) console.log('newRequest from', playerId, 'on', itemId)
this.state.requests[playerId].push(itemId);
Api.putClaim(playerId, itemId)
.then(done => {
if (done.executed) {
// Update cliend-side state
this.state.player_claims[playerId].push(itemId);
} else {
if (this.debug) console.log("API responded with 'false'")
}
});
},
// Withdraws a claim.
cancelRequest(itemId) {
const playerId = this.state.player_id
if (this.debug) console.log('cancelRequest of', playerId, 'on', itemId)
var idx = this.state.requests[playerId].indexOf(itemId);
Api.unClaim(playerId, itemId)
.then(done => {
if (done.executed) {
var idx = this.state.player_claims[playerId].indexOf(itemId);
if (idx > -1) {
this.state.requests[playerId].splice(idx, 1);
this.state.player_claims[playerId].splice(idx, 1);
} else {
if (this.debug) console.log("cancel a non-existent request")
}
} else {
if (this.debug) console.log("API responded with 'false'")
}
});
}
}

View File

@@ -72,7 +72,7 @@
id="`item-${idx}`"
:value="item.id"
v-model="sell_selected">
{{item.sell_value}} GP
{{item.base_price / 2}} GP
</label>
</div>
<PercentInput></PercentInput>
@@ -118,30 +118,32 @@
data () {
return {
app_state: AppStorage.state,
content: [
{id: 10, name: "Épée longue +2 acérée",
sell_value: 15000 },
{id: 5, name: "Ceinture de force +6",
sell_value: 80000 },
],
is_selling: false,
is_adding: false,
sell_selected: [],
};
},
methods: {
fetchLoot () {
}
},
computed: {
content () {
const playerId = this.player;
console.log("Refresh chest of", playerId);
return this.app_state.player_loot[playerId];
},
// Can the active user sell items from this chest ?
canSell () {
return this.player == this.app_state.player_id;
},
totalSellValue () {
const selected = this.sell_selected;
var value = this.content
return this.content
.filter(item => selected.includes(item.id))
.map(item => item.sell_value)
.map(item => item.base_price / 2)
.reduce((total,value) => total + value, 0);
return value;
},
// Can the user grab items from this chest ?
canGrab () {

View File

@@ -19,8 +19,7 @@
this.has_error = isNaN(newValue);
this.$emit(
'input',
// Do the conversion if valid
this.has_error ? newValue : Number(newValue)
this.has_error ? 0 : Number(newValue)
);
}
},

View File

@@ -2,7 +2,8 @@
<div class="column is-one-third-desktop">
<div id="sidebar" class="card">
<header id="sidebar-heading" class="card-header">
<p class="card-header-title">{{ name }}</p>
<p class="card-header-title">
{{ app_state.initiated ? player.name : "..." }}</p>
<div class="dropdown is-right"
:class="{ 'is-active': show_dropdown }">
<div class="dropdown-trigger" ref="dropdown_btn">
@@ -16,8 +17,8 @@
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu"
v-closable="{ exclude: ['dropdown_btn'], handler: 'closeDropdown', visible: show_dropdown }">
<div class="dropdown-content">
<a v-for="(p,i) in state.player_list" :key="i"
<div class="dropdown-content" v-if="app_state.initiated">
<a v-for="(p,i) in app_state.player_list" :key="i"
@click="setActivePlayer(i)"
href="#" class="dropdown-item">
{{ p.name }}</a>
@@ -26,24 +27,23 @@
</div>
</header>
<div class="card-content">
<Wealth :wealth="player.wealth"
:edit="edit_wealth"
@updated="edit_wealth = !edit_wealth">
</Wealth>
<p class="notification is-warning" v-show="!playerIsGroup">Dette : {{ player.debt }}gp</p>
<Wealth :wealth="wealth" :debt="player.debt"></Wealth>
<div class="box is-shadowless" v-show="!playerIsGroup">
<div class="columns is-vcentered" @click="switchPlayerChestVisibility">
<div class="column is-one-fifth">
<span class="icon is-large">
<i class="fas fa-2x fa-box"></i>
</span>
</div>
<footer class="card-footer">
<a @click="switchPlayerChestVisibility" v-show="!playerIsGroup"
href="#" class="card-footer-item is-dark">
Coffre
</a>
<a @click="edit_wealth = !edit_wealth" href="#" class="card-footer-item">Argent</a>
<a href="#" class="card-footer-item disabled">Historique</a>
</footer>
<div class="column if-four-fifth has-text-left">
<p class="is-size-3">Coffre</p>
</div>
<div class="card" v-show="state.show_player_chest" style="margin-top: 1em;">
<div class="card-content">
<Chest :player="state.player_id"></Chest>
</div>
</div>
<Chest :player="app_state.player_id"
v-show="app_state.show_player_chest">
</Chest>
<a href="#" class="button is-link is-fullwidth is-hidden" disabled>Historique</a>
</div>
</div>
</div>
@@ -74,7 +74,7 @@
components: { Chest, Wealth },
data () {
return {
state: AppStorage.state,
app_state: AppStorage.state,
show_dropdown: false,
edit_wealth: false,
handleOutsideClick: null,
@@ -82,16 +82,24 @@
},
computed: {
player () {
const id = this.state.player_id;
const idx = this.state.player_list.findIndex(p => p.id == id);
return this.state.player_list[idx];
if (!this.app_state.initiated) return {}
const idx = this.app_state.player_id;
return this.app_state.player_list[idx];
},
name () {
return this.player.name;
wealth () {
if (!this.app_state.initiated) {
return ["-", "-", "-", "-"];
} else {
const cp = this.player.cp
const sp = this.player.sp
const gp = this.player.gp
const pp = this.player.pp
return [cp, sp, gp, pp];
}
},
// Check if the active player is the special 'Group' player
playerIsGroup () {
return this.state.player_id == 0;
return this.app_state.player_id == 0;
}
},
methods: {
@@ -99,15 +107,14 @@
AppStorage.switchPlayerChestVisibility();
},
hidePlayerChest () {
if (this.state.show_player_chest) {
if (this.app_state.show_player_chest) {
this.switchPlayerChestVisibility();
}
},
setActivePlayer (playerIdx) {
const newId = this.state.player_list[playerIdx].id;
AppStorage.setActivePlayer(newId);
if (newId == 0) { this.hidePlayerChest() }
this.player.name = this.state.player_list[playerIdx].name
var playerIdx = Number(playerIdx);
AppStorage.setActivePlayer(playerIdx);
if (playerIdx == 0) { this.hidePlayerChest() }
},
closeDropdown () {
this.show_dropdown = false
@@ -154,4 +161,5 @@
</script>
<style scoped>
.fa-exchange-alt.disabled { opacity: 0.4; }
</style>

View File

@@ -35,18 +35,17 @@
data () {
return {
state: AppStorage.state,
_requested: false, // Dummy state
};
},
computed: {
// Check if item is requested by active player
isRequested () {
const reqs = this.state.requests[this.state.player_id];
const reqs = this.state.player_claims[this.state.player_id];
return reqs.includes(this.item);
},
// Check if item is requested by multiple players including active one
isInConflict () {
const reqs = this.state.requests;
const reqs = this.state.player_claims;
const playerId = this.state.player_id;
var reqByPlayer = false;
var reqByOther = false;

View File

@@ -1,42 +1,42 @@
<template>
<div class="box is-primary">
<span class="icon is-large">
<div class="box is-shadowless">
<nav class="columns is-mobile is-multiline is-vcentered">
<div class="column">
<span class="icon is-large"
@click="edit = !edit">
<i class="fas fa-2x fa-piggy-bank"></i>
</span>
<nav class="columns is-mobile is-multiline">
<div class="column">
<p class="heading">PC</p>
<p class="is-size-4">{{ wealth[0] }}</p>
<p v-if="debt" class="has-text-danger">-{{ debt }}gp </p>
</div>
<div class="column">
<p class="heading">PA</p>
<p class="is-size-4">{{ wealth[1] }}</p>
</div>
<div class="column">
<p class="heading">PO</p>
<p class="is-size-4">{{ wealth[2] }}</p>
</div>
<div class="column">
<div class="column has-text-info">
<p class="heading">PP</p>
<p class="is-size-4">{{ wealth[3] }}</p>
</div>
<div class="column has-text-warning">
<p class="heading">PO</p>
<p class="is-size-4">{{ wealth[2] }}</p>
</div>
<div class="column has-text-grey">
<p class="heading">PA</p>
<p class="is-size-4">{{ wealth[1] }}</p>
</div>
<div class="column has-text-grey">
<p class="heading">PC</p>
<p class="is-size-4">{{ wealth[0] }}</p>
</div>
</nav>
<div v-if="edit"> <!-- or v-show ? -->
<nav class="columns is-1 is-variable is-mobile">
<template v-for="i in 4">
<div :key="`input-col-${i}`" class="column">
<NumberInput v-model="edit_values[i - 1]"></NumberInput>
</div>
</template>
</nav>
<nav class="columns is-mobile">
<div class="column is-4 is-offset-2">
<div class="column">
<NumberInput v-model="edit_value"></NumberInput>
</div>
<div class="column is-2">
<button class="button is-outlined is-fullwidth is-danger"
@click="updateWealth('minus')">
<span class="icon"><i class="fas fa-2x fa-minus"></i></span>
</button>
</div>
<div class="column is-4">
<div class="column is-2">
<button class="button is-outlined is-primary is-fullwidth"
@click="updateWealth('plus')">
<span class="icon"><i class="fas fa-2x fa-plus"></i></span>
@@ -48,35 +48,44 @@
</template>
<script>
import { AppStorage } from '../AppStorage.js'
import NumberInput from './NumberInput.vue'
export default {
components: { NumberInput },
props: ["wealth", "edit"],
props: ["wealth", "debt"],
data () {
return {
edit_values: [0,0,0,0],
edit: false,
edit_value: 0,
};
},
methods: {
updateWealth (op) {
const values = this.edit_values;
// Is it optimal, considering NumberInput already validates numbers ?
// Check that all fields are valid numbers
const success = values
.map(v => !isNaN(v))
.reduce(
(t,v) => t && v,
true);
if (success) {
console.log('updated', op, values);
var goldValue;
switch (op) {
case 'plus':
goldValue = this.edit_value;
break;
case 'minus':
goldValue = -this.edit_value;
break;
default:
console.log("Error, bad operator !", op);
return;
}
AppStorage.updatePlayerWealth(goldValue)
.then(done => {
if (done) {
this.$emit('updated');
this.resetValues();
} else {
console.log('correct errors');
}
});
},
resetValues () {
this.edit_values.fill(0);
this.edit = false;
this.edit_value = 0;
}
}
}

View File

@@ -1,6 +1,6 @@
extern crate actix_web;
extern crate dotenv;
extern crate env_logger;
extern crate actix_web;
extern crate lootalot_db;
mod server;
@@ -13,5 +13,5 @@ fn main() {
// Server is started with 'serve' subcommand
// $ lootalot serve
// Start the server.
server::serve();
server::serve().ok();
}

View File

@@ -1,9 +1,9 @@
use std::env;
use futures::Future;
use actix_files as fs;
use actix_web::{web, App, HttpServer, HttpResponse, Error};
use actix_cors::Cors;
use lootalot_db::{Pool, DbApi, QueryResult};
use actix_files as fs;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use futures::Future;
use lootalot_db::{DbApi, Pool, QueryResult};
use std::env;
type AppPool = web::Data<Pool>;
@@ -61,8 +61,10 @@ pub(crate) fn serve() -> std::io::Result<()> {
Cors::new()
.allowed_origin("http://localhost:8080")
.allowed_methods(vec!["GET", "POST"])
.max_age(3600)
.max_age(3600),
)
.service(
web::scope("/api")
.route(
"/players",
web::get().to_async(move |pool: AppPool| {
@@ -71,26 +73,51 @@ pub(crate) fn serve() -> std::io::Result<()> {
)
.route(
"/claims",
web::get().to_async(move |pool: AppPool| db_call(pool, move |api| api.fetch_claims())),
web::get().to_async(move |pool: AppPool| {
db_call(pool, move |api| api.fetch_claims())
}),
)
.route(
"/update-wealth/{player_id}/{amount}",
"/{player_id}/update-wealth/{amount}",
web::get().to_async(move |pool: AppPool, data: web::Path<(i32, f32)>| {
db_call(pool, move |api| api.as_player(data.0).update_wealth(data.1))
}),
)
.route(
"/loot/{player_id}",
"/{player_id}/loot",
web::get().to_async(move |pool: AppPool, player_id: web::Path<i32>| {
db_call(pool, move |api| api.as_player(*player_id).loot())
}),
)
.route(
"/admin/add-player/{name}/{wealth}",
web::get().to_async(move |pool: AppPool, data: web::Path<(String, f32)>| {
db_call(pool, move |api| api.as_admin().add_player(data.0.clone(), data.1))
"/{player_id}/claim/{item_id}",
web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| {
db_call(pool, move |api| api.as_player(data.0).claim(data.1))
}),
)
.route(
"/{player_id}/unclaim/{item_id}",
web::get().to_async(move |pool: AppPool, data: web::Path<(i32, i32)>| {
db_call(pool, move |api| api.as_player(data.0).unclaim(data.1))
}),
)
.route(
"/admin/resolve-claims",
web::get().to_async(move |pool: AppPool| {
db_call(pool, move |api| api.as_admin().resolve_claims())
}),
)
.route(
"/admin/add-player/{name}/{wealth}",
web::get().to_async(
move |pool: AppPool, data: web::Path<(String, f32)>| {
db_call(pool, move |api| {
api.as_admin().add_player(data.0.clone(), data.1)
})
},
),
),
)
.service(fs::Files::new("/", www_root.clone()).index_file("index.html"))
})
.bind("127.0.0.1:8088")?