refactor
This commit is contained in:
@@ -10,17 +10,13 @@
|
||||
//! den Mittelpunkt eine Haaresbreite vor der Fläche, damit der Folgeframe nicht
|
||||
//! sofort wieder im Kontakt steckt.
|
||||
//!
|
||||
//! Gefüllt wird die Welt aus Blender-Modellen ([`add_model`]): jedes Objekt
|
||||
//! mit `collide`-Property wird zur **konvexen Hülle** seiner Vertices
|
||||
//! ([`hull_planes`], inkrementeller Quickhull) — das Mesh selbst darf also
|
||||
//! konkav sein, kollidiert wird gegen seine Hülle. Ergänzt um die sechs
|
||||
//! Gefüllt wird die Welt aus Blender-Modellen ([`CollisionWorld::add_model`]):
|
||||
//! jedes Objekt mit `collide`-Property wird zur **konvexen Hülle** seiner
|
||||
//! Vertices ([`hull_planes`], inkrementeller Quickhull) — das Mesh selbst darf
|
||||
//! also konkav sein, kollidiert wird gegen seine Hülle. Ergänzt um die sechs
|
||||
//! achsparallelen AABB-Ebenen (Quake-„Bevel planes"): die `|n|·half`-
|
||||
//! Aufblasung ist nur eine konservative Näherung der Minkowski-Summe; die
|
||||
//! Axial-Ebenen ziehen sie an Kanten schräger Hüllen wieder stramm.
|
||||
//!
|
||||
//! Headless wie der Rest von `engine`: hängt nur an `model` (Quelle der
|
||||
//! Ebenen). Der Player ruft `trace` in `player::step`; gebaut wird die
|
||||
//! Welt einmal vom Frontend aus den Modellen.
|
||||
|
||||
use crate::engine::model::{Model, Object};
|
||||
|
||||
|
||||
+8
-12
@@ -5,11 +5,9 @@
|
||||
//! `ActionCtx` ist der schmale `&mut`-View; er wird über `Game::action_ctx()`
|
||||
//! konstruiert und durch die Dispatch-Pipeline gereicht.
|
||||
//!
|
||||
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) noch nicht
|
||||
//! existiert oder den Kern nichts angeht, produzieren [`Action`]-Werte
|
||||
//! statt direkt zu wirken. Frontends konsumieren die Queue: die CLI druckt
|
||||
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
||||
//! testbar und kennt keines der Subsysteme.
|
||||
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) den Kern nichts
|
||||
//! angeht, produzieren [`Action`]-Werte statt direkt zu wirken. Frontends
|
||||
//! konsumieren die Queue: die CLI druckt sie, das Fenster führt sie aus.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -28,20 +26,18 @@ pub type Signals = HashMap<String, Vec<String>>;
|
||||
pub enum Action {
|
||||
/// `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
|
||||
/// wählbar — der entsteht nur aus dem Story-Ablauf.
|
||||
/// Anzeige-/Eingabemodus wechseln (Spiel, Noclip, Menü). Der Dialog-Modus
|
||||
/// ist bewusst nicht wählbar — er 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.
|
||||
/// Frontend-neutrales Ziel eines Moduswechsels: die nicht-Dialog-Varianten von
|
||||
/// `session::Mode`, ohne dass der Kern dessen Dialog-Daten kennen muss.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeTarget {
|
||||
/// First-Person-Spiel.
|
||||
Play,
|
||||
/// Noclip-Debug-Flycam.
|
||||
/// Noclip: freies Fliegen ohne Kollision.
|
||||
Free,
|
||||
/// Menü (pausiert die Welt, gibt die Maus frei).
|
||||
Menu,
|
||||
|
||||
+6
-13
@@ -1,26 +1,19 @@
|
||||
//! glTF-Binary-Loader (.glb, Blender-Export) → neutrales [`Model`].
|
||||
//!
|
||||
//! Der Aufrüst-Pfad gegenüber engine::obj: glTF trägt, was OBJ nicht kann —
|
||||
//! **Custom Properties** (Blender: Objekt-Eigenschaften, beim Export
|
||||
//! Die tragenden Kanäle: **Custom Properties** (in Blender beim Export
|
||||
//! „Include → Custom Properties" anhaken → landen in `node.extras`) und
|
||||
//! **Empties** (Nodes ohne Mesh) als Entity-Marker für Spawn/Trigger/…
|
||||
//! Beide kommen als `props`/`empties` im Modell an; Bedeutung geben die
|
||||
//! Konsumenten.
|
||||
//! **Empties** (Nodes ohne Mesh) als Entity-Marker. Beide kommen als
|
||||
//! `props`/`empties` im Modell an; Bedeutung geben die Konsumenten.
|
||||
//!
|
||||
//! Subset: GLB-Container (JSON- + BIN-Chunk; reine `.gltf` mit externen
|
||||
//! Buffern sind außerhalb — in Blender „glTF Binary (.glb)" exportieren),
|
||||
//! Dreiecks-Primitives (Mode 4, Default) mit `POSITION`/`TEXCOORD_0`
|
||||
//! (float) und optionalen Indizes (u8/u16/u32). Node-Transforms (Matrix
|
||||
//! oder T·R·S, samt Hierarchie) werden in die Vertices eingebacken —
|
||||
//! Welt-Raum wie beim OBJ-Pfad. Material-*Name* = Texturname (derselbe
|
||||
//! Kontrakt wie `usemtl`); glTF-eigene Texturen/PBR werden ignoriert.
|
||||
//! oder T·R·S, samt Hierarchie) werden in die Vertices eingebacken.
|
||||
//! Material-*Name* = Texturname; glTF-eigene Texturen/PBR bleiben ungenutzt.
|
||||
//!
|
||||
//! Achsen: glTF ist per Spezifikation Y-up/−Z-forward — unser System,
|
||||
//! Blenders Exporter konvertiert selbst. Maßstab 1:1.
|
||||
//!
|
||||
//! JSON via `serde_json`: steckt über bladeink ohnehin im Dependency-Baum,
|
||||
//! ein handgerollter Parser wäre Redundanz ohne Dependency-Gewinn (anders
|
||||
//! als beim trivialen signals-TOML-Subset).
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -125,7 +118,7 @@ fn append_mesh(
|
||||
None => vec![[0.0, 0.0]; positions.len()],
|
||||
};
|
||||
|
||||
// Material-Name → geteilte Namensliste (wie usemtl); ohne Material "".
|
||||
// Material-Name → geteilte Namensliste; ohne Material "".
|
||||
let mat_name = prim["material"].as_u64()
|
||||
.and_then(|m| doc["materials"][m as usize]["name"].as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
+8
-17
@@ -3,30 +3,21 @@
|
||||
//! Schicht-Regel: `engine` kennt keine Frontends. Nach außen kommuniziert
|
||||
//! der Kern nur über Rückgabewerte (`StoryState`, Tags) und die
|
||||
//! [`game::Action`]-Queue — niemals über Aufrufe in Frontend-Module.
|
||||
//! Frontends (CLI-REPL jetzt, Renderer später) sind Geschwister-Module,
|
||||
//! die `engine` konsumieren; die Richtung kehrt sich nie um.
|
||||
//!
|
||||
//! Interner Abhängigkeitsgraph (azyklisch, Pfeil = „benutzt"):
|
||||
//! Interner Abhängigkeitsgraph (azyklisch halten, Pfeil = „benutzt"):
|
||||
//!
|
||||
//! kv ← ink ← game ← story_ctrl ← signals
|
||||
//! ↑ ↑
|
||||
//! kv assets
|
||||
//!
|
||||
//! Bitte azyklisch halten: neue Querverbindungen lieber über Rückgabewerte
|
||||
//! an den Aufrufer lösen (so wie story_ctrl Tags zurückgibt, statt selbst
|
||||
//! signals::dispatch zu rufen).
|
||||
//! Neue Querverbindungen lieber über Rückgabewerte an den Aufrufer lösen —
|
||||
//! so wie story_ctrl Tags zurückgibt, statt selbst `signals::dispatch` zu
|
||||
//! rufen.
|
||||
//!
|
||||
//! `gltf`, `tga` und `wav` sind reine Decoder (Bytes → owned Daten, hängen
|
||||
//! an nichts) — die geteilte Heimat für Format-Dekodierung, die jedes
|
||||
//! Frontend per Pull konsumiert. `gltf` ist DER 3D-Pfad (Blender-first:
|
||||
//! Sichtgeometrie, Collider/Trigger via Custom Properties, Empties aus
|
||||
//! einem Export) und produziert das neutrale `model::Model`. `audio` ist
|
||||
//! die headless Emitter-Logik (Distance ramps) zum Frontend-Treiber
|
||||
//! render::audio.
|
||||
//!
|
||||
//! `player` ist die First-Person-Physik, aus der der Renderer seine View
|
||||
//! ableitet; `collision` (hängt an `model`) liefert ihr die Welt für den
|
||||
//! Swept-AABB-Trace in `player::step`. Beide bleiben headless.
|
||||
//! `gltf`, `tga` und `wav` sind reine Decoder (Bytes → owned Daten); `gltf`
|
||||
//! ist DER 3D-Pfad und produziert das neutrale `model::Model`. `audio` ist
|
||||
//! die Emitter-Logik zum Frontend-Treiber render::audio, `player` die
|
||||
//! First-Person-Physik auf der Welt aus `collision`.
|
||||
|
||||
pub mod assets;
|
||||
pub mod audio;
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Blender-Custom-Properties eines Objekts/Empties (glTF-`extras`),
|
||||
/// Werte zu Strings vereinheitlicht. OBJ kann keine tragen → leer.
|
||||
/// Werte zu Strings vereinheitlicht.
|
||||
pub type Props = HashMap<String, String>;
|
||||
|
||||
pub struct Model {
|
||||
|
||||
+69
-55
@@ -1,24 +1,15 @@
|
||||
//! First-Person-Player: headless Bewegungsphysik (Stufe 1).
|
||||
//! First-Person-Player: headless Bewegungsphysik.
|
||||
//!
|
||||
//! Bewusst frontend- und GPU-frei — wie der Rest von `engine`. Der Renderer
|
||||
//! *leitet* seine View aus dem Player ab (`Mat4::view(pos + eye, yaw, pitch)`),
|
||||
//! der Player kennt den Renderer nie. Damit bleibt die Physik im Terminal
|
||||
//! testbar, und die Schichtrichtung (`engine ← render`) kehrt sich nicht um.
|
||||
//! `step` sweept die Spieler-AABB gegen die [`CollisionWorld`] und gleitet an
|
||||
//! Treffern entlang (move-and-slide). Getract wird der Box-Mittelpunkt
|
||||
//! (Fußpunkt + halbe Höhe), danach geht es zurück auf den Fußpunkt.
|
||||
//! Bodenkontakt fällt aus nach oben zeigenden Trefferflächen ab. `fly` ist der
|
||||
//! Noclip-Gegenpart ohne Welt.
|
||||
//!
|
||||
//! Keine neuen Dependencies: `step` braucht nur `sin_cos` und Komponenten-
|
||||
//! Arithmetik. Vektor-Helfer werden nicht geteilt (collision.rs hält seine
|
||||
//! eigenen privat) — ein gemeinsames Modul lohnt erst, wenn ein dritter
|
||||
//! Nutzer echte dot/cross/normalize braucht.
|
||||
//!
|
||||
//! Collision (Stufe 3): `step` sweept die Spieler-AABB gegen die
|
||||
//! [`CollisionWorld`] (Brushes) und gleitet an Treffern entlang (move-and-slide).
|
||||
//! Getract wird der Box-Mittelpunkt (Fußpunkt + halbe Höhe), danach geht es
|
||||
//! zurück auf den Fußpunkt. Bodenkontakt fällt aus nach oben zeigenden
|
||||
//! Trefferflächen ab.
|
||||
//!
|
||||
//! Konvention wie `render::math::view`/`camera.rs`: yaw=0 blickt −Z, positiver
|
||||
//! Yaw dreht nach links, positiver Pitch hebt den Blick. Die Bewegung ist
|
||||
//! horizontal aus dem Yaw (ohne Pitch) — Hochschauen lässt einen nicht abheben.
|
||||
//! Der Renderer leitet seine View hier ab (`Mat4::view(eye, yaw, pitch)`), der
|
||||
//! Player kennt ihn nie. Konvention: yaw=0 blickt −Z, positiver Yaw dreht nach
|
||||
//! links, positiver Pitch hebt den Blick. Gelaufen wird horizontal aus dem Yaw
|
||||
//! — Hochschauen hebt nicht ab, Fliegen dagegen folgt dem vollen Blick.
|
||||
|
||||
use crate::engine::collision::CollisionWorld;
|
||||
|
||||
@@ -26,13 +17,14 @@ use crate::engine::collision::CollisionWorld;
|
||||
/// `pos + [0, EYE_HEIGHT, 0]`. 1 Engine-Unit = 1 m (= 1 Blender-Meter).
|
||||
pub const EYE_HEIGHT: f32 = 1.6;
|
||||
|
||||
/// Fallbeschleunigung (units/s²). Über realem g (9.81) für knackiges
|
||||
/// Spielgefühl — wie die meisten Shooter. Tuning-Wert.
|
||||
/// Fallbeschleunigung (units/s²). Über realem g für knackiges Spielgefühl.
|
||||
const GRAVITY: f32 = 20.0;
|
||||
/// Laufgeschwindigkeit (units/s). 1 Engine-Unit = 1 m (= 1 Blender-Meter).
|
||||
const WALK_SPEED: f32 = 5.0;
|
||||
/// Absprunggeschwindigkeit (units/s) — bestimmt die Sprunghöhe. Tuning-Wert.
|
||||
/// Absprunggeschwindigkeit (units/s) — bestimmt die Sprunghöhe.
|
||||
const JUMP_SPEED: f32 = 7.0;
|
||||
/// Flug-Geschwindigkeit im Noclip (units/s).
|
||||
const FLY_SPEED: f32 = 4.0;
|
||||
/// Knapp unter 90°: hält den Blick aus der Senkrechten (wie `camera.rs`).
|
||||
const PITCH_LIMIT: f32 = 1.55;
|
||||
/// Halbmaße der Spieler-AABB (units): 0.6 m breit/tief, 1.8 m hoch.
|
||||
@@ -81,43 +73,52 @@ impl Player {
|
||||
[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.
|
||||
/// Blickrichtung inkl. Pitch — dieselbe Formel wie `math::view`. Trägt den
|
||||
/// Point-and-Click-Strahl aus der Bildmitte und das Vorwärts im Noclip.
|
||||
pub fn look_dir(&self) -> [f32; 3] {
|
||||
let (sy, cy) = self.yaw.sin_cos();
|
||||
let (sp, cp) = self.pitch.sin_cos();
|
||||
[-sy * cp, sp, -cy * cp]
|
||||
}
|
||||
|
||||
/// Horizontales Rechts (ohne Pitch) — Strafen kippt nie mit dem Blick.
|
||||
pub fn right_dir(&self) -> [f32; 3] {
|
||||
let (sy, cy) = self.yaw.sin_cos();
|
||||
[cy, 0.0, -sy]
|
||||
}
|
||||
|
||||
/// Noclip: frei durch die Welt, ohne Kollision und ohne Gravitation.
|
||||
/// Vorwärts folgt dem vollen Blick, `up` ist Welt-Y (Auf-/Absteigen bleibt
|
||||
/// unabhängig vom Blickwinkel).
|
||||
pub fn fly(&mut self, fwd: f32, right: f32, up: f32, dt: f32) {
|
||||
let (f, r) = (self.look_dir(), self.right_dir());
|
||||
for i in 0..3 {
|
||||
self.pos[i] += (f[i] * fwd + r[i] * right) * FLY_SPEED * dt;
|
||||
}
|
||||
self.pos[1] += up * FLY_SPEED * dt;
|
||||
self.vel = [0.0; 3];
|
||||
self.grounded = false;
|
||||
}
|
||||
|
||||
/// Einen Physikschritt integrieren. `world` ist die Brush-Kollisionswelt,
|
||||
/// `fwd`/`right` Tastenachsen in [-1, 1], `jump` ein Flankensignal (true =
|
||||
/// Sprungtaste diesen Frame), `dt` die Frame-Zeit in Sekunden.
|
||||
pub fn step(&mut self, world: &CollisionWorld, fwd: f32, right: f32, jump: bool, dt: f32) {
|
||||
let was_grounded = self.grounded;
|
||||
|
||||
// Horizontale Basis aus dem Yaw (ohne Pitch): forward = -Z bei yaw=0,
|
||||
// right = +X bei yaw=0 — exakt wie `camera::forward`/`right` flach.
|
||||
// Wunschgeschwindigkeit sofort setzen — ohne Beschleunigung/Reibung,
|
||||
// mit voller Luftkontrolle. Vorwärts ist der flache Blick, damit
|
||||
// Hochschauen nicht abhebt.
|
||||
let (sy, cy) = self.yaw.sin_cos();
|
||||
let f = [-sy, -cy]; // (x, z)
|
||||
let r = [ cy, -sy]; // (x, z)
|
||||
let r = self.right_dir();
|
||||
self.vel[0] = (-sy * fwd + r[0] * right) * WALK_SPEED;
|
||||
self.vel[2] = (-cy * fwd + r[2] * right) * WALK_SPEED;
|
||||
|
||||
// Wunschgeschwindigkeit sofort setzen (ohne Beschleunigung/Reibung —
|
||||
// das ist Feintuning für später). Volle Luftkontrolle, ebenfalls vorerst.
|
||||
self.vel[0] = (f[0] * fwd + r[0] * right) * WALK_SPEED;
|
||||
self.vel[2] = (f[1] * fwd + r[1] * right) * WALK_SPEED;
|
||||
|
||||
// Sprung nur vom Boden (Vorframe-`grounded`) — kein Doppelsprung.
|
||||
//
|
||||
// Gravitation wirkt *nur in der Luft*. Am Boden wird die Vertikal-
|
||||
// geschwindigkeit stattdessen genullt, denn `slide` projiziert sie auf
|
||||
// die Trefferebene und lässt auf einer Schräge den hangparallelen Rest
|
||||
// stehen — Hangabtrieb, der sich über die Frames aufschaukelt und den
|
||||
// Spieler auch sanfte Rampen hinabkriechen lässt. Ohne vertikale
|
||||
// Geschwindigkeit gibt es nichts zu projizieren, er bleibt stehen; ans
|
||||
// Gefälle heftet ihn der Ground-Snap unten. Steiler als
|
||||
// [`GROUND_NORMAL_Y`] wird `grounded` gar nicht erst gesetzt, dort
|
||||
// greift der Luft-Zweig und der Spieler rutscht wie gewünscht ab.
|
||||
// Sprung nur vom Boden — kein Doppelsprung. Gravitation wirkt nur in
|
||||
// der Luft: am Boden bliebe sonst nach der Slide-Projektion ein
|
||||
// hangparalleler Rest stehen, der sich über die Frames zum Hangabtrieb
|
||||
// aufschaukelt. Steiler als [`GROUND_NORMAL_Y`] ist `grounded` ohnehin
|
||||
// nicht gesetzt — dort greift der Luft-Zweig und man rutscht ab.
|
||||
let jumped = jump && was_grounded;
|
||||
if jumped {
|
||||
self.vel[1] = JUMP_SPEED;
|
||||
@@ -130,11 +131,10 @@ impl Player {
|
||||
|
||||
self.move_and_slide(world, dt);
|
||||
|
||||
// Ground-Snap: war der Spieler eben noch am Boden, ist nicht gesprungen
|
||||
// und steht nach dem Zug knapp in der Luft (abwärts-Slope, Stufe runter,
|
||||
// Brush-Naht), ziehen wir ihn auf den Boden darunter — so klebt er über
|
||||
// Nähte hinweg, statt kurz luftig darüber zu schweben. Nicht beim
|
||||
// Aufsteigen (vel.y > 0), damit Rampen-Absprünge erhalten bleiben.
|
||||
// Ground-Snap: stand er eben noch am Boden und schwebt nach dem Zug
|
||||
// knapp darüber (abwärts-Slope, Stufe runter, Brush-Naht), ziehen wir
|
||||
// ihn hinunter. Nicht beim Aufsteigen — sonst gäbe es keine
|
||||
// Rampen-Absprünge mehr.
|
||||
if was_grounded && !jumped && !self.grounded && self.vel[1] <= 0.0 {
|
||||
self.snap_to_ground(world);
|
||||
}
|
||||
@@ -158,11 +158,9 @@ impl Player {
|
||||
///
|
||||
/// Zuerst ein flacher Slide. Steht der Spieler am Boden, wird zusätzlich
|
||||
/// der Step-up probiert: hoch um [`STEP_HEIGHT`], horizontal sliden, wieder
|
||||
/// runter auf Boden. Kommt das horizontal weiter als der flache Zug und
|
||||
/// endet auf begehbarem Boden, gewinnt es. Das hebt die flache Box-
|
||||
/// Unterkante über Brush-Nähte (Slope-Kanten, Stufen), an denen sie sonst
|
||||
/// als „Wand" verkantet — der Grund, warum man ohne Step-up weder auf eine
|
||||
/// Slope hinauf- noch (wegen der vertikalen Prismen-Seitenfläche) hinabkam.
|
||||
/// runter auf Boden. Kommt das horizontal weiter und endet auf begehbarem
|
||||
/// Boden, gewinnt es. Das hebt die flache Box-Unterkante über Brush-Nähte
|
||||
/// (Slope-Kanten, Stufen), an denen sie sonst als „Wand" verkantet.
|
||||
fn move_and_slide(&mut self, world: &CollisionWorld, dt: f32) {
|
||||
let half = HALF_EXTENTS;
|
||||
let was_grounded = self.grounded; // in step() vor dem Sprung gesetzt
|
||||
@@ -362,6 +360,22 @@ mod tests {
|
||||
assert!(p.pos[2].abs() < 1e-6, "kein Z-Anteil: {}", p.pos[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fly_ignores_gravity_and_geometry() {
|
||||
// Noclip kennt die Welt gar nicht: nach unten durch jeden Boden, und
|
||||
// vorwärts dem vollen Blick nach — ohne Gravitation.
|
||||
let mut p = Player::new([0.0, 2.0, 0.0]);
|
||||
p.fly(0.0, 0.0, -1.0, 1.0);
|
||||
assert!(p.pos[1] < -1.0, "sollte durch den Boden sinken: {}", p.pos[1]);
|
||||
assert_eq!(p.vel, [0.0; 3], "Flug trägt keine Geschwindigkeit");
|
||||
|
||||
p.pitch = std::f32::consts::FRAC_PI_2; // gerade nach oben schauen
|
||||
let y0 = p.pos[1];
|
||||
p.fly(1.0, 0.0, 0.0, 1.0);
|
||||
assert!(p.pos[1] > y0 + 1.0, "vorwärts folgt dem Blick: {y0} -> {}", p.pos[1]);
|
||||
assert!(!p.grounded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wall_blocks_horizontal_movement() {
|
||||
// Wand bei x=1 (Brush x∈[1,3]); Player läuft nach +X (yaw=-90°) dagegen.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Story-Lifecycle: Ink-Skript in den Story-Slot laden und schrittweise
|
||||
//! treiben. Frontend-agnostisch — anders als in irl3d, wo `StoryState`
|
||||
//! direkt in UI-Panels gegossen wurde, konsumiert hier der Aufrufer
|
||||
//! (CLI-REPL jetzt, Panel-UI später) den Zustand selbst.
|
||||
//! treiben. Frontend-agnostisch — den `StoryState` gießt der Aufrufer selbst
|
||||
//! in seine Darstellung.
|
||||
//!
|
||||
//! Tags eines Schritts werden als `Vec<String>` zurückgegeben — der
|
||||
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
||||
|
||||
+1
-5
@@ -2,11 +2,7 @@
|
||||
//!
|
||||
//! Neutral: kennt weder GPU noch UI noch Fonts. Liefert *immer* RGBA8,
|
||||
//! Ursprung oben-links, vollständig konvertiert — der Aufrufer macht keine
|
||||
//! Nachbearbeitung. (Anders als irl3d, wo der Loader BGR zurückgab und die
|
||||
//! RGB-Umrechnung jedem Konsumenten einzeln überließ.) Konsumenten ziehen
|
||||
//! ein `Image` und legen es ab, wie sie es brauchen: Renderer → GPU-Textur,
|
||||
//! UI/Font → eigene Interpretation. Die UV-/V-Achsen-Konvention ist damit
|
||||
//! Sache des Konsumenten beim Sampling, nicht des Loaders.
|
||||
//! Nachbearbeitung, nur das Sampling (und damit die UV-Konvention) gehört ihm.
|
||||
//!
|
||||
//! Unterstützt True-Color unkomprimiert (Typ 2) und RLE (Typ 10) mit 24
|
||||
//! oder 32 bpp, sowie Graustufen (Typ 3/11, 8 bpp → zu RGBA expandiert) für
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
//! Minimaler WAV-Decoder (Blender-/DAW-Export) → Mono-Samples.
|
||||
//!
|
||||
//! Eingebettete Fassung des irl3d-Loaders (../irl3d/src/wav.rs), auf die
|
||||
//! WDS-Loader-Regeln gebracht: reiner Decoder (Bytes → owned Daten),
|
||||
//! `Result` statt Panic — Sounds sind Autorinnen-Content, ein kaputtes
|
||||
//! File wird gemeldet und bleibt still, nie fatal.
|
||||
//! Reiner Decoder (Bytes → owned Daten), `Result` statt Panic — Sounds sind
|
||||
//! Autorinnen-Content, ein kaputtes File wird gemeldet und bleibt still.
|
||||
//!
|
||||
//! Akzeptiert ausschließlich PCM (`format = 1`), 1 Kanal, 16 bit, 44100 Hz,
|
||||
//! little-endian — das feste Format des Mixers (render::audio). „Als WAV
|
||||
|
||||
Reference in New Issue
Block a user