Actions statt Effects
This commit is contained in:
+9
-10
@@ -6,7 +6,7 @@
|
||||
//! konstruiert und durch die Dispatch-Pipeline gereicht.
|
||||
//!
|
||||
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) noch nicht
|
||||
//! existiert oder den Kern nichts angeht, produzieren [`Effect`]-Werte
|
||||
//! 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.
|
||||
@@ -15,30 +15,29 @@ use crate::ink;
|
||||
use crate::kv::Store;
|
||||
use crate::signals::Signals;
|
||||
|
||||
/// Von Actions angeforderte Wirkung außerhalb des Kerns. Die Queue in
|
||||
/// `Game::effects` gehört nach jedem Dispatch geleert (konsumiert).
|
||||
/// 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 Effect {
|
||||
pub enum Action {
|
||||
/// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen.
|
||||
HideObject(String),
|
||||
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
||||
PlaySound(String),
|
||||
/// Palette/Farbschema unter `assets/palettes/<name>` laden.
|
||||
LoadPalette(String),
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
pub kv: Store,
|
||||
pub signals: Signals,
|
||||
pub story: Option<ink::Story>,
|
||||
pub effects: Vec<Effect>,
|
||||
pub actions: Vec<Action>,
|
||||
}
|
||||
|
||||
pub struct ActionCtx<'a> {
|
||||
pub kv: &'a mut Store,
|
||||
pub signals: &'a Signals,
|
||||
pub story: &'a mut Option<ink::Story>,
|
||||
pub effects: &'a mut Vec<Effect>,
|
||||
pub actions: &'a mut Vec<Action>,
|
||||
/// 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
|
||||
@@ -48,7 +47,7 @@ pub struct ActionCtx<'a> {
|
||||
|
||||
impl Game {
|
||||
pub fn new(signals: Signals) -> Self {
|
||||
Self { kv: Store::new(), signals, story: None, effects: Vec::new() }
|
||||
Self { kv: Store::new(), signals, story: None, actions: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn action_ctx(&mut self, instance_name: Option<String>) -> ActionCtx<'_> {
|
||||
@@ -56,7 +55,7 @@ impl Game {
|
||||
kv: &mut self.kv,
|
||||
signals: &self.signals,
|
||||
story: &mut self.story,
|
||||
effects: &mut self.effects,
|
||||
actions: &mut self.actions,
|
||||
instance_name,
|
||||
}
|
||||
}
|
||||
|
||||
+14
-15
@@ -4,7 +4,7 @@
|
||||
//! 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 `Effect`-Queue.
|
||||
//! die `Action`-Queue.
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
@@ -15,15 +15,15 @@ mod kv;
|
||||
mod signals;
|
||||
mod story_ctrl;
|
||||
|
||||
use game::{Effect, Game};
|
||||
use game::{Action, Game};
|
||||
use ink::StoryState;
|
||||
|
||||
fn main() {
|
||||
let signals_path = assets::path("assets/signals.toml");
|
||||
let table = signals::load_signals(&signals_path);
|
||||
let signal_table = signals::load_signals(&signals_path);
|
||||
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
||||
table.len(), signals_path);
|
||||
let mut game = Game::new(table);
|
||||
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);
|
||||
@@ -66,14 +66,14 @@ fn print_help() {
|
||||
println!(" quit beenden");
|
||||
}
|
||||
|
||||
/// Signal dispatchen, Effects ausgeben und — falls eine Action eine Story
|
||||
/// gestartet hat — den Dialog-Loop fahren.
|
||||
/// 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_effects(game);
|
||||
drain_actions(game);
|
||||
if game.story.is_some() {
|
||||
run_story(game);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ fn run_story(game: &mut Game) {
|
||||
for t in &tags { signals::dispatch(t, &mut ctx); }
|
||||
state
|
||||
};
|
||||
drain_effects(game);
|
||||
drain_actions(game);
|
||||
match state {
|
||||
None | Some(StoryState::End) => break,
|
||||
Some(StoryState::Text(text)) => {
|
||||
@@ -115,12 +115,11 @@ fn run_story(game: &mut Game) {
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_effects(game: &mut Game) {
|
||||
for e in game.effects.drain(..) {
|
||||
match e {
|
||||
Effect::HideObject(n) => println!("[effect] hide_object {n}"),
|
||||
Effect::PlaySound(n) => println!("[effect] play_sound {n}"),
|
||||
Effect::LoadPalette(n) => println!("[effect] load_palette {n}"),
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -14,9 +14,8 @@
|
||||
//! `set <name> <value>` KV setzen (true/false/i32/f32/string)
|
||||
//! `inc <name> [<delta>]` KV-Integer inkrementieren (Default +1)
|
||||
//! `clear <name>` KV-Eintrag entfernen
|
||||
//! `hide_object <name>` → Effect::HideObject
|
||||
//! `play_sound <file>` → Effect::PlaySound
|
||||
//! `load_palette <file>` → Effect::LoadPalette
|
||||
//! `hide_object <name>` → Action::HideObject (deferred)
|
||||
//! `play_sound <file>` → Action::PlaySound (deferred)
|
||||
//!
|
||||
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
||||
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
||||
@@ -31,12 +30,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
use crate::game::{ActionCtx, Effect};
|
||||
use crate::game::{Action, ActionCtx};
|
||||
use crate::kv;
|
||||
use crate::story_ctrl;
|
||||
|
||||
pub type Actions = Vec<String>;
|
||||
pub type Signals = HashMap<String, Actions>;
|
||||
pub type Signals = HashMap<String, Vec<String>>;
|
||||
|
||||
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
||||
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
||||
@@ -67,7 +65,8 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) {
|
||||
// $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten,
|
||||
// damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne
|
||||
// instance_name (z.B. `init`-Signal) bleibt `$self` stehen — die Action
|
||||
// zielt dann ins Leere, was für Effects ein No-Op beim Konsumenten ist.
|
||||
// zielt dann ins Leere, was für deferred Actions ein No-Op beim
|
||||
// Konsumenten ist.
|
||||
let cmd_owned: String;
|
||||
let cmd_ref: &str = if cmd.contains("$self") {
|
||||
if let Some(name) = ctx.instance_name.as_deref() {
|
||||
@@ -83,9 +82,8 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) {
|
||||
"set" => kv::apply_set(args, ctx.kv),
|
||||
"inc" => kv::apply_inc(args, ctx.kv),
|
||||
"clear" => kv::apply_clear(args, ctx.kv),
|
||||
"hide_object" => ctx.effects.push(Effect::HideObject(args.to_string())),
|
||||
"play_sound" => ctx.effects.push(Effect::PlaySound(args.to_string())),
|
||||
"load_palette" => ctx.effects.push(Effect::LoadPalette(args.to_string())),
|
||||
"hide_object" => ctx.actions.push(Action::HideObject(args.to_string())),
|
||||
"play_sound" => ctx.actions.push(Action::PlaySound(args.to_string())),
|
||||
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
||||
}
|
||||
}
|
||||
@@ -161,7 +159,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_runs_actions_and_queues_effects() {
|
||||
fn dispatch_runs_actions_and_queues_deferred() {
|
||||
let mut signals = Signals::new();
|
||||
signals.insert("pickup".into(), vec![
|
||||
"inc items".into(),
|
||||
@@ -174,9 +172,9 @@ mod tests {
|
||||
dispatch("pickup", &mut ctx);
|
||||
|
||||
assert_eq!(game.kv["items"].coerce_to_int().unwrap(), 1);
|
||||
assert_eq!(game.effects, vec![
|
||||
Effect::PlaySound("pickup.wav".into()),
|
||||
Effect::HideObject("Mushroom.005".into()),
|
||||
assert_eq!(game.actions, vec![
|
||||
Action::PlaySound("pickup.wav".into()),
|
||||
Action::HideObject("Mushroom.005".into()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -191,6 +189,6 @@ mod tests {
|
||||
// Unbekanntes Verb ist ein stilles No-Op.
|
||||
let mut ctx = game.action_ctx(None);
|
||||
dispatch("color story_a", &mut ctx);
|
||||
assert!(game.effects.is_empty());
|
||||
assert!(game.actions.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user