Actions statt Effects

This commit is contained in:
2026-06-13 02:30:16 +02:00
parent 1af93c897c
commit de853c22bb
3 changed files with 36 additions and 40 deletions
+9 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}