// StarKingdoms.IO, a browser game about drifting through space
// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#![allow(clippy::type_complexity)] // bevy :(
#![allow(clippy::too_many_arguments)] // bevy :(
#![allow(clippy::only_used_in_recursion)] // todo: remove this
use crate::mathutil::rot2d;
use crate::ws::{StkTungsteniteServerConfig, StkTungsteniteServerPlugin, WsEvent};
use bevy::math::{vec2, vec3};
use bevy::{
app::{PluginGroupBuilder, ScheduleRunnerPlugin},
ecs::event::ManualEventReader,
prelude::*,
time::TimePlugin,
};
use bevy_rapier2d::prelude::*;
use component::*;
use hmac::{Hmac, Mac};
use jwt::VerifyWithKey;
use module::component::{Attach, CanAttach, LooseAttach, ModuleTimer, PartBundle, PartFlags, PartType};
use module::save::{construct_save_data, load_savefile};
use module::thruster::search_thrusters;
use module::{attach_on_module_tree, despawn_module_tree, detach_recursive};
use packet::*;
use planet::PlanetType;
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use starkingdoms_common::{pack_savefile, unpack_savefile, SaveData};
use std::fs;
use crate::config::{PartsConfig, PhysicsSolver, PlanetsConfig, StkConfig};
use std::sync::OnceLock;
use std::time::Duration;
pub mod component;
mod config;
pub mod macros;
pub mod mathutil;
pub mod packet;
pub mod ws;
pub mod planet;
pub mod module;
pub mod player;
struct StkPluginGroup;
// factor to multiply positions by to send to the client
pub static CLIENT_SCALE: f32 = 50.0;
// half size of hearty
pub static PART_HALF_SIZE: f32 = 25.0;
// good ol' classic almost useless but still necessary code
static _SERVER_CONFIG: OnceLock<StkConfig> = OnceLock::new();
#[inline]
pub fn server_config() -> StkConfig {
_SERVER_CONFIG.get().unwrap().clone()
}
static _PARTS_CONFIG: OnceLock<PartsConfig> = OnceLock::new();
#[inline]
pub fn parts_config() -> PartsConfig {
_PARTS_CONFIG.get().unwrap().clone()
}
static _PLANETS_CONFIG: OnceLock<PlanetsConfig> = OnceLock::new();
#[inline]
pub fn planets_config() -> PlanetsConfig {
_PLANETS_CONFIG.get().unwrap().clone()
}
// group main stk plugins together
#[cfg(debug_assertions)]
impl PluginGroup for StkPluginGroup {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(TaskPoolPlugin::default())
.add(TypeRegistrationPlugin)
.add(FrameCountPlugin)
.add(TimePlugin)
.add(ScheduleRunnerPlugin::run_loop(Duration::from_millis(
server_config().server.tick_time_ms,
)))
.add(bevy::log::LogPlugin {
level: bevy::log::Level::DEBUG,
filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(),
update_subscriber: None,
})
}
}
#[cfg(not(debug_assertions))]
impl PluginGroup for StkPluginGroup {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(TaskPoolPlugin::default())
.add(TypeRegistrationPlugin)
.add(FrameCountPlugin)
.add(TimePlugin)
.add(ScheduleRunnerPlugin::run_loop(Duration::from_millis(1)))
}
}
// auth token
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserToken {
pub id: i64,
pub username: String,
pub permission_level: i32,
pub expires: std::time::SystemTime,
}
fn main() {
// read the server main config
let server_config = fs::read_to_string("/etc/starkingdoms/config.toml").unwrap();
let parts_config = fs::read_to_string("/etc/starkingdoms/parts.toml").unwrap();
let planets_config = fs::read_to_string("/etc/starkingdoms/planets.toml").unwrap();
// put config in variables
let server_config: StkConfig = toml::from_str(&server_config).unwrap();
_SERVER_CONFIG.set(server_config.clone()).unwrap();
let parts_config: PartsConfig = toml::from_str(&parts_config).unwrap();
_PARTS_CONFIG.set(parts_config.clone()).unwrap();
let planets_config: PlanetsConfig = toml::from_str(&planets_config).unwrap();
_PLANETS_CONFIG.set(planets_config.clone()).unwrap();
// make the game, start it in .run()
App::new()
.insert_resource(AppKeys {
app_key: server_config.security.app_key.as_bytes().to_vec(),
})
.insert_resource(StkTungsteniteServerConfig {
addr: server_config.server.bind.ip,
port: server_config.server.bind.port,
})
.insert_resource(server_config.clone())
.insert_resource(parts_config)
.insert_resource(planets_config)
.add_plugins(StkPluginGroup)
.insert_resource(RapierConfiguration {
gravity: Vect { x: 0.0, y: 0.0 },
..Default::default()
})
.init_resource::<ModuleTimer>()
.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(
server_config.world.pixels_per_meter,
))
.add_plugins(StkTungsteniteServerPlugin)
.add_systems(Startup, setup_integration_parameters)
.add_systems(Startup, planet::spawn_planets)
.add_systems(FixedUpdate, module::module_spawn)
.add_systems(Update, on_message)
.add_systems(Update, on_close)
.add_systems(FixedUpdate, send_player_energy)
.add_systems(FixedUpdate, on_position_change)
.add_systems(
FixedUpdate,
(module::break_modules, gravity_update, player_input_update).chain(),
)
.add_systems(FixedUpdate, module::save::save_eligibility)
.add_systems(FixedUpdate, module::convert_modules)
.insert_resource(Time::<Fixed>::from_seconds(
server_config.server.world_fixed_timestep,
))
.run();
// game is done running
info!("Goodbye!");
}
fn setup_integration_parameters(mut context: ResMut<RapierContext>, server_config: Res<StkConfig>) {
context.integration_parameters = server_config.physics.parameters;
match server_config.physics.solver {
PhysicsSolver::SmallstepPGS => {
context
.integration_parameters
.switch_to_small_steps_pgs_solver();
}
PhysicsSolver::OldPGS => {
context
.integration_parameters
.switch_to_standard_pgs_solver();
}
}
}
fn on_message(
mut commands: Commands,
planet_query: Query<(Entity, &PlanetType, &Transform)>,
mut part_query: Query<
(
Entity,
&PartType,
&mut Transform,
&mut Velocity,
Option<&LooseAttach>,
&mut PartFlags,
),
(Without<PlanetType>, Without<Player>, Without<Attach>),
>,
mut attached_query: Query<
(
Entity,
&PartType,
&mut Transform,
&mut Attach,
&Velocity,
Option<&CanAttach>,
Option<&LooseAttach>,
&mut PartFlags,
),
(Without<PlanetType>, Without<Player>),
>,
mut player_query: Query<
(
Entity,
&mut Player,
&Transform,
&Velocity,
&mut Attach,
&mut PartFlags,
),
Without<PlanetType>,
>,
mut packet_recv: Local<ManualEventReader<WsEvent>>,
mut packet_event_send: ResMut<Events<WsEvent>>,
app_keys: Res<AppKeys>,
server_config: Res<StkConfig>,
) {
let mut event_queue = Vec::new();
for ev in packet_recv.read(&packet_event_send) {
if let WsEvent::Recv { from, message } = ev {
let packet: Packet = err_or_cont!(message.try_into());
match packet {
Packet::ClientLogin {
username,
save,
jwt,
} => {
// auth
// plz no remove
if let Some(token) = jwt {
let key: Hmac<Sha256> = Hmac::new_from_slice(&app_keys.app_key).unwrap();
let claims: UserToken = match token.verify_with_key(&key) {
Ok(c) => c,
Err(e) => {
event_queue.push(WsEvent::Send {
to: *from,
message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Token is invalid or verification failed: {e}. Please log in again, or contact StarKingdoms staff if the problem persists.") }.into(),
});
event_queue.push(WsEvent::Close { addr: *from });
continue;
}
};
if claims.permission_level
< server_config.security.required_permission_level
{
event_queue.push(WsEvent::Send {
to: *from,
message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: format!("Permission level {} is too low, {} is required. If your permissions were just changed, you need to log out and log back in for the change to take effect. If you believe this is a mistake, contact StarKingdoms staff.", claims.permission_level, server_config.security.required_permission_level) }.into(),
});
event_queue.push(WsEvent::Close { addr: *from });
continue;
}
event_queue.push(WsEvent::Send {
to: *from,
message: Packet::Message { message_type: MessageType::Server, actor: "StarKingdoms Team".to_string(), content: "Thank you for participating in the StarKingdoms private alpha! Your feedback is essential to improving the game, so please give us any feedback you have in the Discord! <3".to_string() }.into(),
});
} else if server_config.security.required_permission_level != 0 {
event_queue.push(WsEvent::Send {
to: *from,
message: Packet::Message { message_type: MessageType::Error, actor: "SERVER".to_string(), content: "Authentication is required to join this server at the moment. Log in and try again, or try again later.".to_string() }.into(),
});
event_queue.push(WsEvent::Close { addr: *from });
continue;
}
let angle: f32 = {
let mut rng = rand::thread_rng();
rng.gen::<f32>() * std::f32::consts::PI * 2.
};
let mut transform =
Transform::from_xyz(angle.cos() * 30.0, angle.sin() * 30.0, 0.0);
transform.rotate_z(angle);
let mut player_comp = Player {
addr: *from,
username: username.to_string(),
input: component::Input::default(),
selected: None,
save_eligibility: false,
energy_capacity: part!(PartType::Hearty).energy_capacity,
energy: part!(PartType::Hearty).energy_capacity,
};
let mut entity_id = commands.spawn(PartBundle {
part_type: PartType::Hearty,
transform: TransformBundle::from(transform),
flags: PartFlags { attached: false },
});
entity_id
.insert(Collider::cuboid(0.5, 0.5))
.insert(AdditionalMassProperties::MassProperties(MassProperties {
local_center_of_mass: vec2(0.0, 0.0),
mass: part!(PartType::Hearty).mass,
principal_inertia: 7.5,
}))
.insert(ExternalImpulse {
impulse: Vec2::ZERO,
torque_impulse: 0.0,
})
.insert(ExternalForce::default())
.insert(ReadMassProperties::default())
.insert(Velocity::default())
.insert(RigidBody::Dynamic);
let id = entity_id.id().index();
let entity = entity_id.id();
let mut attach = Attach {
associated_player: None,
parent: None,
children: [None, None, None, None],
};
if let Some(save) = save {
// attempt to decode the savefile
if let Ok(savefile) = unpack_savefile(&app_keys.app_key, save) {
// HEY! GHOSTLY! THIS SAVE FILE IS VALID! PLEASE LOAD IT!
// THANKS!
let children = load_savefile(
&mut commands,
transform,
entity,
entity,
savefile.children,
&mut attached_query,
&mut part_query,
&mut player_query,
&mut player_comp,
);
player_comp.energy = player_comp.energy_capacity;
attach.children = children;
} else {
let packet = Packet::Message {
message_type: packet::MessageType::Error,
actor: "SERVER".to_string(),
content: "Savefile signature corrupted or inner data invalid. Save was not loaded. Contact StarKingdoms staff for assistance.".to_string(),
};
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
}
} else {
// nothing to do
}
let mut entity_id = commands.entity(entity);
entity_id.insert(player_comp);
entity_id.insert(attach);
// tell this player the planets
let mut planets = Vec::new();
for (entity, planet_type, transform) in planet_query.iter() {
let translation = transform.translation;
planets.push((
entity.index(),
Planet {
planet_type: *planet_type,
transform: proto_transform!(Transform::from_translation(
translation * CLIENT_SCALE
)),
radius: match *planet_type {
PlanetType::Earth => {
planet!(PlanetType::Earth).size * CLIENT_SCALE
}
PlanetType::Moon => {
planet!(PlanetType::Moon).size * CLIENT_SCALE
}
PlanetType::Mars => {
planet!(PlanetType::Mars).size * CLIENT_SCALE
}
},
},
));
}
let packet = Packet::PlanetPositions { planets };
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
// tell the player already existing users
let mut players = Vec::new();
for (entity, player, _, _, _, _) in &player_query {
players.push((entity.index(), player.username.clone()));
}
let packet = Packet::PlayerList { players };
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
// tell other players that a player has spawned in
let packet = Packet::SpawnPlayer {
id,
username: username.to_string(),
};
event_queue.push(WsEvent::Broadcast {
message: packet.into(),
});
let packet = Packet::Message {
message_type: packet::MessageType::Server,
actor: "SERVER".to_string(),
content: format!("{} has joined the server!", username),
};
event_queue.push(WsEvent::Broadcast {
message: packet.into(),
});
// tell the player where parts are
let mut parts = Vec::new();
for (entity, part_type, transform, _, _, flags) in &part_query {
parts.push((
entity.index(),
Part {
part_type: *part_type,
transform: proto_transform!(Transform::from_translation(
transform.translation * CLIENT_SCALE
)),
flags: proto_part_flags!(flags),
},
));
}
for (entity, part_type, transform, _, _, _, _, flags) in &attached_query {
parts.push((
entity.index(),
Part {
part_type: *part_type,
transform: proto_transform!(Transform::from_translation(
transform.translation * CLIENT_SCALE
)),
flags: proto_part_flags!(flags),
},
));
}
parts.push((
id,
Part {
part_type: PartType::Hearty,
transform: proto_transform!(Transform::from_translation(
transform.translation
)
.with_rotation(transform.rotation)),
flags: ProtoPartFlags { attached: false },
},
));
let packet = Packet::PartPositions { parts };
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
// and send the welcome message :)
let packet = Packet::Message {
message_type: packet::MessageType::Server,
actor: "SERVER".to_string(),
content: format!(
"starkingdoms-server v{} says hello",
env!("CARGO_PKG_VERSION")
),
};
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
let packet = Packet::Message {
message_type: packet::MessageType::Server,
actor: "SERVER".to_string(),
content: "Welcome to StarKingdoms.IO! Have fun!".to_string(),
};
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
let packet = Packet::Message {
message_type: packet::MessageType::Server,
actor: "SERVER".to_string(),
content: "Found a bug? Have a feature request? Please bring this and all other feedback to the game's official Discord server! Join here: https://discord.gg/3u7Yw8DWtQ".to_string(),
};
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
}
Packet::SendMessage { target, content } => {
// find our player
let mut player = None;
for (_, q_player, _, _, _, _) in &player_query {
if q_player.addr == *from {
player = Some(q_player);
}
}
let player = player.unwrap();
if let Some(target_username) = target {
let mut target_player = None;
for (_, q_player, _, _, _, _) in &player_query {
if q_player.username == target_username {
target_player = Some(q_player);
}
}
let target_player = target_player.unwrap();
let packet = Packet::Message {
message_type: packet::MessageType::Direct,
actor: player.username.clone(),
content,
};
event_queue.push(WsEvent::Send {
to: target_player.addr,
message: packet.clone().into(),
});
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
} else {
// send to general chat
let packet = Packet::Message {
message_type: packet::MessageType::Chat,
actor: player.username.clone(),
content,
};
event_queue.push(WsEvent::Broadcast {
message: packet.into(),
});
}
}
Packet::PlayerInput {
up,
down,
left,
right,
} => {
for (_, mut q_player, _, _, _, _) in &mut player_query {
if q_player.addr == *from {
q_player.input.up = up;
q_player.input.down = down;
q_player.input.left = left;
q_player.input.right = right;
}
}
}
Packet::PlayerMouseInput {
x,
y,
released,
button: _,
} => {
let x = x / CLIENT_SCALE;
let y = y / CLIENT_SCALE;
for (entity, mut q_player, _transform, _velocity, _attach, _) in
&mut player_query
{
if q_player.addr == *from {
if released {
let select = if let Some(s) = q_player.selected {
s
} else {
break;
};
q_player.selected = None;
if attached_query.contains(select) {
let module = attached_query.get(select).unwrap();
let attach = module.3.clone();
let lost_energy_capacity = detach_recursive(
&mut commands,
module.0,
&mut attached_query,
&mut player_query,
);
let mut module = attached_query.get_mut(select).unwrap();
module.2.translation = vec3(x, y, 0.);
if *module.1 == PartType::LandingThruster {
let sub_entity = attach.children[2].unwrap();
let mut suspension =
attached_query.get_mut(sub_entity).unwrap();
suspension.2.translation = vec3(x, y, 0.);
}
let mut player = player_query.get_mut(entity).unwrap().1;
player.energy_capacity -= lost_energy_capacity;
player.energy =
std::cmp::min(player.energy, player.energy_capacity);
break;
}
if attach_on_module_tree(
x,
y,
&mut commands,
entity,
select,
entity,
&mut attached_query,
&mut part_query,
&mut player_query,
) {
let mut part = part_query.get_mut(select).unwrap();
part.5.attached = true; // all of this code is cursed. what the hell is it actually doing
break;
}
// move module to cursor since no attach
let mut part = part_query.get_mut(select).unwrap();
part.2.translation = vec3(x, y, 0.);
if *part.1 == PartType::LandingThruster {
if let Some(loose_attach) = part.4 {
let sub_entity = loose_attach.children[2].unwrap();
let mut part = part_query.get_mut(sub_entity).unwrap();
part.2.translation = vec3(x, y, 0.);
}
}
break;
}
for (m_entity, part_type, transform, m_attach, _velocity, _, _, _) in
&attached_query
{
if *part_type == PartType::LandingThrusterSuspension {
continue;
}
let pos = transform.translation;
let rel_x = pos.x - x;
let rel_y = pos.y - y;
let angle = -transform.rotation.z;
let x = rel_x * angle.cos() - rel_y * angle.sin();
let y = rel_x * angle.sin() + rel_y * angle.cos();
let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
if let PartType::Cargo = part_type {
bound = [-0.375, 0.375, -0.5, 0.4375];
}
if bound[0] < x
&& x < bound[1]
&& bound[2] < y
&& y < bound[3]
&& m_attach.associated_player.unwrap() == entity
{
q_player.selected = Some(m_entity);
break;
}
}
for (entity, part_type, transform, _, _, _) in &part_query {
if *part_type == PartType::LandingThrusterSuspension {
continue;
}
let pos = transform.translation;
let rel_x = pos.x - x;
let rel_y = pos.y - y;
let angle = -transform.rotation.z;
let x = rel_x * angle.cos() - rel_y * angle.sin();
let y = rel_x * angle.sin() + rel_y * angle.cos();
let mut bound = [-0.5, 0.5, -0.5, 0.5]; // left, right, top, bottom
if let PartType::Cargo = part_type {
bound = [-0.375, 0.375, -0.5, 0.4375];
}
if bound[0] < x && x < bound[1] && bound[2] < y && y < bound[3] {
q_player.selected = Some(entity);
break;
}
}
}
}
}
Packet::RequestSave { old_save } => {
for (_, q_player, _, _, attach, _) in &mut player_query {
if q_player.addr == *from {
// HEY! GHOSTLY! PLEASE FILL THIS STRUCT WITH DATA!
// THANKS!
let unused_modules = if let Some(ref old_save) = old_save {
if let Ok(old_savedata) =
unpack_savefile(&app_keys.app_key, old_save.to_string())
{
old_savedata.unused_modules
} else {
Vec::new()
}
} else {
Vec::new()
};
let save = SaveData {
children: construct_save_data(attach.clone(), &attached_query),
unused_modules,
};
let save_string = pack_savefile(&app_keys.app_key, save);
let packet = Packet::SaveData {
payload: save_string,
};
event_queue.push(WsEvent::Send {
to: *from,
message: packet.into(),
});
}
}
}
_ => continue,
}
}
}
for event in event_queue {
packet_event_send.send(event);
}
}
fn on_close(
player_query: Query<(Entity, &Player, &Attach)>,
attached_query: Query<&Attach, With<PartType>>,
part_query: Query<&PartType>,
mut commands: Commands,
mut packet_recv: Local<ManualEventReader<WsEvent>>,
mut packet_send: ResMut<Events<WsEvent>>,
) {
let mut packets = Vec::new();
for packet in packet_recv.read(&packet_send) {
if let WsEvent::Close { addr } = packet {
for (entity, player, attach) in &player_query {
if player.addr == *addr {
despawn_module_tree(
&mut commands,
attach,
&attached_query,
&part_query,
&mut packets,
);
commands.entity(entity).despawn_recursive();
let packet = Packet::PlayerLeave { id: entity.index() };
for (in_entity, player, _) in &player_query {
if entity != in_entity {
packets.push(WsEvent::Send {
to: player.addr,
message: packet.clone().into(),
});
}
}
}
}
}
}
for packet in packets {
packet_send.send(packet);
}
}
fn send_player_energy(player_query: Query<&Player>, mut packet_send: EventWriter<WsEvent>) {
for player in &player_query {
let packet = Packet::EnergyUpdate {
amount: player.energy,
max: player.energy_capacity,
};
packet_send.send(WsEvent::Send {
to: player.addr,
message: packet.into(),
});
}
}
fn on_position_change(
mut commands: Commands,
part_query: Query<(Entity, &PartType, &Transform, &PartFlags), Changed<Transform>>,
planet_query: Query<(Entity, &PlanetType, &Transform), Changed<Transform>>,
mut packet_send: EventWriter<WsEvent>,
) {
let mut updated_parts = Vec::new();
for (entity, part_type, transform, flags) in part_query.iter() {
let id = commands.entity(entity).id().index();
updated_parts.push((
id,
Part {
part_type: *part_type,
transform: proto_transform!(Transform::from_translation(
transform.translation * CLIENT_SCALE,
)
.with_rotation(transform.rotation)),
flags: proto_part_flags!(flags),
},
));
}
if !updated_parts.is_empty() {
let packet = Packet::PartPositions {
parts: updated_parts,
};
packet_send.send(WsEvent::Broadcast {
message: packet.into(),
});
}
let mut planets = Vec::new();
for (entity, planet_type, transform) in planet_query.iter() {
let id = commands.entity(entity).id().index();
planets.push((
id,
Planet {
planet_type: *planet_type,
transform: proto_transform!(Transform::from_translation(
transform.translation * CLIENT_SCALE
)),
radius: match *planet_type {
PlanetType::Earth => planet!(PlanetType::Earth).size * CLIENT_SCALE,
PlanetType::Moon => planet!(PlanetType::Moon).size * CLIENT_SCALE,
PlanetType::Mars => planet!(PlanetType::Mars).size * CLIENT_SCALE,
},
},
));
}
if !planets.is_empty() {
let packet = Packet::PlanetPositions { planets };
packet_send.send(WsEvent::Broadcast {
message: packet.into(),
});
}
}
fn player_input_update(
mut player_and_body_query: Query<(
Entity,
&mut Player,
&Attach,
&mut ExternalForce,
&Transform,
)>,
mut attached_query: Query<
(&Attach, &PartType, &mut ExternalForce, &Transform),
Without<Player>,
>,
) {
for (_, mut player, attach, mut forces, transform) in &mut player_and_body_query {
//forces.torque = 0.0;
//forces.force = Vec2::ZERO;
if !(player.input.up || player.input.down || player.input.right || player.input.left) {
continue;
}
let mut fmul_bottom_left_thruster: f32 = 0.0;
let mut fmul_bottom_right_thruster: f32 = 0.0;
let mut fmul_top_left_thruster: f32 = 0.0;
let mut fmul_top_right_thruster: f32 = 0.0;
if player.input.up {
fmul_bottom_left_thruster -= 1.0;
fmul_bottom_right_thruster -= 1.0;
}
if player.input.down {
fmul_top_left_thruster += 1.0;
fmul_top_right_thruster += 1.0;
}
if player.input.left {
fmul_top_left_thruster += 1.0;
fmul_bottom_right_thruster -= 1.0;
}
if player.input.right {
fmul_top_right_thruster += 1.0;
fmul_bottom_left_thruster -= 1.0;
}
fmul_top_left_thruster = fmul_top_left_thruster.clamp(-1.0, 1.0);
fmul_top_right_thruster = fmul_top_right_thruster.clamp(-1.0, 1.0);
fmul_bottom_left_thruster = fmul_bottom_left_thruster.clamp(-1.0, 1.0);
fmul_bottom_right_thruster = fmul_bottom_right_thruster.clamp(-1.0, 1.0);
if player.input.up {
fmul_bottom_left_thruster -= 2.0;
fmul_bottom_right_thruster -= 2.0;
}
if player.input.down {
fmul_top_left_thruster += 2.0;
fmul_top_right_thruster += 2.0;
}
let rot = transform.rotation.to_euler(EulerRot::ZYX).0;
let thrusters = [
(fmul_bottom_left_thruster, -PART_HALF_SIZE, -PART_HALF_SIZE),
(fmul_bottom_right_thruster, PART_HALF_SIZE, -PART_HALF_SIZE),
(fmul_top_left_thruster, -PART_HALF_SIZE, PART_HALF_SIZE),
(fmul_top_right_thruster, PART_HALF_SIZE, PART_HALF_SIZE),
];
for (force_multiplier, x_offset, y_offset) in thrusters {
if force_multiplier != 0.0 && player.energy >= part!(PartType::Hearty).thruster_energy {
player.energy -= part!(PartType::Hearty).thruster_energy;
let thruster_pos_uncast = vec2(x_offset, y_offset);
let thruster_pos_cast =
rot2d(thruster_pos_uncast, rot) + transform.translation.xy();
let thruster_force = force_multiplier * part!(PartType::Hearty).thruster_force;
let thruster_vec = vec2(-thruster_force * rot.sin(), thruster_force * rot.cos());
let thruster_force = ExternalForce::at_point(
thruster_vec,
thruster_pos_cast,
transform.translation.xy(),
);
forces.force += thruster_force.force;
forces.torque += thruster_force.torque;
}
}
// change to support other thruster types later
if player.energy >= part!(PartType::LandingThruster).thruster_energy {
search_thrusters(
player.input,
attach.clone(),
*transform,
&mut player.energy,
&mut attached_query,
);
}
}
}
fn gravity_update(
mut part_query: Query<
(
&Transform,
&ReadMassProperties,
&mut ExternalForce,
&mut ExternalImpulse,
),
With<PartType>,
>,
planet_query: Query<(&Transform, &ReadMassProperties), With<PlanetType>>,
server_config: Res<StkConfig>,
) {
for (part_transform, part_mp, mut forces, mut impulses) in &mut part_query {
impulses.impulse = Vec2::ZERO;
forces.force = Vec2::ZERO;
forces.torque = 0.;
let part_mp = part_mp.get();
let part_mass = part_mp.mass;
let part_translate = part_transform.translation;
for (planet_transform, planet_mp) in &planet_query {
let planet_mp = planet_mp.get();
let planet_mass = planet_mp.mass;
let planet_translate = planet_transform.translation;
let distance = planet_translate.distance(part_translate);
let force =
server_config.world.gravity * ((part_mass * planet_mass) / (distance * distance));
let direction = (planet_translate - part_translate).normalize() * force;
/*let gravity_force = ExternalForce::at_point(
direction.xy(),
part_transform.translation.xy(),
part_transform.translation.xy(),
);
forces.force += gravity_force.force;
forces.torque += gravity_force.torque;*/
impulses.impulse += direction.xy();
}
}
}