~starkingdoms/starkingdoms

1f4fec66a1e4c2148cc8ef59bf7adfb1cb77e3df — ghostly_zsh an hour ago 785a5a0 ship-editor
ship editor feat: detaching works ish
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),
    ));