Added content

This commit is contained in:
Mark 2025-01-04 17:58:02 -08:00
parent b634575791
commit 3150f64bd1
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
14 changed files with 3218 additions and 0 deletions

32
lib/content/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "galactica-content"
description = "Galactica's game content parser"
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-util = { workspace = true }
galactica-packer = { workspace = true }
nalgebra = { workspace = true }
rhai = { workspace = true }
walkdir = { workspace = true }
smartstring = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
toml = { workspace = true }
serde = { workspace = true }
lazy_static = { workspace = true }
rapier2d = { workspace = true }

39
lib/content/src/error.rs Normal file
View File

@ -0,0 +1,39 @@
use thiserror::Error;
use tracing::error;
pub type ContentLoadResult<T> = Result<T, ContentLoadError>;
#[derive(Debug, Error)]
pub enum ContentLoadError {
/// A generic I/O error
#[error("I/O error: {error}")]
IoError {
#[from]
error: std::io::Error,
},
/// Toml deserialization error
#[error("TOML deserialization error: {error}")]
TomlError {
#[from]
error: toml::de::Error,
},
#[error("Content dir has multiple config tables")]
MultipleConfigTables,
#[error("Content dir does not have a config table")]
MissingConfigTable,
#[error("Content dir has duplicate key `{key}`")]
DuplicateKey { key: String },
#[error("{msg}")]
Generic { msg: String },
#[error("error while compiling rhai script: {error}")]
RhaiCompileError {
#[from]
error: Box<rhai::EvalAltResult>,
},
}

361
lib/content/src/lib.rs Normal file
View File

@ -0,0 +1,361 @@
#![warn(missing_docs)]
//! This subcrate is responsible for loading, parsing, validating game content.
mod error;
mod part;
mod spriteautomaton;
mod util;
use error::ContentLoadResult;
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use rhai::ImmutableString;
use serde::{Deserialize, Deserializer};
use smartstring::{LazyCompact, SmartString};
use std::{
borrow::Borrow,
collections::HashMap,
fmt::Display,
fs::File,
io::Read,
num::NonZeroU32,
path::{Path, PathBuf},
sync::Arc,
};
use toml;
use tracing::{debug, info, warn};
use walkdir::WalkDir;
pub use part::*;
pub use spriteautomaton::*;
mod syntax {
use serde::Deserialize;
use std::{collections::HashMap, fmt::Display, hash::Hash};
use tracing::debug;
use crate::{
config,
error::{ContentLoadError, ContentLoadResult},
part::{effect, faction, outfit, ship, sprite, system},
};
#[derive(Debug, Deserialize)]
pub struct Root {
pub ship: Option<HashMap<String, ship::syntax::Ship>>,
pub system: Option<HashMap<String, system::syntax::System>>,
pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>,
pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>,
pub faction: Option<HashMap<String, faction::syntax::Faction>>,
pub effect: Option<HashMap<String, effect::syntax::Effect>>,
pub config: Option<config::syntax::Config>,
}
fn merge_hashmap<K, V>(
to: &mut Option<HashMap<K, V>>,
mut from: Option<HashMap<K, V>>,
) -> ContentLoadResult<()>
where
K: Hash + Eq + Display,
{
if to.is_none() {
*to = from.take();
return Ok(());
}
if let Some(from_inner) = from {
let to_inner = to.as_mut().unwrap();
for (k, v) in from_inner {
if to_inner.contains_key(&k) {
return Err(ContentLoadError::DuplicateKey { key: k.to_string() });
} else {
to_inner.insert(k, v);
}
}
}
return Ok(());
}
impl Root {
pub fn new() -> Self {
Self {
ship: None,
system: None,
outfit: None,
sprite: None,
faction: None,
effect: None,
config: None,
}
}
pub fn merge(&mut self, other: Root) -> ContentLoadResult<()> {
debug!(message = "Merging ships");
merge_hashmap(&mut self.ship, other.ship)?;
debug!(message = "Merging systems");
merge_hashmap(&mut self.system, other.system)?;
debug!(message = "Merging outfits");
merge_hashmap(&mut self.outfit, other.outfit)?;
debug!(message = "Merging sprites");
merge_hashmap(&mut self.sprite, other.sprite)?;
debug!(message = "Merging factions");
merge_hashmap(&mut self.faction, other.faction)?;
debug!(message = "Merging effects");
merge_hashmap(&mut self.effect, other.effect)?;
if self.config.is_some() {
if other.config.is_some() {
return Err(ContentLoadError::MultipleConfigTables);
}
} else {
self.config = other.config;
}
return Ok(());
}
}
}
trait Build {
type InputSyntaxType;
/// Build a processed System struct from raw serde data
fn build(
root: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> ContentLoadResult<()>
where
Self: Sized;
}
/// The type we use to index content objects
/// These are only unique WITHIN each "type" of object!
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct ContentIndex(Arc<SmartString<LazyCompact>>);
impl From<ImmutableString> for ContentIndex {
fn from(value: ImmutableString) -> Self {
Self(Arc::new(value.into()))
}
}
impl From<ContentIndex> for ImmutableString {
fn from(value: ContentIndex) -> Self {
ImmutableString::from(value.0.as_ref().clone())
}
}
impl From<&ContentIndex> for ImmutableString {
fn from(value: &ContentIndex) -> Self {
let x: &SmartString<LazyCompact> = value.0.borrow();
ImmutableString::from(x.clone())
}
}
impl ContentIndex {
/// Make a new ContentIndex from a strign
pub fn new(s: &str) -> Self {
Self(SmartString::from(s).into())
}
/// Get a &str to this index's content
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl<'d> Deserialize<'d> for ContentIndex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::new(&s))
}
}
impl Display for ContentIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
/// Stores temporary data while building context objects
#[derive(Debug)]
pub(crate) struct ContentBuildContext {
pub effect_index: HashMap<ContentIndex, Arc<Effect>>,
}
impl ContentBuildContext {
fn new() -> Self {
Self {
effect_index: HashMap::new(),
}
}
}
/// Represents static game content
#[derive(Debug)]
pub struct Content {
/// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas,
/// Sprites defined in this game
pub sprites: HashMap<ContentIndex, Arc<Sprite>>,
/// Outfits defined in this game
pub outfits: HashMap<ContentIndex, Arc<Outfit>>,
/// Ships defined in this game
pub ships: HashMap<ContentIndex, Arc<Ship>>,
/// Systems defined in this game
pub systems: HashMap<ContentIndex, Arc<System>>,
/// Factions defined in this game
pub factions: HashMap<ContentIndex, Arc<Faction>>,
/// Effects defined in this game
pub effects: HashMap<ContentIndex, Arc<Effect>>,
/// Game configuration
pub config: Config,
}
// Loading methods
impl Content {
fn try_parse(path: &Path) -> ContentLoadResult<syntax::Root> {
let mut file_string = String::new();
let _ = File::open(path)?.read_to_string(&mut file_string);
let file_string = file_string.trim();
return Ok(toml::from_str(&file_string)?);
}
/// Load content from a directory.
pub fn load_dir(
content_root: PathBuf,
asset_root: PathBuf,
atlas_index: PathBuf,
) -> ContentLoadResult<Self> {
let mut root = syntax::Root::new();
for e in WalkDir::new(&content_root)
.into_iter()
.filter_map(|e| e.ok())
{
if e.metadata().unwrap().is_file() {
// TODO: better warnings
match e.path().extension() {
Some(t) => {
if t.to_str() != Some("toml") {
warn!("{e:#?} is not a toml file, skipping");
continue;
}
}
None => {
warn!("{e:#?} is not a toml file, skipping");
continue;
}
}
let path = e.path();
info!(message = "Loading content file", ?path);
let this_root = Self::try_parse(path)?;
debug!(message = "Merging content", ?path);
root.merge(this_root)?;
}
}
let atlas: SpriteAtlas = {
let mut file_string = String::new();
let _ = File::open(atlas_index)?.read_to_string(&mut file_string);
let file_string = file_string.trim();
toml::from_str(&file_string)?
};
let mut build_context = ContentBuildContext::new();
let mut content = Self {
config: {
if let Some(c) = root.config {
debug!(message = "Parsing config table");
c.build(&asset_root, &content_root, &atlas)?
} else {
return Err(error::ContentLoadError::MissingConfigTable);
}
},
sprite_atlas: atlas,
systems: HashMap::new(),
ships: HashMap::new(),
outfits: HashMap::new(),
sprites: HashMap::new(),
factions: HashMap::new(),
effects: HashMap::new(),
};
// TODO: enforce sprite and image limits
// Order matters. Some content types require another to be fully initialized
if root.sprite.is_some() {
part::sprite::Sprite::build(
root.sprite.take().unwrap(),
&mut build_context,
&mut content,
)?;
}
if root.effect.is_some() {
part::effect::Effect::build(
root.effect.take().unwrap(),
&mut build_context,
&mut content,
)?;
}
// Order below this line does not matter
if root.ship.is_some() {
part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?;
}
if root.outfit.is_some() {
part::outfit::Outfit::build(
root.outfit.take().unwrap(),
&mut build_context,
&mut content,
)?;
}
if root.system.is_some() {
part::system::System::build(
root.system.take().unwrap(),
&mut build_context,
&mut content,
)?;
}
if root.faction.is_some() {
part::faction::Faction::build(
root.faction.take().unwrap(),
&mut build_context,
&mut content,
)?;
}
return Ok(content);
}
}
// Access methods
impl Content {
/// Get the list of atlas files we may use
pub fn atlas_files(&self) -> &Vec<String> {
return &self.sprite_atlas.atlas_list;
}
/// Get sprite atlas metadata
pub fn get_atlas(&self) -> &SpriteAtlas {
return &self.sprite_atlas;
}
/// Get a texture by its index
pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
&self.sprite_atlas.get_by_idx(idx)
}
}

