First Person Controller
This commit is contained in:
+110
-15
@@ -9,20 +9,38 @@
|
||||
//! Frontend — `exec` nimmt eine Befehlszeile und *gibt* Ausgabezeilen
|
||||
//! zurück (statt zu drucken), damit jedes Frontend sie frei darstellt
|
||||
//! (Terminal jetzt, In-Fenster-Konsole später).
|
||||
//!
|
||||
//! Die Session ist auch der alleinige Besitzer der **Lauf-Simulation**:
|
||||
//! `Player` und `CollisionWorld`. Sie hat zwei Eingabe-Tore — `exec` für
|
||||
//! diskrete Befehle/Dialog und `tick` für die kontinuierliche Physik pro
|
||||
//! Frame. Die Frontends sammeln nur Eingaben und stellen den Zustand dar;
|
||||
//! die Flycam dagegen ist reine Frontend-Sicht (kein Sim-Zustand) und bleibt
|
||||
//! im `render`-Modul.
|
||||
|
||||
use crate::engine::collision::CollisionWorld;
|
||||
use crate::engine::game::{Action, Game};
|
||||
use crate::engine::ink::StoryState;
|
||||
use crate::engine::map::{self, Map};
|
||||
use crate::engine::player::Player;
|
||||
use crate::engine::{kv, signals, story_ctrl};
|
||||
|
||||
/// Maus-Empfindlichkeit der Spieler-Sicht (Radiant/Pixel).
|
||||
const LOOK_SENS: f32 = 0.0025;
|
||||
|
||||
/// Was die Frontends gerade darstellen sollen. Geteilt, damit CLI und GUI
|
||||
/// nie uneins sind, ob ein Dialog läuft.
|
||||
pub enum Mode {
|
||||
/// Freie Welt — das Fenster rendert die Szene, die Konsole nimmt
|
||||
/// Engine-Befehle.
|
||||
/// First-Person-Spiel — der Player bewegt sich mit Physik durch die Welt
|
||||
/// (siehe engine::player). Der Renderer leitet seine View aus dem Player
|
||||
/// ab. Das ist der eigentliche Spielmodus.
|
||||
Play,
|
||||
/// Noclip-Debug-Flycam — frei schwebende Kamera ohne Physik. Per
|
||||
/// `noclip`-Befehl gegen `Play` getauscht.
|
||||
Free,
|
||||
/// Menü offen — wie der Dialog pausiert das Fenster die Welt und gibt die
|
||||
/// Maus frei; Eingaben sind weiter Engine-Befehle (das Menü ist nur eine
|
||||
/// andere Darstellung). Per `menu`-Befehl getoggelt.
|
||||
/// andere Darstellung). Per `menu`-Befehl getoggelt; „Fortsetzen" führt
|
||||
/// ins Spiel (`Play`).
|
||||
Menu,
|
||||
/// Ein Dialog läuft — das Fenster pausiert die Welt und zeigt Panels;
|
||||
/// Eingaben treiben den Dialog statt Befehle.
|
||||
@@ -42,9 +60,23 @@ pub struct Dialog {
|
||||
pub struct Session {
|
||||
pub game: Game,
|
||||
pub mode: Mode,
|
||||
pub player: Player,
|
||||
pub collision: CollisionWorld,
|
||||
signals_path: String,
|
||||
}
|
||||
|
||||
/// 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].
|
||||
#[derive(Default)]
|
||||
pub struct FrameInput {
|
||||
pub fwd: f32,
|
||||
pub right: f32,
|
||||
pub jump: bool,
|
||||
pub look_dx: f32,
|
||||
pub look_dy: f32,
|
||||
}
|
||||
|
||||
/// Ergebnis einer Eingabezeile: Ausgabezeilen plus, ob das Frontend
|
||||
/// beenden soll (`quit`/`exit`).
|
||||
pub struct ExecResult {
|
||||
@@ -60,7 +92,33 @@ impl ExecResult {
|
||||
impl Session {
|
||||
pub fn new(signals_path: String) -> Self {
|
||||
let game = Game::new(signals::load_signals(&signals_path));
|
||||
Self { game, mode: Mode::Menu, signals_path }
|
||||
Self {
|
||||
game,
|
||||
mode: Mode::Menu,
|
||||
// Default-Welt, bis `load_world` eine Map einspielt (die CLI
|
||||
// braucht keine — sie tickt nie).
|
||||
player: Player::new([0.0, 1.0, 0.0]),
|
||||
collision: CollisionWorld::empty(),
|
||||
signals_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Welt-Geometrie aus der Map laden: Collision-Brushes bauen und den
|
||||
/// Spieler an `info_player_start` setzen. Einmal vom Fenster-Frontend beim
|
||||
/// Start gerufen.
|
||||
pub fn load_world(&mut self, map: &Map) {
|
||||
self.collision = CollisionWorld::build(map);
|
||||
self.player = Player::new(player_spawn(map));
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn tick(&mut self, input: &FrameInput, dt: f32) {
|
||||
if matches!(self.mode, Mode::Play) {
|
||||
self.player.look(input.look_dx, input.look_dy, LOOK_SENS);
|
||||
self.player.step(&self.collision, input.fwd, input.right, input.jump, dt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserviertes `[init]`-Signal feuern (KV-Defaults, bevor etwas läuft).
|
||||
@@ -76,8 +134,8 @@ impl Session {
|
||||
/// Klick denselben Dialog ohne Sonderpfad.
|
||||
pub fn exec(&mut self, line: &str) -> ExecResult {
|
||||
match self.mode {
|
||||
Mode::Dialog(_) => self.dialog_input(line),
|
||||
Mode::Free | Mode::Menu => self.command(line),
|
||||
Mode::Dialog(_) => self.dialog_input(line),
|
||||
Mode::Play | Mode::Free | Mode::Menu => self.command(line),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +145,24 @@ impl Session {
|
||||
"" => ExecResult::empty(),
|
||||
"quit" | "exit" => ExecResult { output: Vec::new(), quit: true },
|
||||
"menu" => {
|
||||
// Free ↔ Menu umschalten (erreichbar nur aus diesen beiden,
|
||||
// da Dialog anders geroutet wird).
|
||||
// Menü öffnen bzw. ins Spiel zurück. „Fortsetzen" landet im
|
||||
// Spielmodus (Play), nicht in der Debug-Flycam; aus Play oder
|
||||
// Free öffnet es das Menü. (Dialog wird anders geroutet.)
|
||||
self.mode = match self.mode {
|
||||
Mode::Menu => Mode::Free,
|
||||
Mode::Menu => Mode::Play,
|
||||
_ => Mode::Menu,
|
||||
};
|
||||
ExecResult::empty()
|
||||
}
|
||||
"noclip" => {
|
||||
// Debug: zwischen First-Person (Play) und freier Flycam (Free)
|
||||
// umschalten.
|
||||
self.mode = match self.mode {
|
||||
Mode::Play => Mode::Free,
|
||||
_ => Mode::Play,
|
||||
};
|
||||
ExecResult::empty()
|
||||
}
|
||||
"help" => ExecResult::lines(help()),
|
||||
"kv" => ExecResult::lines(self.dump_kv()),
|
||||
"reload" => {
|
||||
@@ -194,6 +262,21 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn-Fußpunkt (Engine-Koords) aus `info_player_start`; sonst ein Default.
|
||||
fn player_spawn(map: &Map) -> [f32; 3] {
|
||||
for e in &map.entities {
|
||||
if e.classname() == Some("info_player_start") {
|
||||
if let Some(origin) = e.props.get("origin") {
|
||||
let v: Vec<f32> = origin.split_whitespace().filter_map(|s| s.parse().ok()).collect();
|
||||
if let [x, y, z] = v[..] {
|
||||
return map::to_engine([x, y, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[0.0, 1.0, 0.0]
|
||||
}
|
||||
|
||||
fn help() -> Vec<String> {
|
||||
vec![
|
||||
" signal <s> Signal feuern oder Action direkt ausführen".into(),
|
||||
@@ -201,7 +284,8 @@ fn help() -> Vec<String> {
|
||||
" use <instance> Objekt-Interaktion simulieren — `use Mushroom.005`".into(),
|
||||
" feuert Signal `Mushroom` mit $self = Mushroom.005".into(),
|
||||
" kv KV-Store anzeigen".into(),
|
||||
" menu Menü öffnen/schließen".into(),
|
||||
" menu Menü öffnen / ins Spiel zurück".into(),
|
||||
" noclip zwischen First-Person und Debug-Flycam wechseln".into(),
|
||||
" reload signals.toml neu laden".into(),
|
||||
" quit beenden".into(),
|
||||
]
|
||||
@@ -239,19 +323,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_command_toggles_free_and_menu() {
|
||||
fn menu_command_opens_and_resumes_to_play() {
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::Free;
|
||||
s.exec("menu");
|
||||
assert!(matches!(s.mode, Mode::Menu));
|
||||
s.exec("menu");
|
||||
assert!(matches!(s.mode, Mode::Free));
|
||||
// Im Menü laufen Engine-Befehle weiter (gleiche Route wie Free).
|
||||
s.exec("menu");
|
||||
s.exec("menu"); // „Fortsetzen" → ins Spiel
|
||||
assert!(matches!(s.mode, Mode::Play));
|
||||
s.exec("menu"); // aus dem Spiel wieder ins Menü
|
||||
assert!(matches!(s.mode, Mode::Menu));
|
||||
// Im Menü laufen Engine-Befehle weiter (gleiche Route wie Play/Free).
|
||||
s.exec("signal set in_menu true");
|
||||
assert!(s.game.kv["in_menu"].coerce_to_bool().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noclip_toggles_play_and_free() {
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::Play;
|
||||
s.exec("noclip");
|
||||
assert!(matches!(s.mode, Mode::Free));
|
||||
s.exec("noclip");
|
||||
assert!(matches!(s.mode, Mode::Play));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_command_reports_and_stays_free() {
|
||||
let mut s = empty_session();
|
||||
|
||||
Reference in New Issue
Block a user