Skip to main content

beacon/
beacon.rs

1//! Walk through a maze with 3D sound and find the beacons. Your best time is saved.
2//!
3//! * Up / Down: walk forward / backward
4//! * Left / Right: turn 45 degrees
5//! * C: where is the next beacon?  F: which way am I facing?
6//! * Escape: pause menu
7//!
8//! All sounds are generated with the synth, so no files are needed.
9//! Run with `cargo run --example beacon` and use headphones.
10
11use audio_code::prelude::*;
12
13const MAP: &str = "
14    ###########
15    #B....#..B#
16    #.###.#.#.#
17    #...#...#.#
18    ###.#####.#
19    #B........#
20    #.#######.#
21    #....P....#
22    ###########
23";
24
25struct Maze {
26    player: BodyId,
27    beacons: Vec<BodyId>,
28    clock: Timer,
29}
30
31impl Maze {
32    fn new(ctx: &mut Context) -> Self {
33        ctx.audio.register("step", Synth::noise().length(0.08).decay(60.0));
34        ctx.audio.register("wall", Synth::sine(90.0).length(0.15).decay(25.0).volume(0.8));
35        ctx.audio.register("beacon", Synth::sine(1320.0).length(0.08).then(Synth::silence(0.04)).then(Synth::sine(1320.0).length(0.08)).then(Synth::silence(0.8)));
36        ctx.audio.register("found", Synth::sine(440.0).sweep_to(1200.0).length(0.6).decay(4.0).volume(0.3));
37
38        // Every character is a 2 meter tile: walls, the player start and three beacons.
39        let level = ctx.physics.build_level(MAP, 2.0);
40        let player = ctx.physics.add(Body::kinematic(level.point('P').unwrap()).with_sphere(0.4).with_tag("player"));
41        ctx.follow(player); // the ears go wherever the player goes and face where it faces
42
43        let mut beacons = Vec::new();
44        for spot in level.points('B') {
45            let beacon = ctx.physics.add(Body::sensor(spot).with_sphere(0.8).with_tag("beacon"));
46            ctx.loop_on(beacon, "beacon"); // stops by itself when the beacon is removed
47            beacons.push(beacon);
48        }
49        Self { player, beacons, clock: Timer::new() }
50    }
51}
52
53impl Scene for Maze {
54    fn start(&mut self, ctx: &mut Context) {
55        ctx.speak("Find all three beacons. Up and down to walk, left and right to turn. C locates the nearest beacon.");
56    }
57
58    fn update(&mut self, ctx: &mut Context) {
59        if ctx.pressed(Key::Escape) {
60            ctx.push_scene(Pause);
61        }
62        if ctx.pressed(Key::Left) || ctx.pressed(Key::Right) {
63            let degrees = if ctx.pressed(Key::Left) { -45.0 } else { 45.0 };
64            let facing = ctx.physics.turn(self.player, degrees);
65            ctx.speak(math::direction_name(facing));
66        }
67
68        let forward = ctx.input.repeating(Key::Up, 0.35, 0.35);
69        let backward = ctx.input.repeating(Key::Down, 0.35, 0.35);
70        if forward || backward {
71            let step = if forward { 1.0 } else { -1.0 };
72            if ctx.physics.move_forward(self.player, step).blocked {
73                ctx.play("wall");
74            } else {
75                ctx.audio.sound("step").random_pitch(0.85, 1.15).play();
76            }
77        }
78
79        if ctx.pressed(Key::C) {
80            let position = ctx.physics.position(self.player);
81            match ctx.physics.nearest_with_tag(position, "beacon") {
82                Some(beacon) => {
83                    let text = ctx.physics.describe(self.player, beacon);
84                    ctx.speak(text);
85                }
86                None => ctx.speak("No beacons left"),
87            }
88        }
89        if ctx.pressed(Key::F) {
90            let facing = ctx.physics.facing(self.player);
91            ctx.speak(format!("Facing {}", math::direction_name(facing)));
92        }
93
94        if let Some(beacon) = ctx.physics.entered_tag(self.player, "beacon") {
95            ctx.physics.remove(beacon);
96            self.beacons.retain(|b| *b != beacon);
97            ctx.play("found");
98            if self.beacons.is_empty() {
99                self.finish(ctx);
100            } else {
101                ctx.speak(format!("Beacon found! {} left.", self.beacons.len()));
102            }
103        }
104        if ctx.ui.closed("done") {
105            ctx.quit();
106        }
107    }
108
109    // The clock should not run while the pause menu is open.
110    fn pause(&mut self, _ctx: &mut Context) {
111        self.clock.pause();
112    }
113
114    fn resume(&mut self, _ctx: &mut Context) {
115        self.clock.resume();
116    }
117}
118
119impl Maze {
120    fn finish(&mut self, ctx: &mut Context) {
121        let seconds = self.clock.elapsed_secs().round() as u32;
122        let best: u32 = ctx.storage.get_or("best_time", u32::MAX);
123        let mut text = format!("You found every beacon in {seconds} seconds.");
124        if seconds < best {
125            ctx.storage.set("best_time", seconds);
126            text.push_str(" That is a new record!");
127        } else {
128            text.push_str(&format!(" Your record is {best} seconds."));
129        }
130        ctx.ui.message("done", text);
131    }
132}
133
134struct Pause;
135
136impl Scene for Pause {
137    fn start(&mut self, ctx: &mut Context) {
138        ctx.time.pause();
139        ctx.ui.open(Menu::new("pause", "Paused").button("resume", "Resume").button("quit", "Quit game"));
140    }
141
142    fn update(&mut self, ctx: &mut Context) {
143        if ctx.ui.clicked("pause", "resume") || ctx.ui.closed("pause") {
144            ctx.ui.close_id("pause");
145            ctx.pop_scene();
146        }
147        if ctx.ui.clicked("pause", "quit") {
148            ctx.quit();
149        }
150    }
151
152    fn end(&mut self, ctx: &mut Context) {
153        ctx.time.resume();
154    }
155}
156
157fn main() -> audio_code::Result<()> {
158    Engine::new("Beacon").echo_speech(true).run_with(Maze::new)
159}