~starkingdoms/starkingdoms

f40e33203245891b3b000e1b35b417e6fd546876 — core 5 months ago 461d3d2
chore(part): part fix #1
M crates/unified/src/attachment.rs => crates/unified/src/attachment.rs +3 -4
@@ 1,8 1,8 @@
use std::ops::Deref;
use bevy::asset::processor::ErasedProcessor;
use bevy::ecs::entity::MapEntities;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::ops::Deref;

#[derive(Component, Serialize, Deserialize)]
/// The primary component for a ship structure


@@ 16,12 16,11 @@ pub struct Parts(#[entities] Vec<Entity>);
#[relationship(relationship_target = Parts)]
pub struct PartInShip(#[entities] Entity);


#[derive(Component, Serialize, Deserialize)]
#[require(Transform)]
pub struct Joint {
    pub id: JointId,
    pub transform: Transform
    pub transform: Transform,
}
#[derive(Component, Serialize, Deserialize)]
pub struct Peer(#[entities] Entity);


@@ 48,4 47,4 @@ impl JointId {
}

#[derive(Serialize, Deserialize, Component)]
pub struct JointSnapFor(#[entities] pub Entity);
\ No newline at end of file
pub struct JointSnapFor(#[entities] pub Entity);

M crates/unified/src/client/key_input.rs => crates/unified/src/client/key_input.rs +24 -26
@@ 1,17 1,20 @@
use bevy::{
    app::{App, Update},
    ecs::{event::EventWriter, system::Res},
    input::{ButtonInput, keyboard::KeyCode},
};
use crate::attachment::{Joint, JointSnapFor};
use crate::ecs::{Part, ThrustEvent};
use bevy::color::palettes::css::{FUCHSIA, GREEN};
use bevy::dev_tools::picking_debug::DebugPickingMode;
use bevy::gizmos::AppGizmoBuilder;
use bevy::log::{debug, info};
use bevy::math::Vec3Swizzles;
use bevy::prelude::{ChildOf, GizmoConfigGroup, Gizmos, GlobalTransform, IntoScheduleConfigs, Query, Reflect, ResMut, Resource, Transform, With};
use bevy::prelude::{
    ChildOf, GizmoConfigGroup, Gizmos, GlobalTransform, IntoScheduleConfigs, Query, Reflect,
    ResMut, Resource, Transform, With,
};
use bevy::{
    app::{App, Update},
    ecs::{event::EventWriter, system::Res},
    input::{ButtonInput, keyboard::KeyCode},
};
use bevy_rapier2d::render::DebugRenderContext;
use crate::attachment::{Joint, JointSnapFor};
use crate::ecs::{Part, ThrustEvent};

pub fn key_input_plugin(app: &mut App) {
    app.add_systems(Update, directional_keys)


@@ 40,11 43,7 @@ fn debug_render_keybind(
    }
}


fn directional_keys(
    keys: Res<ButtonInput<KeyCode>>,
    mut thrust_event: EventWriter<ThrustEvent>,
) {
fn directional_keys(keys: Res<ButtonInput<KeyCode>>, mut thrust_event: EventWriter<ThrustEvent>) {
    if keys.just_pressed(KeyCode::KeyW) || keys.just_pressed(KeyCode::ArrowUp) {
        thrust_event.write(ThrustEvent::Up(true));
    } else if keys.just_released(KeyCode::KeyW) || keys.just_released(KeyCode::ArrowUp) {


@@ 70,20 69,19 @@ fn directional_keys(
    }
}

fn draw_attachment_debug(joints: Query<&GlobalTransform, With<Joint>>, snaps: Query<&GlobalTransform, With<JointSnapFor>>, mut gizmos: Gizmos, mut state: ResMut<AttachmentDebugRes>) {
    if !state.0 { return; }
fn draw_attachment_debug(
    joints: Query<&GlobalTransform, With<Joint>>,
    snaps: Query<&GlobalTransform, With<JointSnapFor>>,
    mut gizmos: Gizmos,
    mut state: ResMut<AttachmentDebugRes>,
) {
    if !state.0 {
        return;
    }
    for joint_target in joints.iter() {
        gizmos.cross_2d(
            joint_target.translation().xy(),
            4.0,
            FUCHSIA
        );
        gizmos.cross_2d(joint_target.translation().xy(), 4.0, FUCHSIA);
    }
    for joint_snap in snaps.iter() {
        gizmos.cross_2d(
            joint_snap.translation().xy(),
            4.0,
            GREEN
        );
        gizmos.cross_2d(joint_snap.translation().xy(), 4.0, GREEN);
    }
}
\ No newline at end of file
}

M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +13 -12
@@ 1,16 1,17 @@
mod colors;
mod parts;
mod key_input;
mod net;
mod particles;
mod parts;
mod planet;
mod starfield;
mod ui;
mod planet;
mod zoom;
mod particles;

use crate::client::parts::parts_plugin;
use planet::incoming_planets::incoming_planets_plugin;
use crate::client::key_input::key_input_plugin;
use crate::client::net::set_config;
use crate::client::parts::parts_plugin;
use crate::client::planet::indicators::indicators_plugin;
use crate::client::starfield::starfield_plugin;
use crate::client::ui::ui_plugin;
use crate::client::zoom::zoom_plugin;


@@ 24,8 25,7 @@ use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy_egui::EguiPlugin;
use bevy_replicon::shared::server_entity_map::ServerEntityMap;
use crate::client::net::set_config;
use crate::client::planet::indicators::indicators_plugin;
use planet::incoming_planets::incoming_planets_plugin;

pub struct ClientPlugin {
    pub server: String,


@@ 71,15 71,16 @@ fn find_me(
    for (entity, player, part) in q_clients.iter() {
        if player.client == entity {
            commands.entity(entity).insert(Me);
            let mut heart_sprite = Sprite::from_image(asset_server.load("sprites/hearty_heart.png"));
            let mut heart_sprite =
                Sprite::from_image(asset_server.load("sprites/hearty_heart.png"));
            heart_sprite.custom_size = Some(Vec2::new(part.width, part.height));
            heart_sprite.color = Color::srgb(20.0, 0.0, 0.0);

            commands.spawn((
                    ChildOf(entity),
                    heart_sprite,
                    Transform::from_xyz(0.0, 0.0, 10.0)
                ));
                ChildOf(entity),
                heart_sprite,
                Transform::from_xyz(0.0, 0.0, 10.0),
            ));
        }
    }
}

M crates/unified/src/client/net.rs => crates/unified/src/client/net.rs +0 -1
@@ 10,7 10,6 @@ pub fn set_config(mut q: Query<&mut TransportConfig, Added<TransportConfig>>) {
    }
}


pub fn on_connecting(
    trigger: Trigger<OnAdd, SessionEndpoint>,
    names: Query<&Name>,

M crates/unified/src/client/particles/mod.rs => crates/unified/src/client/particles/mod.rs +1 -3
@@ 3,7 3,5 @@ use bevy::app::{App, Plugin};
pub struct ParticlePlugin;

impl Plugin for ParticlePlugin {
    fn build(&self, app: &mut App) {
        
    }
    fn build(&self, app: &mut App) {}
}

M crates/unified/src/client/parts.rs => crates/unified/src/client/parts.rs +26 -10
@@ 1,9 1,9 @@
use std::fmt::Debug;
use crate::client::Me;
use crate::ecs::{CursorWorldCoordinates, DragRequestEvent, Part};
use bevy::prelude::*;
use bevy_rapier2d::dynamics::MassProperties;
use bevy_rapier2d::prelude::{AdditionalMassProperties, ReadMassProperties, RigidBody};
use crate::client::Me;
use std::fmt::Debug;

pub fn parts_plugin(app: &mut App) {
    app.insert_resource(DragResource(None));


@@ 63,20 63,36 @@ fn handle_updated_parts(
#[derive(Resource)]
struct DragResource(Option<Entity>);


fn on_part_click(ev: Trigger<Pointer<Pressed>>, sprites: Query<&Sprite, Without<Me>>, mut drag: ResMut<DragResource>) {
    if ev.button != PointerButton::Primary { return; };
    let Ok(sprite) = sprites.get(ev.target()) else { return; };
fn on_part_click(
    ev: Trigger<Pointer<Pressed>>,
    sprites: Query<&Sprite, Without<Me>>,
    mut drag: ResMut<DragResource>,
) {
    if ev.button != PointerButton::Primary {
        return;
    };
    let Ok(sprite) = sprites.get(ev.target()) else {
        return;
    };
    drag.0 = Some(ev.target());
}

fn on_part_release(ev: Trigger<Pointer<Released>>, mut drag: ResMut<DragResource>, mut events: EventWriter<DragRequestEvent>, cursor: Res<CursorWorldCoordinates>) {
    if ev.button != PointerButton::Primary { return; };
fn on_part_release(
    ev: Trigger<Pointer<Released>>,
    mut drag: ResMut<DragResource>,
    mut events: EventWriter<DragRequestEvent>,
    cursor: Res<CursorWorldCoordinates>,
) {
    if ev.button != PointerButton::Primary {
        return;
    };

    if let Some(e) = drag.0 && let Some(c) = cursor.0 {
    if let Some(e) = drag.0
        && let Some(c) = cursor.0
    {
        debug!(?e, ?c, "sending drag request");
        events.write(DragRequestEvent(e, c));
    }

    drag.0 = None;
}
\ No newline at end of file
}

M crates/unified/src/client/planet/incoming_planets.rs => crates/unified/src/client/planet/incoming_planets.rs +11 -10
@@ 11,7 11,7 @@ fn handle_incoming_planets(
    new_planets: Query<(Entity, &Planet), Added<Planet>>,
    asset_server: Res<AssetServer>,
    meshes: ResMut<Assets<Mesh>>,
    materials: ResMut<Assets<ColorMaterial>>
    materials: ResMut<Assets<ColorMaterial>>,
) {
    for (new_entity, new_planet) in new_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&new_planet.sprite));


@@ 21,11 21,11 @@ fn handle_incoming_planets(
            sprite.color = c;
        }

        let mut commands = commands
            .entity(new_entity);
        let mut commands = commands.entity(new_entity);

    commands.insert(AdditionalMassProperties::Mass(new_planet.mass))
    .insert(sprite);
        commands
            .insert(AdditionalMassProperties::Mass(new_planet.mass))
            .insert(sprite);

        trace!(?new_planet, "prepared new planet");
    }


@@ 35,7 35,7 @@ fn handle_updated_planets(
    updated_planets: Query<(Entity, &Planet), Changed<Planet>>,
    asset_server: Res<AssetServer>,
    meshes: ResMut<Assets<Mesh>>,
    materials: ResMut<Assets<ColorMaterial>>
    materials: ResMut<Assets<ColorMaterial>>,
) {
    for (updated_entity, updated_planet) in updated_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&updated_planet.sprite));


@@ 44,18 44,19 @@ fn handle_updated_planets(
            updated_planet.radius * 2.0,
        ));

        if let Some(SpecialSpriteProperties::ForceColor(c)) = updated_planet.special_sprite_properties {
        if let Some(SpecialSpriteProperties::ForceColor(c)) =
            updated_planet.special_sprite_properties
        {
            sprite.color = c;
        }

        let mut commands = commands.entity(updated_entity);
            commands.remove::<AdditionalMassProperties>()
        commands
            .remove::<AdditionalMassProperties>()
            .insert(AdditionalMassProperties::Mass(updated_planet.mass))
            .remove::<Sprite>()
            .insert(sprite);



        trace!(?updated_planet, "updated planet");
    }
}

M crates/unified/src/client/planet/indicators.rs => crates/unified/src/client/planet/indicators.rs +49 -20
@@ 1,8 1,8 @@
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::client::Me;
use crate::config::planet::Planet;
use crate::ecs::MainCamera;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;

pub fn indicators_plugin(app: &mut App) {
    app.add_systems(PreUpdate, (add_indicators, update_indicators))


@@ 14,27 14,45 @@ struct PlanetIndicator(String);
#[derive(Component)]
struct HasIndicator(Entity);

fn add_indicators(planets_wo_indicators: Query<(Entity, &Planet), Without<HasIndicator>>, player: Query<Entity, With<Me>>, asset_server: Res<AssetServer>, mut commands: Commands) {
    let Ok(me) = player.single() else { return; };
fn add_indicators(
    planets_wo_indicators: Query<(Entity, &Planet), Without<HasIndicator>>,
    player: Query<Entity, With<Me>>,
    asset_server: Res<AssetServer>,
    mut commands: Commands,
) {
    let Ok(me) = player.single() else {
        return;
    };
    for (planet, planet_data) in &planets_wo_indicators {
        let Some(indicator_url) = &planet_data.indicator_sprite else { continue };
        let Some(indicator_url) = &planet_data.indicator_sprite else {
            continue;
        };
        let mut sprite = Sprite::from_image(asset_server.load(indicator_url));
        sprite.custom_size = Some(Vec2::new(25.0, 25.0));
        let indicator = commands.spawn((
            ChildOf(me),
            PlanetIndicator(planet_data.name.clone()),
            sprite,
            Transform::from_xyz(0.0, 0.0, 0.0)
        )).id();
        let indicator = commands
            .spawn((
                ChildOf(me),
                PlanetIndicator(planet_data.name.clone()),
                sprite,
                Transform::from_xyz(0.0, 0.0, 0.0),
            ))
            .id();
        commands.entity(planet).insert(HasIndicator(indicator));
    }
}
fn update_indicators(changed_planets_w_indicators: Query<(&Planet, &HasIndicator), Changed<Planet>>, asset_server: Res<AssetServer>, mut commands: Commands) {
fn update_indicators(
    changed_planets_w_indicators: Query<(&Planet, &HasIndicator), Changed<Planet>>,
    asset_server: Res<AssetServer>,
    mut commands: Commands,
) {
    for (planet_data, indicator) in changed_planets_w_indicators.iter() {
        let Some(indicator_sprite) = &planet_data.indicator_sprite else { continue; };
        let Some(indicator_sprite) = &planet_data.indicator_sprite else {
            continue;
        };
        let mut sprite = Sprite::from_image(asset_server.load(indicator_sprite));
        sprite.custom_size = Some(Vec2::new(50.0, 50.0));
        commands.entity(indicator.0)
        commands
            .entity(indicator.0)
            .remove::<Sprite>()
            .insert(sprite);
    }


@@ 42,12 60,21 @@ fn update_indicators(changed_planets_w_indicators: Query<(&Planet, &HasIndicator
fn update_indicators_position(
    planets_w_indicator: Query<(&Transform, &HasIndicator), Without<PlanetIndicator>>,
    player: Query<&Transform, (With<Me>, Without<PlanetIndicator>)>,
    mut indicators: Query<(&mut Transform, &mut Sprite), (With<PlanetIndicator>, Without<HasIndicator>, Without<Me>, Without<MainCamera>)>,
    mut indicators: Query<
        (&mut Transform, &mut Sprite),
        (
            With<PlanetIndicator>,
            Without<HasIndicator>,
            Without<Me>,
            Without<MainCamera>,
        ),
    >,
    window: Query<&Window, With<PrimaryWindow>>,
    camera: Single<&Transform, (With<MainCamera>, Without<PlanetIndicator>)>,
)
{
    let Ok(player_position) = player.single() else { return; };
) {
    let Ok(player_position) = player.single() else {
        return;
    };
    let Ok(window) = window.single() else { return };

    for (planet_position, indicator_id) in &planets_w_indicator {


@@ 60,7 87,9 @@ fn update_indicators_position(
        offset.x = offset.x.clamp(-half_window_width, half_window_width);
        offset.y = offset.y.clamp(-half_window_height, half_window_height);

        let Ok((mut this_indicator, mut this_sprite)) = indicators.get_mut(indicator_id.0) else { continue; };
        let Ok((mut this_indicator, mut this_sprite)) = indicators.get_mut(indicator_id.0) else {
            continue;
        };

        this_sprite.custom_size = Some(Vec2::splat(sprite_size));



@@ 68,4 97,4 @@ fn update_indicators_position(
        this_indicator.translation = inv_rot.mul_vec3(Vec3::new(offset.x, offset.y, 0.0));
        this_indicator.rotation = inv_rot;
    }
}
\ No newline at end of file
}

M crates/unified/src/client/planet/mod.rs => crates/unified/src/client/planet/mod.rs +1 -1
@@ 1,2 1,2 @@
pub mod incoming_planets;
pub mod indicators;
\ No newline at end of file
pub mod indicators;

M crates/unified/src/client/starfield.rs => crates/unified/src/client/starfield.rs +43 -10
@@ 168,12 168,18 @@ pub fn resize_starfield(
    camera: Single<&Transform, With<MainCamera>>,
) {
    for event in resize_event.read() {
        starfield_back.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) * camera.scale.z + Vec2::splat(BACK_STARFIELD_SIZE * 2.0));
        starfield_mid.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) * camera.scale.z + Vec2::splat(MID_STARFIELD_SIZE * 2.0));
        starfield_front.single_mut().unwrap().custom_size =
            Some(Vec2::new(event.width, event.height) * camera.scale.z + Vec2::splat(FRONT_STARFIELD_SIZE * 2.0));
        starfield_back.single_mut().unwrap().custom_size = Some(
            Vec2::new(event.width, event.height) * camera.scale.z
                + Vec2::splat(BACK_STARFIELD_SIZE * 2.0),
        );
        starfield_mid.single_mut().unwrap().custom_size = Some(
            Vec2::new(event.width, event.height) * camera.scale.z
                + Vec2::splat(MID_STARFIELD_SIZE * 2.0),
        );
        starfield_front.single_mut().unwrap().custom_size = Some(
            Vec2::new(event.width, event.height) * camera.scale.z
                + Vec2::splat(FRONT_STARFIELD_SIZE * 2.0),
        );
    }
}



@@ 206,7 212,16 @@ pub fn update_starfield(
        ),
    >,
    window: Single<&Window>,
    camera: Single<&Transform, (With<MainCamera>, Without<Me>, Without<StarfieldFront>, Without<StarfieldMid>, Without<StarfieldBack>)>,
    camera: Single<
        &Transform,
        (
            With<MainCamera>,
            Without<Me>,
            Without<StarfieldFront>,
            Without<StarfieldMid>,
            Without<StarfieldBack>,
        ),
    >,
    player: Query<&Transform, (With<Me>, Without<StarfieldFront>)>,
) {
    let Some(player) = player.iter().next() else {


@@ 218,17 233,35 @@ pub fn update_starfield(
    //starfield_pos.translation = (player.translation / STARFIELD_SIZE).round() * STARFIELD_SIZE;
    starfield_back_pos.translation = player.translation
        + (-player.translation / 3.0) % BACK_STARFIELD_SIZE
        + (Vec3::new(window.resolution.width(), -window.resolution.height() + BACK_STARFIELD_SIZE, 0.0)*camera.scale.z/2.0) % BACK_STARFIELD_SIZE
        + (Vec3::new(
            window.resolution.width(),
            -window.resolution.height() + BACK_STARFIELD_SIZE,
            0.0,
        ) * camera.scale.z
            / 2.0)
            % BACK_STARFIELD_SIZE
        + Vec3::new(0.0, BACK_STARFIELD_SIZE, 0.0)
        - Vec3::new(0.0, 0.0, 5.0);
    starfield_mid_pos.translation = player.translation
        + (-player.translation / 2.5) % MID_STARFIELD_SIZE
        + (Vec3::new(window.resolution.width(), -window.resolution.height() + MID_STARFIELD_SIZE, 0.0)*camera.scale.z/2.0) % MID_STARFIELD_SIZE
        + (Vec3::new(
            window.resolution.width(),
            -window.resolution.height() + MID_STARFIELD_SIZE,
            0.0,
        ) * camera.scale.z
            / 2.0)
            % MID_STARFIELD_SIZE
        + Vec3::new(0.0, MID_STARFIELD_SIZE, 0.0)
        - Vec3::new(0.0, 0.0, 4.5);
    starfield_front_pos.translation = player.translation
        + (-player.translation / 2.0) % FRONT_STARFIELD_SIZE
        + (Vec3::new(window.resolution.width(), -window.resolution.height() + FRONT_STARFIELD_SIZE, 0.0)*camera.scale.z/2.0) % FRONT_STARFIELD_SIZE
        + (Vec3::new(
            window.resolution.width(),
            -window.resolution.height() + FRONT_STARFIELD_SIZE,
            0.0,
        ) * camera.scale.z
            / 2.0)
            % FRONT_STARFIELD_SIZE
        + Vec3::new(0.0, FRONT_STARFIELD_SIZE, 0.0)
        - Vec3::new(0.0, 0.0, 4.0);
}

M crates/unified/src/client/zoom.rs => crates/unified/src/client/zoom.rs +43 -7
@@ 1,6 1,15 @@
use bevy::{input::mouse::{MouseScrollUnit, MouseWheel}, prelude::*};
use bevy::{
    input::mouse::{MouseScrollUnit, MouseWheel},
    prelude::*,
};

use crate::{client::{starfield::{BACK_STARFIELD_SIZE, FRONT_STARFIELD_SIZE, MID_STARFIELD_SIZE}, Me}, ecs::{MainCamera, StarfieldBack, StarfieldFront, StarfieldMid}};
use crate::{
    client::{
        Me,
        starfield::{BACK_STARFIELD_SIZE, FRONT_STARFIELD_SIZE, MID_STARFIELD_SIZE},
    },
    ecs::{MainCamera, StarfieldBack, StarfieldFront, StarfieldMid},
};

pub fn zoom_plugin(app: &mut App) {
    app.add_systems(Update, on_scroll);


@@ 33,8 42,26 @@ fn on_scroll(
            Without<StarfieldMid>,
        ),
    >,
    mut camera: Single<&mut Transform, (With<MainCamera>, Without<Me>, Without<StarfieldFront>, Without<StarfieldMid>, Without<StarfieldBack>)>,
    player: Single<&Transform, (With<Me>, Without<StarfieldFront>, Without<StarfieldMid>, Without<StarfieldBack>, Without<MainCamera>)>,
    mut camera: Single<
        &mut Transform,
        (
            With<MainCamera>,
            Without<Me>,
            Without<StarfieldFront>,
            Without<StarfieldMid>,
            Without<StarfieldBack>,
        ),
    >,
    player: Single<
        &Transform,
        (
            With<Me>,
            Without<StarfieldFront>,
            Without<StarfieldMid>,
            Without<StarfieldBack>,
            Without<MainCamera>,
        ),
    >,
) {
    let (mut starfield_back, mut starfield_back_pos) = starfield_back.into_inner();
    let (mut starfield_mid, mut starfield_mid_pos) = starfield_mid.into_inner();


@@ 55,17 82,26 @@ fn on_scroll(
                    Some(window.size() * camera.scale.z + Vec2::splat(FRONT_STARFIELD_SIZE * 2.0));
                starfield_back_pos.translation = player.translation
                    + (-player.translation / 3.0) % BACK_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)*camera.scale.z/2.0) % BACK_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)
                        * camera.scale.z
                        / 2.0)
                        % BACK_STARFIELD_SIZE
                    + Vec3::new(0.0, BACK_STARFIELD_SIZE, 0.0)
                    - Vec3::new(0.0, 0.0, 5.0);
                starfield_mid_pos.translation = player.translation
                    + (-player.translation / 2.5) % MID_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)*camera.scale.z/2.0) % MID_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)
                        * camera.scale.z
                        / 2.0)
                        % MID_STARFIELD_SIZE
                    + Vec3::new(0.0, MID_STARFIELD_SIZE, 0.0)
                    - Vec3::new(0.0, 0.0, 4.5);
                starfield_front_pos.translation = player.translation
                    + (-player.translation / 2.0) % FRONT_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)*camera.scale.z/2.0) % FRONT_STARFIELD_SIZE
                    + (Vec3::new(window.resolution.width(), -window.resolution.height(), 0.0)
                        * camera.scale.z
                        / 2.0)
                        % FRONT_STARFIELD_SIZE
                    + Vec3::new(0.0, FRONT_STARFIELD_SIZE, 0.0)
                    - Vec3::new(0.0, 0.0, 4.0);
            }

