Compare commits

..

No commits in common. "eca7e7f5e6469a689fa8d1b17d7c3e3a70937617" and "24f46a2471d3bac9c782d66b0f221d3e34cf3d10" have entirely different histories.

47 changed files with 1206 additions and 1709 deletions

View File

@ -76,9 +76,7 @@ log = "0.4.20"
log4rs = { version = "1.2.0", features = ["console_appender"] } log4rs = { version = "1.2.0", features = ["console_appender"] }
rhai = { version = "1.17.0", features = [ rhai = { version = "1.17.0", features = [
"f32_float", "f32_float",
"only_i32",
"metadata", "metadata",
"sync", "sync",
"no_custom_syntax", "no_custom_syntax",
"no_closure",
] } ] }

View File

@ -13,7 +13,6 @@
- Scriptable renderscene: script decides which parts to show - Scriptable renderscene: script decides which parts to show
- No UI zoom scroll - No UI zoom scroll
- Preserve aspect for icons - Preserve aspect for icons
- Check game version in config
## Small jobs ## Small jobs

View File

@ -39,8 +39,6 @@ zoom_max = 2000.0
# TODO: move to user config file # TODO: move to user config file
ui_scale = 2 ui_scale = 2
ui_landed_scene = "ui/landed.rhai"
start_ui_scene = "flying" ui_flying_scene = "ui/flying.rhai"
ui_scene.landed = "ui/landed.rhai" ui_outfitter_scene = "ui/outfitter.rhai"
ui_scene.flying = "ui/flying.rhai"
ui_scene.outfitter = "ui/outfitter.rhai"

View File

@ -28,7 +28,6 @@ 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.luna.sprite = "planet::luna" object.luna.sprite = "planet::luna"

View File

@ -1,55 +1,3 @@
fn config() { fn init(state) { return []; }
let config = SceneConfig(); fn hover(element, hover_state) {}
config.show_starfield(true); fn click(element, click_state) {}
config.show_phys(true);
return config
}
fn init(state) {
let ring = SpriteBuilder(
"ring",
"ui::status",
Rect(
-5.0, -5.0, 100.0, 100.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
)
);
let shield = RadialBuilder(
"shield", 2.5,
Color(0.3, 0.6, 0.8, 1.0),
Rect(
-9.5, -9.5, 91.0, 91.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
)
);
shield.set_progress(0.2);
let hull = RadialBuilder(
"hull", 2.5,
Color(0.8, 0.7, 0.5, 1.0),
Rect(
-13.5, -13.5, 83.0, 83.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
)
);
hull.set_progress(0.4);
return [
ring,
shield,
hull
];
}
fn event(state, event) {
if type_of(event) == "PlayerShipStateEvent" {
if state.player_ship().is_landed() {
return SceneAction::GoTo("landed");
}
return;
}
}

View File

@ -1,13 +1,5 @@
fn config() {
let config = SceneConfig();
config.show_starfield(true);
config.show_phys(false);
return config
}
fn init(state) { fn init(state) {
let player = state.player_ship();
let frame = SpriteBuilder( let frame = SpriteBuilder(
"frame", "frame",
"ui::planet", "ui::planet",
@ -20,13 +12,7 @@ fn init(state) {
let landscape = SpriteBuilder( let landscape = SpriteBuilder(
"landscape", "landscape",
{ state.planet_landscape,
if player.is_landed() {
player.landed_on().image();
} else {
"";
}
},
Rect( Rect(
-180.0, 142.0, 274.0, 135.0, -180.0, 142.0, 274.0, 135.0,
SpriteAnchor::NorthWest, SpriteAnchor::NorthWest,
@ -54,66 +40,32 @@ fn init(state) {
SpriteAnchor::Center SpriteAnchor::Center
) )
); );
if player.is_landed() { title.set_text(state.planet_name);
title.set_text(player.landed_on().name());
} else {
title.set_text("");
}
let desc = TextBoxBuilder(
"desc",
7.5, 8.0, TextBoxFont::SansSerif, TextBoxJustify::Left,
Rect(
-178.92, -20.3, 343.0, 81.467,
SpriteAnchor::NorthWest,
SpriteAnchor::Center
)
);
if player.is_landed() {
desc.set_text(player.landed_on().desc());
} else {
desc.set_text("");
}
return [ return [
button, button,
landscape, landscape,
frame, frame,
title, title,
desc,
]; ];
} }
fn event(state, event) { fn hover(element, hover_state) {
if type_of(event) == "MouseHoverEvent" { if element.has_name("button") {
let element = event.element(); if hover_state {
if element.has_name("button") { element.take_edge("on:top", 0.1);
if event.is_enter() { } else {
element.take_edge("on:top", 0.1); element.take_edge("off:top", 0.1);
} else {
element.take_edge("off:top", 0.1);
}
} }
return; }
} }
fn click(element, click_state) {
if type_of(event) == "MouseClickEvent" { if !click_state {
if !event.is_down() { return SceneAction::None;
return SceneAction::None; }
}
if element.has_name("button") {
let element = event.element(); return SceneAction::SceneOutfitter;
if element.has_name("button") {
return SceneAction::GoTo("outfitter");
}
return;
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
return SceneAction::GoTo("flying");
}
return;
} }
} }

View File

@ -1,9 +1,3 @@
fn config() {
let config = SceneConfig();
config.show_starfield(true);
config.show_phys(false);
return config
}
fn init(state) { fn init(state) {
let se_box = SpriteBuilder( let se_box = SpriteBuilder(
@ -25,7 +19,7 @@ fn init(state) {
SpriteAnchor::SouthWest SpriteAnchor::SouthWest
) )
); );
exit_text.set_text("Exit"); exit_text.set_text(state.planet_name);
let exit_button = SpriteBuilder( let exit_button = SpriteBuilder(
"exit_button", "exit_button",
@ -70,7 +64,7 @@ fn init(state) {
SpriteAnchor::NorthWest SpriteAnchor::NorthWest
) )
); );
ship_name.set_text("Hyperion"); ship_name.set_text(state.planet_name);
let ship_type = TextBoxBuilder( let ship_type = TextBoxBuilder(
"ship_type", "ship_type",
@ -81,11 +75,7 @@ fn init(state) {
SpriteAnchor::NorthWest SpriteAnchor::NorthWest
) )
); );
if state.player_ship().is_some() { ship_type.set_text(state.planet_name);
ship_type.set_text(state.player_ship().name());
} else {
ship_type.set_text("ERR: SHIP IS NONE");
}
let ship_stats = TextBoxBuilder( let ship_stats = TextBoxBuilder(
"ship_stats", "ship_stats",
@ -96,7 +86,11 @@ fn init(state) {
SpriteAnchor::NorthWest, SpriteAnchor::NorthWest,
) )
); );
ship_stats.set_text("Earth"); ship_stats.set_text(state.planet_name);
@ -129,7 +123,7 @@ fn init(state) {
SpriteAnchor::NorthEast, SpriteAnchor::NorthEast,
) )
); );
outfit_name.set_text("Earth"); outfit_name.set_text(state.planet_name);
let outfit_desc = TextBoxBuilder( let outfit_desc = TextBoxBuilder(
"outfit_desc", "outfit_desc",
@ -140,7 +134,7 @@ fn init(state) {
SpriteAnchor::NorthEast, SpriteAnchor::NorthEast,
) )
); );
outfit_desc.set_text("Earth"); outfit_desc.set_text(state.planet_name);
let outfit_stats = TextBoxBuilder( let outfit_stats = TextBoxBuilder(
"outfit_stats", "outfit_stats",
@ -151,7 +145,7 @@ fn init(state) {
SpriteAnchor::NorthEast, SpriteAnchor::NorthEast,
) )
); );
outfit_stats.set_text("Earth"); outfit_stats.set_text(state.planet_name);
return [ return [
ship_bg, ship_bg,
@ -172,37 +166,22 @@ fn init(state) {
]; ];
} }
fn hover(element, hover_state) {
fn event(state, event) { if element.has_name("exit_button") {
if type_of(event) == "MouseHoverEvent" { if hover_state {
let element = event.element(); element.take_edge("on:top", 0.1);
if element.has_name("exit_button") { } else {
if event.is_enter() { element.take_edge("off:top", 0.1);
element.take_edge("on:top", 0.1);
} else {
element.take_edge("off:top", 0.1);
}
} }
return; }
} }
fn click(element, click_state) {
if type_of(event) == "MouseClickEvent" { if !click_state {
if !event.is_down() { return SceneAction::None;
return SceneAction::None; }
}
if element.has_name("exit_button") {
let element = event.element(); return SceneAction::SceneLanded;
if element.has_name("exit_button") {
return SceneAction::GoTo("landed");
}
return;
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
return SceneAction::GoTo("flying");
}
return;
} }
} }

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, num::NonZeroU32, path::PathBuf}; use std::{num::NonZeroU32, path::PathBuf};
use rhai::AST; use rhai::AST;
@ -7,10 +7,7 @@ pub(crate) mod syntax {
use galactica_packer::SpriteAtlas; use galactica_packer::SpriteAtlas;
use rhai::Engine; use rhai::Engine;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::path::{Path, PathBuf};
collections::HashMap,
path::{Path, PathBuf},
};
// 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.
@ -23,8 +20,9 @@ pub(crate) mod syntax {
pub zoom_min: f32, pub zoom_min: f32,
pub zoom_max: f32, pub zoom_max: f32,
pub ui_scale: f32, pub ui_scale: f32,
pub ui_scene: HashMap<String, PathBuf>, pub ui_flying_scene: PathBuf,
pub start_ui_scene: String, pub ui_landed_scene: PathBuf,
pub ui_outfitter_scene: PathBuf,
} }
impl Config { impl Config {
@ -61,19 +59,18 @@ pub(crate) mod syntax {
}; };
let engine = Engine::new(); let engine = Engine::new();
let mut ui_scenes = HashMap::new(); let ui_landed_scene = engine
for (n, p) in self.ui_scene { .compile_file(content_root.join(self.ui_landed_scene))
ui_scenes.insert( .with_context(|| format!("while loading `landed` scene"))
n.clone(), .with_context(|| format!("while loading config"))?;
engine let ui_outfitter_scene = engine
.compile_file(content_root.join(p)) .compile_file(content_root.join(self.ui_outfitter_scene))
.with_context(|| format!("while loading scene script `{}`", n))?, .with_context(|| format!("while loading `outfitter` scene"))
); .with_context(|| format!("while loading config"))?;
} let ui_flying_scene = engine
.compile_file(content_root.join(self.ui_flying_scene))
if !ui_scenes.contains_key(&self.start_ui_scene) { .with_context(|| format!("while loading `flying` scene"))
bail!("starting ui scene `{}` doesn't exist", self.start_ui_scene) .with_context(|| format!("while loading config"))?;
}
return Ok(super::Config { return Ok(super::Config {
sprite_root: asset_root.join(self.sprite_root), sprite_root: asset_root.join(self.sprite_root),
@ -101,8 +98,9 @@ pub(crate) mod syntax {
zoom_max: self.zoom_max, zoom_max: self.zoom_max,
zoom_min: self.zoom_min, zoom_min: self.zoom_min,
ui_scale: self.ui_scale, ui_scale: self.ui_scale,
ui_scenes, ui_landed_scene,
start_ui_scene: self.start_ui_scene, ui_flying_scene,
ui_outfitter_scene,
}); });
} }
} }
@ -189,10 +187,12 @@ pub struct Config {
/// Ui scale factor /// Ui scale factor
pub ui_scale: f32, pub ui_scale: f32,
/// Ui scene scripts /// Ui landed scene script
pub ui_scenes: HashMap<String, AST>, pub ui_landed_scene: AST,
/// The UI scene we start in. /// Ui flying scene script
/// This is guaranteed to be a key in ui_scenes. pub ui_flying_scene: AST,
pub start_ui_scene: String,
/// Ui outfitter scene script
pub ui_outfitter_scene: AST,
} }

