Actions statt Effects
This commit is contained in:
+9
-10
@@ -6,7 +6,7 @@
|
|||||||
//! konstruiert und durch die Dispatch-Pipeline gereicht.
|
//! konstruiert und durch die Dispatch-Pipeline gereicht.
|
||||||
//!
|
//!
|
||||||
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) noch nicht
|
//! 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
|
//! 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
|
//! 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.
|
||||||
@@ -15,30 +15,29 @@ use crate::ink;
|
|||||||
use crate::kv::Store;
|
use crate::kv::Store;
|
||||||
use crate::signals::Signals;
|
use crate::signals::Signals;
|
||||||
|
|
||||||
/// Von Actions angeforderte Wirkung außerhalb des Kerns. Die Queue in
|
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
||||||
/// `Game::effects` gehört nach jedem Dispatch geleert (konsumiert).
|
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
||||||
|
/// geleert (konsumiert).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Effect {
|
pub enum Action {
|
||||||
/// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen.
|
/// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen.
|
||||||
HideObject(String),
|
HideObject(String),
|
||||||
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
||||||
PlaySound(String),
|
PlaySound(String),
|
||||||
/// Palette/Farbschema unter `assets/palettes/<name>` laden.
|
|
||||||
LoadPalette(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
pub kv: Store,
|
pub kv: Store,
|
||||||
pub signals: Signals,
|
pub signals: Signals,
|
||||||
pub story: Option<ink::Story>,
|
pub story: Option<ink::Story>,
|
||||||
pub effects: Vec<Effect>,
|
pub actions: Vec<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActionCtx<'a> {
|
pub struct ActionCtx<'a> {
|
||||||
pub kv: &'a mut Store,
|
pub kv: &'a mut Store,
|
||||||
pub signals: &'a Signals,
|
pub signals: &'a Signals,
|
||||||
pub story: &'a mut Option<ink::Story>,
|
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
|
/// Auslöser-Identifier: bei Objekt-Interaktion der volle Instance-Name
|
||||||
/// (mit Blender-Suffix, z.B. `Mushroom.005`). `$self` in Action-Args
|
/// (mit Blender-Suffix, z.B. `Mushroom.005`). `$self` in Action-Args
|
||||||
/// wird damit substituiert. `None` bei Signalen ohne Quell-Instance
|
/// wird damit substituiert. `None` bei Signalen ohne Quell-Instance
|
||||||
@@ -48,7 +47,7 @@ pub struct ActionCtx<'a> {
|
|||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new(signals: Signals) -> Self {
|
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<'_> {
|
pub fn action_ctx(&mut self, instance_name: Option<String>) -> ActionCtx<'_> {
|
||||||
@@ -56,7 +55,7 @@ impl Game {
|
|||||||
kv: &mut self.kv,
|
kv: &mut self.kv,
|
||||||
signals: &self.signals,
|
signals: &self.signals,
|
||||||
story: &mut self.story,
|
story: &mut self.story,
|
||||||
effects: &mut self.effects,
|
actions: &mut self.actions,
|
||||||
instance_name,
|
instance_name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-15
@@ -4,7 +4,7 @@
|
|||||||
//! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend.
|
//! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend.
|
||||||
//! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und
|
//! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und
|
||||||
//! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und
|
//! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und
|
||||||
//! die `Effect`-Queue.
|
//! die `Action`-Queue.
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
@@ -15,15 +15,15 @@ mod kv;
|
|||||||
mod signals;
|
mod signals;
|
||||||
mod story_ctrl;
|
mod story_ctrl;
|
||||||
|
|
||||||
use game::{Effect, Game};
|
use game::{Action, Game};
|
||||||
use ink::StoryState;
|
use ink::StoryState;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let signals_path = assets::path("assets/signals.toml");
|
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.",
|
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
||||||
table.len(), signals_path);
|
signal_table.len(), signals_path);
|
||||||
let mut game = Game::new(table);
|
let mut game = Game::new(signal_table);
|
||||||
|
|
||||||
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
|
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
|
||||||
fire(&mut game, "init", None);
|
fire(&mut game, "init", None);
|
||||||
@@ -66,14 +66,14 @@ fn print_help() {
|
|||||||
println!(" quit beenden");
|
println!(" quit beenden");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signal dispatchen, Effects ausgeben und — falls eine Action eine Story
|
/// Signal dispatchen, deferred Actions ausgeben und — falls eine Action
|
||||||
/// gestartet hat — den Dialog-Loop fahren.
|
/// eine Story gestartet hat — den Dialog-Loop fahren.
|
||||||
fn fire(game: &mut Game, signal: &str, instance: Option<String>) {
|
fn fire(game: &mut Game, signal: &str, instance: Option<String>) {
|
||||||
{
|
{
|
||||||
let mut ctx = game.action_ctx(instance);
|
let mut ctx = game.action_ctx(instance);
|
||||||
signals::dispatch(signal, &mut ctx);
|
signals::dispatch(signal, &mut ctx);
|
||||||
}
|
}
|
||||||
drain_effects(game);
|
drain_actions(game);
|
||||||
if game.story.is_some() {
|
if game.story.is_some() {
|
||||||
run_story(game);
|
run_story(game);
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ fn run_story(game: &mut Game) {
|
|||||||
for t in &tags { signals::dispatch(t, &mut ctx); }
|
for t in &tags { signals::dispatch(t, &mut ctx); }
|
||||||
state
|
state
|
||||||
};
|
};
|
||||||
drain_effects(game);
|
drain_actions(game);
|
||||||
match state {
|
match state {
|
||||||
None | Some(StoryState::End) => break,
|
None | Some(StoryState::End) => break,
|
||||||
Some(StoryState::Text(text)) => {
|
Some(StoryState::Text(text)) => {
|
||||||
@@ -115,12 +115,11 @@ fn run_story(game: &mut Game) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain_effects(game: &mut Game) {
|
fn drain_actions(game: &mut Game) {
|
||||||
for e in game.effects.drain(..) {
|
for a in game.actions.drain(..) {
|
||||||
match e {
|
match a {
|
||||||
Effect::HideObject(n) => println!("[effect] hide_object {n}"),
|
Action::HideObject(n) => println!("[action] hide_object {n}"),
|
||||||
Effect::PlaySound(n) => println!("[effect] play_sound {n}"),
|
Action::PlaySound(n) => println!("[action] play_sound {n}"),
|
||||||
Effect::LoadPalette(n) => println!("[effect] load_palette {n}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-15
@@ -14,9 +14,8 @@
|
|||||||
//! `set <name> <value>` KV setzen (true/false/i32/f32/string)
|
//! `set <name> <value>` KV setzen (true/false/i32/f32/string)
|
||||||
//! `inc <name> [<delta>]` KV-Integer inkrementieren (Default +1)
|
//! `inc <name> [<delta>]` KV-Integer inkrementieren (Default +1)
|
||||||
//! `clear <name>` KV-Eintrag entfernen
|
//! `clear <name>` KV-Eintrag entfernen
|
||||||
//! `hide_object <name>` → Effect::HideObject
|
//! `hide_object <name>` → Action::HideObject (deferred)
|
||||||
//! `play_sound <file>` → Effect::PlaySound
|
//! `play_sound <file>` → Action::PlaySound (deferred)
|
||||||
//! `load_palette <file>` → Effect::LoadPalette
|
|
||||||
//!
|
//!
|
||||||
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
||||||
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
||||||
@@ -31,12 +30,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
use crate::game::{ActionCtx, Effect};
|
use crate::game::{Action, ActionCtx};
|
||||||
use crate::kv;
|
use crate::kv;
|
||||||
use crate::story_ctrl;
|
use crate::story_ctrl;
|
||||||
|
|
||||||
pub type Actions = Vec<String>;
|
pub type Signals = HashMap<String, Vec<String>>;
|
||||||
pub type Signals = HashMap<String, Actions>;
|
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -67,7 +65,8 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) {
|
|||||||
// $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten,
|
// $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten,
|
||||||
// damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne
|
// damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne
|
||||||
// instance_name (z.B. `init`-Signal) bleibt `$self` stehen — die Action
|
// 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_owned: String;
|
||||||
let cmd_ref: &str = if cmd.contains("$self") {
|
let cmd_ref: &str = if cmd.contains("$self") {
|
||||||
if let Some(name) = ctx.instance_name.as_deref() {
|
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),
|
"set" => kv::apply_set(args, ctx.kv),
|
||||||
"inc" => kv::apply_inc(args, ctx.kv),
|
"inc" => kv::apply_inc(args, ctx.kv),
|
||||||
"clear" => kv::apply_clear(args, ctx.kv),
|
"clear" => kv::apply_clear(args, ctx.kv),
|
||||||
"hide_object" => ctx.effects.push(Effect::HideObject(args.to_string())),
|
"hide_object" => ctx.actions.push(Action::HideObject(args.to_string())),
|
||||||
"play_sound" => ctx.effects.push(Effect::PlaySound(args.to_string())),
|
"play_sound" => ctx.actions.push(Action::PlaySound(args.to_string())),
|
||||||
"load_palette" => ctx.effects.push(Effect::LoadPalette(args.to_string())),
|
|
||||||
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,7 +159,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dispatch_runs_actions_and_queues_effects() {
|
fn dispatch_runs_actions_and_queues_deferred() {
|
||||||
let mut signals = Signals::new();
|
let mut signals = Signals::new();
|
||||||
signals.insert("pickup".into(), vec![
|
signals.insert("pickup".into(), vec![
|
||||||
"inc items".into(),
|
"inc items".into(),
|
||||||
@@ -174,9 +172,9 @@ mod tests {
|
|||||||
dispatch("pickup", &mut ctx);
|
dispatch("pickup", &mut ctx);
|
||||||
|
|
||||||
assert_eq!(game.kv["items"].coerce_to_int().unwrap(), 1);
|
assert_eq!(game.kv["items"].coerce_to_int().unwrap(), 1);
|
||||||
assert_eq!(game.effects, vec![
|
assert_eq!(game.actions, vec![
|
||||||
Effect::PlaySound("pickup.wav".into()),
|
Action::PlaySound("pickup.wav".into()),
|
||||||
Effect::HideObject("Mushroom.005".into()),
|
Action::HideObject("Mushroom.005".into()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +189,6 @@ mod tests {
|
|||||||
// Unbekanntes Verb ist ein stilles No-Op.
|
// Unbekanntes Verb ist ein stilles No-Op.
|
||||||
let mut ctx = game.action_ctx(None);
|
let mut ctx = game.action_ctx(None);
|
||||||
dispatch("color story_a", &mut ctx);
|
dispatch("color story_a", &mut ctx);
|
||||||
assert!(game.effects.is_empty());
|
assert!(game.actions.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user