View File

@ -0,0 +1,203 @@
use rhai::AST;
use std::{collections::HashMap, num::NonZeroU32, path::PathBuf};
pub(crate) mod syntax {
use galactica_packer::SpriteAtlas;
use rhai::{Engine, OptimizationLevel};
use serde::Deserialize;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use tracing::info;
use crate::error::{ContentLoadError, ContentLoadResult};
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Config {
pub fonts: Fonts,
pub sprite_root: PathBuf,
pub starfield: Starfield,
pub zoom_min: f32,
pub zoom_max: f32,
pub ui_scale: f32,
pub ui_scene: HashMap<String, PathBuf>,
pub start_ui_scene: String,
}
impl Config {
// TODO: clean up build trait
pub fn build(
self,
asset_root: &Path,
content_root: &Path,
atlas: &SpriteAtlas,
) -> ContentLoadResult<super::Config> {
for i in &self.fonts.files {
if !asset_root.join(i).exists() {
return Err(ContentLoadError::Generic {
msg: format!("font file `{i:?}` doesn't exist"),
});
}
}
let starfield_density = 0.01;
let starfield_size = self.starfield.max_dist * self.zoom_max;
let starfield_count = (starfield_size * starfield_density) as i32;
// 12, because that should be enough to tile any screen.
// Starfield squares are tiled to cover the viewport, adapting to any screen ratio.
// An insufficient limit will result in some tiles not being drawn
let starfield_instance_limit = 12 * starfield_count as u64;
let starfield_texture = match atlas.get_idx_by_path(&self.starfield.texture) {
Some(s) => s,
None => {
return Err(ContentLoadError::Generic {
msg: format!(
"starfield texture `{:?}` doesn't exist",
self.starfield.texture
),
});
}
};
let mut engine = Engine::new_raw();
engine.set_optimization_level(OptimizationLevel::Full);
engine.set_max_expr_depths(0, 0);
let mut ui_scenes = HashMap::new();
for (n, p) in self.ui_scene {
info!(message = "Loading scene script", script = n);
ui_scenes.insert(n.clone(), engine.compile_file(content_root.join(p))?);
}
if !ui_scenes.contains_key(&self.start_ui_scene) {
return Err(ContentLoadError::Generic {
msg: format!("starting ui scene `{}` doesn't exist", self.start_ui_scene),
});
}
return Ok(super::Config {
sprite_root: asset_root.join(self.sprite_root),
font_files: self
.fonts
.files
.iter()
.map(|i| asset_root.join(i))
.collect(),
font_sans: self.fonts.sans,
font_serif: self.fonts.serif,
font_mono: self.fonts.mono,
//font_cursive: self.fonts.cursive,
//font_fantasy: self.fonts.fantasy,
starfield_max_dist: self.starfield.max_dist,
starfield_min_dist: self.starfield.min_dist,
starfield_max_size: self.starfield.max_size,
starfield_min_size: self.starfield.min_size,
starfield_texture,
starfield_count,
starfield_density,
starfield_size,
starfield_instance_limit,
zoom_max: self.zoom_max,
zoom_min: self.zoom_min,
ui_scale: self.ui_scale,
ui_scenes,
start_ui_scene: self.start_ui_scene,
});
}
}
#[derive(Debug, Deserialize)]
pub struct Fonts {
pub files: Vec<PathBuf>,
pub sans: String,
pub serif: String,
pub mono: String,
//pub cursive: String,
//pub fantasy: String,
}
#[derive(Debug, Deserialize)]
pub struct Starfield {
pub min_size: f32,
pub max_size: f32,
pub min_dist: f32,
pub max_dist: f32,
pub texture: PathBuf,
}
}
/// Content configuration
#[derive(Debug, Clone)]
pub struct Config {
/// The directory where all images are stored.
/// Image paths are always interpreted relative to this path.
/// This is a subdirectory of the asset root.
pub sprite_root: PathBuf,
/// List of font files to load
pub font_files: Vec<PathBuf>,
/// Sans Serif font family name
pub font_sans: String,
/// Serif font family name
pub font_serif: String,
/// Monospace font family name
pub font_mono: String,
//pub font_cursive: String,
//pub font_fantasy: String,
/// Min size of starfield sprite, in game units
pub starfield_min_size: f32,
/// Max size of starfield sprite, in game units
pub starfield_max_size: f32,
/// Minimum z-distance of starfield star, in game units
pub starfield_min_dist: f32,
/// Maximum z-distance of starfield star, in game units
pub starfield_max_dist: f32,
/// Index of starfield texture
pub starfield_texture: NonZeroU32,
/// Size of a square starfield tile, in game units.
/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units
/// will completely cover a (square) screen.
/// This should be big enough to cover the height of the screen at max zoom.
pub starfield_size: f32,
/// Average number of stars per game unit
pub starfield_density: f32,
/// Number of stars in one starfield tile
/// Must be positive
pub starfield_count: i32,
// TODO: this shouldn't be here, it depends on graphics implementation
/// The maximum number of starfield sprites we can create
pub starfield_instance_limit: u64,
/// Minimum zoom, in game units
pub zoom_min: f32,
/// Maximum zoom,in game units
pub zoom_max: f32,
/// Ui scale factor
pub ui_scale: f32,
/// Ui scene scripts
pub ui_scenes: HashMap<String, AST>,
/// The UI scene we start in.
/// This is guaranteed to be a key in ui_scenes.
pub start_ui_scene: String,
}

View File

