~starkingdoms/starkingdoms

acd7621cd003ebaf380f59fdceaa9459ba91fcb2 — core 30 days ago 97a7f3e
chore(netcode-rewrite): further code cleanup & simplification
M crates/unified/src/client/crafting/ui.rs => crates/unified/src/client/crafting/ui.rs +41 -66
@@ 37,7 37,23 @@ struct RecipeCollection {
}
// TODO: use recipe inputs for client-side validation?
#[derive(Component, Clone)]
struct RecipeElement(Entity, String, HashMap<String, u32>); // stores corresponding part and recipe's part name and inputs
struct RecipeElement {
    part_entity: Entity,
    module_name: String,
    inputs: HashMap<String, u32>,
}

fn click_released(current: Interaction, previous: Interaction) -> bool {
    current == Interaction::Hovered && previous == Interaction::Pressed
}

fn button_color(interaction: Interaction, pressed: Color, hovered: Color, none: Color) -> BackgroundColor {
    match interaction {
        Interaction::Pressed => pressed.into(),
        Interaction::Hovered => hovered.into(),
        Interaction::None    => none.into(),
    }
}

fn load_recipes(asset_server: Res<AssetServer>, mut recipe_collection: ResMut<RecipeCollection>) {
    recipe_collection.handle = Some(asset_server.load("config/recipes.rc.toml"));


@@ 239,7 255,7 @@ fn create_recipe_list(
                    width: Val::Auto,
                    ..Default::default()
                },
                RecipeElement(parent_entity, module_name.clone(), recipe.inputs.clone()),
                RecipeElement { part_entity: parent_entity, module_name: module_name.clone(), inputs: recipe.inputs.clone() },
                BackgroundColor(colors::MANTLE),
                PreviousInteraction(Interaction::None),
                Button),


