diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a23e1566f..2c1f1b9c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - New large birds npcs - Day period dependant wildlife spawns +- You can now block and parry with melee weapons ### Changed diff --git a/common/src/combat.rs b/common/src/combat.rs index 52ed671846..031b04d91d 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -17,6 +17,7 @@ use crate::{ }, event::ServerEvent, outcome::Outcome, + states::utils::StageSection, uid::Uid, util::Dir, }; @@ -116,7 +117,12 @@ impl Attack { pub fn effects(&self) -> impl Iterator { 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 block_reduction = match source { AttackSource::Melee => { @@ -125,7 +131,11 @@ impl Attack { { 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 } else { data.static_data.block_strength @@ -164,7 +174,8 @@ impl Attack { .filter(|d| d.target.map_or(true, |t| t == target_group)) .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( damage_reduction, attacker.map(|a| a.uid), diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 804e4d7563..36c4f720ea 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -350,8 +350,8 @@ impl CharacterAbility { pub fn default_block() -> CharacterAbility { CharacterAbility::BasicBlock { - buildup_duration: 0.1, - recover_duration: 0.1, + buildup_duration: 0.3, + recover_duration: 0.2, max_angle: 60.0, block_strength: 0.5, energy_cost: 50.0, diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index f937cb7dc7..9e13b4e327 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -158,7 +158,10 @@ pub enum InputKind { impl InputKind { pub fn is_ability(self) -> bool { - matches!(self, Self::Primary | Self::Secondary | Self::Ability(_)) + matches!( + self, + Self::Primary | Self::Secondary | Self::Ability(_) | Self::Block + ) } } diff --git a/common/src/outcome.rs b/common/src/outcome.rs index ecc1e08b05..2801f3a3c9 100644 --- a/common/src/outcome.rs +++ b/common/src/outcome.rs @@ -59,6 +59,10 @@ pub enum Outcome { Damage { pos: Vec3, }, + Block { + pos: Vec3, + parry: bool, + }, } impl Outcome { @@ -70,7 +74,8 @@ impl Outcome { | Outcome::Beam { pos, .. } | Outcome::SkillPointGain { 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::ExpChange { .. } | Outcome::ComboChange { .. } => None, } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index d145ed26e4..76dd3d1ac7 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -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) { - let hands = |equip_slot| match data.inventory.equipped(equip_slot).map(|i| i.kind()) { - Some(ItemKind::Tool(tool)) => Some(tool.hands), - _ => None, - }; + let hands = get_hands(data); // Mouse1 and Skill1 always use the MainHand slot let always_main_hand = matches!(input, InputKind::Primary | InputKind::Ability(0)); - let no_main_hand = hands(EquipSlot::Mainhand).is_none(); + let no_main_hand = hands.0.is_none(); // skill_index used to select ability for the AbilityKey::Skill2 input let (equip_slot, skill_index) = if no_main_hand { (Some(EquipSlot::Offhand), 1) } else if always_main_hand { (Some(EquipSlot::Mainhand), 0) } else { - let hands = (hands(EquipSlot::Mainhand), hands(EquipSlot::Offhand)); match hands { (Some(Hands::Two), _) => (Some(EquipSlot::Mainhand), 1), (_, 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) { let 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(); if ability.requirements_paid(data, update) { 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, Option) { + 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) { const DEFAULT_CRIT_DATA: (f32, f32) = (0.5, 1.3); use HandInfo::*; diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index 7640195837..594f9ecbe5 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -188,7 +188,7 @@ impl<'a> System<'a> for Sys { inventory: read_data.inventories.get(target), stats: read_data.stats.get(target), health: read_data.healths.get(target), - pos: pos.0, + pos: pos_b.0, ori: read_data.orientations.get(target), char_state: read_data.character_states.get(target), }; diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index ab1688414c..998a67986f 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -146,7 +146,7 @@ impl<'a> System<'a> for Sys { inventory: read_data.inventories.get(target), stats: read_data.stats.get(target), health: read_data.healths.get(target), - pos: pos.0, + pos: pos_b.0, ori: read_data.orientations.get(target), char_state: read_data.char_states.get(target), }; diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index 2a8d10cbdf..b054377cff 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -111,56 +111,58 @@ impl<'a> System<'a> for Sys { .uid_allocator .retrieve_entity_internal(other.into()) { - let owner_entity = projectile.owner.and_then(|u| { - read_data.uid_allocator.retrieve_entity_internal(u.into()) - }); - - let attacker_info = - owner_entity.zip(projectile.owner).map(|(entity, uid)| { - AttackerInfo { - entity, - uid, - energy: read_data.energies.get(entity), - combo: read_data.combos.get(entity), - } + if let Some(pos) = read_data.positions.get(target) { + let owner_entity = projectile.owner.and_then(|u| { + read_data.uid_allocator.retrieve_entity_internal(u.into()) }); - let target_info = TargetInfo { - entity: target, - inventory: read_data.inventories.get(target), - stats: read_data.stats.get(target), - health: read_data.healths.get(target), - pos: pos.0, - // TODO: Let someone smarter figure this out - // ori: orientations.get(target), - ori: None, - char_state: read_data.character_states.get(target), - }; + let attacker_info = + owner_entity.zip(projectile.owner).map(|(entity, uid)| { + AttackerInfo { + entity, + uid, + energy: read_data.energies.get(entity), + combo: read_data.combos.get(entity), + } + }); - if let Some(&body) = read_data.bodies.get(entity) { - outcomes.push(Outcome::ProjectileHit { + let target_info = TargetInfo { + entity: target, + inventory: read_data.inventories.get(target), + stats: read_data.stats.get(target), + health: read_data.healths.get(target), pos: pos.0, - body, - vel: read_data - .velocities - .get(entity) - .map_or(Vec3::zero(), |v| v.0), - source: projectile.owner, - target: read_data.uids.get(target).copied(), - }); - } + // TODO: Let someone smarter figure this out + // ori: orientations.get(target), + ori: None, + char_state: read_data.character_states.get(target), + }; - attack.apply_attack( - target_group, - attacker_info, - target_info, - ori.look_dir(), - false, - 1.0, - AttackSource::Projectile, - |e| server_emitter.emit(e), - |o| outcomes.push(o), - ); + if let Some(&body) = read_data.bodies.get(entity) { + outcomes.push(Outcome::ProjectileHit { + pos: pos.0, + body, + vel: read_data + .velocities + .get(entity) + .map_or(Vec3::zero(), |v| v.0), + source: projectile.owner, + target: read_data.uids.get(target).copied(), + }); + } + + attack.apply_attack( + target_group, + attacker_info, + target_info, + ori.look_dir(), + false, + 1.0, + AttackSource::Projectile, + |e| server_emitter.emit(e), + |o| outcomes.push(o), + ); + } } }, projectile::Effect::Explode(e) => { diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index e77eff0f07..3e86f4c98e 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -193,7 +193,7 @@ impl<'a> System<'a> for Sys { inventory: read_data.inventories.get(target), stats: read_data.stats.get(target), health: read_data.healths.get(target), - pos: pos.0, + pos: pos_b.0, ori: read_data.orientations.get(target), char_state: read_data.character_states.get(target), }; diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 1267d70c15..8ab17c7bbd 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -739,7 +739,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o inventory: inventory_b_maybe, stats: stats_b_maybe, health: Some(health_b), - pos, + pos: pos_b.0, ori: ori_b_maybe, char_state: char_state_b_maybe, }; diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 96ea404ba2..1f2072ed4b 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -407,9 +407,6 @@ impl SfxMgr { 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)); }, - Outcome::ExpChange { .. } - | Outcome::ComboChange { .. } - | Outcome::SummonedCreature { .. } => {}, Outcome::Damage { pos, .. } => { let file_ref = vec![ "voxygen.audio.sfx.character.hit_1", @@ -419,6 +416,13 @@ impl SfxMgr { ][rand::thread_rng().gen_range(1..4)]; 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 { .. } => {}, } } diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index b58bbff27f..852cc28325 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -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::Beam { .. } | Outcome::ExpChange { .. }