Addressed review feedback.

This commit is contained in:
Sam 2021-11-11 17:55:14 -05:00
parent b678f7f46e
commit dfcb8c8519
14 changed files with 221 additions and 128 deletions

View File

@ -755,8 +755,7 @@ impl Client {
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::RequestPlayerPhysics { .. }
| ClientGeneral::RequestLossyTerrainCompression { .. }
| ClientGeneral::ChangeAbility { .. } => {
| ClientGeneral::RequestLossyTerrainCompression { .. } => {
#[cfg(feature = "tracy")]
{
ingame = 1.0;
@ -1397,8 +1396,11 @@ impl Client {
)));
}
pub fn change_ability(&mut self, slot: usize, new_ability: comp::Ability) {
self.send_msg(ClientGeneral::ChangeAbility { slot, new_ability })
pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
slot,
new_ability,
}))
}
/// Execute a single client tick, handle input and update the game state by

View File

@ -75,10 +75,6 @@ pub enum ClientGeneral {
RefundSkill(Skill),
UnlockSkillGroup(SkillGroupKind),
RequestSiteInfo(SiteId),
ChangeAbility {
slot: usize,
new_ability: comp::Ability,
},
//Only in Game, via terrain stream
TerrainChunkRequest {
key: Vec2<i32>,
@ -131,8 +127,7 @@ impl ClientMsg {
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_)
| ClientGeneral::RequestPlayerPhysics { .. }
| ClientGeneral::RequestLossyTerrainCompression { .. }
| ClientGeneral::ChangeAbility { .. } => {
| ClientGeneral::RequestLossyTerrainCompression { .. } => {
c_type == ClientType::Game && presence.is_some()
},
//Always possible

View File

@ -3,7 +3,6 @@ use crate::{
combat::{self, CombatEffect, DamageKind, Knockback},
comp::{
self, aura, beam, buff,
controller::InputKind,
inventory::{
item::{
tool::{Stats, ToolKind},
@ -36,10 +35,10 @@ pub const MAX_ABILITIES: usize = 5;
// considerations.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AbilityPool {
pub primary: Ability,
pub secondary: Ability,
pub movement: Ability,
pub abilities: [Ability; MAX_ABILITIES],
pub primary: PrimaryAbility,
pub secondary: SecondaryAbility,
pub movement: MovementAbility,
pub abilities: [AuxiliaryAbility; MAX_ABILITIES],
}
impl Component for AbilityPool {
@ -49,10 +48,10 @@ impl Component for AbilityPool {
impl Default for AbilityPool {
fn default() -> Self {
Self {
primary: Ability::ToolPrimary,
secondary: Ability::ToolSecondary,
movement: Ability::SpeciesMovement,
abilities: [Ability::Empty; MAX_ABILITIES],
primary: PrimaryAbility::Tool,
secondary: SecondaryAbility::Tool,
movement: MovementAbility::Species,
abilities: [AuxiliaryAbility::Empty; MAX_ABILITIES],
}
}
}
@ -64,28 +63,31 @@ impl AbilityPool {
pool
}
pub fn change_ability(&mut self, slot: usize, new_ability: Ability) {
if new_ability.is_valid_abilities_ability() {
if let Some(ability) = self.abilities.get_mut(slot) {
*ability = new_ability;
}
pub fn change_ability(&mut self, slot: usize, new_ability: AuxiliaryAbility) {
if let Some(ability) = self.abilities.get_mut(slot) {
*ability = new_ability;
}
}
pub fn get_ability(&self, input: InputKind) -> Ability {
pub fn get_ability(&self, input: AbilityInput) -> 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,
AbilityInput::Primary => self.primary.into(),
AbilityInput::Secondary => self.secondary.into(),
AbilityInput::Movement => self.movement.into(),
AbilityInput::Auxiliary(index) => self
.abilities
.get(index)
.copied()
.map(|a| a.into())
.unwrap_or(Ability::Empty),
}
.unwrap_or(Ability::Empty)
}
/// Returns the CharacterAbility from an ability input, and also whether the
/// ability was from a weapon wielded in the offhand
pub fn activate_ability(
&self,
input: InputKind,
input: AbilityInput,
inv: Option<&Inventory>,
skill_set: &SkillSet,
body: &Body,
@ -121,19 +123,19 @@ impl AbilityPool {
Ability::ToolSecondary => ability_set(EquipSlot::ActiveOffhand)
.map(|abilities| abilities.secondary.clone())
.map(|ability| (scale_ability(ability, EquipSlot::ActiveOffhand), true))
.or({
.or_else(|| {
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)
.then(|| CharacterAbility::default_roll)
.map(|ability| (ability().adjusted_by_skills(skill_set, None), false)),
Ability::MainWeaponAux(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)
Ability::OffWeaponAux(index) => ability_set(EquipSlot::ActiveOffhand)
.and_then(|abilities| abilities.abilities.get(index).cloned())
.and_then(unlocked)
.map(|ability| (scale_ability(ability, EquipSlot::ActiveOffhand), true)),
@ -147,11 +149,11 @@ impl AbilityPool {
inv: Option<&Inventory>,
skill_set: Option<&SkillSet>,
equip_slot: EquipSlot,
) -> Vec<Ability> {
) -> Vec<AuxiliaryAbility> {
let ability_from_slot = move |i| match equip_slot {
EquipSlot::ActiveMainhand => Ability::MainWeaponAbility(i),
EquipSlot::ActiveOffhand => Ability::OffWeaponAbility(i),
_ => Ability::Empty,
EquipSlot::ActiveMainhand => AuxiliaryAbility::MainWeapon(i),
EquipSlot::ActiveOffhand => AuxiliaryAbility::OffWeapon(i),
_ => AuxiliaryAbility::Empty,
};
inv
@ -166,28 +168,36 @@ impl AbilityPool {
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);
}
(0..MAX_ABILITIES)
.zip(main_abilities.iter().chain(off_abilities.iter()))
.for_each(|(i, ability)| {
self.change_ability(i, *ability);
})
}
}
pub enum AbilityInput {
Primary,
Secondary,
Movement,
Auxiliary(usize),
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum Ability {
ToolPrimary,
ToolSecondary,
SpeciesMovement,
MainWeaponAbility(usize),
OffWeaponAbility(usize),
MainWeaponAux(usize),
OffWeaponAux(usize),
Empty,
/* For future use
* ArmorAbility(usize), */
}
impl Ability {
pub fn ability_id(self, inv: Option<&Inventory>) -> Option<&String> {
pub fn ability_id(self, inv: Option<&Inventory>) -> Option<&str> {
let ability_id_set = |equip_slot| {
inv.and_then(|inv| inv.equipped(equip_slot))
.map(|i| &i.item_config_expect().ability_ids)
@ -195,30 +205,81 @@ impl Ability {
match self {
Ability::ToolPrimary => {
ability_id_set(EquipSlot::ActiveMainhand).map(|ids| &ids.primary)
ability_id_set(EquipSlot::ActiveMainhand).map(|ids| ids.primary.as_str())
},
Ability::ToolSecondary => ability_id_set(EquipSlot::ActiveOffhand)
.map(|ids| &ids.secondary)
.or_else(|| ability_id_set(EquipSlot::ActiveMainhand).map(|ids| &ids.secondary)),
.map(|ids| ids.secondary.as_str())
.or_else(|| {
ability_id_set(EquipSlot::ActiveMainhand).map(|ids| ids.secondary.as_str())
}),
Ability::SpeciesMovement => None, // TODO: Make not None
Ability::MainWeaponAbility(index) => ability_id_set(EquipSlot::ActiveMainhand)
.and_then(|ids| ids.abilities.get(index).map(|(_, id)| id)),
Ability::OffWeaponAbility(index) => ability_id_set(EquipSlot::ActiveOffhand)
.and_then(|ids| ids.abilities.get(index).map(|(_, id)| id)),
Ability::MainWeaponAux(index) => ability_id_set(EquipSlot::ActiveMainhand)
.and_then(|ids| ids.abilities.get(index).map(|(_, id)| id.as_str())),
Ability::OffWeaponAux(index) => ability_id_set(EquipSlot::ActiveOffhand)
.and_then(|ids| ids.abilities.get(index).map(|(_, id)| id.as_str())),
Ability::Empty => None,
}
}
}
/// Determines whether an ability is a valid entry for the abilities array
/// on the ability pool
pub fn is_valid_abilities_ability(self) -> bool {
match self {
Ability::ToolPrimary => false,
Ability::ToolSecondary => false,
Ability::SpeciesMovement => false,
Ability::MainWeaponAbility(_) => true,
Ability::OffWeaponAbility(_) => true,
Ability::Empty => true,
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum PrimaryAbility {
Tool,
Empty,
}
impl From<PrimaryAbility> for Ability {
fn from(primary: PrimaryAbility) -> Self {
match primary {
PrimaryAbility::Tool => Ability::ToolPrimary,
PrimaryAbility::Empty => Ability::Empty,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum SecondaryAbility {
Tool,
Empty,
}
impl From<SecondaryAbility> for Ability {
fn from(primary: SecondaryAbility) -> Self {
match primary {
SecondaryAbility::Tool => Ability::ToolSecondary,
SecondaryAbility::Empty => Ability::Empty,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum MovementAbility {
Species,
Empty,
}
impl From<MovementAbility> for Ability {
fn from(primary: MovementAbility) -> Self {
match primary {
MovementAbility::Species => Ability::SpeciesMovement,
MovementAbility::Empty => Ability::Empty,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum AuxiliaryAbility {
MainWeapon(usize),
OffWeapon(usize),
Empty,
}
impl From<AuxiliaryAbility> for Ability {
fn from(primary: AuxiliaryAbility) -> Self {
match primary {
AuxiliaryAbility::MainWeapon(i) => Ability::MainWeaponAux(i),
AuxiliaryAbility::OffWeapon(i) => Ability::OffWeaponAux(i),
AuxiliaryAbility::Empty => Ability::Empty,
}
}
}

View File

@ -1,5 +1,6 @@
use crate::{
comp::{
ability,
inventory::slot::{EquipSlot, InvSlotId, Slot},
invite::{InviteKind, InviteResponse},
BuffKind,
@ -135,6 +136,10 @@ pub enum ControlEvent {
RemoveBuff(BuffKind),
Respawn,
Utterance(UtteranceKind),
ChangeAbility {
slot: usize,
new_ability: ability::AuxiliaryAbility,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
@ -189,6 +194,19 @@ impl InputKind {
}
}
impl From<InputKind> for Option<ability::AbilityInput> {
fn from(input: InputKind) -> Option<ability::AbilityInput> {
use ability::AbilityInput;
match input {
InputKind::Primary => Some(AbilityInput::Primary),
InputKind::Secondary => Some(AbilityInput::Secondary),
InputKind::Roll => Some(AbilityInput::Movement),
InputKind::Ability(index) => Some(AbilityInput::Auxiliary(index)),
InputKind::Jump | InputKind::Fly | InputKind::Block => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct InputAttr {
pub select_pos: Option<Vec3<f32>>,

View File

@ -451,14 +451,14 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
if let ItemKind::Tool(tool) = &item.kind {
// TODO: Maybe try to make an ecs resource?
let ability_ids_map =
AbilityMap::<String>::load_expect_cloned("common.abilities.ability_set_manifest");
AbilityMap::<String>::load_expect("common.abilities.ability_set_manifest").read();
// If no custom ability set is specified, fall back to abilityset of tool kind.
let (tool_default, tool_default_ids) = {
let key = &AbilitySpec::Tool(tool.kind);
let tool_default = |tool_kind| {
let key = &AbilitySpec::Tool(tool_kind);
(
ability_map.get_ability_set(key).cloned(),
ability_ids_map.get_ability_set(key).cloned(),
ability_map.get_ability_set(key),
ability_ids_map.get_ability_set(key),
)
};
let (abilities, ability_ids) = if let Some(set_key) = item.ability_spec() {
@ -476,13 +476,17 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
default ability set.",
set_key
);
let (abilities, ids) = tool_default(tool.kind);
(
tool_default.unwrap_or_default(),
tool_default_ids.unwrap_or_default(),
abilities.cloned().unwrap_or_default(),
ids.cloned().unwrap_or_default(),
)
}
} else if let (Some(set), Some(ids)) = (tool_default, tool_default_ids) {
(set.modified_by_tool(tool, msm, &item.components), ids)
} else if let (Some(set), Some(ids)) = tool_default(tool.kind) {
(
set.clone().modified_by_tool(tool, msm, &item.components),
ids.clone(),
)
} else {
error!(
"No ability set defined for tool: {:?}, falling back to default ability set.",

View File

@ -381,17 +381,18 @@ impl Default for AbilitySet<CharacterAbility> {
AbilitySet {
primary: CharacterAbility::default(),
secondary: CharacterAbility::default(),
abilities: vec![],
abilities: Vec::new(),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for AbilitySet<String> {
fn default() -> Self {
AbilitySet {
primary: "".to_string(),
secondary: "".to_string(),
abilities: vec![],
primary: String::new(),
secondary: String::new(),
abilities: Vec::new(),
}
}
}

View File

@ -1,4 +1,5 @@
#[cfg(not(target_arch = "wasm32"))] mod ability;
#[cfg(not(target_arch = "wasm32"))]
pub mod ability;
#[cfg(not(target_arch = "wasm32"))] mod admin;
#[cfg(not(target_arch = "wasm32"))] pub mod agent;
#[cfg(not(target_arch = "wasm32"))]

View File

@ -821,16 +821,18 @@ pub fn handle_jump(
}
fn handle_ability(data: &JoinData<'_>, update: &mut StateUpdate, input: InputKind) {
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,
));
if let Some(ability_input) = input.into() {
if let Some((ability, from_offhand)) = data
.ability_pool
.activate_ability(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,
));
}
}
}

View File

@ -1,7 +1,7 @@
use common::{
comp::{
agent::{Sound, SoundKind},
Body, BuffChange, ControlEvent, Controller, Pos,
AbilityPool, Body, BuffChange, ControlEvent, Controller, Pos,
},
event::{EventBus, ServerEvent},
uid::UidAllocator,
@ -27,16 +27,25 @@ pub struct ReadData<'a> {
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (ReadData<'a>, WriteStorage<'a, Controller>);
type SystemData = (
ReadData<'a>,
WriteStorage<'a, Controller>,
WriteStorage<'a, AbilityPool>,
);
const NAME: &'static str = "controller";
const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, (read_data, mut controllers): Self::SystemData) {
fn run(
_job: &mut Job<Self>,
(read_data, mut controllers, mut ability_pools): Self::SystemData,
) {
let mut server_emitter = read_data.server_bus.emitter();
for (entity, controller) in (&read_data.entities, &mut controllers).join() {
for (entity, controller, mut ability_pool) in
(&read_data.entities, &mut controllers, &mut ability_pools).join()
{
// Sanitize inputs to avoid clients sending bad data
controller.inputs.sanitize();
@ -103,6 +112,9 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Sound { sound });
}
},
ControlEvent::ChangeAbility { slot, new_ability } => {
ability_pool.change_ability(slot, new_ability)
},
}
}
}

View File

@ -3,8 +3,8 @@ use crate::TerrainPersistence;
use crate::{client::Client, presence::Presence, Settings};
use common::{
comp::{
AbilityPool, Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player,
Pos, SkillSet, Vel,
Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet,
Vel,
},
event::{EventBus, ServerEvent},
resources::PlayerPhysicsSettings,
@ -40,7 +40,6 @@ impl Sys {
velocities: &mut WriteStorage<'_, Vel>,
orientations: &mut WriteStorage<'_, Ori>,
controllers: &mut WriteStorage<'_, Controller>,
ability_pools: &mut WriteStorage<'_, AbilityPool>,
settings: &Read<'_, Settings>,
build_areas: &Read<'_, BuildAreas>,
player_physics_settings: &mut Write<'_, PlayerPhysicsSettings>,
@ -282,11 +281,6 @@ impl Sys {
} => {
presence.lossy_terrain_compression = lossy_terrain_compression;
},
ClientGeneral::ChangeAbility { slot, new_ability } => {
if let Some(mut ability_pool) = ability_pools.get_mut(entity) {
ability_pool.change_ability(slot, new_ability);
}
},
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
@ -321,7 +315,6 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Presence>,
WriteStorage<'a, Client>,
WriteStorage<'a, Controller>,
WriteStorage<'a, AbilityPool>,
Read<'a, Settings>,
Read<'a, BuildAreas>,
Write<'a, PlayerPhysicsSettings>,
@ -351,7 +344,6 @@ impl<'a> System<'a> for Sys {
mut presences,
mut clients,
mut controllers,
mut ability_pools,
settings,
build_areas,
mut player_physics_settings,
@ -387,7 +379,6 @@ impl<'a> System<'a> for Sys {
&mut velocities,
&mut orientations,
&mut controllers,
&mut ability_pools,
&settings,
&build_areas,
&mut player_physics_settings,

View File

@ -64,21 +64,25 @@ impl State {
.read_storage::<common::comp::AbilityPool>()
.get(client.entity())
{
use common::comp::Ability;
for (i, ability) in ability_pool.abilities.iter().enumerate() {
if matches!(ability, Ability::Empty) {
self.slots
.iter_mut()
.filter(|s| matches!(s, Some(SlotContents::Ability(index)) if *index == i))
.for_each(|s| *s = None)
} else if let Some(slot) = self
.slots
.iter_mut()
.find(|s| !matches!(s, Some(SlotContents::Ability(index)) if *index != i))
{
*slot = Some(SlotContents::Ability(i));
use common::comp::ability::AuxiliaryAbility;
for ((i, ability), hotbar_slot) in ability_pool
.abilities
.iter()
.enumerate()
.zip(self.slots.iter_mut())
{
if matches!(ability, AuxiliaryAbility::Empty) {
if matches!(hotbar_slot, Some(SlotContents::Ability(_))) {
// If ability is empty but hotbar shows an ability, clear it
*hotbar_slot = None;
}
} else {
// If an ability is not empty show it on the hotbar
*hotbar_slot = Some(SlotContents::Ability(i));
}
}
} else {
self.slots.iter_mut().for_each(|slot| *slot = None)
}
}
}

View File

@ -534,7 +534,7 @@ pub enum Event {
UnlockSkill(Skill),
RequestSiteInfo(SiteId),
// TODO: This variant currently unused. UI is needed for it to be properly used.
ChangeAbility(usize, comp::Ability),
ChangeAbility(usize, comp::ability::AuxiliaryAbility),
SettingsChange(SettingsChange),
}

View File

@ -21,9 +21,9 @@ use i18n::Localization;
use client::{self, Client};
use common::comp::{
self,
controller::InputKind,
ability::AbilityInput,
item::{ItemDesc, MaterialStatManifest},
AbilityPool, Body, Energy, Health, Inventory, SkillSet,
Ability, AbilityPool, Body, Energy, Health, Inventory, SkillSet,
};
use conrod_core::{
color,
@ -609,7 +609,7 @@ impl<'a> Skillbar<'a> {
hotbar::SlotContents::Ability(i) => ability_pool
.abilities
.get(i)
.and_then(|a| a.ability_id(Some(inventory)))
.and_then(|a| Ability::from(*a).ability_id(Some(inventory)))
.and_then(|id| util::ability_description(id)),
})
};
@ -679,7 +679,8 @@ impl<'a> Skillbar<'a> {
.right_from(state.ids.slot5, slot_offset)
.set(state.ids.m1_slot_bg, ui);
let primary_ability_id = self.ability_pool.primary.ability_id(Some(self.inventory));
let primary_ability_id =
Ability::from(self.ability_pool.primary).ability_id(Some(self.inventory));
Button::image(
primary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
@ -693,7 +694,8 @@ impl<'a> Skillbar<'a> {
.right_from(state.ids.m1_slot_bg, slot_offset)
.set(state.ids.m2_slot_bg, ui);
let secondary_ability_id = self.ability_pool.secondary.ability_id(Some(self.inventory));
let secondary_ability_id =
Ability::from(self.ability_pool.secondary).ability_id(Some(self.inventory));
Button::image(
secondary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
@ -705,7 +707,7 @@ impl<'a> Skillbar<'a> {
>= self
.ability_pool
.activate_ability(
InputKind::Secondary,
AbilityInput::Secondary,
Some(self.inventory),
self.skillset,
self.body,

View File

@ -6,7 +6,7 @@ use super::{
};
use crate::ui::slot::{self, SlotKey, SumSlot};
use common::comp::{
controller::InputKind, slot::InvSlotId, AbilityPool, Body, Energy, Inventory, SkillSet,
ability::AbilityInput, slot::InvSlotId, Ability, AbilityPool, Body, Energy, Inventory, SkillSet,
};
use conrod_core::{image, Color};
use specs::Entity as EcsEntity;
@ -138,14 +138,14 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
let ability_id = ability_pool
.abilities
.get(i)
.and_then(|a| a.ability_id(Some(inventory)));
.and_then(|a| Ability::from(*a).ability_id(Some(inventory)));
ability_id
.map(|id| HotbarImage::Ability(id.to_string()))
.and_then(|image| {
ability_pool
.activate_ability(
InputKind::Ability(i),
AbilityInput::Auxiliary(i),
Some(inventory),
skillset,
body,