Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ resolver = "2"
"src/lib/core",
"src/lib/core/state",
"src/lib/derive_macros",
"src/lib/entities",
"src/lib/net",
"src/lib/net/crates/codec",
"src/lib/net/crates/encryption",
Expand Down Expand Up @@ -98,6 +99,7 @@ ferrumc-anvil = { path = "src/lib/adapters/anvil" }
ferrumc-config = { path = "src/lib/config" }
ferrumc-core = { path = "src/lib/core" }
ferrumc-default-commands = { path = "src/lib/default_commands" }
ferrumc-entities = { path = "src/lib/entities" }
ferrumc-commands = { path = "src/lib/commands" }
ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" }
ferrumc-logging = { path = "src/lib/utils/logging" }
Expand Down
1 change: 1 addition & 0 deletions src/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ thiserror = { workspace = true }
ferrumc-core = { workspace = true }
bevy_ecs = { workspace = true }
ferrumc-scheduler = { workspace = true }
ferrumc-entities = { workspace = true }

ferrumc-registry = { workspace = true }
ferrumc-events = { workspace = true }
Expand Down
85 changes: 85 additions & 0 deletions src/bin/src/packet_handlers/play_packets/interact_entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use bevy_ecs::prelude::{EventWriter, Query, Res};
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_core::transform::position::Position;
use ferrumc_entities::DamageEvent;
use ferrumc_entities::EntityNetworkIdIndex;
use ferrumc_net::InteractEntityPacketReceiver;
use ferrumc_state::GlobalStateResource;
use tracing::{debug, warn};

