//! wds — Weirdcore Dating Simulator. //! //! CLI-Phase: Die Kern-Systeme (Signal/Action-Dispatcher, KV-Store, //! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend. //! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und //! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und //! die `Action`-Queue. use std::io::{self, Write}; mod assets; mod game; mod ink; mod kv; mod signals; mod story_ctrl; use game::{Action, Game}; use ink::StoryState; fn main() { let signals_path = assets::path("assets/signals.toml"); let signal_table = signals::load_signals(&signals_path); println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.", signal_table.len(), signals_path); let mut game = Game::new(signal_table); // Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen. fire(&mut game, "init", None); while let Some(line) = read_line("> ") { let line = line.trim(); match line { "" => {} "quit" | "exit" => break, "help" => print_help(), "kv" => dump_kv(&game), "reload" => { game.signals = signals::load_signals(&signals_path); println!("{} Signale geladen.", game.signals.len()); } _ => { if let Some(sig) = line.strip_prefix("signal ") { fire(&mut game, sig.trim(), None); } else if let Some(name) = line.strip_prefix("use ") { // Objekt-Interaktion simulieren: Signal ist der gestrippte // Name, $self der volle — wie der LMB-Klick-Pfad in irl3d. let name = name.trim(); let key = signals::signal_key(name).to_string(); fire(&mut game, &key, Some(name.to_string())); } else { println!("unbekannter Befehl: {line:?} — `help` für Befehle"); } } } } } fn print_help() { println!(" signal Signal feuern oder Action direkt ausführen"); println!(" (z.B. `signal Cube`, `signal set has_key true`)"); println!(" use Objekt-Interaktion simulieren — `use Mushroom.005`"); println!(" feuert Signal `Mushroom` mit $self = Mushroom.005"); println!(" kv KV-Store anzeigen"); println!(" reload signals.toml neu laden"); println!(" quit beenden"); } /// Signal dispatchen, deferred Actions ausgeben und — falls eine Action /// eine Story gestartet hat — den Dialog-Loop fahren. fn fire(game: &mut Game, signal: &str, instance: Option) { { let mut ctx = game.action_ctx(instance); signals::dispatch(signal, &mut ctx); } drain_actions(game); if game.story.is_some() { run_story(game); } } /// Dialog-Loop: Story schrittweise treiben, Tags zurück in den Dispatcher, /// Choices über nummerierte Eingabe. Läuft bis `End` (oder EOF). fn run_story(game: &mut Game) { let mut sel: Option = None; loop { let state = { let mut ctx = game.action_ctx(None); let (state, tags) = story_ctrl::advance(sel, &mut ctx); for t in &tags { signals::dispatch(t, &mut ctx); } state }; drain_actions(game); match state { None | Some(StoryState::End) => break, Some(StoryState::Text(text)) => { println!("\n{text}"); if read_line(" [Enter] ").is_none() { break; } sel = None; } Some(StoryState::Choice { prompt, options }) => { if !prompt.is_empty() { println!("\n{prompt}"); } for (i, o) in options.iter().enumerate() { println!(" {}) {o}", i + 1); } sel = loop { let Some(line) = read_line(" wahl> ") else { return; }; if let Ok(n) = line.trim().parse::() { if (1..=options.len()).contains(&n) { break Some(n - 1); } } println!(" (1..{})", options.len()); }; } } } } fn drain_actions(game: &mut Game) { for a in game.actions.drain(..) { match a { Action::HideObject(n) => println!("[action] hide_object {n}"), Action::PlaySound(n) => println!("[action] play_sound {n}"), } } } fn dump_kv(game: &Game) { let mut keys: Vec<&String> = game.kv.keys().collect(); keys.sort(); for k in keys { println!(" {k} = {}", kv::format_value(&game.kv[k])); } } /// Zeile von stdin lesen; `None` bei EOF (Ctrl-D) oder Lesefehler. fn read_line(prompt: &str) -> Option { print!("{prompt}"); io::stdout().flush().ok(); let mut buf = String::new(); match io::stdin().read_line(&mut buf) { Ok(0) | Err(_) => None, Ok(_) => Some(buf.trim_end().to_string()), } }