TMP
This commit is contained in:
parent
3150f64bd1
commit
1bea5c777c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
*.ignore
|
||||||
|
1395
Cargo.lock
generated
1395
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@ -104,6 +104,26 @@ redundant_feature_names = "deny"
|
|||||||
#
|
#
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
galactica-util = { path = "lib/util" }
|
||||||
|
galactica-packer = { path = "lib/packer" }
|
||||||
|
galactica-content = { path = "lib/content" }
|
||||||
|
|
||||||
|
# Utilities
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
thiserror = "2.0.9"
|
thiserror = "2.0.9"
|
||||||
|
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
crossbeam = "0.8.4"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
# I/O
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
toml = "0.8.19"
|
||||||
|
image = { version = "0.25.5", features = ["png"] }
|
||||||
|
|
||||||
|
# Math
|
||||||
|
nalgebra = "0.33.2"
|
||||||
|
rapier2d = "0.22.0"
|
||||||
|
|
||||||
|
rhai = { version = "1.20.1", features = ["f32_float", "no_custom_syntax"] }
|
||||||
|
18
galac.toml
Normal file
18
galac.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[workspace.dependencies]
|
||||||
|
galactica-content = { path = "crates/content" }
|
||||||
|
galactica-render = { path = "crates/render" }
|
||||||
|
galactica-system = { path = "crates/system" }
|
||||||
|
galactica-playeragent = { path = "crates/playeragent" }
|
||||||
|
galactica = { path = "crates/galactica" }
|
||||||
|
|
||||||
|
image = { version = "0.24", features = ["png"] }
|
||||||
|
winit = "0.28"
|
||||||
|
wgpu = "0.18"
|
||||||
|
bytemuck = { version = "1.12", features = ["derive"] }
|
||||||
|
rapier2d = { version = "0.17.2" }
|
||||||
|
crossbeam = "0.8.3"
|
||||||
|
pollster = "0.3"
|
||||||
|
rand = "0.8.5"
|
||||||
|
glyphon = "0.4.1"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
smartstring = { version = "1.0.1" }
|
26
lib/system/Cargo.toml
Normal file
26
lib/system/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "galactica-system"
|
||||||
|
description = "Galactica's star system simulations"
|
||||||
|
categories = { workspace = true }
|
||||||
|
keywords = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
homepage = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
documentation = { workspace = true }
|
||||||
|
readme = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
galactica-content = { workspace = true }
|
||||||
|
galactica-util = { workspace = true }
|
||||||
|
|
||||||
|
rapier2d = { workspace = true }
|
||||||
|
nalgebra = { workspace = true }
|
||||||
|
crossbeam = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
193
lib/system/src/instance/effect.rs
Normal file
193
lib/system/src/instance/effect.rs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
use galactica_content::{Effect, EffectVelocity, SpriteAutomaton};
|
||||||
|
use nalgebra::{Point2, Rotation2, Vector2};
|
||||||
|
use rand::Rng;
|
||||||
|
use rapier2d::dynamics::{RevoluteJointBuilder, RigidBodyBuilder, RigidBodyHandle, RigidBodyType};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::physwrapper::PhysWrapper;
|
||||||
|
|
||||||
|
/// An instance of an effect in a simulation
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EffectInstance {
|
||||||
|
/// The sprite to use for this effect
|
||||||
|
pub anim: SpriteAutomaton,
|
||||||
|
|
||||||
|
/// This effect's velocity, in world coordinates
|
||||||
|
pub rigid_body: RigidBodyHandle,
|
||||||
|
|
||||||
|
/// This effect's lifetime, in seconds
|
||||||
|
lifetime: f32,
|
||||||
|
|
||||||
|
/// The size of this effect,
|
||||||
|
/// given as height in world units.
|
||||||
|
pub size: f32,
|
||||||
|
|
||||||
|
/// Fade this effect over this many seconds as it expires
|
||||||
|
pub fade: f32,
|
||||||
|
|
||||||
|
/// If true, this effect has been destroyed,
|
||||||
|
/// and needs to be removed from the game.
|
||||||
|
is_destroyed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectInstance {
|
||||||
|
/// Create a new effect inside `Wrapper`
|
||||||
|
pub fn new(
|
||||||
|
wrapper: &mut PhysWrapper,
|
||||||
|
effect: Arc<Effect>,
|
||||||
|
// Where to spawn the particle, in world space.
|
||||||
|
pos: Vector2<f32>,
|
||||||
|
parent: RigidBodyHandle,
|
||||||
|
target: Option<RigidBodyHandle>,
|
||||||
|
) -> Self {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let parent_body = wrapper.get_rigid_body(parent).unwrap();
|
||||||
|
let parent_angle = parent_body.rotation().angle();
|
||||||
|
let parent_pos = *parent_body.translation();
|
||||||
|
let parent_velocity = parent_body.velocity_at_point(parent_body.center_of_mass());
|
||||||
|
|
||||||
|
let angvel = if effect.angvel_rng == 0.0 {
|
||||||
|
effect.angvel
|
||||||
|
} else {
|
||||||
|
effect.angvel + rng.gen_range(-effect.angvel_rng..=effect.angvel_rng)
|
||||||
|
};
|
||||||
|
let angle = if effect.angle_rng == 0.0 {
|
||||||
|
parent_angle + effect.angle
|
||||||
|
} else {
|
||||||
|
parent_angle + effect.angle + rng.gen_range(-effect.angle_rng..=effect.angle_rng)
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_velocity = {
|
||||||
|
if let Some(target) = target {
|
||||||
|
let target_body = wrapper.get_rigid_body(target).unwrap();
|
||||||
|
target_body.velocity_at_point(&Point2::new(pos.x, pos.y))
|
||||||
|
} else {
|
||||||
|
Vector2::new(0.0, 0.0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match effect.velocity {
|
||||||
|
EffectVelocity::StickyTarget | EffectVelocity::StickyParent => {
|
||||||
|
let rigid_body = wrapper.insert_rigid_body(
|
||||||
|
RigidBodyBuilder::new(RigidBodyType::Dynamic)
|
||||||
|
.additional_mass(f32::MIN_POSITIVE)
|
||||||
|
.position(pos.into())
|
||||||
|
.rotation(angle)
|
||||||
|
.angvel(angvel)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match effect.velocity {
|
||||||
|
EffectVelocity::StickyParent => {
|
||||||
|
let d = Rotation2::new(-parent_angle) * (pos - parent_pos);
|
||||||
|
|
||||||
|
wrapper.add_joint(
|
||||||
|
rigid_body,
|
||||||
|
parent,
|
||||||
|
RevoluteJointBuilder::new()
|
||||||
|
.local_anchor1(Point2::new(0.0, 0.0))
|
||||||
|
.local_anchor2(Point2::new(d.x, d.y)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
EffectVelocity::StickyTarget => {
|
||||||
|
if target.is_some() {
|
||||||
|
let target_body = wrapper.get_rigid_body(target.unwrap()).unwrap();
|
||||||
|
|
||||||
|
// Correct for rotation, since joint coordinates are relative
|
||||||
|
// and input coordinates are in world space.
|
||||||
|
let d = Rotation2::new(-target_body.rotation().angle())
|
||||||
|
* (pos - target_body.translation());
|
||||||
|
|
||||||
|
wrapper.add_joint(
|
||||||
|
rigid_body,
|
||||||
|
target.unwrap(),
|
||||||
|
RevoluteJointBuilder::new()
|
||||||
|
.local_anchor1(Point2::new(0.0, 0.0))
|
||||||
|
.local_anchor2(Point2::new(d.x, d.y)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("lol what?"),
|
||||||
|
};
|
||||||
|
|
||||||
|
EffectInstance {
|
||||||
|
anim: SpriteAutomaton::new(effect.sprite.clone()),
|
||||||
|
rigid_body,
|
||||||
|
lifetime: 0f32.max(
|
||||||
|
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Make sure size isn't negative. This check should be on EVERY rng!
|
||||||
|
size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)),
|
||||||
|
fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)),
|
||||||
|
is_destroyed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EffectVelocity::Explicit {
|
||||||
|
scale_parent,
|
||||||
|
scale_parent_rng,
|
||||||
|
scale_target,
|
||||||
|
scale_target_rng,
|
||||||
|
direction_rng,
|
||||||
|
} => {
|
||||||
|
let velocity = {
|
||||||
|
let a = rng.gen_range(-scale_parent_rng..=scale_parent_rng);
|
||||||
|
let b = rng.gen_range(-scale_target_rng..=scale_target_rng);
|
||||||
|
|
||||||
|
let velocity = ((scale_parent + a) * parent_velocity)
|
||||||
|
+ ((scale_target + b) * target_velocity);
|
||||||
|
|
||||||
|
Rotation2::new(rng.gen_range(-direction_rng..=direction_rng)) * velocity
|
||||||
|
};
|
||||||
|
|
||||||
|
let rigid_body = wrapper.insert_rigid_body(
|
||||||
|
RigidBodyBuilder::new(RigidBodyType::KinematicVelocityBased)
|
||||||
|
.position(pos.into())
|
||||||
|
.rotation(angle)
|
||||||
|
.angvel(angvel)
|
||||||
|
.linvel(velocity)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
EffectInstance {
|
||||||
|
anim: SpriteAutomaton::new(effect.sprite.clone()),
|
||||||
|
rigid_body,
|
||||||
|
lifetime: 0f32.max(
|
||||||
|
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Make sure size isn't negative. This check should be on EVERY rng!
|
||||||
|
size: 0f32.max(effect.size + rng.gen_range(-effect.size_rng..=effect.size_rng)),
|
||||||
|
fade: 0f32.max(effect.fade + rng.gen_range(-effect.fade_rng..=effect.fade_rng)),
|
||||||
|
is_destroyed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step this effect's state by `dt` seconds
|
||||||
|
pub fn step(&mut self, dt: f32, wrapper: &mut PhysWrapper) {
|
||||||
|
if self.is_destroyed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.anim.step(dt);
|
||||||
|
self.lifetime -= dt;
|
||||||
|
|
||||||
|
if self.lifetime <= 0.0 {
|
||||||
|
wrapper.remove_rigid_body(self.rigid_body);
|
||||||
|
self.is_destroyed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should this effect be deleted?
|
||||||
|
pub fn is_destroyed(&self) -> bool {
|
||||||
|
self.is_destroyed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The remaining lifetime of this effect, in seconds
|
||||||
|
pub fn remaining_lifetime(&self) -> f32 {
|
||||||
|
self.lifetime
|
||||||
|
}
|
||||||
|
}
|
8
lib/system/src/instance/mod.rs
Normal file
8
lib/system/src/instance/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//! This module contains game objects that may interact with the physics engine.
|
||||||
|
|
||||||
|
mod effect;
|
||||||
|
mod projectile;
|
||||||
|
pub mod ship;
|
||||||
|
|
||||||
|
pub use effect::EffectInstance;
|
||||||
|
pub use projectile::ProjectileInstance;
|
118
lib/system/src/instance/projectile.rs
Normal file
118
lib/system/src/instance/projectile.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use galactica_content::{AnimationState, Faction, Projectile, SpriteAutomaton};
|
||||||
|
use rand::Rng;
|
||||||
|
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
|
||||||
|
|
||||||
|
use crate::{physwrapper::PhysWrapper, NewObjects};
|
||||||
|
|
||||||
|
use super::EffectInstance;
|
||||||
|
|
||||||
|
/// An instance of a projectile in a simulation
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProjectileInstance {
|
||||||
|
/// This projectile's game data
|
||||||
|
pub content: Arc<Projectile>,
|
||||||
|
|
||||||
|
/// This projectile's sprite animation state
|
||||||
|
anim: SpriteAutomaton,
|
||||||
|
|
||||||
|
/// The remaining lifetime of this projectile, in seconds
|
||||||
|
lifetime: f32,
|
||||||
|
|
||||||
|
/// The faction this projectile belongs to
|
||||||
|
pub faction: Arc<Faction>,
|
||||||
|
|
||||||
|
/// This projectile's rigidbody
|
||||||
|
pub rigid_body: RigidBodyHandle,
|
||||||
|
|
||||||
|
/// This projectile's collider
|
||||||
|
pub collider: ColliderHandle,
|
||||||
|
|
||||||
|
/// This projectile's size variation
|
||||||
|
pub size_rng: f32,
|
||||||
|
|
||||||
|
/// If true, this projectile has been destroyed
|
||||||
|
/// and is waiting to be deallocated.
|
||||||
|
is_destroyed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectileInstance {
|
||||||
|
/// Create a new projectile
|
||||||
|
pub fn new(
|
||||||
|
content: Arc<Projectile>,
|
||||||
|
rigid_body: RigidBodyHandle,
|
||||||
|
faction: Arc<Faction>,
|
||||||
|
collider: ColliderHandle,
|
||||||
|
) -> Self {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let size_rng = content.size_rng;
|
||||||
|
let lifetime = content.lifetime;
|
||||||
|
ProjectileInstance {
|
||||||
|
anim: SpriteAutomaton::new(content.sprite.clone()),
|
||||||
|
rigid_body,
|
||||||
|
collider,
|
||||||
|
content,
|
||||||
|
lifetime,
|
||||||
|
faction,
|
||||||
|
size_rng: rng.gen_range(-size_rng..=size_rng),
|
||||||
|
is_destroyed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process this projectile's state after `t` seconds
|
||||||
|
// pub in phys
|
||||||
|
pub(crate) fn step(&mut self, dt: f32, new: &mut NewObjects, wrapper: &mut PhysWrapper) {
|
||||||
|
self.lifetime -= dt;
|
||||||
|
self.anim.step(dt);
|
||||||
|
|
||||||
|
if self.lifetime <= 0.0 {
|
||||||
|
self.destroy(wrapper, new, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroy this projectile without creating an expire effect
|
||||||
|
// pub in phys
|
||||||
|
pub(crate) fn destroy_silent(&mut self, new: &mut NewObjects, wrapper: &mut PhysWrapper) {
|
||||||
|
self.destroy(wrapper, new, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroy this projectile
|
||||||
|
fn destroy(&mut self, wrapper: &mut PhysWrapper, new: &mut NewObjects, expire: bool) {
|
||||||
|
if self.is_destroyed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rb = wrapper.get_rigid_body(self.rigid_body).unwrap();
|
||||||
|
if expire {
|
||||||
|
match &self.content.expire_effect {
|
||||||
|
None => {}
|
||||||
|
Some(effect) => {
|
||||||
|
new.effects.push(EffectInstance::new(
|
||||||
|
wrapper,
|
||||||
|
effect.clone(),
|
||||||
|
*rb.translation(),
|
||||||
|
self.rigid_body,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.is_destroyed = true;
|
||||||
|
wrapper.remove_rigid_body(self.rigid_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should this effect be deleted?
|
||||||
|
// pub in phys
|
||||||
|
pub fn should_remove(&self) -> bool {
|
||||||
|
self.is_destroyed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectileInstance {
|
||||||
|
/// Get this projectile's animation state
|
||||||
|
pub fn get_anim_state(&self) -> AnimationState {
|
||||||
|
self.anim.get_texture_idx()
|
||||||
|
}
|
||||||
|
}
|
40
lib/system/src/instance/ship/autopilot.rs
Normal file
40
lib/system/src/instance/ship/autopilot.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use galactica_util::{clockwise_angle, to_radians};
|
||||||
|
use nalgebra::Vector2;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
shipagent::ShipControls,
|
||||||
|
stateframe::{PhysSimShipHandle, SystemStateframe},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: no wobble
|
||||||
|
// TODO: slow down when near planet
|
||||||
|
// TODO: avoid obstacles
|
||||||
|
|
||||||
|
/// Land this ship on the given object
|
||||||
|
pub fn auto_landing(
|
||||||
|
sf: &SystemStateframe,
|
||||||
|
this_ship: PhysSimShipHandle,
|
||||||
|
target_pos: Vector2<f32>,
|
||||||
|
) -> Option<ShipControls> {
|
||||||
|
let rigid_body = &sf.get_ship(&this_ship).unwrap().rigidbody;
|
||||||
|
let my_pos = *rigid_body.translation();
|
||||||
|
let my_rot = rigid_body.rotation() * Vector2::new(1.0, 0.0);
|
||||||
|
let my_vel = rigid_body.linvel();
|
||||||
|
let my_angvel = rigid_body.angvel();
|
||||||
|
let v_t = target_pos - my_pos; // Vector to target
|
||||||
|
let v_d = v_t - my_vel; // Desired thrust vector
|
||||||
|
let angle_delta = clockwise_angle(&my_rot, &v_d);
|
||||||
|
let mut controls = ShipControls::new();
|
||||||
|
|
||||||
|
if angle_delta < 0.0 && my_angvel > -0.3 {
|
||||||
|
controls.right = true;
|
||||||
|
} else if angle_delta > 0.0 && my_angvel < 0.3 {
|
||||||
|
controls.left = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if my_rot.angle(&v_d) <= to_radians(15.0) {
|
||||||
|
controls.thrust = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(controls);
|
||||||
|
}
|
141
lib/system/src/instance/ship/collapse.rs
Normal file
141
lib/system/src/instance/ship/collapse.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use galactica_content::{CollapseEvent, Ship};
|
||||||
|
use nalgebra::{Point2, Vector2};
|
||||||
|
use rand::{rngs::ThreadRng, Rng};
|
||||||
|
use rapier2d::{
|
||||||
|
dynamics::RigidBodyHandle,
|
||||||
|
geometry::{Collider, ColliderHandle},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{instance::EffectInstance, physwrapper::PhysWrapper, NewObjects};
|
||||||
|
|
||||||
|
use super::data::ShipData;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShipCollapseSequence {
|
||||||
|
rng: ThreadRng,
|
||||||
|
|
||||||
|
/// The total length of this collapse sequence
|
||||||
|
total_length: f32,
|
||||||
|
|
||||||
|
/// How many seconds we've spent playing this sequence
|
||||||
|
elapsed: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipCollapseSequence {
|
||||||
|
pub(super) fn new(total_length: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
rng: rand::thread_rng(),
|
||||||
|
total_length,
|
||||||
|
elapsed: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Has this collapse sequence fully played out?
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
self.elapsed >= self.total_length
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick a random points inside a ship's collider
|
||||||
|
fn random_in_ship(&mut self, ship: &Ship, collider: &Collider) -> Vector2<f32> {
|
||||||
|
let mut y = 0.0;
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut a = false;
|
||||||
|
while !a {
|
||||||
|
x = self.rng.gen_range(-1.0..=1.0) * ship.size / 2.0;
|
||||||
|
y = self.rng.gen_range(-1.0..=1.0) * ship.size * ship.sprite.aspect / 2.0;
|
||||||
|
a = collider.shape().contains_local_point(&Point2::new(x, y));
|
||||||
|
}
|
||||||
|
Vector2::new(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step this sequence `t` seconds
|
||||||
|
pub(super) fn step(
|
||||||
|
&mut self,
|
||||||
|
dt: f32,
|
||||||
|
wrapper: &mut PhysWrapper,
|
||||||
|
new: &mut NewObjects,
|
||||||
|
ship_data: &ShipData,
|
||||||
|
rigid_body_handle: RigidBodyHandle,
|
||||||
|
collider_handle: ColliderHandle,
|
||||||
|
) {
|
||||||
|
let rigid_body = wrapper.get_rigid_body(rigid_body_handle).unwrap().clone();
|
||||||
|
let collider = wrapper.get_collider(collider_handle).unwrap().clone();
|
||||||
|
let ship_content = ship_data.get_content();
|
||||||
|
let ship_pos = rigid_body.translation();
|
||||||
|
let ship_rot = rigid_body.rotation();
|
||||||
|
|
||||||
|
// The fraction of this collapse sequence that has been played
|
||||||
|
let frac_done = self.elapsed / self.total_length;
|
||||||
|
|
||||||
|
// TODO: slight random offset for event effects
|
||||||
|
|
||||||
|
// Trigger collapse events
|
||||||
|
for event in &ship_content.collapse.events {
|
||||||
|
match event {
|
||||||
|
CollapseEvent::Effect(event) => {
|
||||||
|
if (event.time > self.elapsed && event.time <= self.elapsed + dt)
|
||||||
|
|| (event.time == 0.0 && self.elapsed == 0.0)
|
||||||
|
// ^^ Don't miss events scheduled at the very start of the sequence!
|
||||||
|
{
|
||||||
|
for spawner in &event.effects {
|
||||||
|
for _ in 0..spawner.count as usize {
|
||||||
|
let pos: Vector2<f32> = if let Some(pos) = spawner.pos {
|
||||||
|
Vector2::new(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
self.random_in_ship(&ship_content, &collider)
|
||||||
|
};
|
||||||
|
let pos = ship_pos + (ship_rot * pos);
|
||||||
|
|
||||||
|
new.effects.push(EffectInstance::new(
|
||||||
|
wrapper,
|
||||||
|
spawner.effect.clone(),
|
||||||
|
pos,
|
||||||
|
rigid_body_handle,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create collapse effects
|
||||||
|
for spawner in &ship_content.collapse.effects {
|
||||||
|
// Probability of adding an effect instance this frame.
|
||||||
|
// The area of this function over [0, 1] should be 1.
|
||||||
|
let pdf = |x: f32| {
|
||||||
|
let f = 0.2;
|
||||||
|
let y = if x < (1.0 - f) {
|
||||||
|
let x = x / (1.0 - f);
|
||||||
|
(x * x + 0.2) * 1.8 - f
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
|
let p_add = (dt / self.total_length) * pdf(frac_done) * spawner.count;
|
||||||
|
|
||||||
|
if self.rng.gen_range(0.0..=1.0) <= p_add {
|
||||||
|
let pos = if let Some(pos) = spawner.pos {
|
||||||
|
Vector2::new(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
self.random_in_ship(&ship_content, &collider)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Position, adjusted for ship rotation
|
||||||
|
let pos = ship_pos + (ship_rot * pos);
|
||||||
|
new.effects.push(EffectInstance::new(
|
||||||
|
wrapper,
|
||||||
|
spawner.effect.clone(),
|
||||||
|
pos,
|
||||||
|
rigid_body_handle,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.elapsed += dt;
|
||||||
|
}
|
||||||
|
}
|
7
lib/system/src/instance/ship/data/mod.rs
Normal file
7
lib/system/src/instance/ship/data/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
mod outfitset;
|
||||||
|
mod ship;
|
||||||
|
mod shipstate;
|
||||||
|
|
||||||
|
pub use outfitset::*;
|
||||||
|
pub use ship::*;
|
||||||
|
pub use shipstate::*;
|
241
lib/system/src/instance/ship/data/outfitset.rs
Normal file
241
lib/system/src/instance/ship/data/outfitset.rs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use galactica_content::{ContentIndex, GunPoint, Outfit, OutfitSpace, OutfitStats};
|
||||||
|
|
||||||
|
/// Possible outcomes when adding an outfit
|
||||||
|
pub enum OutfitAddResult {
|
||||||
|
/// An outfit was successfully added
|
||||||
|
Ok,
|
||||||
|
|
||||||
|
/// An outfit could not be added because we don't have enough free space.
|
||||||
|
/// The string tells us what kind of space we need:
|
||||||
|
/// `outfit,` `weapon,` `engine,` etc. Note that these sometimes overlap:
|
||||||
|
/// outfits may need outfit AND weapon space. In these cases, this result
|
||||||
|
/// should name the "most specific" kind of space we lack.
|
||||||
|
NotEnoughSpace(String),
|
||||||
|
|
||||||
|
/// An outfit couldn't be added because there weren't enough points for it
|
||||||
|
/// (e.g, gun points, turret points, etc)
|
||||||
|
NotEnoughPoints(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible outcomes when removing an outfit
|
||||||
|
pub enum OutfitRemoveResult {
|
||||||
|
/// This outfit was successfully removed
|
||||||
|
Ok,
|
||||||
|
|
||||||
|
/// This outfit isn't in this set
|
||||||
|
NotExist,
|
||||||
|
// TODO:
|
||||||
|
// This is where we'll add non-removable outfits,
|
||||||
|
// outfits that provide space, etc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple data class, used to keep track of delayed shield generators
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct ShieldGenerator {
|
||||||
|
pub outfit: Arc<Outfit>,
|
||||||
|
pub delay: f32,
|
||||||
|
pub generation: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct keeps track of a ship's outfit loadout.
|
||||||
|
/// This is a fairly static data structure: it does not keep track of cooldowns,
|
||||||
|
/// shield damage, etc. It only provides an interface for static stats which are
|
||||||
|
/// then used elsewhere.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OutfitSet {
|
||||||
|
/// What outfits does this statsum contain?
|
||||||
|
outfits: HashMap<ContentIndex, (Arc<Outfit>, u32)>,
|
||||||
|
|
||||||
|
/// Space available in this outfitset.
|
||||||
|
/// set at creation and never changes.
|
||||||
|
total_space: OutfitSpace,
|
||||||
|
|
||||||
|
/// Space used by the outfits in this set.
|
||||||
|
/// This may be negative if certain outfits provide space!
|
||||||
|
used_space: OutfitSpace,
|
||||||
|
|
||||||
|
/// The gun points available in this ship.
|
||||||
|
/// If value is None, this point is free.
|
||||||
|
/// if value is Some, this point is taken.
|
||||||
|
gun_points: HashMap<GunPoint, Option<Arc<Outfit>>>,
|
||||||
|
|
||||||
|
/// The combined stats of all outfits in this set.
|
||||||
|
/// There are two things to note here:
|
||||||
|
/// First, shield_delay is always zero. That is handled
|
||||||
|
/// seperately, since it is different for every outfit.
|
||||||
|
/// Second, shield_generation represents the MAXIMUM POSSIBLE
|
||||||
|
/// shield generation, after all delays have expired.
|
||||||
|
///
|
||||||
|
/// Note that this field isn't strictly necessary... we could compute stats
|
||||||
|
/// by iterating over the outfits in this set. We don't want to do this every
|
||||||
|
/// frame, though, so we keep track of the total sum here.
|
||||||
|
stats: OutfitStats,
|
||||||
|
|
||||||
|
/// All shield generators in this outfit set
|
||||||
|
// These can't be summed into one value, since each has a
|
||||||
|
// distinct delay.
|
||||||
|
shield_generators: Vec<ShieldGenerator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutfitSet {
|
||||||
|
pub(super) fn new(available_space: OutfitSpace, gun_points: &[GunPoint]) -> Self {
|
||||||
|
Self {
|
||||||
|
outfits: HashMap::new(),
|
||||||
|
total_space: available_space,
|
||||||
|
used_space: OutfitSpace::new(),
|
||||||
|
gun_points: gun_points.iter().map(|x| (x.clone(), None)).collect(),
|
||||||
|
stats: OutfitStats::zero(),
|
||||||
|
shield_generators: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add(&mut self, o: &Arc<Outfit>) -> OutfitAddResult {
|
||||||
|
if !(self.total_space - self.used_space).can_contain(&o.space) {
|
||||||
|
return OutfitAddResult::NotEnoughSpace("TODO".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and return as fast as possible,
|
||||||
|
// BEFORE we make any changes.
|
||||||
|
if o.gun.is_some() {
|
||||||
|
let mut added = false;
|
||||||
|
for (_, outfit) in &mut self.gun_points {
|
||||||
|
if outfit.is_none() {
|
||||||
|
*outfit = Some(o.clone());
|
||||||
|
added = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !added {
|
||||||
|
return OutfitAddResult::NotEnoughPoints("gun".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.used_space += o.space;
|
||||||
|
self.stats.add(&o.stats);
|
||||||
|
|
||||||
|
if o.stats.shield_generation != 0.0 {
|
||||||
|
self.shield_generators.push(ShieldGenerator {
|
||||||
|
outfit: o.clone(),
|
||||||
|
delay: o.stats.shield_delay,
|
||||||
|
generation: o.stats.shield_generation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.outfits.contains_key(&o.index) {
|
||||||
|
self.outfits.get_mut(&o.index).unwrap().1 += 1;
|
||||||
|
} else {
|
||||||
|
self.outfits.insert(o.index.clone(), (o.clone(), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutfitAddResult::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn remove(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
|
||||||
|
if !self.outfits.contains_key(&o.index) {
|
||||||
|
return OutfitRemoveResult::NotExist;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = self.outfits.get_mut(&o.index).unwrap();
|
||||||
|
if n.1 == 1u32 {
|
||||||
|
self.outfits.remove(&o.index);
|
||||||
|
} else {
|
||||||
|
self.outfits.get_mut(&o.index).unwrap().1 -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.gun.is_some() {
|
||||||
|
let (_, x) = self
|
||||||
|
.gun_points
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(_, x)| x.is_some() && x.as_ref().unwrap().index == o.index)
|
||||||
|
.unwrap();
|
||||||
|
*x = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.used_space -= o.space;
|
||||||
|
self.stats.subtract(&o.stats);
|
||||||
|
|
||||||
|
let index = self
|
||||||
|
.shield_generators
|
||||||
|
.iter()
|
||||||
|
.position(|g| g.outfit.index == o.index);
|
||||||
|
if let Some(index) = index {
|
||||||
|
self.shield_generators.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutfitRemoveResult::Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple getters to make sure nobody meddles with our internal state
|
||||||
|
impl OutfitSet {
|
||||||
|
/// The number of outfits in this set
|
||||||
|
pub fn len(&self) -> u32 {
|
||||||
|
self.outfits.iter().map(|(_, (_, x))| x).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all outfits
|
||||||
|
pub fn iter_outfits(&self) -> impl Iterator<Item = &(Arc<Outfit>, u32)> {
|
||||||
|
self.outfits.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all gun points
|
||||||
|
pub fn iter_gun_points(&self) -> impl Iterator<Item = (&GunPoint, &Option<Arc<Outfit>>)> {
|
||||||
|
self.gun_points.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all shield generators
|
||||||
|
pub(crate) fn iter_shield_generators(&self) -> impl Iterator<Item = &ShieldGenerator> {
|
||||||
|
self.shield_generators.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the outfit attached to the given gun point
|
||||||
|
/// Will panic if this gunpoint is not in this outfit set.
|
||||||
|
pub fn get_gun(&self, point: &GunPoint) -> Option<Arc<Outfit>> {
|
||||||
|
self.gun_points.get(point).unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total available outfit space
|
||||||
|
pub fn get_total_space(&self) -> &OutfitSpace {
|
||||||
|
&self.total_space
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used outfit space
|
||||||
|
pub fn get_used_space(&self) -> &OutfitSpace {
|
||||||
|
&self.used_space
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of available (used & free) gun points
|
||||||
|
pub fn total_gun_points(&self) -> usize {
|
||||||
|
self.gun_points.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of free gun points
|
||||||
|
pub fn free_gun_points(&self) -> usize {
|
||||||
|
self.iter_gun_points().filter(|(_, o)| o.is_none()).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does this set contain `count` of `outfit`?
|
||||||
|
pub fn has_outfit(&self, outfit: &Arc<Outfit>, mut count: u32) -> bool {
|
||||||
|
for i in self.iter_outfits() {
|
||||||
|
if count <= 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if i.0.index == outfit.index {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the combined stats of all outfits in this set.
|
||||||
|
/// There are two things to note here:
|
||||||
|
/// First, shield_delay is always zero. That is handled
|
||||||
|
/// seperately, since it is different for every outfit.
|
||||||
|
/// Second, shield_generation represents the MAXIMUM POSSIBLE
|
||||||
|
/// shield generation, after all delays have expired.
|
||||||
|
pub fn get_stats(&self) -> &OutfitStats {
|
||||||
|
&self.stats
|
||||||
|
}
|
||||||
|
}
|
287
lib/system/src/instance/ship/data/ship.rs
Normal file
287
lib/system/src/instance/ship/data/ship.rs
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
use galactica_content::{Faction, GunPoint, Outfit, Ship, SystemObject};
|
||||||
|
use nalgebra::Isometry2;
|
||||||
|
use rand::{rngs::ThreadRng, Rng};
|
||||||
|
use std::{collections::HashMap, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use super::{OutfitSet, ShipState};
|
||||||
|
|
||||||
|
/// Represents all attributes of a single ship
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShipData {
|
||||||
|
// Metadata values
|
||||||
|
ship: Arc<Ship>,
|
||||||
|
faction: Arc<Faction>,
|
||||||
|
outfits: OutfitSet,
|
||||||
|
|
||||||
|
/// Ship state machine. Keeps track of all possible ship state.
|
||||||
|
/// TODO: document this, draw a graph
|
||||||
|
state: ShipState,
|
||||||
|
|
||||||
|
// State values
|
||||||
|
// TODO: unified ship stats struct, like outfit space
|
||||||
|
hull: f32,
|
||||||
|
shields: f32,
|
||||||
|
|
||||||
|
// TODO: index by number?
|
||||||
|
gun_cooldowns: HashMap<GunPoint, f32>,
|
||||||
|
rng: ThreadRng,
|
||||||
|
|
||||||
|
// Utility values
|
||||||
|
/// The last time this ship was damaged
|
||||||
|
last_hit: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipData {
|
||||||
|
/// Create a new ShipData
|
||||||
|
pub(crate) fn new(ship: Arc<Ship>, faction: Arc<Faction>) -> Self {
|
||||||
|
ShipData {
|
||||||
|
ship: ship.clone(),
|
||||||
|
faction,
|
||||||
|
outfits: OutfitSet::new(ship.space, &ship.guns),
|
||||||
|
last_hit: Instant::now(),
|
||||||
|
rng: rand::thread_rng(),
|
||||||
|
|
||||||
|
// TODO: ships must always start landed on planets
|
||||||
|
state: ShipState::Flying,
|
||||||
|
|
||||||
|
// Initial stats
|
||||||
|
hull: ship.hull,
|
||||||
|
shields: 0.0,
|
||||||
|
gun_cooldowns: ship.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Land this ship on `target`
|
||||||
|
/// This does NO checks (speed, range, etc).
|
||||||
|
/// That is the simulation's responsiblity.
|
||||||
|
///
|
||||||
|
/// Will panic if we're not flying.
|
||||||
|
pub fn start_land_on(&mut self, target: Arc<SystemObject>) {
|
||||||
|
match self.state {
|
||||||
|
ShipState::Flying { .. } => {
|
||||||
|
self.state = ShipState::Landing {
|
||||||
|
target,
|
||||||
|
current_z: 1.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Called `start_land_on` on a ship that isn't flying!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When landing, update z position.
|
||||||
|
/// Will panic if we're not landing
|
||||||
|
pub fn set_landing_z(&mut self, z: f32) {
|
||||||
|
match &mut self.state {
|
||||||
|
ShipState::Landing {
|
||||||
|
ref mut current_z, ..
|
||||||
|
} => *current_z = z,
|
||||||
|
_ => unreachable!("Called `set_landing_z` on a ship that isn't landing!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish landing sequence
|
||||||
|
/// Will panic if we're not landing
|
||||||
|
pub fn finish_land_on(&mut self) {
|
||||||
|
match &self.state {
|
||||||
|
ShipState::Landing { target, .. } => {
|
||||||
|
self.state = ShipState::Landed {
|
||||||
|
target: target.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Called `finish_land_on` on a ship that isn't landing!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Land this ship on `target`
|
||||||
|
/// This does NO checks (speed, range, etc).
|
||||||
|
/// That is the simulation's responsiblity.
|
||||||
|
///
|
||||||
|
/// Will panic if we're not flying.
|
||||||
|
pub fn start_unland_to(&mut self, to_position: Isometry2<f32>) {
|
||||||
|
match &self.state {
|
||||||
|
ShipState::Landed { target } => {
|
||||||
|
self.state = ShipState::UnLanding {
|
||||||
|
to_position,
|
||||||
|
from: target.clone(),
|
||||||
|
current_z: target.pos.z,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Called `start_unland_to` on a ship that isn't landed!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When unlanding, update z position.
|
||||||
|
/// Will panic if we're not unlanding
|
||||||
|
pub fn set_unlanding_z(&mut self, z: f32) {
|
||||||
|
match &mut self.state {
|
||||||
|
ShipState::UnLanding {
|
||||||
|
ref mut current_z, ..
|
||||||
|
} => *current_z = z,
|
||||||
|
_ => unreachable!("Called `set_unlanding_z` on a ship that isn't unlanding!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish unlanding sequence
|
||||||
|
/// Will panic if we're not unlanding
|
||||||
|
pub fn finish_unland_to(&mut self) {
|
||||||
|
match self.state {
|
||||||
|
ShipState::UnLanding { .. } => self.state = ShipState::Flying,
|
||||||
|
_ => {
|
||||||
|
unreachable!("Called `finish_unland_to` on a ship that isn't unlanding!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when collapse sequence is finished.
|
||||||
|
/// Will panic if we're not collapsing
|
||||||
|
pub fn finish_collapse(&mut self) {
|
||||||
|
match self.state {
|
||||||
|
ShipState::Collapsing => self.state = ShipState::Dead,
|
||||||
|
_ => {
|
||||||
|
unreachable!("Called `finish_collapse` on a ship that isn't collapsing!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an outfit to this ship
|
||||||
|
pub fn add_outfit(&mut self, o: &Arc<Outfit>) -> super::OutfitAddResult {
|
||||||
|
let r = self.outfits.add(o);
|
||||||
|
self.shields = self.outfits.get_stats().shield_strength;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an outfit from this ship
|
||||||
|
pub fn remove_outfit(&mut self, o: &Arc<Outfit>) -> super::OutfitRemoveResult {
|
||||||
|
self.outfits.remove(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to fire a gun.
|
||||||
|
/// Will panic if `which` isn't a point on this ship.
|
||||||
|
/// Returns `true` if this gun was fired,
|
||||||
|
/// and `false` if it is on cooldown or empty.
|
||||||
|
pub(crate) fn fire_gun(&mut self, which: &GunPoint) -> bool {
|
||||||
|
let c = self.gun_cooldowns.get_mut(which).unwrap();
|
||||||
|
|
||||||
|
if *c > 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(g) = self.outfits.get_gun(which) {
|
||||||
|
let gun = g.gun.as_ref().unwrap();
|
||||||
|
*c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hit this ship with the given amount of damage
|
||||||
|
pub(crate) fn apply_damage(&mut self, mut d: f32) {
|
||||||
|
match self.state {
|
||||||
|
ShipState::Flying { .. } => {}
|
||||||
|
_ => {
|
||||||
|
unreachable!("Cannot apply damage to a ship that is not flying!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.shields >= d {
|
||||||
|
self.shields -= d
|
||||||
|
} else {
|
||||||
|
d -= self.shields;
|
||||||
|
self.shields = 0.0;
|
||||||
|
self.hull = 0f32.max(self.hull - d);
|
||||||
|
}
|
||||||
|
self.last_hit = Instant::now();
|
||||||
|
|
||||||
|
if self.hull <= 0.0 {
|
||||||
|
// This ship has been destroyed, update state
|
||||||
|
self.state = ShipState::Collapsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update this ship's state by `t` seconds
|
||||||
|
pub(crate) fn step(&mut self, t: f32) {
|
||||||
|
match self.state {
|
||||||
|
ShipState::UnLanding { .. } | ShipState::Landing { .. } => {}
|
||||||
|
|
||||||
|
ShipState::Landed { .. } => {
|
||||||
|
// Cooldown guns
|
||||||
|
for (_, c) in &mut self.gun_cooldowns {
|
||||||
|
if *c > 0.0 {
|
||||||
|
*c = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate shields
|
||||||
|
if self.shields != self.outfits.get_stats().shield_strength {
|
||||||
|
self.shields = self.outfits.get_stats().shield_strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipState::Flying { .. } => {
|
||||||
|
// Cooldown guns
|
||||||
|
for (_, c) in &mut self.gun_cooldowns {
|
||||||
|
if *c > 0.0 {
|
||||||
|
*c -= t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate shields
|
||||||
|
let time_since = self.last_hit.elapsed().as_secs_f32();
|
||||||
|
if self.shields != self.outfits.get_stats().shield_strength {
|
||||||
|
for g in self.outfits.iter_shield_generators() {
|
||||||
|
if time_since >= g.delay {
|
||||||
|
self.shields += g.generation * t;
|
||||||
|
if self.shields > self.outfits.get_stats().shield_strength {
|
||||||
|
self.shields = self.outfits.get_stats().shield_strength;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipState::Collapsing {} | ShipState::Dead => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Misc getters, so internal state is untouchable
|
||||||
|
impl ShipData {
|
||||||
|
/// Get this ship's state
|
||||||
|
pub fn get_state(&self) -> &ShipState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's content
|
||||||
|
pub fn get_content(&self) -> &Arc<Ship> {
|
||||||
|
&self.ship
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's current hull
|
||||||
|
pub fn get_hull(&self) -> f32 {
|
||||||
|
self.hull
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's current shields.
|
||||||
|
/// Use get_outfits() for maximum shields
|
||||||
|
pub fn get_shields(&self) -> f32 {
|
||||||
|
self.shields
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all outfits on this ship
|
||||||
|
pub fn get_outfits(&self) -> &OutfitSet {
|
||||||
|
&self.outfits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's faction
|
||||||
|
pub fn get_faction(&self) -> &Arc<Faction> {
|
||||||
|
&self.faction
|
||||||
|
}
|
||||||
|
}
|
124
lib/system/src/instance/ship/data/shipstate.rs
Normal file
124
lib/system/src/instance/ship/data/shipstate.rs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
use galactica_content::SystemObject;
|
||||||
|
use rapier2d::math::Isometry;
|
||||||
|
use std::{num::NonZeroU32, sync::Arc};
|
||||||
|
|
||||||
|
/// Ship state machine.
|
||||||
|
/// Any ship we keep track of is in one of these states.
|
||||||
|
/// Dead ships don't exist---they removed once their collapse
|
||||||
|
/// sequence fully plays out.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ShipState {
|
||||||
|
/// This ship is dead, and should be removed from the game.
|
||||||
|
Dead,
|
||||||
|
|
||||||
|
/// This ship is alive and well in open space
|
||||||
|
Flying {
|
||||||
|
/// The autopilot we're using.
|
||||||
|
/// Overrides ship controller.
|
||||||
|
autopilot: ShipAutoPilot,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// This ship has been destroyed, and is playing its collapse sequence.
|
||||||
|
Collapsing,
|
||||||
|
|
||||||
|
/// This ship is landed on a planet
|
||||||
|
Landed {
|
||||||
|
/// The planet this ship is landed on
|
||||||
|
target: Arc<SystemObject>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// This ship is landing on a planet
|
||||||
|
/// (playing the animation)
|
||||||
|
Landing {
|
||||||
|
/// The planet we're landing on
|
||||||
|
target: Arc<SystemObject>,
|
||||||
|
|
||||||
|
/// Our current z-coordinate
|
||||||
|
current_z: f32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// This ship is taking off from a planet
|
||||||
|
/// (playing the animation)
|
||||||
|
UnLanding {
|
||||||
|
/// The point to which we're going, in world coordinates
|
||||||
|
to_position: Isometry<f32>,
|
||||||
|
|
||||||
|
/// The planet we're taking off from
|
||||||
|
from: Arc<SystemObject>,
|
||||||
|
|
||||||
|
/// Our current z-coordinate
|
||||||
|
current_z: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipState {
|
||||||
|
/// What planet is this ship landed on?
|
||||||
|
pub fn landed_on(&self) -> Option<Arc<SystemObject>> {
|
||||||
|
match self {
|
||||||
|
Self::Landed { target } => Some(target.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An integer representing each state.
|
||||||
|
/// Makes detecting state changes cheap and easy.
|
||||||
|
pub fn as_int(&self) -> NonZeroU32 {
|
||||||
|
NonZeroU32::new(match self {
|
||||||
|
Self::Dead => 1,
|
||||||
|
Self::Collapsing => 2,
|
||||||
|
Self::Flying { .. } => 3,
|
||||||
|
Self::Landed { .. } => 4,
|
||||||
|
Self::Landing { .. } => 5,
|
||||||
|
Self::UnLanding { .. } => 6,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state Dead?
|
||||||
|
pub fn is_dead(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Dead => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state Collapsing?
|
||||||
|
pub fn is_collapsing(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Collapsing => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state Flying?
|
||||||
|
pub fn is_flying(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Flying { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state Landed?
|
||||||
|
pub fn is_landed(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Landed { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state Landing?
|
||||||
|
pub fn is_landing(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Landing { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this state UnLanding?
|
||||||
|
pub fn is_unlanding(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::UnLanding { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
lib/system/src/instance/ship/mod.rs
Normal file
7
lib/system/src/instance/ship/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub mod autopilot;
|
||||||
|
mod collapse;
|
||||||
|
pub(crate) mod data;
|
||||||
|
mod ship;
|
||||||
|
|
||||||
|
pub use collapse::ShipCollapseSequence;
|
||||||
|
pub use ship::ShipInstance;
|
556
lib/system/src/instance/ship/ship.rs
Normal file
556
lib/system/src/instance/ship/ship.rs
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
use galactica_content::{
|
||||||
|
AnimationState, EnginePoint, Faction, GunPoint, Outfit, ProjectileCollider, Ship,
|
||||||
|
SpriteAutomaton,
|
||||||
|
};
|
||||||
|
use nalgebra::{vector, Point2, Rotation2, Vector2};
|
||||||
|
use rand::Rng;
|
||||||
|
use rapier2d::{
|
||||||
|
dynamics::{RigidBodyBuilder, RigidBodyHandle},
|
||||||
|
geometry::{ColliderBuilder, ColliderHandle, Group, InteractionGroups},
|
||||||
|
pipeline::ActiveEvents,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
collapse::ShipCollapseSequence,
|
||||||
|
data::{OutfitRemoveResult, ShipData, ShipState},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
get_phys_id,
|
||||||
|
instance::{EffectInstance, ProjectileInstance},
|
||||||
|
physwrapper::PhysWrapper,
|
||||||
|
shipagent::{ShipAgent, ShipControls},
|
||||||
|
stateframe::{PhysSimShipHandle, ShipStateframe, SystemStateframe},
|
||||||
|
NewObjects,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A ship instance in the system
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ShipInstance {
|
||||||
|
/// This ship's unique id
|
||||||
|
pub uid: u64,
|
||||||
|
|
||||||
|
/// This ship's physics handle
|
||||||
|
pub rigid_body: RigidBodyHandle,
|
||||||
|
|
||||||
|
/// This ship's collider
|
||||||
|
pub collider: ColliderHandle,
|
||||||
|
|
||||||
|
/// This ship's game data
|
||||||
|
pub(crate) data: ShipData,
|
||||||
|
|
||||||
|
/// This ship's sprite animation state
|
||||||
|
anim: SpriteAutomaton,
|
||||||
|
|
||||||
|
/// Animation state for each of this ship's engines
|
||||||
|
engine_anim: Vec<(EnginePoint, SpriteAutomaton)>,
|
||||||
|
|
||||||
|
/// This ship's controls
|
||||||
|
pub(crate) controls: ShipControls,
|
||||||
|
|
||||||
|
/// This ship's controls during the last frame
|
||||||
|
last_controls: ShipControls,
|
||||||
|
|
||||||
|
/// This ship's collapse sequence
|
||||||
|
collapse_sequence: Option<ShipCollapseSequence>,
|
||||||
|
|
||||||
|
/// If set, the given agent temporarily controls this ship
|
||||||
|
autopilot: Option<Box<dyn ShipAgent>>,
|
||||||
|
|
||||||
|
/// The agent that controls this ship when no autopilot is enabled
|
||||||
|
agent: Box<dyn ShipAgent>,
|
||||||
|
|
||||||
|
/// If true, this ship's collider has been destroyed,
|
||||||
|
/// and this struct is waiting to be cleaned up.
|
||||||
|
///
|
||||||
|
/// Note that this is NOT "in-game" destroyed,
|
||||||
|
/// but rather "internal game logic" destroyed.
|
||||||
|
/// In-game destroyed corresponds to the "Dead" state.
|
||||||
|
is_destroyed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipInstance {
|
||||||
|
/// Make a new ship
|
||||||
|
pub(crate) fn new(
|
||||||
|
ship: Arc<Ship>,
|
||||||
|
faction: Arc<Faction>,
|
||||||
|
agent: Box<dyn ShipAgent>,
|
||||||
|
rigid_body: RigidBodyHandle,
|
||||||
|
collider: ColliderHandle,
|
||||||
|
) -> Self {
|
||||||
|
ShipInstance {
|
||||||
|
uid: get_phys_id(),
|
||||||
|
anim: SpriteAutomaton::new(ship.sprite.clone()),
|
||||||
|
rigid_body,
|
||||||
|
collider,
|
||||||
|
data: ShipData::new(ship.clone(), faction),
|
||||||
|
engine_anim: Vec::new(),
|
||||||
|
controls: ShipControls::new(),
|
||||||
|
last_controls: ShipControls::new(),
|
||||||
|
collapse_sequence: Some(ShipCollapseSequence::new(ship.collapse.length)),
|
||||||
|
is_destroyed: false,
|
||||||
|
autopilot: None,
|
||||||
|
agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a snapshot of this ship's state
|
||||||
|
pub fn get_stateframe(&self, phys: &PhysWrapper) -> ShipStateframe {
|
||||||
|
return ShipStateframe {
|
||||||
|
uid: self.uid,
|
||||||
|
data: self.data.clone(),
|
||||||
|
anim: self.anim.clone(),
|
||||||
|
engine_anim: self.engine_anim.clone(),
|
||||||
|
collapse_sequence: self.collapse_sequence.clone(),
|
||||||
|
rigidbody: phys.get_rigid_body(self.rigid_body).unwrap().clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step this ship's state by t seconds
|
||||||
|
/// pub in phys
|
||||||
|
pub(crate) fn step(
|
||||||
|
&mut self,
|
||||||
|
dt: f32,
|
||||||
|
sf: &SystemStateframe,
|
||||||
|
wrapper: &mut PhysWrapper,
|
||||||
|
new: &mut NewObjects,
|
||||||
|
) {
|
||||||
|
if self.is_destroyed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data.step(dt);
|
||||||
|
self.anim.step(dt);
|
||||||
|
|
||||||
|
for (_, e) in &mut self.engine_anim {
|
||||||
|
e.step(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flare animations
|
||||||
|
if !self.controls.thrust && self.last_controls.thrust {
|
||||||
|
let flare = self.get_flare();
|
||||||
|
if let Some(flare) = flare {
|
||||||
|
if flare.engine_flare_on_stop.is_some() {
|
||||||
|
for (_, e) in &mut self.engine_anim {
|
||||||
|
e.jump_to(flare.engine_flare_on_stop.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if self.controls.thrust && !self.last_controls.thrust {
|
||||||
|
let flare = self.get_flare();
|
||||||
|
if let Some(flare) = flare {
|
||||||
|
if flare.engine_flare_on_start.is_some() {
|
||||||
|
for (_, e) in &mut self.engine_anim {
|
||||||
|
e.jump_to(flare.engine_flare_on_start.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Update this now and not later,
|
||||||
|
// since autopilot might change controls.
|
||||||
|
self.last_controls = self.controls.clone();
|
||||||
|
|
||||||
|
match self.data.get_state() {
|
||||||
|
ShipState::Collapsing { .. } => {
|
||||||
|
// Borrow checker hack, so we may pass self.data
|
||||||
|
// to the collapse sequence
|
||||||
|
let mut seq = self.collapse_sequence.take().unwrap();
|
||||||
|
seq.step(dt, wrapper, new, &self.data, self.rigid_body, self.collider);
|
||||||
|
self.collapse_sequence = Some(seq);
|
||||||
|
|
||||||
|
if self.collapse_sequence.as_ref().unwrap().is_done() {
|
||||||
|
self.data.finish_collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShipState::Flying => {
|
||||||
|
let controls = match self.autopilot {
|
||||||
|
Some(x) => x.update_controls(dt, sf, PhysSimShipHandle(self.collider)),
|
||||||
|
None => self
|
||||||
|
.agent
|
||||||
|
.update_controls(dt, sf, PhysSimShipHandle(self.collider)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(controls) = controls {
|
||||||
|
self.controls = controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.controls.landing {
|
||||||
|
let controls = None;
|
||||||
|
/*
|
||||||
|
let controls = autopilot::auto_landing(
|
||||||
|
res,
|
||||||
|
img,
|
||||||
|
PhysSimShipHandle(self.collider),
|
||||||
|
Vector2::new(target.pos.x, target.pos.y),
|
||||||
|
);*/
|
||||||
|
|
||||||
|
// Try to land the ship.
|
||||||
|
// True if success, false if failure.
|
||||||
|
// Failure implies no state changes.
|
||||||
|
let landed = 'landed: {
|
||||||
|
let r = wrapper.get_rigid_body(self.rigid_body).unwrap();
|
||||||
|
|
||||||
|
let t_pos = Vector2::new(target.pos.x, target.pos.y);
|
||||||
|
let s_pos =
|
||||||
|
Vector2::new(r.position().translation.x, r.position().translation.y);
|
||||||
|
|
||||||
|
// TODO: deactivate collider when landing.
|
||||||
|
// Can't just set_active(false), since we still need that collider's mass.
|
||||||
|
|
||||||
|
// We're in land range...
|
||||||
|
if (t_pos - s_pos).magnitude() > target.size / 2.0 {
|
||||||
|
break 'landed false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// And we'll stay in land range long enough.
|
||||||
|
if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0))
|
||||||
|
.magnitude() > target.size / 2.0
|
||||||
|
{
|
||||||
|
break 'landed false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let collider = wrapper.get_collider_mut(self.collider).unwrap();
|
||||||
|
collider.set_collision_groups(InteractionGroups::new(
|
||||||
|
Group::GROUP_1,
|
||||||
|
Group::empty(),
|
||||||
|
));
|
||||||
|
self.data.start_land_on(target.clone());
|
||||||
|
break 'landed true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if landed {
|
||||||
|
self.controls = ShipControls::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.step_physics(dt, wrapper);
|
||||||
|
self.step_effects(dt, wrapper, new);
|
||||||
|
|
||||||
|
// If we're firing, try to fire each gun
|
||||||
|
if self.controls.guns {
|
||||||
|
// TODO: don't allocate here. This is a hack to satisfy the borrow checker,
|
||||||
|
// convert this to a refcell or do the replace dance.
|
||||||
|
let pairs: Vec<(GunPoint, Option<Arc<Outfit>>)> = self
|
||||||
|
.data
|
||||||
|
.get_outfits()
|
||||||
|
.iter_gun_points()
|
||||||
|
.map(|(p, o)| (p.clone(), o.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (gun_point, outfit) in pairs {
|
||||||
|
if self.data.fire_gun(&gun_point) {
|
||||||
|
let outfit = outfit.unwrap();
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap();
|
||||||
|
let ship_pos = rigid_body.translation();
|
||||||
|
let ship_rot = rigid_body.rotation();
|
||||||
|
let ship_ang = ship_rot.angle();
|
||||||
|
let ship_vel =
|
||||||
|
rigid_body.velocity_at_point(rigid_body.center_of_mass());
|
||||||
|
|
||||||
|
let pos = ship_pos + (ship_rot * gun_point.pos);
|
||||||
|
let gun = outfit.gun.as_ref().unwrap();
|
||||||
|
|
||||||
|
let spread =
|
||||||
|
rng.gen_range(-gun.projectile.angle_rng..=gun.projectile.angle_rng);
|
||||||
|
let vel = ship_vel
|
||||||
|
+ (Rotation2::new(ship_ang + spread)
|
||||||
|
* Vector2::new(
|
||||||
|
gun.projectile.speed
|
||||||
|
+ rng.gen_range(
|
||||||
|
-gun.projectile.speed_rng
|
||||||
|
..=gun.projectile.speed_rng,
|
||||||
|
),
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
let rigid_body = RigidBodyBuilder::kinematic_velocity_based()
|
||||||
|
.translation(pos)
|
||||||
|
.rotation(ship_ang)
|
||||||
|
.linvel(vel)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut collider = match &gun.projectile.collider {
|
||||||
|
ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
|
||||||
|
.sensor(true)
|
||||||
|
.active_events(ActiveEvents::COLLISION_EVENTS)
|
||||||
|
.build(),
|
||||||
|
};
|
||||||
|
|
||||||
|
collider.set_collision_groups(InteractionGroups::new(
|
||||||
|
Group::GROUP_2,
|
||||||
|
Group::GROUP_1,
|
||||||
|
));
|
||||||
|
|
||||||
|
let rigid_body = wrapper.insert_rigid_body(rigid_body);
|
||||||
|
let collider = wrapper.insert_collider(collider, rigid_body);
|
||||||
|
|
||||||
|
new.projectiles.push(ProjectileInstance::new(
|
||||||
|
gun.projectile.clone(),
|
||||||
|
rigid_body,
|
||||||
|
self.data.get_faction().clone(),
|
||||||
|
collider,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipState::UnLanding {
|
||||||
|
to_position,
|
||||||
|
current_z,
|
||||||
|
from,
|
||||||
|
} => {
|
||||||
|
let controls = None;
|
||||||
|
/*
|
||||||
|
let controls = autopilot::auto_landing(
|
||||||
|
&res,
|
||||||
|
img,
|
||||||
|
PhysSimShipHandle(self.collider),
|
||||||
|
Vector2::new(to_position.translation.x, to_position.translation.y),
|
||||||
|
);*/
|
||||||
|
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap();
|
||||||
|
let max_d = (Vector2::new(from.pos.x, from.pos.y)
|
||||||
|
- Vector2::new(to_position.translation.x, to_position.translation.y))
|
||||||
|
.magnitude();
|
||||||
|
let now_d = (r.translation()
|
||||||
|
- Vector2::new(to_position.translation.x, to_position.translation.y))
|
||||||
|
.magnitude();
|
||||||
|
let f = now_d / max_d;
|
||||||
|
|
||||||
|
let current_z = *current_z;
|
||||||
|
let zdist = 1.0 - from.pos.z;
|
||||||
|
|
||||||
|
if current_z <= 1.0 {
|
||||||
|
// Finish unlanding ship
|
||||||
|
self.data.finish_unland_to();
|
||||||
|
wrapper
|
||||||
|
.get_collider_mut(self.collider)
|
||||||
|
.unwrap()
|
||||||
|
.set_collision_groups(InteractionGroups::new(
|
||||||
|
Group::GROUP_1,
|
||||||
|
Group::GROUP_1 | Group::GROUP_2,
|
||||||
|
));
|
||||||
|
} else if current_z <= 1.5 {
|
||||||
|
self.data
|
||||||
|
.set_unlanding_z(1f32.max(current_z - (0.5 * dt) / 0.5));
|
||||||
|
self.controls = ShipControls::new();
|
||||||
|
} else {
|
||||||
|
self.data.set_unlanding_z(1.0 - zdist * f);
|
||||||
|
|
||||||
|
if let Some(controls) = controls {
|
||||||
|
self.controls = controls;
|
||||||
|
}
|
||||||
|
self.step_physics(dt, wrapper);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipState::Landing { target, current_z } => {
|
||||||
|
let controls = None;
|
||||||
|
/*
|
||||||
|
let controls = autopilot::auto_landing(
|
||||||
|
&res,
|
||||||
|
img,
|
||||||
|
PhysSimShipHandle(self.collider),
|
||||||
|
Vector2::new(target.pos.x, target.pos.y),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
let current_z = *current_z;
|
||||||
|
let zdist = target.pos.z - 1.0;
|
||||||
|
|
||||||
|
if current_z >= target.pos.z {
|
||||||
|
// Finish landing ship
|
||||||
|
self.data.finish_land_on();
|
||||||
|
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap();
|
||||||
|
r.set_enabled(false);
|
||||||
|
r.set_angvel(0.0, false);
|
||||||
|
r.set_linvel(nalgebra::Vector2::new(0.0, 0.0), false);
|
||||||
|
} else {
|
||||||
|
self.data.set_landing_z(current_z + zdist * dt / 2.0);
|
||||||
|
|
||||||
|
if let Some(controls) = controls {
|
||||||
|
self.controls = controls;
|
||||||
|
}
|
||||||
|
self.step_physics(dt, wrapper);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipState::Landed { .. } => {}
|
||||||
|
|
||||||
|
ShipState::Dead => {
|
||||||
|
wrapper.remove_rigid_body(self.rigid_body);
|
||||||
|
self.is_destroyed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should this ship's data be removed?
|
||||||
|
pub fn should_remove(&self) -> bool {
|
||||||
|
self.is_destroyed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update this frame's physics
|
||||||
|
fn step_physics(&mut self, dt: f32, wrapper: &mut PhysWrapper) {
|
||||||
|
let rigid_body = &mut wrapper.get_rigid_body_mut(self.rigid_body).unwrap();
|
||||||
|
let ship_rot = rigid_body.rotation();
|
||||||
|
let engine_force = ship_rot * (Vector2::new(1.0, 0.0) * dt);
|
||||||
|
|
||||||
|
if self.controls.thrust {
|
||||||
|
rigid_body.apply_impulse(
|
||||||
|
vector![engine_force.x, engine_force.y]
|
||||||
|
* self.data.get_outfits().get_stats().engine_thrust,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.controls.right {
|
||||||
|
rigid_body.apply_torque_impulse(
|
||||||
|
self.data.get_outfits().get_stats().steer_power * -100.0 * dt,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.controls.left {
|
||||||
|
rigid_body.apply_torque_impulse(
|
||||||
|
self.data.get_outfits().get_stats().steer_power * 100.0 * dt,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn this frame's effects
|
||||||
|
fn step_effects(&mut self, dt: f32, wrapper: &mut PhysWrapper, new: &mut NewObjects) {
|
||||||
|
let rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap().clone();
|
||||||
|
let collider = wrapper.get_collider(self.collider).unwrap().clone();
|
||||||
|
|
||||||
|
let ship_content = self.data.get_content();
|
||||||
|
let ship_pos = rigid_body.translation();
|
||||||
|
let ship_rot = rigid_body.rotation();
|
||||||
|
let ship_ang = ship_rot.angle();
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
if self.data.get_hull() <= ship_content.damage.hull {
|
||||||
|
for e in &ship_content.damage.effects {
|
||||||
|
if rng.gen_range(0.0..=1.0) <= dt / e.frequency {
|
||||||
|
let pos = if let Some(pos) = e.pos {
|
||||||
|
Vector2::new(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
// Pick a random point inside this ship's collider
|
||||||
|
let mut y = 0.0;
|
||||||
|
let mut x = 0.0;
|
||||||
|
let mut a = false;
|
||||||
|
while !a {
|
||||||
|
x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
|
||||||
|
y = rng.gen_range(-1.0..=1.0)
|
||||||
|
* ship_content.size * ship_content.sprite.aspect
|
||||||
|
/ 2.0;
|
||||||
|
a = collider.shape().contains_local_point(&Point2::new(x, y));
|
||||||
|
}
|
||||||
|
Vector2::new(x, y)
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = ship_pos + (Rotation2::new(ship_ang) * pos);
|
||||||
|
|
||||||
|
new.effects.push(EffectInstance::new(
|
||||||
|
wrapper,
|
||||||
|
e.effect.clone(),
|
||||||
|
pos,
|
||||||
|
self.rigid_body,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public mutable
|
||||||
|
impl ShipInstance {
|
||||||
|
fn get_flare(&mut self) -> Option<Arc<Outfit>> {
|
||||||
|
// TODO: better way to pick flare sprite
|
||||||
|
for (h, _) in self.data.get_outfits().iter_outfits() {
|
||||||
|
if h.engine_flare_sprite.is_some() {
|
||||||
|
return Some(h.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-create this ship's engine flare animations
|
||||||
|
/// Should be called whenever we change outfits
|
||||||
|
fn update_flares(&mut self) {
|
||||||
|
let flare = self.get_flare();
|
||||||
|
if flare.is_none() {
|
||||||
|
self.engine_anim = Vec::new();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.engine_anim = self
|
||||||
|
.data
|
||||||
|
.get_content()
|
||||||
|
.engines
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
(
|
||||||
|
e.clone(),
|
||||||
|
SpriteAutomaton::new(
|
||||||
|
flare
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.engine_flare_sprite
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add one outfit to this ship
|
||||||
|
pub fn add_outfit(&mut self, o: &Arc<Outfit>) {
|
||||||
|
self.data.add_outfit(o);
|
||||||
|
self.update_flares();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add many outfits to this ship
|
||||||
|
pub fn add_outfits(&mut self, outfits: impl IntoIterator<Item = Arc<Outfit>>) {
|
||||||
|
for o in outfits {
|
||||||
|
self.data.add_outfit(&o);
|
||||||
|
}
|
||||||
|
self.update_flares();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove one outfit from this ship
|
||||||
|
pub fn remove_outfit(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
|
||||||
|
let r = self.data.remove_outfit(o);
|
||||||
|
self.update_flares();
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public immutable
|
||||||
|
impl ShipInstance {
|
||||||
|
/// Get this ship's control state
|
||||||
|
pub fn get_controls(&self) -> &ShipControls {
|
||||||
|
&self.controls
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's engine animations
|
||||||
|
pub fn iter_engine_anim(&self) -> impl Iterator<Item = &(EnginePoint, SpriteAutomaton)> {
|
||||||
|
self.engine_anim.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's animation state
|
||||||
|
pub fn get_anim_state(&self) -> AnimationState {
|
||||||
|
self.anim.get_texture_idx()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get this ship's game data struct
|
||||||
|
pub fn get_data(&self) -> &ShipData {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
228
lib/system/src/lib.rs
Normal file
228
lib/system/src/lib.rs
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use galactica_content::{Content, Relationship};
|
||||||
|
use instance::{
|
||||||
|
ship::{data::ShipState, ShipInstance},
|
||||||
|
EffectInstance, ProjectileInstance,
|
||||||
|
};
|
||||||
|
use nalgebra::{Point2, Vector2};
|
||||||
|
use physwrapper::PhysWrapper;
|
||||||
|
use rapier2d::prelude::ColliderHandle;
|
||||||
|
use stateframe::{EffectStateframe, PhysSimShipHandle, ProjectileStateframe, SystemStateframe};
|
||||||
|
|
||||||
|
mod instance;
|
||||||
|
mod physwrapper;
|
||||||
|
mod shipagent;
|
||||||
|
mod stateframe;
|
||||||
|
|
||||||
|
pub(crate) struct NewObjects {
|
||||||
|
pub projectiles: Vec<ProjectileInstance>,
|
||||||
|
pub effects: Vec<EffectInstance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of new objects to create this frame
|
||||||
|
impl NewObjects {
|
||||||
|
/// Create an empty NewObjects
|
||||||
|
/// This should only be called once in each PhysSim
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
projectiles: Vec::new(),
|
||||||
|
effects: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all lists in this struct.
|
||||||
|
/// Call after saving all new objects.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.projectiles.clear();
|
||||||
|
self.effects.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unique id given to each physics object
|
||||||
|
static PHYS_UID: AtomicU64 = AtomicU64::new(0);
|
||||||
|
fn get_phys_id() -> u64 {
|
||||||
|
PHYS_UID.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates everything in one star system.
|
||||||
|
pub struct SystemSimulation {
|
||||||
|
/// Game content
|
||||||
|
ct: Arc<Content>,
|
||||||
|
|
||||||
|
/// Rapier wrapper
|
||||||
|
phys: PhysWrapper,
|
||||||
|
/// Objects to create in the next frame
|
||||||
|
new: NewObjects,
|
||||||
|
|
||||||
|
effects: Vec<EffectInstance>,
|
||||||
|
projectiles: HashMap<ColliderHandle, ProjectileInstance>,
|
||||||
|
ships: HashMap<ColliderHandle, ShipInstance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemSimulation {
|
||||||
|
pub fn collide_projectile_ship(
|
||||||
|
&mut self,
|
||||||
|
projectile_h: ColliderHandle,
|
||||||
|
ship_h: ColliderHandle,
|
||||||
|
) {
|
||||||
|
let projectile = self.projectiles.get_mut(&projectile_h);
|
||||||
|
let ship = self.ships.get_mut(&ship_h);
|
||||||
|
if projectile.is_none() || ship.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let projectile = projectile.unwrap();
|
||||||
|
let ship = ship.unwrap();
|
||||||
|
|
||||||
|
let r = projectile
|
||||||
|
.faction
|
||||||
|
.relationships
|
||||||
|
.get(&ship.data.get_faction().index)
|
||||||
|
.unwrap();
|
||||||
|
let destory_projectile = match r {
|
||||||
|
Relationship::Hostile => match ship.data.get_state() {
|
||||||
|
ShipState::Flying { .. } => {
|
||||||
|
ship.data.apply_damage(projectile.content.damage);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ShipState::Collapsing { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if destory_projectile {
|
||||||
|
let pr = self.phys.get_rigid_body(projectile.rigid_body).unwrap();
|
||||||
|
let v =
|
||||||
|
pr.velocity_at_point(pr.center_of_mass()).normalize() * projectile.content.force;
|
||||||
|
let pos = *pr.translation();
|
||||||
|
let _ = pr;
|
||||||
|
|
||||||
|
let r = self.phys.get_rigid_body_mut(ship.rigid_body).unwrap();
|
||||||
|
r.apply_impulse_at_point(Vector2::new(v.x, v.y), Point2::new(pos.x, pos.y), true);
|
||||||
|
|
||||||
|
// Borrow again, we can only have one at a time
|
||||||
|
let pr = self.phys.get_rigid_body(projectile.rigid_body).unwrap();
|
||||||
|
let pos = *pr.translation();
|
||||||
|
|
||||||
|
match &projectile.content.impact_effect {
|
||||||
|
None => {}
|
||||||
|
Some(x) => {
|
||||||
|
self.effects.push(EffectInstance::new(
|
||||||
|
&mut self.phys,
|
||||||
|
x.clone(),
|
||||||
|
pos,
|
||||||
|
projectile.rigid_body,
|
||||||
|
Some(ship.rigid_body),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
projectile.destroy_silent(&mut self.new, &mut self.phys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemSimulation {
|
||||||
|
pub fn new(content: Arc<Content>) -> Self {
|
||||||
|
Self {
|
||||||
|
ct: content,
|
||||||
|
|
||||||
|
phys: PhysWrapper::new(),
|
||||||
|
new: NewObjects::new(),
|
||||||
|
|
||||||
|
effects: Vec::new(),
|
||||||
|
projectiles: HashMap::new(),
|
||||||
|
ships: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step this simulation by `dt` seconds.
|
||||||
|
pub fn step(&mut self, dt: f32, sf: &SystemStateframe) {
|
||||||
|
self.phys.step(dt);
|
||||||
|
|
||||||
|
// Handle collision events
|
||||||
|
while let Ok(event) = &self.phys.collision_queue.try_recv() {
|
||||||
|
if event.started() {
|
||||||
|
let a = event.collider1();
|
||||||
|
let b = event.collider2();
|
||||||
|
|
||||||
|
// If projectiles are part of this collision, make sure
|
||||||
|
// `a` is one of them.
|
||||||
|
let (a, b) = if self.projectiles.contains_key(&b) {
|
||||||
|
(b, a)
|
||||||
|
} else {
|
||||||
|
(a, b)
|
||||||
|
};
|
||||||
|
|
||||||
|
let p = self.projectiles.get(&a);
|
||||||
|
let s = self.ships.get_mut(&b);
|
||||||
|
if p.is_none() || s.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.collide_projectile_ship(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step and garbage-collect projectiles
|
||||||
|
self.projectiles.retain(|_, proj| {
|
||||||
|
proj.step(dt, &mut self.new, &mut self.phys);
|
||||||
|
!proj.should_remove()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step and garbage-collect ships
|
||||||
|
self.ships.retain(|_, ship| {
|
||||||
|
ship.step(dt, sf, &mut self.phys, &mut self.new);
|
||||||
|
!ship.should_remove()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step and garbage-collect effects
|
||||||
|
self.effects.retain_mut(|x| {
|
||||||
|
x.step(dt, &mut self.phys);
|
||||||
|
!x.is_destroyed()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process new objects
|
||||||
|
for p in self.new.projectiles.iter() {
|
||||||
|
self.projectiles.insert(p.collider, p.clone());
|
||||||
|
}
|
||||||
|
for e in self.new.effects.iter() {
|
||||||
|
self.effects.push(e.clone());
|
||||||
|
}
|
||||||
|
self.new.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an image with the current simulation state
|
||||||
|
///
|
||||||
|
/// We mutate (rather than create) a new [`SystemStateframe`]
|
||||||
|
/// so we don't need to allocate memory every time we draw a frame.
|
||||||
|
pub fn get_stateframe(&mut self, sf: &mut SystemStateframe) {
|
||||||
|
sf.clear();
|
||||||
|
|
||||||
|
for s in self.ships.values() {
|
||||||
|
sf.ship_map
|
||||||
|
.insert(PhysSimShipHandle(s.collider), sf.ships.len());
|
||||||
|
sf.ships.push(s.get_stateframe(&self.phys))
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in self.projectiles.values() {
|
||||||
|
sf.projectiles.push(ProjectileStateframe {
|
||||||
|
projectile: p.clone(),
|
||||||
|
rigidbody: self.phys.get_rigid_body(p.rigid_body).unwrap().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for e in self.effects.iter() {
|
||||||
|
sf.effects.push(EffectStateframe {
|
||||||
|
effect: e.clone(),
|
||||||
|
rigidbody: self.phys.get_rigid_body(e.rigid_body).unwrap().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
lib/system/src/physwrapper.rs
Normal file
135
lib/system/src/physwrapper.rs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
use crossbeam::channel::Receiver;
|
||||||
|
use rapier2d::{
|
||||||
|
dynamics::{
|
||||||
|
CCDSolver, GenericJoint, ImpulseJointSet, IntegrationParameters, IslandManager,
|
||||||
|
MultibodyJointSet, RigidBody, RigidBodyHandle, RigidBodySet,
|
||||||
|
},
|
||||||
|
geometry::{Collider, ColliderHandle, ColliderSet, CollisionEvent, NarrowPhase},
|
||||||
|
na::vector,
|
||||||
|
pipeline::{ChannelEventCollector, PhysicsPipeline},
|
||||||
|
prelude::BroadPhaseMultiSap,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Wraps Rapier2d physics parameters
|
||||||
|
pub struct PhysWrapper {
|
||||||
|
ip: IntegrationParameters,
|
||||||
|
pp: PhysicsPipeline,
|
||||||
|
im: IslandManager,
|
||||||
|
bp: BroadPhaseMultiSap,
|
||||||
|
np: NarrowPhase,
|
||||||
|
mj: MultibodyJointSet,
|
||||||
|
ccd: CCDSolver,
|
||||||
|
|
||||||
|
rigid_body_set: RigidBodySet,
|
||||||
|
collider_set: ColliderSet,
|
||||||
|
joint_set: ImpulseJointSet,
|
||||||
|
|
||||||
|
collision_handler: ChannelEventCollector,
|
||||||
|
|
||||||
|
/// Collision event queue
|
||||||
|
/// this should be emptied after every frame.
|
||||||
|
pub collision_queue: Receiver<CollisionEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhysWrapper {
|
||||||
|
/// Make a new physics wrapper
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (collision_send, collision_queue) = crossbeam::channel::unbounded();
|
||||||
|
let (contact_force_send, _) = crossbeam::channel::unbounded();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ip: IntegrationParameters::default(),
|
||||||
|
pp: PhysicsPipeline::new(),
|
||||||
|
im: IslandManager::new(),
|
||||||
|
bp: BroadPhaseMultiSap::new(),
|
||||||
|
np: NarrowPhase::new(),
|
||||||
|
mj: MultibodyJointSet::new(),
|
||||||
|
ccd: CCDSolver::new(),
|
||||||
|
collision_queue,
|
||||||
|
collision_handler: ChannelEventCollector::new(collision_send, contact_force_send),
|
||||||
|
|
||||||
|
rigid_body_set: RigidBodySet::new(),
|
||||||
|
collider_set: ColliderSet::new(),
|
||||||
|
joint_set: ImpulseJointSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step physics sim by `t` seconds
|
||||||
|
pub fn step(&mut self, t: f32) {
|
||||||
|
self.ip.dt = t;
|
||||||
|
self.pp.step(
|
||||||
|
&vector![0.0, 0.0],
|
||||||
|
&self.ip,
|
||||||
|
&mut self.im,
|
||||||
|
&mut self.bp,
|
||||||
|
&mut self.np,
|
||||||
|
&mut self.rigid_body_set,
|
||||||
|
&mut self.collider_set,
|
||||||
|
&mut self.joint_set,
|
||||||
|
&mut self.mj,
|
||||||
|
&mut self.ccd,
|
||||||
|
None,
|
||||||
|
&(),
|
||||||
|
&mut self.collision_handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a rigid body and all its colliders from this physics wrapper
|
||||||
|
pub fn remove_rigid_body(&mut self, body: RigidBodyHandle) -> Option<RigidBody> {
|
||||||
|
return self.rigid_body_set.remove(
|
||||||
|
body,
|
||||||
|
&mut self.im,
|
||||||
|
&mut self.collider_set,
|
||||||
|
&mut self.joint_set,
|
||||||
|
&mut self.mj,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhysWrapper {
|
||||||
|
/// Get a rigidbody from this physics wrapper
|
||||||
|
pub fn get_rigid_body(&self, h: RigidBodyHandle) -> Option<&RigidBody> {
|
||||||
|
self.rigid_body_set.get(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a rigidbody from this physics wrapper, mutably
|
||||||
|
pub fn get_rigid_body_mut(&mut self, h: RigidBodyHandle) -> Option<&mut RigidBody> {
|
||||||
|
self.rigid_body_set.get_mut(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a collider from this physics wrapper
|
||||||
|
pub fn get_collider(&self, h: ColliderHandle) -> Option<&Collider> {
|
||||||
|
self.collider_set.get(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a collider from this physics wrapper, mutably
|
||||||
|
pub fn get_collider_mut(&mut self, h: ColliderHandle) -> Option<&mut Collider> {
|
||||||
|
self.collider_set.get_mut(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a rigid body
|
||||||
|
pub fn insert_rigid_body(&mut self, rb: RigidBody) -> RigidBodyHandle {
|
||||||
|
self.rigid_body_set.insert(rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach a collider to a rigid body
|
||||||
|
pub fn insert_collider(
|
||||||
|
&mut self,
|
||||||
|
collider: Collider,
|
||||||
|
parent_handle: RigidBodyHandle,
|
||||||
|
) -> ColliderHandle {
|
||||||
|
self.collider_set
|
||||||
|
.insert_with_parent(collider, parent_handle, &mut self.rigid_body_set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an impulse joint between two bodies
|
||||||
|
pub fn add_joint(
|
||||||
|
&mut self,
|
||||||
|
body1: RigidBodyHandle,
|
||||||
|
body2: RigidBodyHandle,
|
||||||
|
joint: impl Into<GenericJoint>,
|
||||||
|
) {
|
||||||
|
self.joint_set.insert(body1, body2, joint, false);
|
||||||
|
}
|
||||||
|
}
|
56
lib/system/src/shipagent/mod.rs
Normal file
56
lib/system/src/shipagent/mod.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::stateframe::{PhysSimShipHandle, SystemStateframe};
|
||||||
|
|
||||||
|
mod null;
|
||||||
|
mod point;
|
||||||
|
|
||||||
|
pub use null::NullShipAgent;
|
||||||
|
pub use point::PointShipAgent;
|
||||||
|
|
||||||
|
/// A ship's controls
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ShipControls {
|
||||||
|
/// True if turning left
|
||||||
|
pub left: bool,
|
||||||
|
|
||||||
|
/// True if turning right
|
||||||
|
pub right: bool,
|
||||||
|
|
||||||
|
/// True if foward thrust
|
||||||
|
pub thrust: bool,
|
||||||
|
|
||||||
|
/// True if firing guns
|
||||||
|
pub guns: bool,
|
||||||
|
|
||||||
|
/// If set, land on the given object if possible.
|
||||||
|
pub landing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipControls {
|
||||||
|
/// Create a new, empty ShipControls
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ShipControls {
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
thrust: false,
|
||||||
|
guns: false,
|
||||||
|
landing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ShipAgent
|
||||||
|
where
|
||||||
|
Self: Debug,
|
||||||
|
{
|
||||||
|
/// Update a ship's controls based on system state.
|
||||||
|
/// This method returns the ship's new control values,
|
||||||
|
/// or None if no change is to be made.
|
||||||
|
fn update_controls(
|
||||||
|
&mut self,
|
||||||
|
dt: f32,
|
||||||
|
sf: &SystemStateframe,
|
||||||
|
this_ship: PhysSimShipHandle,
|
||||||
|
) -> Option<ShipControls>;
|
||||||
|
}
|
24
lib/system/src/shipagent/null.rs
Normal file
24
lib/system/src/shipagent/null.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use crate::stateframe::{PhysSimShipHandle, SystemStateframe};
|
||||||
|
|
||||||
|
use super::{ShipAgent, ShipControls};
|
||||||
|
|
||||||
|
/// The null agent does nothing
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NullShipAgent {}
|
||||||
|
|
||||||
|
impl NullShipAgent {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipAgent for NullShipAgent {
|
||||||
|
fn update_controls(
|
||||||
|
&mut self,
|
||||||
|
_dt: f32,
|
||||||
|
_sf: &SystemStateframe,
|
||||||
|
_this_ship: PhysSimShipHandle,
|
||||||
|
) -> Option<ShipControls> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
90
lib/system/src/shipagent/point.rs
Normal file
90
lib/system/src/shipagent/point.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use galactica_content::Relationship;
|
||||||
|
use galactica_util::clockwise_angle;
|
||||||
|
use nalgebra::Vector2;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
instance::ship::data::ShipState,
|
||||||
|
stateframe::{PhysSimShipHandle, SystemStateframe},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{ShipAgent, ShipControls};
|
||||||
|
|
||||||
|
/// "Point" ship agent.
|
||||||
|
/// Point and shoot towards the nearest enemy.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PointShipAgent {}
|
||||||
|
|
||||||
|
impl PointShipAgent {
|
||||||
|
/// Create a new ship controller
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShipAgent for PointShipAgent {
|
||||||
|
fn update_controls(
|
||||||
|
&mut self,
|
||||||
|
_dt: f32,
|
||||||
|
sf: &SystemStateframe,
|
||||||
|
this_ship: PhysSimShipHandle,
|
||||||
|
) -> Option<ShipControls> {
|
||||||
|
let mut controls = ShipControls::new();
|
||||||
|
|
||||||
|
let my_ship = match sf.get_ship(&this_ship) {
|
||||||
|
None => return None,
|
||||||
|
Some(s) => s,
|
||||||
|
};
|
||||||
|
|
||||||
|
let this_rigidbody = &my_ship.rigidbody;
|
||||||
|
let my_position = this_rigidbody.translation();
|
||||||
|
let my_rotation = this_rigidbody.rotation();
|
||||||
|
let my_angvel = this_rigidbody.angvel();
|
||||||
|
let my_faction = my_ship.data.get_faction();
|
||||||
|
|
||||||
|
// Iterate all possible targets
|
||||||
|
let mut hostile_ships = sf.iter_ships().filter(
|
||||||
|
// Target only flying ships we're hostile towards
|
||||||
|
|s| match my_faction
|
||||||
|
.relationships
|
||||||
|
.get(&s.data.get_faction().index)
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Relationship::Hostile => match s.data.get_state() {
|
||||||
|
ShipState::Flying { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the closest target
|
||||||
|
let mut closest_enemy_position = match hostile_ships.next() {
|
||||||
|
Some(s) => s.rigidbody.translation(),
|
||||||
|
None => return Some(controls), // Do nothing if no targets are available
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut d = (my_position - closest_enemy_position).magnitude();
|
||||||
|
for s in hostile_ships {
|
||||||
|
let p = s.rigidbody.translation();
|
||||||
|
let new_d = (my_position - p).magnitude();
|
||||||
|
if new_d < d {
|
||||||
|
d = new_d;
|
||||||
|
closest_enemy_position = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let angle = clockwise_angle(
|
||||||
|
&(my_rotation * Vector2::new(1.0, 0.0)),
|
||||||
|
&(closest_enemy_position - my_position),
|
||||||
|
);
|
||||||
|
|
||||||
|
if angle < 0.0 && my_angvel > -0.3 {
|
||||||
|
controls.right = true;
|
||||||
|
} else if angle > 0.0 && my_angvel < 0.3 {
|
||||||
|
controls.left = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.guns = true;
|
||||||
|
return Some(controls);
|
||||||
|
}
|
||||||
|
}
|
126
lib/system/src/stateframe.rs
Normal file
126
lib/system/src/stateframe.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
//! Provides a snapshot of one frame of a physics simulation
|
||||||
|
//! that can then be passed to a renderer to be drawn.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use galactica_content::{EnginePoint, SpriteAutomaton};
|
||||||
|
use rapier2d::{dynamics::RigidBody, prelude::ColliderHandle};
|
||||||
|
|
||||||
|
use crate::instance::{
|
||||||
|
ship::{data::ShipData, ShipCollapseSequence},
|
||||||
|
EffectInstance, ProjectileInstance,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: replace with a more generic handle
|
||||||
|
/// A handle for a ship in this simulation
|
||||||
|
/// This lets other crates reference ships
|
||||||
|
/// without including `rapier2d`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PhysSimShipHandle(pub ColliderHandle);
|
||||||
|
|
||||||
|
/// A snapshot of one frame of a system simulation
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SystemStateframe {
|
||||||
|
/// The ships in this frame
|
||||||
|
pub(crate) ships: Vec<ShipStateframe>,
|
||||||
|
|
||||||
|
/// Map ship handles to indices in ships
|
||||||
|
pub(crate) ship_map: HashMap<PhysSimShipHandle, usize>,
|
||||||
|
|
||||||
|
/// The projectiles in this frame
|
||||||
|
pub(crate) projectiles: Vec<ProjectileStateframe>,
|
||||||
|
|
||||||
|
/// The effects in this frame
|
||||||
|
pub(crate) effects: Vec<EffectStateframe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemStateframe {
|
||||||
|
/// Create an empty simulation image
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ships: Vec::new(),
|
||||||
|
projectiles: Vec::new(),
|
||||||
|
ship_map: HashMap::new(),
|
||||||
|
effects: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all buffers in this image
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
self.ship_map.clear();
|
||||||
|
self.projectiles.clear();
|
||||||
|
self.effects.clear();
|
||||||
|
self.ships.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate ships in this image
|
||||||
|
pub fn iter_ships(&self) -> impl Iterator<Item = &ShipStateframe> {
|
||||||
|
self.ships.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a ship by its handle
|
||||||
|
pub fn get_ship(&self, handle: &PhysSimShipHandle) -> Option<&ShipStateframe> {
|
||||||
|
self.ship_map.get(handle).map(|x| &self.ships[*x])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate projectiles in this image
|
||||||
|
pub fn iter_projectiles(&self) -> impl Iterator<Item = &ProjectileStateframe> {
|
||||||
|
self.projectiles.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate effects in this image
|
||||||
|
pub fn iter_effects(&self) -> impl Iterator<Item = &EffectStateframe> {
|
||||||
|
self.effects.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A snapshot of a ship's state
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ShipStateframe {
|
||||||
|
/// This ship's unique id
|
||||||
|
pub uid: u64,
|
||||||
|
|
||||||
|
/// This ship's game data
|
||||||
|
pub data: ShipData,
|
||||||
|
|
||||||
|
/// This ship's sprite animation state
|
||||||
|
pub anim: SpriteAutomaton,
|
||||||
|
|
||||||
|
/// Animation state for each of this ship's engines
|
||||||
|
pub engine_anim: Vec<(EnginePoint, SpriteAutomaton)>,
|
||||||
|
|
||||||
|
/// This ship's collapse sequence
|
||||||
|
pub collapse_sequence: Option<ShipCollapseSequence>,
|
||||||
|
|
||||||
|
/// The ship's rigidbody
|
||||||
|
pub rigidbody: RigidBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a snapshot of a projectile's state
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProjectileStateframe {
|
||||||
|
/// The projectile's data
|
||||||
|
pub projectile: ProjectileInstance,
|
||||||
|
|
||||||
|
/// The projectile's rigidbody
|
||||||
|
pub rigidbody: RigidBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a snapshot of a projectile's state
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EffectStateframe {
|
||||||
|
/// The effect's data
|
||||||
|
pub effect: EffectInstance,
|
||||||
|
|
||||||
|
/// The effect's rigidbody
|
||||||
|
pub rigidbody: RigidBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectStateframe {
|
||||||
|
/// Get this effect's fade value
|
||||||
|
pub fn get_fade(&self) -> f32 {
|
||||||
|
let f = self.effect.fade;
|
||||||
|
let l = self.effect.remaining_lifetime();
|
||||||
|
return 1f32.min(l / f);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user