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
+3 -3
View File
@@ -283,9 +283,9 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
### D-061: Dialogue box — unified conversation log, bottom screen, max 20% height, no portraits
- **Date:** 2026-02-13
- **Decision:** Dialogue occupies the bottom of the screen, max 20% height, max-width 1200px ([D-076](#d-076-dialogue-box-max-width--1200px-oq-29-resolution)). NO portraits — the NPC is on screen, a portrait is redundant. Monologue floats ABOVE the dialogue box on z-layer 7 — spatial separation allows monologue to contradict dialogue visually (character thinks one thing while NPC says another). Walk-away via WASD, dialogue fades over 300ms, no close button ([D-064](content.md#d-064-walk-away--three-phase-consequences)). Auto-pause in single-player when implant UI is open; overlay design for multiplayer readiness.
- **Unified conversation log (Sprint 14, #535):** The dialogue box is a single chronological log — not separate panels for active vs passive dialogue. Player-NPC conversations and overheard NPC-NPC conversations ([D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter)) flow into the same scrolling log. Each entry shows `Speaker → Target: text` with per-character name colours (hash-indexed from configurable palette in `data/dialogue-theme.yaml`). Player response options render below the log; max 3 visible. Locked options invisible ([D-062](content.md#d-062-invisible-locked-dialogue-options)).
- **Unified conversation log (Sprint 14, #535):** The dialogue box is a single chronological log for player-NPC conversations. Each entry shows `Speaker → Target: text` with per-character name colours (hash-indexed from configurable palette in `data/dialogue-theme.yaml`). Player response options render below the log; max 3 visible. Locked options invisible ([D-062](content.md#d-062-invisible-locked-dialogue-options)).
- **Entry lifecycle:** All entries share the same timeout (15s + 3s fade, configurable via theme YAML). Walk-away clears response options but preserves log entries — earned information is fair game. Panel auto-hides when all entries expire and no active conversation is in progress.
- **Passive overheard lines:** Rendered at reduced opacity (0.9) per [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter). No response options for overheard content. Walk-away does not fire for passive-only display — the panel dismisses naturally when entries expire or the player walks out of earshot.
- **Passive overheard lines:** D-078 (overheard NPC conversations) was scrapped per R-012. Passive dialogue display will be redesigned after Phase 5 walkable environment.
- **Rationale:** Game world stays live above the dialogue box — player sees NPC body language while talking. Monologue above + dialogue below = the character can think one thing while saying another. Max 3 options + invisible locks = player never knows what they're missing. No portrait because the NPC IS on screen. A single unified log avoids a separate UI element for overheard content and makes the flow of conversation feel natural — active and passive dialogue interleave chronologically.
- **Cross-reference:** Invisible locks ([D-062](content.md#d-062-invisible-locked-dialogue-options)), confrontation ([D-063](content.md#d-063-confrontation--same-box-different-weight)), walk-away ([D-064](content.md#d-064-walk-away--three-phase-consequences)), z-stack ([D-049](#d-049-z-level-rendering-stack-8-layers)), max-width ([D-076](#d-076-dialogue-box-max-width--1200px-oq-29-resolution)), overheard NPC conversation ([D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter))
- **Source:** Control & Interaction Workshop (2026-02-13). Amended Sprint 14 (#535): unified log architecture.
@@ -424,7 +424,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Raised by:** Workshop — unanimous
- **Dissent:** None
- **Implements:** Ticket #548
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-083](#d-083-contradiction-detection-pipeline), [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter)
- **Cross-reference:** [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-083](#d-083-contradiction-detection-pipeline), [D-078](#d-078-overheard-npc-conversation--passive-dialogue-panel-with-occlusion-filter) (D-078 scrapped per R-012; knowledge propagation system remains active independently)
### D-081: Unprompted Disclosure Design
- **Date:** 2026-02-24
+72 -35
View File
@@ -279,18 +279,14 @@ fn corp_eligible(corp: &Corp, template_scale: &str, template_corridor: &str) ->
// Match sector to corridor
let sector = corp.geographic_sector.as_deref().unwrap_or("core");
let corridor_matches_sector = match template_corridor {
match template_corridor {
"north_reach" | "north_reach/compact" => sector == "north_reach",
"south_reach" => sector == "south_reach",
"west_reach" | "west_reach/compact" => sector == "west_reach",
"east_reach" | "inner_corridor/east_reach" => {
sector == "east_reach" || sector == "core"
}
"east_reach" | "inner_corridor/east_reach" => sector == "east_reach" || sector == "core",
"frontier" => sector == "deep_frontier",
_ => false,
};
corridor_matches_sector
}
}
// ---------------------------------------------------------------------------
@@ -308,13 +304,7 @@ fn scale_abbrev(scale_tier: &str) -> &'static str {
fn to_slug(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c
} else {
'-'
}
})
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|p| !p.is_empty())
@@ -371,8 +361,16 @@ fn generate_pair(
valid_commodities: &BTreeSet<String>,
) -> Option<GeneratedPair> {
let corridor = parse_template_corridor(&template.naming_pattern);
let halo_bid = halo_id(&corp.corp_id, &template.archetype_group, &template.scale_tier);
let vol_bid = volume_id(&corp.corp_id, &template.archetype_group, &template.scale_tier);
let halo_bid = halo_id(
&corp.corp_id,
&template.archetype_group,
&template.scale_tier,
);
let vol_bid = volume_id(
&corp.corp_id,
&template.archetype_group,
&template.scale_tier,
);
let halo_name = names::generate_halo_name(rng, corridor, &template.brand_category);
let vol_name = names::generate_volume_name(rng, corridor, &template.brand_category);
@@ -493,16 +491,16 @@ fn round2(v: f64) -> f64 {
// Output serialization
// ---------------------------------------------------------------------------
fn write_output(
path: &PathBuf,
products: &[BrandProduct],
inputs: &[BrandInput],
) {
fn write_output(path: &PathBuf, products: &[BrandProduct], inputs: &[BrandInput]) {
let mut out = String::new();
out.push_str("# Generated Minor Brand Products — The Settled Reach\n");
out.push_str("# Auto-generated by generate_brands binary. Do not hand-edit.\n");
out.push_str("# Re-run: tooling/generate-brands\n");
out.push_str(&format!("# Total: {} brand_products, {} brand_inputs\n\n", products.len(), inputs.len()));
out.push_str(&format!(
"# Total: {} brand_products, {} brand_inputs\n\n",
products.len(),
inputs.len()
));
for p in products {
out.push_str("[[brand_products]]\n");
@@ -524,7 +522,10 @@ fn write_output(
out.push_str(&format!("origin_system = {:?}\n", sys));
}
out.push_str(&format!("terroir_locked = {}\n", p.terroir_locked));
out.push_str(&format!("currency_denomination = {:?}\n", p.currency_denomination));
out.push_str(&format!(
"currency_denomination = {:?}\n",
p.currency_denomination
));
out.push_str(&format!("shadow_viable = {}\n", p.shadow_viable));
out.push_str(&format!("brand_tier = {:?}\n", p.brand_tier));
if let Some(ref hid) = p.halo_brand_id {
@@ -578,7 +579,10 @@ fn print_coverage(products: &[BrandProduct]) {
let min_per_corp = by_corp.values().min().copied().unwrap_or(0);
let max_per_corp = by_corp.values().max().copied().unwrap_or(0);
println!(" Corps covered: {}", by_corp.len());
println!(" Brands per corp: min={} max={}", min_per_corp, max_per_corp);
println!(
" Brands per corp: min={} max={}",
min_per_corp, max_per_corp
);
}
// ---------------------------------------------------------------------------
@@ -628,7 +632,11 @@ fn main() {
println!(" [2/5] Loading corporations and commodities...");
let corps = load_corps(&conn);
let valid_commodities = load_valid_commodity_ids(&conn);
println!(" {} corps, {} valid commodities", corps.len(), valid_commodities.len());
println!(
" {} corps, {} valid commodities",
corps.len(),
valid_commodities.len()
);
// Generate brand pairs
println!(" [3/5] Generating brand pairs...");
@@ -640,12 +648,24 @@ fn main() {
for (template_key, template) in &templates {
for corp in &corps {
if !corp_eligible(corp, &template.scale_tier, parse_template_corridor(&template.naming_pattern)) {
if !corp_eligible(
corp,
&template.scale_tier,
parse_template_corridor(&template.naming_pattern),
) {
continue;
}
let halo_bid = halo_id(&corp.corp_id, &template.archetype_group, &template.scale_tier);
let vol_bid = volume_id(&corp.corp_id, &template.archetype_group, &template.scale_tier);
let halo_bid = halo_id(
&corp.corp_id,
&template.archetype_group,
&template.scale_tier,
);
let vol_bid = volume_id(
&corp.corp_id,
&template.archetype_group,
&template.scale_tier,
);
// Skip if IDs already generated (name-collision guard)
if seen_ids.contains(&halo_bid) || seen_ids.contains(&vol_bid) {
@@ -653,7 +673,9 @@ fn main() {
continue;
}
if let Some(pair) = generate_pair(&mut rng, corp, template_key, template, &valid_commodities) {
if let Some(pair) =
generate_pair(&mut rng, corp, template_key, template, &valid_commodities)
{
seen_ids.insert(halo_bid);
seen_ids.insert(vol_bid);
products.push(pair.halo);
@@ -664,7 +686,11 @@ fn main() {
}
}
println!(" {} brand_products generated ({} skipped)", products.len(), skipped);
println!(
" {} brand_products generated ({} skipped)",
products.len(),
skipped
);
// Gap-fill: if under target, add more by re-applying reach_wide templates
// with a counter suffix to avoid ID collisions.
@@ -679,7 +705,8 @@ fn main() {
let mut template_idx = 0usize;
while products.len() < cli.min_brands && !reach_wide_templates.is_empty() {
let (template_key, template) = reach_wide_templates[template_idx % reach_wide_templates.len()];
let (template_key, template) =
reach_wide_templates[template_idx % reach_wide_templates.len()];
let corp = &corps[counter % corps.len()];
counter += 1;
@@ -704,7 +731,9 @@ fn main() {
continue;
}
if let Some(pair) = generate_pair(&mut rng, corp, template_key, template, &valid_commodities) {
if let Some(pair) =
generate_pair(&mut rng, corp, template_key, template, &valid_commodities)
{
let mut halo = pair.halo;
let mut volume = pair.volume;
halo.brand_product_id = halo_bid.clone();
@@ -713,8 +742,12 @@ fn main() {
let mut inputs_halo = pair.inputs_halo;
let mut inputs_volume = pair.inputs_volume;
for i in &mut inputs_halo { i.brand_product_id = halo_bid.clone(); }
for i in &mut inputs_volume { i.brand_product_id = vol_bid.clone(); }
for i in &mut inputs_halo {
i.brand_product_id = halo_bid.clone();
}
for i in &mut inputs_volume {
i.brand_product_id = vol_bid.clone();
}
seen_ids.insert(halo_bid);
seen_ids.insert(vol_bid);
@@ -741,7 +774,11 @@ fn main() {
// Write output
println!("\n [5/5] Writing output...");
write_output(&output_path, &products, &inputs);
println!(" {} brand_products, {} brand_inputs", products.len(), inputs.len());
println!(
" {} brand_products, {} brand_inputs",
products.len(),
inputs.len()
);
println!(" Output: {}", output_path.display());
println!(" Done.\n");
}
+454 -70
View File
@@ -13,60 +13,321 @@ use rand_chacha::ChaCha8Rng;
// ---------------------------------------------------------------------------
const CORE_NAMES: &[&str] = &[
"Alvarez", "Benoit", "Carvalho", "Durand", "Eriksen", "Fournier", "Gao", "Hartmann",
"Ishida", "Johansson", "Kirchner", "Lemaire", "Moreau", "Nakamura", "Olsson", "Pelletier",
"Richter", "Saito", "Torres", "Ueda", "Vasquez", "Werner", "Xu", "Yamada", "Zhou",
"Andersen", "Beaumont", "Costa", "Delacroix", "Engel", "Fujita", "Gutierrez", "Hayashi",
"Ibarra", "Jensen", "Klein", "Laurent", "Mercier", "Novak", "Ortiz", "Park", "Reuter",
"Suzuki", "Takahashi", "Ulrich", "Valentin", "Wagner", "Xie", "Yilmaz", "Zhang",
"Alvarez",
"Benoit",
"Carvalho",
"Durand",
"Eriksen",
"Fournier",
"Gao",
"Hartmann",
"Ishida",
"Johansson",
"Kirchner",
"Lemaire",
"Moreau",
"Nakamura",
"Olsson",
"Pelletier",
"Richter",
"Saito",
"Torres",
"Ueda",
"Vasquez",
"Werner",
"Xu",
"Yamada",
"Zhou",
"Andersen",
"Beaumont",
"Costa",
"Delacroix",
"Engel",
"Fujita",
"Gutierrez",
"Hayashi",
"Ibarra",
"Jensen",
"Klein",
"Laurent",
"Mercier",
"Novak",
"Ortiz",
"Park",
"Reuter",
"Suzuki",
"Takahashi",
"Ulrich",
"Valentin",
"Wagner",
"Xie",
"Yilmaz",
"Zhang",
];
const NORTH_REACH_NAMES: &[&str] = &[
"Andersson", "Bjornsson", "Calloway", "Dalsgaard", "Eklund", "Falk", "Grimstad", "Hedlund",
"Ivarsson", "Jonasson", "Kirkpatrick", "Lindqvist", "MacLeod", "Nordstrom", "Olafsson",
"Pettersson", "Rehn", "Strandberg", "Thorsen", "Ulvskog", "Vikstrom", "Wahlberg", "Aberg",
"Berglund", "Carlsen", "Dalgaard", "Engstrom", "Forsell", "Gustafsson", "Halvorsen",
"Ingvarsson", "Jansson", "Knudsen", "Lundin", "MacPherson", "Nylund", "Ostergaard",
"Palsson", "Rasmussen", "Sjoberg", "Toft", "Ulfsson", "Vestergaard", "Wiklund", "Aasen",
"Brannstrom", "Dahl", "Eide", "Friberg", "Gren",
"Andersson",
"Bjornsson",
"Calloway",
"Dalsgaard",
"Eklund",
"Falk",
"Grimstad",
"Hedlund",
"Ivarsson",
"Jonasson",
"Kirkpatrick",
"Lindqvist",
"MacLeod",
"Nordstrom",
"Olafsson",
"Pettersson",
"Rehn",
"Strandberg",
"Thorsen",
"Ulvskog",
"Vikstrom",
"Wahlberg",
"Aberg",
"Berglund",
"Carlsen",
"Dalgaard",
"Engstrom",
"Forsell",
"Gustafsson",
"Halvorsen",
"Ingvarsson",
"Jansson",
"Knudsen",
"Lundin",
"MacPherson",
"Nylund",
"Ostergaard",
"Palsson",
"Rasmussen",
"Sjoberg",
"Toft",
"Ulfsson",
"Vestergaard",
"Wiklund",
"Aasen",
"Brannstrom",
"Dahl",
"Eide",
"Friberg",
"Gren",
];
const SOUTH_REACH_NAMES: &[&str] = &[
"Adamski", "Baranov", "Chernov", "Dubois", "Egorov", "Filipov", "Gromov", "Horvat",
"Ivanova", "Jankovic", "Kowalski", "Lazarev", "Morozov", "Novikov", "Ostrowski", "Petrov",
"Reznik", "Sokolov", "Tkachenko", "Uvarov", "Volkov", "Wojcik", "Yakimov", "Zheng",
"Babic", "Chernyshev", "Dragunov", "Fedorov", "Grushevsky", "Havel", "Ito", "Jovanovic",
"Katsaros", "Lebedev", "Mazur", "Nemec", "Ochoa", "Popov", "Radic", "Smirnov", "Tanaka",
"Urasawa", "Vasiliev", "Watanabe", "Xiang", "Yegorov", "Zaytsev", "Borysko", "Chen",
"Adamski",
"Baranov",
"Chernov",
"Dubois",
"Egorov",
"Filipov",
"Gromov",
"Horvat",
"Ivanova",
"Jankovic",
"Kowalski",
"Lazarev",
"Morozov",
"Novikov",
"Ostrowski",
"Petrov",
"Reznik",
"Sokolov",
"Tkachenko",
"Uvarov",
"Volkov",
"Wojcik",
"Yakimov",
"Zheng",
"Babic",
"Chernyshev",
"Dragunov",
"Fedorov",
"Grushevsky",
"Havel",
"Ito",
"Jovanovic",
"Katsaros",
"Lebedev",
"Mazur",
"Nemec",
"Ochoa",
"Popov",
"Radic",
"Smirnov",
"Tanaka",
"Urasawa",
"Vasiliev",
"Watanabe",
"Xiang",
"Yegorov",
"Zaytsev",
"Borysko",
"Chen",
"Dimitrov",
];
const WEST_REACH_NAMES: &[&str] = &[
"Albrecht", "Baumann", "Christensen", "Dietrich", "Eisenberg", "Fischer", "Gruber",
"Hoffmann", "Ingolstadt", "Jaeger", "Kessler", "Lehmann", "Mueller", "Neumann", "Obermann",
"Pfeiffer", "Quandt", "Roth", "Schaefer", "Thiel", "Urban", "Vogt", "Weidenfeld",
"Ziegler", "Becker", "Claussen", "Dorfmann", "Eberhardt", "Fleischer", "Gerstner", "Haber",
"Imhof", "Jung", "Kraemer", "Linden", "Metzger", "Niedermann", "Opitz", "Preuss", "Raabe",
"Steinbach", "Trautmann", "Unger", "Vollmer", "Winterberg", "Zahn", "Auerbach", "Bruckner",
"Dahlem", "Eckhardt",
"Albrecht",
"Baumann",
"Christensen",
"Dietrich",
"Eisenberg",
"Fischer",
"Gruber",
"Hoffmann",
"Ingolstadt",
"Jaeger",
"Kessler",
"Lehmann",
"Mueller",
"Neumann",
"Obermann",
"Pfeiffer",
"Quandt",
"Roth",
"Schaefer",
"Thiel",
"Urban",
"Vogt",
"Weidenfeld",
"Ziegler",
"Becker",
"Claussen",
"Dorfmann",
"Eberhardt",
"Fleischer",
"Gerstner",
"Haber",
"Imhof",
"Jung",
"Kraemer",
"Linden",
"Metzger",
"Niedermann",
"Opitz",
"Preuss",
"Raabe",
"Steinbach",
"Trautmann",
"Unger",
"Vollmer",
"Winterberg",
"Zahn",
"Auerbach",
"Bruckner",
"Dahlem",
"Eckhardt",
];
const EAST_REACH_NAMES: &[&str] = &[
"Aquino", "Bautista", "Cruz", "Dalisay", "Espiritu", "Flores", "Garcia", "Hernandez",
"Ilagan", "Jeon", "Kim", "Lim", "Magalang", "Navarro", "Ocampo", "Park", "Quijano",
"Reyes", "Santos", "Tan", "Uy", "Villanueva", "Wong", "Yoo", "Aguilar", "Buenaventura",
"Castillo", "Dizon", "Enriquez", "Fernandez", "Gonzales", "Hwang", "Ignacio", "Jeong",
"Kwon", "Lee", "Marasigan", "Nakamura", "Oh", "Perez", "Ramos", "Son", "Tolentino",
"Umali", "Valdez", "Yun", "Zamora", "Baek", "Choi", "Dela Cruz",
"Aquino",
"Bautista",
"Cruz",
"Dalisay",
"Espiritu",
"Flores",
"Garcia",
"Hernandez",
"Ilagan",
"Jeon",
"Kim",
"Lim",
"Magalang",
"Navarro",
"Ocampo",
"Park",
"Quijano",
"Reyes",
"Santos",
"Tan",
"Uy",
"Villanueva",
"Wong",
"Yoo",
"Aguilar",
"Buenaventura",
"Castillo",
"Dizon",
"Enriquez",
"Fernandez",
"Gonzales",
"Hwang",
"Ignacio",
"Jeong",
"Kwon",
"Lee",
"Marasigan",
"Nakamura",
"Oh",
"Perez",
"Ramos",
"Son",
"Tolentino",
"Umali",
"Valdez",
"Yun",
"Zamora",
"Baek",
"Choi",
"Dela Cruz",
];
const FRONTIER_NAMES: &[&str] = &[
"Adeyemi", "Bergstrom", "Chandra", "Duval", "Emeka", "Fonseca", "Gupta", "Hassan",
"Ibrahim", "Jansson", "Kovac", "Liu", "Martinez", "Nkosi", "Okafor", "Patel", "Quinn",
"Rodriguez", "Sousa", "Thorne", "Uddin", "Varga", "Wu", "Xiong", "Yoshida", "Zhao",
"Abara", "Beaumont", "Cardenas", "Doyle", "Ekwueme", "Ferreira", "Gomes", "Henriksen",
"Idris", "Juma", "Kato", "Larsen", "Morales", "Ndlovu", "Osei", "Petrov", "Ruiz",
"Singh", "Tavares", "Uchida", "Volkov", "Wang", "Yang", "Zaman",
"Adeyemi",
"Bergstrom",
"Chandra",
"Duval",
"Emeka",
"Fonseca",
"Gupta",
"Hassan",
"Ibrahim",
"Jansson",
"Kovac",
"Liu",
"Martinez",
"Nkosi",
"Okafor",
"Patel",
"Quinn",
"Rodriguez",
"Sousa",
"Thorne",
"Uddin",
"Varga",
"Wu",
"Xiong",
"Yoshida",
"Zhao",
"Abara",
"Beaumont",
"Cardenas",
"Doyle",
"Ekwueme",
"Ferreira",
"Gomes",
"Henriksen",
"Idris",
"Juma",
"Kato",
"Larsen",
"Morales",
"Ndlovu",
"Osei",
"Petrov",
"Ruiz",
"Singh",
"Tavares",
"Uchida",
"Volkov",
"Wang",
"Yang",
"Zaman",
];
fn names_for_corridor(corridor: &str) -> &'static [&'static str] {
@@ -85,83 +346,206 @@ fn names_for_corridor(corridor: &str) -> &'static [&'static str] {
// ---------------------------------------------------------------------------
const TERROIR_HALO: &[&str] = &[
"Reserve", "Single", "Estate", "Vintage", "Heritage", "Grand", "Limited", "Prestige",
"Signature", "Cellar", "Select", "Cru", "Premier", "Old", "Aged",
"Reserve",
"Single",
"Estate",
"Vintage",
"Heritage",
"Grand",
"Limited",
"Prestige",
"Signature",
"Cellar",
"Select",
"Cru",
"Premier",
"Old",
"Aged",
];
const TERROIR_VOLUME: &[&str] = &[
"Standard", "Export", "Blend", "Classic", "Field", "Ordinary", "Running", "Table",
"Regular", "Common", "House", "Station", "Corridor", "Transit",
"Standard", "Export", "Blend", "Classic", "Field", "Ordinary", "Running", "Table", "Regular",
"Common", "House", "Station", "Corridor", "Transit",
];
const HERITAGE_CRAFT_HALO: &[&str] = &[
"Heritage", "Limited", "Artisan", "Master", "Guild", "Prestige", "Premium", "Classic",
"Signature", "Original", "Bespoke", "Traditional", "First",
"Heritage",
"Limited",
"Artisan",
"Master",
"Guild",
"Prestige",
"Premium",
"Classic",
"Signature",
"Original",
"Bespoke",
"Traditional",
"First",
];
const HERITAGE_CRAFT_VOLUME: &[&str] = &[
"Standard", "Classic", "Working", "Everyday", "Regular", "Plain", "Field", "Grade",
"Basic", "Workshop", "Studio", "Common",
"Standard", "Classic", "Working", "Everyday", "Regular", "Plain", "Field", "Grade", "Basic",
"Workshop", "Studio", "Common",
];
const TECH_PREMIUM_HALO: &[&str] = &[
"Elite", "Pro", "Advanced", "Precision", "Superior", "Grand", "Signature", "First",
"Prime", "Expert", "Master", "Apex", "Summit",
"Elite",
"Pro",
"Advanced",
"Precision",
"Superior",
"Grand",
"Signature",
"First",
"Prime",
"Expert",
"Master",
"Apex",
"Summit",
];
const TECH_PREMIUM_VOLUME: &[&str] = &[
"Standard", "Series", "Base", "Classic", "Regular", "Field", "Grade", "Value",
"Essential", "Core", "Basic",
"Standard",
"Series",
"Base",
"Classic",
"Regular",
"Field",
"Grade",
"Value",
"Essential",
"Core",
"Basic",
];
const CULTURAL_HALO: &[&str] = &[
"Archive", "Heritage", "Classic", "Definitive", "Prestige", "Limited", "Master",
"Collected", "Curated", "Canonical", "Flagship", "Grand",
"Archive",
"Heritage",
"Classic",
"Definitive",
"Prestige",
"Limited",
"Master",
"Collected",
"Curated",
"Canonical",
"Flagship",
"Grand",
];
const CULTURAL_VOLUME: &[&str] = &[
"Standard", "Classic", "Essential", "Value", "Regular", "Base", "Field",
"Running", "Everyday", "Popular",
"Standard",
"Classic",
"Essential",
"Value",
"Regular",
"Base",
"Field",
"Running",
"Everyday",
"Popular",
];
const SERVICE_PREMIUM_HALO: &[&str] = &[
"Premier", "Elite", "Priority", "Signature", "Prestige", "Grand", "First", "Select",
"Platinum", "Gold", "Senior", "Executive",
"Premier",
"Elite",
"Priority",
"Signature",
"Prestige",
"Grand",
"First",
"Select",
"Platinum",
"Gold",
"Senior",
"Executive",
];
const SERVICE_PREMIUM_VOLUME: &[&str] = &[
"Standard", "Basic", "Classic", "Regular", "Field", "Value", "General", "Common",
"Ordinary", "Essential",
"Standard",
"Basic",
"Classic",
"Regular",
"Field",
"Value",
"General",
"Common",
"Ordinary",
"Essential",
];
const COMMODITY_BRANDED_HALO: &[&str] = &[
"Original", "Select", "Premium", "Reserve", "Classic", "Heritage", "Signature",
"Superior", "First", "Grade", "Certified",
"Original",
"Select",
"Premium",
"Reserve",
"Classic",
"Heritage",
"Signature",
"Superior",
"First",
"Grade",
"Certified",
];
const COMMODITY_BRANDED_VOLUME: &[&str] = &[
"Standard", "Basic", "Regular", "Field", "Value", "Economy", "Bulk", "Run",
"Common", "Grade", "Plain",
"Standard", "Basic", "Regular", "Field", "Value", "Economy", "Bulk", "Run", "Common", "Grade",
"Plain",
];
const DESIGN_HERITAGE_HALO: &[&str] = &[
"Heritage", "Limited", "Prestige", "Grand", "Signature", "Edition", "Series",
"Classic", "Archive", "Collector", "Retrospective",
"Heritage",
"Limited",
"Prestige",
"Grand",
"Signature",
"Edition",
"Series",
"Classic",
"Archive",
"Collector",
"Retrospective",
];
const DESIGN_HERITAGE_VOLUME: &[&str] = &[
"Standard", "Classic", "Regular", "Field", "Value", "Base", "Essential",
"Running", "Contemporary", "Current",
"Standard",
"Classic",
"Regular",
"Field",
"Value",
"Base",
"Essential",
"Running",
"Contemporary",
"Current",
];
const PLATFORM_CATALOGUE_HALO: &[&str] = &[
"Premium", "Pro", "Plus", "Elite", "Advanced", "Signature", "Select", "Grand",
"Unlimited", "Complete", "Full",
"Premium",
"Pro",
"Plus",
"Elite",
"Advanced",
"Signature",
"Select",
"Grand",
"Unlimited",
"Complete",
"Full",
];
const PLATFORM_CATALOGUE_VOLUME: &[&str] = &[
"Standard", "Basic", "Classic", "Regular", "Field", "Value", "Entry", "Lite",
"Essential", "Free",
"Standard",
"Basic",
"Classic",
"Regular",
"Field",
"Value",
"Entry",
"Lite",
"Essential",
"Free",
];
fn halo_descriptors(brand_category: &str) -> &'static [&'static str] {
+8 -3
View File
@@ -14,7 +14,7 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use crate::bridge::types::{BookmarkCatalog, BookmarkWire, CareerKindWire, SnapshotBuffer};
use crate::knowledge::{CultureResolver, resolve_culture};
use crate::knowledge::{resolve_culture, CultureResolver};
/// Immutable registry of bookmark definitions.
///
@@ -190,7 +190,10 @@ mod tests {
assert_eq!(wire.id, "tycoon");
assert_eq!(wire.career, CareerKindWire::Tycoon);
assert!(!wire.default_location.is_empty());
assert_eq!(wire.allowed_locations.len(), wire.allowed_locations_cultures.len());
assert_eq!(
wire.allowed_locations.len(),
wire.allowed_locations_cultures.len()
);
}
#[test]
@@ -216,6 +219,8 @@ mod tests {
let r = make_registry();
let defn = r.get("tycoon").unwrap();
assert!(defn.allowed_locations.contains(&"GJ 35".to_string()));
assert!(!defn.allowed_locations.contains(&"Unknown System".to_string()));
assert!(!defn
.allowed_locations
.contains(&"Unknown System".to_string()));
}
}
+1 -1
View File
@@ -14,9 +14,9 @@ use crate::bridge::types::{
};
use crate::knowledge::EntityRegistry;
use crate::npc::Npc;
use crate::simulation::conversation::NpcName;
use crate::simulation::economy::{EconSimResource, EconStateResource};
use crate::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use crate::simulation::npc_components::NpcName;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::SimulationTime;
use crate::simulation::triangle::TriangleState;
-1
View File
@@ -244,7 +244,6 @@ impl Plugin for BridgePlugin {
crate::simulation::monologue::trigger_event_monologue
.after(crate::simulation::monologue::process_sprint_anomaly_monologue)
.after(crate::simulation::sound::collect_sound_events)
.after(crate::simulation::conversation::run_npc_conversations)
.after(crate::simulation::dialogue::process_walk_away),
crate::simulation::monologue::process_contradiction_monologue
.after(crate::simulation::monologue::trigger_event_monologue),
+2 -4
View File
@@ -304,8 +304,7 @@ mod tests {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
sound_events: vec![],
@@ -447,8 +446,7 @@ mod tests {
dialogue_response: None,
blocked_entities: vec![],
scan_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
sound_events: vec![],
+2 -10
View File
@@ -17,7 +17,7 @@ pub use crate::simulation::time::{DayPhase, TickRate};
/// negotiation is unnecessary. Client should reject snapshots with version !=
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
/// period, then the default is removed once both sides are updated.
pub const PROTOCOL_VERSION: u8 = 22;
pub const PROTOCOL_VERSION: u8 = 23;
/// Handshake message sent as the very first framed message after connection (#555).
/// Client reads this before entering the normal tick loop and validates
@@ -85,6 +85,7 @@ pub struct StartupMessage {
/// EconStateQuery PlayerAction variant (#822).
/// v22 adds: bookmark_catalog (#614, D-115/D-117 CK3-style bookmark system),
/// RequestBookmarkCatalog + ConfirmBookmark PlayerAction variants (#614).
/// v23 removes: conversation_events, conversation_ended (D-078 scrapped per R-012).
/// Future fields: ambient sound events, HUD state (D-020 expansion).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObserverSnapshot {
@@ -142,15 +143,6 @@ pub struct ObserverSnapshot {
/// Empty when no sounds are in range.
#[serde(default)]
pub sound_events: Vec<crate::simulation::sound::SoundEvent>,
/// Overheard NPC-to-NPC conversation lines this tick (#247, D-078).
/// Each event carries pre-occluded text — client renders verbatim.
/// Empty when no conversations are overheard.
#[serde(default)]
pub conversation_events: Vec<crate::simulation::conversation::ConversationEvent>,
/// Conversations that ended this tick (#247, D-078).
/// Client dismisses the passive dialogue panel for these pairs.
#[serde(default)]
pub conversation_ended: Vec<crate::simulation::conversation::ConversationEndEvent>,
/// Follow-mode state for client HUD display (#241).
/// Present when the player is actively following an NPC.
/// Client shows follow indicator with distance, LOS, and tension.
+8 -5
View File
@@ -92,7 +92,10 @@ pub struct CultureResolverResource(pub CultureResolver);
/// - `CultureError::UnknownLocation` — not found in any table.
/// - `CultureError::NoCulture` — found but culture column is NULL (data bug).
/// - `CultureError::Db` — SQLite I/O failure.
pub fn resolve_culture(resolver: &CultureResolver, location_id: &str) -> Result<CultureTag, CultureError> {
pub fn resolve_culture(
resolver: &CultureResolver,
location_id: &str,
) -> Result<CultureTag, CultureError> {
let conn = resolver
.conn
.lock()
@@ -187,8 +190,8 @@ mod tests {
use super::*;
fn fixture_db() -> CultureResolver {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/knowledge/fixtures/culture_test.db");
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/knowledge/fixtures/culture_test.db");
CultureResolver::open(&path).expect("open fixture DB")
}
@@ -257,8 +260,8 @@ mod tests {
use std::sync::Arc;
use std::thread;
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/knowledge/fixtures/culture_test.db");
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/knowledge/fixtures/culture_test.db");
let r = Arc::new(CultureResolver::open(&path).expect("open"));
let handles: Vec<_> = (0..4)
+1 -1
View File
@@ -185,7 +185,7 @@ pub fn process_knowledge_events(
mut queue: ResMut<KnowledgeEventQueue>,
mut contradiction_queue: ResMut<ContradictionDetectedQueue>,
registry: Res<EntityRegistry>,
npc_names: Query<&crate::simulation::conversation::NpcName>,
npc_names: Query<&crate::simulation::npc_components::NpcName>,
mut knowledge_query: Query<&mut KnowledgeGraph>,
) {
let events = queue.drain();
+3 -1
View File
@@ -15,7 +15,9 @@ pub mod registry;
pub mod types;
pub use content_registry::ContentEntityRegistry;
pub use culture::{CultureError, CultureResolver, CultureResolverResource, CultureTag, resolve_culture};
pub use culture::{
resolve_culture, CultureError, CultureResolver, CultureResolverResource, CultureTag,
};
pub use events::{
ContradictionDetectedEvent, ContradictionDetectedQueue, InteractionType, KnowledgeEvent,
KnowledgeEventQueue, KnowledgeEventType, ProcessedEntityGrant, ProcessedFactGrant,
+3 -3
View File
@@ -161,7 +161,9 @@ fn main() {
match settled_reach_server::knowledge::CultureResolver::open(&systems_db_path) {
Ok(resolver) => {
tracing::info!("Culture resolver opened: {:?}", systems_db_path);
app.insert_resource(settled_reach_server::knowledge::CultureResolverResource(resolver));
app.insert_resource(settled_reach_server::knowledge::CultureResolverResource(
resolver,
));
}
Err(e) => {
tracing::warn!(
@@ -343,8 +345,6 @@ fn send_panic_error(app: &App, panic_msg: &str) {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
-11
View File
@@ -17,7 +17,6 @@ use crate::perception::cognitive_delay::CognitiveDelay;
use crate::perception::query::{ActivePerceptionMode, VisibilityGeometry};
use crate::perception::vision_cone::Facing;
use crate::simulation::contraband::ScanEventBuffer;
use crate::simulation::conversation::ConversationEventBuffer;
use crate::simulation::dialogue::DialogueResponseBuffer;
use crate::simulation::examine::ExamineResultBuffer;
use crate::simulation::follow::FollowTarget;
@@ -84,7 +83,6 @@ pub fn compute_observer_snapshot(
Option<&CognitiveDelay>,
Option<&mut DialogueResponseBuffer>,
Option<&mut ScanEventBuffer>,
Option<&mut ConversationEventBuffer>,
Option<&FollowTarget>,
Option<&mut ExamineResultBuffer>,
),
@@ -121,7 +119,6 @@ pub fn compute_observer_snapshot(
cognitive_delay_opt,
mut dialogue_response_opt,
mut scan_event_buffer_opt,
mut conversation_buffer_opt,
follow_target_opt,
mut examine_result_buffer_opt,
)) = observer_query.single_mut()
@@ -225,12 +222,6 @@ pub fn compute_observer_snapshot(
.map(|buf| buf.take())
.unwrap_or_default();
// Drain NPC-to-NPC conversation events (#247, D-078)
let (conversation_events, conversation_ended) = conversation_buffer_opt
.as_mut()
.map(|buf| (buf.take_events(), buf.take_ended()))
.unwrap_or_default();
// Collect sound events audible to the observer (D-038, #124).
// Filter by D-018 range: only events the player can hear based on distance.
let sound_events = if let Some(ref queue) = sound_queue {
@@ -476,8 +467,6 @@ pub fn compute_observer_snapshot(
dialogue_response,
blocked_entities,
scan_events,
conversation_events,
conversation_ended,
follow_state,
character_pressure: pressure_query
.iter()
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
+1 -1
View File
@@ -614,8 +614,8 @@ pub fn setup_gauntlet(app: &mut App, archetype: crate::bridge::types::CharacterA
// 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};
use crate::simulation::npc_components::NpcColorIndex;
// (location, role) pairs matching server/content/campaigns/.../dialogue/ YAML pools.
// Cycling through these gives NPC variety across rooms.
+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
// ---------------------------------------------------------------------------
+1 -2
View File
@@ -63,8 +63,7 @@ fn snapshot_roundtrip_over_unix_socket() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
+1 -2
View File
@@ -49,8 +49,7 @@ fn snapshot_roundtrip_over_tcp() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
+1 -2
View File
@@ -288,8 +288,7 @@ fn snapshot_with_sim_errors_roundtrips() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
+3 -6
View File
@@ -39,8 +39,7 @@ fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
@@ -238,8 +237,7 @@ fn generate_msgpack_fixtures() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
@@ -379,8 +377,7 @@ fn generate_msgpack_fixtures() {
blocked_entities: vec![5, 6],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: Some(0xDEADBEEF),
@@ -5,8 +5,6 @@
3
],
"character_pressure": null,
"conversation_ended": [],
"conversation_events": [],
"current_monologue": null,
"dialogue_response": null,
"entities": [
+4 -8
View File
@@ -27,8 +27,7 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
@@ -293,8 +292,7 @@ fn snapshot_v2_fields_roundtrip() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
@@ -405,8 +403,7 @@ fn all_facing_direction_variants_roundtrip() {
blocked_entities: vec![],
scan_events: vec![],
sound_events: vec![],
conversation_events: vec![],
conversation_ended: vec![],
follow_state: None,
character_pressure: None,
rng_seed: None,
@@ -1456,8 +1453,7 @@ fn serde_default_fields_fill_in_when_missing_from_wire() {
"blocked_entities": [],
"scan_events": [],
"sound_events": [],
"conversation_events": [],
"conversation_ended": [],
"follow_state": null,
"rng_seed": null
});