feat(simulation): add NPC name masking with role labels and color index

Server-side infrastructure for information asymmetry in dialogue.
NPCs display role labels (Worker, Supervisor, Patron) instead of
real names until the player explicitly learns them via KG lookup.
NpcColorIndex (stable_id % 8) persists across name reveal.

ConversationEvent and DialogueResponseEvent carry display names +
color indices on the wire with serde(default) for compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 13:42:59 +01:00
co-authored by Claude Opus 4.6
parent 82e58559ba
commit 28e7e6d9ad
12 changed files with 446 additions and 63 deletions
+29 -3
View File
@@ -21,6 +21,7 @@ use bevy_ecs::prelude::*;
use rand::Rng;
use crate::bridge::types::{DialogueResponseEvent, MonologueEvent, RelationshipState};
use crate::simulation::conversation::{display_label_for_role, NpcColorIndex, NpcName};
use crate::content::line_pool::{
AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier,
};
@@ -360,7 +361,13 @@ pub fn process_talk_interaction(
),
With<PlayerCharacter>,
>,
mut npc_query: Query<(&DialogueProfile, Option<&CurrentMood>, Option<&mut InteractionMemory>)>,
mut npc_query: Query<(
&DialogueProfile,
Option<&CurrentMood>,
Option<&mut InteractionMemory>,
Option<&NpcName>,
Option<&NpcColorIndex>,
)>,
) {
let Some(line_pool) = line_pool else {
return;
@@ -380,8 +387,10 @@ pub fn process_talk_interaction(
let target = talk_request.target;
// Look up NPC dialogue profile, mood, and interaction history (#325)
let Ok((profile, mood_opt, mut interaction_mem_opt)) = npc_query.get_mut(target) else {
// Look up NPC dialogue profile, mood, interaction history, name, and color (#325)
let Ok((profile, mood_opt, mut interaction_mem_opt, npc_name_opt, color_idx_opt)) =
npc_query.get_mut(target)
else {
tracing::debug!(
"Talk target {:?} has no DialogueProfile — cannot select dialogue",
target
@@ -479,10 +488,27 @@ pub fn process_talk_interaction(
return;
};
// Resolve speaker display name: use real name if player KG has "name"
// attribute for the target, otherwise fall back to role label.
let speaker_display_name = {
let known = observer_kg
.entity_knowledge(&speaker_stable)
.map(|e| e.known_attributes.contains_key("name"))
.unwrap_or(false);
if known {
npc_name_opt.map(|n| n.0.clone()).unwrap_or_else(|| "Unknown".to_string())
} else {
display_label_for_role(&profile.role)
}
};
let speaker_color = color_idx_opt.map(|c| c.0).unwrap_or(0u8);
response_buffer.response = Some(DialogueResponseEvent {
line_id: line.id.clone(),
text: line.text.clone(),
speaker_entity_id: speaker_stable.0,
speaker_color_index: speaker_color,
speaker_name: speaker_display_name,
});
cooldown.record(&line.id, time.tick);