Merge branch 'sam/ability_selection' into 'master'

Ability Selection UI

Closes #419, #421, and #1431

See merge request veloren/veloren!3031
This commit is contained in:
Samuel Keiffer 2022-01-16 03:19:21 +00:00
commit dd612a9473
49 changed files with 2086 additions and 724 deletions

View File

@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Piercing damage now ignores an amount of protection equal to damage value
- Slashing damage now reduces target's energy by an amount equal to damage dealt to target post-mitigation
- Crushing damage now does poise damage to a target equal to the amount mitigated by armor
- UI to select abilities and assign to hotbar
- Position of abilities on hotbar is now persisted through the server
### Changed

10
Cargo.lock generated
View File

@ -2651,6 +2651,15 @@ dependencies = [
"serde",
]
[[package]]
name = "inline_tweak"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7033e97b20277cc0d043226d1940fa7719ff08d2305d1fc7421e53066d00eb4b"
dependencies = [
"lazy_static",
]
[[package]]
name = "inotify"
version = "0.7.1"
@ -6521,6 +6530,7 @@ dependencies = [
"iced_native",
"iced_winit",
"image",
"inline_tweak",
"itertools",
"keyboard-keynames",
"lazy_static",

BIN
assets/voxygen/element/ui/diary/abilitiy_desc_frame.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/abilitiy_desc_frame_dual.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_l.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_l_click.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_l_inactive.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_r.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_r_click.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/arrow_r_inactive.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/skilltree.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/spellbook.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/buttons/stats.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/diary_skills_bg.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/diary/spellbook_bg.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -27,7 +27,7 @@ use common::{
group,
invite::{InviteKind, InviteResponse},
skills::Skill,
slot::{InvSlotId, Slot},
slot::{EquipSlot, InvSlotId, Slot},
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
UtteranceKind,
@ -1404,8 +1404,26 @@ impl Client {
}
pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
let auxiliary_key = self
.inventories()
.get(self.entity())
.map_or((None, None), |inv| {
let tool_kind = |slot| {
inv.equipped(slot).and_then(|item| match item.kind() {
comp::item::ItemKind::Tool(tool) => Some(tool.kind),
_ => None,
})
};
(
tool_kind(EquipSlot::ActiveMainhand),
tool_kind(EquipSlot::ActiveOffhand),
)
});
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
slot,
auxiliary_key,
new_ability,
}))
}

View File

@ -31,7 +31,7 @@ macro_rules! dev_panic {
if cfg!(any(debug_assertions, test)) {
panic!("{}", $msg);
} else {
tracing::warn!("{}", $msg);
tracing::error!("{}", $msg);
}
};

View File

@ -761,46 +761,29 @@ impl Damage {
inventory: Option<&Inventory>,
stats: Option<&Stats>,
) -> f32 {
let inventory_dr = if let Some(inventory) = inventory {
let protection = inventory
.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.protection())
} else {
None
}
})
.map(|protection| match protection {
Some(Protection::Normal(protection)) => Some(protection),
Some(Protection::Invincible) => None,
None => Some(0.0),
})
.sum::<Option<f32>>();
let protection = compute_protection(inventory);
let penetration = if let Some(damage) = damage {
if let DamageKind::Piercing = damage.kind {
(damage.value * PIERCING_PENETRATION_FRACTION)
.min(protection.unwrap_or(0.0))
.max(0.0)
} else {
0.0
}
let penetration = if let Some(damage) = damage {
if let DamageKind::Piercing = damage.kind {
(damage.value * PIERCING_PENETRATION_FRACTION)
.min(protection.unwrap_or(0.0))
.max(0.0)
} else {
0.0
};
let protection = protection.map(|p| p - penetration);
const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
match protection {
Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
None => 1.0,
}
} else {
0.0
};
let protection = protection.map(|p| p - penetration);
const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
let inventory_dr = match protection {
Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
None => 1.0,
};
let stats_dr = if let Some(stats) = stats {
stats.damage_reduction
} else {
@ -1172,3 +1155,25 @@ pub fn compute_stealth_coefficient(inventory: Option<&Inventory>) -> f32 {
.fold(2.0, |a, b| a + b.max(0.0))
})
}
/// Computes the total protection provided from armor. Is used to determine the
/// damage reduction applied to damage received by an entity None indicates that
/// the armor equipped makes the entity invulnerable
#[cfg(not(target_arch = "wasm32"))]
pub fn compute_protection(inventory: Option<&Inventory>) -> Option<f32> {
inventory.map_or(Some(0.0), |inv| {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
armor.protection()
} else {
None
}
})
.map(|protection| match protection {
Protection::Normal(protection) => Some(protection),
Protection::Invincible => None,
})
.sum::<Option<f32>>()
})
}

View File

