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 @@
+
+
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| {