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}