Compare commits

..

No commits in common. 'f15421a4612cb5878485303007f7a41dc4ea91ff' and '1cbf30704da17955b38f802344010664c08c44f5' have entirely different histories.

  1. 20
      Cargo.lock
  2. 1
      Cargo.toml
  3. 11
      assets/blocks.manifest.ron
  4. 3
      assets/blocks/default.ron
  5. 3
      assets/blocks/default_cube_gray.ron
  6. 3
      assets/blocks/demon_girl_red.ron
  7. 3
      assets/blocks/dragon_scale_purple.ron
  8. 3
      assets/blocks/idol_teal.ron
  9. 3
      assets/blocks/kitten_blue.ron
  10. 3
      assets/blocks/mermaid_pink.ron
  11. 3
      assets/blocks/platform.ron
  12. 3
      assets/blocks/slime_green.ron
  13. 3
      assets/blocks/star_blonde.ron
  14. 3
      assets/blocks/vampire_black.ron
  15. 32
      client/src/assets/block_visuals.rs
  16. 6
      client/src/assets/mod.rs
  17. 2
      client/src/assets/player.rs
  18. 102
      client/src/input/client_inputs.rs
  19. 85
      client/src/input/cursor_grab.rs
  20. 2
      client/src/input/head_orientation.rs
  21. 20
      client/src/input/mod.rs
  22. 14
      client/src/main.rs
  23. 150
      client/src/ui/block_selection.rs
  24. 9
      client/src/ui/crosshair.rs
  25. 2
      client/src/ui/loading_screen.rs
  26. 33
      client/src/ui/mod.rs
  27. 1
      common/Cargo.toml
  28. 52
      common/src/assets/manifest.rs
  29. 3
      common/src/block.rs
  30. 10
      common/src/identifier.rs

20
Cargo.lock generated

