From c9ec7e9a266bc77241f97c46b28822b0c978562c Mon Sep 17 00:00:00 2001 From: ch-iv Date: Mon, 23 Mar 2026 12:37:12 -0400 Subject: [PATCH] Implement attack handler --- .../packet_handlers/play_packets/interact.rs | 149 ++++++++++++++++++ .../src/packet_handlers/play_packets/mod.rs | 2 + src/bin/src/systems/mobs/combat.rs | 8 + src/bin/src/systems/mobs/mod.rs | 11 +- src/bin/src/systems/physics/friction.rs | 34 ++++ src/bin/src/systems/physics/mod.rs | 2 + src/lib/data/src/lib.rs | 1 - src/lib/net/src/packets/incoming/interact.rs | 92 ++++++++++- 8 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 src/bin/src/packet_handlers/play_packets/interact.rs create mode 100644 src/bin/src/systems/mobs/combat.rs create mode 100644 src/bin/src/systems/physics/friction.rs diff --git a/src/bin/src/packet_handlers/play_packets/interact.rs b/src/bin/src/packet_handlers/play_packets/interact.rs new file mode 100644 index 000000000..d140d1563 --- /dev/null +++ b/src/bin/src/packet_handlers/play_packets/interact.rs @@ -0,0 +1,149 @@ +use bevy_ecs::prelude::{Entity, Query, Res}; +use ferrumc_components::health::Health; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::position::Position; +use ferrumc_core::transform::rotation::Rotation; +use ferrumc_core::transform::velocity::Velocity; +use ferrumc_entities::components::combat::CombatProperties; +use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::incoming::interact::InteractEntity; +use ferrumc_net::packets::outgoing::hurt_animation::HurtAnimationPacket; +use ferrumc_net::InteractEntityReceiver; +use ferrumc_state::GlobalStateResource; +use tracing::error; + +/// Standard knockback horizontal strength. +const KNOCKBACK_STRENGTH: f32 = 0.5; +/// Vertical hop applied on knockback. +const KNOCKBACK_HOP: f32 = 0.4; + +type InteractEntityQuery<'w, 's> = Query< + 'w, + 's, + ( + Entity, + Option<&'static EntityIdentity>, + Option<&'static PlayerIdentity>, + Option<&'static mut Health>, + Option<&'static mut CombatProperties>, + Option<&'static mut Velocity>, + Option<&'static mut OnGround>, + ), +>; + +pub fn handle( + receiver: Res, + mut entity_query: InteractEntityQuery, + attacker_query: Query<(&Position, &Rotation)>, + conn_query: Query<(Entity, &StreamWriter)>, + state: Res, +) { + for (event, attacker_eid) in receiver.0.try_iter() { + if !event.is_attack() { + continue; + } + + for ( + _e, + entity_id_comp, + player_id_comp, + mut health, + mut combat, + mut velocity, + mut on_ground, + ) in entity_query.iter_mut() + { + if !is_target(entity_id_comp, player_id_comp, &event) { + continue; + } + + let Some(ref mut combat) = combat else { + continue; + }; + + if !combat.can_be_damaged() { + continue; + } + + let Ok((_attacker_pos, attacker_rot)) = attacker_query.get(attacker_eid) else { + continue; + }; + + // Mark entity as invulnerable for some amount of time after being attacked + combat.set_default_invulnerability(); + + // Decrease health if the entity has a Health component + // TODO: Ensure death is handled when health reaches 0. + // TODO: Ensure the held item and armor is taken into account for damage calculation. + if let Some(ref mut health) = health { + health.current -= 1.0; + } + + apply_knockback( + velocity.as_deref_mut(), + on_ground.as_deref_mut(), + attacker_rot, + ); + + broadcast_hurt_animation(&conn_query, &state, &event, attacker_rot); + } + } +} + +/// Apply knockback to the entity based on the attacker's facing direction. +fn apply_knockback( + velocity: Option<&mut Velocity>, + on_ground: Option<&mut OnGround>, + attacker_rot: &Rotation, +) { + let Some(v) = velocity else { return }; + + let yaw_rad = attacker_rot.yaw.to_radians(); + + v.vec.x += -yaw_rad.sin() * KNOCKBACK_STRENGTH; + v.vec.y += KNOCKBACK_HOP; + v.vec.z += yaw_rad.cos() * KNOCKBACK_STRENGTH; + + if let Some(og) = on_ground { + og.0 = false; + } +} + +/// Broadcast the hurt animation packet to all connected players. +fn broadcast_hurt_animation( + conn_query: &Query<(Entity, &StreamWriter)>, + state: &Res, + event: &InteractEntity, + attacker_rot: &Rotation, +) { + let hurt_packet = HurtAnimationPacket::new(event.entity_id.0, attacker_rot.yaw); + + for (conn_entity, conn) in conn_query.iter() { + if !state.0.players.is_connected(conn_entity) { + continue; + } + + if let Err(e) = conn.send_packet_ref(&hurt_packet) { + error!("Failed to send damage status packet: {}", e); + } + } +} + +/// Check whether the given entity components match the target of the interact event. +fn is_target( + entity_id_comp: Option<&EntityIdentity>, + player_id_comp: Option<&PlayerIdentity>, + event: &InteractEntity, +) -> bool { + let target_network_id = event.entity_id.0; + + if let Some(eid) = entity_id_comp { + eid.entity_id == target_network_id + } else if let Some(pid) = player_id_comp { + pid.short_uuid == target_network_id + } else { + false + } +} diff --git a/src/bin/src/packet_handlers/play_packets/mod.rs b/src/bin/src/packet_handlers/play_packets/mod.rs index a2c3df943..8ff5224b3 100644 --- a/src/bin/src/packet_handlers/play_packets/mod.rs +++ b/src/bin/src/packet_handlers/play_packets/mod.rs @@ -6,6 +6,7 @@ mod chunk_batch_ack; mod command; mod command_suggestions; mod confirm_player_teleport; +mod interact; mod keep_alive; mod pick_item_from_block; mod place_block; @@ -22,6 +23,7 @@ mod swing_arm; pub fn register_packet_handlers(schedule: &mut Schedule) { // Added separately so if we mess up the signature of one of the systems we can know exactly // which one + schedule.add_systems(interact::handle); schedule.add_systems(chunk_batch_ack::handle); schedule.add_systems(confirm_player_teleport::handle); schedule.add_systems(keep_alive::handle); diff --git a/src/bin/src/systems/mobs/combat.rs b/src/bin/src/systems/mobs/combat.rs new file mode 100644 index 000000000..2556b3a7d --- /dev/null +++ b/src/bin/src/systems/mobs/combat.rs @@ -0,0 +1,8 @@ +use bevy_ecs::prelude::Query; +use ferrumc_entities::components::combat::CombatProperties; + +pub fn tick_combat(mut query: Query<&mut CombatProperties>) { + for mut combat in query.iter_mut() { + combat.tick(); + } +} diff --git a/src/bin/src/systems/mobs/mod.rs b/src/bin/src/systems/mobs/mod.rs index 2688a5e41..51bc92abe 100644 --- a/src/bin/src/systems/mobs/mod.rs +++ b/src/bin/src/systems/mobs/mod.rs @@ -1,5 +1,12 @@ +mod combat; mod pig; -pub fn register_mob_systems(_schedule: &mut bevy_ecs::schedule::Schedule) { - //schedule.add_systems(pig::tick_pig); +#[expect(unused_parens)] +pub fn register_mob_systems(schedule: &mut bevy_ecs::schedule::Schedule) { + schedule.add_systems( + ( + // pig::tick_pig, + combat::tick_combat + ), + ); } diff --git a/src/bin/src/systems/physics/friction.rs b/src/bin/src/systems/physics/friction.rs new file mode 100644 index 000000000..fc7766bfd --- /dev/null +++ b/src/bin/src/systems/physics/friction.rs @@ -0,0 +1,34 @@ +use bevy_ecs::prelude::Query; +use ferrumc_core::transform::grounded::OnGround; +use ferrumc_core::transform::velocity::Velocity; + +const GROUND_FRICTION: f32 = 0.6; +const AIR_FRICTION: f32 = 0.91; +const DRAG: f32 = 0.98; + +const STOP_THRESHOLD: f32 = 0.005; + +pub fn handle(mut query: Query<(&mut Velocity, &OnGround)>) { + for (mut vel, on_ground) in query.iter_mut() { + let h_friction = if on_ground.0 { + GROUND_FRICTION + } else { + AIR_FRICTION + }; + + vel.vec.x *= h_friction; + vel.vec.y *= DRAG; + vel.vec.z *= h_friction; + + // Stop moving completely if velocity is very small + if vel.vec.x.abs() < STOP_THRESHOLD { + vel.vec.x = 0.0; + } + if vel.vec.y.abs() < STOP_THRESHOLD { + vel.vec.y = 0.0; + } + if vel.vec.z.abs() < STOP_THRESHOLD { + vel.vec.z = 0.0; + } + } +} diff --git a/src/bin/src/systems/physics/mod.rs b/src/bin/src/systems/physics/mod.rs index c8582cf4d..d77babdbc 100644 --- a/src/bin/src/systems/physics/mod.rs +++ b/src/bin/src/systems/physics/mod.rs @@ -1,6 +1,7 @@ use bevy_ecs::schedule::IntoScheduleConfigs; pub mod collisions; pub mod drag; +pub mod friction; pub mod gravity; pub mod unground; pub mod velocity; @@ -11,6 +12,7 @@ pub fn register_physics(schedule: &mut bevy_ecs::schedule::Schedule) { unground::handle, gravity::handle, drag::handle, + friction::handle, velocity::handle, collisions::handle, ) diff --git a/src/lib/data/src/lib.rs b/src/lib/data/src/lib.rs index 97af8fbba..6c2755e36 100644 --- a/src/lib/data/src/lib.rs +++ b/src/lib/data/src/lib.rs @@ -1,7 +1,6 @@ // Include generated modules #![feature(const_option_ops)] #![feature(const_trait_impl)] -#![feature(const_cmp)] pub mod generated; diff --git a/src/lib/net/src/packets/incoming/interact.rs b/src/lib/net/src/packets/incoming/interact.rs index 0bef48b77..6232a635a 100644 --- a/src/lib/net/src/packets/incoming/interact.rs +++ b/src/lib/net/src/packets/incoming/interact.rs @@ -21,21 +21,105 @@ pub enum InteractionType { /// Sent when a player interacts with an entity. /// /// This packet is used for both attacking (left-click) and interacting (right-click). -#[derive(NetDecode, Debug)] +#[derive(Debug)] #[packet(packet_id = "interact", state = "play")] pub struct InteractEntity { /// The entity ID being interacted with pub entity_id: VarInt, /// The type of interaction pub interaction_type: InteractionType, - // Note: interact_at has additional target_x, target_y, target_z, hand fields - // For now we'll only handle the attack case properly + pub target_x: Option, + pub target_y: Option, + pub target_z: Option, + pub hand: Option, /// Whether the player is sneaking pub sneaking: bool, } +impl ferrumc_net_codec::decode::NetDecode for InteractEntity { + fn decode( + cursor: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts, + ) -> Result { + let entity_id = VarInt::decode(cursor, opts)?; + let interaction_type = InteractionType::decode(cursor, opts)?; + + let mut target_x = None; + let mut target_y = None; + let mut target_z = None; + let mut hand = None; + + match interaction_type { + InteractionType::InteractAt => { + target_x = Some(f32::decode(cursor, opts)?); + target_y = Some(f32::decode(cursor, opts)?); + target_z = Some(f32::decode(cursor, opts)?); + hand = Some(VarInt::decode(cursor, opts)?); + } + InteractionType::Interact => { + hand = Some(VarInt::decode(cursor, opts)?); + } + InteractionType::Attack => {} + } + + let sneaking = bool::decode(cursor, opts)?; + + let packet = Self { + entity_id, + interaction_type, + target_x, + target_y, + target_z, + hand, + sneaking, + }; + + Ok(packet) + } + + async fn decode_async( + cursor: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts, + ) -> Result { + let entity_id = VarInt::decode_async(cursor, opts).await?; + let interaction_type = InteractionType::decode_async(cursor, opts).await?; + + let mut target_x = None; + let mut target_y = None; + let mut target_z = None; + let mut hand = None; + + match interaction_type { + InteractionType::InteractAt => { + target_x = Some(f32::decode_async(cursor, opts).await?); + target_y = Some(f32::decode_async(cursor, opts).await?); + target_z = Some(f32::decode_async(cursor, opts).await?); + hand = Some(VarInt::decode_async(cursor, opts).await?); + } + InteractionType::Interact => { + hand = Some(VarInt::decode_async(cursor, opts).await?); + } + InteractionType::Attack => {} + } + + let sneaking = bool::decode_async(cursor, opts).await?; + + let packet = Self { + entity_id, + interaction_type, + target_x, + target_y, + target_z, + hand, + sneaking, + }; + + Ok(packet) + } +} + impl InteractEntity { - /// Check if this is an attack interaction. + // Check if this is an attack interaction. pub fn is_attack(&self) -> bool { self.interaction_type == InteractionType::Attack }