pub fn handle(
events: Res<InteractEntityPacketReceiver>,
player_query: Query<(&PlayerIdentity, &Position)>,
entity_query: Query<&Position>,
entity_index: Res<EntityNetworkIdIndex>,
mut damage_events: EventWriter<DamageEvent>,
_state: Res<GlobalStateResource>,
) {
for (event, player_eid) in events.0.try_iter() {
// Get player identity and position
let Ok((player_identity, player_pos)) = player_query.get(player_eid) else {
warn!(
"Player identity/position not found for entity {:?}",
player_eid
);
continue;
};

// Check if this is an attack interaction
if !event.is_attack() {
debug!(
"Non-attack interaction type {} (not implemented yet)",
event.interaction_type.0
);
continue;
}

// Fast O(1) lookup using network ID index
let target_network_id = event.entity_id.0;
let Some(target) = entity_index.get(target_network_id) else {
warn!(
"Player {} attacked non-existent entity with network ID {}",
player_identity.short_uuid, target_network_id
);
continue;
};

// Get target position
let Ok(target_pos) = entity_query.get(target) else {
warn!("Target entity {:?} missing Position component", target);
continue;
};

// Calculate knockback direction (from attacker to target, normalized)
let dx = target_pos.x - player_pos.x;
let dz = target_pos.z - player_pos.z;
let horizontal_dist = (dx * dx + dz * dz).sqrt();

let knockback_direction = if horizontal_dist > 0.0 {
Some((dx / horizontal_dist, 0.0, dz / horizontal_dist))
} else {
None
};

// Base damage for unarmed attack in Minecraft is 1.0
// TODO: Calculate damage based on held item, enchantments, critical hits, etc.
let base_damage = 1.0;

// Base knockback strength (Minecraft default)
let knockback_strength = 0.4;

debug!(
"Player {} attacked entity {:?} (network ID: {}, sneaking: {}) - Damage: {}, Knockback: {:?}",
player_identity.short_uuid, target, target_network_id, event.sneaking, base_damage, knockback_direction
);

// Write damage event
damage_events.write(DamageEvent {
target,
attacker: Some(player_eid),
damage: base_damage,
knockback_direction,
knockback_strength,
});
}
}
2 changes: 2 additions & 0 deletions src/bin/src/packet_handlers/play_packets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod chunk_batch_ack;
mod command;
mod command_suggestions;
mod confirm_player_teleport;
mod interact_entity;
mod keep_alive;
mod pick_item_from_block;
mod place_block;
Expand All @@ -23,6 +24,7 @@ pub fn register_packet_handlers(schedule: &mut Schedule) {
// which one
schedule.add_systems(chunk_batch_ack::handle);
schedule.add_systems(confirm_player_teleport::handle);
schedule.add_systems(interact_entity::handle);
schedule.add_systems(keep_alive::handle);
schedule.add_systems(place_block::handle);
schedule.add_systems(player_action::handle);
Expand Down
3 changes: 3 additions & 0 deletions src/bin/src/register_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bevy_ecs::prelude::World;
use ferrumc_commands::events::{CommandDispatchEvent, ResolvedCommandDispatchEvent};
use ferrumc_core::chunks::cross_chunk_boundary_event::CrossChunkBoundaryEvent;
use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecountEvent;
use ferrumc_entities::{DamageEvent, SpawnEntityEvent};
use ferrumc_events::*;
use ferrumc_net::packets::packet_events::TransformEvent;

Expand All @@ -24,4 +25,6 @@ pub fn register_events(world: &mut World) {
EventRegistry::register_event::<PlayerXPGainEvent>(world);
EventRegistry::register_event::<PlayerLevelUpEvent>(world);
EventRegistry::register_event::<ChangeGameModeEvent>(world);
EventRegistry::register_event::<SpawnEntityEvent>(world);
EventRegistry::register_event::<DamageEvent>(world);
}
2 changes: 2 additions & 0 deletions src/bin/src/register_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bevy_ecs::prelude::World;
use crossbeam_channel::Receiver;
use ferrumc_core::chunks::world_sync_tracker::WorldSyncTracker;
use ferrumc_core::conn::player_count_update_cooldown::PlayerCountUpdateCooldown;
use ferrumc_entities::EntityNetworkIdIndex;
use ferrumc_net::connection::NewConnection;
use ferrumc_state::GlobalStateResource;

Expand All @@ -19,4 +20,5 @@ pub fn register_resources(
world.insert_resource(WorldSyncTracker {
last_synced: std::time::Instant::now(),
});
world.insert_resource(EntityNetworkIdIndex::new());
}
85 changes: 85 additions & 0 deletions src/bin/src/systems/entities/entity_damage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use bevy_ecs::prelude::*;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_entities::components::{EntityId, EntityType, Health, Velocity};
use ferrumc_entities::DamageEvent;
use ferrumc_net::connection::StreamWriter;
use ferrumc_net::packets::outgoing::entity_sound_effect::EntitySoundEffectPacket;
use ferrumc_state::GlobalStateResource;
use tracing::{debug, info};

/// System that processes damage events and applies damage + knockback to entities
pub fn entity_damage_system(
mut damage_events: EventReader<DamageEvent>,
mut entity_query: Query<(&EntityId, &EntityType, &mut Health, &mut Velocity)>,
player_query: Query<(Entity, &StreamWriter), With<PlayerIdentity>>,
state: Res<GlobalStateResource>,
) {
for event in damage_events.read() {
// Get the target entity's components
let Ok((entity_id, entity_type, mut health, mut velocity)) =
entity_query.get_mut(event.target)
else {
debug!(
"Damage event target entity {:?} not found or missing components",
event.target
);
continue;
};

// Apply damage
health.damage(event.damage);

info!(
"Entity {} took {} damage (HP: {}/{})",
entity_id.to_network_id(),
event.damage,
health.current,
health.max
);

// Apply knockback if direction is specified
if let Some((kx, _ky, kz)) = event.knockback_direction {
let strength = event.knockback_strength;
velocity.x += kx * strength;
velocity.y += 0.4; // Minecraft vanilla: always add upward velocity on hit
velocity.z += kz * strength;

debug!(
"Applied knockback to entity {}: velocity ({:.2}, {:.2}, {:.2})",
entity_id.to_network_id(),
velocity.x,
velocity.y,
velocity.z
);
}

// Send hurt sound effect to all connected players
let sound_id = match entity_type {
EntityType::Pig => 1114, // entity.pig.hurt
// TODO: Add more entity types and their hurt sounds
_ => {
debug!("No hurt sound defined for {:?}", entity_type);
continue; // Skip sound if not defined
}
};

let sound_packet = EntitySoundEffectPacket::hurt(sound_id, entity_id.to_network_id());

for (player_entity, stream_writer) in player_query.iter() {
if state.0.players.is_connected(player_entity) {
if let Err(e) = stream_writer.send_packet_ref(&sound_packet) {
debug!("Failed to send hurt sound to player: {}", e);
}
}
}

debug!(
"Sent hurt sound {} for entity {}",
sound_id,
entity_id.to_network_id()
);

// TODO: Send damage animation packet to nearby players
// TODO: Apply invulnerability ticks (prevent damage spam)
}
}
57 changes: 57 additions & 0 deletions src/bin/src/systems/entities/entity_death.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use bevy_ecs::prelude::*;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_entities::components::{EntityId, EntityType, Health};
use ferrumc_entities::EntityNetworkIdIndex;
use ferrumc_net::connection::StreamWriter;
use ferrumc_net::packets::outgoing::remove_entities::RemoveEntitiesPacket;
use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec;
use ferrumc_net_codec::net_types::var_int::VarInt;
use ferrumc_state::GlobalStateResource;
use tracing::{error, info};

/// System that checks for dead entities and despawns them
/// Works for all entity types (pigs, zombies, etc.)
pub fn entity_death_system(
mut commands: Commands,
entity_query: Query<(Entity, &EntityId, &EntityType, &Health)>,
player_query: Query<(Entity, &StreamWriter), With<PlayerIdentity>>,
mut entity_index: ResMut<EntityNetworkIdIndex>,
state: Res<GlobalStateResource>,
) {
for (entity, entity_id, entity_type, health) in entity_query.iter() {
if health.is_dead() {
info!(
"Entity {:?} (ID: {}) died (HP: {}/{}), despawning...",
entity_type,
entity_id.to_network_id(),
health.current,
health.max
);

// Send RemoveEntitiesPacket to all connected players
let network_id = entity_id.to_network_id();
let remove_packet = RemoveEntitiesPacket {
entity_ids: LengthPrefixedVec::new(vec![VarInt::new(network_id)]),
};

for (player_entity, stream_writer) in player_query.iter() {
// Check if player is still connected
if !state.0.players.is_connected(player_entity) {
continue;
}

if let Err(e) = stream_writer.send_packet_ref(&remove_packet) {
error!("Failed to send remove entity packet: {}", e);
}
}

// Remove from network ID index
entity_index.remove(network_id);

// Despawn the entity from ECS
commands.entity(entity).despawn();

info!("Entity {} successfully despawned", network_id);
}
}
}
Loading
Loading