diff --git a/server/src/perception/observer/mod.rs b/server/src/perception/observer/mod.rs index b96b2e3a0..f3f6c8e38 100644 --- a/server/src/perception/observer/mod.rs +++ b/server/src/perception/observer/mod.rs @@ -15,6 +15,7 @@ use crate::knowledge::{EntityRegistry, KnowledgeGraph, StableId}; use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry}; use crate::perception::vision_cone::Facing; use crate::simulation::interaction::NearbyInteractionBuffer; +use crate::simulation::inventory::{CarriedBy, InventorySlot, ItemName}; use crate::simulation::monologue::MonologueBuffer; use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap}; use crate::simulation::stance::Stance; @@ -52,7 +53,7 @@ pub fn compute_observer_snapshot( geometry: Res, registry: Res, mut observer_query: Query< - (&TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer, &mut MonologueBuffer, Option<&Stance>, Option<&CharacterArchetype>), + (Entity, &TilePosition, Option<&Facing>, &KnowledgeGraph, &mut NearbyInteractionBuffer, &mut MonologueBuffer, Option<&Stance>, Option<&CharacterArchetype>), With, >, all_entities: Query<( @@ -61,9 +62,10 @@ pub fn compute_observer_snapshot( Option<&PlayerCharacter>, Option<&crate::npc::Npc>, )>, + inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, mut buffer: ResMut, ) { - let Ok((_observer_pos, facing_opt, observer_kg, mut interaction_buffer, mut monologue_buffer, stance_opt, archetype_opt)) = + let Ok((observer_entity, _observer_pos, facing_opt, observer_kg, mut interaction_buffer, mut monologue_buffer, stance_opt, archetype_opt)) = observer_query.single_mut() else { return; @@ -75,6 +77,18 @@ pub fn compute_observer_snapshot( let archetype = archetype_opt.copied().unwrap_or_default(); + // Collect player inventory (D-065 info boundary: only own items) + let player_inventory = registry + .to_stable(observer_entity) + .map(|player_sid| { + crate::simulation::inventory::collect_inventory_for( + player_sid, + &inventory_items, + ®istry, + ) + }) + .unwrap_or_default(); + let (mut entities, visible_ids) = filter_visible_entities(&geometry, ®istry, observer_kg, &all_entities); @@ -114,7 +128,7 @@ pub fn compute_observer_snapshot( game_time, player_facing: facing, player_stance: stance_opt.map(|s| s.0).unwrap_or_default(), - player_inventory: Vec::new(), + player_inventory, entities, visible_tiles: geometry.visible_tiles.clone(), nearby_interactions, diff --git a/server/src/simulation/input.rs b/server/src/simulation/input.rs index f4a5a1a4c..b0a8cd6cb 100644 --- a/server/src/simulation/input.rs +++ b/server/src/simulation/input.rs @@ -3,7 +3,11 @@ // PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode, ToggleStance) use crate::bridge::types::{PlayerAction, PlayerInput}; +use crate::knowledge::{EntityRegistry, StableId}; use crate::perception::vision_cone::{facing_from_delta, Facing}; +use crate::simulation::inventory::{ + find_next_slot, occupied_slots_for, CarriedBy, InventorySlot, ItemName, MAX_INVENTORY_SLOTS, +}; use crate::simulation::movement::{MoveIntent, PlayerCharacter, TilePosition}; use crate::simulation::stance::{PlayerMoveCooldown, Stance}; use crate::simulation::time::{SimulationTime, TickRate}; @@ -56,16 +60,18 @@ impl InputQueue { } /// Drains InputQueue for the current tick, converts PlayerActions to ECS components. -/// Handles stance toggling (D-053) and movement cooldown based on current stance. +/// Handles stance toggling (D-053), movement cooldown, and Take/Place verbs (#424). #[allow(clippy::type_complexity)] pub fn process_player_input( mut input_queue: ResMut, mut time: ResMut, mut commands: Commands, + registry: Res, mut player_query: Query< (Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>), With, >, + inventory_items: Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, ) { let current_tick = time.tick; let inputs = input_queue.drain_for_tick(current_tick); @@ -137,12 +143,33 @@ pub fn process_player_input( time.tick_rate = rate; tracing::debug!("Tick rate set to {:?} by player input", rate); } - PlayerAction::Interact { target_entity_id, verb } => { - tracing::info!( - "Interact: target={:?}, verb={:?} — logged only, dialogue dispatch future scope (#415)", - target_entity_id, - verb, - ); + PlayerAction::Interact { target_entity_id, ref verb } => { + match verb.as_deref() { + Some("Take") => { + handle_take( + &mut commands, + ®istry, + &player_query, + &inventory_items, + target_entity_id, + ); + } + Some("Place") => { + handle_place( + &mut commands, + ®istry, + &player_query, + target_entity_id, + ); + } + _ => { + tracing::info!( + "Interact: target={:?}, verb={:?} — logged only, dialogue dispatch future scope (#415)", + target_entity_id, + verb, + ); + } + } } PlayerAction::UsePerceptionMode(ref mode) => { tracing::trace!("UsePerceptionMode({}) — no-op for Sprint 1", mode); @@ -195,6 +222,105 @@ fn apply_move( .insert(Facing(facing_from_delta(dx, dy))); } +/// Handle Take verb: pick up an item entity and add it to the player's inventory. +/// Removes TilePosition (item is no longer on the ground — info boundary enforcement), +/// adds CarriedBy + InventorySlot components. +#[allow(clippy::type_complexity)] +fn handle_take( + commands: &mut Commands, + registry: &EntityRegistry, + player_query: &Query< + (Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>), + With, + >, + inventory_items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + target_entity_id: Option, +) { + let Some(target_id) = target_entity_id else { + tracing::warn!("Take verb without target_entity_id"); + return; + }; + + let Ok((player_entity, _, _, _)) = player_query.single() else { + return; + }; + + let Some(player_sid) = registry.to_stable(player_entity) else { + tracing::error!("Player entity not in EntityRegistry"); + return; + }; + + // Resolve wire ID to ECS entity + let target_stable = StableId(target_id); + let Some(target_entity) = registry.to_entity(&target_stable) else { + tracing::warn!(target_id, "Take: target entity not in registry"); + return; + }; + + // Check inventory capacity + let occupied = occupied_slots_for(player_sid, inventory_items); + let Some(slot) = find_next_slot(&occupied) else { + tracing::info!("Inventory full ({} slots), cannot take item", MAX_INVENTORY_SLOTS); + return; + }; + + // Remove TilePosition (item leaves the ground), add CarriedBy + InventorySlot + commands.entity(target_entity) + .remove::() + .insert((CarriedBy(player_sid), InventorySlot(slot))); + + tracing::info!( + target_id, + slot, + "Take: item picked up and added to inventory slot", + ); +} + +/// Handle Place verb: remove an item from inventory and place it on the ground +/// at the player's current position. Removes CarriedBy + InventorySlot, adds +/// TilePosition at the player's current tile. +#[allow(clippy::type_complexity)] +fn handle_place( + commands: &mut Commands, + registry: &EntityRegistry, + player_query: &Query< + (Entity, &TilePosition, Option<&mut Stance>, Option<&mut PlayerMoveCooldown>), + With, + >, + target_entity_id: Option, +) { + let Some(target_id) = target_entity_id else { + tracing::warn!("Place verb without target_entity_id"); + return; + }; + + let Ok((_, player_pos, _, _)) = player_query.single() else { + return; + }; + + let target_stable = StableId(target_id); + let Some(target_entity) = registry.to_entity(&target_stable) else { + tracing::warn!(target_id, "Place: target entity not in registry"); + return; + }; + + let place_pos = *player_pos; + + // Remove inventory components, place item at player's tile + commands.entity(target_entity) + .remove::() + .remove::() + .insert(place_pos); + + tracing::info!( + target_id, + x = place_pos.x, + y = place_pos.y, + z = place_pos.z, + "Place: item dropped at player position", + ); +} + #[cfg(test)] mod tests { use super::*; @@ -245,6 +371,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); let player = world .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) @@ -268,6 +395,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); world.resource_mut::().push(PlayerInput { tick: 0, @@ -286,6 +414,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); world.resource_mut::().push(PlayerInput { tick: 0, @@ -305,6 +434,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); world.resource_mut::().push(PlayerInput { tick: 0, @@ -321,6 +451,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); let player = world .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) @@ -346,6 +477,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); world.spawn(( PlayerCharacter, @@ -373,6 +505,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); world.spawn(( PlayerCharacter, @@ -400,6 +533,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); let player = world .spawn(( @@ -446,6 +580,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); let player = world .spawn(( @@ -483,6 +618,7 @@ mod tests { let mut world = bevy_ecs::world::World::new(); world.insert_resource(InputQueue::default()); world.insert_resource(SimulationTime::default()); + world.init_resource::(); let player = world .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) @@ -507,4 +643,180 @@ mod tests { schedule.run(&mut world); assert!(world.get::(player).is_some()); } + + #[test] + fn take_verb_picks_up_item() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(InputQueue::default()); + world.insert_resource(SimulationTime::default()); + world.init_resource::(); + + // Spawn player and register + let player = world + .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) + .id(); + let player_sid = world.resource_mut::().register(player); + + // Spawn item near player + let item = world + .spawn(( + TilePosition::new(5, 4, 0), + ItemName("Manifest Copy".into()), + )) + .id(); + let item_sid = world.resource_mut::().register(item); + + // Issue Take verb + world.resource_mut::().push(PlayerInput { + tick: 0, + action: PlayerAction::Interact { + target_entity_id: Some(item_sid.0), + verb: Some("Take".into()), + }, + }); + + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(process_player_input); + schedule.run(&mut world); + + // Item should have CarriedBy + InventorySlot, no TilePosition + assert!(world.get::(item).is_none(), "item should leave the ground"); + let carried = world.get::(item).expect("item should have CarriedBy"); + assert_eq!(carried.0, player_sid); + let slot = world.get::(item).expect("item should have slot"); + assert_eq!(slot.0, 0, "first item goes to slot 0"); + } + + #[test] + fn place_verb_drops_item() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(InputQueue::default()); + world.insert_resource(SimulationTime::default()); + world.init_resource::(); + + let player = world + .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) + .id(); + let player_sid = world.resource_mut::().register(player); + + // Spawn item already in inventory (no TilePosition) + let item = world + .spawn(( + CarriedBy(player_sid), + ItemName("Manifest Copy".into()), + InventorySlot(0), + )) + .id(); + let item_sid = world.resource_mut::().register(item); + + // Issue Place verb + world.resource_mut::().push(PlayerInput { + tick: 0, + action: PlayerAction::Interact { + target_entity_id: Some(item_sid.0), + verb: Some("Place".into()), + }, + }); + + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(process_player_input); + schedule.run(&mut world); + + // Item should have TilePosition at player's location, no CarriedBy/InventorySlot + let pos = world.get::(item).expect("item should be on ground"); + assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position"); + assert!(world.get::(item).is_none(), "CarriedBy removed"); + assert!(world.get::(item).is_none(), "InventorySlot removed"); + } + + #[test] + fn take_verb_assigns_sequential_slots() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(InputQueue::default()); + world.insert_resource(SimulationTime::default()); + world.init_resource::(); + + let player = world + .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) + .id(); + let player_sid = world.resource_mut::().register(player); + + // Item already in slot 0 + world.spawn(( + CarriedBy(player_sid), + ItemName("Manifest".into()), + InventorySlot(0), + )); + + // New item on the ground + let item2 = world + .spawn(( + TilePosition::new(5, 4, 0), + ItemName("Token".into()), + )) + .id(); + let item2_sid = world.resource_mut::().register(item2); + + world.resource_mut::().push(PlayerInput { + tick: 0, + action: PlayerAction::Interact { + target_entity_id: Some(item2_sid.0), + verb: Some("Take".into()), + }, + }); + + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(process_player_input); + schedule.run(&mut world); + + let slot = world.get::(item2).expect("item should have slot"); + assert_eq!(slot.0, 1, "second item goes to slot 1"); + } + + #[test] + fn take_verb_full_inventory_rejected() { + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(InputQueue::default()); + world.insert_resource(SimulationTime::default()); + world.init_resource::(); + + let player = world + .spawn((PlayerCharacter, TilePosition::new(5, 5, 0))) + .id(); + let player_sid = world.resource_mut::().register(player); + + // Fill all 9 slots + for slot in 0..MAX_INVENTORY_SLOTS { + world.spawn(( + CarriedBy(player_sid), + ItemName(format!("Item {}", slot)), + InventorySlot(slot), + )); + } + + // Try to take another item + let item = world + .spawn(( + TilePosition::new(5, 4, 0), + ItemName("Overflow".into()), + )) + .id(); + let item_sid = world.resource_mut::().register(item); + + world.resource_mut::().push(PlayerInput { + tick: 0, + action: PlayerAction::Interact { + target_entity_id: Some(item_sid.0), + verb: Some("Take".into()), + }, + }); + + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(process_player_input); + schedule.run(&mut world); + + // Item should still be on the ground + assert!(world.get::(item).is_some(), "item stays on ground"); + assert!(world.get::(item).is_none(), "no CarriedBy when full"); + } } diff --git a/server/src/simulation/inventory.rs b/server/src/simulation/inventory.rs new file mode 100644 index 000000000..675eb6a49 --- /dev/null +++ b/server/src/simulation/inventory.rs @@ -0,0 +1,272 @@ +// Inventory system — CarriedBy component, Take/Place verb handling (#424, D-065) +// +// Items are world entities with CarriedBy(StableId) referencing the carrier. +// When carried, TilePosition is removed — this naturally enforces the info +// boundary: carried items don't appear in spatial queries (visibility, +// interactions) for other observers. Only the carrier's observer snapshot +// includes them via player_inventory. +// +// Take: removes TilePosition, adds CarriedBy + InventorySlot. +// Place: removes CarriedBy + InventorySlot, adds TilePosition at player pos. +// +// 3x3 grid = 9 universal slots (OQ-24 resolved). Slot assignment is +// first-available (0..8). + +use bevy_ecs::prelude::*; + +use crate::bridge::types::InventoryItem; +use crate::knowledge::types::StableId; + +/// Maximum inventory slots (3x3 grid, OQ-24). +pub const MAX_INVENTORY_SLOTS: u8 = 9; + +/// Marks an item as carried by an entity. References the carrier's StableId. +/// When present, the item entity should NOT have a TilePosition — it's +/// in someone's pocket, not on the ground. +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +pub struct CarriedBy(pub StableId); + +/// Display name for an item entity, crossing the wire as InventoryItem.name. +#[derive(Component, Debug, Clone)] +pub struct ItemName(pub String); + +/// Inventory slot assignment (0..8 for 3x3 grid). +/// Assigned on Take, removed on Place. +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +pub struct InventorySlot(pub u8); + +/// Find the next available inventory slot for a carrier. +/// Returns None if all 9 slots are occupied. +pub fn find_next_slot(occupied: &[u8]) -> Option { + for slot in 0..MAX_INVENTORY_SLOTS { + if !occupied.contains(&slot) { + return Some(slot); + } + } + None +} + +/// Collect inventory items for a specific carrier (by StableId). +/// Returns wire-format InventoryItem structs sorted by slot. +pub fn collect_inventory_for( + carrier_id: StableId, + items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + registry: &crate::knowledge::EntityRegistry, +) -> Vec { + let mut result: Vec = items + .iter() + .filter(|(_, carried_by, _, _)| carried_by.0 == carrier_id) + .map(|(entity, _, name, slot)| { + let wire_id = registry + .to_stable(entity) + .map(|sid| sid.0) + .unwrap_or_else(|| { + tracing::error!(?entity, "carried item not in EntityRegistry"); + entity.to_bits() + }); + InventoryItem { + item_id: wire_id, + name: name.0.clone(), + slot: slot.0, + } + }) + .collect(); + result.sort_by_key(|item| item.slot); + result +} + +/// Get the list of occupied slots for a carrier. +pub fn occupied_slots_for( + carrier_id: StableId, + items: &Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, +) -> Vec { + items + .iter() + .filter(|(_, carried_by, _, _)| carried_by.0 == carrier_id) + .map(|(_, _, _, slot)| slot.0) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::knowledge::EntityRegistry; + use bevy_ecs::world::World; + + fn setup_world() -> World { + let mut world = World::new(); + world.init_resource::(); + world + } + + #[test] + fn find_next_slot_empty_returns_zero() { + assert_eq!(find_next_slot(&[]), Some(0)); + } + + #[test] + fn find_next_slot_skips_occupied() { + assert_eq!(find_next_slot(&[0, 1, 2]), Some(3)); + } + + #[test] + fn find_next_slot_fills_gaps() { + assert_eq!(find_next_slot(&[0, 2, 4]), Some(1)); + } + + #[test] + fn find_next_slot_full_returns_none() { + let all: Vec = (0..9).collect(); + assert_eq!(find_next_slot(&all), None); + } + + #[test] + fn collect_inventory_empty_when_no_items() { + let mut world = setup_world(); + let player = world.spawn_empty().id(); + let player_sid = world.resource_mut::().register(player); + + let mut query_state = + world.query::<(Entity, &CarriedBy, &ItemName, &InventorySlot)>(); + + // Can't use system params directly in tests — use world query + // Instead, verify the logic by spawning items and checking + assert_eq!(query_state.iter(&world).count(), 0); + let _ = player_sid; // used for the filter + } + + #[test] + fn carried_item_appears_in_inventory() { + let mut world = setup_world(); + + let player = world.spawn_empty().id(); + let player_sid = world.resource_mut::().register(player); + + let item = world + .spawn(( + CarriedBy(player_sid), + ItemName("Manifest Copy".into()), + InventorySlot(0), + )) + .id(); + world.resource_mut::().register(item); + + // Use system_state for proper Query access + let mut system_state = bevy_ecs::system::SystemState::<( + Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + Res, + )>::new(&mut world); + let (items_query, registry) = system_state.get(&world); + + let inventory = collect_inventory_for(player_sid, &items_query, ®istry); + assert_eq!(inventory.len(), 1); + assert_eq!(inventory[0].name, "Manifest Copy"); + assert_eq!(inventory[0].slot, 0); + } + + #[test] + fn only_own_items_in_inventory() { + let mut world = setup_world(); + + let player = world.spawn_empty().id(); + let player_sid = world.resource_mut::().register(player); + + let other = world.spawn_empty().id(); + let other_sid = world.resource_mut::().register(other); + + // Player's item + let item1 = world + .spawn(( + CarriedBy(player_sid), + ItemName("Manifest Copy".into()), + InventorySlot(0), + )) + .id(); + world.resource_mut::().register(item1); + + // Other entity's item — should NOT appear in player's inventory + let item2 = world + .spawn(( + CarriedBy(other_sid), + ItemName("Access Token".into()), + InventorySlot(0), + )) + .id(); + world.resource_mut::().register(item2); + + let mut system_state = bevy_ecs::system::SystemState::<( + Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + Res, + )>::new(&mut world); + let (items_query, registry) = system_state.get(&world); + + let inventory = collect_inventory_for(player_sid, &items_query, ®istry); + assert_eq!(inventory.len(), 1, "info boundary: only own items"); + assert_eq!(inventory[0].name, "Manifest Copy"); + } + + #[test] + fn inventory_sorted_by_slot() { + let mut world = setup_world(); + + let player = world.spawn_empty().id(); + let player_sid = world.resource_mut::().register(player); + + // Spawn items in reverse slot order + for (slot, name) in [(2, "Comm Log"), (0, "Manifest"), (1, "Token")] { + let item = world + .spawn(( + CarriedBy(player_sid), + ItemName(name.into()), + InventorySlot(slot), + )) + .id(); + world.resource_mut::().register(item); + } + + let mut system_state = bevy_ecs::system::SystemState::<( + Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + Res, + )>::new(&mut world); + let (items_query, registry) = system_state.get(&world); + + let inventory = collect_inventory_for(player_sid, &items_query, ®istry); + assert_eq!(inventory.len(), 3); + assert_eq!(inventory[0].slot, 0); + assert_eq!(inventory[0].name, "Manifest"); + assert_eq!(inventory[1].slot, 1); + assert_eq!(inventory[1].name, "Token"); + assert_eq!(inventory[2].slot, 2); + assert_eq!(inventory[2].name, "Comm Log"); + } + + #[test] + fn occupied_slots_returns_correct_set() { + let mut world = setup_world(); + + let player = world.spawn_empty().id(); + let player_sid = world.resource_mut::().register(player); + + for slot in [0, 3, 7] { + world.spawn(( + CarriedBy(player_sid), + ItemName("Item".into()), + InventorySlot(slot), + )); + } + + let mut system_state = bevy_ecs::system::SystemState::< + Query<(Entity, &CarriedBy, &ItemName, &InventorySlot)>, + >::new(&mut world); + let items_query = system_state.get(&world); + + let mut slots = occupied_slots_for(player_sid, &items_query); + slots.sort(); + assert_eq!(slots, vec![0, 3, 7]); + } + + #[test] + fn max_slots_is_nine() { + assert_eq!(MAX_INVENTORY_SLOTS, 9); + } +} diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index b16cebe25..92c04a52b 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -6,6 +6,7 @@ use bevy_ecs::schedule::IntoScheduleConfigs; pub mod input; pub mod interaction; +pub mod inventory; pub mod monologue; pub mod movement; pub mod path_follow;