//! Owner des dispatch-relevanten Spielzustands und der Borrow-View, durch //! die Signal-Actions und Ink-Story-Steps darauf zugreifen. //! //! `Game` hält die Slots, die Actions lesen oder mutieren dürfen. //! `ActionCtx` ist der schmale `&mut`-View; er wird über `Game::action_ctx()` //! konstruiert und durch die Dispatch-Pipeline gereicht. //! //! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) den Kern nichts //! angeht, produzieren [`Action`]-Werte statt direkt zu wirken. Frontends //! konsumieren die Queue: die CLI druckt sie, das Fenster führt sie aus. use std::collections::HashMap; 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>; /// Action, die der Kern nicht selbst ausführt, sondern ans Frontend /// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch /// geleert (konsumiert). #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { /// `assets/sounds/{name}.wav` als Einmal-SFX abspielen. PlaySound(String), /// Anzeige-/Eingabemodus wechseln (Spiel, Noclip, Menü). Der Dialog-Modus /// ist bewusst nicht wählbar — er entsteht nur aus dem Story-Ablauf. SetMode(ModeTarget), /// Renderer-Post-Effekt schalten. Der Kern kennt weder die Kette noch die /// Effektnamen — das Fenster löst `name` gegen seine Kette auf (unbekannt /// → Meldung), die CLI reicht nur die `[action]`-Zeile. SetPost { name: String, on: bool }, } /// Frontend-neutrales Ziel eines Moduswechsels: die nicht-Dialog-Varianten von /// `session::Mode`, ohne dass der Kern dessen Dialog-Daten kennen muss. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModeTarget { /// First-Person-Spiel. Play, /// Noclip: freies Fliegen ohne Kollision. Free, /// Menü (pausiert die Welt, gibt die Maus frei). Menu, } impl ModeTarget { /// `play`/`free`/`menu` (case-insensitiv) → Ziel; sonst `None`. pub fn parse(s: &str) -> Option { match s.trim().to_ascii_lowercase().as_str() { "play" => Some(Self::Play), "free" => Some(Self::Free), "menu" => Some(Self::Menu), _ => None, } } /// Kanonischer Name fürs Echo/Logging. pub fn label(self) -> &'static str { match self { Self::Play => "play", Self::Free => "free", Self::Menu => "menu", } } } pub struct Game { pub kv: Store, pub signals: Signals, pub story: Option, pub actions: Vec, } pub struct ActionCtx<'a> { pub kv: &'a mut Store, pub signals: &'a Signals, pub story: &'a mut Option, pub actions: &'a mut Vec, /// Auslöser-Identifier: bei Objekt-Interaktion der volle Instance-Name /// (mit Blender-Suffix, z.B. `Mushroom.005`). `$self` in Action-Args /// wird damit substituiert. `None` bei Signalen ohne Quell-Instance /// (z.B. `init`, Story-Tags). pub instance_name: Option, } impl Game { pub fn new(signals: Signals) -> Self { Self { kv: Store::new(), signals, story: None, actions: Vec::new() } } pub fn action_ctx(&mut self, instance_name: Option) -> ActionCtx<'_> { ActionCtx { kv: &mut self.kv, signals: &self.signals, story: &mut self.story, actions: &mut self.actions, instance_name, } } }