M crates/unified/src/ship_editor/components.rs => crates/unified/src/ship_editor/components.rs +5 -0
@@ 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)]
A crates/unified/src/ship_editor/detach.rs => crates/unified/src/ship_editor/detach.rs +155 -0
@@ 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<Pointer<DragStart>>,
+ 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<AssetServer>,
+) {
+ 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::<PartInShip>();
+ 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::<Peer>();
+
+ // 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<PartiallyDisconnected>>,
+ 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<AssetServer>,
+) {
+ 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::<PartInShip>();
+ 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<Entity>,
+) -> 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
M crates/unified/src/ship_editor/input.rs => crates/unified/src/ship_editor/input.rs +8 -7
@@ 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::<RenderLayers>()
+ .observe(try_detach)
.remove::<GhostModule>();
}
}
@@ 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;
M crates/unified/src/ship_editor/mod.rs => crates/unified/src/ship_editor/mod.rs +6 -2
@@ 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),
));