From 1f4fec66a1e4c2148cc8ef59bf7adfb1cb77e3df Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Fri, 31 Jul 2026 00:34:12 -0500 Subject: [PATCH] ship editor feat: detaching works ish --- crates/unified/src/ship_editor/components.rs | 5 + crates/unified/src/ship_editor/detach.rs | 155 +++++++++++++++++++ crates/unified/src/ship_editor/input.rs | 15 +- crates/unified/src/ship_editor/mod.rs | 8 +- 4 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 crates/unified/src/ship_editor/detach.rs diff --git a/crates/unified/src/ship_editor/components.rs b/crates/unified/src/ship_editor/components.rs index d6a9f190ca6549f61f89d63a11ea9751d2771896..40fd94b520be1da081044f1cf320d97bf97299cb 100644 --- a/crates/unified/src/ship_editor/components.rs +++ b/crates/unified/src/ship_editor/components.rs @@ -14,6 +14,8 @@ pub struct GhostModule; #[derive(Component)] pub struct Part; #[derive(Component)] +pub struct Me; +#[derive(Component)] pub struct PartConfigHolder(pub PartConfig); #[derive(Component, Default)] pub struct Selectable { @@ -23,6 +25,9 @@ pub struct Selectable { #[derive(Resource, Default)] pub struct SelectedCount(pub usize); +#[derive(Component)] +pub struct PartInShip; // only one ship (yours) so no need to store entity + #[derive(Component, Default, Clone, Copy, ExtractComponent)] pub struct OutlineCamera; #[derive(Component)] diff --git a/crates/unified/src/ship_editor/detach.rs b/crates/unified/src/ship_editor/detach.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9722707ee7fbe7e90f363fad57e3aec850fd75b --- /dev/null +++ b/crates/unified/src/ship_editor/detach.rs @@ -0,0 +1,155 @@ +use crate::prelude::*; +use crate::shared::attachment::{JointOf, Joints, Peer}; +use crate::shared::config::part::PartConfig; +use crate::ship_editor::components::{Me, PartConfigHolder, PartInShip, Selectable}; + +pub fn detach_plugin(app: &mut App) { + app + .add_systems(Update, continue_detachment); +} + +#[derive(Component)] +struct PartiallyDisconnected; + +/// this method begins the detachment process +pub fn try_detach( + ev: On>, + mut q_joints: Query<&Joints>, + mut q_peer: Query<&Peer>, + mut q_joint_of: Query<&JointOf>, + mut q_is_hearty: Query<&Me>, + + mut q_dragged: Query<(&Selectable, &mut Sprite, &PartConfigHolder)>, + mut commands: Commands, + asset_server: Res, +) { + let Ok((selectable, mut sprite, part_config)) = q_dragged.get_mut(ev.entity) else { + return; + }; + if !selectable.is_selected { return } + debug!("detaching"); + // now that we know the module is selected, we can detach it + + // TEMPORARY BEHAVIOR + // upon dragging a part that will be detached, it will become dissociated from the ship. + // This means it loses its PartInShip component, and disconnected its Peer's. Other connected parts might then be disconnected. + // But, it's impossible to know this tick since there's a Peer connecting the parts. + // So, we need to add a component to flag part to be checked in the next tick. + // If these parts aren't attached anymore, remove their PartInShip and change the sprite to disconnected. + // But, these parts should still be connected to each other. + + // hard to detach a detached module, typically (well, this might do something different later) + if !is_connected_to_hearty(ev.entity, q_joints, q_peer, q_joint_of, q_is_hearty, &mut Vec::new()) { return } + + // here goes nothing + + // needs to happen first so we don't have half-detached a part + let Ok(joints) = q_joints.get(ev.entity) else { + warn!("part does not have a Joints? this should be impossible..."); + return; + }; + + // part no longer in ship + commands.entity(ev.entity).remove::(); + sprite.image = asset_server.load(part_config.0.part.sprite_disconnected.clone()); + + // remove all the Peer's of this part + for joint in joints.iter() { + commands.entity(joint).remove::(); + + // we also need to mark the nearby modules for detachment + let Ok(peer) = q_peer.get(joint) else { + continue + }; + + let other_part = q_joint_of.get(peer.peer_joint_entity_id).expect("Peer had invalid peer_joint_entity_id").0; + if is_connected_to_hearty(other_part, q_joints, q_peer, q_joint_of, q_is_hearty, &mut Vec::new()) { continue } + + commands.entity(other_part).insert(PartiallyDisconnected); + } +} + +/// this function will remove PartInShip and convert sprites to disconnected, but will not separate parts from each other +fn continue_detachment( + mut q_partially_disconnected: Query<(Entity, &PartConfigHolder, &mut Sprite), With>, + mut q_joints: Query<&Joints>, + mut q_peer: Query<&Peer>, + mut q_joint_of: Query<&JointOf>, + mut q_is_hearty: Query<&Me>, + mut commands: Commands, + mut asset_server: Res, +) { + for (part_entity, part_config, mut sprite) in q_partially_disconnected.iter_mut() { + if !is_connected_to_hearty(part_entity, q_joints, q_peer, q_joint_of, q_is_hearty, &mut Vec::new()) { return } + + let Ok(joints) = q_joints.get(part_entity) else { + warn!("part does not have a Joints? this should be impossible..."); + return; + }; + + commands.entity(part_entity).remove::(); + sprite.image = asset_server.load(part_config.0.part.sprite_disconnected.clone()); + + for joint in joints.iter() { + // we also need to mark the nearby modules for detachment + let Ok(peer) = q_peer.get(joint) else { + continue + }; + + let other_part = q_joint_of.get(peer.peer_joint_entity_id).expect("Peer had invalid peer_joint_entity_id").0; + if is_connected_to_hearty(other_part, q_joints, q_peer, q_joint_of, q_is_hearty, &mut Vec::new()) { continue } + + commands.entity(other_part).insert(PartiallyDisconnected); + } + } +} + +fn is_connected_to_hearty( + part: Entity, + + mut q_joints: Query<&Joints>, + mut q_peer: Query<&Peer>, + mut q_joint_of: Query<&JointOf>, + mut q_is_hearty: Query<&Me>, + + visited_joints: &mut Vec, +) -> bool { + if let Ok(_) = q_is_hearty.get(part) { + return true; + } + + let Ok(our_joints) = q_joints.get(part).cloned() else { + warn!("part does not have a Joints? this should be impossible..."); + return false; + }; + + for joint in our_joints.iter() { + visited_joints.push(joint); + + let Ok(peer) = q_peer.get(joint) else { + continue + }; + + if visited_joints.contains(&peer.peer_joint_entity_id) { + continue; + } + + let other_part = q_joint_of.get(peer.peer_joint_entity_id).expect("Peer had invalid peer_joint_entity_id").0; + + // here we detect hearty in the regular codebase. that is dumb. the first thing that happens in the function is finding hearty + + let other_connected = is_connected_to_hearty( + other_part, + q_joints.reborrow(), + q_peer.reborrow(), + q_joint_of.reborrow(), + q_is_hearty.reborrow(), + visited_joints, + ); + if other_connected { + return true; + } + } + + false +} \ No newline at end of file diff --git a/crates/unified/src/ship_editor/input.rs b/crates/unified/src/ship_editor/input.rs index 649c9d0e3f84e2156480c40f94a21cd36972a25e..1189170c3d7db5322bdc508d19b6703d3fe16440 100644 --- a/crates/unified/src/ship_editor/input.rs +++ b/crates/unified/src/ship_editor/input.rs @@ -7,8 +7,9 @@ use bevy::input_focus::InputFocus; use bevy::prelude::*; use bevy::window::PrimaryWindow; use crate::client::components::Me; -use crate::shared::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; -use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, OutlineCamera, Part, PartConfigHolder, Selectable, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; +use crate::shared::attachment::{Joint, JointOf, Joints, Peer, SnapOf, SnapOfJoint}; +use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, OutlineCamera, Part, PartConfigHolder, PartInShip, Selectable, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; +use crate::ship_editor::detach::try_detach; use crate::ship_editor::select::click_select; use crate::ship_editor::spawn_joints; use crate::ship_editor::ui::PartEntry; @@ -96,7 +97,6 @@ fn on_click( mut parts: Query< ( &mut Transform, - Option<&PartInShip>, Entity, &Joints, &Part, @@ -142,7 +142,7 @@ fn on_click( } // target entity (the one being attached to) - let Ok((target_xform, target_in_ship, target_entity, _, _)) = parts.get(target_jt_of.0) else { + let Ok((target_xform, target_entity, _, _)) = parts.get(target_jt_of.0) else { return; }; @@ -153,7 +153,7 @@ fn on_click( commands.entity(source_jt_id).insert(Peer { peer_joint_entity_id: target_jt_id, - physics_joint: Entity::PLACEHOLDER, // reusing components and physics joint is not used in the ship editor + physics_joint: Entity::PLACEHOLDER, // reusing components, and physics joint is not used in the ship editor }); commands.entity(target_jt_id).insert(Peer { peer_joint_entity_id: source_jt_id, @@ -170,11 +170,12 @@ fn on_click( ghost_sprite.color = Color::srgb(1.0, 1.0, 1.0); commands.entity(ghost_entity) .insert(Part) + .insert(PartInShip) .insert(MAIN_RENDER_LAYER) .insert(Pickable::default()) .insert(Selectable::default()) .observe(click_select) - // .remove::() + .observe(try_detach) .remove::(); } } @@ -194,7 +195,7 @@ fn camera_drag( let main_transform = &mut main_camera.0; let delta = (drag.init_cursor_pos - cursor.with_y(-cursor.y)).extend(0.0); - if delta.length() > DRAG_SELECT_CUTOFF { + if delta.length() > DRAG_SELECT_CUTOFF { drag.can_select = false; } main_transform.translation = drag.init_camera_pos.extend(0.0) + delta*projection.scale; diff --git a/crates/unified/src/ship_editor/mod.rs b/crates/unified/src/ship_editor/mod.rs index 9d4826b943c57a06133b0c6424c4c29ddf7e06a9..ad660952eba63a7a64310dc0b975fdfad62d2120 100644 --- a/crates/unified/src/ship_editor/mod.rs +++ b/crates/unified/src/ship_editor/mod.rs @@ -3,6 +3,7 @@ pub mod input; pub mod plugins; pub mod components; pub mod select; +pub mod detach; use bevy::input_focus::InputFocus; use crate::client::colors; @@ -10,7 +11,8 @@ use crate::prelude::*; use crate::shared::attachment::{Joint, JointId, JointOf, SnapOf, SnapOfJoint}; use crate::shared::config::part::{JointConfig, PartConfig}; use crate::shared::config::ship_editor::ShipEditorConfig; -use crate::ship_editor::components::{GhostCamera, MainCamera, OutlineCamera, Part, PartConfigHolder, PlayerPartRequest, Selectable, SelectedCount, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER, OUTLINE_RENDER_LAYER}; +use crate::ship_editor::components::{GhostCamera, MainCamera, Me, OutlineCamera, Part, PartConfigHolder, PlayerPartRequest, Selectable, SelectedCount, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER, OUTLINE_RENDER_LAYER}; +use crate::ship_editor::detach::detach_plugin; use crate::ship_editor::input::input_plugin; use crate::ship_editor::select::click_select; use crate::ship_editor::ui::{ui_plugin, PendingPart}; @@ -25,7 +27,8 @@ impl Plugin for ShipEditorPlugin { .add_systems(Startup, setup) .add_systems(Update, (spawn_player_part_request, spawn_parts)) .add_plugins(input_plugin) - .add_plugins(ui_plugin); + .add_plugins(ui_plugin) + .add_plugins(detach_plugin); } } @@ -71,6 +74,7 @@ fn setup( let rectangle = meshes.add(Rectangle::new(50.0, 50.0)); commands.spawn(( PlayerPartRequest, + Me, MAIN_RENDER_LAYER, Transform::from_xyz(0.0, 0.0, 0.0), ));