Skip to main content

audio_code/
shortcuts.rs

1//! Shortcuts on [`Context`] for the things almost every game does.
2
3use glam::Vec3;
4
5use crate::audio::Sound;
6use crate::engine::Context;
7use crate::input::Key;
8use crate::physics::{BodyId, PhysicsEvent};
9
10/// Storage key under which [`Context::set_language`] remembers the player's language.
11pub(crate) const LANGUAGE_KEY: &str = "language";
12
13/// A sound that follows a physics body.
14#[derive(Debug)]
15pub(crate) struct Attached {
16    body: BodyId,
17    sound: Sound,
18    last: Vec3,
19}
20
21impl Context {
22    /// Speaks text, interrupting current speech. Same as `ctx.speech.speak(text)`.
23    pub fn speak(&mut self, text: impl AsRef<str>) {
24        self.speech.speak(text);
25    }
26
27    /// The text for `key` in the current language. Same as `ctx.lang.get(key)`.
28    ///
29    /// ```no_run
30    /// # use audio_code::prelude::*;
31    /// # fn f(ctx: &mut Context) {
32    /// ctx.speak(ctx.tr("intro"));
33    /// # }
34    /// ```
35    pub fn tr(&self, key: &str) -> String {
36        self.lang.get(key)
37    }
38
39    /// The text for `key` with `{name}` placeholders filled in. Same as
40    /// `ctx.lang.format(key, args)`.
41    ///
42    /// ```no_run
43    /// # use audio_code::prelude::*;
44    /// # fn f(ctx: &mut Context, score: u32) {
45    /// // In lang/en.txt: score = You have {points} points.
46    /// ctx.speak(ctx.tr_with("score", &[("points", &score)]));
47    /// # }
48    /// ```
49    pub fn tr_with(&self, key: &str, args: &[(&str, &dyn std::fmt::Display)]) -> String {
50        self.lang.format(key, args)
51    }
52
53    /// Switches the language and remembers it in [`Context::storage`], so the game starts in
54    /// it next time. Returns `false` if there are no texts for it.
55    ///
56    /// ```no_run
57    /// # use audio_code::prelude::*;
58    /// # fn f(ctx: &mut Context) {
59    /// let mut menu = Menu::new("language", "Language");
60    /// for code in ctx.lang.languages() {
61    ///     menu = menu.button(code.clone(), ctx.lang.name(&code));
62    /// }
63    /// ctx.ui.open(menu);
64    /// // ...later, in update():
65    /// if let Some(code) = ctx.ui.activated("language").map(String::from) {
66    ///     ctx.set_language(&code);
67    ///     ctx.ui.close();
68    /// }
69    /// # }
70    /// ```
71    pub fn set_language(&mut self, code: &str) -> bool {
72        if !self.lang.set(code) {
73            return false;
74        }
75        let current = self.lang.current().to_string();
76        self.storage.set(LANGUAGE_KEY, current);
77        true
78    }
79
80    /// Where `target` is as seen from `from`, in the current language, e.g.
81    /// `"3 o'clock, 12 meters"` or `"3 Uhr, 12 Meter"`. Like
82    /// [`World::describe`](crate::physics::World::describe), which is always English.
83    pub fn describe(&self, from: BodyId, target: BodyId) -> String {
84        let world = &self.physics;
85        self.lang.describe_position(world.position(from), world.facing(from), world.position(target))
86    }
87
88    /// Plays a sound once. Same as `ctx.audio.play(name)`.
89    pub fn play(&mut self, name: &str) -> Sound {
90        self.audio.play(name)
91    }
92
93    /// Plays a sound once at a position. Same as `ctx.audio.play_at(name, position)`.
94    pub fn play_at(&mut self, name: &str, position: Vec3) -> Sound {
95        self.audio.play_at(name, position)
96    }
97
98    /// Whether a key went down this frame. Same as `ctx.input.pressed(key)`.
99    pub fn pressed(&self, key: Key) -> bool {
100        self.input.pressed(key)
101    }
102
103    /// Whether a key is held. Same as `ctx.input.held(key)`.
104    pub fn held(&self, key: Key) -> bool {
105        self.input.held(key)
106    }
107
108    /// Returns `true` the first time it is called with `name`, and `false` afterwards.
109    /// Handy for one-time setup in closure games and for "say this only once" hints.
110    ///
111    /// ```no_run
112    /// # use audio_code::prelude::*;
113    /// # fn f(ctx: &mut Context) {
114    /// if ctx.once("intro") {
115    ///     ctx.speak("Welcome! Press F1 for help.");
116    /// }
117    /// # }
118    /// ```
119    pub fn once(&mut self, name: &str) -> bool {
120        self.once.insert(name.to_string())
121    }
122
123    /// Makes the listener (the player's ears) follow a body every frame: its position, its
124    /// [`facing`](crate::physics::Body::facing) and its [`pitch`](crate::physics::Body::pitch). After this you never call
125    /// `ctx.audio.set_listener` yourself.
126    pub fn follow(&mut self, body: BodyId) {
127        self.follow = Some(body);
128        self.sync_world();
129    }
130
131    /// Stops following a body with the listener.
132    pub fn unfollow(&mut self) {
133        self.follow = None;
134    }
135
136    /// The body the listener follows, if any.
137    pub fn followed(&self) -> Option<BodyId> {
138        self.follow
139    }
140
141    /// Plays a sound once at a body; the sound moves with the body.
142    pub fn play_on(&mut self, body: BodyId, name: &str) -> Sound {
143        let sound = self.audio.play_at(name, self.physics.position(body));
144        self.attach(body, sound.clone());
145        sound
146    }
147
148    /// Plays a looping sound at a body. It moves with the body and stops automatically when
149    /// the body is removed. Perfect for enemies, engines, coins and beacons:
150    ///
151    /// ```no_run
152    /// # use audio_code::prelude::*;
153    /// # fn f(ctx: &mut Context) {
154    /// let enemy = ctx.physics.add(Body::kinematic(vec3(5.0, 5.0, 0.0)).with_tag("enemy"));
155    /// ctx.loop_on(enemy, "growl");
156    /// # }
157    /// ```
158    pub fn loop_on(&mut self, body: BodyId, name: &str) -> Sound {
159        let sound = self.audio.play_looped_at(name, self.physics.position(body));
160        self.attach(body, sound.clone());
161        sound
162    }
163
164    /// Makes any positioned sound follow a body, e.g. one made with the sound builder:
165    /// `let s = ctx.audio.sound("hum").volume(0.3).looped().at(pos).play(); ctx.attach(body, s);`
166    pub fn attach(&mut self, body: BodyId, sound: Sound) {
167        let position = self.physics.position(body);
168        sound.set_position(position);
169        self.attached.push(Attached { body, sound, last: position });
170    }
171
172    /// Stops every sound attached to a body.
173    pub fn stop_sounds_on(&mut self, body: BodyId) {
174        self.attached.retain(|a| {
175            if a.body == body {
176                a.sound.stop();
177                false
178            } else {
179                true
180            }
181        });
182    }
183
184    /// Drives a vehicle with the keyboard: Up or W is the gas pedal, Down or S brakes and,
185    /// once the vehicle stands still, reverses, Left / Right or A / D steer, Space is the
186    /// handbrake. Call it every frame. Does nothing for bodies without a vehicle. While a
187    /// menu is open all pedals are released.
188    ///
189    /// ```no_run
190    /// # use audio_code::prelude::*;
191    /// # fn f(ctx: &mut Context) {
192    /// let car = ctx.physics.add(Body::car(Vec3::ZERO));
193    /// ctx.follow(car);
194    /// // in update():
195    /// ctx.drive(car);
196    /// # }
197    /// ```
198    pub fn drive(&mut self, vehicle: BodyId) {
199        let input = &self.input;
200        let throttle = (input.held(Key::Up) || input.held(Key::W)) as u8 as f32;
201        let brake = (input.held(Key::Down) || input.held(Key::S)) as u8 as f32;
202        let right = (input.held(Key::Right) || input.held(Key::D)) as i32;
203        let left = (input.held(Key::Left) || input.held(Key::A)) as i32;
204        let handbrake = input.held(Key::Space);
205        if let Some(v) = self.physics.vehicle_mut(vehicle) {
206            v.throttle = throttle;
207            v.brake = brake;
208            v.steer = (right - left) as f32;
209            v.handbrake = handbrake;
210        }
211    }
212
213    /// Honks: plays the vehicle's horn sound at the vehicle. Returns a silent handle if it
214    /// has none.
215    pub fn horn(&mut self, vehicle: BodyId) -> Sound {
216        match self.physics.vehicle(vehicle).and_then(|v| v.horn_sound.clone()) {
217            Some(name) => self.play_on(vehicle, &name),
218            None => Sound::none(),
219        }
220    }
221
222    /// Starts the engine of a vehicle and plays its start sound. Returns `false` if the
223    /// body is no vehicle, the engine already runs or the tank is empty.
224    pub fn start_engine(&mut self, vehicle: BodyId) -> bool {
225        let Some(v) = self.physics.vehicle_mut(vehicle) else {
226            return false;
227        };
228        if v.is_engine_on() || !v.start_engine() {
229            return false;
230        }
231        if let Some(name) = v.start_sound.clone() {
232            self.play_on(vehicle, &name);
233        }
234        true
235    }
236
237    /// Stops the engine of a vehicle. Its engine sound fades out and it rolls out.
238    pub fn stop_engine(&mut self, vehicle: BodyId) {
239        if let Some(v) = self.physics.vehicle_mut(vehicle) {
240            v.stop_engine();
241        }
242    }
243
244    /// Keeps the engine loops of vehicles playing, and plays shift and crash sounds.
245    /// Called by the engine once per frame.
246    pub(crate) fn sync_vehicles(&mut self) {
247        let (physics, audio) = (&self.physics, &mut self.audio);
248        // Engine loops: start while the engine runs, follow rpm and throttle, stop otherwise.
249        self.engine_sounds.retain(|(id, sound)| {
250            let running = physics.vehicle(*id).is_some_and(|v| v.is_engine_on() && v.engine_sound.is_some());
251            if !running || !sound.is_playing() {
252                sound.fade_out(0.3);
253            }
254            running && sound.is_playing()
255        });
256        for (id, body) in physics.bodies() {
257            let Some(v) = &body.vehicle else {
258                continue;
259            };
260            let Some(name) = &v.engine_sound else {
261                continue;
262            };
263            if !v.is_engine_on() {
264                continue;
265            }
266            let (pitch, volume) = (v.engine_pitch(), 0.6 + 0.4 * v.throttle.clamp(0.0, 1.0));
267            match self.engine_sounds.iter().find(|(e, _)| *e == id) {
268                Some((_, sound)) => {
269                    sound.set_position(body.position);
270                    sound.set_pitch(pitch);
271                    sound.set_volume(volume);
272                }
273                None => {
274                    let sound = audio.sound(name).looped().at(body.position).pitch(pitch).volume(volume).play();
275                    self.engine_sounds.push((id, sound));
276                }
277            }
278        }
279        for event in physics.events() {
280            let (vehicle, name, volume) = match *event {
281                PhysicsEvent::Crashed { vehicle, speed, .. } => {
282                    (vehicle, physics.vehicle(vehicle).and_then(|v| v.crash_sound.as_deref()), (speed / 15.0).min(1.0))
283                }
284                PhysicsEvent::GearChanged { vehicle, gear } if gear != 0 => {
285                    (vehicle, physics.vehicle(vehicle).and_then(|v| v.shift_sound.as_deref()), 1.0)
286                }
287                _ => continue,
288            };
289            if let Some(name) = name {
290                audio.sound(name).at(physics.position(vehicle)).volume(volume).play();
291            }
292        }
293    }
294
295    /// Moves the listener and attached sounds to their bodies. Called by the engine every frame.
296    pub(crate) fn sync_world(&mut self) {
297        if let Some(id) = self.follow {
298            match self.physics.get(id) {
299                Some(body) => {
300                    let (position, facing, pitch) = (body.position, body.facing, body.pitch);
301                    if position != self.audio.listener_position()
302                        || facing != self.audio.listener_yaw()
303                        || pitch != self.audio.listener_pitch()
304                    {
305                        self.audio.set_listener_3d(position, facing, pitch);
306                    }
307                }
308                None => self.follow = None,
309            }
310        }
311        let physics = &self.physics;
312        self.attached.retain_mut(|a| {
313            if !a.sound.is_playing() {
314                return false;
315            }
316            match physics.get(a.body) {
317                Some(body) => {
318                    if body.position != a.last {
319                        a.sound.set_position(body.position);
320                        a.last = body.position;
321                    }
322                    true
323                }
324                None => {
325                    a.sound.stop();
326                    false
327                }
328            }
329        });
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use crate::prelude::*;
336
337    #[test]
338    fn follow_and_attach() {
339        let mut runner = Engine::new("t").headless().build_fn(|_| {});
340        let ctx = &mut runner.ctx;
341        let player = ctx.physics.add(Body::kinematic(vec3(1.0, 2.0, 0.0)).with_facing(90.0));
342        ctx.follow(player);
343        assert_eq!(ctx.audio.listener_position(), vec3(1.0, 2.0, 0.0));
344        assert_eq!(ctx.audio.listener_yaw(), 90.0);
345        ctx.physics.move_forward(player, 3.0);
346        ctx.sync_world();
347        assert!((ctx.audio.listener_position().x - 4.0).abs() < 1e-4);
348
349        assert!(ctx.once("a"));
350        assert!(!ctx.once("a"));
351    }
352}