View File

@ -1,4 +1,4 @@
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};
@ -31,7 +31,6 @@ pub(crate) mod syntax {
pub landable: Option<bool>, pub landable: Option<bool>,
pub name: Option<String>, pub name: Option<String>,
pub desc: Option<String>, pub desc: Option<String>,
pub image: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -127,13 +126,10 @@ pub struct SystemObject {
pub landable: bool, pub landable: bool,
/// The display name of this object /// The display name of this object
pub name: Option<String>, pub name: String,
/// The description of this object (shown on landed ui) /// The description of this object
pub desc: Option<String>, pub desc: String,
/// This object's image (shown on landed ui)
pub image: Option<SpriteHandle>,
} }
/// Helper function for resolve_position, never called on its own. /// Helper function for resolve_position, never called on its own.
@ -224,43 +220,8 @@ impl crate::Build for System {
Some(t) => *t, Some(t) => *t,
}; };
let image_handle = match &obj.image {
Some(x) => match content.sprite_index.get(x) {
None => bail!(
"In system `{}`: sprite `{}` doesn't exist",
system_name,
obj.sprite
),
Some(t) => Some(*t),
},
None => None,
};
if obj.landable.unwrap_or(false) {
if obj.name.is_none() {
return Err(anyhow!("if an object is landable, it must have a name"))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
if obj.desc.is_none() {
return Err(anyhow!(
"if an object is landable, it must have a description"
))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
if obj.image.is_none() {
return Err(anyhow!("if an object is landable, it must have an image"))
.with_context(|| format!("in object labeled `{}`", label))
.with_context(|| format!("in system `{}`", system_name));
}
}
objects.push(SystemObject { objects.push(SystemObject {
sprite: sprite_handle, sprite: sprite_handle,
image: image_handle,
pos: resolve_position(&system.object, &obj, cycle_detector) pos: resolve_position(&system.object, &obj, cycle_detector)
.with_context(|| format!("in object {:#?}", label))?, .with_context(|| format!("in object {:#?}", label))?,
size: obj.size, size: obj.size,
@ -270,13 +231,19 @@ impl crate::Build for System {
body_index: 0, body_index: 0,
}, },
landable: obj.landable.unwrap_or(false), landable: obj.landable.unwrap_or(false),
name: obj.name.as_ref().map(|x| x.clone()), name: obj
.name
.as_ref()
.map(|x| x.clone())
.unwrap_or("".to_string()),
// TODO: better linebreaks, handle double spaces // TODO: better linebreaks, handle double spaces
// Tabs // Tabs
desc: obj desc: obj
.desc .desc
.as_ref() .as_ref()
.map(|x| x.replace("\n", " ").replace("<br>", "\n")), .map(|x| x.replace("\n", " ").replace("<br>", "\n"))
.unwrap_or("".to_string()),
}); });
} }

View File

