From 7dca55988597dad02df594976f65d37047b9642f Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 14 Nov 2020 20:05:18 -0600 Subject: [PATCH 01/44] Exp is now awarded to specific skill groups. It's automatically split between a general pool and weapon pools based on if you have the weapon in your loadout and if you've unlocked the weapon pools. --- client/src/lib.rs | 19 ++- common/src/combat.rs | 22 +++- common/src/comp/ability.rs | 4 +- common/src/comp/skills.rs | 146 ++++++++++++++++------- server/src/events/entity_manipulation.rs | 68 ++++++++++- 5 files changed, 212 insertions(+), 47 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 21a645c7eb..466778ab1f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -936,7 +936,24 @@ impl Client { /// Send a chat message to the server. pub fn send_chat(&mut self, message: String) { match validate_chat_msg(&message) { - Ok(()) => self.send_msg(ClientGeneral::ChatMsg(message)), + /* Ok(()) => self.send_msg(ClientGeneral::ChatMsg(message)), */ + Ok(()) => { + if message.starts_with('@') { + if message == "@stats" { + let stats = self + .state + .ecs() + .read_storage::() + .get(self.entity) + .cloned() + .unwrap(); + + tracing::info!("{:?}", stats.skill_set); + } + } else { + self.send_msg(ClientGeneral::ChatMsg(message)) + } + }, Err(ChatMsgValidationError::TooLong) => tracing::warn!( "Attempted to send a message that's too long (Over {} bytes)", MAX_BYTES_CHAT_MSG diff --git a/common/src/combat.rs b/common/src/combat.rs index da56410bed..16c63419a2 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -1,6 +1,6 @@ use crate::{ comp::{ - inventory::item::{armor::Protection, ItemKind}, + inventory::{item::{armor::Protection, tool::ToolKind, ItemKind}, slot::EquipSlot}, BuffKind, HealthChange, HealthSource, Inventory, }, uid::Uid, @@ -195,3 +195,23 @@ impl Knockback { } } } + +pub fn get_weapons(inv: &Inventory) -> (Option, Option) { + ( + inv.equipped(EquipSlot::Mainhand).and_then(|i| { + if let ItemKind::Tool(tool) = &i.kind() { + Some(tool.kind) + } else { + None + } + }), + inv.equipped(EquipSlot::Offhand).and_then(|i| { + if let ItemKind::Tool(tool) = &i.kind() { + Some(tool.kind) + } else { + None + } + }), + + ) +} \ No newline at end of file diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index f6584a385f..653af6e834 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1,8 +1,8 @@ use crate::{ assets::{self, Asset}, comp::{ - projectile::ProjectileConstructor, Body, CharacterState, EnergySource, Gravity, - LightEmitter, StateUpdate, + projectile::ProjectileConstructor, + Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, }, states::{ behavior::JoinData, diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index b31d30dfee..8261231763 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -1,3 +1,4 @@ +use crate::comp::item::tool::ToolKind; use hashbrown::{HashMap, HashSet}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; @@ -10,23 +11,34 @@ lazy_static! { // is requested. TODO: Externalise this data in a RON file for ease of modification pub static ref SKILL_GROUP_DEFS: HashMap> = { let mut defs = HashMap::new(); - defs.insert(SkillGroupType::T1, [ Skill::TestT1Skill1, - Skill::TestT1Skill2, - Skill::TestT1Skill3, - Skill::TestT1Skill4, - Skill::TestT1Skill5] - .iter().cloned().collect::>()); - - defs.insert(SkillGroupType::Swords, [ Skill::TestSwordSkill1, - Skill::TestSwordSkill2, - Skill::TestSwordSkill3] - .iter().cloned().collect::>()); - - defs.insert(SkillGroupType::Axes, [ Skill::TestAxeSkill1, - Skill::TestAxeSkill2, - Skill::TestAxeSkill3] - .iter().cloned().collect::>()); - + defs.insert( + SkillGroupType::General, [ + Skill::General(GeneralSkill::HealthIncrease1), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Sword), [ + Skill::Sword(SwordSkill::UnlockSpin), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Axe), [ + Skill::Axe(AxeSkill::UnlockLeap), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Hammer), [ + Skill::Hammer(HammerSkill::UnlockLeap), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Bow), [ + Skill::Bow(BowSkill::UnlockRepeater), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Staff), [ + Skill::Staff(StaffSkill::UnlockShockwave), + ].iter().cloned().collect::>()); + defs.insert( + SkillGroupType::Weapon(ToolKind::Sceptre), [ + Skill::Sceptre(SceptreSkill::Unlock404), + ].iter().cloned().collect::>()); defs }; } @@ -37,30 +49,66 @@ lazy_static! { /// 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, + General(GeneralSkill), + Sword(SwordSkill), + Axe(AxeSkill), + Hammer(HammerSkill), + Bow(BowSkill), + Staff(StaffSkill), + Sceptre(SceptreSkill), +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum SwordSkill { + UnlockSpin, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum AxeSkill { + UnlockLeap, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum HammerSkill { + UnlockLeap, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum BowSkill { + UnlockRepeater, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum StaffSkill { + UnlockShockwave, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum SceptreSkill { + Unlock404, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum GeneralSkill { + HealthIncrease1, + UnlockSwordTree, + UnlockAxeTree, + UnlockHammerTree, + UnlockBowTree, + UnlockStaffTree, + UnlockSceptreTree, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum SkillGroupType { - T1, - Swords, - Axes, + General, + Weapon(ToolKind), } /// 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)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct SkillGroup { pub skill_group_type: SkillGroupType, pub exp: u32, @@ -93,20 +141,17 @@ impl Default for SkillSet { fn default() -> Self { // TODO: Default skill groups for new players? Self { - skill_groups: Vec::new(), + skill_groups: vec![ + SkillGroup::new(SkillGroupType::General), + SkillGroup::new(SkillGroupType::Weapon(ToolKind::Sword)), + SkillGroup::new(SkillGroupType::Weapon(ToolKind::Bow)), + ], 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. @@ -251,6 +296,27 @@ impl SkillSet { warn!("Tried to add skill points to a skill group that player does not have"); } } + + /// Checks if the skill set of an entity contains a particular skill group + /// type + pub fn contains_skill_group(&self, skill_group_type: SkillGroupType) -> bool { + self.skill_groups + .iter() + .any(|x| x.skill_group_type == skill_group_type) + } + + /// Adds experience to the skill group within an entity's skill set + pub fn add_experience(&mut self, skill_group_type: SkillGroupType, amount: u32) { + if let Some(mut skill_group) = self + .skill_groups + .iter_mut() + .find(|x| x.skill_group_type == skill_group_type) + { + skill_group.exp += amount; + } else { + warn!("Tried to add experience to a skill group that player does not have"); + } + } } #[cfg(test)] diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index dc01768a4e..21d83483c5 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -1,11 +1,12 @@ use crate::{ client::Client, - comp::{biped_large, quadruped_low, quadruped_medium, quadruped_small, theropod, PhysicsState}, + comp::{biped_large, quadruped_low, quadruped_medium, quadruped_small, skills::SkillGroupType, theropod, PhysicsState}, rtsim::RtSim, Server, SpawnPoint, StateExt, }; use common::{ assets::AssetExt, + combat, comp::{ self, aura, buff, chat::{KillSource, KillType}, @@ -29,6 +30,7 @@ use common_sys::state::BlockChange; use comp::item::Reagent; use rand::prelude::*; use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt}; +use std::collections::HashSet; use tracing::error; use vek::Vec3; @@ -223,16 +225,76 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc let exp = exp_reward / (num_not_pets_in_range as f32 + ATTACKER_EXP_WEIGHT); exp_reward = exp * ATTACKER_EXP_WEIGHT; members_in_range.into_iter().for_each(|e| { + let (main_tool_kind, second_tool_kind) = + if let Some(inventory) = state.ecs().read_storage::().get(e) { + combat::get_weapons(inventory) + } else { + (None, None) + }; if let Some(mut stats) = stats.get_mut(e) { - stats.exp.change_by(exp.ceil() as i64); + // stats.exp.change_by(exp.ceil() as i64); + let mut xp_pools = HashSet::::new(); + xp_pools.insert(SkillGroupType::General); + if let Some(w) = main_tool_kind { + if stats + .skill_set + .contains_skill_group(SkillGroupType::Weapon(w)) + { + xp_pools.insert(SkillGroupType::Weapon(w)); + } + } + if let Some(w) = second_tool_kind { + if stats + .skill_set + .contains_skill_group(SkillGroupType::Weapon(w)) + { + xp_pools.insert(SkillGroupType::Weapon(w)); + } + } + let num_pools = xp_pools.len() as f32; + for pool in xp_pools.drain() { + stats + .skill_set + .add_experience(pool, (exp / num_pools).ceil() as u32); + } } }); } + let (main_tool_kind, second_tool_kind) = + if let Some(inventory) = state.ecs().read_storage::().get(attacker) { + combat::get_weapons(inventory) + } else { + (None, None) + }; if let Some(mut attacker_stats) = stats.get_mut(attacker) { // TODO: Discuss whether we should give EXP by Player // Killing or not. - attacker_stats.exp.change_by(exp_reward.ceil() as i64); + // attacker_stats.exp.change_by(exp_reward.ceil() as i64); + let mut xp_pools = HashSet::::new(); + xp_pools.insert(SkillGroupType::General); + if let Some(w) = main_tool_kind { + if attacker_stats + .skill_set + .contains_skill_group(SkillGroupType::Weapon(w)) + { + xp_pools.insert(SkillGroupType::Weapon(w)); + } + } + if let Some(w) = second_tool_kind { + if attacker_stats + .skill_set + .contains_skill_group(SkillGroupType::Weapon(w)) + { + xp_pools.insert(SkillGroupType::Weapon(w)); + } + } + let num_pools = xp_pools.len() as f32; + for pool in xp_pools.drain() { + attacker_stats + .skill_set + .add_experience(pool, (exp_reward / num_pools).ceil() as u32); + } } })(); From 1a15625f12b2e318724bed248435c4a7cfe8882b Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 15 Nov 2020 15:05:02 -0600 Subject: [PATCH 02/44] You now gain skill points after a threshold of xp within a particular skill group. Skills can now unlock skill groups. Temp method of using chat to unlock skills. --- client/src/lib.rs | 52 ++++++++++++++++++++---- common/src/comp/skills.rs | 37 +++++++++-------- common/sys/src/stats.rs | 39 +++++++++--------- server/src/events/entity_manipulation.rs | 4 +- 4 files changed, 85 insertions(+), 47 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 466778ab1f..1331838ab5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -939,16 +939,50 @@ impl Client { /* Ok(()) => self.send_msg(ClientGeneral::ChatMsg(message)), */ Ok(()) => { if message.starts_with('@') { - if message == "@stats" { - let stats = self - .state - .ecs() - .read_storage::() - .get(self.entity) - .cloned() - .unwrap(); + use comp::{item::tool::ToolKind::*, skills::*}; + match message.as_str() { + "@stats" => { + let stats = self + .state + .ecs() + .read_storage::() + .get(self.entity) + .cloned() + .unwrap(); - tracing::info!("{:?}", stats.skill_set); + tracing::info!("{:?}", stats.skill_set); + }, + "@unlock sword" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Sword), + ))); + }, + "@unlock axe" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Axe), + ))); + }, + "@unlock hammer" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Hammer), + ))); + }, + "@unlock bow" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Bow), + ))); + }, + "@unlock staff" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Staff), + ))); + }, + "@unlock sceptre" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( + SkillGroupType::Weapon(Sceptre), + ))); + }, + _ => {}, } } else { self.send_msg(ClientGeneral::ChatMsg(message)) diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 8261231763..720572b176 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -14,6 +14,13 @@ lazy_static! { defs.insert( SkillGroupType::General, [ Skill::General(GeneralSkill::HealthIncrease1), + Skill::General(GeneralSkill::HealthIncrease2), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Sword)), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Axe)), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Hammer)), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Bow)), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Staff)), + Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Sceptre)), ].iter().cloned().collect::>()); defs.insert( SkillGroupType::Weapon(ToolKind::Sword), [ @@ -56,6 +63,7 @@ pub enum Skill { Bow(BowSkill), Staff(StaffSkill), Sceptre(SceptreSkill), + UnlockGroup(SkillGroupType), } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] @@ -91,12 +99,7 @@ pub enum SceptreSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum GeneralSkill { HealthIncrease1, - UnlockSwordTree, - UnlockAxeTree, - UnlockHammerTree, - UnlockBowTree, - UnlockStaffTree, - UnlockSceptreTree, + HealthIncrease2, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] @@ -111,8 +114,8 @@ pub enum SkillGroupType { #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct SkillGroup { pub skill_group_type: SkillGroupType, - pub exp: u32, - pub available_sp: u8, + pub exp: u16, + pub available_sp: u16, } impl SkillGroup { @@ -141,11 +144,7 @@ impl Default for SkillSet { fn default() -> Self { // TODO: Default skill groups for new players? Self { - skill_groups: vec![ - SkillGroup::new(SkillGroupType::General), - SkillGroup::new(SkillGroupType::Weapon(ToolKind::Sword)), - SkillGroup::new(SkillGroupType::Weapon(ToolKind::Bow)), - ], + skill_groups: vec![SkillGroup::new(SkillGroupType::General)], skills: HashSet::new(), } } @@ -200,6 +199,9 @@ impl SkillSet { { if skill_group.available_sp > 0 { skill_group.available_sp -= 1; + if let Skill::UnlockGroup(group) = skill { + self.unlock_skill_group(group); + } self.skills.insert(skill); } else { warn!("Tried to unlock skill for skill group with no available SP"); @@ -284,7 +286,7 @@ impl SkillSet { pub fn add_skill_points( &mut self, skill_group_type: SkillGroupType, - number_of_skill_points: u8, + number_of_skill_points: u16, ) { if let Some(mut skill_group) = self .skill_groups @@ -305,14 +307,15 @@ impl SkillSet { .any(|x| x.skill_group_type == skill_group_type) } - /// Adds experience to the skill group within an entity's skill set - pub fn add_experience(&mut self, skill_group_type: SkillGroupType, amount: u32) { + /// Adds/subtracts experience to the skill group within an entity's skill + /// set + pub fn change_experience(&mut self, skill_group_type: SkillGroupType, amount: i32) { if let Some(mut skill_group) = self .skill_groups .iter_mut() .find(|x| x.skill_group_type == skill_group_type) { - skill_group.exp += amount; + skill_group.exp = (skill_group.exp as i32 + amount) as u16; } else { warn!("Tried to add experience to a skill group that player does not have"); } diff --git a/common/sys/src/stats.rs b/common/sys/src/stats.rs index 0ce26b9b80..5de01c0dbe 100644 --- a/common/sys/src/stats.rs +++ b/common/sys/src/stats.rs @@ -1,11 +1,14 @@ use common::{ - comp::{CharacterState, Energy, EnergyChange, EnergySource, Health, HealthSource, Stats}, + comp::{ + skills::SkillGroupType, CharacterState, Energy, EnergyChange, EnergySource, Health, Stats, + }, event::{EventBus, ServerEvent}, metrics::SysMetrics, resources::DeltaTime, span, }; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; +use std::collections::HashSet; const ENERGY_REGEN_ACCEL: f32 = 10.0; @@ -56,13 +59,9 @@ impl<'a> System<'a> for Sys { ) .join() { - let (set_dead, level_up) = { - let stat = stats.get_unchecked(); + let set_dead = { let health = health.get_unchecked(); - ( - health.should_die() && !health.is_dead, - stat.exp.current() >= stat.exp.maximum(), - ) + health.should_die() && !health.is_dead }; if set_dead { @@ -75,20 +74,22 @@ impl<'a> System<'a> for Sys { health.is_dead = true; } - if level_up { - let mut stat = stats.get_mut_unchecked(); - let stat = &mut *stat; - while stat.exp.current() >= stat.exp.maximum() { - stat.exp.change_by(-(stat.exp.maximum() as i64)); - stat.level.change_by(1); - stat.exp.update_maximum(stat.level.level()); - server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level())); + let mut skills_to_level = HashSet::::new(); + let stat = stats.get_unchecked(); + { + for skill_group in stat.skill_set.skill_groups.iter() { + if skill_group.exp >= 300 { + skills_to_level.insert(skill_group.skill_group_type); + } } + } - let mut health = health.get_mut_unchecked(); - let health = &mut *health; - health.update_max_hp(Some(stat.body_type), stat.level.level()); - health.set_to(health.maximum(), HealthSource::LevelUp); + if !skills_to_level.is_empty() { + let mut stat = stats.get_mut_unchecked(); + for skill_group in skills_to_level.drain() { + stat.skill_set.change_experience(skill_group, -300); + stat.skill_set.add_skill_points(skill_group, 1); + } } } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 21d83483c5..ce0f57a066 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -255,7 +255,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc for pool in xp_pools.drain() { stats .skill_set - .add_experience(pool, (exp / num_pools).ceil() as u32); + .change_experience(pool, (exp / num_pools).ceil() as i32); } } }); @@ -293,7 +293,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc for pool in xp_pools.drain() { attacker_stats .skill_set - .add_experience(pool, (exp_reward / num_pools).ceil() as u32); + .change_experience(pool, (exp_reward / num_pools).ceil() as i32); } } })(); From 80f2a20c3539ceeca7e5fda8aadce0513427bfc0 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 18 Nov 2020 19:26:55 -0600 Subject: [PATCH 03/44] Skills can now have prerequisite skills. Skills can now cost different amounts of skill points. --- client/src/lib.rs | 10 ++++++++ common/src/comp/skills.rs | 53 +++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 1331838ab5..c762dddadf 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -982,6 +982,16 @@ impl Client { SkillGroupType::Weapon(Sceptre), ))); }, + "@unlock health1" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::General( + GeneralSkill::HealthIncrease1, + ))); + }, + "@unlock health2" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::General( + GeneralSkill::HealthIncrease2, + ))); + }, _ => {}, } } else { diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 720572b176..2ad19b15a6 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -192,19 +192,24 @@ impl SkillSet { 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) { + let prerequisites_met = self.prerequisites_met(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; - if let Skill::UnlockGroup(group) = skill { - self.unlock_skill_group(group); + if prerequisites_met { + if skill_group.available_sp >= skill.skill_cost() { + skill_group.available_sp -= skill.skill_cost(); + if let Skill::UnlockGroup(group) = skill { + self.unlock_skill_group(group); + } + self.skills.insert(skill); + } else { + warn!("Tried to unlock skill for skill group with insufficient SP"); } - self.skills.insert(skill); } else { - warn!("Tried to unlock skill for skill group with no available SP"); + warn!("Tried to unlock skill without meeting prerequisite skills"); } } else { warn!("Tried to unlock skill for a skill group that player does not have"); @@ -243,7 +248,7 @@ impl SkillSet { .iter_mut() .find(|x| x.skill_group_type == skill_group_type) { - skill_group.available_sp += 1; + skill_group.available_sp += skill.skill_cost(); self.skills.remove(&skill); } else { warn!("Tried to refund skill for a skill group that player does not have"); @@ -320,6 +325,40 @@ impl SkillSet { warn!("Tried to add experience to a skill group that player does not have"); } } + + /// Checks that the skill set contains all prerequisite skills for a + /// particular skill + pub fn prerequisites_met(&self, skill: Skill) -> bool { + skill + .prerequisite_skills() + .iter() + .all(|s| self.skills.contains(s)) + } +} + +impl Skill { + /// Returns a vec of prerequisite skills (it should only be necessary to + /// note direct prerequisites) + pub fn prerequisite_skills(self) -> Vec { + let mut prerequisites = Vec::new(); + use Skill::*; + match self { + General(GeneralSkill::HealthIncrease2) => { + prerequisites.push(General(GeneralSkill::HealthIncrease1)); + }, + _ => {}, + } + prerequisites + } + + /// Returns the cost in skill points of unlocking a particular skill + pub fn skill_cost(self) -> u16 { + use Skill::*; + match self { + General(GeneralSkill::HealthIncrease2) => 2, + _ => 1, + } + } } #[cfg(test)] From 266bea82982a91f459d5a36c455d0ad2065d8b6d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 4 Dec 2020 17:39:03 -0600 Subject: [PATCH 04/44] Skills can now optionally have levels. Max level prevents adding skill of higher level. Support for skills having a prerequisite of a particular level. --- client/src/lib.rs | 9 +-- common/src/comp/skills.rs | 130 +++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 52 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index c762dddadf..df753605d7 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -982,14 +982,9 @@ impl Client { SkillGroupType::Weapon(Sceptre), ))); }, - "@unlock health1" => { + "@unlock health" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::General( - GeneralSkill::HealthIncrease1, - ))); - }, - "@unlock health2" => { - self.send_msg(ClientGeneral::UnlockSkill(Skill::General( - GeneralSkill::HealthIncrease2, + GeneralSkill::HealthIncrease, ))); }, _ => {}, diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 2ad19b15a6..6bcac6ea75 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -13,8 +13,7 @@ lazy_static! { let mut defs = HashMap::new(); defs.insert( SkillGroupType::General, [ - Skill::General(GeneralSkill::HealthIncrease1), - Skill::General(GeneralSkill::HealthIncrease2), + Skill::General(GeneralSkill::HealthIncrease), Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Sword)), Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Axe)), Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Hammer)), @@ -98,8 +97,7 @@ pub enum SceptreSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum GeneralSkill { - HealthIncrease1, - HealthIncrease2, + HealthIncrease, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] @@ -134,9 +132,11 @@ impl SkillGroup { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct SkillSet { pub skill_groups: Vec, - pub skills: HashSet, + pub skills: HashMap, } +pub type Level = Option; + 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 @@ -145,7 +145,7 @@ impl Default for SkillSet { // TODO: Default skill groups for new players? Self { skill_groups: vec![SkillGroup::new(SkillGroupType::General)], - skills: HashSet::new(), + skills: HashMap::new(), } } } @@ -190,38 +190,39 @@ impl SkillSet { /// 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) { - let prerequisites_met = self.prerequisites_met(skill); - if let Some(mut skill_group) = self - .skill_groups - .iter_mut() - .find(|x| x.skill_group_type == skill_group_type) - { - if prerequisites_met { - if skill_group.available_sp >= skill.skill_cost() { - skill_group.available_sp -= skill.skill_cost(); - if let Skill::UnlockGroup(group) = skill { - self.unlock_skill_group(group); - } - self.skills.insert(skill); - } else { - warn!("Tried to unlock skill for skill group with insufficient SP"); + if let Some(skill_group_type) = SkillSet::get_skill_group_type_for_skill(&skill) { + let level = if self.skills.contains_key(&skill) { + self.skills.get(&skill).copied().flatten().map(|l| l + 1) + } else { + skill.get_max_level().map(|_| 1) + }; + let prerequisites_met = self.prerequisites_met(skill, level); + if let Some(mut skill_group) = self + .skill_groups + .iter_mut() + .find(|x| x.skill_group_type == skill_group_type) + { + if prerequisites_met { + if skill_group.available_sp >= skill.skill_cost(level) { + skill_group.available_sp -= skill.skill_cost(level); + if let Skill::UnlockGroup(group) = skill { + self.unlock_skill_group(group); } + self.skills.insert(skill, level); } else { - warn!("Tried to unlock skill without meeting prerequisite skills"); + warn!("Tried to unlock skill for skill group with insufficient SP"); } } else { - warn!("Tried to unlock skill for a skill group that player does not have"); + warn!("Tried to unlock skill without meeting prerequisite skills"); } } else { - warn!( - ?skill, - "Tried to unlock skill that does not exist in any skill group!" - ); + warn!("Tried to unlock skill for a skill group that player does not have"); } } else { - warn!("Tried to unlock already unlocked skill"); + warn!( + ?skill, + "Tried to unlock skill that does not exist in any skill group!" + ); } } @@ -241,15 +242,21 @@ impl SkillSet { /// assert_eq!(skillset.skills.len(), 0); /// ``` pub fn refund_skill(&mut self, skill: Skill) { - if self.skills.contains(&skill) { + if self.skills.contains_key(&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 += skill.skill_cost(); - self.skills.remove(&skill); + // We know key is already contained, so unwrap is safe + let level = *(self.skills.get(&skill).unwrap()); + skill_group.available_sp += skill.skill_cost(level); + if level.map_or(false, |l| l > 1) { + self.skills.insert(skill, level.map(|l| l - 1)); + } else { + self.skills.remove(&skill); + } } else { warn!("Tried to refund skill for a skill group that player does not have"); } @@ -328,23 +335,46 @@ impl SkillSet { /// Checks that the skill set contains all prerequisite skills for a /// particular skill - pub fn prerequisites_met(&self, skill: Skill) -> bool { - skill - .prerequisite_skills() - .iter() - .all(|s| self.skills.contains(s)) + pub fn prerequisites_met(&self, skill: Skill, level: Level) -> bool { + skill.prerequisite_skills(level).iter().all(|(s, l)| { + self.skills.contains_key(s) && self.skills.get(s).map_or(false, |l_b| l_b >= l) + }) } } impl Skill { /// Returns a vec of prerequisite skills (it should only be necessary to /// note direct prerequisites) - pub fn prerequisite_skills(self) -> Vec { - let mut prerequisites = Vec::new(); + pub fn prerequisite_skills(self, level: Level) -> HashMap { + let mut prerequisites = HashMap::new(); use Skill::*; + if let Some(level) = level { + if level > self.get_max_level().unwrap_or(0) { + // Sets a prerequisite of itself for skills beyond the max level + prerequisites.insert(self, Some(level)); + } else if level > 1 { + // For skills above level 1, sets prerequisite of skill of lower level + prerequisites.insert(self, Some(level - 1)); + } + } match self { - General(GeneralSkill::HealthIncrease2) => { - prerequisites.push(General(GeneralSkill::HealthIncrease1)); + UnlockGroup(SkillGroupType::Weapon(ToolKind::Sword)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); + }, + UnlockGroup(SkillGroupType::Weapon(ToolKind::Axe)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); + }, + UnlockGroup(SkillGroupType::Weapon(ToolKind::Hammer)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); + }, + UnlockGroup(SkillGroupType::Weapon(ToolKind::Bow)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); + }, + UnlockGroup(SkillGroupType::Weapon(ToolKind::Staff)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); + }, + UnlockGroup(SkillGroupType::Weapon(ToolKind::Sceptre)) => { + prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); }, _ => {}, } @@ -352,11 +382,21 @@ impl Skill { } /// Returns the cost in skill points of unlocking a particular skill - pub fn skill_cost(self) -> u16 { + pub fn skill_cost(self, level: Level) -> u16 { use Skill::*; match self { - General(GeneralSkill::HealthIncrease2) => 2, - _ => 1, + General(GeneralSkill::HealthIncrease) => 2 * level.unwrap_or(1), + _ => level.unwrap_or(1), + } + } + + /// Returns the maximum level a skill can reach, returns None if the skill + /// doesn't level + pub fn get_max_level(self) -> Option { + use Skill::*; + match self { + General(GeneralSkill::HealthIncrease) => Some(10), + _ => None, } } } From aceaba847dfb67122a6595bf8c907d27352be2bd Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 5 Dec 2020 16:03:42 -0600 Subject: [PATCH 05/44] Ron file now used for skills belonging to each skill group. --- .../common/skills_skill-groups_manifest.ron | 29 ++++++ common/src/comp/skills.rs | 91 ++++++++----------- 2 files changed, 66 insertions(+), 54 deletions(-) create mode 100644 assets/common/skills_skill-groups_manifest.ron diff --git a/assets/common/skills_skill-groups_manifest.ron b/assets/common/skills_skill-groups_manifest.ron new file mode 100644 index 0000000000..af1fba9b12 --- /dev/null +++ b/assets/common/skills_skill-groups_manifest.ron @@ -0,0 +1,29 @@ +({ + General: [ + General(HealthIncrease), + UnlockGroup(Weapon(Sword)), + UnlockGroup(Weapon(Axe)), + UnlockGroup(Weapon(Hammer)), + UnlockGroup(Weapon(Bow)), + UnlockGroup(Weapon(Staff)), + UnlockGroup(Weapon(Sceptre)), + ], + Weapon(Sword): [ + Sword(UnlockSpin), + ], + Weapon(Axe): [ + Axe(UnlockLeap), + ], + Weapon(Hammer): [ + Hammer(UnlockLeap), + ], + Weapon(Bow): [ + Bow(UnlockRepeater), + ], + Weapon(Staff): [ + Staff(UnlockShockwave), + ], + Weapon(Sceptre): [ + Sceptre(Unlock404), + ], +}) \ No newline at end of file diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 6bcac6ea75..5fe78f2241 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -1,51 +1,30 @@ -use crate::comp::item::tool::ToolKind; +use crate::{ + assets::{self, Asset, AssetExt}, + comp::item::tool::ToolKind, +}; use hashbrown::{HashMap, HashSet}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use std::hash::Hash; use tracing::warn; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SkillMap(HashMap>); + +impl Asset for SkillMap { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + 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 + // is requested. pub static ref SKILL_GROUP_DEFS: HashMap> = { - let mut defs = HashMap::new(); - defs.insert( - SkillGroupType::General, [ - Skill::General(GeneralSkill::HealthIncrease), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Sword)), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Axe)), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Hammer)), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Bow)), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Staff)), - Skill::UnlockGroup(SkillGroupType::Weapon(ToolKind::Sceptre)), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Sword), [ - Skill::Sword(SwordSkill::UnlockSpin), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Axe), [ - Skill::Axe(AxeSkill::UnlockLeap), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Hammer), [ - Skill::Hammer(HammerSkill::UnlockLeap), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Bow), [ - Skill::Bow(BowSkill::UnlockRepeater), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Staff), [ - Skill::Staff(StaffSkill::UnlockShockwave), - ].iter().cloned().collect::>()); - defs.insert( - SkillGroupType::Weapon(ToolKind::Sceptre), [ - Skill::Sceptre(SceptreSkill::Unlock404), - ].iter().cloned().collect::>()); - defs + SkillMap::load_expect_cloned( + "common.skills_skill-groups_manifest", + ).0 }; } @@ -142,7 +121,6 @@ impl Default for SkillSet { /// 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![SkillGroup::new(SkillGroupType::General)], skills: HashMap::new(), @@ -191,32 +169,37 @@ impl SkillSet { /// ``` pub fn unlock_skill(&mut self, skill: Skill) { if let Some(skill_group_type) = SkillSet::get_skill_group_type_for_skill(&skill) { - let level = if self.skills.contains_key(&skill) { + let next_level = if self.skills.contains_key(&skill) { self.skills.get(&skill).copied().flatten().map(|l| l + 1) } else { skill.get_max_level().map(|_| 1) }; - let prerequisites_met = self.prerequisites_met(skill, level); - if let Some(mut skill_group) = self - .skill_groups - .iter_mut() - .find(|x| x.skill_group_type == skill_group_type) + let prerequisites_met = self.prerequisites_met(skill, next_level); + if !self.skills.contains_key(&skill) && !matches!(self.skills.get(&skill), Some(&None)) { - if prerequisites_met { - if skill_group.available_sp >= skill.skill_cost(level) { - skill_group.available_sp -= skill.skill_cost(level); - if let Skill::UnlockGroup(group) = skill { - self.unlock_skill_group(group); + if let Some(mut skill_group) = self + .skill_groups + .iter_mut() + .find(|x| x.skill_group_type == skill_group_type) + { + if prerequisites_met { + if skill_group.available_sp >= skill.skill_cost(next_level) { + skill_group.available_sp -= skill.skill_cost(next_level); + if let Skill::UnlockGroup(group) = skill { + self.unlock_skill_group(group); + } + self.skills.insert(skill, next_level); + } else { + warn!("Tried to unlock skill for skill group with insufficient SP"); } - self.skills.insert(skill, level); } else { - warn!("Tried to unlock skill for skill group with insufficient SP"); + warn!("Tried to unlock skill without meeting prerequisite skills"); } } else { - warn!("Tried to unlock skill without meeting prerequisite skills"); + 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!("Tried to unlock skill the player already has") } } else { warn!( From d691cc9260ad7ae2e8fc90259465bcd8b480a9ea Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 6 Dec 2020 13:10:47 -0600 Subject: [PATCH 06/44] Moved skill max level and skill prerequisites into ron files. --- .../common/skill_trees/skill_max_levels.ron | 14 +++ .../skill_trees/skill_prerequisites.ron | 12 +++ .../skills_skill-groups_manifest.ron | 17 +++- common/src/comp/skills.rs | 95 ++++++++++++------- 4 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 assets/common/skill_trees/skill_max_levels.ron create mode 100644 assets/common/skill_trees/skill_prerequisites.ron rename assets/common/{ => skill_trees}/skills_skill-groups_manifest.ron (59%) diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron new file mode 100644 index 0000000000..e12be824ff --- /dev/null +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -0,0 +1,14 @@ +({ + General(HealthIncrease): Some(10), + Sword(TsDamage): Some(3), + Sword(TsRegen): Some(3), + Sword(TsSpeed): Some(3), + Sword(DCost): Some(2), + Sword(DDrain): Some(2), + Sword(DDamage): Some(2), + Sword(DScaling): Some(3), + Sword(SDamage): Some(2), + Sword(SSpeed): Some(2), + Sword(SCost): Some(2), + Sword(SSpins): Some(2), +}) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron new file mode 100644 index 0000000000..8ef33d2e91 --- /dev/null +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -0,0 +1,12 @@ +({ + UnlockGroup(Weapon(Sword)): {General(HealthIncrease): Some(1)}, + UnlockGroup(Weapon(Axe)): {General(HealthIncrease): Some(1)}, + UnlockGroup(Weapon(Hammer)): {General(HealthIncrease): Some(1)}, + UnlockGroup(Weapon(Bow)): {General(HealthIncrease): Some(1)}, + UnlockGroup(Weapon(Staff)): {General(HealthIncrease): Some(1)}, + UnlockGroup(Weapon(Sceptre)): {General(HealthIncrease): Some(1)}, + Sword(SDamage): {Sword(SUnlockSpin): None}, + Sword(SSpeed): {Sword(SUnlockSpin): None}, + Sword(SCost): {Sword(SUnlockSpin): None}, + Sword(SSpins): {Sword(SUnlockSpin): None}, +}) \ No newline at end of file diff --git a/assets/common/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron similarity index 59% rename from assets/common/skills_skill-groups_manifest.ron rename to assets/common/skill_trees/skills_skill-groups_manifest.ron index af1fba9b12..2a45ca00a1 100644 --- a/assets/common/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -9,7 +9,22 @@ UnlockGroup(Weapon(Sceptre)), ], Weapon(Sword): [ - Sword(UnlockSpin), + Sword(InterruptingAttacks), + Sword(TsCombo), + Sword(TsDamage), + Sword(TsRegen), + Sword(TsSpeed), + Sword(DCost), + Sword(DDrain), + Sword(DDamage), + Sword(DScaling), + Sword(DSpeed), + Sword(DInfinite), + Sword(SUnlockSpin), + Sword(SDamage), + Sword(SSpeed), + Sword(SCost), + Sword(SSpins), ], Weapon(Axe): [ Axe(UnlockLeap), diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 5fe78f2241..4513286398 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -9,9 +9,27 @@ use std::hash::Hash; use tracing::warn; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SkillMap(HashMap>); +pub struct SkillTreeMap(HashMap>); -impl Asset for SkillMap { +impl Asset for SkillTreeMap { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SkillLevelMap(HashMap); + +impl Asset for SkillLevelMap { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SkillPrerequisitesMap(HashMap>); + +impl Asset for SkillPrerequisitesMap { type Loader = assets::RonLoader; const EXTENSION: &'static str = "ron"; @@ -22,8 +40,20 @@ lazy_static! { // which of a player's skill groups a particular skill should be added to when a skill unlock // is requested. pub static ref SKILL_GROUP_DEFS: HashMap> = { - SkillMap::load_expect_cloned( - "common.skills_skill-groups_manifest", + SkillTreeMap::load_expect_cloned( + "common.skill_trees.skills_skill-groups_manifest", + ).0 + }; + // Loads the maximum level that a skill can obtain + pub static ref SKILL_MAX_LEVEL: HashMap = { + SkillLevelMap::load_expect_cloned( + "common.skill_trees.skill_max_levels", + ).0 + }; + // Loads the prerequisite skills for a particular skill + pub static ref SKILL_PREREQUISITES: HashMap> = { + SkillPrerequisitesMap::load_expect_cloned( + "common.skill_trees.skill_prerequisites", ).0 }; } @@ -46,7 +76,26 @@ pub enum Skill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum SwordSkill { - UnlockSpin, + // Sword passives + InterruptingAttacks, + // Triple strike upgrades + TsCombo, + TsDamage, + TsRegen, + TsSpeed, + // Dash upgrades + DCost, + DDrain, + DDamage, + DScaling, + DSpeed, + DInfinite, + // Spin upgrades + SUnlockSpin, + SDamage, + SSpeed, + SCost, + SSpins, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] @@ -175,8 +224,7 @@ impl SkillSet { skill.get_max_level().map(|_| 1) }; let prerequisites_met = self.prerequisites_met(skill, next_level); - if !self.skills.contains_key(&skill) && !matches!(self.skills.get(&skill), Some(&None)) - { + if !matches!(self.skills.get(&skill), Some(&None)) { if let Some(mut skill_group) = self .skill_groups .iter_mut() @@ -330,7 +378,6 @@ impl Skill { /// note direct prerequisites) pub fn prerequisite_skills(self, level: Level) -> HashMap { let mut prerequisites = HashMap::new(); - use Skill::*; if let Some(level) = level { if level > self.get_max_level().unwrap_or(0) { // Sets a prerequisite of itself for skills beyond the max level @@ -340,26 +387,8 @@ impl Skill { prerequisites.insert(self, Some(level - 1)); } } - match self { - UnlockGroup(SkillGroupType::Weapon(ToolKind::Sword)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - UnlockGroup(SkillGroupType::Weapon(ToolKind::Axe)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - UnlockGroup(SkillGroupType::Weapon(ToolKind::Hammer)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - UnlockGroup(SkillGroupType::Weapon(ToolKind::Bow)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - UnlockGroup(SkillGroupType::Weapon(ToolKind::Staff)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - UnlockGroup(SkillGroupType::Weapon(ToolKind::Sceptre)) => { - prerequisites.insert(General(GeneralSkill::HealthIncrease), Some(1)); - }, - _ => {}, + if let Some(skills) = SKILL_PREREQUISITES.get(&self) { + prerequisites.extend(skills); } prerequisites } @@ -368,20 +397,14 @@ impl Skill { pub fn skill_cost(self, level: Level) -> u16 { use Skill::*; match self { - General(GeneralSkill::HealthIncrease) => 2 * level.unwrap_or(1), + General(GeneralSkill::HealthIncrease) => 1, _ => level.unwrap_or(1), } } /// Returns the maximum level a skill can reach, returns None if the skill /// doesn't level - pub fn get_max_level(self) -> Option { - use Skill::*; - match self { - General(GeneralSkill::HealthIncrease) => Some(10), - _ => None, - } - } + pub fn get_max_level(self) -> Option { SKILL_MAX_LEVEL.get(&self).copied().flatten() } } #[cfg(test)] From ba263cf4440ebbf3e919b3e77d4608ac86197e3f Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 6 Dec 2020 21:35:29 -0600 Subject: [PATCH 07/44] Added sword skill tree --- assets/common/abilities/axe/doublestrike.ron | 4 +- .../common/abilities/hammer/singlestrike.ron | 4 +- .../common/abilities/sword/triplestrike.ron | 2 +- .../unique/quadlowbasic/singlestrike.ron | 2 +- .../unique/quadlowbasic/triplestrike.ron | 2 +- .../unique/quadlowbreathe/triplestrike.ron | 2 +- .../unique/quadlowquick/quadstrike.ron | 2 +- .../unique/quadlowranged/singlestrike.ron | 2 +- .../unique/quadlowtail/triplestrike.ron | 2 +- .../unique/quadmedbasic/singlestrike.ron | 2 +- .../unique/quadmedbasic/triplestrike.ron | 2 +- .../unique/quadmedcharge/doublestrike.ron | 2 +- .../unique/quadmedjump/doublestrike.ron | 2 +- .../unique/quadmedquick/triplestrike.ron | 2 +- .../unique/quadsmallbasic/singlestrike.ron | 2 +- .../unique/theropodbasic/singlestrike.ron | 2 +- .../unique/theropodbasic/triplestrike.ron | 2 +- .../unique/theropodbird/singlestrike.ron | 2 +- .../unique/theropodbird/triplestrike.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 2 +- .../skill_trees/skill_prerequisites.ron | 8 ++ client/src/lib.rs | 80 +++++++++++ common/src/combat.rs | 3 +- common/src/comp/ability.rs | 131 +++++++++++++++++- common/src/states/behavior.rs | 5 +- common/src/states/combo_melee.rs | 5 +- common/src/states/utils.rs | 72 ++++++++-- common/sys/src/character_behavior.rs | 7 +- .../audio/sfx/event_mapper/combat/tests.rs | 4 +- 29 files changed, 317 insertions(+), 42 deletions(-) diff --git a/assets/common/abilities/axe/doublestrike.ron b/assets/common/abilities/axe/doublestrike.ron index 1f1bde93a8..cf52d4639f 100644 --- a/assets/common/abilities/axe/doublestrike.ron +++ b/assets/common/abilities/axe/doublestrike.ron @@ -28,8 +28,8 @@ ComboMelee( initial_energy_gain: 25, max_energy_gain: 175, energy_increase: 30, - speed_increase: 0.075, - max_speed_increase: 1.6, + speed_increase: 0.1, + max_speed_increase: 0.6, scales_from_combo: 2, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/hammer/singlestrike.ron b/assets/common/abilities/hammer/singlestrike.ron index dde197a35c..7b36b9ef34 100644 --- a/assets/common/abilities/hammer/singlestrike.ron +++ b/assets/common/abilities/hammer/singlestrike.ron @@ -14,8 +14,8 @@ ComboMelee( initial_energy_gain: 50, max_energy_gain: 150, energy_increase: 50, - speed_increase: 0.05, - max_speed_increase: 1.4, + speed_increase: 0.1, + max_speed_increase: 0.4, scales_from_combo: 2, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/sword/triplestrike.ron b/assets/common/abilities/sword/triplestrike.ron index 979368ac47..ab19e0fd43 100644 --- a/assets/common/abilities/sword/triplestrike.ron +++ b/assets/common/abilities/sword/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 200, energy_increase: 25, speed_increase: 0.1, - max_speed_increase: 1.8, + max_speed_increase: 0.8, scales_from_combo: 2, is_interruptible: true, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadlowbasic/singlestrike.ron b/assets/common/abilities/unique/quadlowbasic/singlestrike.ron index fe7b3efaae..dc6c1cd6b2 100644 --- a/assets/common/abilities/unique/quadlowbasic/singlestrike.ron +++ b/assets/common/abilities/unique/quadlowbasic/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadlowbasic/triplestrike.ron b/assets/common/abilities/unique/quadlowbasic/triplestrike.ron index 792ac133e0..94beb996c4 100644 --- a/assets/common/abilities/unique/quadlowbasic/triplestrike.ron +++ b/assets/common/abilities/unique/quadlowbasic/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadlowbreathe/triplestrike.ron b/assets/common/abilities/unique/quadlowbreathe/triplestrike.ron index f74929c154..dd78e2893c 100644 --- a/assets/common/abilities/unique/quadlowbreathe/triplestrike.ron +++ b/assets/common/abilities/unique/quadlowbreathe/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadlowquick/quadstrike.ron b/assets/common/abilities/unique/quadlowquick/quadstrike.ron index 896ebfb479..36700b4d3a 100644 --- a/assets/common/abilities/unique/quadlowquick/quadstrike.ron +++ b/assets/common/abilities/unique/quadlowquick/quadstrike.ron @@ -53,7 +53,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadlowranged/singlestrike.ron b/assets/common/abilities/unique/quadlowranged/singlestrike.ron index 19ce8936a7..bd24b84ae8 100644 --- a/assets/common/abilities/unique/quadlowranged/singlestrike.ron +++ b/assets/common/abilities/unique/quadlowranged/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadlowtail/triplestrike.ron b/assets/common/abilities/unique/quadlowtail/triplestrike.ron index 432ad28bf5..14b1f9bb86 100644 --- a/assets/common/abilities/unique/quadlowtail/triplestrike.ron +++ b/assets/common/abilities/unique/quadlowtail/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/quadmedbasic/singlestrike.ron b/assets/common/abilities/unique/quadmedbasic/singlestrike.ron index e1e6214838..082dfc218f 100644 --- a/assets/common/abilities/unique/quadmedbasic/singlestrike.ron +++ b/assets/common/abilities/unique/quadmedbasic/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadmedbasic/triplestrike.ron b/assets/common/abilities/unique/quadmedbasic/triplestrike.ron index 2a5d562996..6588856b0d 100644 --- a/assets/common/abilities/unique/quadmedbasic/triplestrike.ron +++ b/assets/common/abilities/unique/quadmedbasic/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadmedcharge/doublestrike.ron b/assets/common/abilities/unique/quadmedcharge/doublestrike.ron index cdb2cced95..51a8bf0985 100644 --- a/assets/common/abilities/unique/quadmedcharge/doublestrike.ron +++ b/assets/common/abilities/unique/quadmedcharge/doublestrike.ron @@ -29,7 +29,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadmedjump/doublestrike.ron b/assets/common/abilities/unique/quadmedjump/doublestrike.ron index e4aabd1196..c2c3fa1f4a 100644 --- a/assets/common/abilities/unique/quadmedjump/doublestrike.ron +++ b/assets/common/abilities/unique/quadmedjump/doublestrike.ron @@ -29,7 +29,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadmedquick/triplestrike.ron b/assets/common/abilities/unique/quadmedquick/triplestrike.ron index 1e1ed5b0a6..06057f3178 100644 --- a/assets/common/abilities/unique/quadmedquick/triplestrike.ron +++ b/assets/common/abilities/unique/quadmedquick/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/quadsmallbasic/singlestrike.ron b/assets/common/abilities/unique/quadsmallbasic/singlestrike.ron index 7ad486426f..3870ba9037 100644 --- a/assets/common/abilities/unique/quadsmallbasic/singlestrike.ron +++ b/assets/common/abilities/unique/quadsmallbasic/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/theropodbasic/singlestrike.ron b/assets/common/abilities/unique/theropodbasic/singlestrike.ron index 9ee86add3c..2e6da823b3 100644 --- a/assets/common/abilities/unique/theropodbasic/singlestrike.ron +++ b/assets/common/abilities/unique/theropodbasic/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/theropodbasic/triplestrike.ron b/assets/common/abilities/unique/theropodbasic/triplestrike.ron index 21bdbbf300..92127a6454 100644 --- a/assets/common/abilities/unique/theropodbasic/triplestrike.ron +++ b/assets/common/abilities/unique/theropodbasic/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/abilities/unique/theropodbird/singlestrike.ron b/assets/common/abilities/unique/theropodbird/singlestrike.ron index e5b0ce3380..0b9a84f80e 100644 --- a/assets/common/abilities/unique/theropodbird/singlestrike.ron +++ b/assets/common/abilities/unique/theropodbird/singlestrike.ron @@ -17,7 +17,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) \ No newline at end of file diff --git a/assets/common/abilities/unique/theropodbird/triplestrike.ron b/assets/common/abilities/unique/theropodbird/triplestrike.ron index edb93af186..cf59834a8b 100644 --- a/assets/common/abilities/unique/theropodbird/triplestrike.ron +++ b/assets/common/abilities/unique/theropodbird/triplestrike.ron @@ -41,7 +41,7 @@ ComboMelee( max_energy_gain: 0, energy_increase: 0, speed_increase: 0.0, - max_speed_increase: 1.0, + max_speed_increase: 0.0, scales_from_combo: 0, is_interruptible: false, ) diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index e12be824ff..1970d5fadb 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -1,7 +1,7 @@ ({ General(HealthIncrease): Some(10), Sword(TsDamage): Some(3), - Sword(TsRegen): Some(3), + Sword(TsRegen): Some(2), Sword(TsSpeed): Some(3), Sword(DCost): Some(2), Sword(DDrain): Some(2), diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index 8ef33d2e91..14e844640d 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -9,4 +9,12 @@ Sword(SSpeed): {Sword(SUnlockSpin): None}, Sword(SCost): {Sword(SUnlockSpin): None}, Sword(SSpins): {Sword(SUnlockSpin): None}, + Sword(DDrain): {Sword(DDamage): Some(1)}, + Sword(DCost): {Sword(DDamage): Some(1)}, + Sword(DSpeed): {Sword(DDamage): Some(1)}, + Sword(DInfinite): {Sword(DDamage): Some(1)}, + Sword(DScaling): {Sword(DDamage): Some(1)}, + Sword(TsDamage): {Sword(TsCombo): None}, + Sword(TsRegen): {Sword(TsCombo): None}, + Sword(TsSpeed): {Sword(TsCombo): None}, }) \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index df753605d7..26265ea8d2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -957,6 +957,86 @@ impl Client { SkillGroupType::Weapon(Sword), ))); }, + "@unlock sword interrupt" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::InterruptingAttacks, + ))); + }, + "@unlock sword combo" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::TsCombo, + ))); + }, + "@unlock sword combo damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::TsDamage, + ))); + }, + "@unlock sword combo regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::TsRegen, + ))); + }, + "@unlock sword combo speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::TsSpeed, + ))); + }, + "@unlock sword dash cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DCost, + ))); + }, + "@unlock sword dash drain" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DDrain, + ))); + }, + "@unlock sword dash damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DDamage, + ))); + }, + "@unlock sword dash scaling" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DScaling, + ))); + }, + "@unlock sword dash speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DSpeed, + ))); + }, + "@unlock sword dash infinite" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::DInfinite, + ))); + }, + "@unlock sword spin unlock" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::SUnlockSpin, + ))); + }, + "@unlock sword spin damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::SDamage, + ))); + }, + "@unlock sword spin speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::SSpeed, + ))); + }, + "@unlock sword spin cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::SCost, + ))); + }, + "@unlock sword spin num" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sword( + SwordSkill::SSpins, + ))); + }, "@unlock axe" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( SkillGroupType::Weapon(Axe), diff --git a/common/src/combat.rs b/common/src/combat.rs index 16c63419a2..f23c8a1525 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -65,9 +65,10 @@ impl Damage { DamageSource::Melee => { // Critical hit let mut critdamage = 0.0; + /* Disabled so I can actually test stuff if rand::random() { critdamage = damage * 0.3; - } + }*/ // Armor damage *= 1.0 - damage_reduction; diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 653af6e834..7dc5718ed8 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1,8 +1,9 @@ use crate::{ assets::{self, Asset}, comp::{ + inventory::item::tool::ToolKind, projectile::ProjectileConstructor, - Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, + skills, Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, }, states::{ behavior::JoinData, @@ -11,6 +12,7 @@ use crate::{ }, Knockback, }; +use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use std::time::Duration; use vek::Vec3; @@ -493,6 +495,131 @@ impl CharacterAbility { _ => 0, } } + + pub fn adjusted_by_skills( + mut self, + skills: &HashMap, + tool: Option, + ) -> Self { + use skills::Skill::*; + use CharacterAbility::*; + if let Some(tool) = tool { + match tool { + ToolKind::Sword => { + use skills::SwordSkill::*; + match self { + ComboMelee { + ref mut is_interruptible, + ref mut speed_increase, + ref mut max_speed_increase, + ref mut stage_data, + ref mut max_energy_gain, + ref mut scales_from_combo, + .. + } => { + *is_interruptible = skills.contains_key(&Sword(InterruptingAttacks)); + let speed_segments = + Sword(TsSpeed).get_max_level().map_or(1, |l| l + 1) as f32; + let speed_level = if skills.contains_key(&Sword(TsCombo)) { + skills + .get(&Sword(TsSpeed)) + .copied() + .flatten() + .map_or(1, |l| l + 1) as f32 + } else { + 0.0 + }; + { + *speed_increase *= speed_level / speed_segments; + *max_speed_increase *= speed_level / speed_segments; + } + let energy_level = if let Some(level) = + skills.get(&Sword(TsRegen)).copied().flatten() + { + level + } else { + 0 + }; + { + *max_energy_gain = (*max_energy_gain as f32 + * ((energy_level + 1) * stage_data.len() as u16 - 1) as f32 + / ((Sword(TsRegen).get_max_level().unwrap() + 1) + * stage_data.len() as u16 + - 1) as f32) + as u32; + } + *scales_from_combo = skills + .get(&Sword(TsDamage)) + .copied() + .flatten() + .unwrap_or(0) + .into(); + }, + DashMelee { + ref mut is_interruptible, + ref mut energy_cost, + ref mut energy_drain, + ref mut base_damage, + ref mut scaled_damage, + ref mut forward_speed, + ref mut infinite_charge, + .. + } => { + *is_interruptible = skills.contains_key(&Sword(InterruptingAttacks)); + if let Some(level) = skills.get(&Sword(DCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sword(DDrain)).copied().flatten() { + *energy_drain = + (*energy_drain as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sword(DDamage)).copied().flatten() { + *base_damage = + (*base_damage as f32 * 1.2_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sword(DScaling)).copied().flatten() { + *scaled_damage = + (*scaled_damage as f32 * 1.2_f32.powi(level.into())) as u32; + } + if skills.contains_key(&Sword(DSpeed)) { + *forward_speed *= 1.5; + } + *infinite_charge = skills.contains_key(&Sword(DInfinite)); + }, + SpinMelee { + ref mut is_interruptible, + ref mut base_damage, + ref mut swing_duration, + ref mut energy_cost, + ref mut num_spins, + .. + } => { + *is_interruptible = skills.contains_key(&Sword(InterruptingAttacks)); + if let Some(level) = skills.get(&Sword(SDamage)).copied().flatten() { + *base_damage = + (*base_damage as f32 * 1.4_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sword(SSpeed)).copied().flatten() { + *swing_duration = + (*swing_duration as f32 * 0.8_f32.powi(level.into())) as u64; + } + if let Some(level) = skills.get(&Sword(SCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + *num_spins = skills.get(&Sword(SSpins)).copied().flatten().unwrap_or(0) + as u32 + + 1; + }, + _ => {}, + } + }, + _ => {}, + } + } + self + } } impl From<(&CharacterAbility, AbilityKey)> for CharacterState { @@ -636,7 +763,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState { max_energy_gain: *max_energy_gain, energy_increase: *energy_increase, speed_increase: 1.0 - *speed_increase, - max_speed_increase: *max_speed_increase - 1.0, + max_speed_increase: *max_speed_increase, scales_from_combo: *scales_from_combo, is_interruptible: *is_interruptible, ability_key: key, diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 8ec1837d32..f2b6f1ed7c 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, - Health, Inventory, Ori, PhysicsState, Pos, StateUpdate, Vel, + Health, Inventory, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel, }, resources::DeltaTime, uid::Uid, @@ -57,6 +57,7 @@ pub struct JoinData<'a> { pub physics: &'a PhysicsState, pub attacking: Option<&'a Attacking>, pub updater: &'a LazyUpdate, + pub stats: &'a Stats, } type RestrictedMut<'a, C> = PairedStorage< @@ -83,6 +84,7 @@ pub type JoinTuple<'a> = ( &'a PhysicsState, Option<&'a Attacking>, Option<&'a Beam>, + &'a Stats, ); impl<'a> JoinData<'a> { @@ -102,6 +104,7 @@ impl<'a> JoinData<'a> { body: j.10, physics: j.11, attacking: j.12, + stats: j.14, updater, dt, } diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs index 1410226cfd..e2e18812ae 100644 --- a/common/src/states/combo_melee.rs +++ b/common/src/states/combo_melee.rs @@ -74,9 +74,10 @@ pub struct StaticData { /// Energy gain increase per combo pub energy_increase: u32, /// (100% - speed_increase) is percentage speed increases from current to - /// max when combo increases + /// max per combo increase pub speed_increase: f32, - /// (100% + max_speed_increase) is the max attack speed + /// This value is the highest percentage speed can increase from the base + /// speed pub max_speed_increase: f32, /// Number of times damage scales with combo pub scales_from_combo: u32, diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ed89a63059..471cddbe3a 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -1,8 +1,10 @@ use crate::{ comp::{ inventory::slot::EquipSlot, - item::{Hands, ItemKind, Tool}, - quadruped_low, quadruped_medium, theropod, Body, CharacterState, StateUpdate, + item::{Hands, ItemKind, Tool, ToolKind}, + quadruped_low, quadruped_medium, + skills::{Skill, SwordSkill}, + theropod, Body, CharacterState, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, event::LocalEvent, @@ -386,10 +388,19 @@ pub fn handle_ability1_input(data: &JoinData, update: &mut StateUpdate) { if let Some(ability) = data .inventory .equipped(EquipSlot::Mainhand) - .and_then(|i| i.item_config_expect().ability1.as_ref()) + .and_then(|i| { + i.item_config_expect().ability1.as_ref().map(|a| { + let tool = match i.kind() { + ItemKind::Tool(tool) => Some(tool.kind), + _ => None, + }; + a.clone() + .adjusted_by_skills(&data.stats.skill_set.skills, tool) + }) + }) .filter(|ability| ability.requirements_paid(data, update)) { - update.character = (ability, AbilityKey::Mouse1).into(); + update.character = (&ability, AbilityKey::Mouse1).into(); } } } @@ -422,20 +433,38 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) { if let Some(ability) = data .inventory .equipped(EquipSlot::Mainhand) - .and_then(|i| i.item_config_expect().ability2.as_ref()) + .and_then(|i| { + i.item_config_expect().ability2.as_ref().map(|a| { + let tool = match i.kind() { + ItemKind::Tool(tool) => Some(tool.kind), + _ => None, + }; + a.clone() + .adjusted_by_skills(&data.stats.skill_set.skills, tool) + }) + }) .filter(|ability| ability.requirements_paid(data, update)) { - update.character = (ability, AbilityKey::Mouse2).into(); + update.character = (&ability, AbilityKey::Mouse2).into(); } }, (_, Some(Hands::OneHand)) => { if let Some(ability) = data .inventory .equipped(EquipSlot::Offhand) - .and_then(|i| i.item_config_expect().ability2.as_ref()) + .and_then(|i| { + i.item_config_expect().ability2.as_ref().map(|a| { + let tool = match i.kind() { + ItemKind::Tool(tool) => Some(tool.kind), + _ => None, + }; + a.clone() + .adjusted_by_skills(&data.stats.skill_set.skills, tool) + }) + }) .filter(|ability| ability.requirements_paid(data, update)) { - update.character = (ability, AbilityKey::Mouse2).into(); + update.character = (&ability, AbilityKey::Mouse2).into(); } }, (_, _) => {}, @@ -448,10 +477,33 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { if let Some(ability) = data .inventory .equipped(EquipSlot::Mainhand) - .and_then(|i| i.item_config_expect().ability3.as_ref()) + .and_then(|i| { + let tool = match i.kind() { + ItemKind::Tool(tool) => Some(tool.kind), + _ => None, + }; + i.item_config_expect().ability3 + .as_ref() + .and_then(|s| match tool { + Some(ToolKind::Sword) + if !&data + .stats + .skill_set + .skills + .contains_key(&Skill::Sword(SwordSkill::SUnlockSpin)) => + { + None + }, + _ => Some(s), + }) + .map(|a| { + a.clone() + .adjusted_by_skills(&data.stats.skill_set.skills, tool) + }) + }) .filter(|ability| ability.requirements_paid(data, update)) { - update.character = (ability, AbilityKey::Skill1).into(); + update.character = (&ability, AbilityKey::Skill1).into(); } } } diff --git a/common/sys/src/character_behavior.rs b/common/sys/src/character_behavior.rs index b019e10ba9..5bd0340452 100644 --- a/common/sys/src/character_behavior.rs +++ b/common/sys/src/character_behavior.rs @@ -3,8 +3,8 @@ use specs::{Entities, Join, LazyUpdate, Read, ReadExpect, ReadStorage, System, W use common::{ comp::{ inventory::slot::{EquipSlot, Slot}, - Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Mounting, - Ori, PhysicsState, Pos, StateUpdate, Vel, + Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Mounting, Ori, + PhysicsState, Pos, StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, @@ -71,6 +71,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Beam>, ReadStorage<'a, Uid>, ReadStorage<'a, Mounting>, + ReadStorage<'a, Stats>, ); #[allow(clippy::while_let_on_iterator)] // TODO: Pending review in #587 @@ -98,6 +99,7 @@ impl<'a> System<'a> for Sys { beam_storage, uids, mountings, + stats, ): Self::SystemData, ) { let start_time = std::time::Instant::now(); @@ -120,6 +122,7 @@ impl<'a> System<'a> for Sys { &physics_states, attacking_storage.maybe(), beam_storage.maybe(), + &stats, ) .join() { diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs index f71a1745bf..3a0f3c1dd8 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs @@ -126,7 +126,7 @@ fn matches_ability_stage() { max_energy_gain: 100, energy_increase: 20, speed_increase: 0.05, - max_speed_increase: 1.8, + max_speed_increase: 0.8, scales_from_combo: 2, is_interruptible: true, ability_key: states::utils::AbilityKey::Mouse1, @@ -183,7 +183,7 @@ fn ignores_different_ability_stage() { max_energy_gain: 100, energy_increase: 20, speed_increase: 0.05, - max_speed_increase: 1.8, + max_speed_increase: 0.8, scales_from_combo: 2, is_interruptible: true, ability_key: states::utils::AbilityKey::Mouse1, From 3dd530d49aae985885f0c849af7666f22fae0be5 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 13 Dec 2020 11:21:51 -0600 Subject: [PATCH 08/44] Added persistence for skill trees. --- common/sys/src/stats.rs | 2 +- server/src/events/entity_manipulation.rs | 2 +- server/src/persistence/character.rs | 63 ++++++++++++-- .../src/persistence/character/conversions.rs | 84 ++++++++++++++++++- server/src/persistence/models.rs | 21 ++++- server/src/persistence/schema.rs | 18 ++++ 6 files changed, 180 insertions(+), 10 deletions(-) diff --git a/common/sys/src/stats.rs b/common/sys/src/stats.rs index 5de01c0dbe..943e6d0e0f 100644 --- a/common/sys/src/stats.rs +++ b/common/sys/src/stats.rs @@ -7,8 +7,8 @@ use common::{ resources::DeltaTime, span, }; +use hashbrown::HashSet; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; -use std::collections::HashSet; const ENERGY_REGEN_ACCEL: f32 = 10.0; diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index ce0f57a066..e7cdf99f01 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -28,9 +28,9 @@ use common_net::{ }; use common_sys::state::BlockChange; use comp::item::Reagent; +use hashbrown::HashSet; use rand::prelude::*; use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt}; -use std::collections::HashSet; use tracing::error; use vek::Vec3; diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index b9281ab569..3088a008e2 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -15,6 +15,7 @@ use crate::{ convert_body_from_database, convert_body_to_database_json, convert_character_from_database, convert_inventory_from_database_items, convert_items_to_database_items, convert_loadout_from_database_items, + convert_skill_groups_to_database, convert_skills_to_database, convert_stats_from_database, convert_stats_to_database, convert_waypoint_from_database_json, }, @@ -58,7 +59,9 @@ pub fn load_character_data( char_id: CharacterId, connection: VelorenTransaction, ) -> CharacterDataResult { - use schema::{body::dsl::*, character::dsl::*, item::dsl::*}; + use schema::{ + body::dsl::*, character::dsl::*, item::dsl::*, skill_group::dsl::*, + }; let character_containers = get_pseudo_containers(connection, char_id)?; @@ -100,9 +103,22 @@ pub fn load_character_data( }, }); + let skill_data = schema::skill::dsl::skill + .filter(schema::skill::dsl::character_id.eq(char_id)) + .load::(&*connection)?; + + let skill_group_data = skill_group + .filter(schema::skill_group::dsl::character_id.eq(char_id)) + .load::(&*connection)?; + Ok(( convert_body_from_database(&char_body)?, - convert_stats_from_database(&stats_data, character_data.alias), + convert_stats_from_database( + &stats_data, + character_data.alias, + &skill_data, + &skill_group_data, + ), convert_inventory_from_database_items(&inventory_items, &loadout_items)?, char_waypoint, )) @@ -172,7 +188,7 @@ pub fn create_character( check_character_limit(uuid, connection)?; - use schema::{body, character}; + use schema::{body, character, skill_group}; let (body, stats, inventory, waypoint) = persisted_components; @@ -218,6 +234,8 @@ pub fn create_character( ))); } + let skill_set = stats.skill_set.clone(); + // Insert stats record let db_stats = convert_stats_to_database(character_id, &stats, &waypoint)?; let stats_count = diesel::insert_into(schema::stats::table) @@ -266,6 +284,18 @@ pub fn create_character( ))); } + let db_skill_groups = convert_skill_groups_to_database(character_id, skill_set.skill_groups); + let skill_groups_count = diesel::insert_into(skill_group::table) + .values(&db_skill_groups) + .execute(&*connection)?; + + if skill_groups_count != 1 { + return Err(Error::OtherError(format!( + "Error inserting into skill_group table for char_id {}", + character_id + ))); + } + // Insert default inventory and loadout item records let mut inserts = Vec::new(); @@ -305,7 +335,9 @@ pub fn delete_character( char_id: CharacterId, connection: VelorenTransaction, ) -> CharacterListResult { - use schema::{body::dsl::*, character::dsl::*, stats::dsl::*}; + use schema::{ + body::dsl::*, character::dsl::*, skill::dsl::*, skill_group::dsl::*, stats::dsl::*, + }; // Load the character to delete - ensures that the requesting player // owns the character @@ -317,6 +349,13 @@ pub fn delete_character( ) .first::(&*connection)?; + // Delete skills + diesel::delete(skill_group.filter(schema::skill_group::dsl::character_id.eq(char_id))) + .execute(&*connection)?; + + diesel::delete(skill.filter(schema::skill::dsl::character_id.eq(char_id))) + .execute(&*connection)?; + // Delete character let character_count = diesel::delete( character @@ -529,7 +568,7 @@ pub fn update( char_waypoint: Option, connection: VelorenTransaction, ) -> Result>, Error> { - use super::schema::item::dsl::*; + use super::schema::{item::dsl::*, skill_group::dsl::*}; let pseudo_containers = get_pseudo_containers(connection, char_id)?; @@ -591,6 +630,20 @@ pub fn update( } } + let char_skill_set = char_stats.skill_set.clone(); + + let db_skill_groups = convert_skill_groups_to_database(char_id, char_skill_set.skill_groups); + + diesel::replace_into(skill_group) + .values(&db_skill_groups) + .execute(&*connection)?; + + let db_skills = convert_skills_to_database(char_id, char_skill_set.skills); + + diesel::replace_into(schema::skill::dsl::skill) + .values(&db_skills) + .execute(&*connection)?; + let db_stats = convert_stats_to_database(char_id, &char_stats, &char_waypoint)?; let stats_count = diesel::update(schema::stats::dsl::stats.filter(schema::stats::dsl::stats_id.eq(char_id))) diff --git a/server/src/persistence/character/conversions.rs b/server/src/persistence/character/conversions.rs index f0e7416ae3..63d9bdc647 100644 --- a/server/src/persistence/character/conversions.rs +++ b/server/src/persistence/character/conversions.rs @@ -1,6 +1,6 @@ use crate::persistence::{ character::EntityId, - models::{Body, Character, Item, Stats}, + models::{Body, Character, Item, Skill, SkillGroup, Stats}, }; use crate::persistence::{ @@ -15,11 +15,13 @@ use common::{ loadout_builder::LoadoutBuilder, slot::InvSlotId, }, + skills, Body as CompBody, Waypoint, *, }, resources::Time, }; use core::{convert::TryFrom, num::NonZeroU64}; +use hashbrown::HashMap; use itertools::{Either, Itertools}; use std::sync::Arc; @@ -342,7 +344,12 @@ pub fn convert_character_from_database(character: &Character) -> common::charact } } -pub fn convert_stats_from_database(stats: &Stats, alias: String) -> common::comp::Stats { +pub fn convert_stats_from_database( + stats: &Stats, + alias: String, + skills: &[Skill], + skill_groups: &[SkillGroup], +) -> common::comp::Stats { let mut new_stats = common::comp::Stats::empty(); new_stats.name = alias; new_stats.level.set_level(stats.level as u32); @@ -356,6 +363,10 @@ pub fn convert_stats_from_database(stats: &Stats, alias: String) -> common::comp new_stats.endurance = stats.endurance as u32; new_stats.fitness = stats.fitness as u32; new_stats.willpower = stats.willpower as u32; + new_stats.skill_set = skills::SkillSet { + skill_groups: convert_skill_groups_from_database(skill_groups), + skills: convert_skills_from_database(skills), + }; new_stats } @@ -369,3 +380,72 @@ fn get_item_from_asset(item_definition_id: &str) -> Result Vec { + let mut new_skill_groups = Vec::new(); + for skill_group in skill_groups.iter() { + let skill_group_type = + serde_json::de::from_str::(&skill_group.skill_group_type) + .map_err(|err| { + Error::ConversionError(format!( + "Error de-serializing skill group: {} err: {}", + skill_group.skill_group_type, err + )) + }) + .unwrap(); + let new_skill_group = skills::SkillGroup { + skill_group_type, + exp: skill_group.exp as u16, + available_sp: skill_group.available_sp as u16, + }; + new_skill_groups.push(new_skill_group); + } + new_skill_groups +} + +fn convert_skills_from_database(skills: &[Skill]) -> HashMap { + let mut new_skills = HashMap::new(); + for skill in skills.iter() { + let new_skill = serde_json::de::from_str::(&skill.skill_type) + .map_err(|err| { + Error::ConversionError(format!( + "Error de-serializing skill: {} err: {}", + skill.skill_type, err + )) + }) + .unwrap(); + new_skills.insert(new_skill, skill.level.map(|l| l as u16)); + } + new_skills +} + +pub fn convert_skill_groups_to_database( + character_id: CharacterId, + skill_groups: Vec, +) -> Vec { + let db_skill_groups: Vec<_> = skill_groups + .into_iter() + .map(|sg| SkillGroup { + character_id, + skill_group_type: serde_json::to_string(&sg.skill_group_type).unwrap(), + exp: sg.exp as i32, + available_sp: sg.available_sp as i32, + }) + .collect(); + db_skill_groups +} + +pub fn convert_skills_to_database( + character_id: CharacterId, + skills: HashMap, +) -> Vec { + let db_skills: Vec<_> = skills + .iter() + .map(|(s, l)| Skill { + character_id, + skill_type: serde_json::to_string(&s).unwrap(), + level: l.map(|l| l as i32), + }) + .collect(); + db_skills +} diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index e826ec0f91..d6cea8b91d 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -1,6 +1,6 @@ extern crate serde_json; -use super::schema::{body, character, entity, item, stats}; +use super::schema::{body, character, entity, item, skill, skill_group, stats}; #[derive(Debug, Insertable, PartialEq)] #[table_name = "entity"] @@ -57,3 +57,22 @@ pub struct Body { pub variant: String, pub body_data: String, } + +#[derive(Associations, Identifiable, Insertable, Queryable, Debug)] +#[primary_key(character_id, skill_type)] +#[table_name = "skill"] +pub struct Skill { + pub character_id: i64, + pub skill_type: String, + pub level: Option, +} + +#[derive(Associations, Identifiable, Insertable, Queryable, Debug)] +#[primary_key(character_id, skill_group_type)] +#[table_name = "skill_group"] +pub struct SkillGroup { + pub character_id: i64, + pub skill_group_type: String, + pub exp: i32, + pub available_sp: i32, +} diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs index 8838656ecf..00701c1fea 100644 --- a/server/src/persistence/schema.rs +++ b/server/src/persistence/schema.rs @@ -42,6 +42,24 @@ table! { } } +table! { + skill (character_id, skill_type) { + character_id -> BigInt, + #[sql_name = "skill"] + skill_type -> Text, + level -> Nullable, + } +} + +table! { + skill_group (character_id, skill_group_type) { + character_id -> BigInt, + skill_group_type -> Text, + exp -> Integer, + available_sp -> Integer, + } +} + joinable!(character -> body (character_id)); joinable!(character -> stats (character_id)); From b7c64463f70547443472d38cd23559e2a5ff86a5 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 13 Dec 2020 11:56:56 -0600 Subject: [PATCH 09/44] Added migrations for skill trees. --- .../2020-12-13-172324_skills/down.sql | 4 ++++ .../2020-12-13-172324_skills/up.sql | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 server/src/migrations/2020-12-13-172324_skills/down.sql create mode 100644 server/src/migrations/2020-12-13-172324_skills/up.sql diff --git a/server/src/migrations/2020-12-13-172324_skills/down.sql b/server/src/migrations/2020-12-13-172324_skills/down.sql new file mode 100644 index 0000000000..72c61c6be2 --- /dev/null +++ b/server/src/migrations/2020-12-13-172324_skills/down.sql @@ -0,0 +1,4 @@ +-- Drops skill and skill_group tables + +DROP TABLE skill; +DROP TABLE skill_group; \ No newline at end of file diff --git a/server/src/migrations/2020-12-13-172324_skills/up.sql b/server/src/migrations/2020-12-13-172324_skills/up.sql new file mode 100644 index 0000000000..71e0fd676c --- /dev/null +++ b/server/src/migrations/2020-12-13-172324_skills/up.sql @@ -0,0 +1,22 @@ +-- Creates skill and skill_group tables. Adds General skill tree for players that are already created + +CREATE TABLE skill_group ( + character_id INTEGER NOT NULL, + skill_group_type TEXT NOT NULL, + exp INTEGER NOT NULL, + available_sp INTEGER NOT NULL, + FOREIGN KEY(character_id) REFERENCES character(character_id), + PRIMARY KEY(character_id,skill_group_type) +); + +CREATE TABLE skill ( + character_id INTEGER NOT NULL, + skill TEXT NOT NULL, + level INTEGER, + FOREIGN KEY(character_id) REFERENCES character(character_id), + PRIMARY KEY(character_id,skill) +); + +INSERT INTO skill_group +SELECT c.character_id, '"General"', 0, 0 +FROM character c \ No newline at end of file From 4c61b598567c37b7b8562019c3e49c6bc10e0feb Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 19 Dec 2020 17:07:02 -0500 Subject: [PATCH 10/44] Axe skill tree functional. --- .../items/debug/cultist_purp_2h_boss-0.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 10 ++ .../skill_trees/skill_prerequisites.ron | 11 +++ .../skills_skill-groups_manifest.ron | 15 ++- client/src/lib.rs | 64 +++++++++++++ common/src/comp/ability.rs | 92 +++++++++++++++++++ common/src/comp/skills.rs | 18 +++- common/src/states/utils.rs | 11 ++- 8 files changed, 219 insertions(+), 4 deletions(-) diff --git a/assets/common/items/debug/cultist_purp_2h_boss-0.ron b/assets/common/items/debug/cultist_purp_2h_boss-0.ron index 0ce9a87270..6d57dd5b13 100644 --- a/assets/common/items/debug/cultist_purp_2h_boss-0.ron +++ b/assets/common/items/debug/cultist_purp_2h_boss-0.ron @@ -3,7 +3,7 @@ ItemDef( description: "Shouldn't this be a hammer?", kind: Tool( ( - kind: Sword, + kind: Axe, stats: ( equip_time_millis: 0, power: 1000.0, diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index 1970d5fadb..65e64606ab 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -11,4 +11,14 @@ Sword(SSpeed): Some(2), Sword(SCost): Some(2), Sword(SSpins): Some(2), + Axe(DsDamage): Some(3), + Axe(DsRegen): Some(2), + Axe(DsSpeed): Some(3), + Axe(SDamage): Some(3), + Axe(SSpeed): Some(2), + Axe(SCost): Some(2), + Axe(LDamage): Some(2), + Axe(LKnockback): Some(2), + Axe(LCost): Some(2), + Axe(LDistance): Some(2), }) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index 14e844640d..a8658d83f3 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -17,4 +17,15 @@ Sword(TsDamage): {Sword(TsCombo): None}, Sword(TsRegen): {Sword(TsCombo): None}, Sword(TsSpeed): {Sword(TsCombo): None}, + Axe(DsDamage): {Axe(DsCombo): None}, + Axe(DsSpeed): {Axe(DsCombo): None}, + Axe(DsRegen): {Axe(DsCombo): None}, + Axe(SHelicopter): {Axe(SInfinite): None}, + Axe(SDamage): {Axe(SInfinite): None}, + Axe(SSpeed): {Axe(SInfinite): None}, + Axe(SCost): {Axe(SInfinite): None}, + Axe(LDamage): {Axe(LUnlockLeap): None}, + Axe(LKnockback): {Axe(LUnlockLeap): None}, + Axe(LCost): {Axe(LUnlockLeap): None}, + Axe(LDistance): {Axe(LUnlockLeap): None}, }) \ No newline at end of file diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index 2a45ca00a1..5814aefb20 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -27,7 +27,20 @@ Sword(SSpins), ], Weapon(Axe): [ - Axe(UnlockLeap), + Axe(DsCombo), + Axe(DsDamage), + Axe(DsSpeed), + Axe(DsRegen), + Axe(SInfinite), + Axe(SHelicopter), + Axe(SDamage), + Axe(SSpeed), + Axe(SCost), + Axe(LUnlockLeap), + Axe(LDamage), + Axe(LKnockback), + Axe(LCost), + Axe(LDistance), ], Weapon(Hammer): [ Hammer(UnlockLeap), diff --git a/client/src/lib.rs b/client/src/lib.rs index 26265ea8d2..2dd59604ca 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1042,6 +1042,70 @@ impl Client { SkillGroupType::Weapon(Axe), ))); }, + "@unlock axe combo" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::DsCombo, + ))); + }, + "@unlock axe combo damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::DsDamage, + ))); + }, + "@unlock axe combo speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::DsCombo, + ))); + }, + "@unlock axe combo regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::DsRegen, + ))); + }, + "@unlock axe spin infinite" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::SInfinite, + ))); + }, + "@unlock axe spin helicopter" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::SHelicopter, + ))); + }, + "@unlock axe spin damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::SDamage, + ))); + }, + "@unlock axe spin speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe(AxeSkill::SSpeed))); + }, + "@unlock axe spin cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe(AxeSkill::SCost))); + }, + "@unlock axe leap unlock" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::LUnlockLeap, + ))); + }, + "@unlock axe leap damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::LDamage, + ))); + }, + "@unlock axe leap knockback" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::LKnockback, + ))); + }, + "@unlock axe leap cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe(AxeSkill::LCost))); + }, + "@unlock axe leap distance" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Axe( + AxeSkill::LDistance, + ))); + }, "@unlock hammer" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( SkillGroupType::Weapon(Hammer), diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 7dc5718ed8..8c386a2e7b 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -615,6 +615,98 @@ impl CharacterAbility { _ => {}, } }, + ToolKind::Axe => { + use skills::AxeSkill::*; + match self { + ComboMelee { + ref mut speed_increase, + ref mut max_speed_increase, + ref mut stage_data, + ref mut max_energy_gain, + ref mut scales_from_combo, + .. + } => { + if !skills.contains_key(&Axe(DsCombo)) { + stage_data.pop(); + } + let speed_segments = Axe(DsSpeed).get_max_level().unwrap_or(1) as f32; + let speed_level = + skills.get(&Axe(DsSpeed)).copied().flatten().unwrap_or(0) as f32; + { + *speed_increase *= speed_level / speed_segments; + *max_speed_increase *= speed_level / speed_segments; + } + let energy_level = + if let Some(level) = skills.get(&Axe(DsRegen)).copied().flatten() { + level + } else { + 0 + }; + { + *max_energy_gain = (*max_energy_gain as f32 + * ((energy_level + 1) * stage_data.len() as u16 - 1) as f32 + / ((Axe(DsRegen).get_max_level().unwrap() + 1) + * stage_data.len() as u16 + - 1) as f32) + as u32; + } + *scales_from_combo = skills + .get(&Axe(DsDamage)) + .copied() + .flatten() + .unwrap_or(0) + .into(); + }, + SpinMelee { + ref mut base_damage, + ref mut swing_duration, + ref mut energy_cost, + ref mut is_infinite, + ref mut is_helicopter, + .. + } => { + *is_infinite = skills.contains_key(&Axe(SInfinite)); + *is_helicopter = skills.contains_key(&Axe(SHelicopter)); + if let Some(level) = skills.get(&Axe(SDamage)).copied().flatten() { + *base_damage = + (*base_damage as f32 * 1.4_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Axe(SSpeed)).copied().flatten() { + *swing_duration = + (*swing_duration as f32 * 0.8_f32.powi(level.into())) as u64; + } + if let Some(level) = skills.get(&Axe(SCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + }, + LeapMelee { + ref mut base_damage, + ref mut knockback, + ref mut energy_cost, + ref mut forward_leap_strength, + ref mut vertical_leap_strength, + .. + } => { + if let Some(level) = skills.get(&Axe(LDamage)).copied().flatten() { + *base_damage = + (*base_damage as f32 * 1.4_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Axe(LKnockback)).copied().flatten() { + *knockback *= 1.4_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Axe(LCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Axe(LDistance)).copied().flatten() { + *forward_leap_strength *= 1.4_f32.powi(level.into()); + *vertical_leap_strength *= 1.4_f32.powi(level.into()); + } + }, + _ => {}, + } + }, _ => {}, } } diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index 4513286398..f35b8ffcc6 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -100,7 +100,23 @@ pub enum SwordSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum AxeSkill { - UnlockLeap, + // Double strike upgrades + DsCombo, + DsDamage, + DsSpeed, + DsRegen, + // Spin upgrades + SInfinite, + SHelicopter, + SDamage, + SSpeed, + SCost, + // Leap upgrades + LUnlockLeap, + LDamage, + LKnockback, + LCost, + LDistance, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 471cddbe3a..e9bf0f86a8 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -3,7 +3,7 @@ use crate::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, - skills::{Skill, SwordSkill}, + skills::{AxeSkill, Skill, SwordSkill}, theropod, Body, CharacterState, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, @@ -494,6 +494,15 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { { None }, + Some(ToolKind::Axe) + if !&data + .stats + .skill_set + .skills + .contains_key(&Skill::Axe(AxeSkill::LUnlockLeap)) => + { + None + }, _ => Some(s), }) .map(|a| { From 8d9d4fc62f7446a57c4f9eadc8a0d0ef8e7bb265 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 20 Dec 2020 17:28:17 -0500 Subject: [PATCH 11/44] Hammer skill tree complete. --- .../common/abilities/hammer/singlestrike.ron | 2 +- .../items/debug/cultist_purp_2h_boss-0.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 13 +++ .../skill_trees/skill_prerequisites.ron | 11 ++ .../skills_skill-groups_manifest.ron | 15 ++- client/src/lib.rs | 70 ++++++++++++ common/src/comp/ability.rs | 105 +++++++++++++++++- common/src/comp/skills.rs | 18 ++- common/src/states/combo_melee.rs | 5 + common/src/states/utils.rs | 11 +- 10 files changed, 246 insertions(+), 6 deletions(-) diff --git a/assets/common/abilities/hammer/singlestrike.ron b/assets/common/abilities/hammer/singlestrike.ron index 7b36b9ef34..554f30c73c 100644 --- a/assets/common/abilities/hammer/singlestrike.ron +++ b/assets/common/abilities/hammer/singlestrike.ron @@ -3,7 +3,7 @@ ComboMelee( stage: 1, base_damage: 130, damage_increase: 10, - knockback: 0.0, + knockback: 10.0, range: 4.5, angle: 50.0, base_buildup_duration: 600, diff --git a/assets/common/items/debug/cultist_purp_2h_boss-0.ron b/assets/common/items/debug/cultist_purp_2h_boss-0.ron index 6d57dd5b13..ad1c227278 100644 --- a/assets/common/items/debug/cultist_purp_2h_boss-0.ron +++ b/assets/common/items/debug/cultist_purp_2h_boss-0.ron @@ -3,7 +3,7 @@ ItemDef( description: "Shouldn't this be a hammer?", kind: Tool( ( - kind: Axe, + kind: Hammer, stats: ( equip_time_millis: 0, power: 1000.0, diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index 65e64606ab..289c7dbb5a 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -21,4 +21,17 @@ Axe(LKnockback): Some(2), Axe(LCost): Some(2), Axe(LDistance): Some(2), + Hammer(SsKnockback): Some(2), + Hammer(SsDamage): Some(3), + Hammer(SsRegen): Some(2), + Hammer(SsSpeed): Some(3), + Hammer(CDamage): Some(3), + Hammer(CKnockback): Some(3), + Hammer(CDrain): Some(2), + Hammer(CSpeed): Some(2), + Hammer(LDamage): Some(2), + Hammer(LCost): Some(2), + Hammer(LDistance): Some(2), + Hammer(LKnockback): Some(2), + Hammer(LRange): Some(2), }) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index a8658d83f3..81c9fb7495 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -28,4 +28,15 @@ Axe(LKnockback): {Axe(LUnlockLeap): None}, Axe(LCost): {Axe(LUnlockLeap): None}, Axe(LDistance): {Axe(LUnlockLeap): None}, + Hammer(SsDamage): {Hammer(SsKnockback): Some(1)}, + Hammer(SsRegen): {Hammer(SsKnockback): Some(1)}, + Hammer(SsSpeed): {Hammer(SsKnockback): Some(1)}, + Hammer(CDamage): {Hammer(CKnockback): Some(1)}, + Hammer(CDrain): {Hammer(CKnockback): Some(1)}, + Hammer(CSpeed): {Hammer(CKnockback): Some(1)}, + Hammer(LDamage): {Hammer(LUnlockLeap): None}, + Hammer(LCost): {Hammer(LUnlockLeap): None}, + Hammer(LDistance): {Hammer(LUnlockLeap): None}, + Hammer(LKnockback): {Hammer(LUnlockLeap): None}, + Hammer(LRange): {Hammer(LUnlockLeap): None}, }) \ No newline at end of file diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index 5814aefb20..79ffa9f91e 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -43,7 +43,20 @@ Axe(LDistance), ], Weapon(Hammer): [ - Hammer(UnlockLeap), + Hammer(SsKnockback), + Hammer(SsDamage), + Hammer(SsSpeed), + Hammer(SsRegen), + Hammer(CDamage), + Hammer(CKnockback), + Hammer(CDrain), + Hammer(CSpeed), + Hammer(LUnlockLeap), + Hammer(LDamage), + Hammer(LCost), + Hammer(LDistance), + Hammer(LKnockback), + Hammer(LRange), ], Weapon(Bow): [ Bow(UnlockRepeater), diff --git a/client/src/lib.rs b/client/src/lib.rs index 2dd59604ca..a0632f7cf2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1111,6 +1111,76 @@ impl Client { SkillGroupType::Weapon(Hammer), ))); }, + "@unlock hammer combo" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::SsKnockback, + ))); + }, + "@unlock hammer combo damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::SsDamage, + ))); + }, + "@unlock hammer combo speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::SsSpeed, + ))); + }, + "@unlock hammer combo regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::SsRegen, + ))); + }, + "@unlock hammer charge knockback" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::CKnockback, + ))); + }, + "@unlock hammer charge damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::CDamage, + ))); + }, + "@unlock hammer charge drain" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::CDrain, + ))); + }, + "@unlock hammer charge speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::CSpeed, + ))); + }, + "@unlock hammer leap unlock" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LUnlockLeap, + ))); + }, + "@unlock hammer leap damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LDamage, + ))); + }, + "@unlock hammer leap cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LCost, + ))); + }, + "@unlock hammer leap distance" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LDistance, + ))); + }, + "@unlock hammer leap knockback" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LKnockback, + ))); + }, + "@unlock hammer leap range" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Hammer( + HammerSkill::LRange, + ))); + }, "@unlock bow" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( SkillGroupType::Weapon(Bow), diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 8c386a2e7b..2eca470c80 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -512,7 +512,7 @@ impl CharacterAbility { ref mut is_interruptible, ref mut speed_increase, ref mut max_speed_increase, - ref mut stage_data, + ref stage_data, ref mut max_energy_gain, ref mut scales_from_combo, .. @@ -707,6 +707,109 @@ impl CharacterAbility { _ => {}, } }, + ToolKind::Hammer => { + use skills::HammerSkill::*; + match self { + ComboMelee { + ref mut speed_increase, + ref mut max_speed_increase, + ref mut stage_data, + ref mut max_energy_gain, + ref mut scales_from_combo, + .. + } => { + if let Some(level) = skills.get(&Hammer(SsKnockback)).copied().flatten() + { + *stage_data = (*stage_data) + .iter() + .map(|s| s.modify_strike(1.5_f32.powi(level.into()))) + .collect::>>(); + } + let speed_segments = + Hammer(SsSpeed).get_max_level().unwrap_or(1) as f32; + let speed_level = + skills.get(&Hammer(SsSpeed)).copied().flatten().unwrap_or(0) as f32; + { + *speed_increase *= speed_level / speed_segments; + *max_speed_increase *= speed_level / speed_segments; + } + let energy_level = if let Some(level) = + skills.get(&Hammer(SsRegen)).copied().flatten() + { + level + } else { + 0 + }; + { + *max_energy_gain = (*max_energy_gain as f32 + * ((energy_level + 1) * stage_data.len() as u16) as f32 + / ((Hammer(SsRegen).get_max_level().unwrap() + 1) + * stage_data.len() as u16) + as f32) + as u32; + } + *scales_from_combo = skills + .get(&Hammer(SsDamage)) + .copied() + .flatten() + .unwrap_or(0) + .into(); + }, + ChargedMelee { + ref mut scaled_damage, + ref mut scaled_knockback, + ref mut energy_drain, + ref mut speed, + .. + } => { + if let Some(level) = skills.get(&Hammer(CDamage)).copied().flatten() { + *scaled_damage = + (*scaled_damage as f32 * 1.25_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Hammer(CKnockback)).copied().flatten() + { + *scaled_knockback *= 1.5_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Hammer(CDrain)).copied().flatten() { + *energy_drain = + (*energy_drain as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Hammer(CSpeed)).copied().flatten() { + *speed *= 1.23_f32.powi(level.into()); + } + }, + LeapMelee { + ref mut base_damage, + ref mut knockback, + ref mut energy_cost, + ref mut forward_leap_strength, + ref mut vertical_leap_strength, + ref mut range, + .. + } => { + if let Some(level) = skills.get(&Hammer(LDamage)).copied().flatten() { + *base_damage = + (*base_damage as f32 * 1.4_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Hammer(LKnockback)).copied().flatten() + { + *knockback *= 1.4_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Hammer(LCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Hammer(LDistance)).copied().flatten() { + *forward_leap_strength *= 1.4_f32.powi(level.into()); + *vertical_leap_strength *= 1.4_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Hammer(LRange)).copied().flatten() { + *range += 1.0 * level as f32; + } + }, + _ => {}, + } + }, _ => {}, } } diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index f35b8ffcc6..dbf5537f57 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -121,7 +121,23 @@ pub enum AxeSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum HammerSkill { - UnlockLeap, + // Single strike upgrades + SsKnockback, + SsDamage, + SsSpeed, + SsRegen, + // Charged melee upgrades + CDamage, + CKnockback, + CDrain, + CSpeed, + // Leap upgrades + LUnlockLeap, + LDamage, + LCost, + LDistance, + LKnockback, + LRange, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs index e2e18812ae..e432e693ad 100644 --- a/common/src/states/combo_melee.rs +++ b/common/src/states/combo_melee.rs @@ -58,6 +58,11 @@ impl Stage { self.base_recover_duration = (self.base_recover_duration as f32 / speed) as u64; self } + + pub fn modify_strike(mut self, knockback_mult: f32) -> Self { + self.knockback *= knockback_mult; + self + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index e9bf0f86a8..3d8f4ed359 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -3,7 +3,7 @@ use crate::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, - skills::{AxeSkill, Skill, SwordSkill}, + skills::{AxeSkill, HammerSkill, Skill, SwordSkill}, theropod, Body, CharacterState, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, @@ -503,6 +503,15 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { { None }, + Some(ToolKind::Hammer) + if !&data + .stats + .skill_set + .skills + .contains_key(&Skill::Hammer(HammerSkill::LUnlockLeap)) => + { + None + }, _ => Some(s), }) .map(|a| { From 97f89383b8294c6cc69eb602ca8fd8b22faeb1b7 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 22 Dec 2020 21:28:55 -0500 Subject: [PATCH 12/44] Implemented bow skill tree. --- assets/common/abilities/bow/basic.ron | 2 +- assets/common/abilities/bow/charged.ron | 1 + assets/common/abilities/bow/repeater.ron | 2 +- .../items/debug/cultist_purp_2h_boss-0.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 12 +++ .../skill_trees/skill_prerequisites.ron | 10 ++ .../skills_skill-groups_manifest.ron | 13 +++ client/src/lib.rs | 58 ++++++++++++ common/src/combat.rs | 4 +- common/src/comp/ability.rs | 91 ++++++++++++++++++- common/src/comp/projectile.rs | 9 +- common/src/comp/skills.rs | 17 ++++ common/src/states/charged_ranged.rs | 4 +- common/src/states/utils.rs | 11 ++- 14 files changed, 225 insertions(+), 11 deletions(-) diff --git a/assets/common/abilities/bow/basic.ron b/assets/common/abilities/bow/basic.ron index 9492e92013..5c0c42499a 100644 --- a/assets/common/abilities/bow/basic.ron +++ b/assets/common/abilities/bow/basic.ron @@ -5,7 +5,7 @@ BasicRanged( projectile: Arrow( damage: 80.0, knockback: 5.0, - energy_regen: 100, + energy_regen: 50, ), projectile_body: Object(Arrow), projectile_light: None, diff --git a/assets/common/abilities/bow/charged.ron b/assets/common/abilities/bow/charged.ron index 3162c4631e..6e52bd3103 100644 --- a/assets/common/abilities/bow/charged.ron +++ b/assets/common/abilities/bow/charged.ron @@ -14,4 +14,5 @@ ChargedRanged( projectile_gravity: Some(Gravity(0.2)), initial_projectile_speed: 100.0, scaled_projectile_speed: 400.0, + move_speed: 0.3, ) diff --git a/assets/common/abilities/bow/repeater.ron b/assets/common/abilities/bow/repeater.ron index 619c326200..18dc6d0bd2 100644 --- a/assets/common/abilities/bow/repeater.ron +++ b/assets/common/abilities/bow/repeater.ron @@ -14,5 +14,5 @@ RepeaterRanged( projectile_light: None, projectile_gravity: Some(Gravity(0.2)), projectile_speed: 100.0, - reps_remaining: 5, + reps_remaining: 3, ) \ No newline at end of file diff --git a/assets/common/items/debug/cultist_purp_2h_boss-0.ron b/assets/common/items/debug/cultist_purp_2h_boss-0.ron index ad1c227278..d53c82b1ff 100644 --- a/assets/common/items/debug/cultist_purp_2h_boss-0.ron +++ b/assets/common/items/debug/cultist_purp_2h_boss-0.ron @@ -3,7 +3,7 @@ ItemDef( description: "Shouldn't this be a hammer?", kind: Tool( ( - kind: Hammer, + kind: Bow, stats: ( equip_time_millis: 0, power: 1000.0, diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index 289c7dbb5a..81c0683d41 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -34,4 +34,16 @@ Hammer(LDistance): Some(2), Hammer(LKnockback): Some(2), Hammer(LRange): Some(2), + Bow(ProjSpeed): Some(2), + Bow(BDamage): Some(3), + Bow(BRegen): Some(2), + Bow(CDamage): Some(3), + Bow(CKnockback): Some(2), + Bow(CProjSpeed): Some(2), + Bow(CDrain): Some(2), + Bow(CSpeed): Some(2), + Bow(CMove): Some(2), + Bow(RDamage): Some(2), + Bow(RArrows): Some(2), + Bow(RCost): Some(2), }) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index 81c9fb7495..8c7a67a594 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -39,4 +39,14 @@ Hammer(LDistance): {Hammer(LUnlockLeap): None}, Hammer(LKnockback): {Hammer(LUnlockLeap): None}, Hammer(LRange): {Hammer(LUnlockLeap): None}, + Bow(BRegen): {Bow(BDamage): Some(1)}, + Bow(CKnockback): {Bow(CDamage): Some(1)}, + Bow(CProjSpeed): {Bow(CDamage): Some(1)}, + Bow(CDrain): {Bow(CDamage): Some(1)}, + Bow(CSpeed): {Bow(CDamage): Some(1)}, + Bow(CMove): {Bow(CDamage): Some(1)}, + Bow(RDamage): {Bow(UnlockRepeater): None}, + Bow(RLeap): {Bow(UnlockRepeater): None}, + Bow(RArrows): {Bow(UnlockRepeater): None}, + Bow(RCost): {Bow(UnlockRepeater): None}, }) \ No newline at end of file diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index 79ffa9f91e..ba72a766ee 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -59,7 +59,20 @@ Hammer(LRange), ], Weapon(Bow): [ + Bow(ProjSpeed), + Bow(BDamage), + Bow(BRegen), + Bow(CDamage), + Bow(CKnockback), + Bow(CProjSpeed), + Bow(CDrain), + Bow(CSpeed), + Bow(CMove), Bow(UnlockRepeater), + Bow(RDamage), + Bow(RLeap), + Bow(RArrows), + Bow(RCost), ], Weapon(Staff): [ Staff(UnlockShockwave), diff --git a/client/src/lib.rs b/client/src/lib.rs index a0632f7cf2..d19453c464 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1186,6 +1186,64 @@ impl Client { SkillGroupType::Weapon(Bow), ))); }, + "@unlock bow proj speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::ProjSpeed, + ))); + }, + "@unlock bow basic damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::BDamage, + ))); + }, + "@unlock bow basic regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::BRegen))); + }, + "@unlock bow charged damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::CDamage, + ))); + }, + "@unlock bow charged knockback" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::CKnockback, + ))); + }, + "@unlock bow charged proj speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::CProjSpeed, + ))); + }, + "@unlock bow charged drain" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::CDrain))); + }, + "@unlock bow charged speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::CSpeed))); + }, + "@unlock bow charged move" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::CMove))); + }, + "@unlock bow repeater" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::UnlockRepeater, + ))); + }, + "@unlock bow repeater damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::RDamage, + ))); + }, + "@unlock bow repeater leap" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::RLeap))); + }, + "@unlock bow repeater arrows" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow( + BowSkill::RArrows, + ))); + }, + "@unlock bow repeater cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Bow(BowSkill::RCost))); + }, "@unlock staff" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( SkillGroupType::Weapon(Staff), diff --git a/common/src/combat.rs b/common/src/combat.rs index f23c8a1525..8cb01feba9 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -87,9 +87,9 @@ impl Damage { }, DamageSource::Projectile => { // Critical hit - if rand::random() { + /*if rand::random() { damage *= 1.2; - } + }*/ // Armor damage *= 1.0 - damage_reduction; diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 2eca470c80..ae7787486c 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -189,6 +189,7 @@ pub enum CharacterAbility { projectile_gravity: Option, initial_projectile_speed: f32, scaled_projectile_speed: f32, + move_speed: f32, }, Shockwave { energy_cost: u32, @@ -335,7 +336,7 @@ impl CharacterAbility { } => { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power); + *projectile = projectile.modified_projectile(power, 1_f32); }, RepeaterRanged { ref mut movement_duration, @@ -349,7 +350,7 @@ impl CharacterAbility { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *shoot_duration = (*shoot_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power); + *projectile = projectile.modified_projectile(power, 1_f32); }, Boost { ref mut movement_duration, @@ -810,6 +811,90 @@ impl CharacterAbility { _ => {}, } }, + ToolKind::Bow => { + use skills::BowSkill::*; + match self { + BasicRanged { + ref mut projectile, + ref mut projectile_speed, + .. + } => { + if let Some(level) = skills.get(&Bow(ProjSpeed)).copied().flatten() { + *projectile_speed *= 1.5_f32.powi(level.into()); + } + { + let damage_level = + skills.get(&Bow(BDamage)).copied().flatten().unwrap_or(0); + let regen_level = + skills.get(&Bow(BRegen)).copied().flatten().unwrap_or(0); + let power = 1.3_f32.powi(damage_level.into()); + let regen = 1.5_f32.powi(regen_level.into()); + *projectile = projectile.modified_projectile(power, regen); + } + }, + ChargedRanged { + ref mut scaled_damage, + ref mut scaled_knockback, + ref mut energy_drain, + ref mut speed, + ref mut initial_projectile_speed, + ref mut scaled_projectile_speed, + ref mut move_speed, + .. + } => { + if let Some(level) = skills.get(&Bow(ProjSpeed)).copied().flatten() { + *initial_projectile_speed *= 1.5_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Bow(CDamage)).copied().flatten() { + *scaled_damage = + (*scaled_damage as f32 * 1.25_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Bow(CKnockback)).copied().flatten() { + *scaled_knockback *= 1.5_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Bow(CProjSpeed)).copied().flatten() { + *scaled_projectile_speed *= 1.2_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Bow(CDrain)).copied().flatten() { + *energy_drain = + (*energy_drain as f32 * 0.75_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Bow(CSpeed)).copied().flatten() { + *speed *= 1.25_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Bow(CMove)).copied().flatten() { + *move_speed *= 1.25_f32.powi(level.into()); + } + }, + RepeaterRanged { + ref mut energy_cost, + ref mut leap, + ref mut projectile, + ref mut reps_remaining, + ref mut projectile_speed, + .. + } => { + if let Some(level) = skills.get(&Bow(ProjSpeed)).copied().flatten() { + *projectile_speed *= 1.5_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Bow(RDamage)).copied().flatten() { + let power = 1.3_f32.powi(level.into()); + *projectile = projectile.modified_projectile(power, 1_f32); + } + if !skills.contains_key(&Bow(RLeap)) { + *leap = None; + } + if let Some(level) = skills.get(&Bow(RArrows)).copied().flatten() { + *reps_remaining += level as u32; + } + if let Some(level) = skills.get(&Bow(RCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + }, + _ => {}, + } + }, _ => {}, } } @@ -1085,6 +1170,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState { projectile_gravity, initial_projectile_speed, scaled_projectile_speed, + move_speed, } => CharacterState::ChargedRanged(charged_ranged::Data { static_data: charged_ranged::StaticData { buildup_duration: Duration::from_millis(*buildup_duration), @@ -1101,6 +1187,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState { projectile_gravity: *projectile_gravity, initial_projectile_speed: *initial_projectile_speed, scaled_projectile_speed: *scaled_projectile_speed, + move_speed: *move_speed, ability_key: key, }, timer: Duration::default(), diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs index 3927ea0638..d0239718b4 100644 --- a/common/src/comp/projectile.rs +++ b/common/src/comp/projectile.rs @@ -203,11 +203,16 @@ impl ProjectileConstructor { } } - pub fn modified_projectile(mut self, power: f32) -> Self { + pub fn modified_projectile(mut self, power: f32, regen: f32) -> Self { use ProjectileConstructor::*; match self { - Arrow { ref mut damage, .. } => { + Arrow { + ref mut damage, + ref mut energy_regen, + .. + } => { *damage *= power; + *energy_regen = (*energy_regen as f32 * regen) as u32; }, Fireball { ref mut damage, .. } => { *damage *= power; diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index dbf5537f57..fef5217b15 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -142,7 +142,24 @@ pub enum HammerSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum BowSkill { + // Passives + ProjSpeed, + // Basic ranged upgrades + BDamage, + BRegen, + // Charged ranged upgrades + CDamage, + CKnockback, + CProjSpeed, + CDrain, + CSpeed, + CMove, + // Repeater upgrades UnlockRepeater, + RDamage, + RLeap, + RArrows, + RCost, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs index b48c5a26a2..90c0ec468b 100644 --- a/common/src/states/charged_ranged.rs +++ b/common/src/states/charged_ranged.rs @@ -42,6 +42,8 @@ pub struct StaticData { pub projectile_gravity: Option, pub initial_projectile_speed: f32, pub scaled_projectile_speed: f32, + /// Move speed efficiency + pub move_speed: f32, /// What key is used to press ability pub ability_key: AbilityKey, } @@ -63,7 +65,7 @@ impl CharacterBehavior for Data { fn behavior(&self, data: &JoinData) -> StateUpdate { let mut update = StateUpdate::from(data); - handle_move(data, &mut update, 0.3); + handle_move(data, &mut update, self.static_data.move_speed); handle_jump(data, &mut update); if !ability_key_is_pressed(data, self.static_data.ability_key) { handle_interrupt(data, &mut update, false); diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 3d8f4ed359..e8841a4281 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -3,7 +3,7 @@ use crate::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, - skills::{AxeSkill, HammerSkill, Skill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, SwordSkill}, theropod, Body, CharacterState, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, @@ -512,6 +512,15 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { { None }, + Some(ToolKind::Bow) + if !&data + .stats + .skill_set + .skills + .contains_key(&Skill::Bow(BowSkill::UnlockRepeater)) => + { + None + }, _ => Some(s), }) .map(|a| { From ed107dbe619367ee3159adf83d20209ffca57429 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 24 Dec 2020 12:54:00 -0500 Subject: [PATCH 13/44] Staff skill tree. --- .../items/debug/cultist_purp_2h_boss-0.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 12 +++ .../skill_trees/skill_prerequisites.ron | 10 +++ .../skills_skill-groups_manifest.ron | 12 +++ client/src/lib.rs | 65 ++++++++++++++ common/src/combat.rs | 10 +++ common/src/comp/ability.rs | 84 +++++++++++++++++-- common/src/comp/projectile.rs | 62 +++++++++++++- common/src/comp/skills.rs | 15 ++++ common/src/states/utils.rs | 11 ++- 10 files changed, 272 insertions(+), 11 deletions(-) diff --git a/assets/common/items/debug/cultist_purp_2h_boss-0.ron b/assets/common/items/debug/cultist_purp_2h_boss-0.ron index d53c82b1ff..760a44acbd 100644 --- a/assets/common/items/debug/cultist_purp_2h_boss-0.ron +++ b/assets/common/items/debug/cultist_purp_2h_boss-0.ron @@ -3,7 +3,7 @@ ItemDef( description: "Shouldn't this be a hammer?", kind: Tool( ( - kind: Bow, + kind: Staff, stats: ( equip_time_millis: 0, power: 1000.0, diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index 81c0683d41..add2842774 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -46,4 +46,16 @@ Bow(RDamage): Some(2), Bow(RArrows): Some(2), Bow(RCost): Some(2), + Staff(BDamage): Some(3), + Staff(BRegen): Some(2), + Staff(BRadius): Some(2), + Staff(FRange): Some(2), + Staff(FDamage): Some(3), + Staff(FDrain): Some(2), + Staff(FVelocity): Some(2), + Staff(UnlockShockwave): Some(2), + Staff(SDamage): Some(2), + Staff(SKnockback): Some(2), + Staff(SRange): Some(2), + Staff(SCost): Some(2), }) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index 8c7a67a594..fa00788b4e 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -49,4 +49,14 @@ Bow(RLeap): {Bow(UnlockRepeater): None}, Bow(RArrows): {Bow(UnlockRepeater): None}, Bow(RCost): {Bow(UnlockRepeater): None}, + Staff(BDamage): {Staff(BExplosion): None}, + Staff(BRegen): {Staff(BExplosion): None}, + Staff(BRadius): {Staff(BExplosion): None}, + Staff(FRange): {Staff(FDamage): Some(1)}, + Staff(FDrain): {Staff(FDamage): Some(1)}, + Staff(FVelocity): {Staff(FDamage): Some(1)}, + Staff(SDamage): {Staff(UnlockShockwave): None}, + Staff(SKnockback): {Staff(UnlockShockwave): None}, + Staff(SRange): {Staff(UnlockShockwave): None}, + Staff(SCost): {Staff(UnlockShockwave): None}, }) \ No newline at end of file diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index ba72a766ee..56dcb5b029 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -75,7 +75,19 @@ Bow(RCost), ], Weapon(Staff): [ + Staff(BExplosion), + Staff(BDamage), + Staff(BRegen), + Staff(BRadius), + Staff(FDamage), + Staff(FRange), + Staff(FDrain), + Staff(FVelocity), Staff(UnlockShockwave), + Staff(SDamage), + Staff(SKnockback), + Staff(SRange), + Staff(SCost), ], Weapon(Sceptre): [ Sceptre(Unlock404), diff --git a/client/src/lib.rs b/client/src/lib.rs index d19453c464..4ba7e69c6d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1249,6 +1249,71 @@ impl Client { SkillGroupType::Weapon(Staff), ))); }, + "@unlock staff fireball" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::BExplosion, + ))); + }, + "@unlock staff fireball damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::BDamage, + ))); + }, + "@unlock staff fireball regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::BRegen, + ))); + }, + "@unlock staff fireball radius" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::BRadius, + ))); + }, + "@unlock staff beam damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::FDamage, + ))); + }, + "@unlock staff beam range" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::FRange, + ))); + }, + "@unlock staff beam drain" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::FDrain, + ))); + }, + "@unlock staff beam velocity" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::FVelocity, + ))); + }, + "@unlock staff shockwave" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::UnlockShockwave, + ))); + }, + "@unlock staff shockwave damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::SDamage, + ))); + }, + "@unlock staff shockwave knockback" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::SKnockback, + ))); + }, + "@unlock staff shockwave range" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::SRange, + ))); + }, + "@unlock staff shockwave cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Staff( + StaffSkill::SCost, + ))); + }, "@unlock sceptre" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::UnlockGroup( SkillGroupType::Weapon(Sceptre), diff --git a/common/src/combat.rs b/common/src/combat.rs index 8cb01feba9..4add6e89f9 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -195,6 +195,16 @@ impl Knockback { }, } } + + pub fn modify_strength(mut self, power: f32) -> Self { + use Knockback::*; + match self { + Away(ref mut f) | Towards(ref mut f) | Up(ref mut f) | TowardsUp(ref mut f) => { + *f *= power; + }, + } + self + } } pub fn get_weapons(inv: &Inventory) -> (Option, Option) { diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index ae7787486c..a8d9f9a2f4 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -336,7 +336,7 @@ impl CharacterAbility { } => { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power, 1_f32); + *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); }, RepeaterRanged { ref mut movement_duration, @@ -350,7 +350,7 @@ impl CharacterAbility { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *shoot_duration = (*shoot_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power, 1_f32); + *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); }, Boost { ref mut movement_duration, @@ -493,7 +493,7 @@ impl CharacterAbility { | ChargedRanged { energy_cost, .. } | Shockwave { energy_cost, .. } | BasicBeam { energy_cost, .. } => *energy_cost, - _ => 0, + BasicBlock | Boost { .. } | ComboMelee { .. } => 0, } } @@ -829,7 +829,7 @@ impl CharacterAbility { skills.get(&Bow(BRegen)).copied().flatten().unwrap_or(0); let power = 1.3_f32.powi(damage_level.into()); let regen = 1.5_f32.powi(regen_level.into()); - *projectile = projectile.modified_projectile(power, regen); + *projectile = projectile.modified_projectile(power, regen, 1_f32); } }, ChargedRanged { @@ -879,7 +879,7 @@ impl CharacterAbility { } if let Some(level) = skills.get(&Bow(RDamage)).copied().flatten() { let power = 1.3_f32.powi(level.into()); - *projectile = projectile.modified_projectile(power, 1_f32); + *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); } if !skills.contains_key(&Bow(RLeap)) { *leap = None; @@ -895,6 +895,80 @@ impl CharacterAbility { _ => {}, } }, + ToolKind::Staff => { + use skills::StaffSkill::*; + match self { + BasicRanged { + ref mut projectile, .. + } => { + if !skills.contains_key(&Staff(BExplosion)) { + *projectile = projectile.fireball_to_firebolt(); + } + { + let damage_level = + skills.get(&Staff(BDamage)).copied().flatten().unwrap_or(0); + let regen_level = + skills.get(&Staff(BRegen)).copied().flatten().unwrap_or(0); + let range_level = + skills.get(&Staff(BRadius)).copied().flatten().unwrap_or(0); + let power = 1.2_f32.powi(damage_level.into()); + let regen = 1.2_f32.powi(regen_level.into()); + let range = 1.1_f32.powi(range_level.into()); + *projectile = projectile.modified_projectile(power, regen, range); + } + }, + BasicBeam { + ref mut base_dps, + ref mut range, + ref mut energy_drain, + ref mut beam_duration, + .. + } => { + if let Some(level) = skills.get(&Staff(FDamage)).copied().flatten() { + *base_dps = (*base_dps as f32 * 1.3_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Staff(FRange)).copied().flatten() { + *range *= 1.25_f32.powi(level.into()); + // Duration modified to keep velocity constant + *beam_duration = + (*beam_duration as f32 * 1.4_f32.powi(level.into())) as u64; + } + if let Some(level) = skills.get(&Staff(FDrain)).copied().flatten() { + *energy_drain = + (*energy_drain as f32 * 0.8_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Staff(FVelocity)).copied().flatten() { + let velocity_increase = 1.25_f32.powi(level.into()); + let duration_mod = 1.0 / (1.0 + velocity_increase); + *beam_duration = (*beam_duration as f32 * duration_mod) as u64; + } + }, + Shockwave { + ref mut damage, + ref mut knockback, + ref mut shockwave_duration, + ref mut energy_cost, + .. + } => { + if let Some(level) = skills.get(&Staff(SDamage)).copied().flatten() { + *damage = (*damage as f32 * 1.3_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Staff(SKnockback)).copied().flatten() { + *knockback = knockback.modify_strength(1.3_f32.powi(level.into())); + } + if let Some(level) = skills.get(&Staff(SRange)).copied().flatten() { + *shockwave_duration = (*shockwave_duration as f32 + * 1.2_f32.powi(level.into())) + as u64; + } + if let Some(level) = skills.get(&Staff(SCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.8_f32.powi(level.into())) as u32; + } + }, + _ => {}, + } + }, _ => {}, } } diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs index d0239718b4..2f458a4fca 100644 --- a/common/src/comp/projectile.rs +++ b/common/src/comp/projectile.rs @@ -53,6 +53,10 @@ pub enum ProjectileConstructor { radius: f32, energy_regen: u32, }, + Firebolt { + damage: f32, + energy_regen: u32, + }, Heal { heal: f32, damage: f32, @@ -134,7 +138,24 @@ impl ProjectileConstructor { }), Effect::Vanish, ], - time_left: Duration::from_secs(20), + time_left: Duration::from_secs(10), + owner, + ignore_group: true, + }, + Firebolt { + damage, + energy_regen, + } => Projectile { + hit_solid: vec![Effect::Vanish], + hit_entity: vec![ + Effect::Damage(Some(GroupTarget::OutOfGroup), Damage { + source: DamageSource::Energy, + value: damage, + }), + Effect::RewardEnergy(energy_regen), + Effect::Vanish, + ], + time_left: Duration::from_secs(10), owner, ignore_group: true, }, @@ -189,7 +210,7 @@ impl ProjectileConstructor { }), Effect::Vanish, ], - time_left: Duration::from_secs(20), + time_left: Duration::from_secs(10), owner, ignore_group: true, }, @@ -203,7 +224,7 @@ impl ProjectileConstructor { } } - pub fn modified_projectile(mut self, power: f32, regen: f32) -> Self { + pub fn modified_projectile(mut self, power: f32, regen: f32, range: f32) -> Self { use ProjectileConstructor::*; match self { Arrow { @@ -214,19 +235,52 @@ impl ProjectileConstructor { *damage *= power; *energy_regen = (*energy_regen as f32 * regen) as u32; }, - Fireball { ref mut damage, .. } => { + Fireball { + ref mut damage, + ref mut energy_regen, + ref mut radius, + .. + } => { *damage *= power; + *energy_regen = (*energy_regen as f32 * regen) as u32; + *radius *= range; + }, + Firebolt { + ref mut damage, + ref mut energy_regen, + .. + } => { + *damage *= power; + *energy_regen = (*energy_regen as f32 * regen) as u32; }, Heal { ref mut damage, ref mut heal, + ref mut radius, .. } => { *damage *= power; *heal *= power; + *radius *= range; }, Possess => {}, } self } + + pub fn fireball_to_firebolt(self) -> Self { + if let ProjectileConstructor::Fireball { + damage, + energy_regen, + .. + } = self + { + ProjectileConstructor::Firebolt { + damage, + energy_regen: energy_regen * 2, + } + } else { + self + } + } } diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index fef5217b15..ce70f23f6a 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -164,7 +164,22 @@ pub enum BowSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum StaffSkill { + // Basic ranged upgrades + BExplosion, + BDamage, + BRegen, + BRadius, + // Flamethrower upgrades + FDamage, + FRange, + FDrain, + FVelocity, + // Shockwave upgrades UnlockShockwave, + SDamage, + SKnockback, + SRange, + SCost, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index e8841a4281..2102af81a0 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -3,7 +3,7 @@ use crate::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, theropod, Body, CharacterState, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, @@ -521,6 +521,15 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { { None }, + Some(ToolKind::Staff) + if !&data + .stats + .skill_set + .skills + .contains_key(&Skill::Staff(StaffSkill::UnlockShockwave)) => + { + None + }, _ => Some(s), }) .map(|a| { From 5903578e1ae0ff59029137139504aa3fe7537e00 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 25 Dec 2020 11:28:35 -0500 Subject: [PATCH 14/44] Sceptre skill tree. --- .../items/debug/cultist_purp_2h_boss-0.ron | 2 +- .../common/skill_trees/skill_max_levels.ron | 11 +++ .../skill_trees/skill_prerequisites.ron | 9 ++ .../skills_skill-groups_manifest.ron | 12 ++- client/src/lib.rs | 55 +++++++++++ common/src/comp/ability.rs | 94 +++++++++++++++++-- common/src/comp/projectile.rs | 10 +- common/src/comp/skills.rs | 14 ++- 8 files changed, 194 insertions(+), 13 deletions(-) diff --git a/assets/common/items/debug/cultist_purp_2h_boss-0.ron b/assets/common/items/debug/cultist_purp_2h_boss-0.ron index 760a44acbd..6c1fb695ab 100644 --- a/assets/common/items/debug/cultist_purp_2h_boss-0.ron +++ b/assets/common/items/debug/cultist_purp_2h_boss-0.ron @@ -3,7 +3,7 @@ ItemDef( description: "Shouldn't this be a hammer?", kind: Tool( ( - kind: Staff, + kind: Sceptre, stats: ( equip_time_millis: 0, power: 1000.0, diff --git a/assets/common/skill_trees/skill_max_levels.ron b/assets/common/skill_trees/skill_max_levels.ron index add2842774..aeb424ebb7 100644 --- a/assets/common/skill_trees/skill_max_levels.ron +++ b/assets/common/skill_trees/skill_max_levels.ron @@ -58,4 +58,15 @@ Staff(SKnockback): Some(2), Staff(SRange): Some(2), Staff(SCost): Some(2), + Sceptre(BHeal): Some(3), + Sceptre(BDamage): Some(2), + Sceptre(BRange): Some(2), + Sceptre(BLifesteal): Some(2), + Sceptre(BRegen): Some(2), + Sceptre(BCost): Some(2), + Sceptre(PHeal): Some(3), + Sceptre(PDamage): Some(2), + Sceptre(PRadius): Some(2), + Sceptre(PCost): Some(2), + Sceptre(PProjSpeed): Some(2), }) \ No newline at end of file diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index fa00788b4e..581414c051 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -59,4 +59,13 @@ Staff(SKnockback): {Staff(UnlockShockwave): None}, Staff(SRange): {Staff(UnlockShockwave): None}, Staff(SCost): {Staff(UnlockShockwave): None}, + Sceptre(BDamage): {Sceptre(BHeal): Some(1)}, + Sceptre(BRange): {Sceptre(BHeal): Some(1)}, + Sceptre(BLifesteal): {Sceptre(BHeal): Some(1)}, + Sceptre(BRegen): {Sceptre(BHeal): Some(1)}, + Sceptre(BCost): {Sceptre(BHeal): Some(1)}, + Sceptre(PDamage): {Sceptre(PHeal): Some(1)}, + Sceptre(PRadius): {Sceptre(PHeal): Some(1)}, + Sceptre(PCost): {Sceptre(PHeal): Some(1)}, + Sceptre(PProjSpeed): {Sceptre(PHeal): Some(1)}, }) \ No newline at end of file diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index 56dcb5b029..a9c6d1627d 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -90,6 +90,16 @@ Staff(SCost), ], Weapon(Sceptre): [ - Sceptre(Unlock404), + Sceptre(BHeal), + Sceptre(BDamage), + Sceptre(BRange), + Sceptre(BLifesteal), + Sceptre(BRegen), + Sceptre(BCost), + Sceptre(PHeal), + Sceptre(PDamage), + Sceptre(PRadius), + Sceptre(PCost), + Sceptre(PProjSpeed), ], }) \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index 4ba7e69c6d..75e7c39186 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1319,6 +1319,61 @@ impl Client { SkillGroupType::Weapon(Sceptre), ))); }, + "@unlock sceptre beam heal" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BHeal, + ))); + }, + "@unlock sceptre beam damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BDamage, + ))); + }, + "@unlock sceptre beam range" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BRange, + ))); + }, + "@unlock sceptre beam lifesteal" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BLifesteal, + ))); + }, + "@unlock sceptre beam regen" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BRegen, + ))); + }, + "@unlock sceptre beam cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::BCost, + ))); + }, + "@unlock sceptre proj heal" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::PHeal, + ))); + }, + "@unlock sceptre proj damage" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::PDamage, + ))); + }, + "@unlock sceptre proj radius" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::PRadius, + ))); + }, + "@unlock sceptre proj cost" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::PCost, + ))); + }, + "@unlock sceptre proj speed" => { + self.send_msg(ClientGeneral::UnlockSkill(Skill::Sceptre( + SceptreSkill::PProjSpeed, + ))); + }, "@unlock health" => { self.send_msg(ClientGeneral::UnlockSkill(Skill::General( GeneralSkill::HealthIncrease, diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index a8d9f9a2f4..a698fdbfb8 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -336,7 +336,7 @@ impl CharacterAbility { } => { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); + *projectile = projectile.modified_projectile(power, 1_f32, 1_f32, power); }, RepeaterRanged { ref mut movement_duration, @@ -350,7 +350,7 @@ impl CharacterAbility { *buildup_duration = (*buildup_duration as f32 / speed) as u64; *shoot_duration = (*shoot_duration as f32 / speed) as u64; *recover_duration = (*recover_duration as f32 / speed) as u64; - *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); + *projectile = projectile.modified_projectile(power, 1_f32, 1_f32, power); }, Boost { ref mut movement_duration, @@ -829,7 +829,8 @@ impl CharacterAbility { skills.get(&Bow(BRegen)).copied().flatten().unwrap_or(0); let power = 1.3_f32.powi(damage_level.into()); let regen = 1.5_f32.powi(regen_level.into()); - *projectile = projectile.modified_projectile(power, regen, 1_f32); + *projectile = + projectile.modified_projectile(power, regen, 1_f32, 1_f32); } }, ChargedRanged { @@ -879,7 +880,8 @@ impl CharacterAbility { } if let Some(level) = skills.get(&Bow(RDamage)).copied().flatten() { let power = 1.3_f32.powi(level.into()); - *projectile = projectile.modified_projectile(power, 1_f32, 1_f32); + *projectile = + projectile.modified_projectile(power, 1_f32, 1_f32, 1_f32); } if !skills.contains_key(&Bow(RLeap)) { *leap = None; @@ -914,7 +916,8 @@ impl CharacterAbility { let power = 1.2_f32.powi(damage_level.into()); let regen = 1.2_f32.powi(regen_level.into()); let range = 1.1_f32.powi(range_level.into()); - *projectile = projectile.modified_projectile(power, regen, range); + *projectile = + projectile.modified_projectile(power, regen, range, 1_f32); } }, BasicBeam { @@ -928,10 +931,10 @@ impl CharacterAbility { *base_dps = (*base_dps as f32 * 1.3_f32.powi(level.into())) as u32; } if let Some(level) = skills.get(&Staff(FRange)).copied().flatten() { - *range *= 1.25_f32.powi(level.into()); + let range_mod = 1.25_f32.powi(level.into()); + *range *= range_mod; // Duration modified to keep velocity constant - *beam_duration = - (*beam_duration as f32 * 1.4_f32.powi(level.into())) as u64; + *beam_duration = (*beam_duration as f32 * range_mod) as u64; } if let Some(level) = skills.get(&Staff(FDrain)).copied().flatten() { *energy_drain = @@ -969,6 +972,81 @@ impl CharacterAbility { _ => {}, } }, + ToolKind::Sceptre => { + use skills::SceptreSkill::*; + match self { + BasicBeam { + ref mut base_hps, + ref mut base_dps, + ref mut lifesteal_eff, + ref mut range, + ref mut energy_regen, + ref mut energy_cost, + ref mut beam_duration, + .. + } => { + if let Some(level) = skills.get(&Sceptre(BHeal)).copied().flatten() { + *base_hps = (*base_hps as f32 * 1.3_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sceptre(BDamage)).copied().flatten() { + *base_dps = (*base_dps as f32 * 1.3_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sceptre(BRange)).copied().flatten() { + let range_mod = 1.25_f32.powi(level.into()); + *range *= range_mod; + // Duration modified to keep velocity constant + *beam_duration = (*beam_duration as f32 * range_mod) as u64; + } + if let Some(level) = skills.get(&Sceptre(BLifesteal)).copied().flatten() + { + *lifesteal_eff *= 1.5_f32.powi(level.into()); + } + if let Some(level) = skills.get(&Sceptre(BRegen)).copied().flatten() { + *energy_regen = + (*energy_regen as f32 * 1.25_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sceptre(BCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.75_f32.powi(level.into())) as u32; + } + }, + BasicRanged { + ref mut energy_cost, + ref mut projectile, + ref mut projectile_speed, + .. + } => { + { + let heal_level = + skills.get(&Sceptre(PHeal)).copied().flatten().unwrap_or(0); + let damage_level = skills + .get(&Sceptre(PDamage)) + .copied() + .flatten() + .unwrap_or(0); + let range_level = skills + .get(&Sceptre(PRadius)) + .copied() + .flatten() + .unwrap_or(0); + let heal = 1.2_f32.powi(heal_level.into()); + let power = 1.2_f32.powi(damage_level.into()); + let range = 1.4_f32.powi(range_level.into()); + *projectile = + projectile.modified_projectile(power, 1_f32, range, heal); + } + if let Some(level) = skills.get(&Sceptre(PCost)).copied().flatten() { + *energy_cost = + (*energy_cost as f32 * 0.8_f32.powi(level.into())) as u32; + } + if let Some(level) = skills.get(&Sceptre(PProjSpeed)).copied().flatten() + { + *projectile_speed *= 1.25_f32.powi(level.into()); + } + }, + _ => {}, + } + }, _ => {}, } } diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs index 2f458a4fca..60c9a2b463 100644 --- a/common/src/comp/projectile.rs +++ b/common/src/comp/projectile.rs @@ -224,7 +224,13 @@ impl ProjectileConstructor { } } - pub fn modified_projectile(mut self, power: f32, regen: f32, range: f32) -> Self { + pub fn modified_projectile( + mut self, + power: f32, + regen: f32, + range: f32, + heal_power: f32, + ) -> Self { use ProjectileConstructor::*; match self { Arrow { @@ -260,7 +266,7 @@ impl ProjectileConstructor { .. } => { *damage *= power; - *heal *= power; + *heal *= heal_power; *radius *= range; }, Possess => {}, diff --git a/common/src/comp/skills.rs b/common/src/comp/skills.rs index ce70f23f6a..124c99b7e1 100644 --- a/common/src/comp/skills.rs +++ b/common/src/comp/skills.rs @@ -184,7 +184,19 @@ pub enum StaffSkill { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum SceptreSkill { - Unlock404, + // Beam upgrades + BHeal, + BDamage, + BRange, + BLifesteal, + BRegen, + BCost, + // Projectile upgrades + PHeal, + PDamage, + PRadius, + PCost, + PProjSpeed, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] From 1a57ec4afcb820e1ad80a1d253c5248c4b460dab Mon Sep 17 00:00:00 2001 From: Monty Marz Date: Sat, 26 Dec 2020 12:25:50 +0100 Subject: [PATCH 15/44] Skill-Tree UI switchable tabs rework icons, fix cursor toggle auto slot placing Bow leap skill changed to bow glide skill. --- .../skill_trees/skill_prerequisites.ron | 2 +- .../skills_skill-groups_manifest.ron | 2 +- assets/voxygen/element/buttons/border.png | Bin 1947 -> 1752 bytes assets/voxygen/element/buttons/border_mo.png | Bin 1941 -> 1774 bytes .../voxygen/element/buttons/border_press.png | Bin 1677 -> 1777 bytes .../element/buttons/border_pressed.png | Bin 1935 -> 1771 bytes assets/voxygen/element/icons/axe.png | Bin 1608 -> 1753 bytes assets/voxygen/element/icons/bow.png | Bin 1598 -> 1757 bytes assets/voxygen/element/icons/daggers.png | Bin 1630 -> 1784 bytes assets/voxygen/element/icons/danari_f.png | Bin 8884 -> 9880 bytes assets/voxygen/element/icons/danari_m.png | Bin 15716 -> 16945 bytes assets/voxygen/element/icons/dwarf_f.png | Bin 14064 -> 15233 bytes assets/voxygen/element/icons/dwarf_m.png | Bin 17210 -> 18356 bytes assets/voxygen/element/icons/elf_f.png | Bin 6813 -> 7853 bytes assets/voxygen/element/icons/elf_m.png | Bin 19534 -> 20751 bytes assets/voxygen/element/icons/hammer.png | Bin 1592 -> 1744 bytes assets/voxygen/element/icons/human_f.png | Bin 19083 -> 19738 bytes assets/voxygen/element/icons/human_m.png | Bin 23917 -> 25024 bytes assets/voxygen/element/icons/orc_f.png | Bin 18594 -> 19662 bytes assets/voxygen/element/icons/orc_m.png | Bin 15053 -> 16275 bytes assets/voxygen/element/icons/sceptre.png | Bin 2098 -> 2048 bytes assets/voxygen/element/icons/staff.png | Bin 1590 -> 1746 bytes assets/voxygen/element/icons/sword.png | Bin 1600 -> 1957 bytes assets/voxygen/element/icons/ud_f.png | Bin 19327 -> 20227 bytes assets/voxygen/element/icons/ud_m.png | Bin 16108 -> 17352 bytes assets/voxygen/element/misc_bg/diary_bg.png | Bin 0 -> 4075 bytes .../voxygen/element/misc_bg/diary_frame.png | Bin 0 -> 3860 bytes .../voxygen/element/misc_bg/sword_render.png | Bin 0 -> 11974 bytes assets/voxygen/i18n/en.ron | 668 ++++++++++++++++++ client/src/lib.rs | 4 +- common/src/combat.rs | 7 +- common/src/comp/ability.rs | 6 +- common/src/comp/skills.rs | 2 +- voxygen/src/hud/img_ids.rs | 17 + voxygen/src/hud/mod.rs | 31 +- voxygen/src/hud/spell.rs | 414 ++++++++++- 36 files changed, 1104 insertions(+), 49 deletions(-) create mode 100644 assets/voxygen/element/misc_bg/diary_bg.png create mode 100644 assets/voxygen/element/misc_bg/diary_frame.png create mode 100644 assets/voxygen/element/misc_bg/sword_render.png diff --git a/assets/common/skill_trees/skill_prerequisites.ron b/assets/common/skill_trees/skill_prerequisites.ron index 581414c051..5d7e9aa7fc 100644 --- a/assets/common/skill_trees/skill_prerequisites.ron +++ b/assets/common/skill_trees/skill_prerequisites.ron @@ -46,7 +46,7 @@ Bow(CSpeed): {Bow(CDamage): Some(1)}, Bow(CMove): {Bow(CDamage): Some(1)}, Bow(RDamage): {Bow(UnlockRepeater): None}, - Bow(RLeap): {Bow(UnlockRepeater): None}, + Bow(RGlide): {Bow(UnlockRepeater): None}, Bow(RArrows): {Bow(UnlockRepeater): None}, Bow(RCost): {Bow(UnlockRepeater): None}, Staff(BDamage): {Staff(BExplosion): None}, diff --git a/assets/common/skill_trees/skills_skill-groups_manifest.ron b/assets/common/skill_trees/skills_skill-groups_manifest.ron index a9c6d1627d..c3318789fc 100644 --- a/assets/common/skill_trees/skills_skill-groups_manifest.ron +++ b/assets/common/skill_trees/skills_skill-groups_manifest.ron @@ -70,7 +70,7 @@ Bow(CMove), Bow(UnlockRepeater), Bow(RDamage), - Bow(RLeap), + Bow(RGlide), Bow(RArrows), Bow(RCost), ], diff --git a/assets/voxygen/element/buttons/border.png b/assets/voxygen/element/buttons/border.png index 659a8bc2c2e555c04db32d3dcb7dc492145c5a9f..8148c6310e330218a0dcc9917873aa67b202bd1c 100644 GIT binary patch literal 1752 zcmbVNU5MO79M82@^tj_tun#?ukOQe7*-WxYHW|9!UU$#lqI)jamD>u6lbPAwxSLE& za<_Z;Vkz`NY_Sibf~6{`MT&(=g%cHvzTBJmpi)tM5}!l`eNwBF{fJw+e&CXi$;@wl z|KI;({`*{Y`Ox&_zDYq4rprsE75?6z+_&AzpC_(tdHi*IuyiC6gv?xWPxRiJUl)Wu zzq+-0T(3L=G4nErFo$M(Uck|UaA3X{Al#<0=+LI?=cS)N{6rF6l9yJE3aA7{+H#jR zLwa~~xrR5}*e25agW`c6V0F=0dwTZ{7&S2}RP3sPD? z8it7nOzELOQ8OTMsShML#RY3&r*E8Kg?6Y%{W#)SHN^%k7PF|uu0fqHUuA%Ywo*xL z?5M@_QWH_UxX#@e24qKcRNDxsvO*)a7Gk=%&SlOe-UP51QWUeW##m>RsOm_vsA(Bh z+`sDjgmt5P$2QOsifLX-vL>r4ziJwQz=oP}KTsh6189X2x3Mt})nv`A0Sf{XYU))` zUNQv5=!Rf|VS|Mp;tsnWYEmWeo06D@5f)j8h1@V#mmQxk7mL-9HQWwgh*l0A6w8Z6 zQ?*S~=05i2Rw}UUM=|m-Etm2V4^GB)2{b5jOs7F)!%!U=6Wx-noDO77N1E24Hn4K( z{u0A$NeLwT2d9BB%sKv*r#FC(EP`b~h#?b`vpEgZmJMCYTDhDBR8t$Nc{FtSfkd5g z)(YX)T!)X$C61FXDuVE>DVMm+4n06F1ZoU@y75V9j4C=M;%w} z7Y{@@ITU#bC)GfSG@N#Kh{sJ?9`5h9C?~y6l?<#$tPyunNEezsb~mg-f|MH>NtAgi!a|_J$2@d=fD2t3T{sxgBOpl-1({U zk^IWcEq6^nd;%#yv@VBtzxTHO=2tWC%zaDGzxLy+fBe4p!V_C(?|J-=*-M`nj&#oz zE=>G3^Ww+b_fU%S6QeU`5j{w|!`+Iv)(*!Ab^c6nEIcKhVM*UeKe?>YKS;mP`U eyWf4bc>1xIrrL!w`03=or@XjaI)Ct~WB&ke0WyaG literal 1947 zcmeAS@N?(olHy`uVBq!ia0y~yU_1cA9Be=lQ`H^CK#H+A$lZxy-8q?;3=Hg{o-U3d z6?5L+HS{_fAmSRBJX%dBbX=k>a*(g#2BD{k#O81qr^Thw3m^}l9E&wZP>vG)G{ z@coPp|I1~LZ~OH7yS6AQy5ZX&28-mcU1aWv?N&$OdqMdPr#^{w`Q+|P#R$J;NyzkknuyL}ZG!~PGyKAF5ae*F5|_4A6l zC@9=4QvC%U%vt^Zr|(W;OKG$SUvm0Aqu)(omUXN4JvV+`ql4$ z`W{P|o?#`iE`n8Dz26y@zXBQ*df9frrZ_RR7)&QpmG_1D?t-AOXMQpLz}l9<)78&q Iol`;+0Gk4x_y7O^ diff --git a/assets/voxygen/element/buttons/border_mo.png b/assets/voxygen/element/buttons/border_mo.png index e08bb18f92ef9ef3c7ef756137f38991d7446665..cf6cd8c025f78c2de5d8ff7585a3fa319a38f650 100644 GIT binary patch literal 1774 zcmbVNU1%Id96wVdX|VO76~Vj=8!HjBUvqoAw@c4>xl410UJU7(o)3P^&CK1cx!qZJ z_i~r0n3f_6A&5_cC_)g557M9wLTjq{rV@M-qz^ubv=0Um`rt!RXYb3aq#tBt}`73M*P z`4s4qB4R~}1H+V5T}wb?5ZB0K^XMCF< zNK_wWjZ_iNh&5s=IaaWeR`Ep zZ!i7sQrXcoD1G0m{Xf#XVZk-zRw?hWvJg2VN4=<%6#2jTYOf9Stsa~=!)Vy=!u+G( z$EnMyecq+N+}VAEclgbGHeD=zv-eYabPXK6_lGyux1EdA;hXvko4&ISx zb`E|GPG1{O|5|cU)?==<~wCW+PeJIYrjeKZ~v0N^V#tB*(-~i_ty{qc|Ec7 z?Xh=;ns>MzRkD=*&7>wf$DU({bV zhJW`9Wp1B(zklr?3x+jH4Gb!r3Jji191fEhm?o$Y#eJ;1?AN^e-=A;%#p!V6Z*u(K zmZi(8oAewQCb0-Ks4%i9crq}02ocN8-2B}9_UG#QGv=a<=PK&%y8XZZ)z(p*R7XL5 zQ1a8(p}LOY`IYZ1u|#i%Dm6M|Kj`m^4s6`&tp2EZ?ivdaorvJZ~LoGX_8&o{*-$##n!Q* zc-_9|=1-YOEUIDUy54?vi&TGs9jk!Vz!YvW%6yPPQu#M;3eS1V$uuYQ{%7mH}J)HkUJEmD+%oW8VGxCaZOoK?;~w5TBXP(R*LvytF!qsmFowF{g|Ek z&F}yFf6PCx&&|$^j_e$X#bTrRoH@_lyQBMo?dL&C*0pNR}|rNSJS zr9R=*1Q09$lALBFBvnbt+LN3NWFSiHN(qt%6&)sJuKVMeH6K^tygA*q#lF&fEet&< zip^#-(M%?&Ulk?8Fhn4WvMex!5G=bPY6)&I)?+Y9VEMKe+SKJDMpULtVVY-3J1#h0 zpVke!VPXLjTgVfo1c+Q}1F_Y|c}sr1Z5&%7sS}5|VZg9bAM4dR zpxy#yC4*6j?h3{htWe)U%wgL>RU&$BmFN0lgjrgrJ~PbJB?ss8+3cK8D|VeN1oJbK zTz)F6N`|Tm%*VFeLILL8AVjW3@@AT6!AaOQhFF#{GBhA)KuHQfSBRi1x+*9YO_L3j zEbFS(+iy~9DJp?zf9Et{YB7#~Q~{BsqAOaNlvO6Xoqza@ zZ?gl5>VvG2D$E$A6spLoq6%2kv7iD)7j#rf2}n|iLXd%pl8nl}!<4fFmgD8_K{`NQ z?vCm<*DfB2tmsgrc`K?0g8A;Wy+u6k$#Qppvql){9jc^bJ)o7aiF}f&ve@0V3dP%r z4^ZRI((f&m9ZiGMx2)R#BfT3ItA^YvVI5ZFBWJ{@7j=>%{x@IkwSm6XgR^EB4f|b~ zef0Y{aT&GGy7WV@Z2ZhR{QG$`Q!IV<>-U+F!`zOci^lZxAHSG^*~h-QxN#*saqQr+ zU8l{}3n%~j{OEmO9?guMdhm<&t2a)_#sWTbE_eJT(|l{~()GN@OL}cH{xt~ZS?CO&m2bY{IdS-j%|DHADWr`=;nndzKZ^o@>8?shm(h1 F{RjVnLoxsW literal 1677 zcmeAS@N?(olHy`uVBq!ia0y~yU_1cA9Be=lQ`H^CK#H@#BN9k+3xhBt!>lM{kzyj%R`fB!p9D@LY(NeoO14GhGf4ME$V zi{JiiegD?!Y?hQCHn&ZSYQNe#syi?Mok1jO+IF9L%X9VuABaXTA)+;hd4Y9qSbq0A z%UlLV4NnF}0S5+RP=w|l_KfrV4a%EH)ro03k>13rjJTM>8k-IbjASEBPY^GH)gMF( zlIvXuhPzC}CMQg%kZUV2*vSrIphn_@6jLRpCx{mr2=8M003z}uw6^Et**}c$kMqRV z@7p^4ILC+ocIV4_W9oPE0}BaU+JNF{{w9W)X;B zL(1*7u~ofi%B1po;`DRcgW71 z>C9wz6Og7wg+QKsX`v`SD2iaUNP`$OLVYMG!N>NgUf!f4LVb(kKYMd4=>?a$oH^$^ z-~auW^Y40T_Qb?^d^{G5O%(FEId~tA?y*sLZv6d)1FuKC{K+5|lP9BlU;BfpW-PY< z54*e=E*4)jDR*RI@d}gMjtA&i?C4b6BXo&{LWR|AHzWSOwJi#^l@S+GMO5^%tZwI5 zeKx;3Tc)c^)Ud><=Y*qe69^m@5~1xh+`w#S#15|s<7iqDg$^WK%81jEL1D3260+Q9 zf+i!0B9su)hOBDI6iy!zFv3Vt;Yvwr+Dz(Z0t>yL2-bY7YR=`J@7aQ{j93pt&s3CF zt0lJ*GWTnWY8Zxsu!6A!2q{=`L(-PqV6xAUV*&MTFSNNUM2w`um&1$*O1myN-hkE( zdSQZqDQ)5@s*EC+IzWpKaNe@t=oq)C!WztBZWsVo9bmmW4|!1MccC6E-(dhkTPzN2 z?5V|Z1}1`VrU`EJ0fbg3QUt8_&%8jD*}TbKdADs??V(U@WV` z;RV~Zcq@2%XamcUkY&UuYm%zMRmR9fhKWu3oUZXuC5G2v4piU(oHpCrnEbt zP%;(?$$h~VHLKir2smszq{bA_t%<@QjAoWMxDSRwU1E5?kj<8SUbP#r5X_x8E)-_6 znrdj81U`1;7K>)V4MO5lR>);U2###qmRZr5Zm3w3bezPJUP)3(R}CyxF$gBo}i3mDS6-Xk6lWVyG$RcAnYk1FX}4|p|f5uZ)hAa?hyLgjAa1Jb;= z^ao32SJSZcJ*)QrNbiM()`?qV&|wuZaz=@IQ8y{dfAiH@8|qtqI5fj(IOxLgG3evW z1!^C<^v{oNyaFBmy+UrfymJwAZfw629Af^}G0Fe!cnA zp~>%OZ=e0~{HGH;H=d+l;x|jy?XRz?+t<`v(x1DlU#)I$?cdyZ;o{4qoALUgm67<# zSz+g$C&n(W{q^wagO?v7kAHUN)TIyKKX!fO)>9X5I?>=j>Da#Q(U0FAyB`1O;FYyc u&i;A?hWVS{?9M-W;q&#K-HFR`_1V|YfBQzJxe)z(#R@aCxl6}So%;tyN;=5^ literal 1935 zcmeAS@N?(olHy`uVBq!ia0y~yU_1cA9Be=lQ`H^CK#H+A$lZxy-8q?;3=Hg^o-U3d z6?5L+-I#mWLBQ4VqGs&G;_$UGqB9L+n|C(xADMlUHEv0J@4s&mIrqO8f0btVQGdKJ zM|=POjh`MfZ0KTOnxN9apu(xZ;K{_{Fo`H`@Aet5s_X2}Cw~=gSaJS+?N{Mxv)*y; zVqo+Ta$uOmBG911$fDp$EO*J-dG~Y9=l`CSuf-HoxKD2J|M$Pm+| zwQtCNRo$dVhGRg+Wc~yjQN8Oo`Oc7f&v@%I^MkId%4q89ZJ6T-G@yGywp{ Clb2}# diff --git a/assets/voxygen/element/icons/axe.png b/assets/voxygen/element/icons/axe.png index a113585e52d87b4cb057e44ce5e35c5428b793e3..8cf0e5e90f2276db6b420c665263ae12a151d39c 100644 GIT binary patch delta 460 zcmX@XbCY+1CFA3XR+ANs70fO549!eTjm&ivj0}tn^bLW?RM*hl%EZFT*l6S1D~tk0 zx<+Op20*Tvm64IQf#KwfOtP4Q|Cq{DEX^$vlg)wpfuy-^qPa%u7#Au~pKa{Ea29{#N3H>kJGGb39!f zLp(a)UiRf`P!M2!u+w`r-^Epa4;`&$9?y8%+SSvjc~*XwI*XoMk3!A46)rB(k4rY_ zoZC5WhQ+b!Ke8CbdG2**=4V!D3{d45j zpU9I8W=)QG`26}otxF=SnJq;(6a{cML((*_HUBTfQ6tq)8bHhx+B d@`3&V=3la`nh)g*F999O;OXk;vd$@?2>|%jn?3*l delta 349 zcmcb~dxB?zC8O9xtH}yR3g(u2hGr%vW(GP6Mg~R(`Ua-@hDN%EW>y9!R)*#q-(F!9 zFwiwH3NbPQi5MAb8yHQ#$Rvv?_>ZYu%z{`|{jWTUWkECyOS#}a ziBq)Mxo6($h7jrGnjh8yr4zFH0)5#V8#UK({KyOaAe}71^Je4qr_%Ry69Wv|d}Hej zB@PFjWGmi~V<3K-N%clnK-XF(!5c{et-4GTBZOR%Hz#oBuzV7Hd~?ddCc)+lzrV7y mwnZ?6Nei#$KeW~TFaL9PS%I0;I{bm2WbkzLb6Mw<&;$SmkcV0T diff --git a/assets/voxygen/element/icons/bow.png b/assets/voxygen/element/icons/bow.png index dd6bb2c8f18717001086cbc2ead5e2610a883422..4c4734049382f0f2f4373c050746e829bb9cdff0 100644 GIT binary patch delta 464 zcmdnTbC-95CFA3XR+ANs70fO549!eTjm&ivj0}tn^bLW?RM*hl%EZFT*l6S1D~tk0 zx<+Op20*TBW|SyONwdnyR46WSEi17~OfJdH&$Cr3PAr=o!6N5Dj4?`3z2zCH zK%F3`>l$J>bRUbhypf)vf<|~|UP^v>u_jDcaz{b1vTsazsYe7`UBW@9+ces1UWWeOIyJqs20tTh&!5+Kj-mmJ^qOx$w?JNN8Y zVobAN^*dh<`8eT}UaZqkLq|tWTi*S=B3Bj^7_@3d)v*_6h#ItovCm(@DX}O`VezsB jmCkIfmCOlhz0Ky=osVqJ3n-BXI+nrH)z4*}Q$iB}U!|K5 delta 339 zcmcc1yN_prC8O9xtH}yR3g(u2hGr%vW(GP6Mg~R(`Ua-@hDN%EW>y9!R)*#q-(F!9 zFwiwH3NbPQiKlFVbdf{~O9<;^nx_W#=`Q?@DiC azuGVAcp`H^|F}5NYYd*QelF{r5}E*D(Soi3 diff --git a/assets/voxygen/element/icons/daggers.png b/assets/voxygen/element/icons/daggers.png index 213df798dc3b68813f8c4def659e7dc357c0a307..1ae79c7484fb62fc3beabd4e4d7ac9e76ff283ab 100644 GIT binary patch delta 491 zcmcb|^MiMSCFA3XR+ANs70fO549!eTjm&ivj0}tn^bLW?RM*hl%EZFT*l6S1D~tk0 zx<+Op20*T{m63tAf#KwfOtP4Q|Cq{DQc?_4OwBA4bxqBajdhI;O)PX1(=5$(%~DO0 zEDclAjLcILH_u>BW|SyONwdnyR46WSEi17~OfJdH&$Cr3PAr=o!6N5Dj4?`3z2zCH zK%F3`>l$J>bRUbhypf)vf<|~|UP^v>u_jDcazqohU|`to z>EamTas2JIje^YvJgfn|M^!Xq3|f|-RFG4)wp{+`z9#1aeVyPZ8YL!wsySGkT%ME` z-dzyCc55By3V|%E6&`E1KDH2*Iq>L#!n5XYKUEU!I21S^=dEN)-!S)czw>LQSZRPV=M45X z-u^FnZ-!RZ7iM9bZMtuMdoHTHoh0=m{y9!R)*#q-(F!9 zFwiwH3NbPQi5Qq^8yHQ#$Rvv?_>ZY!7;Y`SrdXTZ4pa z`w}IN={7%6)SuJW{;_PYN#~C>tg8}t|J~`iB7<9}g<)|>vug+QT9d}5Grs?;c8kk5 z%s9I0fY2G1Q}2VjW;(FdXBm9f?sec)d*6QW`P?9buB17S=NvwGy*cPeN@OPAW0`Z! z4t^6nGg6Bt3RtY?;a$5)Cu5~gUiN;iXx5#pb*2`6ee)ak$wm&>TzcYBc L`njxgN@xNA(vFtR diff --git a/assets/voxygen/element/icons/danari_f.png b/assets/voxygen/element/icons/danari_f.png index 677cc239e440dad49432c9ac21cd86e87540da53..cc61ae1bf08ac6bfe0b0b3f526d7805c0a36df95 100644 GIT binary patch literal 9880 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 diff --git a/assets/voxygen/element/icons/danari_m.png b/assets/voxygen/element/icons/danari_m.png index f4451a097726d0856ed84f004fa5d66a8907db1b..af018fbe3525c3b66b83b057be97ed0206e6aa34 100644 GIT binary patch literal 16945 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 M<=tVk6 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 diff --git a/assets/voxygen/element/icons/dwarf_f.png b/assets/voxygen/element/icons/dwarf_f.png index bc3e48eb1f94baad083925d7fd4bebe17676bd2e..20e002ecd533433a1176b05e3a71ce276f8f6fe9 100644 GIT binary patch literal 15233 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 delta 13207 zcmYkjRX|kT_dh(~D5cUV3ewWuseqD7cZuXs(tSn+Q9^`~E~RrokY?!a9vTsbhM_zD z=lT9F-uLR9i?jFIYwfi@E6)d?J?HP@0EE%{m9_>1;?D|!z&}AC=eOW<0|N2lhd?$h zArQ$#2!z@-)}lih0ul9AQB=_No!L&CEoJTrT5QtOI54%fwX@5qoIG$um6yM9dQtU> z1(!m+iuBIsG`ux7=)D6HMpuO=2XC8b{CeweLm5TM#EDht@$l*KzkV>peXA+es942n z@2eCap#v+%|G5s`RKvsHw;Nqy%MJ~%^budlJ z90GCkln%NWpIgONr{Y2&;>sk9h>?4;{OgD`4}qNk*`|OzMB2h_?`cD~9u!jDn&OH= zja@F*C+{9FHo;^SnB>7ls{gl$Wu5$mY}5R~`SpGZ_B|y|bIIOUgWa8@8|xwVS5SNi z56u(XlI>qvwS~2#xiH&qAG{*8wJWz%@4i_Yr&xYk}N`