@ -0,0 +1,270 @@
use std::{collections::HashMap, sync::Arc};
use tracing::info;
use crate::{error::ContentLoadResult, Content, ContentBuildContext, ContentIndex, Sprite};
pub(crate) mod syntax {
use std::sync::Arc;
use galactica_util::to_radians;
use serde::Deserialize;
use crate::{
error::{ContentLoadError, ContentLoadResult},
Content, ContentBuildContext, ContentIndex, StartEdge,
};
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Effect {
pub sprite: ContentIndex,
pub size: f32,
pub size_rng: Option<f32>,
pub lifetime: TextOrFloat,
pub lifetime_rng: Option<f32>,
pub angle: Option<f32>,
pub angle_rng: Option<f32>,
pub angvel: Option<f32>,
pub angvel_rng: Option<f32>,
pub fade: Option<f32>,
pub fade_rng: Option<f32>,
pub velocity: EffectVelocity,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum EffectVelocity {
Sticky {
sticky: String,
},
Explicit {
scale_parent: Option<f32>,
scale_parent_rng: Option<f32>,
scale_target: Option<f32>,
scale_target_rng: Option<f32>,
direction_rng: Option<f32>,
},
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TextOrFloat {
Text(String),
Float(f32),
}
// We implement building here instead of in super::Effect because
// effects may be defined inline (see EffectReference).
impl Effect {
pub fn add_to(
self,
_build_context: &mut ContentBuildContext,
content: &mut Content,
name: &str,
) -> ContentLoadResult<Arc<super::Effect>> {
let sprite = match content.sprites.get(&self.sprite) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("sprite `{}` doesn't exist", self.sprite),
});
}
Some(t) => t.clone(),
};
let lifetime = match self.lifetime {
TextOrFloat::Float(f) => f,
TextOrFloat::Text(s) => {
if s == "inherit" {
// Match lifetime of first section of sprite
match &sprite.start_at {
StartEdge::Top { section } | StartEdge::Bot { section } => {
section.frame_duration * section.frames.len() as f32
}
}
} else {
return Err(ContentLoadError::Generic {
msg: "bad effect lifetime, must be float or \"inherit\"".to_string(),
});
}
}
};
let velocity = match self.velocity {
EffectVelocity::Explicit {
scale_parent,
scale_parent_rng,
scale_target,
scale_target_rng,
direction_rng,
} => super::EffectVelocity::Explicit {
scale_parent: scale_parent.unwrap_or(0.0),
scale_parent_rng: scale_parent_rng.unwrap_or(0.0),
scale_target: scale_target.unwrap_or(0.0),
scale_target_rng: scale_target_rng.unwrap_or(0.0),
direction_rng: direction_rng.unwrap_or(0.0) / 2.0,
},
EffectVelocity::Sticky { sticky } => {
if sticky == "parent" {
super::EffectVelocity::StickyParent
} else if sticky == "target" {
super::EffectVelocity::StickyTarget
} else {
return Err(ContentLoadError::Generic {
msg: format!("bad sticky specification `{}`", sticky),
});
}
}
};
let e = Arc::new(super::Effect {
name: name.to_string(),
sprite,
velocity,
size: self.size,
size_rng: self.size_rng.unwrap_or(0.0),
lifetime,
lifetime_rng: self.lifetime_rng.unwrap_or(0.0),
angle: to_radians(self.angle.unwrap_or(0.0) / 2.0),
angle_rng: to_radians(self.angle_rng.unwrap_or(0.0) / 2.0),
angvel: to_radians(self.angvel.unwrap_or(0.0)),
angvel_rng: to_radians(self.angvel_rng.unwrap_or(0.0)),
fade: self.fade.unwrap_or(0.0),
fade_rng: self.fade_rng.unwrap_or(0.0),
});
content
.effects
.insert(ContentIndex::new(&e.name), e.clone());
return Ok(e);
}
}
// This isn't used here, but is pulled in by other content items.
/// A reference to an effect by name, or an inline definition.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum EffectReference {
Label(ContentIndex),
Effect(Effect),
}
impl EffectReference {
pub fn resolve(
self,
build_context: &mut ContentBuildContext,
content: &mut Content,
name: &str,
) -> ContentLoadResult<Arc<super::Effect>> {
// We do not insert anything into build_context here,
// since inline effects cannot be referenced by name.
Ok(match self {
Self::Effect(e) => e.add_to(build_context, content, name)?,
Self::Label(l) => match build_context.effect_index.get(&l) {
Some(h) => h.clone(),
None => {
return Err(ContentLoadError::Generic {
msg: format!("no effect named `{}`", l),
});
}
},
})
}
}
}
/// The effect a projectile will spawn when it hits something
#[derive(Debug, Clone)]
pub struct Effect {
/// The sprite to use for this effect.
pub sprite: Arc<Sprite>,
/// This effect's name
pub name: String,
/// The height of this effect, in game units.
pub size: f32,
/// Random size variation
pub size_rng: f32,
/// How many seconds this effect should live
pub lifetime: f32,
/// Random lifetime variation
pub lifetime_rng: f32,
/// The angle this effect points once spawned, in radians
pub angle: f32,
/// Random angle variation, in radians
pub angle_rng: f32,
/// How fast this effect spins, in radians/sec
pub angvel: f32,
/// Random angvel variation
pub angvel_rng: f32,
/// Fade this effect out over this many seconds as it ends
pub fade: f32,
/// Random fade variation
pub fade_rng: f32,
/// How to compute this effect's velocity
pub velocity: EffectVelocity,
}
/// How to compute an effect's velocity
#[derive(Debug, Clone)]
pub enum EffectVelocity {
/// Stick to parent
StickyParent,
/// Stick to target.
/// Zero velocity if no target is given.
StickyTarget,
/// Compute velocity from parent and target
Explicit {
/// How much of the parent's velocity to inherit
scale_parent: f32,
/// Random variation of scale_parent
scale_parent_rng: f32,
/// How much of the target's velocity to keep
scale_target: f32,
/// Random variation of scale_target
scale_target_rng: f32,
/// Random variation of travel direction
direction_rng: f32,
},
}
impl crate::Build for Effect {
type InputSyntaxType = HashMap<String, syntax::Effect>;
fn build(
effects: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> ContentLoadResult<()> {
for (effect_name, effect) in effects {
info!(message = "Evaluating effect", effect = effect_name);
let h = effect.add_to(build_context, content, &effect_name)?;
build_context
.effect_index
.insert(ContentIndex::new(&effect_name), h);
}
return Ok(());
}
}

View File

@ -0,0 +1,125 @@
use serde::Deserialize;
use std::{collections::HashMap, sync::Arc};
use crate::{
error::{ContentLoadError, ContentLoadResult},
Content, ContentBuildContext, ContentIndex,
};
pub(crate) mod syntax {
use std::collections::HashMap;
use serde::Deserialize;
use crate::ContentIndex;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Faction {
pub display_name: String,
pub color: [f32; 3],
pub relationship: HashMap<ContentIndex, super::Relationship>,
}
}
/// How two factions should interact with each other.
/// Relationships are directional: the relationship of
/// `a` to `b` may not equal the relationship of `b` to `a`.
///
/// Relationships dictate how a ship of THIS faction
/// will interact with a ship of the OTHER faction.
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum Relationship {
/// Attack this faction
#[serde(rename = "hostile")]
Hostile,
/// Ignore this faction
#[serde(rename = "neutral")]
Neutral,
/// Protect this faction
#[serde(rename = "friend")]
Friend,
}
/// Represents a game faction
#[derive(Debug, Clone)]
pub struct Faction {
/// The name of this faction
pub index: ContentIndex,
/// The pretty name of this faction
pub display_name: String,
/// This faction's color.
/// Format is RGB, with each color between 0 and 1.
pub color: [f32; 3],
/// Relationships between this faction and other factions
/// This is guaranteed to contain an entry for ALL factions.
pub relationships: HashMap<ContentIndex, Relationship>,
}
impl crate::Build for Faction {
type InputSyntaxType = HashMap<String, syntax::Faction>;
fn build(
factions: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext,
content: &mut Content,
) -> ContentLoadResult<()> {
for (faction_name, faction) in &factions {
// Compute relationships
let mut relationships = HashMap::new();
for (other_name, other_faction) in &factions {
if let Some(r) = faction.relationship.get(&ContentIndex::new(other_name)) {
relationships.insert(ContentIndex::new(other_name), *r);
} else {
// Default relationship, if not specified
// Look at reverse direction...
let other = other_faction
.relationship
.get(&ContentIndex::new(&faction_name));
relationships.insert(
ContentIndex::new(other_name),
// ... and pick a relationship based on that.
match other {
Some(Relationship::Hostile) => Relationship::Hostile {},
_ => Relationship::Neutral {},
},
);
}
}
if faction.color[0] > 1.0
|| faction.color[0] < 0.0
|| faction.color[1] > 1.0
|| faction.color[1] < 0.0
|| faction.color[2] > 1.0
|| faction.color[2] < 0.0
{
return Err(ContentLoadError::Generic {
msg: format!(
"Invalid color for faction `{}`. Value out of range.",
faction_name
),
});
}
content.factions.insert(
ContentIndex::new(faction_name),
Arc::new(Self {
index: ContentIndex::new(faction_name),
display_name: faction.display_name.to_owned(),
relationships,
color: faction.color,
}),
);
}
return Ok(());
}
}

View File

@ -0,0 +1,22 @@
//! Content parts
pub(crate) mod config;
pub(crate) mod effect;
pub(crate) mod faction;
pub(crate) mod outfit;
pub(crate) mod outfitspace;
pub(crate) mod ship;
pub(crate) mod sprite;
pub(crate) mod system;
pub use config::Config;
pub use effect::*;
pub use faction::{Faction, Relationship};
pub use outfit::*;
pub use outfitspace::OutfitSpace;
pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,
ShipCollapse,
};
pub use sprite::*;
pub use system::{System, SystemObject};

View File

