diff --git a/artist.md b/artist.md index 8e31579..57df984 100644 --- a/artist.md +++ b/artist.md @@ -60,9 +60,9 @@ Start in der Konsole gemeldet (`[model]`, `[audio]`, `[tex]`, …). - Verdrahtet Signal → Aktionen. Signal-Namen: Objektname (Klick), Wert der `signal`-Property (Zonen/Klick-Ziele), oder frei (aus Ink). - Verben: `start_ink`, `set`, `inc`, `clear`, `debug_log`, `play_sound`, - `hide_object`, `mode play|free|menu`. + `mode play|free|menu`. - `$self` = auslösender Objektname (für generische Einträge wie - `hide_object $self`). + `set $self true`). - `[init]` feuert einmal beim Start — Ort für KV-Defaults. ## Testen diff --git a/assets/signals.toml b/assets/signals.toml index 00f604b..7e5c911 100644 --- a/assets/signals.toml +++ b/assets/signals.toml @@ -8,7 +8,6 @@ # set KV setzen (true/false/i32/f32/string) # inc [] KV-Integer +Delta (Default +1) # clear KV-Eintrag entfernen -# hide_object deferred (Frontend): Instance ausblenden # play_sound deferred (Frontend): SFX aus assets/sounds/{name}.wav # # Reserviertes Signal `[init]` feuert einmal vor der Game-Loop — @@ -26,7 +25,6 @@ actions = [ actions = [ "inc mushrooms_collected", "play_sound pickup", - "hide_object $self", ] [Person] diff --git a/src/engine/game.rs b/src/engine/game.rs index ea730a7..a6b0521 100644 --- a/src/engine/game.rs +++ b/src/engine/game.rs @@ -26,9 +26,7 @@ pub type Signals = HashMap>; /// geleert (konsumiert). #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { - /// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen. - HideObject(String), - /// WAV unter `assets/audio/` als SFX abspielen. + /// `assets/sounds/{name}.wav` als Einmal-SFX abspielen. PlaySound(String), /// Anzeige-/Eingabemodus wechseln (Spiel, Flycam, Menü). Vom Frontend /// auf seinen `Mode` gemappt; der Dialog-Modus ist hier bewusst nicht diff --git a/src/engine/gltf.rs b/src/engine/gltf.rs index 8c478b4..d2a2369 100644 --- a/src/engine/gltf.rs +++ b/src/engine/gltf.rs @@ -139,10 +139,6 @@ fn append_mesh( o.verts.push(transform(world, *p)); o.uvs.push(*uv); } - // glTF-UVs haben den Ursprung oben links — auf die Modell-Konvention - // (unten links, wie OBJ) spiegeln, damit der Render-Konsument beide - // Pfade gleich behandelt. - for uv in &mut o.uvs[base..] { uv[1] = 1.0 - uv[1]; } let idxs: Vec = match prim["indices"].as_u64() { Some(a) => read_indices(doc, bin, a as usize)?, @@ -410,10 +406,10 @@ mod tests { } #[test] - fn uv_origin_is_flipped_to_model_convention() { - // glTF-UV (1,1) (oben-links-Ursprung) → Modell-Konvention (1,0). + fn uvs_pass_through_unchanged() { + // glTF-UVs (Ursprung oben links) sind die Modell-Konvention — kein Flip. let m = parse_glb(&sample_glb()).unwrap(); - assert_eq!(m.objects[0].uvs[2], [1.0, 0.0]); + assert_eq!(m.objects[0].uvs[2], [1.0, 1.0]); } #[test] diff --git a/src/engine/model.rs b/src/engine/model.rs index 69123cb..a747bd5 100644 --- a/src/engine/model.rs +++ b/src/engine/model.rs @@ -39,8 +39,8 @@ pub struct Object { /// Positionen in Engine-Koords (Y-up, Blick −Z), Welt-Raum (Transforms /// vom Loader eingebacken), Maßstab 1:1 (Blender-Meter = Engine-Unit). pub verts: Vec<[f32; 3]>, - /// UVs parallel zu `verts`, V-Ursprung unten links (OBJ-/glTF-Rohform; - /// den Flip auf Bild-Konvention macht der Render-Konsument). + /// UVs parallel zu `verts`, V-Ursprung oben links — glTF- und zugleich + /// `tga::Image`-Konvention, direkt sampelbar ohne Flip. pub uvs: Vec<[f32; 2]>, pub tris: Vec<[usize; 3]>, /// Material-Index je Dreieck, parallel zu `tris` (→ `Model::materials`). diff --git a/src/engine/player.rs b/src/engine/player.rs index 82de847..e5d03ae 100644 --- a/src/engine/player.rs +++ b/src/engine/player.rs @@ -71,6 +71,12 @@ impl Player { self.pitch = (self.pitch - dy * sens).clamp(-PITCH_LIMIT, PITCH_LIMIT); } + /// Augenpunkt (Fußpunkt + [`EYE_HEIGHT`]) — Kamera- und Hörposition, + /// Startpunkt des Point-and-Click-Strahls. + pub fn eye(&self) -> [f32; 3] { + [self.pos[0], self.pos[1] + EYE_HEIGHT, self.pos[2]] + } + /// Blickrichtung inkl. Pitch — dieselbe Formel wie `camera::forward`/ /// `math::view` (yaw=0 → −Z, positiver Pitch hebt). Für den /// Point-and-Click-Strahl aus der Bildmitte. diff --git a/src/engine/signals.rs b/src/engine/signals.rs index 3f26e3d..dca154b 100644 --- a/src/engine/signals.rs +++ b/src/engine/signals.rs @@ -14,7 +14,6 @@ //! `set ` KV setzen (true/false/i32/f32/string) //! `inc []` KV-Integer inkrementieren (Default +1) //! `clear ` KV-Eintrag entfernen -//! `hide_object ` → Action::HideObject (deferred) //! `play_sound ` → Action::PlaySound (deferred): SFX aus //! `assets/sounds/{name}.wav` //! `mode ` → Action::SetMode (deferred): Anzeige-/ @@ -22,7 +21,7 @@ //! //! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den //! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action -//! wie `hide_object $self` für viele duplizierte Instances wirken, ohne pro +//! (z.B. `set $self true`) für viele duplizierte Instances wirken, ohne pro //! Instance einen Eintrag in signals.toml zu brauchen. //! //! Dispatch-Reihenfolge: erst Signal-Lookup; steht der Name in der Table, @@ -79,7 +78,7 @@ pub fn dispatch(signal: &str, ctx: &mut ActionCtx) { /// entscheidet, ob das eine Meldung wert ist — siehe `dispatch`). fn execute(cmd: &str, ctx: &mut ActionCtx) -> bool { // $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten, - // damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne + // damit Tokens wie `set $self true` einheitlich funktionieren. Ohne // instance_name (z.B. `init`-Signal) bleibt `$self` stehen — die Action // zielt dann ins Leere, was für deferred Actions ein No-Op beim // Konsumenten ist. @@ -98,7 +97,6 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) -> bool { "set" => kv::apply_set(args, ctx.kv), "inc" => kv::apply_inc(args, ctx.kv), "clear" => kv::apply_clear(args, ctx.kv), - "hide_object" => ctx.actions.push(Action::HideObject(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)); @@ -183,8 +181,7 @@ mod tests { let mut signals = Signals::new(); signals.insert("pickup".into(), vec![ "inc items".into(), - "play_sound pickup".into(), - "hide_object $self".into(), + "play_sound $self".into(), // $self-Substitution in deferred Action ]); let mut game = Game::new(signals); @@ -193,8 +190,7 @@ mod tests { assert_eq!(game.kv["items"].coerce_to_int().unwrap(), 1); assert_eq!(game.actions, vec![ - Action::PlaySound("pickup".into()), - Action::HideObject("Mushroom.005".into()), + Action::PlaySound("Mushroom.005".into()), ]); } diff --git a/src/engine/tga.rs b/src/engine/tga.rs index 19af8e9..51ae889 100644 --- a/src/engine/tga.rs +++ b/src/engine/tga.rs @@ -19,9 +19,9 @@ pub struct Image { pub rgba: Vec, } -pub fn load(path: &str) -> Image { - let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("tga load {path}: {e}")); - decode(&bytes).unwrap_or_else(|e| panic!("tga decode {path}: {e}")) +pub fn load(path: &str) -> Result { + let bytes = std::fs::read(path).map_err(|e| format!("tga load {path}: {e}"))?; + decode(&bytes).map_err(|e| format!("tga {path}: {e}")) } pub fn decode(d: &[u8]) -> Result { diff --git a/src/render/mod.rs b/src/render/mod.rs index 0a75a41..f782c36 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -101,7 +101,11 @@ pub fn run(mut session: Session) { // FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und // Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der // Luminanz. - let load_keyed = |p: &str| ui::key_luminance(&tga::load(&assets::path(p))); + // UI-Texturen sind Projekt-Assets (kein Autorinnen-Content) — fehlen sie, + // ist die Installation kaputt, da darf es scheitern. + let load_keyed = |p: &str| { + ui::key_luminance(&tga::load(&assets::path(p)).expect("UI-Textur (Projekt-Asset)")) + }; // Font-Atlanten zuerst keyen, daraus die Glyph-Breiten messen (Font), // dann dieselben Bilder in die GPU-Texturliste übernehmen. let ega_img = load_keyed("assets/textures/fonts/oldschool-ega-8x14.tga"); @@ -196,13 +200,17 @@ fn load_sound(name: &str) -> Option { } } +/// Welt-Textur laden. Autorinnen-Content: fehlt oder klemmt die Datei, wird +/// gemeldet und der Platzhalter genommen — der selbst ist Projekt-Asset und +/// darf hart scheitern. fn load_texture(name: &str) -> Image { - let path = assets::path(&format!("assets/textures/{name}.tga")); - if std::path::Path::new(&path).exists() { - tga::load(&path) - } else { - eprintln!("[tex] {name}: keine Datei unter {path} — Platzhalter"); - tga::load(&assets::path(&format!("assets/textures/{}.tga", props::FALLBACK_TEXTURE))) + match tga::load(&assets::path(&format!("assets/textures/{name}.tga"))) { + Ok(img) => img, + Err(e) => { + eprintln!("[tex] {e} — Platzhalter"); + tga::load(&assets::path(&format!("assets/textures/{}.tga", props::FALLBACK_TEXTURE))) + .expect("Platzhalter-Textur (Projekt-Asset)") + } } } @@ -487,8 +495,7 @@ impl App { // Klick-Ziele (`signal`-Property). Steuert den Interakt-Cursor und // löst beim Klick unten das `use` aus. let aim: Option = if play { - let p = self.session.player.pos; - let eye = [p[0], p[1] + player::EYE_HEIGHT, p[2]]; + let eye = self.session.player.eye(); let dir = self.session.player.look_dir(); self.session.pick(eye, dir, crate::session::INTERACT_REACH).map(str::to_string) } else { diff --git a/src/render/props.rs b/src/render/props.rs index 2f23c19..0758612 100644 --- a/src/render/props.rs +++ b/src/render/props.rs @@ -8,11 +8,10 @@ //! `assets/textures/{name}.tga` (macht der Aufrufer). Faces ohne Material //! (`""`) laufen unter [`FALLBACK_TEXTURE`]. //! -//! UVs: das Modell trägt den V-Ursprung unten links, unsere `tga::Image`s -//! oben links — V wird hier gespiegelt (Achsen-Konvention ist Sache des -//! Konsumenten, siehe engine::tga). Nur `visible`-Objekte werden -//! ausgegeben; Collider-/Zonen-Volumen sind Sache von engine::collision -//! und engine::trigger. +//! UVs kommen unverändert durch (Modell und `tga::Image` teilen den +//! Oben-links-Ursprung). Nur `visible`-Objekte werden ausgegeben; +//! Collider-/Zonen-Volumen sind Sache von engine::collision und +//! engine::trigger. use crate::engine::model::Model; use crate::render::scene::{Batch, Mesh, Vertex}; @@ -46,7 +45,7 @@ pub fn build(model: &Model, tex_names: &[String]) -> Mesh { if !o.visible { continue; } let base = verts.len() as u32; for (p, uv) in o.verts.iter().zip(&o.uvs) { - verts.push(Vertex { pos: *p, uv: [uv[0], 1.0 - uv[1]] }); // V-Flip + verts.push(Vertex { pos: *p, uv: *uv }); } for (t, &m) in o.tris.iter().zip(&o.tri_mats) { let name = resolve(&model.materials[m]); @@ -93,7 +92,7 @@ mod tests { } #[test] - fn builds_only_visible_objects_with_v_flip() { + fn builds_only_visible_objects() { let m = sample(); let names = texture_names(&m); assert_eq!(names, vec!["carpet".to_string()]); @@ -103,8 +102,8 @@ mod tests { assert_eq!(mesh.verts.len(), 3); assert_eq!(mesh.indices, vec![0, 1, 2]); assert_eq!(mesh.batches.len(), 1); - // uv (1,1) → V-Flip → (1,0). - assert_eq!(mesh.verts[2].uv, [1.0, 0.0]); + // UVs unverändert (geteilte Oben-links-Konvention). + assert_eq!(mesh.verts[2].uv, [1.0, 1.0]); } #[test] diff --git a/src/session.rs b/src/session.rs index ee6f611..8358223 100644 --- a/src/session.rs +++ b/src/session.rs @@ -130,8 +130,7 @@ impl Session { /// Aktuelle Emitter-Gains (parallel zu [`Session::emitters`]) für die /// Ohrposition des Spielers — jeden Frame vom Audio-Treiber gezogen. pub fn emitter_gains(&self) -> Vec { - let p = self.player.pos; - self.emitters.gains([p[0], p[1] + player::EYE_HEIGHT, p[2]]) + self.emitters.gains(self.player.eye()) } /// Aufgelaufene Einmal-SFX (`play_sound`-Actions) abholen — jeden Frame @@ -357,7 +356,6 @@ impl Session { 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}")); self.pending_sounds.push(n);