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);
}