Own XM/MOD replayer, audio mixer with crossfades, synthesised chip sound effects, music director
- src/xm.rs: FastTracker 2 XM and ProTracker MOD replayer (verified against libopenmpt) - src/audio.rs: music players with crossfade + effect voices, rendered on demand - src/sfx.rs: 29 synthesised chip sound effects - www/dc_plugin.js: AudioContext started on first input, streams samples from WASM - music follows the game: title, rotating build tracks, battle, gate assault, jingles - macroquad audio removed; all tests run natively with cargo test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2dba65be8b
commit
925be17e82
13 files changed
+1711
-205
No files matched your search
+52
-16
@@ -1,5 +1,6 @@
|
||||
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
|
||||
|
||||
mod audio;
|
||||
mod game;
|
||||
mod gfx;
|
||||
mod i18n;
|
||||
@@ -9,8 +10,10 @@ mod platform;
|
||||
mod render3d;
|
||||
mod save;
|
||||
mod settings;
|
||||
mod sfx;
|
||||
mod sound;
|
||||
mod ui;
|
||||
mod xm;
|
||||
|
||||
use game::{Game, Msg};
|
||||
use gfx::Gfx;
|
||||
@@ -61,14 +64,45 @@ fn next_seed(seed: u32) -> u32 {
|
||||
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
|
||||
}
|
||||
|
||||
fn apply_settings(s: &Settings, r3d: &mut R3d, sounds: &mut SoundBank) {
|
||||
fn apply_settings(s: &Settings, r3d: &mut R3d) {
|
||||
i18n::set_language(&s.language);
|
||||
ui::set_ui_scale(s.ui_scale as f32);
|
||||
r3d.outlines = s.outlines;
|
||||
r3d.set_wall_detail(s.wall_subdivisions());
|
||||
r3d.max_lights = s.max_lights as usize;
|
||||
sounds.volume = s.volume;
|
||||
sounds.muted = s.muted;
|
||||
audio::set_volumes(s.music_volume, s.volume, s.muted);
|
||||
}
|
||||
|
||||
// Chooses the music for the current situation; build tracks rotate after two loops
|
||||
struct MusicDirector {
|
||||
build: usize,
|
||||
}
|
||||
|
||||
impl MusicDirector {
|
||||
fn update(&mut self, g: &Game, title: bool) {
|
||||
use audio::Track;
|
||||
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
||||
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.ents.iter().any(|e| e.alive && e.guard);
|
||||
let invasion = g.ents.iter().any(|e| e.alive && e.hero && !e.guard);
|
||||
let want = if title {
|
||||
Track::Title
|
||||
} else if g.state == 1 {
|
||||
Track::Victory
|
||||
} else if g.state == 2 {
|
||||
Track::Defeat
|
||||
} else if gate_fight {
|
||||
Track::Battle2
|
||||
} else if invasion {
|
||||
Track::Battle1
|
||||
} else {
|
||||
if BUILD.contains(&audio::current_track().unwrap_or(Track::Title)) && audio::music_loops() >= 2 {
|
||||
self.build = (self.build + 1) % BUILD.len();
|
||||
}
|
||||
BUILD[self.build]
|
||||
};
|
||||
let fade = if matches!(want, Track::Victory | Track::Defeat) { 1.0 } else { 2.5 };
|
||||
audio::play_music(want, fade);
|
||||
}
|
||||
}
|
||||
|
||||
fn store_settings(s: &Settings) {
|
||||
@@ -107,7 +141,7 @@ impl App {
|
||||
}
|
||||
|
||||
// Load a savegame (settings in the savegame are always applied)
|
||||
fn load_bytes(&mut self, bytes: &[u8], ui: &mut Ui, menu: &mut Menu, r3d: &mut R3d, sounds: &mut SoundBank, now: f64) {
|
||||
fn load_bytes(&mut self, bytes: &[u8], ui: &mut Ui, menu: &mut Menu, r3d: &mut R3d, now: f64) {
|
||||
match save::from_bytes(bytes) {
|
||||
Ok(l) => {
|
||||
self.game = l.game;
|
||||
@@ -116,7 +150,7 @@ impl App {
|
||||
}
|
||||
if let Some(s) = l.settings {
|
||||
self.settings = s;
|
||||
apply_settings(&self.settings, r3d, sounds);
|
||||
apply_settings(&self.settings, r3d);
|
||||
store_settings(&self.settings);
|
||||
}
|
||||
self.game.hints = self.settings.hints;
|
||||
@@ -141,7 +175,7 @@ async fn main() {
|
||||
let gfx = Gfx::new();
|
||||
let mut r3d = R3d::new();
|
||||
let mut sounds = SoundBank::new();
|
||||
sounds.generate_all().await;
|
||||
let mut music = MusicDirector { build: 0 };
|
||||
let mut ui = Ui::new();
|
||||
let mut menu = Menu::new();
|
||||
|
||||
@@ -154,7 +188,7 @@ async fn main() {
|
||||
None => ui.show_langsel = true,
|
||||
}
|
||||
}
|
||||
apply_settings(&settings, &mut r3d, &mut sounds);
|
||||
apply_settings(&settings, &mut r3d);
|
||||
ui.show_help = false;
|
||||
menu.has_autosave = platform::storage_get(save::AUTOSAVE_KEY).is_some();
|
||||
|
||||
@@ -180,7 +214,7 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
if let Some(bytes) = incoming {
|
||||
app.load_bytes(&bytes, &mut ui, &mut menu, &mut r3d, &mut sounds, now);
|
||||
app.load_bytes(&bytes, &mut ui, &mut menu, &mut r3d, now);
|
||||
}
|
||||
|
||||
// ---------------- key capture for the key binding dialog
|
||||
@@ -190,12 +224,12 @@ async fn main() {
|
||||
} else if let Some(k) = get_last_key_pressed() {
|
||||
if bindable_keys().any(|b| b == k) {
|
||||
app.settings.bind(action, k);
|
||||
apply_settings(&app.settings, &mut r3d, &mut sounds);
|
||||
apply_settings(&app.settings, &mut r3d);
|
||||
store_settings(&app.settings);
|
||||
menu.finish_capture();
|
||||
}
|
||||
}
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &inp, now);
|
||||
next_frame().await;
|
||||
continue;
|
||||
}
|
||||
@@ -391,7 +425,7 @@ async fn main() {
|
||||
}
|
||||
MenuAction::Continue => {
|
||||
if let Some(s) = platform::storage_get(save::AUTOSAVE_KEY) {
|
||||
app.load_bytes(s.as_bytes(), &mut ui, &mut menu, &mut r3d, &mut sounds, now);
|
||||
app.load_bytes(s.as_bytes(), &mut ui, &mut menu, &mut r3d, now);
|
||||
}
|
||||
}
|
||||
MenuAction::Load => {
|
||||
@@ -477,7 +511,7 @@ async fn main() {
|
||||
app.cam.tz = app.cam.tz.clamp(0.0, MH as f32);
|
||||
|
||||
if settings_changed {
|
||||
apply_settings(&app.settings, &mut r3d, &mut sounds);
|
||||
apply_settings(&app.settings, &mut r3d);
|
||||
store_settings(&app.settings);
|
||||
app.game.hints = app.settings.hints;
|
||||
}
|
||||
@@ -503,20 +537,22 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
let hear = app.cam.zoom * 1.2;
|
||||
let (right, _) = app.cam.ground_axes();
|
||||
for ev in app.game.events.drain(..) {
|
||||
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), hear, now);
|
||||
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), right, hear, now);
|
||||
}
|
||||
music.update(&app.game, menu.page == Some(Page::Title) || matches!(menu.page, Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })));
|
||||
for m in app.game.msgs.drain(..) {
|
||||
ui.log.push((m, now));
|
||||
}
|
||||
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &sounds, &inp, now);
|
||||
draw_frame(&mut app, &gfx, &mut r3d, &mut ui, &mut menu, &inp, now);
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, sounds: &SoundBank, inp: &Input, now: f64) {
|
||||
fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut Menu, inp: &Input, now: f64) {
|
||||
let (mx, my) = mouse_position();
|
||||
let blocked = menu.open() || ui.show_help || ui.show_langsel;
|
||||
let over_ui = blocked || ui.over_ui(mx, my);
|
||||
@@ -552,7 +588,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
let ent = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
hover_info(&app.game, ent, hover_tile)
|
||||
};
|
||||
ui.draw(&app.game, gfx, &app.cam, sounds, &app.settings, &info, now);
|
||||
ui.draw(&app.game, gfx, &app.cam, &app.settings, &info, now);
|
||||
} else {
|
||||
ui.rects.clear();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user