Refactored Skills/SkillGroups structure and implemented JSON persistence

This commit is contained in:
Ben Wallis 2020-07-03 20:40:37 +01:00
parent 60871461ea
commit 538598eb08
9 changed files with 228 additions and 75 deletions

View File

@ -13,7 +13,7 @@ mod location;
mod phys;
mod player;
pub mod projectile;
mod skills;
pub mod skills;
mod stats;
mod visual;
@ -43,5 +43,6 @@ pub use location::{Waypoint, WaypointArea};
pub use phys::{Collider, ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel};
pub use player::Player;
pub use projectile::Projectile;
pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet};
pub use stats::{Exp, HealthChange, HealthSource, Level, Stats};
pub use visual::{LightAnimation, LightEmitter};

View File

@ -1,7 +1,37 @@
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
hash::Hash,
};
use tracing::warn;
lazy_static! {
// Determines the skills that comprise each skill group - this data is used to determine
// which of a player's skill groups a particular skill should be added to when a skill unlock
// is requested. TODO: Externalise this data in a RON file for ease of modification
pub static ref SKILL_GROUP_DEFS: HashMap<SkillGroupType, HashSet<Skill>> = {
let mut defs = HashMap::new();
defs.insert(SkillGroupType::T1, [ Skill::TestT1Skill1,
Skill::TestT1Skill2,
Skill::TestT1Skill3,
Skill::TestT1Skill4,
Skill::TestT1Skill5]
.iter().cloned().collect::<HashSet<Skill>>());
defs.insert(SkillGroupType::Swords, [ Skill::TestSwordSkill1,
Skill::TestSwordSkill2,
Skill::TestSwordSkill3]
.iter().cloned().collect::<HashSet<Skill>>());
defs.insert(SkillGroupType::Axes, [ Skill::TestAxeSkill1,
Skill::TestAxeSkill2,
Skill::TestAxeSkill3]
.iter().cloned().collect::<HashSet<Skill>>());
defs
};
}
/// Represents a skill that a player can unlock, that either grants them some
/// kind of active ability, or a passive effect etc. Obviously because this is
/// an enum it doesn't describe what the skill actually -does-, this will be
@ -31,56 +61,30 @@ pub enum SkillGroupType {
/// A group of skills that have been unlocked by a player. Each skill group has
/// independent exp and skill points which are used to unlock skills in that
/// skill group.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SkillGroup {
pub skills: Vec<Skill>,
pub skill_group_type: SkillGroupType,
pub exp: u32,
pub available_sp: u8,
}
impl Default for SkillGroup {
fn default() -> Self {
Self {
skills: Vec::new(),
impl SkillGroup {
fn new(skill_group_type: SkillGroupType) -> SkillGroup {
SkillGroup {
skill_group_type,
exp: 0,
available_sp: 0,
}
}
}
lazy_static! {
// Determines the skills that comprise each skill group - this data is used to determine
// which of a player's skill groups a particular skill should be added to when a skill unlock
// is requested. TODO: Externalise this data in a RON file for ease of modification
static ref SKILL_GROUP_DEFS: HashMap<SkillGroupType, Vec<Skill>> = {
let mut defs = HashMap::new();
defs.insert(SkillGroupType::T1, vec![
Skill::TestT1Skill1,
Skill::TestT1Skill2,
Skill::TestT1Skill3,
Skill::TestT1Skill4,
Skill::TestT1Skill5]);
defs.insert(SkillGroupType::Swords, vec![
Skill::TestSwordSkill1,
Skill::TestSwordSkill2,
Skill::TestSwordSkill3]);
defs.insert(SkillGroupType::Axes, vec![
Skill::TestAxeSkill1,
Skill::TestAxeSkill2,
Skill::TestAxeSkill3]);
defs
};
}
/// Contains all of a player's skill groups and provides methods for
/// manipulating assigned skills including unlocking skills, refunding skills
/// etc.
#[derive(Clone, Debug, Serialize, Deserialize)]
/// Contains all of a player's skill groups and skills. Provides methods for
/// manipulating assigned skills and skill groups including unlocking skills,
/// refunding skills etc.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct SkillSet {
pub skill_groups: HashMap<SkillGroupType, SkillGroup>,
pub skill_groups: Vec<SkillGroup>,
pub skills: HashSet<Skill>,
}
impl Default for SkillSet {
@ -88,49 +92,99 @@ impl Default for SkillSet {
/// unlocked skills in them - used when adding a skill set to a new
/// player
fn default() -> Self {
let mut skill_groups = HashMap::new();
skill_groups.insert(SkillGroupType::T1, SkillGroup::default());
skill_groups.insert(SkillGroupType::Swords, SkillGroup::default());
skill_groups.insert(SkillGroupType::Axes, SkillGroup::default());
Self { skill_groups }
// TODO: Default skill groups for new players?
Self {
skill_groups: Vec::new(),
skills: HashSet::new(),
}
}
}
impl SkillSet {
pub fn refund_skill(&mut self, _skill: Skill) {
// TODO: check player has skill, remove skill and increase SP in skill group by 1
pub fn new() -> Self {
Self {
skill_groups: Vec::new(),
skills: HashSet::new(),
}
}
pub fn unlock_skill(&mut self, skill: Skill) {
// Find the skill group type for the skill from the static skill definitions
let skill_group_type = SKILL_GROUP_DEFS.iter().find_map(|(key, val)| {
if val.contains(&skill) {
Some(*key)
} else {
None
}
});
// TODO: Game design to determine how skill groups are unlocked
/// Unlocks a skill group for a player, starting with 0 exp and 0 sp
pub fn unlock_skill_group(&mut self, skill_group_type: SkillGroupType) {
if !self
.skill_groups
.iter()
.any(|x| x.skill_group_type == skill_group_type)
{
self.skill_groups.push(SkillGroup::new(skill_group_type));
} else {
warn!("Tried to unlock already known skill group");
}
}
// Find the skill group for the skill on the player, check that the skill is not
// already unlocked and that they have available SP in that group, and then
// allocate the skill and reduce the player's SP in that skill group by 1.
if let Some(skill_group_type) = skill_group_type {
if let Some(skill_group) = self.skill_groups.get_mut(&skill_group_type) {
if !skill_group.skills.contains(&skill) {
/// Unlocks a skill for a player, assuming they have the relevant Skill
/// Group unlocked and available SP in that skill group
pub fn unlock_skill(&mut self, skill: Skill) {
if self.skills.contains(&skill) {
if let Some(skill_group_type) = SkillSet::get_skill_group_type_for_skill(&skill) {
if let Some(mut skill_group) = self
.skill_groups
.iter_mut()
.find(|x| x.skill_group_type == skill_group_type)
{
if skill_group.available_sp > 0 {
skill_group.skills.push(skill);
skill_group.available_sp -= 1;
} else {
warn!("Tried to unlock skill for skill group with no available SP");
}
} else {
warn!("Tried to unlock already unlocked skill");
warn!("Tried to unlock skill for a skill group that player does not have");
}
} else {
warn!("Tried to unlock skill for a skill group that player does not have");
warn!(
?skill,
"Tried to unlock skill that does not exist in any skill group!"
);
}
} else {
warn!("Tried to unlock skill that does not exist in any skill group!");
warn!("Tried to unlock already unlocked skill");
}
}
/// Removes a skill for a player and refunds 1 SP in the relevant Skill
/// Group
pub fn refund_skill(&mut self, skill: Skill) {
if !self.skills.contains(&skill) {
if let Some(skill_group_type) = SkillSet::get_skill_group_type_for_skill(&skill) {
if let Some(mut skill_group) = self
.skill_groups
.iter_mut()
.find(|x| x.skill_group_type == skill_group_type)
{
skill_group.available_sp += 1;
} else {
warn!("Tried to refund skill for a skill group that player does not have");
}
} else {
warn!(
?skill,
"Tried to refund skill that does not exist in any skill group"
)
}
} else {
warn!("Tried to refund skill that has not been unlocked");
}
}
/// Returns the skill group type for a skill from the static skill group
/// definitions
fn get_skill_group_type_for_skill(skill: &Skill) -> Option<SkillGroupType> {
SKILL_GROUP_DEFS.iter().find_map(|(key, val)| {
if val.contains(&skill) {
Some(*key)
} else {
None
}
})
}
}

View File

@ -1,4 +1,8 @@
use crate::{comp, terrain::block::Block};
use crate::{
comp,
comp::{Skill, SkillGroupType},
terrain::block::Block,
};
use vek::*;
/// Messages sent from the client to the server
@ -40,4 +44,7 @@ pub enum ClientMsg {
},
Disconnect,
Terminate,
UnlockSkill(Skill),
RefundSkill(Skill),
UnlockSkillGroup(SkillGroupType),
}

View File

@ -0,0 +1,22 @@
PRAGMA foreign_keys=off;
-- SQLite does not support removing columns from tables so we must rename the current table,
-- recreate the previous version of the table, then copy over the data from the renamed table
ALTER TABLE stats RENAME TO _stats_old;
CREATE TABLE "stats" (
character_id INT NOT NULL PRIMARY KEY,
level INT NOT NULL DEFAULT 1,
exp INT NOT NULL DEFAULT 0,
endurance INT NOT NULL DEFAULT 0,
fitness INT NOT NULL DEFAULT 0,
willpower INT NOT NULL DEFAULT 0,
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE
);
INSERT INTO "stats" (character_id, level, exp, endurance, fitness, willpower)
SELECT character_id, level, exp, endurance, fitness, willpower FROM _stats_old;
DROP TABLE _stats_old;
PRAGMA foreign_keys=on;

View File

@ -0,0 +1,5 @@
ALTER TABLE "stats" ADD COLUMN skills TEXT;
-- Update all existing stats records to "" which will cause characters to be populated
-- with the default skill groups/skills on next login.
UPDATE "stats" SET skills = '""';

View File

@ -16,7 +16,7 @@ use super::{
},
schema,
};
use crate::comp;
use crate::{comp, persistence::models::SkillSetData};
use common::{
character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
LoadoutBuilder,
@ -411,6 +411,7 @@ fn create_character(
endurance: default_stats.endurance as i32,
fitness: default_stats.fitness as i32,
willpower: default_stats.willpower as i32,
skills: SkillSetData(default_stats.skill_set),
};
diesel::insert_into(stats::table)
@ -576,14 +577,16 @@ fn update(
loadout: &LoadoutUpdate,
connection: &SqliteConnection,
) {
// Update Stats
if let Err(e) =
diesel::update(schema::stats::table.filter(schema::stats::character_id.eq(character_id)))
.set(stats)
.execute(connection)
{
warn!(?e, ?character_id, "Failed to update stats for character",)
error!(?e, ?character_id, "Failed to update stats for character",)
}
// Update Inventory
if let Err(e) = diesel::update(
schema::inventory::table.filter(schema::inventory::character_id.eq(character_id)),
)
@ -597,6 +600,7 @@ fn update(
)
}
// Update Loadout
if let Err(e) = diesel::update(
schema::loadout::table.filter(schema::loadout::character_id.eq(character_id)),
)

View File

@ -92,6 +92,7 @@ pub struct Stats {
pub endurance: i32,
pub fitness: i32,
pub willpower: i32,
pub skills: SkillSetData,
}
impl From<StatsJoinData<'_>> for comp::Stats {
@ -113,7 +114,7 @@ impl From<StatsJoinData<'_>> for comp::Stats {
base_stats.endurance = data.stats.endurance as u32;
base_stats.fitness = data.stats.fitness as u32;
base_stats.willpower = data.stats.willpower as u32;
base_stats.skill_set = data.stats.skills.0.clone();
base_stats
}
}
@ -127,6 +128,7 @@ pub struct StatsUpdate {
pub endurance: i32,
pub fitness: i32,
pub willpower: i32,
pub skills: SkillSetData,
}
impl From<&comp::Stats> for StatsUpdate {
@ -137,10 +139,50 @@ impl From<&comp::Stats> for StatsUpdate {
endurance: stats.endurance as i32,
fitness: stats.fitness as i32,
willpower: stats.willpower as i32,
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()))
},
}
}
}
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)
}
}
/// Inventory storage and conversion. Inventories have a one-to-one relationship
/// with characters.
///
@ -354,6 +396,7 @@ mod tests {
endurance: 2,
fitness: 3,
willpower: 4,
skills: SkillSetData(stats.skill_set)
})
}
@ -380,6 +423,7 @@ mod tests {
endurance: 0,
fitness: 2,
willpower: 3,
skills: SkillSetData(comp::SkillSet::new()),
},
};

