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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user