M crates/unified/src/config/mod.rs => crates/unified/src/config/mod.rs +1 -1
@@ 1,3 1,3 @@
pub mod part;
pub mod planet;
pub mod world;
pub mod part;

M crates/unified/src/config/part.rs => crates/unified/src/config/part.rs +3 -3
@@ 7,7 7,7 @@ use serde::{Deserialize, Serialize};
pub struct PartConfig {
    pub part: PartPartConfig,
    pub physics: PartPhysicsConfig,
    pub joints: Vec<JointConfig>
    pub joints: Vec<JointConfig>,
}
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq)]
pub struct PartPartConfig {


@@ 19,7 19,7 @@ pub struct PartPartConfig {
pub struct PartPhysicsConfig {
    pub width: f32,
    pub height: f32,
    pub mass: f32
    pub mass: f32,
}
#[derive(Deserialize, TypePath, Serialize, Clone, Debug, PartialEq)]
pub struct JointConfig {


@@ 40,4 40,4 @@ impl From<JointOffset> for Transform {
            ..Default::default()
        }
    }
    }
\ No newline at end of file
}

M crates/unified/src/config/planet.rs => crates/unified/src/config/planet.rs +1 -2
@@ 18,10 18,9 @@ pub struct Planet {

#[derive(Deserialize, TypePath, Serialize, Clone, Debug)]
pub enum SpecialSpriteProperties {
    ForceColor(Color)
    ForceColor(Color),
}


#[derive(Bundle)]
pub struct PlanetBundle {
    pub planet: Planet,

M crates/unified/src/config/world.rs => crates/unified/src/config/world.rs +2 -2
@@ 13,14 13,14 @@ pub struct GlobalWorldConfig {
pub struct WorldConfig {
    pub gravity: f32,
    pub spawn_parts_at: String,
    pub spawn_parts_interval_secs: f32
    pub spawn_parts_interval_secs: f32,
}

#[derive(Deserialize, Asset, TypePath, Clone)]
pub struct PartConfig {
    pub default_width: f32,
    pub default_height: f32,
    pub default_mass: f32
    pub default_mass: f32,
}

#[derive(Deserialize, Asset, TypePath, Clone)]

M crates/unified/src/ecs.rs => crates/unified/src/ecs.rs +8 -3
@@ 21,7 21,6 @@ pub struct StarfieldBack;
#[derive(Resource, Default)]
pub struct CursorWorldCoordinates(pub Option<Vec2>);


#[derive(Debug, Deserialize, Event, Serialize)]
pub enum ThrustEvent {
    Up(bool),


@@ 31,7 30,13 @@ pub enum ThrustEvent {
}

#[derive(Component, Serialize, Deserialize, Debug)]
#[require(ReadMassProperties, RigidBody::Dynamic, ExternalForce, ExternalImpulse, Replicated)]
#[require(
    ReadMassProperties,
    RigidBody::Dynamic,
    ExternalForce,
    ExternalImpulse,
    Replicated
)]
pub struct Part {
    pub sprite: String,
    pub width: f32,


@@ 68,4 73,4 @@ pub struct Particles {
}

#[derive(Serialize, Deserialize, Event, Debug, MapEntities, Clone)]
pub struct DragRequestEvent(#[entities] pub Entity, pub Vec2);
\ No newline at end of file
pub struct DragRequestEvent(#[entities] pub Entity, pub Vec2);

M crates/unified/src/lib.rs => crates/unified/src/lib.rs +4 -4
@@ 15,16 15,16 @@ pub mod wasm_entrypoint;
#[cfg(target_arch = "wasm32")]
pub use wasm_entrypoint::*;

pub mod attachment;
pub mod client;
pub mod client_plugins;
pub mod config;
pub mod particles;
pub mod ecs;
#[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
pub mod particle_editor;
pub mod particles;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server_plugins;
pub mod shared_plugins;
#[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
pub mod particle_editor;
pub mod attachment;
\ No newline at end of file

M crates/unified/src/main.rs => crates/unified/src/main.rs +3 -4
@@ 29,7 29,7 @@ enum Cli {
        max_clients: usize,
    },
    #[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
    ParticleEditor {}
    ParticleEditor {},
}

fn main() -> AppExit {


@@ 38,8 38,7 @@ fn main() -> AppExit {

    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::from_default_env()
                .add_directive(Directive::from_str("naga=error").unwrap())
            EnvFilter::from_default_env().add_directive(Directive::from_str("naga=error").unwrap()),
        )
        .finish()
        .init();


@@ 66,7 65,7 @@ fn main() -> AppExit {
                max_clients,
            });
            app.add_plugins(SharedPluginGroup);
        },
        }
        #[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
        Cli::ParticleEditor {} => {
            app.add_plugins(starkingdoms::particle_editor::particle_editor_plugin);

M crates/unified/src/particle_editor/ecs.rs => crates/unified/src/particle_editor/ecs.rs +11 -2
@@ 1,6 1,12 @@
use std::time::Duration;

use bevy::{asset::Handle, ecs::component::Component, render::mesh::Mesh, sprite::ColorMaterial, time::{Timer, TimerMode}};
use bevy::{
    asset::Handle,
    ecs::component::Component,
    render::mesh::Mesh,
    sprite::ColorMaterial,
    time::{Timer, TimerMode},
};

use crate::particles::ParticleEffect;



@@ 11,7 17,10 @@ pub struct Particle;
pub struct LifetimeTimer(pub Timer);
impl LifetimeTimer {
    pub fn new(lifetime: f32) -> LifetimeTimer {
        LifetimeTimer(Timer::new(Duration::from_secs_f32(lifetime), TimerMode::Once))
        LifetimeTimer(Timer::new(
            Duration::from_secs_f32(lifetime),
            TimerMode::Once,
        ))
    }
}


M crates/unified/src/particle_editor/hooks.rs => crates/unified/src/particle_editor/hooks.rs +6 -3
@@ 11,8 11,11 @@ fn init_particle_effect(
    particle_effect: Query<(Entity, &ParticleEffect), Added<ParticleEffect>>,
) {
    for (entity, effect) in particle_effect {
        commands.get_entity(entity).unwrap().insert(
            SpawnDelayTimer::new(effect.batch_spawn_delay_seconds.sample(&mut rand::rng()))
        );
        commands
            .get_entity(entity)
            .unwrap()
            .insert(SpawnDelayTimer::new(
                effect.batch_spawn_delay_seconds.sample(&mut rand::rng()),
            ));
    }
}

M crates/unified/src/particle_editor/mod.rs => crates/unified/src/particle_editor/mod.rs +230 -157
@@ 1,14 1,17 @@
use std::collections::BTreeMap;
use crate::{
    particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin},
    particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2},
};
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiPrimaryContextPass};
use bevy_egui::{EguiContexts, EguiPlugin, EguiPrimaryContextPass, egui};
use bevy_rapier2d::plugin::{NoUserData, RapierPhysicsPlugin};
use ordered_float::OrderedFloat;
use ron::ser::PrettyConfig;
use crate::{particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin}, particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2}};
use std::collections::BTreeMap;