@ -643,7 +643,6 @@ version = "0.1.0"
dependencies = [
"bevy",
"derive-where",
"indexmap",
"lightyear",
"ron 0.12.0",
"serde",
@ -1413,7 +1412,7 @@ dependencies = [
"foldhash 0.2.0",
"futures-channel",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"js-sys",
"portable-atomic",
"portable-atomic-util",
@ -3555,14 +3554,13 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"equivalent",
"foldhash 0.2.0",
"serde",
"serde_core",
]
[[package]]
@ -3774,12 +3772,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.1"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@ -4572,7 +4570,7 @@ dependencies = [
"bevy_platform",
"bevy_reflect",
"bevy_utils",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"paste",
"seahash",
"tracing",
@ -5521,7 +5519,7 @@ dependencies = [
"either",
"ena",
"foldhash 0.2.0",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"log",
"nalgebra",
"num-derive",
@ -5547,7 +5545,7 @@ dependencies = [
"either",
"ena",
"foldhash 0.2.0",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"log",
"nalgebra",
"num-derive",

@ -16,7 +16,6 @@ bevy = { version = "0.17.2", features = [ "serialize" ] }
derive-where = { version = "1.6.0", features = [ "serde" ] }
indexmap = "2.12.1"
ron = "0.12.0"
serde = "1.0.228"
thiserror = "2.0.17"

@ -1,11 +1,4 @@
[
"vampire_black",
"demon_girl_red",
"star_blonde",
"slime_green",
"idol_teal",
"kitten_blue",
"dragon_scale_purple",
"mermaid_pink",
"default_cube_gray",
"default",
"platform",
]

@ -0,0 +1,3 @@
(
color: (124, 144, 255),
)

@ -1,3 +0,0 @@
(
color: (200, 200, 200),
)

@ -1,3 +0,0 @@
(
color: (198, 80, 88),
)

@ -1,3 +0,0 @@
(
color: (128, 67, 145),
)

@ -1,3 +0,0 @@
(
color: (0, 150, 135),
)

@ -1,3 +0,0 @@
(
color: (76, 136, 255),
)

@ -1,3 +0,0 @@
(
color: (255, 187, 217),
)

@ -0,0 +1,3 @@
(
color: (64, 64, 64),
)

@ -1,3 +0,0 @@
(
color: (161, 197, 109),
)

@ -1,3 +0,0 @@
(
color: (255, 230, 177),
)

@ -1,3 +0,0 @@
(
color: (26, 28, 30),
)

@ -5,7 +5,7 @@ use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use serde::Deserialize;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
let mesh = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
let cube = app.world_mut().add_asset(mesh);
app.insert_resource(BuiltinBlockMeshes { cube });
@ -22,25 +22,10 @@ struct BuiltinBlockMeshes {
}
#[derive(Asset, TypePath)]
pub struct BlockVisuals {
id: Identifier<Block>,
color: Color,
material: Handle<StandardMaterial>,
struct BlockVisuals {
_id: Identifier<Block>,
// mesh: Handle<Mesh>,
}
impl BlockVisuals {
pub fn id(&self) -> &Identifier<Block> {
&self.id
}
pub fn color(&self) -> Color {
self.color
}
// pub fn material(&self) -> &Handle<StandardMaterial> {
// &self.material
// }
material: Handle<StandardMaterial>,
}
impl ManifestEntry for BlockVisuals {
@ -72,19 +57,14 @@ impl AssetLoader for BlockVisualsLoader {
let raw = ron::de::from_bytes::<BlockTypeVisualsRaw>(&bytes)?;
let (r, g, b) = raw.color;
let color = Color::srgb_u8(r, g, b);
let material = StandardMaterial::from(color);
let material = StandardMaterial::from(Color::srgb_u8(r, g, b));
let material = load_context.add_labeled_asset("material".to_string(), material);
// TODO: Figure out how to reference a procedural mesh from here.
// let mesh = load_context.load(???);
let id = load_context.path().try_into()?;
Ok(BlockVisuals {
id,
color,
material,
})
Ok(BlockVisuals { _id: id, material })
}
fn extensions(&self) -> &[&str] {

@ -6,10 +6,10 @@
use bevy::prelude::*;
pub mod block_visuals;
pub mod player;
mod block_visuals;
mod player;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.add_plugins(block_visuals::plugin);
app.add_plugins(player::plugin);
}

@ -1,7 +1,7 @@
use bevy::prelude::*;
use common::prelude::*;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.load_resource::<PlayerAssets>();
app.add_observer(insert_player_visuals);
}

@ -1,5 +1,5 @@
//! Handles client-side input using [`lightyear`], updating the
//! [`ActionState`] of the affected entities, such as the [`Player`].
//! [`ActionState`] of the affected entities, such as the `Player`.
use bevy::prelude::*;
use common::prelude::*;
@ -8,61 +8,11 @@ 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::<CurrentAction>();
pub fn plugin(app: &mut App) {
app.add_systems(
FixedPreUpdate,
(buffer_input, buffer_action).in_set(InputSystems::WriteClientInputs),
);
app.add_observer(insert_block_pickable);
app.add_observer(place_or_break_blocks);
}
fn insert_block_pickable(event: On<Add, Block>, mut commands: Commands) {
let mut block = commands.entity(event.entity);
block.insert(Pickable::default());
}
#[derive(Resource, Default)]
struct CurrentAction(Action);
fn place_or_break_blocks(
mut event: On<Pointer<Press>>,
selected_block: Option<Res<SelectedBlock>>,
mut current_action: ResMut<CurrentAction>,
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
PointerButton::Middle => return, // not handled
};
if cursor.grab_mode != CursorGrabMode::Locked {
return; // Cursor is not grabbed.
}
let Some(block_pos) = blocks.position(event.entity) else {
return; // Clicked entity is not a block.
};
let Some(normal) = event.hit.normal else {
return; // Picking system didn't return a normal.
};
current_action.0 = if is_place {
// FIXME: This only works for axis-aligned normals.
let offset = normal.normalize().round().as_ivec3();
Action::PlaceBlock(block_pos + offset, selected_block.clone())
} else {
Action::BreakBlock(block_pos)
};
// Handle the event.
event.propagate(false);
}
fn buffer_input(
@ -94,10 +44,50 @@ fn buffer_input(
}
}
fn buffer_action(
// TODO: Use picking system instead of manually raycasting.
pub fn buffer_action(
buttons: Res<ButtonInput<MouseButton>>,
mut player: Single<&mut ActionState<Action>, With<InputMarker<Action>>>,
mut current_action: ResMut<CurrentAction>,
cursor: Single<&CursorOptions>,
window: Single<(&Window, &CursorOptions)>,
camera: Single<(&GlobalTransform, &Camera)>,
mut ray_cast: MeshRayCast,
blocks: Blocks,
) {
player.0 = current_action.0.clone();
current_action.0 = Action::None;
player.0 = Action::None;
if cursor.grab_mode == CursorGrabMode::None {
return;
}
if !buttons.any_just_pressed([MouseButton::Right, MouseButton::Left]) {
return;
}
let (window, cursor) = window.into_inner();
let (cam_transform, camera) = camera.into_inner();
let ray = if cursor.grab_mode == CursorGrabMode::Locked {
Ray3d::new(cam_transform.translation(), cam_transform.forward())
} else if let Some(cursor_pos) = window.cursor_position() {
camera.viewport_to_world(cam_transform, cursor_pos).unwrap()
} else {
return; // cursor outside window area
};
let settings = MeshRayCastSettings::default();
let Some((block, hit)) = ray_cast.cast_ray(ray, &settings).first() else {
return; // ray didn't hit anything
};
let Some(block_pos) = blocks.position(*block) else {
return; // entity hit is not a block
};
player.0 = if buttons.just_pressed(MouseButton::Right) {
// FIXME: This only works for axis-aligned normals.
let offset = hit.normal.normalize().round().as_ivec3();
// TODO: Don't hardcode block type.
Action::PlaceBlock(block_pos + offset, Block::DEFAULT)
} else {
Action::BreakBlock(block_pos)
};
}

@ -1,74 +1,63 @@
use bevy::prelude::*;
use bevy::input::common_conditions::input_just_pressed;
use bevy::picking::pointer::{PointerId, PointerLocation};
use bevy::window::{CursorGrabMode, CursorOptions};
use crate::Screen;
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));
pub fn plugin(app: &mut App) {
app.add_systems(
Update,
ungrab_cursor.run_if(
is_cursor_grabbed.and(input_just_pressed(KeyCode::Escape).or(not(is_window_focused))),
),
PreUpdate,
update_cursor_grab.run_if(in_state(Screen::Gameplay)),
);
}
fn add_window_click_observer(window: Single<Entity, With<Window>>, mut commands: Commands) {
commands.entity(*window).observe(
|mut event: On<Pointer<Click>>, mut cursor: Single<&mut CursorOptions>| {
fn update_cursor_grab(
mut mouse_button_input: ResMut<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
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.visible = false;
event.propagate(false);
},
);
}
fn ungrab_cursor(mut cursor: Single<&mut CursorOptions>) {
cursor.grab_mode = CursorGrabMode::None;
cursor.visible = true;
}
// To prevent other systems (such as `place_break_blocks`)
// from seeing the mouse button inputs, clear the state here.
mouse_button_input.clear();
}
/// Sets the cursor (and pointer) position to the center of the
/// window, so that once it is ungrabbed, it will reappear there.
fn center_cursor(
mut window: Single<&mut Window>,
pointers: Query<(&PointerId, &mut PointerLocation)>,
) {
let center = window.resolution.size() / 2.;
if is_grabbed && request_ungrab && !request_grab {
cursor.grab_mode = CursorGrabMode::None;
}
// 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.
if is_grabbed && !request_ungrab {
// Set the cursor position to the center of the window, so that when
// it is ungrabbed, it will reappear there. Because `is_grabbed` is
// not updated on grab, this block is delayed by one frame.
//
// 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`, and would log an error.
//
// 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
// while it's grabbed.
//
// On the web, the cursor can be locked, but setting its position isn't
// supported at all, so this would instead log a bunch of errors.
#[cfg(not(target_family = "wasm"))]
// On the web, the cursor can be locked, but setting its position is
// not supported at all, so this would instead log a bunch of errors.
let center = window.resolution.size() / 2.;
#[cfg(not(target_family = "wasm"))] // skip on web
window.set_cursor_position(Some(center));
// Update the `PointerLocation` for the mouse to be at the center
// 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;
}
}
}
// Keep cursor visbility in sync with `grab_mode`.
cursor.visible = cursor.grab_mode == CursorGrabMode::None;
}
pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool {
cursor.grab_mode != CursorGrabMode::None
}
pub fn is_window_focused(window: Single<&Window>) -> bool {
window.focused
}

