feat(simulation): smuggler inventory — CarriedBy, Take/Place verbs (#424)

Implement physical inventory system per D-065:

- CarriedBy(StableId) component marks items as carried by an entity
- ItemName and InventorySlot components for display and slot assignment
- Take verb: removes TilePosition, adds CarriedBy + InventorySlot
- Place verb: removes CarriedBy + InventorySlot, adds TilePosition
- Observer snapshot populates player_inventory from carried items query
- Info boundary: carried items lack TilePosition, naturally excluded
  from all spatial queries (visibility, interactions) for other entities
- 9-slot capacity (3x3 grid per OQ-24), auto-assigns first free slot
- 8 new tests covering take, place, sequential slots, full rejection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 15:44:37 +01:00
co-authored by Claude Opus 4.6
parent 32422ac4bc
commit 6ef7029707
4 changed files with 609 additions and 10 deletions
+17 -3
View File
@@ -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<VisibilityGeometry>,
registry: Res<EntityRegistry>,
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<PlayerCharacter>,
>,
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<SnapshotBuffer>,
) {
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,
&registry,
)
})
.unwrap_or_default();
let (mut entities, visible_ids) =
filter_visible_entities(&geometry, &registry, 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,