@ -0,0 +1,440 @@
use serde::Deserialize;
use std::{collections::HashMap, sync::Arc};
use tracing::info;
use crate::{
error::{ContentLoadError, ContentLoadResult},
resolve_edge_as_edge, Content, ContentBuildContext, ContentIndex, Effect, OutfitSpace,
SectionEdge, Sprite,
};
pub(crate) mod syntax {
use std::sync::Arc;
use crate::{
effect,
error::{ContentLoadError, ContentLoadResult},
part::outfitspace,
sprite::syntax::SectionEdge,
ContentBuildContext, ContentIndex,
};
use galactica_util::to_radians;
use serde::Deserialize;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Outfit {
pub thumbnail: ContentIndex,
pub name: String,
pub desc: String,
pub cost: u32,
pub engine: Option<Engine>,
pub steering: Option<Steering>,
pub space: outfitspace::syntax::OutfitSpace,
pub shield: Option<Shield>,
pub gun: Option<Gun>,
}
#[derive(Debug, Deserialize)]
pub struct Shield {
pub strength: Option<f32>,
pub generation: Option<f32>,
pub delay: Option<f32>,
// more stats: permiability, shield armor, ramp, etc
}
#[derive(Debug, Deserialize)]
pub struct Engine {
pub thrust: f32,
pub flare: EngineFlare,
}
#[derive(Debug, Deserialize)]
pub struct EngineFlare {
pub sprite: ContentIndex,
pub on_start: Option<SectionEdge>,
pub on_stop: Option<SectionEdge>,
}
#[derive(Debug, Deserialize)]
pub struct Steering {
pub power: f32,
}
#[derive(Debug, Deserialize)]
pub struct Gun {
pub projectile: Projectile,
pub rate: f32,
pub rate_rng: Option<f32>,
}
impl Gun {
pub fn build(
self,
build_context: &mut ContentBuildContext,
content: &mut crate::Content,
) -> ContentLoadResult<super::Gun> {
let projectile_sprite = match content
.sprites
.get(&ContentIndex::new(&self.projectile.sprite))
{
None => {
return Err(ContentLoadError::Generic {
msg: format!(
"projectile sprite `{}` doesn't exist",
self.projectile.sprite,
),
});
}
Some(t) => t.clone(),
};
let impact_effect = match self.projectile.impact_effect {
Some(e) => Some(e.resolve(build_context, content, "")?),
None => None,
};
let expire_effect = match self.projectile.expire_effect {
Some(e) => Some(e.resolve(build_context, content, "")?),
None => None,
};
return Ok(super::Gun {
rate: self.rate,
rate_rng: self.rate_rng.unwrap_or(0.0),
projectile: Arc::new(super::Projectile {
force: self.projectile.force,
sprite: projectile_sprite,
size: self.projectile.size,
size_rng: self.projectile.size_rng,
speed: self.projectile.speed,
speed_rng: self.projectile.speed_rng,
lifetime: self.projectile.lifetime,
lifetime_rng: self.projectile.lifetime_rng,
damage: self.projectile.damage,
// Divide by 2, so the angle matches the angle of the fire cone.
// This should ALWAYS be done in the content parser.
angle_rng: to_radians(self.projectile.angle_rng / 2.0).into(),
impact_effect,
expire_effect,
collider: self.projectile.collider,
}),
});
}
}
#[derive(Debug, Deserialize)]
pub struct Projectile {
pub sprite: String,
pub size: f32,
pub size_rng: f32,
pub speed: f32,
pub speed_rng: f32,
pub lifetime: f32,
pub lifetime_rng: f32,
pub damage: f32,
pub angle_rng: f32,
pub impact_effect: Option<effect::syntax::EffectReference>,
pub expire_effect: Option<effect::syntax::EffectReference>,
pub collider: super::ProjectileCollider,
pub force: f32,
}
}
/// Represents an outfit that may be attached to a ship.
#[derive(Debug, Clone)]
pub struct Outfit {
/// This outfit's thumbnail
pub thumbnail: Arc<Sprite>,
/// The cost of this outfit, in credits
pub cost: u32,
/// How much space this outfit requires
pub space: OutfitSpace,
/// The name of this outfit
pub display_name: String,
/// The description of this outfit
pub desc: String,
/// Thie outfit's index
pub index: ContentIndex,
/// The engine flare sprite this outfit creates.
/// Its location and size is determined by a ship's
/// engine points.
pub engine_flare_sprite: Option<Arc<Sprite>>,
/// Jump to this edge when engines turn on
pub engine_flare_on_start: Option<SectionEdge>,
/// Jump to this edge when engines turn off
pub engine_flare_on_stop: Option<SectionEdge>,
/// This outfit's gun stats.
/// If this is some, this outfit requires a gun point.
pub gun: Option<Gun>,
/// The stats this outfit provides
pub stats: OutfitStats,
}
/// Outfit statistics
#[derive(Debug, Clone)]
pub struct OutfitStats {
/// How much engine thrust this outfit produces
pub engine_thrust: f32,
/// How much steering power this outfit provids
pub steer_power: f32,
/// Shield hit points
pub shield_strength: f32,
/// Shield regeneration rate, per second
pub shield_generation: f32,
/// Wait this many seconds after taking damage before regenerating shields
pub shield_delay: f32,
}
impl OutfitStats {
/// Create a new `OutfitStats`, with all values set to zero.
pub fn zero() -> Self {
Self {
engine_thrust: 0.0,
steer_power: 0.0,
shield_strength: 0.0,
shield_generation: 0.0,
shield_delay: 0.0,
}
}
/// Add all the stats in `other` to the stats in `self`.
/// Sheld delay is not affected.
pub fn add(&mut self, other: &Self) {
self.engine_thrust += other.engine_thrust;
self.steer_power += other.steer_power;
self.shield_strength += other.shield_strength;
self.shield_generation += other.shield_generation;
}
/// Subtract all the stats in `other` from the stats in `self`.
/// Sheld delay is not affected.
pub fn subtract(&mut self, other: &Self) {
self.engine_thrust -= other.engine_thrust;
self.steer_power -= other.steer_power;
self.shield_strength -= other.shield_strength;
self.shield_generation -= other.shield_generation;
}
}
/// Defines a projectile's collider
#[derive(Debug, Deserialize, Clone)]
pub enum ProjectileCollider {
/// A ball collider
#[serde(rename = "ball")]
Ball(BallCollider),
}
/// A simple ball-shaped collider, centered at the object's position
#[derive(Debug, Deserialize, Clone)]
pub struct BallCollider {
/// The radius of this ball
pub radius: f32,
}
/// Represents gun stats of an outfit.
/// If an outfit has this value, it requires a gun point.
#[derive(Debug, Clone)]
pub struct Gun {
/// The projectile this gun produces
pub projectile: Arc<Projectile>,
/// Average delay between projectiles, in seconds.
pub rate: f32,
/// Random variation of projectile delay, in seconds.
/// Each shot waits (rate += rate_rng).
pub rate_rng: f32,
}
/// Represents a projectile that a [`Gun`] produces.
#[derive(Debug, Clone)]
pub struct Projectile {
/// The projectile sprite
pub sprite: Arc<Sprite>,
/// The average size of this projectile
/// (height in game units)
pub size: f32,
/// Random size variation
pub size_rng: f32,
/// The speed of this projectile, in game units / second
pub speed: f32,
/// Random speed variation
pub speed_rng: f32,
/// The lifespan of this projectile.
/// It will vanish if it lives this long without hitting anything.
pub lifetime: f32,
/// Random lifetime variation
pub lifetime_rng: f32,
/// The damage this projectile does
pub damage: f32,
/// The force this projectile applies
pub force: f32,
/// The angle variation of this projectile, in radians
pub angle_rng: f32,
/// The effect this projectile will spawn when it hits something
pub impact_effect: Option<Arc<Effect>>,
/// The effect this projectile will spawn when it expires
pub expire_effect: Option<Arc<Effect>>,
/// Collider parameters for this projectile
pub collider: ProjectileCollider,
}
impl crate::Build for Outfit {
type InputSyntaxType = HashMap<String, syntax::Outfit>;
fn build(
outfits: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> ContentLoadResult<()> {
for (outfit_name, outfit) in outfits {
info!(message = "Building outfit", outfit = ?outfit_name);
let gun = match outfit.gun {
None => None,
Some(g) => Some(g.build(build_context, content)?),
};
let thumbnail = match content.sprites.get(&outfit.thumbnail) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("thumbnail sprite `{}` doesn't exist", outfit.thumbnail),
});
}
Some(t) => t.clone(),
};
let mut o = Self {
index: ContentIndex::new(&outfit_name),
display_name: outfit.name,
cost: outfit.cost,
thumbnail,
gun,
desc: outfit.desc,
engine_flare_sprite: None,
engine_flare_on_start: None,
engine_flare_on_stop: None,
space: OutfitSpace::from(outfit.space),
stats: OutfitStats::zero(),
};
// Engine stats
if let Some(engine) = outfit.engine {
let sprite = match content.sprites.get(&engine.flare.sprite) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("flare sprite `{}` doesn't exist", engine.flare.sprite),
});
}
Some(t) => t.clone(),
};
o.stats.engine_thrust = engine.thrust;
o.engine_flare_sprite = Some(sprite.clone());
// Flare animation will traverse this edge when the player presses the thrust key
// This leads from the idle animation to the transition animation
o.engine_flare_on_start = {
let x = engine.flare.on_start;
if x.is_none() {
None
} else {
let x = x.unwrap();
let mut e = resolve_edge_as_edge(&sprite.sections, &x.val, 0.0)?;
match &mut e {
// Inherit duration from transition sequence
SectionEdge::Top {
section,
ref mut duration,
}
| SectionEdge::Bot {
section,
ref mut duration,
} => {
*duration = section.frame_duration;
}
_ => {
return Err(ContentLoadError::Generic {
msg: format!("bad edge `{}`: must be `top` or `bot`", x.val),
});
}
};
Some(e)
}
};
// Flare animation will traverse this edge when the player releases the thrust key
// This leads from the idle animation to the transition animation
o.engine_flare_on_stop = {
let x = engine.flare.on_stop;
if x.is_none() {
None
} else {
let x = x.unwrap();
let mut e = resolve_edge_as_edge(&sprite.sections, &x.val, 0.0)?;
match &mut e {
// Inherit duration from transition sequence
SectionEdge::Top {
section,
ref mut duration,
}
| SectionEdge::Bot {
section,
ref mut duration,
} => {
*duration = section.frame_duration;
}
_ => {
return Err(ContentLoadError::Generic {
msg: format!("bad edge `{}`: must be `top` or `bot`", x.val),
});
}
};
Some(e)
}
};
}
// Steering stats
if let Some(steer) = outfit.steering {
o.stats.steer_power = steer.power;
}
// Shield stats
if let Some(shield) = outfit.shield {
o.stats.shield_delay = shield.delay.unwrap_or(0.0);
o.stats.shield_generation = shield.generation.unwrap_or(0.0);
o.stats.shield_strength = shield.strength.unwrap_or(0.0);
}
content.outfits.insert(o.index.clone(), Arc::new(o));
}
return Ok(());
}
}