@ -25,12 +25,14 @@ use crate::{
},
terrain::SpriteKind,
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::{convert::TryFrom, time::Duration};
pub const MAX_ABILITIES: usize = 5;
pub type AuxiliaryKey = (Option<ToolKind>, Option<ToolKind>);
// TODO: Potentially look into storing previous ability sets for weapon
// combinations and automatically reverting back to them on switching to that
@ -41,7 +43,7 @@ pub struct ActiveAbilities {
pub primary: PrimaryAbility,
pub secondary: SecondaryAbility,
pub movement: MovementAbility,
pub abilities: [AuxiliaryAbility; MAX_ABILITIES],
pub auxiliary_sets: HashMap<AuxiliaryKey, [AuxiliaryAbility; MAX_ABILITIES]>,
}
impl Component for ActiveAbilities {
@ -54,31 +56,72 @@ impl Default for ActiveAbilities {
primary: PrimaryAbility::Tool,
secondary: SecondaryAbility::Tool,
movement: MovementAbility::Species,
abilities: [AuxiliaryAbility::Empty; MAX_ABILITIES],
auxiliary_sets: HashMap::new(),
}
}
}
impl ActiveAbilities {
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 new(auxiliary_sets: HashMap<AuxiliaryKey, [AuxiliaryAbility; MAX_ABILITIES]>) -> Self {
ActiveAbilities {
auxiliary_sets,
..Self::default()
}
}
pub fn change_ability(&mut self, slot: usize, new_ability: AuxiliaryAbility) {
if let Some(ability) = self.abilities.get_mut(slot) {
pub fn change_ability(
&mut self,
slot: usize,
auxiliary_key: AuxiliaryKey,
new_ability: AuxiliaryAbility,
inventory: Option<&Inventory>,
skill_set: Option<&SkillSet>,
) {
let auxiliary_set = self
.auxiliary_sets
.entry(auxiliary_key)
.or_insert(Self::default_ability_set(inventory, skill_set));
if let Some(ability) = auxiliary_set.get_mut(slot) {
*ability = new_ability;
}
}
pub fn get_ability(&self, input: AbilityInput) -> Ability {
pub fn auxiliary_set(
&self,
inv: Option<&Inventory>,
skill_set: Option<&SkillSet>,
) -> [AuxiliaryAbility; MAX_ABILITIES] {
let tool_kind = |slot| {
inv.and_then(|inv| inv.equipped(slot))
.and_then(|item| match item.kind() {
ItemKind::Tool(tool) => Some(tool.kind),
_ => None,
})
};
let aux_key = (
tool_kind(EquipSlot::ActiveMainhand),
tool_kind(EquipSlot::ActiveOffhand),
);
self.auxiliary_sets
.get(&aux_key)
.copied()
.unwrap_or_else(|| Self::default_ability_set(inv, skill_set))
}
pub fn get_ability(
&self,
input: AbilityInput,
inventory: Option<&Inventory>,
skill_set: Option<&SkillSet>,
) -> Ability {
match input {
AbilityInput::Primary => self.primary.into(),
AbilityInput::Secondary => self.secondary.into(),
AbilityInput::Movement => self.movement.into(),
AbilityInput::Auxiliary(index) => self
.abilities
.auxiliary_set(inventory, skill_set)
.get(index)
.copied()
.map(|a| a.into())
@ -96,7 +139,7 @@ impl ActiveAbilities {
body: Option<&Body>,
// bool is from_offhand
) -> Option<(CharacterAbility, bool)> {
let ability = self.get_ability(input);
let ability = self.get_ability(input, inv, Some(skill_set));
let ability_set = |equip_slot| {
inv.and_then(|inv| inv.equipped(equip_slot))
@ -150,35 +193,35 @@ impl ActiveAbilities {
}
}
// 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<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
equip_slot: EquipSlot,
) -> impl Iterator<Item = usize> + 'a {
inv.and_then(|inv| inv.equipped(equip_slot))
.into_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(i)
})
}
let main_abilities = iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveMainhand)
.map(AuxiliaryAbility::MainWeapon);
let off_abilities = iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveOffhand)
.map(AuxiliaryAbility::OffWeapon);
(0..MAX_ABILITIES)
.zip(main_abilities.chain(off_abilities))
.for_each(|(i, ability)| {
self.change_ability(i, ability);
pub fn iter_unlocked_abilities<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
equip_slot: EquipSlot,
) -> impl Iterator<Item = usize> + 'a {
inv.and_then(|inv| inv.equipped(equip_slot))
.into_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(i)
})
}
fn default_ability_set<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
) -> [AuxiliaryAbility; MAX_ABILITIES] {
let mut iter = Self::iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveMainhand)
.map(AuxiliaryAbility::MainWeapon)
.chain(
Self::iter_unlocked_abilities(inv, skill_set, EquipSlot::ActiveOffhand)
.map(AuxiliaryAbility::OffWeapon),
);
[(); MAX_ABILITIES].map(|()| iter.next().unwrap_or(AuxiliaryAbility::Empty))
}
}
pub enum AbilityInput {

View File

@ -138,6 +138,7 @@ pub enum ControlEvent {
Utterance(UtteranceKind),
ChangeAbility {
slot: usize,
auxiliary_key: ability::AuxiliaryKey,
new_ability: ability::AuxiliaryAbility,
},
}

View File

@ -48,7 +48,10 @@ pub mod visual;
// Reexports
#[cfg(not(target_arch = "wasm32"))]
pub use self::{
ability::{Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType},
ability::{
Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType,
MAX_ABILITIES,
},
admin::{Admin, AdminRole},
agent::{Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, PidController},
anchor::Anchor,

View File

@ -231,15 +231,14 @@ impl Poise {
.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.poise_resilience())
armor.poise_resilience()
} else {
None
}
})
.map(|protection| match protection {
Some(Protection::Normal(protection)) => Some(protection),
Some(Protection::Invincible) => None,
None => Some(0.0),
Protection::Normal(protection) => Some(protection),
Protection::Invincible => None,
})
.sum::<Option<f32>>();
match protection {

View File

@ -115,6 +115,7 @@ pub enum ServerEvent {
comp::Inventory,
Option<comp::Waypoint>,
Vec<(comp::Pet, comp::Body, comp::Stats)>,
comp::ActiveAbilities,
),
},
ExitIngame {
@ -203,6 +204,12 @@ pub enum ServerEvent {
EntityAttackedHook {
entity: EcsEntity,
},
ChangeAbility {
entity: EcsEntity,
slot: usize,
auxiliary_key: comp::ability::AuxiliaryKey,
new_ability: comp::ability::AuxiliaryAbility,
},
}
pub struct EventBus<E> {

View File

@ -1,7 +1,7 @@
use common::{
comp::{
agent::{Sound, SoundKind},
ActiveAbilities, Body, BuffChange, ControlEvent, Controller, Pos,
Body, BuffChange, ControlEvent, Controller, Pos,
},
event::{EventBus, ServerEvent},
uid::UidAllocator,
@ -27,20 +27,13 @@ pub struct ReadData<'a> {
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
ReadData<'a>,
WriteStorage<'a, Controller>,
WriteStorage<'a, ActiveAbilities>,
);
type SystemData = (ReadData<'a>, WriteStorage<'a, Controller>);
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, mut active_abilities): Self::SystemData,
) {
fn run(_job: &mut Job<Self>, (read_data, mut controllers): Self::SystemData) {
let mut server_emitter = read_data.server_bus.emitter();
for (entity, controller) in (&read_data.entities, &mut controllers).join() {
@ -110,10 +103,17 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Sound { sound });
}
},
ControlEvent::ChangeAbility { slot, new_ability } => {
if let Some(mut active_abilities) = active_abilities.get_mut(entity) {
active_abilities.change_ability(slot, new_ability);
}
ControlEvent::ChangeAbility {
slot,
auxiliary_key,
new_ability,
} => {
server_emitter.emit(ServerEvent::ChangeAbility {
entity,
slot,
auxiliary_key,
new_ability,
});
},
}
}

View File