@ -4,7 +4,7 @@ use anyhow::{bail, Result};
use clap::Parser; use clap::Parser;
use galactica_content::{Content, SystemHandle}; use galactica_content::{Content, SystemHandle};
use galactica_playeragent::{PlayerAgent, PlayerStatus}; use galactica_playeragent::{PlayerAgent, PlayerStatus};
use galactica_render::RenderInput; use galactica_render::{RenderInput, RenderScenes};
use galactica_system::{ use galactica_system::{
data::ShipState, data::ShipState,
phys::{PhysImage, PhysSimShipHandle}, phys::{PhysImage, PhysSimShipHandle},
@ -121,20 +121,25 @@ fn try_main() -> Result<()> {
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, content.clone()))?; let mut gpu = pollster::block_on(galactica_render::GPUState::new(
window,
content.clone(),
RenderScenes::System,
))?;
gpu.init(&content); gpu.init(&content);
// TODO: don't clone content // TODO: don't clone content
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 = Rc::new(PlayerAgent::new(p.0)); let mut player = PlayerAgent::new(p.0);
Rc::get_mut(&mut player).unwrap().set_camera_aspect( player.set_camera_aspect(
gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32, gpu.window().inner_size().width as f32 / gpu.window().inner_size().height as f32,
); );
let mut phys_img = Rc::new(PhysImage::new()); let mut phys_img = PhysImage::new();
let mut last_run = Instant::now(); let mut last_run = Instant::now();
let mut was_landed = false;
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
match event { match event {
@ -144,8 +149,8 @@ fn try_main() -> Result<()> {
camera_zoom: player.camera.zoom, 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,
player: player.clone(), player: &player,
time_since_last_run: last_run.elapsed().as_secs_f32(), time_since_last_run: last_run.elapsed().as_secs_f32(),
current_system: SystemHandle { index: 0 }, current_system: SystemHandle { index: 0 },
timing: game.get_timing().clone(), timing: game.get_timing().clone(),
@ -162,9 +167,9 @@ fn try_main() -> Result<()> {
} }
Event::MainEventsCleared => { Event::MainEventsCleared => {
game.update_player_controls(Rc::get_mut(&mut player).unwrap()); game.update_player_controls(&mut player);
game.step(&phys_img); game.step(&phys_img);
game.update_image(Rc::get_mut(&mut phys_img).unwrap()); game.update_image(&mut phys_img);
// TODO: clean up // TODO: clean up
let player_status = { let player_status = {
@ -175,9 +180,21 @@ fn try_main() -> Result<()> {
ShipState::Landing { .. } ShipState::Landing { .. }
| ShipState::UnLanding { .. } | ShipState::UnLanding { .. }
| ShipState::Collapsing { .. } | ShipState::Collapsing { .. }
| ShipState::Flying { .. } => Some(*o.rigidbody.translation()), | ShipState::Flying { .. } => {
if was_landed {
was_landed = false;
gpu.set_scene(RenderScenes::System);
}
Some(*o.rigidbody.translation())
}
ShipState::Landed { target } => { ShipState::Landed { target } => {
if !was_landed {
was_landed = true;
gpu.set_scene(RenderScenes::Landed);
}
let b = content.get_system_object(*target); let b = content.get_system_object(*target);
Some(Vector2::new(b.pos.x, b.pos.y)) Some(Vector2::new(b.pos.x, b.pos.y))
} }
@ -193,10 +210,9 @@ fn try_main() -> Result<()> {
}; };
// This must be updated BEFORE rendering! // This must be updated BEFORE rendering!
Rc::get_mut(&mut player) player.step(&content, player_status);
.unwrap()
.step(&content, player_status); player.input.clear_inputs();
Rc::get_mut(&mut player).unwrap().input.clear_inputs();
gpu.window().request_redraw(); gpu.window().request_redraw();
} }
@ -217,39 +233,27 @@ fn try_main() -> Result<()> {
}, },
.. ..
} => { } => {
Rc::get_mut(&mut player) player.input.process_key(state, key);
.unwrap()
.input
.process_key(state, key);
} }
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
Rc::get_mut(&mut player) player.input.process_mouse(position);
.unwrap()
.input
.process_mouse(position);
} }
WindowEvent::MouseInput { state, button, .. } => { WindowEvent::MouseInput { state, button, .. } => {
Rc::get_mut(&mut player) player.input.process_click(state, button);
.unwrap()
.input
.process_click(state, button);
} }
WindowEvent::MouseWheel { delta, phase, .. } => { WindowEvent::MouseWheel { delta, phase, .. } => {
Rc::get_mut(&mut player) player.input.process_scroll(delta, phase);
.unwrap()
.input
.process_scroll(delta, phase);
} }
WindowEvent::Resized(_) => { WindowEvent::Resized(_) => {
gpu.resize(&content); gpu.resize(&content);
Rc::get_mut(&mut player).unwrap().set_camera_aspect( player.set_camera_aspect(
gpu.window().inner_size().width as f32 gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32, / gpu.window().inner_size().height as f32,
); );
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
gpu.resize(&content); gpu.resize(&content);
Rc::get_mut(&mut player).unwrap().set_camera_aspect( player.set_camera_aspect(
gpu.window().inner_size().width as f32 gpu.window().inner_size().width as f32
/ gpu.window().inner_size().height as f32, / gpu.window().inner_size().height as f32,
); );

View File

@ -3,7 +3,6 @@ use winit::{
event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode}, event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode},
}; };
#[derive(Debug)]
pub struct InputStatus { pub struct InputStatus {
// Parameters // Parameters
scroll_speed: f32, scroll_speed: f32,

View File

@ -25,7 +25,6 @@ impl PlayerSelection {
} }
} }
#[derive(Debug)]
pub struct PlayerAgent { pub struct PlayerAgent {
/// Which ship this player is controlling /// Which ship this player is controlling
pub ship: Option<ColliderHandle>, pub ship: Option<ColliderHandle>,

View File

@ -0,0 +1,51 @@
// Given an anchored position and sprite dimensions,
// return the translation that should be applied on
// vertex coordinates
fn anchor(
anchor: u32, // Anchor index
position: vec2<f32>, // Anchored position
dim: vec2<f32>, // Sprite dimensions (width, height)
) -> vec2<f32> {
// TODO: remove, do in ui rust lib
var trans: vec2<f32> = vec2(0.0, 0.0);
let window_dim = (
vec2(global_data.window_size_w, global_data.window_size_h) /
global_data.window_scale
);
if anchor == 0u { // NW C (screen anchor, sprite anchor)
trans += vec2(-window_dim.x, window_dim.y) / 2.0; // origin
trans += vec2(0.0, 0.0) / 2.0; // offset
} else if anchor == 1u { // NW NW
trans += vec2(-window_dim.x, window_dim.y) / 2.0;
trans += vec2(dim.x, -dim.y) / 2.0;
} else if anchor == 2u { // NW NE
trans += vec2(-window_dim.x, window_dim.y) / 2.0;
trans += vec2(-dim.x, -dim.y) / 2.0;
} else if anchor == 3u { // NW SW
trans += vec2(-window_dim.x, window_dim.y) / 2.0;
trans += vec2(dim.x, dim.y) / 2.0;
} else if anchor == 4u { // NW SE
trans += vec2(-window_dim.x, window_dim.y) / 2.0;
trans += vec2(-dim.x, dim.y) / 2.0;
} else if anchor == 5u { // NE NE
trans += vec2(window_dim.x, window_dim.y) / 2.0;
trans += vec2(-dim.x, -dim.y) / 2.0;
} else if anchor == 6u { // C C
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(0.0, 0.0) / 2.0;
} else if anchor == 7u { // C NW
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(dim.x, -dim.y) / 2.0;
} else { // center / center as default, since it's the most visible variant.
trans += vec2(0.0, 0.0) / 2.0;
trans += vec2(0.0, 0.0) / 2.0;
}
trans += position;
// This renders correctly, but the offsets here are off by a factor of two.
// I'm not sure why... might be because WGPU screen coordinates are -1 to 1.
return (trans / window_dim) * 2.0;
}

View File

@ -1,11 +1,12 @@
// INCLUDE: global uniform header // INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) position: vec2<f32>, @location(2) anchor: u32,
@location(3) diameter: f32, @location(3) position: vec2<f32>,
@location(4) stroke: f32, @location(4) diameter: f32,
@location(5) angle: f32, @location(5) stroke: f32,
@location(6) color: vec4<f32>, @location(6) angle: f32,
@location(7) color: vec4<f32>,
}; };
struct VertexInput { struct VertexInput {
@ -28,6 +29,9 @@ var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
var sampler_array: binding_array<sampler>; var sampler_array: binding_array<sampler>;
// INCLUDE: anchor.wgsl
@vertex @vertex
fn vertex_main( fn vertex_main(
vertex: VertexInput, vertex: VertexInput,
@ -41,18 +45,17 @@ fn vertex_main(
out.color = instance.color; out.color = instance.color;
out.angle = instance.angle; out.angle = instance.angle;
let window_dim = (
vec2(global_data.window_size_w, global_data.window_size_h) /
global_data.window_scale
);
// Center of this radial bar, in logical pixels, // Center of this radial bar, in logical pixels,
// with (0, 0) at the center of the screen. // with (0, 0) at the center of the screen.
out.center = (instance.position / window_dim) * ( out.center = anchor(
instance.anchor,
instance.position,
vec2(instance.diameter, instance.diameter)
) / 2.0 * (
vec2(global_data.window_size_w, global_data.window_size_h) / vec2(global_data.window_size_w, global_data.window_size_h) /
global_data.window_scale global_data.window_scale
); );
// ^ slight correction, since anchor gives us a different result than we need here
return out; return out;
} }

View File

@ -1,13 +1,14 @@
// INCLUDE: global uniform header // INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) position: vec2<f32>, @location(2) anchor: u32,
@location(3) angle: f32, @location(3) position: vec2<f32>,
@location(4) dim: vec2<f32>, @location(4) angle: f32,
@location(5) color_transform: vec4<f32>, @location(5) dim: vec2<f32>,
@location(6) texture_index: vec2<u32>, @location(6) color_transform: vec4<f32>,
@location(7) texture_fade: f32, @location(7) texture_index: vec2<u32>,
@location(8) mask_index: vec2<u32>, @location(8) texture_fade: f32,
@location(9) mask_index: vec2<u32>,
}; };
struct VertexInput { struct VertexInput {
@ -33,6 +34,10 @@ var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
var sampler_array: binding_array<sampler>; var sampler_array: binding_array<sampler>;
// INCLUDE: anchor.wgsl
fn transform_vertex( fn transform_vertex(
instance: InstanceInput, instance: InstanceInput,
vertex_position: vec3<f32>, vertex_position: vec3<f32>,
@ -52,19 +57,24 @@ fn transform_vertex(
vertex_position.y * scale vertex_position.y * scale
); );
// Apply rotation (and adjust sprite angle, since sprites point north)
pos = mat2x2( pos = mat2x2(
vec2(cos(instance.angle - 1.5708), sin(instance.angle - 1.5708)), vec2(cos(instance.angle - 1.5708), sin(instance.angle - 1.5708)),
vec2(-sin(instance.angle - 1.5708), cos(instance.angle - 1.5708)) vec2(-sin(instance.angle - 1.5708), cos(instance.angle - 1.5708))
) * pos; ) * pos;
// Apply rotation (and adjust sprite angle, since sprites point north)
// Correct for screen aspect, preserving height // Correct for screen aspect, preserving height
pos = vec2( pos = vec2(
pos.x / global_data.window_aspect, pos.x / global_data.window_aspect,
pos.y pos.y
); );
pos = pos + (instance.position / window_dim) * 2.0; pos = pos + anchor(
instance.anchor,
instance.position,
instance.dim
);
return vec4<f32>(pos, 0.0, 1.0); return vec4<f32>(pos, 0.0, 1.0);
} }

View File

@ -1,24 +1,22 @@
use std::{iter, rc::Rc}; use std::rc::Rc;
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use galactica_content::Content; use galactica_content::Content;
use galactica_system::data::ShipState; use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use galactica_util::to_radians; use log::debug;
use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer};
use nalgebra::{Point2, Point3};
use wgpu; use wgpu;
use winit; use winit;
use crate::{ use crate::{
globaluniform::{GlobalDataContent, GlobalUniform, ObjectData}, globaluniform::{GlobalDataContent, GlobalUniform},
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
renderscene::{LandedScene, RenderScene, SystemScene},
shaderprocessor::preprocess_shader, shaderprocessor::preprocess_shader,
starfield::Starfield, starfield::Starfield,
texturearray::TextureArray, texturearray::TextureArray,
ui::UiManager, ui::{UiManager, UiScene},
vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance}, RenderInput, RenderScenes, RenderState, VertexBuffers,
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.
@ -34,11 +32,16 @@ pub struct GPUState {
pub(crate) texture_array: TextureArray, pub(crate) texture_array: TextureArray,
pub(crate) state: RenderState, pub(crate) state: RenderState,
pub(crate) ui: UiManager, pub(crate) ui: UiManager,
pub(crate) scene: RenderScenes,
} }
impl GPUState { impl GPUState {
/// Make a new GPUState that draws on `window` /// Make a new GPUState that draws on `window`
pub async fn new(window: winit::window::Window, ct: Rc<Content>) -> Result<Self> { pub async fn new(
window: winit::window::Window,
ct: Rc<Content>,
scene: RenderScenes,
) -> Result<Self> {
let window_size = window.inner_size(); let window_size = window.inner_size();
let window_aspect = window_size.width as f32 / window_size.height as f32; let window_aspect = window_size.width as f32 / window_size.height as f32;
@ -215,21 +218,8 @@ impl GPUState {
let mut starfield = Starfield::new(); let mut starfield = Starfield::new();
starfield.regenerate(&ct); starfield.regenerate(&ct);
let mut state = RenderState {
queue,
window,
window_size,
window_aspect,
global_uniform,
vertex_buffers,
text_atlas,
text_cache,
text_font_system,
text_renderer,
};
return Ok(Self { return Ok(Self {
ui: UiManager::new(ct, &mut state), ui: UiManager::new(ct),
device, device,
config, config,
surface, surface,
@ -239,7 +229,20 @@ impl GPUState {
starfield_pipeline, starfield_pipeline,
ui_pipeline, ui_pipeline,
radialbar_pipeline, radialbar_pipeline,
state, scene,
state: RenderState {
queue,
window,
window_size,
window_aspect,
global_uniform,
vertex_buffers,
text_atlas,
text_cache,
text_font_system,
text_renderer,
},
}); });
} }
} }
@ -250,6 +253,18 @@ impl GPUState {
&self.state.window &self.state.window
} }
/// Change the current scenection
pub fn set_scene(&mut self, scene: RenderScenes) {
debug!("switching to {:?}", scene);
match scene {
RenderScenes::Landed => self.ui.set_scene(&mut self.state, UiScene::Landed).unwrap(),
RenderScenes::System => self.ui.set_scene(&mut self.state, UiScene::Flying).unwrap(),
};
self.scene = scene;
}
/// Update window size. /// Update window size.
/// This should be called whenever our window is resized. /// This should be called whenever our window is resized.
pub fn resize(&mut self, ct: &Content) { pub fn resize(&mut self, ct: &Content) {
@ -278,8 +293,6 @@ impl GPUState {
/// 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 = Rc::new(input);
// 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,
@ -303,454 +316,11 @@ impl GPUState {
self.state.frame_reset(); self.state.frame_reset();
let output = self.surface.get_current_texture()?; match self.scene {
let view = output.texture.create_view(&Default::default()); RenderScenes::System => SystemScene::render(self, &input).unwrap(),
RenderScenes::Landed => LandedScene::render(self, &input).unwrap(),
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("render encoder"),
});
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
if self.ui.get_config().show_phys {
// Create sprite instances
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom;
let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom;
// Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter,
// but ships should always be under projectiles.
self.push_system(&input, (clip_ne, clip_sw));
self.push_ships(&input, (clip_ne, clip_sw));
self.push_projectiles(&input, (clip_ne, clip_sw));
self.push_effects(&input, (clip_ne, clip_sw));
} }
self.ui.draw(input.clone(), &mut self.state).unwrap();
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &self.state.global_uniform.bind_group, &[]);
if self.ui.get_config().show_starfield {
// Starfield pipeline
self.state
.vertex_buffers
.get_starfield()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.starfield_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_starfield_counter(),
);
}
if self.ui.get_config().show_phys {
// Sprite pipeline
self.state
.vertex_buffers
.get_object()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.object_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_object_counter(),
);
}
// Ui pipeline
self.state
.vertex_buffers
.get_ui()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.ui_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_ui_counter(),
);
// Radial progress bars
// TODO: do we need to do this every time?
self.state
.vertex_buffers
.get_radialbar()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.radialbar_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..self.state.get_radialbar_counter(),
);
let textareas = self.ui.get_textareas(&input, &self.state);
self.state
.text_renderer
.prepare(
&self.device,
&self.state.queue,
&mut self.state.text_font_system,
&mut self.state.text_atlas,
Resolution {
width: self.state.window_size.width,
height: self.state.window_size.height,
},
textareas,
&mut self.state.text_cache,
)
.unwrap();
self.state
.text_renderer
.render(&self.state.text_atlas, &mut render_pass)
.unwrap();
// begin_render_pass borrows encoder mutably,
// so we need to drop it before calling finish.
drop(render_pass);
self.state.queue.submit(iter::once(encoder.finish()));
output.present();
return Ok(()); return Ok(());
} }
} }
// Render utilities
impl GPUState {
fn push_ships(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for s in input.phys_img.iter_ships() {
let ship_pos;
let ship_ang;
let ship_cnt;
match s.ship.get_data().get_state() {
ShipState::Dead | ShipState::Landed { .. } => continue,
ShipState::Collapsing { .. } | ShipState::Flying { .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
}
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> =
(Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m =
(ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: ship_pos.x,
ypos: ship_pos.y,
zpos: ship_pos.z,
angle: ship_ang,
size: ship_cnt.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
let anim_state = s.ship.get_anim_state();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
if {
let is_flying = match s.ship.get_data().get_state() {
ShipState::Flying { .. }
| ShipState::UnLanding { .. }
| ShipState::Landing { .. } => true,
_ => false,
};
is_flying
} {
for (engine_point, anim) in s.ship.iter_engine_anim() {
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * self.state.get_object_counter() as u64,
bytemuck::cast_slice(&[ObjectData {
// Note that we adjust the y-coordinate for half-height,
// not the x-coordinate, even though our ships point east
// at 0 degrees. This is because this is placed pre-rotation,
// and the parent rotation adjustment in our object shader
// automatically accounts for this.
xpos: engine_point.pos.x,
ypos: engine_point.pos.y - engine_point.size / 2.0,
zpos: 1.0,
// We still need an adjustment here, though,
// since engine sprites point north (with exhaust towards the south)
angle: to_radians(90.0),
size: engine_point.size,
parent: idx as u32,
is_child: 1,
_padding: Default::default(),
}]),
);
let anim_state = anim.get_texture_idx();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: self.state.get_object_counter() as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
}
}
fn push_projectiles(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_projectiles() {
let r = &p.rigidbody;
let proj_pos = *r.translation();
let proj_rot = r.rotation();
let proj_ang = proj_rot.angle();
let proj_cnt = &p.projectile.content; // TODO: don't clone this?
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let pos = (proj_pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: proj_pos.x,
ypos: proj_pos.y,
zpos: 1.0,
angle: proj_ang,
size: 0f32.max(proj_cnt.size + p.projectile.size_rng),
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.projectile.get_anim_state();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
fn push_system(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
let system = input.ct.get_system(input.current_system);
for o in &system.objects {
// Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: o.pos.x,
ypos: o.pos.y,
zpos: o.pos.z,
angle: o.angle,
size: o.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let sprite = input.ct.get_sprite(o.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance
self.state.push_object_buffer(ObjectInstance {
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
fn push_effects(
&mut self,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_effects() {
let r = &p.rigidbody;
let pos = *r.translation();
let rot = r.rotation();
let ang = rot.angle();
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let adjusted_pos = (pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (p.effect.size / 1.0)
* input
.ct
.get_sprite(p.effect.anim.get_sprite())
.aspect
.max(1.0);
// Don't draw sprites that are off the screen
if adjusted_pos.x < screen_clip.0.x - m
|| adjusted_pos.y > screen_clip.0.y + m
|| adjusted_pos.x > screen_clip.1.x + m
|| adjusted_pos.y < screen_clip.1.y - m
{
continue;
}
let idx = self.state.get_object_counter();
// Write this object's location data
self.state.queue.write_buffer(
&self.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: pos.x,
ypos: pos.y,
zpos: 1.0,
angle: ang,
size: p.effect.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.effect.anim.get_texture_idx();
self.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, p.get_fade()],
});
}
}
}

View File

@ -10,7 +10,9 @@
mod globaluniform; mod globaluniform;
mod gpustate; mod gpustate;
mod pipeline; mod pipeline;
mod positionanchor;
mod renderinput; mod renderinput;
mod renderscene;
mod renderstate; mod renderstate;
mod shaderprocessor; mod shaderprocessor;
mod starfield; mod starfield;
@ -19,7 +21,9 @@ mod ui;
mod vertexbuffer; mod vertexbuffer;
pub use gpustate::GPUState; pub use gpustate::GPUState;
pub use positionanchor::PositionAnchor;
pub use renderinput::RenderInput; pub use renderinput::RenderInput;
pub use renderscene::RenderScenes;
use renderstate::*; use renderstate::*;
use nalgebra::Matrix4; use nalgebra::Matrix4;

View File

@ -0,0 +1,57 @@
/// The location of a UI element, in one of a few
/// possible coordinate systems.
///
/// Positive Y always points up,
/// positive X always points right.
#[derive(Debug, Clone)]
pub enum PositionAnchor {
/// Position of this sprite's center,
/// relative to the nw corner of the window.
NwC,
/// Position of this sprite's nw corner,
/// relative to the nw corner of the window.
NwNw,
/// Position of this sprite's ne corner,
/// relative to the nw corner of the window.
NwNe,
/// Position of this sprite's sw corner,
/// relative to the nw corner of the window.
NwSw,
/// Position of this sprite's se corner,
/// relative to the nw corner of the window.
NwSe,
/// Position of this sprite's ne corner,
/// relative to the ne corner of the window.
NeNe,
/// Position of this sprite's center,
/// relative to the center of the window.
CC,
/// Position of this sprite's NW corner,
/// relative to the center of the window.
CNw,
}
// These offsets are implemented in wgsl shaders.
impl PositionAnchor {
/// Get the uint that represents this anchor mode in shaders
pub fn to_int(&self) -> u32 {
match self {
Self::NwC => 0,
Self::NwNw => 1,
Self::NwNe => 2,
Self::NwSw => 3,
Self::NwSe => 4,
Self::NeNe => 5,
Self::CC => 6,
Self::CNw => 7,
}
}
}

View File

@ -7,13 +7,12 @@ use galactica_util::timing::Timing;
use nalgebra::Vector2; 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)] pub struct RenderInput<'a> {
pub struct RenderInput {
/// Camera position, in world units /// Camera position, in world units
pub camera_pos: Vector2<f32>, pub camera_pos: Vector2<f32>,
/// Player ship data /// Player ship data
pub player: Rc<PlayerAgent>, pub player: &'a PlayerAgent,
/// The system we're currently in /// The system we're currently in
pub current_system: SystemHandle, pub current_system: SystemHandle,
@ -22,7 +21,7 @@ pub struct RenderInput {
pub camera_zoom: f32, pub camera_zoom: f32,
/// The world state to render /// The world state to render
pub phys_img: Rc<PhysImage>, pub phys_img: &'a PhysImage,
// TODO: handle overflow. is it a problem? // TODO: handle overflow. is it a problem?
/// The current time, in seconds /// The current time, in seconds

View File

@ -0,0 +1,117 @@
use anyhow::Result;
use glyphon::Resolution;
use std::iter;
use super::RenderScene;
use crate::vertexbuffer::consts::SPRITE_INDICES;
pub struct LandedScene {}
impl RenderScene for LandedScene {
fn render(g: &mut crate::GPUState, input: &crate::RenderInput) -> Result<()> {
let output = g.surface.get_current_texture()?;
let view = output.texture.create_view(&Default::default());
let mut encoder = g
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("render encoder"),
});
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
// Create sprite instances
g.ui.draw(&input, &mut g.state)?;
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &g.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &g.state.global_uniform.bind_group, &[]);
// Starfield pipeline
g.state
.vertex_buffers
.get_starfield()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.starfield_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_starfield_counter(),
);
// Ui pipeline
g.state
.vertex_buffers
.get_ui()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.ui_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_ui_counter(),
);
// Radial progress bars
g.state
.vertex_buffers
.get_radialbar()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.radialbar_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_radialbar_counter(),
);
let textareas = g.ui.get_textareas(input, &g.state);
g.state
.text_renderer
.prepare(
&g.device,
&g.state.queue,
&mut g.state.text_font_system,
&mut g.state.text_atlas,
Resolution {
width: g.state.window_size.width,
height: g.state.window_size.height,
},
textareas,
&mut g.state.text_cache,
)
.unwrap();
g.state
.text_renderer
.render(&g.state.text_atlas, &mut render_pass)
.unwrap();
// begin_render_pass borrows encoder mutably,
// so we need to drop it before calling finish.
drop(render_pass);
g.state.queue.submit(iter::once(encoder.finish()));
output.present();
return Ok(());
}
}

View File

@ -0,0 +1,32 @@
mod landed;
mod system;
use std::fmt::Debug;
pub use landed::LandedScene;
pub use system::SystemScene;
use crate::{GPUState, RenderInput};
use anyhow::Result;
pub trait RenderScene {
fn render(g: &mut GPUState, input: &RenderInput) -> Result<()>;
}
/// What render routine to run
pub enum RenderScenes {
/// Draw the system we're in
System,
/// Draw the landed UI
Landed,
}
impl Debug for RenderScenes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Landed => write!(f, "RenderScenes::Landed"),
Self::System => write!(f, "RenderScenes::System"),
}
}
}

View File

@ -0,0 +1,4 @@
mod phys;
mod system;
pub use system::SystemScene;

View File

@ -0,0 +1,321 @@
use bytemuck;
use galactica_system::data::ShipState;
use galactica_util::to_radians;
use nalgebra::{Point2, Point3};
use crate::{
globaluniform::ObjectData, vertexbuffer::types::ObjectInstance, GPUState, RenderInput,
};
use super::SystemScene;
impl SystemScene {
pub(super) fn push_ships(
g: &mut GPUState,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for s in input.phys_img.iter_ships() {
let ship_pos;
let ship_ang;
let ship_cnt;
match s.ship.get_data().get_state() {
ShipState::Dead | ShipState::Landed { .. } => continue,
ShipState::Collapsing { .. } | ShipState::Flying { .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, 1.0);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
ShipState::UnLanding { current_z, .. } | ShipState::Landing { current_z, .. } => {
let r = &s.rigidbody;
let pos = *r.translation();
ship_pos = Point3::new(pos.x, pos.y, *current_z);
let ship_rot = r.rotation();
ship_ang = ship_rot.angle();
ship_cnt = input.ct.get_ship(s.ship.get_data().get_content());
}
}
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> =
(Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m =
(ship_cnt.size / ship_pos.z) * input.ct.get_sprite(ship_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = g.state.get_object_counter();
// Write this object's location data
g.state.queue.write_buffer(
&g.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: ship_pos.x,
ypos: ship_pos.y,
zpos: ship_pos.z,
angle: ship_ang,
size: ship_cnt.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
// Push this object's instance
let anim_state = s.ship.get_anim_state();
g.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
if {
let is_flying = match s.ship.get_data().get_state() {
ShipState::Flying { .. }
| ShipState::UnLanding { .. }
| ShipState::Landing { .. } => true,
_ => false,
};
is_flying
} {
for (engine_point, anim) in s.ship.iter_engine_anim() {
g.state.queue.write_buffer(
&g.state.global_uniform.object_buffer,
ObjectData::SIZE * g.state.get_object_counter() as u64,
bytemuck::cast_slice(&[ObjectData {
// Note that we adjust the y-coordinate for half-height,
// not the x-coordinate, even though our ships point east
// at 0 degrees. This is because this is placed pre-rotation,
// and the parent rotation adjustment in our object shader
// automatically accounts for this.
xpos: engine_point.pos.x,
ypos: engine_point.pos.y - engine_point.size / 2.0,
zpos: 1.0,
// We still need an adjustment here, though,
// since engine sprites point north (with exhaust towards the south)
angle: to_radians(90.0),
size: engine_point.size,
parent: idx as u32,
is_child: 1,
_padding: Default::default(),
}]),
);
let anim_state = anim.get_texture_idx();
g.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: g.state.get_object_counter() as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
}
}
pub(super) fn push_projectiles(
g: &mut GPUState,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_projectiles() {
let r = &p.rigidbody;
let proj_pos = *r.translation();
let proj_rot = r.rotation();
let proj_ang = proj_rot.angle();
let proj_cnt = &p.projectile.content; // TODO: don't clone this?
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let pos = (proj_pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (proj_cnt.size / 1.0) * input.ct.get_sprite(proj_cnt.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = g.state.get_object_counter();
// Write this object's location data
g.state.queue.write_buffer(
&g.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: proj_pos.x,
ypos: proj_pos.y,
zpos: 1.0,
angle: proj_ang,
size: 0f32.max(proj_cnt.size + p.projectile.size_rng),
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.projectile.get_anim_state();
g.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
pub(super) fn push_system(
g: &mut GPUState,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
let system = input.ct.get_system(input.current_system);
for o in &system.objects {
// Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (o.size / o.pos.z) * input.ct.get_sprite(o.sprite).aspect.max(1.0);
// Don't draw sprites that are off the screen
if pos.x < screen_clip.0.x - m
|| pos.y > screen_clip.0.y + m
|| pos.x > screen_clip.1.x + m
|| pos.y < screen_clip.1.y - m
{
continue;
}
let idx = g.state.get_object_counter();
// Write this object's location data
g.state.queue.write_buffer(
&g.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: o.pos.x,
ypos: o.pos.y,
zpos: o.pos.z,
angle: o.angle,
size: o.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let sprite = input.ct.get_sprite(o.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance
g.state.push_object_buffer(ObjectInstance {
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, 1.0],
});
}
}
pub(super) fn push_effects(
g: &mut GPUState,
input: &RenderInput,
// NE and SW corners of screen
screen_clip: (Point2<f32>, Point2<f32>),
) {
for p in input.phys_img.iter_effects() {
let r = &p.rigidbody;
let pos = *r.translation();
let rot = r.rotation();
let ang = rot.angle();
// Position adjusted for parallax
// TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles
let adjusted_pos = (pos - input.camera_pos) / 1.0;
// Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger.
// This is in game units.
//
// We take the maximum to account for rotated sprites.
let m = (p.effect.size / 1.0)
* input
.ct
.get_sprite(p.effect.anim.get_sprite())
.aspect
.max(1.0);
// Don't draw sprites that are off the screen
if adjusted_pos.x < screen_clip.0.x - m
|| adjusted_pos.y > screen_clip.0.y + m
|| adjusted_pos.x > screen_clip.1.x + m
|| adjusted_pos.y < screen_clip.1.y - m
{
continue;
}
let idx = g.state.get_object_counter();
// Write this object's location data
g.state.queue.write_buffer(
&g.state.global_uniform.object_buffer,
ObjectData::SIZE * idx as u64,
bytemuck::cast_slice(&[ObjectData {
xpos: pos.x,
ypos: pos.y,
zpos: 1.0,
angle: ang,
size: p.effect.size,
parent: 0,
is_child: 0,
_padding: Default::default(),
}]),
);
let anim_state = p.effect.anim.get_texture_idx();
g.state.push_object_buffer(ObjectInstance {
texture_index: anim_state.texture_index(),
texture_fade: anim_state.fade,
object_index: idx as u32,
color: [1.0, 1.0, 1.0, p.get_fade()],
});
}
}
}

View File

@ -0,0 +1,144 @@
use anyhow::Result;
use glyphon::Resolution;
use nalgebra::Point2;
use std::iter;
use super::super::RenderScene;
use crate::vertexbuffer::consts::SPRITE_INDICES;
pub struct SystemScene {}
impl RenderScene for SystemScene {
fn render(g: &mut crate::GPUState, input: &crate::RenderInput) -> Result<()> {
let output = g.surface.get_current_texture()?;
let view = output.texture.create_view(&Default::default());
let mut encoder = g
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("render encoder"),
});
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
// Create sprite instances
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::new(-g.state.window_aspect, 1.0) * input.camera_zoom;
let clip_sw = Point2::new(g.state.window_aspect, -1.0) * input.camera_zoom;
// Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter,
// but ships should always be under projectiles.
Self::push_system(g, &input, (clip_ne, clip_sw));
Self::push_ships(g, &input, (clip_ne, clip_sw));
Self::push_projectiles(g, &input, (clip_ne, clip_sw));
Self::push_effects(g, &input, (clip_ne, clip_sw));
g.ui.draw(&input, &mut g.state)?;
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &g.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &g.state.global_uniform.bind_group, &[]);
// Starfield pipeline
g.state
.vertex_buffers
.get_starfield()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.starfield_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_starfield_counter(),
);
// Sprite pipeline
g.state
.vertex_buffers
.get_object()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.object_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_object_counter(),
);
// Ui pipeline
g.state
.vertex_buffers
.get_ui()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.ui_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_ui_counter(),
);
// Radial progress bars
// TODO: do we need to do this every time?
g.state
.vertex_buffers
.get_radialbar()
.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&g.radialbar_pipeline);
render_pass.draw_indexed(
0..SPRITE_INDICES.len() as u32,
0,
0..g.state.get_radialbar_counter(),
);
let textareas = g.ui.get_textareas(input, &g.state);
g.state
.text_renderer
.prepare(
&g.device,
&g.state.queue,
&mut g.state.text_font_system,
&mut g.state.text_atlas,
Resolution {
width: g.state.window_size.width,
height: g.state.window_size.height,
},
textareas,
&mut g.state.text_cache,
)
.unwrap();
g.state
.text_renderer
.render(&g.state.text_atlas, &mut render_pass)
.unwrap();
// begin_render_pass borrows encoder mutably,
// so we need to drop it before calling finish.
drop(render_pass);
g.state.queue.submit(iter::once(encoder.finish()));
output.present();
return Ok(());
}
}

View File

@ -1,7 +1,5 @@
use galactica_content::Content; use galactica_content::Content;
use galactica_util::constants::{ use galactica_util::constants::{OBJECT_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT};
OBJECT_SPRITE_INSTANCE_LIMIT, RADIALBAR_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT,
};
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer}; use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc; use std::rc::Rc;
use wgpu::BufferAddress; use wgpu::BufferAddress;
@ -154,6 +152,7 @@ impl RenderState {
self.vertex_buffers.object_counter as u32 self.vertex_buffers.object_counter as u32
} }
/*
pub fn push_radialbar_buffer(&mut self, instance: RadialBarInstance) { pub fn push_radialbar_buffer(&mut self, instance: RadialBarInstance) {
// Enforce buffer limit // Enforce buffer limit
if self.vertex_buffers.radialbar_counter as u64 > RADIALBAR_SPRITE_INSTANCE_LIMIT { if self.vertex_buffers.radialbar_counter as u64 > RADIALBAR_SPRITE_INSTANCE_LIMIT {
@ -168,6 +167,7 @@ impl RenderState {
); );
self.vertex_buffers.radialbar_counter += 1; self.vertex_buffers.radialbar_counter += 1;
} }
*/
pub fn get_radialbar_counter(&self) -> u32 { pub fn get_radialbar_counter(&self) -> u32 {
self.vertex_buffers.radialbar_counter as u32 self.vertex_buffers.radialbar_counter as u32

View File

@ -14,5 +14,15 @@ pub(crate) fn preprocess_shader(
&global_uniform.shader_header(global_uniform_group), &global_uniform.shader_header(global_uniform_group),
); );
// Insert common functions
let shader = shader.replace(
"// INCLUDE: anchor.wgsl",
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/include/",
"anchor.wgsl"
)),
);
return shader; return shader;
} }

View File

@ -1,25 +0,0 @@
use nalgebra::Vector4;
use rhai::{CustomType, TypeBuilder};
#[derive(Debug, Clone)]
pub struct Color {
pub val: Vector4<f32>,
}
impl Color {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self {
val: Vector4::new(r, g, b, a),
}
}
pub fn as_array(&self) -> [f32; 4] {
[self.val.x, self.val.y, self.val.z, self.val.w]
}
}
impl CustomType for Color {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Color").with_fn("Color", Self::new);
}
}

