Blocking now works if no weapon is equipped in main hand.

Added temp sfx for blocking and parrying.
Added temp particles for successful parry.
Tweaked values of default block ability.
This commit is contained in:
Sam 2021-04-19 01:35:46 -04:00
parent fad31bd7ab
commit 288a6f3a82
13 changed files with 114 additions and 66 deletions

View File

@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Missing translations can be displayed in English. - Missing translations can be displayed in English.
- New large birds npcs - New large birds npcs
- Day period dependant wildlife spawns - Day period dependant wildlife spawns
- You can now block and parry with melee weapons
### Changed ### Changed

View File

@ -17,6 +17,7 @@ use crate::{
}, },
event::ServerEvent, event::ServerEvent,
outcome::Outcome, outcome::Outcome,
states::utils::StageSection,
uid::Uid, uid::Uid,
util::Dir, util::Dir,
}; };
@ -116,7 +117,12 @@ impl Attack {
pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() } pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
pub fn compute_damage_reduction(target: &TargetInfo, source: AttackSource, dir: Dir) -> f32 { pub fn compute_damage_reduction(
target: &TargetInfo,
source: AttackSource,
dir: Dir,
mut emit_outcome: impl FnMut(Outcome),
) -> f32 {
let damage_reduction = Damage::compute_damage_reduction(target.inventory, target.stats); let damage_reduction = Damage::compute_damage_reduction(target.inventory, target.stats);
let block_reduction = match source { let block_reduction = match source {
AttackSource::Melee => { AttackSource::Melee => {
@ -125,7 +131,11 @@ impl Attack {
{ {
if ori.look_vec().angle_between(-*dir) < data.static_data.max_angle.to_radians() if ori.look_vec().angle_between(-*dir) < data.static_data.max_angle.to_radians()
{ {
if data.parry { emit_outcome(Outcome::Block {
parry: data.parry,
pos: target.pos,
});
if data.parry && matches!(data.stage_section, StageSection::Buildup) {
1.0 1.0
} else { } else {
data.static_data.block_strength data.static_data.block_strength
@ -164,7 +174,8 @@ impl Attack {
.filter(|d| d.target.map_or(true, |t| t == target_group)) .filter(|d| d.target.map_or(true, |t| t == target_group))
.filter(|d| !(matches!(d.target, Some(GroupTarget::OutOfGroup)) && target_dodging)) .filter(|d| !(matches!(d.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
{ {
let damage_reduction = Attack::compute_damage_reduction(&target, attack_source, dir); let damage_reduction =
Attack::compute_damage_reduction(&target, attack_source, dir, |o| emit_outcome(o));
let change = damage.damage.calculate_health_change( let change = damage.damage.calculate_health_change(
damage_reduction, damage_reduction,
attacker.map(|a| a.uid), attacker.map(|a| a.uid),

View File

@ -350,8 +350,8 @@ impl CharacterAbility {
pub fn default_block() -> CharacterAbility { pub fn default_block() -> CharacterAbility {
CharacterAbility::BasicBlock { CharacterAbility::BasicBlock {
buildup_duration: 0.1, buildup_duration: 0.3,
recover_duration: 0.1, recover_duration: 0.2,
max_angle: 60.0, max_angle: 60.0,
block_strength: 0.5, block_strength: 0.5,
energy_cost: 50.0, energy_cost: 50.0,

View File

@ -158,7 +158,10 @@ pub enum InputKind {
impl InputKind { impl InputKind {
pub fn is_ability(self) -> bool { pub fn is_ability(self) -> bool {
matches!(self, Self::Primary | Self::Secondary | Self::Ability(_)) matches!(
self,
Self::Primary | Self::Secondary | Self::Ability(_) | Self::Block
)
} }
} }

View File

@ -59,6 +59,10 @@ pub enum Outcome {
Damage { Damage {
pos: Vec3<f32>, pos: Vec3<f32>,
}, },
Block {
pos: Vec3<f32>,
parry: bool,
},
} }
impl Outcome { impl Outcome {
@ -70,7 +74,8 @@ impl Outcome {
| Outcome::Beam { pos, .. } | Outcome::Beam { pos, .. }
| Outcome::SkillPointGain { pos, .. } | Outcome::SkillPointGain { pos, .. }
| Outcome::SummonedCreature { pos, .. } | Outcome::SummonedCreature { pos, .. }
| Outcome::Damage { pos, .. } => Some(*pos), | Outcome::Damage { pos, .. }
| Outcome::Block { pos, .. } => Some(*pos),
Outcome::BreakBlock { pos, .. } => Some(pos.map(|e| e as f32 + 0.5)), Outcome::BreakBlock { pos, .. } => Some(pos.map(|e| e as f32 + 0.5)),
Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => None, Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => None,
} }

View File

@ -538,21 +538,17 @@ pub fn handle_jump(data: &JoinData, update: &mut StateUpdate, strength: f32) ->
} }
fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) { fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) {
let hands = |equip_slot| match data.inventory.equipped(equip_slot).map(|i| i.kind()) { let hands = get_hands(data);
Some(ItemKind::Tool(tool)) => Some(tool.hands),
_ => None,
};
// Mouse1 and Skill1 always use the MainHand slot // Mouse1 and Skill1 always use the MainHand slot
let always_main_hand = matches!(input, InputKind::Primary | InputKind::Ability(0)); let always_main_hand = matches!(input, InputKind::Primary | InputKind::Ability(0));
let no_main_hand = hands(EquipSlot::Mainhand).is_none(); let no_main_hand = hands.0.is_none();
// skill_index used to select ability for the AbilityKey::Skill2 input // skill_index used to select ability for the AbilityKey::Skill2 input
let (equip_slot, skill_index) = if no_main_hand { let (equip_slot, skill_index) = if no_main_hand {
(Some(EquipSlot::Offhand), 1) (Some(EquipSlot::Offhand), 1)
} else if always_main_hand { } else if always_main_hand {
(Some(EquipSlot::Mainhand), 0) (Some(EquipSlot::Mainhand), 0)
} else { } else {
let hands = (hands(EquipSlot::Mainhand), hands(EquipSlot::Offhand));
match hands { match hands {
(Some(Hands::Two), _) => (Some(EquipSlot::Mainhand), 1), (Some(Hands::Two), _) => (Some(EquipSlot::Mainhand), 1),
(_, Some(Hands::One)) => (Some(EquipSlot::Offhand), 0), (_, Some(Hands::One)) => (Some(EquipSlot::Offhand), 0),
@ -631,7 +627,10 @@ pub fn attempt_input(data: &JoinData, update: &mut StateUpdate) {
pub fn handle_block_input(data: &JoinData, update: &mut StateUpdate) { pub fn handle_block_input(data: &JoinData, update: &mut StateUpdate) {
let can_block = let can_block =
|equip_slot| matches!(unwrap_tool_data(data, equip_slot), Some(tool) if tool.can_block()); |equip_slot| matches!(unwrap_tool_data(data, equip_slot), Some(tool) if tool.can_block());
if input_is_pressed(data, InputKind::Block) && can_block(EquipSlot::Mainhand) { let hands = get_hands(data);
if input_is_pressed(data, InputKind::Block)
&& (can_block(EquipSlot::Mainhand) || (hands.0.is_none() && can_block(EquipSlot::Offhand)))
{
let ability = CharacterAbility::default_block(); let ability = CharacterAbility::default_block();
if ability.requirements_paid(data, update) { if ability.requirements_paid(data, update) {
update.character = CharacterState::from(( update.character = CharacterState::from((
@ -678,6 +677,17 @@ pub fn unwrap_tool_data<'a>(data: &'a JoinData, equip_slot: EquipSlot) -> Option
} }
} }
pub fn get_hands(data: &JoinData) -> (Option<Hands>, Option<Hands>) {
let hand = |slot| {
if let Some(ItemKind::Tool(tool)) = data.inventory.equipped(slot).map(|i| i.kind()) {
Some(tool.hands)
} else {
None
}
};
(hand(EquipSlot::Mainhand), hand(EquipSlot::Offhand))
}
pub fn get_crit_data(data: &JoinData, ai: AbilityInfo) -> (f32, f32) { pub fn get_crit_data(data: &JoinData, ai: AbilityInfo) -> (f32, f32) {
const DEFAULT_CRIT_DATA: (f32, f32) = (0.5, 1.3); const DEFAULT_CRIT_DATA: (f32, f32) = (0.5, 1.3);
use HandInfo::*; use HandInfo::*;

View File

@ -188,7 +188,7 @@ impl<'a> System<'a> for Sys {
inventory: read_data.inventories.get(target), inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target), stats: read_data.stats.get(target),
health: read_data.healths.get(target), health: read_data.healths.get(target),
pos: pos.0, pos: pos_b.0,
ori: read_data.orientations.get(target), ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target), char_state: read_data.character_states.get(target),
}; };

View File

@ -146,7 +146,7 @@ impl<'a> System<'a> for Sys {
inventory: read_data.inventories.get(target), inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target), stats: read_data.stats.get(target),
health: read_data.healths.get(target), health: read_data.healths.get(target),
pos: pos.0, pos: pos_b.0,
ori: read_data.orientations.get(target), ori: read_data.orientations.get(target),
char_state: read_data.char_states.get(target), char_state: read_data.char_states.get(target),
}; };

View File

@ -111,6 +111,7 @@ impl<'a> System<'a> for Sys {
.uid_allocator .uid_allocator
.retrieve_entity_internal(other.into()) .retrieve_entity_internal(other.into())
{ {
if let Some(pos) = read_data.positions.get(target) {
let owner_entity = projectile.owner.and_then(|u| { let owner_entity = projectile.owner.and_then(|u| {
read_data.uid_allocator.retrieve_entity_internal(u.into()) read_data.uid_allocator.retrieve_entity_internal(u.into())
}); });
@ -162,6 +163,7 @@ impl<'a> System<'a> for Sys {
|o| outcomes.push(o), |o| outcomes.push(o),
); );
} }
}
}, },
projectile::Effect::Explode(e) => { projectile::Effect::Explode(e) => {
server_emitter.emit(ServerEvent::Explosion { server_emitter.emit(ServerEvent::Explosion {

View File

@ -193,7 +193,7 @@ impl<'a> System<'a> for Sys {
inventory: read_data.inventories.get(target), inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target), stats: read_data.stats.get(target),
health: read_data.healths.get(target), health: read_data.healths.get(target),
pos: pos.0, pos: pos_b.0,
ori: read_data.orientations.get(target), ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target), char_state: read_data.character_states.get(target),
}; };

View File

@ -739,7 +739,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
inventory: inventory_b_maybe, inventory: inventory_b_maybe,
stats: stats_b_maybe, stats: stats_b_maybe,
health: Some(health_b), health: Some(health_b),
pos, pos: pos_b.0,
ori: ori_b_maybe, ori: ori_b_maybe,
char_state: char_state_b_maybe, char_state: char_state_b_maybe,
}; };

View File

@ -407,9 +407,6 @@ impl SfxMgr {
let file_ref = "voxygen.audio.sfx.footsteps.stone_step_1"; let file_ref = "voxygen.audio.sfx.footsteps.stone_step_1";
audio.play_sfx(file_ref, pos.map(|e| e as f32 + 0.5), Some(3.0)); audio.play_sfx(file_ref, pos.map(|e| e as f32 + 0.5), Some(3.0));
}, },
Outcome::ExpChange { .. }
| Outcome::ComboChange { .. }
| Outcome::SummonedCreature { .. } => {},
Outcome::Damage { pos, .. } => { Outcome::Damage { pos, .. } => {
let file_ref = vec![ let file_ref = vec![
"voxygen.audio.sfx.character.hit_1", "voxygen.audio.sfx.character.hit_1",
@ -419,6 +416,13 @@ impl SfxMgr {
][rand::thread_rng().gen_range(1..4)]; ][rand::thread_rng().gen_range(1..4)];
audio.play_sfx(file_ref, *pos, None); audio.play_sfx(file_ref, *pos, None);
}, },
Outcome::Block { pos, parry: _parry } => {
// TODO: Get audio for blocking and parrying
audio.play_sfx("voxygen.audio.sfx.character.arrow_hit", *pos, Some(2.0));
},
Outcome::ExpChange { .. }
| Outcome::ComboChange { .. }
| Outcome::SummonedCreature { .. } => {},
} }
} }

View File

@ -203,6 +203,18 @@ impl ParticleMgr {
}); });
} }
}, },
Outcome::Block { pos, parry } => {
if *parry {
self.particles.resize_with(self.particles.len() + 20, || {
Particle::new(
Duration::from_millis(200),
time,
ParticleMode::GunPowderSpark,
*pos + Vec3::unit_z(),
)
});
}
},
Outcome::ProjectileShot { .. } Outcome::ProjectileShot { .. }
| Outcome::Beam { .. } | Outcome::Beam { .. }
| Outcome::ExpChange { .. } | Outcome::ExpChange { .. }