From 2295de00435792bdff7e6ed74e2ff3a0ea4aba7d Mon Sep 17 00:00:00 2001 From: "Elijah C. Voigt" Date: Sun, 5 May 2024 12:36:37 -0700 Subject: [PATCH] Loading is borked, but tutorial worked... --- assets/martian.tweak.toml | 69 ++++++++++++++++++++++++++++++------- src/display3d.rs | 72 +++++++++++++++++++-------------------- src/game.rs | 54 ++++++++++++++++++----------- src/loading.rs | 5 +-- src/menu.rs | 7 ++-- src/tutorial.rs | 65 +++++++++++++++++++---------------- 6 files changed, 171 insertions(+), 101 deletions(-) diff --git a/assets/martian.tweak.toml b/assets/martian.tweak.toml index c92d8f4..3fadfca 100644 --- a/assets/martian.tweak.toml +++ b/assets/martian.tweak.toml @@ -83,21 +83,34 @@ But first things first! How do you play the dang game? """, ] objective = [ - "The Goal of Martian Chess is to have more points than your opponent when the game ends.", - "You get points by capturing your opponent's pieces.", - "The game ends when one player has no more pieces in their zone.", +""" +The Goal of Martian Chess is to have more points than your opponent when the game ends. +""", +""" +You get points by capturing your opponent's pieces. +""", +""" +The game ends when one player has no more pieces in their zone. +""", ] ownership = [ """ -This here line is called the canal. And blow me down if she aint a beaut! See herein lies the kicker of Martian warfare: You control any and only the pieces on your side of the canal. When you move a piece across the canal, your opponent assumes control over it. +This here line down the middle is called the canal. And blow me down if she aint a beaut! See herein lies the kicker of Martian warfare: You control any and only the pieces on your side of the canal. When you move a piece across the canal, your opponent assumes control over it. +""", +""" +Keep playing and try to score some points! """, -"Keep playing and try to score some points!", ] promotions = [ """ -Oh and one last thing: real nerds occasionally employ the field promotions strategy. Here's how it works: If you control no drones, you may combine two pawns to make a drone. Similarly, if you control no Queens, you may combine two drones to make a queen. +Real nerds occasionally employ the field promotions strategy. Here's how it works: If you control no drones, you may combine two pawns to make a drone. Similarly, if you control no Queens, you may combine a drone and a pawn to make a queen. """, ] +undo = [ +""" +One last thing: no take-backsies. You cannot 'undo' a piece's last move if it crossed the canal. +""" +] outro = [ """ Ok I gotta go now, but You now know enough to prevent human extinction. Do you want to finish this practice round? @@ -105,12 +118,44 @@ Ok I gotta go now, but You now know enough to prevent human extinction. Do you w ] [tutorial.pieces] -prompt = ["Try picking up a piece"] -pawn = ["Pawn. The Pawn is worth 1 point, and moves 1 space diagonally. This thing ain't worth your time. Put it down."] -drone = ["Drone. The Drone is worth 2 points, and moves 1 or 2 spaces horizontally or vertically. Neat. Back on the board."] -queen = ["Queen. The Queen is worth 3 Points, and moves any distance in a straight line. Woah, watch where you point that thing. Best put it back down."] -jumping = ["Note that jumping is not allowed. Martians banned any and all jumping in their society long ago."] -end = ["Ok, now move some pieces somewheres!"] +prompt = [ +""" +Try picking up a piece +""" +] +pawn = [ +""" +Pawn. + +A captured Pawn is worth 1 point, and moves 1 space diagonally. This thing ain't worth your time. Put it down. +""", +] +drone = [ +""" +Drone. + +A captured Drone is worth 2 points, and moves 1 or 2 spaces horizontally or vertically. Neat. Back on the board. +""" +] +queen = [ +""" +Queen. + +A captured Queen is worth 3 Points, and moves any distance in a straight line. Woah, watch where you point that thing. Best put it back down. +""" +] +jumping = [ +""" +Note that jumping over pieces is not allowed. + +Martians banned any and all jumping in their society long ago. +""" +] +end = [ +""" +Ok, now move some pieces somewheres! +""" +] [resolution] diff --git a/src/display3d.rs b/src/display3d.rs index cd8dadf..9c56a7a 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -22,9 +22,8 @@ impl Plugin for Display3dPlugin { .add_systems( OnExit(GameState::Loading), ( - fix_skybox.before(initialize), - initialize, - update_tweaks.run_if(resource_exists::), + fix_skybox, + initialize.after(fix_skybox), ), ) // Systems related to color and camera @@ -84,6 +83,7 @@ impl Plugin for Display3dPlugin { select .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)) + .run_if(not(in_state(MenuState::On))) .run_if(just_pressed(MouseButton::Left)), pick_up.run_if(any_component_added::()), put_down.run_if(any_component_removed::()), @@ -221,7 +221,7 @@ fn load_assets( /// Initialize the board and pieces fn initialize(mut commands: Commands, board: Res, assets: Res) { - info!("Initializing root"); + debug!("Initializing root"); commands .spawn(( SpatialBundle { @@ -232,7 +232,7 @@ fn initialize(mut commands: Commands, board: Res, assets: Res("display3d_models_skybox_file") .unwrap(); @@ -339,7 +339,7 @@ fn hydrate_camera( .get::("display3d_environment_map_light_intensity") .unwrap(); - info!("Hydrating camera {:?}", entity); + debug!("Hydrating camera {:?}", entity); // Populate the components for the camera commands.entity(entity).insert(( DisplayState::Display3d, @@ -446,7 +446,7 @@ fn fix_skybox( .unwrap(); let image = images.get_mut(handle).unwrap(); - info!("Loaded skybox image"); + debug!("Loaded skybox image"); // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture, // so they appear as one texture. The following code reconfigures the texture as necessary. if image.texture_descriptor.array_layer_count() == 1 { @@ -470,7 +470,7 @@ fn set_board_model( .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); boards.iter_mut().for_each(|mut handle| { - info!("Setting board model"); + debug!("Setting board model"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); @@ -503,7 +503,7 @@ fn set_title_model( titles .iter_mut() .for_each(|(mut handle, mut transform, mut visibility)| { - info!("Setting title model"); + debug!("Setting title model"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); @@ -546,7 +546,7 @@ fn board_translation(&BoardIndex { x, y }: &BoardIndex) -> Vec3 { } fn capture_translation(side: &Side, num: usize) -> Vec3 { - info!("Side: {:?} Num: {:?}", side, num); + debug!("Side: {:?} Num: {:?}", side, num); let x = 5.0 - ((num % 4) as f32 * 1.3); let y = -1.3; let z = 4.0 + ((num / 4) as f32 * 1.3); @@ -634,7 +634,7 @@ fn update_pieces( // Set position of piece let new_translation = board_translation(board_index); if transform.translation != new_translation { - info!("Updating piece transform"); + debug!("Updating piece transform"); transform.translation = new_translation; } @@ -644,7 +644,7 @@ fn update_pieces( .count(); if animating > 0 { - info!("Piece {:?} is animating. Skipping...", entity); + debug!("Piece {:?} is animating. Skipping...", entity); } else { debug!("Checking piece object scene for {:?}", entity); @@ -679,7 +679,6 @@ fn select( selected: Query>, state: Res>, ) { - // info!("Board index selected: {:?}", board_index); match *piece { PiecePointer(Some(e)) => { query.get(e).iter().for_each(|(board_index, side)| { @@ -799,7 +798,7 @@ fn set_valid_move_model( .get_handle::("display3d_models_assets_file") .unwrap(); if let Some(gltf) = gltfs.get(assets_handle) { - info!("Setting valid move model"); + debug!("Setting valid move model"); events.iter_mut().for_each(|(mut handle, mut visibility)| { *handle = gltf @@ -834,19 +833,19 @@ fn pick_up( .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); events.iter_mut().for_each(|(entity, piece)| { - info!("Picking up piece"); + debug!("Picking up piece"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); - info!("Pickup animation for {:?}", entity); + debug!("Pickup animation for {:?}", entity); children.iter_descendants(entity).for_each(|child| { - info!(" Child: {:?}", child); + debug!(" Child: {:?}", child); if let Ok((name, mut player)) = players.get_mut(child) { let pickup_animation = format!("display3d_models_animations_pick_up_{}", name.as_str(),); - info!( + debug!( "Picking up {:?} ({:?}) {:?} {:?}", name, entity, piece, pickup_animation ); @@ -905,7 +904,7 @@ fn put_down( .expect("Load tweakfile"); events.read().for_each(|entity| { if let Ok(_) = query.get_mut(entity) { - info!("Putting down piece"); + debug!("Putting down piece"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") @@ -913,7 +912,7 @@ fn put_down( let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); children.iter_descendants(entity).for_each(|child| { if let Ok((name, mut player)) = players.get_mut(child) { - info!("Putting down {:?}", entity); + debug!("Putting down {:?}", entity); let putdown_animation = format!("display3d_models_animations_put_down_{}", name.as_str()); let putdown_handle = gltf @@ -927,7 +926,7 @@ fn put_down( .expect("PutDown Animation"); if let Some(putdown_clip) = clips.get(putdown_handle) { if putdown_clip.compatible_with(name) { - info!("Compatible with put-down clip!"); + debug!("Compatible with put-down clip!"); player .start_with_transition( putdown_handle.clone(), @@ -935,20 +934,20 @@ fn put_down( ) .set_repeat(RepeatAnimation::Never); } else { - info!( + debug!( "Clip {:?}({:?}) not compatible with {:?}", putdown_animation, putdown_clip, name ); } } else { - info!("Clip not found"); + debug!("Clip not found"); } } else { - info!("Player not found"); + debug!("Player not found"); } }) } else { - info!("Piece+Side not found for entity"); + debug!("Piece+Side not found for entity"); } }) } @@ -957,7 +956,7 @@ fn set_tile_hitbox( mut events: Query<(&mut Transform, &BoardIndex), (With, Added)>, ) { events.iter_mut().for_each(|(mut transform, index)| { - info!("Setting tile hitbox"); + debug!("Setting tile hitbox"); *transform = Transform::from_translation(board_translation(index)); }); @@ -965,7 +964,7 @@ fn set_tile_hitbox( fn opening_animation(mut players: Query<&mut AnimationPlayer, (With, With)>) { players.iter_mut().for_each(|mut player| { - info!("Playing intro camera animation"); + debug!("Playing intro camera animation"); player.resume() }); } @@ -1009,7 +1008,7 @@ fn switch_sides( .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); players.iter_mut().for_each(|mut player| { - info!("Switching sides"); + debug!("Switching sides"); let animation = match state.get() { game::TurnState(game::Side::A) => gltf.named_animations.get( @@ -1072,7 +1071,7 @@ fn setup_dissolve_materials( .filter(|(child, _, _)| query.iter_many(parents.iter_ancestors(*child)).count() > 0) // Handle this entity (mesh) .for_each(|(child, std_handle, name)| { - info!("Setting up dissolve material for {:?} {:?}", name, child); + debug!("Setting up dissolve material for {:?} {:?}", name, child); // Get dissolvable data for percentage start let dissolvable = query @@ -1138,7 +1137,7 @@ fn capture_piece( // Wait for fade-out animation // If all pieces are done dissolving if dissolving.is_empty() { - info!("Nothing dissolving, moving on to next step!"); + debug!("Nothing dissolving, moving on to next step!"); // Move to next state now that animation is done *state = s.next(); // This takes effect after updating all children @@ -1150,7 +1149,7 @@ fn capture_piece( .expect("Visibility and Transform of captured piece"); // HACK: This is dirty. Why side, but score.captures(!side)? - info!( + debug!( "Capture translation: ({:?}, {:?}) {:?}", side, score.captures(!*side).saturating_sub(1), @@ -1168,7 +1167,7 @@ fn capture_piece( game::CaptureFlow::FadeIn(_entity) => { // If we have completed the dissolve-in animation if dissolving.is_empty() { - info!("Nothing dissolving, moving on to next step!"); + debug!("Nothing dissolving, moving on to next step!"); // Move to next state now that animation is done *state = s.next(); @@ -1215,14 +1214,14 @@ fn monitor_animations( // Remove Animating component from players that are done active.iter().for_each(|(entity, player)| { if player.is_finished() { - info!("Entity {:?} is done, removing animating marker", entity); + debug!("Entity {:?} is done, removing animating marker", entity); commands.entity(entity).remove::(); } }); // Set inactive entities to active inactive.iter_mut().for_each(|(entity, mut player)| { if !player.is_finished() && *player.animation_clip() != Handle::::default() { - info!( + debug!( "Entity {:?} is playing {:?}, adding animating marker", entity, player.animation_clip() @@ -1328,7 +1327,7 @@ fn dissolve_animation( // If animation is done, remove dissolving component if percentage <= 0.0 || percentage >= 1.0 { - info!( + debug!( "Removing dissolving from {:?} with percentage {:?}", entity, percentage ); @@ -1485,3 +1484,4 @@ fn animate_title_light_out( }) }); } + diff --git a/src/game.rs b/src/game.rs index 67aae5b..3648708 100644 --- a/src/game.rs +++ b/src/game.rs @@ -158,7 +158,7 @@ pub(crate) struct ValidMove; pub(crate) struct Captured; #[derive(Debug, Component)] -pub(crate) struct Merged; +pub(crate) struct Promoted; // manually for the type. impl std::fmt::Display for Piece { @@ -300,7 +300,7 @@ pub(crate) enum MoveType { #[default] Invalid, Capture, - Merge(Piece), + Promotion(Piece), } #[derive(Debug, Component, PartialEq, Clone, Copy, Eq, Hash, Default)] @@ -372,10 +372,14 @@ impl Board { fn new() -> Board { Board::from_ascii( - r#".....dqq - dpp..pdq - qdp..ppd - qqd....."#, + // r#".....dqq + // dpp..pdq + // qdp..ppd + // qqd....."#, + r#"........ + ...p.... + ..p..... + ........"#, ) } @@ -439,11 +443,10 @@ impl Board { MoveType::Invalid => { Err(GameError::InvalidMove) }, - MoveType::Valid | MoveType::Capture | MoveType::Merge(..) => { + MoveType::Valid | MoveType::Capture | MoveType::Promotion(..) => { // The current epoch is the last epoch + 1 let epoch = self.current_epoch(); - // Local moves vec we can return let mut moves = vec![]; @@ -465,7 +468,20 @@ impl Board { move_type: move_type.clone(), }); - self.inner[to.y][to.x] = Some(*from_piece); + self.inner[to.y][to.x] = match move_type { + MoveType::Promotion(_) => { + match (self.at(from), self.at(to)) { + (Some(Piece::Pawn), Some(Piece::Pawn)) => { + Some(Piece::Drone) + }, + (Some(Piece::Pawn), Some(Piece::Drone)) | (Some(Piece::Drone), Some(Piece::Pawn)) => { + Some(Piece::Queen) + }, + _ => panic!("Merges can only happen between pawn+pawn or pawn+drone!") + } + }, + _ => Some(*from_piece) + }; self.inner[from.y][from.x] = None; self.moves.extend(moves.clone()); @@ -554,10 +570,10 @@ impl Board { Some(to_piece) => { match (piece, to_piece) { (Piece::Pawn, Piece::Pawn) => { - (!side_has_drone).then_some(MoveType::Merge(Piece::Drone)) + (!side_has_drone).then_some(MoveType::Promotion(Piece::Drone)) } (Piece::Drone, Piece::Pawn) | (Piece::Pawn, Piece::Drone) => { - (!side_has_queen).then_some(MoveType::Merge(Piece::Queen)) + (!side_has_queen).then_some(MoveType::Promotion(Piece::Queen)) }, _ => { Some(MoveType::Invalid) @@ -605,7 +621,7 @@ impl Board { let result = self.move_type(current_board_index, *move_index); match result { MoveType::Invalid => None, - MoveType::Capture | MoveType::Merge(..) | MoveType::Valid => { + MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => { Some(*move_index) }, } @@ -764,7 +780,7 @@ mod test { assert_eq!(expected, given, "Pawn can merge to make a drone"); let move_type = board.move_type((0, 0).into(), (1, 1).into()); - assert_eq!(MoveType::Merge(Piece::Drone), move_type); + assert_eq!(MoveType::Promotion(Piece::Drone), move_type); } // Pawn cannot merge with queen @@ -909,10 +925,10 @@ mod test { assert_eq!(MoveType::Capture, capture_move); let merge_move = board.move_type((3, 1).into(), (2, 1).into()); - assert_eq!(MoveType::Merge(Piece::Queen), merge_move); + assert_eq!(MoveType::Promotion(Piece::Queen), merge_move); let long_merge_move = board.move_type((3, 1).into(), (3, 3).into()); - assert_eq!(MoveType::Merge(Piece::Queen), long_merge_move); + assert_eq!(MoveType::Promotion(Piece::Queen), long_merge_move); let jump_move = board.move_type((3, 1).into(), (1, 1).into()); assert_eq!(MoveType::Invalid, jump_move); @@ -936,7 +952,7 @@ mod test { // Pawn + Pawn on same side = Promotion let merge_move = board.move_type((3, 1).into(), (2, 2).into()); - assert_eq!(MoveType::Merge(Piece::Drone), merge_move); + assert_eq!(MoveType::Promotion(Piece::Drone), merge_move); // Pawn + Pawn on other side = Capture let capture_move = board.move_type((3, 1).into(), (4, 2).into()); @@ -1023,7 +1039,7 @@ pub(crate) fn update_board( } if *from != *to_idx { match move_type { - MoveType::Merge(piece) => { + MoveType::Promotion(piece) => { error!("Hey Eli, this is where you are merging pieces"); // Here we insert a new Piece type based on the merge // Other system should automatically update the model @@ -1049,11 +1065,11 @@ pub(crate) fn update_board( .remove::() .insert(Captured); }, - MoveType::Merge(..) => { + MoveType::Promotion(..) => { commands .entity(entity) .remove::() - .insert(Merged); + .insert(Promoted); }, _ => { panic!("How did you do this!?"); diff --git a/src/loading.rs b/src/loading.rs index 9364771..ea00c26 100644 --- a/src/loading.rs +++ b/src/loading.rs @@ -70,9 +70,10 @@ fn loading( .all(|id| server.is_loaded_with_dependencies(id)); let f = (!fonts.is_empty()) && fonts.ids().all(|id| server.is_loaded_with_dependencies(id)); - debug!("s {} g {} t {} f {}", s, g, t, f); + info!("Loading :: images: {} :: gltfs: {} :: tweaks: {} :: fonts: {}", s, g, t, f); - if t { + if s && g && t && f { + info!("Starting game intro"); next_state.set(GameState::Intro) } } diff --git a/src/menu.rs b/src/menu.rs index b2a7c59..f0c605a 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -17,7 +17,7 @@ impl Plugin for MenuPlugin { manage_state_entities::().run_if(state_changed::), ) .add_systems( - Update, + PostUpdate, ( handle_button_press::, handle_button_press::, @@ -340,5 +340,8 @@ fn handle_button_press( .filter_map(|(interaction, button_action)| { (*interaction == Interaction::Pressed).then_some(button_action) }) - .for_each(|ButtonAction(ba)| next_state.set(ba.clone())); + .for_each(|ButtonAction(ba)| { + info!("Button press: {:?} => {:?}", next_state, ba); + next_state.set(ba.clone()) + }); } diff --git a/src/tutorial.rs b/src/tutorial.rs index fb78784..624653b 100644 --- a/src/tutorial.rs +++ b/src/tutorial.rs @@ -17,31 +17,29 @@ impl Plugin for TutorialPlugin { ( // Evaluate if a piece is selected step.run_if( - state_exists::.and_then( - // A piece changes sides - any_component_changed::() - // When a piece is selected, we - .or_else(any_component_added::()) - // A piece is de-selected - .or_else(any_component_removed::()) - // TEMP: The user hits 'enter' - .or_else( - just_pressed(KeyCode::Enter) - .or_else(just_pressed(MouseButton::Left)), - ), - ), + // A piece changes sides + any_component_changed::() + // When a piece is selected, we + .or_else(any_component_added::()) + // A piece is de-selected + .or_else(any_component_removed::()) + // TEMP: The user hits 'enter' + .or_else( + just_pressed(KeyCode::Enter) + .or_else(just_pressed(MouseButton::Left)), + ), ), ), ) // Manage visible/hidden states .add_systems( Update, - manage_state_entities::().run_if(state_changed::), - ) - .add_systems( - Update, - activate_tutorial_step - .run_if(state_exists::.and_then(state_changed::)), + ( + manage_state_entities::(), + activate_tutorial_step, + ) + .run_if(not(in_state(GameState::Loading))) + .run_if(state_changed::), ); } } @@ -60,7 +58,7 @@ pub(crate) enum TutorialState { PieceJump, PieceEnd, Ownership, - _Promotions, + Promotions, Outro, } @@ -106,7 +104,7 @@ fn initialize_tutorial( .expect("Tutorial ownership"), ), ( - TutorialState::_Promotions, + TutorialState::Promotions, tweak .get::>("tutorial_promotions") .expect("Tutorial promotions"), @@ -201,7 +199,7 @@ fn initialize_tutorial( .map(|line| TextSection { value: line.clone(), style: TextStyle { - font_size: 8.0, + font_size: 12.0, color: Color::hex(&text_visible_hex).unwrap(), font: ui_font.handle.clone(), }, @@ -248,7 +246,7 @@ fn initialize_tutorial( value: "R e s t a r t".into(), style: TextStyle { color: Color::WHITE, - font_size: 8.0, + font_size: 10.0, font: font_handle.clone(), }, }], @@ -287,7 +285,7 @@ fn initialize_tutorial( value: "C o n t i n u e".into(), style: TextStyle { color: Color::WHITE, - font_size: 12.0, + font_size: 8.0, font: font_handle.clone(), }, }], @@ -341,6 +339,7 @@ fn step( | TutorialState::PieceEnd | TutorialState::PieceJump | TutorialState::Empty + | TutorialState::Promotions | TutorialState::Ownership => { // PERF: Doing all of this work up front results in unnecessary work let piece_selected = !pieces.is_empty(); @@ -351,6 +350,7 @@ fn step( let ownership_done = seen.0.contains(&TutorialState::Ownership); let ownership_check = !ownership_done && transitions.iter().count() > 0; let jump_done = seen.0.contains(&TutorialState::PieceJump); + let promotions_done = seen.0.contains(&TutorialState::Promotions); let queen_selected = pieces.iter().filter(|&p| *p == game::Piece::Queen).count() > 0; let queen_seen = seen.0.contains(&TutorialState::PieceQueen); let drone_selected = pieces.iter().filter(|&p| *p == game::Piece::Drone).count() > 0; @@ -375,7 +375,7 @@ fn step( // There are no pieces selected } else { // All move, jump, and ownership tutorials done, say goodbye - if all_moves_done && jump_done && ownership_done { + if all_moves_done && jump_done && ownership_done && promotions_done { TutorialState::Outro } // A piece moves sides, so talk about ownership @@ -385,11 +385,17 @@ fn step( // We have not touched on jumping yet else if all_moves_done && !jump_done { TutorialState::PieceJump + } + // We have not touched on field promotions yet + else if all_moves_done && !promotions_done { + TutorialState::Promotions + } // All pieces tutorialized, so prompt user to move pieces for more tutorial - } else if all_moves_done { + else if all_moves_done { TutorialState::PieceEnd + } // Default, empty (tutorial doesn't always need to show something) - } else { + else { TutorialState::PieceIntro } }; @@ -398,7 +404,6 @@ fn step( } // After the outro, we exit the tutorial TutorialState::Outro => error!("Press a button!"), - TutorialState::_Promotions => todo!("Not implemented yet!"), } } @@ -406,7 +411,7 @@ fn activate_tutorial_step( state: Res>, mut query: Query<(&mut Visibility, &TutorialState), Without>, ) { - info!("Activating step {:?}", state.get()); + info!("Activating tutorial step {:?}", state.get()); // Iterate over all entities with TutorialState components query.iter_mut().for_each(|(mut v, s)| { @@ -418,4 +423,4 @@ fn activate_tutorial_step( *v = Visibility::Hidden; } }); -} +} \ No newline at end of file