Modularize (#1)

* configure workspaces

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

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

* add theme for instruments

* trunk serve works, remove audio and textures

* remove loading indicator and assets

* rename models to systems

* seperate systems and components from models

* Refactor instrument cluster to leverage reusable composition utilities.

---------

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
crates/systems/Cargo.toml Normal file
View 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
View 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};

View 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()
}

View 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);
}
}

View 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
View File

@@ -0,0 +1,5 @@
../../target/
.idea/
.DS_Store
dist/

5519
crates/yachtpit/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View 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"

View File

@@ -0,0 +1,161 @@
# Icons Required for Yacht yachtpit Application
This document lists all the icons that need to be generated for the yacht yachtpit application based on the current UI implementation.
## Navigation & Compass Icons
### Primary Navigation
- **Compass Rose Icon** - For the central navigation display
- **North Arrow Icon** - Directional indicator
- **GPS Satellite Icon** - GPS status indicator
- **Waypoint Icon** - Navigation waypoints
- **Route Line Icon** - Planned route visualization
## Instrument Gauge Icons
### Speed Gauge
- **Speedometer Icon** - Circular gauge background
- **Speed Needle Icon** - Gauge pointer/needle
- **Knots Unit Icon** - "KTS" stylized icon
### Depth Gauge
- **Depth Sounder Icon** - Sonar/depth measurement icon
- **Water Depth Icon** - Underwater depth visualization
- **Meter Unit Icon** - "M" stylized icon
## Engine & Systems Icons
### Engine Status
- **Engine Icon** - Marine engine representation
- **Temperature Gauge Icon** - Engine temperature indicator
- **Cooling System Icon** - Engine cooling status
- **Engine Alert Icon** - Warning/alert indicator
### Fuel System
- **Fuel Tank Icon** - Fuel level indicator
- **Fuel Pump Icon** - Fuel system status
- **Fuel Drop Icon** - Fuel consumption indicator
### Electrical System
- **Battery Icon** - Battery level indicator
- **Charging Icon** - Battery charging status
- **Power Icon** - Electrical system status
- **Voltage Meter Icon** - Electrical measurement
## Communication & Navigation Systems
### GPS System
- **GPS Icon** - Global positioning system
- **Satellite Signal Icon** - Signal strength indicator
- **Location Pin Icon** - Current position marker
### Radar System
- **Radar Dish Icon** - Radar antenna representation
- **Radar Sweep Icon** - Radar scanning animation
- **Target Blip Icon** - Radar contact indicator
### AIS (Automatic Identification System)
- **AIS Icon** - Ship identification system
- **Ship Icon** - Other vessel representation
- **Radio Wave Icon** - Communication signal
## Weather & Environmental Icons
### Wind Information
- **Wind Vane Icon** - Wind direction indicator
- **Wind Speed Icon** - Anemometer representation
- **Wind Arrow Icon** - Directional wind indicator
- **Beaufort Scale Icon** - Wind force scale
### Weather Conditions
- **Barometer Icon** - Atmospheric pressure
- **Temperature Icon** - Air temperature
- **Humidity Icon** - Relative humidity indicator
## Status & Alert Icons
### System Status Indicators
- **Green Status Dot** - System operational
- **Red Status Dot** - System fault/offline
- **Yellow Status Dot** - System warning
- **Blue Status Dot** - System standby
### Alert Icons
- **Warning Triangle** - General warning
- **Critical Alert** - Emergency situation
- **Information Icon** - General information
- **Maintenance Icon** - Service required
## UI Control Icons
### Navigation Controls
- **Menu Icon** - Main menu access
- **Settings Icon** - Configuration access
- **Home Icon** - Return to main display
- **Back Arrow** - Navigation back
### Display Controls
- **Brightness Icon** - Screen brightness control
- **Contrast Icon** - Display contrast
- **Night Mode Icon** - Low-light display mode
- **Full Screen Icon** - Display mode toggle
## Chart & Mapping Icons
### Chart Elements
- **Chart Icon** - Nautical chart representation
- **Depth Contour Icon** - Underwater topography
- **Buoy Icon** - Navigation aids
- **Harbor Icon** - Port/marina indicator
- **Anchor Icon** - Anchorage areas
### Measurement Tools
- **Ruler Icon** - Distance measurement
- **Protractor Icon** - Bearing measurement
- **Scale Icon** - Chart scale indicator
## Safety & Emergency Icons
### Safety Equipment
- **Life Ring Icon** - Safety equipment
- **Fire Extinguisher Icon** - Emergency equipment
- **First Aid Icon** - Medical supplies
- **Emergency Radio Icon** - Distress communication
### Emergency Procedures
- **SOS Icon** - Distress signal
- **Mayday Icon** - Emergency call
- **Coast Guard Icon** - Emergency services
- **Evacuation Icon** - Emergency procedures
## File Formats Required
All icons should be generated in the following formats:
- **PNG**: 16x16, 24x24, 32x32, 48x48, 64x64, 128x128, 256x256 pixels
- **SVG**: Scalable vector format for high-DPI displays
- **ICO**: Windows icon format (for desktop application)
## Design Guidelines
### Style Requirements
- **Nautical Theme**: Maritime-inspired design language
- **High Contrast**: Suitable for marine lighting conditions
- **Monochromatic**: Primary colors should be cyan/blue theme
- **Clean Lines**: Minimalist, professional appearance
- **Scalable**: Must remain legible at small sizes
### Color Palette
- **Primary**: Cyan (#00CCFF) - Main UI elements
- **Secondary**: Green (#00FF80) - Operational status
- **Warning**: Orange (#FF8000) - Caution states
- **Alert**: Red (#FF0040) - Critical alerts
- **Neutral**: Gray (#999999) - Inactive elements
## Implementation Notes
These icons will replace the current text-based placeholders in:
- `src/player.rs` - Main instrument cluster
- `src/menu.rs` - Menu system icons
- `src/loading.rs` - Loading screen elements
The icons should be placed in the `assets/textures/icons/` directory and loaded through the existing `TextureAssets` resource system.

View File

@@ -0,0 +1,6 @@
[build]
public_url = "./"
wasm_bindgen = "0.2.100"
[serve]
port = 8080

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

10
crates/yachtpit/build.rs Normal file
View File

@@ -0,0 +1,10 @@
extern crate embed_resource;
use std::env;
fn main() {
let target = env::var("TARGET").unwrap();
if target.contains("windows") {
// on windows we will set our game icon as icon for the executable
embed_resource::compile("build/windows/icon.rc");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
rm -rf AppIcon.iconset/*
mkdir -p AppIcon.iconset
sips -z 16 16 ../icon_1024x1024.png --out AppIcon.iconset/icon_16x16.png
sips -z 32 32 ../icon_1024x1024.png --out AppIcon.iconset/icon_16x16@2x.png
sips -z 32 32 ../icon_1024x1024.png --out AppIcon.iconset/icon_32x32.png
sips -z 64 64 ../icon_1024x1024.png --out AppIcon.iconset/icon_32x32@2x.png
sips -z 128 128 ../icon_1024x1024.png --out AppIcon.iconset/icon_128x128.png
sips -z 256 256 ../icon_1024x1024.png --out AppIcon.iconset/icon_128x128@2x.png
sips -z 256 256 ../icon_1024x1024.png --out AppIcon.iconset/icon_256x256.png
sips -z 512 512 ../icon_1024x1024.png --out AppIcon.iconset/icon_256x256@2x.png
sips -z 512 512 ../icon_1024x1024.png --out AppIcon.iconset/icon_512x512.png
cp ../icon_1024x1024.png AppIcon.iconset/icon_512x512@2x.png
iconutil -c icns AppIcon.iconset
mkdir -p src/Game.app/Contents/Resources
mv AppIcon.icns src/Game.app/Contents/Resources/

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
rm -rf AppIcon.iconset/*
mkdir -p AppIcon.iconset
convert ../icon_1024x1024.png -resize 16x16 AppIcon.iconset/icon_16x16.png
convert ../icon_1024x1024.png -resize 32x32 AppIcon.iconset/icon_16x16@2x.png
convert ../icon_1024x1024.png -resize 32x32 AppIcon.iconset/icon_32x32.png
convert ../icon_1024x1024.png -resize 64x64 AppIcon.iconset/icon_32x32@2x.png
convert ../icon_1024x1024.png -resize 128x128 AppIcon.iconset/icon_128x128.png
convert ../icon_1024x1024.png -resize 256x256 AppIcon.iconset/icon_128x128@2x.png
convert ../icon_1024x1024.png -resize 256x256 AppIcon.iconset/icon_256x256.png
convert ../icon_1024x1024.png -resize 512x512 AppIcon.iconset/icon_256x256@2x.png
convert ../icon_1024x1024.png -resize 512x512 AppIcon.iconset/icon_512x512.png
cp ../icon_1024x1024.png AppIcon.iconset/icon_512x512@2x.png
png2icns ./AppIcon.icns AppIcon.iconset/icon_16x16.png AppIcon.iconset/icon_32x32.png AppIcon.iconset/icon_128x128.png AppIcon.iconset/icon_256x256.png AppIcon.iconset/icon_512x512.png
mkdir -p src/Game.app/Contents/Resources
mv AppIcon.icns src/Game.app/Contents/Resources/

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>yachtpit</string>
<key>CFBundleExecutable</key>
<string>yachtpit</string>
<key>CFBundleIconFile</key>
<string>AppIcon.icns</string>
<key>CFBundleIdentifier</key>
<string>io.gs.yachtpit</string> <!-- ToDo replace all instances with io.gs.yachtpit -->
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>yachtpit</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<!-- Version -->
<string>0.1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,62 @@
// Insert hack to make sound autoplay on Chrome as soon as the user interacts with the tab:
// https://developers.google.com/web/updates/2018/11/web-audio-autoplay#moving-forward
// the following function keeps track of all AudioContexts and resumes them on the first user
// interaction with the page. If the function is called and all contexts are already running,
// it will remove itself from all event listeners.
(function () {
// An array of all contexts to resume on the page
const audioContextList = [];
// An array of various user interaction events we should listen for
const userInputEventNames = [
"click",
"contextmenu",
"auxclick",
"dblclick",
"mousedown",
"mouseup",
"pointerup",
"touchend",
"keydown",
"keyup",
];
// A proxy object to intercept AudioContexts and
// add them to the array for tracking and resuming later
self.AudioContext = new Proxy(self.AudioContext, {
construct(target, args) {
const result = new target(...args);
audioContextList.push(result);
return result;
},
});
// To resume all AudioContexts being tracked
function resumeAllContexts(_event) {
let count = 0;
audioContextList.forEach((context) => {
if (context.state !== "running") {
context.resume();
} else {
count++;
}
});
// If all the AudioContexts have now resumed then we unbind all
// the event listeners from the page to prevent unnecessary resume attempts
// Checking count > 0 ensures that the user interaction happens AFTER the game started up
if (count > 0 && count === audioContextList.length) {
userInputEventNames.forEach((eventName) => {
document.removeEventListener(eventName, resumeAllContexts);
});
}
}
// We bind the resume function for each user interaction
// event on the page
userInputEventNames.forEach((eventName) => {
document.addEventListener(eventName, resumeAllContexts);
});
})();

View File

@@ -0,0 +1,53 @@
body, html {
height: 100%;
}
body {
background-color: transparent;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.game-container {
width: 800px;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
}
.lds-dual-ring {
display: inline-block;
position: absolute;
left: 0;
right: 0;
margin: auto;
width: 80px;
height: 80px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 64px;
height: 64px;
border-radius: 50%;
border: 6px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#bevy {
z-index: 2;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -0,0 +1 @@
app_icon ICON "icon.ico"

View File

@@ -0,0 +1,2 @@
/bin/
/obj/

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<!-- Copied from https://github.com/wixtoolset/wix4/blob/6f2511f58f60e63a15357a2fe37f83343dea3090/src/ext/UI/wixlib/WixUI_InstallDir.wxs#L9
but with the license dialog disabled.
See: https://wixtoolset.org/docs/v3/wixui/wixui_customizations/#changing-the-ui-sequence-of-a-built-in-dialog-set. -->
<Fragment>
<UI Id="WixUI_CustomInstallDir">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Condition="NOT Installed" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH" />
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1" Condition="NOT Installed" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2" Condition="Installed AND NOT PATCH" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2" Condition="Installed AND PATCH" />
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg" />
<Property Id="ARPNOMODIFY" Value="1" />
<InstallUISequence>
<Show Dialog="FatalError" OnExit="error" />
<Show Dialog="UserExit" OnExit="cancel" />
<Show Dialog="ExitDialog" OnExit="success" />
</InstallUISequence>
<AdminUISequence>
<Show Dialog="FatalError" OnExit="error" />
<Show Dialog="UserExit" OnExit="cancel" />
<Show Dialog="ExitDialog" OnExit="success" />
</AdminUISequence>
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="WixUI_CustomInstallDir_$(WIXUIARCH)">
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
</Fragment>
<?endforeach?>
</Wix>

View File

@@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "Installer.wixproj", "{340293B0-F46C-46A0-88D8-4BB2F3465C53}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|Any CPU.ActiveCfg = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|Any CPU.Build.0 = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|ARM64.ActiveCfg = Debug|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|ARM64.Build.0 = Debug|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x64.ActiveCfg = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x64.Build.0 = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x86.ActiveCfg = Debug|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x86.Build.0 = Debug|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|Any CPU.ActiveCfg = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|Any CPU.Build.0 = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|ARM64.ActiveCfg = Release|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|ARM64.Build.0 = Release|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x64.ActiveCfg = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x64.Build.0 = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x86.ActiveCfg = Release|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Sdk="WixToolset.Sdk/4.0.0">
<PropertyGroup>
<DebugType>none</DebugType>
<OutputName>installer</OutputName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.UI.wixext" Version="4.0.0" />
<PackageReference Include="WixToolset.Heat" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="..\..\..\assets">
<ComponentGroupName>AssetsDirectory</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressRootDirectory>false</SuppressRootDirectory>
</HarvestDirectory>
<BindPath Include="..\..\..\assets" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="..\..\..\credits">
<ComponentGroupName>CreditsDirectory</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressRootDirectory>false</SuppressRootDirectory>
</HarvestDirectory>
<BindPath Include="..\..\..\credits" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
</WixLocalization>

View File

@@ -0,0 +1,50 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package
Name="yachtpit"
Manufacturer="gsio"
UpgradeCode="ac8709c2-1d6f-4440-b424-386e4e315425"
Version="0.0.1"
Scope="perUserOrMachine">
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<Media Id="1" Cabinet="myapplication.cab" EmbedCab="yes" />
<Property Id="ApplicationFolderName" Value="!(bind.Property.ProductName)" />
<!-- Installer Icon -->
<Icon Id="icon.ico" SourceFile="..\icon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
<!-- Sets the default installation folder -->
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="!(bind.Property.ProductName)" />
</StandardDirectory>
<!-- Shows a UI that allows customizing the install location -->
<ui:WixUI Id="WixUI_CustomInstallDir" InstallDirectory="INSTALLFOLDER" />
<Feature Id="Main">
<ComponentGroupRef Id="MainComponent" />
<ComponentRef Id="StartMenuShortcut" />
<ComponentGroupRef Id="AssetsDirectory" />
<ComponentGroupRef Id="CreditsDirectory" />
</Feature>
<!-- Installs the actual files -->
<ComponentGroup Id="MainComponent" Directory="INSTALLFOLDER">
<Component>
<File Id="Executable" Source="..\..\..\target\dist\yachtpit.exe" Vital="true" />
</Component>
</ComponentGroup>
<!-- Start menu shortcut -->
<!-- Source: https://wixtoolset.org/docs/v3/howtos/files_and_registry/create_start_menu_shortcut/ -->
<StandardDirectory Id="ProgramMenuFolder">
<Component Id="StartMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut" Name="!(bind.Property.ProductName)" Target="[!Executable]" WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU" Key="Software\!(bind.Property.ProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
</Package>
</Wix>

View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
}
}

View File

@@ -0,0 +1,5 @@
# Credits
## Assets
* Bevy icon: [MIT License](licenses/Bevy_MIT_License.md);

View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>yachtpit</title>
<link data-trunk rel="copy-dir" href="assets"/>
<link data-trunk rel="copy-dir" href="credits"/>
<link data-trunk rel="copy-file" href="build/windows/icon.ico"/>
<link rel="icon" href="icon.ico">
<link data-trunk rel="inline" href="build/web/styles.css"/>
</head>
<body>
<link data-trunk rel="inline" href="build/web/sound.js"/>
<div class="game-container">
<div class="lds-dual-ring"></div>
<canvas id="yachtpit-canvas">
Javascript and support for canvas is required
</canvas>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
# Flag to notify the compiler we're building for the iOS simulator from an Apple silicon mac
# This needs some workarounds for now
# See https://github.com/bevyengine/bevy/pull/10178 - remove if it's not needed anymore.
[target.aarch64-apple-ios-sim]
rustflags = ["--cfg=ios_simulator"]

3
crates/yachtpit/mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
mobile.xcodeproj/xcuserdata/
mobile.xcodeproj/project.xcworkspace/
build/

View File

@@ -0,0 +1,34 @@
[package]
name = "mobile"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "mobile"
crate-type = ["staticlib", "cdylib"]
[dependencies]
yachtpit = { path = "../" }
bevy = { version = "0.16.0", default-features = false, features = ["android-native-activity"] }
[target."cfg(target_os = \"ios\")".dependencies]
objc2-avf-audio = { version = "0.3.0", features = [
"AVAudioSession",
"AVAudioSessionTypes",
] }
[package.metadata.android]
package = "io.gs.yachtpit"
apk_name = "yachtpit"
assets = "../assets"
strip = "strip"
resources = "../build/android/res"
build_targets = ["aarch64-linux-android"]
[package.metadata.android.sdk]
target_sdk_version = 35
[package.metadata.android.application]
icon = "@mipmap/icon"
label = "yachtpit"

View File

@@ -0,0 +1,28 @@
.PHONY: xcodebuild run install boot-sim generate clean
DEVICE = ${DEVICE_ID}
ifndef DEVICE_ID
DEVICE=$(shell xcrun simctl list devices 'iOS' | grep -v 'unavailable' | grep -v '^--' | grep -v '==' | head -n 1 | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
endif
run: install
xcrun simctl launch --console $(DEVICE) io.gs.yachtpit
boot-sim:
xcrun simctl boot $(DEVICE) || true
install: xcodebuild-simulator boot-sim
xcrun simctl install $(DEVICE) build/Build/Products/Debug-iphonesimulator/mobile.app
xcodebuild-simulator:
IOS_TARGETS=x86_64-apple-ios xcodebuild -scheme mobile -configuration Debug -derivedDataPath build -destination "id=$(DEVICE)"
xcodebuild-iphone:
IOS_TARGETS=aarch64-apple-ios xcodebuild -scheme mobile -configuration Debug -derivedDataPath build -arch arm64
xcodebuild-iphone-release:
IOS_TARGETS=aarch64-apple-ios xcodebuild -scheme mobile -configuration Release -derivedDataPath build -arch arm64
clean:
rm -r build
cargo clean

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# based on https://github.com/mozilla/glean/blob/main/build-scripts/xc-universal-binary.sh
set -eux
PATH=$PATH:$HOME/.cargo/bin
RELFLAG=
if [[ "$CONFIGURATION" != "Debug" ]]; then
RELFLAG="--profile dist"
fi
set -euvx
if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then
# Assume we're in Xcode, which means we're probably cross-compiling.
# In this case, we need to add an extra library search path for build scripts and proc-macros,
# which run on the host instead of the target.
# (macOS Big Sur does not have linkable libraries in /usr/lib/.)
export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}"
fi
# add homebrew bin path, as it's the most commonly used package manager on macOS
# this is needed for cmake on apple arm processors as it's not available by default
export PATH="$PATH:/opt/homebrew/bin"
IS_SIMULATOR=0
if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then
IS_SIMULATOR=1
fi
for arch in $ARCHS; do
case "$arch" in
x86_64)
if [ $IS_SIMULATOR -eq 0 ]; then
echo "Building for x86_64, but not a simulator build. What's going on?" >&2
exit 2
fi
# Intel iOS simulator
export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios"
cargo rustc --crate-type staticlib --lib $RELFLAG --target x86_64-apple-ios
;;
arm64)
if [ $IS_SIMULATOR -eq 0 ]; then
# Hardware iOS targets
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios
else
# M1 iOS simulator -- currently in Nightly only and requires to build `libstd`
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios-sim
fi
esac
done

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "icon_1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.1</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleVersion</key>
<string>0.1.1</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" fixedFrame="YES" image="bevy.png" translatesAutoresizingMaskIntoConstraints="NO" id="sic-lC-kjy">
<rect key="frame" x="113" y="346" width="164" height="153"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="bevy.png" width="152.33058166503906" height="152.33058166503906"/>
</resources>
</document>

View File

@@ -0,0 +1 @@
void main_rs(void);

View File

@@ -0,0 +1,6 @@
#import "bindings.h"
int main() {
main_rs();
return 0;
}

View File

@@ -0,0 +1,11 @@
android:
gradle: true
# this assets configuration is currently only used without gradle!
#assets:
# - "../assets/*"
icon: "ios-src/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png"
manifest:
package: "io.gs.yachtpit"
version_code: 1
application:
label: "yachtpit"

View File

@@ -0,0 +1,473 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
134866208A035F8615C99114 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96A1E5B62F48B379829E8A0D /* Metal.framework */; };
2469A4292A6F9AC200ACF4EF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2469A4282A6F9AC200ACF4EF /* Assets.xcassets */; };
2469A42B2A6FAC7000ACF4EF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */; };
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE71FBCAA714DB4F42459106 /* UIKit.framework */; };
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F1B41978FA53999AA836D0F /* Security.framework */; };
55892F1396056740E1AF9685 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AF7DE91055EBD05ED77E57F9 /* main.m */; };
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = A39528EB2CCB182F5328223A /* libc++.tbd */; };
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57CD6305253C7A940098CD4A /* AudioToolbox.framework */; };
57CD630E253C80EC0098CD4A /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 57CD630A253C7F5F0098CD4A /* assets */; };
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */; };
D4A53EFF2DDD2FF70035BC01 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8DBF1E2B5C613DA41701F6D9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D08AEBE0B1A9C9A7B8C7B33F;
remoteInfo = cargo_ios;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
160DB77300A3F1806F024D47 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
2469A4282A6F9AC200ACF4EF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "ios-src/Assets.xcassets"; sourceTree = SOURCE_ROOT; };
2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ios-src/LaunchScreen.storyboard; sourceTree = "<group>"; };
55EAC02897847195D2F44C15 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
57CD6305253C7A940098CD4A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
57CD630A253C7F5F0098CD4A /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../assets; sourceTree = "<group>"; };
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
8EE7F1E3B0303533925D7E33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
96A1E5B62F48B379829E8A0D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
9F1B41978FA53999AA836D0F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
A39528EB2CCB182F5328223A /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
AF7DE91055EBD05ED77E57F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
FE71FBCAA714DB4F42459106 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D5A822CB2D6847BA8800BE4C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */,
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */,
134866208A035F8615C99114 /* Metal.framework in Frameworks */,
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */,
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */,
D4A53EFF2DDD2FF70035BC01 /* AVFoundation.framework in Frameworks */,
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
321F7D6A765B38E746C35105 /* Products */ = {
isa = PBXGroup;
children = (
55EAC02897847195D2F44C15 /* mobile.app */,
);
name = Products;
sourceTree = "<group>";
};
4F1D6F28B8A5D1927AB0ADED /* ios-src */ = {
isa = PBXGroup;
children = (
2469A4282A6F9AC200ACF4EF /* Assets.xcassets */,
57CD630A253C7F5F0098CD4A /* assets */,
160DB77300A3F1806F024D47 /* bindings.h */,
8EE7F1E3B0303533925D7E33 /* Info.plist */,
AF7DE91055EBD05ED77E57F9 /* main.m */,
);
path = "ios-src";
sourceTree = "<group>";
};
8F2E3E6040EAD2EC9F3FA530 = {
isa = PBXGroup;
children = (
2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */,
4F1D6F28B8A5D1927AB0ADED /* ios-src */,
EB028409C2D0655412DA6E44 /* Frameworks */,
321F7D6A765B38E746C35105 /* Products */,
);
sourceTree = "<group>";
};
EB028409C2D0655412DA6E44 /* Frameworks */ = {
isa = PBXGroup;
children = (
D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */,
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */,
57CD6305253C7A940098CD4A /* AudioToolbox.framework */,
A39528EB2CCB182F5328223A /* libc++.tbd */,
96A1E5B62F48B379829E8A0D /* Metal.framework */,
9F1B41978FA53999AA836D0F /* Security.framework */,
FE71FBCAA714DB4F42459106 /* UIKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXLegacyTarget section */
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */ = {
isa = PBXLegacyTarget;
buildArgumentsString = build_rust_deps.sh;
buildConfigurationList = AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */;
buildPhases = (
FE045B3D04D57B713A565FF8 /* Sources */,
);
buildToolPath = /bin/sh;
buildWorkingDirectory = .;
dependencies = (
);
name = cargo_ios;
passBuildSettingsInEnvironment = 1;
productName = cargo_ios;
};
/* End PBXLegacyTarget section */
/* Begin PBXNativeTarget section */
3BDB8152E4962373181B4FE5 /* mobile */ = {
isa = PBXNativeTarget;
buildConfigurationList = E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "mobile" */;
buildPhases = (
9F13800790AD9DBC2BC0F116 /* Sources */,
D5A822CB2D6847BA8800BE4C /* Frameworks */,
57CD630D253C80E60098CD4A /* Resources */,
);
buildRules = (
);
dependencies = (
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */,
);
name = mobile;
productName = mobile;
productReference = 55EAC02897847195D2F44C15 /* mobile.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8DBF1E2B5C613DA41701F6D9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1240;
};
buildConfigurationList = 9D43D41707A5C30B227B83F9 /* Build configuration list for PBXProject "mobile" */;
compatibilityVersion = "Xcode 10.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 8F2E3E6040EAD2EC9F3FA530;
projectDirPath = "";
projectRoot = "";
targets = (
3BDB8152E4962373181B4FE5 /* mobile */,
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
57CD630D253C80E60098CD4A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2469A42B2A6FAC7000ACF4EF /* LaunchScreen.storyboard in Resources */,
2469A4292A6F9AC200ACF4EF /* Assets.xcassets in Resources */,
57CD630E253C80EC0098CD4A /* assets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
9F13800790AD9DBC2BC0F116 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
55892F1396056740E1AF9685 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE045B3D04D57B713A565FF8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */;
targetProxy = 26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
4AD7BC6FDD56FF18FA6DA7D7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
5B14EC4ADC81FBF1F8CF20E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_CXX_LANGUAGE_STANDARD = "c++11";
CLANG_CXX_LIBRARY = "libc++";
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios/dist",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios-sim/dist",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../target/x86_64-apple-ios/dist",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lmobile",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER=io.gs.yachtpit;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
79E3C28F06346EA58420A93D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
8265913A25816D964A847F1B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A2D5B73DD30D562B6F366526 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_CXX_LANGUAGE_STANDARD = "c++11";
CLANG_CXX_LIBRARY = "libc++";
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios-sim/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../target/x86_64-apple-ios/debug",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lmobile",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER=io.gs.yachtpit;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
FEA9B18D9236F9F6DC6DF799 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
9D43D41707A5C30B227B83F9 /* Build configuration list for PBXProject "mobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4AD7BC6FDD56FF18FA6DA7D7 /* Debug */,
79E3C28F06346EA58420A93D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8265913A25816D964A847F1B /* Debug */,
FEA9B18D9236F9F6DC6DF799 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "mobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A2D5B73DD30D562B6F366526 /* Debug */,
5B14EC4ADC81FBF1F8CF20E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 8DBF1E2B5C613DA41701F6D9 /* Project object */;
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,41 @@
use bevy::prelude::*;
use bevy::window::WindowMode;
use bevy::winit::WinitSettings;
use yachtpit::GamePlugin;
#[unsafe(no_mangle)]
unsafe extern "C" fn main_rs() {
main();
}
// this macro is a no-op on ios and only needed for anroid since bevy 0.16
// see https://github.com/bevyengine/bevy/pull/14780
#[bevy_main]
fn main() {
#[cfg(target_os = "ios")]
unsafe {
// Sets our audio session to Ambient mode to prevent background music from stopping.
// The default for iOS apps is SoloAmbient, which stops background music.
// See apple docs: https://developer.apple.com/documentation/avfaudio/avaudiosession/category-swift.struct/ambient
if let Err(e) = objc2_avf_audio::AVAudioSession::sharedInstance()
.setCategory_error(objc2_avf_audio::AVAudioSessionCategoryAmbient.unwrap())
{
println!("Error setting audio session category: {:?}", e);
}
}
App::new()
.insert_resource(WinitSettings::mobile())
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resizable: false,
mode: WindowMode::BorderlessFullscreen(MonitorSelection::Current),
..default()
}),
..default()
}),
GamePlugin,
))
.run();
}

