TMP
This commit is contained in:
parent
3150f64bd1
commit
1bea5c777c
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/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]
|
||||
galactica-util = { path = "lib/util" }
|
||||
galactica-packer = { path = "lib/packer" }
|
||||
galactica-content = { path = "lib/content" }
|
||||
|
||||
# Utilities
|
||||
tracing = "0.1"
|
||||
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