Modularize (#1)

* configure workspaces

* Modularize domain logic by creating a new `models` crate.

* Moved `LoadingPlugin` and `MenuPlugin` from `core` to a new `ui` module. Updated imports accordingly.

* add theme for instruments

* trunk serve works, remove audio and textures

* remove loading indicator and assets

* rename models to systems

* seperate systems and components from models

* Refactor instrument cluster to leverage reusable composition utilities.

---------

Co-authored-by: geoffsee <>
This commit is contained in:
Geoff Seemueller
2025-07-01 22:22:40 -04:00
committed by GitHub
parent 66b8a855b5
commit 456fd31684
86 changed files with 7936 additions and 1523 deletions

View File

@@ -0,0 +1,17 @@
[package]
name = "components"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
bevy = { workspace = true, features = [
"bevy_asset",
"bevy_color",
"bevy_core_pipeline",
"bevy_render",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"bevy_window",
] }

View File

@@ -0,0 +1,228 @@
use bevy::prelude::*;
use super::instruments::*;
use super::theme::*;
use super::composition::*;
#[derive(Component)]
pub struct InstrumentCluster;
#[derive(Component)]
pub struct GpsIndicator;
#[derive(Component)]
pub struct RadarIndicator;
#[derive(Component)]
pub struct AisIndicator;
#[derive(Component)]
pub struct SystemDisplay;
#[derive(Component)]
pub struct WindDisplay;
/// Sets up the main instrument cluster UI using composable components
pub fn setup_instrument_cluster(mut commands: Commands) {
// Spawn camera since we're bypassing the menu system
commands.spawn((Camera2d, Msaa::Off));
// Create main container using composition
commands.spawn((
main_container_node(),
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
InstrumentCluster,
))
.with_children(|parent| {
// Top row - Main navigation and speed (60% height)
parent.spawn(row_container_node(60.0, 20.0))
.with_children(|row| {
// Speed Gauge
row.spawn((
circular_gauge_node(),
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
BorderColor(BORDER_COLOR_PRIMARY),
SpeedGauge,
))
.with_children(|gauge| {
gauge.spawn(create_text("SPEED", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
gauge.spawn(create_text("12.5", FONT_SIZE_LARGE, TEXT_COLOR_SUCCESS));
gauge.spawn(create_text("KTS", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// Central Navigation Display
row.spawn((
navigation_display_node(),
BackgroundColor(BACKGROUND_COLOR_ACCENT),
BorderColor(BORDER_COLOR_PRIMARY),
NavigationDisplay,
))
.with_children(|nav| {
nav.spawn(create_text("NAVIGATION", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
nav.spawn((
create_text("045°", FONT_SIZE_LARGE, TEXT_COLOR_PRIMARY).0,
create_text("045°", FONT_SIZE_LARGE, TEXT_COLOR_PRIMARY).1,
create_text("045°", FONT_SIZE_LARGE, TEXT_COLOR_PRIMARY).2,
CompassGauge,
));
nav.spawn(create_text("HEADING", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
});
// Depth Gauge
row.spawn((
circular_gauge_node(),
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
BorderColor(BORDER_COLOR_PRIMARY),
DepthGauge,
))
.with_children(|gauge| {
gauge.spawn(create_text("DEPTH", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
gauge.spawn(create_text("15.2", FONT_SIZE_LARGE, Color::linear_rgb(0.0, 1.0, 0.8)));
gauge.spawn(create_text("M", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
});
// Bottom row - Engine and system status (40% height)
parent.spawn(row_container_node(40.0, 20.0))
.with_children(|row| {
// Engine Status Panel
row.spawn((
status_panel_node(200.0, 150.0),
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
BorderColor(BORDER_COLOR_PRIMARY),
EngineStatus,
))
.with_children(|panel| {
panel.spawn(create_text("ENGINE", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
panel.spawn(create_text("82°C", FONT_SIZE_LARGE, TEXT_COLOR_SUCCESS));
panel.spawn(create_text("TEMP NORMAL", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// System Status Grid
row.spawn((
status_panel_node(250.0, 150.0),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
BorderColor(BORDER_COLOR_SECONDARY),
))
.with_children(|grid| {
grid.spawn(create_text("SYSTEMS", 12.0, TEXT_COLOR_SECONDARY));
// Fuel Level Bar
grid.spawn(progress_bar_node())
.with_children(|bar| {
bar.spawn(create_text("FUEL", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
bar.spawn((
progress_bar_background_node(),
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
BorderColor(TEXT_COLOR_PRIMARY),
))
.with_children(|bar_bg| {
bar_bg.spawn((
progress_bar_fill_node(75.0),
BackgroundColor(TEXT_COLOR_SUCCESS),
));
});
bar.spawn(create_text("75%", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// Battery Level Bar
grid.spawn(progress_bar_node())
.with_children(|bar| {
bar.spawn(create_text("BATTERY", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
bar.spawn((
progress_bar_background_node(),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
BorderColor(BORDER_COLOR_SECONDARY),
))
.with_children(|bar_bg| {
bar_bg.spawn((
progress_bar_fill_node(88.0),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
));
});
bar.spawn(create_text("88%", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// System Status Indicators
grid.spawn(Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
..default()
})
.with_children(|indicators| {
// GPS Indicator
indicators.spawn((
Button,
system_indicator_node(),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
BorderColor(BORDER_COLOR_SECONDARY),
GpsIndicator,
))
.with_children(|indicator| {
indicator.spawn(create_text("🛰️", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
indicator.spawn(create_text("GPS", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// RADAR Indicator
indicators.spawn((
Button,
system_indicator_node(),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
BorderColor(BORDER_COLOR_SECONDARY),
RadarIndicator,
))
.with_children(|indicator| {
indicator.spawn(create_text("📡", FONT_SIZE_NORMAL, Color::linear_rgb(0.0, 1.0, 0.0)));
indicator.spawn(create_text("RADAR", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
// AIS Indicator
indicators.spawn((
Button,
system_indicator_node(),
BackgroundColor(BACKGROUND_COLOR_SECONDARY),
BorderColor(BORDER_COLOR_SECONDARY),
AisIndicator,
))
.with_children(|indicator| {
indicator.spawn(create_text("🚢", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
indicator.spawn(create_text("AIS", FONT_SIZE_SMALL, TEXT_COLOR_PRIMARY));
});
});
});
// Wind Information
row.spawn((
status_panel_node(200.0, 150.0),
BackgroundColor(BACKGROUND_COLOR_ACCENT),
BorderColor(BORDER_COLOR_PRIMARY),
WindDisplay,
))
.with_children(|panel| {
panel.spawn(create_text("WIND", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
panel.spawn(create_text("8.3 KTS", FONT_SIZE_NORMAL, TEXT_COLOR_SUCCESS));
panel.spawn(create_text("120° REL", FONT_SIZE_NORMAL, TEXT_COLOR_PRIMARY));
});
});
// System Display Area
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Px(200.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.0)),
..default()
},
BackgroundColor(BACKGROUND_COLOR_PRIMARY),
BorderColor(BORDER_COLOR_PRIMARY),
SystemDisplay,
))
.with_children(|display| {
display.spawn(create_text("Select a system above to view details", FONT_SIZE_SMALL, TEXT_COLOR_SECONDARY));
});
});
}

View File

@@ -0,0 +1,123 @@
use bevy::prelude::*;
/// Composition utilities for building instrument cluster components
/// This module provides reusable building blocks for creating UI components
/// Creates a circular gauge node bundle
pub fn circular_gauge_node() -> Node {
Node {
width: Val::Px(180.0),
height: Val::Px(180.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
}
}
/// Creates a status panel node bundle
pub fn status_panel_node(width: f32, height: f32) -> Node {
Node {
width: Val::Px(width),
height: Val::Px(height),
border: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(10.0)),
..default()
}
}
/// Creates a progress bar container node
pub fn progress_bar_node() -> Node {
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
..default()
}
}
/// Creates a progress bar background node
pub fn progress_bar_background_node() -> Node {
Node {
width: Val::Px(80.0),
height: Val::Px(8.0),
border: UiRect::all(Val::Px(1.0)),
..default()
}
}
/// Creates a progress bar fill node
pub fn progress_bar_fill_node(percentage: f32) -> Node {
Node {
width: Val::Percent(percentage),
height: Val::Percent(100.0),
..default()
}
}
/// Creates a system indicator button node
pub fn system_indicator_node() -> Node {
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
padding: UiRect::all(Val::Px(8.0)),
width: Val::Px(60.0),
height: Val::Px(40.0),
border: UiRect::all(Val::Px(1.0)),
..default()
}
}
/// Creates a navigation display node
pub fn navigation_display_node() -> Node {
Node {
width: Val::Px(300.0),
height: Val::Px(300.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
}
}
/// Creates a row container node
pub fn row_container_node(height_percent: f32, padding: f32) -> Node {
Node {
width: Val::Percent(100.0),
height: Val::Percent(height_percent),
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(padding)),
..default()
}
}
/// Creates the main container node
pub fn main_container_node() -> Node {
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
..default()
}
}
/// Creates a text bundle with specified content, font size, and color
pub fn create_text(content: &str, font_size: f32, color: Color) -> (Text, TextFont, TextColor) {
(
Text::new(content),
TextFont {
font_size,
..default()
},
TextColor(color),
)
}

View File

@@ -0,0 +1,106 @@
use bevy::prelude::*;
/// Individual instrument components
#[derive(Component)]
pub struct SpeedGauge;
#[derive(Component)]
pub struct DepthGauge;
#[derive(Component)]
pub struct CompassGauge;
#[derive(Component)]
pub struct EngineStatus;
#[derive(Component)]
pub struct NavigationDisplay;
/// Yacht data resource containing all sensor readings
#[derive(Resource)]
pub struct YachtData {
pub speed: f32, // knots
pub depth: f32, // meters
pub heading: f32, // degrees
pub engine_temp: f32, // celsius
pub fuel_level: f32, // percentage
pub battery_level: f32, // percentage
pub wind_speed: f32, // knots
pub wind_direction: f32, // degrees
}
impl Default for YachtData {
fn default() -> Self {
Self {
speed: 12.5,
depth: 15.2,
heading: 045.0,
engine_temp: 82.0,
fuel_level: 75.0,
battery_level: 88.0,
wind_speed: 8.3,
wind_direction: 120.0,
}
}
}
/// Updates yacht data with simulated sensor readings
pub fn update_yacht_data(mut yacht_data: ResMut<YachtData>, time: Res<Time>) {
let t = time.elapsed_secs();
// Simulate realistic yacht data with some variation
yacht_data.speed = 12.5 + (t * 0.3).sin() * 2.0;
yacht_data.depth = 15.2 + (t * 0.1).sin() * 3.0;
yacht_data.heading = (yacht_data.heading + time.delta_secs() * 5.0) % 360.0;
yacht_data.engine_temp = 82.0 + (t * 0.2).sin() * 3.0;
yacht_data.wind_speed = 8.3 + (t * 0.4).sin() * 1.5;
yacht_data.wind_direction = (yacht_data.wind_direction + time.delta_secs() * 10.0) % 360.0;
// Slowly drain fuel and battery (very slowly for demo purposes)
yacht_data.fuel_level = (yacht_data.fuel_level - time.delta_secs() * 0.01).max(0.0);
yacht_data.battery_level = (yacht_data.battery_level - time.delta_secs() * 0.005).max(0.0);
}
/// Updates the display values for all instrument gauges
pub fn update_instrument_displays(
yacht_data: Res<YachtData>,
mut speed_query: Query<&mut Text, (With<SpeedGauge>, Without<DepthGauge>, Without<CompassGauge>)>,
mut depth_query: Query<&mut Text, (With<DepthGauge>, Without<SpeedGauge>, Without<CompassGauge>)>,
mut compass_query: Query<&mut Text, (With<CompassGauge>, Without<SpeedGauge>, Without<DepthGauge>)>,
) {
// Update speed display
for mut text in speed_query.iter_mut() {
if text.0.contains('.') {
text.0 = format!("{:.1}", yacht_data.speed);
}
}
// Update depth display
for mut text in depth_query.iter_mut() {
if text.0.contains('.') {
text.0 = format!("{:.1}", yacht_data.depth);
}
}
// Update compass display
for mut text in compass_query.iter_mut() {
if text.0.contains('°') {
text.0 = format!("{:03.0}°", yacht_data.heading);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yacht_data_default() {
let yacht_data = YachtData::default();
assert_eq!(yacht_data.speed, 12.5);
assert_eq!(yacht_data.depth, 15.2);
assert_eq!(yacht_data.heading, 45.0);
assert_eq!(yacht_data.fuel_level, 75.0);
assert_eq!(yacht_data.battery_level, 88.0);
}
}

View File

@@ -0,0 +1,16 @@
#![allow(clippy::type_complexity)]
// Components crate for yacht pit application
// This crate contains reusable UI and game components
pub mod ui;
pub mod instruments;
pub mod theme;
pub mod cluster;
pub mod composition;
pub use ui::*;
pub use instruments::*;
pub use theme::*;
pub use cluster::*;
pub use composition::*;

View File

@@ -0,0 +1,34 @@
use bevy::prelude::*;
use bevy::color::Color;
pub const BACKGROUND_COLOR_PRIMARY: Color = Color::linear_rgb(0.05, 0.05, 0.1);
pub const BACKGROUND_COLOR_SECONDARY: Color = Color::linear_rgb(0.1, 0.1, 0.15);
pub const BACKGROUND_COLOR_ACCENT: Color = Color::linear_rgb(0.1, 0.15, 0.2);
pub const BORDER_COLOR_PRIMARY: Color = Color::linear_rgb(0.0, 0.8, 1.0);
pub const BORDER_COLOR_SECONDARY: Color = Color::linear_rgb(0.8, 0.4, 0.0);
pub const BORDER_COLOR_TERTIARY: Color = Color::linear_rgb(0.4, 0.4, 0.6);
pub const TEXT_COLOR_PRIMARY: Color = Color::linear_rgb(0.0, 0.8, 1.0);
pub const TEXT_COLOR_SECONDARY: Color = Color::linear_rgb(0.6, 0.6, 0.6);
pub const TEXT_COLOR_SUCCESS: Color = Color::linear_rgb(0.0, 1.0, 0.0);
pub const TEXT_COLOR_WARNING: Color = Color::linear_rgb(0.8, 0.4, 0.0);
pub const TEXT_COLOR_DANGER: Color = Color::linear_rgb(0.8, 0.0, 0.0);
pub const FONT_SIZE_SMALL: f32 = 10.0;
pub const FONT_SIZE_NORMAL: f32 = 14.0;
pub const FONT_SIZE_LARGE: f32 = 32.0;
pub const PADDING_DEFAULT: f32 = 20.0;
pub const BORDER_WIDTH_DEFAULT: f32 = 2.0;
pub fn create_node_style(width: Val, height: Val, direction: FlexDirection) -> Node {
Node {
width,
height,
flex_direction: direction,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
}
}

View File

@@ -0,0 +1,14 @@
// UI components for the yacht pit application
use bevy::prelude::*;
// Placeholder for UI components
// This module will contain reusable UI components for the yacht pit application
pub struct ComponentsPlugin;
impl Plugin for ComponentsPlugin {
fn build(&self, _app: &mut App) {
// Add systems and resources for components here
}
}