feat(simulation): Sprint 6 Touch — stance, tile presence, verbs, protocol v6

Implements the core Sprint 6: Touch systems across 5 tickets:

- #449 ObserverSnapshot v6: add player_stance (MovementStance) and
  player_inventory (Vec<InventoryItem>) wire fields with serde defaults
  for backward compatibility. Bump PROTOCOL_VERSION 5→6.

- #417 Stance system: Sprint/Walk/Careful/Crouch movement stance with
  tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers,
  and PlayerMoveCooldown component. ToggleStanceUp/Down player actions.

- #420 TilePresence: posture-layer collision system allowing same-tile
  occupancy for different layers (Standing/Prone/Seated/Fixture).
  Layer-based collision in validate_movement.

- #421 ObjectType component: Readable/Container/Terminal/Door/Pickup/
  Furniture types with Phase 1 verb sets computed from type + proximity.

- #422 Phase 2 verb filter: KG-gated observer-side verb processing —
  POI priority flips, Confront injection at KnowsDetails+, contradiction
  marking, archetype-specific label relabeling (Smuggler/Detective).

217 unit tests + 17 integration tests passing. All MessagePack fixtures
regenerated for v6 wire format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 15:38:59 +01:00
co-authored by Claude Opus 4.6
parent 651b1d34a6
commit 98f4cedc03
18 changed files with 1825 additions and 95 deletions
+173 -4
View File
@@ -15,7 +15,7 @@ pub use crate::simulation::time::{DayPhase, TickRate};
/// negotiation is unnecessary. Client should reject snapshots with version !=
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
/// period, then the default is removed once both sides are updated.
pub const PROTOCOL_VERSION: u8 = 5;
pub const PROTOCOL_VERSION: u8 = 6;
/// The ONLY data structure crossing the client-server boundary (D-020)
/// Contains all information visible to the observer at a given tick.
@@ -24,10 +24,11 @@ pub const PROTOCOL_VERSION: u8 = 5;
/// v3 adds: relationship (D-033 entity color), observation (Visible/Remembered).
/// v4 adds: nearby_interactions (D-060, #404 proximity + verbs[]).
/// v5 adds: current_monologue (#414 internal monologue pipeline).
/// v6 adds: player_stance (#449, D-053), player_inventory (#449, D-065).
/// Future fields: ambient sound events, HUD state (D-020 expansion).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObserverSnapshot {
/// Protocol version for forward compatibility. Current: 4.
/// Protocol version for forward compatibility. Current: 6.
pub version: u8,
/// Simulation tick when this snapshot was produced
pub tick: u64,
@@ -35,6 +36,15 @@ pub struct ObserverSnapshot {
pub game_time: GameTime,
/// Player character's facing direction for vision cone (D-015)
pub player_facing: FacingDirection,
/// Player's current movement stance for HUD display (#449, D-053).
/// Defaults to Walk when stance component is absent.
#[serde(default)]
pub player_stance: MovementStance,
/// Items in the player's inventory (#449, D-065).
/// Visible only to this observer per D-010 info boundary.
/// Empty when no CarriedBy component is present.
#[serde(default)]
pub player_inventory: Vec<InventoryItem>,
/// All entities visible to the observer (filtered by LOS + vision cone)
pub entities: Vec<VisibleEntity>,
/// Tiles visible to the observer for fog rendering
@@ -62,6 +72,81 @@ pub struct GameTime {
pub tick_rate: TickRate,
}
/// Player movement stance for tick-based movement speed (#449, D-053).
/// Sprint/Walk/Careful/Crouch affect movement ticks, monologue rate, and
/// interaction buffer availability. Wire format for ObserverSnapshot.
/// v0.1 scope: Sprint/Walk/Careful/Crouch only (Prone deferred).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum MovementStance {
/// 1 tile/tick, monologue at 40%, interaction buffer suppressed (D-055)
Sprint,
/// 1 tile/2 ticks, monologue at 100% (default)
#[default]
Walk,
/// 1 tile/3 ticks, monologue at 150%
Careful,
/// 1 tile/4 ticks, uses Prone/Seated posture layer (D-054)
Crouch,
}
impl MovementStance {
/// Move one step up the stance ladder (toward Sprint).
/// Returns self if already at the top.
pub fn step_up(self) -> Self {
match self {
Self::Crouch => Self::Careful,
Self::Careful => Self::Walk,
Self::Walk => Self::Sprint,
Self::Sprint => Self::Sprint,
}
}
/// Move one step down the stance ladder (toward Crouch).
/// Returns self if already at the bottom.
pub fn step_down(self) -> Self {
match self {
Self::Sprint => Self::Walk,
Self::Walk => Self::Careful,
Self::Careful => Self::Crouch,
Self::Crouch => Self::Crouch,
}
}
/// Ticks per movement step for this stance.
pub fn ticks_per_move(self) -> u32 {
match self {
Self::Sprint => 1,
Self::Walk => 2,
Self::Careful => 3,
Self::Crouch => 4,
}
}
/// Monologue rate multiplier as a percentage (100 = baseline).
/// Sprint suppresses to 40%, Careful enhances to 150% (D-053).
pub fn monologue_rate_percent(self) -> u32 {
match self {
Self::Sprint => 40,
Self::Walk => 100,
Self::Careful => 150,
Self::Crouch => 100,
}
}
}
/// An item in the player's inventory, crossing the wire boundary (#449, D-065).
/// Only items carried by the observer are included (D-010 info boundary).
/// Slot positions map to a 3x3 grid (0-8), 9 slots universal.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryItem {
/// Wire-format entity identifier for the item
pub item_id: u64,
/// Display name for inventory UI
pub name: String,
/// Inventory slot index (0-8 for 3x3 grid)
pub slot: u8,
}
/// 8-directional facing direction, matching movement system.
/// Used for vision cone computation (D-015) and snapshot wire format.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
@@ -142,6 +227,43 @@ pub enum EntityKind {
Terrain,
}
/// Object type for D-057 Phase 1 verb computation (#421).
///
/// Determines the maximum possible verb set for an interactable world object.
/// NPCs don't use ObjectType — they have their own verb logic (Talk/ExamineNpc).
/// Phase 2 (#422) filters these verbs by the observer's knowledge graph.
///
/// Defined here in bridge::types because it appears on the wire in
/// NearbyInteraction.object_type for Phase 2 context.
/// VerbDef and verb_set() remain in simulation::interaction.
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ObjectType {
/// Documents, manifests, notices — can be read
Readable,
/// Crates, lockers, cargo containers — can be opened and searched
Container,
/// Access terminals, comms panels — can be used
Terminal,
/// Doors, hatches, bulkheads — can be opened/closed
Door,
/// Small items that can be picked up (physical inventory, D-065)
Pickup,
/// Chairs, benches, consoles — can be sat at
Furniture,
}
/// Character archetype for Phase 2 verb filtering (#422) and monologue pool
/// selection. Determines how the character perceives and labels interactions.
/// v0.1: Smuggler and Detective (the two playable characters).
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum CharacterArchetype {
/// Smuggler character — sees Move/Stash on containers, physical manipulation verbs
Smuggler,
/// Detective character — sees Scan/Flag on containers, investigation verbs
#[default]
Detective,
}
/// Semantic player actions, not raw key events (D-020)
/// Timestamped for deterministic processing
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -174,6 +296,10 @@ pub enum PlayerAction {
Unpause,
/// Set tick rate: Full (1.0), Half (0.5), or Paused (0.0) per D-052
SetTickRate(TickRate),
/// Move one step up the stance ladder (toward Sprint) per D-053
ToggleStanceUp,
/// Move one step down the stance ladder (toward Crouch) per D-053
ToggleStanceDown,
}
/// Available interaction verbs for a nearby entity (D-060, #404)
@@ -188,6 +314,15 @@ pub struct NearbyInteraction {
pub distance: u32,
/// Available verbs sorted by priority (index 0 = highest priority)
pub verbs: Vec<VerbOption>,
/// Object type for Phase 2 verb filter context (#422).
/// None for NPCs and untyped objects. Enables archetype-specific
/// label remapping (smuggler/detective see different labels for same verb).
#[serde(default)]
pub object_type: Option<ObjectType>,
/// Whether the observer has contradicted knowledge about this entity (#422).
/// Client may render a contradiction indicator (e.g., amber warning icon).
#[serde(default)]
pub contradicted: bool,
}
/// A single available verb on a nearby entity
@@ -203,14 +338,48 @@ pub struct VerbOption {
pub available: bool,
}
/// Verb types for the interaction system (D-060)
/// Verb types for the interaction system (D-057, D-060)
/// Only active verbs appear in verbs[]. Passive (Look, Overhear) and
/// reactive (Monologue) verbs fire independently.
///
/// Phase 1 verbs (simulation, no KG): derived from ObjectType component (#421).
/// Phase 2 verbs (observer, reads KG): filtered/augmented by #422.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VerbKind {
ExamineObject,
// --- NPC verbs ---
/// Observe an NPC (available at mid + close range)
ExamineNpc,
/// Talk to an NPC (close range only)
Talk,
// --- Object verbs (D-057, per ObjectType) ---
/// Generic observation — common to all object types
Observe,
/// Readable objects (manifests, logs, notices)
Read,
/// Container / Door — open it
Open,
/// Door — close it
Close,
/// Container — deeper search (distinct from Open)
Search,
/// Terminal — access logs, comms
Use,
/// Pickup items — physical inventory (D-065)
Take,
/// Furniture — sit/use
Sit,
// --- Phase 2 verbs (observer, KG-gated, #422) ---
/// Confront an NPC about known facts/contradictions.
/// Phase 2 only: injected when observer has KnowsDetails+ confidence.
/// Close range only. Opens confrontation dialogue (D-063).
Confront,
// --- Legacy fallback ---
/// Untyped object examination (entities without ObjectType component).
/// Prefer ObjectType-derived verbs for new content.
ExamineObject,
}
/// Internal monologue event sent to the client for display (#414).