diff --git a/src/.display3d.rs.rustfmt b/src/.display3d.rs.rustfmt new file mode 100644 index 0000000..351b848 --- /dev/null +++ b/src/.display3d.rs.rustfmt @@ -0,0 +1,609 @@ +// TODO: Camera Animations +// Intro animation +// Turn changing animations +// TODO: Pickup/put-down sound in 3d +// TODO: Background during menu + +use crate::{ + game::{Board, BoardIndex, Piece, Side}, + prelude::*, +}; +use bevy::{ + core_pipeline::{tonemapping::DebandDither, Skybox}, + input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + render::{ + render_resource::{TextureViewDescriptor, TextureViewDimension}, + view::ColorGrading, + }, + window::PrimaryWindow, +}; + +pub(crate) struct Display3dPlugin; + +impl Plugin for Display3dPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(GameState::Loading), load_assets) + .add_systems( + OnExit(GameState::Loading), + (initialize, fix_skybox.before(initialize)), + ) + .add_systems( + Update, + ( + hydrate_camera.run_if(any_component_added::), + set_piece_model.run_if(any_component_added::), + set_board_model.run_if(any_component_added::), + set_tile_hitbox.run_if(any_component_added::), + set_piece_position.run_if(any_component_changed::), + set_piece_texture.run_if(any_component_changed::), + select + .run_if(in_state(GameState::Play)) + .run_if(in_state(DisplayState::Display3d)) + .run_if(on_event::()), + pick_up.run_if(any_component_added::), + put_down.run_if(any_component_removed::()), + ), + ) + .add_systems( + Update, + ( + move_camera.run_if(on_event::()), + mouse_zoom.run_if(on_event::()), + gizmo_system, + selected_gizmo, + moves_gizmo, + ) + .run_if(resource_exists::()) + .run_if(in_state(GameState::Play)) + .run_if(in_state(DisplayState::Display3d)), + ) + .add_systems( + OnEnter(DisplayState::Display3d), + (activate::, set_piece_texture), + ) + .add_systems(OnExit(DisplayState::Display3d), deactivate::); + } +} + +#[derive(Debug, Component)] +pub(crate) struct Display3d; + +#[derive(Debug, Resource)] +struct AssetsMap { + models: Handle, + skybox: Handle, + 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( + server: Res, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let hitbox_shape = meshes.add(shape::Box::new(1.0, 0.1, 1.0).into()); + 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 { + models: server.load("models/Martian Chess.glb"), + skybox: server.load("images/skybox.png"), + 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, + )) + .with_children(|parent| { + info!("Initializing 3D lights!"); + parent.spawn(( + Display3d, + PointLightBundle { + point_light: PointLight { + intensity: 3000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(0.0, 10.0, 5.0), + ..default() + }, + )); + + info!("Intializing 3D Board!"); + parent + .spawn((Display3d, game::BoardComponent, SceneBundle { ..default() })) + .with_children(|parent| { + // Hitboxes + game::tiles().for_each(|(index, tile)| { + parent.spawn(( + Display3d, + index, + tile, + PbrBundle { + mesh: assets.hitbox_shape.clone(), + material: assets.hitbox_material.clone(), + visibility: Visibility::Hidden, + ..default() + }, + game::Selectable, + )); + }); + + // Pieces + board.pieces().iter().for_each(|(index, piece)| { + let side = Board::side(*index).expect("Spawn valid side"); + + parent.spawn(( + side, + Display3d, + piece.clone(), + index.clone(), + SceneBundle { ..default() }, + game::Selectable, + )); + }); + }); + }); +} + +fn hydrate_camera( + events: Query<(&Name, Entity), Added>, + assets: Res, + mut commands: Commands, +) { + events + .iter() + .filter(|(name, _)| name.as_str() == "GameCam") + .for_each(|(_, entity)| { + info!("Initialize 3d camera"); + commands.entity(entity).insert(( + Display3d, + Camera3dBundle { + camera: Camera { + is_active: false, + hdr: true, + ..default() + }, + transform: Transform::from_xyz(0.0, 12.5, 6.0).looking_at(Vec3::ZERO, Vec3::Y), + dither: DebandDither::Enabled, + color_grading: ColorGrading { ..default() }, + ..default() + }, + Skybox(assets.skybox.clone()), + EnvironmentMapLight { + diffuse_map: assets.skybox.clone(), + specular_map: assets.skybox.clone(), + }, + UiCameraConfig { show_ui: true }, + FogSettings { + color: Color::WHITE, + falloff: FogFalloff::from_visibility_colors(100.0, Color::NONE, Color::NONE), + ..default() + }, + )); + }); +} + +fn fix_skybox(mut images: ResMut>, assets: Res) { + let image = images.get_mut(&assets.skybox).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() + }); + } +} + +/// Set the model for each piece based on the game::Piece::* marker +fn set_piece_model( + mut events: Query<(&mut Handle, &Piece), (Added, With)>, + assets_map: Res, + gltfs: Res>, +) { + events.iter_mut().for_each(|(mut handle, piece)| { + let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); + *handle = match piece { + game::Piece::Pawn => gltf.named_scenes.get("Pawn"), + game::Piece::Drone => gltf.named_scenes.get("Drone"), + game::Piece::Queen => gltf.named_scenes.get("Queen"), + } + .expect("Game board model") + .clone(); + }); +} + +fn set_board_model( + mut events: Query<&mut Handle, (With, With)>, + assets_map: Res, + gltfs: Res>, +) { + events.iter_mut().for_each(|mut handle| { + let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); + *handle = gltf + .named_scenes + .get("Gameboard") + .expect("Game board model") + .clone(); + }) +} + +/// Sets a piece location given it's board index +fn set_piece_position( + mut events: Query< + (&mut Transform, &BoardIndex), + (With, With, Changed), + >, +) { + events.iter_mut().for_each(|(mut t, i)| { + t.translation = board_translation(i); + }); +} + +/// 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.325 + } else { + x as f32 * 1.3 + 1.0 + }; + + let y = y as f32 * 1.3 + 0.65; + + Vec3::new(x, 0.0, y) +} + +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.iter().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.iter().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); + }); + }); +} + +/// Set the Texture for a piece given it's position (left or right) on the bord. +/// Executed when Side is changed or upon entry to Display3d state +/// Getting this to run _after_ GLTF is loaded is a pain. +/// PERF: We are saving what to work on in a Vector which is bad. +/// CAVEAT: We are only exeucting this when a piece changes or state is changed. +fn set_piece_texture( + events: Query<(Entity, &Piece, &Side), (With, With, Changed)>, + all: Query<(Entity, &Piece, &Side), (With, With)>, + gltfs: Res>, + assets_map: Res, + children: Query<&Children>, + mut models: Query<(&Name, &mut Handle)>, +) { + let pieces = if events.is_empty() { + all.iter().collect::>() + } else { + events.iter().collect::>() + }; + pieces.iter().for_each(|(entity, piece, side)| { + if let Some(gltf) = gltfs.get(&assets_map.models) { + children.iter_descendants(*entity).for_each(|child| { + if let Ok((n, mut m)) = models.get_mut(child) { + match (*piece, *side, n.as_str()) { + (Piece::Queen, Side::A, "Queen.0") => { + *m = gltf + .named_materials + .get("Queen") + .expect("Load Red Queen texture") + .clone() + } + (Piece::Queen, Side::A, "Queen.1") => { + *m = gltf + .named_materials + .get("Dots") + .expect("Load Red Dots texture") + .clone() + } + (Piece::Queen, Side::B, "Queen.0") => { + *m = gltf + .named_materials + .get("QueenBlue") + .expect("Load Blue Queen texture") + .clone() + } + (Piece::Queen, Side::B, "Queen.1") => { + *m = gltf + .named_materials + .get("DotsBlue") + .expect("Load Red Dots texture") + .clone() + } + (Piece::Drone, Side::A, "Drone.0") => { + *m = gltf + .named_materials + .get("Drone") + .expect("Load Red Drone texture") + .clone() + } + (Piece::Drone, Side::A, "Drone.1") => { + *m = gltf + .named_materials + .get("Dots") + .expect("Load Red Dots texture") + .clone() + } + (Piece::Drone, Side::B, "Drone.0") => { + *m = gltf + .named_materials + .get("DroneBlue") + .expect("Load Blue Drone texture") + .clone() + } + (Piece::Drone, Side::B, "Drone.1") => { + *m = gltf + .named_materials + .get("DotsBlue") + .expect("Load Blue Dots texture") + .clone() + } + (Piece::Pawn, Side::A, "Pawn.0") => { + *m = gltf + .named_materials + .get("Pawn") + .expect("Load Red Pawn texture") + .clone() + } + (Piece::Pawn, Side::A, "Pawn.1") => { + *m = gltf + .named_materials + .get("Dots") + .expect("Load Red Dots texture") + .clone() + } + (Piece::Pawn, Side::B, "Pawn.0") => { + *m = gltf + .named_materials + .get("DroneBlue") // TODO: FIX + .expect("Load Blue Pawn texture") + .clone() + } + (Piece::Pawn, Side::B, "Pawn.1") => { + *m = gltf + .named_materials + .get("DotsBlue") + .expect("Load Blue Dots texture") + .clone() + } + _ => warn!("???"), + } + } + }); + } + }) +} + +/// 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, With)>, + children: Query<&Children>, + mut commands: Commands, + selected: Query< + Entity, + ( + With, + With, + With, + ), + >, +) { + events + .iter() + .filter(|ev| ev.state == ButtonState::Pressed) + .for_each(|_| { + windows.iter().for_each(|window| { + window.cursor_position().and_then(|pos| { + cameras.iter().for_each(|(camera, gt)| { + camera.viewport_to_world(gt, pos).and_then(|ray| { + query + .iter() + .filter_map(|(entity, handle, gt)| { + meshes.get(handle).map(|mesh| (entity, mesh, gt)) + }) + .for_each(|(entity, mesh, gt)| { + hit::intersects3d(&ray, mesh, >).and_then(|_hit| { + selectable + .iter() + .find(|&e| { + // A child was hit (pieces) + let primary = entity == e; + // This entity was hit (tile hitboxes) + let secondary = children + .iter_descendants(e) + .any(|child| child == entity); + + primary || secondary + }) + .iter() + .for_each(|&e| { + commands.entity(e).insert(game::Selected); + }); + Some(()) + }); + }); + Some(()) + }); + }); + Some(()) + }); + }); + }); +} + +fn selected_gizmo( + selected: Query<&Transform, (With, With)>, + mut gizmos: Gizmos, +) { + selected.iter().for_each(|transform| { + gizmos.cuboid(transform.clone(), Color::GREEN); + }) +} + +fn moves_gizmo( + selected: Query<&BoardIndex, (With, With, With)>, + board: Res, + mut gizmos: Gizmos, +) { + selected.iter().for_each(|idx| { + board + .possible_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), + >, + assets_map: Res, + gltfs: Res>, + children: Query<&Children>, + mut players: Query<&mut AnimationPlayer>, +) { + events.iter_mut().for_each(|(entity, piece)| { + let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); + children.iter_descendants(entity).for_each(|child| { + if let Ok(mut player) = players.get_mut(child) { + let animation = match piece { + game::Piece::Queen => gltf.named_animations.get("QueenPickup"), + game::Piece::Drone => gltf.named_animations.get("DronePickup"), + game::Piece::Pawn => gltf.named_animations.get("PawnPickup"), + }; + let idle = match piece { + game::Piece::Queen => gltf.named_animations.get("QueenIdle"), + game::Piece::Drone => gltf.named_animations.get("DroneIdle"), + game::Piece::Pawn => gltf.named_animations.get("PawnIdle"), + }; + player + .start_with_transition( + animation.expect("Pickup Animation").clone(), + Duration::from_secs_f32(0.75), + ) + .start_with_transition( + idle.expect("Idle animation").clone(), + Duration::from_secs_f32(1.5), + ) + .repeat(); + } + }) + }); +} + +fn put_down( + mut events: RemovedComponents, + mut query: Query<&game::Piece, (With, With, With)>, + assets_map: Res, + gltfs: Res>, + children: Query<&Children>, + mut players: Query<&mut AnimationPlayer>, +) { + events.iter().for_each(|entity| { + if let Ok(piece) = query.get_mut(entity) { + let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); + children.iter_descendants(entity).for_each(|child| { + if let Ok(mut player) = players.get_mut(child) { + let animation = match piece { + game::Piece::Queen => gltf.named_animations.get("QueenPutDown"), + game::Piece::Drone => gltf.named_animations.get("DronePutDown"), + game::Piece::Pawn => gltf.named_animations.get("PawnPutDown"), + }; + player + .start_with_transition( + animation.expect("PutDown Animation").clone(), + Duration::from_secs_f32(0.75), + ) + .stop_repeating(); + } + }) + } + }) +} + +fn set_tile_hitbox( + mut events: Query<(&mut Transform, &BoardIndex), (With, Added)>, +) { + events.iter_mut().for_each(|(mut transform, index)| { + *transform = Transform::from_translation(board_translation(index)); + }); +} diff --git a/src/display3d.rs b/src/display3d.rs index 351b848..f130f17 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -456,7 +456,7 @@ fn select( selectable: Query, With)>, children: Query<&Children>, mut commands: Commands, - selected: Query< + _selected: Query< Entity, ( With, @@ -545,6 +545,7 @@ fn pick_up( let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); children.iter_descendants(entity).for_each(|child| { if let Ok(mut player) = players.get_mut(child) { + info!("Picking up {:?} {:?}", entity, piece); let animation = match piece { game::Piece::Queen => gltf.named_animations.get("QueenPickup"), game::Piece::Drone => gltf.named_animations.get("DronePickup"), @@ -583,6 +584,7 @@ fn put_down( let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content"); children.iter_descendants(entity).for_each(|child| { if let Ok(mut player) = players.get_mut(child) { + info!("Putting down {:?}", entity); let animation = match piece { game::Piece::Queen => gltf.named_animations.get("QueenPutDown"), game::Piece::Drone => gltf.named_animations.get("DronePutDown"), diff --git a/src/game.rs b/src/game.rs index 29ec82b..e42c506 100644 --- a/src/game.rs +++ b/src/game.rs @@ -12,12 +12,12 @@ impl Plugin for GamePlugin { Update, ( menu::exit_to_menu.run_if(in_state(GameState::Play)), - update_board.run_if(on_event::()), + update_board.run_if(on_event::()).after(select_sync), set_side.run_if(on_event::()), // TODO: correct run_if? cancel_place.run_if(|buttons: Res>| -> bool { buttons.just_pressed(MouseButton::Right) }), - select_sync.run_if(any_component_added::), + select_sync.run_if(any_component_added::).after(deselect_sync), deselect_sync.run_if(any_component_removed::()), move_piece.run_if(any_component_added::), capture_piece.run_if(any_component_added::), @@ -30,13 +30,13 @@ impl Plugin for GamePlugin { .run_if(any_component_added::), ), ) - .add_systems( - PostUpdate, - ( - asserts::, - asserts::, - ), - ) + // .add_systems( + // PreUpdate, + // ( + // asserts::, + // asserts::, + // ), + // ) .add_systems( PostUpdate, (debug_board.run_if(resource_exists::()),), @@ -162,7 +162,7 @@ impl Board { match self.at(from.clone()) { Some(from_val) => { // The current epoch is the last epoch + 1 - let epoch = self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1; + let epoch = self.current_epoch(); // Local moves vec we can return let mut moves = vec![]; @@ -258,6 +258,10 @@ impl Board { None => std::iter::empty().collect(), } } + + pub(crate) fn current_epoch(&self) -> usize { + self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1 + } } impl std::fmt::Display for Board { @@ -340,7 +344,6 @@ fn debug_board(board: Res, mut debug_info: ResMut) { } /// Update this method to use a diff between the board and the state of the 2d/3d worlds -/// Only update the tiles without a corresponding board piece pub(crate) fn update_board( mut events: EventReader, mut pieces: Query<(Entity, &mut BoardIndex), With>, @@ -352,9 +355,11 @@ pub(crate) fn update_board( if *index == *from { match to { Some(to_idx) => { + info!("Moving piece {:?} to {:?}", entity, to_idx); *index = to_idx.clone(); } None => { + info!("Capturing piece {:?}", entity); commands .entity(entity) .remove::() @@ -364,6 +369,7 @@ pub(crate) fn update_board( } }); selected.iter().for_each(|entity| { + info!("De-selecting selected piece {:?}", entity); commands.entity(entity).remove::(); }); }) @@ -411,6 +417,7 @@ fn deselect_sync( .iter() .find(|(e, idx)| *idx == this_idx && *e != this_e) { + info!("De-selecting entangled piece {:?}", entity); commands.entity(entangled).remove::(); } } @@ -472,6 +479,7 @@ fn null_selections( ) { events.iter().for_each(|entity| { if selected_pieces.is_empty() { + info!("De-selecting piece that should not be selected {:?}", entity); commands.entity(entity).remove::(); writer.send(audio::AudioEvent::PutDown); } @@ -492,11 +500,11 @@ fn asserts( selected_tiles: Query, With, With)>, ) { if selected_pieces.iter().len() > 2 { - panic!("More than two piece is selected"); + panic!("More than two pieces selected"); } if selected_tiles.iter().len() > 2 { panic!( - "More than two tile is selected {:?}", + "More than two tiles selected {:?}", selected_tiles.iter().len() ); } diff --git a/src/hit.rs b/src/hit.rs index 063e57d..442bd01 100644 --- a/src/hit.rs +++ b/src/hit.rs @@ -11,14 +11,14 @@ use crate::prelude::*; /// Hit data for 2d sprites #[derive(Debug)] pub(crate) struct Hit2d { - point: Vec2, + _point: Vec2, } /// Hit data for 3d objects in Global (not Local) space and the distance from the Camera #[derive(Debug)] pub(crate) struct Hit3d { distance: f32, - point: Vec3, + _point: Vec3, } /// A 3D Triangle used for ray-intersection tests @@ -106,7 +106,7 @@ pub(crate) fn intersects3d(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Opti }; hit.then_some(Hit3d { distance: d, - point: p, + _point: p, }) } else { None diff --git a/src/main.rs b/src/main.rs index df1a610..c7d4507 100644 --- a/src/main.rs +++ b/src/main.rs @@ -170,7 +170,7 @@ pub(crate) fn any_component_added(q: Query>) -> b !q.is_empty() } -pub(crate) fn any_component_added_or_changed( +pub(crate) fn _any_component_added_or_changed( q: Query, Changed)>>, ) -> bool { !q.is_empty()