use crate::client::network::*; use crate::protocol::*; use crate::shared::champion::*; use crate::shared::health::*; use crate::shared::imperative::*; use crate::shared::projectile::*; use crate::shared::*; use bevy::input::keyboard::*; use bevy::input::mouse::MouseButton; use bevy::prelude::*; use bevy::sprite::{MaterialMesh2dBundle, Mesh2dHandle}; use lightyear::client::input::InputSystemSet; use lightyear::prelude::*; use std::net::SocketAddr; mod network; const PLAYER_RADIUS: f32 = 10.; const PLAYER_HOVER_INDICATOR_RADIUS: f32 = 13.; const PLAYER_HOVER_RADIUS: f32 = 20.; pub fn main( server_addr: Option, client_id: u64, transport: TransportConfig, champion: Champion, ) { App::new() .add_plugins(DefaultPlugins) .add_plugins(ClientPlugin { server_addr, client_id, transport, champion, }) .run(); } #[derive(Resource)] struct ClientId(pub u64); #[derive(Resource)] struct MyChampion(pub Champion); #[derive(Resource)] struct Attack(Option); struct ClientPlugin { pub server_addr: Option, pub client_id: u64, pub transport: TransportConfig, pub champion: Champion, } impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { app.insert_resource(ClientId(self.client_id)) .insert_resource(MyChampion(self.champion)) .insert_resource(Attack(None)) .add_plugins(NetworkPlugin { server_addr: self.server_addr.clone(), client_id: self.client_id, transport: self.transport.clone(), }) .add_systems(Startup, setup) .add_systems(PreUpdate, choose_attack) .add_systems(Update, (move_players, move_projectiles)) .add_systems( Update, ( render_players.after(move_players), render_projectiles.after(move_projectiles), render_health, ), ) .add_systems( FixedPreUpdate, buffer_input.in_set(InputSystemSet::BufferInputs), ) .add_systems(Last, (gizmos_hover_indicator, gizmos_attack_indicator)); } } fn setup(mut client: ClientMut, mut commands: Commands, champion: Res) { commands.spawn(Camera2dBundle::default()); client.connect().unwrap(); client .send_message::(SelectChampion(champion.0)) .unwrap(); } fn render_players( mut commands: Commands, mut materials: ResMut>, mut meshes: ResMut>, players: Query<(Entity, &PlayerPosition, &PlayerColor), Added>, ) { for (entity, position, color) in players.iter() { commands.entity(entity).insert(MaterialMesh2dBundle { mesh: Mesh2dHandle(meshes.add(Circle { radius: PLAYER_RADIUS, })), material: materials.add(color.0), transform: Transform::from_xyz(position.0.x, position.0.y, 0.), ..Default::default() }); } } fn render_projectiles( mut commands: Commands, mut materials: ResMut>, mut meshes: ResMut>, projectiles: Query<(Entity, &ProjectilePosition), Added>, ) { for (entity, position) in projectiles.iter() { commands.entity(entity).insert(MaterialMesh2dBundle { mesh: Mesh2dHandle(meshes.add(Circle { radius: 2. })), material: materials.add(Color::RED), transform: Transform::from_xyz(position.0.x, position.0.y, 1.), ..Default::default() }); } } fn move_projectiles( mut projectiles: Query<(&mut Transform, &ProjectilePosition), Changed>, ) { for (mut transform, projectile_position) in projectiles.iter_mut() { transform.translation = Vec3::new(projectile_position.0.x, projectile_position.0.y, 0.); } } fn move_players(mut players: Query<(&mut Transform, &PlayerPosition), Changed>) { for (mut transform, player_position) in players.iter_mut() { transform.translation = Vec3::new(player_position.0.x, player_position.0.y, 0.); } } fn buffer_input( attackables: Query<(&PlayerId, &PlayerPosition)>, cameras: Query<(&Camera, &GlobalTransform)>, client_id: Res, mouse_input: Res>, mut client: ClientMut, windows: Query<&Window>, attack: Res, ) { if mouse_input.just_pressed(MouseButton::Left) { if let Some((target_player, _)) = hovered_other_player(&cameras, &client_id, &attackables, &windows) { client.add_input(Inputs::Imperative(Imperative::Attack( attack.0.unwrap_or(AttackKey::A), target_player, ))); } else { if let Some(world_position) = cursor_world_position(&windows, &cameras) { client.add_input(Inputs::Imperative(Imperative::WalkTo(world_position))); } } } } fn choose_attack(keyboard_input: Res>, mut attack_key: ResMut) { if keyboard_input.pressed(KeyCode::KeyA) { attack_key.0 = Some(AttackKey::A); } else if keyboard_input.pressed(KeyCode::KeyQ) { attack_key.0 = Some(AttackKey::Q); } else if keyboard_input.pressed(KeyCode::KeyW) { attack_key.0 = Some(AttackKey::W); } else if keyboard_input.pressed(KeyCode::KeyE) { attack_key.0 = Some(AttackKey::E); } else if keyboard_input.pressed(KeyCode::KeyR) { attack_key.0 = Some(AttackKey::R); } else if keyboard_input.pressed(KeyCode::KeyD) { attack_key.0 = Some(AttackKey::D); } else if keyboard_input.pressed(KeyCode::KeyF) { attack_key.0 = Some(AttackKey::F); } else if keyboard_input.pressed(KeyCode::Escape) { attack_key.0 = None; } } fn gizmos_hover_indicator( cameras: Query<(&Camera, &GlobalTransform)>, client_id: Res, hoverables: Query<(&PlayerId, &PlayerPosition)>, mut gizmos: Gizmos, windows: Query<&Window>, ) { let Some((_, position)) = hovered_other_player(&cameras, &client_id, &hoverables, &windows) else { return; }; gizmos.circle_2d(position.0, PLAYER_HOVER_INDICATOR_RADIUS, Color::GREEN); } fn cursor_world_position( windows: &Query<&Window>, cameras: &Query<(&Camera, &GlobalTransform)>, ) -> Option { let window = windows.single(); let (camera, camera_transform) = cameras.single(); let Some(cursor_position) = window.cursor_position() else { return None; }; camera.viewport_to_world_2d(camera_transform, cursor_position) } fn hovered_other_player( cameras: &Query<(&Camera, &GlobalTransform)>, client_id: &Res, hoverables: &Query<(&PlayerId, &PlayerPosition)>, windows: &Query<&Window>, ) -> Option<(PlayerId, PlayerPosition)> { let Some(world_position) = cursor_world_position(&windows, &cameras) else { return None; }; let mut hovered_player = None; let mut hovered_distance = None; for (id, position) in hoverables.iter() { if id.0 == client_id.0 { continue; } let distance = position.0.distance(world_position); if distance < PLAYER_HOVER_RADIUS { if hovered_distance.map_or(true, |hovered_distance| distance < hovered_distance) { hovered_player = Some((*id, *position)); hovered_distance = Some(distance); } } } hovered_player } fn gizmos_attack_indicator( cameras: Query<(&Camera, &GlobalTransform)>, client_id: Res, hoverables: Query<(&PlayerId, &PlayerPosition)>, mut gizmos: Gizmos, player_positions: Query<(&PlayerId, &PlayerPosition)>, player_champions: Query<(&PlayerId, &Champion)>, windows: Query<&Window>, ) { let Some(position) = player_position(&client_id, &player_positions) else { return; }; let Some(champion) = player_champion(&client_id, &player_champions) else { return; }; if hovered_other_player(&cameras, &client_id, &hoverables, &windows).is_some() { gizmos.circle_2d( position.0, Stats::from_champion(champion).attack_range, Color::YELLOW, ); } } fn player_position( client_id: &Res, players: &Query<(&PlayerId, &PlayerPosition)>, ) -> Option { for (id, position) in players.iter() { if id.0 == client_id.0 { return Some(*position); } } None } fn player_champion( client_id: &Res, players: &Query<(&PlayerId, &Champion)>, ) -> Option { for (id, champion) in players.iter() { if id.0 == client_id.0 { return Some(*champion); } } None } const HEALTH_OFFSET: f32 = 4.; fn render_health(players: Query<(&Health, &PlayerPosition)>, mut gizmos: Gizmos) { for (health, position) in players.iter() { let start = position.0 + Vec2::new(-PLAYER_RADIUS, PLAYER_RADIUS + HEALTH_OFFSET); let end = position.0 + Vec2::new(PLAYER_RADIUS, PLAYER_RADIUS + HEALTH_OFFSET); gizmos.line_2d(start, start.lerp(end, health.0 / MAX_HEALTH), Color::RED); } }