This commit is contained in:
Mark 2025-04-04 09:44:20 -07:00
parent 3150f64bd1
commit 1bea5c777c
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
22 changed files with 3839 additions and 2 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
*.ignore

1395
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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 }

View 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
}
}

View 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;

View 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()
}
}

View 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);
}

View 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;
}
}

View File

@ -0,0 +1,7 @@
mod outfitset;
mod ship;
mod shipstate;
pub use outfitset::*;
pub use ship::*;
pub use shipstate::*;

View 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
}
}

View 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
}
}

View 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,
}
}
}

View File

@ -0,0 +1,7 @@
pub mod autopilot;
mod collapse;
pub(crate) mod data;
mod ship;
pub use collapse::ShipCollapseSequence;
pub use ship::ShipInstance;

View 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
View 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(),
})
}
}
}

View 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);
}
}

View 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>;
}

View 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
}
}

View 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);
}
}

View 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);
}
}