@ -15,7 +15,7 @@ impl Default for MouseSensitivity {
}
}
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.init_resource::<MouseSensitivity>();
app.add_systems(Update, update_head_orientation.run_if(is_cursor_grabbed));
}

@ -1,27 +1,15 @@
use bevy::prelude::*;
pub mod client_inputs;
pub mod cursor_grab;
pub mod head_orientation;
mod client_inputs;
mod cursor_grab;
mod head_orientation;
pub use cursor_grab::is_cursor_grabbed;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.add_plugins((
client_inputs::plugin,
cursor_grab::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);
});
}

@ -26,7 +26,7 @@ fn main() -> Result {
let mut app = App::new();
app.insert_resource(args);
app.add_plugins((
app.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
@ -47,11 +47,10 @@ fn main() -> Result {
meta_check: AssetMetaCheck::Never,
..default()
}),
MeshPickingPlugin, // allow Mesh entities to be "picked" (ray-traced against)
bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
));
);
app.add_plugins((
bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
common::assets::plugin,
common::network::plugin,
assets::plugin,
@ -104,19 +103,18 @@ fn start_server_or_connect(args: Res<Args>, mut commands: Commands) -> Result {
/// When the server is started, spawn the initial blocks the world is made of.
fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
const PLATFORM: Identifier<Block> = 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, PLATFORM.clone()).unwrap();
blocks.spawn(pos, Block::PLATFORM).unwrap();
}
}
}
/// When the `Predicted` player we control is being spawned, insert necessary components.
fn handle_predicted_player_spawn(event: On<Add, Predicted>, mut commands: Commands) {
let mut player = commands.entity(event.entity);
player.insert((
let player = event.entity;
commands.entity(player).insert((
// Handle inputs on this entity.
InputMarker::<Inputs>::default(),
InputMarker::<Action>::default(),

@ -1,150 +0,0 @@
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();
}

@ -5,7 +5,7 @@ use bevy::window::{CursorGrabMode, CursorOptions};
use crate::Screen;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.load_resource::<CrosshairAssets>();
app.add_systems(OnEnter(Screen::Gameplay), setup_crosshair);
app.add_systems(Update, update_crosshair_visibility);
@ -13,7 +13,7 @@ pub(super) fn plugin(app: &mut App) {
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
struct CrosshairAssets {
pub struct CrosshairAssets {
#[dependency]
image: Handle<Image>,
}
@ -21,8 +21,9 @@ struct CrosshairAssets {
impl FromWorld for CrosshairAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
let image = assets.load("crosshair.png");
Self { image }
Self {
image: assets.load("crosshair.png"),
}
}
}

@ -4,7 +4,7 @@ use common::assets::asset_tracking::ResourceHandles;
use crate::Screen;
pub(super) fn plugin(app: &mut App) {
pub fn plugin(app: &mut App) {
app.add_systems(
Update,
enter_gameplay_screen.run_if(in_state(Screen::Loading).and(all_assets_loaded)),

@ -1,33 +1,8 @@
use bevy::prelude::*;
pub mod block_selection;
pub mod crosshair;
pub mod loading_screen;
mod crosshair;
mod loading_screen;
pub(super) fn plugin(app: &mut App) {
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,
));
pub fn plugin(app: &mut App) {
app.add_plugins((crosshair::plugin, loading_screen::plugin));
}

@ -9,7 +9,6 @@ lightyear.workspace = true
derive-where.workspace = true
indexmap.workspace = true
ron.workspace = true
serde.workspace = true
thiserror.workspace = true

@ -17,13 +17,13 @@ pub trait ManifestEntry: Asset {
#[derive(Resource, Asset, Reflect)]
#[derive_where(Clone)]
pub struct ManifestResource<T: ManifestEntry> {
struct ManifestResource<T: ManifestEntry> {
#[dependency]
handle: Handle<ManifestAsset<T>>,
}
#[derive(Asset, TypePath)]
pub struct ManifestAsset<T: ManifestEntry> {
struct ManifestAsset<T: ManifestEntry> {
#[dependency]
map: IdentifierMap<T::Marker, Handle<T>>,
}
@ -58,9 +58,7 @@ impl<T: ManifestEntry> AssetLoader for ManifestAssetLoader<T> {
for id in raw {
let id = Identifier::<T::Marker>::new(id)?;
let handle = load_context.load(format!("blocks/{id}.ron"));
if map.insert(id, handle).is_some() {
return Err("duplicate entry".into());
}
map.try_insert(id, handle).map_err(|_| "duplicate entry")?;
}
Ok(ManifestAsset { map })
}
@ -78,11 +76,6 @@ pub struct Manifest<'w, T: ManifestEntry> {
}
impl<T: ManifestEntry> Manifest<'_, T> {
/// System condition which returns `true` if this manifest was added or changed.
pub fn changed(res: Option<Res<ManifestResource<T>>>) -> bool {
resource_exists_and_changed(res)
}
/// Looks up an [`Asset`] of type `T` from its associated manifest.
///
/// Returns `None` if an asset with the given identifier is missing from
@ -97,31 +90,6 @@ impl<T: ManifestEntry> Manifest<'_, T> {
let entry = self.entry_assets.get(entry_handle).unwrap();
Some(entry)
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
let Some(manifest_handle) = self.resource.as_deref().map(|res| &res.handle) else {
return ManifestIter(None);
};
// // SAFETY: Manifest loads this as a dependency, so it should exist.
let manifest = self.manifest_assets.get(manifest_handle).unwrap();
let values = manifest.map.values();
// // SAFETY: Entry loads this as a dependency, so they should exist.
let iter = values.map(|h| self.entry_assets.get(h).unwrap());
ManifestIter(Some(iter))
}
}
struct ManifestIter<I>(Option<I>);
impl<'a, T, I> Iterator for ManifestIter<I>
where
T: ManifestEntry,
I: Iterator<Item = &'a T>,
{
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.0.as_mut().and_then(|i| i.next())
}
}
pub trait InitManifest {
@ -137,19 +105,5 @@ impl InitManifest for App {
// Insert a `Resource` that holds a handle to the `ManifestAsset` once it's loaded.
let handle = self.world_mut().load_asset(path);
self.load_resource_with(ManifestResource::<T> { handle });
self.add_systems(PostUpdate, trigger_change_on_loaded::<T>);
}
}
fn trigger_change_on_loaded<T: ManifestEntry>(
mut events: MessageReader<AssetEvent<ManifestAsset<T>>>,
mut resource: Option<ResMut<ManifestResource<T>>>,
) {
for event in events.read() {
if let AssetEvent::LoadedWithDependencies { id: _ } = event {
// Trigger change detection on the `ManifestResource`.
resource.as_deref_mut();
}
}
}

@ -24,6 +24,9 @@ pub struct Block {
}
impl Block {
pub const DEFAULT: Identifier<Block> = Identifier::<Block>::new_const("default");
pub const PLATFORM: Identifier<Block> = Identifier::<Block>::new_const("platform");
pub fn pos(&self) -> BlockPos {
self.pos
}

@ -5,14 +5,13 @@ use std::path::Path;
use bevy::prelude::*;
use bevy::asset::{UntypedAssetId, VisitAssetDependencies};
use bevy::platform::collections::HashMap;
use derive_where::derive_where;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Reflect, Deref)]
#[derive_where(Deserialize, Serialize)]
#[derive_where(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[derive_where(Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Debug)]
#[serde(try_from = "String", into = "String")]
pub struct Identifier<T> {
#[deref]
@ -55,8 +54,7 @@ impl<T> Identifier<T> {
let mut i = 0;
while i < value.len() {
if let Some(c) = char::from_u32(bytes[i] as u32) {
// TODO: Disallow leading underscore or multiple in a row?
if c.is_ascii_lowercase() || c == '_' {
if c.is_ascii_lowercase() {
i += 1;
continue;
}
@ -119,7 +117,7 @@ impl<T> TryFrom<&Path> for Identifier<T> {
#[derive(Deserialize, Serialize, Reflect, Deref, DerefMut)]
#[derive_where(Default)]
pub struct IdentifierMap<T, V>(IndexMap<Identifier<T>, V>);
pub struct IdentifierMap<T, V>(HashMap<Identifier<T>, V>);
impl<T, V: Clone> Clone for IdentifierMap<T, V> {
fn clone(&self) -> Self {

Loading…
Cancel
Save