Merge branch 'xvar/skills' into 'master'

Initial implementation of skills and skill groups

See merge request veloren/veloren!1135
This commit is contained in:
Forest Anderson 2020-07-05 00:33:11 +00:00
commit 7ac5876743
10 changed files with 442 additions and 11 deletions

View File

@ -13,6 +13,7 @@ mod location;
mod phys;
mod player;
pub mod projectile;
pub mod skills;
mod stats;
mod visual;
@ -42,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};

328
common/src/comp/skills.rs Normal file
View File

@ -0,0 +1,328 @@
use lazy_static::lazy_static;
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
/// handled by dedicated ECS systems.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum Skill {
TestT1Skill1,
TestT1Skill2,
TestT1Skill3,
TestT1Skill4,
TestT1Skill5,
TestSwordSkill1,
TestSwordSkill2,
TestSwordSkill3,
TestAxeSkill1,
TestAxeSkill2,
TestAxeSkill3,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkillGroupType {
T1,
Swords,
Axes,
}
/// 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, Eq, PartialEq, Serialize, Deserialize)]
pub struct SkillGroup {
pub skill_group_type: SkillGroupType,
pub exp: u32,
pub available_sp: u8,
}
impl SkillGroup {
fn new(skill_group_type: SkillGroupType) -> SkillGroup {
SkillGroup {
skill_group_type,
exp: 0,
available_sp: 0,
}
}
}
/// 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: Vec<SkillGroup>,
pub skills: HashSet<Skill>,
}
impl Default for SkillSet {
/// Instantiate a new skill set with the default skill groups with no
/// unlocked skills in them - used when adding a skill set to a new
/// player
fn default() -> Self {
// TODO: Default skill groups for new players?
Self {
skill_groups: Vec::new(),
skills: HashSet::new(),
}
}
}
impl SkillSet {
pub fn new() -> Self {
Self {
skill_groups: Vec::new(),
skills: HashSet::new(),
}
}
// TODO: Game design to determine how skill groups are unlocked
/// Unlocks a skill group for a player. It starts with 0 exp and 0 skill
/// points.
///
/// ```
/// use veloren_common::comp::skills::{SkillGroupType, SkillSet};
///
/// let mut skillset = SkillSet::new();
/// skillset.unlock_skill_group(SkillGroupType::Axes);
///
/// assert_eq!(skillset.skill_groups.len(), 1);
/// ```
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");
}
}
/// Unlocks a skill for a player, assuming they have the relevant skill
/// group unlocked and available SP in that skill group.
///
/// ```
/// use veloren_common::comp::skills::{Skill, SkillGroupType, SkillSet};
///
/// let mut skillset = SkillSet::new();
/// skillset.unlock_skill_group(SkillGroupType::Axes);
/// skillset.add_skill_points(SkillGroupType::Axes, 1);
///
/// skillset.unlock_skill(Skill::TestAxeSkill2);
///
/// assert_eq!(skillset.skills.len(), 1);
/// ```
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.available_sp -= 1;
self.skills.insert(skill);
} else {
warn!("Tried to unlock skill for skill group with no available SP");
}
} else {
warn!("Tried to unlock skill for a skill group that player does not have");
}
} else {
warn!(
?skill,
"Tried to unlock skill that does not exist in any skill group!"
);
}
} else {
warn!("Tried to unlock already unlocked skill");
}
}
/// Removes a skill from a player and refunds 1 skill point in the relevant
/// skill group.
///
/// ```
/// use veloren_common::comp::skills::{Skill, SkillGroupType, SkillSet};
///
/// let mut skillset = SkillSet::new();
/// skillset.unlock_skill_group(SkillGroupType::Axes);
/// skillset.add_skill_points(SkillGroupType::Axes, 1);
/// skillset.unlock_skill(Skill::TestAxeSkill2);
///
/// skillset.refund_skill(Skill::TestAxeSkill2);
///
/// assert_eq!(skillset.skills.len(), 0);
/// ```
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;
self.skills.remove(&skill);
} 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
}
})
}
/// Adds skill points to a skill group as long as the player has that skill
/// group type.
///
/// ```
/// use veloren_common::comp::skills::{SkillGroupType, SkillSet};
///
/// let mut skillset = SkillSet::new();
/// skillset.unlock_skill_group(SkillGroupType::Axes);
/// skillset.add_skill_points(SkillGroupType::Axes, 1);
///
/// assert_eq!(skillset.skill_groups[0].available_sp, 1);
/// ```
pub fn add_skill_points(
&mut self,
skill_group_type: SkillGroupType,
number_of_skill_points: u8,
) {
if let Some(mut skill_group) = self
.skill_groups
.iter_mut()
.find(|x| x.skill_group_type == skill_group_type)
{
skill_group.available_sp += number_of_skill_points;
} else {
warn!("Tried to add skill points to a skill group that player does not have");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_refund_skill() {
let mut skillset = SkillSet::new();
skillset.unlock_skill_group(SkillGroupType::Axes);
skillset.add_skill_points(SkillGroupType::Axes, 1);
skillset.unlock_skill(Skill::TestAxeSkill2);
assert_eq!(skillset.skill_groups[0].available_sp, 0);
assert_eq!(skillset.skills.len(), 1);
assert_eq!(
skillset.skills.get(&Skill::TestAxeSkill2),
Some(&Skill::TestAxeSkill2)
);
skillset.refund_skill(Skill::TestAxeSkill2);
assert_eq!(skillset.skill_groups[0].available_sp, 1);
assert_eq!(skillset.skills.get(&Skill::TestAxeSkill2), None);
}
#[test]
fn test_unlock_skillgroup() {
let mut skillset = SkillSet::new();
skillset.unlock_skill_group(SkillGroupType::Axes);
assert_eq!(skillset.skill_groups.len(), 1);
assert_eq!(
skillset.skill_groups[0],
SkillGroup::new(SkillGroupType::Axes)
);
}
#[test]
fn test_unlock_skill() {
let mut skillset = SkillSet::new();
skillset.unlock_skill_group(SkillGroupType::Axes);
skillset.add_skill_points(SkillGroupType::Axes, 1);
assert_eq!(skillset.skill_groups[0].available_sp, 1);
assert_eq!(skillset.skills.len(), 0);
// Try unlocking a skill with enough skill points
skillset.unlock_skill(Skill::TestAxeSkill2);
assert_eq!(skillset.skill_groups[0].available_sp, 0);
assert_eq!(skillset.skills.len(), 1);
assert_eq!(
skillset.skills.get(&Skill::TestAxeSkill2),
Some(&Skill::TestAxeSkill2)
);
// Try unlocking a skill without enough skill points
skillset.unlock_skill(Skill::TestAxeSkill1);
assert_eq!(skillset.skills.len(), 1);
assert_eq!(skillset.skills.get(&Skill::TestAxeSkill1), None);
}
#[test]
fn test_add_skill_points() {
let mut skillset = SkillSet::new();
skillset.unlock_skill_group(SkillGroupType::Axes);
skillset.add_skill_points(SkillGroupType::Axes, 1);
assert_eq!(skillset.skill_groups[0].available_sp, 1);
}
}

View File

@ -1,10 +1,11 @@
use crate::{
comp,
comp::{body::humanoid::Species, Body},
comp::{body::humanoid::Species, skills::SkillSet, Body},
sync::Uid,
};
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use std::{error::Error, fmt};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthChange {
@ -73,7 +74,7 @@ pub enum StatChangeError {
Underflow,
Overflow,
}
use std::{error::Error, fmt};
impl fmt::Display for StatChangeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self {
@ -124,6 +125,7 @@ pub struct Stats {
pub health: Health,
pub level: Level,
pub exp: Exp,
pub skill_set: SkillSet,
pub endurance: u32,
pub fitness: u32,
pub willpower: u32,
@ -179,6 +181,7 @@ impl Stats {
current: 0,
maximum: 50,
},
skill_set: SkillSet::default(),
endurance,
fitness,
willpower,

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,