View File

@@ -0,0 +1,16 @@
{
"name": "yachtpit",
"version": "1.0.0",
"description": "",
"main": "index.js",
"files": [
"../../dist"
],
"scripts": {
"clean": "../../cleanup.sh",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,35 @@
use bevy::prelude::{ButtonInput, KeyCode, Res};
pub enum GameControl {
Up,
Down,
Left,
Right,
}
impl GameControl {
pub fn pressed(&self, keyboard_input: &Res<ButtonInput<KeyCode>>) -> bool {
match self {
GameControl::Up => {
keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp)
}
GameControl::Down => {
keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown)
}
GameControl::Left => {
keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft)
}
GameControl::Right => {
keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight)
}
}
}
}
pub fn get_movement(control: GameControl, input: &Res<ButtonInput<KeyCode>>) -> f32 {
if control.pressed(input) {
1.0
} else {
0.0
}
}

View File

@@ -0,0 +1,44 @@
use bevy::prelude::*;
use crate::core::actions::game_control::{get_movement, GameControl};
use crate::GameState;
mod game_control;
pub const FOLLOW_EPSILON: f32 = 5.;
pub struct ActionsPlugin;
// This plugin listens for keyboard input and converts the input into Actions.
// Actions can then be used as a resource in other systems to act on the player input.
impl Plugin for ActionsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Actions>().add_systems(
Update,
set_movement_actions.run_if(in_state(GameState::Playing)),
);
}
}
#[derive(Default, Resource)]
pub struct Actions {
pub player_movement: Option<Vec2>,
}
pub fn set_movement_actions(
mut actions: ResMut<Actions>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let player_movement = Vec2::new(
get_movement(GameControl::Right, &keyboard_input)
- get_movement(GameControl::Left, &keyboard_input),
get_movement(GameControl::Up, &keyboard_input)
- get_movement(GameControl::Down, &keyboard_input),
);
if player_movement != Vec2::ZERO {
actions.player_movement = Some(player_movement.normalize());
} else {
actions.player_movement = None;
}
}

