use crate::prelude::*; pub(crate) struct Display3dPlugin; impl Plugin for Display3dPlugin { fn build(&self, app: &mut App) { app.add_plugins(( TemporalAntiAliasPlugin, MaterialPlugin::::default(), )) .insert_resource(Msaa::Off) .add_systems( OnExit(GameState::Loading), ( fix_skybox.before(initialize), initialize, update_tweaks.run_if(resource_exists::), ), ) .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::), update_pieces.run_if( resource_exists::.and_then( any_component_changed::() .or_else(any_component_changed::()) .or_else(any_component_changed::()) .or_else(any_component_removed::()) .or_else(any_component_removed::()), ), ), set_board_model.run_if(any_component_added::()), set_valid_move_model.run_if(any_component_added::()), set_tile_hitbox.run_if(any_component_added::()), select .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)) .run_if(just_pressed(MouseButton::Left)), pick_up.run_if(any_component_added::()), put_down.run_if(any_component_removed::()), setup_capture_piece.run_if(any_component_added::>().or_else(any_component_changed::>())), capture_piece.run_if(any_with_component::), skip_animation .run_if(just_pressed(KeyCode::Enter).or_else(just_pressed(MouseButton::Left))) .run_if(in_state(GameState::Play)), monitor_animations.run_if(in_state(GameState::Play)), ), ) .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(resource_exists::) .run_if(in_state(GameState::Play)) .run_if(in_state(DisplayState::Display3d)), ) .add_systems( OnEnter(GameState::Intro), ( // Toggle hidden/visible 3d entities manage_state_entities::(), ), ) .add_systems( OnEnter(GameState::Play), ( update_pieces.run_if(resource_exists::), update_tweaks.run_if(resource_exists::), opening_animation .run_if(run_once()) .run_if(in_state(DisplayState::Display3d)), ), ); } } #[derive(Debug, Component, PartialEq)] pub(crate) struct Display3d; #[derive(Debug, Resource, Clone)] struct AssetsMap { hitbox_shape: Handle, hitbox_material: 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>, ) { 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() }); commands.insert_resource(AssetsMap { hitbox_shape, hitbox_material, }); } /// Initialize the 3d board fn initialize(mut commands: Commands, board: Res, assets: Res) { info!("Initializing root"); commands .spawn(( SpatialBundle { visibility: Visibility::Hidden, ..default() }, Display3d, DisplayState::Display3d, )) .with_children(|parent| { info!("Intializing 3D Board!"); parent .spawn(( Display3d, DisplayState::Display3d, game::BoardComponent, SceneBundle { ..default() }, )) .with_children(|parent| { // 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, ..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, index, SceneBundle { visibility: Visibility::Hidden, transform: Transform::from_translation(board_translation(&index)), ..default() }, side, game::ValidMove, )); }); // Pieces board.pieces().iter().for_each(|(index, piece)| { let side = Board::side(*index).expect("Spawn valid side"); parent.spawn(( side, DisplayState::Display3d, Display3d, *piece, *index, SceneBundle { ..default() }, game::Selectable, )); }); }); }); } 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)| { info!("Initialize 3d camera"); let skybox_handle = tweak .get_handle::("display3d_models_skybox_file") .unwrap(); info!("Hydrating camera {:?}", entity); // Populate the components for the camera commands.entity(entity).insert(( DisplayState::Display3d, Display3d, Camera3dBundle { camera: Camera { is_active: true, hdr: tweak.get::("display3d_hdr").unwrap(), ..default() }, dither: DebandDither::Enabled, color_grading: ColorGrading { ..default() }, ..default() }, BloomSettings { ..default() }, Skybox { image: skybox_handle.clone(), brightness: 150.0, }, EnvironmentMapLight { diffuse_map: skybox_handle.clone(), specular_map: skybox_handle.clone(), intensity: 1.0, }, FogSettings { ..default() }, ScreenSpaceAmbientOcclusionBundle { ..default() }, TemporalAntiAliasSettings { ..default() }, MotionVectorPrepass, // Name::new("3D Camera"), )); 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< ( Entity, &mut FogSettings, &mut ColorGrading, &mut Tonemapping, &mut BloomSettings, ), With, >, tweaks_file: Res, tweaks: Res>, mut commands: Commands, ) { if let Some(tweak) = tweaks.get(tweaks_file.handle.clone()) { warn!("Updating tweaks!"); camera_settings.iter_mut().for_each( |(entity, mut fog, mut color_grading, mut tonemapping, mut bloom)| { *fog = tweak .get::("display3d_fog") .unwrap() .into(); *color_grading = tweak .get::("display3d_color_grading") .unwrap() .into(); *tonemapping = tweak .get::("display3d_color_tonemapping") .unwrap() .into(); *bloom = tweak .get::("display3d_bloom") .unwrap() .into(); let quality_level = tweak .get::( "display3d_ssao_quality_level", ); match quality_level { Some(quality_level) => { commands .entity(entity) .insert(ScreenSpaceAmbientOcclusionSettings { quality_level: quality_level.into(), }); commands.insert_resource(Msaa::Off); } None => { commands .entity(entity) .remove::(); let msaa: Msaa = tweak .get::("display3d_msaa") .unwrap() .into(); commands.insert_resource(msaa); } } }, ); } } 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(); info!("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() }); } } fn set_board_model( mut boards: Query<&mut Handle, (Added, With)>, gltfs: Res>, tweaks: Res>, tweaks_file: Res, ) { let tweak = tweaks .get(tweaks_file.handle.clone()) .expect("Load tweakfile"); boards.iter_mut().for_each(|mut handle| { info!("Setting board model"); let assets_handle = tweak .get_handle::("display3d_models_assets_file") .unwrap(); let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); *handle = gltf .named_scenes .get( tweak .get::("display3d_models_scenes_board") .unwrap() .as_str(), ) .expect("Game board model") .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 { info!("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<( Entity, &Piece, &Side, &BoardIndex, &mut Transform, &mut Handle, )>, 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 models = gltfs.get(assets_handle).unwrap(); query.iter_mut().for_each( |(entity, piece, side, board_index, mut transform, mut scene)| { // Set position of piece let new_translation = board_translation(board_index); if transform.translation != new_translation { info!("Updating piece transform"); transform.translation = new_translation; } // Check if any children are animating let animating = active_animation_players .iter_many(children.iter_descendants(entity)) .count(); if animating > 0 { debug!("Piece {:?} is animating. Skipping...", entity); } else { debug!("Checking piece object scene for {:?}", entity); // Find name of this piece's model scene let scene_tweak_name: Option = match (piece, side) { (Piece::Pawn, Side::A) => tweak.get("display3d_models_scenes_pawn_red"), (Piece::Pawn, Side::B) => tweak.get("display3d_models_scenes_pawn_blue"), (Piece::Drone, Side::A) => tweak.get("display3d_models_scenes_drone_red"), (Piece::Drone, Side::B) => tweak.get("display3d_models_scenes_drone_blue"), (Piece::Queen, Side::A) => tweak.get("display3d_models_scenes_queen_red"), (Piece::Queen, Side::B) => tweak.get("display3d_models_scenes_queen_blue"), }; // Get model scene for this piece let scene_handle = models.named_scenes.get(&scene_tweak_name.unwrap()).unwrap(); // Set scene model for this piece if *scene != *scene_handle { warn!("Updating scene for piece {:?}", entity); *scene = scene_handle.clone(); } } }, ); } /// Select tiles and pieces in 3d /// There is a bug where we are selecting multiple entities... /// TODO: Selectable generalize picking pieces **and** hitboxes fn select( mut events: EventReader, query: Query<(Entity, &Handle, &GlobalTransform)>, meshes: Res>, cameras: Query<(&Camera, &GlobalTransform)>, windows: Query<&Window, With>, selectable: Query<(Entity, &BoardIndex, &Side), (With, With)>, selected: Query>, children: Query<&Children>, mut selections: EventWriter, state: Res>, ) { // For every mouse click event events .read() // Only read button presses .filter(|ev| ev.state == ButtonState::Pressed && ev.button == MouseButton::Left) .for_each(|_| { // 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 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, &board_index, &side)| { // Check the side of the selection if no piece is selected // Otherwise this is fine, select away let side_check = !selected.is_empty() || state.get().0 == side; 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 (side_check && hit_check).then_some(board_index) }) .map(|board_index| { // Include the hit from the camera (hit, board_index) }) }) // 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() }) // Iterate over the 0 or 1 outcomes of the above .iter() // Send an event that this board index was selected .for_each(|(_, board_index)| { info!("Board index selected: {:?}", board_index); selections.send(game::Selection(*board_index)); }); } }); } }); }); } 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 set_valid_move_model( mut events: Query< (&mut Handle, &mut Visibility), (With, Added), >, 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(); if let Some(gltf) = gltfs.get(assets_handle) { info!("Setting valid move model"); events.iter_mut().for_each(|(mut handle, mut visibility)| { *handle = gltf .named_scenes .get( tweak .get::("display3d_models_scenes_valid_move") .unwrap() .as_str(), ) .unwrap() .clone(); *visibility = Visibility::Hidden; }) } } fn pick_up( mut events: Query< (Entity, &game::Piece, &game::Side), (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, side)| { info!("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); children.iter_descendants(entity).for_each(|child| { info!(" Child: {:?}", child); if let Ok((name, mut player)) = players.get_mut(child) { info!("Picking up {:?} ({:?}) {:?}", name, entity, piece); let pickup_animation = format!( "display3d_models_animations_pick_up_{:?}_{}", piece, side.color_str() ) .to_ascii_lowercase(); 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_{:?}_{}", piece, side.color_str() ) .to_ascii_lowercase(); 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 put_down( mut events: RemovedComponents, mut query: Query< (&game::Piece, &game::Side), (With, 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((piece, side)) = query.get_mut(entity) { info!("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) { info!("Putting down {:?}", entity); let putdown_animation = format!( "display3d_models_animations_put_down_{:?}_{}", piece, side.color_str() ) .to_ascii_lowercase(); 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) { info!("Compatible with put-down clip!"); player .start_with_transition( putdown_handle.clone(), Duration::from_secs_f32(0.75), ) .set_repeat(RepeatAnimation::Never); } } } }) } }) } fn set_tile_hitbox( mut events: Query<(&mut Transform, &BoardIndex), (With, Added)>, ) { events.iter_mut().for_each(|(mut transform, index)| { info!("Setting tile hitbox"); *transform = Transform::from_translation(board_translation(index)); }); } fn opening_animation(mut players: Query<&mut AnimationPlayer, (With, With)>) { players.iter_mut().for_each(|mut player| { info!("Playing intro camera animation"); player.resume() }); } // When called skips any running animations fn skip_animation( mut players: Query<&mut AnimationPlayer>, clips: Res>, time: Res