View File

@ -0,0 +1,109 @@
use std::{
collections::HashMap,
ops::{Add, AddAssign, Sub, SubAssign},
};
pub(crate) mod syntax {
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct OutfitSpace {
pub outfit: Option<u32>,
pub weapon: Option<u32>,
pub engine: Option<u32>,
}
}
// TODO: user-defined space values
/// Represents outfit space, either that available in a ship
/// or that used by an outfit.
#[derive(Debug, Clone, Copy)]
pub struct OutfitSpace {
/// Total available outfit space.
/// This should be greater than weapon and engine.
pub outfit: u32,
/// Space for weapons
pub weapon: u32,
/// Space for engine
pub engine: u32,
}
impl OutfitSpace {
/// Make a new, zeroed OutfitSpace
pub fn new() -> Self {
Self {
outfit: 0,
weapon: 0,
engine: 0,
}
}
/// Can this outfit contain `smaller`?
pub fn can_contain(&self, smaller: &Self) -> bool {
self.outfit >= smaller.outfit
&& self.weapon >= smaller.weapon
&& self.engine >= smaller.engine
}
/// Return a map of "space name" -> number
pub fn to_hashmap(&self) -> HashMap<String, u32> {
let mut m = HashMap::new();
m.insert("outfit".to_string(), self.outfit);
m.insert("weapon".to_string(), self.weapon);
m.insert("engine".to_string(), self.engine);
m
}
}
impl Sub for OutfitSpace {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
OutfitSpace {
outfit: self.outfit - rhs.outfit,
weapon: self.weapon - rhs.weapon,
engine: self.engine - rhs.engine,
}
}
}
impl SubAssign for OutfitSpace {
fn sub_assign(&mut self, rhs: Self) {
self.outfit -= rhs.outfit;
self.weapon -= rhs.weapon;
self.engine -= rhs.engine;
}
}
impl Add for OutfitSpace {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
OutfitSpace {
outfit: self.outfit + rhs.outfit,
weapon: self.weapon + rhs.weapon,
engine: self.engine + rhs.engine,
}
}
}
impl AddAssign for OutfitSpace {
fn add_assign(&mut self, rhs: Self) {
self.outfit += rhs.outfit;
self.weapon += rhs.weapon;
self.engine += rhs.engine;
}
}
impl From<syntax::OutfitSpace> for OutfitSpace {
fn from(value: syntax::OutfitSpace) -> Self {
Self {
outfit: value.outfit.unwrap_or(0),
engine: value.engine.unwrap_or(0),
weapon: value.weapon.unwrap_or(0),
}
}
}

View File

@ -0,0 +1,463 @@
use galactica_util::to_radians;
use nalgebra::{Point2, Rotation2, Vector2};
use rapier2d::geometry::{Collider, ColliderBuilder};
use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc};
use tracing::info;
use crate::{
error::{ContentLoadError, ContentLoadResult},
Content, ContentBuildContext, ContentIndex, Effect, OutfitSpace, Sprite,
};
pub(crate) mod syntax {
use crate::{
part::{effect::syntax::EffectReference, outfitspace},
ContentIndex,
};
use serde::Deserialize;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Ship {
pub name: String,
pub sprite: ContentIndex,
pub thumbnail: ContentIndex,
pub size: f32,
pub engines: Vec<Engine>,
pub guns: Vec<Gun>,
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<Collapse>,
pub damage: Option<Damage>,
}
#[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<DamageEffectSpawner>,
}
#[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<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>,
}
}
// Processed data structs.
// These are exported.
/// Represents a ship chassis.
#[derive(Debug, Clone)]
pub struct Ship {
/// This ship's display name
pub display_name: String,
/// This object's index
pub index: ContentIndex,
/// This ship's sprite
pub sprite: Arc<Sprite>,
/// This ship's thumbnail
pub thumbnail: Arc<Sprite>,
/// 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<EnginePoint>,
/// Gun points on this ship.
/// A gun outfit can be mounted on each.
pub guns: Vec<GunPoint>,
/// This ship's hull strength
pub hull: f32,
/// Collision shape for this ship
pub collider: CollisionDebugWrapper,
/// 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<f32>,
/// 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<f32>,
}
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);
}
}
/// 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: Arc<Effect>,
/// 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
#[derive(Debug, Clone)]
pub struct CollapseEffectSpawner {
/// The effect to create
pub effect: Arc<Effect>,
/// How many effects to create
pub count: f32,
/// Where to create this effect.
/// 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>,
}
impl crate::Build for Ship {
type InputSyntaxType = HashMap<String, syntax::Ship>;
fn build(
ship: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
ct: &mut Content,
) -> ContentLoadResult<()> {
for (ship_name, ship) in ship {
info!(message = "Building ship", ship = ?ship_name);
let sprite = match ct.sprites.get(&ship.sprite) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("Sprite `{}` doesn't exist", ship.sprite),
});
}
Some(t) => t.clone(),
};
let thumbnail = match ct.sprites.get(&ship.thumbnail) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("Thumbnail sprite `{}` doesn't exist", ship.thumbnail),
});
}
Some(t) => t.clone(),
};
let size = ship.size;
let collapse = {
if let Some(c) = ship.collapse {
let mut effects = Vec::new();
for e in c.effects {
effects.push(CollapseEffectSpawner {
effect: e.effect.resolve(build_context, ct, "")?,
count: e.count,
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * sprite.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.resolve(build_context, ct, "")?,
count: g.count,
pos: g.pos.map(|p| {
Point2::new(
p[0] * (size / 2.0) * sprite.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.resolve(build_context, ct, "")?,
frequency: e.frequency,
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * sprite.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 * sprite.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<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,
// 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) * sprite.aspect, x[1] * size / 2.0)
})
.collect();
ColliderBuilder::convex_decomposition(&points[..], &indices[..])
.mass(ship.mass)
.build()
};
ct.ships.insert(
ContentIndex::new(&ship_name),
Arc::new(Self {
sprite: sprite.clone(),
thumbnail,
collapse,
damage,
index: ContentIndex::new(&ship_name),
display_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 * sprite.aspect / 2.0, e.y * size / 2.0),
size: e.size,
})
.collect(),
guns,
collider: CollisionDebugWrapper(collider),
}),
);
}
return Ok(());
}
}

View File

