centralized state

This commit is contained in:
2026-06-13 13:44:02 +02:00
parent 0a8861a5b1
commit 48c499e356
11 changed files with 693 additions and 206 deletions
+19 -111
View File
@@ -1,126 +1,34 @@
//! Konsolen-REPL — das erste Frontend des Kerns.
//! Konsolen-REPL — Headless-Treiber für Tests und Betrieb ohne GPU.
//!
//! Konsumiert die `engine`-Schnittstellen: Signale via `signals::dispatch`,
//! Dialoge via `story_ctrl::advance`, deferred Actions aus `game.actions`.
//! Der Renderer wird später dieselben Schnittstellen konsumieren; dieses
//! Modul bleibt daneben als Debug-Werkzeug bestehen.
//! Liest Zeilen von stdin, gibt sie an `Session::exec` und druckt die
//! Ausgabe. Befehls- und Dialoglogik liegen in `session`, damit das
//! Fenster-Frontend dieselbe Session ohne Duplikat steuern kann.
use std::io::{self, Write};
use crate::engine::game::{Action, Game};
use crate::engine::ink::StoryState;
use crate::engine::{kv, signals, story_ctrl};
use crate::session::{Mode, Session};
/// REPL-Hauptschleife. Übernimmt das Game, feuert das reservierte
/// `[init]`-Signal (KV-Defaults, bevor Stories laufen) und liest dann
/// Befehle bis `quit`/EOF. `signals_path` wird für `reload` gebraucht.
pub fn run(mut game: Game, signals_path: &str) {
pub fn run(mut session: Session, signals_path: &str) {
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
game.signals.len(), signals_path);
session.game.signals.len(), signals_path);
fire(&mut game, "init", None);
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
for line in session.start() { println!("{line}"); }
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 <s> Signal feuern oder Action direkt ausführen");
println!(" (z.B. `signal Cube`, `signal set has_key true`)");
println!(" use <instance> 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<String>) {
{
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<usize> = 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::<usize>() {
if (1..=options.len()).contains(&n) { break Some(n - 1); }
}
println!(" (1..{})", options.len());
};
}
}
let Some(input) = read_line(prompt_for(&session.mode)) else { break };
let r = session.exec(input.trim());
for line in r.output { println!("{line}"); }
if r.quit { break; }
}
}
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]));
/// Prompt je nach Zustand: Befehl, Dialog-Weiterblättern oder Choice-Wahl.
fn prompt_for(mode: &Mode) -> &'static str {
match mode {
Mode::Dialog(d) if !d.choices.is_empty() => " wahl> ",
Mode::Dialog(_) => " [Enter] ",
Mode::Free => "> ",
}
}