use crate::config::planet::Planet; use crate::ecs::{DragRequestEvent, Part, Player, PlayerStorage, PlayerThrust, ThrustEvent}; use crate::server::part::SpawnPartRequest; use crate::server::system_sets::PlayerInputSet; use crate::server::world_config::WorldConfigResource; use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity}; use bevy::prelude::*; use bevy_rapier2d::prelude::{ExternalForce, FixedJointBuilder, ImpulseJoint, Velocity}; use bevy_replicon::prelude::FromClient; use std::f32::consts::PI; use crate::attachment::{Joint, JointOf, PartInShip, Peer, SnapOf, SnapOfJoint}; pub fn player_management_plugin(app: &mut App) { app.add_systems( Update, (handle_new_players, player_thrust, magic_fuel_regen, dragging).in_set(PlayerInputSet), ); } fn dragging( mut events: EventReader>, mut parts: Query<(&mut Transform, Option<&PartInShip>, Entity, &mut Velocity), (With, Without)>, snaps: Query<(&SnapOf, &SnapOfJoint)>, joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>, clients: Query<&ConnectedNetworkEntity>, mut commands: Commands ) { for FromClient { event, client_entity } in events.read() { let ConnectedNetworkEntity { game_entity: player_hearty_entity } = clients.get(*client_entity).unwrap(); debug!(?event, "got drag request event"); let mut teleport_to_translation = Vec2::new(0.0, 0.0); let mut teleport_to_rotation = Quat::from_rotation_z(0.0); let mut new_vel = None; if let Some(snap_to) = event.snap_target && let Some(peer_snap) = event.peer_snap { let Ok(snap_on_target) = snaps.get(snap_to) else { continue }; let Ok(snap_on_source) = snaps.get(peer_snap) else { continue }; let Ok(target_joint) = joints.get(snap_on_target.1.0) else { continue }; let Ok(source_joint) = joints.get(snap_on_source.1.0) else { continue }; // validation step 1: everything must match. if not, ignore the request if snap_on_target.0.0 != target_joint.1.0 { warn!("drag request: mismatched target entities (potential manipulation?), ignoring"); continue; } if snap_on_source.0.0 != source_joint.1.0 { warn!("drag request: mismatched source entities (potential manipulation?), ignoring request"); continue; } // we've passed initial validation. // do not allow drags with the source or destination if they already have a peer (are attached) if target_joint.3.is_some() { warn!("drag request: cannot attach to a joint that already has a peer, ignoring request"); continue; } if source_joint.3.is_some() { warn!("drag request: dragging from a part that is already attached is currently not supported, ignoring request"); continue; } // great, the attachment appears to be valid // let's make sure this player is allowed to drag onto this part let target_part = { let Ok(target_part) = parts.get(target_joint.1.0) else { continue }; target_part.clone() }; let source_part = { let Ok(source_part) = parts.get(source_joint.1.0) else { continue }; source_part.clone() }; let allowed = target_joint.1.0 == *player_hearty_entity || target_part.1.is_some_and(|u| u.0 == *player_hearty_entity); if !allowed { warn!("drag request: this player cannot move this part, ignoring request"); continue; } // TODO - validate source_part? // great, we have a valid peering request // create the peering component... commands.entity(source_joint.4) .insert(Peer(target_joint.4)); commands.entity(target_joint.4) .insert(Peer(source_joint.4)); // propagate PartInShip... let part_in_ship = if target_joint.1.0 == *player_hearty_entity { PartInShip(*player_hearty_entity) } else { PartInShip(target_part.1.unwrap().0) // unwrap: checked above (during 'allowed' calculation) }; commands.entity(source_part.2) .insert(part_in_ship); let target_position = target_part.0.mul_transform(*target_joint.2); let rotation = event.set_rotation.to_scaled_axis().z; let rotation = Rot2::radians(rotation); // create the joint... let joint = FixedJointBuilder::new() .local_anchor1(target_joint.2.translation.xy()) .local_basis2(0.0); commands.entity(source_part.2) .insert(ImpulseJoint::new(target_part.2, joint)); teleport_to_translation = target_position.translation.xy(); teleport_to_rotation = event.set_rotation; new_vel = Some(*target_part.3); // and we're done! } else { warn!("blindly accepting non-attachment request, someone should change this eventually"); warn!("dragging already attached entities may cause inconsistent behavior!!"); teleport_to_translation = event.drag_to; teleport_to_rotation = event.set_rotation; } let mut part = parts.get_mut(event.drag_target).unwrap(); part.0.translation.x = teleport_to_translation.x; part.0.translation.y = teleport_to_translation.y; part.0.rotation = teleport_to_rotation; // client calculates this; no reason to recalculate if let Some(new_vel) = new_vel { *part.3 = new_vel; } // ( the math sucks ) } } fn handle_new_players( mut commands: Commands, q_new_clients: Query>, world_config: Res, planets: Query<(&Transform, &Planet)>, asset_server: Res, ) { let Some(wc) = &world_config.config else { return; }; for joined_player in &q_new_clients { trace!(?joined_player, "detected joined player!"); // find earth let (spawn_planet_pos, spawn_planet) = planets .iter() .find(|p| p.1.name == wc.hearty.spawn_at) .unwrap_or_else(|| { panic!( "spawn planet {} is missing? (check that the planet is named exactly '{}')", wc.hearty.spawn_at, wc.hearty.spawn_at ) }); let angle = rand::random::() * std::f32::consts::TAU; let offset = spawn_planet.radius + 150.0; let mut new_transform = Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0); new_transform.rotate_z(angle); new_transform.translation += spawn_planet_pos.translation; info!(?new_transform, ?joined_player, "set player's position!"); commands .entity(joined_player) .insert(new_transform) .insert(SpawnPartRequest( asset_server.load("config/parts/hearty.part.toml"), )) .insert(PlayerThrust::default()) .insert(PlayerStorage { fuel_capacity: 25.0, fuel: 25.0, power_capacity: 25.0, power: 25.0, }) .insert(Velocity::default()) .insert(Player { client: joined_player, }); } } fn magic_fuel_regen( mut players: Query<&mut PlayerStorage, With>, time: Res