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", "rapier2d",
"rhai", "rhai",
"serde", "serde",
"smartstring",
"toml", "toml",
"walkdir", "walkdir",
] ]
@ -876,6 +877,7 @@ dependencies = [
"galactica-content", "galactica-content",
"galactica-playeragent", "galactica-playeragent",
"galactica-util", "galactica-util",
"log",
"nalgebra", "nalgebra",
"rand", "rand",
"rapier2d", "rapier2d",

View File

@ -75,3 +75,4 @@ clap = { version = "4.4.18", features = ["derive"] }
log = "0.4.20" log = "0.4.20"
log4rs = { version = "1.2.0", features = ["console_appender"] } log4rs = { version = "1.2.0", features = ["console_appender"] }
rhai = { version = "1.17.1", features = ["f32_float", "no_custom_syntax"] } 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 # Specific projects
## Now: ## Now:
- Replace handles with Arcs in content
- clean up content - clean up content
- Clean up state api - Clean up state api
- Persistent variables - Clean up & document UI api
- Persistent variables in ui scripts
- Better planet icons - Better planet icons
- Clean up scripting errors - Clean up scripting errors
- Mouse colliders - Mouse colliders
- UI captures input? - Fade sprites and text in scrollbox
- No UI zoom scroll - Selection while flying
- outfitter - outfitter
- fps textbox positioning - fps textbox positioning
@ -24,12 +24,10 @@
- No wobble for ai ships & autopilot - No wobble for ai ships & autopilot
- 🌟 User-configurable outfit space types - 🌟 User-configurable outfit space types
- 🌟 Sticky radar - 🌟 Sticky radar
- Configurable radar
- 🌟 Ship damage events - 🌟 Ship damage events
- Better landing animation (slow down) - Better landing animation (slow down)
- Land from farther away - Land from farther away
- Ship collapse: damage + force events - Ship collapse: damage + force events
- Redesign UI elements
- Background haze: 3d perlin? - Background haze: 3d perlin?
- nova dust parallax - nova dust parallax
- Motion blur - Motion blur
@ -38,13 +36,12 @@
- Reverse engines + flares - Reverse engines + flares
- Turn flares (physics by location?) - Turn flares (physics by location?)
- Angled engines & guns - Angled engines & guns
- Unified content dir
## Misc fixes & Optimizations ## Misc fixes & Optimizations
- 🌟 Better errors when content/asset dirs don't exist - 🌟 Better errors when content/asset dirs don't exist
- Clear `// TODO:` comments - Clear `// TODO:` comments
- Correct drawing order (player on top, landing ships) - Correct drawing order (player on top, landing ships)
- Faster handles (better than a hashmap?)
- Check for handle leaks
- Better physshiphandle - Better physshiphandle
- Clean up & faster frame timings (average) - Clean up & faster frame timings (average)
- 🌟 Handle lost focus - 🌟 Handle lost focus
@ -150,6 +147,7 @@
- Muzzle effect - Muzzle effect
- Effect / sprite color variation - Effect / sprite color variation
- UI Animations - UI Animations
- in-game console?
## Game & Story ## Game & Story
@ -183,7 +181,6 @@
- Outfit pipeline - Outfit pipeline
- Collision detection - Collision detection
- Ship AI - Ship AI
- Handles
- Content specification and pipeline - Content specification and pipeline
- How packer and optimizations work, and why - How packer and optimizations work, and why
- How big should sprite resolutions be? How about frame rate? - 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"] [outfit."plasma engines"]
name = "Plasma Engines"
thumbnail = "icon::engine" thumbnail = "icon::engine"
space.engine = 20 space.engine = 20
@ -11,6 +12,7 @@ steering.power = 20
[outfit."shield generator"] [outfit."shield generator"]
thumbnail = "icon::shield" thumbnail = "icon::shield"
name = "Shield Generator"
space.outfit = 5 space.outfit = 5
shield.generation = 10 shield.generation = 10
@ -19,7 +21,8 @@ shield.delay = 2.0
[outfit."blaster"] [outfit."blaster"]
thumbnail = "icon::shield" thumbnail = "icon::blaster"
name = "Blaster"
space.weapon = 10 space.weapon = 10

View File

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

View File

@ -1,7 +1,8 @@
# TODO: big objects in one config # TODO: big objects in one config
# TODO: satisfy conditions to land
[system."12 Autumn Above"] [system."12 Autumn Above"]
name = "12 Autumn Above"
object.star.sprite = "star::star" object.star.sprite = "star::star"
object.star.position = [0.0, 0.0, 30.0] object.star.position = [0.0, 0.0, 30.0]
object.star.size = 2000 object.star.size = 2000
@ -13,10 +14,9 @@ object.earth.position.angle = 0
object.earth.position.z = 10.0 object.earth.position.z = 10.0
object.earth.size = 1000 object.earth.size = 1000
# TODO: satisfy conditions to land
object.earth.landable = true object.earth.landable.name = "Earth"
object.earth.name = "Earth" object.earth.landable.desc = """
object.earth.desc = """
The ancestral home world of humanity, Earth has a population twice that of any other inhabited planet. 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. 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 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 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. 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.sprite = "planet::luna"
object.luna.position.center = "earth" object.luna.position.center = "earth"

View File

@ -82,6 +82,31 @@ fn event(state, event) {
} }
return; 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) { fn step(state) {
@ -131,7 +156,7 @@ fn step(state) {
// Ships // Ships
{ {
for s in state.ships() { for s in state.ships() {
let uid = s.get_uid(); let uid = s.phys_uid();
let sprite_name = `radar.ship.${uid}`; let sprite_name = `radar.ship.${uid}`;
if ( if (
@ -200,7 +225,8 @@ fn step(state) {
// System objects // System objects
{ {
for o in state.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 !o.is_some() {
if sprite::exists(sprite_name) { if sprite::exists(sprite_name) {

View File

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

View File

@ -84,7 +84,7 @@ fn init(state) {
textbox::font_sans("ship_type"); textbox::font_sans("ship_type");
textbox::align_center("ship_type"); textbox::align_center("ship_type");
if state.player_ship().is_some() { 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::font_mono("outfit_stats");
textbox::set_text("outfit_stats", "Earth"); 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(); let element = event.element();
if element == "exit_button" { if element == "exit_button" {
if event.is_enter() { if event.is_enter() {
sprite::take_edge("exit_button", "on:top", 0.1); sprite::jump_to("exit_button", "on:top", 0.1);
} else { } else {
sprite::take_edge("exit_button", "off:top", 0.1); sprite::jump_to("exit_button", "off:top", 0.1);
} }
} }
return; return;

View File

@ -30,3 +30,4 @@ rapier2d = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }
log = { workspace = true } log = { workspace = true }
rhai = { 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)] #![warn(missing_docs)]
//! This subcrate is responsible for loading, parsing, validating game content, //! This subcrate is responsible for loading, parsing, validating game content.
//! which is usually stored in `./content`.
mod handle;
mod part; mod part;
mod spriteautomaton; mod spriteautomaton;
mod util; mod util;
@ -11,18 +8,21 @@ mod util;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use log::warn; use log::warn;
use rhai::ImmutableString;
use serde::{Deserialize, Deserializer};
use smartstring::{LazyCompact, SmartString};
use std::{ use std::{
//cell::OnceCell,
collections::HashMap, collections::HashMap,
fmt::Display,
fs::File, fs::File,
io::Read, io::Read,
num::NonZeroU32, num::NonZeroU32,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use toml; use toml;
use walkdir::WalkDir; use walkdir::WalkDir;
pub use handle::*;
pub use part::*; pub use part::*;
pub use spriteautomaton::*; pub use spriteautomaton::*;
@ -123,11 +123,49 @@ trait Build {
Self: Sized; 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 /// Stores temporary data while building context objects
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ContentBuildContext { pub(crate) struct ContentBuildContext {
/// Map effect names to handles pub effect_index: HashMap<ContentIndex, Arc<Effect>>,
pub effect_index: HashMap<String, EffectHandle>,
} }
impl ContentBuildContext { impl ContentBuildContext {
@ -141,22 +179,29 @@ impl ContentBuildContext {
/// Represents static game content /// Represents static game content
#[derive(Debug)] #[derive(Debug)]
pub struct Content { 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 /// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas, sprite_atlas: SpriteAtlas,
outfits: Vec<Outfit>, /// Sprites defined in this game
guns: Vec<Gun>, // TODO: merge with outfit pub sprites: HashMap<ContentIndex, Arc<Sprite>>,
ships: Vec<Ship>,
systems: Vec<System>, /// Outfits defined in this game
factions: Vec<Faction>, pub outfits: HashMap<ContentIndex, Arc<Outfit>>,
effects: Vec<Effect>,
config: Config, /// 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 // Loading methods
@ -224,14 +269,12 @@ impl Content {
}, },
sprite_atlas: atlas, sprite_atlas: atlas,
systems: Vec::new(), systems: HashMap::new(),
ships: Vec::new(), ships: HashMap::new(),
guns: Vec::new(), outfits: HashMap::new(),
outfits: Vec::new(), sprites: HashMap::new(),
sprites: Vec::new(), factions: HashMap::new(),
factions: Vec::new(), effects: HashMap::new(),
effects: Vec::new(),
sprite_index: HashMap::new(),
}; };
// TODO: enforce sprite and image limits // TODO: enforce sprite and image limits
@ -285,23 +328,6 @@ impl Content {
// Access methods // Access methods
impl Content { 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 /// Get the list of atlas files we may use
pub fn atlas_files(&self) -> &Vec<String> { pub fn atlas_files(&self) -> &Vec<String> {
return &self.sprite_atlas.atlas_list; return &self.sprite_atlas.atlas_list;
@ -316,63 +342,4 @@ impl Content {
pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage { pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
&self.sprite_atlas.get_by_idx(idx) &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 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 { pub(crate) mod syntax {
use std::sync::Arc;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use galactica_util::to_radians; use galactica_util::to_radians;
use serde::Deserialize; use serde::Deserialize;
use crate::{Content, ContentBuildContext, EffectHandle, StartEdge}; use crate::{Content, ContentBuildContext, ContentIndex, StartEdge};
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Effect { pub struct Effect {
pub sprite: String, pub sprite: ContentIndex,
pub size: f32, pub size: f32,
pub size_rng: Option<f32>, pub size_rng: Option<f32>,
pub lifetime: TextOrFloat, pub lifetime: TextOrFloat,
@ -59,10 +61,11 @@ pub(crate) mod syntax {
self, self,
_build_context: &mut ContentBuildContext, _build_context: &mut ContentBuildContext,
content: &mut Content, content: &mut Content,
) -> Result<EffectHandle> { name: &str,
let sprite = match content.sprite_index.get(&self.sprite) { ) -> Result<Arc<super::Effect>> {
let sprite = match content.sprites.get(&self.sprite) {
None => bail!("sprite `{}` doesn't exist", self.sprite), None => bail!("sprite `{}` doesn't exist", self.sprite),
Some(t) => *t, Some(t) => t.clone(),
}; };
let lifetime = match self.lifetime { let lifetime = match self.lifetime {
@ -70,12 +73,11 @@ pub(crate) mod syntax {
TextOrFloat::Text(s) => { TextOrFloat::Text(s) => {
if s == "inherit" { if s == "inherit" {
// Match lifetime of first section of sprite // Match lifetime of first section of sprite
let sprite = content.get_sprite(sprite); match &sprite.start_at {
let sec = match sprite.start_at { StartEdge::Top { section } | StartEdge::Bot { section } => {
StartEdge::Top { section } => sprite.get_section(section), section.frame_duration * section.frames.len() as f32
StartEdge::Bot { section } => sprite.get_section(section), }
}; }
sec.frame_duration * sec.frames.len() as f32
} else { } else {
bail!("bad effect lifetime, must be float or \"inherit\"",) bail!("bad effect lifetime, must be float or \"inherit\"",)
} }
@ -107,11 +109,8 @@ pub(crate) mod syntax {
} }
}; };
let handle = EffectHandle { let e = Arc::new(super::Effect {
index: content.effects.len(), name: name.to_string(),
};
content.effects.push(super::Effect {
handle,
sprite, sprite,
velocity, velocity,
size: self.size, size: self.size,
@ -126,7 +125,11 @@ pub(crate) mod syntax {
fade_rng: self.fade_rng.unwrap_or(0.0), 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)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum EffectReference { pub enum EffectReference {
Label(String), Label(ContentIndex),
Effect(Effect), Effect(Effect),
} }
impl EffectReference { impl EffectReference {
pub fn to_handle( pub fn resolve(
self, self,
build_context: &mut ContentBuildContext, build_context: &mut ContentBuildContext,
content: &mut Content, content: &mut Content,
) -> Result<EffectHandle> { name: &str,
) -> Result<Arc<super::Effect>> {
// We do not insert anything into build_context here, // We do not insert anything into build_context here,
// since inline effects cannot be referenced by name. // since inline effects cannot be referenced by name.
Ok(match self { 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) { Self::Label(l) => match build_context.effect_index.get(&l) {
Some(h) => *h, Some(h) => h.clone(),
None => bail!("no effect named `{}`", l), None => bail!("no effect named `{}`", l),
}, },
}) })
@ -161,11 +165,11 @@ pub(crate) mod syntax {
/// The effect a projectile will spawn when it hits something /// The effect a projectile will spawn when it hits something
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Effect { pub struct Effect {
/// This effect's handle
pub handle: EffectHandle,
/// The sprite to use for this effect. /// 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. /// The height of this effect, in game units.
pub size: f32, pub size: f32,
@ -240,9 +244,11 @@ impl crate::Build for Effect {
) -> Result<()> { ) -> Result<()> {
for (effect_name, effect) in effects { for (effect_name, effect) in effects {
let h = effect let h = effect
.add_to(build_context, content) .add_to(build_context, content, &effect_name)
.with_context(|| format!("while evaluating effect `{}`", 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(()); return Ok(());

View File

@ -1,13 +1,15 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use serde::Deserialize; 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 { pub(crate) mod syntax {
use std::collections::HashMap; use std::collections::HashMap;
use serde::Deserialize; use serde::Deserialize;
use crate::ContentIndex;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -15,7 +17,7 @@ pub(crate) mod syntax {
pub struct Faction { pub struct Faction {
pub display_name: String, pub display_name: String,
pub color: [f32; 3], 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)] #[derive(Debug, Clone)]
pub struct Faction { pub struct Faction {
/// The name of this 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. /// This faction's color.
/// Format is RGB, with each color between 0 and 1. /// Format is RGB, with each color between 0 and 1.
pub color: [f32; 3], pub color: [f32; 3],
/// This faction's handle
pub handle: FactionHandle,
/// Relationships between this faction and other factions /// Relationships between this faction and other factions
/// This is guaranteed to contain an entry for ALL 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 { impl crate::Build for Faction {
@ -66,34 +68,21 @@ impl crate::Build for Faction {
_build_context: &mut ContentBuildContext, _build_context: &mut ContentBuildContext,
content: &mut Content, content: &mut Content,
) -> Result<()> { ) -> Result<()> {
// Keeps track of position in faction array. for (faction_name, faction) in &factions {
// 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 };
// Compute relationships // Compute relationships
let mut relationships = HashMap::new(); let mut relationships = HashMap::new();
for i in 0..faction_names.len() { for (other_name, other_faction) in &factions {
let f_other = &faction_names[i]; if let Some(r) = faction.relationship.get(&ContentIndex::new(other_name)) {
let h_other = FactionHandle { index: i }; relationships.insert(ContentIndex::new(other_name), *r);
if let Some(r) = faction.relationship.get(f_other) {
relationships.insert(h_other, *r);
} else { } else {
// Default relationship, if not specified // Default relationship, if not specified
// Look at reverse direction... // 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( relationships.insert(
h_other, ContentIndex::new(other_name),
// ... and pick a relationship based on that. // ... and pick a relationship based on that.
match other { match other {
Some(Relationship::Hostile) => Relationship::Hostile {}, Some(Relationship::Hostile) => Relationship::Hostile {},
@ -116,12 +105,15 @@ impl crate::Build for Faction {
); );
} }
content.factions.push(Self { content.factions.insert(
name: faction_name.to_owned(), ContentIndex::new(faction_name),
handle: h, Arc::new(Self {
index: ContentIndex::new(faction_name),
display_name: faction.display_name.to_owned(),
relationships, relationships,
color: faction.color, color: faction.color,
}); }),
);
} }
return Ok(()); return Ok(());

View File

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

View File

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

View File

@ -1,14 +1,14 @@
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use lazy_static::lazy_static; 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 { pub(crate) mod syntax {
use crate::{AnimSectionHandle, Content}; use crate::{Content, ContentIndex};
use anyhow::{bail, Ok, Result}; use anyhow::{bail, Ok, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf, sync::Arc};
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
@ -49,7 +49,7 @@ pub(crate) mod syntax {
/// The proper, full sprite definition /// The proper, full sprite definition
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CompleteSprite { pub struct CompleteSprite {
pub section: HashMap<String, SpriteSection>, pub section: HashMap<ContentIndex, SpriteSection>,
pub start_at: SectionEdge, pub start_at: SectionEdge,
} }
@ -63,14 +63,28 @@ pub(crate) mod syntax {
} }
impl SpriteSection { impl SpriteSection {
pub fn add_to<F>( pub fn resolve_edges(
&self, &self,
ct: &mut Content, sections: &HashMap<ContentIndex, Arc<super::SpriteSection>>,
get_handle: F, sec: &mut super::SpriteSection,
) -> Result<((u32, u32), super::SpriteSection)> ) -> Result<()> {
where let edge_top = match &self.top {
F: Fn(&str) -> Option<AnimSectionHandle>, 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 // Make sure all frames have the same size and add them
// to the frame vector // to the frame vector
let mut dim = None; let mut dim = None;
@ -111,23 +125,14 @@ pub(crate) mod syntax {
bail!("frame duration must be positive (and therefore nonzero).") 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(( return Ok((
dim, dim,
super::SpriteSection { super::SpriteSection {
frames, frames,
frame_duration, frame_duration,
edge_top, // These are changed later, after all sections are built
edge_bot, 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 /// An edge between two animation sections
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum SectionEdge { pub enum SectionEdge {
/// Stop at the last frame of this section /// Stop at the last frame of this section
Stop, Stop,
@ -169,7 +155,7 @@ pub enum SectionEdge {
/// Play the given section from the bottm /// Play the given section from the bottm
Bot { Bot {
/// The section to play /// The section to play
section: AnimSectionHandle, section: Arc<SpriteSection>,
/// The length of this edge, in seconds /// The length of this edge, in seconds
duration: f32, duration: f32,
@ -178,7 +164,7 @@ pub enum SectionEdge {
/// Play the given section from the top /// Play the given section from the top
Top { Top {
/// The section to play /// The section to play
section: AnimSectionHandle, section: Arc<SpriteSection>,
/// The length of this edge, in seconds /// The length of this edge, in seconds
duration: f32, duration: f32,
@ -198,18 +184,18 @@ pub enum SectionEdge {
} }
/// Where to start an animation /// Where to start an animation
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum StartEdge { pub enum StartEdge {
/// Play the given section from the bottm /// Play the given section from the bottm
Bot { Bot {
/// The section to play /// The section to play
section: AnimSectionHandle, section: Arc<SpriteSection>,
}, },
/// Play the given section from the top /// Play the given section from the top
Top { Top {
/// The section to play /// 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. /// Represents a sprite that may be used in the game.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Sprite { pub struct Sprite {
/// The name of this sprite /// This object's index
pub name: String, pub index: ContentIndex,
/// This sprite's handle
pub handle: SpriteHandle,
/// Where this sprite starts playing /// Where this sprite starts playing
pub start_at: StartEdge, pub start_at: StartEdge,
/// This sprite's animation sections /// This sprite's animation sections
sections: Vec<SpriteSection>, pub sections: HashMap<ContentIndex, Arc<SpriteSection>>,
/// Allows us to get sprite sections by name
sections_by_name: HashMap<String, AnimSectionHandle>,
/// Aspect ratio of this sprite (width / height) /// Aspect ratio of this sprite (width / height)
pub aspect: f32, pub aspect: f32,
} }
lazy_static! { 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], frames: vec![0],
frame_duration: 0.0, frame_duration: 0.0,
edge_bot: SectionEdge::Stop, edge_bot: SectionEdge::Stop,
edge_top: SectionEdge::Stop, edge_top: SectionEdge::Stop,
}; });
} }
impl Sprite { 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 /// Get the index of the texture of this sprite's first frame
pub fn get_first_frame(&self) -> u32 { pub fn get_first_frame(&self) -> u32 {
match self.start_at { match &self.start_at {
StartEdge::Bot { section } => *self.get_section(section).frames.last().unwrap(), StartEdge::Bot { section } => *section.frames.last().unwrap(),
StartEdge::Top { section } => *self.get_section(section).frames.first().unwrap(), StartEdge::Top { section } => *section.frames.first().unwrap(),
} }
} }
/// Get this sprite's starting section /// Get this sprite's starting section
pub fn get_start_section(&self) -> AnimSectionHandle { pub fn get_start_section(&self) -> Arc<SpriteSection> {
match self.start_at { match &self.start_at {
StartEdge::Bot { section } => section, StartEdge::Bot { section } => section.clone(),
StartEdge::Top { section } => section, StartEdge::Top { section } => section.clone(),
} }
} }
/// Iterate this sprite's sections /// Iterate this sprite's sections
pub fn iter_sections(&self) -> impl Iterator<Item = &SpriteSection> { pub fn iter_sections(&self) -> impl Iterator<Item = &Arc<SpriteSection>> {
self.sections.iter() self.sections.values()
} }
} }
@ -327,66 +283,73 @@ pub struct SpriteSection {
} }
/// Resolve an edge specification string as a StartEdge /// Resolve an edge specification string as a StartEdge
pub fn resolve_edge_as_start<F>(s: &str, get_handle: F) -> Result<super::StartEdge> pub fn resolve_edge_as_start(
where sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
F: Fn(&str) -> Option<AnimSectionHandle>, edge_string: &str,
{ ) -> Result<super::StartEdge> {
let e = resolve_edge_as_edge(s, 0.0, get_handle) let e = resolve_edge_as_edge(sections, edge_string, 0.0)
.with_context(|| format!("while resolving start edge"))?; .with_context(|| format!("while resolving start edge"))?;
match e { match e {
super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }), super::SectionEdge::Bot { section, .. } => Ok(super::StartEdge::Bot { section }),
super::SectionEdge::Top { section, .. } => Ok(super::StartEdge::Top { 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 /// 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> pub fn resolve_edge_as_edge(
where sections: &HashMap<ContentIndex, Arc<SpriteSection>>,
F: Fn(&str) -> Option<AnimSectionHandle>, edge_string: &str,
{ duration: f32,
if s == "hidden" { ) -> Result<super::SectionEdge> {
if edge_string == "hidden" {
return Ok(super::SectionEdge::Top { return Ok(super::SectionEdge::Top {
section: crate::AnimSectionHandle::Hidden, section: crate::HIDDEN_SPRITE_SECTION.clone(),
duration, duration,
}); });
} }
if s == "stop" { if edge_string == "stop" {
return Ok(super::SectionEdge::Stop); return Ok(super::SectionEdge::Stop);
} }
if s == "reverse" { if edge_string == "reverse" {
return Ok(super::SectionEdge::Reverse { duration }); return Ok(super::SectionEdge::Reverse { duration });
} }
if s == "repeat" { if edge_string == "repeat" {
return Ok(super::SectionEdge::Repeat { duration }); 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, Some(x) => x,
None => { 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, Some(s) => s,
None => { None => {
return Err(anyhow!("bad section edge specification `{}`", s)) return Err(anyhow!("bad section edge specification `{}`", section_name))
.with_context(|| format!("section `{}` doesn't exist", s)); .with_context(|| format!("section `{}` doesn't exist", section_name));
} }
}; };
match p { match start_point {
"top" => Ok(super::SectionEdge::Top { section, duration }), "top" => Ok(super::SectionEdge::Top {
"bot" => Ok(super::SectionEdge::Bot { section, duration }), section: section.clone(),
duration,
}),
"bot" => Ok(super::SectionEdge::Bot {
section: section.clone(),
duration,
}),
_ => { _ => {
return Err(anyhow!("bad section edge specification `{}`", s)) return Err(anyhow!("bad section edge specification `{}`", section_name))
.with_context(|| format!("invalid target `{}`", p)); .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 img = &ct.sprite_atlas.get_by_idx(idx);
let aspect = img.w / img.h; let aspect = img.w / img.h;
let h = SpriteHandle { let section = Arc::new(SpriteSection {
index: ct.sprites.len(),
};
ct.sprite_index.insert(sprite_name.clone(), h);
ct.sprites.push(Self {
name: sprite_name,
start_at: StartEdge::Top {
section: AnimSectionHandle::Idx(0),
},
sections: vec![SpriteSection {
frames: vec![img.idx.into()], frames: vec![img.idx.into()],
// We implement unanimated sprites with a very fast framerate // We implement unanimated sprites with a very fast framerate
// and STOP endpoints. // and STOP endpoints.
frame_duration: 0.01, frame_duration: 0.01,
edge_top: SectionEdge::Stop, edge_top: SectionEdge::Stop,
edge_bot: SectionEdge::Stop, edge_bot: SectionEdge::Stop,
}], });
sections_by_name: HashMap::new(),
handle: h, let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at: StartEdge::Top {
section: section.clone(),
},
sections: {
let mut h = HashMap::new();
h.insert(ContentIndex::new("anim"), section);
h
},
aspect, aspect,
}); });
ct.sprites.insert(sprite.index.clone(), sprite);
} }
syntax::Sprite::OneSection(s) => { 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 let (dim, section) = s
.add_to(ct, |s| { .add_to(ct)
if s == "anim" {
Some(AnimSectionHandle::Idx(0))
} else {
None
}
})
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?; .with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
let aspect = dim.0 as f32 / dim.1 as f32; let aspect = dim.0 as f32 / dim.1 as f32;
let mut sections = Vec::new(); let section = Arc::new(section);
sections.push(section);
ct.sprites.push(Self { let sprite = Arc::new(Self {
name: sprite_name, index: ContentIndex::new(&sprite_name),
sections,
start_at: StartEdge::Top { start_at: StartEdge::Top {
section: AnimSectionHandle::Idx(0), section: section.clone(),
}, },
sections_by_name: { sections: {
let mut h = HashMap::new(); let mut h = HashMap::new();
h.insert("anim".to_string(), AnimSectionHandle::Idx(0)); h.insert(ContentIndex::new("anim"), section.clone());
h h
}, },
handle: sprite_handle,
aspect, aspect,
}); });
ct.sprites.insert(sprite.index.clone(), sprite);
} }
syntax::Sprite::Complete(s) => { syntax::Sprite::Complete(s) => {
let mut section_names = HashMap::new(); let mut sections = HashMap::new();
for (name, _) in &s.section {
section_names
.insert(name.to_owned(), AnimSectionHandle::Idx(section_names.len()));
}
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; let mut dim = None;
// Make sure we add sections in order for (name, sec) in &s.section {
let mut names = section_names.iter().collect::<Vec<_>>(); let (d, s) = sec
names.sort_by(|a, b| (a.1).get_idx().unwrap().cmp(&(b.1).get_idx().unwrap())); .add_to(ct)
.with_context(|| format!("while parsing section `{}`", name))
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))
.with_context(|| format!("while parsing sprite `{}`", sprite_name))?; .with_context(|| format!("while parsing sprite `{}`", sprite_name))?;
// Make sure all dimensions are the same // Make sure all dimensions are the same
@ -515,23 +445,35 @@ impl crate::Build for Sprite {
bail!( bail!(
"could not load sprite `{}`, image sizes in section `{}` are different", "could not load sprite `{}`, image sizes in section `{}` are different",
sprite_name, 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 dim = dim.unwrap();
let aspect = dim.0 as f32 / dim.1 as f32; let aspect = dim.0 as f32 / dim.1 as f32;
ct.sprites.push(Self { let start_at = resolve_edge_as_start(&sections, &s.start_at.val)
name: sprite_name, .with_context(|| format!("while loading sprite `{}`", sprite_name))?;
sections,
let sprite = Arc::new(Self {
index: ContentIndex::new(&sprite_name),
start_at, start_at,
handle: sprite_handle, sections,
sections_by_name: section_names,
aspect, 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 galactica_util::to_radians;
use nalgebra::{Point2, Point3}; use nalgebra::{Point2, Point3};
use std::collections::{HashMap, HashSet}; use std::{
collections::{HashMap, HashSet},
use crate::{ sync::Arc,
handle::SpriteHandle, util::Polar, Content, ContentBuildContext, SystemHandle,
SystemObjectHandle,
}; };
use crate::{util::Polar, Content, ContentBuildContext, ContentIndex, Outfit, Sprite};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::ContentIndex;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct System { pub struct System {
pub object: HashMap<String, Object>, pub name: String,
pub object: HashMap<ContentIndex, Object>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Object { pub struct Object {
pub sprite: String, pub sprite: ContentIndex,
pub position: Position, pub position: Position,
pub size: f32, pub size: f32,
pub radius: Option<f32>, pub radius: Option<f32>,
pub angle: Option<f32>, pub angle: Option<f32>,
pub landable: Option<bool>, pub landable: Option<Landable>,
pub name: Option<String>, }
pub desc: Option<String>,
pub image: Option<String>, #[derive(Debug, Deserialize)]
pub struct Landable {
pub name: String,
pub desc: String,
pub image: ContentIndex,
pub outfitter: Vec<ContentIndex>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -52,14 +58,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum CoordinatesTwo { pub enum CoordinatesTwo {
Label(String), Label(ContentIndex),
Coords([f32; 2]), Coords([f32; 2]),
} }
impl ToString for CoordinatesTwo { impl ToString for CoordinatesTwo {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Self::Label(s) => s.to_owned(), Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v), Self::Coords(v) => format!("{:?}", v),
} }
} }
@ -68,14 +74,14 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum CoordinatesThree { pub enum CoordinatesThree {
Label(String), Label(ContentIndex),
Coords([f32; 3]), Coords([f32; 3]),
} }
impl ToString for CoordinatesThree { impl ToString for CoordinatesThree {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Self::Label(s) => s.to_owned(), Self::Label(s) => s.to_string(),
Self::Coords(v) => format!("{:?}", v), Self::Coords(v) => format!("{:?}", v),
} }
} }
@ -88,14 +94,14 @@ pub(crate) mod syntax {
/// Represents a star system /// Represents a star system
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct System { pub struct System {
/// This star system's name /// This object's name
pub name: String, pub display_name: String,
/// This star system's handle /// This object's index
pub handle: SystemHandle, pub index: ContentIndex,
/// Objects in this system /// Objects in this system
pub objects: Vec<SystemObject>, pub objects: HashMap<ContentIndex, Arc<SystemObject>>,
} }
/// Represents an orbiting body in a star system /// Represents an orbiting body in a star system
@ -104,11 +110,11 @@ pub struct System {
/// System objects to not interact with the physics engine. /// System objects to not interact with the physics engine.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SystemObject { pub struct SystemObject {
/// This object's sprite /// This object's index
pub sprite: SpriteHandle, pub index: ContentIndex,
/// This object's handle /// This object's sprite
pub handle: SystemObjectHandle, pub sprite: Arc<Sprite>,
/// This object's size. /// This object's size.
/// Measured as height in game units. /// Measured as height in game units.
@ -124,26 +130,29 @@ pub struct SystemObject {
pub angle: f32, pub angle: f32,
/// If true, ships may land on this object /// If true, ships may land on this object
pub landable: bool, pub landable: Option<LandableSystemObject>,
}
/// The pretty display name of this object #[derive(Debug, Clone)]
pub name: Option<String>, pub struct LandableSystemObject {
/// This object's name
pub display_name: String,
/// The system-unique label of this object /// The description of this object
pub label: String, pub desc: String,
/// The description of this object (shown on landed ui) /// This object's image
pub desc: Option<String>, pub image: Arc<Sprite>,
/// This object's image (shown on landed ui) /// The outfits we can buy here
pub image: Option<SpriteHandle>, pub outfitter: Vec<Arc<Outfit>>,
} }
/// Helper function for resolve_position, never called on its own. /// Helper function for resolve_position, never called on its own.
fn resolve_coordinates( fn resolve_coordinates(
objects: &HashMap<String, syntax::Object>, objects: &HashMap<ContentIndex, syntax::Object>,
cor: &syntax::CoordinatesThree, cor: &syntax::CoordinatesThree,
mut cycle_detector: HashSet<String>, mut cycle_detector: HashSet<ContentIndex>,
) -> Result<Point3<f32>> { ) -> Result<Point3<f32>> {
match cor { match cor {
syntax::CoordinatesThree::Coords(c) => Ok((*c).into()), 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) { let p = match objects.get(l) {
Some(p) => p, Some(p) => p,
@ -174,9 +183,9 @@ fn resolve_coordinates(
/// Given an object, resolve its position as a Point3. /// Given an object, resolve its position as a Point3.
fn resolve_position( fn resolve_position(
objects: &HashMap<String, syntax::Object>, objects: &HashMap<ContentIndex, syntax::Object>,
obj: &syntax::Object, obj: &syntax::Object,
cycle_detector: HashSet<String>, cycle_detector: HashSet<ContentIndex>,
) -> Result<Point3<f32>> { ) -> Result<Point3<f32>> {
match &obj.position { match &obj.position {
syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?), syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?),
@ -208,99 +217,78 @@ impl crate::Build for System {
content: &mut Content, content: &mut Content,
) -> Result<()> { ) -> Result<()> {
for (system_name, system) in system { for (system_name, system) in system {
let mut objects = Vec::new(); let mut objects = HashMap::new();
let system_handle = SystemHandle { for (index, object) in &system.object {
index: content.systems.len(),
};
for (label, obj) in &system.object {
let mut cycle_detector = HashSet::new(); 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!( None => bail!(
"In system `{}`: sprite `{}` doesn't exist", "In system `{}`: sprite `{}` doesn't exist",
system_name, system_name,
obj.sprite object.sprite
), ),
Some(t) => *t, Some(t) => t.clone(),
}; };
let image_handle = match &obj.image { let landable = 'landable: {
Some(x) => match content.sprite_index.get(x) { 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!( None => bail!(
"In system `{}`: sprite `{}` doesn't exist", "In system `{}`: sprite `{}` doesn't exist",
system_name, system_name,
obj.sprite object.sprite
), ),
Some(t) => Some(*t), Some(t) => t.clone(),
},
None => None,
}; };
if obj.landable.unwrap_or(false) { let mut outfitter = Vec::new();
if obj.name.is_none() { for o in &l.outfitter {
return Err(anyhow!("if an object is landable, it must have a name")) match content.outfits.get(&o) {
.with_context(|| format!("in object labeled `{}`", label)) Some(x) => outfitter.push(x.clone()),
.with_context(|| format!("in system `{}`", system_name)); None => {
bail!("In system `{}`: outfit `{}` doesn't exist", system_name, o)
} }
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 { break 'landable Some(LandableSystemObject {
label: label.clone(), image,
sprite: sprite_handle, outfitter,
image: image_handle, display_name: l.name.clone(),
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 // TODO: better linebreaks, handle double spaces
// Tabs // Tabs
desc: obj desc: l.desc.replace("\n", " ").replace("<br>", "\n"),
.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 content.systems.insert(
// rendered in this order. We need far objects to be behind ContentIndex::new(&system_name),
// near objects! Arc::new(Self {
objects.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z)); index: ContentIndex::new(&system_name),
display_name: system.name,
// 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, objects,
}); }),
);
} }
return Ok(()); 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 /// A single frame's state
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -43,11 +44,11 @@ enum AnimDirection {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SpriteAutomaton { pub struct SpriteAutomaton {
/// The sprite we're animating /// The sprite we're animating
sprite: SpriteHandle, sprite: Arc<Sprite>,
/// Which animation section we're on /// Which animation section we're on
/// This MUST be a section from this Automaton's sprite /// This MUST be a section from this Automaton's sprite
current_section: AnimSectionHandle, current_section: Arc<SpriteSection>,
/// Which frame we're on /// Which frame we're on
current_frame: usize, current_frame: usize,
@ -75,39 +76,35 @@ pub struct SpriteAutomaton {
impl SpriteAutomaton { impl SpriteAutomaton {
/// Create a new AnimAutomaton /// Create a new AnimAutomaton
pub fn new(ct: &Content, sprite_handle: SpriteHandle) -> Self { pub fn new(sprite: Arc<Sprite>) -> Self {
let sprite = ct.get_sprite(sprite_handle); let (current_section, texture, current_direction) = {
match &sprite.start_at {
let (current_section, texture, current_direction) = match sprite.start_at {
StartEdge::Top { section } => ( StartEdge::Top { section } => (
section, section,
*sprite.get_section(section).frames.first().unwrap(), *section.frames.first().unwrap(),
AnimDirection::Down, AnimDirection::Down,
), ),
StartEdge::Bot { section } => ( StartEdge::Bot { section } => {
section, (section, *section.frames.last().unwrap(), AnimDirection::Up)
*sprite.get_section(section).frames.last().unwrap(), }
AnimDirection::Up, }
),
}; };
let sec = sprite.get_section(current_section);
Self { Self {
sprite: sprite.handle, sprite: sprite.clone(),
current_frame: 0, current_frame: 0,
current_edge_progress: match current_direction { current_edge_progress: match current_direction {
AnimDirection::Down => 0.0, AnimDirection::Down => 0.0,
AnimDirection::Up => sec.frame_duration, AnimDirection::Up => current_section.frame_duration,
AnimDirection::Stop => unreachable!("how'd you get here?"), 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, next_edge_override: None,
current_direction, current_direction,
current_section, current_section: current_section.clone(),
last_texture: texture, last_texture: texture,
next_texture: texture, next_texture: texture,
} }
@ -119,14 +116,11 @@ impl SpriteAutomaton {
} }
/// Force a transition to the given section right now /// Force a transition to the given section right now
pub fn jump_to(&mut self, ct: &Content, start: SectionEdge) { pub fn jump_to(&mut self, start: &SectionEdge) {
self.take_edge(ct, start); self.take_edge(start);
} }
fn take_edge(&mut self, ct: &Content, e: SectionEdge) { fn take_edge(&mut self, e: &SectionEdge) {
let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section);
let last = match self.current_direction { let last = match self.current_direction {
AnimDirection::Stop => self.next_texture, AnimDirection::Stop => self.next_texture,
AnimDirection::Down => self.next_texture, AnimDirection::Down => self.next_texture,
@ -141,7 +135,7 @@ impl SpriteAutomaton {
self.current_frame = 0; self.current_frame = 0;
} }
AnimDirection::Down => { 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; self.current_direction = AnimDirection::Stop;
} }
SectionEdge::Top { section, duration } => { SectionEdge::Top { section, duration } => {
self.current_section = section; self.current_section = section.clone();
self.current_edge_duration = duration; self.current_edge_duration = *duration;
self.current_frame = 0; self.current_frame = 0;
self.current_direction = AnimDirection::Down; self.current_direction = AnimDirection::Down;
} }
SectionEdge::Bot { section, duration } => { SectionEdge::Bot { section, duration } => {
let s = sprite.get_section(section); self.current_section = section.clone();
self.current_section = section; self.current_frame = section.frames.len() - 1;
self.current_frame = s.frames.len() - 1; self.current_edge_duration = *duration;
self.current_edge_duration = duration;
self.current_direction = AnimDirection::Up; self.current_direction = AnimDirection::Up;
} }
SectionEdge::Repeat { duration } => { SectionEdge::Repeat { duration } => {
match self.current_direction { match self.current_direction {
AnimDirection::Stop => {} AnimDirection::Stop => {}
AnimDirection::Up => { AnimDirection::Up => {
self.current_frame = current_section.frames.len() - 1; self.current_frame = self.current_section.frames.len() - 1;
} }
AnimDirection::Down => { AnimDirection::Down => {
self.current_frame = 0; self.current_frame = 0;
} }
} }
self.current_edge_duration = duration; self.current_edge_duration = *duration;
} }
SectionEdge::Reverse { duration } => { SectionEdge::Reverse { duration } => {
match self.current_direction { match self.current_direction {
@ -180,7 +173,7 @@ impl SpriteAutomaton {
// Jump to SECOND frame, since we've already shown the // Jump to SECOND frame, since we've already shown the
// first during the fade transition // first during the fade transition
self.current_frame = { self.current_frame = {
if current_section.frames.len() == 1 { if self.current_section.frames.len() == 1 {
0 0
} else { } else {
1 1
@ -190,45 +183,39 @@ impl SpriteAutomaton {
} }
AnimDirection::Down => { AnimDirection::Down => {
self.current_frame = { self.current_frame = {
if current_section.frames.len() == 1 { if self.current_section.frames.len() == 1 {
0 0
} else { } else {
current_section.frames.len() - 2 self.current_section.frames.len() - 2
} }
}; };
self.current_direction = AnimDirection::Up; self.current_direction = AnimDirection::Up;
} }
} }
self.current_edge_duration = duration; self.current_edge_duration = *duration;
} }
} }
match self.current_direction { match self.current_direction {
AnimDirection::Stop => { AnimDirection::Stop => {
let current_section = sprite.get_section(self.current_section); self.next_texture = self.current_section.frames[self.current_frame];
self.next_texture = current_section.frames[self.current_frame]; self.last_texture = self.current_section.frames[self.current_frame];
self.last_texture = current_section.frames[self.current_frame];
} }
AnimDirection::Down => { AnimDirection::Down => {
let current_section = sprite.get_section(self.current_section);
self.last_texture = last; 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; self.current_edge_progress = 0.0;
} }
AnimDirection::Up => { AnimDirection::Up => {
let current_section = sprite.get_section(self.current_section);
self.next_texture = last; 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; self.current_edge_progress = self.current_edge_duration;
} }
} }
} }
/// Step this animation by `t` seconds /// Step this animation by `t` seconds
pub fn step(&mut self, ct: &Content, t: f32) { pub fn step(&mut self, t: f32) {
let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section);
// Current_fade and current_frame keep track of where we are in the current section. // 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 // 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. // 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! // Note that frame_duration may be zero!
// This is only possible in the hidden texture, since // This is only possible in the hidden texture, since
// user-provided sections are always checked to be positive. // 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 { match self.current_direction {
AnimDirection::Stop => { AnimDirection::Stop => {
// Edge case: we're stopped and got a request to transition. // Edge case: we're stopped and got a request to transition.
// we should transition right away. // we should transition right away.
if let Some(e) = self.next_edge_override.take() { if self.next_edge_override.is_some() {
self.take_edge(ct, e); let e = self.next_edge_override.take().unwrap();
self.take_edge(&e);
} }
return; return;
@ -259,21 +247,21 @@ impl SpriteAutomaton {
// We're stepping foward and finished this frame // We're stepping foward and finished this frame
if self.current_edge_progress > self.current_edge_duration { 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.current_frame += 1;
self.last_texture = self.next_texture; 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_progress = 0.0;
self.current_edge_duration = current_section.frame_duration; self.current_edge_duration = self.current_section.frame_duration;
} else { } else {
let e = { let e = {
if self.next_edge_override.is_some() { if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap() self.next_edge_override.take().unwrap()
} else { } 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 { if self.current_frame > 0 {
self.current_frame -= 1; self.current_frame -= 1;
self.next_texture = self.last_texture; self.next_texture = self.last_texture;
self.last_texture = current_section.frames[self.current_frame]; self.last_texture = self.current_section.frames[self.current_frame];
self.current_edge_progress = current_section.frame_duration; self.current_edge_progress = self.current_section.frame_duration;
self.current_edge_duration = current_section.frame_duration; self.current_edge_duration = self.current_section.frame_duration;
} else { } else {
let e = { let e = {
if self.next_edge_override.is_some() { if self.next_edge_override.is_some() {
self.next_edge_override.take().unwrap() self.next_edge_override.take().unwrap()
} else { } 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 /// Get the sprite this automaton is using
pub fn get_sprite(&self) -> SpriteHandle { pub fn get_sprite(&self) -> Arc<Sprite> {
self.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_playeragent::PlayerAgent;
use galactica_system::data::ShipPersonality; use galactica_system::data::ShipPersonality;
use galactica_system::phys::{PhysImage, PhysSim, PhysSimShipHandle, PhysStepResources}; use galactica_system::phys::{PhysImage, PhysSim, PhysSimShipHandle, PhysStepResources};
use galactica_system::PlayerDirective;
use galactica_util::timing::Timing; use galactica_util::timing::Timing;
use nalgebra::Point2; use nalgebra::Point2;
use std::sync::Arc; use std::sync::Arc;
@ -24,82 +25,101 @@ unsafe impl<'a> Send for Game {}
impl<'a> Game { impl<'a> Game {
pub fn make_player(&mut self) -> PhysSimShipHandle { pub fn make_player(&mut self) -> PhysSimShipHandle {
let player = self.phys_sim.add_ship( let player = self.phys_sim.add_ship(
&self.ct, self.ct
ShipHandle { index: 0 }, .ships
FactionHandle { index: 0 }, .get(&ContentIndex::new("gypsum"))
.unwrap()
.clone(),
self.ct
.factions
.get(&ContentIndex::new("player"))
.unwrap()
.clone(),
ShipPersonality::Player, ShipPersonality::Player,
Point2::new(0.0, 4000.0), Point2::new(0.0, 4000.0),
); );
let s = self.phys_sim.get_ship_mut(&player).unwrap(); let s = self.phys_sim.get_ship_mut(&player).unwrap();
s.add_outfits( s.add_outfits([
&self.ct, self.ct
[ .outfits
OutfitHandle { index: 0 }, .get(&ContentIndex::new("plasma engines"))
OutfitHandle { index: 1 }, .unwrap()
OutfitHandle { index: 2 }, .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; return player;
} }
pub fn new(ct: Arc<Content>) -> Self { 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( let a = phys_sim.add_ship(
&ct, ct.ships.get(&ContentIndex::new("gypsum")).unwrap().clone(),
ShipHandle { index: 0 }, ct.factions
FactionHandle { index: 1 }, .get(&ContentIndex::new("enemy"))
ShipPersonality::Point, .unwrap()
Point2::new(1000.0, 0.0), .clone(),
);
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 },
ShipPersonality::Point, ShipPersonality::Point,
Point2::new(1000.0, 4000.0), Point2::new(1000.0, 4000.0),
); );
let s = phys_sim.get_ship_mut(&a).unwrap(); let s = phys_sim.get_ship_mut(&a).unwrap();
s.add_outfits( s.add_outfits([
&ct, ct.outfits
[ .get(&ContentIndex::new("plasma engines"))
OutfitHandle { index: 0 }, .unwrap()
OutfitHandle { index: 1 }, .clone(),
OutfitHandle { index: 2 }, ct.outfits
], .get(&ContentIndex::new("shield generator"))
); .unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
let a = phys_sim.add_ship( let a = phys_sim.add_ship(
&ct, ct.ships.get(&ContentIndex::new("gypsum")).unwrap().clone(),
ShipHandle { index: 0 }, ct.factions
FactionHandle { index: 0 }, .get(&ContentIndex::new("player"))
.unwrap()
.clone(),
ShipPersonality::Dummy, ShipPersonality::Dummy,
Point2::new(200.0, 2000.0), Point2::new(200.0, 2000.0),
); );
let s = phys_sim.get_ship_mut(&a).unwrap(); let s = phys_sim.get_ship_mut(&a).unwrap();
s.add_outfits( s.add_outfits([
&ct, ct.outfits
[ .get(&ContentIndex::new("plasma engines"))
OutfitHandle { index: 0 }, .unwrap()
OutfitHandle { index: 1 }, .clone(),
OutfitHandle { index: 2 }, ct.outfits
], .get(&ContentIndex::new("shield generator"))
); .unwrap()
.clone(),
ct.outfits
.get(&ContentIndex::new("blaster"))
.unwrap()
.clone(),
]);
Game { Game {
ct, ct,
@ -111,8 +131,10 @@ impl<'a> Game {
} }
} }
pub fn update_player_controls(&mut self, player: &mut PlayerAgent) { pub fn apply_directive(&mut self, directive: PlayerDirective, player: &PlayerAgent) {
self.phys_sim.update_player_controls(&self.ct, player) match directive {
_ => self.phys_sim.apply_directive(directive, player),
}
} }
pub fn step(&mut self, phys_img: &PhysImage) { pub fn step(&mut self, phys_img: &PhysImage) {

View File

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

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

View File

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

View File

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

View File

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

View File

@ -30,36 +30,32 @@ impl Starfield {
pub fn regenerate(&mut self, ct: &Content) { pub fn regenerate(&mut self, ct: &Content) {
// TODO: save seed in system, regenerate on jump // TODO: save seed in system, regenerate on jump
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let sz = ct.get_config().starfield_size as f32 / 2.0; let sz = ct.config.starfield_size as f32 / 2.0;
self.stars = (0..ct.get_config().starfield_count) self.stars = (0..ct.config.starfield_count)
.map(|_| StarfieldStar { .map(|_| StarfieldStar {
pos: Point3::new( pos: Point3::new(
rng.gen_range(-sz..=sz), rng.gen_range(-sz..=sz),
rng.gen_range(-sz..=sz), rng.gen_range(-sz..=sz),
rng.gen_range( rng.gen_range(ct.config.starfield_min_dist..=ct.config.starfield_max_dist),
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,
), ),
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)), tint: Vector2::new(rng.gen_range(0.0..=1.0), rng.gen_range(0.0..=1.0)),
}) })
.collect(); .collect();
} }
pub fn update_buffer(&mut self, ct: &Content, state: &mut RenderState) { 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 // Compute window size in starfield tiles
let mut nw_tile = { let mut nw_tile = {
// Game coordinates (relative to camera) of nw corner of screen. // 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. // Parallax correction.
// Also, adjust v for mod to work properly // Also, adjust v for mod to work properly
// (v is centered at 0) // (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)); let v_adj = Point2::new(v.x + (sz / 2.0), v.y + (sz / 2.0));
#[rustfmt::skip] #[rustfmt::skip]
@ -83,8 +79,8 @@ impl Starfield {
// Truncate tile grid to buffer size // Truncate tile grid to buffer size
// (The window won't be full of stars if our instance limit is too small) // (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) while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * ct.config.starfield_count as i32)
> ct.get_config().starfield_instance_limit as i32 > ct.config.starfield_instance_limit as i32
{ {
nw_tile -= Vector2::new(1, 1); 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"); 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 conf;
mod radialbar; mod radialbar;
mod scrollbox;
mod sprite; mod sprite;
mod textbox; mod textbox;
mod ui; mod ui;
pub use conf::build_conf_module; pub use conf::build_conf_module;
pub use radialbar::build_radialbar_module; pub use radialbar::build_radialbar_module;
pub use scrollbox::build_scrollbox_module;
pub use sprite::build_sprite_module; pub use sprite::build_sprite_module;
pub use textbox::build_textbox_module; pub use textbox::build_textbox_module;
pub use ui::build_ui_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 std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect}; 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 { pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
let mut module = Module::new(); let mut module = Module::new();
@ -23,11 +23,13 @@ pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::RadialBar(UiRadialBar::new(
ui_state.elements.insert(
name.clone(), 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 rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc, sync::Arc}; 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}; 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) .with_namespace(FnNamespace::Internal)
.set_into_module( .set_into_module(
&mut 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 mut ui_state = state.borrow_mut();
let sprite_handle = ct.get_sprite_handle(sprite.as_str()); let sprite = ct.sprites.get(&sprite_name.clone().into());
if sprite_handle.is_none() { if sprite.is_none() {
error!("made a sprite using an invalid source `{sprite}`"); error!("made a sprite using an invalid source `{sprite_name}`");
return; return;
} }
@ -31,11 +31,11 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::Sprite(UiSprite::new(
ui_state.elements.insert(
name.clone(), 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| { .set_into_module(&mut module, move |name: ImmutableString| {
let mut ui_state = state.borrow_mut(); let mut ui_state = state.borrow_mut();
if ui_state.elements.contains_key(&name) { if ui_state.elements.contains_key(&name) {
ui_state.elements.remove(&name).unwrap(); ui_state.remove_element(&name);
ui_state.names.retain(|x| *x != name);
} else { } else {
error!("called `sprite::remove` on an invalid name `{name}`") 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) { match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => { 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() { if m.is_none() {
error!("called `set_sprite_mask` with an invalid mask `{mask}`"); error!("called `set_sprite_mask` with an invalid mask `{mask}`");
return; 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 state = state_src.clone();
let ct = ct_src.clone(); let _ = FuncRegistration::new("jump_to")
let _ = FuncRegistration::new("take_edge")
.with_namespace(FnNamespace::Internal) .with_namespace(FnNamespace::Internal)
.set_into_module( .set_into_module(
&mut 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) { match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => { Some(UiElement::Sprite(x)) => {
let sprite_handle = x.anim.get_sprite(); let sprite = x.anim.get_sprite();
let sprite = &ct.get_sprite(sprite_handle);
let edge = resolve_edge_as_edge(edge_name.as_str(), duration, |x| { let edge =
sprite.get_section_handle_by_name(x) resolve_edge_as_edge(&sprite.sections, edge_name.as_str(), duration);
});
let edge = match edge { let edge = match edge {
Err(_) => { Err(_) => {
error!( error!(
"called `sprite::take_edge` on an invalid edge `{}` on sprite `{}`", "called `sprite::jump_to` on an invalid edge `{}` on sprite `{}`",
edge_name, sprite.name edge_name, sprite.index
); );
return; return;
} }
Ok(s) => s, 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 std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect}; use super::super::{Color, Rect};
use crate::ui::{elements::TextBox, UiElement, UiState}; use crate::ui::{elements::UiTextBox, UiElement, UiState};
pub fn build_textbox_module( pub fn build_textbox_module(
font_src: Rc<RefCell<FontSystem>>, font_src: Rc<RefCell<FontSystem>>,
@ -32,18 +32,14 @@ pub fn build_textbox_module(
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::Text(UiTextBox::new(
ui_state.elements.insert(
name.clone(),
UiElement::Text(TextBox::new(
&mut font.borrow_mut(), &mut font.borrow_mut(),
name.clone(), name.clone(),
font_size, font_size,
line_height, line_height,
rect, rect,
color, color,
)), )));
);
}, },
); );

View File

@ -15,5 +15,17 @@ pub fn build_ui_module(state_src: Rc<RefCell<UiState>>) -> Module {
ui_state.set_scene(scene); 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; return module;
} }

View File

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

View File

@ -1,17 +1,19 @@
mod directive;
mod event; mod event;
mod functions; mod functions;
mod helpers; mod helpers;
mod state; mod state;
pub use directive::*;
pub use event::*; pub use event::*;
use glyphon::FontSystem;
pub use helpers::{anchor::*, color::*, rect::*, vector::*}; pub use helpers::{anchor::*, color::*, rect::*, vector::*};
use log::debug;
pub use state::*; pub use state::*;
use super::UiState; use super::UiState;
use galactica_content::Content; 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}; use std::{cell::RefCell, rc::Rc, sync::Arc};
pub fn register_into_engine( pub fn register_into_engine(
@ -29,21 +31,27 @@ pub fn register_into_engine(
.build_type::<State>() .build_type::<State>()
.build_type::<ShipState>() .build_type::<ShipState>()
.build_type::<SystemObjectState>() .build_type::<SystemObjectState>()
.build_type::<OutfitState>()
// Events // Events
.build_type::<MouseClickEvent>() .build_type::<MouseClickEvent>()
.build_type::<MouseHoverEvent>() .build_type::<MouseHoverEvent>()
.build_type::<PlayerShipStateEvent>() .build_type::<PlayerShipStateEvent>()
.build_type::<KeyboardEvent>()
.build_type::<ScrollEvent>()
// Bigger modules // Bigger modules
.register_type_with_name::<Anchor>("Anchor") .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 // Extra functions
engine.register_fn("print", move |d: Dynamic| { engine.register_fn("clamp", |x: f32, l: f32, h: f32| x.clamp(l, h));
debug!("{:?}", d);
});
engine.register_fn("clamp", move |x: f32, l: f32, h: f32| x.clamp(l, h));
// Modules // Modules
engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into());
engine.register_static_module( engine.register_static_module(
"sprite", "sprite",
functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(), functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(),
@ -56,7 +64,10 @@ pub fn register_into_engine(
"radialbar", "radialbar",
functions::build_radialbar_module(state_src.clone()).into(), 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( engine.register_static_module(
"conf", "conf",
functions::build_conf_module(state_src.clone()).into(), 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::{ use galactica_system::{
data::{self}, data::{self},
phys::{objects::PhysShip, PhysSimShipHandle}, phys::{objects::PhysShip, PhysSimShipHandle},
@ -19,14 +19,15 @@ pub struct ShipState {
} }
impl ShipState { impl ShipState {
// All functions passed to rhai MUST be mut,
// even getters.
fn get_content(&mut self) -> &Ship { fn get_content(&mut self) -> &Ship {
let ship = self let ship = self
.input .input
.phys_img .phys_img
.get_ship(self.ship.as_ref().unwrap()) .get_ship(self.ship.as_ref().unwrap())
.unwrap(); .unwrap();
let handle = ship.ship.get_data().get_content(); ship.ship.get_data().get_content()
self.input.ct.get_ship(handle)
} }
fn get_ship(&mut self) -> &PhysShip { fn get_ship(&mut self) -> &PhysShip {
@ -48,20 +49,13 @@ impl ShipState {
} }
fn landed_on(&mut self) -> SystemObjectState { fn landed_on(&mut self) -> SystemObjectState {
let input = self.input.clone();
match self.get_ship().get_data().get_state() { match self.get_ship().get_data().get_state() {
data::ShipState::Landed { target } => { data::ShipState::Landed { target } => {
return SystemObjectState { return SystemObjectState {
input, object: Some(target.clone()),
object: Some(*target),
}
}
_ => {
return SystemObjectState {
input,
object: None,
} }
} }
_ => return SystemObjectState { object: None },
}; };
} }
} }
@ -89,8 +83,15 @@ impl CustomType for ShipState {
.with_fn("is_collapsing", |s: &mut Self| { .with_fn("is_collapsing", |s: &mut Self| {
s.get_ship().get_data().get_state().is_collapsing() s.get_ship().get_data().get_state().is_collapsing()
}) })
.with_fn("name", |s: &mut Self| s.get_content().name.clone()) .with_fn("display_name", |s: &mut Self| {
.with_fn("thumbnail", |s: &mut Self| s.get_content().thumbnail) 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("landed_on", |s: &mut Self| s.landed_on())
.with_fn("get_shields", |s: &mut Self| { .with_fn("get_shields", |s: &mut Self| {
s.get_ship().get_data().get_shields() s.get_ship().get_data().get_shields()
@ -103,28 +104,58 @@ impl CustomType for ShipState {
s.get_ship().get_data().get_hull() s.get_ship().get_data().get_hull()
}) })
.with_fn("get_size", |s: &mut Self| s.get_content().size) .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| { .with_fn("get_pos", |s: &mut Self| {
let t = s.get_body().translation(); let t = s.get_body().translation();
UiVector::new(t.x, t.y) UiVector::new(t.x, t.y)
}) })
.with_fn("get_faction_color", |s: &mut Self| { .with_fn("get_faction_color", |s: &mut Self| {
let h = s.get_ship().get_data().get_faction(); 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) 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)] #[derive(Debug, Clone)]
pub struct SystemObjectState { pub struct SystemObjectState {
object: Option<SystemObjectHandle>, object: Option<Arc<SystemObject>>,
input: Arc<RenderInput>,
} }
impl SystemObjectState { impl SystemObjectState {
fn get_content(&mut self) -> &SystemObject { fn outfitter(&mut self) -> Array {
self.input.ct.get_system_object(self.object.unwrap()) 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>) { fn build(mut builder: TypeBuilder<Self>) {
builder builder
.with_name("SystemObjectState") .with_name("SystemObjectState")
.with_fn("outfitter", Self::outfitter)
// //
// Get landable name // Get landable name
.with_fn("name", |s: &mut Self| { .with_fn("display_name", |s: &mut Self| {
s.get_content() s.object
.name
.as_ref() .as_ref()
.map(|x| x.to_string()) .unwrap()
.landable
.as_ref()
.map(|x| x.display_name.clone())
.unwrap_or_else(|| { .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() "".to_string()
}) })
}) })
// //
// Get landable description // Get landable description
.with_fn("desc", |s: &mut Self| { .with_fn("desc", |s: &mut Self| {
s.get_content() s.object
.desc
.as_ref() .as_ref()
.map(|x| x.to_string()) .unwrap()
.landable
.as_ref()
.map(|x| x.desc.clone())
.unwrap_or_else(|| { .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() "".to_string()
}) })
}) })
// //
// Get landable landscape image // Get landable landscape image
.with_fn("image", |s: &mut Self| { .with_fn("image", |s: &mut Self| {
let handle = s.get_content().image; s.object
if let Some(handle) = handle { .as_ref()
s.input.ct.get_sprite(handle).name.clone() .unwrap()
} else { .landable
error!("UI called `image()` on a system object which doesn't provide one"); .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() "".to_string()
} })
}) })
.with_fn("is_some", |s: &mut Self| s.object.is_some()) .with_fn("is_some", |s: &mut Self| s.object.is_some())
.with_fn("==", |a: &mut Self, b: Self| a.object == b.object) .with_fn("is_landable", |s: &mut Self| {
.with_fn("get_size", |s: &mut Self| s.get_content().size) s.object.as_ref().unwrap().landable.is_some()
.with_fn("get_label", |s: &mut Self| { })
ImmutableString::from(&s.get_content().label) .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| { .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| { .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) UiVector::new(t.x, t.y)
}) })
.with_fn("get_pos_z", |s: &mut Self| { .with_fn("get_pos_z", |s: &mut Self| s.object.as_ref().unwrap().pos.z);
let t = s.get_content().pos;
t.z
});
} }
} }
@ -225,11 +268,9 @@ impl State {
pub fn objects(&mut self) -> Array { pub fn objects(&mut self) -> Array {
let mut a = Array::new(); let mut a = Array::new();
let s = self.input.current_system; for (_, o) in &self.input.current_system.objects {
for o in &self.input.ct.get_system(s).objects {
a.push(Dynamic::from(SystemObjectState { a.push(Dynamic::from(SystemObjectState {
input: self.input.clone(), object: Some(o.clone()),
object: Some(o.handle),
})); }));
} }
return a; return a;
@ -243,7 +284,6 @@ impl CustomType for State {
.with_fn("player_ship", Self::player_ship) .with_fn("player_ship", Self::player_ship)
.with_fn("ships", Self::ships) .with_fn("ships", Self::ships)
.with_fn("objects", Self::objects) .with_fn("objects", Self::objects)
.with_fn("window_aspect", |s: &mut Self| s.window_aspect) .with_fn("window_aspect", |s: &mut Self| s.window_aspect);
.with_fn("camera_zoom", |s: &mut Self| s.input.camera_zoom);
} }
} }

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

View File

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

View File

@ -1,27 +1,28 @@
use glyphon::{ use glyphon::{
cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics, cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics,
Shaping, Style, TextArea, TextBounds, Weight, Shaping, Style, TextBounds, Weight,
}; };
use nalgebra::Vector2; use nalgebra::Vector2;
use rhai::ImmutableString; use rhai::ImmutableString;
use std::rc::Rc;
use winit::window::Window; use winit::window::Window;
use super::super::api::Rect; use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::api, RenderInput}; use crate::{ui::api, RenderInput};
#[derive(Debug)] #[derive(Debug)]
pub struct TextBox { pub struct UiTextBox {
pub name: ImmutableString, pub name: ImmutableString,
text: String, text: String,
justify: Align, justify: Align,
rect: Rect, rect: Rect,
buffer: Buffer, buffer: Rc<Buffer>,
color: api::Color, color: api::Color,
attrs: AttrsOwned, attrs: AttrsOwned,
} }
impl TextBox { impl UiTextBox {
pub fn new( pub fn new(
font: &mut FontSystem, font: &mut FontSystem,
name: ImmutableString, name: ImmutableString,
@ -38,7 +39,7 @@ impl TextBox {
Self { Self {
name, name,
rect, rect,
buffer, buffer: Rc::new(buffer),
color, color,
justify: Align::Left, justify: Align::Left,
attrs: AttrsOwned::new(Attrs::new()), attrs: AttrsOwned::new(Attrs::new()),
@ -47,14 +48,14 @@ impl TextBox {
} }
fn reflow(&mut self, font: &mut FontSystem) { fn reflow(&mut self, font: &mut FontSystem) {
self.buffer let buffer = Rc::get_mut(&mut self.buffer).unwrap();
.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced); 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)); 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) { pub fn set_text(&mut self, font: &mut FontSystem, text: &str) {
@ -84,11 +85,19 @@ impl TextBox {
} }
} }
impl<'a, 'b: 'a> TextBox { impl<'a, 'b: 'a> UiTextBox {
pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> TextArea<'a> { pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> OwnedTextArea {
let rect = self self.get_textarea_with_offset(input, window, Vector2::new(0.0, 0.0))
.rect }
.to_centered(window, input.ct.get_config().ui_scale);
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 // Glypon works with physical pixels, so we must do some conversion
let fac = window.scale_factor() as f32; 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 corner_sw = corner_ne + rect.dim * fac;
let c = self.color.as_array_u8(); let c = self.color.as_array_u8();
TextArea { OwnedTextArea {
buffer: &self.buffer, buffer: self.buffer.clone(),
top: corner_ne.y, top: corner_ne.y,
left: corner_ne.x, left: corner_ne.x,
scale: input.ct.get_config().ui_scale, scale: input.ct.config.ui_scale,
bounds: TextBounds { bounds: TextBounds {
top: (corner_ne.y) as i32, top: (corner_ne.y) as i32,
bottom: (corner_sw.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 anyhow::{Context, Result};
use galactica_content::Content; use galactica_content::Content;
use galactica_system::phys::PhysSimShipHandle; use galactica_system::{phys::PhysSimShipHandle, PlayerDirective};
use galactica_util::rhai_error_to_anyhow; use galactica_util::rhai_error_to_anyhow;
use log::debug; use log::{debug, error};
use rhai::{ use rhai::{Dynamic, Engine, ImmutableString, Scope};
packages::{BasicArrayPackage, BasicStringPackage, LogicPackage, MoreStringPackage, Package},
Dynamic, Engine, ImmutableString, Scope,
};
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc}; use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use winit::event::VirtualKeyCode;
use super::{ use super::{
api::{self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent}, api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent},
event::Event,
UiConfig, UiElement, UiState, UiConfig, UiElement, UiState,
}; };
use crate::{ui::api::State, RenderInput, RenderState}; use crate::{ui::api::State, InputEvent, RenderInput, RenderState};
pub(crate) struct UiScriptExecutor { pub(crate) struct UiScriptExecutor {
engine: Engine, engine: Engine,
@ -31,14 +28,8 @@ impl UiScriptExecutor {
let scope = Scope::new(); let scope = Scope::new();
let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state))); let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state)));
let mut engine = Engine::new_raw(); // TODO: document all functions rhai provides
let mut engine = Engine::new();
// 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());
engine.set_max_expr_depths(0, 0); engine.set_max_expr_depths(0, 0);
// Enables custom operators // Enables custom operators
@ -64,6 +55,129 @@ impl UiScriptExecutor {
(*self.state).borrow().config.clone() (*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 /// Change the current scene
pub fn set_scene(&mut self, state: &RenderState, input: Arc<RenderInput>) -> Result<()> { pub fn set_scene(&mut self, state: &RenderState, input: Arc<RenderInput>) -> Result<()> {
let current_scene = (*self.state).borrow().get_scene().clone(); let current_scene = (*self.state).borrow().get_scene().clone();
@ -87,12 +201,12 @@ impl UiScriptExecutor {
let mut elm = self.state.borrow_mut(); let mut elm = self.state.borrow_mut();
elm.clear(); elm.clear();
drop(elm); drop(elm);
let ct = (*self.state).borrow().ct.clone();
let ct = (*self.state).borrow().ct.clone();
rhai_error_to_anyhow( rhai_error_to_anyhow(
self.engine.call_fn( self.engine.call_fn(
&mut self.scope, &mut self.scope,
ct.get_config() ct.config
.ui_scenes .ui_scenes
.get(current_scene.as_ref().unwrap().as_str()) .get(current_scene.as_ref().unwrap().as_str())
.unwrap(), .unwrap(),
@ -114,7 +228,7 @@ impl UiScriptExecutor {
if (*self.state).borrow().get_scene().is_none() { if (*self.state).borrow().get_scene().is_none() {
(*self.state) (*self.state)
.borrow_mut() .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())?; self.set_scene(state, input.clone())?;
let current_scene = (*self.state).borrow().get_scene().clone(); let current_scene = (*self.state).borrow().get_scene().clone();
@ -122,9 +236,8 @@ impl UiScriptExecutor {
(*self.state).borrow_mut().step(state, input.clone()); (*self.state).borrow_mut().step(state, input.clone());
// Run step() (if it is defined) // Run step() (if it is defined)
let ast = ct let ast = ct
.get_config() .config
.ui_scenes .ui_scenes
.get(current_scene.as_ref().unwrap().as_str()) .get(current_scene.as_ref().unwrap().as_str())
.unwrap(); .unwrap();
@ -158,79 +271,25 @@ impl UiScriptExecutor {
true true
} }
} { } {
rhai_error_to_anyhow( self.run_event_callback(state, input.clone(), Dynamic::from(PlayerShipStateEvent {}))?;
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()))?;
} }
let len = (*self.state).borrow().len(); let len = (*self.state).borrow().len();
for i in 0..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) => { UiElement::Sprite(sprite) => {
// Draw and update sprites
sprite.step(&input, state);
sprite.push_to_buffer(&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) => { UiElement::RadialBar(x) => {
// Draw and update radialbar
x.step(&input, state);
x.push_to_buffer(&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 { UiElement::SubElement { .. } | UiElement::Text(..) => {}
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()))?;
} }
} }

View File

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

View File

@ -1,19 +1,39 @@
use galactica_content::Content; use galactica_content::Content;
use glyphon::TextArea;
use log::{debug, error}; use log::{debug, error};
use rhai::ImmutableString; use rhai::ImmutableString;
use std::collections::HashMap; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, time::Instant};
use std::sync::Arc;
use winit::window::Window; 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}; use crate::{RenderInput, RenderState};
#[derive(Debug)] #[derive(Debug)]
pub enum UiElement { pub enum UiElement {
Sprite(Sprite), Sprite(UiSprite),
RadialBar(RadialBar), RadialBar(UiRadialBar),
Text(TextBox), 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)] #[derive(Clone, Debug)]
@ -31,8 +51,13 @@ pub(crate) struct UiState {
show_timings: bool, show_timings: bool,
fps_indicator: FpsIndicator, fps_indicator: FpsIndicator,
last_step: Instant,
pub config: UiConfig, pub config: UiConfig,
/// The player's camera.
/// Only used when drawing physics.
pub camera: Camera,
} }
// TODO: remove this // TODO: remove this
unsafe impl Send for UiState {} unsafe impl Send for UiState {}
@ -52,6 +77,8 @@ impl UiState {
show_phys: false, show_phys: false,
show_starfield: 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) { 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"); error!("tried to switch to ui scene `{scene}`, which doesn't exist");
return; return;
} }
@ -105,15 +132,57 @@ impl UiState {
} }
pub fn step(&mut self, state: &mut RenderState, input: Arc<RenderInput>) { 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 { if self.show_timings {
self.fps_indicator self.fps_indicator
.step(&input, &mut state.text_font_system.borrow_mut()); .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 { 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); let mut v = Vec::with_capacity(32);
if self.current_scene.is_none() { if self.current_scene.is_none() {
@ -124,9 +193,10 @@ impl<'a> UiState {
v.push(self.fps_indicator.get_textarea(input, window)) v.push(self.fps_indicator.get_textarea(input, window))
} }
for t in self.elements.values() { for e in self.elements.values() {
match &t { match &e {
UiElement::Text(x) => v.push(x.get_textarea(input, window)), 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 } nalgebra = { workspace = true }
crossbeam = { workspace = true } crossbeam = { workspace = true }
rand = { 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 /// Possible outcomes when adding an outfit
pub enum OutfitAddResult { pub enum OutfitAddResult {
@ -34,7 +34,7 @@ pub enum OutfitRemoveResult {
/// A simple data class, used to keep track of delayed shield generators /// A simple data class, used to keep track of delayed shield generators
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct ShieldGenerator { pub(crate) struct ShieldGenerator {
pub outfit: OutfitHandle, pub outfit: Arc<Outfit>,
pub delay: f32, pub delay: f32,
pub generation: f32, pub generation: f32,
} }
@ -46,7 +46,7 @@ pub(crate) struct ShieldGenerator {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OutfitSet { pub struct OutfitSet {
/// What outfits does this statsum contain? /// What outfits does this statsum contain?
outfits: HashMap<OutfitHandle, u32>, outfits: HashMap<ContentIndex, (Arc<Outfit>, u32)>,
/// Space available in this outfitset. /// Space available in this outfitset.
/// set at creation and never changes. /// set at creation and never changes.
@ -59,7 +59,7 @@ pub struct OutfitSet {
/// The gun points available in this ship. /// The gun points available in this ship.
/// If value is None, this point is free. /// If value is None, this point is free.
/// if value is Some, this point is taken. /// if value is Some, this point is taken.
gun_points: HashMap<GunPoint, Option<OutfitHandle>>, gun_points: HashMap<GunPoint, Option<Arc<Outfit>>>,
/// Outfit values /// Outfit values
/// This isn't strictly necessary, but we don't want to /// 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) { if !(self.total_space - self.used_space).can_contain(&o.space) {
return OutfitAddResult::NotEnoughSpace("TODO".to_string()); return OutfitAddResult::NotEnoughSpace("TODO".to_string());
} }
@ -100,7 +100,7 @@ impl OutfitSet {
let mut added = false; let mut added = false;
for (_, outfit) in &mut self.gun_points { for (_, outfit) in &mut self.gun_points {
if outfit.is_none() { if outfit.is_none() {
*outfit = Some(o.handle); *outfit = Some(o.clone());
added = true; added = true;
} }
} }
@ -115,29 +115,29 @@ impl OutfitSet {
self.steer_power += o.steer_power; self.steer_power += o.steer_power;
self.shield_strength += o.shield_strength; self.shield_strength += o.shield_strength;
self.shield_generators.push(ShieldGenerator { self.shield_generators.push(ShieldGenerator {
outfit: o.handle, outfit: o.clone(),
delay: o.shield_delay, delay: o.shield_delay,
generation: o.shield_generation, generation: o.shield_generation,
}); });
if self.outfits.contains_key(&o.handle) { if self.outfits.contains_key(&o.index) {
*self.outfits.get_mut(&o.handle).unwrap() += 1; self.outfits.get_mut(&o.index).unwrap().1 += 1;
} else { } else {
self.outfits.insert(o.handle, 1); self.outfits.insert(o.index.clone(), (o.clone(), 1));
} }
return OutfitAddResult::Ok; return OutfitAddResult::Ok;
} }
pub(super) fn remove(&mut self, o: &Outfit) -> OutfitRemoveResult { pub(super) fn remove(&mut self, o: &Arc<Outfit>) -> OutfitRemoveResult {
if !self.outfits.contains_key(&o.handle) { if !self.outfits.contains_key(&o.index) {
return OutfitRemoveResult::NotExist; return OutfitRemoveResult::NotExist;
} else { } else {
let n = *self.outfits.get(&o.handle).unwrap(); let n = self.outfits.get_mut(&o.index).unwrap();
if n == 1u32 { if n.1 == 1u32 {
self.outfits.remove(&o.handle); self.outfits.remove(&o.index);
} else { } 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 let index = self
.shield_generators .shield_generators
.iter() .iter()
.position(|g| g.outfit == o.handle) .position(|g| g.outfit.index == o.index)
.unwrap(); .unwrap();
self.shield_generators.remove(index); self.shield_generators.remove(index);
} }
@ -165,16 +165,16 @@ impl OutfitSet {
impl OutfitSet { impl OutfitSet {
/// The number of outfits in this set /// The number of outfits in this set
pub fn len(&self) -> u32 { pub fn len(&self) -> u32 {
self.outfits.iter().map(|(_, x)| x).sum() self.outfits.iter().map(|(_, (_, x))| x).sum()
} }
/// Iterate over all outfits /// Iterate over all outfits
pub fn iter_outfits(&self) -> impl Iterator<Item = (&OutfitHandle, &u32)> { pub fn iter_outfits(&self) -> impl Iterator<Item = &(Arc<Outfit>, u32)> {
self.outfits.iter() self.outfits.values()
} }
/// Iterate over all gun points /// 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() self.gun_points.iter()
} }
@ -190,7 +190,7 @@ impl OutfitSet {
/// Get the outfit attached to the given gun point /// Get the outfit attached to the given gun point
/// Will panic if this gunpoint is not in this outfit set. /// 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() 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 nalgebra::Isometry2;
use rand::{rngs::ThreadRng, Rng}; 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}; use super::{OutfitSet, ShipAutoPilot, ShipPersonality, ShipState};
@ -9,8 +9,8 @@ use super::{OutfitSet, ShipAutoPilot, ShipPersonality, ShipState};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShipData { pub struct ShipData {
// Metadata values // Metadata values
ct_handle: ShipHandle, ship: Arc<Ship>,
faction: FactionHandle, faction: Arc<Faction>,
outfits: OutfitSet, outfits: OutfitSet,
personality: ShipPersonality, personality: ShipPersonality,
@ -34,16 +34,14 @@ pub struct ShipData {
impl ShipData { impl ShipData {
/// Create a new ShipData /// Create a new ShipData
pub(crate) fn new( pub(crate) fn new(
ct: &Content, ship: Arc<Ship>,
ct_handle: ShipHandle, faction: Arc<Faction>,
faction: FactionHandle,
personality: ShipPersonality, personality: ShipPersonality,
) -> Self { ) -> Self {
let s = ct.get_ship(ct_handle);
ShipData { ShipData {
ct_handle, ship: ship.clone(),
faction, faction,
outfits: OutfitSet::new(s.space, &s.guns), outfits: OutfitSet::new(ship.space, &ship.guns),
personality, personality,
last_hit: Instant::now(), last_hit: Instant::now(),
rng: rand::thread_rng(), rng: rand::thread_rng(),
@ -54,9 +52,9 @@ impl ShipData {
}, },
// Initial stats // Initial stats
hull: s.hull, hull: ship.hull,
shields: 0.0, 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. /// That is the simulation's responsiblity.
/// ///
/// Will panic if we're not flying. /// 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 { match self.state {
ShipState::Flying { .. } => { ShipState::Flying { .. } => {
self.state = ShipState::Landing { self.state = ShipState::Landing {
target: target_handle, target,
current_z: 1.0, current_z: 1.0,
}; };
} }
@ -108,9 +106,11 @@ impl ShipData {
/// Finish landing sequence /// Finish landing sequence
/// Will panic if we're not landing /// Will panic if we're not landing
pub fn finish_land_on(&mut self) { pub fn finish_land_on(&mut self) {
match self.state { match &self.state {
ShipState::Landing { target, .. } => { 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!") unreachable!("Called `finish_land_on` on a ship that isn't landing!")
@ -123,14 +123,13 @@ impl ShipData {
/// That is the simulation's responsiblity. /// That is the simulation's responsiblity.
/// ///
/// Will panic if we're not flying. /// Will panic if we're not flying.
pub fn start_unland_to(&mut self, ct: &Content, to_position: Isometry2<f32>) { pub fn start_unland_to(&mut self, to_position: Isometry2<f32>) {
match self.state { match &self.state {
ShipState::Landed { target } => { ShipState::Landed { target } => {
let obj = ct.get_system_object(target);
self.state = ShipState::UnLanding { self.state = ShipState::UnLanding {
to_position, to_position,
from: target, from: target.clone(),
current_z: obj.pos.z, current_z: target.pos.z,
}; };
} }
_ => { _ => {
@ -177,14 +176,14 @@ impl ShipData {
} }
/// Add an outfit to this ship /// 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); let r = self.outfits.add(o);
self.shields = self.outfits.get_total_shields(); self.shields = self.outfits.get_total_shields();
return r; return r;
} }
/// Remove an outfit from this ship /// 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) self.outfits.remove(o)
} }
@ -192,16 +191,14 @@ impl ShipData {
/// Will panic if `which` isn't a point on this ship. /// Will panic if `which` isn't a point on this ship.
/// Returns `true` if this gun was fired, /// Returns `true` if this gun was fired,
/// and `false` if it is on cooldown or empty. /// 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(); let c = self.gun_cooldowns.get_mut(which).unwrap();
if *c > 0.0 { if *c > 0.0 {
return false; return false;
} }
let g = self.outfits.get_gun(which); if let Some(g) = self.outfits.get_gun(which) {
if g.is_some() {
let g = ct.get_outfit(g.unwrap());
let gun = g.gun.as_ref().unwrap(); let gun = g.gun.as_ref().unwrap();
*c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng)); *c = 0f32.max(gun.rate + self.rng.gen_range(-gun.rate_rng..=gun.rate_rng));
return true; return true;
@ -288,13 +285,12 @@ impl ShipData {
&self.state &self.state
} }
/// Get a handle to this ship's content /// Get this ship's content
pub fn get_content(&self) -> ShipHandle { pub fn get_content(&self) -> &Arc<Ship> {
self.ct_handle &self.ship
} }
/// Get this ship's current hull. /// Get this ship's current hull
/// Use content handle to get maximum hull
pub fn get_hull(&self) -> f32 { pub fn get_hull(&self) -> f32 {
self.hull self.hull
} }
@ -316,12 +312,7 @@ impl ShipData {
} }
/// Get this ship's faction /// Get this ship's faction
pub fn get_faction(&self) -> FactionHandle { pub fn get_faction(&self) -> &Arc<Faction> {
self.faction &self.faction
}
/// Get this ship's content handle
pub fn get_ship(&self) -> ShipHandle {
self.ct_handle
} }
} }

View File

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

View File

@ -6,3 +6,6 @@
pub mod data; pub mod data;
pub mod phys; 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 nalgebra::{Point2, Rotation2, Vector2};
use rand::Rng; use rand::Rng;
use rapier2d::dynamics::{RevoluteJointBuilder, RigidBodyBuilder, RigidBodyHandle, RigidBodyType}; use rapier2d::dynamics::{RevoluteJointBuilder, RigidBodyBuilder, RigidBodyHandle, RigidBodyType};
use std::sync::Arc;
use crate::phys::{PhysStepResources, PhysWrapper}; use crate::phys::{PhysStepResources, PhysWrapper};
@ -32,16 +33,13 @@ pub struct PhysEffect {
impl PhysEffect { impl PhysEffect {
/// Create a new effect inside `Wrapper` /// Create a new effect inside `Wrapper`
pub fn new( pub fn new(
ct: &Content,
wrapper: &mut PhysWrapper, wrapper: &mut PhysWrapper,
effect: EffectHandle, effect: Arc<Effect>,
// Where to spawn the particle, in world space. // Where to spawn the particle, in world space.
pos: Vector2<f32>, pos: Vector2<f32>,
parent: RigidBodyHandle, parent: RigidBodyHandle,
target: Option<RigidBodyHandle>, target: Option<RigidBodyHandle>,
) -> Self { ) -> Self {
let effect = ct.get_effect(effect);
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let parent_body = wrapper.get_rigid_body(parent).unwrap(); let parent_body = wrapper.get_rigid_body(parent).unwrap();
let parent_angle = parent_body.rotation().angle(); let parent_angle = parent_body.rotation().angle();
@ -113,7 +111,7 @@ impl PhysEffect {
}; };
PhysEffect { PhysEffect {
anim: SpriteAutomaton::new(ct, effect.sprite), anim: SpriteAutomaton::new(effect.sprite.clone()),
rigid_body, rigid_body,
lifetime: 0f32.max( lifetime: 0f32.max(
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng), effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
@ -153,7 +151,7 @@ impl PhysEffect {
); );
PhysEffect { PhysEffect {
anim: SpriteAutomaton::new(ct, effect.sprite), anim: SpriteAutomaton::new(effect.sprite.clone()),
rigid_body, rigid_body,
lifetime: 0f32.max( lifetime: 0f32.max(
effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng), effect.lifetime + rng.gen_range(-effect.lifetime_rng..=effect.lifetime_rng),
@ -174,7 +172,7 @@ impl PhysEffect {
return; return;
} }
self.anim.step(&res.ct, res.t); self.anim.step(res.t);
self.lifetime -= res.t; self.lifetime -= res.t;
if self.lifetime <= 0.0 { 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 rand::Rng;
use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle}; use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle};
@ -10,7 +12,7 @@ use super::PhysEffect;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PhysProjectile { pub struct PhysProjectile {
/// This projectile's game data /// This projectile's game data
pub content: Projectile, pub content: Arc<Projectile>,
/// This projectile's sprite animation state /// This projectile's sprite animation state
anim: SpriteAutomaton, anim: SpriteAutomaton,
@ -19,7 +21,7 @@ pub struct PhysProjectile {
lifetime: f32, lifetime: f32,
/// The faction this projectile belongs to /// The faction this projectile belongs to
pub faction: FactionHandle, pub faction: Arc<Faction>,
/// This projectile's rigidbody /// This projectile's rigidbody
pub rigid_body: RigidBodyHandle, pub rigid_body: RigidBodyHandle,
@ -38,17 +40,16 @@ pub struct PhysProjectile {
impl PhysProjectile { impl PhysProjectile {
/// Create a new projectile /// Create a new projectile
pub fn new( pub fn new(
ct: &Content, content: Arc<Projectile>,
content: Projectile, // TODO: use a handle?
rigid_body: RigidBodyHandle, rigid_body: RigidBodyHandle,
faction: FactionHandle, faction: Arc<Faction>,
collider: ColliderHandle, collider: ColliderHandle,
) -> Self { ) -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let size_rng = content.size_rng; let size_rng = content.size_rng;
let lifetime = content.lifetime; let lifetime = content.lifetime;
PhysProjectile { PhysProjectile {
anim: SpriteAutomaton::new(ct, content.sprite), anim: SpriteAutomaton::new(content.sprite.clone()),
rigid_body, rigid_body,
collider, collider,
content, content,
@ -67,31 +68,24 @@ impl PhysProjectile {
wrapper: &mut PhysWrapper, wrapper: &mut PhysWrapper,
) { ) {
self.lifetime -= res.t; self.lifetime -= res.t;
self.anim.step(&res.ct, res.t); self.anim.step(res.t);
if self.lifetime <= 0.0 { if self.lifetime <= 0.0 {
self.destroy(res, new, wrapper, true); self.destroy(new, wrapper, true);
} }
} }
/// Destroy this projectile without creating an expire effect /// Destroy this projectile without creating an expire effect
pub(in crate::phys) fn destroy_silent( pub(in crate::phys) fn destroy_silent(
&mut self, &mut self,
res: &PhysStepResources,
new: &mut NewObjects, new: &mut NewObjects,
wrapper: &mut PhysWrapper, wrapper: &mut PhysWrapper,
) { ) {
self.destroy(res, new, wrapper, false); self.destroy(new, wrapper, false);
} }
/// Destroy this projectile /// Destroy this projectile
fn destroy( fn destroy(&mut self, new: &mut NewObjects, wrapper: &mut PhysWrapper, expire: bool) {
&mut self,
res: &PhysStepResources,
new: &mut NewObjects,
wrapper: &mut PhysWrapper,
expire: bool,
) {
if self.is_destroyed { if self.is_destroyed {
return; return;
} }
@ -100,11 +94,10 @@ impl PhysProjectile {
if expire { if expire {
match &self.content.expire_effect { match &self.content.expire_effect {
None => {} None => {}
Some(handle) => { Some(effect) => {
new.effects.push(PhysEffect::new( new.effects.push(PhysEffect::new(
&res.ct,
wrapper, wrapper,
*handle, effect.clone(),
*rb.translation(), *rb.translation(),
self.rigid_body, self.rigid_body,
None, None,

View File

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

View File

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

View File

@ -1,6 +1,8 @@
use std::sync::Arc;
use galactica_content::{ use galactica_content::{
AnimationState, Content, EnginePoint, FactionHandle, GunPoint, OutfitHandle, AnimationState, EnginePoint, Faction, GunPoint, Outfit, ProjectileCollider, Ship,
ProjectileCollider, ShipHandle, SpriteAutomaton, SpriteAutomaton,
}; };
use nalgebra::{vector, Point2, Rotation2, Vector2}; use nalgebra::{vector, Point2, Rotation2, Vector2};
use rand::Rng; use rand::Rng;
@ -66,24 +68,22 @@ pub struct PhysShip {
impl PhysShip { impl PhysShip {
/// Make a new ship /// Make a new ship
pub(crate) fn new( pub(crate) fn new(
ct: &Content, ship: Arc<Ship>,
handle: ShipHandle,
personality: ShipPersonality, personality: ShipPersonality,
faction: FactionHandle, faction: Arc<Faction>,
rigid_body: RigidBodyHandle, rigid_body: RigidBodyHandle,
collider: ColliderHandle, collider: ColliderHandle,
) -> Self { ) -> Self {
let ship_ct = ct.get_ship(handle);
PhysShip { PhysShip {
uid: get_phys_id(), uid: get_phys_id(),
anim: SpriteAutomaton::new(ct, ship_ct.sprite), anim: SpriteAutomaton::new(ship.sprite.clone()),
rigid_body, rigid_body,
collider, collider,
data: ShipData::new(ct, handle, faction, personality), data: ShipData::new(ship.clone(), faction, personality),
engine_anim: Vec::new(), engine_anim: Vec::new(),
controls: ShipControls::new(), controls: ShipControls::new(),
last_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, is_destroyed: false,
controller: match personality { controller: match personality {
ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(), ShipPersonality::Dummy | ShipPersonality::Player => ShipController::new_null(),
@ -105,32 +105,28 @@ impl PhysShip {
} }
self.data.step(res.t); self.data.step(res.t);
self.anim.step(&res.ct, res.t); self.anim.step(res.t);
for (_, e) in &mut self.engine_anim { for (_, e) in &mut self.engine_anim {
e.step(&res.ct, res.t); e.step(res.t);
} }
// Flare animations // Flare animations
if !self.controls.thrust && self.last_controls.thrust { if !self.controls.thrust && self.last_controls.thrust {
let flare = self.get_flare(&res.ct); let flare = self.get_flare();
if flare.is_some() { if let Some(flare) = flare {
let flare_outfit = flare.unwrap();
let flare = res.ct.get_outfit(flare_outfit);
if flare.engine_flare_on_stop.is_some() { if flare.engine_flare_on_stop.is_some() {
for (_, e) in &mut self.engine_anim { 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 { } else if self.controls.thrust && !self.last_controls.thrust {
let flare = self.get_flare(&res.ct); let flare = self.get_flare();
if flare.is_some() { if let Some(flare) = flare {
let flare_outfit = flare.unwrap();
let flare = res.ct.get_outfit(flare_outfit);
if flare.engine_flare_on_start.is_some() { if flare.engine_flare_on_start.is_some() {
for (_, e) in &mut self.engine_anim { 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 // Compute new controls
let controls = match autopilot { let controls = match autopilot {
ShipAutoPilot::Landing { target } => { ShipAutoPilot::Landing { target } => {
let target_obj = res.ct.get_system_object(*target);
let controls = autopilot::auto_landing( let controls = autopilot::auto_landing(
res, res,
img, img,
PhysSimShipHandle(self.collider), 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. // Try to land the ship.
@ -176,7 +171,7 @@ impl PhysShip {
let landed = 'landed: { let landed = 'landed: {
let r = wrapper.get_rigid_body(self.rigid_body).unwrap(); 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( let s_pos = Vector2::new(
r.position().translation.x, r.position().translation.x,
r.position().translation.y, r.position().translation.y,
@ -186,13 +181,13 @@ impl PhysShip {
// Can't just set_active(false), since we still need that collider's mass. // Can't just set_active(false), since we still need that collider's mass.
// We're in land range... // 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; break 'landed false;
} }
// And we'll stay in land range long enough. // And we'll stay in land range long enough.
if (t_pos - (s_pos + r.velocity_at_point(r.center_of_mass()) * 2.0)) 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; break 'landed false;
} }
@ -202,7 +197,7 @@ impl PhysShip {
Group::GROUP_1, Group::GROUP_1,
Group::empty(), Group::empty(),
)); ));
self.data.start_land_on(*target); self.data.start_land_on(target.clone());
break 'landed true; break 'landed true;
}; };
@ -229,7 +224,7 @@ impl PhysShip {
if self.controls.guns { if self.controls.guns {
// TODO: don't allocate here. This is a hack to satisfy the borrow checker, // TODO: don't allocate here. This is a hack to satisfy the borrow checker,
// convert this to a refcell or do the replace dance. // 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 .data
.get_outfits() .get_outfits()
.iter_gun_points() .iter_gun_points()
@ -237,7 +232,7 @@ impl PhysShip {
.collect(); .collect();
for (gun_point, outfit) in pairs { 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 outfit = outfit.unwrap();
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -250,20 +245,17 @@ impl PhysShip {
rigid_body.velocity_at_point(rigid_body.center_of_mass()); rigid_body.velocity_at_point(rigid_body.center_of_mass());
let pos = ship_pos + (ship_rot * gun_point.pos); let pos = ship_pos + (ship_rot * gun_point.pos);
let gun = outfit.gun.as_ref().unwrap();
let outfit = res.ct.get_outfit(outfit); let spread =
let outfit = outfit.gun.as_ref().unwrap(); rng.gen_range(-gun.projectile.angle_rng..=gun.projectile.angle_rng);
let spread = rng.gen_range(
-outfit.projectile.angle_rng..=outfit.projectile.angle_rng,
);
let vel = ship_vel let vel = ship_vel
+ (Rotation2::new(ship_ang + spread) + (Rotation2::new(ship_ang + spread)
* Vector2::new( * Vector2::new(
outfit.projectile.speed gun.projectile.speed
+ rng.gen_range( + rng.gen_range(
-outfit.projectile.speed_rng -gun.projectile.speed_rng
..=outfit.projectile.speed_rng, ..=gun.projectile.speed_rng,
), ),
0.0, 0.0,
)); ));
@ -274,7 +266,7 @@ impl PhysShip {
.linvel(vel) .linvel(vel)
.build(); .build();
let mut collider = match &outfit.projectile.collider { let mut collider = match &gun.projectile.collider {
ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius) ProjectileCollider::Ball(b) => ColliderBuilder::ball(b.radius)
.sensor(true) .sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS) .active_events(ActiveEvents::COLLISION_EVENTS)
@ -290,10 +282,9 @@ impl PhysShip {
let collider = wrapper.insert_collider(collider, rigid_body); let collider = wrapper.insert_collider(collider, rigid_body);
new.projectiles.push(PhysProjectile::new( new.projectiles.push(PhysProjectile::new(
&res.ct, gun.projectile.clone(),
outfit.projectile.clone(),
rigid_body, rigid_body,
self.data.get_faction(), self.data.get_faction().clone(),
collider, collider,
)); ));
} }
@ -306,8 +297,6 @@ impl PhysShip {
current_z, current_z,
from, from,
} => { } => {
let from_obj = res.ct.get_system_object(*from);
let controls = autopilot::auto_landing( let controls = autopilot::auto_landing(
&res, &res,
img, img,
@ -315,7 +304,7 @@ impl PhysShip {
Vector2::new(to_position.translation.x, to_position.translation.y), Vector2::new(to_position.translation.x, to_position.translation.y),
); );
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap(); 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)) - Vector2::new(to_position.translation.x, to_position.translation.y))
.magnitude(); .magnitude();
let now_d = (r.translation() let now_d = (r.translation()
@ -324,7 +313,7 @@ impl PhysShip {
let f = now_d / max_d; let f = now_d / max_d;
let current_z = *current_z; 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 { if current_z <= 1.0 {
// Finish unlanding ship // Finish unlanding ship
@ -351,18 +340,17 @@ impl PhysShip {
} }
ShipState::Landing { target, current_z } => { ShipState::Landing { target, current_z } => {
let target_obj = res.ct.get_system_object(*target);
let controls = autopilot::auto_landing( let controls = autopilot::auto_landing(
&res, &res,
img, img,
PhysSimShipHandle(self.collider), 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 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 // Finish landing ship
self.data.finish_land_on(); self.data.finish_land_on();
let r = wrapper.get_rigid_body_mut(self.rigid_body).unwrap(); 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 rigid_body = wrapper.get_rigid_body(self.rigid_body).unwrap().clone();
let collider = wrapper.get_collider(self.collider).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_pos = rigid_body.translation();
let ship_rot = rigid_body.rotation(); let ship_rot = rigid_body.rotation();
let ship_ang = ship_rot.angle(); let ship_ang = ship_rot.angle();
@ -451,7 +439,7 @@ impl PhysShip {
while !a { while !a {
x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0; x = rng.gen_range(-1.0..=1.0) * ship_content.size / 2.0;
y = rng.gen_range(-1.0..=1.0) y = rng.gen_range(-1.0..=1.0)
* ship_content.size * res.ct.get_sprite(ship_content.sprite).aspect * ship_content.size * ship_content.sprite.aspect
/ 2.0; / 2.0;
a = collider.shape().contains_local_point(&Point2::new(x, y)); 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); let pos = ship_pos + (Rotation2::new(ship_ang) * pos);
new.effects.push(PhysEffect::new( new.effects.push(PhysEffect::new(
&res.ct,
wrapper, wrapper,
e.effect, e.effect.clone(),
pos, pos,
self.rigid_body, self.rigid_body,
None, None,
@ -476,12 +463,11 @@ impl PhysShip {
/// Public mutable /// Public mutable
impl PhysShip { 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 // TODO: better way to pick flare sprite
for (h, _) in self.data.get_outfits().iter_outfits() { for (h, _) in self.data.get_outfits().iter_outfits() {
let c = ct.get_outfit(*h); if h.engine_flare_sprite.is_some() {
if c.engine_flare_sprite.is_some() { return Some(h.clone());
return Some(*h);
} }
} }
return None; return None;
@ -489,37 +475,47 @@ impl PhysShip {
/// Re-create this ship's engine flare animations /// Re-create this ship's engine flare animations
/// Should be called whenever we change outfits /// Should be called whenever we change outfits
fn update_flares(&mut self, ct: &Content) { fn update_flares(&mut self) {
let flare_outfit = self.get_flare(ct); let flare = self.get_flare();
if flare_outfit.is_none() { if flare.is_none() {
self.engine_anim = Vec::new(); self.engine_anim = Vec::new();
return; return;
} }
let flare = ct
.get_outfit(flare_outfit.unwrap())
.engine_flare_sprite
.unwrap();
self.engine_anim = ct self.engine_anim = self
.get_ship(self.data.get_content()) .data
.get_content()
.engines .engines
.iter() .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(); .collect();
} }
/// Add one outfit to this ship /// Add one outfit to this ship
pub fn add_outfit(&mut self, ct: &Content, o: OutfitHandle) { pub fn add_outfit(&mut self, o: Arc<Outfit>) {
self.data.add_outfit(ct.get_outfit(o)); self.data.add_outfit(&o);
self.update_flares(ct); self.update_flares();
} }
/// Add many outfits to this ship /// 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 { 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::Relationship;
use galactica_content::{Content, FactionHandle, ShipHandle, SystemHandle}; use galactica_content::Ship;
use galactica_playeragent::PlayerAgent; use galactica_playeragent::PlayerAgent;
use log::error;
use nalgebra::{Isometry2, Point2, Rotation2, Vector2}; use nalgebra::{Isometry2, Point2, Rotation2, Vector2};
use rand::Rng; use rand::Rng;
use rapier2d::{ use rapier2d::{
@ -8,6 +10,7 @@ use rapier2d::{
geometry::{ColliderHandle, Group, InteractionGroups}, geometry::{ColliderHandle, Group, InteractionGroups},
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use super::PhysEffectImage; use super::PhysEffectImage;
use super::{ use super::{
@ -15,6 +18,7 @@ use super::{
PhysImage, PhysProjectileImage, PhysShipImage, PhysStepResources, PhysWrapper, PhysImage, PhysProjectileImage, PhysShipImage, PhysStepResources, PhysWrapper,
}; };
use crate::data::{ShipAutoPilot, ShipPersonality, ShipState}; use crate::data::{ShipAutoPilot, ShipPersonality, ShipState};
use crate::PlayerDirective;
// TODO: replace with a more generic handle // TODO: replace with a more generic handle
/// A handle for a ship in this simulation /// A handle for a ship in this simulation
@ -49,13 +53,8 @@ impl NewObjects {
/// Manages the physics state of one system /// Manages the physics state of one system
pub struct PhysSim { pub struct PhysSim {
/// The system this sim is attached to
_system: SystemHandle,
wrapper: PhysWrapper, wrapper: PhysWrapper,
new: NewObjects, new: NewObjects,
effects: Vec<PhysEffect>, effects: Vec<PhysEffect>,
projectiles: HashMap<ColliderHandle, PhysProjectile>, projectiles: HashMap<ColliderHandle, PhysProjectile>,
ships: HashMap<ColliderHandle, PhysShip>, ships: HashMap<ColliderHandle, PhysShip>,
@ -63,10 +62,9 @@ pub struct PhysSim {
// Private methods // Private methods
impl PhysSim { 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 ship = self.ships.get_mut(&collider).unwrap();
let obj = ship.data.get_state().landed_on().unwrap(); let obj = ship.data.get_state().landed_on().unwrap();
let obj = ct.get_system_object(obj);
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let radius = rng.gen_range(500.0..=1500.0); 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_trans = Vector2::new(obj.pos.x, obj.pos.y) + target_offset;
let target_pos = Isometry2::new(target_trans, angle); 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(); let r = self.wrapper.get_rigid_body_mut(ship.rigid_body).unwrap();
r.set_enabled(true); r.set_enabled(true);
@ -87,7 +85,6 @@ impl PhysSim {
pub(super) fn collide_projectile_ship( pub(super) fn collide_projectile_ship(
&mut self, &mut self,
res: &mut PhysStepResources,
projectile_h: ColliderHandle, projectile_h: ColliderHandle,
ship_h: ColliderHandle, ship_h: ColliderHandle,
) { ) {
@ -99,8 +96,11 @@ impl PhysSim {
let projectile = projectile.unwrap(); let projectile = projectile.unwrap();
let ship = ship.unwrap(); let ship = ship.unwrap();
let f = res.ct.get_faction(projectile.faction); let r = projectile
let r = f.relationships.get(&ship.data.get_faction()).unwrap(); .faction
.relationships
.get(&ship.data.get_faction().index)
.unwrap();
let destory_projectile = match r { let destory_projectile = match r {
Relationship::Hostile => match ship.data.get_state() { Relationship::Hostile => match ship.data.get_state() {
ShipState::Flying { .. } => { ShipState::Flying { .. } => {
@ -131,9 +131,8 @@ impl PhysSim {
None => {} None => {}
Some(x) => { Some(x) => {
self.effects.push(PhysEffect::new( self.effects.push(PhysEffect::new(
&res.ct,
&mut self.wrapper, &mut self.wrapper,
*x, x.clone(),
pos, pos,
projectile.rigid_body, projectile.rigid_body,
Some(ship.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 // Public methods
impl PhysSim { impl PhysSim {
/// Create a new physics system /// Create a new physics system
pub fn new(_ct: &Content, system: SystemHandle) -> Self { pub fn new() -> Self {
Self { Self {
_system: system,
wrapper: PhysWrapper::new(), wrapper: PhysWrapper::new(),
new: NewObjects::new(), new: NewObjects::new(),
effects: Vec::new(), effects: Vec::new(),
projectiles: HashMap::new(), projectiles: HashMap::new(),
@ -164,19 +161,17 @@ impl PhysSim {
/// Add a ship to this physics system /// Add a ship to this physics system
pub fn add_ship( pub fn add_ship(
&mut self, &mut self,
ct: &Content, ship: Arc<Ship>,
handle: ShipHandle, faction: Arc<Faction>,
faction: FactionHandle,
personality: ShipPersonality, personality: ShipPersonality,
position: Point2<f32>, position: Point2<f32>,
) -> PhysSimShipHandle { ) -> PhysSimShipHandle {
let ship_content = ct.get_ship(handle); let mut cl = ship.collider.0.clone();
let mut cl = ship_content.collider.0.clone();
// TODO: additonal ship mass from outfits and cargo // TODO: additonal ship mass from outfits and cargo
let rb = RigidBodyBuilder::dynamic() let rb = RigidBodyBuilder::dynamic()
.angular_damping(ship_content.angular_drag) .angular_damping(ship.angular_drag)
.linear_damping(ship_content.linear_drag) .linear_damping(ship.linear_drag)
.translation(Vector2::new(position.x, position.y)) .translation(Vector2::new(position.x, position.y))
.can_sleep(false); .can_sleep(false);
@ -190,14 +185,14 @@ impl PhysSim {
self.ships.insert( self.ships.insert(
collider, collider,
PhysShip::new(ct, handle, personality, faction, ridid_body, collider), PhysShip::new(ship, personality, faction, ridid_body, collider),
); );
return PhysSimShipHandle(collider); return PhysSimShipHandle(collider);
} }
/// Update a player ship's controls /// 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() { if player.ship.is_none() {
return; return;
} }
@ -211,35 +206,49 @@ impl PhysSim {
ShipState::Flying { ShipState::Flying {
autopilot: ShipAutoPilot::None, autopilot: ShipAutoPilot::None,
} => { } => match directive {
ship_object.controls.guns = player.input.pressed_guns(); PlayerDirective::None => {}
ship_object.controls.left = player.input.pressed_left(); PlayerDirective::Engine(x) => ship_object.controls.thrust = x,
ship_object.controls.right = player.input.pressed_right(); PlayerDirective::TurnLeft(x) => ship_object.controls.left = x,
ship_object.controls.thrust = player.input.pressed_thrust(); PlayerDirective::TurnRight(x) => ship_object.controls.right = x,
PlayerDirective::Guns(x) => ship_object.controls.guns = x,
if player.input.pressed_land() { PlayerDirective::Land => {
if let Some(target) = player.selection.get_planet() {
ship_object.data.set_autopilot(ShipAutoPilot::Landing { ship_object.data.set_autopilot(ShipAutoPilot::Landing {
target: player.selection.get_planet().unwrap(), target: target.clone(),
}) })
} }
} }
_ => {
error!("Got an invalid directive {directive:?} in shipstate `Flying`");
}
},
ShipState::Flying { .. } => { ShipState::Flying { .. } => match directive {
// Any input automatically releases autopilot PlayerDirective::None => {}
if player.input.pressed_left() PlayerDirective::Engine(_)
|| player.input.pressed_right() | PlayerDirective::TurnLeft(_)
|| player.input.pressed_thrust() | PlayerDirective::TurnRight(_)
|| player.input.pressed_guns() | PlayerDirective::Land
{ | PlayerDirective::Guns(_) => {
// Disable autopilot and apply action
ship_object.data.set_autopilot(ShipAutoPilot::None); ship_object.data.set_autopilot(ShipAutoPilot::None);
self.apply_directive(directive, player);
} }
_ => {
error!("Got an invalid directive {directive:?} in shipstate `Flying`");
} }
},
ShipState::Landed { .. } => { ShipState::Landed { .. } => match directive {
if player.input.pressed_land() { PlayerDirective::None => {}
self.start_unland_ship(ct, player.ship.unwrap()); 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() { if p.is_none() || s.is_none() {
continue; continue;
} }
self.collide_projectile_ship(&mut res, a, b); self.collide_projectile_ship(a, b);
} }
} }
res.timing.mark_physics_step(); 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,
}