Refactor
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
|||||||
|
//! Konsolen-REPL — das erste Frontend des Kerns.
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use crate::engine::game::{Action, Game};
|
||||||
|
use crate::engine::ink::StoryState;
|
||||||
|
use crate::engine::{kv, signals, story_ctrl};
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
||||||
|
game.signals.len(), signals_path);
|
||||||
|
|
||||||
|
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 <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());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,15 @@
|
|||||||
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
||||||
//! testbar und kennt keines der Subsysteme.
|
//! testbar und kennt keines der Subsysteme.
|
||||||
|
|
||||||
use crate::ink;
|
use std::collections::HashMap;
|
||||||
use crate::kv::Store;
|
|
||||||
use crate::signals::Signals;
|
use crate::engine::ink;
|
||||||
|
use crate::engine::kv::Store;
|
||||||
|
|
||||||
|
/// Signal-Table aus `assets/signals.toml`: Signal-Name → Action-Strings.
|
||||||
|
/// Geladen/geparst von [`crate::engine::signals`]; definiert ist der Typ
|
||||||
|
/// hier beim Zustand, damit `game` ← `signals` eine Einbahnstraße bleibt.
|
||||||
|
pub type Signals = HashMap<String, Vec<String>>;
|
||||||
|
|
||||||
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
||||||
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
||||||
@@ -15,7 +15,7 @@ use std::fs::read_to_string;
|
|||||||
|
|
||||||
use bladeink::story::Story as BladeStory;
|
use bladeink::story::Story as BladeStory;
|
||||||
|
|
||||||
use crate::kv::Store;
|
use crate::engine::kv::Store;
|
||||||
|
|
||||||
pub enum StoryState {
|
pub enum StoryState {
|
||||||
Text(String),
|
Text(String),
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar-
|
//! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar-
|
||||||
//! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores;
|
//! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores;
|
||||||
//! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe
|
//! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe
|
||||||
//! [`crate::ink`]). Damit der KV authoritativ bleibt: externe Actions
|
//! [`crate::engine::ink`]). Damit der KV authoritativ bleibt: externe Actions
|
||||||
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
||||||
//!
|
//!
|
||||||
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//! Headless-Kern: alles, was läuft, ohne dass ein Frontend existiert.
|
||||||
|
//!
|
||||||
|
//! Schicht-Regel: `engine` kennt keine Frontends. Nach außen kommuniziert
|
||||||
|
//! der Kern nur über Rückgabewerte (`StoryState`, Tags) und die
|
||||||
|
//! [`game::Action`]-Queue — niemals über Aufrufe in Frontend-Module.
|
||||||
|
//! Frontends (CLI-REPL jetzt, Renderer später) sind Geschwister-Module,
|
||||||
|
//! die `engine` konsumieren; die Richtung kehrt sich nie um.
|
||||||
|
//!
|
||||||
|
//! Interner Abhängigkeitsgraph (azyklisch, Pfeil = „benutzt"):
|
||||||
|
//!
|
||||||
|
//! kv ← ink ← game ← story_ctrl ← signals
|
||||||
|
//! ↑ ↑
|
||||||
|
//! kv assets
|
||||||
|
//!
|
||||||
|
//! Bitte azyklisch halten: neue Querverbindungen lieber über Rückgabewerte
|
||||||
|
//! an den Aufrufer lösen (so wie story_ctrl Tags zurückgibt, statt selbst
|
||||||
|
//! signals::dispatch zu rufen).
|
||||||
|
|
||||||
|
pub mod assets;
|
||||||
|
pub mod game;
|
||||||
|
pub mod ink;
|
||||||
|
pub mod kv;
|
||||||
|
pub mod signals;
|
||||||
|
pub mod story_ctrl;
|
||||||
@@ -30,11 +30,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
use crate::game::{Action, ActionCtx};
|
use crate::engine::game::{Action, ActionCtx, Signals};
|
||||||
use crate::kv;
|
use crate::engine::kv;
|
||||||
use crate::story_ctrl;
|
use crate::engine::story_ctrl;
|
||||||
|
|
||||||
pub type Signals = HashMap<String, Vec<String>>;
|
|
||||||
|
|
||||||
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
||||||
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
||||||
@@ -129,7 +127,7 @@ pub fn load_signals(path: &str) -> Signals {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::game::Game;
|
use crate::engine::game::Game;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn signal_key_strips_blender_suffix() {
|
fn signal_key_strips_blender_suffix() {
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
||||||
//! (vermeidet den Modul-Zyklus signals → story_ctrl → signals).
|
//! (vermeidet den Modul-Zyklus signals → story_ctrl → signals).
|
||||||
|
|
||||||
use crate::assets;
|
use crate::engine::assets;
|
||||||
use crate::game::ActionCtx;
|
use crate::engine::game::ActionCtx;
|
||||||
use crate::ink::{Story, StoryState};
|
use crate::engine::ink::{Story, StoryState};
|
||||||
|
|
||||||
/// Lädt ein Ink-Skript (Pfad relativ zu `assets/interactions/`) in den
|
/// Lädt ein Ink-Skript (Pfad relativ zu `assets/interactions/`) in den
|
||||||
/// Story-Slot. Der erste Schritt passiert noch nicht — das Frontend ruft
|
/// Story-Slot. Der erste Schritt passiert noch nicht — das Frontend ruft
|
||||||
+10
-136
@@ -1,144 +1,18 @@
|
|||||||
//! wds — Weirdcore Dating Simulator.
|
//! wds — Weirdcore Dating Simulator.
|
||||||
//!
|
//!
|
||||||
//! CLI-Phase: Die Kern-Systeme (Signal/Action-Dispatcher, KV-Store,
|
//! Aufbau: `engine/` ist der headless Kern (Signal/Action-Dispatcher,
|
||||||
//! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend.
|
//! KV-Store, Ink-Stories); Frontends konsumieren ihn über `StoryState`
|
||||||
//! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und
|
//! und die `Action`-Queue. Aktuell einziges Frontend: die CLI-REPL in
|
||||||
//! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und
|
//! `cli`. Der Renderer kommt als zweites dazu (notes/renderer-plan.md).
|
||||||
//! die `Action`-Queue.
|
|
||||||
|
|
||||||
use std::io::{self, Write};
|
mod cli;
|
||||||
|
mod engine;
|
||||||
|
|
||||||
mod assets;
|
use engine::game::Game;
|
||||||
mod game;
|
use engine::{assets, signals};
|
||||||
mod ink;
|
|
||||||
mod kv;
|
|
||||||
mod signals;
|
|
||||||
mod story_ctrl;
|
|
||||||
|
|
||||||
use game::{Action, Game};
|
|
||||||
use ink::StoryState;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let signals_path = assets::path("assets/signals.toml");
|
let signals_path = assets::path("assets/signals.toml");
|
||||||
let signal_table = signals::load_signals(&signals_path);
|
let game = Game::new(signals::load_signals(&signals_path));
|
||||||
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
cli::run(game, &signals_path);
|
||||||
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 <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());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String> {
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user