@ -0,0 +1,505 @@
use lazy_static::lazy_static;
use std::{collections::HashMap, sync::Arc};
use tracing::{debug, info};
use crate::{
error::{ContentLoadError, ContentLoadResult},
Content, ContentBuildContext, ContentIndex,
};
pub(crate) mod syntax {
use crate::{
error::{ContentLoadError, ContentLoadResult},
Content, ContentIndex,
};
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
// Raw serde syntax structs.
// These are never seen by code outside this crate.
/// Convenience variants of sprite definitions
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Sprite {
Static(StaticSprite),
OneSection(SpriteSection),
Complete(CompleteSprite),
}
/// Two ways to specify animation length
#[derive(Debug, Deserialize)]
pub enum TimingVariant {
/// The duration of this whole section
#[serde(rename = "duration")]
Duration(f32),
/// The fps of this section
#[serde(rename = "fps")]
Fps(f32),
}
#[derive(Debug, Deserialize)]
pub struct Timing {
#[serde(flatten)]
pub variant: TimingVariant,
}
/// An unanimated sprite
#[derive(Debug, Deserialize)]
pub struct StaticSprite {
pub file: PathBuf,
}
/// The proper, full sprite definition
#[derive(Debug, Deserialize)]
pub struct CompleteSprite {
pub section: HashMap<ContentIndex, SpriteSection>,
pub start_at: SectionEdge,
}
/// A single animation section
#[derive(Debug, Deserialize)]
pub struct SpriteSection {
pub frames: Vec<PathBuf>,
pub timing: Timing,
pub top: Option<SectionEdge>,
pub bot: Option<SectionEdge>,
}
impl SpriteSection {
pub fn resolve_edges(
&self,
sections: &HashMap<ContentIndex, Arc<super::SpriteSection>>,
sec: &mut super::SpriteSection,
) -> ContentLoadResult<()> {
let edge_top = match &self.top {
Some(x) => super::resolve_edge_as_edge(sections, &x.val, sec.frame_duration)?,
None => super::SectionEdge::Stop,
};
let edge_bot = match &self.bot {
Some(x) => super::resolve_edge_as_edge(sections, &x.val, sec.frame_duration)?,
None => super::SectionEdge::Stop,
};
sec.edge_bot = edge_bot;
sec.edge_top = edge_top;
return Ok(());
}
pub fn add_to(
&self,
ct: &mut Content,
) -> ContentLoadResult<((u32, u32), super::SpriteSection)> {
// Make sure all frames have the same size and add them
// to the frame vector
let mut dim = None;
let mut frames: Vec<u32> = Vec::new();
for f in &self.frames {
let idx = match ct.sprite_atlas.get_idx_by_path(f) {
Some(s) => s,
None => {
return Err(ContentLoadError::Generic {
msg: format!("file `{}` isn't in the sprite atlas", f.display()),
});
}
};
let img = &ct.sprite_atlas.get_by_idx(idx);
match dim {
None => dim = Some(img.true_size),
Some(e) => {
if img.true_size != e {
return Err(ContentLoadError::Generic {
msg: "failed to load section frames because frames have different sizes".to_string(),
});
}
}
}
frames.push(img.idx.into());
}
if frames.len() == 0 {
return Err(ContentLoadError::Generic {
msg: "sprite sections must not be empty".to_string(),
});
}
let dim = dim.unwrap();
let frame_duration = match self.timing.variant {
TimingVariant::Duration(d) => d / self.frames.len() as f32,
TimingVariant::Fps(f) => 1.0 / f,
};
if frame_duration <= 0.0 {
return Err(ContentLoadError::Generic {
msg: "frame duration must be positive (and therefore nonzero)".to_string(),
});
}
return Ok((
dim,
super::SpriteSection {
frames,
frame_duration,
// These are changed later, after all sections are built
edge_top: super::SectionEdge::Stop,
edge_bot: super::SectionEdge::Stop,
},
));
}
}
/// A link between two animation sections
#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct SectionEdge {
pub val: String,
}
}
/// An edge between two animation sections
#[derive(Debug, Clone)]
pub enum SectionEdge {
/// Stop at the last frame of this section
Stop,
/// Play the given section from the bottm
Bot {
/// The section to play
section: Arc<SpriteSection>,
/// The length of this edge, in seconds
duration: f32,
},
/// Play the given section from the top
Top {
/// The section to play
section: Arc<SpriteSection>,
/// The length of this edge, in seconds
duration: f32,
},
/// Replay this section in the opposite direction
Reverse {
/// The length of this edge, in seconds
duration: f32,
},
/// Restart this section from the opposite end
Repeat {
/// The length of this edge, in seconds
duration: f32,
},
}
/// Where to start an animation
#[derive(Debug, Clone)]
pub enum StartEdge {
/// Play the given section from the bottm
Bot {
/// The section to play
section: Arc<SpriteSection>,
},
/// Play the given section from the top
Top {
/// The section to play
section: Arc<SpriteSection>,
},
}
impl Into<SectionEdge> for StartEdge {
fn into(self) -> SectionEdge {
match self {
Self::Bot { section } => SectionEdge::Bot {
section,
duration: 0.0,
},
Self::Top { section } => SectionEdge::Top {
section,
duration: 0.0,
},
}
}
}
/// Represents a sprite that may be used in the game.
#[derive(Debug, Clone)]
pub struct Sprite {
/// This object's index
pub index: ContentIndex,
/// Where this sprite starts playing
pub start_at: StartEdge,
/// This sprite's animation sections
pub sections: HashMap<ContentIndex, Arc<SpriteSection>>,
/// Aspect ratio of this sprite (width / height)
pub aspect: f32,
}
lazy_static! {
/// The universal "hidden" sprite section, available in all sprites.
/// A SpriteAutomaton in this section will not be drawn.
pub static ref HIDDEN_SPRITE_SECTION: Arc<SpriteSection> = Arc::new(SpriteSection {
frames: vec![0],
frame_duration: 0.0,
edge_bot: SectionEdge::Stop,
edge_top: SectionEdge::Stop,
});
}
impl Sprite {
/// Get the index of the texture of this sprite's first frame
pub fn get_first_frame(&self) -> u32 {
match &self.start_at {
StartEdge::Bot { section } => *section.frames.last().unwrap(),
StartEdge::Top { section } => *section.frames.first().unwrap(),
}
}
/// Get this sprite's starting section
pub fn get_start_section(&self) -> Arc<SpriteSection> {
match &self.start_at {
StartEdge::Bot { section } => section.clone(),
StartEdge::Top { section } => section.clone(),
}
}
/// Iterate this sprite's sections
pub fn iter_sections(&self) -> impl Iterator<Item = &Arc<SpriteSection>> {
self.sections.values()
}
}
/// A part of a sprite's animation
#[derive(Debug, Clone)]
pub struct SpriteSection {
/// The texture index of each frame in this animation section.
/// unanimated sections have one frame.
pub frames: Vec<u32>,
/// The speed of this sprite's animation.
/// This must always be positive (and therefore, nonzero)
pub frame_duration: f32,
/// What to do when we reach the top of this section
pub edge_top: SectionEdge,
/// What to do when we reach the bottom of this section
pub edge_bot: SectionEdge,
}
/// Resolve an edge specification string as a StartEdge
pub fn resolve_edge_as_start(
sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
edge_string: &str,
) -> ContentLoadResult<super::StartEdge> {
debug!(message = "Resolving start edge");
let e = resolve_edge_as_edge(sections, edge_string, 0.0)?;
match e {
super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }),
super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { section }),
_ => {
return Err(ContentLoadError::Generic {
msg: format!("bad section start specification `{}`", edge_string),
});
}
}
}
/// Resolve an edge specifiation string as a SectionEdge
pub fn resolve_edge_as_edge(
sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
edge_string: &str,
duration: f32,
) -> ContentLoadResult<super::SectionEdge> {
if edge_string == "hidden" {
return Ok(super::SectionEdge::Top {
section: crate::HIDDEN_SPRITE_SECTION.clone(),
duration,
});
}
if edge_string == "stop" {
return Ok(super::SectionEdge::Stop);
}
if edge_string == "reverse" {
return Ok(super::SectionEdge::Reverse { duration });
}
if edge_string == "repeat" {
return Ok(super::SectionEdge::Repeat { duration });
}
let (section_name, start_point) = match edge_string.split_once(":") {
Some(x) => x,
None => {
return Err(ContentLoadError::Generic {
msg: format!("bad section edge specification `{}`", edge_string),
});
}
};
let section = match sections.get(&ContentIndex::new(section_name)) {
Some(s) => s,
None => {
return Err(ContentLoadError::Generic {
msg: format!("section `{}` doesn't exist", section_name),
});
}
};
match start_point {
"top" => Ok(super::SectionEdge::Top {
section: section.clone(),
duration,
}),
"bot" => Ok(super::SectionEdge::Bot {
section: section.clone(),
duration,
}),
_ => {
return Err(ContentLoadError::Generic {
msg: format!("invalid target `{}`", start_point),
});
}
}
}
impl crate::Build for Sprite {
type InputSyntaxType = HashMap<String, syntax::Sprite>;
fn build(
sprites: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext,
ct: &mut Content,
) -> ContentLoadResult<()> {
for (sprite_name, t) in sprites {
info!(message = "Parsing sprite", sprite = sprite_name);
match t {
syntax::Sprite::Static(t) => {
let idx = match ct.sprite_atlas.get_idx_by_path(&t.file) {
Some(s) => s,
None => {
return Err(ContentLoadError::Generic {
msg: format!(
"file `{}` isn't in the sprite atlas, cannot proceed",
t.file.display()
),
});
}
};
let img = &ct.sprite_atlas.get_by_idx(idx);
let aspect = img.w / img.h;
let section = Arc::new(SpriteSection {
frames: vec![img.idx.into()],
// We implement unanimated sprites with a very fast framerate
// and STOP endpoints.
frame_duration: 0.01,
edge_top: SectionEdge::Stop,
edge_bot: SectionEdge::Stop,
});
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at: StartEdge::Top {
section: section.clone(),
},
sections: {
let mut h = HashMap::new();
h.insert(ContentIndex::new("anim"), section);
h
},
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
syntax::Sprite::OneSection(s) => {
let (dim, section) = s.add_to(ct)?;
let aspect = dim.0 as f32 / dim.1 as f32;
let section = Arc::new(section);
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at: StartEdge::Top {
section: section.clone(),
},
sections: {
let mut h = HashMap::new();
h.insert(ContentIndex::new("anim"), section.clone());
h
},
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
syntax::Sprite::Complete(s) => {
let mut sections = HashMap::new();
let mut dim = None;
for (name, sec) in &s.section {
info!(
message = "Parsing sprite section",
sprite = sprite_name,
section = ?name
);
let (d, s) = sec.add_to(ct)?;
// Make sure all dimensions are the same
if dim.is_none() {
dim = Some(d);
} else if dim.unwrap() != d {
return Err(ContentLoadError::Generic {
msg: format!(
"could not load sprite, image sizes in section `{}` are different",
name
),
});
}
sections.insert(name.clone(), Arc::new(s));
}
// We need to clone this here, thanks to self-reference.
let tmp_sections = sections.clone();
for (name, sec) in &s.section {
let parsed_sec = sections.get_mut(name).unwrap();
let parsed_sec = Arc::make_mut(parsed_sec);
sec.resolve_edges(&tmp_sections, parsed_sec)?;
}
let dim = dim.unwrap();
let aspect = dim.0 as f32 / dim.1 as f32;
let start_at = resolve_edge_as_start(&sections, &s.start_at.val)?;
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at,
sections,
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
}
}
return Ok(());
}
}

View File

@ -0,0 +1,317 @@
use galactica_util::to_radians;
use nalgebra::{Point2, Point3};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use tracing::info;
use crate::{
error::{ContentLoadError, ContentLoadResult},
util::Polar,
Content, ContentBuildContext, ContentIndex, Outfit, Sprite,
};
pub(crate) mod syntax {
use serde::Deserialize;
use std::collections::HashMap;
use crate::ContentIndex;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct System {
pub name: String,
pub object: HashMap<ContentIndex, Object>,
}
#[derive(Debug, Deserialize)]
pub struct Object {
pub sprite: ContentIndex,
pub position: Position,
pub size: f32,
pub radius: Option<f32>,
pub angle: Option<f32>,
pub landable: Option<Landable>,
}
#[derive(Debug, Deserialize)]
pub struct Landable {
pub name: String,
pub desc: String,
pub image: ContentIndex,
pub outfitter: Vec<ContentIndex>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Position {
Polar(PolarCoords),
Cartesian(CoordinatesThree),
}
#[derive(Debug, Deserialize)]
pub struct PolarCoords {
pub center: CoordinatesTwo,
pub radius: f32,
pub angle: f32,
pub z: f32,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CoordinatesTwo {
Label(ContentIndex),
Coords([f32; 2]),
}
impl ToString for CoordinatesTwo {
fn to_string(&self) -> String {
match self {
Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CoordinatesThree {
Label(ContentIndex),
Coords([f32; 3]),
}
impl ToString for CoordinatesThree {
fn to_string(&self) -> String {
match self {
Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v),
}
}
}
}
// Processed data structs.
// These are exported.
/// Represents a star system
#[derive(Debug, Clone)]
pub struct System {
/// This object's name
pub display_name: String,
/// This object's index
pub index: ContentIndex,
/// Objects in this system
pub objects: HashMap<ContentIndex, Arc<SystemObject>>,
}
/// Represents an orbiting body in a star system
/// (A star, planet, moon, satellite, etc)
/// These may be landable and may be decorative.
/// System objects to not interact with the physics engine.
#[derive(Debug, Clone)]
pub struct SystemObject {
/// This object's index
pub index: ContentIndex,
/// This object's sprite
pub sprite: Arc<Sprite>,
/// This object's size.
/// Measured as height in game units.
/// This value is scaled for distance
/// (i.e, the z-value of position)
pub size: f32,
/// This object's position, in game coordinates,
/// relative to the system's center (0, 0).
pub pos: Point3<f32>,
/// This object's sprite's angle, in radians
pub angle: f32,
/// If true, ships may land on this object
pub landable: Option<LandableSystemObject>,
}
#[derive(Debug, Clone)]
pub struct LandableSystemObject {
/// This object's name
pub display_name: String,
/// The description of this object
pub desc: String,
/// This object's image
pub image: Arc<Sprite>,
/// The outfits we can buy here
/// If this is empty, this landable has no outfitter.
pub outfitter: Vec<Arc<Outfit>>,
}
/// Helper function for resolve_position, never called on its own.
fn resolve_coordinates(
objects: &HashMap<ContentIndex, syntax::Object>,
cor: &syntax::CoordinatesThree,
mut cycle_detector: HashSet<ContentIndex>,
) -> ContentLoadResult<Point3<f32>> {
match cor {
syntax::CoordinatesThree::Coords(c) => Ok((*c).into()),
syntax::CoordinatesThree::Label(l) => {
if cycle_detector.contains(l) {
return Err(ContentLoadError::Generic {
msg: format!(
"Found coordinate cycle: `{}`",
cycle_detector.iter().fold(String::new(), |sum, a| {
if sum.is_empty() {
a.to_string()
} else {
format!("{sum} -> {a}")
}
})
),
});
}
cycle_detector.insert(l.clone());
let p = match objects.get(l) {
Some(p) => p,
None => {
return Err(ContentLoadError::Generic {
msg: format!("Could not resolve coordinate label `{l}`"),
});
}
};
Ok(resolve_position(&objects, &p, cycle_detector)?)
}
}
}
/// Given an object, resolve its position as a Point3.
fn resolve_position(
objects: &HashMap<ContentIndex, syntax::Object>,
obj: &syntax::Object,
cycle_detector: HashSet<ContentIndex>,
) -> ContentLoadResult<Point3<f32>> {
match &obj.position {
syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?),
syntax::Position::Polar(p) => {
let three = match &p.center {
syntax::CoordinatesTwo::Label(s) => syntax::CoordinatesThree::Label(s.clone()),
syntax::CoordinatesTwo::Coords(v) => {
syntax::CoordinatesThree::Coords([v[0], v[1], f32::NAN])
}
};
let r = resolve_coordinates(&objects, &three, cycle_detector)?;
let plane = Polar {
center: Point2::new(r.x, r.y),
radius: p.radius,
angle: to_radians(p.angle),
}
.to_cartesian();
Ok(Point3::new(plane.x, plane.y, p.z))
}
}
}
impl crate::Build for System {
type InputSyntaxType = HashMap<String, syntax::System>;
fn build(
system: Self::InputSyntaxType,
_build_context: &mut ContentBuildContext,
content: &mut Content,
) -> ContentLoadResult<()> {
for (system_name, system) in system {
info!(message = "Parsing system", system = system_name);
let mut objects = HashMap::new();
for (index, object) in &system.object {
info!(
message = "Parsing system object",
system = system_name,
object_idx = ?index
);
let mut cycle_detector = HashSet::new();
cycle_detector.insert(index.clone());
let sprite = match content.sprites.get(&object.sprite) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("sprite `{}` doesn't exist", object.sprite),
});
}
Some(t) => t.clone(),
};
let landable = 'landable: {
if object.landable.is_none() {
break 'landable None;
}
let l = object.landable.as_ref().unwrap();
let image = match content.sprites.get(&l.image) {
None => {
return Err(ContentLoadError::Generic {
msg: format!("sprite `{}` doesn't exist", object.sprite),
});
}
Some(t) => t.clone(),
};
let mut outfitter = Vec::new();
for o in &l.outfitter {
match content.outfits.get(&o) {
Some(x) => outfitter.push(x.clone()),
None => {
return Err(ContentLoadError::Generic {
msg: format!("outfit `{o}` doesn't exist"),
});
}
}
}
break 'landable Some(LandableSystemObject {
image,
outfitter,
display_name: l.name.clone(),
// TODO: better linebreaks, handle double spaces
// Tabs
desc: l.desc.replace("\n", " ").replace("<br>", "\n"),
});
};
objects.insert(
index.clone(),
Arc::new(SystemObject {
index: index.clone(),
sprite,
pos: resolve_position(&system.object, &object, cycle_detector)?,
size: object.size,
angle: to_radians(object.angle.unwrap_or(0.0)),
landable,
}),
);
}
content.systems.insert(
ContentIndex::new(&system_name),
Arc::new(Self {
index: ContentIndex::new(&system_name),
display_name: system.name,
objects,
}),
);
}
return Ok(());
}
}

