-
-
Notifications
You must be signed in to change notification settings - Fork 145
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a example for using an inventory with the ``readonly`` flag as a item menu (using a ``ItemMenu`` component, that can be attached to a player) related #307 video clip: https://github.com/user-attachments/assets/441e165a-139a-43ec-9970-3055c7f7f79a
- Loading branch information
1 parent
b1f18a7
commit 76fb18f
Showing
1 changed file
with
228 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
//! This example shows how to use a read-only [`OpenInventory`] as a menu, | ||
//! in which the player is able to select items by clicking on them. | ||
//! This is commonly used on minigame servers (e.g for team selection). | ||
#![allow(clippy::type_complexity)] | ||
|
||
const SPAWN_Y: i32 = 64; | ||
|
||
use item_menu::{ItemMenu, ItemMenuPlugin, MenuItemSelectEvent}; | ||
use valence::interact_item::InteractItemEvent; | ||
use valence::prelude::*; | ||
use valence::protocol::sound::SoundCategory; | ||
use valence::protocol::Sound; | ||
use valence_inventory::HeldItem; | ||
|
||
pub fn main() { | ||
App::new() | ||
.add_plugins(DefaultPlugins) | ||
.add_plugins(ItemMenuPlugin) | ||
.add_systems(Startup, setup) | ||
.add_systems( | ||
Update, | ||
( | ||
init_clients, | ||
despawn_disconnected_clients, | ||
on_item_interact, | ||
on_make_selection, | ||
), | ||
) | ||
.run(); | ||
} | ||
|
||
fn setup( | ||
mut commands: Commands, | ||
server: Res<Server>, | ||
dimensions: Res<DimensionTypeRegistry>, | ||
biomes: Res<BiomeRegistry>, | ||
) { | ||
let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); | ||
|
||
for z in -5..5 { | ||
for x in -5..5 { | ||
layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); | ||
} | ||
} | ||
|
||
for z in -25..25 { | ||
for x in -25..25 { | ||
layer | ||
.chunk | ||
.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); | ||
} | ||
} | ||
|
||
commands.spawn(layer); | ||
} | ||
|
||
fn init_clients( | ||
mut clients: Query< | ||
( | ||
&mut Position, | ||
&mut EntityLayerId, | ||
&mut VisibleChunkLayer, | ||
&mut VisibleEntityLayers, | ||
&mut GameMode, | ||
&mut Inventory, | ||
), | ||
Added<Client>, | ||
>, | ||
layers: Query<Entity, (With<ChunkLayer>, With<EntityLayer>)>, | ||
) { | ||
for ( | ||
mut pos, | ||
mut layer_id, | ||
mut visible_chunk_layer, | ||
mut visible_entity_layers, | ||
mut game_mode, | ||
mut inventory, | ||
) in &mut clients | ||
{ | ||
let layer = layers.single(); | ||
|
||
pos.0 = [0.0, f64::from(SPAWN_Y) + 1.0, 0.0].into(); | ||
layer_id.0 = layer; | ||
visible_chunk_layer.0 = layer; | ||
visible_entity_layers.0.insert(layer); | ||
*game_mode = GameMode::Survival; | ||
|
||
// 40 is the fifth hotbar slot | ||
inventory.set_slot(40, ItemStack::new(ItemKind::Compass, 1, None)); | ||
} | ||
} | ||
|
||
fn on_item_interact( | ||
mut commands: Commands, | ||
clients: Query<(Entity, &HeldItem, &Inventory)>, | ||
mut events: EventReader<InteractItemEvent>, | ||
) { | ||
for event in events.read() { | ||
let Ok((player_ent, held_item, inventory)) = clients.get(event.client) else { | ||
continue; | ||
}; | ||
if *inventory.slot(held_item.slot()) == ItemStack::new(ItemKind::Compass, 1, None) { | ||
open_menu(&mut commands, player_ent); | ||
} | ||
} | ||
} | ||
|
||
fn open_menu(commands: &mut Commands, player: Entity) { | ||
let mut menu_inv = Inventory::new(InventoryKind::Generic3x3); | ||
|
||
menu_inv.set_slot(3, ItemStack::new(ItemKind::RedWool, 1, None)); | ||
menu_inv.set_slot(5, ItemStack::new(ItemKind::GreenWool, 1, None)); | ||
|
||
let menu = ItemMenu::new(menu_inv); | ||
commands.entity(player).insert(menu); | ||
} | ||
|
||
fn on_make_selection( | ||
mut clients: Query<(&mut Client, &Position)>, | ||
mut events: EventReader<MenuItemSelectEvent>, | ||
) { | ||
for event in events.read() { | ||
let Ok((mut client, pos)) = clients.get_mut(event.client) else { | ||
continue; | ||
}; | ||
|
||
let selected_color = match event.idx { | ||
3 => "§cRED", | ||
5 => "§aGREEN", | ||
_ => continue, | ||
}; | ||
|
||
client.play_sound( | ||
Sound::BlockNoteBlockBit, | ||
SoundCategory::Block, | ||
pos.0, | ||
1.0, | ||
1.0, | ||
); | ||
client.send_chat_message(format!("you clicked: {selected_color}")); | ||
} | ||
} | ||
|
||
mod item_menu { | ||
use valence::prelude::*; | ||
use valence_inventory::ClickSlotEvent; | ||
|
||
pub(crate) struct ItemMenuPlugin; | ||
|
||
impl Plugin for ItemMenuPlugin { | ||
fn build(&self, app: &mut App) { | ||
app.add_systems(Update, (open_menu, select_menu_item)) | ||
.add_event::<MenuItemSelectEvent>() | ||
.observe(close_menu); | ||
} | ||
} | ||
|
||
/// This event is fired when the player interacts with an item in the menu. | ||
#[derive(Debug, Clone, PartialEq, Eq, Event)] | ||
pub(crate) struct MenuItemSelectEvent { | ||
/// Player entity | ||
pub client: Entity, | ||
/// Index of the item in the menu | ||
pub idx: u16, | ||
} | ||
|
||
/// The [`ItemMenu`] is a component, so it will open up once you attach it | ||
/// to a player and it will close once you remove it from the player (or | ||
/// in this implementation also if the player closes it). | ||
#[derive(Debug, Clone, Component)] | ||
pub(crate) struct ItemMenu { | ||
/// Item menu | ||
pub menu: Inventory, | ||
} | ||
|
||
impl ItemMenu { | ||
pub(crate) fn new(mut menu: Inventory) -> Self { | ||
menu.readonly = true; | ||
Self { menu } | ||
} | ||
} | ||
|
||
fn open_menu( | ||
mut commands: Commands, | ||
mut clients: Query<(Entity, &mut ItemMenu), Added<ItemMenu>>, | ||
) { | ||
for (player, item_menu) in &mut clients { | ||
let inventory = commands.spawn(item_menu.menu.clone()).id(); | ||
|
||
commands | ||
.entity(player) | ||
.insert(OpenInventory::new(inventory)); | ||
} | ||
} | ||
|
||
fn close_menu( | ||
_trigger: Trigger<OnRemove, OpenInventory>, | ||
mut commands: Commands, | ||
clients: Query<Entity, With<ItemMenu>>, | ||
) { | ||
for player in clients.iter() { | ||
commands.entity(player).remove::<ItemMenu>(); | ||
} | ||
} | ||
|
||
fn select_menu_item( | ||
mut clients: Query<(Entity, &ItemMenu)>, | ||
mut events: EventReader<ClickSlotEvent>, | ||
mut event_writer: EventWriter<MenuItemSelectEvent>, | ||
) { | ||
for event in events.read() { | ||
let selected_slot = event.slot_id; | ||
let Ok((player, item_menu)) = clients.get_mut(event.client) else { | ||
continue; | ||
}; | ||
// check that the selected item is not in the player's own inventory | ||
if selected_slot as u16 >= item_menu.menu.slot_count() { | ||
continue; | ||
} | ||
|
||
event_writer.send(MenuItemSelectEvent { | ||
client: player, | ||
idx: selected_slot as u16, | ||
}); | ||
} | ||
} | ||
} |