View File

@ -1,28 +0,0 @@
use rhai::{CustomType, TypeBuilder};
#[derive(Debug, Clone)]
pub struct SceneConfig {
pub show_phys: bool,
pub show_starfield: bool,
}
impl SceneConfig {
pub fn new() -> Self {
Self {
show_phys: false,
show_starfield: false,
}
}
}
impl CustomType for SceneConfig {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SceneConfig")
.with_fn("SceneConfig", Self::new)
.with_fn("show_phys", |s: &mut Self, x: bool| s.show_phys = x)
.with_fn("show_starfield", |s: &mut Self, x: bool| {
s.show_starfield = x
});
}
}

View File

@ -1,42 +0,0 @@
use rhai::{CustomType, TypeBuilder};
use super::SpriteElement;
#[derive(Debug, Clone)]
pub struct MouseClickEvent {
pub down: bool,
pub element: SpriteElement,
}
impl CustomType for MouseClickEvent {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("MouseClickEvent")
.with_fn("is_down", |s: &mut Self| s.down)
.with_fn("element", |s: &mut Self| s.element.clone());
}
}
#[derive(Debug, Clone)]
pub struct MouseHoverEvent {
pub enter: bool,
pub element: SpriteElement,
}
impl CustomType for MouseHoverEvent {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("MouseHoverEvent")
.with_fn("is_enter", |s: &mut Self| s.enter)
.with_fn("element", |s: &mut Self| s.element.clone());
}
}
#[derive(Debug, Clone)]
pub struct PlayerShipStateEvent {}
impl CustomType for PlayerShipStateEvent {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("PlayerShipStateEvent");
}
}