View File

@ -45,6 +45,7 @@ table! {
endurance -> Integer,
fitness -> Integer,
willpower -> Integer,
skills -> Text,
}
}
@ -53,4 +54,4 @@ joinable!(inventory -> character (character_id));
joinable!(loadout -> character (character_id));
joinable!(stats -> character (character_id));
allow_tables_to_appear_in_same_query!(body, character, inventory, loadout, stats,);
allow_tables_to_appear_in_same_query!(body, character, inventory, loadout, stats);

View File

@ -43,7 +43,7 @@ impl Sys {
uids: &ReadStorage<'_, Uid>,
can_build: &ReadStorage<'_, CanBuild>,
force_updates: &ReadStorage<'_, ForceUpdate>,
stats: &ReadStorage<'_, Stats>,
stats: &mut WriteStorage<'_, Stats>,
chat_modes: &ReadStorage<'_, ChatMode>,
accounts: &mut WriteExpect<'_, AuthProvider>,
block_changes: &mut Write<'_, BlockChange>,
@ -367,6 +367,21 @@ impl Sys {
);
}
},
ClientMsg::UnlockSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill(skill));
},
ClientMsg::RefundSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.refund_skill(skill));
},
ClientMsg::UnlockSkillGroup(skill_group_type) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill_group(skill_group_type));
},
}
}
}
@ -386,7 +401,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Uid>,
ReadStorage<'a, CanBuild>,
ReadStorage<'a, ForceUpdate>,
ReadStorage<'a, Stats>,
WriteStorage<'a, Stats>,
ReadStorage<'a, ChatMode>,
WriteExpect<'a, AuthProvider>,
Write<'a, BlockChange>,
@ -416,7 +431,7 @@ impl<'a> System<'a> for Sys {
uids,
can_build,
force_updates,
stats,
mut stats,
chat_modes,
mut accounts,
mut block_changes,
@ -476,7 +491,7 @@ impl<'a> System<'a> for Sys {
&uids,
&can_build,
&force_updates,
&stats,
&mut stats,
&chat_modes,
&mut accounts,
&mut block_changes,