Map-Loader entfernt: glTF ist das einzige 3D-Format

engine/map.rs + render/brush.rs raus, Welt kommt komplett aus .glb.
Spawn via Empty mit role=spawn statt info_player_start; testscene.glb
(Boden, Wände, Spawn) ersetzt test.map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 19:35:02 +02:00
parent 186ee9a817
commit 56d92cdb46
13 changed files with 94 additions and 898 deletions
+33 -31
View File
@@ -1,7 +1,6 @@
use crate::engine::collision::CollisionWorld;
use crate::engine::game::{Action, Game, ModeTarget};
use crate::engine::ink::StoryState;
use crate::engine::map::{self, Map};
use crate::engine::player::{self, Player};
use crate::engine::trigger::Triggers;
use crate::engine::{kv, model, signals, story_ctrl};
@@ -83,8 +82,8 @@ impl Session {
Self {
game,
mode: Mode::Menu,
// Default-Welt, bis `load_world` eine Map einspielt (die CLI
// braucht keine — sie tickt nie).
// Default-Spawn, bis ein Modell einen `role=spawn`-Empty
// mitbringt (die CLI braucht keine Welt — sie tickt nie).
player: Player::new([0.0, 1.0, 0.0]),
collision: CollisionWorld::empty(),
triggers: Triggers::new(),
@@ -94,23 +93,21 @@ impl Session {
}
}
/// 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.triggers = Triggers::new();
self.player = Player::new(player_spawn(map));
}
/// Sim-Seite eines Blender-Modells in die bestehende Welt einfügen —
/// nach `load_world` rufen, einmal pro Modell: Collider (`collide`-
/// Property → konvexe Hülle) und Trigger (`signal`-Property →
/// Klick-Ziele/Zonen). Die Sichtgeometrie desselben Modells ist Sache
/// des Renderers (render::props).
/// 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) 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).
pub fn load_props(&mut self, model: &model::Model) {
self.collision.add_model(model);
self.triggers.add_model(model);
for e in &model.empties {
if e.props.get(model::ROLE_PROP).map(String::as_str) == Some(model::ROLE_SPAWN) {
self.player = Player::new(e.pos);
}
}
}
/// Einen Simulationsschritt treiben — das kontinuierliche Gegenstück zu
@@ -362,20 +359,6 @@ 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")
&& 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(),
@@ -578,6 +561,25 @@ mod tests {
assert_eq!(s.pick([5.5, 0.5, 0.0], [0.0, 0.0, 1.0], INTERACT_REACH), None);
}
#[test]
fn spawn_empty_places_player() {
let mut s = empty_session();
let mut props = crate::engine::model::Props::new();
props.insert("role".into(), "spawn".into());
s.load_props(&crate::engine::model::Model {
objects: Vec::new(),
empties: vec![
crate::engine::model::Empty { name: "start".into(), pos: [3.0, 0.5, -2.0], props },
crate::engine::model::Empty {
name: "deko".into(), pos: [9.0, 9.0, 9.0],
props: crate::engine::model::Props::new(),
},
],
materials: Vec::new(),
});
assert_eq!(s.player.pos, [3.0, 0.5, -2.0], "role=spawn setzt den Fußpunkt");
}
#[test]
fn unknown_command_reports_and_stays_free() {
let mut s = empty_session();