Compare commits
11 Commits
1cbf30704d
...
f15421a461
| Author | SHA1 | Date |
|---|---|---|
|
|
f15421a461 | 1 month ago |
|
|
8700c23893 | 1 month ago |
|
|
83f3dcf78e | 1 month ago |
|
|
be253e8cb7 | 1 month ago |
|
|
a436180ae4 | 1 month ago |
|
|
90bd171212 | 1 month ago |
|
|
4381fc325b | 1 month ago |
|
|
2aa370dc60 | 1 month ago |
|
|
af0732fdc4 | 1 month ago |
|
|
72625dd949 | 1 month ago |
|
|
5467859d6d | 1 month ago |
30 changed files with 453 additions and 147 deletions
@ -1,4 +1,11 @@ |
|||||||
[ |
[ |
||||||
"default", |
"vampire_black", |
||||||
"platform", |
"demon_girl_red", |
||||||
|
"star_blonde", |
||||||
|
"slime_green", |
||||||
|
"idol_teal", |
||||||
|
"kitten_blue", |
||||||
|
"dragon_scale_purple", |
||||||
|
"mermaid_pink", |
||||||
|
"default_cube_gray", |
||||||
] |
] |
||||||
|
|||||||
@ -1,3 +0,0 @@ |
|||||||
( |
|
||||||
color: (124, 144, 255), |
|
||||||
) |
|
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (200, 200, 200), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (198, 80, 88), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (128, 67, 145), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (0, 150, 135), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (76, 136, 255), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (255, 187, 217), |
||||||
|
) |
||||||
@ -1,3 +0,0 @@ |
|||||||
( |
|
||||||
color: (64, 64, 64), |
|
||||||
) |
|
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (161, 197, 109), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (255, 230, 177), |
||||||
|
) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
( |
||||||
|
color: (26, 28, 30), |
||||||
|
) |
||||||
@ -1,63 +1,74 @@ |
|||||||
use bevy::prelude::*; |
use bevy::prelude::*; |
||||||
|
|
||||||
|
use bevy::input::common_conditions::input_just_pressed; |
||||||
|
use bevy::picking::pointer::{PointerId, PointerLocation}; |
||||||
use bevy::window::{CursorGrabMode, CursorOptions}; |
use bevy::window::{CursorGrabMode, CursorOptions}; |
||||||
|
|
||||||
use crate::Screen; |
use crate::Screen; |
||||||
|
|
||||||
pub fn plugin(app: &mut App) { |
pub(super) fn plugin(app: &mut App) { |
||||||
|
app.add_systems(OnEnter(Screen::Gameplay), add_window_click_observer); |
||||||
|
app.add_systems(PreUpdate, center_cursor.run_if(is_cursor_grabbed)); |
||||||
app.add_systems( |
app.add_systems( |
||||||
PreUpdate, |
Update, |
||||||
update_cursor_grab.run_if(in_state(Screen::Gameplay)), |
ungrab_cursor.run_if( |
||||||
|
is_cursor_grabbed.and(input_just_pressed(KeyCode::Escape).or(not(is_window_focused))), |
||||||
|
), |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
fn update_cursor_grab( |
fn add_window_click_observer(window: Single<Entity, With<Window>>, mut commands: Commands) { |
||||||
mut mouse_button_input: ResMut<ButtonInput<MouseButton>>, |
commands.entity(*window).observe( |
||||||
key_input: Res<ButtonInput<KeyCode>>, |
|mut event: On<Pointer<Click>>, mut cursor: Single<&mut CursorOptions>| { |
||||||
window: Single<(&mut Window, &mut CursorOptions)>, |
|
||||||
) { |
|
||||||
let (mut window, mut cursor) = window.into_inner(); |
|
||||||
|
|
||||||
let is_grabbed = cursor.grab_mode != CursorGrabMode::None; |
|
||||||
let request_grab = mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]); |
|
||||||
let request_ungrab = !window.focused || key_input.just_pressed(KeyCode::Escape); |
|
||||||
|
|
||||||
if !is_grabbed && request_grab && !request_ungrab { |
|
||||||
cursor.grab_mode = CursorGrabMode::Locked; |
cursor.grab_mode = CursorGrabMode::Locked; |
||||||
|
cursor.visible = false; |
||||||
// To prevent other systems (such as `place_break_blocks`)
|
event.propagate(false); |
||||||
// from seeing the mouse button inputs, clear the state here.
|
}, |
||||||
mouse_button_input.clear(); |
); |
||||||
} |
} |
||||||
|
|
||||||
if is_grabbed && request_ungrab && !request_grab { |
fn ungrab_cursor(mut cursor: Single<&mut CursorOptions>) { |
||||||
cursor.grab_mode = CursorGrabMode::None; |
cursor.grab_mode = CursorGrabMode::None; |
||||||
|
cursor.visible = true; |
||||||
} |
} |
||||||
|
|
||||||
if is_grabbed && !request_ungrab { |
/// Sets the cursor (and pointer) position to the center of the
|
||||||
// Set the cursor position to the center of the window, so that when
|
/// window, so that once it is ungrabbed, it will reappear there.
|
||||||
// it is ungrabbed, it will reappear there. Because `is_grabbed` is
|
fn center_cursor( |
||||||
// not updated on grab, this block is delayed by one frame.
|
mut window: Single<&mut Window>, |
||||||
//
|
pointers: Query<(&PointerId, &mut PointerLocation)>, |
||||||
// On Wayland, since the cursor is locked into place, this only needs
|
) { |
||||||
// to be done once. Unfortunately, for some reason this doesn't work
|
let center = window.resolution.size() / 2.; |
||||||
// in the same frame as setting `grab_mode`, and would log an error.
|
|
||||||
|
// On Wayland, since the cursor is locked into place, this only needs to be
|
||||||
|
// done once. Unfortunately, for some reason, this doesn't work in the same
|
||||||
|
// frame as setting `grab_mode`, logging an error. This system is scheduled
|
||||||
|
// on `PreUpdate`, so it runs with one frame delay.
|
||||||
//
|
//
|
||||||
// On X11, the cursor can't be locked into place, only confined to the
|
// On X11, the cursor can't be locked into place, only confined to the
|
||||||
// window bounds, so we repeatedly move the cursor back to the center
|
// window bounds, so we repeatedly move the cursor back to the center
|
||||||
// while it's grabbed.
|
// while it's grabbed.
|
||||||
//
|
//
|
||||||
// On the web, the cursor can be locked, but setting its position is
|
// On the web, the cursor can be locked, but setting its position isn't
|
||||||
// not supported at all, so this would instead log a bunch of errors.
|
// supported at all, so this would instead log a bunch of errors.
|
||||||
let center = window.resolution.size() / 2.; |
#[cfg(not(target_family = "wasm"))] |
||||||
#[cfg(not(target_family = "wasm"))] // skip on web
|
|
||||||
window.set_cursor_position(Some(center)); |
window.set_cursor_position(Some(center)); |
||||||
} |
|
||||||
|
|
||||||
// Keep cursor visbility in sync with `grab_mode`.
|
// Update the `PointerLocation` for the mouse to be at the center
|
||||||
cursor.visible = cursor.grab_mode == CursorGrabMode::None; |
// of the window. Otherwise it might use the last known position.
|
||||||
|
for (id, mut pointer) in pointers { |
||||||
|
if id.is_mouse() { |
||||||
|
if let Some(location) = pointer.location.as_mut() { |
||||||
|
location.position = center; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { |
pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { |
||||||
cursor.grab_mode != CursorGrabMode::None |
cursor.grab_mode != CursorGrabMode::None |
||||||
} |
} |
||||||
|
|
||||||
|
pub fn is_window_focused(window: Single<&Window>) -> bool { |
||||||
|
window.focused |
||||||
|
} |
||||||
|
|||||||
@ -1,15 +1,27 @@ |
|||||||
use bevy::prelude::*; |
use bevy::prelude::*; |
||||||
|
|
||||||
mod client_inputs; |
pub mod client_inputs; |
||||||
mod cursor_grab; |
pub mod cursor_grab; |
||||||
mod head_orientation; |
pub mod head_orientation; |
||||||
|
|
||||||
pub use cursor_grab::is_cursor_grabbed; |
pub use cursor_grab::is_cursor_grabbed; |
||||||
|
|
||||||
pub fn plugin(app: &mut App) { |
pub(super) fn plugin(app: &mut App) { |
||||||
app.add_plugins(( |
app.add_plugins(( |
||||||
client_inputs::plugin, |
client_inputs::plugin, |
||||||
cursor_grab::plugin, |
cursor_grab::plugin, |
||||||
head_orientation::plugin, |
head_orientation::plugin, |
||||||
)); |
)); |
||||||
|
|
||||||
|
// Make entities require the `Pickable` component if
|
||||||
|
// they should be considered for the picking system.
|
||||||
|
app.insert_resource(MeshPickingSettings { |
||||||
|
require_markers: true, |
||||||
|
..default() |
||||||
|
}); |
||||||
|
|
||||||
|
// Insert `MeshPickingCamera` component on any 3D camera.
|
||||||
|
app.add_observer(|event: On<Add, Camera3d>, mut commands: Commands| { |
||||||
|
commands.entity(event.entity).insert(MeshPickingCamera); |
||||||
|
}); |
||||||
} |
} |
||||||
|
|||||||
@ -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::<BlockVisuals>::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::<SelectedBlock>; |
||||||
|
app.add_systems(Update, update_selected_cell.run_if(selection_changed)); |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Resource, Deref, DerefMut)] |
||||||
|
pub struct SelectedBlock(Identifier<Block>); |
||||||
|
|
||||||
|
#[derive(Component)] |
||||||
|
struct BlockSelectionLayout; |
||||||
|
|
||||||
|
#[derive(Component)] |
||||||
|
#[require(Button, Pickable)] |
||||||
|
struct BlockSelectionCell { |
||||||
|
id: Identifier<Block>, |
||||||
|
} |
||||||
|
|
||||||
|
fn setup_selection_ui( |
||||||
|
existing: Option<Single<Entity, With<BlockSelectionLayout>>>, |
||||||
|
blocks: Manifest<BlockVisuals>, |
||||||
|
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<Pointer<Click>>| { |
||||||
|
// 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<CursorOptions>>, |
||||||
|
crosshair: Single<&mut Visibility, With<BlockSelectionLayout>>, |
||||||
|
) { |
||||||
|
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<SelectedBlock>, |
||||||
|
) { |
||||||
|
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<Pointer<Click>>, |
||||||
|
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<Pointer<Over>>, |
||||||
|
mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>, |
||||||
|
selected_block: Option<Res<SelectedBlock>>, |
||||||
|
) { |
||||||
|
// 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<Pointer<Out>>, |
||||||
|
mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>, |
||||||
|
selected_block: Option<Res<SelectedBlock>>, |
||||||
|
) { |
||||||
|
// 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(); |
||||||
|
} |
||||||
@ -1,8 +1,33 @@ |
|||||||
use bevy::prelude::*; |
use bevy::prelude::*; |
||||||
|
|
||||||
mod crosshair; |
pub mod block_selection; |
||||||
mod loading_screen; |
pub mod crosshair; |
||||||
|
pub mod loading_screen; |
||||||
|
|
||||||
pub fn plugin(app: &mut App) { |
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.
|
||||||
|
app.insert_resource(UiPickingSettings { |
||||||
|
require_markers: true, |
||||||
|
}); |
||||||
|
|
||||||
|
app.add_systems(Startup, setup_ui_camera); |
||||||
|
} |
||||||
|
|
||||||
|
fn setup_ui_camera(mut commands: Commands) { |
||||||
|
commands.spawn(( |
||||||
|
Camera2d, |
||||||
|
Camera { |
||||||
|
order: 1, // above 3D camera
|
||||||
|
..default() |
||||||
|
}, |
||||||
|
IsDefaultUiCamera, |
||||||
|
UiPickingCamera, |
||||||
|
)); |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue