use bevy::core_pipeline::{ contrast_adaptive_sharpening::ContrastAdaptiveSharpeningSettings, fxaa::Fxaa, }; use crate::prelude::*; pub(crate) struct Display3dPlugin; impl Plugin for Display3dPlugin { fn build(&self, app: &mut App) { app.add_plugins(( TemporalAntiAliasPlugin, MaterialPlugin::::default(), )) .init_resource::() .init_resource::() .insert_resource(Msaa::Off) .insert_resource(AmbientLight { color: Color::WHITE, brightness: 100.0, }) .insert_resource(PointLightShadowMap { size: 512 }) .insert_resource(DirectionalLightShadowMap { size: 512 }) .add_systems( OnExit(GameState::Loading), ( fix_skybox, initialize.after(fix_skybox), ), ) // Systems related to color and camera .add_systems( Update, ( color_grading_tweak .run_if(resource_exists::) .run_if(on_event::>()), fog_tweak .run_if(resource_exists::) .run_if(on_event::>()), bloom_tweak .run_if(resource_exists::) .run_if(on_event::>()), ), ) // Piece selection .add_systems( Update, ( entity_pointer .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)) // Run if in debug state on mouse events // Or just run when the left mouse button is clicked .run_if( in_state(debug::DebugState::Enabled) .and_then(on_event::()) .or_else(just_pressed(MouseButton::Left)), ) .before(select), 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::()), de_select.run_if(just_pressed(MouseButton::Right)), put_down.run_if(any_component_removed::()), ) ) .add_systems( Update, ( debug_textures, load_assets .run_if(in_state(GameState::Loading)) .run_if(on_event::>()), hydrate_camera.run_if(any_component_added::()), update_tweaks .run_if(on_event::>()) .run_if(resource_exists::), switch_sides .run_if(in_state(GameState::Play)) .run_if(state_changed::) .run_if(any_component_added::() .or_else(any_component_removed::()) ) .run_if(should_switch_sides), // Camera moving up when first piece is selected in the game vantage_point .run_if(in_state(GameState::Play)) .run_if(any_component_added::()), update_pieces .run_if(resource_exists::) .run_if(in_state(GameState::Play)) .run_if( any_component_added::() .or_else(any_component_changed::()) .or_else(any_component_added::()) .or_else(any_component_changed::()) .or_else(any_component_added::()) .or_else(any_component_changed::()) .or_else(any_component_removed::()) .or_else(any_component_removed::()) .or_else(any_component_removed::()) ), set_models .run_if(resource_exists::) .run_if( any_component_changed::() .or_else(any_component_added::()) .or_else(any_component_removed::()) ), dissolve_animation.run_if(any_with_component::), capture_piece_start.run_if(any_component_added::()), capture_piece_end.run_if(any_component_removed::()), monitor_animations .run_if(in_state(GameState::Play)) .run_if(any_component_changed::()), set_animation_player_speed .run_if(any_component_added::() .or_else(resource_changed::) ), set_animation_speed .run_if(in_state(GameState::Play)) .run_if(not(in_state(MenuState::On))) .run_if( just_pressed(KeyCode::Enter) .or_else(just_pressed(MouseButton::Left)) .or_else(just_released(KeyCode::Enter)) .or_else(just_released(MouseButton::Left)) ) ), ) .add_systems( Update, ( move_camera.run_if(on_event::()), mouse_zoom.run_if(on_event::()), gizmo_system, selected_gizmo, moves_gizmo, debug_selected.run_if(any_with_component::), ) .run_if(in_state(debug::DebugState::Enabled)) .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)), ) .add_systems( OnEnter(GameState::Intro), ( color_grading_tweak.run_if(resource_exists::), fog_tweak.run_if(resource_exists::), bloom_tweak.run_if(resource_exists::), set_models.run_if(resource_exists::), update_pieces.run_if(resource_exists::).after(set_models), ), ) .add_systems( Update, ( setup_dissolve_materials .run_if(any_component_added::>()), ) ) .add_systems( OnEnter(GameState::Play), ( opening_animation .run_if(run_once()) .run_if(in_state(DisplayState::Display3d)), ), ) .add_systems( OnEnter(GameState::Title), ( fade_title_in, fixup_shadows ) ) .add_systems(OnExit(GameState::Title), fade_title_out) .add_systems( Update, ( fade_title .run_if(any_with_component::), continue_title .run_if(in_state(GameState::Title)) .run_if(not(any_with_component::)) .run_if(just_pressed(KeyCode::Enter).or_else(just_pressed(MouseButton::Left))), ) ); } } #[derive(Debug, Component, PartialEq)] pub(crate) struct Display3d; #[derive(Debug, Component)] pub(crate) struct TitleText; #[derive(Debug, Component)] pub(crate) struct Dissolvable { start: f32, duration: f32, } #[derive(Debug, Component)] pub(crate) enum Dissolving { In(f32), Out(f32), } #[derive(Debug, Component)] pub(crate) enum Fading { In(f32), Out(f32), } #[derive(Debug, Resource)] pub(crate) struct AnimationSpeed { pub movement: f32, pub dissolve: f32, } impl Default for AnimationSpeed { fn default() -> Self { AnimationSpeed { movement: 1.0, dissolve: 1.0, } } } #[derive(Debug, Resource, Clone)] struct AssetsMap { hitbox_shape: Handle, hitbox_material: Handle, title_image: Handle, } /// Load 3d models /// This is kind of pulling double duty. /// Both loads the GLTF file _and_ populates the ModelMap once that is loaded. fn load_assets( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, tweaks_file: Res, tweaks: Res>, ) { let hitbox_shape = meshes.add(Cuboid::new(1.0, 0.1, 1.0)); let hitbox_material = materials.add(StandardMaterial { base_color: Color::NONE, perceptual_roughness: 0.0, reflectance: 0.0, alpha_mode: AlphaMode::Blend, ..default() }); let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); let title_image = tweak.get_handle::("title_image").unwrap(); commands.insert_resource(AssetsMap { hitbox_shape, hitbox_material, title_image, }); } /// Initialize the board and pieces fn initialize(mut commands: Commands, board: Res, assets: Res) { debug!("Initializing root"); // Title commands.spawn(( DisplayState::Display3d, Display3d, ImageBundle { style: Style { width: Val::Percent(75.0), height: Val::Percent(75.0), align_self: AlignSelf::Center, justify_self: JustifySelf::Center, ..default() }, background_color: Color::WHITE.with_a(0.0).into(), image: UiImage { texture: assets.title_image.clone(), ..default() }, visibility: Visibility::Hidden, ..default() }, TitleText, )); // 3D objects root commands .spawn(( SpatialBundle { ..default() }, Display3d, DisplayState::Display3d, )) .with_children(|parent| { debug!("Intializing 3D Board!"); // Board parent .spawn(( Display3d, DisplayState::Display3d, game::BoardComponent, DisplayModel("display3d_models_scenes_board"), SceneBundle { visibility: Visibility::Hidden, ..default() }, Dissolvable { start: 0.0, duration: 12.0, }, // Marks pieces as dissolving )); // Hitboxes game::tiles().for_each(|(index, tile)| { let side = Board::side(index).expect("Spawn valid side"); parent.spawn(( DisplayState::Display3d, Display3d, index, tile, PbrBundle { mesh: assets.hitbox_shape.clone(), material: assets.hitbox_material.clone(), visibility: Visibility::Hidden, transform: Transform::from_translation(board_translation(&index)), ..default() }, side, game::Selectable, )); }); // Valid move indicators game::tiles().for_each(|(index, _)| { let side = Board::side(index).expect("Spawn valid side"); parent.spawn(( DisplayState::Display3d, Display3d, DisplayModel("display3d_models_scenes_valid_move"), index, SceneBundle { visibility: Visibility::Hidden, transform: Transform::from_translation(board_translation(&index)), ..default() }, side, game::ValidMove, )); }); // Pieces let mut angle = 0.0; board.pieces().iter().for_each(|(index, piece)| { let side = Board::side(*index).expect("Spawn valid side"); // Rotates each piece 90 degrees offset from the previous piece let rotation = Quat::from_rotation_y(angle); let transform = Transform::default().with_rotation(rotation); angle += std::f32::consts::PI / 2.0; parent.spawn(( side, DisplayState::Display3d, Display3d, *piece, *index, SceneBundle { visibility: Visibility::Hidden, transform, ..default() }, DisplayModel(game::piece_model_key(*piece, side)), game::Selectable, Dissolvable { start: 1.0, duration: 3.0, }, // Marks pieces as dissolving )); }); }); } fn hydrate_camera( events: Query<(&Name, Entity), Added>, gltfs: Res>, state: Res>, mut players: Query<&mut AnimationPlayer>, mut commands: Commands, tweaks_file: Res, tweaks: Res>, ) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); events .iter() .filter(|(name, _)| name.as_str() == "GameCam") .for_each(|(_, entity)| { debug!("Initialize 3d camera"); let skybox_handle = tweak .get_handle::("display3d_models_skybox_file") .unwrap(); let skybox_brightness = tweak.get::("display3d_skybox_brightness").unwrap(); let environment_map_intensity = tweak .get::("display3d_environment_map_light_intensity") .unwrap(); let side = state.get().0; debug!("Hydrating camera {:?}", entity); // Populate the components for the camera commands.entity(entity).insert(( DisplayState::Display3d, Display3d, side, Camera3dBundle { camera: Camera { is_active: true, hdr: true, ..default() }, dither: DebandDither::Enabled, color_grading: ColorGrading { exposure: 1.0, gamma: 1.0, ..default() }, tonemapping: Tonemapping::BlenderFilmic, ..default() }, Skybox { image: skybox_handle.clone(), brightness: skybox_brightness, }, EnvironmentMapLight { diffuse_map: skybox_handle.clone(), specular_map: skybox_handle.clone(), intensity: environment_map_intensity, }, Fxaa { enabled: true, ..default() }, ContrastAdaptiveSharpeningSettings { enabled: false, ..default() }, BloomSettings { intensity: 0.0, ..default() }, FogSettings { color: Color::rgba(0.25, 0.25, 0.25, 1.0), falloff: FogFalloff::Exponential { density: 0.0 }, ..default() }, )); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); // Set it to the default position by starting the initial animation if let Ok(mut player) = players.get_mut(entity) { debug!("Animations: {:?}", gltf.named_animations.keys()); // GameCamIntro1, GameCamIntro2, GameCamSide1>2, GameCamSide2>1 let animation = match state.get() { game::TurnState(game::Side::A) => gltf.named_animations.get( tweak .get::("display3d_models_animations_intro_a") .unwrap() .as_str(), ), game::TurnState(game::Side::B) => gltf.named_animations.get( tweak .get::("display3d_models_animations_intro_b") .unwrap() .as_str(), ), } .expect("Camera Startup"); player.play(animation.clone()).pause(); } }); } /// Update display3d tweaks in the game /// Triggered on entering 3d state and when the tweakfile is updated. fn update_tweaks( mut camera_settings: Query<(&mut Skybox, &mut EnvironmentMapLight), With>, tweaks_file: Res, tweaks: Res>, ) { if let Some(tweak) = tweaks.get(tweaks_file.handle.clone()) { debug!("Updating tweaks!"); camera_settings .iter_mut() .for_each(|(mut skybox, mut environment_map_light)| { skybox.brightness = tweak.get::("display3d_skybox_brightness").unwrap(); environment_map_light.intensity = tweak .get::("display3d_environment_map_light_intensity") .unwrap(); }); } } fn fix_skybox( mut images: ResMut>, tweaks_file: Res, tweaks: Res>, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).unwrap(); let handle = tweak .get_handle_unchecked::("display3d_models_skybox_file") .unwrap(); let image = images.get_mut(handle).unwrap(); 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 { image.reinterpret_stacked_2d_as_array( image.texture_descriptor.size.height / image.texture_descriptor.size.width, ); image.texture_view_descriptor = Some(TextureViewDescriptor { dimension: Some(TextureViewDimension::Cube), ..default() }); } } #[derive(Debug, Component)] struct DisplayModel(&'static str); fn set_models( mut query: Query<(Entity, &mut Handle, &DisplayModel)>, children: Query<&Children>, active_animation_players: Query<&AnimationPlayer, With>, gltfs: Res>, tweaks: Res>, tweaks_file: Res, ) { let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweakfile"); let assets_handle = tweak.get_handle::("display3d_models_assets_file").unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); query.iter_mut().for_each(|(entity, mut handle, DisplayModel(key))| { // Check if any children are animating if active_animation_players .iter_many(children.iter_descendants(entity)) .count() > 0 { debug!("Piece {:?} is animating. Skipping...", entity); } else { let scene = tweak.get::(key).unwrap(); let new_handle = gltf.named_scenes.get(scene.as_str()).expect("Game board model"); if *new_handle != *handle { debug!("Updating piece for {:?}", entity); *handle = new_handle.clone(); } } }) } /// Given a board index returns the Vec3 location in space fn board_translation(&BoardIndex { x, y }: &BoardIndex) -> Vec3 { // Scale x down by 4 to account for -4..4 scaling let x = x as i8 - 4; // Mirror y axis because our board index is inverted... let y = -(y as i8) + 1; let x = if x < 0 { x as f32 * 1.3 + 0.275 // 0.325 } else { x as f32 * 1.3 + 1.05 // 1.0 }; let y = y as f32 * 1.3 + 0.65; Vec3::new(x, 0.0, y) } fn capture_translation(side: &Side, num: usize) -> Vec3 { 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); match side { Side::B => Vec3::new(-x, y, z), Side::A => Vec3::new(x, y, -z), } } fn gizmo_system(mut gizmos: Gizmos) { for y in 0..4 { for x in 0..8 { gizmos.cuboid( Transform::from_translation(board_translation(&BoardIndex { x, y })) .with_scale(Vec3::new(1.0, 0.1, 1.0)), Color::PURPLE, ) } } } /// TODO: This has bad feel, needs to be tuned fn move_camera( buttons: Res>, mut events: EventReader, mut camera: Query<&mut Transform, (With, With)>, ) { events.read().for_each(|MouseMotion { delta }| { if buttons.pressed(MouseButton::Left) { camera.iter_mut().for_each(|mut t| { t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0)); t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0)); t.look_at(Vec3::ZERO, Vec3::Y); }); } }); } fn mouse_zoom( mut events: EventReader, mut camera: Query<&mut Transform, (With, With)>, ) { events.read().for_each(|MouseWheel { unit, y, .. }| { camera.iter_mut().for_each(|mut t| { match unit { MouseScrollUnit::Line => { t.translation *= 1.0 - (*y / 4.0); } MouseScrollUnit::Pixel => { t.translation *= 1.0 - (*y / 64.0); } } t.look_at(Vec3::ZERO, Vec3::Y); }); }); } // The dumbest way to achieve this goal // Essentially we iterate over every piece and set the appropriate model and texture fn update_pieces( mut query: Query<( &BoardIndex, &mut Transform, &mut DisplayModel, &Side, &Piece, )>, ) { query.iter_mut().for_each( |(board_index, mut transform, mut display_model, side, piece)| { // Set position of piece let new_translation = board_translation(board_index); if transform.translation != new_translation { debug!("Updating piece transform"); transform.translation = new_translation; } let key = game::piece_model_key(*piece, *side); if display_model.0 != key { *display_model = DisplayModel(key); } }, ); } fn select( // Query Selectable with BoardIndex query: Query<(&BoardIndex, &Side), With>, piece: Res, selected: Query<&BoardIndex, With>, state: Res>, mut selections: EventWriter, mut moves: EventWriter ) { match *piece { // Something is selected, so send an event saying such // Why send the event instead of just detect the resource update? PiecePointer(Some(e)) => { query.get(e).iter().for_each(|(board_index, side)| { let side_check = !selected.is_empty() || state.get().0 == **side; if side_check { selections.send(game::Selection(**board_index)); } }); } // Nothing selected, cancel the selection PiecePointer(None) => { selected.iter().for_each(|board_index| { moves.send(Move { from: *board_index, to: Some(*board_index), ..default() }); }) }, } } #[derive(Debug, Default, Resource)] pub(crate) struct PiecePointer(pub Option); /// Select tiles and pieces in 3d /// There is a bug where we are selecting multiple entities... /// TODO: Selectable generalize picking pieces **and** hitboxes fn entity_pointer( query: Query<(Entity, &Handle, &GlobalTransform)>, meshes: Res>, cameras: Query<(&Camera, &GlobalTransform)>, windows: Query<&Window, With>, selectable: Query, With)>, children: Query<&Children>, mut pointer: ResMut, ) { // For each window (there should be only one) windows.iter().for_each(|window| { // Get the cursor position if let Some(pos) = window.cursor_position() { // iterate over every camera cameras.iter().for_each(|(camera, gt)| { // Get a ray from the camera through the cursor into the world if let Some(ray) = camera.viewport_to_world(gt, pos) { // Iterate over every entity with a 3d scene let selected = query .iter() // Transform the scene handle into mesh data .filter_map(|(entity, handle, gt)| { meshes.get(handle).map(|mesh| (entity, mesh, gt)) }) // Iterate over every mesh + global transform .filter_map(|(entity, mesh, gt)| { // If the camera -> cursor -> * ray intersects with the mesh hit3d::intersects3d(&ray, mesh, gt).map(|hit| (entity, hit)) }) .filter_map(|(entity, hit)| { // Find this entity in the set of selectable entities selectable.iter().find_map(|e| { let hit_check = { // This entity was hit (tile hitboxes) let primary = entity == e; // A child was hit (pieces) let secondary = children.iter_descendants(e).any(|child| child == entity); primary || secondary }; // Return the board index of this piece hit_check.then_some((e, hit.clone())) }) }) // Compare the distance of all hits, choosing the closest one .min_by(|(_, hit_a), (_, hit_b)| { hit_a.distance.partial_cmp(&hit_b.distance).unwrap() }) .map(|(e, _)| e); *pointer = PiecePointer(selected) } }); } }); } fn selected_gizmo( selected: Query<&Transform, (With, With)>, mut gizmos: Gizmos, ) { selected.iter().for_each(|transform| { gizmos.cuboid(*transform, Color::GREEN); }) } fn moves_gizmo( selected: Query<&BoardIndex, (With, With, With)>, board: Res, mut gizmos: Gizmos, ) { selected.iter().for_each(|idx| { board .valid_moves(*idx) .iter() .map(|i| Transform::from_translation(board_translation(i))) .for_each(|t| gizmos.cuboid(t, Color::WHITE)) }); } fn pick_up( mut events: Query< (Entity, &game::Piece), (With, With, Added), >, gltfs: Res>, children: Query<&Children>, mut players: Query<(&Name, &mut AnimationPlayer)>, clips: Res>, tweaks: Res>, tweaks_file: Res, ) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); events.iter_mut().for_each(|(entity, 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"); debug!("Pickup animation for {:?}", entity); children.iter_descendants(entity).for_each(|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(),); debug!( "Picking up {:?} ({:?}) {:?} {:?}", name, entity, piece, pickup_animation ); let pickup_handle = gltf .named_animations .get( tweak .get::(pickup_animation.as_str()) .unwrap() .as_str(), ) .expect("Pickup Animation"); let idle_animation = format!("display3d_models_animations_idle_{}", name.as_str()); let idle_handle = gltf .named_animations .get( tweak .get::(idle_animation.as_str()) .unwrap() .as_str(), ) .expect("Idle animation"); if let Some(pickup_clip) = clips.get(pickup_handle) { if let Some(idle_clip) = clips.get(idle_handle) { if pickup_clip.compatible_with(name) && idle_clip.compatible_with(name) { player .start_with_transition( pickup_handle.clone(), Duration::from_secs_f32(0.75), ) .start_with_transition( idle_handle.clone(), Duration::from_secs_f32(1.5), ) .set_repeat(RepeatAnimation::Forever); } }; } } }) }); } fn de_select( query: Query>, mut commands: Commands, ) { query.iter().for_each(|e| { commands.entity(e).remove::(); }) } fn put_down( mut events: RemovedComponents, mut query: Query, With, With)>, gltfs: Res>, children: Query<&Children>, mut players: Query<(&Name, &mut AnimationPlayer)>, clips: Res>, tweaks: Res>, tweaks_file: Res, ) { let tweak = tweaks .get(&tweaks_file.handle.clone()) .expect("Load tweakfile"); events.read().for_each(|entity| { if let Ok(_) = query.get_mut(entity) { debug!("Putting down piece"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); 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) { debug!("Putting down {:?}", entity); let putdown_animation = format!("display3d_models_animations_put_down_{}", name.as_str()); let putdown_handle = gltf .named_animations .get( tweak .get::(putdown_animation.as_str()) .unwrap() .as_str(), ) .expect("PutDown Animation"); if let Some(putdown_clip) = clips.get(putdown_handle) { if putdown_clip.compatible_with(name) { debug!("Compatible with put-down clip!"); player .start_with_transition( putdown_handle.clone(), Duration::from_secs_f32(0.75), ) .set_repeat(RepeatAnimation::Never); } else { debug!( "Clip {:?}({:?}) not compatible with {:?}", putdown_animation, putdown_clip, name ); } } else { debug!("Clip not found"); } } else { debug!("Player not found"); } }) } else { debug!("Piece+Side not found for entity"); } }) } fn opening_animation( mut players: Query<&mut AnimationPlayer, (With, With)>, mut query: Query<(Entity, &Dissolvable), Or<(With, With)>>, mut commands: Commands, ) { players.iter_mut().for_each(|mut player| { debug!("Playing intro camera animation"); player.resume() }); query.iter_mut().for_each(|(e, d)| { commands.entity(e).insert(Dissolving::In(d.duration)); }); } fn set_animation_speed( mut animation_speed: ResMut, tweaks: Res>, tweaks_file: Res, keys: Res>, mouse: Res>, ) { *animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); let movement = tweak.get::("animation_fast_movement_speed").unwrap(); let dissolve = tweak.get::("animation_fast_dissolve_speed").unwrap(); AnimationSpeed { movement, dissolve } } else { AnimationSpeed::default() }; debug!("Set animation speeds {:?}", *animation_speed); } // When an animation starts, or the animation speed changes, update player speed fn set_animation_player_speed( mut players: Query<&mut AnimationPlayer, With>, animation_speed: Res, ) { players.iter_mut().for_each(|mut p| { p.set_speed(animation_speed.movement); }) } fn should_switch_sides( query: Query<&Side, (With, With)>, state: Res>, ) -> bool { query.iter().all(|side| { match state.get() { ai::PlayState::AiBogo => match side { Side::A => true, Side::B => false, } ai::PlayState::Human => true, } }) } fn switch_sides( mut players: Query<(&mut AnimationPlayer, &mut Side), (With, With)>, gltfs: Res>, state: Res>, tweaks: Res>, tweaks_file: Res, ) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); players.iter_mut().for_each(|(mut player, mut side)| { let animation_key = match state.get() { game::TurnState(game::Side::A) => "display3d_models_animations_turn_a", game::TurnState(game::Side::B) => "display3d_models_animations_turn_b", }; let animation_val = tweak.get::(animation_key).unwrap(); let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation"); player.start_with_transition( animation.clone(), Duration::from_secs_f32(1.00), ); *side = state.get().0; }); } fn vantage_point( mut players: Query<&mut AnimationPlayer, (With, With)>, gltfs: Res>, state: Res>, tweaks: Res>, tweaks_file: Res, mut up: Local ) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); players.iter_mut().for_each(|mut player| { debug!("Getting a better view"); // play down events on state transitions let animation_key = if !*up { *up = true; match state.get() { game::TurnState(game::Side::A) => "display3d_models_animations_turn_up_a", game::TurnState(game::Side::B) => "display3d_models_animations_turn_up_b", } } else { "" }; if animation_key != "" { let animation_val = tweak.get::(animation_key).unwrap(); let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation"); player.start_with_transition( animation.clone(), Duration::from_secs_f32(1.00), ); } }); } /// Type expressing the extended material of standardMaterial + dissolveMaterial type DissolveMaterial = ExtendedMaterial; /// Material extension for dissolving effect #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] struct DissolveExtension { #[uniform(100)] percentage: f32, } impl MaterialExtension for DissolveExtension { fn fragment_shader() -> ShaderRef { "shaders/dissolve.wgsl".into() } fn deferred_fragment_shader() -> ShaderRef { "shaders/dissolve.wgsl".into() } } /// Sets up all pieces to have an associated "dissolve" material ready for capture fn setup_dissolve_materials( // All entities with materials are candidates for this procedure events: Query<(Entity, &Handle, &Name), (With, Added>)>, // Only process newly created pieces (we do not delete pieces at runtime) query: Query<&Dissolvable>, // Children of pieces are the actual meshes that need materials parents: Query<&Parent>, // Used to create DissolveMaterial standard_materials: Res>, // Used to create Handle mut dissolve_materials: ResMut>, // Used to insert Handle; mut commands: Commands, ) { debug!("Setting up dissolve materials..."); events .iter() // Handle this entity (mesh) .for_each(|(child, std_handle, name)| { if let Some(dissolvable) = query .iter_many(parents.iter_ancestors(child)) .next() { debug!("Setting up dissolve material for {:?} {:?}", name, child); // Extension we will add to existing gltf-sourced materials let extension = DissolveExtension { percentage: dissolvable.start, }; // Base material we will extend for the duration of the dissolve effect let mut base: StandardMaterial = standard_materials .get(std_handle) .expect("Resolve material data") .clone(); base.alpha_mode = AlphaMode::Mask(1.0); // base.base_color = base.base_color.clone().with_a(0.0); let dis_handle = dissolve_materials.add(ExtendedMaterial { base, extension }); // Add the dissolve handle as a Backup(T) commands .entity(child) .insert(dis_handle.clone()) .remove::>(); } }); } /// When a piece is captured... /// 1. Play a cool "captured" animation and a neat sound fn capture_piece_start( events: Query<(Entity, &Dissolvable), Added>, mut commands: Commands, ) { events.iter().for_each(|(entity, dissolvable)| { commands .entity(entity) .insert(Dissolving::Out(dissolvable.duration)); }); } /// Once done fading out: /// 1. Move piece to side of board /// 2. Remove BeingCaptured component /// 3. Add Captured marker fn capture_piece_end( mut events: RemovedComponents, mut query: Query<(Entity, &Dissolvable, &Side, &mut Transform), With>, mut commands: Commands, board: Res, score: Res, ) { events.read().for_each(|e| { if let Ok((entity, dissolvable, side, mut transform)) = query.get_mut(e) { transform.translation = capture_translation(side, score.captures(!*side)); commands .entity(entity) .insert(Dissolving::In(dissolvable.duration)) .remove::() .insert(Captured { epoch: board.current_epoch() - 1 }); } }); } fn debug_selected( query: Query<(Entity, &BoardIndex, &Piece, &Side), With>, mut debug_info: ResMut, ) { query.iter().for_each(|(e, bi, p, s)| { debug_info.set( "Active".into(), format!( "\n>>ID: {:?}\n>>Piece: {:?}\n>>Side: {:?}\n>>Index: {:?}", e, p, s, bi ), ); }); } fn debug_textures(mut events: EventReader>) { events.read().for_each(|event| { debug!("Material Event: {:?}", event); }); } // Marker for entities which are currently animating #[derive(Component, Debug)] pub(crate) struct Animating; fn monitor_animations( active: Query<(Entity, &AnimationPlayer), (Changed, With)>, mut inactive: Query< (Entity, &mut AnimationPlayer), (Changed, Without), >, mut commands: Commands, ) { // Remove Animating component from players that are done active.iter().for_each(|(entity, player)| { if player.is_finished() { 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() { debug!( "Entity {:?} is playing {:?}, adding animating marker", entity, player.animation_clip() ); commands.entity(entity).insert(Animating); player.set_speed(1.0); } }); } fn continue_title(mut next_state: ResMut>) { next_state.set(GameState::Play) } /// When a piece is tagged with `Dissolving` play the dissolve animation /// This is done by updating the Material and referencing the `Dissolvable` tag /// Calculating how far along the animation it should be update the material's percentage /// Materials are on the children of the tagged entity fn dissolve_animation( mut query: Query<(Entity, &Dissolvable, &mut Dissolving, &mut Visibility)>, children: Query<&Children>, mut dissolve_materials: ResMut>, object_materials: Query<(Entity, &Handle)>, mut commands: Commands, time: Res