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
+50
View File
@@ -537,6 +537,56 @@ pub fn setup_gauntlet(app: &mut App) {
}
}
// --- Dialogue fixup: wire up DialogueProfile on all Npc entities missing one ---
// Gauntlet room builders (except dialogue_room) don't include DialogueProfile.
// Without it, the Talk verb silently no-ops. This fixup ensures every NPC
// can respond to Talk using content from the YAML dialogue pools.
{
use crate::npc::Npc;
use crate::simulation::conversation::NpcColorIndex;
use crate::simulation::dialogue::{CurrentMood, DialogueProfile};
// (location, role) pairs matching content/campaigns/.../dialogue/ YAML pools.
// Cycling through these gives NPC variety across rooms.
const DIALOGUE_ROLES: &[(&str, &str)] = &[
("the-terminal", "dock-worker"),
("the-terminal", "courier"),
("the-terminal", "maintenance-tech"),
("the-terminal", "new-hire"),
("the-terminal", "scheduler"),
("the-terminal", "shift-supervisor"),
("the-last-shift", "bartender"),
("the-last-shift", "bar-regular"),
("the-last-shift", "day-worker"),
("maintenance-corridors", "transit-worker"),
];
let missing: Vec<bevy_ecs::prelude::Entity> = {
let mut q = app
.world_mut()
.query_filtered::<bevy_ecs::prelude::Entity, (
bevy_ecs::prelude::With<Npc>,
bevy_ecs::prelude::Without<DialogueProfile>,
)>();
q.iter(app.world()).collect()
};
for (i, entity) in missing.iter().enumerate() {
let (location, role) = DIALOGUE_ROLES[i % DIALOGUE_ROLES.len()];
let color_index = registry
.to_stable(*entity)
.map(|sid| (sid.0 % 8) as u8)
.unwrap_or(0u8);
app.world_mut().entity_mut(*entity).insert((
DialogueProfile {
location: location.to_string(),
role: role.to_string(),
},
CurrentMood::default(),
NpcColorIndex(color_index),
));
}
}
app.insert_resource(registry);
}
@@ -24,10 +24,6 @@ use crate::simulation::path_follow::MovementSpeed;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::DayPhase;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 64;
const ORIGIN_Y: i32 = 78;
/// Observer position for Shift Change tests (absolute).
pub const OBSERVER_POS: TilePosition = TilePosition { x: 72, y: 90, z: 0 };