Merge branch 'BottledByte/ability_design' into 'master'

Add ability IDs and use them in GUI

See merge request veloren/veloren!1264
This commit is contained in:
Imbris 2020-08-06 04:44:51 +00:00
commit a0b1259b7e
9 changed files with 234 additions and 145 deletions

View File

@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Loading-Screen tips
- Feeding animation for some animals
- Power stat to weapons which affects weapon damage
- Add IDs to abilities.
### Changed

View File

@ -43,6 +43,63 @@ impl From<&CharacterState> for CharacterAbilityType {
}
}
/// This enum is used for matching ability to image in GUI.
/// This is beacuse `CharacterAbility` can have more "meanings"
/// (like axe swing and sword swing, both are BasicMelee)
/// and different GUI can be done for each.
///
/// TODO: Dehardcode this in case of modding.
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum AbilityId {
// Sword
SwordCut,
SwordThrust,
// Axe
AxeSwing,
AxeSpin,
// Hammer
HammerSmash,
HammerLeap,
// Bow
BowShot,
BowCharged,
// Staff
StaffSwing,
StaffShot,
StaffFireball,
StaffHeal,
// Dagger
DaggerStab,
DaggerDash,
// Shield
ShieldBash,
ShieldBlock,
// Farming
FarmingAttack,
// Debug
DebugFlyDirection,
DebugFlyUp,
DebugPossesArrow,
// Special
// TODO: Review usage
Roll,
Block,
Empty,
}
/// Unique ability config.
/// It composes from an ID (currently used to inform Client's GUI)
/// and data, which is the actual ability behaviour/config.
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Ability {
pub id: AbilityId,
pub data: CharacterAbility,
}
impl Ability {
pub fn new(id: AbilityId, data: CharacterAbility) -> Ability { Ability { id, data } }
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum CharacterAbility {
BasicMelee {
@ -158,11 +215,11 @@ impl CharacterAbility {
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ItemConfig {
pub item: Item,
pub ability1: Option<CharacterAbility>,
pub ability2: Option<CharacterAbility>,
pub ability3: Option<CharacterAbility>,
pub block_ability: Option<CharacterAbility>,
pub dodge_ability: Option<CharacterAbility>,
pub ability1: Option<Ability>,
pub ability2: Option<Ability>,
pub ability3: Option<Ability>,
pub block_ability: Option<Ability>,
pub dodge_ability: Option<Ability>,
}
#[derive(Arraygen, Clone, PartialEq, Default, Debug, Serialize, Deserialize)]
@ -360,6 +417,10 @@ impl From<&CharacterAbility> for CharacterState {
}
}
impl From<&Ability> for CharacterState {
fn from(ability: &Ability) -> Self { CharacterState::from(&ability.data) }
}
impl Component for Loadout {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -2,7 +2,8 @@
// version in voxygen\src\meta.rs in order to reset save files to being empty
use crate::comp::{
body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile,
body::object, projectile, Ability, AbilityId, Body, CharacterAbility, Gravity, LightEmitter,
Projectile,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -106,62 +107,62 @@ impl Tool {
Duration::from_millis(self.stats.equip_time_millis as u64)
}
pub fn get_abilities(&self) -> Vec<CharacterAbility> {
pub fn get_abilities(&self) -> Vec<Ability> {
use CharacterAbility::*;
use ToolKind::*;
match &self.kind {
Sword(_) => vec![
TripleStrike {
Ability::new(AbilityId::SwordCut, TripleStrike {
base_damage: (60.0 * self.base_power()) as u32,
needs_timing: false,
},
DashMelee {
}),
Ability::new(AbilityId::SwordThrust, DashMelee {
energy_cost: 700,
buildup_duration: Duration::from_millis(500),
recover_duration: Duration::from_millis(500),
base_damage: (120.0 * self.base_power()) as u32,
},
}),
],
Axe(_) => vec![
TripleStrike {
Ability::new(AbilityId::AxeSwing, TripleStrike {
base_damage: (80.0 * self.base_power()) as u32,
needs_timing: true,
},
SpinMelee {
}),
Ability::new(AbilityId::AxeSpin, SpinMelee {
energy_cost: 100,
buildup_duration: Duration::from_millis(125),
recover_duration: Duration::from_millis(125),
base_damage: (60.0 * self.base_power()) as u32,
},
}),
],
Hammer(_) => vec![
BasicMelee {
Ability::new(AbilityId::HammerSmash, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(700),
recover_duration: Duration::from_millis(300),
base_healthchange: (-120.0 * self.base_power()) as i32,
range: 3.5,
max_angle: 60.0,
},
LeapMelee {
}),
Ability::new(AbilityId::HammerLeap, LeapMelee {
energy_cost: 800,
movement_duration: Duration::from_millis(500),
buildup_duration: Duration::from_millis(1000),
recover_duration: Duration::from_millis(100),
base_damage: (240.0 * self.base_power()) as u32,
},
}),
],
Farming(_) => vec![BasicMelee {
Farming(_) => vec![Ability::new(AbilityId::FarmingAttack, BasicMelee {
energy_cost: 1,
buildup_duration: Duration::from_millis(700),
recover_duration: Duration::from_millis(150),
base_healthchange: (-50.0 * self.base_power()) as i32,
range: 3.0,
max_angle: 60.0,
}],
})],
Bow(_) => vec![
BasicRanged {
Ability::new(AbilityId::BowShot, BasicRanged {
energy_cost: 0,
holdable: true,
prepare_duration: Duration::from_millis(100),
@ -180,8 +181,8 @@ impl Tool {
projectile_body: Body::Object(object::Body::Arrow),
projectile_light: None,
projectile_gravity: Some(Gravity(0.2)),
},
ChargedRanged {
}),
Ability::new(AbilityId::BowCharged, ChargedRanged {
energy_cost: 0,
energy_drain: 300,
initial_damage: (40.0 * self.base_power()) as u32,
@ -193,55 +194,55 @@ impl Tool {
recover_duration: Duration::from_millis(500),
projectile_body: Body::Object(object::Body::Arrow),
projectile_light: None,
},
}),
],
Dagger(_) => vec![
BasicMelee {
Ability::new(AbilityId::DaggerStab, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(100),
recover_duration: Duration::from_millis(400),
base_healthchange: (-50.0 * self.base_power()) as i32,
range: 3.5,
max_angle: 60.0,
},
DashMelee {
}),
Ability::new(AbilityId::DaggerDash, DashMelee {
energy_cost: 700,
buildup_duration: Duration::from_millis(500),
recover_duration: Duration::from_millis(500),
base_damage: (100.0 * self.base_power()) as u32,
},
}),
],
Staff(kind) => {
if kind == "Sceptre" {
vec![
BasicMelee {
Ability::new(AbilityId::StaffSwing, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(0),
recover_duration: Duration::from_millis(300),
base_healthchange: (-10.0 * self.base_power()) as i32,
range: 10.0,
max_angle: 45.0,
},
BasicMelee {
}),
Ability::new(AbilityId::StaffHeal, BasicMelee {
energy_cost: 350,
buildup_duration: Duration::from_millis(0),
recover_duration: Duration::from_millis(1000),
base_healthchange: (150.0 * self.base_power()) as i32,
range: 10.0,
max_angle: 45.0,
},
}),
]
} else {
vec![
BasicMelee {
Ability::new(AbilityId::StaffSwing, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(100),
recover_duration: Duration::from_millis(300),
base_healthchange: (-40.0 * self.base_power()) as i32,
range: 10.0,
max_angle: 45.0,
},
BasicRanged {
}),
Ability::new(AbilityId::StaffShot, BasicRanged {
energy_cost: 0,
holdable: false,
prepare_duration: Duration::from_millis(250),
@ -263,8 +264,8 @@ impl Tool {
}),
projectile_gravity: None,
},
BasicRanged {
}),
Ability::new(AbilityId::StaffFireball, BasicRanged {
energy_cost: 400,
holdable: true,
prepare_duration: Duration::from_millis(800),
@ -292,33 +293,33 @@ impl Tool {
}),
projectile_gravity: None,
},
}),
]
}
},
Shield(_) => vec![
BasicMelee {
Ability::new(AbilityId::ShieldBash, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(100),
recover_duration: Duration::from_millis(400),
base_healthchange: (-40.0 * self.base_power()) as i32,
range: 3.0,
max_angle: 120.0,
},
BasicBlock,
}),
Ability::new(AbilityId::ShieldBlock, BasicBlock),
],
Debug(kind) => {
if kind == "Boost" {
vec![
CharacterAbility::Boost {
Ability::new(AbilityId::DebugFlyDirection, CharacterAbility::Boost {
duration: Duration::from_millis(50),
only_up: false,
},
CharacterAbility::Boost {
}),
Ability::new(AbilityId::DebugFlyUp, CharacterAbility::Boost {
duration: Duration::from_millis(50),
only_up: true,
},
BasicRanged {
}),
Ability::new(AbilityId::DebugPossesArrow, BasicRanged {
energy_cost: 0,
holdable: false,
prepare_duration: Duration::from_millis(0),
@ -338,20 +339,20 @@ impl Tool {
..Default::default()
}),
projectile_gravity: None,
},
}),
]
} else {
vec![]
}
},
Empty => vec![BasicMelee {
Empty => vec![Ability::new(AbilityId::Empty, BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(0),
recover_duration: Duration::from_millis(1000),
base_healthchange: -20,
range: 5.0,
max_angle: 60.0,
}],
})],
}
}

