Audio: WAV + tinyaudio, Ambient-Emitter mit Distance ramps; artist.md
engine/wav.rs (Decoder) und render/audio.rs (Mixer, 8 SFX-Voices + Loop-Voice je Emitter) aus irl3d eingebettet; Emitter-Logik headless in engine/audio.rs. Empties mit sound/radius-Property senden positional, play_sound-Signale laufen als SFX über Session::take_sounds. Platzhalter-Sounds + Emitter in der Testszene; artist.md fasst die Content-Regeln für die Autorin zusammen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+47
-5
@@ -1,3 +1,4 @@
|
||||
use crate::engine::audio::Emitters;
|
||||
use crate::engine::collision::CollisionWorld;
|
||||
use crate::engine::game::{Action, Game, ModeTarget};
|
||||
use crate::engine::ink::StoryState;
|
||||
@@ -43,6 +44,13 @@ pub struct Session {
|
||||
/// Klick-Ziele und Betretens-Zonen aus den Blender-Modellen
|
||||
/// (`signal`-Property, siehe engine::trigger).
|
||||
triggers: Triggers,
|
||||
/// Ambient-Emitter aus den Blender-Modellen (`sound`-Property auf
|
||||
/// 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).
|
||||
pending_sounds: Vec<String>,
|
||||
signals_path: String,
|
||||
/// Aufgelaufene, noch nicht simulierte Zeit (s) für den Fixed-Timestep.
|
||||
sim_accum: f32,
|
||||
@@ -87,6 +95,8 @@ impl Session {
|
||||
player: Player::new([0.0, 1.0, 0.0]),
|
||||
collision: CollisionWorld::empty(),
|
||||
triggers: Triggers::new(),
|
||||
emitters: Emitters::new(),
|
||||
pending_sounds: Vec::new(),
|
||||
signals_path,
|
||||
sim_accum: 0.0,
|
||||
pending_jump: false,
|
||||
@@ -96,13 +106,14 @@ 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) 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).
|
||||
/// 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).
|
||||
pub fn load_props(&mut self, model: &model::Model) {
|
||||
self.collision.add_model(model);
|
||||
self.triggers.add_model(model);
|
||||
self.emitters.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);
|
||||
@@ -110,6 +121,25 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Die Ambient-Emitter in fester Reihenfolge — der Audio-Treiber legt
|
||||
/// beim Start je Emitter eine Loop-Voice an.
|
||||
pub fn emitters(&self) -> &[crate::engine::audio::Emitter] {
|
||||
self.emitters.list()
|
||||
}
|
||||
|
||||
/// 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<f32> {
|
||||
let p = self.player.pos;
|
||||
self.emitters.gains([p[0], p[1] + player::EYE_HEIGHT, p[2]])
|
||||
}
|
||||
|
||||
/// Aufgelaufene Einmal-SFX (`play_sound`-Actions) abholen — jeden Frame
|
||||
/// vom Fenster-Frontend gerufen, das sie an den Audio-Treiber gibt.
|
||||
pub fn take_sounds(&mut self) -> Vec<String> {
|
||||
std::mem::take(&mut self.pending_sounds)
|
||||
}
|
||||
|
||||
/// 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:
|
||||
@@ -328,7 +358,10 @@ impl Session {
|
||||
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}")),
|
||||
Action::PlaySound(n) => {
|
||||
out.push(format!("[action] play_sound {n}"));
|
||||
self.pending_sounds.push(n);
|
||||
}
|
||||
Action::SetMode(t) => {
|
||||
self.set_mode(t);
|
||||
out.push(format!("[action] mode {}", t.label()));
|
||||
@@ -561,6 +594,15 @@ mod tests {
|
||||
assert_eq!(s.pick([5.5, 0.5, 0.0], [0.0, 0.0, 1.0], INTERACT_REACH), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_sound_action_queues_for_frontend() {
|
||||
let mut s = empty_session();
|
||||
// Fall-through-Builtin über den Signal-Trichter, wie ein Ink-Tag.
|
||||
s.exec("signal play_sound klick");
|
||||
assert_eq!(s.take_sounds(), vec!["klick".to_string()]);
|
||||
assert!(s.take_sounds().is_empty(), "take leert die Queue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_empty_places_player() {
|
||||
let mut s = empty_session();
|
||||
|
||||
Reference in New Issue
Block a user