diff --git a/client/src/assets/block_visuals.rs b/client/src/assets/block_visuals.rs index fd3551b..d60ab4f 100644 --- a/client/src/assets/block_visuals.rs +++ b/client/src/assets/block_visuals.rs @@ -23,16 +23,16 @@ struct BuiltinBlockMeshes { #[derive(Asset, TypePath)] pub struct BlockVisuals { - _id: Identifier, + id: Identifier, color: Color, material: Handle, // mesh: Handle, } impl BlockVisuals { - // pub fn id(&self) -> &Identifier { - // &self.id - // } + pub fn id(&self) -> &Identifier { + &self.id + } pub fn color(&self) -> Color { self.color @@ -81,7 +81,7 @@ impl AssetLoader for BlockVisualsLoader { let id = load_context.path().try_into()?; Ok(BlockVisuals { - _id: id, + id, color, material, }) diff --git a/client/src/input/client_inputs.rs b/client/src/input/client_inputs.rs index 550c6cc..a99cfc4 100644 --- a/client/src/input/client_inputs.rs +++ b/client/src/input/client_inputs.rs @@ -8,6 +8,8 @@ use lightyear::prelude::input::native::*; use bevy::window::{CursorGrabMode, CursorOptions}; +use crate::ui::block_selection::SelectedBlock; + pub(super) fn plugin(app: &mut App) { app.init_resource::(); app.add_systems( @@ -28,10 +30,14 @@ struct CurrentAction(Action); fn place_or_break_blocks( mut event: On>, + selected_block: Option>, mut current_action: ResMut, cursor: Single<&CursorOptions>, blocks: Blocks, ) { + let Some(selected_block) = selected_block else { + return; // No block selected. + }; let is_place = match event.button { PointerButton::Primary => false, // left-click PointerButton::Secondary => true, // right-click @@ -50,8 +56,7 @@ fn place_or_break_blocks( current_action.0 = if is_place { // FIXME: This only works for axis-aligned normals. let offset = normal.normalize().round().as_ivec3(); - // TODO: Don't hardcode block type. - Action::PlaceBlock(block_pos + offset, Block::DEFAULT) + Action::PlaceBlock(block_pos + offset, selected_block.clone()) } else { Action::BreakBlock(block_pos) }; diff --git a/client/src/main.rs b/client/src/main.rs index 5aeb8ea..2a6c0b5 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -104,10 +104,11 @@ fn start_server_or_connect(args: Res, mut commands: Commands) -> Result { /// When the server is started, spawn the initial blocks the world is made of. fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { + const PLATFORM: Identifier = Identifier::new_const("vampire_black"); for x in -8..8 { for z in -8..8 { let pos = BlockPos::new(x, 0, z); - blocks.spawn(pos, Block::PLATFORM).unwrap(); + blocks.spawn(pos, PLATFORM.clone()).unwrap(); } } } diff --git a/client/src/ui/block_selection.rs b/client/src/ui/block_selection.rs new file mode 100644 index 0000000..398f4cd --- /dev/null +++ b/client/src/ui/block_selection.rs @@ -0,0 +1,150 @@ +use bevy::color::palettes::css::*; +use bevy::prelude::*; +use common::prelude::*; + +use bevy::window::{CursorGrabMode, CursorOptions}; + +use crate::assets::block_visuals::BlockVisuals; + +const SCALE: i32 = 4; + +pub(super) fn plugin(app: &mut App) { + let blocks_changed = Manifest::::changed; + app.add_systems(PostUpdate, setup_selection_ui.run_if(blocks_changed)); + app.add_systems(Update, update_selection_visibility); + let selection_changed = resource_exists_and_changed::; + app.add_systems(Update, update_selected_cell.run_if(selection_changed)); +} + +#[derive(Resource, Deref, DerefMut)] +pub struct SelectedBlock(Identifier); + +#[derive(Component)] +struct BlockSelectionLayout; + +#[derive(Component)] +#[require(Button, Pickable)] +struct BlockSelectionCell { + id: Identifier, +} + +fn setup_selection_ui( + existing: Option>>, + blocks: Manifest, + mut commands: Commands, +) { + // Despawn previous block selection UI, if present. + if let Some(existing) = existing { + commands.entity(*existing).despawn(); + } + + commands + .spawn(( + BlockSelectionLayout, + Node { + width: percent(100), + height: percent(100), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + )) + .with_children(|builder| { + builder + .spawn(( + Pickable::default(), + Node { + display: Display::Grid, + padding: UiRect::all(px(4 * SCALE)), + grid_template_columns: RepeatedGridTrack::auto(9), + overflow: Overflow::scroll_y(), + ..default() + }, + BackgroundColor(BLACK.with_alpha(0.4).into()), + )) + .observe(|mut event: On>| { + // Ensure the event doesn't propagate up to the + // `Window`, which would grab the mouse cursor. + event.propagate(false); + }) + .with_children(|builder| { + for block in blocks.iter() { + spawn_block_cell(builder, block); + } + }); + }); +} + +fn spawn_block_cell(builder: &mut ChildSpawnerCommands, block: &BlockVisuals) { + let id = block.id().clone(); + builder + .spawn(( + BlockSelectionCell { id }, + Node { + width: px(16 * SCALE), + height: px(16 * SCALE), + margin: UiRect::all(px(2 * SCALE)), + padding: UiRect::all(px(2 * SCALE)), + border: UiRect::all(px(2 * SCALE)), + ..default() + }, + BackgroundColor(block.color()), + BorderColor::all(BLACK), + )) + .observe(on_cell_click) + .observe(on_cell_over) + .observe(on_cell_out); +} + +// TODO: Ideally visibility should be toggled when pressing `E`, but this works for now. +fn update_selection_visibility( + cursor: Single<&CursorOptions, Changed>, + crosshair: Single<&mut Visibility, With>, +) { + let is_grabbed = cursor.grab_mode != CursorGrabMode::None; + let mut selection_visibility = crosshair.into_inner(); + *selection_visibility = is_grabbed.then_some(Visibility::Hidden).unwrap_or_default(); +} + +fn update_selected_cell( + cells: Query<(&BlockSelectionCell, &mut BorderColor)>, + selected_block: Res, +) { + for (cell, mut color) in cells { + let is_selected = cell.id == **selected_block; + *color = if is_selected { WHITE } else { BLACK }.into(); + } +} + +fn on_cell_click( + event: On>, + mut cells: Query<&BlockSelectionCell>, + mut commands: Commands, +) { + // SAFETY: Entity is known to have this component. + let cell = cells.get_mut(event.entity).unwrap(); + // Resource mightn't've been added yet, so let's just insert / replace it. + commands.insert_resource(SelectedBlock(cell.id.clone())); +} + +fn on_cell_over( + event: On>, + mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>, + selected_block: Option>, +) { + // SAFETY: Entity is known to have these components. + let (cell, mut color) = cells.get_mut(event.entity).unwrap(); + let is_selected = selected_block.is_some_and(|s| cell.id == **s); + *color = if is_selected { WHITE } else { GRAY }.into(); +} + +fn on_cell_out( + event: On>, + mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>, + selected_block: Option>, +) { + // SAFETY: Entity is known to have these components. + let (cell, mut color) = cells.get_mut(event.entity).unwrap(); + let is_selected = selected_block.is_some_and(|s| cell.id == **s); + *color = if is_selected { WHITE } else { BLACK }.into(); +} diff --git a/client/src/ui/mod.rs b/client/src/ui/mod.rs index 6f9b1ff..d8c1cb9 100644 --- a/client/src/ui/mod.rs +++ b/client/src/ui/mod.rs @@ -1,10 +1,15 @@ use bevy::prelude::*; +pub mod block_selection; pub mod crosshair; pub mod loading_screen; pub(super) fn plugin(app: &mut App) { - app.add_plugins((crosshair::plugin, loading_screen::plugin)); + app.add_plugins(( + block_selection::plugin, + crosshair::plugin, + loading_screen::plugin, + )); // Make entities require the `Pickable` component if // they should be considered for the picking system. diff --git a/common/src/block.rs b/common/src/block.rs index e205979..ca863a4 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -24,9 +24,6 @@ pub struct Block { } impl Block { - pub const PLATFORM: Identifier = Identifier::new_const("vampire_black"); - pub const DEFAULT: Identifier = Identifier::new_const("default_cube_gray"); - pub fn pos(&self) -> BlockPos { self.pos }