Compare commits

...

3 Commits

Author SHA1 Message Date
Mark 97720ea6fe
Improved flare animation 2024-01-20 13:09:58 -08:00
Mark 67c4a8c259
Updated TODOs 2024-01-20 12:42:57 -08:00
Mark 088d1f7fb9
Added engine ease in/out 2024-01-20 12:42:20 -08:00
16 changed files with 293 additions and 178 deletions

View File

@ -1,11 +1,13 @@
# Specific projects # Specific projects
## Currently working on: ## Currently working on:
- player selection - first: fix particles & physics
- clickable buttons
- planet outfitter - planet outfitter
## Small jobs ## Small jobs
- 🌟 clean up and document short sprite sections
- 🌟 Better planet desc formatting - 🌟 Better planet desc formatting
- Procedural suns - Procedural suns
- 🌟 Back arrow -> reverse - 🌟 Back arrow -> reverse
@ -27,7 +29,6 @@
- Turn flares (physics by location?) - Turn flares (physics by location?)
- Angled engines & guns - Angled engines & guns
- Fix effect interaction with sprite sections - Fix effect interaction with sprite sections
- Clean up starfieldsprite
## Misc fixes & Optimizations ## Misc fixes & Optimizations
- 🌟 Better errors when content/asset dirs don't exist - 🌟 Better errors when content/asset dirs don't exist

2
assets

@ -1 +1 @@
Subproject commit b4fe8111c7b1cb17987bbff095bd50e61e2d8998 Subproject commit f749d99ae63bcdefb4a62774d21f30a7eebc20d3

View File

@ -3,7 +3,9 @@
space.engine = 20 space.engine = 20
engine.thrust = 100 engine.thrust = 100
engine.flare_sprite = "flare::ion" engine.flare.sprite = "flare::ion"
engine.flare.on_start = "rise:top"
engine.flare.on_stop = "rise:bot"
steering.power = 20 steering.power = 20

View File

