Compare commits

...

7 Commits

Author SHA1 Message Date
Mark 92a03ccbd1
Improved outfitter UI 2024-02-16 13:29:47 -08:00
Mark 69b35bd43c
API updates 2024-02-16 13:29:19 -08:00
Mark 327e2b43b3
Minor cleanup 2024-02-16 13:29:01 -08:00
Mark 5528f6ed4e
API tweaks 2024-02-16 13:28:47 -08:00
Mark 308e380241
API fixes 2024-02-16 13:25:50 -08:00
Mark 855eb0680b
Organized outfits & added descriptions 2024-02-16 13:24:52 -08:00
Mark f53e191de3
Minor fix 2024-02-16 13:23:44 -08:00
24 changed files with 804 additions and 284 deletions

View File

@ -1,8 +1,11 @@
# Specific projects
## Now:
- init on resize
- hide ui element
- zoom limits
- text scrolling
- scrollbox scroll limits
- clean up content
- Clean up state api
- Clean up & document UI api
@ -12,6 +15,8 @@
- Selection while flying
- outfitter
- fps textbox positioning
- shield generation curve
- clippy & rules
## Small jobs
- Better planet icons in radar

2
assets

@ -1 +1 @@
Subproject commit b8ea8c5a04c0216304a81ebf396b7320a709d6ed
Subproject commit bd425ccb10bb16661620916ff4f1a387b54ce7ca

View File

@ -2,6 +2,14 @@
name = "Plasma Engines"
thumbnail = "icon::engine"
desc = """
This is the smallest of the plasma propulsion systems produced by
Delta V Corporation, suitable for very light fighters and interceptors.
Plasma engines are a bit more powerful than ion engines of the same size,
but they are less energy efficient and produce more heat.
"""
space.engine = 20
engine.thrust = 100
engine.flare.sprite = "flare::ion"
@ -14,6 +22,15 @@ steering.power = 20
thumbnail = "icon::shield"
name = "Shield Generator"
desc = """
This is the standard shield generator for fighters and interceptors,
as well as for many non-combat starships. Although it is possible for
a ship to have no shield generator at all, recharging its shield matrix
only when landed in a hospitable spaceport, life in deep space is
unpredictable enough that most pilots find shield generators to be
well worth the space they take up.
"""
space.outfit = 5
shield.generation = 10
shield.strength = 500
@ -24,6 +41,13 @@ shield.delay = 2.0
thumbnail = "icon::blaster"
name = "Blaster"
desc = """
Although not the most accurate or damaging of weapons, the Energy Blaster is popular because it
is small enough to be installed on even the tiniest of ships. One blaster is not enough to do
appreciable damage to anything larger than a fighter, but a ship equipped with several of them
becomes a force to be reckoned with.
"""
space.weapon = 10
# Average delay between shots

View File

