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
## Currently working on:
- player selection
- first: fix particles & physics
- clickable buttons
- planet outfitter
## Small jobs
- 🌟 clean up and document short sprite sections
- 🌟 Better planet desc formatting
- Procedural suns
- 🌟 Back arrow -> reverse
@ -27,7 +29,6 @@
- Turn flares (physics by location?)
- Angled engines & guns
- Fix effect interaction with sprite sections
- Clean up starfieldsprite
## Misc fixes & Optimizations
- 🌟 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
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

View File

@ -66,6 +66,9 @@ pub struct AnimAutomaton {
/// 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 AnimAutomaton {
@ -90,6 +93,7 @@ impl AnimAutomaton {
sprite: sprite.handle,
current_frame: 0,
current_fade: 0.0,
next_edge_override: None,
current_direction,
current_section,
@ -98,23 +102,58 @@ impl AnimAutomaton {
}
}
/// Reset this animation
pub fn reset(&mut self, ct: &Content) {
*self = Self::new(ct, self.sprite);
/// Override the next section transition
pub fn next_edge(&mut self, s: SectionEdge) {
self.next_edge_override = Some(s);
}
/// Reverse this animation's direction
pub fn reverse(&mut self) {
/// Force a transition to the given section right now
pub fn jump_to(&mut self, ct: &Content, start: StartEdge) {
self.take_edge(ct, start.into());
}
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;
}
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;
}
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;
}
}
}
}
}
/// Step this animation by `t` seconds
pub fn step(&mut self, ct: &Content, t: f32) {
@ -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 if we are stepping backwards, it decreases.
// If this is zero, this section isn't animated.
if current_section.frame_duration == 0.0 {
return;
}
// This should always be positive, and this fact is enforced by the content loader.
// if we get here, something is very wrong.
assert!(current_section.frame_duration > 0.0);
match self.current_direction {
AnimDirection::Down => 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
@ -150,32 +195,14 @@ impl AnimAutomaton {
if self.current_frame < current_section.frames.len() - 1 {
self.current_frame += 1;
} else {
match current_section.edge_bot {
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;
}
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 e = {
if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap()
} else {
current_section.edge_bot.clone()
}
};
self.take_edge(ct, e);
}
let current_section = sprite.get_section(self.current_section);
@ -193,30 +220,14 @@ impl AnimAutomaton {
if self.current_frame > 0 {
self.current_frame -= 1;
} else {
match current_section.edge_top {
SectionEdge::Stop => {
self.current_fade = 0.0;
self.current_frame = 0;
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section } => {
self.current_section = section;
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 e = {
if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap()
} else {
current_section.edge_top.clone()
}
};
self.take_edge(ct, e);
}
let current_section = sprite.get_section(self.current_section);

View File

@ -8,25 +8,9 @@
use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpriteHandle {
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

View File

@ -13,6 +13,7 @@ use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use std::{
collections::HashMap,
fs::File,
hash::Hash,
io::Read,
path::{Path, PathBuf},
};
@ -122,13 +123,18 @@ trait Build {
/// Stores temporary data while building context objects
#[derive(Debug)]
pub(crate) struct ContentBuildContext {
/// Map effect names to handles
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 {
fn new() -> Self {
Self {
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 std::collections::HashMap;
use crate::{
handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitHandle, OutfitSpace,
StartEdge,
};
pub(crate) mod syntax {
use crate::{effect, part::outfitspace, ContentBuildContext};
use crate::{effect, part::outfitspace, sprite::syntax::SectionEdge, ContentBuildContext};
use anyhow::{bail, Result};
use galactica_util::to_radians;
use serde::Deserialize;
@ -34,7 +35,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct Engine {
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)]
@ -139,6 +147,12 @@ pub struct Outfit {
/// engine points.
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
pub shield_strength: f32,
@ -253,6 +267,8 @@ impl crate::Build for Outfit {
engine_thrust: 0.0,
steer_power: 0.0,
engine_flare_sprite: None,
engine_flare_on_start: None,
engine_flare_on_stop: None,
space: OutfitSpace::from(outfit.space),
shield_delay: 0.0,
shield_generation: 0.0,
@ -261,16 +277,44 @@ impl crate::Build for Outfit {
// Engine stats
if let Some(engine) = outfit.engine {
let th = match content.sprite_index.get(&engine.flare_sprite) {
None => bail!(
"In outfit `{}`: flare sprite `{}` doesn't exist",
outfit_name,
engine.flare_sprite
),
let sprite_handle = match content.sprite_index.get(&engine.flare.sprite) {
None => {
return Err(anyhow!(
"flare sprite `{}` doesn't exist",
engine.flare.sprite
))
.with_context(|| format!("in outfit `{}`", outfit_name));
}
Some(t) => *t,
};
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

View File

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

View File

@ -39,6 +39,7 @@ impl GPUState {
(device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
// TODO: remove nonuniform sampled textures
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
limits: wgpu::Limits::default(),

View File

@ -12,11 +12,11 @@ use crate::{
impl GPUState {
pub(super) fn phys_push_ship(
&mut self,
state: &RenderInput,
input: &RenderInput,
// NE and SW corners of screen
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?
let ship_pos;
@ -26,21 +26,21 @@ impl GPUState {
ShipState::Dead | ShipState::Landed { .. } => continue,
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();
ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation();
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, .. } => {
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();
ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation();
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?
// 1.0 is z-coordinate, which is constant for ships
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.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// 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
if pos.x < screen_clip.0.x - m
@ -98,7 +99,7 @@ impl GPUState {
| ShipState::Landing { .. } => true,
_ => false,
};
ship.get_controls().thrust && is_flying
is_flying
} {
for (engine_point, anim) in ship.iter_engine_anim() {
self.state.queue.write_buffer(
@ -136,12 +137,12 @@ impl GPUState {
pub(super) fn phys_push_projectile(
&mut self,
state: &RenderInput,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in state.systemsim.iter_projectiles() {
let r = state.systemsim.get_rigid_body(p.rigid_body).unwrap();
for p in input.systemsim.iter_projectiles() {
let r = input.systemsim.get_rigid_body(p.rigid_body).unwrap();
let proj_pos = *r.translation();
let proj_rot = r.rotation();
let proj_ang = proj_rot.angle();
@ -150,14 +151,14 @@ impl GPUState {
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 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.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// 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
if pos.x < screen_clip.0.x - m
@ -196,22 +197,22 @@ impl GPUState {
pub(super) fn phys_push_system(
&mut self,
state: &RenderInput,
input: &RenderInput,
// NE and SW corners of screen
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 {
// 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.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// 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
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
// Push this object's instance

View File

@ -53,6 +53,7 @@ impl Planet {
current_object: None,
planet_desc: UiTextArea::new(
ct,
state,
ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0),
@ -67,6 +68,7 @@ impl Planet {
),
planet_name: UiTextArea::new(
ct,
state,
ct.get_sprite_handle("ui::planet"),
Point2::new(0.0, 0.0),
@ -135,10 +137,10 @@ impl Planet {
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_name.get_textarea(state),
self.planet_desc.get_textarea(input, 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)
}
};
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> = {
if s.collider == input.player.ship.unwrap() {
self.last_player_position

View File

@ -38,7 +38,10 @@ impl UiSprite {
/// Add this image to the gpu sprite buffer
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
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 {
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 nalgebra::{Point2, Vector2};
use super::SpriteRect;
use crate::RenderState;
use crate::{RenderInput, RenderState};
/// Represents a text area inside a sprite.
pub(crate) struct UiTextArea {
@ -32,6 +32,7 @@ pub(crate) struct UiTextArea {
impl UiTextArea {
pub fn new(
ct: &Content,
state: &mut RenderState,
sprite: SpriteHandle,
sprite_position: Point2<f32>,
@ -50,16 +51,16 @@ impl UiTextArea {
align,
color,
};
s.set_size(state, sprite_size);
s.set_size(ct, state, sprite_size);
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.buffer.set_size(
&mut state.text_font_system,
(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,
);
}
@ -74,9 +75,9 @@ impl UiTextArea {
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 w = self.sprite.aspect * h;
let w = input.ct.get_sprite(self.sprite).aspect * h;
// Glypon works with physical pixels, so we must convert
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 rand::{rngs::ThreadRng, Rng};
use rapier2d::{dynamics::RigidBody, geometry::Collider};
@ -32,13 +32,14 @@ impl ShipCollapseSequence {
}
/// 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 x = 0.0;
let mut a = false;
while !a {
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));
}
Vector2::new(x, y)
@ -76,7 +77,7 @@ impl ShipCollapseSequence {
let pos: Vector2<f32> = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y)
} else {
self.random_in_ship(ship_content, collider)
self.random_in_ship(res.ct, ship_content, collider)
};
let pos = ship_pos + (ship_rot * pos);
@ -120,7 +121,7 @@ impl ShipCollapseSequence {
let pos = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y)
} else {
self.random_in_ship(ship_content, collider)
self.random_in_ship(res.ct, ship_content, collider)
};
// Position, adjusted for ship rotation

View File

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