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
+4 -86
View File
@@ -1,15 +1,13 @@
//! Observer integration for the voice pipeline (D-138, Phase 3).
//!
//! Two enrichment systems run before `compute_observer_snapshot` and rewrite
//! NPC text in the dialogue and conversation buffers with voiced variants
//! looked up from the cache. Cache miss → base text (never blocks).
//! One enrichment system runs before `compute_observer_snapshot` and rewrites
//! NPC dialogue text with voiced variants looked up from the cache.
//! Cache miss → base text (never blocks).
//!
//! ## System ordering
//!
//! ```text
//! process_talk_interaction ─┐
//! run_npc_conversations ─┤─► voice_enrich_dialogue_response ─┐
//! └─► voice_enrich_conversation_events ─► compute_observer_snapshot
//! process_talk_interaction ─► voice_enrich_dialogue_response ─► compute_observer_snapshot
//! ```
//!
//! ## Content index derivation
@@ -24,7 +22,6 @@ use bevy_ecs::prelude::*;
use crate::knowledge::{EntityRegistry, StableId};
use crate::npc::tell_state::DerivedTellState;
use crate::npc::{Npc, NpcVoiceProfile};
use crate::simulation::conversation::ConversationEventBuffer;
use crate::simulation::dialogue::DialogueResponseBuffer;
use crate::simulation::movement::{PlayerCharacter, TilePosition};
use crate::simulation::zone::ZoneMap;
@@ -127,85 +124,6 @@ pub fn voice_enrich_dialogue_response(
response.text = voiced;
}
/// Enrich the conversation event buffer with voiced lines from the cache.
///
/// Must run after `run_npc_conversations` and before
/// `compute_observer_snapshot`. No-op when the voice cache resource is absent.
///
/// Each event in the buffer is processed: the pre-occlusion base text is
/// not available at this point (occlusion has already been applied), so the
/// `occluded_line` is treated as the base text for the voice lookup.
/// This means the voice register wraps the already-occluded line.
///
/// ## Accepted risk: re-voicing of heavily occluded text
///
/// When many words are dropped by D-078 occlusion, the remaining text may
/// be fragmentary ("... the ... came in ..."). Re-voicing such fragments can
/// produce incoherent output. This is acceptable for two reasons:
/// 1. Cache misses are common for conversation text (no pre-baking pipeline),
/// so the base text fallback in `voiced_behavior()` fires most of the time.
/// 2. Even incoherent voiced output is no worse than the already-degraded
/// overheard line — the occlusion itself has already broken coherence.
/// The player's inability to fully parse overheard speech is the mechanic.
#[allow(clippy::type_complexity)]
pub fn voice_enrich_conversation_events(
voice_cache: Option<Res<VoiceCacheResource>>,
registry: Res<EntityRegistry>,
zone_map: Option<Res<ZoneMap>>,
player_query: Query<&TilePosition, With<PlayerCharacter>>,
npc_voice_query: Query<(&NpcVoiceProfile, Option<&DerivedTellState>), With<Npc>>,
mut conversation_buffer: Query<&mut ConversationEventBuffer, With<PlayerCharacter>>,
) {
let Some(voice_cache) = voice_cache else {
return;
};
let Ok(mut buffer) = conversation_buffer.single_mut() else {
return;
};
if buffer.events.is_empty() {
return;
}
let zone_id = player_zone_id(&player_query, zone_map.as_deref());
for event in &mut buffer.events {
let speaker_entity = registry.to_entity(&StableId(event.speaker_id));
let Some(entity) = speaker_entity else {
continue;
};
let Ok((voice_profile, tell_state_opt)) = npc_voice_query.get(entity) else {
continue;
};
let tell_state = tell_state_opt.and_then(|t| t.category);
// Conversation lines don't have a stable line_id; derive content_index
// from the occluded line text. The full cache key is
// (culture_id, npc_stable_id, content_type, content_index, tell_state),
// so a u16 hash collision requires two different lines from the same
// speaker with the same tell state to hash identically — at ~65K
// possible values this is extremely rare, and the worst outcome is
// a stale voiced line being served instead of base text. Acceptable.
let content_index = content_index_from_line_id(&event.occluded_line);
let voiced = voiced_behavior(
&voice_cache.cache,
zone_id,
&voice_profile.culture_id,
event.speaker_id,
ContentType::Dialogue,
content_index,
tell_state,
&event.occluded_line,
false,
);
event.occluded_line = voiced;
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------