feat(engine): retire D-078 overheard conversation system (#848)

Per R-012: delete conversation.rs, both overheard content files, and
remove all 6 wire-up points (social_plugin, bridge/types, monologue,
voice/integration). Protocol version 22 → 23. Scope confirmed by
#842 audit — npc/ and content/global/ untouched. Surviving NPC
components (NpcName, NpcColorIndex, NpcConversation) migrated to
simulation/npc_components.rs for use by D-080 knowledge propagation.
Also applies pre-existing cargo fmt debt (names.rs and 4 others).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:08:26 +02:00
co-authored by Claude Sonnet 4.6
parent e11a9c308a
commit e86e53ec06
29 changed files with 645 additions and 1818 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -27,7 +27,6 @@ use crate::knowledge::types::{FactId, KnowledgeConfidence, KnowledgeSource, Stab
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
use crate::npc::interaction::{InteractionEvent, InteractionEventKind, InteractionMemory};
use crate::npc::relationships::{TrustEvent, TrustEventQueue};
use crate::simulation::conversation::{display_label_for_role, NpcColorIndex, NpcName};
use crate::simulation::knowledge_grant::KnowledgeGrant;
use crate::simulation::line_pool::LinePoolIndexResource;
use crate::simulation::line_pool::{
@@ -35,6 +34,7 @@ use crate::simulation::line_pool::{
};
use crate::simulation::monologue::{MonologueBuffer, MonologueState};
use crate::simulation::movement::PlayerCharacter;
use crate::simulation::npc_components::{display_label_for_role, NpcColorIndex, NpcName};
use crate::simulation::rng::SimRng;
use crate::simulation::time::SimulationTime;
use crate::storyteller::EngagementRecord;
+9 -3
View File
@@ -3,11 +3,11 @@
// PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode, ToggleStance)
use crate::bookmark::{BookmarkRegistry, SelectedBookmark};
use crate::knowledge::CultureResolverResource;
use crate::bridge::debug::DebugCommandBuffer;
use crate::bridge::types::{
FacingDirection, ObjectType, PlayerAction, PlayerInput, SimError, SimErrorKind, SnapshotBuffer,
};
use crate::knowledge::CultureResolverResource;
use crate::knowledge::{EntityRegistry, StableId};
use crate::perception::vision_cone::{facing_from_delta, Facing};
use crate::settings::{SettingsCommand, SettingsCommandBuffer};
@@ -449,7 +449,9 @@ pub fn process_player_input(
buf.pending_bookmark_catalog = Some(registry.build_catalog(resolver));
tracing::debug!("RequestBookmarkCatalog: catalog staged");
} else {
tracing::warn!("RequestBookmarkCatalog: BookmarkRegistry or SnapshotBuffer not available");
tracing::warn!(
"RequestBookmarkCatalog: BookmarkRegistry or SnapshotBuffer not available"
);
}
}
PlayerAction::ConfirmBookmark {
@@ -1208,7 +1210,11 @@ fn handle_confirm_bookmark(
if let Some(ref mut sel) = selected_bookmark {
sel.bookmark_id = Some(bookmark_id.clone());
sel.starting_location_id = Some(starting_location_id.clone());
tracing::info!(bookmark_id, starting_location_id, "ConfirmBookmark: selection recorded");
tracing::info!(
bookmark_id,
starting_location_id,
"ConfirmBookmark: selection recorded"
);
}
}
+1 -1
View File
@@ -7,7 +7,6 @@ use bevy_ecs::schedule::IntoScheduleConfigs;
pub mod chunk_streaming;
// Phase sub-plugins (#843)
pub mod contraband;
pub mod conversation;
pub mod dialogue;
pub mod economy;
pub mod economy_plugin;
@@ -25,6 +24,7 @@ pub mod modification;
pub mod monologue;
pub mod movement;
pub mod movement_plugin;
pub mod npc_components;
pub mod npc_knowledge_transfer;
pub mod path_follow;
pub mod pathfinding;
+6 -103
View File
@@ -16,8 +16,8 @@ use rand::Rng;
use crate::bridge::types::MonologueEvent;
use crate::knowledge::{ContradictionDetectedQueue, EntityRegistry};
use crate::perception::interpretation::ObservationTrigger;
use crate::simulation::conversation::NpcName;
use crate::simulation::movement::{PlayerCharacter, TilePosition};
use crate::simulation::npc_components::NpcName;
use crate::simulation::rng::EntityRng;
use crate::simulation::time::SimulationTime;
use crate::storyteller::EngagementRecord;
@@ -84,15 +84,6 @@ const HEAR_SOUND_LINES: &[(&str, &str)] = &[
("hear_sound_03", "Something just happened nearby."),
];
/// Hardcoded v0.1 witness_interaction monologue lines.
/// Fire when the player overhears an NPC-to-NPC conversation (D-078).
/// Future: move to content pools with trigger="witness_interaction".
const WITNESS_INTERACTION_LINES: &[(&str, &str)] = &[
("witness_01", "Interesting. Wonder what that was about."),
("witness_02", "I should remember what they just said."),
("witness_03", "They didn't know I was listening."),
];
/// Hardcoded v0.1 post_conversation monologue lines.
/// Fire after a player-NPC dialogue concludes (walk-away or natural end).
/// Future: move to content pools with trigger="post_conversation".
@@ -375,7 +366,6 @@ fn select_hardcoded_fallback(trigger: &str, rng: &mut impl Rng) -> (String, Stri
let lines = match trigger {
"observe_npc" => OBSERVE_NPC_LINES,
"hear_sound" => HEAR_SOUND_LINES,
"witness_interaction" => WITNESS_INTERACTION_LINES,
"post_conversation" => POST_CONVERSATION_LINES,
unknown => {
tracing::warn!(
@@ -413,8 +403,7 @@ fn sound_range_tiles(range: &crate::knowledge::types::SoundRange) -> u32 {
/// Priority order (first match wins):
/// 1. observe_npc (new entity spotted — uses previous-tick observation events)
/// 2. hear_sound (non-routine sound: Machinery, Alert)
/// 3. witness_interaction (overheard NPC-to-NPC conversation, D-078)
/// 4. post_conversation (player-NPC dialogue concluded)
/// 3. post_conversation (player-NPC dialogue concluded)
///
/// System ordering: after all event producers + recognition/anomaly monologue
/// systems, before compute_observer_snapshot.
@@ -429,7 +418,6 @@ pub fn trigger_event_monologue(
&TilePosition,
&mut MonologueState,
&mut MonologueBuffer,
Option<&crate::simulation::conversation::ConversationEventBuffer>,
&mut EntityRng,
),
With<PlayerCharacter>,
@@ -441,9 +429,7 @@ pub fn trigger_event_monologue(
// Saved for NPC attribution (engagement tracking #570) and trigger detection.
let post_conv_npcs: Vec<Entity> = post_conv_queue.drain();
let Ok((player_pos, mut state, mut buffer, conv_buffer_opt, mut entity_rng)) =
query.single_mut()
else {
let Ok((player_pos, mut state, mut buffer, mut entity_rng)) = query.single_mut() else {
return;
};
@@ -468,11 +454,6 @@ pub fn trigger_event_monologue(
.unwrap_or(false)
{
Some("hear_sound")
} else if conv_buffer_opt
.map(|b| !b.events.is_empty())
.unwrap_or(false)
{
Some("witness_interaction")
} else if !post_conv_npcs.is_empty() {
Some("post_conversation")
} else {
@@ -511,7 +492,7 @@ pub fn trigger_event_monologue(
);
// Engagement tracking (#570): attribute monologue_trigger_count to specific NPCs.
// Only NPC-context triggers are attributed — hear_sound/witness_interaction are not NPC-specific.
// Only NPC-context triggers are attributed — hear_sound is not NPC-specific.
match trigger {
"observe_npc" => {
// Attribute to all NPCs whose NewEntity event triggered this monologue
@@ -537,7 +518,7 @@ pub fn trigger_event_monologue(
}
}
}
_ => {} // hear_sound, witness_interaction: no NPC-specific attribution
_ => {} // hear_sound: no NPC-specific attribution
}
}
@@ -1345,7 +1326,6 @@ mod tests {
use crate::perception::interpretation::{
ObservationEvent, ObservationEventQueue, ObservationTrigger,
};
use crate::simulation::conversation::ConversationEventBuffer;
use crate::simulation::sound::{SoundEvent, SoundEventKind, SoundEventQueue};
fn setup_event_world() -> World {
@@ -1366,7 +1346,6 @@ mod tests {
TilePosition::new(10, 10, 0),
MonologueState::default(),
MonologueBuffer::default(),
ConversationEventBuffer::default(),
))
.id()
}
@@ -1521,36 +1500,6 @@ mod tests {
);
}
#[test]
fn witness_interaction_fires_on_conversation_event() {
let mut world = setup_event_world();
let player = spawn_event_player(&mut world);
// Pre-fill ConversationEventBuffer with an overheard conversation
world
.get_mut::<ConversationEventBuffer>(player)
.unwrap()
.events
.push(crate::simulation::conversation::ConversationEvent {
occluded_line: "Keep your head down today.".to_string(),
speaker_id: 100,
target_id: 101,
speaker_name: "Worker".to_string(),
target_name: "Courier".to_string(),
speaker_color_index: 0,
target_color_index: 1,
});
run_event_system(&mut world);
let buf = world.get::<MonologueBuffer>(player).unwrap();
assert!(
buf.event.is_some(),
"witness_interaction should fire when conversation overheard"
);
assert!(buf.event.as_ref().unwrap().id.starts_with("witness_"));
}
#[test]
fn post_conversation_fires_on_queue_entry() {
let mut world = setup_event_world();
@@ -1668,47 +1617,6 @@ mod tests {
);
}
#[test]
fn priority_hear_sound_over_witness_interaction() {
let mut world = setup_event_world();
let player = spawn_event_player(&mut world);
// Sound event
world
.resource_mut::<SoundEventQueue>()
.events
.push(SoundEvent::at(
&TilePosition::new(11, 10, 0),
SoundEventKind::Alert,
1.0,
crate::knowledge::types::SoundRange::Medium,
None,
));
// Conversation event
world
.get_mut::<ConversationEventBuffer>(player)
.unwrap()
.events
.push(crate::simulation::conversation::ConversationEvent {
occluded_line: "Test".to_string(),
speaker_id: 100,
target_id: 101,
speaker_name: "A".to_string(),
target_name: "B".to_string(),
speaker_color_index: 0,
target_color_index: 1,
});
run_event_system(&mut world);
let buf = world.get::<MonologueBuffer>(player).unwrap();
assert!(
buf.event.as_ref().unwrap().id.starts_with("hear_sound_"),
"hear_sound should have priority over witness_interaction"
);
}
#[test]
fn event_trigger_updates_last_fired_tick() {
let mut world = setup_event_world();
@@ -1813,12 +1721,7 @@ mod tests {
#[test]
fn hardcoded_lines_all_valid() {
for lines in &[
OBSERVE_NPC_LINES,
HEAR_SOUND_LINES,
WITNESS_INTERACTION_LINES,
POST_CONVERSATION_LINES,
] {
for lines in &[OBSERVE_NPC_LINES, HEAR_SOUND_LINES, POST_CONVERSATION_LINES] {
assert!(!lines.is_empty());
for (id, text) in *lines {
assert!(!id.is_empty(), "line id should not be empty");
+50
View File
@@ -0,0 +1,50 @@
// NPC component types shared across simulation systems.
//
// Extracted from conversation.rs (D-078 scrapped per R-012). These types
// survive because they are used by dialogue, debug, knowledge transfer,
// monologue, and the D-080 knowledge propagation system.
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
/// Display name for an NPC.
/// Attached during content spawn.
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct NpcName(pub String);
/// Map a dialogue role string to a display label for use when the player
/// does not yet know the NPC's real name.
pub fn display_label_for_role(role: &str) -> String {
match role {
"dock-worker" => "Dock Worker",
"courier" => "Courier",
"maintenance-tech" => "Technician",
"new-hire" | "day-worker" | "transit-worker" => "Worker",
"scheduler" => "Scheduler",
"shift-supervisor" => "Supervisor",
"bartender" => "Bartender",
"bar-regular" => "Patron",
_ => "Bystander",
}
.to_string()
}
/// Color index (0-7) for rendering this NPC with a distinct color.
/// Assigned at spawn time as `(stable_id % 8)`.
#[derive(Component, Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NpcColorIndex(pub u8);
/// Active NPC-to-NPC conversation session (D-080 knowledge propagation).
/// Attached to the "speaker" NPC (the one who initiated).
/// The "listener" is tracked by entity reference.
#[derive(Component, Debug)]
pub struct NpcConversation {
/// The other NPC in the conversation.
pub partner: Entity,
/// Tick when the conversation started.
pub started_tick: u64,
/// Tick when the conversation will end.
pub end_tick: u64,
/// Ticks since last line was spoken (for pacing).
pub ticks_since_last_line: u64,
}
@@ -28,8 +28,8 @@ use crate::knowledge::{
};
use crate::npc::relationships::RelationshipGraph;
use crate::npc::Npc;
use crate::simulation::conversation::NpcConversation;
use crate::simulation::movement::{PlayerCharacter, TilePosition};
use crate::simulation::npc_components::NpcConversation;
use crate::simulation::rng::SimRng;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::SimulationTime;
@@ -438,8 +438,8 @@ mod tests {
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
use crate::npc::relationships::{RelationshipEdge, RelationshipGraph};
use crate::npc::RelationshipKind;
use crate::simulation::conversation::NpcConversation;
use crate::simulation::movement::TilePosition;
use crate::simulation::npc_components::NpcConversation;
use crate::simulation::rng::SimRng;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::SimulationTime;
+4 -11
View File
@@ -1,8 +1,6 @@
//! Social simulation plugin — NPC conversations, knowledge transfer, disclosure.
//! Social simulation plugin — NPC knowledge transfer, disclosure, and social systems.
//!
//! All systems run in [`TickPhase::Simulation`]. Intra-phase ordering:
//! - conversations → knowledge_transfer (transfer reads conversation results)
//! - conversations → sound collection (sound reads conversation events)
//! All systems run in [`TickPhase::Simulation`].
use bevy_app::prelude::*;
use bevy_ecs::schedule::IntoScheduleConfigs;
@@ -20,16 +18,11 @@ impl Plugin for SocialPlugin {
.add_systems(
Update,
(
super::conversation::run_npc_conversations,
super::npc_knowledge_transfer::transfer_npc_knowledge
.after(super::conversation::run_npc_conversations),
super::sound::collect_sound_events
.after(super::conversation::run_npc_conversations),
super::npc_knowledge_transfer::transfer_npc_knowledge,
super::sound::collect_sound_events,
// Voice enrichment (D-138) — rewrite NPC text with voiced variants.
// No-op when VoiceCacheResource is absent.
crate::voice::integration::voice_enrich_dialogue_response,
crate::voice::integration::voice_enrich_conversation_events
.after(super::conversation::run_npc_conversations),
// POI discovery reads visibility geometry (also Simulation phase)
super::poi_discovery::discover_pois,
// Follow state reads visibility geometry + movement