~starkingdoms/starkingdoms

5591c7a32888c9950b5d21d1d18a6150907d6ea7 — ghostly_zsh 8 hours ago 144af6a master
feat: part list & dragging ghosts
A crates/unified/assets/config/ship_editor.se.toml => crates/unified/assets/config/ship_editor.se.toml +5 -0
@@ 0,0 1,5 @@
[part_list]
Hub = { order = 0 }
Basic_Thruster = { order = 1 }
Storage_Hub = { order = 2 }
Thruster = { order = 3 }
\ No newline at end of file

M crates/unified/src/shared/config/mod.rs => crates/unified/src/shared/config/mod.rs +1 -0
@@ 2,3 2,4 @@ pub mod part;
pub mod planet;
pub mod world;
pub mod recipe;
pub mod ship_editor;

A crates/unified/src/shared/config/ship_editor.rs => crates/unified/src/shared/config/ship_editor.rs +12 -0
@@ 0,0 1,12 @@
use std::collections::HashMap;
use crate::prelude::*;

#[derive(Deserialize, Asset, TypePath, Component, Serialize, Clone, Debug)]
pub struct ShipEditorConfig {
    pub part_list: HashMap<String, PartIcon>,
}

#[derive(Deserialize, TypePath, Component, Serialize, Clone, Debug)]
pub struct PartIcon {
    pub order: usize,
}
\ No newline at end of file

M crates/unified/src/shared/plugins.rs => crates/unified/src/shared/plugins.rs +2 -0
@@ 10,6 10,7 @@ use crate::prelude::*;
use crate::shared::config::part::PartConfig;
use crate::shared::config::planet::PlanetConfigCollection;
use crate::shared::config::recipe::RecipesConfig;
use crate::shared::config::ship_editor::ShipEditorConfig;
use crate::shared::config::world::GlobalWorldConfig;
//use crate::shared::net::register_replication;
use crate::shared::world_config::world_config_plugin;


@@ 42,6 43,7 @@ impl PluginGroup for SharedPluginGroup {
            .add(TomlAssetPlugin::<PlanetConfigCollection>::new(&["pc.toml"]))
            .add(TomlAssetPlugin::<PartConfig>::new(&["part.toml"]))
            .add(TomlAssetPlugin::<RecipesConfig>::new(&["rc.toml"]))
            .add(TomlAssetPlugin::<ShipEditorConfig>::new(&["se.toml"]))
    }
}


A crates/unified/src/ship_editor/components.rs => crates/unified/src/ship_editor/components.rs +13 -0
@@ 0,0 1,13 @@
use bevy::camera::visibility::RenderLayers;
use crate::prelude::Component;

pub const MAIN_RENDER_LAYER: RenderLayers = RenderLayers::layer(0);
pub const GHOST_RENDER_LAYER: RenderLayers = RenderLayers::layer(1);

#[derive(Component)]
pub struct GhostModule;

#[derive(Component)]
pub struct MainCamera;
#[derive(Component)]
pub struct GhostCamera;
\ No newline at end of file

M crates/unified/src/ship_editor/input.rs => crates/unified/src/ship_editor/input.rs +83 -5
@@ 1,18 1,24 @@
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::input_focus::InputFocus;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, GHOST_RENDER_LAYER};
use crate::ship_editor::ui::PartEntry;

pub fn input_plugin(app: &mut App) {
    app
        .insert_resource(ShipEditorDrag::default())
        .add_systems(Update, on_scroll)
        .add_systems(Update, on_click)
        .add_systems(Update, drag);
        .add_systems(Update, camera_drag)
        .add_systems(Update, ghost_drag)
        .add_systems(Update, part_button_interaction);
}

#[derive(Resource, Default)]
struct ShipEditorDrag {
    is_dragging: bool,
    is_holding_module: bool,
    init_cursor_pos: Vec2,
    init_camera_pos: Vec2,
}


