From a8bec0280ce40a6860f9400cfbffa6dc69f668ec Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Nov 2021 12:56:07 -0500 Subject: [PATCH] Ability pool mostly functional. --- common/net/src/msg/ecs_packet.rs | 7 + common/src/comp/ability.rs | 170 ++++++++++++++++++++++- common/src/comp/mod.rs | 2 +- common/src/states/behavior.rs | 5 +- common/src/states/utils.rs | 65 ++------- common/state/src/state.rs | 1 + common/systems/src/character_behavior.rs | 17 ++- server/src/state_ext.rs | 6 + 8 files changed, 206 insertions(+), 67 deletions(-) diff --git a/common/net/src/msg/ecs_packet.rs b/common/net/src/msg/ecs_packet.rs index 3dbe51e384..13a51ee6bd 100644 --- a/common/net/src/msg/ecs_packet.rs +++ b/common/net/src/msg/ecs_packet.rs @@ -15,6 +15,7 @@ sum_type! { CanBuild(comp::CanBuild), Stats(comp::Stats), SkillSet(comp::SkillSet), + AbilityPool(comp::AbilityPool), Buffs(comp::Buffs), Auras(comp::Auras), Energy(comp::Energy), @@ -50,6 +51,7 @@ sum_type! { CanBuild(PhantomData), Stats(PhantomData), SkillSet(PhantomData), + AbilityPool(PhantomData), Buffs(PhantomData), Auras(PhantomData), Energy(PhantomData), @@ -85,6 +87,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::SkillSet(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::AbilityPool(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Auras(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), @@ -124,6 +127,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::SkillSet(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::AbilityPool(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Auras(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), @@ -163,6 +167,9 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::CanBuild(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Stats(_) => sync::handle_remove::(entity, world), EcsCompPhantom::SkillSet(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::AbilityPool(_) => { + sync::handle_remove::(entity, world) + }, EcsCompPhantom::Buffs(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Auras(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Energy(_) => sync::handle_remove::(entity, world), diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index b6eb61fe98..e4233412e2 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -3,9 +3,17 @@ use crate::{ combat::{self, CombatEffect, DamageKind, Knockback}, comp::{ self, aura, beam, buff, - inventory::item::tool::{Stats, ToolKind}, + controller::InputKind, + inventory::{ + item::{ + tool::{Stats, ToolKind}, + ItemKind, + }, + slot::EquipSlot, + Inventory, + }, projectile::ProjectileConstructor, - skills::{self, SKILL_MODIFIERS}, + skills::{self, Skill, SkillSet, SKILL_MODIFIERS}, Body, CharacterState, LightEmitter, StateUpdate, }, states::{ @@ -16,8 +24,162 @@ use crate::{ terrain::SpriteKind, }; use serde::{Deserialize, Serialize}; +use specs::{Component, DerefFlaggedStorage}; +use specs_idvs::IdvStorage; use std::{convert::TryFrom, time::Duration}; +pub const MAX_ABILITIES: usize = 5; + +// TODO: Should primary, secondary, and dodge be moved into here? Would +// essentially require custom enum that are only used for those (except maybe +// dodge if we make movement and have potentially differ based off of armor) but +// would also allow logic to be a bit more centralized +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AbilityPool { + primary: Ability, + secondary: Ability, + movement: Ability, + abilities: [Ability; MAX_ABILITIES], +} + +impl Component for AbilityPool { + type Storage = DerefFlaggedStorage>; +} + +impl Default for AbilityPool { + fn default() -> Self { + Self { + primary: Ability::ToolPrimary, + secondary: Ability::ToolSecondary, + movement: Ability::SpeciesMovement, + abilities: [Ability::Empty; MAX_ABILITIES], + } + } +} + +impl AbilityPool { + pub fn new(inv: Option<&Inventory>, skill_set: Option<&SkillSet>) -> Self { + let mut pool = Self::default(); + pool.auto_update(inv, skill_set); + pool + } + + pub fn change_ability(&mut self, slot: usize, new_ability: Ability) { + if let Some(ability) = self.abilities.get_mut(slot) { + *ability = new_ability; + } + } + + pub fn activate_ability( + &self, + input: InputKind, + inv: Option<&Inventory>, + skill_set: &SkillSet, + body: &Body, + // bool is from_offhand + ) -> Option<(CharacterAbility, bool)> { + let ability = match input { + InputKind::Primary => Some(self.primary), + InputKind::Secondary => Some(self.secondary), + InputKind::Roll => Some(self.movement), + InputKind::Ability(index) => self.abilities.get(index).copied(), + _ => None, + } + .unwrap_or(Ability::Empty); + + let ability_set = |equip_slot| { + inv.and_then(|inv| inv.equipped(equip_slot)) + .map(|i| &i.item_config_expect().abilities) + }; + + let scale_ability = |ability: CharacterAbility, equip_slot| { + let tool_kind = + inv.and_then(|inv| inv.equipped(equip_slot)) + .and_then(|item| match &item.kind { + ItemKind::Tool(tool) => Some(tool.kind), + _ => None, + }); + ability.adjusted_by_skills(skill_set, tool_kind) + }; + + let unlocked = |(s, a): (Option, CharacterAbility)| { + // If there is a skill requirement and the skillset does not contain the + // required skill, return None + s.map_or(true, |s| skill_set.has_skill(s)).then_some(a) + }; + + match ability { + Ability::ToolPrimary => ability_set(EquipSlot::ActiveMainhand) + .map(|abilities| abilities.primary.clone()) + .map(|ability| (scale_ability(ability, EquipSlot::ActiveMainhand), false)), + Ability::ToolSecondary => ability_set(EquipSlot::ActiveOffhand) + .map(|abilities| abilities.secondary.clone()) + .map(|ability| (scale_ability(ability, EquipSlot::ActiveOffhand), true)) + .or({ + ability_set(EquipSlot::ActiveMainhand) + .map(|abilities| abilities.secondary.clone()) + .map(|ability| (scale_ability(ability, EquipSlot::ActiveMainhand), false)) + }), + Ability::SpeciesMovement => matches!(body, Body::Humanoid(_)) + .then_some(CharacterAbility::default_roll()) + .map(|ability| (ability.adjusted_by_skills(skill_set, None), false)), + Ability::MainWeaponAbility(index) => ability_set(EquipSlot::ActiveMainhand) + .and_then(|abilities| abilities.abilities.get(index).cloned()) + .and_then(unlocked) + .map(|ability| (scale_ability(ability, EquipSlot::ActiveMainhand), false)), + Ability::OffWeaponAbility(index) => ability_set(EquipSlot::ActiveOffhand) + .and_then(|abilities| abilities.abilities.get(index).cloned()) + .and_then(unlocked) + .map(|ability| (scale_ability(ability, EquipSlot::ActiveOffhand), true)), + Ability::Empty => None, + } + } + + // TODO: Potentially remove after there is an actual UI + pub fn auto_update(&mut self, inv: Option<&Inventory>, skill_set: Option<&SkillSet>) { + fn iter_unlocked_abilities( + inv: Option<&Inventory>, + skill_set: Option<&SkillSet>, + equip_slot: EquipSlot, + ) -> Vec { + let ability_from_slot = move |i| match equip_slot { + EquipSlot::ActiveMainhand => Ability::MainWeaponAbility(i), + EquipSlot::ActiveOffhand => Ability::OffWeaponAbility(i), + _ => Ability::Empty, + }; + + inv + .and_then(|inv| inv.equipped(equip_slot)) + .iter() + .flat_map(|i| &i.item_config_expect().abilities.abilities) + .enumerate() + .filter_map(move |(i, (skill, _))| skill.map_or(true, |s| skill_set.map_or(false, |ss| ss.has_skill(s))).then_some(ability_from_slot(i))) + // TODO: Let someone smarter than borrow checker remove collect + .collect() + } + + let main_abilities = iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveMainhand); + let off_abilities = iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveOffhand); + for (i, ability) in + (0..MAX_ABILITIES).zip(main_abilities.iter().chain(off_abilities.iter())) + { + self.change_ability(i, *ability); + } + } +} + +#[derive(Copy, Clone, Serialize, Deserialize, Debug)] +pub enum Ability { + ToolPrimary, + ToolSecondary, + SpeciesMovement, + MainWeaponAbility(usize), + OffWeaponAbility(usize), + Empty, + /* For future use + * ArmorAbility(usize), */ +} + #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum CharacterAbilityType { BasicMelee, @@ -899,7 +1061,7 @@ impl CharacterAbility { #[warn(clippy::pedantic)] fn adjusted_by_mining_skills(&mut self, skillset: &skills::SkillSet) { - use skills::{MiningSkill::Speed, Skill}; + use skills::MiningSkill::Speed; if let CharacterAbility::BasicMelee { ref mut buildup_duration, @@ -921,8 +1083,6 @@ impl CharacterAbility { #[warn(clippy::pedantic)] fn adjusted_by_general_skills(&mut self, skillset: &skills::SkillSet) { - use skills::Skill; - if let CharacterAbility::Roll { ref mut energy_cost, ref mut roll_strength, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 4d6411722c..3c8f9be3ff 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -47,7 +47,7 @@ pub mod visual; // Reexports #[cfg(not(target_arch = "wasm32"))] pub use self::{ - ability::{CharacterAbility, CharacterAbilityType}, + ability::{Ability, AbilityPool, CharacterAbility, CharacterAbilityType}, admin::{Admin, AdminRole}, agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController}, anchor::Anchor, diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index d98f1a0b84..ea8dda5cf2 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -1,6 +1,6 @@ use crate::{ comp::{ - self, character_state::OutputEvents, item::MaterialStatManifest, Beam, Body, + self, character_state::OutputEvents, item::MaterialStatManifest, AbilityPool, Beam, Body, CharacterState, Combo, ControlAction, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, StateUpdate, Stats, Vel, @@ -124,6 +124,7 @@ pub struct JoinData<'a> { pub updater: &'a LazyUpdate, pub stats: &'a Stats, pub skill_set: &'a SkillSet, + pub ability_pool: &'a AbilityPool, pub msm: &'a MaterialStatManifest, pub combo: &'a Combo, pub alignment: Option<&'a comp::Alignment>, @@ -149,6 +150,7 @@ pub struct JoinStruct<'a> { pub beam: Option<&'a Beam>, pub stat: &'a Stats, pub skill_set: &'a SkillSet, + pub ability_pool: &'a AbilityPool, pub combo: &'a Combo, pub alignment: Option<&'a comp::Alignment>, pub terrain: &'a TerrainGrid, @@ -186,6 +188,7 @@ impl<'a> JoinData<'a> { combo: j.combo, alignment: j.alignment, terrain: j.terrain, + ability_pool: j.ability_pool, } } } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 039157f7a0..832a7f86f6 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -821,61 +821,16 @@ pub fn handle_jump( } fn handle_ability(data: &JoinData<'_>, update: &mut StateUpdate, input: InputKind) { - let hands = get_hands(data); - - // Mouse1 and Skill1 always use the MainHand slot - let always_main_hand = matches!(input, InputKind::Primary | InputKind::Ability(0)); - let no_main_hand = hands.0.is_none(); - // skill_index used to select ability for the AbilityKey::Skill2 input - let (equip_slot, skill_index) = if no_main_hand { - (Some(EquipSlot::ActiveOffhand), 1) - } else if always_main_hand { - (Some(EquipSlot::ActiveMainhand), 0) - } else { - match hands { - (Some(Hands::Two), _) => (Some(EquipSlot::ActiveMainhand), 1), - (_, Some(Hands::One)) => (Some(EquipSlot::ActiveOffhand), 0), - (Some(Hands::One), _) => (Some(EquipSlot::ActiveMainhand), 1), - (_, _) => (None, 0), - } - }; - - let unlocked = |(s, a): (Option, CharacterAbility)| { - s.map_or(true, |s| data.skill_set.has_skill(s)).then_some(a) - }; - - if let Some(equip_slot) = equip_slot { - if let Some(ability) = data - .inventory - .and_then(|inv| inv.equipped(equip_slot)) - .map(|i| &i.item_config_expect().abilities) - .and_then(|abilities| match input { - InputKind::Primary => Some(abilities.primary.clone()), - InputKind::Secondary => Some(abilities.secondary.clone()), - InputKind::Ability(0) => abilities.abilities.get(0).cloned().and_then(unlocked), - InputKind::Ability(i) => abilities - .abilities - .get(if i < 2 { skill_index } else { i }) - .cloned() - .and_then(unlocked), - InputKind::Roll | InputKind::Jump | InputKind::Fly | InputKind::Block => None, - }) - .map(|a| { - let tool = unwrap_tool_data(data, equip_slot).map(|t| t.kind); - a.adjusted_by_skills(data.skill_set, tool) - }) - .filter(|ability| ability.requirements_paid(data, update)) - { - update.character = CharacterState::from(( - &ability, - AbilityInfo::from_input( - data, - matches!(equip_slot, EquipSlot::ActiveOffhand), - input, - ), - data, - )); - } + if let Some((ability, from_offhand)) = data + .ability_pool + .activate_ability(input, data.inventory, data.skill_set, data.body) + .filter(|(ability, _)| ability.requirements_paid(data, update)) + { + update.character = CharacterState::from(( + &ability, + AbilityInfo::from_input(data, from_offhand, input), + data, + )); } } diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 599d6013d4..8a0c817148 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -129,6 +129,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index e9ec5fb443..6adff0a268 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -5,9 +5,10 @@ use specs::{ use common::{ comp::{ - self, character_state::OutputEvents, inventory::item::MaterialStatManifest, Beam, Body, - CharacterState, Combo, Controller, Density, Energy, Health, Inventory, InventoryManip, - Mass, Melee, Mounting, Ori, PhysicsState, Poise, Pos, SkillSet, StateUpdate, Stats, Vel, + self, character_state::OutputEvents, inventory::item::MaterialStatManifest, AbilityPool, + Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, Inventory, + InventoryManip, Mass, Melee, Mounting, Ori, PhysicsState, Poise, Pos, SkillSet, + StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, outcome::Outcome, @@ -38,6 +39,7 @@ pub struct ReadData<'a> { mountings: ReadStorage<'a, Mounting>, stats: ReadStorage<'a, Stats>, skill_sets: ReadStorage<'a, SkillSet>, + ability_pools: ReadStorage<'a, AbilityPool>, msm: Read<'a, MaterialStatManifest>, combos: ReadStorage<'a, Combo>, alignments: ReadStorage<'a, comp::Alignment>, @@ -107,7 +109,7 @@ impl<'a> System<'a> for Sys { health, body, physics, - (stat, skill_set), + (stat, skill_set, ability_pool), combo, ) in ( &read_data.entities, @@ -124,7 +126,11 @@ impl<'a> System<'a> for Sys { read_data.healths.maybe(), &read_data.bodies, &read_data.physics_states, - (&read_data.stats, &read_data.skill_sets), + ( + &read_data.stats, + &read_data.skill_sets, + &read_data.ability_pools, + ), &read_data.combos, ) .join() @@ -181,6 +187,7 @@ impl<'a> System<'a> for Sys { beam: read_data.beams.get(entity), stat, skill_set, + ability_pool, combo, alignment: read_data.alignments.get(entity), terrain: &read_data.terrain, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 78f0bc6e49..04442a00cb 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -215,6 +215,7 @@ impl StateExt for State { .unwrap_or(0), )) .with(stats) + .with(comp::AbilityPool::new(Some(&inventory), Some(&skill_set))) .with(skill_set) .maybe_with(health) .with(poise) @@ -267,6 +268,7 @@ impl StateExt for State { .with(comp::Energy::new(ship.into(), 0)) .with(comp::Stats::new("Airship".to_string())) .with(comp::SkillSet::default()) + .with(comp::AbilityPool::default()) .with(comp::Combo::default()); if mountable { @@ -504,6 +506,10 @@ impl StateExt for State { self.write_component_ignore_entity_dead(entity, comp::Energy::new(body, energy_level)); self.write_component_ignore_entity_dead(entity, comp::Poise::new(body)); self.write_component_ignore_entity_dead(entity, stats); + self.write_component_ignore_entity_dead( + entity, + comp::AbilityPool::new(Some(&inventory), Some(&skill_set)), + ); self.write_component_ignore_entity_dead(entity, skill_set); self.write_component_ignore_entity_dead(entity, inventory); self.write_component_ignore_entity_dead(