diff --git a/asset_sources/folder.svg b/asset_sources/folder.svg new file mode 100644 index 0000000..fef7ab5 --- /dev/null +++ b/asset_sources/folder.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/asset_sources/gear.svg b/asset_sources/gear.svg new file mode 100644 index 0000000..ac606ec --- /dev/null +++ b/asset_sources/gear.svg @@ -0,0 +1,56 @@ + + + + Sketch1_adj.dxf - scale = 1.000000, origin = (0.000000, 0.000000), method = manual + + + + + + diff --git a/assets/fonts/ChakraPetch-Regular-PixieWrangler.ttf b/assets/fonts/ChakraPetch-Regular-PixieWrangler.ttf index ae2b59d..7130e3b 100644 Binary files a/assets/fonts/ChakraPetch-Regular-PixieWrangler.ttf and b/assets/fonts/ChakraPetch-Regular-PixieWrangler.ttf differ diff --git a/src/main.rs b/src/main.rs index bba69cd..88eb705 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,12 +96,12 @@ fn main() { )); app.init_state::(); + app.enable_state_scoped_entities::(); app.add_systems( OnEnter(GameState::Playing), (reset_game, spawn_level, spawn_game_ui).chain(), ); - app.add_systems(OnExit(GameState::Playing), playing_exit_system); app.configure_sets(Update, DrawingInput.run_if(in_state(GameState::Playing))); app.add_systems( @@ -562,12 +562,15 @@ fn pixie_button_system( let mut timer = Timer::from_seconds(duration * *count as f32, TimerMode::Repeating); timer.set_elapsed(Duration::from_secs_f32((*i + 1) as f32 * duration)); - commands.spawn(PixieEmitter { - flavor: *flavor, - path: world_path.clone(), - remaining: pixies, - timer, - }); + commands.spawn(( + PixieEmitter { + flavor: *flavor, + path: world_path.clone(), + remaining: pixies, + timer, + }, + StateScoped(GameState::Playing), + )); *i += 1; } @@ -679,9 +682,11 @@ fn draw_mouse_system( ShapeBuilder::with(&shape).stroke((color, 2.0)).build(), Transform::from_translation(mouse_snapped.0.extend(layer::CURSOR)), Cursor, + StateScoped(GameState::Playing), )); } + // TODO move this bit to a separate system in road_drawing.rs if !line_drawing.is_changed() { return; } @@ -704,6 +709,7 @@ fn draw_mouse_system( .build(), Transform::from_xyz(0.0, 0.0, layer::ROAD_OVERLAY), DrawingLine, + StateScoped(GameState::Playing), )); } } @@ -851,6 +857,7 @@ fn spawn_road_segment( .build(), Transform::from_xyz(0.0, 0.0, layer::ROAD - segment.layer as f32), segment.clone(), + StateScoped(GameState::Playing), )) .with_children(|parent| { parent.spawn(( @@ -890,6 +897,7 @@ fn spawn_obstacle(commands: &mut Commands, obstacle: &Obstacle) { .fill(theme::OBSTACLE) .build(), Transform::from_translation(origin.extend(layer::OBSTACLE)), + StateScoped(GameState::Playing), )) .with_children(|parent| { parent.spawn(( @@ -942,6 +950,7 @@ fn spawn_name( TextColor(theme::LEVEL_NAME.into()), Anchor::TopLeft, Transform::from_translation((name_position + Vec2::new(8., -8.)).extend(layer::GRID)), + StateScoped(GameState::Playing), )); } @@ -965,6 +974,7 @@ fn spawn_terminus( .build(), Transform::from_translation(terminus.point.extend(layer::TERMINUS)), terminus.clone(), + StateScoped(GameState::Playing), )) .with_children(|parent| { parent.spawn((Collider::Point(terminus.point), ColliderLayer(1))); @@ -1176,15 +1186,6 @@ fn update_elapsed_text_system( } } -fn playing_exit_system( - mut commands: Commands, - query: Query, Without)>, -) { - for entity in query.iter() { - commands.entity(entity).despawn(); - } -} - fn save_solution_system( query: Query<&RoadSegment>, graph: Res, @@ -1236,6 +1237,7 @@ fn spawn_level( .build(), Transform::from_xyz(x as f32, y as f32, layer::GRID), GridPoint, + StateScoped(GameState::Playing), )); } } @@ -1308,14 +1310,18 @@ fn spawn_game_ui( let mut tool_button_ids = vec![]; commands - .spawn(Node { - width: Val::Percent(100.), - height: Val::Percent(100.), - flex_direction: FlexDirection::ColumnReverse, - justify_content: JustifyContent::FlexStart, - align_items: AlignItems::Center, - ..default() - }) + .spawn(( + Name::new("GameUiRoot"), + Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + flex_direction: FlexDirection::ColumnReverse, + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Center, + ..default() + }, + StateScoped(GameState::Playing), + )) .with_children(|parent| { // bottom bar parent diff --git a/src/net_ripping.rs b/src/net_ripping.rs index 678f5b1..a6765c1 100644 --- a/src/net_ripping.rs +++ b/src/net_ripping.rs @@ -11,8 +11,8 @@ use crate::{ collision::{point_segment_collision, PointCollision}, layer, sim::SimulationState, - Collider, ColliderLayer, DrawingInteraction, DrawingMouseMovement, MouseSnappedPos, RoadGraph, - RoadSegment, SegmentGraphNodes, SelectedTool, Tool, + Collider, ColliderLayer, DrawingInteraction, DrawingMouseMovement, GameState, MouseSnappedPos, + RoadGraph, RoadSegment, SegmentGraphNodes, SelectedTool, Tool, }; pub struct NetRippingPlugin; @@ -159,6 +159,7 @@ fn draw_net_ripping_system( .build(), Transform::from_xyz(0.0, 0.0, layer::ROAD_OVERLAY), RippingLine, + StateScoped(GameState::Playing), )); } } diff --git a/src/pixie.rs b/src/pixie.rs index 45c78de..74ee698 100644 --- a/src/pixie.rs +++ b/src/pixie.rs @@ -170,6 +170,7 @@ pub fn explode_pixies_system(mut commands: Commands, query: Query<(Entity, &Pixi direction: Vec2::new(cos, sin), ..default() }, + StateScoped(GameState::Playing), )); } } @@ -518,6 +519,7 @@ pub fn emit_pixies_system(mut q_emitters: Query<&mut PixieEmitter>, mut commands path_index: 0, ..default() }, + StateScoped(GameState::Playing), )); emitter.remaining -= 1; diff --git a/src/ui/level_select.rs b/src/ui/level_select.rs index f11606a..609d87b 100644 --- a/src/ui/level_select.rs +++ b/src/ui/level_select.rs @@ -1,4 +1,5 @@ use crate::{level::Level, loading::NUM_LEVELS, save::BestScores, theme, GameState, Handles}; + use bevy::prelude::*; pub struct LevelSelectPlugin; @@ -6,6 +7,10 @@ pub struct LevelSelectPlugin; pub struct LevelSelectScreen; #[derive(Component)] pub struct LevelSelectButton(u32); +#[derive(Component)] +struct SettingsPanelBody; +#[derive(Component)] +struct LevelsPanelBody; impl Plugin for LevelSelectPlugin { fn build(&self, app: &mut App) { @@ -13,14 +18,19 @@ impl Plugin for LevelSelectPlugin { app.add_systems( Update, - (level_select_update, level_select_button_system) - .run_if(in_state(GameState::LevelSelect)), + (level_select_button_system).run_if(in_state(GameState::LevelSelect)), ); app.add_systems(OnExit(GameState::LevelSelect), level_select_exit); + + // TODO these are not firing when re-entering GameState::LevelSelect?? + app.add_observer(populate_settings_panel_body); + app.add_observer(populate_levels_panel_body); } } +// TODO add "diagonal scrolling grid" background + fn level_select_button_system( query: Query<(&Interaction, &LevelSelectButton), Changed>, mut next_state: ResMut>, @@ -43,46 +53,63 @@ fn level_select_button_system( } } -fn level_select_enter( - mut commands: Commands, - best_scores: Res, - handles: Res, - levels: Res>, -) { +fn level_select_enter(mut commands: Commands, best_scores: Res, handles: Res) { let total_score: u32 = best_scores.0.iter().map(|(_, v)| v).sum(); - commands + let root = commands .spawn(( Node { - width: Val::Percent(100.), - height: Val::Percent(100.), + width: Val::Percent(100.0), + height: Val::Percent(100.0), flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - justify_content: JustifyContent::SpaceEvenly, + overflow: Overflow::clip(), ..default() }, LevelSelectScreen, )) + .id(); + + let bottom_bar = commands + .spawn(( + Node { + width: Val::Percent(100.), + flex_shrink: 0.0, + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceBetween, + padding: UiRect { + left: Val::Px(20.), + right: Val::Px(20.), + top: Val::Px(10.), + bottom: Val::Px(10.), + }, + ..default() + }, + BackgroundColor(theme::UI_PANEL_BACKGROUND.into()), + )) .with_children(|parent| { + parent.spawn(( + Node { + align_self: AlignSelf::Center, + ..default() + }, + Text::new("₽IXIE WRANGLER"), + TextFont { + font: handles.fonts[0].clone(), + font_size: 25.0, + ..default() + }, + TextColor(theme::PIXIE[1].into()), + )); + // Right side of top bar parent .spawn(Node { - flex_direction: FlexDirection::Column, + align_items: AlignItems::FlexStart, + justify_content: JustifyContent::Center, + column_gap: Val::Px(10.), ..default() }) .with_children(|parent| { - parent.spawn(( - Node { - align_self: AlignSelf::Center, - ..default() - }, - Text::new("₽IXIE WRANGLER"), - TextFont { - font: handles.fonts[0].clone(), - font_size: 50.0, - ..default() - }, - TextColor(theme::PIXIE[1].into()), - )); parent.spawn(( Node { align_self: AlignSelf::Center, @@ -96,121 +123,190 @@ fn level_select_enter( }, TextColor(theme::FINISHED_ROAD[1].into()), )); + // TODO add total star count + // TODO clock for flavor? }); + }) + .id(); - let cols = (NUM_LEVELS as f32 / 3.).ceil() as u16; + let main_content = commands + .spawn((Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + padding: UiRect::all(Val::Px(20.)), + column_gap: Val::Px(20.), + display: Display::Grid, + grid_template_columns: vec![GridTrack::flex(0.75), GridTrack::flex(0.25)], + ..default() + },)) + .id(); - parent - .spawn(Node { - display: Display::Grid, - grid_template_rows: RepeatedGridTrack::auto(3), - grid_template_columns: RepeatedGridTrack::auto(cols), - row_gap: Val::Px(10.), - column_gap: Val::Px(10.), + let settings_panel = commands + .spawn(panel( + "\u{01a9} SETTINGS", + &handles, + Node::default(), + SettingsPanelBody, + )) + .id(); + + let levels_panel = commands + .spawn(panel( + "\u{0393} /user/levels", + &handles, + Node { + column_gap: Val::Px(10.), + row_gap: Val::Px(10.), + flex_wrap: FlexWrap::Wrap, + align_content: AlignContent::FlexStart, + ..default() + }, + LevelsPanelBody, + )) + .id(); + + commands + .entity(main_content) + .add_children(&[levels_panel, settings_panel]); + + commands + .entity(root) + .add_children(&[main_content, bottom_bar]); +} + +fn panel( + title: impl Into, + handles: &Handles, + body_node: Node, + body_marker: M, +) -> impl Bundle { + ( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + flex_direction: FlexDirection::Column, + overflow: Overflow::hidden(), + ..default() + }, + Name::new("Panel"), + Children::spawn(( + Spawn(( + Name::new("PanelTitle"), + Node { + padding: UiRect { + left: Val::Px(20.), + right: Val::Px(20.), + top: Val::Px(10.), + bottom: Val::Px(10.), + }, + align_self: AlignSelf::FlexStart, ..default() - }) - .with_children(|parent| { - for i in 1..=NUM_LEVELS { - parent - .spawn(( - Button, - Node { - width: Val::Px(150.), - height: Val::Px(150.), - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), - LevelSelectButton(i), - )) - .with_children(|parent| { - let level = handles - .levels - .get(i as usize - 1) - .and_then(|h| levels.get(h)); - - let level_color = match level { - Some(_) => theme::UI_LABEL, - None => theme::UI_LABEL_BAD, - }; - - let (score_text, star_text_one, star_text_two) = - if let (Some(score), Some(level)) = - (best_scores.0.get(&i), level) - { - let stars = level - .star_thresholds - .iter() - .filter(|t| **t <= *score) - .count(); - - ( - format!("Æ{score}"), - "★".repeat(stars), - "★".repeat(3 - stars), - ) - } else { - ("".to_string(), "".to_string(), "".to_string()) - }; - - parent - .spawn(( - Text::default(), - // See Bevy#16521 - TextFont { - font: handles.fonts[0].clone(), - ..default() - }, - )) - .with_children(|parent| { - parent.spawn(( - TextSpan::new(star_text_one), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_LABEL.into()), - )); - parent.spawn(( - TextSpan::new(star_text_two), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_LABEL_MUTED.into()), - )); - }); - - parent.spawn(( - Text::new(format!("{i}")), - TextFont { - font: handles.fonts[0].clone(), - font_size: 50.0, - ..default() - }, - TextColor(level_color.into()), - )); - - parent.spawn(( - Text::new(score_text), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::FINISHED_ROAD[1].into()), - )); - }); - } - }); - }); + }, + BackgroundColor(theme::UI_NORMAL_BUTTON.into()), + Children::spawn(Spawn(( + Text::new(title), + TextFont { + font: handles.fonts[0].clone(), + font_size: 25.0, + ..default() + }, + TextColor(theme::UI_LABEL.into()), + ))), + )), + Spawn(( + Name::new("PanelBody"), + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + padding: UiRect::all(Val::Px(10.)), + overflow: Overflow::scroll_y(), + ..body_node + }, + BackgroundColor(theme::UI_PANEL_BACKGROUND.into()), + body_marker, + )), + )), + ) } -fn level_select_update() {} +fn level_item( + level: &Level, + level_index: u32, + best_scores: &BestScores, + font_handle: &Handle, +) -> impl Bundle { + let (score_text, star_text_one, star_text_two) = + if let Some(score) = best_scores.0.get(&level_index) { + let stars = level + .star_thresholds + .iter() + .filter(|t| **t <= *score) + .count(); + + ( + format!("Æ{score}"), + "★".repeat(stars), + "★".repeat(3 - stars), + ) + } else { + ("".to_string(), "".to_string(), "".to_string()) + }; + + // TODO display level name + + ( + Button, + Name::new("LevelItem"), + Node { + width: Val::Px(150.), + height: Val::Px(150.), + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(theme::UI_NORMAL_BUTTON.into()), + LevelSelectButton(level_index), + Children::spawn(( + Spawn(( + Text::new(star_text_one), + TextFont { + font: font_handle.clone(), + font_size: 25.0, + ..default() + }, + TextColor(theme::UI_LABEL.into()), + Children::spawn(Spawn(( + TextSpan::new(star_text_two), + TextFont { + font: font_handle.clone(), + font_size: 25.0, + ..default() + }, + TextColor(theme::UI_LABEL_MUTED.into()), + ))), + )), + Spawn(( + Text::new(format!("{level_index}")), + TextFont { + font: font_handle.clone(), + font_size: 50.0, + ..default() + }, + TextColor(theme::UI_LABEL.into()), + )), + Spawn(( + Text::new(score_text), + TextFont { + font: font_handle.clone(), + font_size: 25.0, + ..default() + }, + TextColor(theme::FINISHED_ROAD[1].into()), + )), + )), + ) +} fn level_select_exit( mut commands: Commands, @@ -224,3 +320,40 @@ fn level_select_exit( mouse.reset(MouseButton::Left); mouse.clear(); } + +fn populate_settings_panel_body( + trigger: Trigger, + mut commands: Commands, + handles: Res, +) { + commands.entity(trigger.target()).with_child(( + Text::new("There aren't any settings yet! Soon!"), + TextFont::from_font(handles.fonts[0].clone()), + )); +} + +fn populate_levels_panel_body( + trigger: Trigger, + mut commands: Commands, + handles: Res, + best_scores: Res, + levels: Res>, +) { + for level_index in 1..=NUM_LEVELS { + let Some(handle) = handles.levels.get(level_index as usize - 1) else { + warn!("No level handle for level {level_index}"); + continue; + }; + let Some(level) = levels.get(handle) else { + warn!("No level asset for level {level_index}"); + continue; + }; + + commands.entity(trigger.target()).with_child(level_item( + level, + level_index, + &best_scores, + &handles.fonts[0], + )); + } +} diff --git a/src/ui/score_dialog.rs b/src/ui/score_dialog.rs index c3aeae6..9b057eb 100644 --- a/src/ui/score_dialog.rs +++ b/src/ui/score_dialog.rs @@ -95,6 +95,7 @@ fn show_score_dialog_system( ), BackgroundColor(theme::UI_PANEL_BACKGROUND.into()), ScoreDialog, + StateScoped(GameState::Playing), )) .with_children(|parent| { parent.spawn(Text::default()).with_children(|parent| {