@ -1,4 +1,4 @@
use crate::persistence::character_updater::CharacterUpdater;
use crate::persistence::{character_updater::CharacterUpdater, PersistedComponents};
use common::{
character::CharacterId,
comp::{inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats},
@ -65,12 +65,15 @@ pub fn create_character(
let waypoint = None;
character_updater.create_character(
entity,
player_uuid,
character_alias,
(body, stats, skill_set, inventory, waypoint, Vec::new()),
);
character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets: Vec::new(),
active_abilities: Default::default(),
});
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::{client::Client, sys, Server, StateExt};
use crate::{client::Client, persistence::PersistedComponents, sys, Server, StateExt};
use common::{
character::CharacterId,
comp::{
@ -35,14 +35,7 @@ pub fn handle_initialize_character(
pub fn handle_loaded_character_data(
server: &mut Server,
entity: EcsEntity,
loaded_components: (
comp::Body,
comp::Stats,
comp::SkillSet,
comp::Inventory,
Option<comp::Waypoint>,
Vec<(comp::Pet, comp::Body, comp::Stats)>,
),
loaded_components: PersistedComponents,
) {
server
.state

View File

@ -1,6 +1,7 @@
use crate::{
client::Client,
comp::{
ability,
agent::{Agent, AgentEvent, Sound, SoundKind},
skillset::SkillGroupKind,
BuffKind, BuffSource, PhysicsState,
@ -1260,3 +1261,26 @@ pub fn handle_entity_attacked_hook(server: &Server, entity: EcsEntity) {
buff_change: buff::BuffChange::RemoveByKind(buff::BuffKind::Saturation),
});
}
pub fn handle_change_ability(
server: &Server,
entity: EcsEntity,
slot: usize,
auxiliary_key: ability::AuxiliaryKey,
new_ability: ability::AuxiliaryAbility,
) {
let ecs = &server.state.ecs();
let inventories = ecs.read_storage::<comp::Inventory>();
let skill_sets = ecs.read_storage::<comp::SkillSet>();
if let Some(mut active_abilities) = ecs.write_storage::<comp::ActiveAbilities>().get_mut(entity)
{
active_abilities.change_ability(
slot,
auxiliary_key,
new_ability,
inventories.get(entity),
skill_sets.get(entity),
);
}
}

View File

@ -767,19 +767,6 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
// manipulating the inventory mutated the trade, so reset the accept flags
trades.implicit_mutation_occurred(&uid);
}
// After any inventory manipulation, update the ability
// TODO: Make less hacky, probably remove entirely but needs UI
if let Some(mut active_abilities) = state
.ecs()
.write_storage::<comp::ActiveAbilities>()
.get_mut(entity)
{
active_abilities.auto_update(
state.ecs().read_storage::<comp::Inventory>().get(entity),
state.ecs().read_storage::<comp::SkillSet>().get(entity),
);
}
}
fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(

View File

@ -1,4 +1,7 @@
use crate::{events::interaction::handle_tame_pet, state_ext::StateExt, Server};
use crate::{
events::interaction::handle_tame_pet, persistence::PersistedComponents, state_ext::StateExt,
Server,
};
use common::event::{EventBus, ServerEvent};
use common_base::span;
use entity_creation::{
@ -6,10 +9,10 @@ use entity_creation::{
handle_initialize_character, handle_loaded_character_data, handle_shockwave, handle_shoot,
};
use entity_manipulation::{
handle_aura, handle_bonk, handle_buff, handle_combo_change, handle_delete, handle_destroy,
handle_energy_change, handle_entity_attacked_hook, handle_explosion, handle_health_change,
handle_knockback, handle_land_on_ground, handle_parry, handle_poise, handle_respawn,
handle_teleport_to,
handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_combo_change,
handle_delete, handle_destroy, handle_energy_change, handle_entity_attacked_hook,
handle_explosion, handle_health_change, handle_knockback, handle_land_on_ground, handle_parry,
handle_poise, handle_respawn, handle_teleport_to,
};
use group_manip::handle_group;
use information::handle_site_info;
@ -129,6 +132,17 @@ impl Server {
character_id,
} => handle_initialize_character(self, entity, character_id),
ServerEvent::UpdateCharacterData { entity, components } => {
let (body, stats, skill_set, inventory, waypoint, pets, active_abilities) =
components;
let components = PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
};
handle_loaded_character_data(self, entity, components);
},
ServerEvent::ExitIngame { entity } => {
@ -233,6 +247,12 @@ impl Server {
ServerEvent::EntityAttackedHook { entity } => {
handle_entity_attacked_hook(self, entity)
},
ServerEvent::ChangeAbility {
entity,
slot,
auxiliary_key,
new_ability,
} => handle_change_ability(self, entity, slot, auxiliary_key, new_ability),
}
}

View File

@ -202,6 +202,7 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
Some(presence),
Some(skill_set),
Some(inventory),
Some(active_abilities),
Some(player_uid),
Some(player_info),
mut character_updater,
@ -210,6 +211,9 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
state.read_storage::<Presence>().get(entity),
state.read_storage::<comp::SkillSet>().get(entity),
state.read_storage::<comp::Inventory>().get(entity),
state
.read_storage::<comp::ability::ActiveAbilities>()
.get(entity),
state.read_storage::<Uid>().get(entity),
state.read_storage::<comp::Player>().get(entity),
state.ecs().fetch_mut::<CharacterUpdater>(),
@ -251,7 +255,13 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
character_updater.add_pending_logout_update(
char_id,
(skill_set.clone(), inventory.clone(), pets, waypoint),
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint,
active_abilities.clone(),
),
);
},
PresenceKind::Spectator => { /* Do nothing, spectators do not need persisting */ },

View File

@ -55,6 +55,7 @@ use crate::{
connection_handler::ConnectionHandler,
data_dir::DataDir,
login_provider::LoginProvider,
persistence::PersistedComponents,
presence::{Presence, RegionSubscription, RepositionOnChunkLoad},
rtsim::RtSim,
state_ext::StateExt,
@ -844,9 +845,29 @@ impl Server {
},
CharacterLoaderResponseKind::CharacterData(result) => {
let message = match *result {
Ok(character_data) => ServerEvent::UpdateCharacterData {
entity: query_result.entity,
components: character_data,
Ok(character_data) => {
let PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
} = character_data;
let character_data = (
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
);
ServerEvent::UpdateCharacterData {
entity: query_result.entity,
components: character_data,
}
},
Err(error) => {
// We failed to load data for the character from the DB. Notify the

View File

@ -0,0 +1,12 @@
-- Creates new ability_set table
CREATE TABLE "ability_set" (
"entity_id" INT NOT NULL,
"ability_sets" TEXT NOT NULL,
PRIMARY KEY("entity_id"),
FOREIGN KEY("entity_id") REFERENCES "character"("character_id")
);
-- Inserts starting ability sets for everyone
INSERT INTO ability_set
SELECT c.character_id, '[]'
FROM character c

View File

@ -8,10 +8,10 @@ extern crate rusqlite;
use super::{error::PersistenceError, models::*};
use crate::{
comp,
comp::Inventory,
comp::{self, Inventory},
persistence::{
character::conversions::{
convert_active_abilities_from_database, convert_active_abilities_to_database,
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,
@ -234,19 +234,34 @@ pub fn load_character_data(
})
.collect::<Vec<(comp::Pet, comp::Body, comp::Stats)>>();
Ok((
convert_body_from_database(&body_data.variant, &body_data.body_data)?,
convert_stats_from_database(character_data.alias),
convert_skill_set_from_database(&skill_group_data),
convert_inventory_from_database_items(
let mut stmt = connection.prepare_cached(
"
SELECT ability_sets
FROM ability_set
WHERE entity_id = ?1",
)?;
let ability_set_data = stmt.query_row(&[char_id], |row| {
Ok(AbilitySets {
entity_id: char_id,
ability_sets: row.get(0)?,
})
})?;
Ok(PersistedComponents {
body: convert_body_from_database(&body_data.variant, &body_data.body_data)?,
stats: convert_stats_from_database(character_data.alias),
skill_set: convert_skill_set_from_database(&skill_group_data),
inventory: convert_inventory_from_database_items(
character_containers.inventory_container_id,
&inventory_items,
character_containers.loadout_container_id,
&loadout_items,
)?,
char_waypoint,
waypoint: char_waypoint,
pets,
))
active_abilities: convert_active_abilities_from_database(&ability_set_data),
})
}
/// Loads a list of characters belonging to the player. This data is a small
@ -327,14 +342,22 @@ pub fn create_character(
uuid: &str,
character_alias: &str,
persisted_components: PersistedComponents,
transactionn: &mut Transaction,
transaction: &mut Transaction,
) -> CharacterCreationResult {
check_character_limit(uuid, transactionn)?;
check_character_limit(uuid, transaction)?;
let (body, _stats, skill_set, inventory, waypoint, _) = persisted_components;
let PersistedComponents {
body,
stats: _,
skill_set,
inventory,
waypoint,
pets: _,
active_abilities,
} = persisted_components;
// Fetch new entity IDs for character, inventory and loadout
let mut new_entity_ids = get_new_entity_ids(transactionn, |next_id| next_id + 3)?;
let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 3)?;
// Create pseudo-container items for character
let character_id = new_entity_ids.next().unwrap();
@ -365,7 +388,7 @@ pub fn create_character(
},
];
let mut stmt = transactionn.prepare_cached(
let mut stmt = transaction.prepare_cached(
"
INSERT INTO item (item_id,
parent_container_item_id,
@ -386,7 +409,7 @@ pub fn create_character(
}
drop(stmt);
let mut stmt = transactionn.prepare_cached(
let mut stmt = transaction.prepare_cached(
"
INSERT INTO body (body_id,
variant,
@ -402,7 +425,7 @@ pub fn create_character(
])?;
drop(stmt);
let mut stmt = transactionn.prepare_cached(
let mut stmt = transaction.prepare_cached(
"
INSERT INTO character (character_id,
player_uuid,
@ -421,7 +444,7 @@ pub fn create_character(
let db_skill_groups = convert_skill_groups_to_database(character_id, skill_set.skill_groups());
let mut stmt = transactionn.prepare_cached(
let mut stmt = transaction.prepare_cached(
"
INSERT INTO skill_group (entity_id,
skill_group_kind,
@ -444,10 +467,25 @@ pub fn create_character(
}
drop(stmt);
let ability_sets = convert_active_abilities_to_database(character_id, &active_abilities);
let mut stmt = transaction.prepare_cached(
"
INSERT INTO ability_set (entity_id,
ability_sets)
VALUES (?1, ?2)",
)?;
stmt.execute(&[
&character_id as &dyn ToSql,
&ability_sets.ability_sets as &dyn ToSql,
])?;
drop(stmt);
// Insert default inventory and loadout item records
let mut inserts = Vec::new();
get_new_entity_ids(transactionn, |mut next_id| {
get_new_entity_ids(transaction, |mut next_id| {
let inserts_ = convert_items_to_database_items(
loadout_container_id,
&inventory,
@ -458,7 +496,7 @@ pub fn create_character(
next_id
})?;
let mut stmt = transactionn.prepare_cached(
let mut stmt = transaction.prepare_cached(
"
INSERT INTO item (item_id,
parent_container_item_id,
@ -479,7 +517,7 @@ pub fn create_character(
}
drop(stmt);
load_character_list(uuid, transactionn).map(|list| (character_id, list))
load_character_list(uuid, transaction).map(|list| (character_id, list))
}
pub fn edit_character(
@ -579,6 +617,17 @@ pub fn delete_character(
delete_pets(transaction, char_id, Rc::new(pet_ids))?;
}
// Delete ability sets
let mut stmt = transaction.prepare_cached(
"
DELETE
FROM ability_set
WHERE entity_id = ?1",
)?;
stmt.execute(&[&char_id])?;
drop(stmt);
// Delete character
let mut stmt = transaction.prepare_cached(
"
@ -892,6 +941,7 @@ pub fn update(
inventory: comp::Inventory,
pets: Vec<PetPersistenceData>,
char_waypoint: Option<comp::Waypoint>,
active_abilities: comp::ability::ActiveAbilities,
transaction: &mut Transaction,
) -> Result<(), PersistenceError> {
// Run pet persistence
@ -1035,5 +1085,27 @@ pub fn update(
)));
}
let ability_sets = convert_active_abilities_to_database(char_id, &active_abilities);
let mut stmt = transaction.prepare_cached(
"
UPDATE ability_set
SET ability_sets = ?1
WHERE entity_id = ?2
",
)?;
let ability_sets_count = stmt.execute(&[
&ability_sets.ability_sets as &dyn ToSql,
&char_id as &dyn ToSql,
])?;
if ability_sets_count != 1 {
return Err(PersistenceError::OtherError(format!(
"Error updating ability_set table for char_id {}",
char_id
)));
}
Ok(())
}

View File

@ -1,11 +1,11 @@
use crate::persistence::{
character::EntityId,
models::{Character, Item, SkillGroup},
models::{AbilitySets, Character, Item, SkillGroup},
};
use crate::persistence::{
error::PersistenceError,
json_models::{self, CharacterPosition, GenericBody, HumanoidBody},
json_models::{self, CharacterPosition, DatabaseAbilitySet, GenericBody, HumanoidBody},
};
use common::{
character::CharacterId,
@ -602,3 +602,28 @@ pub fn convert_skill_groups_to_database<'a, I: Iterator<Item = &'a skillset::Ski
})
.collect()
}
pub fn convert_active_abilities_to_database(
entity_id: CharacterId,
active_abilities: &ability::ActiveAbilities,
) -> AbilitySets {
let ability_sets = json_models::active_abilities_to_db_model(active_abilities);
AbilitySets {
entity_id,
ability_sets: serde_json::to_string(&ability_sets).unwrap_or_default(),
}
}
pub fn convert_active_abilities_from_database(
ability_sets: &AbilitySets,
) -> ability::ActiveAbilities {
let ability_sets = serde_json::from_str::<Vec<DatabaseAbilitySet>>(&ability_sets.ability_sets)
.unwrap_or_else(|err| {
common_base::dev_panic!(format!(
"Failed to parse ability sets. Error: {:#?}\nAbility sets:\n{:#?}",
err, ability_sets.ability_sets
));
Vec::new()
});
json_models::active_abilities_from_db_model(ability_sets)
}

View File

@ -24,6 +24,7 @@ pub type CharacterUpdateData = (
comp::Inventory,
Vec<PetPersistenceData>,
Option<comp::Waypoint>,
comp::ability::ActiveAbilities,
);
pub type PetPersistenceData = (comp::Pet, comp::Body, comp::Stats);
@ -330,21 +331,25 @@ impl CharacterUpdater {
&'a comp::Inventory,
Vec<PetPersistenceData>,
Option<&'a comp::Waypoint>,
&'a comp::ability::ActiveAbilities,
),
>,
) {
let updates = updates
.map(|(character_id, skill_set, inventory, pets, waypoint)| {
(
character_id,
.map(
|(character_id, skill_set, inventory, pets, waypoint, active_abilities)| {
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint.cloned(),
),
)
})
character_id,
(
skill_set.clone(),
inventory.clone(),
pets,
waypoint.cloned(),
active_abilities.clone(),
),
)
},
)
.chain(self.pending_logout_updates.drain())
.collect::<Vec<_>>();
@ -382,18 +387,19 @@ fn execute_batch_update(
let mut transaction = connection.connection.transaction()?;
transaction.set_drop_behavior(DropBehavior::Rollback);
trace!("Transaction started for character batch update");
updates
.into_iter()
.try_for_each(|(character_id, (stats, inventory, pets, waypoint))| {
updates.into_iter().try_for_each(
|(character_id, (stats, inventory, pets, waypoint, active_abilities))| {
super::character::update(
character_id,
stats,
inventory,
pets,
waypoint,
active_abilities,
&mut transaction,
)
})?;
},
)?;
transaction.commit()?;
trace!("Commit for character batch update completed");

View File

@ -1,4 +1,6 @@
use common::comp;
use common_base::dev_panic;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::string::ToString;
use vek::Vec3;
@ -106,3 +108,176 @@ pub fn db_string_to_skill_group(skill_group_string: &str) -> comp::skillset::Ski
),
}
}
#[derive(Serialize, Deserialize)]
pub struct DatabaseAbilitySet {
mainhand: String,
offhand: String,
abilities: Vec<String>,
}
fn aux_ability_to_string(ability: common::comp::ability::AuxiliaryAbility) -> String {
use common::comp::ability::AuxiliaryAbility;
match ability {
AuxiliaryAbility::MainWeapon(index) => format!("Main Weapon:index:{}", index),
AuxiliaryAbility::OffWeapon(index) => format!("Off Weapon:index:{}", index),
AuxiliaryAbility::Empty => String::from("Empty"),
}
}
fn aux_ability_from_string(ability: &str) -> common::comp::ability::AuxiliaryAbility {
use common::comp::ability::AuxiliaryAbility;
let mut parts = ability.split(":index:");
match parts.next() {
Some("Main Weapon") => match parts
.next()
.map(|index| index.parse::<usize>().map_err(|_| index))
{
Some(Ok(index)) => AuxiliaryAbility::MainWeapon(index),
Some(Err(error)) => {
dev_panic!(format!(
"Conversion from database to ability set failed. Unable to parse index for \
mainhand abilities: {}",
error
));
AuxiliaryAbility::Empty
},
None => {
dev_panic!(String::from(
"Conversion from database to ability set failed. Unable to find an index for \
mainhand abilities"
));
AuxiliaryAbility::Empty
},
},
Some("Off Weapon") => match parts
.next()
.map(|index| index.parse::<usize>().map_err(|_| index))
{
Some(Ok(index)) => AuxiliaryAbility::OffWeapon(index),
Some(Err(error)) => {
dev_panic!(format!(
"Conversion from database to ability set failed. Unable to parse index for \
offhand abilities: {}",
error
));
AuxiliaryAbility::Empty
},
None => {
dev_panic!(String::from(
"Conversion from database to ability set failed. Unable to find an index for \
offhand abilities"
));
AuxiliaryAbility::Empty
},
},
Some("Empty") => AuxiliaryAbility::Empty,
unknown => {
dev_panic!(format!(
"Conversion from database to ability set failed. Unknown auxiliary ability: {:#?}",
unknown
));
AuxiliaryAbility::Empty
},
}
}
fn tool_kind_to_string(tool: Option<common::comp::item::tool::ToolKind>) -> String {
use common::comp::item::tool::ToolKind::*;
String::from(match tool {
Some(Sword) => "Sword",
Some(Axe) => "Axe",
Some(Hammer) => "Hammer",
Some(Bow) => "Bow",
Some(Staff) => "Staff",
Some(Sceptre) => "Sceptre",
Some(Dagger) => "Dagger",
Some(Shield) => "Shield",
Some(Spear) => "Spear",
Some(Blowgun) => "Blowgun",
Some(Pick) => "Pick",
// Toolkinds that are not anticipated to have many active aiblities (if any at all)
Some(Farming) => "Farming",
Some(Debug) => "Debug",
Some(Natural) => "Natural",
Some(Empty) => "Empty",
None => "None",
})
}
fn tool_kind_from_string(tool: String) -> Option<common::comp::item::tool::ToolKind> {
use common::comp::item::tool::ToolKind::*;
match tool.as_str() {
"Sword" => Some(Sword),
"Axe" => Some(Axe),
"Hammer" => Some(Hammer),
"Bow" => Some(Bow),
"Staff" => Some(Staff),
"Sceptre" => Some(Sceptre),
"Dagger" => Some(Dagger),
"Shield" => Some(Shield),
"Spear" => Some(Spear),
"Blowgun" => Some(Blowgun),
"Pick" => Some(Pick),
"Farming" => Some(Farming),
"Debug" => Some(Debug),
"Natural" => Some(Natural),
"Empty" => Some(Empty),
"None" => None,
unknown => {
dev_panic!(format!(
"Conversion from database to ability set failed. Unknown toolkind: {:#?}",
unknown
));
None
},
}
}
pub fn active_abilities_to_db_model(
active_abilities: &comp::ability::ActiveAbilities,
) -> Vec<DatabaseAbilitySet> {
active_abilities
.auxiliary_sets
.iter()
.map(|((mainhand, offhand), abilities)| DatabaseAbilitySet {
mainhand: tool_kind_to_string(*mainhand),
offhand: tool_kind_to_string(*offhand),
abilities: abilities
.iter()
.map(|ability| aux_ability_to_string(*ability))
.collect(),
})
.collect::<Vec<_>>()
}
pub fn active_abilities_from_db_model(
ability_sets: Vec<DatabaseAbilitySet>,
) -> comp::ability::ActiveAbilities {
let ability_sets = ability_sets
.into_iter()
.map(
|DatabaseAbilitySet {
mainhand,
offhand,
abilities,
}| {
let mut auxiliary_abilities =
[comp::ability::AuxiliaryAbility::Empty; comp::ability::MAX_ABILITIES];
for (empty, ability) in auxiliary_abilities.iter_mut().zip(abilities.into_iter()) {
*empty = aux_ability_from_string(&ability);
}
(
(
tool_kind_from_string(mainhand),
tool_kind_from_string(offhand),
),
auxiliary_abilities,
)
},
)
.collect::<HashMap<_, _>>();
comp::ability::ActiveAbilities::new(ability_sets)
}

View File

@ -21,15 +21,17 @@ use std::{
};
use tracing::info;
/// A tuple of the components that are persisted to the DB for each character
pub type PersistedComponents = (
comp::Body,
comp::Stats,
comp::SkillSet,
comp::Inventory,
Option<comp::Waypoint>,
Vec<PetPersistenceData>,
);
/// A struct of the components that are persisted to the DB for each character
#[derive(Debug)]
pub struct PersistedComponents {
pub body: comp::Body,
pub stats: comp::Stats,
pub skill_set: comp::SkillSet,
pub inventory: comp::Inventory,
pub waypoint: Option<comp::Waypoint>,
pub pets: Vec<PetPersistenceData>,
pub active_abilities: comp::ActiveAbilities,
}
pub type EditableComponents = (comp::Body,);

View File

@ -35,3 +35,8 @@ pub struct Pet {
pub body_variant: String,
pub body_data: String,
}
pub struct AbilitySets {
pub entity_id: i64,
pub ability_sets: String,
}

View File

@ -238,10 +238,7 @@ impl StateExt for State {
.unwrap_or(0),
))
.with(stats)
.with(comp::ActiveAbilities::new(
Some(&inventory),
Some(&skill_set),
))
.with(comp::ActiveAbilities::default())
.with(skill_set)
.maybe_with(health)
.with(poise)
@ -500,7 +497,15 @@ impl StateExt for State {
}
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) {
let (body, stats, skill_set, inventory, waypoint, pets) = components;
let PersistedComponents {
body,
stats,
skill_set,
inventory,
waypoint,
pets,
active_abilities,
} = components;
if let Some(player_uid) = self.read_component_copied::<Uid>(entity) {
// Notify clients of a player list update
@ -530,10 +535,7 @@ 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::ActiveAbilities::new(Some(&inventory), Some(&skill_set)),
);
self.write_component_ignore_entity_dead(entity, active_abilities);
self.write_component_ignore_entity_dead(entity, skill_set);
self.write_component_ignore_entity_dead(entity, inventory);
self.write_component_ignore_entity_dead(

View File

@ -2,7 +2,7 @@ use crate::{persistence::character_updater, presence::Presence, sys::SysSchedule
use common::{
comp::{
pet::{is_tameable, Pet},
Alignment, Body, Inventory, SkillSet, Stats, Waypoint,
ActiveAbilities, Alignment, Body, Inventory, SkillSet, Stats, Waypoint,
},
uid::Uid,
};
@ -25,6 +25,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Waypoint>,
ReadStorage<'a, Pet>,
ReadStorage<'a, Stats>,
ReadStorage<'a, ActiveAbilities>,
WriteExpect<'a, character_updater::CharacterUpdater>,
Write<'a, SysScheduler<Self>>,
);
@ -45,6 +46,7 @@ impl<'a> System<'a> for Sys {
player_waypoints,
pets,
stats,
active_abilities,
mut updater,
mut scheduler,
): Self::SystemData,
@ -57,11 +59,18 @@ impl<'a> System<'a> for Sys {
&player_inventories,
&uids,
player_waypoints.maybe(),
&active_abilities,
)
.join()
.filter_map(
|(presence, skill_set, inventory, player_uid, waypoint)| match presence.kind
{
|(
presence,
skill_set,
inventory,
player_uid,
waypoint,
active_abilities,
)| match presence.kind {
PresenceKind::Character(id) => {
let pets = (&alignments, &bodies, &stats, &pets)
.join()
@ -78,7 +87,7 @@ impl<'a> System<'a> for Sys {
})
.collect();
Some((id, skill_set, inventory, pets, waypoint))
Some((id, skill_set, inventory, pets, waypoint, active_abilities))
},
PresenceKind::Spectator => None,
},

View File

@ -122,7 +122,7 @@ treeculler = "0.2"
tokio = { version = "1.14", default-features = false, features = ["rt-multi-thread"] }
num_cpus = "1.0"
# vec_map = { version = "0.8.2" }
# inline_tweak = "1.0.2"
inline_tweak = "1.0.2"
itertools = "0.10.0"
# Tracy

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
use crate::hud::item_imgs::ItemKey;
use common::comp::inventory::item::Item;
use common::comp::{self, inventory::item::Item};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq)]
@ -70,7 +70,13 @@ impl State {
{
use common::comp::ability::AuxiliaryAbility;
for ((i, ability), hotbar_slot) in active_abilities
.abilities
.auxiliary_set(
client.inventories().get(client.entity()),
client
.state()
.read_storage::<comp::SkillSet>()
.get(client.entity()),
)
.iter()
.enumerate()
.zip(self.slots.iter_mut())

View File

@ -68,8 +68,18 @@ image_ids! {
diary_exp_bg: "voxygen.element.ui.diary.diary_exp_bg",
diary_exp_frame: "voxygen.element.ui.diary.diary_exp_frame",
pixel: "voxygen.element.ui.diary.pixel",
arrow_l: "voxygen.element.ui.diary.buttons.arrow_l",
arrow_l_click: "voxygen.element.ui.diary.buttons.arrow_l_click",
arrow_l_inactive: "voxygen.element.ui.diary.buttons.arrow_l_inactive",
arrow_r: "voxygen.element.ui.diary.buttons.arrow_r",
arrow_r_click: "voxygen.element.ui.diary.buttons.arrow_r_click",
arrow_r_inactive: "voxygen.element.ui.diary.buttons.arrow_r_inactive",
ability_frame: "voxygen.element.ui.diary.abilitiy_desc_frame",
ability_frame_dual: "voxygen.element.ui.diary.abilitiy_desc_frame_dual",
// Skill Trees
book_bg: "voxygen.element.ui.diary.spellbook_bg",
skills_bg: "voxygen.element.ui.diary.diary_skills_bg",
slot_skills: "voxygen.element.ui.diary.buttons.slot_skilltree",
swords_crossed: "voxygen.element.weapons.swords_crossed",
sceptre: "voxygen.element.weapons.sceptre",
@ -81,6 +91,9 @@ image_ids! {
mining: "voxygen.element.weapons.mining",
pickaxe: "voxygen.element.skills.pickaxe",
pickaxe_ico: "voxygen.element.weapons.pickaxe",
skilltree_ico: "voxygen.element.ui.diary.buttons.skilltree",
spellbook_ico: "voxygen.element.ui.diary.buttons.spellbook",
stats_ico: "voxygen.element.ui.diary.buttons.stats",
lock: "voxygen.element.ui.diary.buttons.lock",
wpn_icon_border_skills: "voxygen.element.ui.diary.buttons.border_skills",
wpn_icon_border: "voxygen.element.ui.generic.buttons.border",

View File

@ -74,7 +74,9 @@ use client::Client;
use common::{
combat,
comp::{
self, fluid_dynamics,
self,
ability::AuxiliaryAbility,
fluid_dynamics,
inventory::{slot::InvSlotId, trade_pricing::TradePricing},
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
skillset::{skills::Skill, SkillGroupKind},
@ -535,8 +537,7 @@ pub enum Event {
RemoveBuff(BuffKind),
UnlockSkill(Skill),
RequestSiteInfo(SiteId),
// TODO: This variant currently unused. UI is needed for it to be properly used.
ChangeAbility(usize, comp::ability::AuxiliaryAbility),
ChangeAbility(usize, AuxiliaryAbility),
SettingsChange(SettingsChange),
AcknowledgePersistenceLoadError,
@ -651,7 +652,7 @@ pub struct Show {
ingame: bool,
chat_tab_settings_index: Option<usize>,
settings_tab: SettingsTab,
skilltreetab: SelectedSkillTree,
diary_fields: diary::DiaryShow,
crafting_tab: CraftingTab,
crafting_search_key: Option<String>,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
@ -746,6 +747,7 @@ impl Show {
self.salvage = false;
self.bag = false;
self.map = false;
self.diary_fields = diary::DiaryShow::default();
self.diary = open;
self.want_grab = !open;
}
@ -851,7 +853,7 @@ impl Show {
}
fn open_skill_tree(&mut self, tree_sel: SelectedSkillTree) {
self.skilltreetab = tree_sel;
self.diary_fields.skilltreetab = tree_sel;
self.social = false;
}
@ -1045,7 +1047,7 @@ impl Hud {
group_menu: false,
chat_tab_settings_index: None,
settings_tab: SettingsTab::Interface,
skilltreetab: SelectedSkillTree::General,
diary_fields: diary::DiaryShow::default(),
crafting_tab: CraftingTab::All,
crafting_search_key: None,
craft_sprite: None,
@ -3097,17 +3099,42 @@ impl Hud {
if self.show.diary {
let entity = client.entity();
let skill_sets = ecs.read_storage::<comp::SkillSet>();
if let Some(skill_set) = skill_sets.get(entity) {
if let (
Some(skill_set),
Some(active_abilities),
Some(inventory),
Some(health),
Some(energy),
Some(body),
Some(poise),
) = (
skill_sets.get(entity),
active_abilities.get(entity),
inventories.get(entity),
healths.get(entity),
energies.get(entity),
bodies.get(entity),
poises.get(entity),
) {
for event in Diary::new(
&self.show,
client,
global_state,
skill_set,
active_abilities,
inventory,
health,
energy,
poise,
body,
&msm,
&self.imgs,
&self.item_imgs,
&self.fonts,
i18n,
&self.rot_imgs,
tooltip_manager,
&mut self.slot_manager,
self.pulse,
)
.set(self.ids.diary, ui_widgets)
@ -3122,6 +3149,9 @@ impl Hud {
self.show.open_skill_tree(tree_sel)
},
diary::Event::UnlockSkill(skill) => events.push(Event::UnlockSkill(skill)),
diary::Event::ChangeSection(section) => {
self.show.diary_fields.section = section;
},
}
}
}
@ -3278,7 +3308,7 @@ impl Hud {
// Maintain slot manager
'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
use comp::slot::Slot;
use slots::{InventorySlot, SlotKind::*};
use slots::{AbilitySlot, InventorySlot, SlotKind::*};
let to_slot = |slot_kind| match slot_kind {
Inventory(InventorySlot {
slot, ours: true, ..
@ -3287,6 +3317,7 @@ impl Hud {
Equip(e) => Some(Slot::Equip(e)),
Hotbar(_) => None,
Trade(_) => None,
Ability(_) => None,
};
match event {
slot::Event::Dragged(a, b) => {
@ -3336,6 +3367,33 @@ impl Hud {
}
}
}
} else if let (Ability(a), Ability(b)) = (a, b) {
match (a, b) {
(AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
events.push(Event::ChangeAbility(index, ability));
},
(AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
let me = client.entity();
if let Some(active_abilities) = active_abilities.get(me) {
let ability_a = active_abilities
.auxiliary_set(inventories.get(me), skill_sets.get(me))
.get(a)
.copied()
.unwrap_or(AuxiliaryAbility::Empty);
let ability_b = active_abilities
.auxiliary_set(inventories.get(me), skill_sets.get(me))
.get(b)
.copied()
.unwrap_or(AuxiliaryAbility::Empty);
events.push(Event::ChangeAbility(a, ability_b));
events.push(Event::ChangeAbility(b, ability_a));
}
},
(AbilitySlot::Slot(index), _) => {
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
},
(AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
}
}
},
slot::Event::Dropped(from) => {
@ -3355,6 +3413,8 @@ impl Hud {
}));
}
}
} else if let Ability(AbilitySlot::Slot(index)) = from {
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
}
},
slot::Event::SplitDropped(from) => {
@ -3364,6 +3424,8 @@ impl Hud {
} else if let Hotbar(h) = from {
self.hotbar.clear_slot(h);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
} else if let Ability(AbilitySlot::Slot(index)) = from {
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
}
},
slot::Event::SplitDragged(a, b) => {
@ -3407,6 +3469,33 @@ impl Hud {
}
}
}
} else if let (Ability(a), Ability(b)) = (a, b) {
match (a, b) {
(AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
events.push(Event::ChangeAbility(index, ability));
},
(AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
let me = client.entity();
if let Some(active_abilities) = active_abilities.get(me) {
let ability_a = active_abilities
.auxiliary_set(inventories.get(me), skill_sets.get(me))
.get(a)
.copied()
.unwrap_or(AuxiliaryAbility::Empty);
let ability_b = active_abilities
.auxiliary_set(inventories.get(me), skill_sets.get(me))
.get(b)
.copied()
.unwrap_or(AuxiliaryAbility::Empty);
events.push(Event::ChangeAbility(a, ability_b));
events.push(Event::ChangeAbility(b, ability_a));
}
},
(AbilitySlot::Slot(index), _) => {
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
},
(AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
}
}
},
slot::Event::Used(from) => {
@ -3442,6 +3531,8 @@ impl Hud {
},
hotbar::SlotContents::Ability(_) => {},
});
} else if let Ability(AbilitySlot::Slot(index)) = from {
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
}
},
slot::Event::Request {

View File

@ -601,13 +601,13 @@ impl<'a> Skillbar<'a> {
// Helper
let tooltip_text = |slot| {
let (hotbar, inventory, _, _, active_abilities, _) = content_source;
let (hotbar, inventory, _, skill_set, active_abilities, _) = content_source;
hotbar.get(slot).and_then(|content| match content {
hotbar::SlotContents::Inventory(i, _) => inventory
.get_by_hash(i)
.map(|item| (item.name(), item.description())),
hotbar::SlotContents::Ability(i) => active_abilities
.abilities
.auxiliary_set(Some(inventory), Some(skill_set))
.get(i)
.and_then(|a| Ability::from(*a).ability_id(Some(inventory)))
.map(util::ability_description),

View File

@ -6,8 +6,9 @@ use super::{
};
use crate::ui::slot::{self, SlotKey, SumSlot};
use common::comp::{
ability::AbilityInput, slot::InvSlotId, Ability, ActiveAbilities, Body, Energy, Inventory,
SkillSet,
ability::{Ability, AbilityInput, AuxiliaryAbility},
slot::InvSlotId,
ActiveAbilities, Body, Energy, Inventory, SkillSet,
};
use conrod_core::{image, Color};
use specs::Entity as EcsEntity;
@ -20,6 +21,7 @@ pub enum SlotKind {
Equip(EquipSlot),
Hotbar(HotbarSlot),
Trade(TradeSlot),
Ability(AbilitySlot),
/* Spellbook(SpellbookSlot), TODO */
}
@ -141,7 +143,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
},
hotbar::SlotContents::Ability(i) => {
let ability_id = active_abilities
.abilities
.auxiliary_set(Some(inventory), Some(skillset))
.get(i)
.and_then(|a| Ability::from(*a).ability_id(Some(inventory)));
@ -192,6 +194,42 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AbilitySlot {
Slot(usize),
Ability(AuxiliaryAbility),
}
type AbilitiesSource<'a> = (&'a ActiveAbilities, &'a Inventory, &'a SkillSet);
impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
type ImageKey = String;
fn image_key(
&self,
(active_abilities, inventory, skillset): &AbilitiesSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> {
let ability_id = match self {
Self::Slot(index) => active_abilities
.get_ability(
AbilityInput::Auxiliary(*index),
Some(inventory),
Some(skillset),
)
.ability_id(Some(inventory)),
Self::Ability(ability) => Ability::from(*ability).ability_id(Some(inventory)),
};
ability_id.map(|id| (String::from(id), None))
}
fn amount(&self, _source: &AbilitiesSource) -> Option<u32> { None }
fn image_ids(ability_id: &Self::ImageKey, imgs: &img_ids::Imgs) -> Vec<image::Id> {
vec![util::ability_image(imgs, ability_id)]
}
}
impl From<InventorySlot> for SlotKind {
fn from(inventory: InventorySlot) -> Self { Self::Inventory(inventory) }
}
@ -207,4 +245,15 @@ impl From<TradeSlot> for SlotKind {
fn from(trade: TradeSlot) -> Self { Self::Trade(trade) }
}
impl SumSlot for SlotKind {}
impl From<AbilitySlot> for SlotKind {
fn from(ability: AbilitySlot) -> Self { Self::Ability(ability) }
}
impl SumSlot for SlotKind {
fn drag_size(&self) -> Option<[f64; 2]> {
Some(match self {
Self::Ability(_) => [80.0; 2],
_ => return None,
})
}
}

View File

@ -365,52 +365,41 @@ pub fn ability_description(ability_id: &str) -> (&str, &str) {
// Debug stick
"common.abilities.debug.possess" => (
"Possessing Arrow",
"\n\
Shoots a poisonous arrow.\n\
Lets you control your target.",
"Shoots a poisonous arrow. Lets you control your target.",
),
// Sword
"common.abilities.sword.spin" => (
"Whirlwind",
"\n\
Move forward while spinning with your sword.",
"Move forward while spinning with your sword.",
),
// Axe
"common.abilities.axe.leap" => (
"Axe Jump",
"\n\
A jump with the slashing leap to position of cursor.",
"A jump with the slashing leap to position of cursor.",
),
// Hammer
"common.abilities.hammer.leap" => (
"Smash of Doom",
"\n\
An AOE attack with knockback.\n\
Leaps to position of cursor.",
"An AOE attack with knockback. Leaps to position of cursor.",
),
// Bow
"common.abilities.bow.shotgun" => (
"Burst",
"\n\
Launches a burst of arrows",
"Launches a burst of arrows",
),
// Staff
"common.abilities.staff.fireshockwave" => (
"Ring of Fire",
"\n\
Ignites the ground with fiery shockwave.",
"Ignites the ground with fiery shockwave.",
),
// Sceptre
"common.abilities.sceptre.wardingaura" => (
"Thorn Bulwark",
"\n\
Protects you and your group with thorns\n\
for a short amount of time.",
"Protects you and your group with thorns for a short amount of time.",
),
_ => (
"Ability has no title",
"\n\
Ability has no description."
"Ability has no description."
),
}
}

View File

@ -19,7 +19,9 @@ pub trait SlotKey<C, I>: Copy {
fn image_ids(key: &Self::ImageKey, source: &I) -> Vec<image::Id>;
}
pub trait SumSlot: Sized + PartialEq + Copy + Send + 'static {}
pub trait SumSlot: Sized + PartialEq + Copy + Send + 'static {
fn drag_size(&self) -> Option<[f64; 2]>;
}
pub struct ContentSize {
// Width divided by height
@ -217,6 +219,12 @@ where
let content_img = *content_img;
let drag_amount = *drag_amount;
let dragged_size = if let Some(dragged_size) = slot.drag_size() {
dragged_size
} else {
self.drag_img_size.map(|e| e as f64).into_array()
};
// If we are dragging and we right click, drop half the stack
// on the ground or into the slot under the cursor. This only
// works with open slots or slots containing the same kind of
@ -260,9 +268,8 @@ where
// Draw image of contents being dragged
let [mouse_x, mouse_y] = input.mouse.xy;
let size = self.drag_img_size.map(|e| e as f64).into_array();
super::ghost_image::GhostImage::new(content_img)
.wh(size)
.wh(dragged_size)
.xy([mouse_x, mouse_y])
.set(self.drag_id, ui);