#![allow(clippy::type_complexity)] use bevy_ecs::query::WorldQuery; use rand::Rng; use valence::entity::EntityStatuses; use valence::math::Vec3Swizzles; use valence::prelude::*; const SPAWN_Y: i32 = 64; const ARENA_RADIUS: i32 = 32; /// Attached to every client. #[derive(Component)] struct CombatState { /// The tick the client was last attacked. last_attacked_tick: i64, has_bonus_knockback: bool, } pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(EventLoopUpdate, handle_combat_events) .add_systems( Update, ( init_clients, despawn_disconnected_clients, teleport_oob_clients, ), ) .run(); } fn setup( mut commands: Commands, server: Res, dimensions: Res, biomes: Res, ) { let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } let mut rng = rand::thread_rng(); // Create circular arena. for z in -ARENA_RADIUS..ARENA_RADIUS { for x in -ARENA_RADIUS..ARENA_RADIUS { let dist = f64::hypot(x as _, z as _) / ARENA_RADIUS as f64; if dist > 1.0 { continue; } let block = if rng.gen::() < dist { BlockState::STONE } else { BlockState::DEEPSLATE }; for y in 0..SPAWN_Y { layer.chunk.set_block([x, y, z], block); } } } commands.spawn(layer); } fn init_clients( mut clients: Query< ( &mut EntityLayerId, &mut VisibleChunkLayer, &mut VisibleEntityLayers, &mut Position, &mut GameMode, ), Added, >, layers: Query, With)>, ) { for ( mut layer_id, mut visible_chunk_layer, mut visible_entity_layers, mut pos, mut game_mode, ) in &mut clients { let layer = layers.single(); layer_id.0 = layer; visible_chunk_layer.0 = layer; visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } } #[derive(WorldQuery)] #[world_query(mutable)] struct CombatQuery { client: &'static mut Client, pos: &'static Position, state: &'static mut CombatState, statuses: &'static mut EntityStatuses, } fn handle_combat_events( server: Res, mut clients: Query, mut sprinting: EventReader, mut interact_entity: EventReader, ) { for &SprintEvent { client, state } in sprinting.iter() { if let Ok(mut client) = clients.get_mut(client) { client.state.has_bonus_knockback = state == SprintState::Start; } } for &InteractEntityEvent { client: attacker_client, entity: victim_client, .. } in interact_entity.iter() { let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) else { // Victim or attacker does not exist, or the attacker is attacking itself. continue; }; if server.current_tick() - victim.state.last_attacked_tick < 10 { // Victim is still on attack cooldown. continue; } victim.state.last_attacked_tick = server.current_tick(); let victim_pos = victim.pos.0.xz(); let attacker_pos = attacker.pos.0.xz(); let dir = (victim_pos - attacker_pos).normalize().as_vec2(); let knockback_xz = if attacker.state.has_bonus_knockback { 18.0 } else { 8.0 }; let knockback_y = if attacker.state.has_bonus_knockback { 8.432 } else { 6.432 }; victim .client .set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]); attacker.state.has_bonus_knockback = false; victim.client.trigger_status(EntityStatus::PlayAttackSound); victim.statuses.trigger(EntityStatus::PlayAttackSound); } } fn teleport_oob_clients(mut clients: Query<&mut Position, With>) { for mut pos in &mut clients { if pos.0.y < 0.0 { pos.set([0.0, SPAWN_Y as _, 0.0]); } } }