// TODO: // * Debug Panel // * Describe cell I am hovering over (Note, Octave, etc) // * Count # of cells on the board // * Fix tiling repeating // * Negative coordinates should not mirror // * Fix randomly audio not working // * space -> play/pause sim // * m -> un/mute audio #![allow(clippy::complexity)] // Only because of FromTemplat macros unfortunately... #![allow(dead_code)] use engine::*; mod actions; use actions::*; const TILE: &str = "tileGrey_01.png"; const SIM_FRAME_DURATION: f32 = 0.5; fn main() { App::new() .add_plugins(DefaultPlugins.set(AssetPlugin { file_path: "assets".into(), ..default() })) .add_plugins(FeathersPlugins) .add_plugins(GameActionsPlugin) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .insert_resource(ClearColor(WHITE.into())) .insert_resource(UiTheme(create_light_theme())) .init_state::() .init_state::() .add_message::() .add_systems( Startup, ( the_camera.spawn(), the_ui.spawn(), load_audio_tones, load_materials, load_meshes, ), ) // Cell management/Simulation .add_systems( Update, ( cell_lifecycle.run_if(on_message::), update_grid_position.run_if(on_message::), de_spawn_cells .run_if(not(is_ui_hovered)) .run_if(not(is_panning)) .run_if(input_just_released(MouseButton::Left)), simulation_step .run_if(in_state(SimulationPlayback::Run)) .run_if(cooldown_secs(SIM_FRAME_DURATION)), simulation_step.run_if(state_changed::), pause_simulation.run_if(in_state(SimulationPlayback::Step)), update_material, ), ) // Audio Systems .add_systems( Update, ( control_volume, update_pitch_map_resource_ui.run_if(resource_changed::), reassign_cell_pitch.run_if(resource_changed::), update_audio, ), ) // Debugging .add_systems( Update, ( assert_cell_uniqueness.run_if(any_with_component::), grid_gizmo, update_debug_info_cell_count, update_debug_info_coordinates, ), ) // Ui Controllers .add_systems( Update, ( // TODO: Visualize audio play/mute buttons enable/disable manage_interactive::, // TODO Visualize simulation run/pause buttons enable/disable manage_interactive::, update_note_interactivity, ), ) // Input handling .add_systems( Update, (mouse_pan.run_if(on_message::), set_pan_state), ) .run(); } const SCALE: f32 = 100.0; #[derive(Component, PartialEq, Clone, Default)] enum DebugInfo { CellCount, Coordinates, #[default] Empty, } #[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)] enum AudioPlayback { #[default] Mute, Play, } #[derive(Debug, Component, PartialEq, Default, Clone, FromTemplate)] #[require(Visibility, Transform)] enum UiButton { Power, Pause, Step, Play, ZoomOut, ZoomIn, PlayAudio, MuteAudio, AddHighOctave, SubHighOctave, AddLowOctave, SubLowOctave, Note, Reset, Wipe, #[default] None, } impl UiButton { fn icon(&self) -> &'static str { match self { Self::Power => "power.png", Self::Pause => "pause.png", Self::Step => "next.png", Self::Play => "forward.png", Self::ZoomOut => "zoomOut.png", Self::ZoomIn => "zoomIn.png", Self::PlayAudio => "audioOn.png", Self::MuteAudio => "audioOff.png", Self::AddHighOctave => "todo.png", Self::SubHighOctave => "todo.png", Self::AddLowOctave => "todo.png", Self::SubLowOctave => "todo.png", Self::Note => "todo.png", Self::Reset => "return.png", Self::Wipe => "trashcan.png", Self::None => todo!(), } } } fn ui_checkbox(text: &str) -> impl Scene { bsn! { @FeathersCheckbox { @caption: bsn! { Text(text) ThemedText } } ActivateOnPress on(checkbox_self_update) } } fn ui_button(button: UiButton) -> impl Scene { bsn! { @FeathersButton Hovered Node { top: Val::Px(2.5), left: Val::Px(2.5), margin: UiRect::all(Val::Px(2.5)), } Children [ Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, width: Val::Percent(100.0), height: Val::Percent(100.0), } ImageNode { image: HandleTemplate::Path(AssetPath::parse(button.icon())), } ] } } fn the_camera() -> impl Scene { bsn! { Camera2d } } fn update_pitch_map_resource_ui( query: Query<(Entity, &Note), With>, pitch_map: Res, mut commands: Commands, ) { query.iter().for_each(|(e, n)| { if pitch_map.notes.contains(n) { commands.entity(e).insert(Checked); } else { commands.entity(e).remove::(); } }); } fn text_setup() -> impl Scene { bsn! { // Feathers propagates text styles only through entities that themselves have // ThemedText, so a body holding loose text must carry these itself. InheritableThemeTextColor(tokens::TEXT_MAIN) InheritableFont { font: fonts::REGULAR, font_size: size::MEDIUM_FONT, weight: FontWeight::NORMAL, } } } fn the_ui() -> impl Scene { bsn! { Node { width: Val::Percent(100.0), top: Val::Px(0.0), left: Val::Px(0.0), display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, } Transform Children [ primary_ui(), debug_ui(), ] } } fn debug_ui() -> impl Scene { bsn! { Node { justify_self: JustifySelf::End, display: Display::Flex, flex_direction: FlexDirection::Column, } // TODO: Toggle visibility when debugging disabled pane() Transform Children [ pane_header() Children [ Text("Debug Info") ThemedText, ], pane_body() Children [ subpane() Node { display: Display::Flex, flex_direction: FlexDirection::Row, } Children [ subpane_header() Children [ Text("Cell Count") ThemedText ] subpane_body() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } text_setup() Children [ Text("###") ThemedText template_value(DebugInfo::CellCount) ] ], subpane() Node { display: Display::Flex, flex_direction: FlexDirection::Row, } Children [ subpane_header() Children [ Text("Coordinates") ThemedText ] subpane_body() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } text_setup() Children [ Text("##,##") ThemedText template_value(DebugInfo::Coordinates) ] ] ] ] } } fn primary_ui() -> impl Scene { fn toggle_note( change: On>, notes: Query<&Note, With>, mut pitch_map: ResMut, ) { if change.is_final { // TODO: if last selected note and de-selected, undo that move please let this_note = notes.get(change.source).unwrap(); pitch_map.set_note(this_note, change.value); debug!("(note {:?}) Updated pitch map: {:#?}", this_note, pitch_map); } } bsn! { Node { display: Display::Flex, flex_direction: FlexDirection::Column, justify_content: JustifyContent::Start, } Transform pane() Children [ pane_header() Children [ Text("Conways Game of Life With Sound") ThemedText, ], pane_body() Children [ subpane() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } Children [ subpane_header() Children [ Text("Nav") ThemedText ] subpane_body() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } Children [ ui_button(UiButton::Power) ActionSource(Action::Quit), ui_button(UiButton::Pause) SimulationPlayback::Pause ActionSource(SimAction::Toggle), ui_button(UiButton::Play) SimulationPlayback::Run ActionSource(SimAction::Toggle), ui_button(UiButton::Step) ActionSource(SimAction::Step), ui_button(UiButton::Reset) ActionSource(SimAction::Reset), ui_button(UiButton::Wipe) ActionSource(SimAction::Wipe), ui_button(UiButton::ZoomOut) ActionSource(ZoomAction::ZoomOut), ui_button(UiButton::ZoomIn) ActionSource(ZoomAction::ZoomIn), ui_button(UiButton::MuteAudio) AudioPlayback::Mute ActionSource(AudioAction::Toggle), ui_button(UiButton::PlayAudio) AudioPlayback::Play ActionSource(AudioAction::Toggle), ] ], subpane() Children [ subpane_header() Children [ Text("Notes") ThemedText ] subpane_body() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } Children [ ui_checkbox("A") template_value(Note::A) on(toggle_note), ui_checkbox("A#") template_value(Note::ASharp) on(toggle_note), ui_checkbox("B") template_value(Note::B) on(toggle_note), ui_checkbox("C") template_value(Note::C) on(toggle_note), ui_checkbox("C#") template_value(Note::CSharp) on(toggle_note), ui_checkbox("D") template_value(Note::D) on(toggle_note), ui_checkbox("D#") template_value(Note::DSharp) on(toggle_note), ui_checkbox("E") template_value(Note::E) on(toggle_note), ui_checkbox("F") template_value(Note::F) on(toggle_note), ui_checkbox("F#") template_value(Note::FSharp) on(toggle_note), ui_checkbox("G") template_value(Note::G) on(toggle_note), ui_checkbox("G#") template_value(Note::GSharp) on(toggle_note), ] ], subpane() subpane_header() Children [ Text("Octaves") ThemedText ] subpane_body() Node { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: AlignItems::Stretch, justify_content: JustifyContent::Start, } Children [ @FeathersSlider { @max: 5., @value: 3., @min: 1. } SliderStep(1.) SliderPrecision(0) on(|value_change: On>, mut pitch_map: ResMut| { if value_change.is_final { pitch_map.octaves_up = value_change.value as usize; debug!("(octave up) Updated pitch map: {:#?}", pitch_map); } }) on(slider_self_update), @FeathersSlider { @max: 5., @value: 3., @min: 1. } SliderStep(1.) SliderPrecision(0) on(|value_change: On>, mut pitch_map: ResMut| { if value_change.is_final { pitch_map.octaves_down = value_change.value as usize; debug!("(octave down) Updated pitch map: {:#?}", pitch_map); } }) on(slider_self_update), ], ] ] } } #[derive(Default, Debug, Resource)] struct NextCoordinates(Coordinates); #[derive(Default, Debug, Resource)] struct LastBoardState { coordinates: Vec, } #[derive(Debug, Default, Clone, PartialEq, Hash, Eq, Component, FromTemplate)] #[require(Visibility, Transform)] struct Coordinates { x: isize, y: isize, } impl Coordinates { fn neighbors(&self) -> impl Iterator { [ Coordinates { x: self.x, y: self.y + 1, }, Coordinates { x: self.x + 1, y: self.y + 1, }, Coordinates { x: self.x + 1, y: self.y, }, Coordinates { x: self.x + 1, y: self.y - 1, }, Coordinates { x: self.x, y: self.y - 1, }, Coordinates { x: self.x - 1, y: self.y - 1, }, Coordinates { x: self.x - 1, y: self.y, }, Coordinates { x: self.x - 1, y: self.y + 1, }, ] .into_iter() } fn as_translation(&self) -> Vec3 { (Vec2::new(self.x as f32, self.y as f32) * SCALE).extend(0.0) } /// For the purposes of creating assests, what does the given coordinate translate to CellPitch wise fn to_cell_pitch(&self) -> CellPitch { let note = match self.x { -6 => Note::A, -5 => Note::ASharp, -4 => Note::B, -3 => Note::C, -2 => Note::CSharp, -1 => Note::D, 0 => Note::DSharp, 1 => Note::E, 2 => Note::F, 3 => Note::FSharp, 4 => Note::G, 5 => Note::GSharp, _ => panic!("This shouldn't happen!"), }; let octave = self.y; CellPitch { note, octave } } } /// When the cursor moves, update the Coordiantes fn update_grid_position( mut coordinates: ResMut, q_window: Single<&Window>, q_camera: Single<(&Camera, &GlobalTransform)>, ) { if let Some(cursor_pos) = q_window.cursor_position() { let (cam, cam_gt) = *q_camera; if let Ok(pos) = cam.viewport_to_world_2d(cam_gt, cursor_pos) { coordinates.0.x = (pos / SCALE).round().x as isize; coordinates.0.y = (pos / SCALE).round().y as isize; } } } /// Draw a gizmo at the current coordiantes fn grid_gizmo(mut gizmos: Gizmos, coordinates: Res) { let isometry = Isometry2d { translation: coordinates.0.as_translation().truncate(), ..default() }; gizmos.rect_2d(isometry, Vec2::ONE * SCALE, PURPLE) } #[derive(Component, Debug, PartialEq, Hash, Eq, FromTemplate)] struct Cell; // When left mouse clicked, spawn a cell at GridPosition fn de_spawn_cells( coordinates: Res, cells: Query<(Entity, &Coordinates), With>, mut lifecycle: MessageWriter, ) { if let Some(e) = cells .iter() .find_map(|(e, c)| (coordinates.0 == *c).then_some(e)) { lifecycle.write(Lifecycle::Dead(e)); } else { lifecycle.write(Lifecycle::Alive(coordinates.0.clone())); } } /// Run condition if the UI is the active element on the screen fn is_ui_hovered(hovered: Query<&Hovered>) -> bool { hovered.iter().any(|Hovered(h)| *h) } fn assert_cell_uniqueness(q: Query<(Entity, &Coordinates), With>) { q.iter().for_each(|(e1, c1)| { q.iter().for_each(|(e2, c2)| { if e1 != e2 && c1 == c2 { panic!("Duplicate cells at {:?}", c1); } }) }) } #[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)] #[allow(dead_code)] enum SimulationPlayback { #[default] Pause, Run, Step, } /// Simulate the Game of Life Rules: /// 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation. /// 2. Any live cell with two or three live neighbours lives on to the next generation. /// 3. Any live cell with more than three live neighbours dies, as if by overpopulation. /// 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. /// /// TODO: There is a lot of optimizations to be had here fn simulation_step( q: Query<(Entity, &Coordinates), With>, mut writer: MessageWriter, ) { fn is_cell_alive( coordinates: &Coordinates, query: &Query<(Entity, &Coordinates), With>, ) -> bool { query.iter().any(|(_e, c)| *c == *coordinates) } fn living_neighbors( c: &Coordinates, query: &Query<(Entity, &Coordinates), With>, ) -> usize { c.neighbors() .filter(|neighbor| { query .iter() .any(|(_e, coordinates)| *neighbor == *coordinates) }) .count() } fn cell_entity( coordinates: &Coordinates, query: Query<(Entity, &Coordinates), With>, ) -> Option { query .iter() .find_map(|(e, c)| (*c == *coordinates).then_some(e)) } // Collect list of candidate positions... let mut coordinates: HashSet = HashSet::new(); // Put all dead cells neighboring living cells into the coordinates hashset // Each living cell q.iter().for_each(|(_e, c)| { coordinates.insert(c.clone()); // Iterator over neighbor positions // Filter to just dead neighbors c.neighbors() .filter(|c| !is_cell_alive(c, &q)) .for_each(|c| { coordinates.insert(c); }); }); // Get Neighbor Count of living cells coordinates.iter().for_each(|c| { let num_living_neighbors = living_neighbors(c, &q); let is_living = is_cell_alive(c, &q); match num_living_neighbors { ..2 | 4.. => { if is_living { writer.write(Lifecycle::Dead(cell_entity(c, q).unwrap())); } } 2 => debug!("continue"), 3 => { if !is_living { writer.write(Lifecycle::Alive(c.clone())); } else { debug!("continue") } } }; }); } /// Bring a cell to life, or kill it #[derive(Message)] enum Lifecycle { Alive(Coordinates), Dead(Entity), } fn new_cell( Coordinates { x, y }: Coordinates, cell_assets: &Res, sinks: &Query>, sim_state: &Res>, audio_state: &Res>, pitch_map: &Res, ) -> impl Scene { let c = Coordinates { x, y }; let volume = { if *sim_state.get() == SimulationPlayback::Run && *audio_state.get() == AudioPlayback::Play { let ratio = 1.0 / (sinks.count() + 1) as f32; Volume::Linear(ratio) } else { Volume::SILENT } }; let playback_settings = PlaybackSettings::ONCE.with_volume(volume); let cell_pitch = pitch_map.coordinates_cell_pitch(&c); #[derive(Clone, Default, Component)] enum Foo { #[default] A, } bsn! { Cell Coordinates { x, y } template_value(cell_pitch) template_value(playback_settings) template_value(Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0))) template_value(Mesh2dTemplate(cell_assets.mesh.clone().into())) // AudioPlayer added by update_audio // MeshMaterial2d added by update_material template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&pitch_map.coordinates_cell_pitch(&c)).unwrap().clone().into())) } } fn cell_lifecycle( mut reader: MessageReader, mut commands: Commands, cell_assets: Res, query: Query>, sim_state: Res>, audio_state: Res>, pitch_map: Res, ) { reader.read().for_each(|msg| match msg { Lifecycle::Alive(c) => { debug!("creating {:?}", c); // TODO: Safeguard: ensure no other cell there? commands.spawn_scene(new_cell( c.clone(), &cell_assets, &query, &sim_state, &audio_state, &pitch_map, )); } Lifecycle::Dead(e) => { commands.entity(*e).despawn(); } }) } // Run condition for running a system every `input` seconds fn cooldown_secs(input: f32) -> impl FnMut(Local, Res