use crate::client::network::*; use crate::protocol::*; use crate::shared::ability::*; use crate::shared::activation::*; use crate::shared::area_of_effect::*; use crate::shared::buffs::*; use crate::shared::champion::*; use crate::shared::cooldown::*; use crate::shared::health::*; use crate::shared::health_event::*; use crate::shared::imperative::*; use crate::shared::player::*; use crate::shared::projectile::*; use crate::shared::stats::*; use crate::shared::*; use bevy::input::keyboard::*; use bevy::input::mouse::MouseButton; use bevy::prelude::*; use bevy::sprite::{MaterialMesh2dBundle, Mesh2dHandle}; use lightyear::client::events::*; use lightyear::client::input::InputSystemSet; use lightyear::prelude::*; use std::net::SocketAddr; mod network; 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_area_of_effects, render_health, render_buffs, ), ) .add_systems( Update, ( (hotbar_hotbar_display, hotbar_hotbar_highlight), hotbar_cooldown, ( health_indicator_despawn, health_indicator_tick, health_indicator_move_up, health_indicator_spawn, ) .chain(), ), ) .add_systems( FixedPreUpdate, buffer_input.in_set(InputSystemSet::BufferInputs), ) .add_systems(Last, (gizmos_hover_indicator, gizmos_attack_indicator)); } } #[derive(Component, PartialEq, Eq, Debug)] pub struct Hotbar(AbilitySlot); #[derive(Component, PartialEq, Eq, Debug)] pub struct HotbarCooldown(AbilitySlot); fn setup(mut client: ClientMut, mut commands: Commands, champion: Res) { commands.spawn(Camera2dBundle::default()); commands .spawn(NodeBundle { style: Style { position_type: PositionType::Absolute, bottom: Val::Px(4.), width: Val::Percent(100.), flex_direction: FlexDirection::Row, justify_content: JustifyContent::Center, column_gap: Val::Px(4.), ..Default::default() }, ..Default::default() }) .with_children(|builder| { let mut hotkey = |ability_slot: AbilitySlot| { let label = ability_slot.to_label(); let hotbar = Hotbar(ability_slot); let hotbar_cooldown = HotbarCooldown(ability_slot); builder .spawn(NodeBundle { style: Style { align_items: AlignItems::Center, justify_content: JustifyContent::Center, width: Val::Px(24.), height: Val::Px(24.), border: UiRect { top: Val::Px(0.5), right: Val::Px(0.5), bottom: Val::Px(0.5), left: Val::Px(0.5), }, ..Default::default() }, //background_color: BackgroundColor(Color::GREEN), border_color: BorderColor(Color::WHITE), ..Default::default() }) .with_children(|builder| { builder.spawn(( TextBundle::from_section( label, TextStyle { font_size: 12., color: Color::GRAY, ..Default::default() }, ), hotbar, )); builder.spawn(( TextBundle::from_section( "0.00", TextStyle { font_size: 10., color: Color::GRAY, ..Default::default() }, ) .with_style(Style { display: Display::None, ..Default::default() }), hotbar_cooldown, )); }); }; for ability_slot in AbilitySlot::all() { hotkey(ability_slot); } }); 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, &Projectile), Added>, ) { for (entity, projectile) in projectiles.iter() { let Some(position) = (match projectile.type_ { ProjectileType::Free(FreeProjectile { position, .. }) => Some(position), ProjectileType::Instant(InstantProjectile { .. }) => None, ProjectileType::Targeted(TargetedProjectile { position, .. }) => Some(position), }) else { continue; }; commands.entity(entity).insert(MaterialMesh2dBundle { mesh: Mesh2dHandle(meshes.add(Circle { radius: 2. })), material: materials.add(Color::RED), transform: Transform::from_xyz(position.x, position.y, 1.), ..Default::default() }); } } fn render_area_of_effects( mut commands: Commands, mut materials: ResMut>, mut meshes: ResMut>, area_of_effects: Query<(Entity, &AreaOfEffect), Added>, ) { for (entity, area_of_effect) in area_of_effects.iter() { if area_of_effect.duration.is_none() { continue; } commands.entity(entity).insert(MaterialMesh2dBundle { mesh: Mesh2dHandle(meshes.add(Circle { radius: area_of_effect.radius, })), material: materials.add(Color::BLACK), transform: Transform::from_xyz( area_of_effect.position.x, area_of_effect.position.y, 1., ), ..Default::default() }); } } fn move_projectiles(mut projectiles: Query<(&mut Transform, &Projectile), Changed>) { for (mut transform, projectile) in projectiles.iter_mut() { let Some(position) = (match projectile.type_ { ProjectileType::Free(FreeProjectile { position, .. }) => Some(position), ProjectileType::Instant(InstantProjectile { .. }) => None, ProjectileType::Targeted(TargetedProjectile { position, .. }) => Some(position), }) else { continue; }; transform.translation = Vec3::new(position.x, position.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( players: Query<(&PlayerId, &PlayerPosition)>, mut attack: ResMut, cameras: Query<(&Camera, &GlobalTransform)>, client_id: Res, mouse_input: Res>, mut client: ClientMut, windows: Query<&Window>, champion: Res, ) { if mouse_input.just_pressed(MouseButton::Left) { match attack.0 { Some(ability_slot) => match champion.0.ability(ability_slot) { Ability::Activated(_) => {} Ability::Directional(_) => { let Some(world_position) = cursor_world_position(&windows, &cameras) else { return; }; let Some(position) = player_position(&client_id, &players) else { return; }; let Some(direction) = (world_position - position.0).try_normalize() else { return; }; client.add_input(Inputs::Imperative(Imperative::AttackDirection( ability_slot, direction, ))); attack.0 = None; } Ability::Targeted(_) => { let Some((target_player, _)) = hovered_other_player(&cameras, &client_id, &players, &windows) else { return; }; client.add_input(Inputs::Imperative(Imperative::AttackTarget( ability_slot, target_player, ))); attack.0 = None; } }, None => { if let Some((target_player, _)) = hovered_other_player(&cameras, &client_id, &players, &windows) { client.add_input(Inputs::Imperative(Imperative::AttackTarget( AbilitySlot::A, target_player, ))); } else { if let Some(world_position) = cursor_world_position(&windows, &cameras) { client.add_input(Inputs::Imperative(Imperative::WalkTo(world_position))); } } } } } else { client.add_input(Inputs::None); } } fn choose_attack( champion: Res, keyboard_input: Res>, mut attack: ResMut, mut client: ClientMut, ) { if keyboard_input.just_pressed(KeyCode::KeyA) { attack.0 = Some(AbilitySlot::A); } else if keyboard_input.just_pressed(KeyCode::KeyQ) { attack.0 = Some(AbilitySlot::Q); } else if keyboard_input.just_pressed(KeyCode::KeyW) { attack.0 = Some(AbilitySlot::W); } else if keyboard_input.just_pressed(KeyCode::KeyE) { attack.0 = Some(AbilitySlot::E); } else if keyboard_input.just_pressed(KeyCode::KeyR) { attack.0 = Some(AbilitySlot::R); } else if keyboard_input.just_pressed(KeyCode::KeyF) { attack.0 = Some(AbilitySlot::F); } else if keyboard_input.just_pressed(KeyCode::KeyG) { attack.0 = Some(AbilitySlot::G); } else if keyboard_input.just_pressed(KeyCode::Escape) { attack.0 = None; } else if keyboard_input.just_pressed(KeyCode::ShiftLeft) { attack.0 = None; } match attack.0 { Some(ability_slot) => match champion.0.ability(ability_slot) { Ability::Activated(_) => { client.add_input(Inputs::Activation(Activation::Activate(ability_slot))); attack.0 = None; } _ => {} }, 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 world_to_viewport(cameras: &Query<(&Camera, &GlobalTransform)>, position: Vec2) -> Option { let (camera, camera_transform) = cameras.single(); camera.world_to_viewport(camera_transform, Vec3::new(position.x, position.y, 0.)) } 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( attack: Res, cameras: Query<(&Camera, &GlobalTransform)>, client_id: Res, effective_statses: Query<(&PlayerId, &EffectiveStats)>, hoverables: Query<(&PlayerId, &PlayerPosition)>, mut gizmos: Gizmos, player_champions: Query<(&PlayerId, &Champion)>, player_positions: Query<(&PlayerId, &PlayerPosition)>, 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; }; let Some(ability_slot) = attack.0.or_else(|| { hovered_other_player(&cameras, &client_id, &hoverables, &windows).map(|_| AbilitySlot::A) }) else { return; }; let Some(effective_stats) = player_effective_stats(&client_id, &effective_statses) else { return; }; match champion.ability(ability_slot) { Ability::Activated(_) => {} Ability::Directional(ability) => { let Some(world_position) = cursor_world_position(&windows, &cameras) else { return; }; let Some(direction) = (world_position - position.0).try_normalize() else { return; }; match ability { DirectionalAbility::Dash(Dash { max_distance, .. }) => { let DashCollisionResult { dash_end, .. } = dash_collision( PlayerId(client_id.0), position.0, direction, max_distance, &player_positions, ); gizmos.arrow_2d(position.0, dash_end, Color::YELLOW); } DirectionalAbility::Pull(Pull { max_distance, .. }) => { let Some(PullCollision { pull_end, collision_position, .. }) = pull_collision( PlayerId(client_id.0), position.0, direction, max_distance, &player_positions, ) else { let pull_direction = -max_distance * direction; let pull_start = position.0 - pull_direction; let pull_end = pull_start + (pull_direction.length() - 2. * PLAYER_RADIUS) * pull_direction.normalize_or_zero(); gizmos.arrow_2d(pull_start, pull_end, Color::YELLOW); return; }; gizmos.arrow_2d(collision_position, pull_end, Color::YELLOW); } DirectionalAbility::Spear(Spear { max_distance, .. }) => { gizmos.arrow_2d( position.0, position.0 + max_distance * direction, Color::YELLOW, ); } } } Ability::Targeted(_) => { gizmos.circle_2d(position.0, effective_stats.0.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_effective_stats( client_id: &Res, players: &Query<(&PlayerId, &EffectiveStats)>, ) -> Option { for (id, effective_stats) in players.iter() { if id.0 == client_id.0 { return Some(*effective_stats); } } 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, &EffectiveStats)>, mut gizmos: Gizmos) { for (health, position, effective_stats) 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); let health_start = start; let health_end = start.lerp(end, health.health / effective_stats.0.max_health); let px_per_health = (end.x - start.x) / effective_stats.0.max_health; let shield_start = health_end; let shield_end = health_end + Vec2::new(health.shield * px_per_health, 0.); gizmos.line_2d(health_start, health_end, Color::RED); gizmos.line_2d(shield_start, shield_end, Color::GRAY); } } fn render_buffs(players: Query<(&Buffs, &PlayerPosition)>, mut gizmos: Gizmos) { for (buffs, position) in players.iter() { let mut start = position.0 + Vec2::new(-PLAYER_RADIUS + 1., PLAYER_RADIUS + HEALTH_OFFSET + 2.); if buffs.haste.is_some() { gizmos.rect_2d(start, 0., Vec2::new(2., 2.), Color::RED); start = start + Vec2::new(3., 0.); } if buffs.shield.is_some() { gizmos.rect_2d(start, 0., Vec2::new(2., 2.), Color::GRAY); start = start + Vec2::new(3., 0.); } if buffs.slow.is_some() { gizmos.rect_2d(start, 0., Vec2::new(2., 2.), Color::BLUE); start = start + Vec2::new(3., 0.); } if buffs.speed.is_some() { gizmos.rect_2d(start, 0., Vec2::new(2., 2.), Color::GREEN); //start = start + Vec2::new(3., 0.); } } } fn hotbar_cooldown( client_id: Res, cooldowns: Query<(&PlayerId, &Cooldown)>, mut hotbar_cooldowns: Query<(&HotbarCooldown, &mut Text, &mut Style)>, ) { let Some(cooldown) = player_cooldown(&client_id, &cooldowns) else { return; }; for (hotbar_cooldown, mut text, mut style) in hotbar_cooldowns.iter_mut() { let cooldown = cooldown.0[hotbar_cooldown.0].as_secs_f32(); if text.sections.len() <= 0 { continue; } if cooldown <= 0. { style.display = Display::None; } else { style.display = Display::Flex; text.sections[0].value = format!("{cooldown:.1}"); } } } fn player_cooldown( client_id: &Res, players: &Query<(&PlayerId, &Cooldown)>, ) -> Option { for (id, cooldown) in players.iter() { if id.0 == client_id.0 { return Some(*cooldown); } } None } fn hotbar_hotbar_display( client_id: Res, cooldowns: Query<(&PlayerId, &Cooldown)>, mut hotbars: Query<(&Hotbar, &mut Style)>, ) { let Some(cooldown) = player_cooldown(&client_id, &cooldowns) else { return; }; for (hotbar, mut style) in hotbars.iter_mut() { if cooldown.0[hotbar.0].as_secs_f32() <= 0. { style.display = Display::Flex; } else { style.display = Display::None; } } } fn hotbar_hotbar_highlight(attack: Res, mut hotbars: Query<(&Hotbar, &mut Text)>) { for (hotbar, mut text) in hotbars.iter_mut() { let is_highlighted = attack.0 == Some(hotbar.0); if text.sections.len() <= 0 { continue; } if is_highlighted { text.sections[0].style.color = Color::WHITE; } else { text.sections[0].style.color = Color::GRAY; } } } #[derive(Component)] pub struct HealthIndicator { position: Vec2, timer: Timer, } fn health_indicator_spawn( cameras: Query<(&Camera, &GlobalTransform)>, mut commands: Commands, mut event_reader: EventReader>, players: Query<(&PlayerId, &PlayerPosition)>, ) { for event in event_reader.read() { let HealthChanged(HealthEvent { target_player, health_gained, }) = event.message(); let Some(position) = any_player_position(*target_player, &players) else { continue; }; let health_gained_or_lost = health_gained.abs(); let Some(mut screen_position) = world_to_viewport(&cameras, position.0) else { continue; }; screen_position += Vec2::new(12.5, -12.5); commands.spawn(( TextBundle::from_section( format!("{health_gained_or_lost}"), TextStyle { font_size: 6., color: if *health_gained < 0. { Color::RED } else { Color::GREEN }, ..Default::default() }, ) .with_style(Style { position_type: PositionType::Absolute, top: Val::Px(screen_position.y), left: Val::Px(screen_position.x), ..Default::default() }), HealthIndicator { position: position.0, timer: Timer::from_seconds(1., TimerMode::Once), }, )); } } fn any_player_position( player_id: PlayerId, players: &Query<(&PlayerId, &PlayerPosition)>, ) -> Option { for (id, position) in players.iter() { if *id == player_id { return Some(*position); } } None } fn health_indicator_move_up( cameras: Query<(&Camera, &GlobalTransform)>, mut health_indicators: Query<(&mut Style, &HealthIndicator)>, ) { for (mut style, health_indicator) in health_indicators.iter_mut() { let Some(mut screen_position) = world_to_viewport(&cameras, health_indicator.position) else { continue; }; screen_position += Vec2::new(12.5, -12.5); let s = health_indicator.timer.fraction(); style.top = Val::Px((1. - s) * screen_position.y + s * (screen_position.y - 20.)); style.left = Val::Px(screen_position.x); } } fn health_indicator_tick(mut health_indicators: Query<&mut HealthIndicator>, time: Res