Added content
This commit is contained in:
parent
b634575791
commit
3150f64bd1
32
lib/content/Cargo.toml
Normal file
32
lib/content/Cargo.toml
Normal 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
39
lib/content/src/error.rs
Normal 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
361
lib/content/src/lib.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
203
lib/content/src/part/config.rs
Normal file
203
lib/content/src/part/config.rs
Normal 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,
|
||||||
|
}
|
270
lib/content/src/part/effect.rs
Normal file
270
lib/content/src/part/effect.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
125
lib/content/src/part/faction.rs
Normal file
125
lib/content/src/part/faction.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
22
lib/content/src/part/mod.rs
Normal file
22
lib/content/src/part/mod.rs
Normal 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};
|
440
lib/content/src/part/outfit.rs
Normal file
440
lib/content/src/part/outfit.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
109
lib/content/src/part/outfitspace.rs
Normal file
109
lib/content/src/part/outfitspace.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
463
lib/content/src/part/ship.rs
Normal file
463
lib/content/src/part/ship.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
505
lib/content/src/part/sprite.rs
Normal file
505
lib/content/src/part/sprite.rs
Normal 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(§ions, &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(());
|
||||||
|
}
|
||||||
|
}
|
317
lib/content/src/part/system.rs
Normal file
317
lib/content/src/part/system.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
308
lib/content/src/spriteautomaton.rs
Normal file
308
lib/content/src/spriteautomaton.rs
Normal 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
24
lib/content/src/util.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user