View File

@ -1,49 +1,19 @@
mod color;
mod config;
mod event;
mod radialbuilder;
mod rect; mod rect;
mod sceneaction;
mod sprite; mod sprite;
mod spritebuilder; mod spritebuilder;
mod state; mod state;
mod textboxbuilder; mod textboxbuilder;
mod util; mod util;
pub use color::*;
pub use config::*;
pub use event::*;
pub use radialbuilder::*;
pub use rect::*;
pub use sceneaction::*;
pub use sprite::*;
pub use spritebuilder::*;
pub use state::*;
pub use textboxbuilder::*;
pub use util::*;
use rhai::{exported_module, Engine}; use rhai::{exported_module, Engine};
pub fn register_into_engine(engine: &mut Engine) { pub fn register_into_engine(engine: &mut Engine) {
engine engine
// Helpers
.build_type::<Rect>()
.build_type::<Color>()
.build_type::<SceneConfig>()
// State
.build_type::<State>() .build_type::<State>()
.build_type::<ShipState>() .build_type::<Rect>()
.build_type::<SystemObjectState>()
// Builders
.build_type::<RadialBuilder>()
.build_type::<SpriteElement>() .build_type::<SpriteElement>()
.build_type::<SpriteBuilder>() .build_type::<SpriteBuilder>()
.build_type::<TextBoxBuilder>() .build_type::<TextBoxBuilder>()
// Events
.build_type::<MouseClickEvent>()
.build_type::<MouseHoverEvent>()
.build_type::<PlayerShipStateEvent>()
// Larger modules
.register_type_with_name::<SpriteAnchor>("SpriteAnchor") .register_type_with_name::<SpriteAnchor>("SpriteAnchor")
.register_static_module("SpriteAnchor", exported_module!(spriteanchor_mod).into()) .register_static_module("SpriteAnchor", exported_module!(spriteanchor_mod).into())
.register_type_with_name::<TextBoxFont>("TextBoxFont") .register_type_with_name::<TextBoxFont>("TextBoxFont")
@ -59,3 +29,10 @@ pub fn register_into_engine(engine: &mut Engine) {
.register_type_with_name::<SceneAction>("SceneAction") .register_type_with_name::<SceneAction>("SceneAction")
.register_static_module("SceneAction", exported_module!(sceneaction_mod).into()); .register_static_module("SceneAction", exported_module!(sceneaction_mod).into());
} }
pub use rect::*;
pub use sprite::*;
pub use spritebuilder::*;
pub use state::*;
pub use textboxbuilder::*;
pub use util::*;

View File

@ -1,39 +0,0 @@
use nalgebra::clamp;
use rhai::{CustomType, ImmutableString, TypeBuilder};
use super::{color::Color, Rect};
#[derive(Debug, Clone)]
pub struct RadialBuilder {
pub name: ImmutableString,
pub rect: Rect,
pub stroke: f32,
pub color: Color,
pub progress: f32,
}
impl RadialBuilder {
pub fn new(name: ImmutableString, stroke: f32, color: Color, rect: Rect) -> Self {
Self {
name,
rect,
stroke,
color,
progress: 0.0,
}
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = clamp(progress, 0.0, 1.0);
}
}
impl CustomType for RadialBuilder {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("RadialBuilder")
.with_fn("RadialBuilder", Self::new)
.with_fn("set_progress", Self::set_progress);
}
}

View File

@ -1,18 +0,0 @@
use rhai::plugin::*;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum SceneAction {
None,
GoTo(String),
}
#[export_module]
pub mod sceneaction_mod {
#[allow(non_upper_case_globals)]
pub const None: SceneAction = SceneAction::None;
#[allow(non_snake_case)]
pub fn GoTo(scene: String) -> SceneAction {
SceneAction::GoTo(scene)
}
}

View File

