Compare commits

...

10 Commits

Author SHA1 Message Date
Mark 23451eeb4b
Minor asset edit 2024-02-07 15:59:51 -08:00
Mark b648ef369f
Updated TODO 2024-02-07 15:59:37 -08:00
Mark bb269285bb
UI script updates 2024-02-07 15:58:48 -08:00
Mark c8f2001426
Adapted game logic for Directives 2024-02-07 15:58:38 -08:00
Mark b170f3f53f
Reworked renderer for Directives
Added OwnedTextArea & reworked textarea creation
Added ScrollBox
2024-02-07 15:58:14 -08:00
Mark 55319d6872
Reworked player agent for directives 2024-02-07 15:42:11 -08:00
Mark 5dab73ec24
Added Directives to system sim 2024-02-07 15:41:43 -08:00
Mark f56fd7ea49
Minor content cleanup 2024-02-07 15:40:43 -08:00
Mark acb5b9d31c
Minor error 2024-02-07 15:38:26 -08:00
Mark e8c9622832
Replaced handles with Arcs, added display names 2024-02-05 18:29:05 -08:00
66 changed files with 2104 additions and 1702 deletions

2
Cargo.lock generated
View File

@ -816,6 +816,7 @@ dependencies = [
"rapier2d",
"rhai",
"serde",
"smartstring",
"toml",
"walkdir",
]
@ -876,6 +877,7 @@ dependencies = [
"galactica-content",
"galactica-playeragent",
"galactica-util",
"log",
"nalgebra",
"rand",
"rapier2d",

View File

@ -75,3 +75,4 @@ clap = { version = "4.4.18", features = ["derive"] }
log = "0.4.20"
log4rs = { version = "1.2.0", features = ["console_appender"] }
rhai = { version = "1.17.1", features = ["f32_float", "no_custom_syntax"] }
smartstring = { version = "1.0.1" }

15
TODO.md
View File

@ -1,15 +1,15 @@
# Specific projects
## Now:
- Replace handles with Arcs in content
- clean up content
- Clean up state api
- Persistent variables
- Clean up & document UI api
- Persistent variables in ui scripts
- Better planet icons
- Clean up scripting errors
- Mouse colliders
- UI captures input?
- No UI zoom scroll
- Fade sprites and text in scrollbox
- Selection while flying
- outfitter
- fps textbox positioning
@ -24,12 +24,10 @@
- No wobble for ai ships & autopilot
- 🌟 User-configurable outfit space types
- 🌟 Sticky radar
- Configurable radar
- 🌟 Ship damage events
- Better landing animation (slow down)
- Land from farther away
- Ship collapse: damage + force events
- Redesign UI elements
- Background haze: 3d perlin?
- nova dust parallax
- Motion blur
@ -38,13 +36,12 @@
- Reverse engines + flares
- Turn flares (physics by location?)
- Angled engines & guns
- Unified content dir
## Misc fixes & Optimizations
- 🌟 Better errors when content/asset dirs don't exist
- Clear `// TODO:` comments
- Correct drawing order (player on top, landing ships)
- Faster handles (better than a hashmap?)
- Check for handle leaks
- Better physshiphandle
- Clean up & faster frame timings (average)
- 🌟 Handle lost focus
@ -150,6 +147,7 @@
- Muzzle effect
- Effect / sprite color variation
- UI Animations
- in-game console?
## Game & Story
@ -183,7 +181,6 @@
- Outfit pipeline
- Collision detection
- Ship AI
- Handles
- Content specification and pipeline
- How packer and optimizations work, and why
- How big should sprite resolutions be? How about frame rate?

2
assets

@ -1 +1 @@
Subproject commit fba4f1083b5a07a10445cf28bcae4bb05c2cede6
Subproject commit 1400f7bb89f1190a11a7371bb23778881073a49f

View File

@ -1,4 +1,5 @@
[outfit."plasma engines"]
name = "Plasma Engines"
thumbnail = "icon::engine"
space.engine = 20
@ -11,6 +12,7 @@ steering.power = 20
[outfit."shield generator"]
thumbnail = "icon::shield"
name = "Shield Generator"
space.outfit = 5
shield.generation = 10
@ -19,7 +21,8 @@ shield.delay = 2.0
[outfit."blaster"]
thumbnail = "icon::shield"
thumbnail = "icon::blaster"
name = "Blaster"
space.weapon = 10

View File

@ -1,4 +1,5 @@
[ship."Gypsum"]
[ship."gypsum"]
name = "Gypsum"
sprite = "ship::gypsum"
thumbnail = "icon::gypsum"
size = 100
@ -65,7 +66,7 @@ collision = [
# Scripted explosion
[[ship."Gypsum".collapse.event]]
[[ship."gypsum".collapse.event]]
time = 4.9
effects = [
#[rustfmt:skip],
@ -76,7 +77,7 @@ effects = [
]
# Scripted explosion
[[ship."Gypsum".collapse.event]]
[[ship."gypsum".collapse.event]]
time = 0.0
effects = [
#[rustfmt:skip],

View File

@ -1,7 +1,8 @@
# TODO: big objects in one config
# TODO: satisfy conditions to land
[system."12 Autumn Above"]
name = "12 Autumn Above"
object.star.sprite = "star::star"
object.star.position = [0.0, 0.0, 30.0]
object.star.size = 2000
@ -13,10 +14,9 @@ object.earth.position.angle = 0
object.earth.position.z = 10.0
object.earth.size = 1000
# TODO: satisfy conditions to land
object.earth.landable = true
object.earth.name = "Earth"
object.earth.desc = """
object.earth.landable.name = "Earth"
object.earth.landable.desc = """
The ancestral home world of humanity, Earth has a population twice that of any other inhabited planet.
Sprawling cities cover large portions of its surface, many of them overcrowded and dangerous.
Some people work to scrape together enough money to leave, while at the same time others, born
@ -28,8 +28,13 @@ one planet has a greater population than a hundred planets elsewhere. As a resul
settlements of less than a million are grouped together into planetary districts that
elect a single representative between them - a source of much frustration in the frontier worlds.
"""
object.earth.image = "ui::landscape::test"
object.earth.landable.image = "ui::landscape::test"
object.earth.landable.outfitter = [
"plasma engines",
"shield generator",
"blaster",
]
object.luna.sprite = "planet::luna"
object.luna.position.center = "earth"

View File

@ -82,6 +82,31 @@ fn event(state, event) {
}
return;
}
if type_of(event) == "ScrollEvent" {
return ui::set_camera_zoom(
ui::get_camera_zoom()
- (5.0 * event.val())
);
}
if type_of(event) == "KeyboardEvent" {
if event.key() == "up" {
return PlayerDirective::Engine(event.is_down());
}
if event.key() == "left" {
return PlayerDirective::TurnLeft(event.is_down());
}
if event.key() == "right" {
return PlayerDirective::TurnRight(event.is_down());
}
if event.key() == "space" {
return PlayerDirective::Guns(event.is_down());
}
if event.key() == "L" && event.is_down() {
return PlayerDirective::Land;
}
}
}
fn step(state) {
@ -131,7 +156,7 @@ fn step(state) {
// Ships
{
for s in state.ships() {
let uid = s.get_uid();
let uid = s.phys_uid();
let sprite_name = `radar.ship.${uid}`;
if (
@ -200,7 +225,8 @@ fn step(state) {
// System objects
{
for o in state.objects() {
let sprite_name = `radar.object.${o.get_label()}`;
let l = o.get_index();
let sprite_name = `radar.object.${l}`;
if !o.is_some() {
if sprite::exists(sprite_name) {
@ -297,5 +323,5 @@ fn step(state) {
Anchor::NorthWest
)
);
}
}
}

View File

@ -55,7 +55,7 @@ fn init(state) {
textbox::font_serif("title");
textbox::weight_bold("title");
if player.is_landed() {
textbox::set_text("title", player.landed_on().name());
textbox::set_text("title", player.landed_on().display_name());
}
textbox::add(
@ -78,9 +78,9 @@ fn event(state, event) {
let element = event.element();
if element == "button" {
if event.is_enter() {
sprite::take_edge("button", "on:top", 0.1);
sprite::jump_to("button", "on:top", 0.1);
} else {
sprite::take_edge("button", "off:top", 0.1);
sprite::jump_to("button", "off:top", 0.1);
}
}
return;
@ -100,6 +100,22 @@ fn event(state, event) {
return;
}
if type_of(event) == "KeyboardEvent" {
if !event.is_down() {
return;
}
if event.key() == "L" {
return PlayerDirective::UnLand;
}
if event.key() == "O" {
ui::go_to_scene("outfitter");
return;
}
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
ui::go_to_scene("flying");

View File

@ -84,7 +84,7 @@ fn init(state) {
textbox::font_sans("ship_type");
textbox::align_center("ship_type");
if state.player_ship().is_some() {
textbox::set_text("ship_type", state.player_ship().name());
textbox::set_text("ship_type", state.player_ship().display_name());
}
@ -161,6 +161,80 @@ fn init(state) {
textbox::font_mono("outfit_stats");
textbox::set_text("outfit_stats", "Earth");
// width should be calculated as a fraction of screen width
let scrollbox_rect = Rect(
222.0, -16.0, 470.0, 480.0,
Anchor::NorthWest,
Anchor::NorthWest,
);
scrollbox::add("outfit_list", scrollbox_rect);
let p = state.player_ship();
if p.is_landed() {
let s = "";
let x = scrollbox_rect.pos().x() + 45.0;
let y = scrollbox_rect.pos().y() - 45.0;
for xxx in ["1","2","3"] {
for i in p.landed_on().outfitter() {
s = s + i.display_name() + "\n";
let thumb_name = "outfit.thumb." + i.index() + xxx;
let backg_name = "outfit.backg." + i.index() + xxx;
let title_name = "outfit.title." + i.index() + xxx;
sprite::add(
backg_name,
"ui::outfitbg",
Rect(
x, y, 90.0, 90.0,
Anchor::Center,
Anchor::NorthWest
)
);
sprite::preserve_aspect(backg_name, true);
scrollbox::add_element("outfit_list", backg_name);
sprite::add(
thumb_name,
i.thumbnail(),
Rect(
x, y, 75.0, 75.0,
Anchor::Center,
Anchor::NorthWest
)
);
sprite::preserve_aspect(thumb_name, true);
scrollbox::add_element("outfit_list", thumb_name);
textbox::add(
title_name,
10.0, 10.0,
Rect(
x, y - 50.0, 90.0, 10.0,
Anchor::Center,
Anchor::NorthWest,
),
Color(1.0, 1.0, 1.0, 1.0)
);
textbox::font_sans(title_name);
textbox::align_center(title_name);
textbox::set_text(title_name, i.display_name());
scrollbox::add_element("outfit_list", title_name);
x = x + 120.0;
if x > (
scrollbox_rect.pos().x() + scrollbox_rect.dim().x() - 45.0
) {
x = scrollbox_rect.pos().x() + 45.0;
y = y - 120.0;
}
}
}
textbox::set_text("outfit_stats", s);
}
}
@ -169,9 +243,9 @@ fn event(state, event) {
let element = event.element();
if element == "exit_button" {
if event.is_enter() {
sprite::take_edge("exit_button", "on:top", 0.1);
sprite::jump_to("exit_button", "on:top", 0.1);
} else {
sprite::take_edge("exit_button", "off:top", 0.1);
sprite::jump_to("exit_button", "off:top", 0.1);
}
}
return;

View File

@ -30,3 +30,4 @@ rapier2d = { workspace = true }
lazy_static = { workspace = true }
log = { workspace = true }
rhai = { workspace = true }
smartstring = { workspace = true }

View File

@ -1,64 +0,0 @@
//! This module defines lightweight handles for every content type.
//! This isn't strictly necessary (we could refer to them using their string keys),
//! but this approach doesn't require frequent string cloning, and
//! gives each content type a distinct Rust type.
//!
//! We could also use raw references to content types, but that creates a mess of lifetimes
//! in our code. It's managable, but the approach here is simpler and easier to understand.
use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpriteHandle {
pub(crate) index: usize,
}
/// A lightweight representation of system body
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SystemObjectHandle {
/// TODO: pub in crate
pub system_handle: SystemHandle,
/// The index of this object in system.objects
pub body_index: usize,
}
/// A lightweight representation of an outfit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OutfitHandle {
/// TODO: pub in crate, currently for debug (same with all other handles)
pub index: usize,
}
/// A lightweight representation of a gun
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GunHandle {
/// TODO
pub index: usize,
}
/// A lightweight representation of a ship
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShipHandle {
/// TODO
pub index: usize,
}
/// A lightweight representation of a star system
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SystemHandle {
/// TODO
pub index: usize,
}
/// A lightweight representation of a faction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FactionHandle {
/// TODO
pub index: usize,
}
/// A lightweight representation of an effect
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EffectHandle {
pub(crate) index: usize,
}

View File

@ -1,9 +1,6 @@
#![warn(missing_docs)]
//! This subcrate is responsible for loading, parsing, validating game content,
//! which is usually stored in `./content`.
mod handle;
//! This subcrate is responsible for loading, parsing, validating game content.
mod part;
mod spriteautomaton;
mod util;
@ -11,18 +8,21 @@ mod util;
use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use log::warn;
use rhai::ImmutableString;
use serde::{Deserialize, Deserializer};
use smartstring::{LazyCompact, SmartString};
use std::{
//cell::OnceCell,
collections::HashMap,
fmt::Display,
fs::File,
io::Read,
num::NonZeroU32,
path::{Path, PathBuf},
sync::Arc,
};
use toml;
use walkdir::WalkDir;
pub use handle::*;
pub use part::*;
pub use spriteautomaton::*;
@ -123,11 +123,49 @@ trait Build {
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::new(&value)
}
}
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 {
/// Map effect names to handles
pub effect_index: HashMap<String, EffectHandle>,
pub effect_index: HashMap<ContentIndex, Arc<Effect>>,
}
impl ContentBuildContext {
@ -141,22 +179,29 @@ impl ContentBuildContext {
/// Represents static game content
#[derive(Debug)]
pub struct Content {
/// Sprites
pub sprites: Vec<Sprite>,
/// Map strings to texture names.
/// This is only necessary because we need to hard-code a few texture names for UI elements.
sprite_index: HashMap<String, SpriteHandle>,
/// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas,
outfits: Vec<Outfit>,
guns: Vec<Gun>, // TODO: merge with outfit
ships: Vec<Ship>,
systems: Vec<System>,
factions: Vec<Faction>,
effects: Vec<Effect>,
config: Config,
/// 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
@ -224,14 +269,12 @@ impl Content {
},
sprite_atlas: atlas,
systems: Vec::new(),
ships: Vec::new(),
guns: Vec::new(),
outfits: Vec::new(),
sprites: Vec::new(),
factions: Vec::new(),
effects: Vec::new(),
sprite_index: HashMap::new(),
systems: HashMap::new(),
ships: HashMap::new(),
outfits: HashMap::new(),
sprites: HashMap::new(),
factions: HashMap::new(),
effects: HashMap::new(),
};
// TODO: enforce sprite and image limits
@ -285,23 +328,6 @@ impl Content {
// Access methods
impl Content {
/// Iterate over all valid system handles
pub fn iter_systems(&self) -> impl Iterator<Item = SystemHandle> {
(0..self.systems.len()).map(|x| SystemHandle { index: x })
}
/// Get a handle from a sprite name
pub fn get_sprite_handle(&self, name: &str) -> Option<SpriteHandle> {
self.sprite_index.get(name).map(|x| *x)
}
/// Get a sprite from a handle
pub fn get_sprite(&self, h: SpriteHandle) -> &Sprite {
// In theory, this could fail if h has a bad index, but that shouldn't ever happen.
// The only handles that exist should be created by this crate.
return &self.sprites[h.index as usize];
}
/// Get the list of atlas files we may use
pub fn atlas_files(&self) -> &Vec<String> {
return &self.sprite_atlas.atlas_list;
@ -316,63 +342,4 @@ impl Content {
pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
&self.sprite_atlas.get_by_idx(idx)
}
/// Get an outfit from a handle
pub fn get_outfit(&self, h: OutfitHandle) -> &Outfit {
return &self.outfits[h.index];
}
/// Get a gun from a handle
pub fn get_gun(&self, h: GunHandle) -> &Gun {
return &self.guns[h.index];
}
/// Get a ship from a handle
pub fn get_ship(&self, h: ShipHandle) -> &Ship {
return &self.ships[h.index];
}
/// Get a system from a handle
pub fn get_system(&self, h: SystemHandle) -> &System {
return &self.systems[h.index];
}
/// Get a system object from a handle
pub fn get_system_object(&self, h: SystemObjectHandle) -> &SystemObject {
return &self.get_system(h.system_handle).objects[h.body_index];
}
/// Get a faction from a handle
pub fn get_faction(&self, h: FactionHandle) -> &Faction {
return &self.factions[h.index];
}
/// Get an effect from a handle
pub fn get_effect(&self, h: EffectHandle) -> &Effect {
return &self.effects[h.index];
}
/// Get content configuration
pub fn get_config(&self) -> &Config {
return &self.config;
}
}
/*
TODO: don't pass content around?
static mut CONTENT: OnceCell<Content> = OnceCell::new();
/// Initialize content::CONTENT with the given paths
pub fn init(content_dir: PathBuf, asset_dir: PathBuf, atlas_index: PathBuf) -> Result<()> {
let content = Content::load_dir(content_dir, asset_dir, atlas_index)?;
unsafe {
match CONTENT.set(content) {
Ok(()) => {}
Err(_) => {
bail!("cannot initialize content, already set.")
}
};
}
return Ok(());
}
*/

View File

@ -1,20 +1,22 @@
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle};
use crate::{Content, ContentBuildContext, ContentIndex, Sprite};
pub(crate) mod syntax {
use std::sync::Arc;
use anyhow::{bail, Result};
use galactica_util::to_radians;
use serde::Deserialize;
use crate::{Content, ContentBuildContext, EffectHandle, StartEdge};
use crate::{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: String,
pub sprite: ContentIndex,
pub size: f32,
pub size_rng: Option<f32>,
pub lifetime: TextOrFloat,
@ -59,10 +61,11 @@ pub(crate) mod syntax {
self,
_build_context: &mut ContentBuildContext,
content: &mut Content,
) -> Result<EffectHandle> {
let sprite = match content.sprite_index.get(&self.sprite) {
name: &str,
) -> Result<Arc<super::Effect>> {
let sprite = match content.sprites.get(&self.sprite) {
None => bail!("sprite `{}` doesn't exist", self.sprite),
Some(t) => *t,
Some(t) => t.clone(),
};
let lifetime = match self.lifetime {
@ -70,12 +73,11 @@ pub(crate) mod syntax {
TextOrFloat::Text(s) => {
if s == "inherit" {
// Match lifetime of first section of sprite
let sprite = content.get_sprite(sprite);
let sec = match sprite.start_at {
StartEdge::Top { section } => sprite.get_section(section),
StartEdge::Bot { section } => sprite.get_section(section),
};
sec.frame_duration * sec.frames.len() as f32
match &sprite.start_at {
StartEdge::Top { section } | StartEdge::Bot { section } => {
section.frame_duration * section.frames.len() as f32
}
}
} else {
bail!("bad effect lifetime, must be float or \"inherit\"",)
}
@ -107,11 +109,8 @@ pub(crate) mod syntax {
}
};
let handle = EffectHandle {
index: content.effects.len(),
};
content.effects.push(super::Effect {
handle,
let e = Arc::new(super::Effect {
name: name.to_string(),
sprite,
velocity,
size: self.size,
@ -126,7 +125,11 @@ pub(crate) mod syntax {
fade_rng: self.fade_rng.unwrap_or(0.0),
});
return Ok(handle);
content
.effects
.insert(ContentIndex::new(&e.name), e.clone());
return Ok(e);
}
}
@ -135,22 +138,23 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum EffectReference {
Label(String),
Label(ContentIndex),
Effect(Effect),
}
impl EffectReference {
pub fn to_handle(
pub fn resolve(
self,
build_context: &mut ContentBuildContext,
content: &mut Content,
) -> Result<EffectHandle> {
name: &str,
) -> Result<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)?,
Self::Effect(e) => e.add_to(build_context, content, name)?,
Self::Label(l) => match build_context.effect_index.get(&l) {
Some(h) => *h,
Some(h) => h.clone(),
None => bail!("no effect named `{}`", l),
},
})
@ -161,11 +165,11 @@ pub(crate) mod syntax {
/// The effect a projectile will spawn when it hits something
#[derive(Debug, Clone)]
pub struct Effect {
/// This effect's handle
pub handle: EffectHandle,
/// The sprite to use for this effect.
pub sprite: SpriteHandle,
pub sprite: Arc<Sprite>,
/// This effect's name
pub name: String,
/// The height of this effect, in game units.
pub size: f32,
@ -240,9 +244,11 @@ impl crate::Build for Effect {
) -> Result<()> {
for (effect_name, effect) in effects {
let h = effect
.add_to(build_context, content)
.add_to(build_context, content, &effect_name)
.with_context(|| format!("while evaluating effect `{}`", effect_name))?;
build_context.effect_index.insert(effect_name, h);
build_context
.effect_index
.insert(ContentIndex::new(&effect_name), h);
}
return Ok(());

View File

@ -1,13 +1,15 @@
use anyhow::{bail, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::{handle::FactionHandle, Content, ContentBuildContext};
use crate::{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.
@ -15,7 +17,7 @@ pub(crate) mod syntax {
pub struct Faction {
pub display_name: String,
pub color: [f32; 3],
pub relationship: HashMap<String, super::Relationship>,
pub relationship: HashMap<ContentIndex, super::Relationship>,
}
}
@ -44,18 +46,18 @@ pub enum Relationship {
#[derive(Debug, Clone)]
pub struct Faction {
/// The name of this faction
pub name: String,
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],
/// This faction's handle
pub handle: FactionHandle,
/// Relationships between this faction and other factions
/// This is guaranteed to contain an entry for ALL factions.
pub relationships: HashMap<FactionHandle, Relationship>,
pub relationships: HashMap<ContentIndex, Relationship>,
}
impl crate::Build for Faction {
@ -66,34 +68,21 @@ impl crate::Build for Faction {
_build_context: &mut ContentBuildContext,
content: &mut Content,
) -> Result<()> {
// Keeps track of position in faction array.
// This lets us build FactionHandles before finishing all factions.
let faction_names: Vec<String> = factions.keys().map(|x| x.to_owned()).collect();
// Indexing will break if this is false.
assert!(content.factions.len() == 0);
for f_idx in 0..faction_names.len() {
let faction_name = &faction_names[f_idx];
let faction = &factions[faction_name];
// Handle for this faction
let h = FactionHandle { index: f_idx };
for (faction_name, faction) in &factions {
// Compute relationships
let mut relationships = HashMap::new();
for i in 0..faction_names.len() {
let f_other = &faction_names[i];
let h_other = FactionHandle { index: i };
if let Some(r) = faction.relationship.get(f_other) {
relationships.insert(h_other, *r);
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 = factions[f_other].relationship.get(faction_name);
let other = other_faction
.relationship
.get(&ContentIndex::new(&faction_name));
relationships.insert(
h_other,
ContentIndex::new(other_name),
// ... and pick a relationship based on that.
match other {
Some(Relationship::Hostile) => Relationship::Hostile {},
@ -116,12 +105,15 @@ impl crate::Build for Faction {
);
}
content.factions.push(Self {
name: faction_name.to_owned(),
handle: h,
relationships,
color: faction.color,
});
content.factions.insert(
ContentIndex::new(faction_name),
Arc::new(Self {
index: ContentIndex::new(faction_name),
display_name: faction.display_name.to_owned(),
relationships,
color: faction.color,
}),
);
}
return Ok(());

View File

@ -1,14 +1,18 @@
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::{
handle::SpriteHandle, resolve_edge_as_edge, Content, ContentBuildContext, EffectHandle,
OutfitHandle, OutfitSpace, SectionEdge,
resolve_edge_as_edge, Content, ContentBuildContext, ContentIndex, Effect, OutfitSpace,
SectionEdge, Sprite,
};
pub(crate) mod syntax {
use crate::{effect, part::outfitspace, sprite::syntax::SectionEdge, ContentBuildContext};
use std::sync::Arc;
use crate::{
effect, part::outfitspace, sprite::syntax::SectionEdge, ContentBuildContext, ContentIndex,
};
use anyhow::{bail, Result};
use galactica_util::to_radians;
use serde::Deserialize;
@ -17,7 +21,8 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct Outfit {
pub thumbnail: String,
pub thumbnail: ContentIndex,
pub name: String,
pub engine: Option<Engine>,
pub steering: Option<Steering>,
pub space: outfitspace::syntax::OutfitSpace,
@ -41,7 +46,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct EngineFlare {
pub sprite: String,
pub sprite: ContentIndex,
pub on_start: Option<SectionEdge>,
pub on_stop: Option<SectionEdge>,
}
@ -64,30 +69,33 @@ pub(crate) mod syntax {
build_context: &mut ContentBuildContext,
content: &mut crate::Content,
) -> Result<super::Gun> {
let projectile_sprite_handle = match content.sprite_index.get(&self.projectile.sprite) {
let projectile_sprite = match content
.sprites
.get(&ContentIndex::new(&self.projectile.sprite))
{
None => bail!(
"projectile sprite `{}` doesn't exist",
self.projectile.sprite,
),
Some(t) => *t,
Some(t) => t.clone(),
};
let impact_effect = match self.projectile.impact_effect {
Some(e) => Some(e.to_handle(build_context, content)?),
Some(e) => Some(e.resolve(build_context, content, "")?),
None => None,
};
let expire_effect = match self.projectile.expire_effect {
Some(e) => Some(e.to_handle(build_context, content)?),
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: super::Projectile {
projectile: Arc::new(super::Projectile {
force: self.projectile.force,
sprite: projectile_sprite_handle,
sprite: projectile_sprite,
size: self.projectile.size,
size_rng: self.projectile.size_rng,
speed: self.projectile.speed,
@ -102,7 +110,7 @@ pub(crate) mod syntax {
impact_effect,
expire_effect,
collider: self.projectile.collider,
},
}),
});
}
}
@ -129,16 +137,16 @@ pub(crate) mod syntax {
#[derive(Debug, Clone)]
pub struct Outfit {
/// This outfit's thumbnail
pub thumbnail: SpriteHandle,
pub thumbnail: Arc<Sprite>,
/// How much space this outfit requires
pub space: OutfitSpace,
/// This outfit's handle
pub handle: OutfitHandle,
/// The name of this outfit
pub name: String,
pub display_name: String,
/// Thie outfit's index
pub index: ContentIndex,
/// How much engine thrust this outfit produces
pub engine_thrust: f32,
@ -149,7 +157,7 @@ pub struct Outfit {
/// The engine flare sprite this outfit creates.
/// Its location and size is determined by a ship's
/// engine points.
pub engine_flare_sprite: Option<SpriteHandle>,
pub engine_flare_sprite: Option<Arc<Sprite>>,
/// Jump to this edge when engines turn on
pub engine_flare_on_start: Option<SectionEdge>,
@ -191,7 +199,7 @@ pub struct BallCollider {
#[derive(Debug, Clone)]
pub struct Gun {
/// The projectile this gun produces
pub projectile: Projectile,
pub projectile: Arc<Projectile>,
/// Average delay between projectiles, in seconds.
pub rate: f32,
@ -205,7 +213,7 @@ pub struct Gun {
#[derive(Debug, Clone)]
pub struct Projectile {
/// The projectile sprite
pub sprite: SpriteHandle,
pub sprite: Arc<Sprite>,
/// The average size of this projectile
/// (height in game units)
@ -234,10 +242,10 @@ pub struct Projectile {
pub angle_rng: f32,
/// The effect this projectile will spawn when it hits something
pub impact_effect: Option<EffectHandle>,
pub impact_effect: Option<Arc<Effect>>,
/// The effect this projectile will spawn when it expires
pub expire_effect: Option<EffectHandle>,
pub expire_effect: Option<Arc<Effect>>,
/// Collider parameters for this projectile
pub collider: ProjectileCollider,
@ -252,10 +260,6 @@ impl crate::Build for Outfit {
content: &mut Content,
) -> Result<()> {
for (outfit_name, outfit) in outfits {
let handle = OutfitHandle {
index: content.outfits.len(),
};
let gun = match outfit.gun {
None => None,
Some(g) => Some(
@ -264,7 +268,7 @@ impl crate::Build for Outfit {
),
};
let thumb_handle = match content.sprite_index.get(&outfit.thumbnail) {
let thumbnail = match content.sprites.get(&outfit.thumbnail) {
None => {
return Err(anyhow!(
"thumbnail sprite `{}` doesn't exist",
@ -272,14 +276,14 @@ impl crate::Build for Outfit {
))
.with_context(|| format!("in outfit `{}`", outfit_name));
}
Some(t) => *t,
Some(t) => t.clone(),
};
let mut o = Self {
thumbnail: thumb_handle,
index: ContentIndex::new(&outfit_name),
display_name: outfit.name,
thumbnail,
gun,
handle,
name: outfit_name.clone(),
engine_thrust: 0.0,
steer_power: 0.0,
engine_flare_sprite: None,
@ -293,7 +297,7 @@ impl crate::Build for Outfit {
// Engine stats
if let Some(engine) = outfit.engine {
let sprite_handle = match content.sprite_index.get(&engine.flare.sprite) {
let sprite = match content.sprites.get(&engine.flare.sprite) {
None => {
return Err(anyhow!(
"flare sprite `{}` doesn't exist",
@ -301,11 +305,10 @@ impl crate::Build for Outfit {
))
.with_context(|| format!("in outfit `{}`", outfit_name));
}
Some(t) => *t,
Some(t) => t.clone(),
};
o.engine_thrust = engine.thrust;
o.engine_flare_sprite = Some(sprite_handle);
let sprite = content.get_sprite(sprite_handle);
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
@ -315,11 +318,9 @@ impl crate::Build for Outfit {
None
} else {
let x = x.unwrap();
let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| {
sprite.get_section_handle_by_name(x)
})
.with_context(|| format!("in outfit `{}`", outfit_name))?;
match e {
let mut e = resolve_edge_as_edge(&sprite.sections, &x.val, 0.0)
.with_context(|| format!("in outfit `{}`", outfit_name))?;
match &mut e {
// Inherit duration from transition sequence
SectionEdge::Top {
section,
@ -329,7 +330,7 @@ impl crate::Build for Outfit {
section,
ref mut duration,
} => {
*duration = sprite.get_section(section).frame_duration;
*duration = section.frame_duration;
}
_ => {
return Err(anyhow!(
@ -351,11 +352,9 @@ impl crate::Build for Outfit {
None
} else {
let x = x.unwrap();
let mut e = resolve_edge_as_edge(&x.val, 0.0, |x| {
sprite.get_section_handle_by_name(x)
})
.with_context(|| format!("in outfit `{}`", outfit_name))?;
match e {
let mut e = resolve_edge_as_edge(&sprite.sections, &x.val, 0.0)
.with_context(|| format!("in outfit `{}`", outfit_name))?;
match &mut e {
// Inherit duration from transition sequence
SectionEdge::Top {
section,
@ -365,7 +364,7 @@ impl crate::Build for Outfit {
section,
ref mut duration,
} => {
*duration = sprite.get_section(section).frame_duration;
*duration = section.frame_duration;
}
_ => {
return Err(anyhow!(
@ -392,7 +391,7 @@ impl crate::Build for Outfit {
o.shield_strength = shield.strength.unwrap_or(0.0);
}
content.outfits.push(o);
content.outfits.insert(o.index.clone(), Arc::new(o));
}
return Ok(());

View File

@ -2,12 +2,15 @@ use anyhow::{bail, Context, Result};
use galactica_util::to_radians;
use nalgebra::{Point2, Rotation2, Vector2};
use rapier2d::geometry::{Collider, ColliderBuilder};
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc};
use crate::{handle::SpriteHandle, Content, ContentBuildContext, EffectHandle, OutfitSpace};
use crate::{Content, ContentBuildContext, ContentIndex, Effect, OutfitSpace, Sprite};
pub(crate) mod syntax {
use crate::part::{effect::syntax::EffectReference, outfitspace};
use crate::{
part::{effect::syntax::EffectReference, outfitspace},
ContentIndex,
};
use serde::Deserialize;
// Raw serde syntax structs.
@ -15,8 +18,9 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct Ship {
pub sprite: String,
pub thumbnail: String,
pub name: String,
pub sprite: ContentIndex,
pub thumbnail: ContentIndex,
pub size: f32,
pub engines: Vec<Engine>,
pub guns: Vec<Gun>,
@ -91,14 +95,17 @@ pub(crate) mod syntax {
/// Represents a ship chassis.
#[derive(Debug, Clone)]
pub struct Ship {
/// This ship's name
pub name: String,
/// This ship's display name
pub display_name: String,
/// This object's index
pub index: ContentIndex,
/// This ship's sprite
pub sprite: SpriteHandle,
pub sprite: Arc<Sprite>,
/// This ship's thumbnail
pub thumbnail: SpriteHandle,
pub thumbnail: Arc<Sprite>,
/// The size of this ship.
/// Measured as unrotated height,
@ -122,9 +129,6 @@ pub struct Ship {
/// Collision shape for this ship
pub collider: CollisionDebugWrapper,
/// Remove later
pub aspect: f32,
/// Reduction in angular velocity over time
pub angular_drag: f32,
@ -217,7 +221,7 @@ pub struct ShipDamage {
#[derive(Debug, Clone)]
pub struct DamageEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
pub effect: Arc<Effect>,
/// How often to create this effect
pub frequency: f32,
@ -231,7 +235,7 @@ pub struct DamageEffectSpawner {
#[derive(Debug, Clone)]
pub struct CollapseEffectSpawner {
/// The effect to create
pub effect: EffectHandle,
pub effect: Arc<Effect>,
/// How many effects to create
pub count: f32,
@ -267,26 +271,25 @@ impl crate::Build for Ship {
ct: &mut Content,
) -> Result<()> {
for (ship_name, ship) in ship {
let sprite = match ct.sprite_index.get(&ship.sprite) {
let sprite = match ct.sprites.get(&ship.sprite) {
None => bail!(
"In ship `{}`: sprite `{}` doesn't exist",
ship_name,
ship.sprite
),
Some(t) => *t,
Some(t) => t.clone(),
};
let thumbnail = match ct.sprite_index.get(&ship.thumbnail) {
let thumbnail = match ct.sprites.get(&ship.thumbnail) {
None => bail!(
"In ship `{}`: thumbnail sprite `{}` doesn't exist",
ship_name,
ship.thumbnail
),
Some(t) => *t,
Some(t) => t.clone(),
};
let size = ship.size;
let aspect = ct.get_sprite(sprite).aspect;
let collapse = {
if let Some(c) = ship.collapse {
@ -295,11 +298,11 @@ impl crate::Build for Ship {
effects.push(CollapseEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.resolve(build_context, ct, "")
.with_context(|| format!("while loading ship `{}`", ship_name))?,
count: e.count,
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * aspect, p[1] * size / 2.0)
Point2::new(p[0] * (size / 2.0) * sprite.aspect, p[1] * size / 2.0)
}),
});
}
@ -313,14 +316,14 @@ impl crate::Build for Ship {
effects.push(CollapseEffectSpawner {
effect: g
.effect
.to_handle(build_context, ct)
.resolve(build_context, ct, "")
.with_context(|| {
format!("while loading ship `{}`", ship_name)
})?,
count: g.count,
pos: g.pos.map(|p| {
Point2::new(
p[0] * (size / 2.0) * aspect,
p[0] * (size / 2.0) * sprite.aspect,
p[1] * size / 2.0,
)
}),
@ -357,11 +360,11 @@ impl crate::Build for Ship {
effects.push(DamageEffectSpawner {
effect: e
.effect
.to_handle(build_context, ct)
.resolve(build_context, ct, "")
.with_context(|| format!("while loading ship `{}`", ship_name))?,
frequency: e.frequency,
pos: e.pos.map(|p| {
Point2::new(p[0] * (size / 2.0) * aspect, p[1] * size / 2.0)
Point2::new(p[0] * (size / 2.0) * sprite.aspect, p[1] * size / 2.0)
}),
});
}
@ -388,7 +391,7 @@ impl crate::Build for Ship {
// Angle adjustment, since sprites point north
// and 0 degrees is east in the game
pos: Rotation2::new(to_radians(-90.0))
* Vector2::new(g.x * size * aspect / 2.0, g.y * size / 2.0),
* Vector2::new(g.x * size * sprite.aspect / 2.0, g.y * size / 2.0),
})
}
@ -418,7 +421,7 @@ impl crate::Build for Ship {
// If we don't, rapier2 will compute local points pre-rotation,
// which will break effect placement on top of ships (i.e, collapse effects)
Rotation2::new(to_radians(-90.0))
* Point2::new(x[0] * (size / 2.0) * aspect, x[1] * size / 2.0)
* Point2::new(x[0] * (size / 2.0) * sprite.aspect, x[1] * size / 2.0)
})
.collect();
@ -427,33 +430,36 @@ impl crate::Build for Ship {
.build()
};
ct.ships.push(Self {
sprite,
thumbnail,
aspect,
collapse,
damage,
name: ship_name,
mass: ship.mass,
space: OutfitSpace::from(ship.space),
angular_drag: ship.angular_drag,
linear_drag: ship.linear_drag,
size,
hull: ship.hull,
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 * aspect / 2.0, e.y * size / 2.0),
size: e.size,
})
.collect(),
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,
guns,
collider: CollisionDebugWrapper(collider),
});
collider: CollisionDebugWrapper(collider),
}),
);
}
return Ok(());

View File

@ -1,14 +1,14 @@
use anyhow::{anyhow, bail, Context, Result};
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::{handle::SpriteHandle, Content, ContentBuildContext};
use crate::{Content, ContentBuildContext, ContentIndex};
pub(crate) mod syntax {
use crate::{AnimSectionHandle, Content};
use crate::{Content, ContentIndex};
use anyhow::{bail, Ok, Result};
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf, sync::Arc};
// Raw serde syntax structs.
// These are never seen by code outside this crate.
@ -49,7 +49,7 @@ pub(crate) mod syntax {
/// The proper, full sprite definition
#[derive(Debug, Deserialize)]
pub struct CompleteSprite {
pub section: HashMap<String, SpriteSection>,
pub section: HashMap<ContentIndex, SpriteSection>,
pub start_at: SectionEdge,
}
@ -63,14 +63,28 @@ pub(crate) mod syntax {
}
impl SpriteSection {
pub fn add_to<F>(
pub fn resolve_edges(
&self,
ct: &mut Content,
get_handle: F,
) -> Result<((u32, u32), super::SpriteSection)>
where
F: Fn(&str) -> Option<AnimSectionHandle>,
{
sections: &HashMap<ContentIndex, Arc<super::SpriteSection>>,
sec: &mut super::SpriteSection,
) -> Result<()> {
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) -> Result<((u32, u32), super::SpriteSection)> {
// Make sure all frames have the same size and add them
// to the frame vector
let mut dim = None;
@ -111,23 +125,14 @@ pub(crate) mod syntax {
bail!("frame duration must be positive (and therefore nonzero).")
}
let edge_top = match &self.top {
Some(x) => super::resolve_edge_as_edge(&x.val, frame_duration, &get_handle)?,
None => super::SectionEdge::Stop,
};
let edge_bot = match &self.bot {
Some(x) => super::resolve_edge_as_edge(&x.val, frame_duration, &get_handle)?,
None => super::SectionEdge::Stop,
};
return Ok((
dim,
super::SpriteSection {
frames,
frame_duration,
edge_top,
edge_bot,
// These are changed later, after all sections are built
edge_top: super::SectionEdge::Stop,
edge_bot: super::SectionEdge::Stop,
},
));
}
@ -141,27 +146,8 @@ pub(crate) mod syntax {
}
}
/// A handle for an animation section inside a sprite
#[derive(Debug, Copy, Clone)]
pub enum AnimSectionHandle {
/// The hidden section
Hidden,
/// An index into this sprite's section array
Idx(usize),
}
impl AnimSectionHandle {
fn get_idx(&self) -> Option<usize> {
match self {
Self::Hidden => None,
Self::Idx(idx) => Some(*idx),
}
}
}
/// An edge between two animation sections
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub enum SectionEdge {
/// Stop at the last frame of this section
Stop,
@ -169,7 +155,7 @@ pub enum SectionEdge {
/// Play the given section from the bottm
Bot {
/// The section to play
section: AnimSectionHandle,
section: Arc<SpriteSection>,
/// The length of this edge, in seconds
duration: f32,
@ -178,7 +164,7 @@ pub enum SectionEdge {
/// Play the given section from the top
Top {
/// The section to play
section: AnimSectionHandle,
section: Arc<SpriteSection>,
/// The length of this edge, in seconds
duration: f32,
@ -198,18 +184,18 @@ pub enum SectionEdge {
}
/// Where to start an animation
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub enum StartEdge {
/// Play the given section from the bottm
Bot {
/// The section to play
section: AnimSectionHandle,
section: Arc<SpriteSection>,
},
/// Play the given section from the top
Top {
/// The section to play
section: AnimSectionHandle,
section: Arc<SpriteSection>,
},
}
@ -231,80 +217,50 @@ impl Into<SectionEdge> for StartEdge {
/// Represents a sprite that may be used in the game.
#[derive(Debug, Clone)]
pub struct Sprite {
/// The name of this sprite
pub name: String,
/// This sprite's handle
pub handle: SpriteHandle,
/// This object's index
pub index: ContentIndex,
/// Where this sprite starts playing
pub start_at: StartEdge,
/// This sprite's animation sections
sections: Vec<SpriteSection>,
/// Allows us to get sprite sections by name
sections_by_name: HashMap<String, AnimSectionHandle>,
pub sections: HashMap<ContentIndex, Arc<SpriteSection>>,
/// Aspect ratio of this sprite (width / height)
pub aspect: f32,
}
lazy_static! {
static ref HIDDEN_SECTION: SpriteSection = SpriteSection {
/// 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 an animation section from a handle
pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection {
match section {
AnimSectionHandle::Hidden => &HIDDEN_SECTION,
AnimSectionHandle::Idx(idx) => &self.sections[idx],
}
}
/// Get an animation section by name.
/// Returns None for invalid names
pub fn get_section_by_name(&self, name: &str) -> Option<&SpriteSection> {
match self.sections_by_name.get(name) {
None => return None,
Some(h) => Some(self.get_section(*h)),
}
}
/// Get an animation section's handle by name.
/// Returns None for invalid names
pub fn get_section_handle_by_name(&self, name: &str) -> Option<AnimSectionHandle> {
match self.sections_by_name.get(name) {
None => return None,
Some(h) => Some(*h),
}
}
/// 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 } => *self.get_section(section).frames.last().unwrap(),
StartEdge::Top { section } => *self.get_section(section).frames.first().unwrap(),
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) -> AnimSectionHandle {
match self.start_at {
StartEdge::Bot { section } => section,
StartEdge::Top { section } => 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 = &SpriteSection> {
self.sections.iter()
pub fn iter_sections(&self) -> impl Iterator<Item = &Arc<SpriteSection>> {
self.sections.values()
}
}
@ -327,66 +283,73 @@ pub struct SpriteSection {
}
/// Resolve an edge specification string as a StartEdge
pub fn resolve_edge_as_start<F>(s: &str, get_handle: F) -> Result<super::StartEdge>
where
F: Fn(&str) -> Option<AnimSectionHandle>,
{
let e = resolve_edge_as_edge(s, 0.0, get_handle)
pub fn resolve_edge_as_start(
sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
edge_string: &str,
) -> Result<super::StartEdge> {
let e = resolve_edge_as_edge(sections, edge_string, 0.0)
.with_context(|| format!("while resolving start edge"))?;
match e {
super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }),
super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { section }),
_ => {
bail!("bad section start specification `{}`", s);
bail!("bad section start specification `{}`", edge_string);
}
}
}
/// Resolve an edge specifiation string as a SectionEdge
pub fn resolve_edge_as_edge<F>(s: &str, duration: f32, get_handle: F) -> Result<super::SectionEdge>
where
F: Fn(&str) -> Option<AnimSectionHandle>,
{
if s == "hidden" {
pub fn resolve_edge_as_edge(
sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
edge_string: &str,
duration: f32,
) -> Result<super::SectionEdge> {
if edge_string == "hidden" {
return Ok(super::SectionEdge::Top {
section: crate::AnimSectionHandle::Hidden,
section: crate::HIDDEN_SPRITE_SECTION.clone(),
duration,
});
}
if s == "stop" {
if edge_string == "stop" {
return Ok(super::SectionEdge::Stop);
}
if s == "reverse" {
if edge_string == "reverse" {
return Ok(super::SectionEdge::Reverse { duration });
}
if s == "repeat" {
if edge_string == "repeat" {
return Ok(super::SectionEdge::Repeat { duration });
}
let (s, p) = match s.split_once(":") {
let (section_name, start_point) = match edge_string.split_once(":") {
Some(x) => x,
None => {
bail!("bad section edge specification `{}`", s);
bail!("bad section edge specification `{}`", edge_string);
}
};
let section = match get_handle(s) {
let section = match sections.get(&ContentIndex::new(section_name)) {
Some(s) => s,
None => {
return Err(anyhow!("bad section edge specification `{}`", s))
.with_context(|| format!("section `{}` doesn't exist", s));
return Err(anyhow!("bad section edge specification `{}`", section_name))
.with_context(|| format!("section `{}` doesn't exist", section_name));
}
};
match p {
"top" => Ok(super::SectionEdge::Top { section, duration }),
"bot" => Ok(super::SectionEdge::Bot { section, duration }),
match start_point {
"top" => Ok(super::SectionEdge::Top {
section: section.clone(),
duration,
}),
"bot" => Ok(super::SectionEdge::Bot {
section: section.clone(),
duration,
}),
_ => {
return Err(anyhow!("bad section edge specification `{}`", s))
.with_context(|| format!("invalid target `{}`", p));
return Err(anyhow!("bad section edge specification `{}`", section_name))
.with_context(|| format!("invalid target `{}`", start_point));
}
}
}
@ -419,93 +382,60 @@ impl crate::Build for Sprite {
let img = &ct.sprite_atlas.get_by_idx(idx);
let aspect = img.w / img.h;
let h = SpriteHandle {
index: ct.sprites.len(),
};
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,
});
ct.sprite_index.insert(sprite_name.clone(), h);
ct.sprites.push(Self {
name: sprite_name,
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at: StartEdge::Top {
section: AnimSectionHandle::Idx(0),
section: section.clone(),
},
sections: {
let mut h = HashMap::new();
h.insert(ContentIndex::new("anim"), section);
h
},
sections: vec![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,
}],
sections_by_name: HashMap::new(),
handle: h,
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
syntax::Sprite::OneSection(s) => {
let sprite_handle = SpriteHandle {
index: ct.sprites.len(),
};
ct.sprite_index.insert(sprite_name.clone(), sprite_handle);
let (dim, section) = s
.add_to(ct, |s| {
if s == "anim" {
Some(AnimSectionHandle::Idx(0))
} else {
None
}
})
.add_to(ct)
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
let aspect = dim.0 as f32 / dim.1 as f32;
let mut sections = Vec::new();
sections.push(section);
let section = Arc::new(section);
ct.sprites.push(Self {
name: sprite_name,
sections,
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at: StartEdge::Top {
section: AnimSectionHandle::Idx(0),
section: section.clone(),
},
sections_by_name: {
sections: {
let mut h = HashMap::new();
h.insert("anim".to_string(), AnimSectionHandle::Idx(0));
h.insert(ContentIndex::new("anim"), section.clone());
h
},
handle: sprite_handle,
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
syntax::Sprite::Complete(s) => {
let mut section_names = HashMap::new();
for (name, _) in &s.section {
section_names
.insert(name.to_owned(), AnimSectionHandle::Idx(section_names.len()));
}
let mut sections = HashMap::new();
let sprite_handle = SpriteHandle {
index: ct.sprites.len(),
};
ct.sprite_index.insert(sprite_name.clone(), sprite_handle);
let start_at =
resolve_edge_as_start(&s.start_at.val, |x| section_names.get(x).copied())
.with_context(|| format!("while loading sprite `{}`", sprite_name))?;
let mut sections = Vec::with_capacity(section_names.len());
let mut dim = None;
// Make sure we add sections in order
let mut names = section_names.iter().collect::<Vec<_>>();
names.sort_by(|a, b| (a.1).get_idx().unwrap().cmp(&(b.1).get_idx().unwrap()));
for (k, _) in names {
let v = s.section.get(k).unwrap();
let (d, s) = v
.add_to(ct, |x| section_names.get(x).copied())
.with_context(|| format!("while parsing section `{}`", k))
for (name, sec) in &s.section {
let (d, s) = sec
.add_to(ct)
.with_context(|| format!("while parsing section `{}`", name))
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
// Make sure all dimensions are the same
@ -515,23 +445,35 @@ impl crate::Build for Sprite {
bail!(
"could not load sprite `{}`, image sizes in section `{}` are different",
sprite_name,
k
name
);
}
sections.push(s);
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;
ct.sprites.push(Self {
name: sprite_name,
sections,
let start_at = resolve_edge_as_start(&sections, &s.start_at.val)
.with_context(|| format!("while loading sprite `{}`", sprite_name))?;
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at,
handle: sprite_handle,
sections_by_name: section_names,
sections,
aspect,
});
ct.sprites.insert(sprite.index.clone(), sprite);
}
}
}

View File

@ -1,37 +1,43 @@
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{bail, Context, Result};
use galactica_util::to_radians;
use nalgebra::{Point2, Point3};
use std::collections::{HashMap, HashSet};
use crate::{
handle::SpriteHandle, util::Polar, Content, ContentBuildContext, SystemHandle,
SystemObjectHandle,
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::{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 object: HashMap<String, Object>,
pub name: String,
pub object: HashMap<ContentIndex, Object>,
}
#[derive(Debug, Deserialize)]
pub struct Object {
pub sprite: String,
pub sprite: ContentIndex,
pub position: Position,
pub size: f32,
pub radius: Option<f32>,
pub angle: Option<f32>,
pub landable: Option<bool>,
pub name: Option<String>,
pub desc: Option<String>,
pub image: Option<String>,
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)]
@ -52,14 +58,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CoordinatesTwo {
Label(String),
Label(ContentIndex),
Coords([f32; 2]),
}
impl ToString for CoordinatesTwo {
fn to_string(&self) -> String {
match self {
Self::Label(s) => s.to_owned(),
Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v),
}
}
@ -68,14 +74,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CoordinatesThree {
Label(String),
Label(ContentIndex),
Coords([f32; 3]),
}
impl ToString for CoordinatesThree {
fn to_string(&self) -> String {
match self {
Self::Label(s) => s.to_owned(),
Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v),
}
}
@ -88,14 +94,14 @@ pub(crate) mod syntax {
/// Represents a star system
#[derive(Debug, Clone)]
pub struct System {
/// This star system's name
pub name: String,
/// This object's name
pub display_name: String,
/// This star system's handle
pub handle: SystemHandle,
/// This object's index
pub index: ContentIndex,
/// Objects in this system
pub objects: Vec<SystemObject>,
pub objects: HashMap<ContentIndex, Arc<SystemObject>>,
}
/// Represents an orbiting body in a star system
@ -104,11 +110,11 @@ pub struct System {
/// System objects to not interact with the physics engine.
#[derive(Debug, Clone)]
pub struct SystemObject {
/// This object's sprite
pub sprite: SpriteHandle,
/// This object's index
pub index: ContentIndex,
/// This object's handle
pub handle: SystemObjectHandle,
/// This object's sprite
pub sprite: Arc<Sprite>,
/// This object's size.
/// Measured as height in game units.
@ -124,26 +130,29 @@ pub struct SystemObject {
pub angle: f32,
/// If true, ships may land on this object
pub landable: bool,
pub landable: Option<LandableSystemObject>,
}
/// The pretty display name of this object
pub name: Option<String>,
#[derive(Debug, Clone)]
pub struct LandableSystemObject {
/// This object's name
pub display_name: String,
/// The system-unique label of this object
pub label: String,
/// The description of this object
pub desc: String,
/// The description of this object (shown on landed ui)
pub desc: Option<String>,
/// This object's image
pub image: Arc<Sprite>,
/// This object's image (shown on landed ui)
pub image: Option<SpriteHandle>,
/// The outfits we can buy here
pub outfitter: Vec<Arc<Outfit>>,
}
/// Helper function for resolve_position, never called on its own.
fn resolve_coordinates(
objects: &HashMap<String, syntax::Object>,
objects: &HashMap<ContentIndex, syntax::Object>,
cor: &syntax::CoordinatesThree,
mut cycle_detector: HashSet<String>,
mut cycle_detector: HashSet<ContentIndex>,
) -> Result<Point3<f32>> {
match cor {
syntax::CoordinatesThree::Coords(c) => Ok((*c).into()),
@ -160,7 +169,7 @@ fn resolve_coordinates(
})
);
}
cycle_detector.insert(l.to_owned());
cycle_detector.insert(l.clone());
let p = match objects.get(l) {
Some(p) => p,
@ -174,9 +183,9 @@ fn resolve_coordinates(
/// Given an object, resolve its position as a Point3.
fn resolve_position(
objects: &HashMap<String, syntax::Object>,
objects: &HashMap<ContentIndex, syntax::Object>,
obj: &syntax::Object,
cycle_detector: HashSet<String>,
cycle_detector: HashSet<ContentIndex>,
) -> Result<Point3<f32>> {
match &obj.position {
syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?),
@ -208,99 +217,78 @@ impl crate::Build for System {
content: &mut Content,
) -> Result<()> {
for (system_name, system) in system {
let mut objects = Vec::new();
let mut objects = HashMap::new();
let system_handle = SystemHandle {
index: content.systems.len(),
};
for (label, obj) in &system.object {
for (index, object) in &system.object {
let mut cycle_detector = HashSet::new();
cycle_detector.insert(label.clone());
cycle_detector.insert(index.clone());
let sprite_handle = match content.sprite_index.get(&obj.sprite) {
let sprite = match content.sprites.get(&object.sprite) {
None => bail!(
"In system `{}`: sprite `{}` doesn't exist",
system_name,
obj.sprite
object.sprite
),
Some(t) => *t,
Some(t) => t.clone(),
};
let image_handle = match &obj.image {
Some(x) => match content.sprite_index.get(x) {
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 => bail!(
"In system `{}`: sprite `{}` doesn't exist",
system_name,
obj.sprite
object.sprite
),
Some(t) => Some(*t),
},
None => None,
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 => {
bail!("In system `{}`: outfit `{}` doesn't exist", system_name, o)
}
}
}
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"),
});
};
if obj.landable.unwrap_or(false) {
if obj.name.is_none() {
return Err(anyhow!("if an object is landable, it must have a name"))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
if obj.desc.is_none() {
return Err(anyhow!(
"if an object is landable, it must have a description"
))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
if obj.image.is_none() {
return Err(anyhow!("if an object is landable, it must have an image"))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
}
objects.push(SystemObject {
label: label.clone(),
sprite: sprite_handle,
image: image_handle,
pos: resolve_position(&system.object, &obj, cycle_detector)
.with_context(|| format!("in object {:#?}", label))?,
size: obj.size,
angle: to_radians(obj.angle.unwrap_or(0.0)),
handle: SystemObjectHandle {
system_handle,
body_index: 0,
},
landable: obj.landable.unwrap_or(false),
name: obj.name.as_ref().map(|x| x.clone()),
// TODO: better linebreaks, handle double spaces
// Tabs
desc: obj
.desc
.as_ref()
.map(|x| x.replace("\n", " ").replace("<br>", "\n")),
});
objects.insert(
index.clone(),
Arc::new(SystemObject {
index: index.clone(),
sprite,
pos: resolve_position(&system.object, &object, cycle_detector)
.with_context(|| format!("in object {:#?}", index))?,
size: object.size,
angle: to_radians(object.angle.unwrap_or(0.0)),
landable: landable,
}),
);
}
// Sort by z-distance. This is important, since these are
// rendered in this order. We need far objects to be behind
// near objects!
objects.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z));
// Update object handles
let mut i = 0;
for o in &mut objects {
o.handle.body_index = i;
i += 1;
}
content.systems.push(Self {
handle: system_handle,
name: system_name,
objects,
});
content.systems.insert(
ContentIndex::new(&system_name),
Arc::new(Self {
index: ContentIndex::new(&system_name),
display_name: system.name,
objects,
}),
);
}
return Ok(());

View File

@ -1,4 +1,5 @@
use crate::{AnimSectionHandle, Content, SectionEdge, SpriteHandle, StartEdge};
use crate::{SectionEdge, Sprite, SpriteSection, StartEdge};
use std::sync::Arc;
/// A single frame's state
#[derive(Debug, Clone)]
@ -43,11 +44,11 @@ enum AnimDirection {
#[derive(Debug, Clone)]
pub struct SpriteAutomaton {
/// The sprite we're animating
sprite: SpriteHandle,
sprite: Arc<Sprite>,
/// Which animation section we're on
/// This MUST be a section from this Automaton's sprite
current_section: AnimSectionHandle,
current_section: Arc<SpriteSection>,
/// Which frame we're on
current_frame: usize,
@ -75,39 +76,35 @@ pub struct SpriteAutomaton {
impl SpriteAutomaton {
/// Create a new AnimAutomaton
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self {
let sprite = ct.get_sprite(sprite_handle);
let (current_section, texture, current_direction) = match sprite.start_at {
StartEdge::Top { section } => (
section,
*sprite.get_section(section).frames.first().unwrap(),
AnimDirection::Down,
),
StartEdge::Bot { section } => (
section,
*sprite.get_section(section).frames.last().unwrap(),
AnimDirection::Up,
),
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)
}
}
};
let sec = sprite.get_section(current_section);
Self {
sprite: sprite.handle,
sprite: sprite.clone(),
current_frame: 0,
current_edge_progress: match current_direction {
AnimDirection::Down => 0.0,
AnimDirection::Up => sec.frame_duration,
AnimDirection::Up => current_section.frame_duration,
AnimDirection::Stop => unreachable!("how'd you get here?"),
},
current_edge_duration: sec.frame_duration,
current_edge_duration: current_section.frame_duration,
next_edge_override: None,
current_direction,
current_section,
current_section: current_section.clone(),
last_texture: texture,
next_texture: texture,
}
@ -119,14 +116,11 @@ impl SpriteAutomaton {
}
/// Force a transition to the given section right now
pub fn jump_to(&mut self, ct: &Content, start: SectionEdge) {
self.take_edge(ct, start);
pub fn jump_to(&mut self, start: &SectionEdge) {
self.take_edge(start);
}
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);
fn take_edge(&mut self, e: &SectionEdge) {
let last = match self.current_direction {
AnimDirection::Stop => self.next_texture,
AnimDirection::Down => self.next_texture,
@ -141,7 +135,7 @@ impl SpriteAutomaton {
self.current_frame = 0;
}
AnimDirection::Down => {
self.current_frame = current_section.frames.len() - 1;
self.current_frame = self.current_section.frames.len() - 1;
}
}
@ -149,29 +143,28 @@ impl SpriteAutomaton {
self.current_direction = AnimDirection::Stop;
}
SectionEdge::Top { section, duration } => {
self.current_section = section;
self.current_edge_duration = duration;
self.current_section = section.clone();
self.current_edge_duration = *duration;
self.current_frame = 0;
self.current_direction = AnimDirection::Down;
}
SectionEdge::Bot { section, duration } => {
let s = sprite.get_section(section);
self.current_section = section;
self.current_frame = s.frames.len() - 1;
self.current_edge_duration = 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 = current_section.frames.len() - 1;
self.current_frame = self.current_section.frames.len() - 1;
}
AnimDirection::Down => {
self.current_frame = 0;
}
}
self.current_edge_duration = duration;
self.current_edge_duration = *duration;
}
SectionEdge::Reverse { duration } => {
match self.current_direction {
@ -180,7 +173,7 @@ impl SpriteAutomaton {
// Jump to SECOND frame, since we've already shown the
// first during the fade transition
self.current_frame = {
if current_section.frames.len() == 1 {
if self.current_section.frames.len() == 1 {
0
} else {
1
@ -190,45 +183,39 @@ impl SpriteAutomaton {
}
AnimDirection::Down => {
self.current_frame = {
if current_section.frames.len() == 1 {
if self.current_section.frames.len() == 1 {
0
} else {
current_section.frames.len() - 2
self.current_section.frames.len() - 2
}
};
self.current_direction = AnimDirection::Up;
}
}
self.current_edge_duration = duration;
self.current_edge_duration = *duration;
}
}
match self.current_direction {
AnimDirection::Stop => {
let current_section = sprite.get_section(self.current_section);
self.next_texture = current_section.frames[self.current_frame];
self.last_texture = current_section.frames[self.current_frame];
self.next_texture = self.current_section.frames[self.current_frame];
self.last_texture = self.current_section.frames[self.current_frame];
}
AnimDirection::Down => {
let current_section = sprite.get_section(self.current_section);
self.last_texture = last;
self.next_texture = current_section.frames[self.current_frame];
self.next_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = 0.0;
}
AnimDirection::Up => {
let current_section = sprite.get_section(self.current_section);
self.next_texture = last;
self.last_texture = current_section.frames[self.current_frame];
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, ct: &Content, t: f32) {
let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section);
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.
@ -240,15 +227,16 @@ impl SpriteAutomaton {
// 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!(current_section.frame_duration >= 0.0);
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 let Some(e) = self.next_edge_override.take() {
self.take_edge(ct, e);
if self.next_edge_override.is_some() {
let e = self.next_edge_override.take().unwrap();
self.take_edge(&e);
}
return;
@ -259,21 +247,21 @@ impl SpriteAutomaton {
// We're stepping foward and finished this frame
if self.current_edge_progress > self.current_edge_duration {
if self.current_frame < current_section.frames.len() - 1 {
if self.current_frame < self.current_section.frames.len() - 1 {
self.current_frame += 1;
self.last_texture = self.next_texture;
self.next_texture = current_section.frames[self.current_frame];
self.next_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = 0.0;
self.current_edge_duration = 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 {
current_section.edge_bot.clone()
self.current_section.edge_bot.clone()
}
};
self.take_edge(ct, e);
self.take_edge(&e);
}
}
}
@ -286,18 +274,18 @@ impl SpriteAutomaton {
if self.current_frame > 0 {
self.current_frame -= 1;
self.next_texture = self.last_texture;
self.last_texture = current_section.frames[self.current_frame];
self.current_edge_progress = current_section.frame_duration;
self.current_edge_duration = current_section.frame_duration;
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 {
current_section.edge_top.clone()
self.current_section.edge_top.clone()
}
};
self.take_edge(ct, e);
self.take_edge(&e);
}
}
}
@ -314,7 +302,7 @@ impl SpriteAutomaton {
}
/// Get the sprite this automaton is using
pub fn get_sprite(&self) -> SpriteHandle {
self.sprite
pub fn get_sprite(&self) -> Arc<Sprite> {
self.sprite.clone()
}
}

View File

@ -1,7 +1,8 @@
use galactica_content::{Content, FactionHandle, OutfitHandle, ShipHandle, SystemHandle};
use galactica_content::{Content, ContentIndex};
use galactica_playeragent::PlayerAgent;
use galactica_system::data::ShipPersonality;
use galactica_system::phys::{PhysImage, PhysSim, PhysSimShipHandle, PhysStepResources};
use galactica_system::PlayerDirective;
use galactica_util::timing::Timing;
use nalgebra::Point2;
use std::sync::Arc;
@ -24,82 +25,101 @@ unsafe impl<'a> Send for Game {}
impl<'a> Game {
pub fn make_player(&mut self) -> PhysSimShipHandle {
let player = self.phys_sim.add_ship(
&self.ct,
ShipHandle { index: 0 },
FactionHandle { index: 0 },
self.ct
.ships
.get(&ContentIndex::new("gypsum"))
.unwrap()
.clone(),
self.ct
.factions
.get(&ContentIndex::new("player"))
.unwrap()
.clone(),
ShipPersonality::Player,
Point2::new(0.0, 4000.0),
);
let s = self.phys_sim.get_ship_mut(&player).unwrap();
s.add_outfits(
&self.ct,
[
OutfitHandle { index: 0 },
OutfitHandle { index: 1 },
OutfitHandle { index: 2 },
],
);
s.add_outfits([
self.ct
.outfits
.get(&ContentIndex::new("plasma engines"))
.unwrap()
.clone(),
self.ct
.outfits
.get(&ContentIndex::new("shield generator"))
.unwrap()
.clone(),
self.ct
.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
self.ct
.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
return player;
}
pub fn new(ct: Arc<Content>) -> Self {
let mut phys_sim = PhysSim::new(&ct, SystemHandle { index: 0 });
let mut phys_sim = PhysSim::new();
let a = phys_sim.add_ship(
&ct,
ShipHandle { index: 0 },
FactionHandle { index: 1 },
ShipPersonality::Point,
Point2::new(1000.0, 0.0),
);
let s = phys_sim.get_ship_mut(&a).unwrap();
s.add_outfits(
&ct,
[
OutfitHandle { index: 0 },
OutfitHandle { index: 1 },
OutfitHandle { index: 2 },
],
);
let a = phys_sim.add_ship(
&ct,
ShipHandle { index: 0 },
FactionHandle { index: 1 },
ct.ships.get(&ContentIndex::new("gypsum")).unwrap().clone(),
ct.factions
.get(&ContentIndex::new("enemy"))
.unwrap()
.clone(),
ShipPersonality::Point,
Point2::new(1000.0, 4000.0),
);
let s = phys_sim.get_ship_mut(&a).unwrap();
s.add_outfits(
&ct,
[
OutfitHandle { index: 0 },
OutfitHandle { index: 1 },
OutfitHandle { index: 2 },
],
);
s.add_outfits([
ct.outfits
.get(&ContentIndex::new("plasma engines"))
.unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("shield generator"))
.unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
let a = phys_sim.add_ship(
&ct,
ShipHandle { index: 0 },
FactionHandle { index: 0 },
ct.ships.get(&ContentIndex::new("gypsum")).unwrap().clone(),
ct.factions
.get(&ContentIndex::new("player"))
.unwrap()
.clone(),
ShipPersonality::Dummy,
Point2::new(200.0, 2000.0),
);
let s = phys_sim.get_ship_mut(&a).unwrap();
s.add_outfits(
&ct,
[
OutfitHandle { index: 0 },
OutfitHandle { index: 1 },
OutfitHandle { index: 2 },
],
);
s.add_outfits([
ct.outfits
.get(&ContentIndex::new("plasma engines"))
.unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("shield generator"))
.unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
Game {
ct,
@ -111,8 +131,10 @@ impl<'a> Game {
}
}
pub fn update_player_controls(&mut self, player: &mut PlayerAgent) {
self.phys_sim.update_player_controls(&self.ct, player)
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &PlayerAgent) {
match directive {
_ => self.phys_sim.apply_directive(directive, player),
}
}
pub fn step(&mut self, phys_img: &PhysImage) {

View File

@ -2,13 +2,10 @@ mod game;
use anyhow::{bail, Result};
use clap::Parser;
use galactica_content::{Content, SystemHandle};
use galactica_playeragent::{PlayerAgent, PlayerStatus};
use galactica_render::RenderInput;
use galactica_system::{
data::ShipState,
phys::{PhysImage, PhysSimShipHandle},
};
use galactica_content::Content;
use galactica_playeragent::PlayerAgent;
use galactica_render::{InputEvent, RenderInput};
use galactica_system::phys::PhysImage;
use galactica_util::constants::ASSET_CACHE;
use log::LevelFilter;
use log4rs::{
@ -17,15 +14,13 @@ use log4rs::{
encode::pattern::PatternEncoder,
Config,
};
use nalgebra::Vector2;
use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
};
use winit::{
event::{Event, KeyboardInput, WindowEvent},
event::{ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
@ -128,31 +123,21 @@ fn try_main() -> Result<()> {
let mut game = game::Game::new(content.clone());
let p = game.make_player();
let mut player = Arc::new(PlayerAgent::new(p.0));
Arc::get_mut(&mut player).unwrap().set_camera_aspect(
gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32,
);
let player = Arc::new(PlayerAgent::new(&content, p.0));
let mut phys_img = Arc::new(PhysImage::new());
let mut last_run = Instant::now();
event_loop.run(move |event, _, control_flow| {
match event {
Event::RedrawRequested(window_id) if window_id == gpu.window().id() => {
let render_input = RenderInput {
camera_pos: player.camera.pos,
camera_zoom: player.camera.zoom,
match gpu.render(RenderInput {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: phys_img.clone(),
player: player.clone(),
time_since_last_run: last_run.elapsed().as_secs_f32(),
current_system: SystemHandle { index: 0 },
// TODO: this is a hack for testing.
current_system: content.systems.values().next().unwrap().clone(),
timing: game.get_timing().clone(),
};
last_run = Instant::now();
match gpu.render(render_input) {
}) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(&content),
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
@ -162,41 +147,8 @@ fn try_main() -> Result<()> {
}
Event::MainEventsCleared => {
game.update_player_controls(Arc::get_mut(&mut player).unwrap());
game.step(&phys_img);
game.update_image(Arc::get_mut(&mut phys_img).unwrap());
// TODO: clean up
let player_status = {
let pos = {
let o = phys_img.get_ship(&PhysSimShipHandle(player.ship.unwrap()));
if let Some(o) = o {
match o.ship.get_data().get_state() {
ShipState::Landing { .. }
| ShipState::UnLanding { .. }
| ShipState::Collapsing { .. }
| ShipState::Flying { .. } => Some(*o.rigidbody.translation()),
ShipState::Landed { target } => {
let b = content.get_system_object(*target);
Some(Vector2::new(b.pos.x, b.pos.y))
}
ShipState::Dead => None,
}
} else {
None
}
};
PlayerStatus { pos }
};
// This must be updated BEFORE rendering!
Arc::get_mut(&mut player)
.unwrap()
.step(&content, player_status);
Arc::get_mut(&mut player).unwrap().input.clear_inputs();
gpu.window().request_redraw();
}
@ -205,7 +157,7 @@ fn try_main() -> Result<()> {
window_id,
} if window_id == gpu.window().id() => match event {
WindowEvent::Focused(_state) => {
//game.set_paused(!state);
// TODO: handle focus loss
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput {
@ -217,42 +169,94 @@ fn try_main() -> Result<()> {
},
..
} => {
Arc::get_mut(&mut player)
.unwrap()
.input
.process_key(state, key);
let directive = gpu
.process_input(
RenderInput {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: phys_img.clone(),
player: player.clone(),
current_system: content.systems.values().next().unwrap().clone(),
timing: game.get_timing().clone(),
},
InputEvent::Keyboard {
down: state == &ElementState::Pressed,
key: *key,
},
)
.unwrap();
game.apply_directive(directive, &player);
}
WindowEvent::CursorMoved { position, .. } => {
Arc::get_mut(&mut player)
.unwrap()
.input
.process_mouse(position);
let directive = gpu
.process_input(
RenderInput {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: phys_img.clone(),
player: player.clone(),
current_system: content.systems.values().next().unwrap().clone(),
timing: game.get_timing().clone(),
},
InputEvent::MouseMove(position.cast()),
)
.unwrap();
game.apply_directive(directive, &player);
}
WindowEvent::MouseInput { state, button, .. } => {
Arc::get_mut(&mut player)
.unwrap()
.input
.process_click(state, button);
let down = state == &ElementState::Pressed;
let event = match button {
MouseButton::Left => Some(InputEvent::MouseLeftClick(down)),
MouseButton::Right => Some(InputEvent::MouseRightClick(down)),
_ => None,
};
if let Some(event) = event {
let directive = gpu
.process_input(
RenderInput {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: phys_img.clone(),
player: player.clone(),
current_system: content
.systems
.values()
.next()
.unwrap()
.clone(),
timing: game.get_timing().clone(),
},
event,
)
.unwrap();
game.apply_directive(directive, &player);
}
}
WindowEvent::MouseWheel { delta, phase, .. } => {
Arc::get_mut(&mut player)
.unwrap()
.input
.process_scroll(delta, phase);
WindowEvent::MouseWheel { delta, .. } => {
let directive = gpu
.process_input(
RenderInput {
current_time: game.get_current_time(),
ct: content.clone(),
phys_img: phys_img.clone(),
player: player.clone(),
current_system: content.systems.values().next().unwrap().clone(),
timing: game.get_timing().clone(),
},
InputEvent::Scroll(match delta {
MouseScrollDelta::LineDelta(_h, v) => *v,
MouseScrollDelta::PixelDelta(v) => v.x as f32,
}),
)
.unwrap();
game.apply_directive(directive, &player);
}
WindowEvent::Resized(_) => {
gpu.resize(&content);
Arc::get_mut(&mut player).unwrap().set_camera_aspect(
gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32,
);
}
WindowEvent::ScaleFactorChanged { .. } => {
gpu.resize(&content);
Arc::get_mut(&mut player).unwrap().set_camera_aspect(
gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32,
);
gpu.window().request_redraw();
}
_ => {}
},

View File

@ -1,24 +0,0 @@
use nalgebra::Vector2;
#[derive(Debug, Clone, Copy)]
pub struct Camera {
/// Camera center
pub pos: Vector2<f32>,
/// Camera zoom
/// (How many game units tall is the viewport?)
pub zoom: f32,
/// Aspect ratio of viewport (width / height)
pub aspect: f32,
}
impl Camera {
pub fn new() -> Self {
Self {
pos: Vector2::new(0.0, 0.0),
zoom: 500.0,
aspect: 1.0,
}
}
}

View File

@ -1,151 +0,0 @@
use winit::{
dpi::PhysicalPosition,
event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode},
};
#[derive(Debug)]
pub struct InputStatus {
// Parameters
scroll_speed: f32,
mouse_position: PhysicalPosition<f32>,
// Continuous keys
key_left: bool,
key_right: bool,
key_thrust: bool,
key_guns: bool,
key_leftclick: bool,
// One-shot keys (automatically released at the end of each frame)
key_land: bool,
v_scroll: f32,
}
impl InputStatus {
pub fn new() -> Self {
InputStatus {
key_left: false,
key_right: false,
key_thrust: false,
key_guns: false,
key_land: false,
key_leftclick: false,
mouse_position: PhysicalPosition { x: 0.0, y: 0.0 },
v_scroll: 0.0,
scroll_speed: 10.0,
}
}
pub fn release_all(&mut self) {
self.key_left = false;
self.key_right = false;
self.key_thrust = false;
self.key_guns = false;
self.key_land = false;
}
/// Called at the end of every frame,
/// resets one-shot keys.
pub fn clear_inputs(&mut self) {
self.key_land = false;
self.v_scroll = 0.0;
}
pub fn process_key(&mut self, state: &ElementState, key: &VirtualKeyCode) {
let down = state == &ElementState::Pressed;
match key {
VirtualKeyCode::Left => {
self.key_left = down;
if down {
self.key_right = false;
}
}
VirtualKeyCode::Right => {
self.key_right = down;
if down {
self.key_left = false;
}
}
VirtualKeyCode::Up => self.key_thrust = down,
VirtualKeyCode::Space => self.key_guns = down,
VirtualKeyCode::L => self.key_land = down,
_ => {}
}
}
pub fn process_mouse(&mut self, position: &PhysicalPosition<f64>) {
self.mouse_position = PhysicalPosition {
x: position.x as f32,
y: position.y as f32,
};
}
pub fn process_click(&mut self, state: &ElementState, key: &MouseButton) {
let down = state == &ElementState::Pressed;
match key {
MouseButton::Left => self.key_leftclick = down,
_ => {}
}
}
pub fn process_scroll(&mut self, delta: &MouseScrollDelta, _phase: &TouchPhase) {
match delta {
MouseScrollDelta::LineDelta(_h, v) => {
self.v_scroll -= self.scroll_speed * v;
}
// TODO: handle this better
MouseScrollDelta::PixelDelta(v) => {
self.v_scroll -= v.x as f32;
}
}
}
}
// Public get-state methods
impl InputStatus {
/// Has the player applied vertical scroll?
/// This is measured in lines, scaled by scroll_speed
///
/// A positive value means scroll up, a negative value means scroll down.
/// This is reset to zero at the end of each frame.
pub fn get_v_scroll(&self) -> f32 {
self.v_scroll
}
/// Get the current mouse position
pub fn get_mouse_pos(&self) -> PhysicalPosition<f32> {
self.mouse_position
}
/// Is the player pressing the "turn left" key?
pub fn pressed_left(&self) -> bool {
self.key_left
}
/// Is the player pressing the "turn right" key?
pub fn pressed_right(&self) -> bool {
self.key_right
}
/// Is the player pressing the "fowards" key?
pub fn pressed_thrust(&self) -> bool {
self.key_thrust
}
/// Is the player pressing the "fire guns" key?
pub fn pressed_guns(&self) -> bool {
self.key_guns
}
/// Is the player pressing the left mouse button?
pub fn pressed_leftclick(&self) -> bool {
self.key_leftclick
}
/// Has the player pressed the "land" key?
/// (One-shot, reset to false at the start of each frame)
pub fn pressed_land(&self) -> bool {
self.key_land
}
}

View File

@ -1,9 +1,5 @@
mod camera;
mod inputstatus;
mod playeragent;
mod playerstatus;
pub use camera::Camera;
pub use inputstatus::InputStatus;
pub use playeragent::PlayerAgent;
pub use playerstatus::PlayerStatus;

View File

@ -1,25 +1,24 @@
use galactica_content::{Content, SystemHandle, SystemObjectHandle};
use galactica_content::{Content, ContentIndex, SystemObject};
use rapier2d::geometry::ColliderHandle;
use crate::{camera::Camera, inputstatus::InputStatus, PlayerStatus};
use std::sync::Arc;
/// What the player has selected
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub enum PlayerSelection {
/// We have nothing selected
None,
/// We have a system body selected
OrbitingBody(SystemObjectHandle),
OrbitingBody(Arc<SystemObject>),
/// We have a ship selected
Ship(ColliderHandle),
}
impl PlayerSelection {
pub fn get_planet(&self) -> Option<SystemObjectHandle> {
pub fn get_planet(&self) -> Option<&Arc<SystemObject>> {
match self {
Self::OrbitingBody(h) => Some(*h),
Self::OrbitingBody(h) => Some(h),
_ => None,
}
}
@ -32,39 +31,22 @@ pub struct PlayerAgent {
/// What the player has selected
pub selection: PlayerSelection,
/// This player's camera
pub camera: Camera,
/// What buttons this player is pressing
pub input: InputStatus,
}
impl PlayerAgent {
pub fn new(ship: ColliderHandle) -> Self {
pub fn new(ct: &Content, ship: ColliderHandle) -> Self {
Self {
input: InputStatus::new(),
camera: Camera::new(),
ship: Some(ship),
selection: PlayerSelection::OrbitingBody(SystemObjectHandle {
system_handle: SystemHandle { index: 0 },
body_index: 1,
}),
}
}
pub fn set_camera_aspect(&mut self, v: f32) {
self.camera.aspect = v
}
pub fn step(&mut self, ct: &Content, status: PlayerStatus) {
if self.input.get_v_scroll() != 0.0 {
self.camera.zoom = (self.camera.zoom + self.input.get_v_scroll())
.clamp(ct.get_config().zoom_min, ct.get_config().zoom_max);
}
if status.pos.is_some() {
self.camera.pos = status.pos.unwrap();
selection: PlayerSelection::OrbitingBody(
ct.systems
.values()
.next()
.unwrap()
.objects
.get(&ContentIndex::new("earth"))
.unwrap()
.clone(),
),
}
}
}

View File

@ -1,13 +1,13 @@
use anyhow::Result;
use bytemuck;
use galactica_content::Content;
use galactica_system::data::ShipState;
use galactica_system::{data::ShipState, phys::PhysSimShipHandle, PlayerDirective};
use galactica_util::to_radians;
use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer};
use nalgebra::{Point2, Point3};
use nalgebra::{Point2, Point3, Vector2};
use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use wgpu;
use winit;
use winit::{self};
use crate::{
globaluniform::{GlobalDataContent, GlobalUniform, ObjectData},
@ -17,7 +17,7 @@ use crate::{
texturearray::TextureArray,
ui::UiScriptExecutor,
vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance},
RenderInput, RenderState, VertexBuffers,
InputEvent, RenderInput, RenderState, VertexBuffers,
};
/// A high-level GPU wrapper. Reads game state (via RenderInput), produces pretty pictures.
@ -120,21 +120,20 @@ impl GPUState {
glyphon::fontdb::Database::new(),
);
let conf = ct.get_config();
for font in &conf.font_files {
for font in &ct.config.font_files {
text_font_system.db_mut().load_font_file(font)?;
}
// TODO: nice error if no family with this name is found
text_font_system
.db_mut()
.set_sans_serif_family(conf.font_sans.clone());
.set_sans_serif_family(ct.config.font_sans.clone());
text_font_system
.db_mut()
.set_serif_family(conf.font_serif.clone());
.set_serif_family(ct.config.font_serif.clone());
text_font_system
.db_mut()
.set_monospace_family(conf.font_mono.clone());
.set_monospace_family(ct.config.font_mono.clone());
//text_font_system
// .db_mut()
// .set_cursive_family(conf.font_cursive.clone());
@ -260,6 +259,8 @@ impl GPUState {
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
}
// TODO: this takes a long time. fix!
self.starfield.update_buffer(ct, &mut self.state);
}
@ -275,28 +276,64 @@ impl GPUState {
self.starfield.update_buffer(ct, &mut self.state);
}
/// Handle user input
pub fn process_input(
&mut self,
input: RenderInput,
event: InputEvent,
) -> Result<PlayerDirective> {
let input = Arc::new(input);
self.ui.process_input(&mut self.state, input, event)
}
/// Main render function. Draws sprites on a window.
pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> {
let input = Arc::new(input);
if let Some(ship) = input.player.ship {
let o = input.phys_img.get_ship(&PhysSimShipHandle(ship));
if let Some(o) = o {
match o.ship.get_data().get_state() {
ShipState::Landing { .. }
| ShipState::UnLanding { .. }
| ShipState::Collapsing { .. }
| ShipState::Flying { .. } => self
.ui
.state
.borrow_mut()
.camera
.set_pos(*o.rigidbody.translation()),
ShipState::Landed { target } => self
.ui
.state
.borrow_mut()
.camera
.set_pos(Vector2::new(target.pos.x, target.pos.y)),
ShipState::Dead => {}
}
}
}
// Update global values
self.state.queue.write_buffer(
&self.state.global_uniform.data_buffer,
0,
bytemuck::cast_slice(&[GlobalDataContent {
camera_position_x: input.camera_pos.x,
camera_position_y: input.camera_pos.y,
camera_zoom: input.camera_zoom,
camera_zoom_min: input.ct.get_config().zoom_min,
camera_zoom_max: input.ct.get_config().zoom_max,
camera_position_x: self.ui.state.borrow().camera.get_pos().x,
camera_position_y: self.ui.state.borrow().camera.get_pos().y,
camera_zoom: self.ui.state.borrow().camera.get_zoom(),
camera_zoom_min: input.ct.config.zoom_min,
camera_zoom_max: input.ct.config.zoom_max,
window_size_w: self.state.window_size.width as f32,
window_size_h: self.state.window_size.height as f32,
window_scale: self.state.window.scale_factor() as f32,
window_aspect: self.state.window_aspect,
starfield_sprite: input.ct.get_config().starfield_texture.into(),
starfield_tile_size: input.ct.get_config().starfield_size,
starfield_size_min: input.ct.get_config().starfield_min_size,
starfield_size_max: input.ct.get_config().starfield_max_size,
starfield_sprite: input.ct.config.starfield_texture.into(),
starfield_tile_size: input.ct.config.starfield_size,
starfield_size_min: input.ct.config.starfield_min_size,
starfield_size_max: input.ct.config.starfield_max_size,
}]),
);
@ -339,8 +376,10 @@ impl GPUState {
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom;
let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom;
let clip_ne = Point2::new(-self.state.window_aspect, 1.0)
* self.ui.state.borrow().camera.get_zoom();
let clip_sw = Point2::new(self.state.window_aspect, -1.0)
* self.ui.state.borrow().camera.get_zoom();
// Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter,
@ -425,7 +464,9 @@ impl GPUState {
},
(*self.ui.state)
.borrow_mut()
.get_textareas(&input, &self.state.window),
.get_textareas(&input, &self.state.window)
.iter()
.map(|x| x.get_textarea()),
&mut self.state.text_cache,
)
.unwrap();
@ -468,7 +509,7 @@ impl GPUState {
ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
ship_cnt = s.ship.get_data().get_content();
}
ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => {
@ -477,23 +518,23 @@ impl GPUState {
ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
ship_cnt = s.ship.get_data().get_content();
}
}
// Position adjusted for parallax
// 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) - input.camera_pos) / ship_pos.z;
let pos: Point2<f32> = (Point2::new(ship_pos.x, ship_pos.y)
- self.ui.state.borrow().camera.get_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) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0);
let m = (ship_cnt.size / ship_pos.z) * ship_cnt.sprite.aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
@ -590,14 +631,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 - input.camera_pos) / 1.0;
let pos = (proj_pos - self.ui.state.borrow().camera.get_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) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0);
let m = (proj_cnt.size / 1.0) * proj_cnt.sprite.aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
@ -641,18 +682,25 @@ impl GPUState {
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
let system = input.ct.get_system(input.current_system);
// TODO: sort once, give objects state
for o in &system.objects {
// Sort by z-distance. This is important, since these are
// rendered in this order. We need far objects to be behind
// near objects!
let mut v: Vec<_> = input.current_system.objects.values().collect();
v.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z));
for o in v {
// Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z;
let pos: Point2<f32> =
(Point2::new(o.pos.x, o.pos.y) - self.ui.state.borrow().camera.get_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) * input.ct.get_sprite(o.sprite).aspect.max(1.0);
let m = (o.size / o.pos.z) * o.sprite.aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
@ -680,8 +728,7 @@ impl GPUState {
}]),
);
let sprite = input.ct.get_sprite(o.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
let texture_a = o.sprite.get_first_frame(); // ANIMATE
// Push this object's instance
self.state.push_object_buffer(ObjectInstance {
@ -708,19 +755,14 @@ impl GPUState {
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let adjusted_pos = (pos - input.camera_pos) / 1.0;
let adjusted_pos = (pos - self.ui.state.borrow().camera.get_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 = (p.effect.size / 1.0)
* input
.ct
.get_sprite(p.effect.anim.get_sprite())
.aspect
.max(1.0);
let m = (p.effect.size / 1.0) * p.effect.anim.get_sprite().aspect.max(1.0);
// Don't draw sprites that are off the screen
if adjusted_pos.x < screen_clip.0.x - m

View File

@ -0,0 +1,29 @@
use winit::{dpi::PhysicalPosition, event::VirtualKeyCode};
/// Input received from the user
#[derive(Debug)]
pub enum InputEvent {
/// Mouse was moved
MouseMove(PhysicalPosition<f32>),
/// Mouse left button was clicked.
/// True if pressed, false if released.
MouseLeftClick(bool),
/// Mouse left button was clicked.
/// True if pressed, false if released.
MouseRightClick(bool),
/// Mouse was scrolled.
/// Value is number of lines, positive or negative.
Scroll(f32),
/// A key was pressed or released
Keyboard {
/// True if pressed, false if released
down: bool,
/// The key that was pressed
key: VirtualKeyCode,
},
}

View File

@ -9,6 +9,7 @@
mod globaluniform;
mod gpustate;
mod inputevent;
mod pipeline;
mod renderinput;
mod renderstate;
@ -18,7 +19,8 @@ mod texturearray;
mod ui;
mod vertexbuffer;
pub use gpustate::GPUState;
pub use gpustate::*;
pub use inputevent::*;
pub use renderinput::RenderInput;
use renderstate::*;

View File

@ -1,25 +1,18 @@
use std::sync::Arc;
use galactica_content::{Content, SystemHandle};
use galactica_content::{Content, System};
use galactica_playeragent::PlayerAgent;
use galactica_system::phys::PhysImage;
use galactica_util::timing::Timing;
use nalgebra::Vector2;
/// Bundles parameters passed to a single call to GPUState::render
#[derive(Debug)]
pub struct RenderInput {
/// Camera position, in world units
pub camera_pos: Vector2<f32>,
/// Player ship data
pub player: Arc<PlayerAgent>,
/// The system we're currently in
pub current_system: SystemHandle,
/// Height of screen, in world units
pub camera_zoom: f32,
pub current_system: Arc<System>,
/// The world state to render
pub phys_img: Arc<PhysImage>,
@ -28,9 +21,6 @@ pub struct RenderInput {
/// The current time, in seconds
pub current_time: f32,
/// The amount of time that has passed since the last frame was drawn
pub time_since_last_run: f32,
/// Game content
pub ct: Arc<Content>,

View File

@ -39,7 +39,7 @@ impl<'a> VertexBuffers {
ui_counter: 0,
radialbar_counter: 0,
starfield_counter: 0,
starfield_limit: ct.get_config().starfield_instance_limit,
starfield_limit: ct.config.starfield_instance_limit,
object: VertexBuffer::new::<TexturedVertex, ObjectInstance>(
"object",
@ -54,7 +54,7 @@ impl<'a> VertexBuffers {
&device,
Some(SPRITE_VERTICES),
Some(SPRITE_INDICES),
ct.get_config().starfield_instance_limit,
ct.config.starfield_instance_limit,
),
ui: VertexBuffer::new::<TexturedVertex, UiInstance>(

View File

@ -30,36 +30,32 @@ impl Starfield {
pub fn regenerate(&mut self, ct: &Content) {
// TODO: save seed in system, regenerate on jump
let mut rng = rand::thread_rng();
let sz = ct.get_config().starfield_size as f32 / 2.0;
self.stars = (0..ct.get_config().starfield_count)
let sz = ct.config.starfield_size as f32 / 2.0;
self.stars = (0..ct.config.starfield_count)
.map(|_| StarfieldStar {
pos: Point3::new(
rng.gen_range(-sz..=sz),
rng.gen_range(-sz..=sz),
rng.gen_range(
ct.get_config().starfield_min_dist..=ct.get_config().starfield_max_dist,
),
),
size: rng.gen_range(
ct.get_config().starfield_min_size..ct.get_config().starfield_max_size,
rng.gen_range(ct.config.starfield_min_dist..=ct.config.starfield_max_dist),
),
size: rng.gen_range(ct.config.starfield_min_size..ct.config.starfield_max_size),
tint: Vector2::new(rng.gen_range(0.0..=1.0), rng.gen_range(0.0..=1.0)),
})
.collect();
}
pub fn update_buffer(&mut self, ct: &Content, state: &mut RenderState) {
let sz = ct.get_config().starfield_size as f32;
let sz = ct.config.starfield_size as f32;
// Compute window size in starfield tiles
let mut nw_tile = {
// Game coordinates (relative to camera) of nw corner of screen.
let clip_nw = Point2::new(state.window_aspect, 1.0) * ct.get_config().zoom_max;
let clip_nw = Point2::new(state.window_aspect, 1.0) * ct.config.zoom_max;
// Parallax correction.
// Also, adjust v for mod to work properly
// (v is centered at 0)
let v: Point2<f32> = clip_nw * ct.get_config().starfield_min_dist;
let v: Point2<f32> = clip_nw * ct.config.starfield_min_dist;
let v_adj = Point2::new(v.x + (sz / 2.0), v.y + (sz / 2.0));
#[rustfmt::skip]
@ -83,8 +79,8 @@ impl Starfield {
// Truncate tile grid to buffer size
// (The window won't be full of stars if our instance limit is too small)
while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * ct.get_config().starfield_count as i32)
> ct.get_config().starfield_instance_limit as i32
while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * ct.config.starfield_count as i32)
> ct.config.starfield_instance_limit as i32
{
nw_tile -= Vector2::new(1, 1);
}

View File

@ -0,0 +1,28 @@
use rhai::{plugin::*, Dynamic, Module};
#[export_module]
#[allow(non_snake_case)]
#[allow(non_upper_case_globals)]
pub mod player_directive_module {
use galactica_system::PlayerDirective;
pub const None: PlayerDirective = PlayerDirective::None;
pub const Land: PlayerDirective = PlayerDirective::Land;
pub const UnLand: PlayerDirective = PlayerDirective::UnLand;
pub fn Engine(state: bool) -> PlayerDirective {
PlayerDirective::Engine(state)
}
pub fn TurnLeft(state: bool) -> PlayerDirective {
PlayerDirective::TurnLeft(state)
}
pub fn TurnRight(state: bool) -> PlayerDirective {
PlayerDirective::TurnRight(state)
}
pub fn Guns(state: bool) -> PlayerDirective {
PlayerDirective::Guns(state)
}
}

View File

@ -38,3 +38,31 @@ impl CustomType for PlayerShipStateEvent {
builder.with_name("PlayerShipStateEvent");
}
}
#[derive(Debug, Clone)]
pub struct KeyboardEvent {
pub down: bool,
pub key: ImmutableString,
}
impl CustomType for KeyboardEvent {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("KeyboardEvent")
.with_fn("is_down", |s: &mut Self| s.down)
.with_fn("key", |s: &mut Self| s.key.clone());
}
}
#[derive(Debug, Clone)]
pub struct ScrollEvent {
pub val: f32,
}
impl CustomType for ScrollEvent {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("ScrollEvent")
.with_fn("val", |s: &mut Self| s.val);
}
}

View File

@ -1,11 +1,13 @@
mod conf;
mod radialbar;
mod scrollbox;
mod sprite;
mod textbox;
mod ui;
pub use conf::build_conf_module;
pub use radialbar::build_radialbar_module;
pub use scrollbox::build_scrollbox_module;
pub use sprite::build_sprite_module;
pub use textbox::build_textbox_module;
pub use ui::build_ui_module;

View File

@ -3,7 +3,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect};
use crate::ui::{elements::RadialBar, UiElement, UiState};
use crate::ui::{elements::UiRadialBar, UiElement, UiState};
pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
let mut module = Module::new();
@ -23,11 +23,13 @@ pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
return;
}
ui_state.names.push(name.clone());
ui_state.elements.insert(
ui_state.add_element(UiElement::RadialBar(UiRadialBar::new(
name.clone(),
UiElement::RadialBar(RadialBar::new(name.clone(), stroke, color, rect, 1.0)),
);
stroke,
color,
rect,
1.0,
)));
},
);

View File

@ -0,0 +1,77 @@
use log::error;
use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc};
use super::super::Rect;
use crate::ui::{elements::UiScrollbox, UiElement, UiState};
pub fn build_scrollbox_module(state_src: Rc<RefCell<UiState>>) -> Module {
let mut module = Module::new();
module.set_id("GalacticaScrollboxModule");
let state = state_src.clone();
let _ = FuncRegistration::new("add")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |name: ImmutableString, rect: Rect| {
let mut ui_state = state.borrow_mut();
if ui_state.elements.contains_key(&name) {
error!("tried to make a scrollbox using an existing name `{name}`");
return;
}
ui_state.add_element(UiElement::Scrollbox(UiScrollbox::new(name.clone(), rect)));
});
let state = state_src.clone();
let _ = FuncRegistration::new("add_element")
.with_namespace(FnNamespace::Internal)
.set_into_module(
&mut module,
move |name: ImmutableString, target: ImmutableString| {
let mut ui_state = state.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Scrollbox(_)) => {
match ui_state.get_mut_by_name(&target) {
Some(UiElement::Text(_)) | Some(UiElement::Sprite(_)) => {
let e = match ui_state.remove_element_incomplete(&target) {
Some(UiElement::Sprite(s)) => {
Rc::new(RefCell::new(UiElement::Sprite(s)))
}
Some(UiElement::Text(t)) => {
Rc::new(RefCell::new(UiElement::Text(t)))
}
_ => unreachable!(),
};
// Add a subelement pointing to this sprite
ui_state.add_element(UiElement::SubElement {
parent: name.clone(),
element: e.clone(),
});
// Add this sprite to a scrollbox
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Scrollbox(s)) => {
s.add_element(e);
}
_ => unreachable!(),
};
}
Some(_) => {
error!("cannot add `{name}` to scrollbox `{name}`, invalid type.")
}
None => {
error!("called `scrollbox::add_element` with a non-existing target `{target}`")
}
}
}
_ => {
error!("called `scrollbox::add_element` on an invalid name `{name}`")
}
}
},
);
return module;
}

View File

@ -3,7 +3,7 @@ use log::error;
use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc, sync::Arc};
use crate::ui::{elements::Sprite, UiElement, UiState};
use crate::ui::{elements::UiSprite, UiElement, UiState};
use super::super::{Color, Rect};
@ -17,12 +17,12 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
.with_namespace(FnNamespace::Internal)
.set_into_module(
&mut module,
move |name: ImmutableString, sprite: ImmutableString, rect: Rect| {
move |name: ImmutableString, sprite_name: ImmutableString, rect: Rect| {
let mut ui_state = state.borrow_mut();
let sprite_handle = ct.get_sprite_handle(sprite.as_str());
if sprite_handle.is_none() {
error!("made a sprite using an invalid source `{sprite}`");
let sprite = ct.sprites.get(&sprite_name.clone().into());
if sprite.is_none() {
error!("made a sprite using an invalid source `{sprite_name}`");
return;
}
@ -31,11 +31,11 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
return;
}
ui_state.names.push(name.clone());
ui_state.elements.insert(
ui_state.add_element(UiElement::Sprite(UiSprite::new(
name.clone(),
UiElement::Sprite(Sprite::new(&ct, name.clone(), sprite_handle.unwrap(), rect)),
);
sprite.unwrap().clone(),
rect,
)));
},
);
@ -56,8 +56,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
.set_into_module(&mut module, move |name: ImmutableString| {
let mut ui_state = state.borrow_mut();
if ui_state.elements.contains_key(&name) {
ui_state.elements.remove(&name).unwrap();
ui_state.names.retain(|x| *x != name);
ui_state.remove_element(&name);
} else {
error!("called `sprite::remove` on an invalid name `{name}`")
}
@ -74,12 +73,12 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => {
let m = ct.get_sprite_handle(mask.as_str());
let m = ct.sprites.get(&mask.clone().into()).clone();
if m.is_none() {
error!("called `set_sprite_mask` with an invalid mask `{mask}`");
return;
}
x.set_mask(m)
x.set_mask(m.cloned())
}
_ => {
@ -90,8 +89,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
);
let state = state_src.clone();
let ct = ct_src.clone();
let _ = FuncRegistration::new("take_edge")
let _ = FuncRegistration::new("jump_to")
.with_namespace(FnNamespace::Internal)
.set_into_module(
&mut module,
@ -100,27 +98,25 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => {
let sprite_handle = x.anim.get_sprite();
let sprite = &ct.get_sprite(sprite_handle);
let sprite = x.anim.get_sprite();
let edge = resolve_edge_as_edge(edge_name.as_str(), duration, |x| {
sprite.get_section_handle_by_name(x)
});
let edge =
resolve_edge_as_edge(&sprite.sections, edge_name.as_str(), duration);
let edge = match edge {
Err(_) => {
error!(
"called `sprite::take_edge` on an invalid edge `{}` on sprite `{}`",
edge_name, sprite.name
"called `sprite::jump_to` on an invalid edge `{}` on sprite `{}`",
edge_name, sprite.index
);
return;
}
Ok(s) => s,
};
x.anim.jump_to(&ct, edge);
x.anim.jump_to(&edge);
}
_ => {
error!("called `sprite::take_edge` on an invalid name `{name}`")
error!("called `sprite::jump_to` on an invalid name `{name}`")
}
}
},

View File

@ -4,7 +4,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect};
use crate::ui::{elements::TextBox, UiElement, UiState};
use crate::ui::{elements::UiTextBox, UiElement, UiState};
pub fn build_textbox_module(
font_src: Rc<RefCell<FontSystem>>,
@ -32,18 +32,14 @@ pub fn build_textbox_module(
return;
}
ui_state.names.push(name.clone());
ui_state.elements.insert(
ui_state.add_element(UiElement::Text(UiTextBox::new(
&mut font.borrow_mut(),
name.clone(),
UiElement::Text(TextBox::new(
&mut font.borrow_mut(),
name.clone(),
font_size,
line_height,
rect,
color,
)),
);
font_size,
line_height,
rect,
color,
)));
},
);

View File

@ -15,5 +15,17 @@ pub fn build_ui_module(state_src: Rc<RefCell<UiState>>) -> Module {
ui_state.set_scene(scene);
});
let state = state_src.clone();
let _ = FuncRegistration::new("get_camera_zoom")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move || state.borrow().camera.get_zoom());
let state = state_src.clone();
let _ = FuncRegistration::new("set_camera_zoom")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |z: f32| {
state.borrow_mut().camera.set_zoom(z)
});
return module;
}

View File

@ -1,9 +1,12 @@
use nalgebra::{Point2, Vector2};
use rhai::{CustomType, TypeBuilder};
use winit::{dpi::LogicalSize, window::Window};
use winit::{
dpi::{LogicalSize, PhysicalPosition},
window::Window,
};
use super::anchor::Anchor;
use crate::{RenderInput, RenderState};
use super::{anchor::Anchor, vector::UiVector};
use crate::RenderState;
#[derive(Debug, Clone)]
pub struct Rect {
@ -78,7 +81,11 @@ impl Rect {
impl CustomType for Rect {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Rect").with_fn("Rect", Self::new);
builder
.with_name("Rect")
.with_fn("Rect", Self::new)
.with_fn("pos", |s: &mut Self| UiVector::new(s.pos.x, s.pos.y))
.with_fn("dim", |s: &mut Self| UiVector::new(s.dim.x, s.dim.y));
}
}
@ -100,17 +107,16 @@ impl CenteredRect {
return (pt.y < ne.y && pt.y > sw.y) && (pt.x > ne.x && pt.x < sw.x);
}
pub fn contains_mouse(&self, input: &RenderInput, state: &RenderState) -> bool {
pub fn contains_mouse(&self, state: &RenderState, mouse_pos: &PhysicalPosition<f32>) -> bool {
let fac = state.window.scale_factor() as f32;
let window_size = Vector2::new(
state.window_size.width as f32 / fac,
state.window_size.height as f32 / fac,
);
let pos = input.player.input.get_mouse_pos();
let mouse_pos = Point2::new(
pos.x / fac - window_size.x / 2.0,
window_size.y / 2.0 - pos.y / fac,
mouse_pos.x / fac - window_size.x / 2.0,
window_size.y / 2.0 - mouse_pos.y / fac,
);
return self.contains_point(mouse_pos);

View File

@ -1,17 +1,19 @@
mod directive;
mod event;
mod functions;
mod helpers;
mod state;
pub use directive::*;
pub use event::*;
use glyphon::FontSystem;
pub use helpers::{anchor::*, color::*, rect::*, vector::*};
use log::debug;
pub use state::*;
use super::UiState;
use galactica_content::Content;
use rhai::{exported_module, Dynamic, Engine};
use galactica_system::PlayerDirective;
use glyphon::FontSystem;
use rhai::{exported_module, Engine};
use std::{cell::RefCell, rc::Rc, sync::Arc};
pub fn register_into_engine(
@ -29,21 +31,27 @@ pub fn register_into_engine(
.build_type::<State>()
.build_type::<ShipState>()
.build_type::<SystemObjectState>()
.build_type::<OutfitState>()
// Events
.build_type::<MouseClickEvent>()
.build_type::<MouseHoverEvent>()
.build_type::<PlayerShipStateEvent>()
.build_type::<KeyboardEvent>()
.build_type::<ScrollEvent>()
// Bigger modules
.register_type_with_name::<Anchor>("Anchor")
.register_static_module("Anchor", exported_module!(anchor_mod).into());
.register_static_module("Anchor", exported_module!(anchor_mod).into())
.register_type_with_name::<PlayerDirective>("PlayerDirective")
.register_static_module(
"PlayerDirective",
exported_module!(player_directive_module).into(),
);
// Extra functions
engine.register_fn("print", move |d: Dynamic| {
debug!("{:?}", d);
});
engine.register_fn("clamp", move |x: f32, l: f32, h: f32| x.clamp(l, h));
engine.register_fn("clamp", |x: f32, l: f32, h: f32| x.clamp(l, h));
// Modules
engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into());
engine.register_static_module(
"sprite",
functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(),
@ -56,7 +64,10 @@ pub fn register_into_engine(
"radialbar",
functions::build_radialbar_module(state_src.clone()).into(),
);
engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into());
engine.register_static_module(
"scrollbox",
functions::build_scrollbox_module(state_src.clone()).into(),
);
engine.register_static_module(
"conf",
functions::build_conf_module(state_src.clone()).into(),

View File

@ -1,4 +1,4 @@
use galactica_content::{Ship, SystemObject, SystemObjectHandle};
use galactica_content::{Outfit, Ship, SystemObject};
use galactica_system::{
data::{self},
phys::{objects::PhysShip, PhysSimShipHandle},
@ -19,14 +19,15 @@ pub struct ShipState {
}
impl ShipState {
// All functions passed to rhai MUST be mut,
// even getters.
fn get_content(&mut self) -> &Ship {
let ship = self
.input
.phys_img
.get_ship(self.ship.as_ref().unwrap())
.unwrap();
let handle = ship.ship.get_data().get_content();
self.input.ct.get_ship(handle)
ship.ship.get_data().get_content()
}
fn get_ship(&mut self) -> &PhysShip {
@ -48,20 +49,13 @@ impl ShipState {
}
fn landed_on(&mut self) -> SystemObjectState {
let input = self.input.clone();
match self.get_ship().get_data().get_state() {
data::ShipState::Landed { target } => {
return SystemObjectState {
input,
object: Some(*target),
}
}
_ => {
return SystemObjectState {
input,
object: None,
object: Some(target.clone()),
}
}
_ => return SystemObjectState { object: None },
};
}
}
@ -89,8 +83,15 @@ impl CustomType for ShipState {
.with_fn("is_collapsing", |s: &mut Self| {
s.get_ship().get_data().get_state().is_collapsing()
})
.with_fn("name", |s: &mut Self| s.get_content().name.clone())
.with_fn("thumbnail", |s: &mut Self| s.get_content().thumbnail)
.with_fn("display_name", |s: &mut Self| {
s.get_content().display_name.clone()
})
.with_fn("content_index", |s: &mut Self| {
s.get_content().display_name.clone()
})
.with_fn("thumbnail", |s: &mut Self| {
s.get_content().thumbnail.clone()
})
.with_fn("landed_on", |s: &mut Self| s.landed_on())
.with_fn("get_shields", |s: &mut Self| {
s.get_ship().get_data().get_shields()
@ -103,28 +104,58 @@ impl CustomType for ShipState {
s.get_ship().get_data().get_hull()
})
.with_fn("get_size", |s: &mut Self| s.get_content().size)
.with_fn("get_uid", |s: &mut Self| format!("{}", s.get_ship().uid))
.with_fn("phys_uid", |s: &mut Self| format!("{}", s.get_ship().uid))
.with_fn("get_pos", |s: &mut Self| {
let t = s.get_body().translation();
UiVector::new(t.x, t.y)
})
.with_fn("get_faction_color", |s: &mut Self| {
let h = s.get_ship().get_data().get_faction();
let c = s.input.ct.get_faction(h).color;
let c = h.color;
Color::new(c[0], c[1], c[2], 1.0)
});
}
}
#[derive(Debug, Clone)]
pub struct OutfitState {
outfit: Arc<Outfit>,
}
impl OutfitState {}
impl CustomType for OutfitState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("OutfitState")
.with_fn("display_name", |s: &mut Self| s.outfit.display_name.clone())
.with_fn("index", |s: &mut Self| s.outfit.index.to_string())
.with_fn("thumbnail", |s: &mut Self| {
s.outfit.thumbnail.index.to_string()
});
}
}
#[derive(Debug, Clone)]
pub struct SystemObjectState {
object: Option<SystemObjectHandle>,
input: Arc<RenderInput>,
object: Option<Arc<SystemObject>>,
}
impl SystemObjectState {
fn get_content(&mut self) -> &SystemObject {
self.input.ct.get_system_object(self.object.unwrap())
fn outfitter(&mut self) -> Array {
let mut a = Array::new();
for o in &self
.object
.as_ref()
.unwrap()
.landable
.as_ref()
.unwrap()
.outfitter
{
a.push(Dynamic::from(OutfitState { outfit: o.clone() }));
}
return a;
}
}
@ -132,58 +163,70 @@ impl CustomType for SystemObjectState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SystemObjectState")
.with_fn("outfitter", Self::outfitter)
//
// Get landable name
.with_fn("name", |s: &mut Self| {
s.get_content()
.name
.with_fn("display_name", |s: &mut Self| {
s.object
.as_ref()
.map(|x| x.to_string())
.unwrap()
.landable
.as_ref()
.map(|x| x.display_name.clone())
.unwrap_or_else(|| {
error!("UI called `name()` on a system object which doesn't provide one");
error!("UI called `name()` on a system object which isn't landable");
"".to_string()
})
})
//
// Get landable description
.with_fn("desc", |s: &mut Self| {
s.get_content()
.desc
s.object
.as_ref()
.map(|x| x.to_string())
.unwrap()
.landable
.as_ref()
.map(|x| x.desc.clone())
.unwrap_or_else(|| {
error!("UI called `name()` on a system object which doesn't provide one");
error!("UI called `desc()` on a system object which isn't landable");
"".to_string()
})
})
//
// Get landable landscape image
.with_fn("image", |s: &mut Self| {
let handle = s.get_content().image;
if let Some(handle) = handle {
s.input.ct.get_sprite(handle).name.clone()
} else {
error!("UI called `image()` on a system object which doesn't provide one");
"".to_string()
}
s.object
.as_ref()
.unwrap()
.landable
.as_ref()
.map(|x| x.image.index.to_string())
.unwrap_or_else(|| {
error!("UI called `image()` on a system object which isn't landable");
"".to_string()
})
})
.with_fn("is_some", |s: &mut Self| s.object.is_some())
.with_fn("==", |a: &mut Self, b: Self| a.object == b.object)
.with_fn("get_size", |s: &mut Self| s.get_content().size)
.with_fn("get_label", |s: &mut Self| {
ImmutableString::from(&s.get_content().label)
.with_fn("is_landable", |s: &mut Self| {
s.object.as_ref().unwrap().landable.is_some()
})
.with_fn("==", |a: &mut Self, b: Self| match (&a.object, &b.object) {
(None, _) => false,
(_, None) => false,
(Some(a), Some(b)) => a.index == b.index,
})
.with_fn("get_size", |s: &mut Self| s.object.as_ref().unwrap().size)
.with_fn("get_index", |s: &mut Self| {
ImmutableString::from(s.object.as_ref().unwrap().index.as_str())
})
.with_fn("get_angle", |s: &mut Self| {
to_degrees(s.get_content().angle)
to_degrees(s.object.as_ref().unwrap().angle)
})
.with_fn("get_pos", |s: &mut Self| {
let t = s.get_content().pos;
let t = s.object.as_ref().unwrap().pos;
UiVector::new(t.x, t.y)
})
.with_fn("get_pos_z", |s: &mut Self| {
let t = s.get_content().pos;
t.z
});
.with_fn("get_pos_z", |s: &mut Self| s.object.as_ref().unwrap().pos.z);
}
}
@ -225,11 +268,9 @@ impl State {
pub fn objects(&mut self) -> Array {
let mut a = Array::new();
let s = self.input.current_system;
for o in &self.input.ct.get_system(s).objects {
for (_, o) in &self.input.current_system.objects {
a.push(Dynamic::from(SystemObjectState {
input: self.input.clone(),
object: Some(o.handle),
object: Some(o.clone()),
}));
}
return a;
@ -243,7 +284,6 @@ impl CustomType for State {
.with_fn("player_ship", Self::player_ship)
.with_fn("ships", Self::ships)
.with_fn("objects", Self::objects)
.with_fn("window_aspect", |s: &mut Self| s.window_aspect)
.with_fn("camera_zoom", |s: &mut Self| s.input.camera_zoom);
.with_fn("window_aspect", |s: &mut Self| s.window_aspect);
}
}

View File

@ -0,0 +1,35 @@
use nalgebra::Vector2;
#[derive(Debug, Clone)]
pub(crate) struct Camera {
/// The position of the camera, in game units
pos: Vector2<f32>,
/// The height of the viewport, in game units.
zoom: f32,
}
impl Camera {
pub fn new() -> Self {
Self {
pos: Vector2::new(0.0, 0.0),
zoom: 500.0,
}
}
pub fn set_pos(&mut self, pos: Vector2<f32>) {
self.pos = pos
}
pub fn get_pos(&self) -> Vector2<f32> {
self.pos
}
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom
}
pub fn get_zoom(&self) -> f32 {
self.zoom
}
}

View File

@ -1,10 +1,13 @@
use glyphon::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, TextArea, TextBounds};
use glyphon::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, TextBounds};
use std::rc::Rc;
use winit::window::Window;
use crate::{RenderInput, RenderState};
use super::OwnedTextArea;
pub(crate) struct FpsIndicator {
buffer: Buffer,
buffer: Rc<Buffer>,
update_counter: u32,
}
@ -21,7 +24,7 @@ impl FpsIndicator {
);
Self {
buffer,
buffer: Rc::new(buffer),
update_counter: 0,
}
}
@ -29,29 +32,31 @@ impl FpsIndicator {
impl FpsIndicator {
pub fn step(&mut self, input: &RenderInput, font: &mut FontSystem) {
let buffer = Rc::get_mut(&mut self.buffer).unwrap();
if self.update_counter > 0 {
self.update_counter -= 1;
return;
}
self.update_counter = 100;
self.buffer.set_text(
buffer.set_text(
font,
&input.timing.get_string(),
Attrs::new().family(Family::Monospace),
Shaping::Basic,
);
self.buffer.shape_until_scroll(font);
buffer.shape_until_scroll(font);
}
}
impl<'a, 'b: 'a> FpsIndicator {
pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> TextArea<'a> {
TextArea {
buffer: &self.buffer,
pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> OwnedTextArea {
OwnedTextArea {
buffer: self.buffer.clone(),
left: 10.0,
top: 400.0,
scale: input.ct.get_config().ui_scale,
scale: input.ct.config.ui_scale,
bounds: TextBounds {
left: 10,
top: 400,

View File

@ -1,9 +1,38 @@
mod fpsindicator;
mod radialbar;
mod scrollbox;
mod sprite;
mod textbox;
pub(super) use fpsindicator::*;
pub(super) use radialbar::*;
pub(super) use scrollbox::*;
pub(super) use sprite::*;
pub(super) use textbox::*;
use glyphon::{Buffer, Color, TextArea, TextBounds};
use std::rc::Rc;
/// A hack that lets us easily construct TextAreas
/// for [`UiTextBox`]es wrapped in Rcs.
pub struct OwnedTextArea {
pub buffer: Rc<Buffer>,
pub left: f32,
pub top: f32,
pub scale: f32,
pub bounds: TextBounds,
pub default_color: Color,
}
impl OwnedTextArea {
pub fn get_textarea(&self) -> TextArea {
TextArea {
buffer: &self.buffer,
top: self.top,
left: self.left,
scale: self.scale,
bounds: self.bounds,
default_color: self.default_color,
}
}
}

View File

@ -6,7 +6,7 @@ use super::super::api::Rect;
use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState};
#[derive(Debug, Clone)]
pub struct RadialBar {
pub struct UiRadialBar {
pub name: ImmutableString,
rect: Rect,
stroke: f32,
@ -14,7 +14,7 @@ pub struct RadialBar {
progress: f32,
}
impl RadialBar {
impl UiRadialBar {
pub fn new(
name: ImmutableString,
stroke: f32,
@ -38,16 +38,14 @@ impl RadialBar {
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let rect = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
.to_centered(&state.window, input.ct.config.ui_scale);
state.push_radialbar_buffer(RadialBarInstance {
position: [rect.pos.x, rect.pos.y],
diameter: rect.dim.x.min(rect.dim.y),
stroke: self.stroke * input.ct.get_config().ui_scale,
stroke: self.stroke * input.ct.config.ui_scale,
color: self.color.as_array(),
angle: self.progress * TAU,
});
}
pub fn step(&mut self, _input: &RenderInput, _state: &mut RenderState) {}
}

View File

@ -0,0 +1,136 @@
use nalgebra::Vector2;
use rhai::{Dynamic, ImmutableString};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use winit::window::Window;
use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::UiElement, InputEvent, RenderInput, RenderState};
#[derive(Debug)]
pub struct UiScrollbox {
pub name: ImmutableString,
pub rect: Rect,
pub offset: Vector2<f32>,
pub elements: HashMap<ImmutableString, Rc<RefCell<UiElement>>>,
has_mouse: bool,
}
impl UiScrollbox {
pub fn new(name: ImmutableString, rect: Rect) -> Self {
Self {
name,
rect,
elements: HashMap::new(),
offset: Vector2::new(0.0, 0.0),
has_mouse: false,
}
}
pub fn add_element(&mut self, e: Rc<RefCell<UiElement>>) {
let name = e.borrow().get_name().clone();
self.elements.insert(name, e);
}
pub fn remove_element(&mut self, sprite: &ImmutableString) {
self.elements.remove(sprite);
}
pub fn step(&mut self, t: f32) {
for (_name, e) in &self.elements {
match &mut *e.clone().borrow_mut() {
UiElement::Sprite(sprite) => sprite.step(t),
UiElement::RadialBar(_) => {}
UiElement::Text(..) => {}
UiElement::Scrollbox(..) => {}
UiElement::SubElement { .. } => {}
}
}
}
pub fn handle_event(
&mut self,
input: &RenderInput,
state: &mut RenderState,
event: &InputEvent,
) -> Option<Dynamic> {
let r = self
.rect
.to_centered(&state.window, input.ct.config.ui_scale);
// TODO: handle only if used in event()
// i.e, scrollable sprites shouldn't break scrollboxes
// First, check if this event is captured by any sub-elements
for (_, e) in &mut self.elements {
let arg = match &mut *e.borrow_mut() {
UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event),
UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event),
UiElement::RadialBar(_) | UiElement::Text(..) => None,
// Subelements are intentionally skipped,
// they should be handled by their parent's `handle_event` method.
UiElement::SubElement { .. } => None,
};
if arg.is_some() {
return arg;
}
}
// If no inner events were captured, handle self events.
match event {
InputEvent::MouseMove(pos) => {
if r.contains_mouse(state, pos) && !self.has_mouse {
self.has_mouse = true;
}
if !r.contains_mouse(state, pos) && self.has_mouse {
self.has_mouse = false;
}
}
InputEvent::Scroll(x) => {
if self.has_mouse {
self.offset.y -= x;
}
}
_ => return None,
}
return None;
}
}
impl UiScrollbox {
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
for (_name, e) in &self.elements {
match &*e.clone().borrow() {
UiElement::Sprite(sprite) => {
sprite.push_to_buffer_with_offset(input, state, self.offset)
}
UiElement::RadialBar(..) => {}
UiElement::Text(..) => {}
UiElement::Scrollbox(..) => {}
UiElement::SubElement { .. } => {}
}
}
}
}
// TODO: don't allocate here
impl<'a> UiScrollbox {
pub fn get_textareas(&'a self, input: &RenderInput, window: &Window) -> Vec<OwnedTextArea> {
let mut v = Vec::with_capacity(32);
for e in self.elements.values() {
match &*e.clone().borrow() {
UiElement::Text(x) => {
v.push(x.get_textarea_with_offset(input, window, self.offset))
}
_ => {}
}
}
return v;
}
}

View File

@ -1,15 +1,18 @@
use std::sync::Arc;
use super::super::api::Rect;
use crate::{
ui::{api::Color, event::Event},
ui::api::{Color, MouseClickEvent, MouseHoverEvent},
vertexbuffer::types::UiInstance,
RenderInput, RenderState,
InputEvent, RenderInput, RenderState,
};
use galactica_content::{Content, SpriteAutomaton, SpriteHandle};
use galactica_content::{Sprite, SpriteAutomaton};
use galactica_util::to_radians;
use rhai::ImmutableString;
use nalgebra::Vector2;
use rhai::{Dynamic, ImmutableString};
#[derive(Debug, Clone)]
pub struct Sprite {
pub struct UiSprite {
pub anim: SpriteAutomaton,
pub name: ImmutableString,
@ -21,32 +24,29 @@ pub struct Sprite {
preserve_aspect: bool,
rect: Rect,
mask: Option<SpriteHandle>,
mask: Option<Arc<Sprite>>,
color: Color,
/// If true, ignore mouse events until click is released
waiting_for_release: bool,
has_mouse: bool,
has_click: bool,
}
impl Sprite {
pub fn new(ct: &Content, name: ImmutableString, sprite: SpriteHandle, rect: Rect) -> Self {
impl UiSprite {
pub fn new(name: ImmutableString, sprite: Arc<Sprite>, rect: Rect) -> Self {
Self {
name,
anim: SpriteAutomaton::new(&ct, sprite),
anim: SpriteAutomaton::new(sprite),
rect,
angle: 0.0,
color: Color::new(1.0, 1.0, 1.0, 1.0),
mask: None,
has_mouse: false,
has_click: false,
waiting_for_release: false,
preserve_aspect: false,
}
}
pub fn set_mask(&mut self, mask: Option<SpriteHandle>) {
pub fn set_mask(&mut self, mask: Option<Arc<Sprite>>) {
self.mask = mask;
}
@ -65,15 +65,27 @@ impl Sprite {
pub fn set_preserve_aspect(&mut self, preserve_aspect: bool) {
self.preserve_aspect = preserve_aspect;
}
}
impl UiSprite {
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
self.push_to_buffer_with_offset(input, state, Vector2::new(0.0, 0.0))
}
pub fn push_to_buffer_with_offset(
&self,
input: &RenderInput,
state: &mut RenderState,
offset: Vector2<f32>,
) {
let mut rect = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
.to_centered(&state.window, input.ct.config.ui_scale);
rect.pos += offset;
if self.preserve_aspect {
let rect_aspect = rect.dim.x / rect.dim.y;
let sprite_aspect = input.ct.get_sprite(self.anim.get_sprite()).aspect;
let sprite_aspect = self.anim.get_sprite().aspect;
// "wide rect" case => match height, reduce width
if rect_aspect > sprite_aspect {
@ -97,64 +109,74 @@ impl Sprite {
texture_fade: anim_state.fade,
mask_index: self
.mask
.as_ref()
.map(|x| {
let sprite = input.ct.get_sprite(x);
let texture_b = sprite.get_first_frame(); // TODO: animate?
let texture_b = x.get_first_frame(); // TODO: animate?
[1, texture_b]
})
.unwrap_or([0, 0]),
});
}
pub fn check_events(&mut self, input: &RenderInput, state: &mut RenderState) -> Event {
pub fn handle_event(
&mut self,
input: &RenderInput,
state: &mut RenderState,
event: &InputEvent,
) -> Option<Dynamic> {
let r = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() {
self.waiting_for_release = false;
}
if !self.waiting_for_release
&& self.has_mouse
&& !self.has_click
&& input.player.input.pressed_leftclick()
{
self.has_click = true;
return Event::MouseClick;
}
if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() {
self.has_click = false;
return Event::MouseRelease;
}
.to_centered(&state.window, input.ct.config.ui_scale);
// Release mouse when cursor leaves box
if self.has_click && !self.has_mouse {
self.has_click = false;
return Event::MouseRelease;
}
if r.contains_mouse(input, state) && !self.has_mouse {
if input.player.input.pressed_leftclick() {
// If we're holding click when the cursor enters,
// don't trigger the `Click` event.
self.waiting_for_release = true;
match event {
InputEvent::MouseMove(pos) => {
if r.contains_mouse(state, pos) && !self.has_mouse {
self.has_mouse = true;
return Some(Dynamic::from(MouseHoverEvent {
enter: true,
element: self.name.clone(),
}));
}
if !r.contains_mouse(state, pos) && self.has_mouse {
self.has_mouse = false;
return Some(Dynamic::from(MouseHoverEvent {
enter: false,
element: self.name.clone(),
}));
}
}
self.has_mouse = true;
return Event::MouseHover;
InputEvent::MouseLeftClick(pressed) => {
if self.has_mouse && !self.has_click && *pressed {
self.has_click = true;
return Some(Dynamic::from(MouseClickEvent {
down: true,
element: self.name.clone(),
}));
}
if self.has_mouse && self.has_click && !*pressed {
self.has_click = false;
return Some(Dynamic::from(MouseClickEvent {
down: false,
element: self.name.clone(),
}));
}
}
_ => return None,
}
if !r.contains_mouse(input, state) && self.has_mouse {
self.waiting_for_release = false;
self.has_mouse = false;
return Event::MouseUnhover;
}
return Event::None;
return None;
}
pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) {
self.anim.step(&input.ct, input.time_since_last_run);
pub fn step(&mut self, t: f32) {
self.anim.step(t);
}
}

View File

@ -1,27 +1,28 @@
use glyphon::{
cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics,
Shaping, Style, TextArea, TextBounds, Weight,
Shaping, Style, TextBounds, Weight,
};
use nalgebra::Vector2;
use rhai::ImmutableString;
use std::rc::Rc;
use winit::window::Window;
use super::super::api::Rect;
use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::api, RenderInput};
#[derive(Debug)]
pub struct TextBox {
pub struct UiTextBox {
pub name: ImmutableString,
text: String,
justify: Align,
rect: Rect,
buffer: Buffer,
buffer: Rc<Buffer>,
color: api::Color,
attrs: AttrsOwned,
}
impl TextBox {
impl UiTextBox {
pub fn new(
font: &mut FontSystem,
name: ImmutableString,
@ -38,7 +39,7 @@ impl TextBox {
Self {
name,
rect,
buffer,
buffer: Rc::new(buffer),
color,
justify: Align::Left,
attrs: AttrsOwned::new(Attrs::new()),
@ -47,14 +48,14 @@ impl TextBox {
}
fn reflow(&mut self, font: &mut FontSystem) {
self.buffer
.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced);
let buffer = Rc::get_mut(&mut self.buffer).unwrap();
buffer.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced);
for l in &mut self.buffer.lines {
for l in &mut buffer.lines {
l.set_align(Some(self.justify));
}
self.buffer.shape_until_scroll(font);
buffer.shape_until_scroll(font);
}
pub fn set_text(&mut self, font: &mut FontSystem, text: &str) {
@ -84,11 +85,19 @@ impl TextBox {
}
}
impl<'a, 'b: 'a> TextBox {
pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> TextArea<'a> {
let rect = self
.rect
.to_centered(window, input.ct.get_config().ui_scale);
impl<'a, 'b: 'a> UiTextBox {
pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> OwnedTextArea {
self.get_textarea_with_offset(input, window, Vector2::new(0.0, 0.0))
}
pub fn get_textarea_with_offset(
&'b self,
input: &RenderInput,
window: &Window,
offset: Vector2<f32>,
) -> OwnedTextArea {
let mut rect = self.rect.to_centered(window, input.ct.config.ui_scale);
rect.pos += offset;
// Glypon works with physical pixels, so we must do some conversion
let fac = window.scale_factor() as f32;
@ -99,11 +108,11 @@ impl<'a, 'b: 'a> TextBox {
let corner_sw = corner_ne + rect.dim * fac;
let c = self.color.as_array_u8();
TextArea {
buffer: &self.buffer,
OwnedTextArea {
buffer: self.buffer.clone(),
top: corner_ne.y,
left: corner_ne.x,
scale: input.ct.get_config().ui_scale,
scale: input.ct.config.ui_scale,
bounds: TextBounds {
top: (corner_ne.y) as i32,
bottom: (corner_sw.y) as i32,

View File

@ -1,8 +0,0 @@
#[derive(Debug, Copy, Clone)]
pub enum Event {
None,
MouseClick,
MouseRelease,
MouseHover,
MouseUnhover,
}

View File

@ -1,20 +1,17 @@
use anyhow::{Context, Result};
use galactica_content::Content;
use galactica_system::phys::PhysSimShipHandle;
use galactica_system::{phys::PhysSimShipHandle, PlayerDirective};
use galactica_util::rhai_error_to_anyhow;
use log::debug;
use rhai::{
packages::{BasicArrayPackage, BasicStringPackage, LogicPackage, MoreStringPackage, Package},
Dynamic, Engine, ImmutableString, Scope,
};
use log::{debug, error};
use rhai::{Dynamic, Engine, ImmutableString, Scope};
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use winit::event::VirtualKeyCode;
use super::{
api::{self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent},
event::Event,
api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent},
UiConfig, UiElement, UiState,
};
use crate::{ui::api::State, RenderInput, RenderState};
use crate::{ui::api::State, InputEvent, RenderInput, RenderState};
pub(crate) struct UiScriptExecutor {
engine: Engine,
@ -31,14 +28,8 @@ impl UiScriptExecutor {
let scope = Scope::new();
let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state)));
let mut engine = Engine::new_raw();
// Required for array iteration
// We may need to add more packages here later.
engine.register_global_module(BasicArrayPackage::new().as_shared_module());
engine.register_global_module(LogicPackage::new().as_shared_module());
engine.register_global_module(BasicStringPackage::new().as_shared_module());
engine.register_global_module(MoreStringPackage::new().as_shared_module());
// TODO: document all functions rhai provides
let mut engine = Engine::new();
engine.set_max_expr_depths(0, 0);
// Enables custom operators
@ -64,6 +55,129 @@ impl UiScriptExecutor {
(*self.state).borrow().config.clone()
}
pub fn process_input(
&mut self,
state: &mut RenderState,
input: Arc<RenderInput>,
event: InputEvent,
) -> Result<PlayerDirective> {
let current_scene = (*self.state).borrow().get_scene().clone();
if current_scene.is_none() {
return Ok(PlayerDirective::None);
}
let mut arg: Option<Dynamic> = None;
// First, check if this event is captured by any ui elements.
for (_, e) in &mut self.state.borrow_mut().elements {
arg = match e {
UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event),
UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event),
UiElement::RadialBar(_) | UiElement::Text(..) => None,
// Subelements are intentionally skipped,
// they should be handled by their parent's `handle_event` method.
UiElement::SubElement { .. } => None,
};
if arg.is_some() {
break;
}
}
// If nothing was caught, check global events
if arg.is_none() {
arg = match event {
InputEvent::Scroll(val) => Some(Dynamic::from(ScrollEvent { val })),
InputEvent::Keyboard { down, key } => {
let str = match key {
VirtualKeyCode::A => Some("A"),
VirtualKeyCode::B => Some("B"),
VirtualKeyCode::C => Some("C"),
VirtualKeyCode::D => Some("D"),
VirtualKeyCode::E => Some("E"),
VirtualKeyCode::F => Some("F"),
VirtualKeyCode::G => Some("G"),
VirtualKeyCode::H => Some("H"),
VirtualKeyCode::I => Some("I"),
VirtualKeyCode::J => Some("J"),
VirtualKeyCode::K => Some("K"),
VirtualKeyCode::L => Some("L"),
VirtualKeyCode::M => Some("M"),
VirtualKeyCode::N => Some("N"),
VirtualKeyCode::O => Some("O"),
VirtualKeyCode::P => Some("P"),
VirtualKeyCode::Q => Some("Q"),
VirtualKeyCode::R => Some("R"),
VirtualKeyCode::S => Some("S"),
VirtualKeyCode::T => Some("T"),
VirtualKeyCode::U => Some("U"),
VirtualKeyCode::V => Some("V"),
VirtualKeyCode::W => Some("W"),
VirtualKeyCode::X => Some("X"),
VirtualKeyCode::Y => Some("Y"),
VirtualKeyCode::Z => Some("Z"),
VirtualKeyCode::Up => Some("up"),
VirtualKeyCode::Down => Some("down"),
VirtualKeyCode::Left => Some("left"),
VirtualKeyCode::Right => Some("right"),
VirtualKeyCode::Space => Some("space"),
_ => None,
};
if let Some(str) = str {
Some(Dynamic::from(KeyboardEvent {
down,
key: ImmutableString::from(str),
}))
} else {
None
}
}
_ => None,
};
}
if let Some(arg) = arg {
self.run_event_callback(state, input, arg)
} else {
return Ok(PlayerDirective::None);
}
}
fn run_event_callback(
&mut self,
state: &mut RenderState,
input: Arc<RenderInput>,
arg: Dynamic,
) -> Result<PlayerDirective> {
let current_scene = (*self.state).borrow().get_scene().clone();
if current_scene.is_none() {
return Ok(PlayerDirective::None);
}
let current_scene = current_scene.unwrap();
let ct = (*self.state).borrow().ct.clone();
let d: Dynamic = rhai_error_to_anyhow(self.engine.call_fn(
&mut self.scope,
ct.config.ui_scenes.get(current_scene.as_str()).unwrap(),
"event",
(State::new(state, input.clone()), arg.clone()),
))
.with_context(|| format!("while handling event `{:?}`", arg))
.with_context(|| format!("in ui scene `{}`", current_scene))?;
if d.is::<PlayerDirective>() {
return Ok(d.cast());
} else if !(d.is_unit()) {
error!(
"`event()` in UI scene `{current_scene}` returned invalid type `{}`",
d
)
}
return Ok(PlayerDirective::None);
}
/// Change the current scene
pub fn set_scene(&mut self, state: &RenderState, input: Arc<RenderInput>) -> Result<()> {
let current_scene = (*self.state).borrow().get_scene().clone();
@ -87,12 +201,12 @@ impl UiScriptExecutor {
let mut elm = self.state.borrow_mut();
elm.clear();
drop(elm);
let ct = (*self.state).borrow().ct.clone();
let ct = (*self.state).borrow().ct.clone();
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
ct.config
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
@ -114,7 +228,7 @@ impl UiScriptExecutor {
if (*self.state).borrow().get_scene().is_none() {
(*self.state)
.borrow_mut()
.set_scene(ImmutableString::from(&ct.get_config().start_ui_scene));
.set_scene(ImmutableString::from(&ct.config.start_ui_scene));
}
self.set_scene(state, input.clone())?;
let current_scene = (*self.state).borrow().get_scene().clone();
@ -122,9 +236,8 @@ impl UiScriptExecutor {
(*self.state).borrow_mut().step(state, input.clone());
// Run step() (if it is defined)
let ast = ct
.get_config()
.config
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap();
@ -158,79 +271,25 @@ impl UiScriptExecutor {
true
}
} {
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
"event",
(State::new(state, input.clone()), PlayerShipStateEvent {}),
),
)
.with_context(|| format!("while handling player state change event"))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
self.run_event_callback(state, input.clone(), Dynamic::from(PlayerShipStateEvent {}))?;
}
let len = (*self.state).borrow().len();
for i in 0..len {
let event_arg = match (*self.state).borrow_mut().get_mut_by_idx(i).unwrap() {
match (*self.state).borrow_mut().get_mut_by_idx(i).unwrap() {
UiElement::Sprite(sprite) => {
// Draw and update sprites
sprite.step(&input, state);
sprite.push_to_buffer(&input, state);
let event = sprite.check_events(&input, state);
match event {
Event::None => None,
Event::MouseClick => Some(Dynamic::from(MouseClickEvent {
down: true,
element: sprite.name.clone(),
})),
Event::MouseRelease => Some(Dynamic::from(MouseClickEvent {
down: false,
element: sprite.name.clone(),
})),
Event::MouseHover => Some(Dynamic::from(MouseHoverEvent {
enter: true,
element: sprite.name.clone(),
})),
Event::MouseUnhover => Some(Dynamic::from(MouseHoverEvent {
enter: false,
element: sprite.name.clone(),
})),
}
}
UiElement::RadialBar(x) => {
// Draw and update radialbar
x.step(&input, state);
x.push_to_buffer(&input, state);
None
}
UiElement::Text(..) => None,
};
UiElement::Scrollbox(x) => {
x.push_to_buffer(&input, state);
}
if let Some(event_arg) = event_arg {
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
"event",
(State::new(state, input.clone()), event_arg.clone()),
),
)
.with_context(|| format!("while handling event `{:?}`", event_arg))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
UiElement::SubElement { .. } | UiElement::Text(..) => {}
}
}

View File

@ -1,9 +1,9 @@
mod api;
mod event;
mod camera;
mod elements;
mod executor;
mod state;
mod elements;
pub(crate) use camera::*;
pub(crate) use executor::UiScriptExecutor;
pub(crate) use state::*;

View File

@ -1,19 +1,39 @@
use galactica_content::Content;
use glyphon::TextArea;
use log::{debug, error};
use rhai::ImmutableString;
use std::collections::HashMap;
use std::sync::Arc;
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, time::Instant};
use winit::window::Window;
use super::elements::{FpsIndicator, RadialBar, Sprite, TextBox};
use super::{
elements::{FpsIndicator, OwnedTextArea, UiRadialBar, UiScrollbox, UiSprite, UiTextBox},
Camera,
};
use crate::{RenderInput, RenderState};
#[derive(Debug)]
pub enum UiElement {
Sprite(Sprite),
RadialBar(RadialBar),
Text(TextBox),
Sprite(UiSprite),
RadialBar(UiRadialBar),
Text(UiTextBox),
Scrollbox(UiScrollbox),
/// This is a sub-element managed by another element
SubElement {
parent: ImmutableString,
element: Rc<RefCell<UiElement>>,
},
}
impl UiElement {
pub fn get_name(&self) -> ImmutableString {
match self {
Self::Sprite(x) => x.name.clone(),
Self::RadialBar(x) => x.name.clone(),
Self::Text(x) => x.name.clone(),
Self::Scrollbox(x) => x.name.clone(),
Self::SubElement { element, .. } => element.borrow().get_name(),
}
}
}
#[derive(Clone, Debug)]
@ -31,8 +51,13 @@ pub(crate) struct UiState {
show_timings: bool,
fps_indicator: FpsIndicator,
last_step: Instant,
pub config: UiConfig,
/// The player's camera.
/// Only used when drawing physics.
pub camera: Camera,
}
// TODO: remove this
unsafe impl Send for UiState {}
@ -52,6 +77,8 @@ impl UiState {
show_phys: false,
show_starfield: false,
},
last_step: Instant::now(),
camera: Camera::new(),
}
}
@ -95,7 +122,7 @@ impl UiState {
}
pub fn set_scene(&mut self, scene: ImmutableString) {
if !self.ct.get_config().ui_scenes.contains_key(scene.as_str()) {
if !self.ct.config.ui_scenes.contains_key(scene.as_str()) {
error!("tried to switch to ui scene `{scene}`, which doesn't exist");
return;
}
@ -105,15 +132,57 @@ impl UiState {
}
pub fn step(&mut self, state: &mut RenderState, input: Arc<RenderInput>) {
let t = self.last_step.elapsed().as_secs_f32();
for (_, e) in &mut self.elements {
match e {
UiElement::Sprite(sprite) => sprite.step(t),
UiElement::Scrollbox(sbox) => sbox.step(t),
_ => {}
}
}
if self.show_timings {
self.fps_indicator
.step(&input, &mut state.text_font_system.borrow_mut());
}
self.last_step = Instant::now();
}
pub fn add_element(&mut self, e: UiElement) {
self.names.push(e.get_name().clone());
self.elements.insert(e.get_name().clone(), e);
}
// Remove an element from this sprite.
// This does NOT remove subelements from their parent sprites.
pub fn remove_element_incomplete(&mut self, name: &ImmutableString) -> Option<UiElement> {
let e = self.elements.remove(name);
self.names.retain(|x| *x != name);
return e;
}
// Remove an element from this sprite and from all subsprites.
pub fn remove_element(&mut self, name: &ImmutableString) {
let e = self.elements.remove(name);
self.names.retain(|x| *x != name);
match e {
Some(UiElement::SubElement { parent, element }) => {
let x = Rc::into_inner(element).unwrap().into_inner();
let parent = self.elements.get_mut(&parent).unwrap();
match parent {
UiElement::Scrollbox(s) => s.remove_element(&x.get_name()),
_ => unreachable!("invalid subelement parent"),
}
}
_ => {}
}
}
}
// TODO: don't allocate here, return an iterator
impl<'a> UiState {
pub fn get_textareas(&'a mut self, input: &RenderInput, window: &Window) -> Vec<TextArea<'a>> {
pub fn get_textareas(&'a self, input: &RenderInput, window: &Window) -> Vec<OwnedTextArea> {
let mut v = Vec::with_capacity(32);
if self.current_scene.is_none() {
@ -124,9 +193,10 @@ impl<'a> UiState {
v.push(self.fps_indicator.get_textarea(input, window))
}
for t in self.elements.values() {
match &t {
UiElement::Text(x) => v.push(x.get_textarea(input, window)),
for e in self.elements.values() {
match &e {
UiElement::Text(t) => v.push(t.get_textarea(input, window)),
UiElement::Scrollbox(b) => v.extend(b.get_textareas(input, window)),
_ => {}
}
}

View File

@ -25,3 +25,4 @@ rapier2d = { workspace = true }
nalgebra = { workspace = true }
crossbeam = { workspace = true }
rand = { workspace = true }
log = { workspace = true }

View File

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use galactica_content::{GunPoint, Outfit, OutfitHandle, OutfitSpace};
use galactica_content::{ContentIndex, GunPoint, Outfit, OutfitSpace};
/// Possible outcomes when adding an outfit
pub enum OutfitAddResult {
@ -34,7 +34,7 @@ pub enum OutfitRemoveResult {
/// A simple data class, used to keep track of delayed shield generators
#[derive(Debug, Clone)]
pub(crate) struct ShieldGenerator {
pub outfit: OutfitHandle,
pub outfit: Arc<Outfit>,
pub delay: f32,
pub generation: f32,
}
@ -46,7 +46,7 @@ pub(crate) struct ShieldGenerator {
#[derive(Debug, Clone)]
pub struct OutfitSet {
/// What outfits does this statsum contain?
outfits: HashMap<OutfitHandle, u32>,
outfits: HashMap<ContentIndex, (Arc<Outfit>, u32)>,
/// Space available in this outfitset.
/// set at creation and never changes.
@ -59,7 +59,7 @@ pub struct OutfitSet {
/// The gun points available in this ship.
/// If value is None, this point is free.
/// if value is Some, this point is taken.
gun_points: HashMap<GunPoint, Option<OutfitHandle>>,
gun_points: HashMap<GunPoint, Option<Arc<Outfit>>>,
/// Outfit values
/// This isn't strictly necessary, but we don't want to
@ -89,7 +89,7 @@ impl OutfitSet {
}
}
pub(super) fn add(&mut self, o: &Outfit) -> OutfitAddResult {
pub(super) fn add(&mut self, o: &Arc<Outfit>) -> OutfitAddResult {
if !(self.total_space - self.used_space).can_contain(&o.space) {
return OutfitAddResult::NotEnoughSpace("TODO".to_string());
}
@ -100,7 +100,7 @@ impl OutfitSet {
let mut added = false;
for (_, outfit) in &mut self.gun_points {
if outfit.is_none() {
*outfit = Some(o.handle);
*outfit = Some(o.clone());
added = true;
}
}
@ -115,29 +115,29 @@ impl OutfitSet {
self.steer_power += o.steer_power;
self.shield_strength += o.shield_strength;
self.shield_generators.push(ShieldGenerator {
outfit: o.handle,
outfit: o.clone(),
delay: o.shield_delay,
generation: o.shield_generation,
});
if self.outfits.contains_key(&o.handle) {
*self.outfits.get_mut(&o.handle).unwrap() += 1;
if self.outfits.contains_key(&o.index) {
self.outfits.get_mut(&o.index).unwrap().1 += 1;
} else {
self.outfits.insert(o.handle, 1);
self.outfits.insert(o.index.clone(), (o.clone(), 1));
}
return OutfitAddResult::Ok;
}
pub(super) fn remove(&mut self, o: &Outfit) -> OutfitRemoveResult {
if !self.outfits.contains_key(&o.handle) {
pub(super) fn remove(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
if !self.outfits.contains_key(&o.index) {
return OutfitRemoveResult::NotExist;
} else {
let n = *self.outfits.get(&o.handle).unwrap();
if n == 1u32 {
self.outfits.remove(&o.handle);
let n = self.outfits.get_mut(&o.index).unwrap();
if n.1 == 1u32 {
self.outfits.remove(&o.index);
} else {
*self.outfits.get_mut(&o.handle).unwrap() -= 1;
self.outfits.get_mut(&o.index).unwrap().1 -= 1;
}
}
@ -152,7 +152,7 @@ impl OutfitSet {
let index = self
.shield_generators
.iter()
.position(|g| g.outfit == o.handle)
.position(|g| g.outfit.index == o.index)
.unwrap();
self.shield_generators.remove(index);
}
@ -165,16 +165,16 @@ impl OutfitSet {
impl OutfitSet {
/// The number of outfits in this set
pub fn len(&self) -> u32 {
self.outfits.iter().map(|(_, x)| x).sum()
self.outfits.iter().map(|(_, (_, x))| x).sum()
}
/// Iterate over all outfits
pub fn iter_outfits(&self) -> impl Iterator<Item = (&OutfitHandle, &u32)> {
self.outfits.iter()
pub fn iter_outfits(&self) -> impl Iterator<Item = &(Arc<Outfit>, u32)> {
self.outfits.values()
}
/// Iterate over all gun points
pub fn iter_gun_points(&self) -> impl Iterator<Item = (&GunPoint, &Option<OutfitHandle>)> {
pub fn iter_gun_points(&self) -> impl Iterator<Item = (&GunPoint, &Option<Arc<Outfit>>)> {
self.gun_points.iter()
}
@ -190,7 +190,7 @@ impl OutfitSet {
/// Get the outfit attached to the given gun point
/// Will panic if this gunpoint is not in this outfit set.
pub fn get_gun(&self, point: &GunPoint) -> Option<OutfitHandle> {
pub fn get_gun(&self, point: &GunPoint) -> Option<Arc<Outfit>> {
self.gun_points.get(point).unwrap().clone()
}

View File

@ -1,7 +1,7 @@
use galactica_content::{Content, FactionHandle, GunPoint, Outfit, ShipHandle, SystemObjectHandle};
use galactica_content::{Faction, GunPoint, Outfit, Ship, SystemObject};
use nalgebra::Isometry2;
use rand::{rngs::ThreadRng, Rng};
use std::{collections::HashMap, time::Instant};
use std::{collections::HashMap, sync::Arc, time::Instant};
use super::{OutfitSet, ShipAutoPilot, ShipPersonality, ShipState};
@ -9,8 +9,8 @@ use super::{OutfitSet, ShipAutoPilot, ShipPersonality, ShipState};
#[derive(Debug, Clone)]
pub struct ShipData {
// Metadata values
ct_handle: ShipHandle,
faction: FactionHandle,
ship: Arc<Ship>,
faction: Arc<Faction>,
outfits: OutfitSet,
personality: ShipPersonality,
@ -34,16 +34,14 @@ pub struct ShipData {
impl ShipData {
/// Create a new ShipData
pub(crate) fn new(
ct: &Content,
ct_handle: ShipHandle,
faction: FactionHandle,
ship: Arc<Ship>,
faction: Arc<Faction>,
personality: ShipPersonality,
) -> Self {
let s = ct.get_ship(ct_handle);
ShipData {
ct_handle,
ship: ship.clone(),
faction,
outfits: OutfitSet::new(s.space, &s.guns),
outfits: OutfitSet::new(ship.space, &ship.guns),
personality,
last_hit: Instant::now(),
rng: rand::thread_rng(),
@ -54,9 +52,9 @@ impl ShipData {
},
// Initial stats
hull: s.hull,
hull: ship.hull,
shields: 0.0,
gun_cooldowns: s.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
gun_cooldowns: ship.guns.iter().map(|x| (x.clone(), 0.0)).collect(),
}
}
@ -80,11 +78,11 @@ impl ShipData {
/// That is the simulation's responsiblity.
///
/// Will panic if we're not flying.
pub fn start_land_on(&mut self, target_handle: SystemObjectHandle) {
pub fn start_land_on(&mut self, target: Arc<SystemObject>) {
match self.state {
ShipState::Flying { .. } => {
self.state = ShipState::Landing {
target: target_handle,
target,
current_z: 1.0,
};
}
@ -108,9 +106,11 @@ impl ShipData {
/// Finish landing sequence
/// Will panic if we're not landing
pub fn finish_land_on(&mut self) {
match self.state {
match &self.state {
ShipState::Landing { target, .. } => {
self.state = ShipState::Landed { target };
self.state = ShipState::Landed {
target: target.clone(),
};
}
_ => {
unreachable!("Called `finish_land_on` on a ship that isn't landing!")
@ -123,14 +123,13 @@ impl ShipData {
/// That is the simulation's responsiblity.
///
/// Will panic if we're not flying.
pub fn start_unland_to(&mut self, ct: &Content, to_position: Isometry2<f32>) {
match self.state {
pub fn start_unland_to(&mut self, to_position: Isometry2<f32>) {
match &self.state {
ShipState::Landed { target } => {
let obj = ct.get_system_object(target);
self.state = ShipState::UnLanding {
to_position,
from: target,
current_z: obj.pos.z,
from: target.clone(),
current_z: target.pos.z,
};
}
_ => {
@ -177,14 +176,14 @@ impl ShipData {
}
/// Add an outfit to this ship
pub fn add_outfit(&mut self, o: &Outfit) -> super::OutfitAddResult {
pub fn add_outfit(&mut self, o: &Arc<Outfit>) -> super::OutfitAddResult {
let r = self.outfits.add(o);
self.shields = self.outfits.get_total_shields();
return r;
}
/// Remove an outfit from this ship
pub fn remove_outfit(&mut self, o: &Outfit) -> super::OutfitRemoveResult {
pub fn remove_outfit(&mut self, o: &Arc<Outfit>) -> super::OutfitRemoveResult {
self.outfits.remove(o)
}
@ -192,16 +191,14 @@ impl ShipData {
/// Will panic if `which` isn't a point on this ship.
/// Returns `true` if this gun was fired,
/// and `false` if it is on cooldown or empty.
pub(crate) fn fire_gun(&mut self, ct: &Content, which: &GunPoint) -> bool {
pub(crate) fn fire_gun(&mut self, which: &GunPoint) -> bool {
let c = self.gun_cooldowns.get_mut(which).unwrap();
if *c > 0.0 {
return false;
}
let g = self.outfits.get_gun(which);
if g.is_some() {
let g = ct.get_outfit(g.unwrap());
if let Some(g) = self.outfits.get_gun(which) {
let gun = g.gun.as_ref().unwrap();
*c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng));
return true;
@ -288,13 +285,12 @@ impl ShipData {
&self.state
}
/// Get a handle to this ship's content
pub fn get_content(&self) -> ShipHandle {
self.ct_handle
/// Get this ship's content
pub fn get_content(&self) -> &Arc<Ship> {
&self.ship
}
/// Get this ship's current hull.
/// Use content handle to get maximum hull
/// Get this ship's current hull
pub fn get_hull(&self) -> f32 {
self.hull
}
@ -316,12 +312,7 @@ impl ShipData {
}
/// Get this ship's faction
pub fn get_faction(&self) -> FactionHandle {
self.faction
}
/// Get this ship's content handle
pub fn get_ship(&self) -> ShipHandle {
self.ct_handle
pub fn get_faction(&self) -> &Arc<Faction> {
&self.faction
}
}

View File

@ -1,7 +1,6 @@
use std::num::NonZeroU32;
use galactica_content::SystemObjectHandle;
use galactica_content::SystemObject;
use rapier2d::math::Isometry;
use std::{num::NonZeroU32, sync::Arc};
/// A ship autopilot.
/// An autopilot is a lightweight ShipController that
@ -14,7 +13,7 @@ pub enum ShipAutoPilot {
/// Automatically arrange for landing on the given object
Landing {
/// The body we want to land on
target: SystemObjectHandle,
target: Arc<SystemObject>,
},
}
@ -40,14 +39,14 @@ pub enum ShipState {
/// This ship is landed on a planet
Landed {
/// The planet this ship is landed on
target: SystemObjectHandle,
target: Arc<SystemObject>,
},
/// This ship is landing on a planet
/// (playing the animation)
Landing {
/// The planet we're landing on
target: SystemObjectHandle,
target: Arc<SystemObject>,
/// Our current z-coordinate
current_z: f32,
@ -60,7 +59,7 @@ pub enum ShipState {
to_position: Isometry<f32>,
/// The planet we're taking off from
from: SystemObjectHandle,
from: Arc<SystemObject>,
/// Our current z-coordinate
current_z: f32,
@ -69,9 +68,9 @@ pub enum ShipState {
impl ShipState {
/// What planet is this ship landed on?
pub fn landed_on(&self) -> Option<SystemObjectHandle> {
pub fn landed_on(&self) -> Option<Arc<SystemObject>> {
match self {
Self::Landed { target } => Some(*target),
Self::Landed { target } => Some(target.clone()),
_ => None,
}
}

View File

@ -6,3 +6,6 @@
pub mod data;
pub mod phys;
mod playerdirective;
pub use playerdirective::PlayerDirective;

View File

@ -1,7 +1,8 @@
use galactica_content::{Content, EffectHandle, EffectVelocity, SpriteAutomaton};
use galactica_content::{Effect, EffectVelocity, SpriteAutomaton};
use nalgebra::{Point2, Rotation2, Vector2};
use rand::Rng;
use rapier2d::dynamics::{RevoluteJointBuilder, RigidBodyBuilder, RigidBodyHandle, RigidBodyType};
use std::sync::Arc;
use crate::phys::{PhysStepResources, PhysWrapper};
@ -32,16 +33,13 @@ pub struct PhysEffect {
impl PhysEffect {
/// Create a new effect inside `Wrapper`
pub fn new(
ct: &Content,
wrapper: &mut PhysWrapper,
effect: EffectHandle,
effect: Arc<Effect>,
// Where to spawn the particle, in world space.
pos: Vector2<f32>,
parent: RigidBodyHandle,
target: Option<RigidBodyHandle>,
) -> Self {
let effect = ct.get_effect(effect);
let mut rng = rand::thread_rng();
let parent_body = wrapper.get_rigid_body(parent).unwrap();
let parent_angle = parent_body.rotation().angle();
@ -113,7 +111,7 @@ impl PhysEffect {
};
PhysEffect {
anim: SpriteAutomaton::new(ct, effect.sprite),
anim: SpriteAutomaton::new(effect.sprite.clone()),
rigid_body,
lifetime: 0f32.max(
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
@ -153,7 +151,7 @@ impl PhysEffect {
);
PhysEffect {
anim: SpriteAutomaton::new(ct, effect.sprite),
anim: SpriteAutomaton::new(effect.sprite.clone()),
rigid_body,
lifetime: 0f32.max(
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
@ -174,7 +172,7 @@ impl PhysEffect {
return;
}
self.anim.step(&res.ct, res.t);
self.anim.step(res.t);
self.lifetime -= res.t;
if self.lifetime <= 0.0 {

View File

@ -1,4 +1,6 @@
use galactica_content::{AnimationState, Content, FactionHandle, Projectile, SpriteAutomaton};
use std::sync::Arc;
use galactica_content::{AnimationState, Faction, Projectile, SpriteAutomaton};
use rand::Rng;
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
@ -10,7 +12,7 @@ use super::PhysEffect;
#[derive(Debug, Clone)]
pub struct PhysProjectile {
/// This projectile's game data
pub content: Projectile,
pub content: Arc<Projectile>,
/// This projectile's sprite animation state
anim: SpriteAutomaton,
@ -19,7 +21,7 @@ pub struct PhysProjectile {
lifetime: f32,
/// The faction this projectile belongs to
pub faction: FactionHandle,
pub faction: Arc<Faction>,
/// This projectile's rigidbody
pub rigid_body: RigidBodyHandle,
@ -38,17 +40,16 @@ pub struct PhysProjectile {
impl PhysProjectile {
/// Create a new projectile
pub fn new(
ct: &Content,
content: Projectile, // TODO: use a handle?
content: Arc<Projectile>,
rigid_body: RigidBodyHandle,
faction: FactionHandle,
faction: Arc<Faction>,
collider: ColliderHandle,
) -> Self {
let mut rng = rand::thread_rng();
let size_rng = content.size_rng;
let lifetime = content.lifetime;
PhysProjectile {
anim: SpriteAutomaton::new(ct, content.sprite),
anim: SpriteAutomaton::new(content.sprite.clone()),
rigid_body,
collider,
content,
@ -67,31 +68,24 @@ impl PhysProjectile {
wrapper: &mut PhysWrapper,
) {
self.lifetime -= res.t;
self.anim.step(&res.ct, res.t);
self.anim.step(res.t);
if self.lifetime <= 0.0 {
self.destroy(res, new, wrapper, true);
self.destroy(new, wrapper, true);
}
}
/// Destroy this projectile without creating an expire effect
pub(in crate::phys) fn destroy_silent(
&mut self,
res: &PhysStepResources,
new: &mut NewObjects,
wrapper: &mut PhysWrapper,
) {
self.destroy(res, new, wrapper, false);
self.destroy(new, wrapper, false);
}
/// Destroy this projectile
fn destroy(
&mut self,
res: &PhysStepResources,
new: &mut NewObjects,
wrapper: &mut PhysWrapper,
expire: bool,
) {
fn destroy(&mut self, new: &mut NewObjects, wrapper: &mut PhysWrapper, expire: bool) {
if self.is_destroyed {
return;
}
@ -100,11 +94,10 @@ impl PhysProjectile {
if expire {
match &self.content.expire_effect {
None => {}
Some(handle) => {
Some(effect) => {
new.effects.push(PhysEffect::new(
&res.ct,
wrapper,
*handle,
effect.clone(),
*rb.translation(),
self.rigid_body,
None,

View File

@ -1,4 +1,4 @@
use galactica_content::{CollapseEvent, Content, Ship};
use galactica_content::{CollapseEvent, Ship};
use nalgebra::{Point2, Vector2};
use rand::{rngs::ThreadRng, Rng};
use rapier2d::{
@ -37,14 +37,13 @@ impl ShipCollapseSequence {
}
/// Pick a random points inside a ship's collider
fn random_in_ship(&mut self, ct: &Content, ship: &Ship, collider: &Collider) -> Vector2<f32> {
fn random_in_ship(&mut self, 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 * ct.get_sprite(ship.sprite).aspect
/ 2.0;
y = self.rng.gen_range(-1.0..=1.0) * ship.size * ship.sprite.aspect / 2.0;
a = collider.shape().contains_local_point(&Point2::new(x, y));
}
Vector2::new(x, y)
@ -62,7 +61,7 @@ impl ShipCollapseSequence {
) {
let rigid_body = wrapper.get_rigid_body(rigid_body_handle).unwrap().clone();
let collider = wrapper.get_collider(collider_handle).unwrap().clone();
let ship_content = res.ct.get_ship(ship_data.get_content());
let ship_content = ship_data.get_content();
let ship_pos = rigid_body.translation();
let ship_rot = rigid_body.rotation();
@ -84,14 +83,13 @@ impl ShipCollapseSequence {
let pos: Vector2<f32> = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y)
} else {
self.random_in_ship(&res.ct, ship_content, &collider)
self.random_in_ship(&ship_content, &collider)
};
let pos = ship_pos + (ship_rot * pos);
new.effects.push(PhysEffect::new(
&res.ct,
wrapper,
spawner.effect,
spawner.effect.clone(),
pos,
rigid_body_handle,
None,
@ -124,15 +122,14 @@ impl ShipCollapseSequence {
let pos = if let Some(pos) = spawner.pos {
Vector2::new(pos.x, pos.y)
} else {
self.random_in_ship(&res.ct, ship_content, &collider)
self.random_in_ship(&ship_content, &collider)
};
// Position, adjusted for ship rotation
let pos = ship_pos + (ship_rot * pos);
new.effects.push(PhysEffect::new(
&res.ct,
wrapper,
spawner.effect,
spawner.effect.clone(),
pos,
rigid_body_handle,
None,

View File

@ -25,7 +25,7 @@ impl PointShipController {
impl ShipControllerStruct for PointShipController {
fn update_controls(
&mut self,
res: &PhysStepResources,
_res: &PhysStepResources,
img: &PhysImage,
this_ship: PhysSimShipHandle,
) -> Option<ShipControls> {
@ -40,14 +40,14 @@ impl ShipControllerStruct for PointShipController {
let my_position = this_rigidbody.translation();
let my_rotation = this_rigidbody.rotation();
let my_angvel = this_rigidbody.angvel();
let my_faction = res.ct.get_faction(my_ship.ship.data.get_faction());
let my_faction = my_ship.ship.data.get_faction();
// Iterate all possible targets
let mut hostile_ships = img.iter_ships().filter(
// Target only flying ships we're hostile towards
|s| match my_faction
.relationships
.get(&s.ship.data.get_faction())
.get(&s.ship.data.get_faction().index)
.unwrap()
{
Relationship::Hostile => match s.ship.data.get_state() {

View File

@ -1,6 +1,8 @@
use std::sync::Arc;
use galactica_content::{
AnimationState, Content, EnginePoint, FactionHandle, GunPoint, OutfitHandle,
ProjectileCollider, ShipHandle, SpriteAutomaton,
AnimationState, EnginePoint, Faction, GunPoint, Outfit, ProjectileCollider, Ship,
SpriteAutomaton,
};
use nalgebra::{vector, Point2, Rotation2, Vector2};
use rand::Rng;
@ -66,24 +68,22 @@ pub struct PhysShip {
impl PhysShip {
/// Make a new ship
pub(crate) fn new(
ct: &Content,
handle: ShipHandle,
ship: Arc<Ship>,
personality: ShipPersonality,
faction: FactionHandle,
faction: Arc<Faction>,
rigid_body: RigidBodyHandle,
collider: ColliderHandle,
) -> Self {
let ship_ct = ct.get_ship(handle);
PhysShip {
uid: get_phys_id(),
anim: SpriteAutomaton::new(ct, ship_ct.sprite),
anim: SpriteAutomaton::new(ship.sprite.clone()),
rigid_body,
collider,
data: ShipData::new(ct, handle, faction, personality),
data: ShipData::new(ship.clone(), faction, personality),
engine_anim: Vec::new(),
controls: ShipControls::new(),
last_controls: ShipControls::new(),
collapse_sequence: Some(ShipCollapseSequence::new(ship_ct.collapse.length)),
collapse_sequence: Some(ShipCollapseSequence::new(ship.collapse.length)),
is_destroyed: false,
controller: match personality {
ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(),
@ -105,32 +105,28 @@ impl PhysShip {
}
self.data.step(res.t);
self.anim.step(&res.ct, res.t);
self.anim.step(res.t);
for (_, e) in &mut self.engine_anim {
e.step(&res.ct, res.t);
e.step(res.t);
}
// Flare animations
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);
let flare = self.get_flare();
if let Some(flare) = flare {
if flare.engine_flare_on_stop.is_some() {
for (_, e) in &mut self.engine_anim {
e.jump_to(&res.ct, flare.engine_flare_on_stop.unwrap());
e.jump_to(flare.engine_flare_on_stop.as_ref().unwrap());
}
}
};
} 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);
let flare = self.get_flare();
if let Some(flare) = flare {
if flare.engine_flare_on_start.is_some() {
for (_, e) in &mut self.engine_anim {
e.jump_to(&res.ct, flare.engine_flare_on_start.unwrap());
e.jump_to(flare.engine_flare_on_start.as_ref().unwrap());
}
}
};
@ -162,12 +158,11 @@ impl PhysShip {
// Compute new controls
let controls = match autopilot {
ShipAutoPilot::Landing { target } => {
let target_obj = res.ct.get_system_object(*target);
let controls = autopilot::auto_landing(
res,
img,
PhysSimShipHandle(self.collider),
Vector2::new(target_obj.pos.x, target_obj.pos.y),
Vector2::new(target.pos.x, target.pos.y),
);
// Try to land the ship.
@ -176,7 +171,7 @@ impl PhysShip {
let landed = 'landed: {
let r = wrapper.get_rigid_body(self.rigid_body).unwrap();
let t_pos = Vector2::new(target_obj.pos.x, target_obj.pos.y);
let t_pos = Vector2::new(target.pos.x, target.pos.y);
let s_pos = Vector2::new(
r.position().translation.x,
r.position().translation.y,
@ -186,13 +181,13 @@ impl PhysShip {
// Can't just set_active(false), since we still need that collider's mass.
// We're in land range...
if (t_pos - s_pos).magnitude() > target_obj.size / 2.0 {
if (t_pos - s_pos).magnitude() > target.size / 2.0 {
break 'landed false;
}
// And we'll stay in land range long enough.
if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0))
.magnitude() > target_obj.size / 2.0
.magnitude() > target.size / 2.0
{
break 'landed false;
}
@ -202,7 +197,7 @@ impl PhysShip {
Group::GROUP_1,
Group::empty(),
));
self.data.start_land_on(*target);
self.data.start_land_on(target.clone());
break 'landed true;
};
@ -229,7 +224,7 @@ impl PhysShip {
if self.controls.guns {
// TODO: don't allocate here. This is a hack to satisfy the borrow checker,
// convert this to a refcell or do the replace dance.
let pairs: Vec<(GunPoint, Option<OutfitHandle>)> = self
let pairs: Vec<(GunPoint, Option<Arc<Outfit>>)> = self
.data
.get_outfits()
.iter_gun_points()
@ -237,7 +232,7 @@ impl PhysShip {
.collect();
for (gun_point, outfit) in pairs {
if self.data.fire_gun(&res.ct, &gun_point) {
if self.data.fire_gun(&gun_point) {
let outfit = outfit.unwrap();
let mut rng = rand::thread_rng();
@ -250,20 +245,17 @@ impl PhysShip {
rigid_body.velocity_at_point(rigid_body.center_of_mass());
let pos = ship_pos + (ship_rot * gun_point.pos);
let gun = outfit.gun.as_ref().unwrap();
let outfit = res.ct.get_outfit(outfit);
let outfit = outfit.gun.as_ref().unwrap();
let spread = rng.gen_range(
-outfit.projectile.angle_rng..=outfit.projectile.angle_rng,
);
let spread =
rng.gen_range(-gun.projectile.angle_rng..=gun.projectile.angle_rng);
let vel = ship_vel
+ (Rotation2::new(ship_ang + spread)
* Vector2::new(
outfit.projectile.speed
gun.projectile.speed
+ rng.gen_range(
-outfit.projectile.speed_rng
..=outfit.projectile.speed_rng,
-gun.projectile.speed_rng
..=gun.projectile.speed_rng,
),
0.0,
));
@ -274,7 +266,7 @@ impl PhysShip {
.linvel(vel)
.build();
let mut collider = match &outfit.projectile.collider {
let mut collider = match &gun.projectile.collider {
ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
.sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS)
@ -290,10 +282,9 @@ impl PhysShip {
let collider = wrapper.insert_collider(collider, rigid_body);
new.projectiles.push(PhysProjectile::new(
&res.ct,
outfit.projectile.clone(),
gun.projectile.clone(),
rigid_body,
self.data.get_faction(),
self.data.get_faction().clone(),
collider,
));
}
@ -306,8 +297,6 @@ impl PhysShip {
current_z,
from,
} => {
let from_obj = res.ct.get_system_object(*from);
let controls = autopilot::auto_landing(
&res,
img,
@ -315,7 +304,7 @@ impl PhysShip {
Vector2::new(to_position.translation.x, to_position.translation.y),
);
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap();
let max_d = (Vector2::new(from_obj.pos.x, from_obj.pos.y)
let max_d = (Vector2::new(from.pos.x, from.pos.y)
- Vector2::new(to_position.translation.x, to_position.translation.y))
.magnitude();
let now_d = (r.translation()
@ -324,7 +313,7 @@ impl PhysShip {
let f = now_d / max_d;
let current_z = *current_z;
let zdist = 1.0 - from_obj.pos.z;
let zdist = 1.0 - from.pos.z;
if current_z <= 1.0 {
// Finish unlanding ship
@ -351,18 +340,17 @@ impl PhysShip {
}
ShipState::Landing { target, current_z } => {
let target_obj = res.ct.get_system_object(*target);
let controls = autopilot::auto_landing(
&res,
img,
PhysSimShipHandle(self.collider),
Vector2::new(target_obj.pos.x, target_obj.pos.y),
Vector2::new(target.pos.x, target.pos.y),
);
let current_z = *current_z;
let zdist = target_obj.pos.z - 1.0;
let zdist = target.pos.z - 1.0;
if current_z >= target_obj.pos.z {
if current_z >= target.pos.z {
// Finish landing ship
self.data.finish_land_on();
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap();
@ -432,7 +420,7 @@ impl PhysShip {
let rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap().clone();
let collider = wrapper.get_collider(self.collider).unwrap().clone();
let ship_content = res.ct.get_ship(self.data.get_content());
let ship_content = self.data.get_content();
let ship_pos = rigid_body.translation();
let ship_rot = rigid_body.rotation();
let ship_ang = ship_rot.angle();
@ -451,7 +439,7 @@ impl PhysShip {
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 * res.ct.get_sprite(ship_content.sprite).aspect
* ship_content.size * ship_content.sprite.aspect
/ 2.0;
a = collider.shape().contains_local_point(&Point2::new(x, y));
}
@ -461,9 +449,8 @@ impl PhysShip {
let pos = ship_pos + (Rotation2::new(ship_ang) * pos);
new.effects.push(PhysEffect::new(
&res.ct,
wrapper,
e.effect,
e.effect.clone(),
pos,
self.rigid_body,
None,
@ -476,12 +463,11 @@ impl PhysShip {
/// Public mutable
impl PhysShip {
fn get_flare(&mut self, ct: &Content) -> Option<OutfitHandle> {
fn get_flare(&mut self) -> Option<Arc<Outfit>> {
// TODO: better way to pick flare sprite
for (h, _) in self.data.get_outfits().iter_outfits() {
let c = ct.get_outfit(*h);
if c.engine_flare_sprite.is_some() {
return Some(*h);
if h.engine_flare_sprite.is_some() {
return Some(h.clone());
}
}
return None;
@ -489,37 +475,47 @@ impl PhysShip {
/// 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() {
fn update_flares(&mut self) {
let flare = self.get_flare();
if flare.is_none() {
self.engine_anim = Vec::new();
return;
}
let flare = ct
.get_outfit(flare_outfit.unwrap())
.engine_flare_sprite
.unwrap();
self.engine_anim = ct
.get_ship(self.data.get_content())
self.engine_anim = self
.data
.get_content()
.engines
.iter()
.map(|e| (e.clone(), SpriteAutomaton::new(ct, flare)))
.map(|e| {
(
e.clone(),
SpriteAutomaton::new(
flare
.as_ref()
.unwrap()
.engine_flare_sprite
.as_ref()
.unwrap()
.clone(),
),
)
})
.collect();
}
/// Add one outfit to this ship
pub fn add_outfit(&mut self, ct: &Content, o: OutfitHandle) {
self.data.add_outfit(ct.get_outfit(o));
self.update_flares(ct);
pub fn add_outfit(&mut self, o: Arc<Outfit>) {
self.data.add_outfit(&o);
self.update_flares();
}
/// Add many outfits to this ship
pub fn add_outfits(&mut self, ct: &Content, outfits: impl IntoIterator<Item = OutfitHandle>) {
pub fn add_outfits(&mut self, outfits: impl IntoIterator<Item = Arc<Outfit>>) {
for o in outfits {
self.data.add_outfit(ct.get_outfit(o));
self.data.add_outfit(&o);
}
self.update_flares(ct);
self.update_flares();
}
}

View File

@ -1,6 +1,8 @@
use galactica_content::Faction;
use galactica_content::Relationship;
use galactica_content::{Content, FactionHandle, ShipHandle, SystemHandle};
use galactica_content::Ship;
use galactica_playeragent::PlayerAgent;
use log::error;
use nalgebra::{Isometry2, Point2, Rotation2, Vector2};
use rand::Rng;
use rapier2d::{
@ -8,6 +10,7 @@ use rapier2d::{
geometry::{ColliderHandle, Group, InteractionGroups},
};
use std::collections::HashMap;
use std::sync::Arc;
use super::PhysEffectImage;
use super::{
@ -15,6 +18,7 @@ use super::{
PhysImage, PhysProjectileImage, PhysShipImage, PhysStepResources, PhysWrapper,
};
use crate::data::{ShipAutoPilot, ShipPersonality, ShipState};
use crate::PlayerDirective;
// TODO: replace with a more generic handle
/// A handle for a ship in this simulation
@ -49,13 +53,8 @@ impl NewObjects {
/// Manages the physics state of one system
pub struct PhysSim {
/// The system this sim is attached to
_system: SystemHandle,
wrapper: PhysWrapper,
new: NewObjects,
effects: Vec<PhysEffect>,
projectiles: HashMap<ColliderHandle, PhysProjectile>,
ships: HashMap<ColliderHandle, PhysShip>,
@ -63,10 +62,9 @@ pub struct PhysSim {
// Private methods
impl PhysSim {
pub(super) fn start_unland_ship(&mut self, ct: &Content, collider: ColliderHandle) {
pub(super) fn start_unland_ship(&mut self, collider: ColliderHandle) {
let ship = self.ships.get_mut(&collider).unwrap();
let obj = ship.data.get_state().landed_on().unwrap();
let obj = ct.get_system_object(obj);
let mut rng = rand::thread_rng();
let radius = rng.gen_range(500.0..=1500.0);
@ -75,7 +73,7 @@ impl PhysSim {
let target_trans = Vector2::new(obj.pos.x, obj.pos.y) + target_offset;
let target_pos = Isometry2::new(target_trans, angle);
ship.data.start_unland_to(ct, target_pos);
ship.data.start_unland_to(target_pos);
let r = self.wrapper.get_rigid_body_mut(ship.rigid_body).unwrap();
r.set_enabled(true);
@ -87,7 +85,6 @@ impl PhysSim {
pub(super) fn collide_projectile_ship(
&mut self,
res: &mut PhysStepResources,
projectile_h: ColliderHandle,
ship_h: ColliderHandle,
) {
@ -99,8 +96,11 @@ impl PhysSim {
let projectile = projectile.unwrap();
let ship = ship.unwrap();
let f = res.ct.get_faction(projectile.faction);
let r = f.relationships.get(&ship.data.get_faction()).unwrap();
let r = projectile
.faction
.relationships
.get(&ship.data.get_faction().index)
.unwrap();
let destory_projectile = match r {
Relationship::Hostile => match ship.data.get_state() {
ShipState::Flying { .. } => {
@ -131,9 +131,8 @@ impl PhysSim {
None => {}
Some(x) => {
self.effects.push(PhysEffect::new(
&res.ct,
&mut self.wrapper,
*x,
x.clone(),
pos,
projectile.rigid_body,
Some(ship.rigid_body),
@ -141,7 +140,7 @@ impl PhysSim {
}
};
projectile.destroy_silent(res, &mut self.new, &mut self.wrapper);
projectile.destroy_silent(&mut self.new, &mut self.wrapper);
}
}
}
@ -149,11 +148,9 @@ impl PhysSim {
// Public methods
impl PhysSim {
/// Create a new physics system
pub fn new(_ct: &Content, system: SystemHandle) -> Self {
pub fn new() -> Self {
Self {
_system: system,
wrapper: PhysWrapper::new(),
new: NewObjects::new(),
effects: Vec::new(),
projectiles: HashMap::new(),
@ -164,19 +161,17 @@ impl PhysSim {
/// Add a ship to this physics system
pub fn add_ship(
&mut self,
ct: &Content,
handle: ShipHandle,
faction: FactionHandle,
ship: Arc<Ship>,
faction: Arc<Faction>,
personality: ShipPersonality,
position: Point2<f32>,
) -> PhysSimShipHandle {
let ship_content = ct.get_ship(handle);
let mut cl = ship_content.collider.0.clone();
let mut cl = ship.collider.0.clone();
// TODO: additonal ship mass from outfits and cargo
let rb = RigidBodyBuilder::dynamic()
.angular_damping(ship_content.angular_drag)
.linear_damping(ship_content.linear_drag)
.angular_damping(ship.angular_drag)
.linear_damping(ship.linear_drag)
.translation(Vector2::new(position.x, position.y))
.can_sleep(false);
@ -190,14 +185,14 @@ impl PhysSim {
self.ships.insert(
collider,
PhysShip::new(ct, handle, personality, faction, ridid_body, collider),
PhysShip::new(ship, personality, faction, ridid_body, collider),
);
return PhysSimShipHandle(collider);
}
/// Update a player ship's controls
pub fn update_player_controls(&mut self, ct: &Content, player: &PlayerAgent) {
pub fn apply_directive(&mut self, directive: PlayerDirective, player: &PlayerAgent) {
if player.ship.is_none() {
return;
}
@ -211,35 +206,49 @@ impl PhysSim {
ShipState::Flying {
autopilot: ShipAutoPilot::None,
} => {
ship_object.controls.guns = player.input.pressed_guns();
ship_object.controls.left = player.input.pressed_left();
ship_object.controls.right = player.input.pressed_right();
ship_object.controls.thrust = player.input.pressed_thrust();
if player.input.pressed_land() {
ship_object.data.set_autopilot(ShipAutoPilot::Landing {
target: player.selection.get_planet().unwrap(),
})
} => match directive {
PlayerDirective::None => {}
PlayerDirective::Engine(x) => ship_object.controls.thrust = x,
PlayerDirective::TurnLeft(x) => ship_object.controls.left = x,
PlayerDirective::TurnRight(x) => ship_object.controls.right = x,
PlayerDirective::Guns(x) => ship_object.controls.guns = x,
PlayerDirective::Land => {
if let Some(target) = player.selection.get_planet() {
ship_object.data.set_autopilot(ShipAutoPilot::Landing {
target: target.clone(),
})
}
}
}
_ => {
error!("Got an invalid directive {directive:?} in shipstate `Flying`");
}
},
ShipState::Flying { .. } => {
// Any input automatically releases autopilot
if player.input.pressed_left()
|| player.input.pressed_right()
|| player.input.pressed_thrust()
|| player.input.pressed_guns()
{
ShipState::Flying { .. } => match directive {
PlayerDirective::None => {}
PlayerDirective::Engine(_)
| PlayerDirective::TurnLeft(_)
| PlayerDirective::TurnRight(_)
| PlayerDirective::Land
| PlayerDirective::Guns(_) => {
// Disable autopilot and apply action
ship_object.data.set_autopilot(ShipAutoPilot::None);
self.apply_directive(directive, player);
}
}
_ => {
error!("Got an invalid directive {directive:?} in shipstate `Flying`");
}
},
ShipState::Landed { .. } => {
if player.input.pressed_land() {
self.start_unland_ship(ct, player.ship.unwrap());
ShipState::Landed { .. } => match directive {
PlayerDirective::None => {}
PlayerDirective::UnLand => {
self.start_unland_ship(player.ship.unwrap());
}
}
_ => {
error!("Got an invalid directive {directive:?} in shipstate `Landed`");
}
},
};
}
}
@ -271,7 +280,7 @@ impl PhysSim {
if p.is_none() || s.is_none() {
continue;
}
self.collide_projectile_ship(&mut res, a, b);
self.collide_projectile_ship(a, b);
}
}
res.timing.mark_physics_step();

View File

@ -0,0 +1,24 @@
/// An action the player wants to take in the game.
#[derive(Debug, Clone)]
pub enum PlayerDirective {
/// Do nothing
None,
/// Set main engine state
Engine(bool),
/// Set left turn thruster state
TurnLeft(bool),
/// Set right turn thruster state
TurnRight(bool),
/// Set main gun state
Guns(bool),
/// Land on the currently selected planet
Land,
/// Take off from the planet we're landed on
UnLand,
}