//! 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) noch nicht //! existiert oder den Kern nichts angeht, produzieren [`Action`]-Werte //! statt direkt zu wirken. Frontends konsumieren die Queue: die CLI druckt //! sie, die Engine führt sie später aus. So bleibt der Kern headless //! testbar und kennt keines der Subsysteme. 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 { /// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen. HideObject(String), /// WAV unter `assets/audio/` als SFX abspielen. PlaySound(String), /// Anzeige-/Eingabemodus wechseln (Spiel, Flycam, Menü). Vom Frontend /// auf seinen `Mode` gemappt; der Dialog-Modus ist hier bewusst nicht /// wählbar — der entsteht nur aus dem Story-Ablauf. SetMode(ModeTarget), } /// Frontend-neutrales Ziel eines Moduswechsels. Spiegelt die nicht-Dialog- /// Varianten von `session::Mode`, ohne dass der Kern die rich `Mode`-Daten /// (Dialog-State) kennen muss. So bleibt `game` ← `session` eine Einbahnstraße. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModeTarget { /// First-Person-Spiel. Play, /// Noclip-Debug-Flycam. 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, } } }