View File

@@ -0,0 +1,5 @@
pub mod actions;
pub mod system_manager;
pub use actions::ActionsPlugin;
pub use system_manager::{SystemManagerPlugin};

View 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");
}
}

View File

@@ -0,0 +1,59 @@
#![allow(clippy::type_complexity)]
mod core;
mod ui;
use bevy::app::App;
#[cfg(debug_assertions)]
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
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 game uses States to separate logic
// See https://bevy-cheatbook.github.io/programming/states.html
// Or https://github.com/bevyengine/bevy/blob/main/examples/ecs/state.rs
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
enum GameState {
// During the loading State the LoadingPlugin will load our assets
#[default]
Loading,
// During this State the actual game logic is executed
Playing,
// Here the menu is drawn and waiting for player interaction
Menu,
}
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 {
fn build(&self, app: &mut App) {
app.init_state::<GameState>().add_plugins((
LoadingPlugin,
MenuPlugin,
ActionsPlugin,
SystemManagerPlugin,
PlayerPlugin,
))
.add_systems(OnEnter(GameState::Playing), (setup_instrument_cluster, initialize_yacht_systems));
#[cfg(debug_assertions)]
{
app.add_plugins((
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
));
}
}
}

View File

@@ -0,0 +1,60 @@
// disable console on windows for release builds
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use bevy::asset::AssetMetaCheck;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy::winit::WinitWindows;
use bevy::DefaultPlugins;
use yachtpit::GamePlugin;
use std::io::Cursor;
use winit::window::Icon;
fn main() {
App::new()
.insert_resource(ClearColor(Color::NONE))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
// Bind to canvas included in `index.html`
canvas: Some("#yachtpit-canvas".to_owned()),
fit_canvas_to_parent: true,
// Tells wasm not to override default event handling, like F5 and Ctrl+R
prevent_default_event_handling: false,
..default()
}),
..default()
})
.set(AssetPlugin {
meta_check: AssetMetaCheck::Never,
..default()
}),
)
.add_plugins(GamePlugin)
.add_systems(Startup, set_window_icon)
.run();
}
// Sets the icon on windows and X11
fn set_window_icon(
windows: NonSend<WinitWindows>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) -> Result {
let primary_entity = primary_window.single()?;
let Some(primary) = windows.get_window(primary_entity) else {
return Err(BevyError::from("No primary window!"));
};
let icon_buf = Cursor::new(include_bytes!(
"../build/macos/AppIcon.iconset/icon_256x256.png"
));
if let Ok(image) = image::load(icon_buf, image::ImageFormat::Png) {
let image = image.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
let icon = Icon::from_rgba(rgba, width, height).unwrap();
primary.set_window_icon(Some(icon));
};
Ok(())
}