@ -66,6 +66,9 @@ pub struct AnimAutomaton {
/// The texture we're fading to /// The texture we're fading to
/// (if we're moving downwards) /// (if we're moving downwards)
next_texture: u32, next_texture: u32,
/// If this is some, take this edge next
next_edge_override: Option<SectionEdge>,
} }
impl AnimAutomaton { impl AnimAutomaton {
@ -90,6 +93,7 @@ impl AnimAutomaton {
sprite: sprite.handle, sprite: sprite.handle,
current_frame: 0, current_frame: 0,
current_fade: 0.0, current_fade: 0.0,
next_edge_override: None,
current_direction, current_direction,
current_section, current_section,
@ -98,21 +102,56 @@ impl AnimAutomaton {
} }
} }
/// Reset this animation /// Override the next section transition
pub fn reset(&mut self, ct: &Content) { pub fn next_edge(&mut self, s: SectionEdge) {
*self = Self::new(ct, self.sprite); self.next_edge_override = Some(s);
} }
/// Reverse this animation's direction /// Force a transition to the given section right now
pub fn reverse(&mut self) { pub fn jump_to(&mut self, ct: &Content, start: StartEdge) {
match self.current_direction { self.take_edge(ct, start.into());
AnimDirection::Stop => {} }
AnimDirection::Up => {
fn take_edge(&mut self, ct: &Content, e: SectionEdge) {
let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section);
match e {
SectionEdge::Stop => {
self.current_fade = 0.0;
self.current_frame = current_section.frames.len() - 1;
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section } => {
self.current_section = section;
self.current_frame = 0;
self.current_direction = AnimDirection::Down; self.current_direction = AnimDirection::Down;
} }
AnimDirection::Down => { SectionEdge::Bot { section } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
self.current_direction = AnimDirection::Up; self.current_direction = AnimDirection::Up;
} }
SectionEdge::Restart => {
self.current_frame = 0;
}
SectionEdge::Reverse => {
// Jump to SECOND frame, since we've already shown the
// first during the fade transition
match self.current_direction {
AnimDirection::Stop => {}
AnimDirection::Up => {
self.current_frame = 0;
self.current_direction = AnimDirection::Down;
}
AnimDirection::Down => {
self.current_frame = current_section.frames.len() - 1;
self.current_direction = AnimDirection::Up;
}
}
}
} }
} }
@ -129,15 +168,21 @@ impl AnimAutomaton {
// and we switch to the next frame when it hits 1.0. If we are stepping foward, it increases, // 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. // and if we are stepping backwards, it decreases.
// If this is zero, this section isn't animated. // This should always be positive, and this fact is enforced by the content loader.
if current_section.frame_duration == 0.0 { // if we get here, something is very wrong.
return; assert!(current_section.frame_duration > 0.0);
}
match self.current_direction { match self.current_direction {
AnimDirection::Down => self.current_fade += t / current_section.frame_duration, AnimDirection::Down => self.current_fade += t / current_section.frame_duration,
AnimDirection::Up => self.current_fade -= t / current_section.frame_duration, AnimDirection::Up => self.current_fade -= t / current_section.frame_duration,
AnimDirection::Stop => {} AnimDirection::Stop => {
// Edge case: we're stopped and got a request to transition.
// we should transition right away.
if let Some(e) = self.next_edge_override {
self.take_edge(ct, e);
}
}
} }
// We're stepping foward and finished this frame // We're stepping foward and finished this frame
@ -150,32 +195,14 @@ impl AnimAutomaton {
if self.current_frame < current_section.frames.len() - 1 { if self.current_frame < current_section.frames.len() - 1 {
self.current_frame += 1; self.current_frame += 1;
} else { } else {
match current_section.edge_bot { let e = {
SectionEdge::Stop => { if self.next_edge_override.is_some() {
self.current_fade = 0.0; self.next_edge_override.take().unwrap()
self.current_frame = current_section.frames.len() - 1; } else {
self.current_direction = AnimDirection::Stop; current_section.edge_bot.clone()
} }
SectionEdge::Top { section } => { };
self.current_section = section; self.take_edge(ct, e);
self.current_frame = 0;
}
SectionEdge::Bot { section } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
self.reverse();
}
SectionEdge::Restart => {
self.current_frame = 0;
}
SectionEdge::Reverse => {
// Jump to SECOND frame, since we've already shown the
// first during the fade transition
self.current_frame = current_section.frames.len() - 1;
self.reverse()
}
}
} }
let current_section = sprite.get_section(self.current_section); let current_section = sprite.get_section(self.current_section);
@ -193,30 +220,14 @@ impl AnimAutomaton {
if self.current_frame > 0 { if self.current_frame > 0 {
self.current_frame -= 1; self.current_frame -= 1;
} else { } else {
match current_section.edge_top { let e = {
SectionEdge::Stop => { if self.next_edge_override.is_some() {
self.current_fade = 0.0; self.next_edge_override.take().unwrap()
self.current_frame = 0; } else {
self.current_direction = AnimDirection::Stop; current_section.edge_top.clone()
} }
SectionEdge::Top { section } => { };
self.current_section = section; self.take_edge(ct, e);
self.current_frame = 0;
self.reverse();
}
SectionEdge::Bot { section } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
}
SectionEdge::Reverse => {
self.current_frame = 0;
self.reverse();
}
SectionEdge::Restart => {
self.current_frame = current_section.frames.len() - 1;
}
}
} }
let current_section = sprite.get_section(self.current_section); let current_section = sprite.get_section(self.current_section);

View File

@ -8,25 +8,9 @@
use std::{cmp::Eq, hash::Hash}; use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a sprite /// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpriteHandle { pub struct SpriteHandle {
pub(crate) index: usize, pub(crate) index: usize,
/// The aspect ratio of this sprite (width / height)
pub aspect: f32,
}
impl Hash for SpriteHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state)
}
}
impl Eq for SpriteHandle {}
impl PartialEq for SpriteHandle {
fn eq(&self, other: &Self) -> bool {
self.index.eq(&other.index)
}
} }
/// A lightweight representation of system body /// A lightweight representation of system body

View File

