462 lines
10 KiB
Rust
Raw Normal View History

2024-01-05 18:04:30 -08:00
use anyhow::{bail, Context, Result};
2024-01-12 22:47:40 -08:00
use galactica_util::to_radians;
use nalgebra::{Point2, Rotation2, Vector2};
use rapier2d::geometry::{Collider, ColliderBuilder};
use std::{collections::HashMap, fmt::Debug, hash::Hash};
2024-01-05 18:04:30 -08:00
use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace};
2023-12-30 21:05:06 -08:00
2023-12-30 16:57:03 -08:00
pub(crate) mod syntax {
2024-01-05 18:04:30 -08:00
use crate::part::{effect::syntax::EffectReference, outfitspace};
2023-12-27 19:51:58 -08:00
use serde::Deserialize;
2023-12-30 21:05:06 -08:00
2023-12-27 19:51:58 -08:00
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Ship {
pub sprite: String,
2024-02-02 16:45:23 -08:00
pub thumb: String,
2023-12-27 19:51:58 -08:00
pub size: f32,
pub engines: Vec<Engine>,
pub guns: Vec<Gun>,
pub hull: f32,
2023-12-30 11:07:20 -08:00
pub mass: f32,
2024-01-05 13:25:44 -08:00
pub collision: Vec<[f32; 2]>,
2023-12-30 17:51:57 -08:00
pub angular_drag: f32,
pub linear_drag: f32,
2024-01-05 12:09:59 -08:00
pub space: outfitspace::syntax::OutfitSpace,
2024-01-05 18:04:30 -08:00
pub collapse: Option<Collapse>,
pub damage: Option<Damage>,
}
2023-12-27 19:51:58 -08:00
#[derive(Debug, Deserialize)]
pub struct Engine {
pub x: f32,
pub y: f32,
pub size: f32,
}
#[derive(Debug, Deserialize)]
pub struct Gun {
pub x: f32,
pub y: f32,
}
2024-01-05 18:04:30 -08:00
#[derive(Debug, Deserialize)]
pub struct Damage {
pub hull: f32,
pub effects: Vec<DamageEffectSpawner>,
}
#[derive(Debug, Deserialize)]
pub struct DamageEffectSpawner {
pub effect: EffectReference,
pub frequency: f32,
pub pos: Option<[f32; 2]>,
}
2024-01-05 18:04:30 -08:00
// TODO:
// plural or not? document!
#[derive(Debug, Deserialize)]
pub struct Collapse {
pub length: f32,
pub effects: Vec<CollapseEffectSpawner>,
pub event: Vec<CollapseEvent>,
}
#[derive(Debug, Deserialize)]
pub struct CollapseEffectSpawner {
pub effect: EffectReference,
pub count: f32,
pub pos: Option<[f32; 2]>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CollapseEvent {
Effect(EffectCollapseEvent),
}
#[derive(Debug, Deserialize)]
pub struct EffectCollapseEvent {
pub time: f32,
pub effects: Vec<CollapseEffectSpawner>,
}
2023-12-27 19:51:58 -08:00
}
// Processed data structs.
// These are exported.
2023-12-29 15:46:09 -08:00
/// Represents a ship chassis.
2023-12-27 19:51:58 -08:00
#[derive(Debug, Clone)]
pub struct Ship {
2023-12-29 15:46:09 -08:00
/// This ship's name
2023-12-27 19:51:58 -08:00
pub name: String,
2023-12-29 15:46:09 -08:00
/// This ship's sprite
pub sprite: SpriteHandle,
2023-12-29 15:46:09 -08:00
2024-02-02 16:45:23 -08:00
/// This ship's thumbnail
pub thumb: SpriteHandle,
2023-12-29 15:46:09 -08:00
/// The size of this ship.
/// Measured as unrotated height,
/// in terms of game units.
2023-12-27 19:51:58 -08:00
pub size: f32,
2023-12-29 15:46:09 -08:00
2023-12-30 11:07:20 -08:00
/// The mass of this ship
pub mass: f32,
2023-12-29 15:46:09 -08:00
/// Engine points on this ship.
/// This is where engine flares are drawn.
2023-12-28 17:04:41 -08:00
pub engines: Vec<EnginePoint>,
2023-12-29 15:46:09 -08:00
/// Gun points on this ship.
/// A gun outfit can be mounted on each.
2023-12-28 17:04:41 -08:00
pub guns: Vec<GunPoint>,
2023-12-29 15:46:09 -08:00
/// This ship's hull strength
pub hull: f32,
/// Collision shape for this ship
2024-01-12 22:47:40 -08:00
pub collider: CollisionDebugWrapper,
/// Remove later
pub aspect: f32,
2023-12-30 17:51:57 -08:00
/// Reduction in angular velocity over time
pub angular_drag: f32,
/// Reduction in velocity over time
pub linear_drag: f32,
2023-12-30 21:05:06 -08:00
/// Outfit space in this ship
pub space: OutfitSpace,
2024-01-05 18:04:30 -08:00
/// Ship collapse sequence
pub collapse: ShipCollapse,
/// Damaged ship effects
pub damage: ShipDamage,
}
2024-01-12 22:47:40 -08:00
/// Hack to give `Collider` a fake debug method.
/// Pretend this is transparent, get the collider with .0.
#[derive(Clone)]
pub struct CollisionDebugWrapper(pub Collider);
impl Debug for CollisionDebugWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
"CollisionDebugWrapper".fmt(f)
}
2023-12-27 19:51:58 -08:00
}
2023-12-29 15:46:09 -08:00
/// An engine point on a ship.
/// This is where flares are drawn.
2023-12-27 19:51:58 -08:00
#[derive(Debug, Clone)]
2023-12-28 17:04:41 -08:00
pub struct EnginePoint {
2023-12-29 15:46:09 -08:00
/// This engine point's position, in game units,
/// relative to the ship's center.
2024-01-12 22:47:40 -08:00
pub pos: Vector2<f32>,
2023-12-29 15:46:09 -08:00
/// The size of the flare that should be drawn
/// at this point, measured as height in game units.
2023-12-27 19:51:58 -08:00
pub size: f32,
}
2023-12-29 15:46:09 -08:00
/// A gun point on a ship.
2023-12-27 19:51:58 -08:00
#[derive(Debug, Clone)]
2023-12-28 17:04:41 -08:00
pub struct GunPoint {
2024-01-08 22:38:36 -08:00
/// This gun point's index in this ship
pub idx: u32,
2023-12-29 15:46:09 -08:00
/// This gun point's position, in game units,
/// relative to the ship's center.
2024-01-12 22:47:40 -08:00
pub pos: Vector2<f32>,
2023-12-27 19:51:58 -08:00
}
2024-01-08 22:38:36 -08:00
impl Eq for GunPoint {}
impl PartialEq for GunPoint {
fn eq(&self, other: &Self) -> bool {
self.idx == other.idx
}
}
// We use a hashmap of these in OutfitSet
impl Hash for GunPoint {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.idx.hash(state);
}
}
2024-01-05 18:04:30 -08:00
/// Parameters for a ship's collapse sequence
#[derive(Debug, Clone)]
pub struct ShipCollapse {
/// Collapse sequence length, in seconds
pub length: f32,
/// Effects to create during collapse
pub effects: Vec<CollapseEffectSpawner>,
/// Scripted events during ship collapse
pub events: Vec<CollapseEvent>,
}
/// Parameters for damaged ship effects
#[derive(Debug, Clone)]
pub struct ShipDamage {
/// Show damaged ship effects if hull is below this value
pub hull: f32,
/// Effects to create during collapse
pub effects: Vec<DamageEffectSpawner>,
}
/// An effect shown when a ship is damaged
#[derive(Debug, Clone)]
pub struct DamageEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
/// How often to create this effect
pub frequency: f32,
/// Where to create is effect.
/// Position is random if None.
pub pos: Option<Point2<f32>>,
}
/// An effect shown during a ship collapse sequence
2024-01-05 18:04:30 -08:00
#[derive(Debug, Clone)]
pub struct CollapseEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
/// How many effects to create
pub count: f32,
/// Where to create this effect.
2024-01-05 18:04:30 -08:00
/// Position is random if None.
pub pos: Option<Point2<f32>>,
}
/// A scripted event during a ship collapse sequence
#[derive(Debug, Clone)]
pub enum CollapseEvent {
/// A scripted effect during a ship collapse sequence
Effect(EffectCollapseEvent),
}
/// A scripted effect during a ship collapse sequence
#[derive(Debug, Clone)]
pub struct EffectCollapseEvent {
/// When to trigger this event
pub time: f32,
/// The effect to create
pub effects: Vec<CollapseEffectSpawner>,
}
2023-12-30 16:57:03 -08:00
impl crate::Build for Ship {
type InputSyntaxType = HashMap<String, syntax::Ship>;
2024-01-05 12:09:59 -08:00
fn build(
ship: Self::InputSyntaxType,
2024-01-05 18:04:30 -08:00
build_context: &mut ContentBuildContext,
ct: &mut Content,
2024-01-05 12:09:59 -08:00
) -> Result<()> {
2023-12-27 20:13:39 -08:00
for (ship_name, ship) in ship {
2024-02-02 16:45:23 -08:00
let sprite = match ct.sprite_index.get(&ship.sprite) {
None => bail!(
"In ship `{}`: sprite `{}` doesn't exist",
ship_name,
ship.sprite
),
Some(t) => *t,
};
2024-02-02 16:45:23 -08:00
let thumb = match ct.sprite_index.get(&ship.thumb) {
None => bail!(
"In ship `{}`: thumbnail sprite `{}` doesn't exist",
ship_name,
ship.thumb
),
Some(t) => *t,
};
2023-12-30 11:07:20 -08:00
let size = ship.size;
2024-02-02 16:45:23 -08:00
let aspect = ct.get_sprite(sprite).aspect;
2024-01-05 18:04:30 -08:00
let collapse = {
if let Some(c) = ship.collapse {
let mut effects = Vec::new();
for e in c.effects {
effects.push(CollapseEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.with_context(|| format!("while loading ship `{}`", ship_name))?,
count: e.count,
2024-01-12 22:47:40 -08:00
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * aspect, p[1] * size / 2.0)
}),
});
}
2024-01-05 18:04:30 -08:00
let mut events = Vec::new();
for e in c.event {
match e {
syntax::CollapseEvent::Effect(e) => {
let mut effects = Vec::new();
for g in e.effects {
effects.push(CollapseEffectSpawner {
effect: g
.effect
.to_handle(build_context, ct)
.with_context(|| {
format!("while loading ship `{}`", ship_name)
})?,
count: g.count,
2024-01-12 22:47:40 -08:00
pos: g.pos.map(|p| {
Point2::new(
p[0] * (size / 2.0) * aspect,
p[1] * size / 2.0,
)
}),
})
}
events.push(CollapseEvent::Effect(EffectCollapseEvent {
time: e.time,
effects,
}))
2024-01-05 18:04:30 -08:00
}
}
}
ShipCollapse {
length: c.length,
effects,
events,
}
} else {
// Default collapse sequence
ShipCollapse {
length: 0.0,
effects: vec![],
events: vec![],
}
2024-01-05 18:04:30 -08:00
}
};
let damage = {
if let Some(c) = ship.damage {
let mut effects = Vec::new();
for e in c.effects {
effects.push(DamageEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.with_context(|| format!("while loading ship `{}`", ship_name))?,
frequency: e.frequency,
2024-01-12 22:47:40 -08:00
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * aspect, p[1] * size / 2.0)
}),
});
}
ShipDamage {
hull: c.hull,
effects: effects,
}
} else {
// Default damage effects
ShipDamage {
hull: 0.0,
effects: vec![],
}
2024-01-05 18:04:30 -08:00
}
};
2023-12-30 11:07:20 -08:00
2024-01-08 22:38:36 -08:00
// TODO: document this
let mut guns = Vec::new();
for g in ship.guns {
guns.push(GunPoint {
idx: guns.len() as u32,
2024-01-12 22:47:40 -08:00
// Angle adjustment, since sprites point north
// and 0 degrees is east in the game
pos: Rotation2::new(to_radians(-90.0))
* Vector2::new(g.x * size * aspect / 2.0, g.y * size / 2.0),
2024-01-08 22:38:36 -08:00
})
}
2024-01-12 22:47:40 -08:00
// Build rapier2d collider
let collider = {
let indices: Vec<[u32; 2]> = (0..ship.collision.len())
.map(|x| {
// Auto-generate mesh lines:
// [ [0, 1], [1, 2], ..., [n, 0] ]
let next = if x == ship.collision.len() - 1 {
0
} else {
x + 1
};
[x as u32, next as u32]
})
.collect();
let points: Vec<Point2<f32>> = ship
.collision
.iter()
.map(|x| {
// Angle adjustment: rotate collider to match sprite
// (Sprites (and their colliders) point north, but 0 is east in the game world)
// We apply this pointwise so that local points inside the collider work as we expect.
//
// If we don't, rapier2 will compute local points pre-rotation,
2024-01-23 18:44:32 -08:00
// which will break effect placement on top of ships (i.e, collapse effects)
2024-01-12 22:47:40 -08:00
Rotation2::new(to_radians(-90.0))
* Point2::new(x[0] * (size / 2.0) * aspect, x[1] * size / 2.0)
})
.collect();
ColliderBuilder::convex_decomposition(&points[..], &indices[..])
.mass(ship.mass)
.build()
};
2024-01-05 18:04:30 -08:00
ct.ships.push(Self {
2024-02-02 16:45:23 -08:00
sprite,
thumb,
aspect,
2024-01-05 18:04:30 -08:00
collapse,
damage,
2023-12-30 11:07:20 -08:00
name: ship_name,
mass: ship.mass,
2023-12-30 21:05:06 -08:00
space: OutfitSpace::from(ship.space),
2023-12-30 17:51:57 -08:00
angular_drag: ship.angular_drag,
linear_drag: ship.linear_drag,
2023-12-28 20:19:33 -08:00
size,
hull: ship.hull,
2024-01-05 13:25:44 -08:00
2023-12-27 19:51:58 -08:00
engines: ship
.engines
.iter()
2023-12-28 17:04:41 -08:00
.map(|e| EnginePoint {
2024-01-12 22:47:40 -08:00
pos: Vector2::new(e.x * size * aspect / 2.0, e.y * size / 2.0),
2023-12-27 19:51:58 -08:00
size: e.size,
})
.collect(),
2024-01-05 13:25:44 -08:00
2024-01-08 22:38:36 -08:00
guns,
2024-01-05 13:25:44 -08:00
2024-01-12 22:47:40 -08:00
collider: CollisionDebugWrapper(collider),
2023-12-27 19:51:58 -08:00
});
}
return Ok(());
2023-12-27 19:51:58 -08:00
}
}