feat(simulation): movement profile per archetype and sprint interaction suppression (#418, #419)

Add MovementProfile component with per-archetype default stances (D-053).
Sprint stance now explicitly clears the interaction buffer — no verbs
computed or sent during sprint (D-055). Anomaly monologue pipeline is
unaffected (handled by separate system).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 16:00:09 +01:00
co-authored by Claude Opus 4.6
parent 2fc8bf9545
commit 1d76100f5d
2 changed files with 216 additions and 3 deletions
+88
View File
@@ -12,6 +12,43 @@ use bevy_ecs::prelude::*;
use crate::bridge::types::MovementStance;
/// Per-archetype default movement configuration (D-053).
/// Stores the default stance so spawn code can initialize Stance from it.
///
/// v0.1: smuggler and detective both default to Walk.
/// Future archetypes may differ (e.g., maintenance worker → Careful).
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct MovementProfile {
pub default_stance: MovementStance,
}
impl Default for MovementProfile {
fn default() -> Self {
Self {
default_stance: MovementStance::Walk,
}
}
}
impl MovementProfile {
pub fn smuggler() -> Self {
Self {
default_stance: MovementStance::Walk,
}
}
pub fn detective() -> Self {
Self {
default_stance: MovementStance::Walk,
}
}
/// Create the initial Stance component from this profile's default.
pub fn initial_stance(&self) -> Stance {
Stance(self.default_stance)
}
}
/// ECS component tracking an entity's current movement stance.
/// Attached to PlayerCharacter (and potentially NPCs in future).
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
@@ -159,4 +196,55 @@ mod tests {
// Switch to sprint mid-cooldown
assert!(cd.try_move(MovementStance::Sprint)); // sprint allows every tick
}
// -----------------------------------------------------------------------
// MovementProfile tests (#418, D-053)
// -----------------------------------------------------------------------
#[test]
fn movement_profile_default_is_walk() {
let profile = MovementProfile::default();
assert_eq!(profile.default_stance, MovementStance::Walk);
}
#[test]
fn movement_profile_smuggler_defaults_to_walk() {
let profile = MovementProfile::smuggler();
assert_eq!(profile.default_stance, MovementStance::Walk);
}
#[test]
fn movement_profile_detective_defaults_to_walk() {
let profile = MovementProfile::detective();
assert_eq!(profile.default_stance, MovementStance::Walk);
}
#[test]
fn movement_profile_initial_stance_matches_default() {
let profile = MovementProfile::smuggler();
let stance = profile.initial_stance();
assert_eq!(stance.0, profile.default_stance);
}
#[test]
fn movement_profile_custom_default_stance() {
let profile = MovementProfile {
default_stance: MovementStance::Careful,
};
assert_eq!(profile.default_stance, MovementStance::Careful);
assert_eq!(profile.initial_stance().0, MovementStance::Careful);
}
#[test]
fn movement_profile_as_ecs_component() {
let mut world = bevy_ecs::world::World::new();
let profile = MovementProfile::smuggler();
let entity = world.spawn((profile, profile.initial_stance(), PlayerMoveCooldown::default())).id();
let stored = world.get::<MovementProfile>(entity).unwrap();
assert_eq!(stored.default_stance, MovementStance::Walk);
let stance = world.get::<Stance>(entity).unwrap();
assert_eq!(stance.0, MovementStance::Walk);
}
}