2020-06-01 21:34:52 +00:00
|
|
|
extern crate serde_json;
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
use super::schema::{body, character, inventory, loadout, stats};
|
2020-05-09 15:41:25 +00:00
|
|
|
use crate::comp;
|
2020-07-06 21:06:03 +00:00
|
|
|
use common::character::Character as CharacterData;
|
2020-06-01 21:34:52 +00:00
|
|
|
use diesel::sql_types::Text;
|
|
|
|
use serde::{Deserialize, Serialize};
|
2020-06-21 14:26:06 +00:00
|
|
|
use tracing::warn;
|
2020-05-09 15:41:25 +00:00
|
|
|
|
2020-05-11 10:06:53 +00:00
|
|
|
/// The required elements to build comp::Stats from database data
|
2020-04-20 08:44:29 +00:00
|
|
|
pub struct StatsJoinData<'a> {
|
2020-05-11 10:06:53 +00:00
|
|
|
pub alias: &'a str,
|
2020-04-20 08:44:29 +00:00
|
|
|
pub body: &'a comp::Body,
|
|
|
|
pub stats: &'a Stats,
|
|
|
|
}
|
|
|
|
|
2020-05-09 15:41:25 +00:00
|
|
|
/// `Character` represents a playable character belonging to a player
|
|
|
|
#[derive(Identifiable, Queryable, Debug)]
|
|
|
|
#[table_name = "character"]
|
|
|
|
pub struct Character {
|
|
|
|
pub id: i32,
|
|
|
|
pub player_uuid: String,
|
|
|
|
pub alias: String,
|
|
|
|
pub tool: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Insertable)]
|
|
|
|
#[table_name = "character"]
|
|
|
|
pub struct NewCharacter<'a> {
|
|
|
|
pub player_uuid: &'a str,
|
|
|
|
pub alias: &'a str,
|
|
|
|
pub tool: Option<&'a str>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&Character> for CharacterData {
|
|
|
|
fn from(character: &Character) -> CharacterData {
|
|
|
|
CharacterData {
|
|
|
|
id: Some(character.id),
|
|
|
|
alias: String::from(&character.alias),
|
|
|
|
tool: character.tool.clone(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
/// `Body` represents the body variety for a character, which has a one-to-one
|
|
|
|
/// relationship with Characters. This data is set during player creation, and
|
|
|
|
/// while there is currently no in-game functionality to modify it, it will
|
|
|
|
/// likely be added in the future.
|
2020-05-09 15:41:25 +00:00
|
|
|
#[derive(Associations, Identifiable, Queryable, Debug, Insertable)]
|
|
|
|
#[belongs_to(Character)]
|
|
|
|
#[primary_key(character_id)]
|
|
|
|
#[table_name = "body"]
|
|
|
|
pub struct Body {
|
|
|
|
pub character_id: i32,
|
2020-05-29 18:23:00 +00:00
|
|
|
pub species: i16,
|
2020-05-09 15:41:25 +00:00
|
|
|
pub body_type: i16,
|
|
|
|
pub hair_style: i16,
|
|
|
|
pub beard: i16,
|
2020-05-29 18:23:00 +00:00
|
|
|
pub eyes: i16,
|
2020-05-09 15:41:25 +00:00
|
|
|
pub accessory: i16,
|
|
|
|
pub hair_color: i16,
|
|
|
|
pub skin: i16,
|
|
|
|
pub eye_color: i16,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&Body> for comp::Body {
|
|
|
|
fn from(body: &Body) -> comp::Body {
|
|
|
|
comp::Body::Humanoid(comp::humanoid::Body {
|
2020-05-29 18:23:00 +00:00
|
|
|
species: comp::humanoid::ALL_SPECIES[body.species as usize],
|
2020-05-09 15:41:25 +00:00
|
|
|
body_type: comp::humanoid::ALL_BODY_TYPES[body.body_type as usize],
|
|
|
|
hair_style: body.hair_style as u8,
|
|
|
|
beard: body.beard as u8,
|
2020-05-29 18:23:00 +00:00
|
|
|
eyes: body.eyes as u8,
|
2020-05-09 15:41:25 +00:00
|
|
|
accessory: body.accessory as u8,
|
|
|
|
hair_color: body.hair_color as u8,
|
|
|
|
skin: body.skin as u8,
|
|
|
|
eye_color: body.eye_color as u8,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-04-20 08:44:29 +00:00
|
|
|
|
2020-06-02 08:16:23 +00:00
|
|
|
/// `Stats` represents the stats for a character, and have a one-to-one
|
|
|
|
/// relationship with `Character`.
|
2020-05-10 16:28:11 +00:00
|
|
|
#[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
|
2020-04-20 08:44:29 +00:00
|
|
|
#[belongs_to(Character)]
|
|
|
|
#[primary_key(character_id)]
|
|
|
|
#[table_name = "stats"]
|
|
|
|
pub struct Stats {
|
|
|
|
pub character_id: i32,
|
|
|
|
pub level: i32,
|
|
|
|
pub exp: i32,
|
|
|
|
pub endurance: i32,
|
|
|
|
pub fitness: i32,
|
|
|
|
pub willpower: i32,
|
2020-07-03 19:40:37 +00:00
|
|
|
pub skills: SkillSetData,
|
2020-04-20 08:44:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<StatsJoinData<'_>> for comp::Stats {
|
|
|
|
fn from(data: StatsJoinData) -> comp::Stats {
|
2020-05-14 03:48:19 +00:00
|
|
|
let level = data.stats.level as u32;
|
|
|
|
|
2020-05-11 10:06:53 +00:00
|
|
|
let mut base_stats = comp::Stats::new(String::from(data.alias), *data.body);
|
2020-04-20 08:44:29 +00:00
|
|
|
|
2020-05-14 03:48:19 +00:00
|
|
|
base_stats.level.set_level(level);
|
|
|
|
base_stats.exp.update_maximum(level);
|
|
|
|
|
2020-04-20 08:44:29 +00:00
|
|
|
base_stats.exp.set_current(data.stats.exp as u32);
|
|
|
|
|
2020-07-09 00:04:25 +00:00
|
|
|
base_stats.update_max_hp(base_stats.body_type);
|
2020-04-25 13:41:27 +00:00
|
|
|
base_stats
|
|
|
|
.health
|
|
|
|
.set_to(base_stats.health.maximum(), comp::HealthSource::Revive);
|
|
|
|
|
2020-04-20 08:44:29 +00:00
|
|
|
base_stats.endurance = data.stats.endurance as u32;
|
|
|
|
base_stats.fitness = data.stats.fitness as u32;
|
|
|
|
base_stats.willpower = data.stats.willpower as u32;
|
2020-07-03 19:40:37 +00:00
|
|
|
base_stats.skill_set = data.stats.skills.0.clone();
|
2020-04-20 08:44:29 +00:00
|
|
|
base_stats
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-11 10:06:53 +00:00
|
|
|
#[derive(AsChangeset, Debug, PartialEq)]
|
2020-04-20 08:44:29 +00:00
|
|
|
#[primary_key(character_id)]
|
|
|
|
#[table_name = "stats"]
|
|
|
|
pub struct StatsUpdate {
|
2020-05-10 16:28:11 +00:00
|
|
|
pub level: i32,
|
|
|
|
pub exp: i32,
|
|
|
|
pub endurance: i32,
|
|
|
|
pub fitness: i32,
|
|
|
|
pub willpower: i32,
|
2020-07-03 19:40:37 +00:00
|
|
|
pub skills: SkillSetData,
|
2020-04-20 08:44:29 +00:00
|
|
|
}
|
2020-05-11 10:06:53 +00:00
|
|
|
|
|
|
|
impl From<&comp::Stats> for StatsUpdate {
|
|
|
|
fn from(stats: &comp::Stats) -> StatsUpdate {
|
|
|
|
StatsUpdate {
|
|
|
|
level: stats.level.level() as i32,
|
|
|
|
exp: stats.exp.current() as i32,
|
|
|
|
endurance: stats.endurance as i32,
|
|
|
|
fitness: stats.fitness as i32,
|
|
|
|
willpower: stats.willpower as i32,
|
2020-07-03 19:40:37 +00:00
|
|
|
skills: SkillSetData(stats.skill_set.clone()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A wrapper type for the SkillSet of a character used to serialise to and from
|
|
|
|
/// JSON If the column contains malformed JSON, a default skillset is returned
|
|
|
|
#[derive(AsExpression, Debug, Deserialize, Serialize, PartialEq, FromSqlRow)]
|
|
|
|
#[sql_type = "Text"]
|
|
|
|
pub struct SkillSetData(pub comp::SkillSet);
|
|
|
|
|
|
|
|
impl<DB> diesel::deserialize::FromSql<Text, DB> for SkillSetData
|
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
String: diesel::deserialize::FromSql<Text, DB>,
|
|
|
|
{
|
|
|
|
fn from_sql(
|
|
|
|
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
|
|
|
|
) -> diesel::deserialize::Result<Self> {
|
|
|
|
let t = String::from_sql(bytes)?;
|
|
|
|
|
|
|
|
match serde_json::from_str(&t) {
|
|
|
|
Ok(data) => Ok(Self(data)),
|
|
|
|
Err(e) => {
|
|
|
|
warn!(?e, "Failed to deserialize skill set data");
|
|
|
|
Ok(Self(comp::SkillSet::default()))
|
|
|
|
},
|
2020-05-11 10:06:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 19:40:37 +00:00
|
|
|
impl<DB> diesel::serialize::ToSql<Text, DB> for SkillSetData
|
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
{
|
|
|
|
fn to_sql<W: std::io::Write>(
|
|
|
|
&self,
|
|
|
|
out: &mut diesel::serialize::Output<W, DB>,
|
|
|
|
) -> diesel::serialize::Result {
|
|
|
|
let s = serde_json::to_string(&self.0)?;
|
|
|
|
<String as diesel::serialize::ToSql<Text, DB>>::to_sql(&s, out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
/// Inventory storage and conversion. Inventories have a one-to-one relationship
|
|
|
|
/// with characters.
|
|
|
|
///
|
2020-06-02 08:16:23 +00:00
|
|
|
/// We store inventory rows as a (character_id, json) tuples, where the json is
|
|
|
|
/// a serialised Inventory component.
|
2020-06-01 21:34:52 +00:00
|
|
|
#[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
|
|
|
|
#[belongs_to(Character)]
|
|
|
|
#[primary_key(character_id)]
|
|
|
|
#[table_name = "inventory"]
|
|
|
|
pub struct Inventory {
|
|
|
|
character_id: i32,
|
|
|
|
items: InventoryData,
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
/// A wrapper type for Inventory components used to serialise to and from JSON
|
|
|
|
/// If the column contains malformed JSON, a default inventory is returned
|
|
|
|
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
|
|
|
#[sql_type = "Text"]
|
|
|
|
pub struct InventoryData(comp::Inventory);
|
|
|
|
|
|
|
|
impl<DB> diesel::deserialize::FromSql<Text, DB> for InventoryData
|
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
String: diesel::deserialize::FromSql<Text, DB>,
|
|
|
|
{
|
|
|
|
fn from_sql(
|
|
|
|
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
|
|
|
|
) -> diesel::deserialize::Result<Self> {
|
|
|
|
let t = String::from_sql(bytes)?;
|
2020-07-06 21:06:03 +00:00
|
|
|
serde_json::from_str(&t).map_err(Box::from)
|
2020-06-04 11:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<DB> diesel::serialize::ToSql<Text, DB> for InventoryData
|
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
{
|
|
|
|
fn to_sql<W: std::io::Write>(
|
|
|
|
&self,
|
|
|
|
out: &mut diesel::serialize::Output<W, DB>,
|
|
|
|
) -> diesel::serialize::Result {
|
|
|
|
let s = serde_json::to_string(&self.0)?;
|
|
|
|
<String as diesel::serialize::ToSql<Text, DB>>::to_sql(&s, out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-01 21:34:52 +00:00
|
|
|
impl From<(i32, comp::Inventory)> for Inventory {
|
|
|
|
fn from(data: (i32, comp::Inventory)) -> Inventory {
|
|
|
|
let (character_id, inventory) = data;
|
|
|
|
|
|
|
|
Inventory {
|
|
|
|
character_id,
|
|
|
|
items: InventoryData(inventory),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<Inventory> for comp::Inventory {
|
|
|
|
fn from(inventory: Inventory) -> comp::Inventory { inventory.items.0 }
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(AsChangeset, Debug, PartialEq)]
|
|
|
|
#[primary_key(character_id)]
|
|
|
|
#[table_name = "inventory"]
|
|
|
|
pub struct InventoryUpdate {
|
|
|
|
pub items: InventoryData,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&comp::Inventory> for InventoryUpdate {
|
|
|
|
fn from(inventory: &comp::Inventory) -> InventoryUpdate {
|
|
|
|
InventoryUpdate {
|
|
|
|
items: InventoryData(inventory.clone()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
/// Loadout holds the armor and weapons owned by a character. This data is
|
|
|
|
/// seperate from the inventory. At the moment, characters have a single Loadout
|
|
|
|
/// which is loaded with their character data, however there are plans for each
|
|
|
|
/// character to have multiple Loadouts which they can switch between during
|
|
|
|
/// gameplay. Due to this Loadouts have a many to one relationship with
|
2020-08-25 12:21:25 +00:00
|
|
|
/// characters, and a distinct `id`.
|
2020-06-04 11:44:33 +00:00
|
|
|
#[derive(Associations, Queryable, Debug, Identifiable)]
|
|
|
|
#[belongs_to(Character)]
|
|
|
|
#[primary_key(id)]
|
|
|
|
#[table_name = "loadout"]
|
|
|
|
pub struct Loadout {
|
|
|
|
pub id: i32,
|
|
|
|
pub character_id: i32,
|
|
|
|
pub items: LoadoutData,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A wrapper type for Loadout components used to serialise to and from JSON
|
|
|
|
/// If the column contains malformed JSON, a default loadout is returned, with
|
|
|
|
/// the starter sword set as the main weapon
|
2020-06-01 21:34:52 +00:00
|
|
|
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
|
|
|
|
#[sql_type = "Text"]
|
2020-06-04 11:44:33 +00:00
|
|
|
pub struct LoadoutData(comp::Loadout);
|
2020-06-01 21:34:52 +00:00
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
impl<DB> diesel::deserialize::FromSql<Text, DB> for LoadoutData
|
2020-06-01 21:34:52 +00:00
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
String: diesel::deserialize::FromSql<Text, DB>,
|
|
|
|
{
|
|
|
|
fn from_sql(
|
|
|
|
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
|
|
|
|
) -> diesel::deserialize::Result<Self> {
|
|
|
|
let t = String::from_sql(bytes)?;
|
2020-07-06 21:06:03 +00:00
|
|
|
serde_json::from_str(&t).map_err(Box::from)
|
2020-06-01 21:34:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
impl<DB> diesel::serialize::ToSql<Text, DB> for LoadoutData
|
2020-06-01 21:34:52 +00:00
|
|
|
where
|
|
|
|
DB: diesel::backend::Backend,
|
|
|
|
{
|
|
|
|
fn to_sql<W: std::io::Write>(
|
|
|
|
&self,
|
|
|
|
out: &mut diesel::serialize::Output<W, DB>,
|
|
|
|
) -> diesel::serialize::Result {
|
|
|
|
let s = serde_json::to_string(&self.0)?;
|
|
|
|
<String as diesel::serialize::ToSql<Text, DB>>::to_sql(&s, out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 11:44:33 +00:00
|
|
|
impl From<&Loadout> for comp::Loadout {
|
|
|
|
fn from(loadout: &Loadout) -> comp::Loadout { loadout.items.0.clone() }
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Insertable, PartialEq, Debug)]
|
|
|
|
#[table_name = "loadout"]
|
|
|
|
pub struct NewLoadout {
|
|
|
|
pub character_id: i32,
|
|
|
|
pub items: LoadoutData,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<(i32, &comp::Loadout)> for NewLoadout {
|
|
|
|
fn from(data: (i32, &comp::Loadout)) -> NewLoadout {
|
|
|
|
let (character_id, loadout) = data;
|
|
|
|
|
|
|
|
NewLoadout {
|
|
|
|
character_id,
|
|
|
|
items: LoadoutData(loadout.clone()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Insertable, PartialEq, Debug, AsChangeset)]
|
|
|
|
#[table_name = "loadout"]
|
|
|
|
pub struct LoadoutUpdate {
|
|
|
|
pub character_id: i32,
|
|
|
|
pub items: LoadoutData,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<(i32, &comp::Loadout)> for LoadoutUpdate {
|
|
|
|
fn from(data: (i32, &comp::Loadout)) -> LoadoutUpdate {
|
|
|
|
let (character_id, loadout) = data;
|
|
|
|
|
|
|
|
LoadoutUpdate {
|
|
|
|
character_id,
|
|
|
|
items: LoadoutData(loadout.clone()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-11 10:06:53 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::comp;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn stats_update_from_stats() {
|
|
|
|
let mut stats = comp::Stats::new(
|
|
|
|
String::from("Test"),
|
|
|
|
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
|
|
|
);
|
|
|
|
|
|
|
|
stats.level.set_level(2);
|
|
|
|
stats.exp.set_current(20);
|
|
|
|
|
|
|
|
stats.endurance = 2;
|
|
|
|
stats.fitness = 3;
|
|
|
|
stats.willpower = 4;
|
|
|
|
|
|
|
|
assert_eq!(StatsUpdate::from(&stats), StatsUpdate {
|
|
|
|
level: 2,
|
|
|
|
exp: 20,
|
|
|
|
endurance: 2,
|
|
|
|
fitness: 3,
|
|
|
|
willpower: 4,
|
2020-07-03 19:40:37 +00:00
|
|
|
skills: SkillSetData(stats.skill_set)
|
2020-05-11 10:06:53 +00:00
|
|
|
})
|
|
|
|
}
|
2020-05-14 03:48:19 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn loads_stats_with_correct_level() {
|
|
|
|
let data = StatsJoinData {
|
|
|
|
alias: "test",
|
|
|
|
body: &comp::Body::from(&Body {
|
|
|
|
character_id: 0,
|
2020-05-29 18:23:00 +00:00
|
|
|
species: 0,
|
2020-05-14 03:48:19 +00:00
|
|
|
body_type: comp::humanoid::BodyType::Female as i16,
|
|
|
|
hair_style: 0,
|
|
|
|
beard: 0,
|
2020-05-29 18:23:00 +00:00
|
|
|
eyes: 0,
|
2020-05-14 03:48:19 +00:00
|
|
|
accessory: 0,
|
|
|
|
hair_color: 0,
|
|
|
|
skin: 0,
|
|
|
|
eye_color: 0,
|
|
|
|
}),
|
|
|
|
stats: &Stats {
|
|
|
|
character_id: 0,
|
|
|
|
level: 3,
|
|
|
|
exp: 70,
|
|
|
|
endurance: 0,
|
|
|
|
fitness: 2,
|
|
|
|
willpower: 3,
|
2020-07-03 19:40:37 +00:00
|
|
|
skills: SkillSetData(comp::SkillSet::new()),
|
2020-05-14 03:48:19 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let stats = comp::Stats::from(data);
|
|
|
|
|
|
|
|
assert_eq!(stats.level.level(), 3);
|
|
|
|
assert_eq!(stats.exp.current(), 70);
|
|
|
|
}
|
2020-05-11 10:06:53 +00:00
|
|
|
}
|