View File

@ -0,0 +1,308 @@
use crate::{SectionEdge, Sprite, SpriteSection, StartEdge};
use std::sync::Arc;
/// A single frame's state
#[derive(Debug, Clone)]
pub struct AnimationState {
/// The index of the texture we're fading from
pub texture_a: u32,
/// The index of the texture we're fading to
pub texture_b: u32,
/// Between 0.0 and 1.0, denoting how far we are between
/// texture_a and texture_b
/// 0.0 means fully show texture_a;
/// 1.0 means fully show texture_b.
pub fade: f32,
}
impl AnimationState {
/// Convenience method.
/// Get texture index as an array
pub fn texture_index(&self) -> [u32; 2] {
[self.texture_a, self.texture_b]
}
}
/// What direction are we playing our animation in?
#[derive(Debug, Clone, Copy)]
enum AnimDirection {
/// Top to bottom, with increasing frame indices
/// (normal)
Up,
/// Bottom to top, with decreasing frame indices
/// (reverse)
Down,
/// Stopped on one frame
Stop,
}
/// Manages a single sprite's animation state.
#[derive(Debug, Clone)]
pub struct SpriteAutomaton {
/// The sprite we're animating
sprite: Arc<Sprite>,
/// Which animation section we're on
/// This MUST be a section from this Automaton's sprite
current_section: Arc<SpriteSection>,
/// Which frame we're on
current_frame: usize,
/// Where we are between frames.
current_edge_progress: f32,
/// The length of the current edge we're on, in seconds
current_edge_duration: f32,
/// In what direction are we playing the current section?
current_direction: AnimDirection,
/// The texture we're fading from
/// (if we're moving downwards)
last_texture: u32,
/// The texture we're fading to
/// (if we're moving downwards)
next_texture: u32,
/// If this is some, take this edge next
next_edge_override: Option<SectionEdge>,
}
impl SpriteAutomaton {
/// Create a new AnimAutomaton
pub fn new(sprite: Arc<Sprite>) -> Self {
let (current_section, texture, current_direction) = {
match &sprite.start_at {
StartEdge::Top { section } => (
section,
*section.frames.first().unwrap(),
AnimDirection::Down,
),
StartEdge::Bot { section } => {
(section, *section.frames.last().unwrap(), AnimDirection::Up)
}
}
};
Self {
sprite: sprite.clone(),
current_frame: 0,
current_edge_progress: match current_direction {
AnimDirection::Down => 0.0,
AnimDirection::Up => current_section.frame_duration,
AnimDirection::Stop => unreachable!("how'd you get here?"),
},
current_edge_duration: current_section.frame_duration,
next_edge_override: None,
current_direction,
current_section: current_section.clone(),
last_texture: texture,
next_texture: texture,
}
}
/// Override the next section transition
pub fn next_edge(&mut self, s: SectionEdge) {
self.next_edge_override = Some(s);
}
/// Force a transition to the given section right now
pub fn jump_to(&mut self, start: &SectionEdge) {
self.take_edge(start);
}
fn take_edge(&mut self, e: &SectionEdge) {
let last = match self.current_direction {
AnimDirection::Stop => self.next_texture,
AnimDirection::Down => self.next_texture,
AnimDirection::Up => self.last_texture,
};
match e {
SectionEdge::Stop => {
match self.current_direction {
AnimDirection::Stop => {}
AnimDirection::Up => {
self.current_frame = 0;
}
AnimDirection::Down => {
self.current_frame = self.current_section.frames.len() - 1;
}
}
self.current_edge_duration = 1.0;
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section, duration } => {
self.current_section = section.clone();
self.current_edge_duration = *duration;
self.current_frame = 0;
self.current_direction = AnimDirection::Down;
}
SectionEdge::Bot { section, duration } => {
self.current_section = section.clone();
self.current_frame = section.frames.len() - 1;
self.current_edge_duration = *duration;
self.current_direction = AnimDirection::Up;
}
SectionEdge::Repeat { duration } => {
match self.current_direction {
AnimDirection::Stop => {}
AnimDirection::Up => {
self.current_frame = self.current_section.frames.len() - 1;
}
AnimDirection::Down => {
self.current_frame = 0;
}
}
self.current_edge_duration = *duration;
}
SectionEdge::Reverse { duration } => {
match self.current_direction {
AnimDirection::Stop => {}
AnimDirection::Up => {
// Jump to SECOND frame, since we've already shown the
// first during the fade transition
self.current_frame = {
if self.current_section.frames.len() == 1 {
0
} else {
1
}
};
self.current_direction = AnimDirection::Down;
}
AnimDirection::Down => {
self.current_frame = {
if self.current_section.frames.len() == 1 {
0
} else {
self.current_section.frames.len() - 2
}
};
self.current_direction = AnimDirection::Up;
}
}
self.current_edge_duration = *duration;
}
}
match self.current_direction {
AnimDirection::Stop => {
self.next_texture = self.current_section.frames[self.current_frame];
self.last_texture = self.current_section.frames[self.current_frame];
}
AnimDirection::Down => {
self.last_texture = last;
self.next_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = 0.0;
}
AnimDirection::Up => {
self.next_texture = last;
self.last_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = self.current_edge_duration;
}
}
}
/// Step this animation by `t` seconds
pub fn step(&mut self, t: f32) {
// Current_fade and current_frame keep track of where we are in the current section.
// current_frame indexes this section frames. When it exceeds the number of frames
// or falls below zero (when moving in reverse), we switch to the next section.
//
// current_fade keeps track of our state between frames. It is zero once a frame starts,
// and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases,
// and if we are stepping backwards, it decreases.
// Note that frame_duration may be zero!
// This is only possible in the hidden texture, since
// user-provided sections are always checked to be positive.
assert!(self.current_section.frame_duration >= 0.0);
match self.current_direction {
AnimDirection::Stop => {
// Edge case: we're stopped and got a request to transition.
// we should transition right away.
if self.next_edge_override.is_some() {
let e = self.next_edge_override.take().unwrap();
self.take_edge(&e);
}
return;
}
AnimDirection::Down => {
self.current_edge_progress += t;
// We're stepping foward and finished this frame
if self.current_edge_progress > self.current_edge_duration {
if self.current_frame < self.current_section.frames.len() - 1 {
self.current_frame += 1;
self.last_texture = self.next_texture;
self.next_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = 0.0;
self.current_edge_duration = self.current_section.frame_duration;
} else {
let e = {
if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap()
} else {
self.current_section.edge_bot.clone()
}
};
self.take_edge(&e);
}
}
}
AnimDirection::Up => {
self.current_edge_progress -= t;
// We're stepping backward and finished this frame
if self.current_edge_progress < 0.0 {
if self.current_frame > 0 {
self.current_frame -= 1;
self.next_texture = self.last_texture;
self.last_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = self.current_section.frame_duration;
self.current_edge_duration = self.current_section.frame_duration;
} else {
let e = {
if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap()
} else {
self.current_section.edge_top.clone()
}
};
self.take_edge(&e);
}
}
}
}
}
/// Get the current frame of this animation
pub fn get_texture_idx(&self) -> AnimationState {
return AnimationState {
texture_a: self.last_texture,
texture_b: self.next_texture,
fade: self.current_edge_progress / self.current_edge_duration,
};
}
/// Get the sprite this automaton is using
pub fn get_sprite(&self) -> Arc<Sprite> {
self.sprite.clone()
}
}

24
lib/content/src/util.rs Normal file
View File

@ -0,0 +1,24 @@
use nalgebra::{Point2, Vector2};
#[derive(Debug, Clone, Copy)]
pub struct Polar {
/// The center of this polar coordinate
pub center: Point2<f32>,
/// The radius of this polar coordinate
pub radius: f32,
/// In radians
pub angle: f32,
}
impl Polar {
pub fn to_cartesian(self) -> Point2<f32> {
let v = Vector2::new(
self.radius * self.angle.sin(),
self.radius * self.angle.cos(),
);
return self.center + v;
}
}