@ -11,7 +11,6 @@ pub struct SpriteElement {
pub ct: Rc<Content>, pub ct: Rc<Content>,
} }
// TODO: remove this
unsafe impl Send for SpriteElement {} unsafe impl Send for SpriteElement {}
unsafe impl Sync for SpriteElement {} unsafe impl Sync for SpriteElement {}
@ -26,18 +25,7 @@ impl SpriteElement {
} }
pub fn take_edge(&mut self, edge_name: &str, duration: f32) { pub fn take_edge(&mut self, edge_name: &str, duration: f32) {
if self.sprite.as_ref().borrow().anim.is_none() { let sprite_handle = self.sprite.as_ref().borrow().anim.get_sprite();
return;
}
let sprite_handle = self
.sprite
.as_ref()
.borrow()
.anim
.as_ref()
.unwrap()
.get_sprite();
let sprite = self.ct.get_sprite(sprite_handle); let sprite = self.ct.get_sprite(sprite_handle);
let edge = resolve_edge_as_edge(edge_name, duration, |x| { let edge = resolve_edge_as_edge(edge_name, duration, |x| {
@ -59,8 +47,6 @@ impl SpriteElement {
.as_ref() .as_ref()
.borrow_mut() .borrow_mut()
.anim .anim
.as_mut()
.unwrap()
.jump_to(&self.ct, edge); .jump_to(&self.ct, edge);
} }
} }

View File

@ -1,17 +1,17 @@
use rhai::{CustomType, ImmutableString, TypeBuilder}; use rhai::{CustomType, TypeBuilder};
use super::Rect; use super::Rect;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SpriteBuilder { pub struct SpriteBuilder {
pub name: ImmutableString, pub name: String,
pub rect: Rect, pub rect: Rect,
pub sprite: ImmutableString, pub sprite: String,
pub mask: Option<ImmutableString>, pub mask: Option<String>,
} }
impl SpriteBuilder { impl SpriteBuilder {
pub fn new(name: ImmutableString, sprite: ImmutableString, rect: Rect) -> Self { pub fn new(name: String, sprite: String, rect: Rect) -> Self {
Self { Self {
name, name,
rect, rect,
@ -20,7 +20,7 @@ impl SpriteBuilder {
} }
} }
pub fn set_mask(&mut self, mask: ImmutableString) { pub fn set_mask(&mut self, mask: String) {
self.mask = Some(mask); self.mask = Some(mask);
} }
} }

View File

@ -1,175 +1,7 @@
use galactica_content::{Ship, SystemObject, SystemObjectHandle};
use galactica_system::{
data::{self, ShipData},
phys::PhysSimShipHandle,
};
use log::error;
use rhai::{CustomType, TypeBuilder}; use rhai::{CustomType, TypeBuilder};
use std::rc::Rc;
use crate::RenderInput; #[derive(Debug, Clone, CustomType)]
#[derive(Debug, Clone)]
pub struct ShipState {
ship: Option<PhysSimShipHandle>,
input: Rc<RenderInput>,
}
// TODO: remove this
unsafe impl Send for ShipState {}
unsafe impl Sync for ShipState {}
impl ShipState {
fn get_content(&mut self) -> &Ship {
let ship = self
.input
.phys_img
.get_ship(self.ship.as_ref().unwrap())
.unwrap();
let handle = ship.ship.get_data().get_content();
self.input.ct.get_ship(handle)
}
fn get_data(&mut self) -> &ShipData {
let ship = self
.input
.phys_img
.get_ship(self.ship.as_ref().unwrap())
.unwrap();
ship.ship.get_data()
}
fn landed_on(&mut self) -> SystemObjectState {
let input = self.input.clone();
match self.get_data().get_state() {
data::ShipState::Landed { target } => {
return SystemObjectState {
input,
object: Some(*target),
}
}
_ => {
return SystemObjectState {
input,
object: None,
}
}
};
}
}
impl CustomType for ShipState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("ShipState")
.with_fn("is_some", |s: &mut Self| s.ship.is_some())
.with_fn("is_dead", |s: &mut Self| s.get_data().get_state().is_dead())
.with_fn("is_landed", |s: &mut Self| {
s.get_data().get_state().is_landed()
})
.with_fn("is_landing", |s: &mut Self| {
s.get_data().get_state().is_landing()
})
.with_fn("is_flying", |s: &mut Self| {
s.get_data().get_state().is_flying()
})
.with_fn("is_unlanding", |s: &mut Self| {
s.get_data().get_state().is_unlanding()
})
.with_fn("is_collapsing", |s: &mut Self| {
s.get_data().get_state().is_collapsing()
})
.with_fn("name", |s: &mut Self| s.get_content().name.clone())
.with_fn("thumbnail", |s: &mut Self| s.get_content().thumb)
.with_fn("landed_on", |s: &mut Self| s.landed_on());
}
}
#[derive(Debug, Clone)]
pub struct SystemObjectState {
object: Option<SystemObjectHandle>,
input: Rc<RenderInput>,
}
// TODO: remove this
unsafe impl Send for SystemObjectState {}
unsafe impl Sync for SystemObjectState {}
impl SystemObjectState {
fn get_content(&mut self) -> &SystemObject {
self.input.ct.get_system_object(self.object.unwrap())
}
}
impl CustomType for SystemObjectState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SystemObjectState")
//
// Get landable name
.with_fn("name", |s: &mut Self| {
s.get_content()
.name
.as_ref()
.map(|x| x.to_string())
.unwrap_or_else(|| {
error!("UI called `name()` on a system object which doesn't provide one");
"".to_string()
})
})
//
// Get landable description
.with_fn("desc", |s: &mut Self| {
s.get_content()
.desc
.as_ref()
.map(|x| x.to_string())
.unwrap_or_else(|| {
error!("UI called `name()` on a system object which doesn't provide one");
"".to_string()
})
})
//
// Get landable landscape image
.with_fn("image", |s: &mut Self| {
let handle = s.get_content().image;
if let Some(handle) = handle {
s.input.ct.get_sprite(handle).name.clone()
} else {
error!("UI called `image()` on a system object which doesn't provide one");
"".to_string()
}
})
.with_fn("is_some", |s: &mut Self| s.object.is_some());
}
}
#[derive(Debug, Clone)]
pub struct State { pub struct State {
input: Rc<RenderInput>, pub planet_landscape: String,
} pub planet_name: String,
// TODO: remove this
unsafe impl Send for State {}
unsafe impl Sync for State {}
impl State {
pub fn new(input: Rc<RenderInput>) -> Self {
Self { input }
}
pub fn player_ship(&mut self) -> ShipState {
ShipState {
input: self.input.clone(),
ship: self.input.player.ship.map(|x| PhysSimShipHandle(x)),
}
}
}
impl CustomType for State {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("State")
.with_fn("player_ship", Self::player_ship);
}
} }

View File

@ -1,21 +1,21 @@
use rhai::{CustomType, ImmutableString, TypeBuilder}; use rhai::{CustomType, TypeBuilder};
use super::{Rect, TextBoxFont, TextBoxJustify}; use super::{Rect, TextBoxFont, TextBoxJustify};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextBoxBuilder { pub struct TextBoxBuilder {
pub name: ImmutableString, pub name: String,
pub font_size: f32, pub font_size: f32,
pub line_height: f32, pub line_height: f32,
pub font: TextBoxFont, pub font: TextBoxFont,
pub justify: TextBoxJustify, pub justify: TextBoxJustify,
pub rect: Rect, pub rect: Rect,
pub text: ImmutableString, pub text: String,
} }
impl TextBoxBuilder { impl TextBoxBuilder {
pub fn new( pub fn new(
name: ImmutableString, name: String,
font_size: f32, font_size: f32,
line_height: f32, line_height: f32,
font: TextBoxFont, font: TextBoxFont,
@ -29,11 +29,11 @@ impl TextBoxBuilder {
font, font,
justify, justify,
rect, rect,
text: ImmutableString::new(), text: String::new(),
} }
} }
pub fn set_text(&mut self, text: ImmutableString) { pub fn set_text(&mut self, text: String) {
self.text = text self.text = text
} }
} }

View File

@ -60,3 +60,22 @@ pub mod textboxjustify_mod {
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
pub const Left: TextBoxJustify = TextBoxJustify::Left; pub const Left: TextBoxJustify = TextBoxJustify::Left;
} }
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum SceneAction {
None,
SceneOutfitter,
SceneLanded,
}
#[export_module]
pub mod sceneaction_mod {
#[allow(non_upper_case_globals)]
pub const None: SceneAction = SceneAction::None;
#[allow(non_upper_case_globals)]
pub const SceneOutfitter: SceneAction = SceneAction::SceneOutfitter;
#[allow(non_upper_case_globals)]
pub const SceneLanded: SceneAction = SceneAction::SceneLanded;
}

View File

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

View File

