1use audio_code::prelude::*;
13
14const LANES: i32 = 3;
16const LANE_WIDTH: f32 = 1.5;
17const LIVES: u32 = 3;
18const FIRST_FALL: f32 = 3.0;
20const FASTEST_FALL: f32 = 1.0;
21
22fn main() -> audio_code::Result<()> {
23 Engine::new("Bell Catcher").run_with(|ctx| {
24 ctx.audio.register("bell", Synth::sine(1568.0).with(Synth::sine(3136.0).volume(0.3)).length(0.35).decay(9.0).volume(0.6));
25 ctx.audio.register("step", Synth::noise().length(0.05).decay(80.0).volume(0.5));
26 ctx.audio.register("edge", Synth::sine(110.0).length(0.12).decay(30.0).volume(0.8));
27 ctx.audio.register(
28 "catch",
29 Synth::sine(784.0).length(0.08).then(Synth::sine(1047.0).length(0.08)).then(Synth::sine(1568.0).length(0.2).decay(12.0)).volume(0.4),
30 );
31 ctx.audio.register("miss", Synth::saw(220.0).sweep_to(80.0).length(0.5).decay(5.0).volume(0.35));
32
33 if let Some(voice) = ctx.storage.get::<String>("voice") {
35 ctx.speech.set_voice(voice);
36 }
37 let rate: f32 = ctx.storage.get_or("rate", 50.0);
38 if rate != 50.0 {
39 ctx.speech.set_rate(rate / 100.0);
40 }
41 Title
42 })
43}
44
45struct Title;
48
49fn title_menu(ctx: &Context) -> Menu {
50 let voice = ctx.speech.voice().map_or("default".to_string(), |v| v.name.clone());
51 let best: u32 = ctx.storage.get_or("best", 0);
52 Menu::new("title", "Bell Catcher")
53 .button("play", "Play")
54 .button("voice", format!("Voice: {voice}"))
55 .slider("rate", "Speech rate", ctx.storage.get_or("rate", 50.0), 0.0, 100.0, 5.0)
56 .button("help", "How to play")
57 .label(format!("Best score: {best}"))
58 .button("quit", "Quit")
59 .closable(false)
60 .wrap(true)
61}
62
63fn voice_menu(ctx: &Context) -> Option<Menu> {
65 let voices = ctx.speech.voices();
66 if voices.is_empty() {
67 return None;
68 }
69 let mut menu = Menu::new("voices", "Choose a voice").button("default", "Default voice");
70 for (i, voice) in voices.iter().enumerate() {
71 menu = menu.button(format!("voice{i}"), voice.to_string());
72 }
73 if let Some(i) = ctx.speech.voice().and_then(|current| voices.iter().position(|v| v == current)) {
74 menu = menu.focus(&format!("voice{i}"));
75 }
76 Some(menu.button("back", "Back"))
77}
78
79fn refresh_title(ctx: &mut Context, item: &str) {
81 ctx.ui.close_id("title");
82 let menu = title_menu(ctx).focus(item);
83 ctx.ui.open(menu);
84}
85
86impl Scene for Title {
87 fn start(&mut self, ctx: &mut Context) {
88 let menu = title_menu(ctx);
89 ctx.ui.open(menu);
90 }
91
92 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
93 match event {
94 UiEvent::Activated { menu, item } if menu == "title" => match item.as_str() {
95 "play" => {
96 ctx.ui.close_all();
97 ctx.switch_scene(Game::default());
98 }
99 "voice" => match voice_menu(ctx) {
100 Some(menu) => ctx.ui.open(menu),
101 None => ctx.ui.message(
102 "no voices",
103 "Your screen reader speaks with its own voice. Change it in the screen reader's settings.",
104 ),
105 },
106 "help" => ctx.ui.message(
107 "help",
108 "Bells fall in one of seven lanes in front of you. Listen where a bell rings, \
109 step under it with left and right, and catch it before it lands. \
110 The bell rings faster and higher the closer it gets. You have three lives. \
111 Press S for your score, Escape for the menu.",
112 ),
113 "quit" => ctx.quit(),
114 _ => {}
115 },
116 UiEvent::Activated { menu, item } if menu == "voices" => {
117 if item == "back" {
118 ctx.ui.close();
119 return;
120 }
121 if item == "default" {
122 ctx.speech.reset_voice();
123 ctx.storage.remove("voice");
124 } else if let Some(voice) =
125 item.strip_prefix("voice").and_then(|i| i.parse::<usize>().ok()).and_then(|i| ctx.speech.voices().get(i).cloned())
126 && ctx.speech.set_voice(&voice.id)
127 {
128 ctx.storage.set("voice", &voice.id);
129 }
130 ctx.ui.close();
131 refresh_title(ctx, "voice");
132 let name = ctx.speech.voice().map_or("Default voice".to_string(), |v| v.name.clone());
133 ctx.speech.speak_queued(format!("{name}. This is how I sound now."));
134 }
135 UiEvent::Changed { menu, item, value } if menu == "title" && item == "rate" => {
136 ctx.speech.set_rate(value.as_f32() / 100.0);
137 ctx.storage.set("rate", value.as_f32());
138 }
139 _ => {}
140 }
141 }
142
143 fn update(&mut self, _ctx: &mut Context) {}
144}
145
146struct Game {
149 lane: i32,
150 bell_lane: i32,
151 fall: f32,
153 left: f32,
155 next_ring: f32,
156 score: u32,
157 lives: u32,
158 over: bool,
159}
160
161impl Default for Game {
162 fn default() -> Self {
163 Self { lane: 0, bell_lane: 0, fall: FIRST_FALL, left: 0.0, next_ring: 0.0, score: 0, lives: LIVES, over: false }
164 }
165}
166
167impl Game {
168 fn drop_bell(&mut self) {
169 self.bell_lane = random::int(-LANES as i64, LANES as i64) as i32;
170 self.left = self.fall;
171 self.next_ring = 0.0;
172 }
173
174 fn bell_position(&self) -> Vec3 {
176 vec3((self.bell_lane - self.lane) as f32 * LANE_WIDTH, 2.0, 0.0)
177 }
178
179 fn ring(&mut self, ctx: &mut Context) {
180 let progress = 1.0 - self.left / self.fall;
182 let position = self.bell_position();
183 ctx.audio.sound("bell").at(position).pitch(1.0 + progress * 0.6).play();
184 self.next_ring = 0.12 + 0.45 * (1.0 - progress);
185 }
186
187 fn land(&mut self, ctx: &mut Context) {
188 if self.lane == self.bell_lane {
189 self.score += 1;
190 self.fall = (self.fall * 0.93).max(FASTEST_FALL);
191 ctx.play("catch");
192 if self.score.is_multiple_of(5) {
193 ctx.speak(format!("{} bells!", self.score));
194 }
195 } else {
196 self.lives -= 1;
197 ctx.play("miss");
198 match self.lives {
199 0 => return self.game_over(ctx),
200 1 => ctx.speak("Missed! Last life."),
201 n => ctx.speak(format!("Missed! {n} lives left.")),
202 }
203 }
204 self.left = -0.8;
206 }
207
208 fn game_over(&mut self, ctx: &mut Context) {
209 self.over = true;
210 let best: u32 = ctx.storage.get_or("best", 0);
211 let text = if self.score > best {
212 ctx.storage.set("best", self.score);
213 format!("Game over. {} bells, a new best score!", self.score)
214 } else {
215 format!("Game over. {} bells. Your best is {best}.", self.score)
216 };
217 ctx.ui.message("over", text);
218 }
219}
220
221impl Scene for Game {
222 fn start(&mut self, ctx: &mut Context) {
223 ctx.speak("Catch the bells! Left and right to move.");
224 self.left = -1.5;
226 }
227
228 fn update(&mut self, ctx: &mut Context) {
229 if self.over {
230 if ctx.ui.closed("over") {
231 ctx.switch_scene(Title);
232 }
233 return;
234 }
235 if ctx.pressed(Key::Escape) {
236 ctx.switch_scene(Title);
237 return;
238 }
239 if ctx.pressed(Key::S) {
240 ctx.speak(format!("{} bells, {} lives", self.score, self.lives));
241 }
242 for (key, step) in [(Key::Left, -1), (Key::Right, 1)] {
243 if ctx.input.repeating(key, 0.3, 0.15) {
244 let lane = (self.lane + step).clamp(-LANES, LANES);
245 if lane == self.lane {
246 ctx.play("edge");
247 } else {
248 self.lane = lane;
249 ctx.audio.sound("step").at(vec3(step as f32 * 0.5, 0.0, 0.0)).random_pitch(0.9, 1.1).play();
250 }
251 }
252 }
253
254 let dt = ctx.time.delta();
255 if self.left < 0.0 {
256 self.left += dt;
257 if self.left >= 0.0 {
258 self.drop_bell();
259 }
260 return;
261 }
262 self.left -= dt;
263 self.next_ring -= dt;
264 if self.left <= 0.0 {
265 self.land(ctx);
266 } else if self.next_ring <= 0.0 {
267 self.ring(ctx);
268 }
269 }
270}
271