View File

@ -2,7 +2,7 @@ use crate::{
comp,
comp::{item, item::armor},
};
use comp::{Inventory, Loadout};
use comp::{Ability, AbilityId, Inventory, Loadout};
use serde::{Deserialize, Serialize};
use tracing::warn;
@ -100,8 +100,11 @@ fn item_config(item: item::Item) -> comp::ItemConfig {
ability1: abilities.next(),
ability2: abilities.next(),
ability3: abilities.next(),
block_ability: Some(comp::CharacterAbility::BasicBlock),
dodge_ability: Some(comp::CharacterAbility::Roll),
block_ability: Some(Ability::new(
AbilityId::Block,
comp::CharacterAbility::BasicBlock,
)),
dodge_ability: Some(Ability::new(AbilityId::Roll, comp::CharacterAbility::Roll)),
}
}

View File

@ -20,7 +20,9 @@ mod stats;
mod visual;
// Reexports
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use ability::{
Ability, AbilityId, CharacterAbility, CharacterAbilityType, ItemConfig, Loadout,
};
pub use admin::{Admin, AdminList};
pub use agent::{Agent, Alignment};
pub use body::{

View File

@ -2,7 +2,7 @@ use crate::{
assets,
comp::{
item::{Item, ItemKind},
Body, CharacterAbility, ItemConfig, Loadout,
Ability, AbilityId, Body, CharacterAbility, ItemConfig, Loadout,
},
};
use std::time::Duration;
@ -67,14 +67,17 @@ impl LoadoutBuilder {
Self(Loadout {
active_item: Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.empty.empty"),
ability1: Some(CharacterAbility::BasicMelee {
energy_cost: 10,
buildup_duration: Duration::from_millis(600),
recover_duration: Duration::from_millis(100),
base_healthchange: -(body.base_dmg() as i32),
range: body.base_range(),
max_angle: 80.0,
}),
ability1: Some(Ability::new(
AbilityId::Empty,
CharacterAbility::BasicMelee {
energy_cost: 10,
buildup_duration: Duration::from_millis(600),
recover_duration: Duration::from_millis(100),
base_healthchange: -(body.base_dmg() as i32),
range: body.base_range(),
max_angle: 80.0,
},
)),
ability2: None,
ability3: None,
block_ability: None,
@ -113,8 +116,11 @@ impl LoadoutBuilder {
ability1: ability_drain.next(),
ability2: ability_drain.next(),
ability3: ability_drain.next(),
block_ability: Some(CharacterAbility::BasicBlock),
dodge_ability: Some(CharacterAbility::Roll),
block_ability: Some(Ability::new(
AbilityId::Block,
CharacterAbility::BasicBlock,
)),
dodge_ability: Some(Ability::new(AbilityId::Roll, CharacterAbility::Roll)),
});
}
}

View File

@ -203,7 +203,7 @@ pub fn handle_ability1_input(data: &JoinData, update: &mut StateUpdate) {
.active_item
.as_ref()
.and_then(|i| i.ability1.as_ref())
.filter(|ability| ability.requirements_paid(data, update))
.filter(|ability| ability.data.requirements_paid(data, update))
{
update.character = ability.into();
}
@ -233,7 +233,7 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) {
.active_item
.as_ref()
.and_then(|i| i.ability2.as_ref())
.filter(|ability| ability.requirements_paid(data, update))
.filter(|ability| ability.data.requirements_paid(data, update))
{
update.character = ability.into();
}
@ -244,7 +244,7 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) {
.second_item
.as_ref()
.and_then(|i| i.ability2.as_ref())
.filter(|ability| ability.requirements_paid(data, update))
.filter(|ability| ability.data.requirements_paid(data, update))
{
update.character = ability.into();
}
@ -262,7 +262,7 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) {
.active_item
.as_ref()
.and_then(|i| i.ability3.as_ref())
.filter(|ability| ability.requirements_paid(data, update))
.filter(|ability| ability.data.requirements_paid(data, update))
{
update.character = ability.into();
}
@ -278,7 +278,7 @@ pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) {
.active_item
.as_ref()
.and_then(|i| i.dodge_ability.as_ref())
.filter(|ability| ability.requirements_paid(data, update))
.filter(|ability| ability.data.requirements_paid(data, update))
{
if data.character.is_wield() {
update.character = ability.into();

View File

@ -127,20 +127,26 @@ impl<'a> System<'a> for Sys {
ability2: ability_drain.next(),
ability3: ability_drain.next(),
block_ability: None,
dodge_ability: Some(comp::CharacterAbility::Roll),
dodge_ability: Some(comp::Ability::new(
comp::AbilityId::Roll,
comp::CharacterAbility::Roll,
)),
})
} else {
Some(ItemConfig {
// We need the empty item so npcs can attack
item: assets::load_expect_cloned("common.items.weapons.empty.empty"),
ability1: Some(CharacterAbility::BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(0),
recover_duration: Duration::from_millis(400),
base_healthchange: -60,
range: 5.0,
max_angle: 80.0,
}),
ability1: Some(comp::Ability::new(
comp::AbilityId::Empty,
CharacterAbility::BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(0),
recover_duration: Duration::from_millis(400),
base_healthchange: -60,
range: 5.0,
max_angle: 80.0,
},
)),
ability2: None,
ability3: None,
block_ability: None,
@ -254,14 +260,17 @@ impl<'a> System<'a> for Sys {
item: assets::load_expect_cloned(
"common.items.weapons.sword.zweihander_sword_0",
),
ability1: Some(CharacterAbility::BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(800),
recover_duration: Duration::from_millis(200),
base_healthchange: -100,
range: 3.5,
max_angle: 60.0,
}),
ability1: Some(comp::Ability::new(
comp::AbilityId::Empty,
CharacterAbility::BasicMelee {
energy_cost: 0,
buildup_duration: Duration::from_millis(800),
recover_duration: Duration::from_millis(200),
base_healthchange: -100,
range: 3.5,
max_angle: 60.0,
},
)),
ability2: None,
ability3: None,
block_ability: None,

View File

@ -20,7 +20,7 @@ use common::comp::{
tool::{Tool, ToolKind},
Hands, ItemKind,
},
CharacterState, ControllerInputs, Energy, Inventory, Loadout, Stats,
AbilityId, CharacterState, ControllerInputs, Energy, Inventory, Loadout, Stats,
};
use conrod_core::{
color,
@ -181,6 +181,44 @@ impl<'a> Skillbar<'a> {
show,
}
}
/// Pairs ability with image.
///
/// TODO: Dehardcode this into a .ron file
fn get_ability_image(&self, ability: AbilityId) -> conrod_core::image::Id {
use AbilityId::*;
match ability {
// Sword
SwordCut => self.imgs.twohsword_m1,
SwordThrust => self.imgs.twohsword_m2,
// Axe
AxeSwing => self.imgs.twohaxe_m1,
AxeSpin => self.imgs.axespin,
// Hammer
HammerSmash => self.imgs.twohhammer_m1,
HammerLeap => self.imgs.hammerleap,
// Bow
BowShot => self.imgs.bow_m1,
BowCharged => self.imgs.bow_m2,
// Staff
StaffSwing => self.imgs.staff_m1,
StaffShot => self.imgs.staff_m2,
StaffFireball => self.imgs.fire_spell_1,
StaffHeal => self.imgs.heal_0,
// Dagger
DaggerStab => self.imgs.onehdagger_m1,
DaggerDash => self.imgs.onehdagger_m2,
// Shield
ShieldBash => self.imgs.onehshield_m1,
ShieldBlock => self.imgs.onehshield_m2,
// Debug
DebugFlyDirection => self.imgs.flyingrod_m1,
DebugFlyUp => self.imgs.flyingrod_m2,
DebugPossesArrow => self.imgs.snake_arrow_0,
_ => self.imgs.nothing,
}
}
}
pub struct State {
@ -608,20 +646,13 @@ impl<'a> Widget for Skillbar<'a> {
.middle_of(state.ids.m1_slot)
.set(state.ids.m1_slot_bg, ui);
Button::image(
match self.loadout.active_item.as_ref().map(|i| &i.item.kind) {
Some(ItemKind::Tool(Tool { kind, .. })) => match kind {
ToolKind::Sword(_) => self.imgs.twohsword_m1,
ToolKind::Dagger(_) => self.imgs.onehdagger_m1,
ToolKind::Shield(_) => self.imgs.onehshield_m1,
ToolKind::Hammer(_) => self.imgs.twohhammer_m1,
ToolKind::Axe(_) => self.imgs.twohaxe_m1,
ToolKind::Bow(_) => self.imgs.bow_m1,
ToolKind::Staff(_) => self.imgs.staff_m1,
ToolKind::Debug(kind) => match kind.as_ref() {
"Boost" => self.imgs.flyingrod_m1,
_ => self.imgs.nothing,
},
_ => self.imgs.nothing,
match self.loadout.active_item.as_ref().map(|i| &i.ability1) {
Some(ability) => {
if let Some(ability) = ability {
self.get_ability_image(ability.id)
} else {
self.imgs.nothing
}
},
_ => self.imgs.nothing,
},
@ -669,63 +700,38 @@ impl<'a> Widget for Skillbar<'a> {
_ => None,
};
let tool_kind = match (
let active_tool_secondary_ability =
match self.loadout.active_item.as_ref().map(|i| &i.ability2) {
Some(Some(ability)) => Some(ability.id),
_ => None,
};
let second_tool_secondary_ability =
match self.loadout.second_item.as_ref().map(|i| &i.ability2) {
Some(Some(ability)) => Some(ability.id),
_ => None,
};
let used_secondary_ability = match (
active_tool_kind.map(|tk| tk.hands()),
second_tool_kind.map(|tk| tk.hands()),
) {
(Some(Hands::TwoHand), _) => active_tool_kind,
(_, Some(Hands::OneHand)) => second_tool_kind,
(Some(Hands::TwoHand), _) => active_tool_secondary_ability,
(Some(Hands::OneHand), Some(Hands::OneHand)) => second_tool_secondary_ability,
(_, _) => None,
};
Image::new(self.imgs.skillbar_slot_big_bg)
.w_h(38.0 * scale, 38.0 * scale)
.color(match tool_kind {
Some(ToolKind::Bow(_)) => Some(BG_COLOR_2),
Some(ToolKind::Staff(_)) => Some(BG_COLOR_2),
_ => Some(BG_COLOR_2),
})
.color(Some(BG_COLOR_2))
.middle_of(state.ids.m2_slot)
.set(state.ids.m2_slot_bg, ui);
Button::image(match tool_kind {
Some(ToolKind::Sword(_)) => self.imgs.twohsword_m2,
Some(ToolKind::Dagger(_)) => self.imgs.onehdagger_m2,
Some(ToolKind::Shield(_)) => self.imgs.onehshield_m2,
Some(ToolKind::Hammer(_)) => self.imgs.hammerleap,
Some(ToolKind::Axe(_)) => self.imgs.axespin,
Some(ToolKind::Bow(_)) => self.imgs.bow_m2,
Some(ToolKind::Staff(kind)) => match kind.as_ref() {
"Sceptre" => self.imgs.heal_0,
_ => self.imgs.staff_m2,
},
Some(ToolKind::Debug(kind)) => match kind.as_ref() {
"Boost" => self.imgs.flyingrod_m2,
_ => self.imgs.nothing,
},
Button::image(match used_secondary_ability {
Some(ability) => self.get_ability_image(ability),
_ => self.imgs.nothing,
})
.w_h(32.0 * scale, 32.0 * scale)
.middle_of(state.ids.m2_slot_bg)
.image_color(match tool_kind {
Some(ToolKind::Sword(_)) => {
if self.energy.current() as f64 >= 200.0 {
Color::Rgba(1.0, 1.0, 1.0, 1.0)
} else {
Color::Rgba(0.3, 0.3, 0.3, 0.8)
}
},
Some(ToolKind::Staff(kind)) => match kind.as_ref() {
"Sceptre" => {
if self.energy.current() as f64 >= 400.0 {
Color::Rgba(1.0, 1.0, 1.0, 1.0)
} else {
Color::Rgba(0.3, 0.3, 0.3, 0.8)
}
},
_ => Color::Rgba(1.0, 1.0, 1.0, 1.0),
},
_ => Color::Rgba(1.0, 1.0, 1.0, 1.0),
})
.set(state.ids.m2_content, ui);
// Slots
let content_source = (self.hotbar, self.inventory, self.loadout, self.energy); // TODO: avoid this