@ -13,6 +13,7 @@ use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::File, fs::File,
hash::Hash,
io::Read, io::Read,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -122,13 +123,18 @@ trait Build {
/// Stores temporary data while building context objects /// Stores temporary data while building context objects
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ContentBuildContext { pub(crate) struct ContentBuildContext {
/// Map effect names to handles
pub effect_index: HashMap<String, EffectHandle>, pub effect_index: HashMap<String, EffectHandle>,
/// Maps sprite handles to a map of section name -> section index
pub sprite_section_index: HashMap<SpriteHandle, HashMap<String, AnimSectionHandle>>,
} }
impl ContentBuildContext { impl ContentBuildContext {
fn new() -> Self { fn new() -> Self {
Self { Self {
effect_index: HashMap::new(), effect_index: HashMap::new(),
sprite_section_index: HashMap::new(),
} }
} }
} }

View File

@ -1,13 +1,14 @@
use anyhow::{bail, Context, Result}; use anyhow::{anyhow, Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace, handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace,
StartEdge,
}; };
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::{effect, part::outfitspace, ContentBuildContext}; use crate::{effect, part::outfitspace, sprite::syntax::SectionEdge, ContentBuildContext};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use galactica_util::to_radians; use galactica_util::to_radians;
use serde::Deserialize; use serde::Deserialize;
@ -34,7 +35,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Engine { pub struct Engine {
pub thrust: f32, pub thrust: f32,
pub flare_sprite: String, pub flare: EngineFlare,
}
#[derive(Debug, Deserialize)]
pub struct EngineFlare {
pub sprite: String,
pub on_start: Option<SectionEdge>,
pub on_stop: Option<SectionEdge>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -139,6 +147,12 @@ pub struct Outfit {
/// engine points. /// engine points.
pub engine_flare_sprite: Option<SpriteHandle>, pub engine_flare_sprite: Option<SpriteHandle>,
/// Jump to this edge when engines turn on
pub engine_flare_on_start: Option<StartEdge>,
/// Jump to this edge when engines turn off
pub engine_flare_on_stop: Option<StartEdge>,
/// Shield hit points /// Shield hit points
pub shield_strength: f32, pub shield_strength: f32,
@ -253,6 +267,8 @@ impl crate::Build for Outfit {
engine_thrust: 0.0, engine_thrust: 0.0,
steer_power: 0.0, steer_power: 0.0,
engine_flare_sprite: None, engine_flare_sprite: None,
engine_flare_on_start: None,
engine_flare_on_stop: None,
space: OutfitSpace::from(outfit.space), space: OutfitSpace::from(outfit.space),
shield_delay: 0.0, shield_delay: 0.0,
shield_generation: 0.0, shield_generation: 0.0,
@ -261,16 +277,44 @@ impl crate::Build for Outfit {
// Engine stats // Engine stats
if let Some(engine) = outfit.engine { if let Some(engine) = outfit.engine {
let th = match content.sprite_index.get(&engine.flare_sprite) { let sprite_handle = match content.sprite_index.get(&engine.flare.sprite) {
None => bail!( None => {
"In outfit `{}`: flare sprite `{}` doesn't exist", return Err(anyhow!(
outfit_name, "flare sprite `{}` doesn't exist",
engine.flare_sprite engine.flare.sprite
), ))
.with_context(|| format!("in outfit `{}`", outfit_name));
}
Some(t) => *t, Some(t) => *t,
}; };
o.engine_thrust = engine.thrust; o.engine_thrust = engine.thrust;
o.engine_flare_sprite = Some(th); o.engine_flare_sprite = Some(sprite_handle);
o.engine_flare_on_start = {
let x = engine.flare.on_start;
if x.is_none() {
None
} else {
let x = x.unwrap();
Some(
x.resolve_as_start(sprite_handle, build_context)
.with_context(|| format!("in outfit `{}`", outfit_name))?,
)
}
};
o.engine_flare_on_stop = {
let x = engine.flare.on_stop;
if x.is_none() {
None
} else {
let x = x.unwrap();
Some(
x.resolve_as_start(sprite_handle, build_context)
.with_context(|| format!("in outfit `{}`", outfit_name))?,
)
}
};
} }
// Steering stats // Steering stats

View File

@ -4,13 +4,11 @@ use std::collections::HashMap;
use crate::{handle::SpriteHandle, Content, ContentBuildContext}; use crate::{handle::SpriteHandle, Content, ContentBuildContext};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::{Content, ContentBuildContext}; use crate::{Content, ContentBuildContext, SpriteHandle};
use anyhow::{anyhow, bail, Context, Ok, Result}; use anyhow::{anyhow, bail, Context, Ok, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use super::AnimSectionHandle;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -66,12 +64,9 @@ pub(crate) mod syntax {
impl SpriteSection { impl SpriteSection {
pub fn add_to( pub fn add_to(
&self, &self,
_build_context: &mut ContentBuildContext, this_sprite: SpriteHandle,
build_context: &mut ContentBuildContext,
content: &mut Content, content: &mut Content,
// An index of all sections in this sprite, used to resolve
// top and bot edges.
all_sections: &HashMap<String, AnimSectionHandle>,
) -> Result<((u32, u32), super::SpriteSection)> { ) -> Result<((u32, u32), super::SpriteSection)> {
// Make sure all frames have the same size and add them // Make sure all frames have the same size and add them
// to the frame vector // to the frame vector
@ -109,12 +104,12 @@ pub(crate) mod syntax {
} }
let edge_top = match &self.top { let edge_top = match &self.top {
Some(x) => x.resolve_as_edge(all_sections)?, Some(x) => x.resolve_as_edge(this_sprite, build_context)?,
None => super::SectionEdge::Stop, None => super::SectionEdge::Stop,
}; };
let edge_bot = match &self.bot { let edge_bot = match &self.bot {
Some(x) => x.resolve_as_edge(all_sections)?, Some(x) => x.resolve_as_edge(this_sprite, build_context)?,
None => super::SectionEdge::Stop, None => super::SectionEdge::Stop,
}; };
@ -140,10 +135,11 @@ pub(crate) mod syntax {
impl SectionEdge { impl SectionEdge {
pub fn resolve_as_start( pub fn resolve_as_start(
&self, &self,
all_sections: &HashMap<String, AnimSectionHandle>, sprite: SpriteHandle,
build_context: &ContentBuildContext,
) -> Result<super::StartEdge> { ) -> Result<super::StartEdge> {
let e = self let e = self
.resolve_as_edge(all_sections) .resolve_as_edge(sprite, build_context)
.with_context(|| format!("while resolving start edge"))?; .with_context(|| format!("while resolving start edge"))?;
match e { match e {
super::SectionEdge::Bot { section } => Ok(super::StartEdge::Bot { section }), super::SectionEdge::Bot { section } => Ok(super::StartEdge::Bot { section }),
@ -156,8 +152,11 @@ pub(crate) mod syntax {
pub fn resolve_as_edge( pub fn resolve_as_edge(
&self, &self,
all_sections: &HashMap<String, AnimSectionHandle>, sprite: SpriteHandle,
build_context: &ContentBuildContext,
) -> Result<super::SectionEdge> { ) -> Result<super::SectionEdge> {
let all_sections = build_context.sprite_section_index.get(&sprite).unwrap();
if self.val == "stop" { if self.val == "stop" {
return Ok(super::SectionEdge::Stop); return Ok(super::SectionEdge::Stop);
} }
@ -203,7 +202,7 @@ pub(crate) mod syntax {
pub struct AnimSectionHandle(pub(crate) usize); pub struct AnimSectionHandle(pub(crate) usize);
/// An edge between two animation sections /// An edge between two animation sections
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
pub enum SectionEdge { pub enum SectionEdge {
/// Stop at the last frame of this section /// Stop at the last frame of this section
Stop, Stop,
@ -228,7 +227,7 @@ pub enum SectionEdge {
} }
/// Where to start an animation /// Where to start an animation
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
pub enum StartEdge { pub enum StartEdge {
/// Play the given section from the bottm /// Play the given section from the bottm
Bot { Bot {
@ -243,6 +242,15 @@ pub enum StartEdge {
}, },
} }
impl Into<SectionEdge> for StartEdge {
fn into(self) -> SectionEdge {
match self {
Self::Bot { section } => SectionEdge::Bot { section },
Self::Top { section } => SectionEdge::Top { section },
}
}
}
/// Represents a sprite that may be used in the game. /// Represents a sprite that may be used in the game.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Sprite { pub struct Sprite {
@ -276,6 +284,14 @@ impl Sprite {
} }
} }
/// Get this sprite's starting section
pub fn get_start_section(&self) -> AnimSectionHandle {
match self.start_at {
StartEdge::Bot { section } => section,
StartEdge::Top { section } => section,
}
}
/// Iterate this sprite's sections /// Iterate this sprite's sections
pub fn iter_sections(&self) -> impl Iterator<Item = &SpriteSection> { pub fn iter_sections(&self) -> impl Iterator<Item = &SpriteSection> {
self.sections.iter() self.sections.iter()
@ -330,10 +346,12 @@ impl crate::Build for Sprite {
let h = SpriteHandle { let h = SpriteHandle {
index: content.sprites.len(), index: content.sprites.len(),
aspect,
}; };
content.sprite_index.insert(sprite_name.clone(), h); content.sprite_index.insert(sprite_name.clone(), h);
let mut smap = HashMap::new();
smap.insert("anim".to_string(), AnimSectionHandle(0));
build_context.sprite_section_index.insert(h, smap);
content.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
@ -357,26 +375,33 @@ impl crate::Build for Sprite {
// Name the one section in this sprite "anim" // Name the one section in this sprite "anim"
section_names.insert("anim".to_owned(), AnimSectionHandle(0)); section_names.insert("anim".to_owned(), AnimSectionHandle(0));
let sprite_handle = SpriteHandle {
index: content.sprites.len(),
};
content
.sprite_index
.insert(sprite_name.clone(), sprite_handle);
let mut smap = HashMap::new();
smap.insert("anim".to_string(), AnimSectionHandle(0));
build_context
.sprite_section_index
.insert(sprite_handle, smap);
let (dim, section) = s let (dim, section) = s
.add_to(build_context, content, &section_names) .add_to(sprite_handle, build_context, content)
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?; .with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
let aspect = dim.0 as f32 / dim.1 as f32; let aspect = dim.0 as f32 / dim.1 as f32;
let h = SpriteHandle {
index: content.sprites.len(),
aspect,
};
let mut sections = Vec::new(); let mut sections = Vec::new();
sections.push(section); sections.push(section);
content.sprite_index.insert(sprite_name.clone(), h);
content.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
sections, sections,
start_at: StartEdge::Bot { start_at: StartEdge::Bot {
section: AnimSectionHandle(0), section: AnimSectionHandle(0),
}, },
handle: h, handle: sprite_handle,
aspect, aspect,
}); });
} }
@ -388,9 +413,19 @@ impl crate::Build for Sprite {
idx += 1; idx += 1;
} }
let sprite_handle = SpriteHandle {
index: content.sprites.len(),
};
content
.sprite_index
.insert(sprite_name.clone(), sprite_handle);
build_context
.sprite_section_index
.insert(sprite_handle, section_names.clone());
let start_at = s let start_at = s
.start_at .start_at
.resolve_as_start(&section_names) .resolve_as_start(sprite_handle, build_context)
.with_context(|| format!("while loading sprite `{}`", sprite_name))?; .with_context(|| format!("while loading sprite `{}`", sprite_name))?;
let mut sections = Vec::with_capacity(idx); let mut sections = Vec::with_capacity(idx);
@ -403,7 +438,7 @@ impl crate::Build for Sprite {
for (k, _) in names { for (k, _) in names {
let v = s.section.get(k).unwrap(); let v = s.section.get(k).unwrap();
let (d, s) = v let (d, s) = v
.add_to(build_context, content, &section_names) .add_to(sprite_handle, build_context, content)
.with_context(|| format!("while parsing sprite `{}`", sprite_name)) .with_context(|| format!("while parsing sprite `{}`", sprite_name))
.with_context(|| format!("while parsing section `{}`", k))?; .with_context(|| format!("while parsing section `{}`", k))?;
@ -423,17 +458,11 @@ impl crate::Build for Sprite {
let dim = dim.unwrap(); let dim = dim.unwrap();
let aspect = dim.0 as f32 / dim.1 as f32; let aspect = dim.0 as f32 / dim.1 as f32;
let h = SpriteHandle {
index: content.sprites.len(),
aspect,
};
content.sprite_index.insert(sprite_name.clone(), h);
content.sprites.push(Self { content.sprites.push(Self {
name: sprite_name, name: sprite_name,
sections, sections,
start_at, start_at,
handle: h, handle: sprite_handle,
aspect, aspect,
}); });
} }

View File

@ -39,6 +39,7 @@ impl GPUState {
(device, queue) = adapter (device, queue) = adapter
.request_device( .request_device(
&wgpu::DeviceDescriptor { &wgpu::DeviceDescriptor {
// TODO: remove nonuniform sampled textures
features: wgpu::Features::TEXTURE_BINDING_ARRAY | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, features: wgpu::Features::TEXTURE_BINDING_ARRAY | wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
// We may need limits if we compile for wasm // We may need limits if we compile for wasm
limits: wgpu::Limits::default(), limits: wgpu::Limits::default(),

View File

@ -12,11 +12,11 @@ use crate::{
impl GPUState { impl GPUState {
pub(super) fn phys_push_ship( pub(super) fn phys_push_ship(
&mut self, &mut self,
state: &RenderInput, input: &RenderInput,
// NE and SW corners of screen // NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>), screen_clip: (Point2<f32>, Point2<f32>),
) { ) {
for ship in state.systemsim.iter_ships() { for ship in input.systemsim.iter_ships() {
// TODO: move collapse sequence here? // TODO: move collapse sequence here?
let ship_pos; let ship_pos;
@ -26,21 +26,21 @@ impl GPUState {
ShipState::Dead | ShipState::Landed { .. } => continue, ShipState::Dead | ShipState::Landed { .. } => continue,
ShipState::Collapsing { .. } | ShipState::Flying { .. } => { ShipState::Collapsing { .. } | ShipState::Flying { .. } => {
let r = state.systemsim.get_rigid_body(ship.rigid_body).unwrap(); let r = input.systemsim.get_rigid_body(ship.rigid_body).unwrap();
let pos = *r.translation(); let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, 1.0); ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation(); let ship_rot = r.rotation();
ship_ang = ship_rot.angle(); ship_ang = ship_rot.angle();
ship_cnt = state.ct.get_ship(ship.get_data().get_content()); ship_cnt = input.ct.get_ship(ship.get_data().get_content());
} }
ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => { ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => {
let r = state.systemsim.get_rigid_body(ship.rigid_body).unwrap(); let r = input.systemsim.get_rigid_body(ship.rigid_body).unwrap();
let pos = *r.translation(); let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, *current_z); ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation(); let ship_rot = r.rotation();
ship_ang = ship_rot.angle(); ship_ang = ship_rot.angle();
ship_cnt = state.ct.get_ship(ship.get_data().get_content()); ship_cnt = input.ct.get_ship(ship.get_data().get_content());
} }
} }
@ -48,14 +48,15 @@ impl GPUState {
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships // 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> = let pos: Point2<f32> =
(Point2::new(ship_pos.x, ship_pos.y) - state.camera_pos) / ship_pos.z; (Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z;
// Game dimensions of this sprite post-scale. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
// This is in game units. // This is in game units.
// //
// We take the maximum to account for rotated sprites. // We take the maximum to account for rotated sprites.
let m = (ship_cnt.size / ship_pos.z) * ship_cnt.sprite.aspect.max(1.0); let m =
(ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen // Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m if pos.x < screen_clip.0.x - m
@ -98,7 +99,7 @@ impl GPUState {
| ShipState::Landing { .. } => true, | ShipState::Landing { .. } => true,
_ => false, _ => false,
}; };
ship.get_controls().thrust && is_flying is_flying
} { } {
for (engine_point, anim) in ship.iter_engine_anim() { for (engine_point, anim) in ship.iter_engine_anim() {
self.state.queue.write_buffer( self.state.queue.write_buffer(
@ -136,12 +137,12 @@ impl GPUState {
pub(super) fn phys_push_projectile( pub(super) fn phys_push_projectile(
&mut self, &mut self,
state: &RenderInput, input: &RenderInput,
// NE and SW corners of screen // NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>), screen_clip: (Point2<f32>, Point2<f32>),
) { ) {
for p in state.systemsim.iter_projectiles() { for p in input.systemsim.iter_projectiles() {
let r = state.systemsim.get_rigid_body(p.rigid_body).unwrap(); let r = input.systemsim.get_rigid_body(p.rigid_body).unwrap();
let proj_pos = *r.translation(); let proj_pos = *r.translation();
let proj_rot = r.rotation(); let proj_rot = r.rotation();
let proj_ang = proj_rot.angle(); let proj_ang = proj_rot.angle();
@ -150,14 +151,14 @@ impl GPUState {
// Position adjusted for parallax // Position adjusted for parallax
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles // 1.0 is z-coordinate, which is constant for projectiles
let pos = (proj_pos - state.camera_pos) / 1.0; let pos = (proj_pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
// This is in game units. // This is in game units.
// //
// We take the maximum to account for rotated sprites. // We take the maximum to account for rotated sprites.
let m = (proj_cnt.size / 1.0) * proj_cnt.sprite.aspect.max(1.0); let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen // Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m if pos.x < screen_clip.0.x - m
@ -196,22 +197,22 @@ impl GPUState {
pub(super) fn phys_push_system( pub(super) fn phys_push_system(
&mut self, &mut self,
state: &RenderInput, input: &RenderInput,
// NE and SW corners of screen // NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>), screen_clip: (Point2<f32>, Point2<f32>),
) { ) {
let system = state.ct.get_system(state.current_system); let system = input.ct.get_system(input.current_system);
for o in &system.objects { for o in &system.objects {
// Position adjusted for parallax // Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - state.camera_pos) / o.pos.z; let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z;
// Game dimensions of this sprite post-scale. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
// This is in game units. // This is in game units.
// //
// We take the maximum to account for rotated sprites. // We take the maximum to account for rotated sprites.
let m = (o.size / o.pos.z) * o.sprite.aspect.max(1.0); let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen // Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m if pos.x < screen_clip.0.x - m
@ -239,7 +240,7 @@ impl GPUState {
}]), }]),
); );
let sprite = state.ct.get_sprite(o.sprite); let sprite = input.ct.get_sprite(o.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance // Push this object's instance

View File

@ -53,6 +53,7 @@ impl Planet {
current_object: None, current_object: None,
planet_desc: UiTextArea::new( planet_desc: UiTextArea::new(
ct,
state, state,
ct.get_sprite_handle("ui::planet"), ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0), Point2::new(0.0, 0.0),
@ -67,6 +68,7 @@ impl Planet {
), ),
planet_name: UiTextArea::new( planet_name: UiTextArea::new(
ct,
state, state,
ct.get_sprite_handle("ui::planet"), ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0), Point2::new(0.0, 0.0),
@ -135,10 +137,10 @@ impl Planet {
self.sprite.push_to_buffer(input, state); self.sprite.push_to_buffer(input, state);
} }
pub fn get_textarea(&self, _input: &RenderInput, state: &RenderState) -> [TextArea; 2] { pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> [TextArea; 2] {
[ [
self.planet_desc.get_textarea(state), self.planet_desc.get_textarea(input, state),
self.planet_name.get_textarea(state), self.planet_name.get_textarea(input, state),
] ]
} }
} }

View File

@ -133,7 +133,7 @@ impl Radar {
([c[0], c[1], c[2], 1.0], 1.0) ([c[0], c[1], c[2], 1.0], 1.0)
} }
}; };
let size = (ship.size * ship.sprite.aspect) * ship_scale * z_scale; let size = (ship.size * input.ct.get_sprite(ship.sprite).aspect) * ship_scale * z_scale;
let p: Point2<f32> = { let p: Point2<f32> = {
if s.collider == input.player.ship.unwrap() { if s.collider == input.player.ship.unwrap() {
self.last_player_position self.last_player_position

View File

@ -38,7 +38,10 @@ impl UiSprite {
/// Add this image to the gpu sprite buffer /// Add this image to the gpu sprite buffer
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let pos = Point2::new(self.rect.pos.x, self.rect.pos.y); let pos = Point2::new(self.rect.pos.x, self.rect.pos.y);
let dim = Vector2::new(self.rect.dim.y * self.sprite.aspect, self.rect.dim.y); let dim = Vector2::new(
self.rect.dim.y * input.ct.get_sprite(self.sprite).aspect,
self.rect.dim.y,
);
for c in &self.children_under { for c in &self.children_under {
c.push_to_buffer_child(input, state, pos, dim); c.push_to_buffer_child(input, state, pos, dim);

View File

@ -1,9 +1,9 @@
use galactica_content::SpriteHandle; use galactica_content::{Content, SpriteHandle};
use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds}; use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds};
use nalgebra::{Point2, Vector2}; use nalgebra::{Point2, Vector2};
use super::SpriteRect; use super::SpriteRect;
use crate::RenderState; use crate::{RenderInput, RenderState};
/// Represents a text area inside a sprite. /// Represents a text area inside a sprite.
pub(crate) struct UiTextArea { pub(crate) struct UiTextArea {
@ -32,6 +32,7 @@ pub(crate) struct UiTextArea {
impl UiTextArea { impl UiTextArea {
pub fn new( pub fn new(
ct: &Content,
state: &mut RenderState, state: &mut RenderState,
sprite: SpriteHandle, sprite: SpriteHandle,
sprite_position: Point2<f32>, sprite_position: Point2<f32>,
@ -50,16 +51,16 @@ impl UiTextArea {
align, align,
color, color,
}; };
s.set_size(state, sprite_size); s.set_size(ct, state, sprite_size);
return s; return s;
} }
pub fn set_size(&mut self, state: &mut RenderState, sprite_size: f32) { pub fn set_size(&mut self, ct: &Content, state: &mut RenderState, sprite_size: f32) {
self.sprite_size = sprite_size; self.sprite_size = sprite_size;
self.buffer.set_size( self.buffer.set_size(
&mut state.text_font_system, &mut state.text_font_system,
(self.rect.dim.x * self.sprite_size) * state.window.scale_factor() as f32, (self.rect.dim.x * self.sprite_size) * state.window.scale_factor() as f32,
(self.rect.dim.y * self.sprite_size * self.sprite.aspect) (self.rect.dim.y * self.sprite_size * ct.get_sprite(self.sprite).aspect)
* state.window.scale_factor() as f32, * state.window.scale_factor() as f32,
); );
} }
@ -74,9 +75,9 @@ impl UiTextArea {
self.buffer.shape_until_scroll(&mut state.text_font_system); self.buffer.shape_until_scroll(&mut state.text_font_system);
} }
pub fn get_textarea(&self, state: &RenderState) -> TextArea { pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> TextArea {
let h = self.sprite_size; let h = self.sprite_size;
let w = self.sprite.aspect * h; let w = input.ct.get_sprite(self.sprite).aspect * h;
// Glypon works with physical pixels, so we must convert // Glypon works with physical pixels, so we must convert
let fac = state.window.scale_factor() as f32; let fac = state.window.scale_factor() as f32;

View File

@ -1,4 +1,4 @@
use galactica_content::{CollapseEvent, Ship}; use galactica_content::{CollapseEvent, Content, Ship};
use nalgebra::{Point2, Vector2}; use nalgebra::{Point2, Vector2};
use rand::{rngs::ThreadRng, Rng}; use rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider}; use rapier2d::{dynamics::RigidBody, geometry::Collider};
@ -32,13 +32,14 @@ impl ShipCollapseSequence {
} }
/// Pick a random points inside a ship's collider /// Pick a random points inside a ship's collider
fn random_in_ship(&mut self, ship: &Ship, collider: &Collider) -> Vector2<f32> { fn random_in_ship(&mut self, ct: &Content, ship: &Ship, collider: &Collider) -> Vector2<f32> {
let mut y = 0.0; let mut y = 0.0;
let mut x = 0.0; let mut x = 0.0;
let mut a = false; let mut a = false;
while !a { while !a {
x = self.rng.gen_range(-1.0..=1.0) * ship.size / 2.0; x = self.rng.gen_range(-1.0..=1.0) * ship.size / 2.0;
y = self.rng.gen_range(-1.0..=1.0) * ship.size * ship.sprite.aspect / 2.0; y = self.rng.gen_range(-1.0..=1.0) * ship.size * ct.get_sprite(ship.sprite).aspect
/ 2.0;
a = collider.shape().contains_local_point(&Point2::new(x, y)); a = collider.shape().contains_local_point(&Point2::new(x, y));
} }
Vector2::new(x, y) Vector2::new(x, y)
@ -76,7 +77,7 @@ impl ShipCollapseSequence {
let pos: Vector2<f32> = if let Some(pos) = spawner.pos { let pos: Vector2<f32> = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y) Vector2::new(pos.x, pos.y)
} else { } else {
self.random_in_ship(ship_content, collider) self.random_in_ship(res.ct, ship_content, collider)
}; };
let pos = ship_pos + (ship_rot * pos); let pos = ship_pos + (ship_rot * pos);
@ -120,7 +121,7 @@ impl ShipCollapseSequence {
let pos = if let Some(pos) = spawner.pos { let pos = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y) Vector2::new(pos.x, pos.y)
} else { } else {
self.random_in_ship(ship_content, collider) self.random_in_ship(res.ct, ship_content, collider)
}; };
// Position, adjusted for ship rotation // Position, adjusted for ship rotation

View File

@ -64,6 +64,9 @@ pub struct PhysSimShip {
/// This ship's controls /// This ship's controls
pub(crate) controls: ShipControls, pub(crate) controls: ShipControls,
/// This ship's controls during the last frame
last_controls: ShipControls,
/// This ship's collapse sequence /// This ship's collapse sequence
collapse_sequence: Option<ShipCollapseSequence>, collapse_sequence: Option<ShipCollapseSequence>,
} }
@ -86,6 +89,7 @@ impl PhysSimShip {
data: ShipData::new(ct, handle, faction, personality), data: ShipData::new(ct, handle, faction, personality),
engine_anim: Vec::new(), engine_anim: Vec::new(),
controls: ShipControls::new(), controls: ShipControls::new(),
last_controls: ShipControls::new(),
collapse_sequence: Some(ShipCollapseSequence::new(ship_ct.collapse.length)), collapse_sequence: Some(ShipCollapseSequence::new(ship_ct.collapse.length)),
} }
} }
@ -100,14 +104,32 @@ impl PhysSimShip {
self.data.step(res.t); self.data.step(res.t);
self.anim.step(res.ct, res.t); self.anim.step(res.ct, res.t);
if self.controls.thrust { for (_, e) in &mut self.engine_anim {
for (_, e) in &mut self.engine_anim { e.step(res.ct, res.t);
e.step(res.ct, res.t); }
}
} else { if !self.controls.thrust && self.last_controls.thrust {
for (_, e) in &mut self.engine_anim { let flare = self.get_flare(res.ct);
e.reset(res.ct); if flare.is_some() {
} let flare_outfit = flare.unwrap();
let flare = res.ct.get_outfit(flare_outfit);
if flare.engine_flare_on_stop.is_some() {
for (_, e) in &mut self.engine_anim {
e.next_edge(flare.engine_flare_on_stop.unwrap().into());
}
}
};
} else if self.controls.thrust && !self.last_controls.thrust {
let flare = self.get_flare(res.ct);
if flare.is_some() {
let flare_outfit = flare.unwrap();
let flare = res.ct.get_outfit(flare_outfit);
if flare.engine_flare_on_start.is_some() {
for (_, e) in &mut self.engine_anim {
e.next_edge(flare.engine_flare_on_start.unwrap().into());
}
}
};
} }
match self.data.get_state() { match self.data.get_state() {
@ -133,6 +155,8 @@ impl PhysSimShip {
ShipState::Dead | ShipState::Landed { .. } => {} ShipState::Dead | ShipState::Landed { .. } => {}
} }
self.last_controls = self.controls.clone();
} }
/// Update this frame's physics /// Update this frame's physics
@ -196,7 +220,7 @@ impl PhysSimShip {
while !a { while !a {
x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0; x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
y = rng.gen_range(-1.0..=1.0) y = rng.gen_range(-1.0..=1.0)
* ship_content.size * ship_content.sprite.aspect * ship_content.size * res.ct.get_sprite(ship_content.sprite).aspect
/ 2.0; / 2.0;
a = collider.shape().contains_local_point(&point![x, y]); a = collider.shape().contains_local_point(&point![x, y]);
} }
@ -221,25 +245,30 @@ impl PhysSimShip {
/// Public mutable /// Public mutable
impl PhysSimShip { impl PhysSimShip {
/// Re-create this ship's engine flare animations fn get_flare(&mut self, ct: &Content) -> Option<OutfitHandle> {
/// Should be called whenever we change outfits
fn update_flares(&mut self, ct: &Content) {
// TODO: better way to pick flare sprite // TODO: better way to pick flare sprite
let mut flare = None;
for (h, _) in self.data.get_outfits().iter_outfits() { for (h, _) in self.data.get_outfits().iter_outfits() {
let c = ct.get_outfit(*h); let c = ct.get_outfit(*h);
if c.engine_flare_sprite.is_some() { if c.engine_flare_sprite.is_some() {
flare = c.engine_flare_sprite; return Some(*h);
break;
} }
} }
return None;
}
if flare.is_none() { /// Re-create this ship's engine flare animations
/// Should be called whenever we change outfits
fn update_flares(&mut self, ct: &Content) {
let flare_outfit = self.get_flare(ct);
if flare_outfit.is_none() {
self.engine_anim = Vec::new(); self.engine_anim = Vec::new();
return; return;
} }
let flare = ct
.get_outfit(flare_outfit.unwrap())
.engine_flare_sprite
.unwrap();
let flare = flare.unwrap();
self.engine_anim = ct self.engine_anim = ct
.get_ship(self.data.get_content()) .get_ship(self.data.get_content())
.engines .engines