@@ 52,7 58,7 @@ fn on_scroll(
fn on_click(
    ev: Res<ButtonInput<MouseButton>>,
    mut drag: ResMut<ShipEditorDrag>,
    camera: Single<&Transform, With<Camera2d>>,
    camera: Single<&Transform, (With<Camera2d>, With<MainCamera>)>,
    window: Single<&Window, With<PrimaryWindow>>,
) {
    let Some(cursor_pos) = window.cursor_position() else { return };


@@ 66,12 72,12 @@ fn on_click(
    }
}

fn drag(
fn camera_drag(
    drag: ResMut<ShipEditorDrag>,
    mut camera: Single<(&mut Transform, &Projection), With<Camera2d>>,
    mut camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<MainCamera>)>,
    window: Single<&Window, With<PrimaryWindow>>,
) {
    if !drag.is_dragging { return }
    if !drag.is_dragging || drag.is_holding_module { return }
    let Some(cursor) = window.cursor_position() else { return };
    let projection = camera.1.clone();
    let Projection::Orthographic(projection) = projection else { return };


@@ 79,3 85,75 @@ fn drag(

    transform.translation = drag.init_camera_pos.extend(0.0) - (cursor.with_y(-cursor.y) - drag.init_cursor_pos).extend(0.0)*projection.scale;
}
fn ghost_drag(
    drag: Res<ShipEditorDrag>,
    window: Single<&Window, With<PrimaryWindow>>,
    mut ghost_module: Query<&mut Transform, With<GhostModule>>,
    mut ghost_camera: Single<(&Camera, &GlobalTransform), (With<Camera2d>, With<GhostCamera>)>,
) {
    if !drag.is_dragging || !drag.is_holding_module { return }
    let Some(cursor) = window.cursor_position() else { return };
    let (ghost_camera, camera_transform) = *ghost_camera;
    let Ok(ghost_position) = ghost_camera.viewport_to_world_2d(camera_transform, cursor) else {
        error!("Unable to convert cursor position to world space");
        return // return because this error happens when something is invalid with the camera
    };

    for mut ghost_transform in ghost_module.iter_mut() {
        ghost_transform.translation = ghost_position.extend(0.0);
    }
}

fn part_button_interaction(
    mouse_ev: Res<ButtonInput<MouseButton>>,
    part_button_query: Query<(Entity, &PartEntry, &Interaction), (With<Button>, Changed<Interaction>)>,
    ghost_part_query: Query<Entity, With<GhostModule>>,
    mut drag: ResMut<ShipEditorDrag>,
    mut input_focus: ResMut<InputFocus>,
    mut commands: Commands,
    mut ghost_camera: Single<(&Camera, &GlobalTransform), (With<Camera2d>, With<GhostCamera>)>,
    asset_server: Res<AssetServer>,
    window: Single<&Window, With<PrimaryWindow>>,
) {
    let Some(cursor) = window.cursor_position() else { return };
    let (ghost_camera, camera_transform) = *ghost_camera;
    for (entity, part_entry, interaction) in part_button_query.iter() {
        match interaction {
            Interaction::None => {
                if !mouse_ev.pressed(MouseButton::Left) {
                    drag.is_holding_module = false;
                    for ghost_entity in ghost_part_query.iter() {
                        commands.entity(ghost_entity).despawn();
                    }
                }
            }
            Interaction::Hovered => {
                if !mouse_ev.pressed(MouseButton::Left) {
                    drag.is_holding_module = false;
                    for ghost_entity in ghost_part_query.iter() {
                        commands.entity(ghost_entity).despawn();
                    }
                }
            }
            Interaction::Pressed => {
                input_focus.set(entity);
                drag.is_holding_module = true;
                debug!("pressed {:?}", part_entry.0.part.name);

                let mut sprite = Sprite::from_image(asset_server.load(part_entry.0.part.sprite_disconnected.clone()));
                sprite.custom_size = Some(vec2(part_entry.0.physics.width as f32, part_entry.0.physics.height as f32));
                sprite.color = Color::srgb(0.7, 0.7, 0.7);
                let Ok(ghost_position) = ghost_camera.viewport_to_world_2d(camera_transform, cursor) else {
                    error!("Unable to convert cursor position to world space");
                    return // return because this error happens when something is invalid with the camera
                };
                commands.spawn((
                    sprite,
                    Transform::from_translation(ghost_position.extend(0.0)),
                    GhostModule,
                    GHOST_RENDER_LAYER,
                ));
            }
        }
    }
}
\ No newline at end of file

M crates/unified/src/ship_editor/mod.rs => crates/unified/src/ship_editor/mod.rs +21 -1
@@ 1,9 1,12 @@
pub mod ui;
pub mod input;
pub mod plugins;
pub mod components;

use bevy::input_focus::InputFocus;
use crate::client::colors;
use crate::prelude::*;
use crate::ship_editor::components::{GhostCamera, MainCamera, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER};
use crate::ship_editor::input::input_plugin;
use crate::ship_editor::ui::ui_plugin;



@@ 12,6 15,7 @@ pub struct ShipEditorPlugin;
impl Plugin for ShipEditorPlugin {
    fn build(&self, app: &mut App) {
        app
            .init_resource::<InputFocus>()
            .add_systems(Startup, setup)
            .add_plugins(input_plugin)
            .add_plugins(ui_plugin);


@@ 25,8 29,24 @@ fn setup(
) {
    commands.insert_resource(ClearColor(colors::BASE));
    commands.spawn((
        Camera2d::default(),
        Camera2d,
        Camera {
            order: 0,
            ..Default::default()
        },
        Transform::from_xyz(0.0, 0.0, 0.0),
        MainCamera,
        IsDefaultUiCamera,
        MAIN_RENDER_LAYER,
    ));
    commands.spawn((
        Camera2d::default(),
        Camera {
            order: 1,
            ..Default::default()
        },
        GhostCamera,
        GHOST_RENDER_LAYER,
    ));
    let rectangle = meshes.add(Rectangle::new(50.0, 50.0));
    commands.spawn((

M crates/unified/src/ship_editor/plugins.rs => crates/unified/src/ship_editor/plugins.rs +16 -0
@@ 1,4 1,9 @@
use bevy::app::PluginGroupBuilder;
use bevy::diagnostic::{DiagnosticsPlugin, FrameCountPlugin};
use bevy::input::InputPlugin;
use bevy::log::LogPlugin;
use bevy::state::app::StatesPlugin;
use bevy::time::TimePlugin;
use crate::prelude::*;

pub struct ShipEditorPluginGroup;


@@ 6,5 11,16 @@ pub struct ShipEditorPluginGroup;
impl PluginGroup for ShipEditorPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add_group(
                DefaultPlugins.build()
                    .disable::<LogPlugin>()
                    .disable::<TaskPoolPlugin>()
                    .disable::<FrameCountPlugin>()
                    .disable::<TimePlugin>()
                    .disable::<TransformPlugin>()
                    .disable::<DiagnosticsPlugin>()
                    .disable::<AssetPlugin>()
                    .disable::<StatesPlugin>()
            )
    }
}
\ No newline at end of file

M crates/unified/src/ship_editor/ui.rs => crates/unified/src/ship_editor/ui.rs +107 -8
@@ 1,8 1,38 @@
use bevy::prelude::*;
use bevy::input_focus::InputFocus;
use crate::client::colors;
use bevy::prelude::*;
use bevy::ui::FocusPolicy;
use bevy::ui::widget::ImageNodeSize;
use crate::shared::config::part::PartConfig;
use crate::shared::config::recipe::RecipesConfig;
use crate::shared::config::ship_editor::ShipEditorConfig;

pub fn ui_plugin(app: &mut App) {
    app.add_systems(Startup, setup_ui);
    app
        .insert_resource(ShipEditorConfigHolder::default())
        .add_systems(Startup, (setup_ui, load_ship_editor_config))
        .add_systems(PreUpdate, (fill_part_list, fill_parts));
}

#[derive(Component)]
pub struct PendingPartList;
#[derive(Component)]
pub struct PendingPart(pub Handle<PartConfig>);
#[derive(Component)]
pub struct PartList;
#[derive(Component)]
pub struct PartEntry(pub PartConfig);

#[derive(Resource, Default)]
struct ShipEditorConfigHolder {
    handle: Option<Handle<ShipEditorConfig>>,
}

fn load_ship_editor_config(
    mut config: ResMut<ShipEditorConfigHolder>,
    asset_server: Res<AssetServer>,
) {
    config.handle = Some(asset_server.load("config/ship_editor.se.toml"));
}

fn setup_ui(mut commands: Commands) {


@@ 12,7 42,8 @@ fn setup_ui(mut commands: Commands) {
            height: Val::Percent(100.0),
            ..Default::default()
        },
        children![(
        children![
        (
            Node {
                position_type: PositionType::Absolute,
                margin: UiRect::AUTO.with_bottom(Val::Px(0.0)),


@@ 21,19 52,87 @@ fn setup_ui(mut commands: Commands) {
                ..Default::default()
            },
            BackgroundColor(colors::MANTLE),
            children![(

            )]
            children![()]
        ),
        (
            Node {
                position_type: PositionType::Absolute,
                margin: UiRect::AUTO.with_left(Val::Px(0.0)),
                width: Val::Px(20.0),
                width: Val::Px(200.0),
                height: Val::Percent(100.0),
                ..Default::default()
            },
            BackgroundColor(colors::MANTLE),
        )],
            children![
            (
                Node {
                    display: Display::Grid,
                    width: Val::Percent(100.0),
                    height: Val::Percent(100.0),
                    grid_template_columns: RepeatedGridTrack::auto(2),
                    grid_template_rows: RepeatedGridTrack::px(2, 100.0),
                    justify_items: JustifyItems::Center,
                    align_items: AlignItems::Center,
                    ..Default::default()
                },
                PendingPartList,
            )
            ]
        )
        ],
    ));
}

fn fill_part_list(
    pending_part_list: Query<Entity, With<PendingPartList>>,
    mut commands: Commands,
    ship_editor_config_holder: Res<ShipEditorConfigHolder>,
    ship_editor_config: Res<Assets<ShipEditorConfig>>,
    asset_server: Res<AssetServer>,
) {
    let Some(handle) = ship_editor_config_holder.handle.clone() else { return };
    if let Some(strong_ship_editor_config) = ship_editor_config.get(&handle) {
        for pending_part_list in pending_part_list.iter() {
            let mut pending_part_list = commands.entity(pending_part_list);
            pending_part_list
                .insert(PartList)
                .remove::<PendingPartList>();

            pending_part_list.with_children(|parent| {
                let mut part_list = Vec::with_capacity(strong_ship_editor_config.part_list.len());
                for (i, (part_name, icon)) in strong_ship_editor_config.part_list.iter().enumerate() {
                    part_list.push((
                        icon.order,
                        (Node {
                            width: Val::Px(50.0),
                            height: Val::Px(50.0),
                            margin: UiRect::axes(Val::Px(10.0), Val::Px(0.0)),
                            ..Default::default()
                        },
                        Button,
                        PendingPart(asset_server.load(format!("config/parts/{}.part.toml", part_name.to_lowercase())))),
                    ));
                }
                part_list.sort_by(|a, b| a.0.cmp(&b.0));
                for (_, part) in part_list {
                    parent.spawn(part);
                }
            });
        }
    }
}
fn fill_parts(
    pending_parts: Query<(Entity, &PendingPart)>,
    mut commands: Commands,
    assets: Res<Assets<PartConfig>>,
    asset_server: Res<AssetServer>,
) {
    for (entity, pending_part) in pending_parts.iter() {
        if let Some(strong_part_config) = assets.get(&pending_part.0) {
            commands.entity(entity)
                .insert(PartEntry(strong_part_config.clone()))
                .insert(ImageNode::new(asset_server.load(strong_part_config.part.sprite_disconnected.clone())))
                .remove::<PendingPart>();
        }
    }
}

M crates/unified/src/universal_entrypoint.rs => crates/unified/src/universal_entrypoint.rs +2 -5
@@ 17,18 17,16 @@ use crate::ship_editor::ShipEditorPlugin;
pub fn run(cli: StkArgs) -> AppExit {
    let mut app = App::new();

    app.add_plugins(MinimalPlugins.build().disable::<ScheduleRunnerPlugin>());
    app.add_plugins(SharedPluginGroup);

    match cli {
        StkArgs::Client { server } => {
            app.add_plugins(MinimalPlugins.build().disable::<ScheduleRunnerPlugin>());
            app.add_plugins(SharedPluginGroup);
            app.add_plugins(ClientPluginGroup);
            app.add_plugins(ClientPlugin { server: Some(server) });
        },
        #[cfg(not(target_arch = "wasm32"))]
        StkArgs::Server { bind_to, with_client } => {
            app.add_plugins(MinimalPlugins.build().disable::<ScheduleRunnerPlugin>());
            app.add_plugins(SharedPluginGroup);
            app.add_plugins(ServerPluginGroup);
            app.add_plugins(ServerPlugin { bind: bind_to });
            if with_client {


@@ 38,7 36,6 @@ pub fn run(cli: StkArgs) -> AppExit {
            }
        }
        StkArgs::ShipEditor {} => {
            app.add_plugins(DefaultPlugins);
            app.add_plugins(ShipEditorPluginGroup);
            app.add_plugins(ShipEditorPlugin);
        }