mod spawn;
mod hooks;
mod ecs;
mod hooks;
mod spawn;

pub fn particle_editor_plugin(app: &mut App) {
    app.add_plugins(DefaultPlugins);


@@ 26,40 29,46 @@ pub fn particle_editor_plugin(app: &mut App) {
        add_scale_v: 0.0,
        add_color_t: 0.0,
        add_color_v: [0u8; 4],
        scale_curve: LifetimeCurve::new(&[
            (0.0f32, 5.0),
            (2.0, 0.0)
        ]).0.iter().map(|u| (u.0.clone(), u.1.clone())).collect::<Vec<_>>(),
        color_curve: vec![(OrderedFloat(0.0f32), [255, 0, 0, 255])]
        scale_curve: LifetimeCurve::new(&[(0.0f32, 5.0), (2.0, 0.0)])
            .0
            .iter()
            .map(|u| (u.0.clone(), u.1.clone()))
            .collect::<Vec<_>>(),
        color_curve: vec![(OrderedFloat(0.0f32), [255, 0, 0, 255])],
    });
}

fn setup_editor_effect(
    mut commands: Commands,
) {
fn setup_editor_effect(mut commands: Commands) {
    commands.spawn((
        ParticleEffect {
            lifetime_seconds: RandF32 {
                value: 2.0,
                randomness: 0.1,
            },
            batch_spawn_delay_seconds: RandF32 { value: 0.1, randomness: 0.05 },
            batch_spawn_delay_seconds: RandF32 {
                value: 0.1,
                randomness: 0.05,
            },
            particles_in_batch: RandUsize {
                value: 1,
                randomness: 1
                randomness: 1,
            },
            initial_linear_velocity: RandVec2 {
                x: RandF32 { value: 0.0, randomness: 0.5 },
                y: RandF32 { value: 10.0, randomness: 1.0 }
                x: RandF32 {
                    value: 0.0,
                    randomness: 0.5,
                },
                y: RandF32 {
                    value: 10.0,
                    randomness: 1.0,
                },
            },
            initial_angular_velocity: RandF32 { value: 1.0, randomness: 0.5 },
            scale: LifetimeCurve::new(&[
                (0.0f32, 5.0),
                (2.0, 0.0)
            ]),
            color: LifetimeCurve::new(&[
                (0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into()),
            ]),
            initial_angular_velocity: RandF32 {
                value: 1.0,
                randomness: 0.5,
            },
            scale: LifetimeCurve::new(&[(0.0f32, 5.0), (2.0, 0.0)]),
            color: LifetimeCurve::new(&[(0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into())]),
        },
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));


@@ 81,149 90,213 @@ struct EditorResource {
    add_color_v: [u8; 4],

    scale_curve: Vec<(OrderedFloat<f32>, f32)>,
    color_curve: Vec<(OrderedFloat<f32>, [u8; 4])>
    color_curve: Vec<(OrderedFloat<f32>, [u8; 4])>,
}


fn editor_ui(mut contexts: EguiContexts, effect: Single<&mut ParticleEffect>, mut editor_resource: ResMut<EditorResource>) -> Result {
fn editor_ui(
    mut contexts: EguiContexts,
    effect: Single<&mut ParticleEffect>,
    mut editor_resource: ResMut<EditorResource>,
) -> Result {
    let mut effect = effect.into_inner();
    egui::Window::new("Particle Effect").resizable(false).show(contexts.ctx_mut()?, |ui| {
        egui::Grid::new("effect").striped(true).show(ui, |ui| {
            draw_rand_f32(&mut effect.lifetime_seconds, "Lifetime (seconds): ", ui);
            draw_rand_f32(&mut effect.batch_spawn_delay_seconds, "Delay in between batches (seconds): ", ui);
            draw_rand_usize(&mut effect.particles_in_batch, "Number of particles in batch: ", ui);

            draw_rand_f32(&mut effect.initial_linear_velocity.x, "Linear velocity (x-axis, m/s): ", ui);
            draw_rand_f32(&mut effect.initial_linear_velocity.y, "Linear velocity (y-axis, m/s): ", ui);
            draw_rand_f32(&mut effect.initial_angular_velocity, "Angular velocity (radians/second): ", ui);

            ui.separator();
            ui.label("Scale curve");
            if ui.button("sort").clicked() {
                editor_resource.scale_curve.sort_by_key(|u|u.0);
            }
            ui.end_row();

            editor_resource.scale_curve.retain_mut(|(k, v)| {
                ui.label("scale t=");
                ui.add(egui::DragValue::new(k.as_mut()).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value));
    egui::Window::new("Particle Effect")
        .resizable(false)
        .show(contexts.ctx_mut()?, |ui| {
            egui::Grid::new("effect").striped(true).show(ui, |ui| {
                draw_rand_f32(&mut effect.lifetime_seconds, "Lifetime (seconds): ", ui);
                draw_rand_f32(
                    &mut effect.batch_spawn_delay_seconds,
                    "Delay in between batches (seconds): ",
                    ui,
                );
                draw_rand_usize(
                    &mut effect.particles_in_batch,
                    "Number of particles in batch: ",
                    ui,
                );

                draw_rand_f32(
                    &mut effect.initial_linear_velocity.x,
                    "Linear velocity (x-axis, m/s): ",
                    ui,
                );
                draw_rand_f32(
                    &mut effect.initial_linear_velocity.y,
                    "Linear velocity (y-axis, m/s): ",
                    ui,
                );
                draw_rand_f32(
                    &mut effect.initial_angular_velocity,
                    "Angular velocity (radians/second): ",
                    ui,
                );

                ui.separator();
                ui.label("Scale curve");
                if ui.button("sort").clicked() {
                    editor_resource.scale_curve.sort_by_key(|u| u.0);
                }
                ui.end_row();

                editor_resource.scale_curve.retain_mut(|(k, v)| {
                    ui.label("scale t=");
                    ui.add(
                        egui::DragValue::new(k.as_mut())
                            .speed(0.01)
                            .range(0.0f32..=effect.lifetime_seconds.value),
                    );
                    ui.label("v=");
                    ui.add(egui::DragValue::new(v).speed(0.01));
                    let r = ui.button("-");
                    ui.end_row();
                    !r.clicked()
                });

                ui.separator();
                ui.end_row();

                ui.label("new scale: t=");
                ui.add(
                    egui::DragValue::new(&mut editor_resource.add_scale_t)
                        .speed(0.01)
                        .range(0.0f32..=effect.lifetime_seconds.value),
                );
                ui.label("v=");
                ui.add(egui::DragValue::new(v).speed(0.01));
                let r = ui.button("-");
                ui.add(egui::DragValue::new(&mut editor_resource.add_scale_v).speed(0.01));
                if ui.button("+").clicked() {
                    let new_v = (
                        OrderedFloat(editor_resource.add_scale_t),
                        editor_resource.add_scale_v,
                    )
                        .clone();
                    editor_resource.scale_curve.push(new_v);
                }
                ui.end_row();
                !r.clicked()
            });

            ui.separator();
            ui.end_row();

            ui.label("new scale: t=");
            ui.add(egui::DragValue::new(&mut editor_resource.add_scale_t).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value));
            ui.label("v=");
            ui.add(egui::DragValue::new(&mut editor_resource.add_scale_v).speed(0.01));
            if ui.button("+").clicked() {
                let new_v = (OrderedFloat(editor_resource.add_scale_t), editor_resource.add_scale_v).clone();
                editor_resource.scale_curve.push(new_v);
            }
            ui.end_row();

            effect.scale = LifetimeCurve(BTreeMap::from_iter(editor_resource.scale_curve.iter().copied()));

            ui.separator();
            ui.end_row();

            ui.separator();
            ui.label("Color curve");
            if ui.button("sort").clicked() {
                editor_resource.color_curve.sort_by_key(|u|u.0);
            }
            ui.end_row();

            editor_resource.color_curve.retain_mut(|(k, v)| {
                ui.label("color t=");
                ui.add(egui::DragValue::new(k.as_mut()).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value));
                effect.scale = LifetimeCurve(BTreeMap::from_iter(
                    editor_resource.scale_curve.iter().copied(),
                ));

                ui.separator();
                ui.end_row();

                ui.separator();
                ui.label("Color curve");
                if ui.button("sort").clicked() {
                    editor_resource.color_curve.sort_by_key(|u| u.0);
                }
                ui.end_row();

                editor_resource.color_curve.retain_mut(|(k, v)| {
                    ui.label("color t=");
                    ui.add(
                        egui::DragValue::new(k.as_mut())
                            .speed(0.01)
                            .range(0.0f32..=effect.lifetime_seconds.value),
                    );
                    ui.label("v=");
                    ui.color_edit_button_srgba_unmultiplied(v);
                    let r = ui.button("-");
                    ui.end_row();
                    !r.clicked()
                });

                ui.separator();
                ui.end_row();

                ui.label("new color: t=");
                ui.add(
                    egui::DragValue::new(&mut editor_resource.add_color_t)
                        .speed(0.01)
                        .range(0.0f32..=effect.lifetime_seconds.value),
                );
                ui.label("v=");
                ui.color_edit_button_srgba_unmultiplied(v);
                let r = ui.button("-");
                ui.color_edit_button_srgba_unmultiplied(&mut editor_resource.add_color_v);
                if ui.button("+").clicked() {
                    let new_v = (
                        OrderedFloat(editor_resource.add_color_t),
                        editor_resource.add_color_v,
                    )
                        .clone();
                    editor_resource.color_curve.push(new_v);
                }
                ui.end_row();
                !r.clicked()
            });

            ui.separator();
            ui.end_row();

            ui.label("new color: t=");
            ui.add(egui::DragValue::new(&mut editor_resource.add_color_t).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value));
            ui.label("v=");
            ui.color_edit_button_srgba_unmultiplied(&mut editor_resource.add_color_v);
            if ui.button("+").clicked() {
                let new_v = (OrderedFloat(editor_resource.add_color_t), editor_resource.add_color_v).clone();
                editor_resource.color_curve.push(new_v);
            }
            ui.end_row();

            let curve_copied = editor_resource.color_curve.clone();
            effect.color = LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(
                |(u, v)| (
                    *u,
                    Color::Srgba(Srgba::new(
                        v[0] as f32 / 256.0,
                        v[1] as f32 / 256.0,
                        v[2] as f32 / 256.0,
                        v[3] as f32 / 256.0
                    ))
                )
            )));

            ui.separator();
            ui.end_row();
        });
        ui.horizontal(|ui| {
            if ui.button("Generate").clicked() {
                effect.scale = LifetimeCurve(BTreeMap::from_iter(editor_resource.scale_curve.iter().copied()));
                let curve_copied = editor_resource.color_curve.clone();
                effect.color = LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(
                    |(u, v)| (
                        *u,
                        Color::Srgba(Srgba::new(
                            v[0] as f32 / 256.0,
                            v[1] as f32 / 256.0,
                            v[2] as f32 / 256.0,
                            v[3] as f32 / 256.0
                        ))
                    )
                )));

                editor_resource.ser_field = ron::ser::to_string(effect.as_ref()).unwrap();
                editor_resource.status = "Ready; Generated OK".to_string();
            }
            if ui.button("Load").clicked() {
                match ron::from_str(&editor_resource.ser_field) {
                    Ok(e) => {
                        *effect = e;
                        editor_resource.scale_curve = effect.scale.0.iter().map(|u| (u.0.clone(), u.1.clone())).collect::<Vec<_>>();
                        editor_resource.color_curve = effect.color.0.iter().map(|u| (
                            u.0.clone(),
                            {
                                let mut r = [0u8; 4];
                                let srgba: Srgba = (*u.1).into();
                                r[0] = (srgba.red * 256.0).floor() as u8;
                                r[1] = (srgba.green * 256.0).floor() as u8;
                                r[2] = (srgba.blue * 256.0).floor() as u8;
                                r[3] = (srgba.alpha * 256.0).floor() as u8;
                                r
                            }
                            )).collect::<Vec<_>>();
                    },
                    Err(e) => {
                        editor_resource.status = e.to_string();
                    }
                };
            }
                effect.color =
                    LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
                        (
                            *u,
                            Color::Srgba(Srgba::new(
                                v[0] as f32 / 256.0,
                                v[1] as f32 / 256.0,
                                v[2] as f32 / 256.0,
                                v[3] as f32 / 256.0,
                            )),
                        )
                    })));

                ui.separator();
                ui.end_row();
            });
            ui.horizontal(|ui| {
                if ui.button("Generate").clicked() {
                    effect.scale = LifetimeCurve(BTreeMap::from_iter(
                        editor_resource.scale_curve.iter().copied(),
                    ));
                    let curve_copied = editor_resource.color_curve.clone();
                    effect.color =
                        LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map(|(u, v)| {
                            (
                                *u,
                                Color::Srgba(Srgba::new(
                                    v[0] as f32 / 256.0,
                                    v[1] as f32 / 256.0,
                                    v[2] as f32 / 256.0,
                                    v[3] as f32 / 256.0,
                                )),
                            )
                        })));

                    editor_resource.ser_field = ron::ser::to_string(effect.as_ref()).unwrap();
                    editor_resource.status = "Ready; Generated OK".to_string();
                }
                if ui.button("Load").clicked() {
                    match ron::from_str(&editor_resource.ser_field) {
                        Ok(e) => {
                            *effect = e;
                            editor_resource.scale_curve = effect
                                .scale
                                .0
                                .iter()
                                .map(|u| (u.0.clone(), u.1.clone()))
                                .collect::<Vec<_>>();
                            editor_resource.color_curve = effect
                                .color
                                .0
                                .iter()
                                .map(|u| {
                                    (u.0.clone(), {
                                        let mut r = [0u8; 4];
                                        let srgba: Srgba = (*u.1).into();
                                        r[0] = (srgba.red * 256.0).floor() as u8;
                                        r[1] = (srgba.green * 256.0).floor() as u8;
                                        r[2] = (srgba.blue * 256.0).floor() as u8;
                                        r[3] = (srgba.alpha * 256.0).floor() as u8;
                                        r
                                    })
                                })
                                .collect::<Vec<_>>();
                        }
                        Err(e) => {
                            editor_resource.status = e.to_string();
                        }
                    };
                }
            });
            ui.text_edit_multiline(&mut editor_resource.ser_field);
            ui.text_edit_multiline(&mut editor_resource.status.as_str());
        });
        ui.text_edit_multiline(&mut editor_resource.ser_field);
        ui.text_edit_multiline(&mut editor_resource.status.as_str());
    });
    Ok(())
}