@ -57,8 +57,8 @@ fn init(state) {
(radar_size / 2.0 + 5),
(radar_size / -2.0 - 5),
3.5, 3.5,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
);
sprite::add("radar.frame.ne", "ui::radarframe", init_pos);
@ -114,12 +114,12 @@ fn event(state, event) {
fn step(state) {
radialbar::set_val("shield",
state.player_ship().get_shields()
/ state.player_ship().get_total_shields()
state.player_ship().current_shields()
/ state.player_ship().stat_shield_strength()
);
radialbar::set_val("hull",
state.player_ship().get_hull()
/ state.player_ship().get_total_hull()
state.player_ship().current_hull()
/ state.player_ship().total_hull()
);
@ -138,8 +138,8 @@ fn step(state) {
Rect(
pos.x(), pos.y(),
5.0, 5.0,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::set_angle("radar.arrow", angle - 90.0);
@ -201,8 +201,8 @@ fn step(state) {
Rect(
pos.x(), pos.y(),
size, size,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
} else {
@ -211,8 +211,8 @@ fn step(state) {
Rect(
pos.x(), pos.y(),
size, size,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
}
@ -263,8 +263,8 @@ fn step(state) {
Rect(
pos.x(), pos.y(),
size, size,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
} else {
@ -273,8 +273,8 @@ fn step(state) {
Rect(
pos.x(), pos.y(),
size, size,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
}
@ -291,8 +291,8 @@ fn step(state) {
(radar_size / 2.0 + 5) - dx,
(radar_size / -2.0 - 5) + dy,
3.5, 3.5,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::set_rect("radar.frame.se",
@ -300,8 +300,8 @@ fn step(state) {
(radar_size / 2.0 + 5) - dx,
(radar_size / -2.0 - 5) - dy,
3.5, 3.5,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::set_rect("radar.frame.sw",
@ -309,8 +309,8 @@ fn step(state) {
(radar_size / 2.0 + 5) + dx,
(radar_size / -2.0 - 5) - dy,
3.5, 3.5,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::set_rect("radar.frame.nw",
@ -318,8 +318,8 @@ fn step(state) {
(radar_size / 2.0 + 5) + dx,
(radar_size / -2.0 - 5) + dy,
3.5, 3.5,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
}

View File

@ -7,8 +7,8 @@ fn init(state) {
"ui::planet::button",
Rect(
99.0, 128.0, 73.898, 18.708,
Anchor::NorthWest,
Anchor::Center
Anchor::Center,
Anchor::NorthWest
)
);
@ -23,8 +23,8 @@ fn init(state) {
},
Rect(
-180.0, 142.0, 274.0, 135.0,
Anchor::NorthWest,
Anchor::Center
Anchor::Center,
Anchor::NorthWest
)
);
sprite::set_mask("landscape", "ui::landscapemask");
@ -40,17 +40,17 @@ fn init(state) {
);
// If this is not set, the button will
// not receive events
sprite::set_disable_events("frame", true);
sprite::disable_events("frame", true);
textbox::add(
"title", 10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-70.79, 138.0, 59.867, 10.0,
Anchor::NorthWest,
Anchor::Center
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::Center,
Anchor::NorthWest
)
);
textbox::align_center("title");
textbox::font_serif("title");
@ -61,12 +61,12 @@ fn init(state) {
textbox::add(
"desc", 7.5, 8.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-178.92, -20.3, 343.0, 81.467,
Anchor::NorthWest,
Anchor::Center
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::Center,
Anchor::NorthWest
)
);
textbox::font_sans("desc");
if state.player_ship().is_landed() {

View File

@ -3,6 +3,11 @@ fn init(state) {
conf::show_starfield(true);
conf::show_phys(false);
if !state.player_ship().is_landed() {
print("UI error: player isn't landed when initializing outfitter scene");
return;
}
sprite::add(
"se_box",
"ui::outfitterbox",
@ -15,12 +20,13 @@ fn init(state) {
textbox::add(
"exit_text", 10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
122.71, 48.0, 51.0, 12.0,
Anchor::NorthWest,
Anchor::SouthWest
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::SouthWest,
Anchor::NorthWest
)
);
textbox::font_serif("exit_text");
textbox::align_center("exit_text");
@ -31,8 +37,8 @@ fn init(state) {
"ui::button",
Rect(
113.35, 52.0, 69.8, 18.924,
Anchor::NorthWest,
Anchor::SouthWest
Anchor::SouthWest,
Anchor::NorthWest
)
);
@ -53,53 +59,46 @@ fn init(state) {
"icon::gypsum",
Rect(
111.0, -95.45, 90.0, 90.0,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::preserve_aspect("ship_thumb", true);
textbox::add(
"ship_name", 10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
111.0, -167.27, 145.0, 10.0,
Anchor::Center,
Anchor::NorthWest
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::NorthWest,
Anchor::Center
)
);
textbox::font_serif("ship_name");
textbox::align_center("ship_name");
textbox::set_text("ship_name", "Hyperion");
textbox::add(
"ship_type", 7.0, 8.5,
Color(0.7, 0.7, 0.7, 1.0),
Rect(
111.0, -178.0, 145.0, 8.5,
Anchor::Center,
Anchor::NorthWest
),
Color(0.7, 0.7, 0.7, 1.0)
Anchor::NorthWest,
Anchor::Center
)
);
textbox::font_sans("ship_type");
textbox::align_center("ship_type");
if state.player_ship().is_some() {
textbox::set_text("ship_type", state.player_ship().display_name());
}
textbox::add(
"ship_stats", 7.0, 8.5,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
38.526, -192.332, 144.948, 154.5,
Anchor::NorthWest,
Anchor::NorthWest,
),
Color(1.0, 1.0, 1.0, 1.0)
)
);
textbox::font_mono("ship_stats");
textbox::set_text("ship_stats", "Earth");
sprite::add(
"outfit_bg",
@ -116,8 +115,8 @@ fn init(state) {
"icon::engine",
Rect(
-166.0, -109.0, 90.0, 90.0,
Anchor::Center,
Anchor::NorthEast
Anchor::NorthEast,
Anchor::Center
)
);
sprite::preserve_aspect("outfit_thumb", true);
@ -125,76 +124,182 @@ fn init(state) {
textbox::add(
"outfit_name", 16.0, 16.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-312.0, -20.0, 200.0, 16.0,
Anchor::NorthWest,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::NorthWest
)
);
textbox::font_serif("outfit_name");
textbox::weight_bold("outfit_name");
textbox::set_text("outfit_name", "Earth");
textbox::set_text("outfit_name", "");
textbox::add(
"outfit_desc", 7.0, 8.5,
"outfit_desc", 8.5, 9.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-166.0, -219.0, 260.0, 78.0,
Anchor::Center,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::Center
)
);
textbox::font_serif("outfit_desc");
textbox::set_text("outfit_desc", "Earth");
textbox::set_text("outfit_desc", "");
textbox::add(
"outfit_stats", 7.0, 8.5,
"outfit_stats", 8.5, 9.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-295.0, -271.0, 164.0, 216.0,
Anchor::NorthWest,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::NorthWest
)
);
textbox::font_mono("outfit_stats");
textbox::set_text("outfit_stats", "Earth");
textbox::set_text("outfit_stats", "");
sprite::add(
"buy_button",
"ui::button",
Rect(
-110.71, -281.0, 69.8, 18.924,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::add(
"buy_text", 10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-100.84, -285.34, 51.0, 12.0,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::font_serif("buy_text");
textbox::align_center("buy_text");
textbox::set_text("buy_text", "Buy");
sprite::add(
"sell_button",
"ui::button",
Rect(
-110.71, -306.2, 69.8, 18.924,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::add(
"sell_text", 10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
-100.84, -311.15, 51.0, 12.0,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::font_serif("sell_text");
textbox::align_center("sell_text");
textbox::set_text("sell_text", "Sell");
let times_five = false;
let times_ten = false;
let times_hundred = false;
textbox::add(
"five_text", 7.5, 7.5,
Color(0.5, 0.5, 0.5, 1.0),
Rect(
-110.71, -331.2, 69.8, 18.924,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::font_mono("five_text");
textbox::set_text("five_text", "[shift] x5");
textbox::add(
"ten_text", 7.5, 7.5,
Color(0.5, 0.5, 0.5, 1.0),
Rect(
-110.71, -341.2, 69.8, 18.924,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::font_mono("ten_text");
textbox::set_text("ten_text", "[ctrl] x10");
textbox::add(
"hundred_text", 7.5, 7.5,
Color(0.5, 0.5, 0.5, 1.0),
Rect(
-110.71, -351.2, 69.8, 18.924,
Anchor::NorthEast,
Anchor::NorthWest
)
);
textbox::font_mono("hundred_text");
textbox::set_text("hundred_text", "[alt] x100");
// A string containing the selected outfit's index.
// This is always set---if we have an outfitter, we must have at least one outfit.
let selected_outfit = false;
{
let scrollbox_width = state.window_size().x() - (190 + 16 + 300 + 16);
if scrollbox_width <= 0 {
scrollbox_width = 1;
}
// width should be calculated as a fraction of screen width
let scrollbox_rect = Rect(
222.0, -16.0, 470.0, 480.0,
Anchor::NorthWest,
222.0, -16.0, scrollbox_width, 480.0,
Anchor::NorthWest,
Anchor::NorthWest
);
scrollbox::add("outfit_list", scrollbox_rect);
let selected_outfit = false;
{
// p cannot be saved in the global scope.
let p = state.player_ship();
if p.is_landed() {
let s = "";
let x = scrollbox_rect.pos().x() + 45.0;
let y = scrollbox_rect.pos().y() - 45.0;
for xxx in ["1","2","3"] {
for i in p.landed_on().outfitter() {
if selected_outfit == false {
selected_outfit = i.index();
update_outfit_info(selected_outfit);
}
s = s + i.display_name() + "\n";
let thumb_name = "outfit.thumb." + i.index() + xxx;
let backg_name = "outfit.backg." + i.index() + xxx;
let title_name = "outfit.title." + i.index() + xxx;
let thumb_name = "outfit.thumb." + i.index();
let backg_name = "outfit.backg." + i.index();
let title_name = "outfit.title." + i.index();
sprite::add(
backg_name,
"ui::outfitbg",
Rect(
x, y, 90.0, 90.0,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::preserve_aspect(backg_name, true);
@ -205,8 +310,8 @@ fn init(state) {
i.thumbnail(),
Rect(
x, y, 75.0, 75.0,
Anchor::Center,
Anchor::NorthWest
Anchor::NorthWest,
Anchor::Center
)
);
sprite::preserve_aspect(thumb_name, true);
@ -215,12 +320,12 @@ fn init(state) {
textbox::add(
title_name,
10.0, 10.0,
Color(1.0, 1.0, 1.0, 1.0),
Rect(
x, y - 50.0, 90.0, 10.0,
Anchor::Center,
Anchor::NorthWest,
),
Color(1.0, 1.0, 1.0, 1.0)
Anchor::Center
)
);
textbox::font_sans(title_name);
textbox::align_center(title_name);
@ -236,63 +341,93 @@ fn init(state) {
}
}
}
textbox::set_text("outfit_stats", s);
}
}
update_ship_info(state);
}
fn event(state, event) {
if type_of(event) == "MouseHoverEvent" {
let element = event.element();
if element == "exit_button" {
if (
element == "exit_button"
|| element == "buy_button"
|| element == "sell_button"
){
if event.is_enter() {
sprite::jump_to("exit_button", "on:top", 0.1);
sprite::jump_to(element, "on:top", 0.1);
} else {
sprite::jump_to("exit_button", "off:top", 0.1);
sprite::jump_to(element, "off:top", 0.1);
}
}
if element.starts_with("outfit.backg.") && element != selected_outfit {
if element.starts_with("outfit.backg.") {
if event.element().split("outfit.backg.".len())[1] == selected_outfit {
if event.is_enter() {
sprite::jump_to(element, "hoverselected:top", 0.1);
} else {
sprite::jump_to(element, "selected:top", 0.1);
}
} else {
if event.is_enter() {
sprite::jump_to(element, "hover:top", 0.1);
} else {
sprite::jump_to(element, "off:top", 0.1);
}
}
}
return PlayerDirective::None;
}
// TODO: this occasionally breaks because of sprite ordering.
// Clicks go to se_box instead!
if type_of(event) == "MouseClickEvent" {
if !event.is_down() {
return PlayerDirective::None;
}
print(event.element());
let element = event.element();
if element == "exit_button" {
ui::go_to_scene("landed");
return PlayerDirective::None;
}
if element.starts_with("outfit.backg.") && element != selected_outfit {
if (
element.starts_with("outfit.backg.") &&
event.element().split("outfit.backg.".len())[1] != selected_outfit
) {
if selected_outfit != false {
sprite::jump_to(selected_outfit, "off:top", 0.1);
sprite::jump_to("outfit.backg." + selected_outfit, "off:top", 0.1);
}
sprite::jump_to(element, "selected:top", 0.1);
selected_outfit = element;
/// We can't click on this sprite without first hovering!
sprite::jump_to(element, "hoverselected:top", 0.1);
selected_outfit = event.element().split("outfit.backg.".len())[1];
update_outfit_info(selected_outfit);
return PlayerDirective::None;
}
return;
}
if type_of(event) == "KeyboardEvent" {
if event.key() == "up" {
times_five = event.is_down();
}
if event.key() == "left" {
times_ten = event.is_down();
}
if event.key() == "right" {
times_hundred = event.is_down();
}
update_outfit_box(times_five, times_ten, times_hundred);
return PlayerDirective::None;
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
ui::go_to_scene("flying");
@ -300,3 +435,142 @@ fn event(state, event) {
return PlayerDirective::None;
}
}
fn update_outfit_box(times_five, times_ten, times_hundred) {
let times = 1;
if times_five {
times *= 5;
textbox::set_color("five_text", Color(1.0, 1.0, 1.0, 1.0));
} else {
textbox::set_color("five_text", Color(0.5, 0.5, 0.5, 1.0));
}
if times_ten {
times *= 10;
textbox::set_color("ten_text", Color(1.0, 1.0, 1.0, 1.0));
} else {
textbox::set_color("ten_text", Color(0.5, 0.5, 0.5, 1.0));
}
if times_hundred {
times *= 100;
textbox::set_color("hundred_text", Color(1.0, 1.0, 1.0, 1.0));
} else {
textbox::set_color("hundred_text", Color(0.5, 0.5, 0.5, 1.0));
}
if times != 1 {
textbox::set_text("buy_text", "Buy x" + times);
textbox::set_text("sell_text", "Sell x" + times);
} else {
textbox::set_text("buy_text", "Buy");
textbox::set_text("sell_text", "Sell");
}
}
fn update_outfit_info(selected_outfit) {
let outfit = ct::get_outfit(selected_outfit);
if outfit.is_some() {
let stats = "";
let tlen = 20;
if outfit.stat_thrust() != 0 {
let s = "thrust ";
s.pad(tlen, " ");
stats += s + outfit.stat_thrust();
stats += "\n";
}
if outfit.stat_steer_power() != 0 {
let s = "steer power ";
s.pad(tlen, " ");
stats += s + outfit.stat_steer_power();
stats += "\n";
}
if outfit.stat_shield_strength() != 0 {
let s = "shield strength ";
s.pad(tlen, " ");
stats += s + outfit.stat_shield_strength();
stats += "\n";
}
if outfit.stat_shield_generation() != 0 {
let s = "shield regen";
s.pad(tlen, " ");
stats += s + outfit.stat_shield_generation();
stats += "\n";
}
if outfit.stat_shield_delay() != 0 {
let s = "shield delay ";
s.pad(tlen, " ");
stats += s + outfit.stat_shield_delay();
stats += "\n";
}
if outfit.stat_shield_dps() != 0 {
let s = "shield dps ";
s.pad(tlen, " ");
stats += s + outfit.stat_shield_dps();
stats += "\n";
}
sprite::set_sprite("outfit_thumb", outfit.thumbnail());
textbox::set_text("outfit_name", outfit.display_name());
textbox::set_text("outfit_desc", outfit.desc());
textbox::set_text("outfit_stats", stats);
}
}
fn update_ship_info(state) {
let ship = state.player_ship();
if ship.is_some() {
let stats = "";
let tlen = 20;
// TODO: outfits add mass
// TODO: calculate radial acceleration
{
let s = "shield strength ";
s.pad(tlen, " ");
stats += s + ship.stat_shield_strength();
stats += "\n";
}
{
let s = "hull strength ";
s.pad(tlen, " ");
stats += s + ship.total_hull();
stats += "\n\n";
}
{
let s = "mass ";
s.pad(tlen, " ");
stats += s + ship.empty_mass();
stats += "\n";
}
{
let s = "thrust ";
s.pad(tlen, " ");
stats += s + ship.stat_thrust();
stats += "\n";
}
{
let s = "steer power ";
s.pad(tlen, " ");
stats += s + ship.stat_steer_power();
stats += "\n";
}
{
let s = "max shield regen";
s.pad(tlen, " ");
stats += s + ship.stat_max_shield_generation();
stats += "\n";
}
sprite::set_sprite("ship_thumb", ship.thumbnail());
textbox::set_text("ship_name", "Hyperion");
textbox::set_text("ship_type", state.player_ship().display_name());
textbox::set_text("ship_stats", stats);
}
}

View File

@ -137,7 +137,7 @@ impl From<ImmutableString> for ContentIndex {
impl From<ContentIndex> for ImmutableString {
fn from(value: ContentIndex) -> Self {
ImmutableString::from(Arc::into_inner(value.0).unwrap())
ImmutableString::from(value.0.as_ref().clone())
}
}

View File

@ -12,7 +12,7 @@ pub(crate) mod system;
pub use config::Config;
pub use effect::*;
pub use faction::{Faction, Relationship};
pub use outfit::{Gun, Outfit, Projectile, ProjectileCollider};
pub use outfit::*;
pub use outfitspace::OutfitSpace;
pub use ship::{
CollapseEffectSpawner, CollapseEvent, EffectCollapseEvent, EnginePoint, GunPoint, Ship,

View File

@ -23,6 +23,7 @@ pub(crate) mod syntax {
pub struct Outfit {
pub thumbnail: ContentIndex,
pub name: String,
pub desc: String,
pub engine: Option<Engine>,
pub steering: Option<Steering>,
pub space: outfitspace::syntax::OutfitSpace,
@ -145,15 +146,12 @@ pub struct Outfit {
/// The name of this outfit
pub display_name: String,
/// The description of this outfit
pub desc: String,
/// Thie outfit's index
pub index: ContentIndex,
/// How much engine thrust this outfit produces
pub engine_thrust: f32,
/// How much steering power this outfit provids
pub steer_power: f32,
/// The engine flare sprite this outfit creates.
/// Its location and size is determined by a ship's
/// engine points.
@ -165,6 +163,23 @@ pub struct Outfit {
/// Jump to this edge when engines turn off
pub engine_flare_on_stop: Option<SectionEdge>,
/// This outfit's gun stats.
/// If this is some, this outfit requires a gun point.
pub gun: Option<Gun>,
/// The stats this outfit provides
pub stats: OutfitStats,
}
/// Outfit statistics
#[derive(Debug, Clone)]
pub struct OutfitStats {
/// How much engine thrust this outfit produces
pub engine_thrust: f32,
/// How much steering power this outfit provids
pub steer_power: f32,
/// Shield hit points
pub shield_strength: f32,
@ -173,10 +188,37 @@ pub struct Outfit {
/// Wait this many seconds after taking damage before regenerating shields
pub shield_delay: f32,
}
/// This outfit's gun stats.
/// If this is some, this outfit requires a gun point.
pub gun: Option<Gun>,
impl OutfitStats {
/// Create a new `OutfitStats`, with all values set to zero.
pub fn zero() -> Self {
Self {
engine_thrust: 0.0,
steer_power: 0.0,
shield_strength: 0.0,
shield_generation: 0.0,
shield_delay: 0.0,
}
}
/// Add all the stats in `other` to the stats in `self`.
/// Sheld delay is not affected.
pub fn add(&mut self, other: &Self) {
self.engine_thrust += other.engine_thrust;
self.steer_power += other.steer_power;
self.shield_strength += other.shield_strength;
self.shield_generation += other.shield_generation;
}
/// Subtract all the stats in `other` from the stats in `self`.
/// Sheld delay is not affected.
pub fn subtract(&mut self, other: &Self) {
self.engine_thrust -= other.engine_thrust;
self.steer_power -= other.steer_power;
self.shield_strength -= other.shield_strength;
self.shield_generation -= other.shield_generation;
}
}
/// Defines a projectile's collider
@ -284,15 +326,12 @@ impl crate::Build for Outfit {
display_name: outfit.name,
thumbnail,
gun,
engine_thrust: 0.0,
steer_power: 0.0,
desc: outfit.desc,
engine_flare_sprite: None,
engine_flare_on_start: None,
engine_flare_on_stop: None,
space: OutfitSpace::from(outfit.space),
shield_delay: 0.0,
shield_generation: 0.0,
shield_strength: 0.0,
stats: OutfitStats::zero(),
};
// Engine stats
@ -307,7 +346,7 @@ impl crate::Build for Outfit {
}
Some(t) => t.clone(),
};
o.engine_thrust = engine.thrust;
o.stats.engine_thrust = engine.thrust;
o.engine_flare_sprite = Some(sprite.clone());
// Flare animation will traverse this edge when the player presses the thrust key
@ -381,14 +420,14 @@ impl crate::Build for Outfit {
// Steering stats
if let Some(steer) = outfit.steering {
o.steer_power = steer.power;
o.stats.steer_power = steer.power;
}
// Shield stats
if let Some(shield) = outfit.shield {
o.shield_delay = shield.delay.unwrap_or(0.0);
o.shield_generation = shield.generation.unwrap_or(0.0);
o.shield_strength = shield.strength.unwrap_or(0.0);
o.stats.shield_delay = shield.delay.unwrap_or(0.0);
o.stats.shield_generation = shield.generation.unwrap_or(0.0);
o.stats.shield_strength = shield.strength.unwrap_or(0.0);
}
content.outfits.insert(o.index.clone(), Arc::new(o));

View File

@ -145,6 +145,7 @@ pub struct LandableSystemObject {
pub image: Arc<Sprite>,
/// The outfits we can buy here
/// If this is empty, this landable has no outfitter.
pub outfitter: Vec<Arc<Outfit>>,
}

View File

@ -2,7 +2,6 @@ use crate::globaluniform::{AtlasArray, AtlasImageLocation};
use anyhow::Result;
use bytemuck::Zeroable;
use galactica_content::Content;
use galactica_packer::SpriteAtlasImage;
use galactica_util::constants::ASSET_CACHE;
use image::GenericImageView;
use log::info;
@ -72,16 +71,6 @@ impl RawTexture {
}
}
#[derive(Debug, Clone)]
pub struct Texture {
pub index: u32, // Index in texture array
pub len: u32, // Number of frames
pub frame_duration: f32, // Frames per second
pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture
pub location: Vec<SpriteAtlasImage>,
}
pub struct TextureArray {
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: BindGroupLayout,

View File

@ -0,0 +1,121 @@
use galactica_content::{Content, Outfit};
use log::error;
use rhai::{CustomType, FnNamespace, FuncRegistration, ImmutableString, Module, TypeBuilder};
use std::sync::Arc;
pub fn build_ct_module(ct_src: Arc<Content>) -> Module {
let mut module = Module::new();
module.set_id("GalacticaContentModule");
let ct = ct_src.clone();
let _ = FuncRegistration::new("get_outfit")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |outfit_name: ImmutableString| {
let outfit = ct.outfits.get(&outfit_name.clone().into());
match outfit {
Some(o) => OutfitState::new(o.clone()),
None => {
error!("called `ct::get_outfit` with an invalid outfit `{outfit_name:?}`");
OutfitState::new_none()
}
}
});
return module;
}
#[derive(Debug, Clone)]
pub struct OutfitState {
outfit: Option<Arc<Outfit>>,
}
impl OutfitState {
pub fn new(outfit: Arc<Outfit>) -> Self {
Self {
outfit: Some(outfit),
}
}
pub fn new_none() -> Self {
Self { outfit: None }
}
}
impl CustomType for OutfitState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("OutfitState")
.with_fn("is_some", |s: &mut Self| s.outfit.is_some())
.with_fn("display_name", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.display_name.clone())
.unwrap_or("".to_string())
})
.with_fn("desc", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.desc.clone())
.unwrap_or("".to_string())
})
.with_fn("index", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.index.to_string())
.unwrap_or("".to_string())
})
.with_fn("thumbnail", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.thumbnail.index.to_string())
.unwrap_or("".to_string())
})
//
// Stat getters
//
.with_fn("stat_thrust", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.stats.steer_power)
.unwrap_or(0.0)
})
.with_fn("stat_steer_power", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.stats.steer_power)
.unwrap_or(0.0)
})
.with_fn("stat_shield_strength", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.stats.shield_strength)
.unwrap_or(0.0)
})
.with_fn("stat_shield_generation", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.stats.shield_generation)
.unwrap_or(0.0)
})
.with_fn("stat_shield_delay", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| x.stats.shield_delay)
.unwrap_or(0.0)
})
.with_fn("stat_shield_dps", |s: &mut Self| {
s.outfit
.as_ref()
.map(|x| {
if x.gun.is_some() {
(1.0 / x.gun.as_ref().unwrap().rate)
* x.gun.as_ref().unwrap().projectile.damage
} else {
0.0
}
})
.unwrap_or(0.0)
});
}
}

View File

@ -1,4 +1,5 @@
mod conf;
mod ct;
mod radialbar;
mod scrollbox;
mod sprite;
@ -6,6 +7,7 @@ mod textbox;
mod ui;
pub use conf::build_conf_module;
pub use ct::{build_ct_module, OutfitState};
pub use radialbar::build_radialbar_module;
pub use scrollbox::build_scrollbox_module;
pub use sprite::build_sprite_module;

View File

@ -62,6 +62,32 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
}
});
let state = state_src.clone();
let ct = ct_src.clone();
let _ = FuncRegistration::new("set_sprite")
.with_namespace(FnNamespace::Internal)
.set_into_module(
&mut module,
move |name: ImmutableString, sprite: ImmutableString| {
let mut ui_state = state.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => {
let m = ct.sprites.get(&sprite.clone().into()).clone();
if m.is_none() {
error!("called `sprite::set_sprite` with an invalid sprite `{sprite}`");
return;
}
x.set_sprite(m.unwrap().clone())
}
_ => {
error!("called `sprite::set_sprite` on an invalid name `{sprite}`")
}
}
},
);
let state = state_src.clone();
let ct = ct_src.clone();
let _ = FuncRegistration::new("set_mask")
@ -198,6 +224,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
e.set_color(x);
});
// TODO: fix click collider when preserving aspect
let state = state_src.clone();
let _ = FuncRegistration::new("preserve_aspect")
.with_namespace(FnNamespace::Internal)
@ -221,8 +248,9 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
e.set_preserve_aspect(x);
});
// TODO: maybe remove?
let state = state_src.clone();
let _ = FuncRegistration::new("set_disable_events")
let _ = FuncRegistration::new("disable_events")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |name: ImmutableString, x: bool| {
let mut ui_state = state.borrow_mut();
@ -230,13 +258,13 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
Some(UiElement::SubElement { element, .. }) => match &mut **element {
UiElement::Sprite(x) => x,
_ => {
error!("called `sprite::set_disable_events` on an invalid name `{name}`");
error!("called `sprite::disable_events` on an invalid name `{name}`");
return;
}
},
Some(UiElement::Sprite(x)) => x,
_ => {
error!("called `sprite::set_disable_events` on an invalid name `{name}`");
error!("called `sprite::disable_events` on an invalid name `{name}`");
return;
}
};

View File

@ -23,8 +23,8 @@ pub fn build_textbox_module(
move |name: ImmutableString,
font_size: f32,
line_height: f32,
rect: Rect,
color: Color| {
color: Color,
rect: Rect| {
let mut ui_state = state.borrow_mut();
if ui_state.contains_name(&name) {
@ -72,6 +72,32 @@ pub fn build_textbox_module(
},
);
let state = state_src.clone();
let font = font_src.clone();
let _ = FuncRegistration::new("set_color")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |name: ImmutableString, color: Color| {
let mut ui_state = state.borrow_mut();
let e = match ui_state.get_mut_by_name(&name) {
Some(UiElement::SubElement { element, .. }) => match &mut **element {
UiElement::Text(x) => x,
_ => {
error!("called `textbox::set_color` on an invalid name `{name}`");
return;
}
},
Some(UiElement::Text(x)) => x,
_ => {
error!("called `textbox::set_color` on an invalid name `{name}`");
return;
}
};
e.set_color(&mut font.borrow_mut(), color);
});
let state = state_src.clone();
let font = font_src.clone();
let _ = FuncRegistration::new("align_left")

View File

@ -10,19 +10,11 @@ pub enum Anchor {
}
#[export_module]
#[allow(non_upper_case_globals)]
pub mod anchor_mod {
#[allow(non_upper_case_globals)]
pub const Center: Anchor = Anchor::Center;
#[allow(non_upper_case_globals)]
pub const NorthWest: Anchor = Anchor::NorthWest;
#[allow(non_upper_case_globals)]
pub const NorthEast: Anchor = Anchor::NorthEast;
#[allow(non_upper_case_globals)]
pub const SouthWest: Anchor = Anchor::SouthWest;
#[allow(non_upper_case_globals)]
pub const SouthEast: Anchor = Anchor::SouthEast;
}

View File

@ -17,7 +17,7 @@ pub struct Rect {
}
impl Rect {
pub fn new(x: f32, y: f32, w: f32, h: f32, anchor_self: Anchor, anchor_parent: Anchor) -> Self {
pub fn new(x: f32, y: f32, w: f32, h: f32, anchor_parent: Anchor, anchor_self: Anchor,) -> Self {
Self {
pos: Point2::new(x, y),
dim: Vector2::new(w, h),

View File

@ -6,6 +6,7 @@ mod state;
pub use directive::*;
pub use event::*;
pub use functions::OutfitState;
pub use helpers::{anchor::*, color::*, rect::*, vector::*};
pub use state::*;
@ -52,6 +53,7 @@ pub fn register_into_engine(
// Modules
engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into());
engine.register_static_module("ct", functions::build_ct_module(ct_src.clone()).into());
engine.register_static_module(
"sprite",
functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(),

View File

@ -1,15 +1,16 @@
use galactica_content::{Outfit, Ship, SystemObject};
use galactica_content::{Ship, SystemObject};
use galactica_system::{
data::{self},
phys::{objects::PhysShip, PhysSimShipHandle},
};
use galactica_util::to_degrees;
use log::error;
use nalgebra::Vector2;
use rapier2d::dynamics::RigidBody;
use rhai::{Array, CustomType, Dynamic, ImmutableString, TypeBuilder};
use std::sync::Arc;
use super::{Color, UiVector};
use super::{functions::OutfitState, Color, UiVector};
use crate::{RenderInput, RenderState};
#[derive(Debug, Clone)]
@ -89,18 +90,16 @@ impl CustomType for ShipState {
.with_fn("content_index", |s: &mut Self| {
s.get_content().display_name.clone()
})
.with_fn("thumbnail", |s: &mut Self| {
s.get_content().thumbnail.clone()
.with_fn("thumbnail", |s: &mut Self| -> ImmutableString {
s.get_content().thumbnail.index.clone().into()
})
.with_fn("landed_on", |s: &mut Self| s.landed_on())
.with_fn("get_shields", |s: &mut Self| {
.with_fn("current_shields", |s: &mut Self| {
s.get_ship().get_data().get_shields()
})
.with_fn("get_total_shields", |s: &mut Self| {
s.get_ship().get_data().get_outfits().get_total_shields()
})
.with_fn("get_total_hull", |s: &mut Self| s.get_content().hull)
.with_fn("get_hull", |s: &mut Self| {
.with_fn("total_hull", |s: &mut Self| s.get_content().hull)
.with_fn("empty_mass", |s: &mut Self| s.get_content().mass)
.with_fn("current_hull", |s: &mut Self| {
s.get_ship().get_data().get_hull()
})
.with_fn("get_size", |s: &mut Self| s.get_content().size)
@ -113,25 +112,37 @@ impl CustomType for ShipState {
let h = s.get_ship().get_data().get_faction();
let c = h.color;
Color::new(c[0], c[1], c[2], 1.0)
});
}
}
#[derive(Debug, Clone)]
pub struct OutfitState {
outfit: Arc<Outfit>,
}
impl OutfitState {}
impl CustomType for OutfitState {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("OutfitState")
.with_fn("display_name", |s: &mut Self| s.outfit.display_name.clone())
.with_fn("index", |s: &mut Self| s.outfit.index.to_string())
.with_fn("thumbnail", |s: &mut Self| {
s.outfit.thumbnail.index.to_string()
})
//
// Stat getters
//
.with_fn("stat_thrust", |s: &mut Self| {
s.get_ship()
.get_data()
.get_outfits()
.get_stats()
.engine_thrust
})
.with_fn("stat_steer_power", |s: &mut Self| {
s.get_ship()
.get_data()
.get_outfits()
.get_stats()
.steer_power
})
.with_fn("stat_shield_strength", |s: &mut Self| {
s.get_ship()
.get_data()
.get_outfits()
.get_stats()
.shield_strength
})
.with_fn("stat_max_shield_generation", |s: &mut Self| {
s.get_ship()
.get_data()
.get_outfits()
.get_stats()
.shield_generation
});
}
}
@ -153,7 +164,7 @@ impl SystemObjectState {
.unwrap()
.outfitter
{
a.push(Dynamic::from(OutfitState { outfit: o.clone() }));
a.push(Dynamic::from(OutfitState::new(o.clone())));
}
return a;
}
@ -234,6 +245,7 @@ impl CustomType for SystemObjectState {
pub struct State {
input: Arc<RenderInput>,
window_aspect: f32,
window_size: Vector2<u32>,
}
// TODO: remove this
@ -245,6 +257,7 @@ impl State {
Self {
input: input.clone(),
window_aspect: state.window_aspect,
window_size: Vector2::new(state.window_size.width, state.window_size.height),
}
}
@ -284,6 +297,12 @@ impl CustomType for State {
.with_fn("player_ship", Self::player_ship)
.with_fn("ships", Self::ships)
.with_fn("objects", Self::objects)
.with_fn("window_aspect", |s: &mut Self| s.window_aspect);
.with_fn("window_aspect", |s: &mut Self| s.window_aspect)
.with_fn("window_size", |s: &mut Self| {
UiVector::new(
s.window_size.x as f32 / s.input.ct.config.ui_scale,
s.window_size.y as f32 / s.input.ct.config.ui_scale,
)
});
}
}

View File

@ -50,6 +50,10 @@ impl UiSprite {
}
}
pub fn set_sprite(&mut self, sprite: Arc<Sprite>) {
self.anim = SpriteAutomaton::new(sprite);
}
pub fn set_mask(&mut self, mask: Option<Arc<Sprite>>) {
self.mask = mask;
}

View File

@ -64,6 +64,11 @@ impl UiTextBox {
self.reflow(font);
}
pub fn set_color(&mut self, font: &mut FontSystem, color: api::Color) {
self.color = color;
self.reflow(font);
}
pub fn set_align(&mut self, font: &mut FontSystem, align: Align) {
self.justify = align;
self.reflow(font);

View File

@ -1,6 +1,6 @@
use std::{collections::HashMap, sync::Arc};
use galactica_content::{ContentIndex, GunPoint, Outfit, OutfitSpace};
use galactica_content::{ContentIndex, GunPoint, Outfit, OutfitSpace, OutfitStats};
/// Possible outcomes when adding an outfit
pub enum OutfitAddResult {
@ -61,12 +61,17 @@ pub struct OutfitSet {
/// if value is Some, this point is taken.
gun_points: HashMap<GunPoint, Option<Arc<Outfit>>>,
/// Outfit values
/// This isn't strictly necessary, but we don't want to
/// re-compute this on each frame.
engine_thrust: f32,
steer_power: f32,
shield_strength: f32,
/// The combined stats of all outfits in this set.
/// There are two things to note here:
/// First, shield_delay is always zero. That is handled
/// seperately, since it is different for every outfit.
/// Second, shield_generation represents the MAXIMUM POSSIBLE
/// shield generation, after all delays have expired.
///
/// Note that this field isn't strictly necessary... we could compute stats
/// by iterating over the outfits in this set. We don't want to do this every
/// frame, though, so we keep track of the total sum here.
stats: OutfitStats,
/// All shield generators in this outfit set
// These can't be summed into one value, since each has a
@ -81,10 +86,7 @@ impl OutfitSet {
total_space: available_space,
used_space: OutfitSpace::new(),
gun_points: gun_points.iter().map(|x| (x.clone(), None)).collect(),
engine_thrust: 0.0,
steer_power: 0.0,
shield_strength: 0.0,
stats: OutfitStats::zero(),
shield_generators: Vec::new(),
}
}
@ -111,13 +113,12 @@ impl OutfitSet {
self.used_space += o.space;
self.engine_thrust += o.engine_thrust;
self.steer_power += o.steer_power;
self.shield_strength += o.shield_strength;
self.stats.add(&o.stats);
self.shield_generators.push(ShieldGenerator {
outfit: o.clone(),
delay: o.shield_delay,
generation: o.shield_generation,
delay: o.stats.shield_delay,
generation: o.stats.shield_generation,
});
if self.outfits.contains_key(&o.index) {
@ -143,9 +144,7 @@ impl OutfitSet {
self.used_space -= o.space;
self.engine_thrust -= o.engine_thrust;
self.steer_power -= o.steer_power;
self.shield_strength -= o.shield_strength;
self.stats.subtract(&o.stats);
{
// This index will exist, since we checked the hashmap
@ -183,11 +182,6 @@ impl OutfitSet {
self.shield_generators.iter()
}
/// Get maximum possible shield regen
pub fn get_max_shield_regen(&self) -> f32 {
self.shield_generators.iter().map(|x| x.generation).sum()
}
/// Get the outfit attached to the given gun point
/// Will panic if this gunpoint is not in this outfit set.
pub fn get_gun(&self, point: &GunPoint) -> Option<Arc<Outfit>> {
@ -204,18 +198,13 @@ impl OutfitSet {
&self.used_space
}
/// Total foward thrust
pub fn get_engine_thrust(&self) -> f32 {
self.engine_thrust
}
/// Total steer power
pub fn get_steer_power(&self) -> f32 {
self.steer_power
}
/// Total shield strength
pub fn get_total_shields(&self) -> f32 {
self.shield_strength
/// Get the combined stats of all outfits in this set.
/// There are two things to note here:
/// First, shield_delay is always zero. That is handled
/// seperately, since it is different for every outfit.
/// Second, shield_generation represents the MAXIMUM POSSIBLE
/// shield generation, after all delays have expired.
pub fn get_stats(&self) -> &OutfitStats {
&self.stats
}
}

View File

@ -178,7 +178,7 @@ impl ShipData {
/// Add an outfit to this ship
pub fn add_outfit(&mut self, o: &Arc<Outfit>) -> super::OutfitAddResult {
let r = self.outfits.add(o);
self.shields = self.outfits.get_total_shields();
self.shields = self.outfits.get_stats().shield_strength;
return r;
}
@ -245,8 +245,8 @@ impl ShipData {
}
// Regenerate shields
if self.shields != self.outfits.get_total_shields() {
self.shields = self.outfits.get_total_shields();
if self.shields != self.outfits.get_stats().shield_strength {
self.shields = self.outfits.get_stats().shield_strength;
}
}
@ -260,12 +260,12 @@ impl ShipData {
// Regenerate shields
let time_since = self.last_hit.elapsed().as_secs_f32();
if self.shields != self.outfits.get_total_shields() {
if self.shields != self.outfits.get_stats().shield_strength {
for g in self.outfits.iter_shield_generators() {
if time_since >= g.delay {
self.shields += g.generation * t;
if self.shields > self.outfits.get_total_shields() {
self.shields = self.outfits.get_total_shields();
if self.shields > self.outfits.get_stats().shield_strength {
self.shields = self.outfits.get_stats().shield_strength;
break;
}
}

View File

@ -390,21 +390,21 @@ impl PhysShip {
if self.controls.thrust {
rigid_body.apply_impulse(
vector![engine_force.x, engine_force.y]
* self.data.get_outfits().get_engine_thrust(),
* self.data.get_outfits().get_stats().engine_thrust,
true,
);
}
if self.controls.right {
rigid_body.apply_torque_impulse(
self.data.get_outfits().get_steer_power() * -100.0 * res.t,
self.data.get_outfits().get_stats().steer_power * -100.0 * res.t,
true,
);
}
if self.controls.left {
rigid_body.apply_torque_impulse(
self.data.get_outfits().get_steer_power() * 100.0 * res.t,
self.data.get_outfits().get_stats().steer_power * 100.0 * res.t,
true,
);
}