feat(simulation): basic environmental interaction — doors, examine, terminals (#246)
- Add DoorState component tracking is_open and blocking_tile; add DoorInteractRequest per-player component consumed by new process_door_interaction system (toggles walkability each use) - Add TerminalInteracted event, TerminalInteractedQueue resource, TerminalInteractRequest component, and process_terminal_interaction system (emits event on Use verb) - Add ExamineText(String) component for authored object examine text; extend process_examine_interaction with object examine path: uses ExamineText if present, falls back to generic string if absent - Fix: add Without<ObjectType> filter to npc_query in process_examine_interaction — previously any entity with TilePosition was mis-routed through the NPC text generator - Add SaveStateV1.open_doors: Vec<StableId> with #[serde(default)] for backward-compatible serialization - Add "Open"/"Close" → DoorInteractRequest and "Use" → TerminalInteractRequest dispatch in process_player_input - 10 integration tests in tests/environmental_interaction.rs covering all acceptance criteria: door toggle (both directions), open-to-close, invalid target, readable examine (with/without ExamineText), out-of-range, terminal event emission, request cleanup, and save state round-trip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Interaction system — proximity detection + multi-verb InteractionOptions
|
||||
// Implements #404: server-side verb computation for context-sensitive [E] key
|
||||
// Extended by #421: ObjectType component + verb sets per type (D-057)
|
||||
// Extended by #246: door toggle, terminal event, examine-text (#246)
|
||||
// Spec: docs/design/interaction-verbs-v0.1.md
|
||||
// D-060: actions[] renamed to verbs[] across all surfaces
|
||||
//
|
||||
@@ -10,14 +11,17 @@
|
||||
// Phase 2 filtering (KG-gated verbs) handled by #422.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Re-export ObjectType for backward compatibility — definition moved to bridge::types (#422).
|
||||
pub use crate::bridge::types::ObjectType;
|
||||
use crate::bridge::types::{EntityKind, MovementStance, NearbyInteraction, VerbKind, VerbOption};
|
||||
use crate::knowledge::EntityRegistry;
|
||||
use crate::knowledge::types::StableId;
|
||||
use crate::npc::Npc;
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition};
|
||||
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
||||
use crate::simulation::stance::Stance;
|
||||
use crate::simulation::time::SimulationTime;
|
||||
|
||||
/// Interaction range thresholds (Manhattan distance, same z-level)
|
||||
pub(crate) const CLOSE_RANGE: u32 = 2;
|
||||
@@ -317,6 +321,157 @@ impl NearbyInteractionBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// #246 — Door behavior (toggle walkability)
|
||||
// ===========================================================================
|
||||
|
||||
/// Component tracking the open/closed state of a door and its blocking tile.
|
||||
///
|
||||
/// Attach to any entity with `ObjectType::Door`. The `blocking_tile` is the
|
||||
/// tile that becomes walkable when the door opens and impassable when it closes.
|
||||
///
|
||||
/// Door state is persisted in `SaveStateV1.open_doors` (D-010).
|
||||
#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DoorState {
|
||||
/// Whether the door is currently open (walkable) or closed (blocking).
|
||||
pub is_open: bool,
|
||||
/// The tile whose walkability is toggled by this door.
|
||||
pub blocking_tile: TilePosition,
|
||||
}
|
||||
|
||||
impl DoorState {
|
||||
pub fn new(blocking_tile: TilePosition) -> Self {
|
||||
DoorState {
|
||||
is_open: false,
|
||||
blocking_tile,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker: player requested a door interaction (Open or Close) this tick.
|
||||
///
|
||||
/// Inserted by `process_player_input` when verb is "Open" or "Close" on a
|
||||
/// Door entity. Consumed and removed by `process_door_interaction`.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct DoorInteractRequest {
|
||||
/// The ECS entity of the door to toggle.
|
||||
pub door_entity: Entity,
|
||||
}
|
||||
|
||||
/// Event emitted when a player uses a Terminal (#246).
|
||||
///
|
||||
/// Downstream systems (dialogue hook — future work) subscribe to this queue.
|
||||
/// The queue is not automatically drained — consumers must call `drain()`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TerminalInteracted {
|
||||
/// Stable ID of the terminal entity.
|
||||
pub terminal_id: StableId,
|
||||
/// Tick when the interaction occurred.
|
||||
pub tick: u64,
|
||||
}
|
||||
|
||||
/// Resource: queue of terminal interaction events (#246).
|
||||
#[derive(Resource, Default)]
|
||||
pub struct TerminalInteractedQueue {
|
||||
pub events: Vec<TerminalInteracted>,
|
||||
}
|
||||
|
||||
impl TerminalInteractedQueue {
|
||||
pub fn push(&mut self, event: TerminalInteracted) {
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> Vec<TerminalInteracted> {
|
||||
std::mem::take(&mut self.events)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.events.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker: player requested a Terminal interaction this tick.
|
||||
///
|
||||
/// Inserted by `process_player_input` when verb is "Use" on a Terminal entity.
|
||||
/// Consumed and removed by `process_terminal_interaction`.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct TerminalInteractRequest {
|
||||
pub terminal_entity: Entity,
|
||||
}
|
||||
|
||||
/// System: toggle door open/closed state and update walkability map (#246).
|
||||
///
|
||||
/// Reads `DoorInteractRequest` on the player entity. Toggles `DoorState.is_open`
|
||||
/// and updates `WalkabilityMap` for the door's `blocking_tile`.
|
||||
///
|
||||
/// Ordering: after `process_player_input`, before movement validation.
|
||||
pub fn process_door_interaction(
|
||||
mut commands: Commands,
|
||||
mut walkability: ResMut<WalkabilityMap>,
|
||||
player_query: Query<(Entity, &DoorInteractRequest), With<PlayerCharacter>>,
|
||||
mut door_query: Query<&mut DoorState>,
|
||||
) {
|
||||
let Ok((player_entity, req)) = player_query.single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let door_entity = req.door_entity;
|
||||
commands.entity(player_entity).remove::<DoorInteractRequest>();
|
||||
|
||||
let Ok(mut door) = door_query.get_mut(door_entity) else {
|
||||
tracing::warn!(?door_entity, "process_door_interaction: no DoorState on target");
|
||||
return;
|
||||
};
|
||||
|
||||
// Toggle state
|
||||
door.is_open = !door.is_open;
|
||||
let walkable = door.is_open;
|
||||
let tile = door.blocking_tile;
|
||||
|
||||
walkability.set_walkable(&tile, walkable);
|
||||
|
||||
tracing::info!(
|
||||
?tile,
|
||||
is_open = door.is_open,
|
||||
"Door toggled: tile walkability set to {walkable}"
|
||||
);
|
||||
}
|
||||
|
||||
/// System: emit TerminalInteracted event when player uses a Terminal (#246).
|
||||
///
|
||||
/// Reads `TerminalInteractRequest` on the player entity, emits to
|
||||
/// `TerminalInteractedQueue`, and removes the request.
|
||||
pub fn process_terminal_interaction(
|
||||
mut commands: Commands,
|
||||
time: Res<SimulationTime>,
|
||||
registry: Res<EntityRegistry>,
|
||||
player_query: Query<(Entity, &TerminalInteractRequest), With<PlayerCharacter>>,
|
||||
mut queue: ResMut<TerminalInteractedQueue>,
|
||||
) {
|
||||
let Ok((player_entity, req)) = player_query.single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let terminal_entity = req.terminal_entity;
|
||||
commands.entity(player_entity).remove::<TerminalInteractRequest>();
|
||||
|
||||
let terminal_id = registry
|
||||
.to_stable(terminal_entity)
|
||||
.unwrap_or(StableId(terminal_entity.to_bits()));
|
||||
|
||||
queue.push(TerminalInteracted {
|
||||
terminal_id,
|
||||
tick: time.tick,
|
||||
});
|
||||
|
||||
tracing::info!(
|
||||
?terminal_entity,
|
||||
terminal_id = terminal_id.0,
|
||||
tick = time.tick,
|
||||
"Terminal used: TerminalInteracted event queued"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user