@ -1,43 +1,100 @@
use anyhow::{Context, Result}; use anyhow::Result;
use galactica_content::Content; use galactica_content::Content;
use galactica_system::phys::PhysSimShipHandle;
use glyphon::TextArea; use glyphon::TextArea;
use log::{debug, error, trace}; use log::{debug, error, trace};
use rhai::{Array, Dynamic, Engine, Scope}; use rhai::{Array, Dynamic, Engine, Scope, AST};
use std::{collections::HashSet, num::NonZeroU32, rc::Rc}; use std::{cell::RefCell, collections::HashSet, fmt::Debug, rc::Rc};
use super::{ use super::{
api::{ api::{self, SceneAction, SpriteElement, TextBoxBuilder},
self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent, SceneAction, SceneConfig, util::{Sprite, TextBox},
SpriteElement, TextBoxBuilder,
},
event::Event,
util::{FpsIndicator, RadialBar, TextBox},
UiElement,
}; };
use crate::{ use crate::{
ui::{ ui::api::{SpriteBuilder, State},
api::{RadialBuilder, SpriteBuilder, State},
util::Sprite,
},
RenderInput, RenderState, RenderInput, RenderState,
}; };
#[derive(Debug, Copy, Clone)]
pub enum MouseEvent {
Click,
Release,
Enter,
Leave,
None,
}
impl MouseEvent {
pub fn is_enter(&self) -> bool {
match self {
Self::Enter => true,
_ => false,
}
}
pub fn is_click(&self) -> bool {
match self {
Self::Click => true,
_ => false,
}
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum UiScene {
Landed,
Flying,
Outfitter,
}
impl ToString for UiScene {
fn to_string(&self) -> String {
match self {
Self::Flying => "flying".to_string(),
Self::Landed => "landed".to_string(),
Self::Outfitter => "outfitter".to_string(),
}
}
}
enum UiElement {
Sprite(Rc<RefCell<Sprite>>),
Text(TextBox),
}
impl UiElement {
pub fn new_sprite(sprite: Sprite) -> Self {
Self::Sprite(Rc::new(RefCell::new(sprite)))
}
pub fn new_text(text: TextBox) -> Self {
Self::Text(text)
}
pub fn sprite(&self) -> Option<Rc<RefCell<Sprite>>> {
match self {
Self::Sprite(s) => Some(s.clone()),
_ => None,
}
}
pub fn text(&self) -> Option<&TextBox> {
match self {
Self::Text(t) => Some(t),
_ => None,
}
}
}
pub(crate) struct UiManager { pub(crate) struct UiManager {
current_scene: Option<String>, current_scene: UiScene,
current_scene_config: SceneConfig,
engine: Engine, engine: Engine,
scope: Scope<'static>, scope: Scope<'static>,
elements: Vec<UiElement>, elements: Vec<UiElement>,
ct: Rc<Content>, ct: Rc<Content>,
last_player_state: u32,
show_timings: bool,
fps_indicator: FpsIndicator,
} }
impl UiManager { impl UiManager {
pub fn new(ct: Rc<Content>, state: &mut RenderState) -> Self { pub fn new(ct: Rc<Content>) -> Self {
let scope = Scope::new(); let scope = Scope::new();
let mut engine = Engine::new(); let mut engine = Engine::new();
@ -45,125 +102,73 @@ impl UiManager {
Self { Self {
ct, ct,
current_scene: None, current_scene: UiScene::Flying,
current_scene_config: SceneConfig::new(),
engine, engine,
scope, scope,
elements: Vec::new(), elements: Vec::new(),
show_timings: true,
fps_indicator: FpsIndicator::new(state),
last_player_state: 0,
} }
} }
pub fn get_config(&self) -> &SceneConfig { pub fn get_scene_ast(ct: &Content, scene: UiScene) -> &AST {
&self.current_scene_config match scene {
UiScene::Landed => &ct.get_config().ui_landed_scene,
UiScene::Flying => &ct.get_config().ui_flying_scene,
UiScene::Outfitter => &ct.get_config().ui_outfitter_scene,
}
} }
/// Change the current scene /// Change the current scene
pub fn set_scene( pub fn set_scene(&mut self, state: &mut RenderState, scene: UiScene) -> Result<()> {
&mut self,
state: &mut RenderState,
input: Rc<RenderInput>,
scene: String,
) -> Result<()> {
if !self.ct.get_config().ui_scenes.contains_key(&scene) {
error!("tried to switch to ui scene `{scene}`, which doesn't exist");
return Ok(());
}
debug!("switching to {:?}", scene); debug!("switching to {:?}", scene);
self.current_scene = Some(scene); self.current_scene = scene;
self.current_scene_config = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"config",
(),
)
.with_context(|| format!("while handling `config()`"))
.with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?;
self.scope.clear(); self.scope.clear();
self.elements.clear(); self.elements.clear();
let mut used_names = HashSet::new(); let mut used_names = HashSet::new();
let builders: Array = self trace!("running init for `{}`", self.current_scene.to_string());
.engine
.call_fn( let builders: Array = self.engine.call_fn(
&mut self.scope, &mut self.scope,
&self Self::get_scene_ast(&self.ct, self.current_scene),
.ct "init",
.get_config() (State {
.ui_scenes planet_landscape: "ui::landscape::test".to_string(),
.get(self.current_scene.as_ref().unwrap()) planet_name: "Earth".to_string(),
.unwrap(), },),
"init", )?;
(State::new(input.clone()),),
)
.with_context(|| format!("while running `init()`"))
.with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?;
trace!("found {:?} builders", builders.len()); trace!("found {:?} builders", builders.len());
for v in builders { for v in builders {
if v.is::<SpriteBuilder>() { if v.is::<SpriteBuilder>() {
let s = v.cast::<SpriteBuilder>(); let s = v.cast::<SpriteBuilder>();
if used_names.contains(s.name.as_str()) { if used_names.contains(&s.name) {
error!( error!(
"UI scene `{}` re-uses element name `{}`", "UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(), self.current_scene.to_string(),
s.name s.name
); );
} else { } else {
used_names.insert(s.name.to_string()); used_names.insert(s.name.clone());
} }
self.elements.push(UiElement::new_sprite(Sprite::new( self.elements.push(UiElement::new_sprite(Sprite::new(
&self.ct, &self.ct, s.name, s.sprite, s.mask, s.rect,
s.name.to_string(),
s.sprite.to_string(),
s.mask.map(|x| x.to_string()),
s.rect,
)));
} else if v.is::<RadialBuilder>() {
let r = v.cast::<RadialBuilder>();
if used_names.contains(r.name.as_str()) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(),
r.name
);
} else {
used_names.insert(r.name.to_string());
}
self.elements.push(UiElement::new_radialbar(RadialBar::new(
&self.ct,
r.name.to_string(),
r.stroke,
r.color,
r.rect,
r.progress,
))); )));
} else if v.is::<TextBoxBuilder>() { } else if v.is::<TextBoxBuilder>() {
let t = v.cast::<TextBoxBuilder>(); let t = v.cast::<TextBoxBuilder>();
if used_names.contains(t.name.as_str()) { if used_names.contains(&t.name) {
error!( error!(
"UI scene `{}` re-uses element name `{}`", "UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(), self.current_scene.to_string(),
t.name t.name
); );
} else { } else {
used_names.insert(t.name.to_string()); used_names.insert(t.name.clone());
} }
let mut b = TextBox::new( let mut b = TextBox::new(
state, state,
t.name.to_string(), t.name,
t.font_size, t.font_size,
t.line_height, t.line_height,
t.font, t.font,
@ -181,161 +186,65 @@ impl UiManager {
} }
/// Draw all ui elements /// Draw all ui elements
pub fn draw(&mut self, input: Rc<RenderInput>, state: &mut RenderState) -> Result<()> { pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) -> Result<()> {
// Initialize start scene if we haven't yet let mut iter = self
if self.current_scene.is_none() { .elements
self.set_scene( .iter()
state, .map(|x| x.sprite())
input.clone(), .filter(|x| x.is_some())
self.ct.get_config().start_ui_scene.clone(), .map(|x| x.unwrap());
)?;
}
// Update timings if they're being displayed let action: SceneAction = loop {
if self.show_timings { let e = match iter.next() {
self.fps_indicator.step(&input, state); Some(e) => e,
} None => break SceneAction::None,
};
// Send player state change events let mut x = (*e).borrow_mut();
if { let m = x.check_mouse(input, state);
let player = input.player.ship; x.step(input, state);
if let Some(player) = player { x.push_to_buffer(input, state);
let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap(); drop(x);
if self.last_player_state == 0 // we MUST drop here, since script calls mutate the sprite RefCell
|| NonZeroU32::new(self.last_player_state).unwrap()
!= ship.ship.get_data().get_state().as_int() let action: Dynamic = match m {
{ MouseEvent::None => Dynamic::from(SceneAction::None),
self.last_player_state = ship.ship.get_data().get_state().as_int().into();
true MouseEvent::Release | MouseEvent::Click => self.engine.call_fn(
} else {
false
}
} else {
self.last_player_state = 0;
true
}
} {
let action: Dynamic = self
.engine
.call_fn(
&mut self.scope, &mut self.scope,
&self Self::get_scene_ast(&self.ct, self.current_scene),
.ct "click",
.get_config() (SpriteElement::new(self.ct.clone(), e.clone()), m.is_click()),
.ui_scenes )?,
.get(self.current_scene.as_ref().unwrap())
.unwrap(), MouseEvent::Leave | MouseEvent::Enter => self.engine.call_fn(
"event", &mut self.scope,
(State::new(input.clone()), PlayerShipStateEvent {}), Self::get_scene_ast(&self.ct, self.current_scene),
) "hover",
.with_context(|| format!("while handling player state change event")) (SpriteElement::new(self.ct.clone(), e.clone()), m.is_enter()),
.with_context(|| { )?,
format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()) };
})?;
if let Some(action) = action.try_cast::<SceneAction>() { if let Some(action) = action.try_cast::<SceneAction>() {
if self.handle_action(state, input.clone(), action)? { match action {
return Ok(()); SceneAction::None => {}
} _ => {
} break action;
}
for i in 0..self.elements.len() {
match &self.elements[i] {
UiElement::Sprite(e) => {
// Draw and update sprites
let mut sprite = (*e).borrow_mut();
sprite.step(&input, state);
sprite.push_to_buffer(&input, state);
let event = sprite.check_events(&input, state);
// we MUST drop here, since script calls mutate the sprite RefCell
drop(sprite);
match event {
Event::None => continue,
_ => {}
}
let event_arg = match event {
Event::None => unreachable!("this shouldn't happen"),
Event::MouseClick => Dynamic::from(MouseClickEvent {
down: true,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseRelease => Dynamic::from(MouseClickEvent {
down: false,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseHover => Dynamic::from(MouseHoverEvent {
enter: true,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseUnhover => Dynamic::from(MouseHoverEvent {
enter: false,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
};
let action: Dynamic = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"event",
(State::new(input.clone()), event_arg),
)
.with_context(|| format!("while handling event `{:?}`", event))
.with_context(|| {
format!("in ui scene `{}`", self.current_scene.as_ref().unwrap())
})?;
if let Some(action) = action.try_cast::<SceneAction>() {
if self.handle_action(state, input.clone(), action)? {
return Ok(());
}
} }
} }
UiElement::RadialBar(e) => {
// Draw and update radialbar
let mut x = (*e).borrow_mut();
x.step(&input, state);
x.push_to_buffer(&input, state);
}
UiElement::Text(..) => {}
} }
};
drop(iter);
match action {
SceneAction::None => {}
SceneAction::SceneOutfitter => self.set_scene(state, UiScene::Outfitter)?,
SceneAction::SceneLanded => self.set_scene(state, UiScene::Landed)?,
} }
return Ok(()); return Ok(());
} }
/// Do a SceneAction.
/// If this returns true, all evaluation stops immediately.
fn handle_action(
&mut self,
state: &mut RenderState,
input: Rc<RenderInput>,
action: SceneAction,
) -> Result<bool> {
Ok(match action {
SceneAction::None => false,
SceneAction::GoTo(s) => {
self.set_scene(state, input.clone(), s.clone())?;
true
}
})
}
} }
impl<'a> UiManager { impl<'a> UiManager {
@ -345,27 +254,12 @@ impl<'a> UiManager {
input: &RenderInput, input: &RenderInput,
state: &RenderState, state: &RenderState,
) -> Vec<TextArea<'a>> { ) -> Vec<TextArea<'a>> {
let mut v = Vec::with_capacity(32); self.elements
if self.current_scene.is_none() {
return v;
}
if self.show_timings {
v.push(self.fps_indicator.get_textarea(state, input))
}
for t in self
.elements
.iter() .iter()
.map(|x| x.text()) .map(|x| x.text())
.filter(|x| x.is_some()) .filter(|x| x.is_some())
.map(|x| x.unwrap()) .map(|x| x.unwrap())
.map(|x| x.get_textarea(state, input)) .map(|x| x.get_textarea(state, input))
{ .collect()
v.push(t)
}
v
} }
} }

View File

@ -1,8 +1,6 @@
mod api; mod api;
mod event;
mod manager; mod manager;
mod uielement;
mod util; mod util;
pub(crate) use manager::UiManager; pub(crate) use manager::UiManager;
pub(crate) use uielement::UiElement; pub(crate) use manager::UiScene;

View File

@ -1,31 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use super::util::{RadialBar, Sprite, TextBox};
#[derive(Debug)]
pub enum UiElement {
Sprite(Rc<RefCell<Sprite>>),
RadialBar(Rc<RefCell<RadialBar>>),
Text(TextBox),
}
impl UiElement {
pub fn new_sprite(sprite: Sprite) -> Self {
Self::Sprite(Rc::new(RefCell::new(sprite)))
}
pub fn new_radialbar(bar: RadialBar) -> Self {
Self::RadialBar(Rc::new(RefCell::new(bar)))
}
pub fn new_text(text: TextBox) -> Self {
Self::Text(text)
}
pub fn text(&self) -> Option<&TextBox> {
match self {
Self::Text(t) => Some(t),
_ => None,
}
}
}

View File

@ -1,60 +0,0 @@
use glyphon::{Attrs, Buffer, Color, Family, Metrics, Shaping, TextArea, TextBounds};
use crate::{RenderInput, RenderState};
pub(crate) struct FpsIndicator {
buffer: Buffer,
update_counter: u32,
}
impl FpsIndicator {
pub fn new(state: &mut RenderState) -> Self {
let mut buffer = Buffer::new(&mut state.text_font_system, Metrics::new(7.0, 8.0));
buffer.set_size(
&mut state.text_font_system,
state.window_size.width as f32,
state.window_size.height as f32,
);
Self {
buffer,
update_counter: 0,
}
}
}
impl FpsIndicator {
pub fn step(&mut self, input: &RenderInput, state: &mut RenderState) {
if self.update_counter > 0 {
self.update_counter -= 1;
return;
}
self.update_counter = 100;
self.buffer.set_text(
&mut state.text_font_system,
&input.timing.get_string(),
Attrs::new().family(Family::Monospace),
Shaping::Basic,
);
self.buffer.shape_until_scroll(&mut state.text_font_system);
}
}
impl<'a, 'b: 'a> FpsIndicator {
pub fn get_textarea(&'b self, _state: &RenderState, input: &RenderInput) -> TextArea<'a> {
TextArea {
buffer: &self.buffer,
left: 10.0,
top: 400.0,
scale: input.ct.get_config().ui_scale,
bounds: TextBounds {
left: 10,
top: 400,
right: 300,
bottom: 800,
},
default_color: Color::rgb(255, 255, 255),
}
}
}

View File

@ -1,9 +1,5 @@
mod fpsindicator;
mod radialbar;
mod sprite; mod sprite;
mod textbox; mod textbox;
pub(super) use fpsindicator::*; pub use sprite::*;
pub(super) use radialbar::*; pub use textbox::*;
pub(super) use sprite::*;
pub(super) use textbox::*;

View File

@ -1,47 +0,0 @@
use galactica_content::Content;
use std::f32::consts::TAU;
use super::super::api::Rect;
use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState};
#[derive(Debug, Clone)]
pub struct RadialBar {
pub name: String,
pub rect: Rect,
pub stroke: f32,
pub color: Color,
pub progress: f32,
}
impl RadialBar {
pub fn new(
_ct: &Content,
name: String,
stroke: f32,
color: Color,
rect: Rect,
progress: f32,
) -> Self {
Self {
name,
rect,
stroke,
color,
progress,
}
}
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
state.push_radialbar_buffer(RadialBarInstance {
position: [rect.pos.x, rect.pos.y],
diameter: rect.dim.x.min(rect.dim.y),
stroke: self.stroke * input.ct.get_config().ui_scale,
color: self.color.as_array(),
angle: self.progress * TAU,
});
}
pub fn step(&mut self, _input: &RenderInput, _state: &mut RenderState) {}
}

View File

@ -1,17 +1,14 @@
use galactica_content::{Content, SpriteAutomaton, SpriteHandle}; use galactica_content::{Content, SpriteAutomaton, SpriteHandle};
use galactica_util::to_radians; use galactica_util::to_radians;
use log::error;
use super::super::api::Rect; use super::super::api::Rect;
use crate::{ui::event::Event, vertexbuffer::types::UiInstance, RenderInput, RenderState}; use crate::{ui::manager::MouseEvent, vertexbuffer::types::UiInstance, RenderInput, RenderState};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Sprite { pub struct Sprite {
/// If this is none, this was constructed with an invalid sprite pub anim: SpriteAutomaton,
pub anim: Option<SpriteAutomaton>, pub rect: Rect,
pub mask: Option<SpriteHandle>,
rect: Rect,
mask: Option<SpriteHandle>,
pub name: String, pub name: String,
has_mouse: bool, has_mouse: bool,
@ -28,24 +25,10 @@ impl Sprite {
mask: Option<String>, mask: Option<String>,
rect: Rect, rect: Rect,
) -> Self { ) -> Self {
let sprite_handle = ct.get_sprite_handle(&sprite); let sprite_handle = ct.get_sprite_handle(&sprite).unwrap(); // TODO: errors
if sprite_handle.is_none() {
error!("UI constructed a sprite named `{name}` using an invalid source `{sprite}`")
}
let mask = { let mask = {
if mask.is_some() { if mask.is_some() {
let m = ct.get_sprite_handle(mask.as_ref().unwrap()); Some(ct.get_sprite_handle(&mask.unwrap()).unwrap())
if m.is_none() {
error!(
"UI constructed a sprite named `{name}` using an invalid mask` {}`",
mask.unwrap()
);
None
} else {
m
}
} else { } else {
None None
} }
@ -53,7 +36,7 @@ impl Sprite {
Self { Self {
name, name,
anim: sprite_handle.map(|x| SpriteAutomaton::new(&ct, x)), anim: SpriteAutomaton::new(&ct, sprite_handle),
rect, rect,
mask, mask,
has_mouse: false, has_mouse: false,
@ -63,17 +46,14 @@ impl Sprite {
} }
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
if self.anim.is_none() {
return;
}
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale); let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
// TODO: use both dimensions, // TODO: use both dimensions,
// not just height // not just height
let anim_state = self.anim.as_ref().unwrap().get_texture_idx(); let anim_state = self.anim.get_texture_idx();
state.push_ui_buffer(UiInstance { state.push_ui_buffer(UiInstance {
anchor: crate::PositionAnchor::CC.to_int(),
position: rect.pos.into(), position: rect.pos.into(),
angle: to_radians(90.0), angle: to_radians(90.0),
dim: rect.dim.into(), dim: rect.dim.into(),
@ -91,7 +71,7 @@ impl Sprite {
}); });
} }
pub fn check_events(&mut self, input: &RenderInput, state: &mut RenderState) -> Event { pub fn check_mouse(&mut self, input: &RenderInput, state: &mut RenderState) -> MouseEvent {
let r = self.rect.to_centered(state, input.ct.get_config().ui_scale); let r = self.rect.to_centered(state, input.ct.get_config().ui_scale);
if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() { if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() {
@ -104,18 +84,18 @@ impl Sprite {
&& input.player.input.pressed_leftclick() && input.player.input.pressed_leftclick()
{ {
self.has_click = true; self.has_click = true;
return Event::MouseClick; return MouseEvent::Click;
} }
if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() { if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() {
self.has_click = false; self.has_click = false;
return Event::MouseRelease; return MouseEvent::Release;
} }
// 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; return MouseEvent::Release;
} }
if r.contains_mouse(input, state) && !self.has_mouse { if r.contains_mouse(input, state) && !self.has_mouse {
@ -125,26 +105,19 @@ impl Sprite {
self.waiting_for_release = true; self.waiting_for_release = true;
} }
self.has_mouse = true; self.has_mouse = true;
return Event::MouseHover; return MouseEvent::Enter;
} }
if !r.contains_mouse(input, state) && self.has_mouse { if !r.contains_mouse(input, state) && self.has_mouse {
self.waiting_for_release = false; self.waiting_for_release = false;
self.has_mouse = false; self.has_mouse = false;
return Event::MouseUnhover; return MouseEvent::Leave;
} }
return Event::None; return MouseEvent::None;
} }
pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) { pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) {
if self.anim.is_none() { self.anim.step(&input.ct, input.time_since_last_run);
return;
}
self.anim
.as_mut()
.unwrap()
.step(&input.ct, input.time_since_last_run);
} }
} }

View File

@ -143,6 +143,10 @@ impl BufferObject for ObjectInstance {
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiInstance { pub struct UiInstance {
/// How to interpret this object's coordinates.
/// this should be generated from an AnchoredUiPosition
pub anchor: u32,
/// Position of this object in logical pixels /// Position of this object in logical pixels
pub position: [f32; 2], pub position: [f32; 2],
@ -179,46 +183,52 @@ impl BufferObject for UiInstance {
// instance when the shader starts processing a new instance // instance when the shader starts processing a new instance
step_mode: wgpu::VertexStepMode::Instance, step_mode: wgpu::VertexStepMode::Instance,
attributes: &[ attributes: &[
// Anchor
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Uint32,
},
// Position // Position
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 0]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 1]>() as wgpu::BufferAddress,
shader_location: 2, shader_location: 3,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x2,
}, },
// Angle // Angle
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 3, shader_location: 4,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Dim // Dim
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 4, shader_location: 5,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x2,
}, },
// Color // Color
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
shader_location: 5, shader_location: 6,
format: wgpu::VertexFormat::Float32x4, format: wgpu::VertexFormat::Float32x4,
}, },
// Texture // Texture
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 9]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
shader_location: 6, shader_location: 7,
format: wgpu::VertexFormat::Uint32x2, format: wgpu::VertexFormat::Uint32x2,
}, },
// Texture fade // Texture fade
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 7, shader_location: 8,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Mask // Mask
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 13]>() as wgpu::BufferAddress,
shader_location: 8, shader_location: 9,
format: wgpu::VertexFormat::Uint32x2, format: wgpu::VertexFormat::Uint32x2,
}, },
], ],
@ -229,6 +239,10 @@ impl BufferObject for UiInstance {
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RadialBarInstance { pub struct RadialBarInstance {
/// How to interpret this object's coordinates.
/// this should be generated from an AnchoredUiPosition
pub anchor: u32,
/// Position of this object in logical pixels /// Position of this object in logical pixels
pub position: [f32; 2], pub position: [f32; 2],
@ -253,34 +267,40 @@ impl BufferObject for RadialBarInstance {
array_stride: Self::SIZE, array_stride: Self::SIZE,
step_mode: wgpu::VertexStepMode::Instance, step_mode: wgpu::VertexStepMode::Instance,
attributes: &[ attributes: &[
// Anchor
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Uint32,
},
// Position // Position
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 0]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 1]>() as wgpu::BufferAddress,
shader_location: 2, shader_location: 3,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x2,
}, },
// Diameter // Diameter
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
},
// Width
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 4, shader_location: 4,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Angle // Width
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 5, shader_location: 5,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Color // Angle
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
shader_location: 6, shader_location: 6,
format: wgpu::VertexFormat::Float32,
},
// Color
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4, format: wgpu::VertexFormat::Float32x4,
}, },
], ],