M crates/unified/src/particle_editor/spawn.rs => crates/unified/src/particle_editor/spawn.rs +48 -9
@@ 3,7 3,10 @@ use std::time::Duration;
use bevy::prelude::*;
use bevy_rapier2d::prelude::{RigidBody, Velocity};

use crate::{particle_editor::ecs::{CircleMesh, LifetimeTimer, ParentEffect, Particle, SpawnDelayTimer}, particles::ParticleEffect};
use crate::{
    particle_editor::ecs::{CircleMesh, LifetimeTimer, ParentEffect, Particle, SpawnDelayTimer},
    particles::ParticleEffect,
};

pub fn spawn_plugin(app: &mut App) {
    app.add_systems(Update, spawn_particles);


@@ 21,24 24,41 @@ fn spawn_particles(
        delay_timer.0.tick(time.delta());
        if delay_timer.0.just_finished() {
            for _ in 0..effect.particles_in_batch.sample(&mut rand::rng()) {
                let circle = CircleMesh(meshes.add(Circle::new(1.0)),
                    materials.add(effect.color.sample(effect.color.clamp_time(0.0).unwrap()).unwrap()));
                let circle = CircleMesh(
                    meshes.add(Circle::new(1.0)),
                    materials.add(
                        effect
                            .color
                            .sample(effect.color.clamp_time(0.0).unwrap())
                            .unwrap(),
                    ),
                );
                commands.spawn((
                    RigidBody::Dynamic,
                    Particle,
                    transform.with_scale(Vec3::splat(effect.scale.sample(effect.scale.clamp_time(0.0).unwrap()).unwrap())),
                    transform.with_scale(Vec3::splat(
                        effect
                            .scale
                            .sample(effect.scale.clamp_time(0.0).unwrap())
                            .unwrap(),
                    )),
                    Mesh2d(circle.0.clone()),
                    MeshMaterial2d(circle.1.clone()),
                    Velocity {
                        linvel: effect.initial_linear_velocity.sample(&mut rand::rng()),
                        angvel: effect.initial_angular_velocity.sample(&mut rand::rng()),
                    },
                    LifetimeTimer(Timer::from_seconds(effect.lifetime_seconds.sample(&mut rand::rng()), TimerMode::Once)),
                    LifetimeTimer(Timer::from_seconds(
                        effect.lifetime_seconds.sample(&mut rand::rng()),
                        TimerMode::Once,
                    )),
                    circle,
                    ParentEffect(effect.clone()),
                ));
            }
            delay_timer.0.set_duration(Duration::from_secs_f32(effect.batch_spawn_delay_seconds.sample(&mut rand::rng())));
            delay_timer.0.set_duration(Duration::from_secs_f32(
                effect.batch_spawn_delay_seconds.sample(&mut rand::rng()),
            ));
            delay_timer.0.reset();
        }
    }


@@ 46,15 66,34 @@ fn spawn_particles(

fn lifetime_particles(
    mut commands: Commands,
    mut particles: Query<(Entity, &mut LifetimeTimer, &mut Transform, &CircleMesh, &ParentEffect), With<Particle>>,
    mut particles: Query<
        (
            Entity,
            &mut LifetimeTimer,
            &mut Transform,
            &CircleMesh,
            &ParentEffect,
        ),
        With<Particle>,
    >,
    time: ResMut<Time>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    for (entity, mut timer, mut transform, circle, parent) in &mut particles {
        timer.0.tick(time.delta());
        transform.scale = Vec3::splat(parent.0.scale.sample(parent.0.scale.clamp_time(timer.0.elapsed_secs()).unwrap()).unwrap());
        materials.get_mut(&circle.1).unwrap().color = parent.0.color.sample(parent.0.color.clamp_time(timer.0.elapsed_secs()).unwrap()).unwrap();
        transform.scale = Vec3::splat(
            parent
                .0
                .scale
                .sample(parent.0.scale.clamp_time(timer.0.elapsed_secs()).unwrap())
                .unwrap(),
        );
        materials.get_mut(&circle.1).unwrap().color = parent
            .0
            .color
            .sample(parent.0.color.clamp_time(timer.0.elapsed_secs()).unwrap())
            .unwrap();
        if timer.0.just_finished() {
            commands.entity(entity).despawn();
            meshes.remove(&circle.0);

M crates/unified/src/particles.rs => crates/unified/src/particles.rs +23 -17
@@ 1,16 1,15 @@
use std::collections::BTreeMap;
use std::ops::Bound::{Included, Excluded, Unbounded};
use bevy::color::{Color, LinearRgba};
use bevy::math::Vec2;
use bevy::prelude::Component;
use ordered_float::{FloatCore, OrderedFloat};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::ops::Bound::{Excluded, Included, Unbounded};

#[derive(Deserialize, Serialize, Component, Clone)]
pub struct ParticleEffect {
    // -- lifetime / spawning -- //

    /// Particle lifetime in seconds
    pub lifetime_seconds: RandF32,
    /// Delay inbetween each batch of particles spawned


@@ 19,7 18,6 @@ pub struct ParticleEffect {
    pub particles_in_batch: RandUsize,

    // -- velocity -- //

    /// Initial linear velocity added to the particle's velocity when it is spawned
    pub initial_linear_velocity: RandVec2,
    /// Initial angular velocity added to the particle's rotation when it is spawned


@@ 39,15 37,22 @@ pub struct ParticleEffect {
#[derive(Serialize, Deserialize, Clone)]
pub struct LifetimeCurve<P: Lerp>(pub BTreeMap<OrderedFloat<f32>, P>);
impl<P: Lerp + Copy> LifetimeCurve<P> {
    pub fn new<'a>(points: impl IntoIterator<Item = &'a (f32, P)>) -> Self where P: 'a {
        Self(BTreeMap::from_iter(points.into_iter().map(|u| (OrderedFloat(u.0), u.1.clone()))))
    pub fn new<'a>(points: impl IntoIterator<Item = &'a (f32, P)>) -> Self
    where
        P: 'a,
    {
        Self(BTreeMap::from_iter(
            points.into_iter().map(|u| (OrderedFloat(u.0), u.1.clone())),
        ))
    }

    /// Sample for the value at T. Returns None if the curve has no points, or if T is outside
    /// the domain of the points specified.
    pub fn sample(&self, at: f32) -> Option<P> {
        if self.0.is_empty() { return None; }
        
        if self.0.is_empty() {
            return None;
        }

        if self.0.len() == 1 && at == self.0.iter().nth(0).unwrap().0.0 {
            return Some(*self.0.iter().nth(0).unwrap().1);
        }


@@ 78,14 83,17 @@ impl<P: Lerp + Copy> LifetimeCurve<P> {
    /// Given an input time value, use the ends of the spline's time to clamp
    /// the input value.
    pub fn clamp_time(&self, time: f32) -> Option<f32> {
        let Some(first) = self.0.iter().nth(0) else { return None; };
        let Some(last) = self.0.iter().last() else { return None; };
        let Some(first) = self.0.iter().nth(0) else {
            return None;
        };
        let Some(last) = self.0.iter().last() else {
            return None;
        };

        Some(time.clamp(first.0.0, last.0.0))
    }
}


pub trait Lerp {
    fn lerp(&self, other: &Self, t: f32) -> Self;
}


@@ 102,31 110,29 @@ impl Lerp for Color {
    }
}


#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandF32 {
    pub value: f32,
    pub randomness: f32
    pub randomness: f32,
}
impl RandF32 {
    pub fn sample(&self, rng: &mut impl Rng) -> f32 {
        rng.random_range(self.value-self.randomness .. self.value+self.randomness)
        rng.random_range(self.value - self.randomness..self.value + self.randomness)
    }
}

#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandUsize {
    pub value: usize,
    pub randomness: usize
    pub randomness: usize,
}
impl RandUsize {
    pub fn sample(&self, rng: &mut impl Rng) -> usize {
        let lower_bound = self.value.checked_sub(self.randomness).unwrap_or(0);
        rng.random_range(lower_bound .. self.value+self.randomness)
        rng.random_range(lower_bound..self.value + self.randomness)
    }
}


#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandVec2 {
    pub x: RandF32,

M crates/unified/src/server/earth_parts.rs => crates/unified/src/server/earth_parts.rs +18 -16
@@ 1,34 1,33 @@
use crate::config::planet::Planet;
use crate::ecs::{Part, PartBundle};
use crate::server::part::SpawnPartRequest;
use crate::server::world_config::WorldConfigResource;
use bevy::app::App;
use bevy::math::Vec2;
use bevy::prelude::*;
use bevy::prelude::{Commands, Query, Res, Transform};
use bevy::time::Time;
use bevy_rapier2d::dynamics::{AdditionalMassProperties, MassProperties};
use bevy_rapier2d::geometry::Collider;
use bevy_replicon::prelude::Replicated;
use crate::config::planet::Planet;
use crate::ecs::{Part, PartBundle};
use crate::server::world_config::WorldConfigResource;
use bevy::prelude::*;
use crate::server::part::SpawnPart;

#[derive(Resource, Default)]
struct PartTimerRes {
    timer: Timer
    timer: Timer,
}


pub fn spawn_parts_plugin(app: &mut App) {
    app.init_resource::<PartTimerRes>()
        .add_systems(Update, spawn_parts_on_earth);
}


pub fn spawn_parts_on_earth(
    mut commands: Commands,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
    mut timer: ResMut<PartTimerRes>,
    time: Res<Time>
    asset_server: Res<AssetServer>,
    time: Res<Time>,
) {
    let Some(wc) = &world_config.config else {
        return;


@@ 41,17 40,20 @@ pub fn spawn_parts_on_earth(
    timer.timer = Timer::from_seconds(wc.world.spawn_parts_interval_secs, TimerMode::Once);

    // find earth
    let Some((spawn_planet_pos, spawn_planet)) = planets
        .iter()
        .find(|p| p.1.name == wc.hearty.spawn_at) else { return; };
    let Some((spawn_planet_pos, spawn_planet)) =
        planets.iter().find(|p| p.1.name == wc.hearty.spawn_at)
    else {
        return;
    };
    let angle = rand::random::<f32>() * std::f32::consts::TAU;
    let offset = spawn_planet.radius + 150.0;
    let mut new_transform =
        Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
    let mut new_transform = Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
    new_transform.rotate_z(angle);
    new_transform.translation += spawn_planet_pos.translation;

    commands
        .spawn(SpawnPart("config/parts/chassis.part.toml".to_string()))
        .spawn(SpawnPartRequest(
            asset_server.load("config/parts/chassis.part.toml"),
        ))
        .insert(new_transform);
}
\ No newline at end of file
}

M crates/unified/src/server/gravity.rs => crates/unified/src/server/gravity.rs +0 -2
@@ 18,8 18,6 @@ fn update_gravity(
        return;
    };



    for (part_transform, part_mass, mut forces) in &mut part_query {
        forces.force = Vec2::ZERO;
        forces.torque = 0.0;

M crates/unified/src/server/mod.rs => crates/unified/src/server/mod.rs +5 -5
@@ 1,11 1,13 @@
mod earth_parts;
mod gravity;
mod part;
pub mod planets;
pub mod player;
mod world_config;
mod earth_parts;
mod part;

use crate::server::earth_parts::spawn_parts_plugin;
use crate::server::gravity::newtonian_gravity_plugin;
use crate::server::part::part_management_plugin;
use crate::server::planets::planets_plugin;
use crate::server::player::player_management_plugin;
use crate::server::world_config::world_config_plugin;


@@ 17,8 19,6 @@ use aeronet_websocket::server::WebSocketServer;
use bevy::prelude::*;
use bevy_replicon::prelude::Replicated;
use std::net::SocketAddr;
use crate::server::earth_parts::spawn_parts_plugin;
use crate::server::part::part_config_plugin;

pub struct ServerPlugin {
    pub bind: SocketAddr,


@@ 47,7 47,7 @@ impl Plugin for ServerPlugin {
            .add_plugins(newtonian_gravity_plugin)
            .add_plugins(player_management_plugin)
            .add_plugins(spawn_parts_plugin)
            .add_plugins(part_config_plugin);
            .add_plugins(part_management_plugin);
    }
}
impl ServerPlugin {

M crates/unified/src/server/part.rs => crates/unified/src/server/part.rs +4 -134
@@ 1,138 1,8 @@
use bevy::asset::Handle;
use crate::config::world::PartConfig;
use bevy::prelude::Component;
use bevy::prelude::*;
use bevy_rapier2d::prelude::{AdditionalMassProperties, Collider};
use bevy_replicon::prelude::Replicated;
use crate::attachment::{Joint, JointId, JointOf, JointSnapFor, JointSnaps, Joints};
use crate::config::part::{JointOffset, PartConfig};
use crate::ecs::Part;

pub fn part_config_plugin(app: &mut App) {
    app.add_systems(Update, handle_spawn_part_requests)
        // delay 1 tick
        .add_systems(PreUpdate, update_part_requests);
}
pub fn part_management_plugin(app: &mut App) {}

#[derive(Component, Debug)]
#[require(Transform, Replicated)]
pub struct SpawnPart(pub String);
#[derive(Component)]
struct LoadingPart(Handle<PartConfig>);
#[derive(Component, Debug)]
struct PartType(AssetId<PartConfig>);
#[derive(Component)]
/// STOP DELETING MY ASSET BEVY
struct LiveConfigHandle(Handle<PartConfig>);

// watch for SpawnPart components and start loading their config files
fn handle_spawn_part_requests(new_parts: Query<(Entity, &SpawnPart), (With<GlobalTransform>)>, mut commands: Commands, asset_server: Res<AssetServer>, assets: Res<Assets<PartConfig>>, parts: Query<(&Joints, &JointSnaps), With<Part>>,transform: Query<&GlobalTransform>) {
    for (new_part, request) in &new_parts {
        trace!(?new_part, ?request, "answering part request");

        let hdl: Handle<PartConfig> = asset_server.load(request.0.clone());

        commands.entity(new_part)
            .insert(LiveConfigHandle(hdl.clone()))
            .remove::<SpawnPart>();

        if let Some(cfg) = assets.get(&hdl) {
            spawn_part(commands.reborrow(), new_part, cfg, &hdl.id(), parts, transform,false);
        } else {
            commands.entity(new_part)
                .insert(LoadingPart(hdl.clone()));
        }
    }
}
fn update_part_requests(
    mut ev_config: EventReader<AssetEvent<PartConfig>>,
    loading_parts: Query<(Entity, &LoadingPart)>,
    existing_parts: Query<(Entity, &PartType)>,
    mut assets: ResMut<Assets<PartConfig>>,
    mut commands: Commands,
    parts: Query<(&Joints, &JointSnaps), With<Part>>,
    transform: Query<&GlobalTransform>
) {
    for ev in ev_config.read() {
        match ev {
            AssetEvent::Added { id } => {
                trace!(?id, "asset added");
                for (loading_part, req) in &loading_parts {
                    if req.0.id() == *id {
                        let Some(asset) = assets.get(*id) else { continue; };

                        spawn_part(commands.reborrow(), loading_part, asset, id, parts, transform,false);
                    }
                }
            },
            AssetEvent::Modified { id } => {
                trace!(?id, "updating part");
                for (existing_part, ptype) in &existing_parts {
                    if ptype.0 == *id {
                        let Some(asset) = assets.get(ptype.0) else { continue; };
                        spawn_part(commands.reborrow(), existing_part, asset, id, parts, transform,true);
                    }
                }
            }
            _ => {}
        }
    }
}

fn spawn_part(mut commands: Commands, entity: Entity, part: &PartConfig, id: &AssetId<PartConfig>, parts: Query<(&Joints, &JointSnaps), With<Part>>, transform: Query<&GlobalTransform>, is_update: bool) {
    commands.entity(entity)
        .remove::<LoadingPart>()
        .insert(Part {
            sprite: part.part.sprite_disconnected.clone(),
            width: part.physics.width,
            height: part.physics.height,
            mass: part.physics.mass,
        })
        .insert(Collider::cuboid(part.physics.width / 2.0, part.physics.height / 2.0))
        .insert(AdditionalMassProperties::Mass(part.physics.mass))
        .insert(PartType(*id))
        .insert(Replicated);

    for joint in &part.joints {
        if is_update {
            // find all entities
            for (joints, snaps) in &parts {
                for joint_entity in joints.iter() {
                    commands.entity(joint_entity).insert((
                        ChildOf(entity),
                        Joint {
                            id: JointId::from_part_and_joint_id(part.part.name.clone(), joint.id.clone()),
                            transform: joint.target.into()
                        },
                        <JointOffset as Into<Transform>>::into(joint.target),
                        JointOf(entity),
                        Replicated,
                    ));
                }
                for snap_entity in snaps.iter() {
                    commands.entity(snap_entity).insert((
                        <JointOffset as Into<Transform>>::into(joint.snap),
                    ));
                }
            }
        } else {
            let e = commands.spawn((
                ChildOf(entity),
                Joint {
                    id: JointId::from_part_and_joint_id(part.part.name.clone(), joint.id.clone()),
                    transform: joint.target.into()
                },
                <JointOffset as Into<Transform>>::into(joint.target),
                JointOf(entity),
                Replicated,
            )).id();
            trace!(?e, "spawned joint");

            let e = commands.spawn((
                ChildOf(entity),
                JointSnapFor(e),
                <JointOffset as Into<Transform>>::into(joint.snap),
                Replicated,
            )).id();
            trace!(?e, "spawned jointsnap");
        }
    }
}
\ No newline at end of file
pub struct SpawnPartRequest(pub Handle<PartConfig>);

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +23 -13
@@ 1,7 1,10 @@
use std::f32::consts::PI;

use crate::config::planet::Planet;
use crate::ecs::{DragRequestEvent, Part, PartBundle, Particles, Player, PlayerThrust, ThrustEvent};
use crate::ecs::{
    DragRequestEvent, Part, PartBundle, Particles, Player, PlayerThrust, ThrustEvent,
};
use crate::server::part::SpawnPartRequest;
use crate::server::world_config::WorldConfigResource;
use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity};
use bevy::prelude::*;


@@ 9,14 12,15 @@ use bevy_rapier2d::prelude::{
    AdditionalMassProperties, Collider, ExternalForce, ExternalImpulse, MassProperties,
};
use bevy_replicon::prelude::{FromClient, Replicated};
use crate::server::part::{SpawnPart};

pub fn player_management_plugin(app: &mut App) {
    app
        .add_systems(Update, (handle_new_players, player_thrust, dragging));
    app.add_systems(Update, (handle_new_players, player_thrust, dragging));
}

fn dragging(mut events: EventReader<FromClient<DragRequestEvent>>, mut parts: Query<&mut Transform, With<Part>>) {
fn dragging(
    mut events: EventReader<FromClient<DragRequestEvent>>,
    mut parts: Query<&mut Transform, With<Part>>,
) {
    for event in events.read() {
        let mut part = parts.get_mut(event.event.0).unwrap();
        part.translation.x = event.event.1.x;


@@ 31,7 35,7 @@ fn handle_new_players(
    planets: Query<(&Transform, &Planet)>,
    asset_server: Res<AssetServer>,
    gt: Query<&GlobalTransform>,
    child_of: Query<&ChildOf>
    child_of: Query<&ChildOf>,
) {
    let Some(wc) = &world_config.config else {
        return;


@@ 42,7 46,12 @@ fn handle_new_players(
        let (spawn_planet_pos, spawn_planet) = planets
            .iter()
            .find(|p| p.1.name == wc.hearty.spawn_at)
            .unwrap_or_else(|| panic!("spawn planet {} is missing? (check that the planet is named exactly '{}')", wc.hearty.spawn_at, wc.hearty.spawn_at));
            .unwrap_or_else(|| {
                panic!(
                    "spawn planet {} is missing? (check that the planet is named exactly '{}')",
                    wc.hearty.spawn_at, wc.hearty.spawn_at
                )
            });
        let angle = rand::random::<f32>() * std::f32::consts::TAU;
        let offset = spawn_planet.radius + 150.0;
        let mut new_transform =


@@ 52,14 61,15 @@ fn handle_new_players(

        info!(?new_transform, ?joined_player, "set player's position!");

        commands.entity(joined_player)
            .insert(new_transform);

        commands.entity(joined_player)
            .insert(SpawnPart("config/parts/hearty.part.toml".to_string()))
        commands
            .entity(joined_player)
            .insert(new_transform)
            .insert(SpawnPartRequest(
                asset_server.load("config/parts/hearty.part.toml"),
            ))
            .insert(PlayerThrust::default())
            .insert(Player {
                client: joined_player
                client: joined_player,
            });
    }
}

M crates/unified/src/server_plugins.rs => crates/unified/src/server_plugins.rs +8 -10
@@ 1,19 1,21 @@
use crate::config::part::PartConfig;
use crate::config::planet::PlanetConfigCollection;
use crate::config::world::{GlobalWorldConfig};
use crate::config::world::GlobalWorldConfig;
use aeronet_replicon::server::AeronetRepliconServerPlugin;
use aeronet_websocket::server::WebSocketServerPlugin;
use bevy::app::{App, PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, Startup, TaskPoolPlugin};
use bevy::app::{
    App, PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, Startup, TaskPoolPlugin,
};
use bevy::asset::AssetPlugin;
use bevy::diagnostic::FrameCountPlugin;
use bevy::math::Vec2;
use bevy::prelude::Query;
use bevy::time::TimePlugin;
use bevy_common_assets::toml::TomlAssetPlugin;
use bevy_rapier2d::plugin::{NoUserData, RapierConfiguration, RapierPhysicsPlugin};
use bevy_replicon::RepliconPlugins;
use std::net::SocketAddr;
use std::time::Duration;
use bevy::math::Vec2;
use bevy::prelude::Query;
use bevy_rapier2d::plugin::{NoUserData, RapierConfiguration, RapierPhysicsPlugin};
use crate::config::part::PartConfig;

pub struct ServerPluginGroup {
    pub bind: SocketAddr,


@@ 32,18 34,14 @@ impl PluginGroup for ServerPluginGroup {
            .add_group(RepliconPlugins)
            .add(WebSocketServerPlugin)
            .add(AeronetRepliconServerPlugin)

            /* Assets */
            .add(AssetPlugin::default())
            .add(TomlAssetPlugin::<GlobalWorldConfig>::new(&["wc.toml"]))
            .add(TomlAssetPlugin::<PlanetConfigCollection>::new(&["pc.toml"]))
            .add(TomlAssetPlugin::<PartConfig>::new(&["part.toml"]))

            .add(crate::server::ServerPlugin {
                bind: self.bind,
                max_clients: self.max_clients,
            })
    }
}



M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +2 -5
@@ 1,10 1,10 @@
use crate::attachment::{Joint, JointOf, JointSnapFor, PartInShip, Peer, Ship};
use crate::config::planet::Planet;
use crate::ecs::{DragRequestEvent, Part, Particles, Player, ThrustEvent};
use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use bevy_replicon::prelude::{AppRuleExt, Channel, ClientEventAppExt};
use crate::attachment::{Joint, JointOf, JointSnapFor, PartInShip, Peer, Ship};

pub struct SharedPluginGroup;



@@ 13,14 13,12 @@ impl PluginGroup for SharedPluginGroup {
        PluginGroupBuilder::start::<Self>()
            .add(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
            .add(physics_setup_plugin)

            .add(register_everything)
    }
}

pub fn register_everything(app: &mut App) {
    app
        .add_client_event::<ThrustEvent>(Channel::Ordered)
    app.add_client_event::<ThrustEvent>(Channel::Ordered)
        .add_mapped_client_event::<DragRequestEvent>(Channel::Ordered)
        .replicate::<Transform>()
        .replicate::<GlobalTransform>()


@@ 31,7 29,6 @@ pub fn register_everything(app: &mut App) {
        .replicate::<Player>()
        .replicate::<Particles>()
        .replicate::<ChildOf>()

        .replicate::<Ship>()
        .replicate::<PartInShip>()
        .replicate::<Joint>()