use anyhow::{bail, Context, Result}; use galactica_util::to_radians; use nalgebra::{Point2, Rotation2, Vector2}; use rapier2d::geometry::{Collider, ColliderBuilder}; use std::{collections::HashMap, fmt::Debug, hash::Hash}; use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace}; pub(crate) mod syntax { use crate::part::{effect::syntax::EffectReference, outfitspace}; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] pub struct Ship { pub sprite: String, pub thumbnail: String, pub size: f32, pub engines: Vec, pub guns: Vec, pub hull: f32, pub mass: f32, pub collision: Vec<[f32; 2]>, pub angular_drag: f32, pub linear_drag: f32, pub space: outfitspace::syntax::OutfitSpace, pub collapse: Option, pub damage: Option, } #[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, } #[derive(Debug, Deserialize)] pub struct Damage { pub hull: f32, pub effects: Vec, } #[derive(Debug, Deserialize)] pub struct DamageEffectSpawner { pub effect: EffectReference, pub frequency: f32, pub pos: Option<[f32; 2]>, } // TODO: // plural or not? document! #[derive(Debug, Deserialize)] pub struct Collapse { pub length: f32, pub effects: Vec, pub event: Vec, } #[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, } } // Processed data structs. // These are exported. /// Represents a ship chassis. #[derive(Debug, Clone)] pub struct Ship { /// This ship's name pub name: String, /// This ship's sprite pub sprite: SpriteHandle, /// This ship's thumbnail pub thumbnail: SpriteHandle, /// The size of this ship. /// Measured as unrotated height, /// in terms of game units. pub size: f32, /// The mass of this ship pub mass: f32, /// Engine points on this ship. /// This is where engine flares are drawn. pub engines: Vec, /// Gun points on this ship. /// A gun outfit can be mounted on each. pub guns: Vec, /// This ship's hull strength pub hull: f32, /// Collision shape for this ship pub collider: CollisionDebugWrapper, /// Remove later pub aspect: f32, /// Reduction in angular velocity over time pub angular_drag: f32, /// Reduction in velocity over time pub linear_drag: f32, /// Outfit space in this ship pub space: OutfitSpace, /// Ship collapse sequence pub collapse: ShipCollapse, /// Damaged ship effects pub damage: ShipDamage, } /// 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) } } /// An engine point on a ship. /// This is where flares are drawn. #[derive(Debug, Clone)] pub struct EnginePoint { /// This engine point's position, in game units, /// relative to the ship's center. pub pos: Vector2, /// The size of the flare that should be drawn /// at this point, measured as height in game units. pub size: f32, } /// A gun point on a ship. #[derive(Debug, Clone)] pub struct GunPoint { /// This gun point's index in this ship pub idx: u32, /// This gun point's position, in game units, /// relative to the ship's center. pub pos: Vector2, } 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(&self, state: &mut H) { self.idx.hash(state); } } /// 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, /// Scripted events during ship collapse pub events: Vec, } /// 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, } /// 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>, } /// An effect shown during a ship collapse sequence #[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. /// Position is random if None. pub pos: Option>, } /// 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, } impl crate::Build for Ship { type InputSyntaxType = HashMap; fn build( ship: Self::InputSyntaxType, build_context: &mut ContentBuildContext, ct: &mut Content, ) -> Result<()> { for (ship_name, ship) in ship { let sprite = match ct.sprite_index.get(&ship.sprite) { None => bail!( "In ship `{}`: sprite `{}` doesn't exist", ship_name, ship.sprite ), Some(t) => *t, }; let thumbnail = match ct.sprite_index.get(&ship.thumbnail) { None => bail!( "In ship `{}`: thumbnail sprite `{}` doesn't exist", ship_name, ship.thumbnail ), Some(t) => *t, }; let size = ship.size; let aspect = ct.get_sprite(sprite).aspect; 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, pos: e.pos.map(|p| { Point2::new(p[0] * (size / 2.0) * aspect, p[1] * size / 2.0) }), }); } 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, 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, })) } } } ShipCollapse { length: c.length, effects, events, } } else { // Default collapse sequence ShipCollapse { length: 0.0, effects: vec![], events: vec![], } } }; 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, 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![], } } }; // TODO: document this let mut guns = Vec::new(); for g in ship.guns { guns.push(GunPoint { idx: guns.len() as u32, // 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), }) } // 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> = 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, // which will break effect placement on top of ships (i.e, collapse effects) 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() }; ct.ships.push(Self { sprite, thumbnail, aspect, collapse, damage, name: ship_name, mass: ship.mass, space: OutfitSpace::from(ship.space), angular_drag: ship.angular_drag, linear_drag: ship.linear_drag, size, hull: ship.hull, engines: ship .engines .iter() .map(|e| EnginePoint { pos: Vector2::new(e.x * size * aspect / 2.0, e.y * size / 2.0), size: e.size, }) .collect(), guns, collider: CollisionDebugWrapper(collider), }); } return Ok(()); } }