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 <>
38
.gitignore
vendored
@@ -1,5 +1,35 @@
|
|||||||
target/
|
# dependencies (bun install)
|
||||||
.idea/
|
node_modules
|
||||||
.DS_Store
|
target
|
||||||
|
**/dist/
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
dist/
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
1262
Cargo.lock
generated
90
Cargo.toml
@@ -1,87 +1,11 @@
|
|||||||
[package]
|
|
||||||
name = "yachtpit"
|
|
||||||
version = "0.1.0"
|
|
||||||
publish = false
|
|
||||||
authors = ["seemueller-io <git@github.geoffsee>"]
|
|
||||||
edition = "2021"
|
|
||||||
exclude = ["dist", "build", "assets", "credits"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["mobile"]
|
members = ["crates/yachtpit", "crates/yachtpit/mobile", "crates/systems", "crates/components"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
default-members = [
|
||||||
opt-level = 3
|
"crates/yachtpit"
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
opt-level = 1
|
|
||||||
|
|
||||||
# This is used by trunk as it doesn't support custom profiles: https://github.com/trunk-rs/trunk/issues/605
|
|
||||||
# xbuild also uses this profile for building android AABs because I couldn't find a configuration for it
|
|
||||||
[profile.release]
|
|
||||||
opt-level = "z"
|
|
||||||
lto = 'thin'
|
|
||||||
codegen-units = 1
|
|
||||||
strip = true
|
|
||||||
|
|
||||||
# Profile for distribution
|
|
||||||
[profile.dist]
|
|
||||||
inherits = "release"
|
|
||||||
opt-level = 3
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
strip = true
|
|
||||||
|
|
||||||
[features]
|
|
||||||
dev = [
|
|
||||||
"bevy/dynamic_linking",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# All of Bevy's default features exept for the audio related ones (bevy_audio, vorbis), since they clash with bevy_kira_audio
|
[workspace.dependencies]
|
||||||
# and android_shared_stdcxx/android-game-activity, since those are covered in `mobile`
|
bevy = { version = "0.16", default-features = false }
|
||||||
[dependencies]
|
wasm-bindgen = "=0.2.100"
|
||||||
bevy = { version = "0.16", default-features = false, features = [
|
|
||||||
"animation",
|
|
||||||
"bevy_asset",
|
|
||||||
"bevy_color",
|
|
||||||
"bevy_core_pipeline",
|
|
||||||
"bevy_gilrs",
|
|
||||||
"bevy_gizmos",
|
|
||||||
"bevy_gltf",
|
|
||||||
"bevy_log",
|
|
||||||
"bevy_mesh_picking_backend",
|
|
||||||
"bevy_pbr",
|
|
||||||
"bevy_picking",
|
|
||||||
"bevy_render",
|
|
||||||
"bevy_scene",
|
|
||||||
"bevy_sprite",
|
|
||||||
"bevy_sprite_picking_backend",
|
|
||||||
"bevy_state",
|
|
||||||
"bevy_text",
|
|
||||||
"bevy_ui",
|
|
||||||
"bevy_ui_picking_backend",
|
|
||||||
"bevy_window",
|
|
||||||
"bevy_winit",
|
|
||||||
"custom_cursor",
|
|
||||||
"default_font",
|
|
||||||
"hdr",
|
|
||||||
"multi_threaded",
|
|
||||||
"png",
|
|
||||||
"smaa_luts",
|
|
||||||
"sysinfo_plugin",
|
|
||||||
"tonemapping_luts",
|
|
||||||
"webgl2",
|
|
||||||
"x11",
|
|
||||||
] }
|
|
||||||
bevy_kira_audio = { version = "0.23.0", features = ["android_shared_stdcxx"] }
|
|
||||||
bevy_asset_loader = { version = "0.23.0" }
|
|
||||||
rand = { version = "0.8.3" }
|
|
||||||
webbrowser = { version = "1", features = ["hardened"] }
|
|
||||||
|
|
||||||
# keep the following in sync with Bevy's dependencies
|
|
||||||
winit = { version = "0.30", default-features = false }
|
|
||||||
image = { version = "0.25", default-features = false }
|
|
||||||
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
|
||||||
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
embed-resource = "1"
|
|
||||||
|
25
bun.lock
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "yachtpit",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
}
|
||||||
|
}
|
17
crates/components/Cargo.toml
Normal 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",
|
||||||
|
] }
|
228
crates/components/src/cluster.rs
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
123
crates/components/src/composition.rs
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
106
crates/components/src/instruments.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
16
crates/components/src/lib.rs
Normal 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::*;
|
34
crates/components/src/theme.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
14
crates/components/src/ui.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
19
crates/systems/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "systems"
|
||||||
|
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",
|
||||||
|
] }
|
||||||
|
rand = { version = "0.8.3" }
|
||||||
|
components = { path = "../components" }
|
15
crates/systems/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
|
pub mod player;
|
||||||
|
pub mod systems;
|
||||||
|
pub mod yacht_systems;
|
||||||
|
|
||||||
|
// Re-export components from the components crate
|
||||||
|
pub use components::{
|
||||||
|
setup_instrument_cluster, update_instrument_displays, update_yacht_data, YachtData,
|
||||||
|
SpeedGauge, DepthGauge, CompassGauge, EngineStatus, NavigationDisplay,
|
||||||
|
InstrumentCluster, GpsIndicator, RadarIndicator, AisIndicator, SystemDisplay
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use player::{get_yacht_systems, setup_instrument_cluster_system, PlayerPlugin};
|
||||||
|
pub use yacht_systems::{create_yacht_systems, AisSystem, GpsSystem, RadarSystem, SystemInteraction, SystemStatus, YachtSystem};
|
27
crates/systems/src/player.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use components::{setup_instrument_cluster, YachtData, update_yacht_data, update_instrument_displays};
|
||||||
|
use super::yacht_systems::{create_yacht_systems, YachtSystem};
|
||||||
|
|
||||||
|
pub struct PlayerPlugin;
|
||||||
|
|
||||||
|
/// This plugin handles the futuristic yacht instrument cluster
|
||||||
|
/// The main app should handle state management and system registration
|
||||||
|
impl Plugin for PlayerPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<YachtData>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(update_yacht_data, update_instrument_displays)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup function for instrument cluster - to be called by the main app
|
||||||
|
pub fn setup_instrument_cluster_system() -> impl Fn(Commands) {
|
||||||
|
setup_instrument_cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize yacht systems - returns the systems for registration
|
||||||
|
pub fn get_yacht_systems() -> Vec<Box<dyn YachtSystem>> {
|
||||||
|
create_yacht_systems()
|
||||||
|
}
|
149
crates/systems/src/systems.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use components::{YachtData, GpsIndicator, RadarIndicator, AisIndicator, SystemDisplay};
|
||||||
|
|
||||||
|
/// Resource to track which system is currently selected
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct SelectedSystem {
|
||||||
|
pub current: Option<SystemType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Types of navigation systems available
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum SystemType {
|
||||||
|
Gps,
|
||||||
|
Radar,
|
||||||
|
Ais,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles user interactions with system indicator buttons
|
||||||
|
pub fn handle_system_interactions(
|
||||||
|
mut selected_system: ResMut<SelectedSystem>,
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(&Interaction, &mut BackgroundColor, Option<&GpsIndicator>, Option<&RadarIndicator>, Option<&AisIndicator>),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, mut background_color, gps, radar, ais) in &mut interaction_query {
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Pressed => {
|
||||||
|
if gps.is_some() {
|
||||||
|
selected_system.current = Some(SystemType::Gps);
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
||||||
|
} else if radar.is_some() {
|
||||||
|
selected_system.current = Some(SystemType::Radar);
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
||||||
|
} else if ais.is_some() {
|
||||||
|
selected_system.current = Some(SystemType::Ais);
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interaction::Hovered => {
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.15, 0.15, 0.2));
|
||||||
|
}
|
||||||
|
Interaction::None => {
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the system display area with detailed information about the selected system
|
||||||
|
pub fn update_system_display(
|
||||||
|
selected_system: Res<SelectedSystem>,
|
||||||
|
mut display_query: Query<&mut Text, With<SystemDisplay>>,
|
||||||
|
yacht_data: Res<YachtData>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
if let Ok(mut text) = display_query.single_mut() {
|
||||||
|
match selected_system.current {
|
||||||
|
Some(SystemType::Gps) => {
|
||||||
|
text.0 = format!(
|
||||||
|
"GPS NAVIGATION SYSTEM\n\n\
|
||||||
|
Position: 43°38'19.5\"N 1°26'58.3\"W\n\
|
||||||
|
Heading: {:.0}°\n\
|
||||||
|
Speed: {:.1} knots\n\
|
||||||
|
Course Over Ground: {:.0}°\n\
|
||||||
|
Satellites: 12 connected\n\
|
||||||
|
HDOP: 0.8 (Excellent)\n\
|
||||||
|
\n\
|
||||||
|
Next Waypoint: MONACO HARBOR\n\
|
||||||
|
Distance: 127.3 NM\n\
|
||||||
|
ETA: 10h 12m",
|
||||||
|
yacht_data.heading,
|
||||||
|
yacht_data.speed,
|
||||||
|
yacht_data.heading + 5.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(SystemType::Radar) => {
|
||||||
|
let sweep_angle = (time.elapsed_secs() * 60.0) % 360.0;
|
||||||
|
text.0 = format!(
|
||||||
|
"RADAR SYSTEM - 12 NM RANGE\n\n\
|
||||||
|
Status: ACTIVE\n\
|
||||||
|
Sweep: {:.0}°\n\
|
||||||
|
Gain: AUTO\n\
|
||||||
|
Sea Clutter: -15 dB\n\
|
||||||
|
Rain Clutter: OFF\n\
|
||||||
|
\n\
|
||||||
|
CONTACTS DETECTED:\n\
|
||||||
|
• Vessel 1: 2.3 NM @ 045° (15 kts)\n\
|
||||||
|
• Vessel 2: 5.7 NM @ 180° (8 kts)\n\
|
||||||
|
• Land Mass: 8.2 NM @ 270°\n\
|
||||||
|
• Buoy: 1.1 NM @ 315°",
|
||||||
|
sweep_angle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(SystemType::Ais) => {
|
||||||
|
text.0 = format!(
|
||||||
|
"AIS - AUTOMATIC IDENTIFICATION SYSTEM\n\n\
|
||||||
|
Status: RECEIVING\n\
|
||||||
|
Own Ship MMSI: 123456789\n\
|
||||||
|
\n\
|
||||||
|
NEARBY VESSELS:\n\
|
||||||
|
\n\
|
||||||
|
🛥️ M/Y SERENITY\n\
|
||||||
|
MMSI: 987654321\n\
|
||||||
|
Distance: 2.1 NM @ 045°\n\
|
||||||
|
Speed: 12.5 kts\n\
|
||||||
|
Course: 180°\n\
|
||||||
|
\n\
|
||||||
|
🚢 CARGO VESSEL ATLANTIS\n\
|
||||||
|
MMSI: 456789123\n\
|
||||||
|
Distance: 5.8 NM @ 270°\n\
|
||||||
|
Speed: 18.2 kts\n\
|
||||||
|
Course: 090°\n\
|
||||||
|
\n\
|
||||||
|
⛵ S/Y WIND DANCER\n\
|
||||||
|
MMSI: 789123456\n\
|
||||||
|
Distance: 1.3 NM @ 135°\n\
|
||||||
|
Speed: 6.8 kts\n\
|
||||||
|
Course: 225°"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
text.0 = "Select a system above to view details".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_type_enum() {
|
||||||
|
let gps = SystemType::Gps;
|
||||||
|
let radar = SystemType::Radar;
|
||||||
|
let ais = SystemType::Ais;
|
||||||
|
|
||||||
|
assert_ne!(gps, radar);
|
||||||
|
assert_ne!(radar, ais);
|
||||||
|
assert_ne!(ais, gps);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_selected_system_default() {
|
||||||
|
let selected_system = SelectedSystem::default();
|
||||||
|
assert_eq!(selected_system.current, None);
|
||||||
|
}
|
||||||
|
}
|
406
crates/systems/src/yacht_systems.rs
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
//! Concrete implementations of yacht systems using the SystemManager abstraction
|
||||||
|
//!
|
||||||
|
//! This module provides implementations of the YachtSystem trait for GPS, Radar, and AIS systems,
|
||||||
|
//! bridging the existing functionality with the new higher-level abstraction.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use components::YachtData;
|
||||||
|
|
||||||
|
/// Status of a yacht system
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum SystemStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Error(String),
|
||||||
|
Maintenance,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interaction types for yacht systems
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SystemInteraction {
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
|
Reset,
|
||||||
|
Configure(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common trait for all yacht systems
|
||||||
|
pub trait YachtSystem: Send + Sync {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
fn display_name(&self) -> &'static str;
|
||||||
|
fn update(&mut self, yacht_data: &YachtData, time: &Time);
|
||||||
|
fn render_display(&self, yacht_data: &YachtData) -> String;
|
||||||
|
fn handle_interaction(&mut self, interaction: SystemInteraction) -> bool;
|
||||||
|
fn status(&self) -> SystemStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPS Navigation System implementation
|
||||||
|
pub struct GpsSystem {
|
||||||
|
status: SystemStatus,
|
||||||
|
satellites_connected: u8,
|
||||||
|
hdop: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpsSystem {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
status: SystemStatus::Active,
|
||||||
|
satellites_connected: 12,
|
||||||
|
hdop: 0.8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YachtSystem for GpsSystem {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"gps"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> &'static str {
|
||||||
|
"GPS Navigation"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _yacht_data: &YachtData, time: &Time) {
|
||||||
|
// Simulate satellite connection variations
|
||||||
|
let t = time.elapsed_secs();
|
||||||
|
self.satellites_connected = (12.0 + (t * 0.1).sin() * 2.0).max(8.0) as u8;
|
||||||
|
self.hdop = 0.8 + (t * 0.05).sin() * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_display(&self, yacht_data: &YachtData) -> String {
|
||||||
|
format!(
|
||||||
|
"GPS NAVIGATION SYSTEM\n\n\
|
||||||
|
Position: 43°38'19.5\"N 1°26'58.3\"W\n\
|
||||||
|
Heading: {:.0}°\n\
|
||||||
|
Speed: {:.1} knots\n\
|
||||||
|
Course Over Ground: {:.0}°\n\
|
||||||
|
Satellites: {} connected\n\
|
||||||
|
HDOP: {:.1} ({})\n\
|
||||||
|
\n\
|
||||||
|
Next Waypoint: MONACO HARBOR\n\
|
||||||
|
Distance: 127.3 NM\n\
|
||||||
|
ETA: 10h 12m",
|
||||||
|
yacht_data.heading,
|
||||||
|
yacht_data.speed,
|
||||||
|
yacht_data.heading + 5.0,
|
||||||
|
self.satellites_connected,
|
||||||
|
self.hdop,
|
||||||
|
if self.hdop < 1.0 { "Excellent" } else if self.hdop < 2.0 { "Good" } else { "Fair" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_interaction(&mut self, interaction: SystemInteraction) -> bool {
|
||||||
|
match interaction {
|
||||||
|
SystemInteraction::Select => {
|
||||||
|
self.status = SystemStatus::Active;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Reset => {
|
||||||
|
self.satellites_connected = 12;
|
||||||
|
self.hdop = 0.8;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Toggle => {
|
||||||
|
self.status = match self.status {
|
||||||
|
SystemStatus::Active => SystemStatus::Inactive,
|
||||||
|
SystemStatus::Inactive => SystemStatus::Active,
|
||||||
|
_ => SystemStatus::Active,
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> SystemStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Radar System implementation
|
||||||
|
pub struct RadarSystem {
|
||||||
|
status: SystemStatus,
|
||||||
|
range_nm: f32,
|
||||||
|
gain: String,
|
||||||
|
sea_clutter_db: i8,
|
||||||
|
rain_clutter: bool,
|
||||||
|
sweep_angle: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadarSystem {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
status: SystemStatus::Active,
|
||||||
|
range_nm: 12.0,
|
||||||
|
gain: "AUTO".to_string(),
|
||||||
|
sea_clutter_db: -15,
|
||||||
|
rain_clutter: false,
|
||||||
|
sweep_angle: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YachtSystem for RadarSystem {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"radar"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> &'static str {
|
||||||
|
"Radar System"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _yacht_data: &YachtData, time: &Time) {
|
||||||
|
// Update radar sweep angle
|
||||||
|
self.sweep_angle = (time.elapsed_secs() * 60.0) % 360.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_display(&self, _yacht_data: &YachtData) -> String {
|
||||||
|
format!(
|
||||||
|
"RADAR SYSTEM - {:.0} NM RANGE\n\n\
|
||||||
|
Status: {}\n\
|
||||||
|
Sweep: {:.0}°\n\
|
||||||
|
Gain: {}\n\
|
||||||
|
Sea Clutter: {} dB\n\
|
||||||
|
Rain Clutter: {}\n\
|
||||||
|
\n\
|
||||||
|
CONTACTS DETECTED:\n\
|
||||||
|
• Vessel 1: 2.3 NM @ 045° (15 kts)\n\
|
||||||
|
• Vessel 2: 5.7 NM @ 180° (8 kts)\n\
|
||||||
|
• Land Mass: 8.2 NM @ 270°\n\
|
||||||
|
• Buoy: 1.1 NM @ 315°",
|
||||||
|
self.range_nm,
|
||||||
|
match self.status {
|
||||||
|
SystemStatus::Active => "ACTIVE",
|
||||||
|
SystemStatus::Inactive => "STANDBY",
|
||||||
|
SystemStatus::Error(_) => "ERROR",
|
||||||
|
SystemStatus::Maintenance => "MAINTENANCE",
|
||||||
|
},
|
||||||
|
self.sweep_angle,
|
||||||
|
self.gain,
|
||||||
|
self.sea_clutter_db,
|
||||||
|
if self.rain_clutter { "ON" } else { "OFF" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_interaction(&mut self, interaction: SystemInteraction) -> bool {
|
||||||
|
match interaction {
|
||||||
|
SystemInteraction::Select => {
|
||||||
|
self.status = SystemStatus::Active;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Configure(key, value) => {
|
||||||
|
match key.as_str() {
|
||||||
|
"range" => {
|
||||||
|
if let Ok(range) = value.parse::<f32>() {
|
||||||
|
self.range_nm = range.clamp(1.0, 48.0);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"gain" => {
|
||||||
|
self.gain = value;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
"sea_clutter" => {
|
||||||
|
if let Ok(db) = value.parse::<i8>() {
|
||||||
|
self.sea_clutter_db = db.clamp(-30, 0);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"rain_clutter" => {
|
||||||
|
self.rain_clutter = value.to_lowercase() == "on" || value == "true";
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemInteraction::Reset => {
|
||||||
|
self.range_nm = 12.0;
|
||||||
|
self.gain = "AUTO".to_string();
|
||||||
|
self.sea_clutter_db = -15;
|
||||||
|
self.rain_clutter = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Toggle => {
|
||||||
|
self.status = match self.status {
|
||||||
|
SystemStatus::Active => SystemStatus::Inactive,
|
||||||
|
SystemStatus::Inactive => SystemStatus::Active,
|
||||||
|
_ => SystemStatus::Active,
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> SystemStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AIS (Automatic Identification System) implementation
|
||||||
|
pub struct AisSystem {
|
||||||
|
status: SystemStatus,
|
||||||
|
own_mmsi: u32,
|
||||||
|
receiving: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AisSystem {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
status: SystemStatus::Active,
|
||||||
|
own_mmsi: 123456789,
|
||||||
|
receiving: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YachtSystem for AisSystem {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"ais"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> &'static str {
|
||||||
|
"AIS System"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _yacht_data: &YachtData, _time: &Time) {
|
||||||
|
// AIS system is relatively static, but we could simulate
|
||||||
|
// vessel movements or signal strength variations here
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_display(&self, _yacht_data: &YachtData) -> String {
|
||||||
|
format!(
|
||||||
|
"AIS - AUTOMATIC IDENTIFICATION SYSTEM\n\n\
|
||||||
|
Status: {}\n\
|
||||||
|
Own Ship MMSI: {}\n\
|
||||||
|
\n\
|
||||||
|
NEARBY VESSELS:\n\
|
||||||
|
\n\
|
||||||
|
🛥️ M/Y SERENITY\n\
|
||||||
|
MMSI: 987654321\n\
|
||||||
|
Distance: 2.1 NM @ 045°\n\
|
||||||
|
Speed: 12.5 kts\n\
|
||||||
|
Course: 180°\n\
|
||||||
|
\n\
|
||||||
|
🚢 CARGO VESSEL ATLANTIS\n\
|
||||||
|
MMSI: 456789123\n\
|
||||||
|
Distance: 5.8 NM @ 270°\n\
|
||||||
|
Speed: 18.2 kts\n\
|
||||||
|
Course: 090°\n\
|
||||||
|
\n\
|
||||||
|
⛵ S/Y WIND DANCER\n\
|
||||||
|
MMSI: 789123456\n\
|
||||||
|
Distance: 1.3 NM @ 135°\n\
|
||||||
|
Speed: 6.8 kts\n\
|
||||||
|
Course: 225°",
|
||||||
|
if self.receiving { "RECEIVING" } else { "STANDBY" },
|
||||||
|
self.own_mmsi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_interaction(&mut self, interaction: SystemInteraction) -> bool {
|
||||||
|
match interaction {
|
||||||
|
SystemInteraction::Select => {
|
||||||
|
self.status = SystemStatus::Active;
|
||||||
|
self.receiving = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Configure(key, value) => {
|
||||||
|
match key.as_str() {
|
||||||
|
"mmsi" => {
|
||||||
|
if let Ok(mmsi) = value.parse::<u32>() {
|
||||||
|
self.own_mmsi = mmsi;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemInteraction::Toggle => {
|
||||||
|
self.receiving = !self.receiving;
|
||||||
|
self.status = if self.receiving {
|
||||||
|
SystemStatus::Active
|
||||||
|
} else {
|
||||||
|
SystemStatus::Inactive
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
SystemInteraction::Reset => {
|
||||||
|
self.own_mmsi = 123456789;
|
||||||
|
self.receiving = true;
|
||||||
|
self.status = SystemStatus::Active;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> SystemStatus {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to create and register all yacht systems
|
||||||
|
pub fn create_yacht_systems() -> Vec<Box<dyn YachtSystem>> {
|
||||||
|
vec![
|
||||||
|
Box::new(GpsSystem::new()),
|
||||||
|
Box::new(RadarSystem::new()),
|
||||||
|
Box::new(AisSystem::new()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gps_system() {
|
||||||
|
let gps = GpsSystem::new();
|
||||||
|
assert_eq!(gps.id(), "gps");
|
||||||
|
assert_eq!(gps.display_name(), "GPS Navigation");
|
||||||
|
assert_eq!(gps.status(), SystemStatus::Active);
|
||||||
|
|
||||||
|
let yacht_data = YachtData::default();
|
||||||
|
let display = gps.render_display(&yacht_data);
|
||||||
|
assert!(display.contains("GPS NAVIGATION SYSTEM"));
|
||||||
|
assert!(display.contains("Satellites: 12 connected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_radar_system() {
|
||||||
|
let mut radar = RadarSystem::new();
|
||||||
|
assert_eq!(radar.id(), "radar");
|
||||||
|
assert_eq!(radar.display_name(), "Radar System");
|
||||||
|
|
||||||
|
// Test configuration
|
||||||
|
assert!(radar.handle_interaction(SystemInteraction::Configure("range".to_string(), "24".to_string())));
|
||||||
|
let display = radar.render_display(&YachtData::default());
|
||||||
|
assert!(display.contains("24 NM RANGE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ais_system() {
|
||||||
|
let mut ais = AisSystem::new();
|
||||||
|
assert_eq!(ais.id(), "ais");
|
||||||
|
assert_eq!(ais.display_name(), "AIS System");
|
||||||
|
|
||||||
|
// Test toggle
|
||||||
|
assert!(ais.handle_interaction(SystemInteraction::Toggle));
|
||||||
|
assert_eq!(ais.status(), SystemStatus::Inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_yacht_systems() {
|
||||||
|
let systems = create_yacht_systems();
|
||||||
|
assert_eq!(systems.len(), 3);
|
||||||
|
|
||||||
|
let ids: Vec<&str> = systems.iter().map(|s| s.id()).collect();
|
||||||
|
assert!(ids.contains(&"gps"));
|
||||||
|
assert!(ids.contains(&"radar"));
|
||||||
|
assert!(ids.contains(&"ais"));
|
||||||
|
}
|
||||||
|
}
|
5
crates/yachtpit/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
../../target/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
dist/
|
5519
crates/yachtpit/Cargo.lock
generated
Normal file
90
crates/yachtpit/Cargo.toml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
[package]
|
||||||
|
name = "yachtpit"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
authors = ["seemueller-io <git@github.geoffsee>"]
|
||||||
|
edition = "2021"
|
||||||
|
exclude = ["dist", "build", "assets", "credits"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
# This is used by trunk as it doesn't support custom profiles: https://github.com/trunk-rs/trunk/issues/605
|
||||||
|
# xbuild also uses this profile for building android AABs because I couldn't find a configuration for it
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = 'thin'
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
# Profile for distribution
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
dev = [
|
||||||
|
"bevy/dynamic_linking",
|
||||||
|
]
|
||||||
|
|
||||||
|
# All of Bevy's default features exept for the audio related ones (bevy_audio, vorbis), since they clash with bevy_kira_audio
|
||||||
|
# and android_shared_stdcxx/android-game-activity, since those are covered in `mobile`
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.16", default-features = false, features = [
|
||||||
|
"animation",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_color",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_gilrs",
|
||||||
|
"bevy_gizmos",
|
||||||
|
"bevy_gltf",
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_mesh_picking_backend",
|
||||||
|
"bevy_pbr",
|
||||||
|
"bevy_picking",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_scene",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_sprite_picking_backend",
|
||||||
|
"bevy_state",
|
||||||
|
"bevy_text",
|
||||||
|
"bevy_ui",
|
||||||
|
"bevy_ui_picking_backend",
|
||||||
|
"bevy_window",
|
||||||
|
"bevy_winit",
|
||||||
|
"custom_cursor",
|
||||||
|
"default_font",
|
||||||
|
"hdr",
|
||||||
|
"multi_threaded",
|
||||||
|
"png",
|
||||||
|
"smaa_luts",
|
||||||
|
"sysinfo_plugin",
|
||||||
|
"tonemapping_luts",
|
||||||
|
"webgl2",
|
||||||
|
"x11",
|
||||||
|
] }
|
||||||
|
bevy_kira_audio = { version = "0.23.0", features = ["android_shared_stdcxx"] }
|
||||||
|
bevy_asset_loader = { version = "0.23.0" }
|
||||||
|
rand = { version = "0.8.3" }
|
||||||
|
webbrowser = { version = "1", features = ["hardened"] }
|
||||||
|
systems = { path = "../systems" }
|
||||||
|
components = { path = "../components" }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Window"] }
|
||||||
|
|
||||||
|
# keep the following in sync with Bevy's dependencies
|
||||||
|
winit = { version = "0.30", default-features = false }
|
||||||
|
image = { version = "0.25", default-features = false }
|
||||||
|
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
||||||
|
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
embed-resource = "1"
|
@@ -1,5 +1,6 @@
|
|||||||
[build]
|
[build]
|
||||||
public_url = "./"
|
public_url = "./"
|
||||||
|
wasm_bindgen = "0.2.100"
|
||||||
|
|
||||||
[serve]
|
[serve]
|
||||||
port = 8080
|
port = 8080
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
@@ -14,7 +14,7 @@
|
|||||||
<link data-trunk rel="inline" href="build/web/sound.js"/>
|
<link data-trunk rel="inline" href="build/web/sound.js"/>
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
<div class="lds-dual-ring"></div>
|
<div class="lds-dual-ring"></div>
|
||||||
<canvas id="bevy">
|
<canvas id="yachtpit-canvas">
|
||||||
Javascript and support for canvas is required
|
Javascript and support for canvas is required
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
@@ -9,7 +9,7 @@ name = "mobile"
|
|||||||
crate-type = ["staticlib", "cdylib"]
|
crate-type = ["staticlib", "cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
yachtpit = { path = ".." }
|
yachtpit = { path = "../" }
|
||||||
bevy = { version = "0.16.0", default-features = false, features = ["android-native-activity"] }
|
bevy = { version = "0.16.0", default-features = false, features = ["android-native-activity"] }
|
||||||
|
|
||||||
[target."cfg(target_os = \"ios\")".dependencies]
|
[target."cfg(target_os = \"ios\")".dependencies]
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -3,9 +3,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"files": ["dist"],
|
"files": [
|
||||||
|
"../../dist"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "./cleanup.sh",
|
"clean": "../../cleanup.sh",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
@@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::actions::game_control::{get_movement, GameControl};
|
use crate::core::actions::game_control::{get_movement, GameControl};
|
||||||
use crate::GameState;
|
use crate::GameState;
|
||||||
|
|
||||||
mod game_control;
|
mod game_control;
|
5
crates/yachtpit/src/core/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod actions;
|
||||||
|
pub mod system_manager;
|
||||||
|
|
||||||
|
pub use actions::ActionsPlugin;
|
||||||
|
pub use system_manager::{SystemManagerPlugin};
|
223
crates/yachtpit/src/core/system_manager.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
//! Higher-level abstraction for managing yacht systems and their interactions
|
||||||
|
//!
|
||||||
|
//! This module provides a unified approach to handling different yacht systems
|
||||||
|
//! (GPS, Radar, AIS, etc.) with common patterns for state management, UI updates,
|
||||||
|
//! and user interactions.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use systems::{YachtSystem, SystemInteraction, SystemStatus};
|
||||||
|
use components::YachtData;
|
||||||
|
|
||||||
|
/// Resource for managing all yacht systems
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SystemManager {
|
||||||
|
systems: HashMap<String, Box<dyn YachtSystem>>,
|
||||||
|
active_system: Option<String>,
|
||||||
|
system_order: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
systems: HashMap::new(),
|
||||||
|
active_system: None,
|
||||||
|
system_order: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a new yacht system
|
||||||
|
pub fn register_system(&mut self, system: Box<dyn YachtSystem>) {
|
||||||
|
let id = system.id().to_string();
|
||||||
|
self.system_order.push(id.clone());
|
||||||
|
self.systems.insert(id, system);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently active system
|
||||||
|
pub fn active_system(&self) -> Option<&dyn YachtSystem> {
|
||||||
|
self.active_system.as_ref()
|
||||||
|
.and_then(|id| self.systems.get(id))
|
||||||
|
.map(|system| system.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the active system by ID
|
||||||
|
pub fn set_active_system(&mut self, system_id: &str) -> bool {
|
||||||
|
if self.systems.contains_key(system_id) {
|
||||||
|
self.active_system = Some(system_id.to_string());
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all registered systems in order
|
||||||
|
pub fn get_systems(&self) -> Vec<&dyn YachtSystem> {
|
||||||
|
self.system_order.iter()
|
||||||
|
.filter_map(|id| self.systems.get(id))
|
||||||
|
.map(|system| system.as_ref())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all systems
|
||||||
|
pub fn update_systems(&mut self, yacht_data: &YachtData, time: &Time) {
|
||||||
|
for system in self.systems.values_mut() {
|
||||||
|
system.update(yacht_data, time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle interaction with a specific system
|
||||||
|
pub fn handle_system_interaction(&mut self, system_id: &str, interaction: SystemInteraction) -> bool {
|
||||||
|
if let Some(system) = self.systems.get_mut(system_id) {
|
||||||
|
system.handle_interaction(interaction)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get system by ID
|
||||||
|
pub fn get_system(&self, system_id: &str) -> Option<&dyn YachtSystem> {
|
||||||
|
self.systems.get(system_id).map(|s| s.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable system by ID
|
||||||
|
pub fn get_system_mut(&mut self, system_id: &str) -> Option<&mut Box<dyn YachtSystem>> {
|
||||||
|
self.systems.get_mut(system_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SystemManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component for marking UI elements as system indicators
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SystemIndicator {
|
||||||
|
pub system_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component for marking the main system display area
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SystemDisplayArea;
|
||||||
|
|
||||||
|
/// Plugin for the system manager
|
||||||
|
pub struct SystemManagerPlugin;
|
||||||
|
|
||||||
|
impl Plugin for SystemManagerPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<SystemManager>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
update_all_systems,
|
||||||
|
handle_system_indicator_interactions,
|
||||||
|
update_system_display_content,
|
||||||
|
).run_if(in_state(crate::GameState::Playing))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System to update all yacht systems
|
||||||
|
fn update_all_systems(
|
||||||
|
mut system_manager: ResMut<SystemManager>,
|
||||||
|
yacht_data: Res<components::YachtData>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
system_manager.update_systems(&yacht_data, &time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System to handle interactions with system indicator buttons
|
||||||
|
fn handle_system_indicator_interactions(
|
||||||
|
mut system_manager: ResMut<SystemManager>,
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(&Interaction, &mut BackgroundColor, &SystemIndicator),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, mut background_color, indicator) in &mut interaction_query {
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Pressed => {
|
||||||
|
system_manager.set_active_system(&indicator.system_id);
|
||||||
|
system_manager.handle_system_interaction(
|
||||||
|
&indicator.system_id,
|
||||||
|
SystemInteraction::Select
|
||||||
|
);
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
||||||
|
}
|
||||||
|
Interaction::Hovered => {
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.15, 0.15, 0.2));
|
||||||
|
}
|
||||||
|
Interaction::None => {
|
||||||
|
*background_color = BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System to update the main display area with active system content
|
||||||
|
fn update_system_display_content(
|
||||||
|
system_manager: Res<SystemManager>,
|
||||||
|
mut display_query: Query<&mut Text, With<SystemDisplayArea>>,
|
||||||
|
yacht_data: Res<components::YachtData>,
|
||||||
|
) {
|
||||||
|
if let Ok(mut text) = display_query.single_mut() {
|
||||||
|
if let Some(active_system) = system_manager.active_system() {
|
||||||
|
text.0 = active_system.render_display(&yacht_data);
|
||||||
|
} else {
|
||||||
|
text.0 = "Select a system above to view details".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use components::YachtData;
|
||||||
|
|
||||||
|
struct MockSystem {
|
||||||
|
id: &'static str,
|
||||||
|
name: &'static str,
|
||||||
|
status: SystemStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YachtSystem for MockSystem {
|
||||||
|
fn id(&self) -> &'static str { self.id }
|
||||||
|
fn display_name(&self) -> &'static str { self.name }
|
||||||
|
fn update(&mut self, _yacht_data: &YachtData, _time: &Time) {}
|
||||||
|
fn render_display(&self, _yacht_data: &YachtData) -> String {
|
||||||
|
format!("Mock system: {}", self.name)
|
||||||
|
}
|
||||||
|
fn handle_interaction(&mut self, _interaction: SystemInteraction) -> bool { true }
|
||||||
|
fn status(&self) -> SystemStatus { self.status.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_manager_registration() {
|
||||||
|
let mut manager = SystemManager::new();
|
||||||
|
let mock_system = Box::new(MockSystem {
|
||||||
|
id: "test",
|
||||||
|
name: "Test System",
|
||||||
|
status: SystemStatus::Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.register_system(mock_system);
|
||||||
|
assert!(manager.get_system("test").is_some());
|
||||||
|
assert_eq!(manager.get_systems().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_active_system_management() {
|
||||||
|
let mut manager = SystemManager::new();
|
||||||
|
let mock_system = Box::new(MockSystem {
|
||||||
|
id: "test",
|
||||||
|
name: "Test System",
|
||||||
|
status: SystemStatus::Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.register_system(mock_system);
|
||||||
|
assert!(manager.set_active_system("test"));
|
||||||
|
assert!(manager.active_system().is_some());
|
||||||
|
assert_eq!(manager.active_system().unwrap().id(), "test");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,23 +1,19 @@
|
|||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
mod actions;
|
|
||||||
mod audio;
|
|
||||||
mod loading;
|
|
||||||
mod menu;
|
|
||||||
mod player;
|
|
||||||
|
|
||||||
use crate::actions::ActionsPlugin;
|
mod core;
|
||||||
use crate::audio::InternalAudioPlugin;
|
mod ui;
|
||||||
use crate::loading::LoadingPlugin;
|
|
||||||
use crate::menu::MenuPlugin;
|
|
||||||
use crate::player::PlayerPlugin;
|
|
||||||
|
|
||||||
use bevy::app::App;
|
use bevy::app::App;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
|
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use crate::core::{ActionsPlugin, SystemManagerPlugin};
|
||||||
|
use crate::core::system_manager::SystemManager;
|
||||||
|
use crate::ui::{LoadingPlugin, MenuPlugin};
|
||||||
|
use systems::{PlayerPlugin, setup_instrument_cluster, get_yacht_systems};
|
||||||
|
|
||||||
// This example game uses States to separate logic
|
// This game uses States to separate logic
|
||||||
// See https://bevy-cheatbook.github.io/programming/states.html
|
// See https://bevy-cheatbook.github.io/programming/states.html
|
||||||
// Or https://github.com/bevyengine/bevy/blob/main/examples/ecs/state.rs
|
// Or https://github.com/bevyengine/bevy/blob/main/examples/ecs/state.rs
|
||||||
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
||||||
@@ -33,15 +29,24 @@ enum GameState {
|
|||||||
|
|
||||||
pub struct GamePlugin;
|
pub struct GamePlugin;
|
||||||
|
|
||||||
|
/// Initialize yacht systems in the SystemManager
|
||||||
|
fn initialize_yacht_systems(mut system_manager: ResMut<SystemManager>) {
|
||||||
|
let systems = get_yacht_systems();
|
||||||
|
for system in systems {
|
||||||
|
system_manager.register_system(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for GamePlugin {
|
impl Plugin for GamePlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_state::<GameState>().add_plugins((
|
app.init_state::<GameState>().add_plugins((
|
||||||
LoadingPlugin,
|
LoadingPlugin,
|
||||||
MenuPlugin,
|
MenuPlugin,
|
||||||
ActionsPlugin,
|
ActionsPlugin,
|
||||||
InternalAudioPlugin,
|
SystemManagerPlugin,
|
||||||
PlayerPlugin,
|
PlayerPlugin,
|
||||||
));
|
))
|
||||||
|
.add_systems(OnEnter(GameState::Playing), (setup_instrument_cluster, initialize_yacht_systems));
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
@@ -17,9 +17,8 @@ fn main() {
|
|||||||
DefaultPlugins
|
DefaultPlugins
|
||||||
.set(WindowPlugin {
|
.set(WindowPlugin {
|
||||||
primary_window: Some(Window {
|
primary_window: Some(Window {
|
||||||
title: "yachtpit".to_string(),
|
|
||||||
// Bind to canvas included in `index.html`
|
// Bind to canvas included in `index.html`
|
||||||
canvas: Some("#bevy".to_owned()),
|
canvas: Some("#yachtpit-canvas".to_owned()),
|
||||||
fit_canvas_to_parent: true,
|
fit_canvas_to_parent: true,
|
||||||
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
||||||
prevent_default_event_handling: false,
|
prevent_default_event_handling: false,
|
65
crates/yachtpit/src/ui/loading.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::GameState;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_asset_loader::prelude::*;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use web_sys::{Document, Element, HtmlElement, Window};
|
||||||
|
|
||||||
|
pub struct LoadingPlugin;
|
||||||
|
|
||||||
|
/// This plugin loads all assets using [`AssetLoader`] from a third party bevy plugin
|
||||||
|
/// Alternatively you can write the logic to load assets yourself
|
||||||
|
/// If interested, take a look at <https://bevy-cheatbook.github.io/features/assets.html>
|
||||||
|
impl Plugin for LoadingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_loading_state(
|
||||||
|
LoadingState::new(GameState::Loading)
|
||||||
|
.continue_to_state(GameState::Playing)
|
||||||
|
// .load_collection::<AudioAssets>()
|
||||||
|
// .load_collection::<TextureAssets>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a system to hide the loading indicator when transitioning to the Playing state
|
||||||
|
app.add_systems(OnEnter(GameState::Playing), hide_loading_indicator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hides the loading indicator when transitioning to the Playing state
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn hide_loading_indicator() {
|
||||||
|
info!("Hiding loading indicator");
|
||||||
|
|
||||||
|
// Get the window object
|
||||||
|
let window = web_sys::window().expect("Failed to get window");
|
||||||
|
|
||||||
|
// Get the document object
|
||||||
|
let document = window.document().expect("Failed to get document");
|
||||||
|
|
||||||
|
// Get the loading indicator element
|
||||||
|
if let Some(loading_indicator) = document.query_selector(".lds-dual-ring").ok().flatten() {
|
||||||
|
// Set its display property to "none" to hide it
|
||||||
|
let element = loading_indicator.dyn_into::<HtmlElement>().expect("Failed to cast to HtmlElement");
|
||||||
|
element.style().set_property("display", "none").expect("Failed to set style property");
|
||||||
|
} else {
|
||||||
|
warn!("Could not find loading indicator element");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op implementation for non-wasm32 targets
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn hide_loading_indicator() {
|
||||||
|
info!("Hiding loading indicator (no-op on non-wasm32 targets)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following asset collections will be loaded during the State `GameState::Loading`
|
||||||
|
// when done loading, they will be inserted as resources (see <https://github.com/NiklasEi/bevy_asset_loader>)
|
||||||
|
|
||||||
|
|
||||||
|
// #[derive(AssetCollection, Resource)]
|
||||||
|
// pub struct TextureAssets {
|
||||||
|
// #[asset(path = "assets/textures/bevy.png")]
|
||||||
|
// pub bevy: Handle<Image>,
|
||||||
|
// #[asset(path = "assets/textures/github.png")]
|
||||||
|
// pub github: Handle<Image>,
|
||||||
|
// }
|
@@ -1,4 +1,3 @@
|
|||||||
use crate::loading::TextureAssets;
|
|
||||||
use crate::GameState;
|
use crate::GameState;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ impl Default for ButtonColors {
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct Menu;
|
struct Menu;
|
||||||
|
|
||||||
fn setup_menu(mut commands: Commands, textures: Res<TextureAssets>) {
|
fn setup_menu(mut commands: Commands) {
|
||||||
info!("menu");
|
info!("menu");
|
||||||
commands.spawn((Camera2d, Msaa::Off));
|
commands.spawn((Camera2d, Msaa::Off));
|
||||||
commands
|
commands
|
||||||
@@ -114,10 +113,6 @@ fn setup_menu(mut commands: Commands, textures: Res<TextureAssets>) {
|
|||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
||||||
));
|
));
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
ImageNode {
|
|
||||||
image: textures.bevy.clone(),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(32.),
|
width: Val::Px(32.),
|
||||||
..default()
|
..default()
|
||||||
@@ -152,7 +147,6 @@ fn setup_menu(mut commands: Commands, textures: Res<TextureAssets>) {
|
|||||||
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
|
||||||
));
|
));
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
ImageNode::new(textures.github.clone()),
|
|
||||||
Node {
|
Node {
|
||||||
width: Val::Px(32.),
|
width: Val::Px(32.),
|
||||||
..default()
|
..default()
|
5
crates/yachtpit/src/ui/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod loading;
|
||||||
|
pub mod menu;
|
||||||
|
|
||||||
|
pub use loading::LoadingPlugin;
|
||||||
|
pub use menu::MenuPlugin;
|
56
src/audio.rs
@@ -1,56 +0,0 @@
|
|||||||
use crate::actions::{set_movement_actions, Actions};
|
|
||||||
use crate::loading::AudioAssets;
|
|
||||||
use crate::GameState;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_kira_audio::prelude::*;
|
|
||||||
|
|
||||||
pub struct InternalAudioPlugin;
|
|
||||||
|
|
||||||
// This plugin is responsible to control the game audio
|
|
||||||
impl Plugin for InternalAudioPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_plugins(AudioPlugin)
|
|
||||||
.add_systems(OnEnter(GameState::Playing), start_audio)
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
|
||||||
control_flying_sound
|
|
||||||
.after(set_movement_actions)
|
|
||||||
.run_if(in_state(GameState::Playing)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
struct FlyingAudio(Handle<AudioInstance>);
|
|
||||||
|
|
||||||
fn start_audio(mut commands: Commands, audio_assets: Res<AudioAssets>, audio: Res<Audio>) {
|
|
||||||
audio.pause();
|
|
||||||
let handle = audio
|
|
||||||
.play(audio_assets.flying.clone())
|
|
||||||
.looped()
|
|
||||||
.with_volume(0.3)
|
|
||||||
.handle();
|
|
||||||
commands.insert_resource(FlyingAudio(handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn control_flying_sound(
|
|
||||||
actions: Res<Actions>,
|
|
||||||
audio: Res<FlyingAudio>,
|
|
||||||
mut audio_instances: ResMut<Assets<AudioInstance>>,
|
|
||||||
) {
|
|
||||||
if let Some(instance) = audio_instances.get_mut(&audio.0) {
|
|
||||||
match instance.state() {
|
|
||||||
PlaybackState::Paused { .. } => {
|
|
||||||
if actions.player_movement.is_some() {
|
|
||||||
instance.resume(AudioTween::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PlaybackState::Playing { .. } => {
|
|
||||||
if actions.player_movement.is_none() {
|
|
||||||
instance.pause(AudioTween::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
use crate::GameState;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_asset_loader::prelude::*;
|
|
||||||
use bevy_kira_audio::AudioSource;
|
|
||||||
|
|
||||||
pub struct LoadingPlugin;
|
|
||||||
|
|
||||||
/// This plugin loads all assets using [`AssetLoader`] from a third party bevy plugin
|
|
||||||
/// Alternatively you can write the logic to load assets yourself
|
|
||||||
/// If interested, take a look at <https://bevy-cheatbook.github.io/features/assets.html>
|
|
||||||
impl Plugin for LoadingPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_loading_state(
|
|
||||||
LoadingState::new(GameState::Loading)
|
|
||||||
.continue_to_state(GameState::Playing)
|
|
||||||
.load_collection::<AudioAssets>()
|
|
||||||
.load_collection::<TextureAssets>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the following asset collections will be loaded during the State `GameState::Loading`
|
|
||||||
// when done loading, they will be inserted as resources (see <https://github.com/NiklasEi/bevy_asset_loader>)
|
|
||||||
|
|
||||||
#[derive(AssetCollection, Resource)]
|
|
||||||
pub struct AudioAssets {
|
|
||||||
#[asset(path = "audio/flying.ogg")]
|
|
||||||
pub flying: Handle<AudioSource>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(AssetCollection, Resource)]
|
|
||||||
pub struct TextureAssets {
|
|
||||||
#[asset(path = "textures/bevy.png")]
|
|
||||||
pub bevy: Handle<Image>,
|
|
||||||
#[asset(path = "textures/github.png")]
|
|
||||||
pub github: Handle<Image>,
|
|
||||||
}
|
|
799
src/player.rs
@@ -1,799 +0,0 @@
|
|||||||
use crate::GameState;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub struct PlayerPlugin;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct InstrumentCluster;
|
|
||||||
|
|
||||||
#[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;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct GpsIndicator;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct RadarIndicator;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct AisIndicator;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct SystemDisplay;
|
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
pub struct SelectedSystem {
|
|
||||||
pub current: Option<SystemType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum SystemType {
|
|
||||||
Gps,
|
|
||||||
Radar,
|
|
||||||
Ais,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This plugin handles the futuristic yacht instrument cluster
|
|
||||||
/// Instrument cluster is only active during the State `GameState::Playing`
|
|
||||||
impl Plugin for PlayerPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.init_resource::<YachtData>()
|
|
||||||
.init_resource::<SelectedSystem>()
|
|
||||||
.add_systems(OnEnter(GameState::Playing), setup_instrument_cluster)
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
|
||||||
(update_yacht_data, update_instrument_displays, handle_system_interactions, update_system_display)
|
|
||||||
.run_if(in_state(GameState::Playing))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_instrument_cluster(mut commands: Commands) {
|
|
||||||
// Spawn camera since we're bypassing the menu system
|
|
||||||
commands.spawn((Camera2d, Msaa::Off));
|
|
||||||
|
|
||||||
// Main container for the instrument cluster
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.05, 0.05, 0.1)),
|
|
||||||
InstrumentCluster,
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
|
||||||
// Top row - Main navigation and speed
|
|
||||||
parent
|
|
||||||
.spawn(Node {
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(60.0),
|
|
||||||
flex_direction: FlexDirection::Row,
|
|
||||||
justify_content: JustifyContent::SpaceEvenly,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
padding: UiRect::all(Val::Px(20.0)),
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
.with_children(|row| {
|
|
||||||
// Speed Gauge
|
|
||||||
row.spawn((
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
SpeedGauge,
|
|
||||||
))
|
|
||||||
.with_children(|gauge| {
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("SPEED"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 12.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
));
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("12.5"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 32.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
|
|
||||||
));
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("KTS"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 10.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Central Navigation Display
|
|
||||||
row.spawn((
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.15, 0.2)),
|
|
||||||
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
NavigationDisplay,
|
|
||||||
))
|
|
||||||
.with_children(|nav| {
|
|
||||||
nav.spawn((
|
|
||||||
Text::new("NAVIGATION"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
));
|
|
||||||
nav.spawn((
|
|
||||||
Text::new("045°"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 48.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
|
|
||||||
CompassGauge,
|
|
||||||
));
|
|
||||||
nav.spawn((
|
|
||||||
Text::new("HEADING"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 14.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Depth Gauge
|
|
||||||
row.spawn((
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
DepthGauge,
|
|
||||||
))
|
|
||||||
.with_children(|gauge| {
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("DEPTH"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 12.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
|
|
||||||
));
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("15.2"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 32.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
|
|
||||||
));
|
|
||||||
gauge.spawn((
|
|
||||||
Text::new("M"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 10.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bottom row - Engine and system status
|
|
||||||
parent
|
|
||||||
.spawn(Node {
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(40.0),
|
|
||||||
flex_direction: FlexDirection::Row,
|
|
||||||
justify_content: JustifyContent::SpaceEvenly,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
padding: UiRect::all(Val::Px(20.0)),
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
.with_children(|row| {
|
|
||||||
// Engine Status Panel
|
|
||||||
row.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(200.0),
|
|
||||||
height: Val::Px(150.0),
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.8, 0.4, 0.0)),
|
|
||||||
EngineStatus,
|
|
||||||
))
|
|
||||||
.with_children(|engine| {
|
|
||||||
engine.spawn((
|
|
||||||
Text::new("ENGINE"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 14.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.8, 0.4, 0.0)),
|
|
||||||
));
|
|
||||||
engine.spawn((
|
|
||||||
Text::new("82°C"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 24.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
|
|
||||||
));
|
|
||||||
engine.spawn((
|
|
||||||
Text::new("TEMP NORMAL"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 10.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// System Status Grid
|
|
||||||
row.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(250.0),
|
|
||||||
height: Val::Px(150.0),
|
|
||||||
border: UiRect::all(Val::Px(1.0)),
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
justify_content: JustifyContent::SpaceEvenly,
|
|
||||||
padding: UiRect::all(Val::Px(10.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.08, 0.08, 0.12)),
|
|
||||||
BorderColor(Color::linear_rgb(0.4, 0.4, 0.6)),
|
|
||||||
))
|
|
||||||
.with_children(|grid| {
|
|
||||||
grid.spawn((
|
|
||||||
Text::new("SYSTEMS"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 12.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.4, 0.4, 0.8)),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Fuel Level Bar
|
|
||||||
grid.spawn(Node {
|
|
||||||
flex_direction: FlexDirection::Row,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
justify_content: JustifyContent::SpaceBetween,
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
.with_children(|bar| {
|
|
||||||
bar.spawn((
|
|
||||||
Text::new("FUEL"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 10.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.7, 0.7, 0.7)),
|
|
||||||
));
|
|
||||||
|
|
||||||
bar.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(80.0),
|
|
||||||
height: Val::Px(8.0),
|
|
||||||
border: UiRect::all(Val::Px(1.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.2, 0.2, 0.2)),
|
|
||||||
BorderColor(Color::linear_rgb(0.4, 0.4, 0.4)),
|
|
||||||
))
|
|
||||||
.with_children(|bar_bg| {
|
|
||||||
bar_bg.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Percent(75.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.0, 0.8, 0.0)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.spawn((
|
|
||||||
Text::new("75%"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 9.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.8, 0.8, 0.8)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Battery Level Bar
|
|
||||||
grid.spawn(Node {
|
|
||||||
flex_direction: FlexDirection::Row,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
justify_content: JustifyContent::SpaceBetween,
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
.with_children(|bar| {
|
|
||||||
bar.spawn((
|
|
||||||
Text::new("BATTERY"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 10.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.7, 0.7, 0.7)),
|
|
||||||
));
|
|
||||||
|
|
||||||
bar.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(80.0),
|
|
||||||
height: Val::Px(8.0),
|
|
||||||
border: UiRect::all(Val::Px(1.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.2, 0.2, 0.2)),
|
|
||||||
BorderColor(Color::linear_rgb(0.4, 0.4, 0.4)),
|
|
||||||
))
|
|
||||||
.with_children(|bar_bg| {
|
|
||||||
bar_bg.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Percent(88.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.0, 0.6, 1.0)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.spawn((
|
|
||||||
Text::new("88%"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 9.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.8, 0.8, 0.8)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
|
|
||||||
GpsIndicator,
|
|
||||||
))
|
|
||||||
.with_children(|indicator| {
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("🛰️"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
|
|
||||||
));
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("GPS"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 8.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// RADAR Indicator
|
|
||||||
indicators.spawn((
|
|
||||||
Button,
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
|
|
||||||
RadarIndicator,
|
|
||||||
))
|
|
||||||
.with_children(|indicator| {
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("📡"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
|
|
||||||
));
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("RADAR"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 8.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
// AIS Indicator
|
|
||||||
indicators.spawn((
|
|
||||||
Button,
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
|
|
||||||
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
|
|
||||||
AisIndicator,
|
|
||||||
))
|
|
||||||
.with_children(|indicator| {
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("🚢"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.8, 0.0, 0.0)),
|
|
||||||
));
|
|
||||||
indicator.spawn((
|
|
||||||
Text::new("AIS"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 8.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wind Information
|
|
||||||
row.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Px(200.0),
|
|
||||||
height: Val::Px(150.0),
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::linear_rgb(0.1, 0.15, 0.1)),
|
|
||||||
BorderColor(Color::linear_rgb(0.0, 0.8, 0.4)),
|
|
||||||
))
|
|
||||||
.with_children(|wind| {
|
|
||||||
wind.spawn((
|
|
||||||
Text::new("WIND"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 14.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 0.8, 0.4)),
|
|
||||||
));
|
|
||||||
wind.spawn((
|
|
||||||
Text::new("8.3 KTS"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 18.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.0, 1.0, 0.6)),
|
|
||||||
));
|
|
||||||
wind.spawn((
|
|
||||||
Text::new("120° REL"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 14.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.6, 0.8, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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(Color::linear_rgb(0.05, 0.05, 0.1)),
|
|
||||||
BorderColor(Color::linear_rgb(0.2, 0.2, 0.3)),
|
|
||||||
SystemDisplay,
|
|
||||||
))
|
|
||||||
.with_children(|display| {
|
|
||||||
display.spawn((
|
|
||||||
Text::new("Select a system above to view details"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::linear_rgb(0.5, 0.5, 0.6)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_system_interactions(
|
|
||||||
mut selected_system: ResMut<SelectedSystem>,
|
|
||||||
mut interaction_query: Query<
|
|
||||||
(&Interaction, &mut BackgroundColor, Option<&GpsIndicator>, Option<&RadarIndicator>, Option<&AisIndicator>),
|
|
||||||
(Changed<Interaction>, With<Button>),
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
for (interaction, mut background_color, gps, radar, ais) in &mut interaction_query {
|
|
||||||
match *interaction {
|
|
||||||
Interaction::Pressed => {
|
|
||||||
if gps.is_some() {
|
|
||||||
selected_system.current = Some(SystemType::Gps);
|
|
||||||
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
|
||||||
} else if radar.is_some() {
|
|
||||||
selected_system.current = Some(SystemType::Radar);
|
|
||||||
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
|
||||||
} else if ais.is_some() {
|
|
||||||
selected_system.current = Some(SystemType::Ais);
|
|
||||||
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Interaction::Hovered => {
|
|
||||||
*background_color = BackgroundColor(Color::linear_rgb(0.15, 0.15, 0.2));
|
|
||||||
}
|
|
||||||
Interaction::None => {
|
|
||||||
*background_color = BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_system_display(
|
|
||||||
selected_system: Res<SelectedSystem>,
|
|
||||||
mut display_query: Query<&mut Text, With<SystemDisplay>>,
|
|
||||||
yacht_data: Res<YachtData>,
|
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
|
||||||
if let Ok(mut text) = display_query.single_mut() {
|
|
||||||
match selected_system.current {
|
|
||||||
Some(SystemType::Gps) => {
|
|
||||||
text.0 = format!(
|
|
||||||
"GPS NAVIGATION SYSTEM\n\n\
|
|
||||||
Position: 43°38'19.5\"N 1°26'58.3\"W\n\
|
|
||||||
Heading: {:.0}°\n\
|
|
||||||
Speed: {:.1} knots\n\
|
|
||||||
Course Over Ground: {:.0}°\n\
|
|
||||||
Satellites: 12 connected\n\
|
|
||||||
HDOP: 0.8 (Excellent)\n\
|
|
||||||
\n\
|
|
||||||
Next Waypoint: MONACO HARBOR\n\
|
|
||||||
Distance: 127.3 NM\n\
|
|
||||||
ETA: 10h 12m",
|
|
||||||
yacht_data.heading,
|
|
||||||
yacht_data.speed,
|
|
||||||
yacht_data.heading + 5.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(SystemType::Radar) => {
|
|
||||||
let sweep_angle = (time.elapsed_secs() * 60.0) % 360.0;
|
|
||||||
text.0 = format!(
|
|
||||||
"RADAR SYSTEM - 12 NM RANGE\n\n\
|
|
||||||
Status: ACTIVE\n\
|
|
||||||
Sweep: {:.0}°\n\
|
|
||||||
Gain: AUTO\n\
|
|
||||||
Sea Clutter: -15 dB\n\
|
|
||||||
Rain Clutter: OFF\n\
|
|
||||||
\n\
|
|
||||||
CONTACTS DETECTED:\n\
|
|
||||||
• Vessel 1: 2.3 NM @ 045° (15 kts)\n\
|
|
||||||
• Vessel 2: 5.7 NM @ 180° (8 kts)\n\
|
|
||||||
• Land Mass: 8.2 NM @ 270°\n\
|
|
||||||
• Buoy: 1.1 NM @ 315°",
|
|
||||||
sweep_angle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(SystemType::Ais) => {
|
|
||||||
text.0 = format!(
|
|
||||||
"AIS - AUTOMATIC IDENTIFICATION SYSTEM\n\n\
|
|
||||||
Status: RECEIVING\n\
|
|
||||||
Own Ship MMSI: 123456789\n\
|
|
||||||
\n\
|
|
||||||
NEARBY VESSELS:\n\
|
|
||||||
\n\
|
|
||||||
🛥️ M/Y SERENITY\n\
|
|
||||||
MMSI: 987654321\n\
|
|
||||||
Distance: 2.1 NM @ 045°\n\
|
|
||||||
Speed: 12.5 kts\n\
|
|
||||||
Course: 180°\n\
|
|
||||||
\n\
|
|
||||||
🚢 CARGO VESSEL ATLANTIS\n\
|
|
||||||
MMSI: 456789123\n\
|
|
||||||
Distance: 5.8 NM @ 270°\n\
|
|
||||||
Speed: 18.2 kts\n\
|
|
||||||
Course: 090°\n\
|
|
||||||
\n\
|
|
||||||
⛵ S/Y WIND DANCER\n\
|
|
||||||
MMSI: 789123456\n\
|
|
||||||
Distance: 1.3 NM @ 135°\n\
|
|
||||||
Speed: 6.8 kts\n\
|
|
||||||
Course: 225°"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
text.0 = "Select a system above to view details".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_system_type_enum() {
|
|
||||||
let gps = SystemType::Gps;
|
|
||||||
let radar = SystemType::Radar;
|
|
||||||
let ais = SystemType::Ais;
|
|
||||||
|
|
||||||
assert_ne!(gps, radar);
|
|
||||||
assert_ne!(radar, ais);
|
|
||||||
assert_ne!(ais, gps);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_selected_system_default() {
|
|
||||||
let selected_system = SelectedSystem::default();
|
|
||||||
assert_eq!(selected_system.current, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|