View 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>,
// }

View File

@@ -0,0 +1,203 @@
use crate::GameState;
use bevy::prelude::*;
pub struct MenuPlugin;
/// This plugin is responsible for the game menu (containing only one button...)
/// The menu is only drawn during the State `GameState::Menu` and is removed when that state is exited
impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::Menu), setup_menu)
.add_systems(Update, click_play_button.run_if(in_state(GameState::Menu)))
.add_systems(OnExit(GameState::Menu), cleanup_menu);
}
}
#[derive(Component)]
struct ButtonColors {
normal: Color,
hovered: Color,
}
impl Default for ButtonColors {
fn default() -> Self {
ButtonColors {
normal: Color::linear_rgb(0.15, 0.15, 0.15),
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
}
}
}
#[derive(Component)]
struct Menu;
fn setup_menu(mut commands: Commands) {
info!("menu");
commands.spawn((Camera2d, Msaa::Off));
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
Menu,
))
.with_children(|children| {
let button_colors = ButtonColors::default();
children
.spawn((
Button,
Node {
width: Val::Px(140.0),
height: Val::Px(50.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()
},
BackgroundColor(button_colors.normal),
button_colors,
ChangeState(GameState::Playing),
))
.with_child((
Text::new("Play"),
TextFont {
font_size: 40.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
});
commands
.spawn((
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
bottom: Val::Px(5.),
width: Val::Percent(100.),
position_type: PositionType::Absolute,
..default()
},
Menu,
))
.with_children(|children| {
children
.spawn((
Button,
Node {
width: Val::Px(170.0),
height: Val::Px(50.0),
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(5.)),
..Default::default()
},
BackgroundColor(Color::NONE),
ButtonColors {
normal: Color::NONE,
..default()
},
OpenLink("https://bevyengine.org"),
))
.with_children(|parent| {
parent.spawn((
Text::new("Made with Bevy"),
TextFont {
font_size: 15.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
parent.spawn((
Node {
width: Val::Px(32.),
..default()
},
));
});
children
.spawn((
Button,
Node {
width: Val::Px(170.0),
height: Val::Px(50.0),
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(5.)),
..default()
},
BackgroundColor(Color::NONE),
ButtonColors {
normal: Color::NONE,
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
},
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
))
.with_children(|parent| {
parent.spawn((
Text::new("Open source"),
TextFont {
font_size: 15.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
parent.spawn((
Node {
width: Val::Px(32.),
..default()
},
));
});
});
}
#[derive(Component)]
struct ChangeState(GameState);
#[derive(Component)]
struct OpenLink(&'static str);
fn click_play_button(
mut next_state: ResMut<NextState<GameState>>,
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&ButtonColors,
Option<&ChangeState>,
Option<&OpenLink>,
),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
if let Some(state) = change_state {
next_state.set(state.0.clone());
} else if let Some(link) = open_link {
if let Err(error) = webbrowser::open(link.0) {
warn!("Failed to open link {error:?}");
}
}
}
Interaction::Hovered => {
*color = button_colors.hovered.into();
}
Interaction::None => {
*color = button_colors.normal.into();
}
}
}
}
fn cleanup_menu(mut commands: Commands, menu: Query<Entity, With<Menu>>) {
for entity in menu.iter() {
commands.entity(entity).despawn();
}
}

View File

@@ -0,0 +1,5 @@
pub mod loading;
pub mod menu;
pub use loading::LoadingPlugin;
pub use menu::MenuPlugin;