//! line-previewer: CLI tool for previewing dialogue/monologue line selection. //! //! Answers: "Given this NPC state and player state, what line fires?" //! Authoring tool for content authors to test tag behavior without running the game. //! Implements the 4-layer dialogue selection pipeline (D-028) in standalone mode. use clap::{Parser, Subcommand}; use std::path::PathBuf; mod pipeline; mod types; use pipeline::{ evaluate_dialogue, evaluate_monologue, print_coverage_report, print_dialogue_results, print_monologue_results, DialogueContext, MonologueContext, }; use types::{DialoguePool, MonologuePool}; #[derive(Parser)] #[command( name = "line-previewer", about = "Preview dialogue/monologue line selection" )] struct Cli { #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { /// Preview dialogue line selection Dialogue { /// Path to dialogue YAML file #[arg(short, long)] file: PathBuf, /// Access tier(s): public, insider, authority, peer, hostile #[arg(short, long, value_delimiter = ',')] access: Vec, /// Trust level: surface, real, secret #[arg(short, long, default_value = "surface")] trust: String, /// Active situation(s): arrival, shift_start, shift_end, etc. #[arg(short, long, value_delimiter = ',')] situation: Vec, /// Topic filter(s): colleague, routine, cargo, etc. #[arg(long, value_delimiter = ',')] topic: Vec, /// Mood filter(s): fond, comfortable, worried, etc. #[arg(long, value_delimiter = ',')] mood: Vec, /// Show detailed filter pass/fail for each line #[arg(short, long)] verbose: bool, /// RNG seed for weighted selection (default: 42) #[arg(long, default_value = "42")] seed: u64, }, /// Preview monologue line selection Monologue { /// Path to monologue YAML file #[arg(short, long)] file: PathBuf, /// Character: smuggler or detective #[arg(short, long)] character: String, /// Trigger type: enter_location, observe_npc, etc. #[arg(short, long)] trigger: String, /// Known fact IDs (for prerequisite evaluation) #[arg(long, value_delimiter = ',')] known_facts: Vec, /// Show detailed prerequisite evaluation for each line #[arg(short, long)] verbose: bool, /// RNG seed for weighted selection (default: 42) #[arg(long, default_value = "42")] seed: u64, }, /// Show coverage report — which filter combinations have zero eligible lines Coverage { /// Path to dialogue or monologue YAML file #[arg(short, long)] file: PathBuf, }, /// Preview a sequence of monologue lines (walk-through simulation) Sequence { /// Path to monologue YAML file #[arg(short, long)] file: PathBuf, /// Character: smuggler or detective #[arg(short, long)] character: String, /// Trigger sequence (comma-separated): enter_location,observe_npc,time_idle #[arg(short, long, value_delimiter = ',')] triggers: Vec, /// RNG seed (default: 42) #[arg(long, default_value = "42")] seed: u64, }, } fn main() { let cli = Cli::parse(); match cli.command { Command::Dialogue { file, access, trust, situation, topic, mood, verbose, seed, } => { let pool = load_dialogue(&file); let ctx = DialogueContext { access, trust, situation, topic, mood, seed, }; let results = evaluate_dialogue(&pool, &ctx); print_dialogue_results(&results, verbose, seed); } Command::Monologue { file, character, trigger, known_facts, verbose, seed, } => { let pool = load_monologue(&file); if pool.character != character { eprintln!( "Warning: file character '{}' doesn't match requested '{}'", pool.character, character ); } let ctx = MonologueContext { trigger, known_facts, seed, }; let results = evaluate_monologue(&pool, &ctx); print_monologue_results(&results, verbose, seed); } Command::Coverage { file } => { let yaml_str = std::fs::read_to_string(&file).unwrap_or_else(|e| { eprintln!("Error reading {}: {e}", file.display()); std::process::exit(1); }); print_coverage_report(&yaml_str, &file); } Command::Sequence { file, character, triggers, seed, } => { let pool = load_monologue(&file); if pool.character != character { eprintln!( "Warning: file character '{}' doesn't match requested '{}'", pool.character, character ); } println!("[Sequence: {} — {}]", file.display(), character); let mut used_ids: Vec = Vec::new(); for (i, trigger) in triggers.iter().enumerate() { let ctx = MonologueContext { trigger: trigger.clone(), known_facts: vec![], seed: seed.wrapping_add(i as u64), }; let results = evaluate_monologue(&pool, &ctx); // Filter out recently used lines let eligible: Vec<_> = results .iter() .filter(|r| r.passed && !used_ids.contains(&r.line_id)) .collect(); if let Some(selected) = eligible.first() { println!("{}. ({}) {:?}", i + 1, trigger, selected.text); used_ids.push(selected.line_id.clone()); } else { println!("{}. ({}) [no eligible line]", i + 1, trigger); } } } } } fn load_dialogue(path: &PathBuf) -> DialoguePool { let yaml_str = std::fs::read_to_string(path).unwrap_or_else(|e| { eprintln!("Error reading {}: {e}", path.display()); std::process::exit(1); }); serde_yaml::from_str(&yaml_str).unwrap_or_else(|e| { eprintln!("Error parsing dialogue YAML: {e}"); std::process::exit(1); }) } fn load_monologue(path: &PathBuf) -> MonologuePool { let yaml_str = std::fs::read_to_string(path).unwrap_or_else(|e| { eprintln!("Error reading {}: {e}", path.display()); std::process::exit(1); }); serde_yaml::from_str(&yaml_str).unwrap_or_else(|e| { eprintln!("Error parsing monologue YAML: {e}"); std::process::exit(1); }) }