View File

@ -1,5 +1,3 @@
use std::num::NonZeroU32;
use galactica_content::SystemObjectHandle; use galactica_content::SystemObjectHandle;
use rapier2d::math::Isometry; use rapier2d::math::Isometry;
@ -75,66 +73,4 @@ impl ShipState {
_ => None, _ => None,
} }
} }
/// An integer representing each state.
/// Makes detecting state changes cheap and easy.
pub fn as_int(&self) -> NonZeroU32 {
NonZeroU32::new(match self {
Self::Dead => 1,
Self::Collapsing => 2,
Self::Flying { .. } => 3,
Self::Landed { .. } => 4,
Self::Landing { .. } => 5,
Self::UnLanding { .. } => 6,
})
.unwrap()
}
/// Is this state Dead?
pub fn is_dead(&self) -> bool {
match self {
Self::Dead => true,
_ => false,
}
}
/// Is this state Collapsing?
pub fn is_collapsing(&self) -> bool {
match self {
Self::Collapsing => true,
_ => false,
}
}
/// Is this state Flying?
pub fn is_flying(&self) -> bool {
match self {
Self::Flying { .. } => true,
_ => false,
}
}
/// Is this state Landed?
pub fn is_landed(&self) -> bool {
match self {
Self::Landed { .. } => true,
_ => false,
}
}
/// Is this state Landing?
pub fn is_landing(&self) -> bool {
match self {
Self::Landing { .. } => true,
_ => false,
}
}
/// Is this state UnLanding?
pub fn is_unlanding(&self) -> bool {
match self {
Self::UnLanding { .. } => true,
_ => false,
}
}
} }