refactor
This commit is contained in:
+78
-73
@@ -6,19 +6,15 @@ use crate::engine::player::{self, Player};
|
||||
use crate::engine::trigger::Triggers;
|
||||
use crate::engine::{kv, model, signals, story_ctrl};
|
||||
|
||||
/// Maus-Empfindlichkeit der Sicht (Radiant/Pixel). Geteilt zwischen der
|
||||
/// First-Person-Sim hier und der Debug-Flycam im Fenster-Frontend, damit sich
|
||||
/// beide gleich anfühlen (das Frontend referenziert diese Konstante).
|
||||
pub const LOOK_SENS: f32 = 0.0025;
|
||||
/// Maus-Empfindlichkeit der Sicht (Radiant/Pixel).
|
||||
const LOOK_SENS: f32 = 0.0025;
|
||||
|
||||
/// Fester Simulationsschritt (s) = 60 Hz. Die Physik läuft ausschließlich in
|
||||
/// diesem Takt — unabhängig von der Render-Framerate — damit Step-up und
|
||||
/// Ground-Snap sich exakt so verhalten wie in den (bei 1/60 geschriebenen)
|
||||
/// Player-Tests, statt bei 144 Hz oder Rucklern durch andere dt-Werte zu driften.
|
||||
/// Fester Simulationsschritt (s) = 60 Hz, unabhängig von der Render-Framerate:
|
||||
/// Step-up und Ground-Snap verhalten sich sonst bei 144 Hz anders als in den
|
||||
/// (bei 1/60 geschriebenen) Player-Tests.
|
||||
const FIXED_DT: f32 = 1.0 / 60.0;
|
||||
/// Deckel für den Sim-Akkumulator (s): bei einem schweren Ruckler holen wir
|
||||
/// höchstens so viel Zeit nach, sonst spiralt die Sim bei einem Hänger davon
|
||||
/// („spiral of death"). ~0.25 s = maximal 15 Nachhol-Schritte pro Frame.
|
||||
/// Deckel für den Sim-Akkumulator (s): nach einem Hänger höchstens so viel
|
||||
/// Zeit nachholen, sonst spiralt die Sim davon. ~15 Schritte pro Frame.
|
||||
const MAX_ACCUM: f32 = 0.25;
|
||||
|
||||
/// Reichweite (units) des Point-and-Click-Strahls auf `signal`-Klick-Ziele.
|
||||
@@ -48,25 +44,26 @@ pub struct Session {
|
||||
/// Empties, siehe engine::audio).
|
||||
emitters: Emitters,
|
||||
/// Von `Action::PlaySound` aufgelaufene Einmal-SFX — das Fenster-Frontend
|
||||
/// zieht sie per [`Session::take_sounds`] und spielt sie; die CLI lässt
|
||||
/// sie liegen (ihr reicht die `[action]`-Textzeile).
|
||||
/// zieht sie per [`Session::take_sounds`]; der CLI reicht die
|
||||
/// `[action]`-Textzeile.
|
||||
pending_sounds: Vec<String>,
|
||||
signals_path: String,
|
||||
/// Aufgelaufene, noch nicht simulierte Zeit (s) für den Fixed-Timestep.
|
||||
sim_accum: f32,
|
||||
/// Über Frames gepufferte Sprung-Flanke: eine Sprungtaste, die in einen
|
||||
/// Frame ohne Sim-Schritt fällt (hohe FPS), ginge sonst verloren. Wird beim
|
||||
/// nächsten Sub-Schritt konsumiert.
|
||||
/// Gepufferte Sprung-Flanke: bei hohen FPS fällt eine Sprungtaste sonst in
|
||||
/// einen Frame ohne Sim-Schritt und verfällt.
|
||||
pending_jump: bool,
|
||||
}
|
||||
|
||||
/// Kontinuierliche Eingabe für einen Simulationsschritt — vom Frontend pro
|
||||
/// Frame gefüllt. Gegenstück zu [`ExecResult`]/`exec` (diskret). `look_*`
|
||||
/// sind rohe Maus-Pixel, `fwd`/`right` Tastenachsen in [-1, 1].
|
||||
/// sind rohe Maus-Pixel, `fwd`/`right`/`up` Tastenachsen in [-1, 1]; `up`
|
||||
/// wirkt nur im Noclip.
|
||||
#[derive(Default)]
|
||||
pub struct FrameInput {
|
||||
pub fwd: f32,
|
||||
pub right: f32,
|
||||
pub up: f32,
|
||||
pub jump: bool,
|
||||
pub look_dx: f32,
|
||||
pub look_dy: f32,
|
||||
@@ -103,13 +100,10 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sim-Seite eines Blender-Modells in die Welt einfügen — einmal pro
|
||||
/// Modell vom Fenster-Frontend beim Start gerufen: Collider (`collide`-
|
||||
/// Property → konvexe Hülle), Trigger (`signal`-Property → Klick-Ziele/
|
||||
/// Zonen), Ambient-Emitter (`sound`-Property auf Empties) und der
|
||||
/// Spieler-Spawn (Empty mit `role = spawn`; bringen mehrere Modelle
|
||||
/// einen mit, gewinnt der zuletzt geladene). Die Sichtgeometrie
|
||||
/// desselben Modells ist Sache des Renderers (render::props).
|
||||
/// Sim-Seite eines Blender-Modells in die Welt einfügen (die Sichtgeometrie
|
||||
/// ist Sache von render::props): Collider aus `collide`, Trigger aus
|
||||
/// `signal`, Ambient-Emitter aus `sound` und der Spawn aus `role = spawn`.
|
||||
/// Bringen mehrere Modelle einen Spawn mit, gewinnt der zuletzt geladene.
|
||||
pub fn load_props(&mut self, model: &model::Model) {
|
||||
self.collision.add_model(model);
|
||||
self.triggers.add_model(model);
|
||||
@@ -140,12 +134,20 @@ impl Session {
|
||||
}
|
||||
|
||||
/// Einen Simulationsschritt treiben — das kontinuierliche Gegenstück zu
|
||||
/// `exec`. Nur im Spielmodus bewegt sich der Spieler; Menü/Dialog/Flycam
|
||||
/// pausieren die Sim (die Flycam ist reine Frontend-Sicht). Rückgabe:
|
||||
/// `exec`. Im Spielmodus läuft die Physik, im Noclip fliegt derselbe
|
||||
/// Spieler frei; Menü und Dialog pausieren die Welt. Rückgabe:
|
||||
/// Ausgabezeilen gefeuerter Betretens-Zonen (meist leer) — das Frontend
|
||||
/// zeigt sie wie `exec`-Ausgaben an.
|
||||
pub fn tick(&mut self, input: &FrameInput, dt: f32) -> Vec<String> {
|
||||
if !matches!(self.mode, Mode::FirstPerson) { return Vec::new(); }
|
||||
match self.mode {
|
||||
Mode::FirstPerson => {}
|
||||
Mode::Free => {
|
||||
self.player.look(input.look_dx, input.look_dy, LOOK_SENS);
|
||||
self.player.fly(input.fwd, input.right, input.up, dt);
|
||||
return Vec::new();
|
||||
}
|
||||
_ => return Vec::new(),
|
||||
}
|
||||
|
||||
// Blick ist ein direkter Maus-Delta (nicht zeitintegriert) → genau
|
||||
// einmal pro Frame anwenden, sonst skalierte ihn die Zahl der
|
||||
@@ -217,12 +219,13 @@ impl Session {
|
||||
|
||||
fn command(&mut self, line: &str) -> ExecResult {
|
||||
let line = line.trim();
|
||||
match line {
|
||||
"" => ExecResult::empty(),
|
||||
"quit" | "exit" => ExecResult { output: Vec::new(), quit: true },
|
||||
"menu" => {
|
||||
let (verb, args) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
|
||||
match (verb, args.trim()) {
|
||||
("", _) => ExecResult::empty(),
|
||||
("quit" | "exit", _) => ExecResult { output: Vec::new(), quit: true },
|
||||
("menu", _) => {
|
||||
// Menü öffnen bzw. ins Spiel zurück. „Fortsetzen" landet im
|
||||
// Spielmodus (Play), nicht in der Debug-Flycam; aus Play oder
|
||||
// Spielmodus (Play), nicht im Noclip; aus Play oder
|
||||
// Free öffnet es das Menü. (Dialog wird anders geroutet.)
|
||||
let t = match self.mode {
|
||||
Mode::Menu => ModeTarget::Play,
|
||||
@@ -231,56 +234,44 @@ impl Session {
|
||||
self.set_mode(t);
|
||||
ExecResult::empty()
|
||||
}
|
||||
"noclip" => {
|
||||
// Debug: zwischen First-Person (Play) und freier Flycam (Free)
|
||||
// umschalten.
|
||||
("noclip", _) => {
|
||||
let t = match self.mode {
|
||||
Mode::FirstPerson => ModeTarget::Free,
|
||||
_ => ModeTarget::Play,
|
||||
_ => ModeTarget::Play,
|
||||
};
|
||||
self.set_mode(t);
|
||||
ExecResult::empty()
|
||||
}
|
||||
"help" => ExecResult::lines(help()),
|
||||
"kv" => ExecResult::lines(self.dump_kv()),
|
||||
"reload" => {
|
||||
("help", _) => ExecResult::lines(help()),
|
||||
("kv", _) => ExecResult::lines(self.dump_kv()),
|
||||
("reload", _) => {
|
||||
self.game.signals = signals::load_signals(&self.signals_path);
|
||||
ExecResult::lines(vec![
|
||||
format!("{} Signale geladen.", self.game.signals.len()),
|
||||
])
|
||||
}
|
||||
_ => {
|
||||
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))
|
||||
} else if let Some(name) = line.strip_prefix("use ") {
|
||||
// Objekt-Interaktion (LMB-Klick und Konsole laufen beide
|
||||
// hier durch): Klick-Ziele mit `signal`-Property feuern
|
||||
// dieses Signal; sonst den vom Blender-Suffix befreiten
|
||||
// Objektnamen. $self ist immer der volle Name.
|
||||
let name = name.trim();
|
||||
let key = self.triggers.signal_for(name)
|
||||
.unwrap_or_else(|| signals::signal_key(name))
|
||||
.to_string();
|
||||
ExecResult::lines(self.fire(&key, Some(name.to_string())))
|
||||
} else {
|
||||
ExecResult::lines(vec![
|
||||
format!("unbekannter Befehl: {line:?} — `help` für Befehle"),
|
||||
])
|
||||
// Modus über dieselbe Action wie das `mode`-Verb aus
|
||||
// signals.toml/Ink-Tags — ein Wechsel-Pfad für alle Quellen.
|
||||
("mode", m) => 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()]),
|
||||
},
|
||||
("signal", sig) => ExecResult::lines(self.fire(sig, None)),
|
||||
// Objekt-Interaktion (Klick wie Konsole): Klick-Ziele feuern ihre
|
||||
// `signal`-Property, sonst den suffix-befreiten Objektnamen.
|
||||
// `$self` ist immer der volle Name.
|
||||
("use", name) => {
|
||||
let key = self.triggers.signal_for(name)
|
||||
.unwrap_or_else(|| signals::signal_key(name))
|
||||
.to_string();
|
||||
ExecResult::lines(self.fire(&key, Some(name.to_string())))
|
||||
}
|
||||
_ => ExecResult::lines(vec![
|
||||
format!("unbekannter Befehl: {line:?} — `help` für Befehle"),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +389,7 @@ fn help() -> Vec<String> {
|
||||
" feuert Signal `Mushroom` mit $self = Mushroom.005".into(),
|
||||
" kv KV-Store anzeigen".into(),
|
||||
" menu Menü öffnen / ins Spiel zurück".into(),
|
||||
" noclip zwischen First-Person und Debug-Flycam wechseln".into(),
|
||||
" noclip zwischen First-Person und Noclip wechseln".into(),
|
||||
" mode <m> Modus direkt setzen: play | free | menu".into(),
|
||||
" reload signals.toml neu laden".into(),
|
||||
" quit beenden".into(),
|
||||
@@ -525,13 +516,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_only_runs_in_first_person() {
|
||||
// Menü/Free/Dialog pausieren die Sim: kein Schritt, keine Bewegung.
|
||||
fn menu_pauses_the_sim() {
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::Menu;
|
||||
let before = s.player.pos;
|
||||
s.tick(&FrameInput::default(), 0.1);
|
||||
assert_eq!(s.player.pos, before, "Menü pausiert die Sim");
|
||||
assert_eq!(s.player.pos, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noclip_flies_the_player_without_physics() {
|
||||
// Free bewegt denselben Spieler, nur ohne Welt: `up` steigt, und beim
|
||||
// Zurückschalten steht er dort, wo er hingeflogen ist.
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::Free;
|
||||
s.tick(&FrameInput { up: 1.0, ..Default::default() }, 1.0);
|
||||
assert!(s.player.pos[1] > 1.5, "sollte gestiegen sein: {}", s.player.pos[1]);
|
||||
assert!(!s.player.grounded);
|
||||
|
||||
let flown_to = s.player.pos;
|
||||
s.exec("noclip");
|
||||
assert_eq!(s.player.pos, flown_to, "Noclip-Ende teleportiert nicht zurück");
|
||||
}
|
||||
|
||||
/// Modell mit einer Betretens-Zone vor dem Spieler und einem Klick-Ziel,
|
||||
|
||||
Reference in New Issue
Block a user