Compare commits
6 Commits
03c97ae3f2
...
c326c363a0
| Author | SHA1 | Date | |
|---|---|---|---|
| c326c363a0 | |||
| 7543fe3cb7 | |||
| 93480150e5 | |||
| 201d3be322 | |||
| b6aca99d0c | |||
| 44f58bc9c2 |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -50,3 +50,8 @@ actions = [
|
|||||||
actions = [
|
actions = [
|
||||||
"debug_log Person hat sich verabschiedet",
|
"debug_log Person hat sich verabschiedet",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tiffany]
|
||||||
|
actions = [
|
||||||
|
"start_ink cinema.ink.json",
|
||||||
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,44 @@ pub enum Action {
|
|||||||
HideObject(String),
|
HideObject(String),
|
||||||
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
||||||
PlaySound(String),
|
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<Self> {
|
||||||
|
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 struct Game {
|
||||||
|
|||||||
+20
-1
@@ -16,6 +16,8 @@
|
|||||||
//! `clear <name>` KV-Eintrag entfernen
|
//! `clear <name>` KV-Eintrag entfernen
|
||||||
//! `hide_object <name>` → Action::HideObject (deferred)
|
//! `hide_object <name>` → Action::HideObject (deferred)
|
||||||
//! `play_sound <file>` → Action::PlaySound (deferred)
|
//! `play_sound <file>` → Action::PlaySound (deferred)
|
||||||
|
//! `mode <play|free|menu>` → Action::SetMode (deferred): Anzeige-/
|
||||||
|
//! Eingabemodus wechseln
|
||||||
//!
|
//!
|
||||||
//! 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
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
use crate::engine::game::{Action, ActionCtx, Signals};
|
use crate::engine::game::{Action, ActionCtx, ModeTarget, Signals};
|
||||||
use crate::engine::kv;
|
use crate::engine::kv;
|
||||||
use crate::engine::story_ctrl;
|
use crate::engine::story_ctrl;
|
||||||
|
|
||||||
@@ -82,6 +84,9 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) {
|
|||||||
"clear" => kv::apply_clear(args, ctx.kv),
|
"clear" => kv::apply_clear(args, ctx.kv),
|
||||||
"hide_object" => ctx.actions.push(Action::HideObject(args.to_string())),
|
"hide_object" => ctx.actions.push(Action::HideObject(args.to_string())),
|
||||||
"play_sound" => ctx.actions.push(Action::PlaySound(args.to_string())),
|
"play_sound" => ctx.actions.push(Action::PlaySound(args.to_string())),
|
||||||
|
"mode" => if let Some(t) = ModeTarget::parse(args) {
|
||||||
|
ctx.actions.push(Action::SetMode(t));
|
||||||
|
},
|
||||||
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +181,20 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_verb_queues_setmode_action() {
|
||||||
|
let mut game = Game::new(Signals::new());
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
dispatch("mode free", &mut ctx);
|
||||||
|
assert_eq!(game.actions, vec![Action::SetMode(ModeTarget::Free)]);
|
||||||
|
|
||||||
|
// Unbekanntes Ziel → kein Action (stilles No-Op).
|
||||||
|
game.actions.clear();
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
dispatch("mode wat", &mut ctx);
|
||||||
|
assert!(game.actions.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_signal_falls_through_to_builtin() {
|
fn unknown_signal_falls_through_to_builtin() {
|
||||||
let mut game = Game::new(Signals::new());
|
let mut game = Game::new(Signals::new());
|
||||||
|
|||||||
+7
-5
@@ -381,13 +381,15 @@ impl App {
|
|||||||
self.eye_y = target_eye;
|
self.eye_y = target_eye;
|
||||||
}
|
}
|
||||||
|
|
||||||
// View: im Spiel aus dem Player der Session (geglättete Augenhöhe),
|
// View: nur die Noclip-Flycam (Free) rendert aus `self.camera`. Alles
|
||||||
// sonst aus der Flycam.
|
// andere — Spiel, Menü, Dialog — sieht durch die Augen des Players
|
||||||
let view = if play {
|
// (geglättete Augenhöhe); Menü/Dialog frieren die Sim nur ein, die
|
||||||
|
// Sicht bleibt die First-Person-Sicht.
|
||||||
|
let view = if fly {
|
||||||
|
self.camera.view()
|
||||||
|
} else {
|
||||||
let p = self.session.player.pos;
|
let p = self.session.player.pos;
|
||||||
Mat4::view([p[0], self.eye_y, p[2]], self.session.player.yaw, self.session.player.pitch)
|
Mat4::view([p[0], self.eye_y, p[2]], self.session.player.yaw, self.session.player.pitch)
|
||||||
} else {
|
|
||||||
self.camera.view()
|
|
||||||
};
|
};
|
||||||
// Mausposition über die Letterbox in interne Pixel mappen.
|
// Mausposition über die Letterbox in interne Pixel mappen.
|
||||||
let cursor = ui::Cursor {
|
let cursor = ui::Cursor {
|
||||||
|
|||||||
+76
-13
@@ -18,7 +18,7 @@
|
|||||||
//! im `render`-Modul.
|
//! im `render`-Modul.
|
||||||
|
|
||||||
use crate::engine::collision::CollisionWorld;
|
use crate::engine::collision::CollisionWorld;
|
||||||
use crate::engine::game::{Action, Game};
|
use crate::engine::game::{Action, Game, ModeTarget};
|
||||||
use crate::engine::ink::StoryState;
|
use crate::engine::ink::StoryState;
|
||||||
use crate::engine::map::{self, Map};
|
use crate::engine::map::{self, Map};
|
||||||
use crate::engine::player::Player;
|
use crate::engine::player::Player;
|
||||||
@@ -148,19 +148,21 @@ impl Session {
|
|||||||
// Menü öffnen bzw. ins Spiel zurück. „Fortsetzen" landet im
|
// Menü öffnen bzw. ins Spiel zurück. „Fortsetzen" landet im
|
||||||
// Spielmodus (Play), nicht in der Debug-Flycam; aus Play oder
|
// Spielmodus (Play), nicht in der Debug-Flycam; aus Play oder
|
||||||
// Free öffnet es das Menü. (Dialog wird anders geroutet.)
|
// Free öffnet es das Menü. (Dialog wird anders geroutet.)
|
||||||
self.mode = match self.mode {
|
let t = match self.mode {
|
||||||
Mode::Menu => Mode::Play,
|
Mode::Menu => ModeTarget::Play,
|
||||||
_ => Mode::Menu,
|
_ => ModeTarget::Menu,
|
||||||
};
|
};
|
||||||
|
self.set_mode(t);
|
||||||
ExecResult::empty()
|
ExecResult::empty()
|
||||||
}
|
}
|
||||||
"noclip" => {
|
"noclip" => {
|
||||||
// Debug: zwischen First-Person (Play) und freier Flycam (Free)
|
// Debug: zwischen First-Person (Play) und freier Flycam (Free)
|
||||||
// umschalten.
|
// umschalten.
|
||||||
self.mode = match self.mode {
|
let t = match self.mode {
|
||||||
Mode::Play => Mode::Free,
|
Mode::Play => ModeTarget::Free,
|
||||||
_ => Mode::Play,
|
_ => ModeTarget::Play,
|
||||||
};
|
};
|
||||||
|
self.set_mode(t);
|
||||||
ExecResult::empty()
|
ExecResult::empty()
|
||||||
}
|
}
|
||||||
"help" => ExecResult::lines(help()),
|
"help" => ExecResult::lines(help()),
|
||||||
@@ -172,7 +174,20 @@ impl Session {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(sig) = line.strip_prefix("signal ") {
|
if let Some(m) = line.strip_prefix("mode ") {
|
||||||
|
// Modus explizit setzen — über dieselbe Action wie das
|
||||||
|
// `mode`-Verb aus signals.toml/Ink-Tags, damit CLI und
|
||||||
|
// Signals genau einen Wechsel-Pfad teilen.
|
||||||
|
match ModeTarget::parse(m) {
|
||||||
|
Some(t) => {
|
||||||
|
self.game.actions.push(Action::SetMode(t));
|
||||||
|
ExecResult::lines(self.drain_actions())
|
||||||
|
}
|
||||||
|
None => ExecResult::lines(vec![
|
||||||
|
"Modus: play | free | menu".into(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
} else if let Some(sig) = line.strip_prefix("signal ") {
|
||||||
ExecResult::lines(self.fire(sig.trim(), None))
|
ExecResult::lines(self.fire(sig.trim(), None))
|
||||||
} else if let Some(name) = line.strip_prefix("use ") {
|
} else if let Some(name) = line.strip_prefix("use ") {
|
||||||
// Objekt-Interaktion simulieren: Signal ist der gestrippte
|
// Objekt-Interaktion simulieren: Signal ist der gestrippte
|
||||||
@@ -229,7 +244,7 @@ impl Session {
|
|||||||
let mut out = self.drain_actions();
|
let mut out = self.drain_actions();
|
||||||
match state {
|
match state {
|
||||||
None | Some(StoryState::End) => {
|
None | Some(StoryState::End) => {
|
||||||
self.mode = Mode::Free;
|
self.mode = Mode::Play;
|
||||||
}
|
}
|
||||||
Some(StoryState::Text(text)) => {
|
Some(StoryState::Text(text)) => {
|
||||||
out.push(text.clone());
|
out.push(text.clone());
|
||||||
@@ -247,10 +262,33 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drain_actions(&mut self) -> Vec<String> {
|
fn drain_actions(&mut self) -> Vec<String> {
|
||||||
self.game.actions.drain(..).map(|a| match a {
|
// Erst aus der Queue ziehen (gibt den `&mut self.game`-Borrow frei),
|
||||||
Action::HideObject(n) => format!("[action] hide_object {n}"),
|
// dann anwenden — `SetMode` braucht `&mut self.mode`.
|
||||||
Action::PlaySound(n) => format!("[action] play_sound {n}"),
|
let actions: Vec<Action> = self.game.actions.drain(..).collect();
|
||||||
}).collect()
|
let mut out = Vec::with_capacity(actions.len());
|
||||||
|
for a in actions {
|
||||||
|
match a {
|
||||||
|
Action::HideObject(n) => out.push(format!("[action] hide_object {n}")),
|
||||||
|
Action::PlaySound(n) => out.push(format!("[action] play_sound {n}")),
|
||||||
|
Action::SetMode(t) => {
|
||||||
|
self.set_mode(t);
|
||||||
|
out.push(format!("[action] mode {}", t.label()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frontend-neutrales [`ModeTarget`] auf den konkreten [`Mode`] mappen.
|
||||||
|
/// Einziger Schreibpfad für die nicht-Dialog-Modi — `menu`/`noclip`/`mode`
|
||||||
|
/// und die `SetMode`-Action laufen alle hier durch. (Der Dialog-Modus
|
||||||
|
/// entsteht ausschließlich in `step` aus dem Story-Ablauf.)
|
||||||
|
fn set_mode(&mut self, t: ModeTarget) {
|
||||||
|
self.mode = match t {
|
||||||
|
ModeTarget::Play => Mode::Play,
|
||||||
|
ModeTarget::Free => Mode::Free,
|
||||||
|
ModeTarget::Menu => Mode::Menu,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump_kv(&self) -> Vec<String> {
|
fn dump_kv(&self) -> Vec<String> {
|
||||||
@@ -286,6 +324,7 @@ fn help() -> Vec<String> {
|
|||||||
" kv KV-Store anzeigen".into(),
|
" kv KV-Store anzeigen".into(),
|
||||||
" menu Menü öffnen / ins Spiel zurück".into(),
|
" menu Menü öffnen / ins Spiel zurück".into(),
|
||||||
" noclip zwischen First-Person und Debug-Flycam wechseln".into(),
|
" noclip zwischen First-Person und Debug-Flycam wechseln".into(),
|
||||||
|
" mode <m> Modus direkt setzen: play | free | menu".into(),
|
||||||
" reload signals.toml neu laden".into(),
|
" reload signals.toml neu laden".into(),
|
||||||
" quit beenden".into(),
|
" quit beenden".into(),
|
||||||
]
|
]
|
||||||
@@ -337,6 +376,30 @@ mod tests {
|
|||||||
assert!(s.game.kv["in_menu"].coerce_to_bool().unwrap());
|
assert!(s.game.kv["in_menu"].coerce_to_bool().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_command_sets_mode_directly() {
|
||||||
|
let mut s = empty_session();
|
||||||
|
s.exec("mode play");
|
||||||
|
assert!(matches!(s.mode, Mode::Play));
|
||||||
|
s.exec("mode free");
|
||||||
|
assert!(matches!(s.mode, Mode::Free));
|
||||||
|
s.exec("mode menu");
|
||||||
|
assert!(matches!(s.mode, Mode::Menu));
|
||||||
|
// Müll-Argument lässt den Modus stehen und meldet die Nutzung.
|
||||||
|
let r = s.exec("mode dialog");
|
||||||
|
assert!(matches!(s.mode, Mode::Menu));
|
||||||
|
assert_eq!(r.output.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signal_can_switch_mode() {
|
||||||
|
let mut s = empty_session();
|
||||||
|
s.mode = Mode::Free;
|
||||||
|
// `mode`-Verb direkt als Action (Ink-Tag-/signals.toml-Pfad).
|
||||||
|
s.exec("signal mode play");
|
||||||
|
assert!(matches!(s.mode, Mode::Play));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn noclip_toggles_play_and_free() {
|
fn noclip_toggles_play_and_free() {
|
||||||
let mut s = empty_session();
|
let mut s = empty_session();
|
||||||
|
|||||||
Reference in New Issue
Block a user