diff --git a/.cargo/config.toml b/.cargo/config.toml
index c27d4c3..a3637d1 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,8 @@
+# Which game folder gets baked into wasm builds (embedded_world in main.rs).
+# Override per build: MRPGI_WEB_GAME=/abs/path ./web/build.sh
does it.
+[env]
+MRPGI_WEB_GAME = { value = "games/lost-fuse", relative = true }
+
# Web builds: miniquad's JS imports (console_log, init_webgl, ...) are
# resolved at runtime by web/mq_js_bundle.js — tell the linker to allow them.
# The mrpgi_* exports are the page's save/load bridge into the engine.
diff --git a/mrpgi/src/main.rs b/mrpgi/src/main.rs
index cb0fdc9..752a18f 100644
--- a/mrpgi/src/main.rs
+++ b/mrpgi/src/main.rs
@@ -187,10 +187,11 @@ mod web_bridge {
}
/// Browser boot: the game folder is compiled into the binary (there is no
-/// filesystem on the web) and handed to the core fully parsed.
+/// filesystem on the web) and handed to the core fully parsed. Which game is
+/// the MRPGI_WEB_GAME env var at compile time (default set in .cargo/config.toml).
#[cfg(target_arch = "wasm32")]
fn embedded_world() -> World {
- static GAME: include_dir::Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../games/lost-fuse");
+ static GAME: include_dir::Dir = include_dir::include_dir!("$MRPGI_WEB_GAME");
let manifest = GAME
.get_file("game.json")
.and_then(|f| serde_json::from_slice(f.contents()).ok())
@@ -266,9 +267,10 @@ async fn main() {
let mut crt = false;
let mut command = String::new();
let mut menu_open: Option = None;
- let mut audio = sound::AudioOut::load(&game_dir).await;
+ let mut audio = sound::LazyAudio::start(&game_dir);
loop {
+ audio.tick();
let mut want_edit = false;
// --- web save/load bridge: one poll per frame ---------------------
@@ -296,7 +298,7 @@ async fn main() {
ed.stop_editing();
} else if mode == Mode::Play && gs.dlg.is_some() {
for e in gs.apply(Command::EndDialogue) {
- handle(&e, &audio);
+ handle(&e, &mut audio);
}
} else {
break;
@@ -310,7 +312,7 @@ async fn main() {
let room = gs.world.current;
command.clear();
for e in gs.apply(Command::NewGame { room: Some(room) }) {
- handle(&e, &audio);
+ handle(&e, &mut audio);
}
Mode::Play
}
@@ -420,7 +422,7 @@ async fn main() {
// --- game cycles, transitions, AI results --------------------
events.extend(gs.tick(get_frame_time()));
for e in &events {
- handle(e, &audio);
+ handle(e, &mut audio);
}
// --- render ---------------------------------------------------
@@ -480,7 +482,7 @@ async fn main() {
/// The GUI's whole event handler: play the sounds. (Transcript, inventory and
/// win state are drawn straight from GameState each frame.)
-fn handle(e: &Event, audio: &sound::AudioOut) {
+fn handle(e: &Event, audio: &mut sound::LazyAudio) {
if let Event::Audio { cue } = e {
audio.play(*cue);
}
diff --git a/mrpgi/src/sound.rs b/mrpgi/src/sound.rs
index 2a940b3..944349f 100644
--- a/mrpgi/src/sound.rs
+++ b/mrpgi/src/sound.rs
@@ -7,9 +7,70 @@
//! If the audio backend can't start, everything degrades to silence.
use macroquad::audio::{load_sound_from_bytes, play_sound, stop_sound, PlaySoundParams, Sound};
+use macroquad::experimental::coroutines::{start_coroutine, Coroutine};
use mrpgi_core::audio::{music_wav, AudioCue, MOODS};
use std::path::Path;
+/// The loop-facing audio front. Loading (file fetches + decoding) runs in a
+/// coroutine so the first frame never waits on it — browsers gate audio
+/// decoding behind a user gesture, and blocking boot on that meant a black
+/// screen. Until the real [`AudioOut`] arrives this remembers what the game
+/// asked for (mood, mute) and applies it on arrival.
+pub struct LazyAudio {
+ inner: Option,
+ loading: Coroutine,
+ pub muted: bool,
+ mood: u8,
+}
+
+impl LazyAudio {
+ pub fn start(game_dir: &str) -> Self {
+ let dir = game_dir.to_string();
+ LazyAudio {
+ inner: None,
+ loading: start_coroutine(async move { AudioOut::load(&dir).await }),
+ muted: false,
+ mood: 0,
+ }
+ }
+
+ fn poll(&mut self) -> Option<&mut AudioOut> {
+ if self.inner.is_none() {
+ if let Some(mut a) = self.loading.retrieve() {
+ a.muted = self.muted;
+ a.set_music(self.mood);
+ self.inner = Some(a);
+ }
+ }
+ self.inner.as_mut()
+ }
+
+ /// Call once per frame: starts the music as soon as loading finishes.
+ pub fn tick(&mut self) {
+ self.poll();
+ }
+
+ pub fn play(&mut self, cue: AudioCue) {
+ if let Some(a) = self.poll() {
+ a.play(cue);
+ }
+ }
+
+ pub fn set_music(&mut self, mood: u8) {
+ self.mood = mood;
+ if let Some(a) = self.poll() {
+ a.set_music(mood);
+ }
+ }
+
+ pub fn set_muted(&mut self, m: bool) {
+ self.muted = m;
+ if let Some(a) = self.poll() {
+ a.set_muted(m);
+ }
+ }
+}
+
pub struct AudioOut {
sfx: Vec<(AudioCue, Option)>,
music: Vec