@@ 269,24 285,13 @@ fn recipe_buttons(
    mut crafting_message_writer: MessageWriter<CraftPartRequest>,
) {
    for (interaction, mut previous_interaction, mut color, recipe) in &mut interaction_query {
        match *interaction {
            Interaction::Pressed => {
                *color = colors::SURFACE_1.into();
            }
            Interaction::Hovered => {
                *color = colors::SURFACE_0.into();
                if previous_interaction.0 == Interaction::Pressed {
                    // released
                    crafting_message_writer.write(CraftPartRequest {
                        crafting_part: recipe.0,
                        crafted_part: recipe.1.clone(),
                        inputs: recipe.2.clone(),
                    });
                }
            }
            Interaction::None => {
                *color = colors::MANTLE.into();
            }
        *color = button_color(*interaction, colors::SURFACE_1, colors::SURFACE_0, colors::MANTLE);
        if click_released(*interaction, previous_interaction.0) {
            crafting_message_writer.write(CraftPartRequest {
                crafting_part: recipe.part_entity,
                crafted_part: recipe.module_name.clone(),
                inputs: recipe.inputs.clone(),
            });
        }
        previous_interaction.0 = *interaction;
    }


@@ 309,35 314,18 @@ fn drill_button(
    drills: Query<&Drill>,
) {
    for (interaction, mut previous_interaction, mut color, drill_button, _button, children) in &mut interaction_query {
        match *interaction {
            Interaction::Pressed => {
                *color = colors::SURFACE_1.into();
            }
            Interaction::Hovered => {
                *color = colors::SURFACE_0.into();
                if previous_interaction.0 == Interaction::Pressed {
                    // released
                    let mut text = text_query.get_mut(children[0]).unwrap();
                    let Ok(drill) = drills.get(drill_button.0) else {
                        error!("A former drill is now not a drill, causing a problem in the drill button");
                        previous_interaction.0 = *interaction;
                        return
                    };
                    // don't allow drill toggling while not on a planet
                    if drill.on_planet.is_none() { return }
                    // the text is flipped because drill.drilling is an old value,
                    // which was now toggled
                    if drill.drilling {
                        **text = "Start Drill".to_string();
                    } else {
                        **text = "Stop Drill".to_string();
                    }
                    toggle_drill_writer.write(ToggleDrillEvent { drill_entity: drill_button.0 });
                }
            }
            Interaction::None => {
                *color = colors::CRUST.into();
            }
        *color = button_color(*interaction, colors::SURFACE_1, colors::SURFACE_0, colors::CRUST);
        if click_released(*interaction, previous_interaction.0) {
            let mut text = text_query.get_mut(children[0]).unwrap();
            let Ok(drill) = drills.get(drill_button.0) else {
                error!("A former drill is now not a drill, causing a problem in the drill button");
                previous_interaction.0 = *interaction;
                return
            };
            if drill.on_planet.is_none() { return }
            // text is flipped because drill.drilling is the pre-toggle value
            **text = if drill.drilling { "Start Drill" } else { "Stop Drill" }.to_string();
            toggle_drill_writer.write(ToggleDrillEvent { drill_entity: drill_button.0 });
        }
        previous_interaction.0 = *interaction;
    }


@@ 396,24 384,11 @@ fn close_button(
        (&Interaction, &mut PreviousInteraction, &mut BackgroundColor, &CloseButton, &mut Button),
        Changed<Interaction>,
    >,
    _mouse: Res<ButtonInput<MouseButton>>,
) {
    for (interaction, mut previous_interaction, mut color,
        close_button, _button) in &mut interaction_query
    {
        match *interaction {
            Interaction::Pressed => {
                *color = colors::MAROON.into();
            }
            Interaction::Hovered => {
                *color = colors::PINK.into();
                if previous_interaction.0 == Interaction::Pressed {
                    commands.entity(close_button.0).despawn();
                }
            }
            Interaction::None => {
                *color = colors::RED.into();
            }
    for (interaction, mut previous_interaction, mut color, close_button, _button) in &mut interaction_query {
        *color = button_color(*interaction, colors::MAROON, colors::PINK, colors::RED);
        if click_released(*interaction, previous_interaction.0) {
            commands.entity(close_button.0).despawn();
        }
        previous_interaction.0 = *interaction;
    }

M crates/unified/src/client/parts.rs => crates/unified/src/client/parts.rs +4 -4
@@ 33,14 33,14 @@ fn temp_to_color(temperature: &Temperature) -> Color {

fn build_part_sprite(part: &Part, temperature: &Temperature, is_connected: bool, asset_server: &AssetServer) -> Sprite {
    let path = if is_connected {
        &part.strong_config.part.sprite_connected
        &part.config.part.sprite_connected
    } else {
        &part.strong_config.part.sprite_disconnected
        &part.config.part.sprite_disconnected
    };
    let mut sprite = Sprite::from_image(asset_server.load(path));
    sprite.custom_size = Some(Vec2::new(
        part.strong_config.physics.width as f32,
        part.strong_config.physics.height as f32,
        part.config.physics.width as f32,
        part.config.physics.height as f32,
    ));
    sprite.color = temp_to_color(temperature);
    sprite

M crates/unified/src/client/planet/incoming_planets.rs => crates/unified/src/client/planet/incoming_planets.rs +14 -31
@@ 6,54 6,37 @@ pub fn incoming_planets_plugin(app: &mut App) {
    app.add_systems(Update, (handle_incoming_planets, handle_updated_planets));
}

fn build_planet_sprite(planet: &Planet, asset_server: &AssetServer) -> Sprite {
    let mut sprite = Sprite::from_image(asset_server.load(&planet.sprite));
    sprite.custom_size = Some(Vec2::splat(planet.radius as f32 * 2.0));
    if let Some(SpecialSpriteProperties::ForceColor(c)) = planet.special_sprite_properties {
        sprite.color = c;
    }
    sprite
}

fn handle_incoming_planets(
    mut commands: Commands,
    new_planets: Query<(Entity, &Planet), Added<Planet>>,
    asset_server: Res<AssetServer>,
) {
    for (new_entity, new_planet) in new_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&new_planet.sprite));
        sprite.custom_size = Some(Vec2::new(new_planet.radius as f32 * 2.0, new_planet.radius as f32 * 2.0));

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

        let mut commands = commands.entity(new_entity);

        commands
            //.insert(AdditionalMassProperties::Mass(new_planet.mass))
        commands.entity(new_entity)
            .insert(MAIN_STAR_LAYERS.clone())
            .insert(sprite);

            .insert(build_planet_sprite(new_planet, &asset_server));
        trace!(?new_planet, "prepared new planet");
    }
}

fn handle_updated_planets(
    mut commands: Commands,
    updated_planets: Query<(Entity, &Planet), Changed<Planet>>,
    asset_server: Res<AssetServer>,
) {
    for (updated_entity, updated_planet) in updated_planets.iter() {
        let mut sprite = Sprite::from_image(asset_server.load(&updated_planet.sprite));
        sprite.custom_size = Some(Vec2::new(
            updated_planet.radius as f32 * 2.0,
            updated_planet.radius as f32 * 2.0,
        ));

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

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

            .insert(build_planet_sprite(updated_planet, &asset_server));
        trace!(?updated_planet, "updated planet");
    }
}

M crates/unified/src/client/planet/indicators.rs => crates/unified/src/client/planet/indicators.rs +7 -3
@@ 4,6 4,10 @@ use crate::client::components::MainCamera;
use crate::prelude::*;
use bevy::window::PrimaryWindow;

const INDICATOR_SPRITE_SIZE: f32 = 25.0;
const INDICATOR_LARGE_SPRITE_SIZE: f32 = 50.0;
const INDICATOR_CAMERA_SCALE_FACTOR: f32 = 32.0;

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


@@ 28,7 32,7 @@ fn add_indicators(
            continue;
        };
        let mut sprite = Sprite::from_image(asset_server.load(indicator_url));
        sprite.custom_size = Some(Vec2::new(25.0, 25.0));
        sprite.custom_size = Some(Vec2::splat(INDICATOR_SPRITE_SIZE));
        let indicator = commands
            .spawn((
                ChildOf(me),


@@ 50,7 54,7 @@ fn update_indicators(
            continue;
        };
        let mut sprite = Sprite::from_image(asset_server.load(indicator_sprite));
        sprite.custom_size = Some(Vec2::new(50.0, 50.0));
        sprite.custom_size = Some(Vec2::splat(INDICATOR_LARGE_SPRITE_SIZE));
        commands
            .entity(indicator.0)
            .remove::<Sprite>()


@@ 80,7 84,7 @@ fn update_indicators_position(
    for (planet_position, indicator_id) in &planets_w_indicator {
        let mut offset = planet_position.translation - player_position.translation;

        let sprite_size = 32.0 * camera.scale.z;
        let sprite_size = INDICATOR_CAMERA_SCALE_FACTOR * camera.scale.z;

        let half_window_height = window.height() * camera.scale.z / 2.0 - (sprite_size / 2.0);
        let half_window_width = window.width() * camera.scale.z / 2.0 - (sprite_size / 2.0);

M crates/unified/src/client/starfield.rs => crates/unified/src/client/starfield.rs +19 -29
@@ 2,8 2,6 @@ use bevy::{
    app::{App, Startup, Update},
    asset::{AssetEvent, AssetServer, Assets},
    ecs::{
        //entity::Entity,
        //entity_disabling::Disabled,
        query::{With, Without},
        system::{Commands, Query, Res, Single},
    },


@@ 202,11 200,6 @@ pub fn resize_starfield(
            return
        };

        /*if camera.scale.z > 10.0 { // arbitrary
            // TODO: find out how to disable sprites // done
        } else {
            // TODO: find out how to reenable them without toggling on every update :(
        }*/
        starfield_back.custom_size = Some(
            Vec2::new(event.width, event.height) * projection.scale
                + Vec2::splat(BACK_STARFIELD_SIZE * 2.0),


@@ 222,6 215,21 @@ pub fn resize_starfield(
    }
}

pub fn parallax_layer_translation(
    player_pos: Vec3,
    window_size: Vec2,
    camera_scale: f32,
    parallax_factor: f32,
    field_size: f32,
    z: f32,
) -> Vec3 {
    player_pos
        + (-player_pos / parallax_factor) % field_size
        + (Vec3::new(window_size.x, -window_size.y, 0.0) * camera_scale / 2.0) % field_size
        + Vec3::new(0.0, field_size, 0.0)
        - Vec3::new(0.0, 0.0, z)
}

macro_rules! fix_negative_field_translations {
    ($field:ident, $size:expr) => {
        if $field.translation.y < $size / 2.0 {


@@ 283,28 291,10 @@ pub fn update_starfield(
    let mut starfield_back_pos = starfield_back.single_mut().unwrap();
    let mut starfield_mid_pos = starfield_mid.single_mut().unwrap();
    let mut starfield_front_pos = starfield_front.single_mut().unwrap();
    //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(), 0.0) * projection.scale
            / 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) * projection.scale
            / 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) * projection.scale
            / 2.0)
            % FRONT_STARFIELD_SIZE
        + Vec3::new(0.0, FRONT_STARFIELD_SIZE, 0.0)
        - Vec3::new(0.0, 0.0, 4.0);
    let win = window.size();
    starfield_back_pos.translation  = parallax_layer_translation(player.translation, win, projection.scale, 3.0, BACK_STARFIELD_SIZE,  5.0);
    starfield_mid_pos.translation   = parallax_layer_translation(player.translation, win, projection.scale, 2.5, MID_STARFIELD_SIZE,   4.5);
    starfield_front_pos.translation = parallax_layer_translation(player.translation, win, projection.scale, 2.0, FRONT_STARFIELD_SIZE, 4.0);

    fix_negative_field_translations!(starfield_back_pos, BACK_STARFIELD_SIZE);
    fix_negative_field_translations!(starfield_mid_pos, MID_STARFIELD_SIZE);

M crates/unified/src/client/starguide/init.rs => crates/unified/src/client/starguide/init.rs +6 -4
@@ 6,6 6,8 @@ use crate::client::starguide::components::{StarguideCamera, StarguideMe, Stargui
use crate::prelude::*;
use crate::shared::ecs::{Part, STARGUIDE_LAYER};

const STARGUIDE_RENDER_SCALE: f32 = 10.0;

pub fn starguide_init_plugin(app: &mut App) {
    app
        .add_systems(Startup, init_starguide)


@@ 33,13 35,13 @@ pub fn player_init(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    let mut sprite = Sprite::from_image(asset_server.load(&me.2.strong_config.part.sprite_connected));
    let mut sprite = Sprite::from_image(asset_server.load(&me.2.config.part.sprite_connected));
    sprite.custom_size = Some(Vec2::new(
        me.2.strong_config.physics.width as f32,
        me.2.strong_config.physics.height as f32,
        me.2.config.physics.width as f32,
        me.2.config.physics.height as f32,
    ));
    commands.spawn((sprite, StarguideMe, STARGUIDE_LAYER,
            Transform::from_scale(Vec3::splat(10.0))));
            Transform::from_scale(Vec3::splat(STARGUIDE_RENDER_SCALE))));
}

fn player_position_update(

M crates/unified/src/client/starguide/input.rs => crates/unified/src/client/starguide/input.rs +0 -2
@@ 35,12 35,10 @@ fn on_click(
fn starguide_drag(
    drag: ResMut<StarguideDrag>,
    mut camera: Single<&mut Transform, With<StarguideCamera>>,
    //mut orbit: Single<&mut Transform, (With<StarguideOrbit>, Without<StarguideCamera>)>,
    cursor: Res<CursorWorldCoordinates>,
) {
    if !drag.is_dragging { return }
    let Some(cursor) = cursor.0 else { return };

    camera.translation = drag.init_camera_pos.extend(0.0) - (cursor - drag.init_cursor_pos).extend(0.0);
    //orbit.translation = drag.init_camera_pos.extend(0.0) - (cursor - drag.init_cursor_pos).extend(0.0);
}

M crates/unified/src/client/starguide/orbit.rs => crates/unified/src/client/starguide/orbit.rs +4 -2
@@ 5,6 5,8 @@ use crate::prelude::*;
use crate::shared::config::planet::Planet;
use crate::shared::world_config::WorldConfigResource;

const ORBIT_CURVE_STEPS: usize = 200;

pub fn starguide_orbit_plugin(app: &mut App) {
    app
        .add_systems(Update, update_orbits);


@@ 68,8 70,8 @@ fn update_orbits(
    // 200 steps in the revolution
    let mut first_pos = None;
    let mut last_pos = None;
    for i in 0..200 {
        let theta = 2.0*PI*(i as f64)/200.0;
    for i in 0..ORBIT_CURVE_STEPS {
        let theta = 2.0*PI*(i as f64)/(ORBIT_CURVE_STEPS as f64);
        let r = (1.0/2.0) * ((f_x*f_x + f_y*f_y - 4.0*a*a) / (-2.0*a - f_x*theta.cos() - f_y*theta.sin()));

        if r < 0.0 { continue }

M crates/unified/src/client/ui.rs => crates/unified/src/client/ui.rs +0 -10
@@ 36,16 36,6 @@ fn setup_ui(camera: Single<Entity, (With<MainCamera>, With<Camera>)>, mut comman
            children![
                (TextColor(colors::PEACH), Text::new("Fuel: 25"), FuelText,),
                (TextColor(colors::PEACH), Text::new("Power: 25"), PowerText,),
                /*(
                    Node {
                        width: Val::Percent(100.0),
                        height: Val::Px(20.0),
                        margin: UiRect::all(Val::Px(5.0)),
                        ..Default::default()
                    },
                    BorderRadius::all(Val::Px(5.0)),
                    BackgroundColor(colors::CRUST),
                )*/
            ],
        )],
        Visibility::Visible

M crates/unified/src/client/zoom.rs => crates/unified/src/client/zoom.rs +19 -78
@@ 3,9 3,20 @@ use bevy::{
    prelude::*,
};
use crate::client::components::{MainCamera, Me};
use crate::client::starfield::{StarfieldSize, BACK_STARFIELD_SIZE, FRONT_STARFIELD_SIZE, MID_STARFIELD_SIZE};
use crate::client::starfield::{parallax_layer_translation, StarfieldSize, BACK_STARFIELD_SIZE, FRONT_STARFIELD_SIZE, MID_STARFIELD_SIZE};
use crate::client::starguide::components::{StarfieldBack, StarfieldFront, StarfieldMid, StarguideCamera};
use crate::shared::ecs::GameplayState;

fn restore_tiled(sprite: &mut Sprite, size: Option<&StarfieldSize>) {
    if let Some(size) = size {
        sprite.image_mode = SpriteImageMode::Tiled {
            tile_x: true,
            tile_y: true,
            stretch_value: size.0,
        };
    }
}

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


@@ 50,18 61,6 @@ fn on_scroll(
            Without<StarfieldBack>,
        ),
    >,
    /*mut orbit_camera: Single<
        &mut Camera,
        (

            Without<StarguideCamera>,
            Without<MainCamera>,
            Without<Me>,
            Without<StarfieldFront>,
            Without<StarfieldMid>,
            Without<StarfieldBack>,
        ),
    >,*/
    mut camera: Single<
        (&mut Camera, &mut Projection),
        (


@@ 83,19 82,6 @@ fn on_scroll(
            Without<MainCamera>,
        ),
    >,
    /*mut starguide_orbit: Single<
        &mut Transform,
        (
            With<StarguideOrbit>,
            Without<Me>,
            Without<StarfieldFront>,
            Without<StarfieldMid>,
            Without<StarfieldBack>,
            Without<MainCamera>,

            Without<StarguideCamera>,
        ),
    >,*/
) {
    let (mut starfield_back, mut starfield_back_pos, mut visibility_back, size_back) =
        starfield_back.into_inner();


@@ 114,15 100,10 @@ fn on_scroll(
                    camera_projection.scale *= 1.03;
                    starguide_projection.scale *= 1.03;
                }
                //starguide_orbit.scale = Vec3::splat(starguide_projection.scale);

                if camera_projection.scale > 20.0 && matches!(gameplay_state.get(), GameplayState::Main) {
                    camera.0.is_active = false;
                    starguide_camera.0.is_active = true;
                    //orbit_camera.is_active = true;

                    starguide_camera.2.translation = player.translation;
                    //starguide_orbit.translation = player.translation;
                    gameplay_next_state.set(GameplayState::Starguide);

                    starfield_back.image_mode = SpriteImageMode::Auto;


@@ 135,30 116,10 @@ fn on_scroll(
                    camera.0.is_active = true;
                    gameplay_next_state.set(GameplayState::Main);
                    starguide_camera.0.is_active = false;
                    //orbit_camera.is_active = false;

                    if matches!(*visibility_back, Visibility::Hidden) {
                        if let Some(size_back) = size_back {
                            starfield_back.image_mode = SpriteImageMode::Tiled {
                                tile_x: true,
                                tile_y: true,
                                stretch_value: size_back.0,
                            };
                        }
                        if let Some(size_mid) = size_mid {
                            starfield_mid.image_mode = SpriteImageMode::Tiled {
                                tile_x: true,
                                tile_y: true,
                                stretch_value: size_mid.0,
                            };
                        }
                        if let Some(size_front) = size_front{
                            starfield_front.image_mode = SpriteImageMode::Tiled {
                                tile_x: true,
                                tile_y: true,
                                stretch_value: size_front.0,
                            };
                        }
                        restore_tiled(&mut starfield_back, size_back);
                        restore_tiled(&mut starfield_mid, size_mid);
                        restore_tiled(&mut starfield_front, size_front);
                    }
                    *visibility_back = Visibility::Inherited;
                    *visibility_mid = Visibility::Inherited;


@@ 170,30 131,10 @@ fn on_scroll(
                    Some(window.size() * camera_projection.scale + Vec2::splat(MID_STARFIELD_SIZE * 2.0));
                starfield_front.custom_size =
                    Some(window.size() * camera_projection.scale + 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_projection.scale
                        / 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_projection.scale
                        / 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_projection.scale
                        / 2.0)
                        % FRONT_STARFIELD_SIZE
                    + Vec3::new(0.0, FRONT_STARFIELD_SIZE, 0.0)
                    - Vec3::new(0.0, 0.0, 4.0);
                let win = window.size();
                starfield_back_pos.translation  = parallax_layer_translation(player.translation, win, camera_projection.scale, 3.0, BACK_STARFIELD_SIZE,  5.0);
                starfield_mid_pos.translation   = parallax_layer_translation(player.translation, win, camera_projection.scale, 2.5, MID_STARFIELD_SIZE,   4.5);
                starfield_front_pos.translation = parallax_layer_translation(player.translation, win, camera_projection.scale, 2.0, FRONT_STARFIELD_SIZE, 4.0);
            }
        }
    }

M crates/unified/src/server/craft.rs => crates/unified/src/server/craft.rs +3 -1
@@ 4,6 4,8 @@ use crate::{prelude::*, server::part::{SpawnPartBundle, SpawnPartRequest}};
use crate::shared::attachment::{PartInShip, Parts};
use crate::shared::ecs::{CraftPartRequest, Part, Player, SingleStorage, VariableStorage};

const CRAFTED_PART_SPAWN_OFFSET: f32 = 50.0;

pub fn craft_plugin(app: &mut App) {
    app.add_systems(Update, receive_crafting_request);
}


@@ 114,7 116,7 @@ fn receive_crafting_request(
        commands.spawn(SpawnPartBundle {
            req: SpawnPartRequest(asset_server.load(
                         format!("config/parts/{}.part.toml", request.crafted_part.to_lowercase()))),
            transform: transform.with_translation(transform.translation + vec3(50.0, 0.0, 0.0)),
            transform: transform.with_translation(transform.translation + vec3(CRAFTED_PART_SPAWN_OFFSET, 0.0, 0.0)),
            vel: *vel,
        });
    }

M crates/unified/src/server/drill.rs => crates/unified/src/server/drill.rs +32 -60
@@ 1,7 1,7 @@
use crate::server::components::PlanetSensor;
use crate::prelude::*;
use crate::shared::attachment::{PartInShip, Parts};
use crate::shared::config::planet::Planet;
use crate::shared::config::planet::{Planet, PlanetResource};
use crate::shared::ecs::{Drill, Part, Player, SingleStorage, ToggleDrillEvent, VariableStorage};

pub fn drill_plugin(app: &mut App) {


@@ 59,69 59,41 @@ fn do_drilling(
    time: Res<Time>,
) {
    for (entity, drill) in hearty_drills {
        if !drill.drilling || drill.on_planet.is_none() {
            continue
        }
        let planet_name = drill.on_planet.clone().unwrap();
        let mut planet = None;
        for q_planet in planet_query {
            if q_planet.name == planet_name {
                planet = Some(q_planet);
                break;
            }
        }
        // if the planet name doesn't match a planet, we have a big problem
        let planet = planet.expect("In do_drilling, a planet name didn't match a planet");
        let Some(ref planet_resource) = planet.planet_resource else {
            continue
        };
        'adding_resources: {
            if let Ok(parts_list) = parts_query.get(entity) {
                for part_entity in parts_list.iter() {
                    let Ok(mut storage) = single_storage_part_query.get_mut(part_entity) else {
                        continue
                    };
                    let to_add = planet_resource.mining_speed * drill.resource_multiplier * time.delta_secs();
                    add_to_single_storage(&mut storage, to_add, &planet_resource.name);
                }
            }
            let Ok(mut storage) = variable_storage_part_query.get_mut(entity) else {
                break 'adding_resources;
            };
        let Some(planet_resource) = find_drill_resource(drill, &planet_query) else { continue };
        drill_into_parts(entity, drill, planet_resource, &parts_query, &mut single_storage_part_query, &time);
        if let Ok(mut storage) = variable_storage_part_query.get_mut(entity) {
            let to_add = planet_resource.mining_speed * drill.resource_multiplier * time.delta_secs();
            add_to_variable_storage(&mut storage, to_add, &planet_resource.name);
        };
        }
    }
    for (drill, part_in_ship) in drills {
        debug!("drill");
        if !drill.drilling || drill.on_planet.is_none() {
            continue
        }
        let planet_name = drill.on_planet.clone().unwrap();
        let mut planet = None;
        for q_planet in planet_query {
            if q_planet.name == planet_name {
                planet = Some(q_planet);
                break;
            }
        }
        // if the planet name doesn't match a planet, we have a big problem
        let planet = planet.expect("In do_drilling, a planet name didn't match a planet");
        let Some(ref planet_resource) = planet.planet_resource else {
            continue
        };
        let player = part_in_ship.0;
        let Ok(parts_list) = parts_query.get(player) else {
            error!("In do_drilling, there was a player without a Parts");
            continue
        };
        for part_entity in parts_list.iter() {
            let Ok(mut storage) = single_storage_part_query.get_mut(part_entity) else {
                continue
            };
            let to_add = planet_resource.mining_speed * drill.resource_multiplier * time.delta_secs();
            add_to_single_storage(&mut storage, to_add, &planet_resource.name);
        }
        let Some(planet_resource) = find_drill_resource(drill, &planet_query) else { continue };
        drill_into_parts(part_in_ship.0, drill, planet_resource, &parts_query, &mut single_storage_part_query, &time);
    }
}

fn find_drill_resource<'a>(drill: &Drill, planet_query: &'a Query<&Planet>) -> Option<&'a PlanetResource> {
    if !drill.drilling { return None; }
    let planet_name = drill.on_planet.as_ref()?;
    planet_query.iter()
        .find(|p| p.name == *planet_name)
        .expect("In do_drilling, a planet name didn't match a planet")
        .planet_resource.as_ref()
}

fn drill_into_parts(
    ship_entity: Entity,
    drill: &Drill,
    planet_resource: &PlanetResource,
    parts_query: &Query<&Parts>,
    single_storage_query: &mut Query<&mut SingleStorage, With<Part>>,
    time: &Time,
) {
    let Ok(parts_list) = parts_query.get(ship_entity) else { return };
    for part_entity in parts_list.iter() {
        let Ok(mut storage) = single_storage_query.get_mut(part_entity) else { continue };
        let to_add = planet_resource.mining_speed * drill.resource_multiplier * time.delta_secs();
        add_to_single_storage(&mut storage, to_add, &planet_resource.name);
    }
}


M crates/unified/src/server/heat/radiation.rs => crates/unified/src/server/heat/radiation.rs +3 -2
@@ 3,6 3,7 @@ use crate::shared::ecs::{Part, Radiator, Temperature};

const STEFAN_BOLTZMANN: f64 = 5.670374419E-8;
const T_ENV: f64 = 4.0; // units: Kelvin
const NEWTON_ITERATIONS: u32 = 3;

pub fn heat_radiation_plugin(app: &mut App) {
    app.add_systems(Update, part_radiation);


@@ 28,12 29,12 @@ fn part_radiation(
        // where x_n is the previous guess for T_n+1
        let initial_temp = temperature.0;
        let k = (radiator.emissivity * STEFAN_BOLTZMANN * radiator.surface_area)
            / (part.strong_config.physics.mass * part.strong_config.part.specific_heat);
            / (part.config.physics.mass * part.config.part.specific_heat);
        let dt = time.delta_secs() as f64;

        // initial guess
        let mut next_temp = initial_temp;
        for _ in 0..3 {
        for _ in 0..NEWTON_ITERATIONS {
            let g = next_temp + k*dt*next_temp.powi(4) - initial_temp - k*dt*T_ENV;
            let g_prime = 1.0 + 4.0*k*dt*(next_temp).powi(3);


M crates/unified/src/server/orbit/mod.rs => crates/unified/src/server/orbit/mod.rs +18 -6
@@ 8,6 8,9 @@ use crate::shared::config::planet::{Planet, PlanetSpring};
use crate::prelude::{App, Query, Res, Update, Without};
use crate::shared::world_config::WorldConfigResource;

const KEPLER_MAX_ITERATIONS: u32 = 100;
const KEPLER_CONVERGENCE_THRESHOLD: f64 = 1e-10;

pub struct OrbitPlugin;
impl Plugin for OrbitPlugin {
    fn build(&self, app: &mut App) {


@@ 31,9 34,18 @@ fn update_orbits(
        // find parent
        let Some(parent) = planets_2.iter().find(|u| u.0.name == orbit_data.orbiting) else { continue; };

        let a = (planet.default_transform[0] as f64 - parent.0.default_transform[0] as f64) / (1.0 - orbit_data.eccentricity);
        let (parent_data, parent_transform, parent_mass) = parent;
        // Orbital elements:
        //   a   = semi-major axis
        //   e   = eccentricity
        //   t   = orbital period
        //   m   = mean anomaly
        //   e_k = eccentric anomaly (solution to Kepler's equation)
        //   nu  = true anomaly
        //   r   = radial distance
        let a = (planet.default_position.x as f64 - parent_data.default_position.x as f64) / (1.0 - orbit_data.eccentricity);
        let e = orbit_data.eccentricity;
        let t = 2.0*PI*((a*a*a)/(world_config.world.gravity*(**parent.2 as f64))).sqrt();
        let t = 2.0*PI*((a*a*a)/(world_config.world.gravity*(**parent_mass as f64))).sqrt();

        let time = time.elapsed_secs_f64();



@@ 49,8 61,8 @@ fn update_orbits(

        // find the spring
        let Some(mut planet_spring) = planet_springs.iter_mut().find(|u| u.0.name == planet.name) else { continue; };
        planet_spring.1.translation.x = x as f32 + parent.1.translation.x;
        planet_spring.1.translation.y = y as f32 + parent.1.translation.y;
        planet_spring.1.translation.x = x as f32 + parent_transform.translation.x;
        planet_spring.1.translation.y = y as f32 + parent_transform.translation.y;

        let Some(parent_velocity) = parent_velocities.get(&orbit_data.orbiting) else { continue; };
        let de_dt = (TAU / t) / (1.0 - e * e_k.cos());


@@ 64,10 76,10 @@ fn update_orbits(

fn iterative_kepler(m: f64, e: f64) -> f64 {
    let mut output = m;
    for _ in 0..100 {
    for _ in 0..KEPLER_MAX_ITERATIONS {
        let d = (m - output + e * output.sin()) / (1.0 - e * output.cos());
        output += d;
        if d.abs() < 1e-10 {
        if d.abs() < KEPLER_CONVERGENCE_THRESHOLD {
            break;
        }
    }

M crates/unified/src/server/part.rs => crates/unified/src/server/part.rs +1 -1
@@ 129,7 129,7 @@ fn handle_part_reloading(

fn calculate_bundle(config: &PartConfig, handle: &Handle<PartConfig>) -> impl Bundle {
    let part = Part {
        strong_config: config.clone(),
        config: config.clone(),
    };
    let part_handle = PartHandle(handle.clone());
    let collider = Collider::rectangle(config.physics.width, config.physics.height);

M crates/unified/src/server/planets.rs => crates/unified/src/server/planets.rs +10 -27
@@ 5,6 5,8 @@ use crate::shared::config::planet::{PlanetSpring, PlanetSpringJoint};
use crate::shared::config::planet::{Planet, PlanetBundle, PlanetConfigCollection};
use crate::shared::world_config::WorldConfigResource;

const PLANET_SENSOR_PADDING: f64 = 2.0;

pub fn planets_plugin(app: &mut App) {
    app.init_resource::<PlanetConfigResource>()
        .add_systems(Startup, start_loading_planets)


@@ 50,22 52,18 @@ pub fn update_planets(
                    debug!("planet config loaded - creating planets");
                    let planet_config = assets.get(*id).unwrap();
                    for planet in &planet_config.planets {
                        let planet_position = vec3(planet.default_transform[0], planet.default_transform[1], planet.default_transform[2]);
                        let planet_position = planet.default_position;
                        let mut planet_entity = commands
                            .spawn((PlanetBundle {
                                planet: planet.clone(),
                                transform: Transform::from_xyz(
                                    planet.default_transform[0],
                                    planet.default_transform[1],
                                    planet.default_transform[2],
                                ),
                                transform: Transform::from_translation(planet.default_position),
                                collider: Collider::circle(planet.radius),
                                mass: Mass(planet.mass)
                            },
                            SleepingDisabled
                        ));
                        planet_entity.with_child((
                            Collider::circle(planet.radius+2.0),
                            Collider::circle(planet.radius + PLANET_SENSOR_PADDING),
                            Sensor,
                            PlanetSensor(planet.name.clone()),
                            CollisionEventsEnabled,


@@ 73,8 71,7 @@ pub fn update_planets(
                        let planet_entity_id = planet_entity.id();
                        if let Some(orbit) = &planet.orbit {
                            let Some(parent_planet) = planet_config.planets.iter().find(|u| orbit.orbiting == u.name) else { continue };
                            let parent_planet_position = vec3(parent_planet.default_transform[0],
                                parent_planet.default_transform[1], parent_planet.default_transform[2]);
                            let parent_planet_position = parent_planet.default_position;
                            let r = (planet_position - parent_planet_position).as_dvec3();
                            let g = world_config.world.gravity * parent_planet.mass as f64
                                / r.length_squared();


@@ 88,11 85,7 @@ pub fn update_planets(
                                PlanetSpring {
                                    name: planet.name.clone()
                                },
                                Transform::from_xyz(
                                    planet.default_transform[0],
                                    planet.default_transform[1],
                                    planet.default_transform[2],
                                ),
                                Transform::from_translation(planet.default_position),
                                RigidBody::Kinematic,
                            )).id();
                            commands.spawn((


@@ 138,15 131,9 @@ pub fn update_planets(
                            let planet_entity = commands
                                .spawn(PlanetBundle {
                                    planet: planet.clone(),
                                    transform: Transform::from_xyz(
                                        planet.default_transform[0],
                                        planet.default_transform[1],
                                        planet.default_transform[2],
                                    ),
                                    transform: Transform::from_translation(planet.default_position),
                                    collider: Collider::circle(planet.radius),
                                    mass: Mass(
                                        planet.mass,
                                    ),
                                    mass: Mass(planet.mass),
                                }).id();

                            if planet.orbit.is_some() {


@@ 154,11 141,7 @@ pub fn update_planets(
                                    PlanetSpring {
                                        name: planet.name.clone()
                                    },
                                    Transform::from_xyz(
                                        planet.default_transform[0],
                                        planet.default_transform[1],
                                        planet.default_transform[2],
                                    )
                                    Transform::from_translation(planet.default_position),
                                )).id();
                                commands.spawn((
                                    PlanetSpringJoint {

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +3 -1
@@ 1,6 1,8 @@
pub mod join;
pub mod thrust;

const FUEL_REGEN_RATE: f32 = 5.0;

use crate::shared::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint};
use crate::shared::ecs::{DragAction, DragRequestEvent, Part, Player, PlayerStorage};
use crate::server::damping::ModuleJointDamping;


@@ 377,6 379,6 @@ fn dragging(

fn magic_fuel_regen(players: Query<&mut PlayerStorage, With<Player>>, time: Res<Time>) {
    for mut storage in players {
        storage.fuel = (storage.fuel + 5.0 * time.delta_secs()).min(storage.fuel_capacity);
        storage.fuel = (storage.fuel + FUEL_REGEN_RATE * time.delta_secs()).min(storage.fuel_capacity);
    }
}

M crates/unified/src/server/player/join.rs => crates/unified/src/server/player/join.rs +9 -5
@@ 7,6 7,10 @@ use crate::server::ConnectedGameEntity;
use crate::server::part::SpawnPartRequest;
use crate::shared::world_config::WorldConfigResource;

const SPAWN_ORBIT_OFFSET: f64 = 150.0;
const INITIAL_FUEL_CAPACITY: f32 = 25.0;
const INITIAL_POWER_CAPACITY: f32 = 25.0;

fn join_player(joined_player: Entity, mut commands: Commands, wc: &GlobalWorldConfig,
    planets: Query<(&Transform, &LinearVelocity, &Planet)>, asset_server: &AssetServer
) {


@@ 27,7 31,7 @@ fn join_player(joined_player: Entity, mut commands: Commands, wc: &GlobalWorldCo
            )
        });
    let angle = rand::random::<f32>() * std::f32::consts::TAU;
    let offset = spawn_planet.radius + 150.0;
    let offset = spawn_planet.radius + SPAWN_ORBIT_OFFSET;
    let mut new_transform =
        Transform::from_xyz(angle.cos() * offset as f32, angle.sin() * offset as f32, 0.0);
    new_transform.rotate_z(angle);


@@ 42,10 46,10 @@ fn join_player(joined_player: Entity, mut commands: Commands, wc: &GlobalWorldCo
            asset_server.load("config/parts/hearty.part.toml"),
        ))
        .insert(PlayerStorage {
            fuel_capacity: 25.0,
            fuel: 25.0,
            power_capacity: 25.0,
            power: 25.0,
            fuel_capacity: INITIAL_FUEL_CAPACITY,
            fuel: INITIAL_FUEL_CAPACITY,
            power_capacity: INITIAL_POWER_CAPACITY,
            power: INITIAL_POWER_CAPACITY,
        })
        .insert(Player {
            client: joined_player,

M crates/unified/src/shared/config/planet.rs => crates/unified/src/shared/config/planet.rs +2 -1
@@ 14,7 14,8 @@ pub struct Planet {
    pub indicator_sprite: Option<String>,
    pub radius: f64,
    pub mass: f32,
    pub default_transform: [f32; 3],
    #[serde(rename = "default_transform")]
    pub default_position: Vec3,
    pub planet_resource: Option<PlanetResource>,
    pub special_sprite_properties: Option<SpecialSpriteProperties>,
    pub orbit: Option<OrbitData>

M crates/unified/src/shared/config/world.rs => crates/unified/src/shared/config/world.rs +2 -2
@@ 5,7 5,7 @@ use serde::Deserialize;
#[derive(Deserialize, Asset, TypePath, Clone)]
pub struct GlobalWorldConfig {
    pub world: WorldConfig,
    pub part: WPartConfig,
    pub part: WorldPartConfig,
    pub hearty: HeartyConfig
}



@@ 18,7 18,7 @@ pub struct WorldConfig {
}

#[derive(Deserialize, Asset, TypePath, Clone, Debug)]
pub struct WPartConfig {
pub struct WorldPartConfig {
    pub default_width: f64,
    pub default_height: f64,
    pub default_mass: f64,

M crates/unified/src/shared/ecs.rs => crates/unified/src/shared/ecs.rs +1 -1
@@ 29,7 29,7 @@ pub const ORBIT_LAYER: RenderLayers = RenderLayers::layer(2);
    ConstantForce,
)]
pub struct Part {
    pub strong_config: PartConfig,
    pub config: PartConfig,
}
#[derive(Component, Debug)]
pub struct PartHandle(pub Handle<PartConfig>);

M crates/unified/src/shared/plugins.rs => crates/unified/src/shared/plugins.rs +5 -15
@@ 9,17 9,20 @@ use crate::shared::config::recipe::RecipesConfig;
use crate::shared::config::world::GlobalWorldConfig;
use crate::shared::world_config::world_config_plugin;

const PHYSICS_TICK_RATE: f64 = 20.0;
const PHYSICS_LENGTH_UNIT: f64 = 100.0;

pub struct SharedPluginGroup;

impl PluginGroup for SharedPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add(|app: &mut App| {
                app.insert_resource(Time::from_hz(20.0));
                app.insert_resource(Time::from_hz(PHYSICS_TICK_RATE));
            })
            .add_group(
                PhysicsPlugins::default()
                    .with_length_unit(100.0)
                    .with_length_unit(PHYSICS_LENGTH_UNIT)
                    .set(PhysicsInterpolationPlugin::interpolate_all())
                    .build()
                    .disable::<IslandPlugin>()


@@ 49,16 52,3 @@ fn physics_setup_plugin(app: &mut App) {
}

fn setup_physics() {}

/*
fn setup_physics(
    mut rapier_config: Query<&mut RapierConfiguration>,
    mut rapier_context: Query<&mut RapierContextSimulation>,
) {
    let mut cfg = rapier_config.single_mut().unwrap();
    cfg.gravity = Vec2::ZERO;
    let ctx = rapier_context.single_mut().unwrap();
    let mut params = ctx.integration_parameters;
    params.num_internal_stabilization_iterations = 16;
}
*/